aboutsummaryrefslogtreecommitdiffhomepage
path: root/services/nuldoc/nuldoc-src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-01 00:49:15 +0900
committernsfisis <nsfisis@gmail.com>2026-02-01 00:49:19 +0900
commit6dedddc545e2f1930bdc2256784eb1551bd4231d (patch)
tree75fcb5a6043dc0f2c31b098bf3cfd17a2b938599 /services/nuldoc/nuldoc-src
parentd08e3edb65b215152aa26e3518fb2f2cd7071c4b (diff)
downloadnsfisis.dev-6dedddc545e2f1930bdc2256784eb1551bd4231d.tar.gz
nsfisis.dev-6dedddc545e2f1930bdc2256784eb1551bd4231d.tar.zst
nsfisis.dev-6dedddc545e2f1930bdc2256784eb1551bd4231d.zip
feat(nuldoc): rewrite nuldoc in Ruby
Diffstat (limited to 'services/nuldoc/nuldoc-src')
-rw-r--r--services/nuldoc/nuldoc-src/commands/build.ts332
-rw-r--r--services/nuldoc/nuldoc-src/commands/new.ts96
-rw-r--r--services/nuldoc/nuldoc-src/commands/serve.ts57
-rw-r--r--services/nuldoc/nuldoc-src/components/AboutGlobalHeader.ts15
-rw-r--r--services/nuldoc/nuldoc-src/components/BlogGlobalHeader.ts28
-rw-r--r--services/nuldoc/nuldoc-src/components/DefaultGlobalHeader.ts15
-rw-r--r--services/nuldoc/nuldoc-src/components/GlobalFooter.ts9
-rw-r--r--services/nuldoc/nuldoc-src/components/PageLayout.ts76
-rw-r--r--services/nuldoc/nuldoc-src/components/Pagination.ts93
-rw-r--r--services/nuldoc/nuldoc-src/components/PostPageEntry.ts55
-rw-r--r--services/nuldoc/nuldoc-src/components/SlidePageEntry.ts55
-rw-r--r--services/nuldoc/nuldoc-src/components/SlidesGlobalHeader.ts27
-rw-r--r--services/nuldoc/nuldoc-src/components/StaticScript.ts26
-rw-r--r--services/nuldoc/nuldoc-src/components/StaticStylesheet.ts21
-rw-r--r--services/nuldoc/nuldoc-src/components/TableOfContents.ts30
-rw-r--r--services/nuldoc/nuldoc-src/components/TagList.ts19
-rw-r--r--services/nuldoc/nuldoc-src/components/utils.ts8
-rw-r--r--services/nuldoc/nuldoc-src/config.ts52
-rw-r--r--services/nuldoc/nuldoc-src/dom.ts283
-rw-r--r--services/nuldoc/nuldoc-src/errors.ts17
-rw-r--r--services/nuldoc/nuldoc-src/generators/about.ts21
-rw-r--r--services/nuldoc/nuldoc-src/generators/atom.ts84
-rw-r--r--services/nuldoc/nuldoc-src/generators/home.ts17
-rw-r--r--services/nuldoc/nuldoc-src/generators/not_found.ts20
-rw-r--r--services/nuldoc/nuldoc-src/generators/post.ts63
-rw-r--r--services/nuldoc/nuldoc-src/generators/post_list.ts56
-rw-r--r--services/nuldoc/nuldoc-src/generators/slide.ts51
-rw-r--r--services/nuldoc/nuldoc-src/generators/slide_list.ts21
-rw-r--r--services/nuldoc/nuldoc-src/generators/tag.ts32
-rw-r--r--services/nuldoc/nuldoc-src/generators/tag_list.ts22
-rw-r--r--services/nuldoc/nuldoc-src/generators/tagged_page.ts4
-rw-r--r--services/nuldoc/nuldoc-src/main.ts19
-rw-r--r--services/nuldoc/nuldoc-src/markdown/document.ts75
-rw-r--r--services/nuldoc/nuldoc-src/markdown/mdast2ndoc.ts589
-rw-r--r--services/nuldoc/nuldoc-src/markdown/parse.ts47
-rw-r--r--services/nuldoc/nuldoc-src/markdown/to_html.ts496
-rw-r--r--services/nuldoc/nuldoc-src/page.ts10
-rw-r--r--services/nuldoc/nuldoc-src/pages/AboutPage.ts154
-rw-r--r--services/nuldoc/nuldoc-src/pages/AtomPage.ts27
-rw-r--r--services/nuldoc/nuldoc-src/pages/HomePage.ts66
-rw-r--r--services/nuldoc/nuldoc-src/pages/NotFoundPage.ts40
-rw-r--r--services/nuldoc/nuldoc-src/pages/PostListPage.ts48
-rw-r--r--services/nuldoc/nuldoc-src/pages/PostPage.ts94
-rw-r--r--services/nuldoc/nuldoc-src/pages/SlideListPage.ts45
-rw-r--r--services/nuldoc/nuldoc-src/pages/SlidePage.ts140
-rw-r--r--services/nuldoc/nuldoc-src/pages/TagListPage.ts67
-rw-r--r--services/nuldoc/nuldoc-src/pages/TagPage.ts50
-rw-r--r--services/nuldoc/nuldoc-src/render.ts13
-rw-r--r--services/nuldoc/nuldoc-src/renderers/html.ts311
-rw-r--r--services/nuldoc/nuldoc-src/renderers/xml.ts128
-rw-r--r--services/nuldoc/nuldoc-src/revision.ts37
-rw-r--r--services/nuldoc/nuldoc-src/slide/parse.ts20
-rw-r--r--services/nuldoc/nuldoc-src/slide/slide.ts67
53 files changed, 0 insertions, 4248 deletions
diff --git a/services/nuldoc/nuldoc-src/commands/build.ts b/services/nuldoc/nuldoc-src/commands/build.ts
deleted file mode 100644
index 61853816..00000000
--- a/services/nuldoc/nuldoc-src/commands/build.ts
+++ /dev/null
@@ -1,332 +0,0 @@
-import { dirname, join, joinGlobs, relative } from "@std/path";
-import { ensureDir, expandGlob } from "@std/fs";
-import { generateFeedPageFromEntries } from "../generators/atom.ts";
-import { Config, getTagLabel } from "../config.ts";
-import { parseMarkdownFile } from "../markdown/parse.ts";
-import { Page } from "../page.ts";
-import { render } from "../render.ts";
-import { dateToString } from "../revision.ts";
-import { generateAboutPage } from "../generators/about.ts";
-import { generateHomePage } from "../generators/home.ts";
-import { generateNotFoundPage } from "../generators/not_found.ts";
-import {
- generatePostPage,
- getPostPublishedDate,
- PostPage,
-} from "../generators/post.ts";
-import { generatePostListPages } from "../generators/post_list.ts";
-import { generateSlidePage, SlidePage } from "../generators/slide.ts";
-import { generateSlideListPage } from "../generators/slide_list.ts";
-import { generateTagPage, TagPage } from "../generators/tag.ts";
-import { TaggedPage } from "../generators/tagged_page.ts";
-import { generateTagListPage } from "../generators/tag_list.ts";
-import { parseSlideFile } from "../slide/parse.ts";
-
-export async function runBuildCommand(config: Config) {
- const posts = await buildPostPages(config);
- await buildPostListPage(posts, config);
- const slides = await buildSlidePages(config);
- await buildSlideListPage(slides, config);
- const postTags = await buildTagPages(posts, "blog", config);
- await buildTagListPage(postTags, "blog", config);
- const slidesTags = await buildTagPages(slides, "slides", config);
- await buildTagListPage(slidesTags, "slides", config);
- await buildHomePage(config);
- await buildAboutPage(slides, config);
- await buildNotFoundPage("default", config);
- await buildNotFoundPage("about", config);
- await buildNotFoundPage("blog", config);
- await buildNotFoundPage("slides", config);
- await copyStaticFiles(config);
- await copySlidesFiles(slides, config);
- await copyBlogAssetFiles(config);
- await copySlidesAssetFiles(config);
- await copyPostSourceFiles(posts, config);
-}
-
-async function buildPostPages(config: Config): Promise<PostPage[]> {
- const sourceDir = join(Deno.cwd(), config.locations.contentDir, "posts");
- const postFiles = await collectPostFiles(sourceDir);
- const posts = await parsePosts(postFiles, config);
- for (const post of posts) {
- await writePage(post, config);
- }
- return posts;
-}
-
-async function collectPostFiles(sourceDir: string): Promise<string[]> {
- const filePaths = [];
- const globPattern = joinGlobs([sourceDir, "**", "*.md"]);
- for await (const entry of expandGlob(globPattern)) {
- filePaths.push(entry.path);
- }
- return filePaths;
-}
-
-async function parsePosts(
- postFiles: string[],
- config: Config,
-): Promise<PostPage[]> {
- const posts = [];
- for (const postFile of postFiles) {
- posts.push(
- await generatePostPage(await parseMarkdownFile(postFile, config), config),
- );
- }
- return posts;
-}
-
-async function buildPostListPage(posts: PostPage[], config: Config) {
- // Sort posts by published date (newest first)
- const sortedPosts = [...posts].sort((a, b) => {
- const ta = dateToString(getPostPublishedDate(a));
- const tb = dateToString(getPostPublishedDate(b));
- if (ta > tb) return -1;
- if (ta < tb) return 1;
- return 0;
- });
-
- const postListPages = await generatePostListPages(sortedPosts, config);
- for (const page of postListPages) {
- await writePage(page, config);
- }
-
- const postFeedPage = generateFeedPageFromEntries(
- "/posts/",
- "posts",
- `投稿一覧|${config.sites.blog.siteName}`,
- posts,
- "blog",
- config,
- );
- await writePage(postFeedPage, config);
-}
-
-async function buildSlidePages(config: Config): Promise<SlidePage[]> {
- const sourceDir = join(Deno.cwd(), config.locations.contentDir, "slides");
- const slideFiles = await collectSlideFiles(sourceDir);
- const slides = await parseSlides(slideFiles, config);
- for (const slide of slides) {
- await writePage(slide, config);
- }
- return slides;
-}
-
-async function collectSlideFiles(sourceDir: string): Promise<string[]> {
- const filePaths = [];
- const globPattern = joinGlobs([sourceDir, "**", "*.toml"]);
- for await (const entry of expandGlob(globPattern)) {
- filePaths.push(entry.path);
- }
- return filePaths;
-}
-
-async function parseSlides(
- slideFiles: string[],
- config: Config,
-): Promise<SlidePage[]> {
- const slides = [];
- for (const slideFile of slideFiles) {
- slides.push(
- await generateSlidePage(await parseSlideFile(slideFile), config),
- );
- }
- return slides;
-}
-
-async function buildSlideListPage(slides: SlidePage[], config: Config) {
- const slideListPage = await generateSlideListPage(slides, config);
- await writePage(slideListPage, config);
- const slideFeedPage = generateFeedPageFromEntries(
- slideListPage.href,
- "slides",
- `スライド一覧|${config.sites.slides.siteName}`,
- slides,
- "slides",
- config,
- );
- await writePage(slideFeedPage, config);
-}
-
-async function buildHomePage(config: Config) {
- const homePage = await generateHomePage(config);
- await writePage(homePage, config);
-}
-
-async function buildAboutPage(slides: SlidePage[], config: Config) {
- const aboutPage = await generateAboutPage(slides, config);
- await writePage(aboutPage, config);
-}
-
-async function buildNotFoundPage(
- site: "default" | "about" | "blog" | "slides",
- config: Config,
-) {
- const notFoundPage = await generateNotFoundPage(site, config);
- await writePage(notFoundPage, config);
-}
-
-async function buildTagPages(
- pages: TaggedPage[],
- site: "blog" | "slides",
- config: Config,
-): Promise<TagPage[]> {
- const tagsAndPages = collectTags(pages);
- const tags = [];
- for (const [tag, pages] of tagsAndPages) {
- const tagPage = await generateTagPage(tag, pages, site, config);
- await writePage(tagPage, config);
- const tagFeedPage = generateFeedPageFromEntries(
- tagPage.href,
- `tag-${tag}`,
- `タグ「${getTagLabel(config, tag)}」一覧|${config.sites[site].siteName}`,
- pages,
- site,
- config,
- );
- await writePage(tagFeedPage, config);
- tags.push(tagPage);
- }
- return tags;
-}
-
-async function buildTagListPage(
- tags: TagPage[],
- site: "blog" | "slides",
- config: Config,
-) {
- const tagListPage = await generateTagListPage(tags, site, config);
- await writePage(tagListPage, config);
-}
-
-function collectTags(taggedPages: TaggedPage[]): [string, TaggedPage[]][] {
- const tagsAndPages = new Map();
- for (const page of taggedPages) {
- for (const tag of page.tags) {
- if (!tagsAndPages.has(tag)) {
- tagsAndPages.set(tag, []);
- }
- tagsAndPages.get(tag).push(page);
- }
- }
-
- const result: [string, TaggedPage[]][] = [];
- for (const tag of Array.from(tagsAndPages.keys()).sort()) {
- result.push([
- tag,
- tagsAndPages.get(tag).sort((a: TaggedPage, b: TaggedPage) => {
- const ta = dateToString(getPostPublishedDate(a));
- const tb = dateToString(getPostPublishedDate(b));
- if (ta > tb) return -1;
- if (ta < tb) return 1;
- return 0;
- }),
- ]);
- }
- return result;
-}
-
-async function copyStaticFiles(config: Config) {
- const staticDir = join(Deno.cwd(), config.locations.staticDir);
-
- for (const site of Object.keys(config.sites)) {
- const destDir = join(Deno.cwd(), config.locations.destDir, site);
-
- // Copy files from static/_all/ to all sites
- for await (const entry of expandGlob(join(staticDir, "_all", "*"))) {
- await Deno.copyFile(entry.path, join(destDir, entry.name));
- }
-
- // Copy files from static/<site>/ to the corresponding site
- for await (const entry of expandGlob(join(staticDir, site, "*"))) {
- await Deno.copyFile(entry.path, join(destDir, entry.name));
- }
- }
-}
-
-async function copySlidesFiles(slides: SlidePage[], config: Config) {
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir);
- const destDir = join(cwd, config.locations.destDir);
-
- for (const slide of slides) {
- const src = join(contentDir, slide.slideLink);
- const dst = join(destDir, "slides", slide.slideLink);
- await ensureDir(dirname(dst));
- await Deno.copyFile(src, dst);
- }
-}
-
-async function copyBlogAssetFiles(config: Config) {
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir, "posts");
- const destDir = join(cwd, config.locations.destDir, "blog");
-
- const globPattern = joinGlobs([contentDir, "**", "*"]);
- for await (const { isFile, path } of expandGlob(globPattern)) {
- if (!isFile) continue;
-
- // Skip .md, .toml, .pdf files
- if (
- path.endsWith(".md") ||
- path.endsWith(".toml") ||
- path.endsWith(".pdf")
- ) {
- continue;
- }
-
- const src = path;
- const dst = join(destDir, "posts", relative(contentDir, path));
- await ensureDir(dirname(dst));
- await Deno.copyFile(src, dst);
- }
-}
-
-async function copySlidesAssetFiles(config: Config) {
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir, "slides");
- const destDir = join(cwd, config.locations.destDir, "slides");
-
- const globPattern = joinGlobs([contentDir, "**", "*"]);
- for await (const { isFile, path } of expandGlob(globPattern)) {
- if (!isFile) continue;
-
- // Skip .md, .toml, .pdf files
- if (
- path.endsWith(".md") ||
- path.endsWith(".toml") ||
- path.endsWith(".pdf")
- ) {
- continue;
- }
-
- const src = path;
- const dst = join(destDir, "slides", relative(contentDir, path));
- await ensureDir(dirname(dst));
- await Deno.copyFile(src, dst);
- }
-}
-
-async function writePage(page: Page, config: Config) {
- const destFilePath = join(
- Deno.cwd(),
- config.locations.destDir,
- page.site,
- page.destFilePath,
- );
- await ensureDir(dirname(destFilePath));
- await Deno.writeTextFile(destFilePath, render(page.root, page.renderer));
-}
-
-async function copyPostSourceFiles(posts: PostPage[], config: Config) {
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir);
- const destDir = join(cwd, config.locations.destDir, "blog");
-
- for (const post of posts) {
- const src = post.sourceFilePath;
- const dst = join(destDir, relative(contentDir, src));
- await ensureDir(dirname(dst));
- await Deno.copyFile(src, dst);
- }
-}
diff --git a/services/nuldoc/nuldoc-src/commands/new.ts b/services/nuldoc/nuldoc-src/commands/new.ts
deleted file mode 100644
index f355376d..00000000
--- a/services/nuldoc/nuldoc-src/commands/new.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import { dirname, join } from "@std/path";
-import { ensureDir } from "@std/fs";
-import { parseArgs } from "@std/cli";
-import { Config } from "../config.ts";
-
-export async function runNewCommand(config: Config) {
- const parsedArgs = parseArgs(Deno.args, {
- string: ["date"],
- });
-
- const type = parsedArgs._[1];
- if (type !== "post" && type !== "slide") {
- console.log(`Usage: nuldoc new <type>
-
-<type> must be either "post" or "slide".
-
-OPTIONS:
- --date <DATE>
-`);
- Deno.exit(1);
- }
-
- const ymd = (() => {
- if (parsedArgs.date) {
- return parsedArgs.date;
- }
-
- const now = new Date();
- const y = now.getFullYear();
- const d = (now.getMonth() + 1).toString().padStart(2, "0");
- const m = now.getDate().toString().padStart(2, "0");
- return `${y}-${d}-${m}`;
- })();
-
- const destFilePath = join(
- Deno.cwd(),
- config.locations.contentDir,
- getDirPath(type),
- ymd,
- getFilename(type),
- );
-
- await ensureDir(dirname(destFilePath));
- await Deno.writeTextFile(destFilePath, getTemplate(type, ymd));
- console.log(
- `New file ${
- destFilePath.replace(Deno.cwd(), "")
- } was successfully created.`,
- );
-}
-
-function getFilename(type: "post" | "slide"): string {
- return type === "post" ? "TODO.md" : "TODO.toml";
-}
-
-function getDirPath(type: "post" | "slide"): string {
- return type === "post" ? "posts" : "slides";
-}
-
-function getTemplate(type: "post" | "slide", date: string): string {
- const uuid = crypto.randomUUID();
- if (type === "post") {
- return `---
-[article]
-uuid = "${uuid}"
-title = "TODO"
-description = "TODO"
-tags = [
- "TODO",
-]
-
-[[article.revisions]]
-date = "${date}"
-remark = "公開"
----
-# はじめに {#intro}
-
-TODO
-`;
- } else {
- return `[slide]
-uuid = "${uuid}"
-title = "TODO"
-event = "TODO"
-talkType = "TODO"
-link = "TODO"
-tags = [
- "TODO",
-]
-
-[[slide.revisions]]
-date = "${date}"
-remark = "登壇"
-`;
- }
-}
diff --git a/services/nuldoc/nuldoc-src/commands/serve.ts b/services/nuldoc/nuldoc-src/commands/serve.ts
deleted file mode 100644
index 8388d48a..00000000
--- a/services/nuldoc/nuldoc-src/commands/serve.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { parseArgs } from "@std/cli";
-import { serveDir, STATUS_CODE, STATUS_TEXT } from "@std/http";
-import { join } from "@std/path";
-import { Config } from "../config.ts";
-import { runBuildCommand } from "./build.ts";
-
-function isResourcePath(pathname: string): boolean {
- const EXTENSIONS = [
- ".css",
- ".gif",
- ".ico",
- ".jpeg",
- ".jpg",
- ".js",
- ".mjs",
- ".png",
- ".svg",
- ];
- return EXTENSIONS.some((ext) => pathname.endsWith(ext));
-}
-
-export function runServeCommand(config: Config) {
- const parsedArgs = parseArgs(Deno.args, {
- boolean: ["no-rebuild"],
- });
-
- const doRebuild = !parsedArgs["no-rebuild"];
- const siteName = String(parsedArgs._[1]);
- if (siteName === "") {
- throw new Error("Usage: nuldoc serve <site>");
- }
-
- const rootDir = join(Deno.cwd(), config.locations.destDir, siteName);
- Deno.serve({ hostname: "127.0.0.1" }, async (req) => {
- const pathname = new URL(req.url).pathname;
- if (!isResourcePath(pathname) && doRebuild) {
- await runBuildCommand(config);
- console.log("rebuild");
- }
- const res = await serveDir(req, {
- fsRoot: rootDir,
- showIndex: true,
- });
- if (res.status !== STATUS_CODE.NotFound) {
- return res;
- }
-
- const notFoundHtml = await Deno.readTextFile(join(rootDir, "404.html"));
- return new Response(notFoundHtml, {
- status: STATUS_CODE.NotFound,
- statusText: STATUS_TEXT[STATUS_CODE.NotFound],
- headers: {
- "content-type": "text/html",
- },
- });
- });
-}
diff --git a/services/nuldoc/nuldoc-src/components/AboutGlobalHeader.ts b/services/nuldoc/nuldoc-src/components/AboutGlobalHeader.ts
deleted file mode 100644
index df437931..00000000
--- a/services/nuldoc/nuldoc-src/components/AboutGlobalHeader.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Config } from "../config.ts";
-import { a, div, Element, header } from "../dom.ts";
-
-export default function GlobalHeader({ config }: { config: Config }): Element {
- return header(
- { class: "header" },
- div(
- { class: "site-logo" },
- a(
- { href: `https://${config.sites.default.fqdn}/` },
- "nsfisis.dev",
- ),
- ),
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/BlogGlobalHeader.ts b/services/nuldoc/nuldoc-src/components/BlogGlobalHeader.ts
deleted file mode 100644
index ae0fc13a..00000000
--- a/services/nuldoc/nuldoc-src/components/BlogGlobalHeader.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Config } from "../config.ts";
-import { a, div, Element, header, li, nav, ul } from "../dom.ts";
-
-export default function GlobalHeader({ config }: { config: Config }): Element {
- return header(
- { class: "header" },
- div(
- { class: "site-logo" },
- a(
- { href: `https://${config.sites.default.fqdn}/` },
- "nsfisis.dev",
- ),
- ),
- div({ class: "site-name" }, config.sites.blog.siteName),
- nav(
- { class: "nav" },
- ul(
- {},
- li(
- {},
- a({ href: `https://${config.sites.about.fqdn}/` }, "About"),
- ),
- li({}, a({ href: "/posts/" }, "Posts")),
- li({}, a({ href: "/tags/" }, "Tags")),
- ),
- ),
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/DefaultGlobalHeader.ts b/services/nuldoc/nuldoc-src/components/DefaultGlobalHeader.ts
deleted file mode 100644
index df437931..00000000
--- a/services/nuldoc/nuldoc-src/components/DefaultGlobalHeader.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Config } from "../config.ts";
-import { a, div, Element, header } from "../dom.ts";
-
-export default function GlobalHeader({ config }: { config: Config }): Element {
- return header(
- { class: "header" },
- div(
- { class: "site-logo" },
- a(
- { href: `https://${config.sites.default.fqdn}/` },
- "nsfisis.dev",
- ),
- ),
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/GlobalFooter.ts b/services/nuldoc/nuldoc-src/components/GlobalFooter.ts
deleted file mode 100644
index 313a01c5..00000000
--- a/services/nuldoc/nuldoc-src/components/GlobalFooter.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Config } from "../config.ts";
-import { Element, footer } from "../dom.ts";
-
-export default function GlobalFooter({ config }: { config: Config }): Element {
- return footer(
- { class: "footer" },
- `&copy; ${config.site.copyrightYear} ${config.site.author}`,
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/PageLayout.ts b/services/nuldoc/nuldoc-src/components/PageLayout.ts
deleted file mode 100644
index f970c0b6..00000000
--- a/services/nuldoc/nuldoc-src/components/PageLayout.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Config } from "../config.ts";
-import { elem, Element, link, meta, Node } from "../dom.ts";
-import StaticStylesheet from "./StaticStylesheet.ts";
-
-type Props = {
- metaCopyrightYear: number;
- metaDescription: string;
- metaKeywords?: string[];
- metaTitle: string;
- metaAtomFeedHref?: string;
- requiresSyntaxHighlight?: boolean;
- site: "default" | "about" | "blog" | "slides";
- config: Config;
- children: Node;
-};
-
-export default async function PageLayout(
- {
- metaCopyrightYear,
- metaDescription,
- metaKeywords,
- metaTitle,
- metaAtomFeedHref,
- requiresSyntaxHighlight: _,
- site,
- config,
- children,
- }: Props,
-): Promise<Element> {
- return elem(
- "html",
- { lang: "ja-JP" },
- elem(
- "head",
- {},
- meta({ charset: "UTF-8" }),
- meta({
- name: "viewport",
- content: "width=device-width, initial-scale=1.0",
- }),
- meta({ name: "author", content: config.site.author }),
- meta({
- name: "copyright",
- content: `&copy; ${metaCopyrightYear} ${config.site.author}`,
- }),
- meta({ name: "description", content: metaDescription }),
- metaKeywords && metaKeywords.length !== 0
- ? meta({ name: "keywords", content: metaKeywords.join(",") })
- : null,
- meta({ property: "og:type", content: "article" }),
- meta({ property: "og:title", content: metaTitle }),
- meta({ property: "og:description", content: metaDescription }),
- meta({
- property: "og:site_name",
- content: config.sites[site].siteName,
- }),
- meta({ property: "og:locale", content: "ja_JP" }),
- meta({ name: "Hatena::Bookmark", content: "nocomment" }),
- metaAtomFeedHref
- ? link({
- rel: "alternate",
- href: metaAtomFeedHref,
- type: "application/atom+xml",
- })
- : null,
- link({
- rel: "icon",
- href: "/favicon.svg",
- type: "image/svg+xml",
- }),
- elem("title", {}, metaTitle),
- await StaticStylesheet({ fileName: "/style.css", config }),
- ),
- children,
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/Pagination.ts b/services/nuldoc/nuldoc-src/components/Pagination.ts
deleted file mode 100644
index d9203165..00000000
--- a/services/nuldoc/nuldoc-src/components/Pagination.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { a, div, Element, nav, span } from "../dom.ts";
-
-type Props = {
- currentPage: number;
- totalPages: number;
- basePath: string;
-};
-
-export default function Pagination(
- { currentPage, totalPages, basePath }: Props,
-): Element {
- if (totalPages <= 1) {
- return div({});
- }
-
- const pages = generatePageNumbers(currentPage, totalPages);
-
- return nav(
- { class: "pagination" },
- div(
- { class: "pagination-prev" },
- currentPage > 1
- ? a({ href: pageUrlAt(basePath, currentPage - 1) }, "前へ")
- : null,
- ),
- ...pages.map((page) => {
- if (page === "...") {
- return div({ class: "pagination-elipsis" }, "…");
- } else if (page === currentPage) {
- return div(
- { class: "pagination-page pagination-page-current" },
- span({}, String(page)),
- );
- } else {
- return div(
- { class: "pagination-page" },
- a({ href: pageUrlAt(basePath, page) }, String(page)),
- );
- }
- }),
- div(
- { class: "pagination-next" },
- currentPage < totalPages
- ? a({ href: pageUrlAt(basePath, currentPage + 1) }, "次へ")
- : null,
- ),
- );
-}
-
-type PageItem = number | "...";
-
-/**
- * Generates page numbers for pagination display.
- *
- * - Always show the first page
- * - Always show the last page
- * - Always show the current page
- * - Always show the page before and after the current page
- * - If there's only one page gap between displayed pages, fill it
- * - If there are two or more pages gap between displayed pages, show ellipsis
- */
-function generatePageNumbers(
- currentPage: number,
- totalPages: number,
-): PageItem[] {
- const pages = new Set<number>();
- pages.add(1);
- pages.add(Math.max(1, currentPage - 1));
- pages.add(currentPage);
- pages.add(Math.min(totalPages, currentPage + 1));
- pages.add(totalPages);
-
- const sorted = Array.from(pages).sort((a, b) => a - b);
-
- const result: PageItem[] = [];
- for (let i = 0; i < sorted.length; i++) {
- if (i > 0) {
- const gap = sorted[i] - sorted[i - 1];
- if (gap === 2) {
- result.push(sorted[i - 1] + 1);
- } else if (gap > 2) {
- result.push("...");
- }
- }
- result.push(sorted[i]);
- }
-
- return result;
-}
-
-function pageUrlAt(basePath: string, page: number): string {
- return page === 1 ? basePath : `${basePath}${page}/`;
-}
diff --git a/services/nuldoc/nuldoc-src/components/PostPageEntry.ts b/services/nuldoc/nuldoc-src/components/PostPageEntry.ts
deleted file mode 100644
index 482a3a8e..00000000
--- a/services/nuldoc/nuldoc-src/components/PostPageEntry.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- getPostPublishedDate,
- getPostUpdatedDate,
- postHasAnyUpdates,
- PostPage,
-} from "../generators/post.ts";
-import { dateToString } from "../revision.ts";
-import { Config } from "../config.ts";
-import {
- a,
- article,
- elem,
- Element,
- footer,
- h2,
- header,
- p,
- section,
-} from "../dom.ts";
-import TagList from "./TagList.ts";
-
-type Props = { post: PostPage; config: Config };
-
-export default function PostPageEntry({ post, config }: Props): Element {
- return article(
- { class: "post-entry" },
- a(
- { href: post.href },
- header({ class: "entry-header" }, h2({}, post.title)),
- section(
- { class: "entry-content" },
- p({}, post.description),
- ),
- footer(
- { class: "entry-footer" },
- elem(
- "time",
- { datetime: dateToString(getPostPublishedDate(post)) },
- dateToString(getPostPublishedDate(post)),
- ),
- " 投稿",
- postHasAnyUpdates(post) ? "、" : null,
- postHasAnyUpdates(post)
- ? elem(
- "time",
- { datetime: dateToString(getPostUpdatedDate(post)) },
- dateToString(getPostUpdatedDate(post)),
- )
- : null,
- postHasAnyUpdates(post) ? " 更新" : null,
- post.tags.length !== 0 ? TagList({ tags: post.tags, config }) : null,
- ),
- ),
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/SlidePageEntry.ts b/services/nuldoc/nuldoc-src/components/SlidePageEntry.ts
deleted file mode 100644
index b48ab4e5..00000000
--- a/services/nuldoc/nuldoc-src/components/SlidePageEntry.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {
- getPostPublishedDate,
- getPostUpdatedDate,
- postHasAnyUpdates,
-} from "../generators/post.ts";
-import { SlidePage } from "../generators/slide.ts";
-import { dateToString } from "../revision.ts";
-import { Config } from "../config.ts";
-import {
- a,
- article,
- elem,
- Element,
- footer,
- h2,
- header,
- p,
- section,
-} from "../dom.ts";
-import TagList from "./TagList.ts";
-
-type Props = { slide: SlidePage; config: Config };
-
-export default function SlidePageEntry({ slide, config }: Props): Element {
- return article(
- { class: "post-entry" },
- a(
- { href: slide.href },
- header(
- { class: "entry-header" },
- h2({}, slide.title),
- ),
- section({ class: "entry-content" }, p({}, slide.description)),
- footer(
- { class: "entry-footer" },
- elem(
- "time",
- { datetime: dateToString(getPostPublishedDate(slide)) },
- dateToString(getPostPublishedDate(slide)),
- ),
- " 登壇",
- postHasAnyUpdates(slide) ? "、" : null,
- postHasAnyUpdates(slide)
- ? elem(
- "time",
- { datetime: dateToString(getPostUpdatedDate(slide)) },
- dateToString(getPostUpdatedDate(slide)),
- )
- : null,
- postHasAnyUpdates(slide) ? " 更新" : null,
- slide.tags.length !== 0 ? TagList({ tags: slide.tags, config }) : null,
- ),
- ),
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/SlidesGlobalHeader.ts b/services/nuldoc/nuldoc-src/components/SlidesGlobalHeader.ts
deleted file mode 100644
index 666ca0e3..00000000
--- a/services/nuldoc/nuldoc-src/components/SlidesGlobalHeader.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Config } from "../config.ts";
-import { a, div, Element, header, li, nav, ul } from "../dom.ts";
-
-export default function GlobalHeader({ config }: { config: Config }): Element {
- return header(
- { class: "header" },
- div(
- { class: "site-logo" },
- a(
- { href: `https://${config.sites.default.fqdn}/` },
- "nsfisis.dev",
- ),
- ),
- nav(
- { class: "nav" },
- ul(
- {},
- li(
- {},
- a({ href: `https://${config.sites.about.fqdn}/` }, "About"),
- ),
- li({}, a({ href: "/slides/" }, "Slides")),
- li({}, a({ href: "/tags/" }, "Tags")),
- ),
- ),
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/StaticScript.ts b/services/nuldoc/nuldoc-src/components/StaticScript.ts
deleted file mode 100644
index 1a3431a3..00000000
--- a/services/nuldoc/nuldoc-src/components/StaticScript.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { join } from "@std/path";
-import { Config } from "../config.ts";
-import { Element, script } from "../dom.ts";
-import { calculateFileHash } from "./utils.ts";
-
-export default async function StaticScript(
- { site, fileName, type, defer, config }: {
- site?: string;
- fileName: string;
- type?: string;
- defer?: "true";
- config: Config;
- },
-): Promise<Element> {
- const filePath = join(
- Deno.cwd(),
- config.locations.staticDir,
- site || "_all",
- fileName,
- );
- const hash = await calculateFileHash(filePath);
- const attrs: Record<string, string> = { src: `${fileName}?h=${hash}` };
- if (type) attrs.type = type;
- if (defer) attrs.defer = defer;
- return script(attrs);
-}
diff --git a/services/nuldoc/nuldoc-src/components/StaticStylesheet.ts b/services/nuldoc/nuldoc-src/components/StaticStylesheet.ts
deleted file mode 100644
index f2adb473..00000000
--- a/services/nuldoc/nuldoc-src/components/StaticStylesheet.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { join } from "@std/path";
-import { Config } from "../config.ts";
-import { Element, link } from "../dom.ts";
-import { calculateFileHash } from "./utils.ts";
-
-export default async function StaticStylesheet(
- { site, fileName, config }: {
- site?: string;
- fileName: string;
- config: Config;
- },
-): Promise<Element> {
- const filePath = join(
- Deno.cwd(),
- config.locations.staticDir,
- site || "_all",
- fileName,
- );
- const hash = await calculateFileHash(filePath);
- return link({ rel: "stylesheet", href: `${fileName}?h=${hash}` });
-}
diff --git a/services/nuldoc/nuldoc-src/components/TableOfContents.ts b/services/nuldoc/nuldoc-src/components/TableOfContents.ts
deleted file mode 100644
index 1eb79e98..00000000
--- a/services/nuldoc/nuldoc-src/components/TableOfContents.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { TocEntry, TocRoot } from "../markdown/document.ts";
-import { a, Element, h2, li, nav, ul } from "../dom.ts";
-
-type Props = {
- toc: TocRoot;
-};
-
-export default function TableOfContents({ toc }: Props): Element {
- return nav(
- { class: "toc" },
- h2({}, "目次"),
- ul(
- {},
- ...toc.entries.map((entry) => TocEntryComponent({ entry })),
- ),
- );
-}
-
-function TocEntryComponent({ entry }: { entry: TocEntry }): Element {
- return li(
- {},
- a({ href: `#${entry.id}` }, entry.text),
- entry.children.length > 0
- ? ul(
- {},
- ...entry.children.map((child) => TocEntryComponent({ entry: child })),
- )
- : null,
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/TagList.ts b/services/nuldoc/nuldoc-src/components/TagList.ts
deleted file mode 100644
index ed3fc1a1..00000000
--- a/services/nuldoc/nuldoc-src/components/TagList.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Config, getTagLabel } from "../config.ts";
-import { Element, li, span, text, ul } from "../dom.ts";
-
-type Props = {
- tags: string[];
- config: Config;
-};
-
-export default function TagList({ tags, config }: Props): Element {
- return ul(
- { class: "entry-tags" },
- ...tags.map((slug) =>
- li(
- { class: "tag" },
- span({ class: "tag-inner" }, text(getTagLabel(config, slug))),
- )
- ),
- );
-}
diff --git a/services/nuldoc/nuldoc-src/components/utils.ts b/services/nuldoc/nuldoc-src/components/utils.ts
deleted file mode 100644
index 14059b5b..00000000
--- a/services/nuldoc/nuldoc-src/components/utils.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Hash } from "checksum/mod.ts";
-
-export async function calculateFileHash(
- filePath: string,
-): Promise<string> {
- const content = await Deno.readFile(filePath);
- return new Hash("md5").digest(content).hex();
-}
diff --git a/services/nuldoc/nuldoc-src/config.ts b/services/nuldoc/nuldoc-src/config.ts
deleted file mode 100644
index 267a8f99..00000000
--- a/services/nuldoc/nuldoc-src/config.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { join } from "@std/path";
-import { parse as parseToml } from "@std/toml";
-import { z } from "zod/mod.ts";
-
-const ConfigSchema = z.object({
- locations: z.object({
- contentDir: z.string(),
- destDir: z.string(),
- staticDir: z.string(),
- }),
- site: z.object({
- author: z.string(),
- copyrightYear: z.number(),
- }),
- sites: z.object({
- default: z.object({
- fqdn: z.string(),
- siteName: z.string(),
- }),
- about: z.object({
- fqdn: z.string(),
- siteName: z.string(),
- }),
- blog: z.object({
- fqdn: z.string(),
- siteName: z.string(),
- postsPerPage: z.number(),
- }),
- slides: z.object({
- fqdn: z.string(),
- siteName: z.string(),
- }),
- }),
- tagLabels: z.record(z.string(), z.string()),
-});
-
-export type Config = z.infer<typeof ConfigSchema>;
-
-export function getTagLabel(c: Config, slug: string): string {
- if (!(slug in c.tagLabels)) {
- throw new Error(`Unknown tag: ${slug}`);
- }
- return c.tagLabels[slug];
-}
-
-export function getDefaultConfigPath(): string {
- return join(Deno.cwd(), "nuldoc.toml");
-}
-
-export async function loadConfig(filePath: string): Promise<Config> {
- return ConfigSchema.parse(parseToml(await Deno.readTextFile(filePath)));
-}
diff --git a/services/nuldoc/nuldoc-src/dom.ts b/services/nuldoc/nuldoc-src/dom.ts
deleted file mode 100644
index 5faf184a..00000000
--- a/services/nuldoc/nuldoc-src/dom.ts
+++ /dev/null
@@ -1,283 +0,0 @@
-export type Text = {
- kind: "text";
- content: string;
-};
-
-export type RawHTML = {
- kind: "raw";
- html: string;
-};
-
-export type Element = {
- kind: "element";
- name: string;
- attributes: Record<string, string>;
- children: Node[];
-};
-
-export type Node = Element | Text | RawHTML;
-
-export type NodeLike = Node | string | null | undefined | false | NodeLike[];
-
-function flattenChildren(children: NodeLike[]): Node[] {
- const result: Node[] = [];
- for (const child of children) {
- if (child === null || child === undefined || child === false) {
- continue;
- }
- if (typeof child === "string") {
- result.push(text(child));
- } else if (Array.isArray(child)) {
- result.push(...flattenChildren(child));
- } else {
- result.push(child);
- }
- }
- return result;
-}
-
-export function text(content: string): Text {
- return {
- kind: "text",
- content,
- };
-}
-
-export function rawHTML(html: string): RawHTML {
- return {
- kind: "raw",
- html,
- };
-}
-
-export function elem(
- name: string,
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-): Element {
- return {
- kind: "element",
- name,
- attributes: attributes || {},
- children: flattenChildren(children),
- };
-}
-
-// Helper functions for commonly used elements
-export const a = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("a", attributes, ...children);
-
-export const article = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("article", attributes, ...children);
-
-export const button = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("button", attributes, ...children);
-
-export const div = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("div", attributes, ...children);
-
-export const footer = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("footer", attributes, ...children);
-
-export const h1 = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("h1", attributes, ...children);
-
-export const h2 = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("h2", attributes, ...children);
-
-export const h3 = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("h3", attributes, ...children);
-
-export const h4 = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("h4", attributes, ...children);
-
-export const h5 = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("h5", attributes, ...children);
-
-export const h6 = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("h6", attributes, ...children);
-
-export const header = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("header", attributes, ...children);
-
-export const img = (attributes?: Record<string, string>) =>
- elem("img", attributes);
-
-export const li = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("li", attributes, ...children);
-
-export const link = (attributes?: Record<string, string>) =>
- elem("link", attributes);
-
-export const meta = (attributes?: Record<string, string>) =>
- elem("meta", attributes);
-
-export const nav = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("nav", attributes, ...children);
-
-export const ol = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("ol", attributes, ...children);
-
-export const p = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("p", attributes, ...children);
-
-export const script = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("script", attributes, ...children);
-
-export const section = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("section", attributes, ...children);
-
-export const span = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("span", attributes, ...children);
-
-export const ul = (
- attributes?: Record<string, string>,
- ...children: NodeLike[]
-) => elem("ul", attributes, ...children);
-
-export function addClass(e: Element, klass: string) {
- const classes = e.attributes.class;
- if (classes === undefined) {
- e.attributes.class = klass;
- } else {
- const classList = classes.split(" ");
- classList.push(klass);
- classList.sort();
- e.attributes.class = classList.join(" ");
- }
-}
-
-export function findFirstChildElement(
- e: Element,
- name: string,
-): Element | null {
- for (const c of e.children) {
- if (c.kind === "element" && c.name === name) {
- return c;
- }
- }
- return null;
-}
-
-export function findChildElements(e: Element, name: string): Element[] {
- const cs = [];
- for (const c of e.children) {
- if (c.kind === "element" && c.name === name) {
- cs.push(c);
- }
- }
- return cs;
-}
-
-export function innerText(e: Element): string {
- let t = "";
- forEachChild(e, (c) => {
- if (c.kind === "text") {
- t += c.content;
- }
- });
- return t;
-}
-
-export function forEachChild(e: Element, f: (n: Node) => void) {
- for (const c of e.children) {
- f(c);
- }
-}
-
-export async function forEachChildAsync(
- e: Element,
- f: (n: Node) => Promise<void>,
-): Promise<void> {
- for (const c of e.children) {
- await f(c);
- }
-}
-
-export function forEachChildRecursively(e: Element, f: (n: Node) => void) {
- const g = (c: Node) => {
- f(c);
- if (c.kind === "element") {
- forEachChild(c, g);
- }
- };
- forEachChild(e, g);
-}
-
-export async function forEachChildRecursivelyAsync(
- e: Element,
- f: (n: Node) => Promise<void>,
-): Promise<void> {
- const g = async (c: Node) => {
- await f(c);
- if (c.kind === "element") {
- await forEachChildAsync(c, g);
- }
- };
- await forEachChildAsync(e, g);
-}
-
-export function forEachElementOfType(
- root: Element,
- elementName: string,
- f: (e: Element) => void,
-) {
- forEachChildRecursively(root, (n) => {
- if (n.kind === "element" && n.name === elementName) {
- f(n);
- }
- });
-}
-
-export function processTextNodesInElement(
- e: Element,
- f: (text: string) => Node[],
-) {
- const newChildren: Node[] = [];
- for (const child of e.children) {
- if (child.kind === "text") {
- newChildren.push(...f(child.content));
- } else {
- newChildren.push(child);
- }
- }
- e.children = newChildren;
-}
diff --git a/services/nuldoc/nuldoc-src/errors.ts b/services/nuldoc/nuldoc-src/errors.ts
deleted file mode 100644
index 1692a4c8..00000000
--- a/services/nuldoc/nuldoc-src/errors.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export class NuldocError extends Error {
- static {
- this.prototype.name = "NuldocError";
- }
-}
-
-export class SlideError extends Error {
- static {
- this.prototype.name = "SlideError";
- }
-}
-
-export class XmlParseError extends Error {
- static {
- this.prototype.name = "XmlParseError";
- }
-}
diff --git a/services/nuldoc/nuldoc-src/generators/about.ts b/services/nuldoc/nuldoc-src/generators/about.ts
deleted file mode 100644
index 628c370e..00000000
--- a/services/nuldoc/nuldoc-src/generators/about.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import AboutPage from "../pages/AboutPage.ts";
-import { Config } from "../config.ts";
-import { Page } from "../page.ts";
-import { SlidePage } from "./slide.ts";
-
-export type AboutPage = Page;
-
-export async function generateAboutPage(
- slides: SlidePage[],
- config: Config,
-): Promise<AboutPage> {
- const html = await AboutPage(slides, config);
-
- return {
- root: html,
- renderer: "html",
- site: "about",
- destFilePath: "/index.html",
- href: "/",
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/atom.ts b/services/nuldoc/nuldoc-src/generators/atom.ts
deleted file mode 100644
index f501d834..00000000
--- a/services/nuldoc/nuldoc-src/generators/atom.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { Config } from "../config.ts";
-import { Page } from "../page.ts";
-import { PostPage } from "../generators/post.ts";
-import { SlidePage } from "../generators/slide.ts";
-import { dateToRfc3339String } from "../revision.ts";
-import AtomPage from "../pages/AtomPage.ts";
-
-export type Feed = {
- author: string;
- icon: string;
- id: string;
- linkToSelf: string;
- linkToAlternate: string;
- title: string;
- updated: string;
- entries: Entry[];
-};
-
-export type Entry = {
- id: string;
- linkToAlternate: string;
- published: string;
- summary: string;
- title: string;
- updated: string;
-};
-
-const BASE_NAME = "atom.xml";
-
-export function generateFeedPageFromEntries(
- alternateLink: string,
- feedSlug: string,
- feedTitle: string,
- entries: Array<PostPage | SlidePage>,
- site: "default" | "blog" | "slides",
- config: Config,
-): Page {
- const entries_: Entry[] = [];
- for (const entry of entries) {
- entries_.push({
- id: `urn:uuid:${entry.uuid}`,
- linkToAlternate: `https://${
- "event" in entry ? config.sites.slides.fqdn : config.sites.blog.fqdn
- }${entry.href}`,
- title: entry.title,
- summary: entry.description,
- published: dateToRfc3339String(entry.published),
- updated: dateToRfc3339String(entry.updated),
- });
- }
- // Sort by published date in ascending order.
- entries_.sort((a, b) => {
- if (a.published < b.published) {
- return 1;
- } else if (a.published > b.published) {
- return -1;
- }
- return 0;
- });
- const feedPath = `${alternateLink}${BASE_NAME}`;
- const feed: Feed = {
- author: config.site.author,
- icon: `https://${config.sites[site].fqdn}/favicon.svg`,
- id: `tag:${
- config.sites[site].fqdn
- },${config.site.copyrightYear}:${feedSlug}`,
- linkToSelf: `https://${config.sites[site].fqdn}${feedPath}`,
- linkToAlternate: `https://${config.sites[site].fqdn}${alternateLink}`,
- title: feedTitle,
- updated: entries_.reduce(
- (latest, entry) => entry.updated > latest ? entry.updated : latest,
- entries_[0].updated,
- ),
- entries: entries_,
- };
-
- return {
- root: AtomPage({ feed: feed }),
- renderer: "xml",
- site,
- destFilePath: feedPath,
- href: feedPath,
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/home.ts b/services/nuldoc/nuldoc-src/generators/home.ts
deleted file mode 100644
index 1839f5dd..00000000
--- a/services/nuldoc/nuldoc-src/generators/home.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import HomePage from "../pages/HomePage.ts";
-import { Config } from "../config.ts";
-import { Page } from "../page.ts";
-
-export type HomePage = Page;
-
-export async function generateHomePage(config: Config): Promise<HomePage> {
- const html = await HomePage(config);
-
- return {
- root: html,
- renderer: "html",
- site: "default",
- destFilePath: "/index.html",
- href: "/",
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/not_found.ts b/services/nuldoc/nuldoc-src/generators/not_found.ts
deleted file mode 100644
index 8a5593c3..00000000
--- a/services/nuldoc/nuldoc-src/generators/not_found.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import NotFoundPage from "../pages/NotFoundPage.ts";
-import { Config } from "../config.ts";
-import { Page } from "../page.ts";
-
-export type NotFoundPage = Page;
-
-export async function generateNotFoundPage(
- site: "default" | "about" | "blog" | "slides",
- config: Config,
-): Promise<NotFoundPage> {
- const html = await NotFoundPage(site, config);
-
- return {
- root: html,
- renderer: "html",
- site,
- destFilePath: "/404.html",
- href: "/404.html",
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/post.ts b/services/nuldoc/nuldoc-src/generators/post.ts
deleted file mode 100644
index 87205624..00000000
--- a/services/nuldoc/nuldoc-src/generators/post.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { join } from "@std/path";
-import PostPage from "../pages/PostPage.ts";
-import { Config } from "../config.ts";
-import { Document } from "../markdown/document.ts";
-import { Page } from "../page.ts";
-import { Date, Revision } from "../revision.ts";
-
-export interface PostPage extends Page {
- title: string;
- description: string;
- tags: string[];
- revisions: Revision[];
- published: Date;
- updated: Date;
- uuid: string;
- sourceFilePath: string;
-}
-
-export function getPostPublishedDate(page: { revisions: Revision[] }): Date {
- for (const rev of page.revisions) {
- if (!rev.isInternal) {
- return rev.date;
- }
- }
- return page.revisions[0].date;
-}
-
-export function getPostUpdatedDate(page: { revisions: Revision[] }): Date {
- return page.revisions[page.revisions.length - 1].date;
-}
-
-export function postHasAnyUpdates(page: { revisions: Revision[] }): boolean {
- return 2 <= page.revisions.filter((rev) => !rev.isInternal).length;
-}
-
-export async function generatePostPage(
- doc: Document,
- config: Config,
-): Promise<PostPage> {
- const html = await PostPage(doc, config);
-
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir);
- const destFilePath = join(
- doc.sourceFilePath.replace(contentDir, "").replace(".md", ""),
- "index.html",
- );
- return {
- root: html,
- renderer: "html",
- site: "blog",
- destFilePath: destFilePath,
- href: destFilePath.replace("index.html", ""),
- title: doc.title,
- description: doc.description,
- tags: doc.tags,
- revisions: doc.revisions,
- published: getPostPublishedDate(doc),
- updated: getPostUpdatedDate(doc),
- uuid: doc.uuid,
- sourceFilePath: doc.sourceFilePath,
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/post_list.ts b/services/nuldoc/nuldoc-src/generators/post_list.ts
deleted file mode 100644
index 3be4ec05..00000000
--- a/services/nuldoc/nuldoc-src/generators/post_list.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import PostListPage from "../pages/PostListPage.ts";
-import { Config } from "../config.ts";
-import { Page } from "../page.ts";
-import { PostPage } from "./post.ts";
-
-export type PostListPage = Page;
-
-export async function generatePostListPages(
- posts: PostPage[],
- config: Config,
-): Promise<PostListPage[]> {
- const postsPerPage = config.sites.blog.postsPerPage;
- const totalPages = Math.ceil(posts.length / postsPerPage);
- const pages: PostListPage[] = [];
-
- for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) {
- const pagePosts = posts.slice(
- pageIndex * postsPerPage,
- (pageIndex + 1) * postsPerPage,
- );
-
- const page = await generatePostListPage(
- pagePosts,
- config,
- pageIndex + 1,
- totalPages,
- );
-
- pages.push(page);
- }
-
- return pages;
-}
-
-async function generatePostListPage(
- posts: PostPage[],
- config: Config,
- currentPage: number,
- totalPages: number,
-): Promise<PostListPage> {
- const html = await PostListPage(posts, config, currentPage, totalPages);
-
- const destFilePath = currentPage === 1
- ? "/posts/index.html"
- : `/posts/${currentPage}/index.html`;
-
- const href = currentPage === 1 ? "/posts/" : `/posts/${currentPage}/`;
-
- return {
- root: html,
- renderer: "html",
- site: "blog",
- destFilePath,
- href,
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/slide.ts b/services/nuldoc/nuldoc-src/generators/slide.ts
deleted file mode 100644
index c13f6960..00000000
--- a/services/nuldoc/nuldoc-src/generators/slide.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { join } from "@std/path";
-import SlidePage from "../pages/SlidePage.ts";
-import { Config } from "../config.ts";
-import { Page } from "../page.ts";
-import { Date, Revision } from "../revision.ts";
-import { Slide } from "../slide/slide.ts";
-import { getPostPublishedDate, getPostUpdatedDate } from "./post.ts";
-
-export interface SlidePage extends Page {
- title: string;
- description: string;
- event: string;
- talkType: string;
- slideLink: string;
- tags: string[];
- revisions: Revision[];
- published: Date;
- updated: Date;
- uuid: string;
-}
-
-export async function generateSlidePage(
- slide: Slide,
- config: Config,
-): Promise<SlidePage> {
- const html = await SlidePage(slide, config);
-
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir);
- const destFilePath = join(
- slide.sourceFilePath.replace(contentDir, "").replace(".toml", ""),
- "index.html",
- );
- return {
- root: html,
- renderer: "html",
- site: "slides",
- destFilePath: destFilePath,
- href: destFilePath.replace("index.html", ""),
- title: slide.title,
- description: `${slide.event} (${slide.talkType})`,
- event: slide.event,
- talkType: slide.talkType,
- slideLink: slide.slideLink,
- tags: slide.tags,
- revisions: slide.revisions,
- published: getPostPublishedDate(slide),
- updated: getPostUpdatedDate(slide),
- uuid: slide.uuid,
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/slide_list.ts b/services/nuldoc/nuldoc-src/generators/slide_list.ts
deleted file mode 100644
index b65c9db5..00000000
--- a/services/nuldoc/nuldoc-src/generators/slide_list.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import SlideListPage from "../pages/SlideListPage.ts";
-import { Config } from "../config.ts";
-import { Page } from "../page.ts";
-import { SlidePage } from "./slide.ts";
-
-export type SlideListPage = Page;
-
-export async function generateSlideListPage(
- slides: SlidePage[],
- config: Config,
-): Promise<SlideListPage> {
- const html = await SlideListPage(slides, config);
-
- return {
- root: html,
- renderer: "html",
- site: "slides",
- destFilePath: "/slides/index.html",
- href: "/slides/",
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/tag.ts b/services/nuldoc/nuldoc-src/generators/tag.ts
deleted file mode 100644
index efe2da54..00000000
--- a/services/nuldoc/nuldoc-src/generators/tag.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import TagPage from "../pages/TagPage.ts";
-import { Config, getTagLabel } from "../config.ts";
-import { Page } from "../page.ts";
-import { TaggedPage } from "./tagged_page.ts";
-
-export interface TagPage extends Page {
- tagSlug: string;
- tagLabel: string;
- numOfPosts: number;
- numOfSlides: number;
-}
-
-export async function generateTagPage(
- tagSlug: string,
- pages: TaggedPage[],
- site: "blog" | "slides",
- config: Config,
-): Promise<TagPage> {
- const html = await TagPage(tagSlug, pages, site, config);
-
- return {
- root: html,
- renderer: "html",
- site,
- destFilePath: `/tags/${tagSlug}/index.html`,
- href: `/tags/${tagSlug}/`,
- tagSlug: tagSlug,
- tagLabel: getTagLabel(config, tagSlug),
- numOfPosts: pages.filter((p) => !("event" in p)).length,
- numOfSlides: pages.filter((p) => "event" in p).length,
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/tag_list.ts b/services/nuldoc/nuldoc-src/generators/tag_list.ts
deleted file mode 100644
index 96faa663..00000000
--- a/services/nuldoc/nuldoc-src/generators/tag_list.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import TagListPage from "../pages/TagListPage.ts";
-import { Config } from "../config.ts";
-import { Page } from "../page.ts";
-import { TagPage } from "./tag.ts";
-
-export type TagListPage = Page;
-
-export async function generateTagListPage(
- tags: TagPage[],
- site: "blog" | "slides",
- config: Config,
-): Promise<TagListPage> {
- const html = await TagListPage(tags, site, config);
-
- return {
- root: html,
- renderer: "html",
- site,
- destFilePath: "/tags/index.html",
- href: "/tags/",
- };
-}
diff --git a/services/nuldoc/nuldoc-src/generators/tagged_page.ts b/services/nuldoc/nuldoc-src/generators/tagged_page.ts
deleted file mode 100644
index 23de8cb4..00000000
--- a/services/nuldoc/nuldoc-src/generators/tagged_page.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { PostPage } from "./post.ts";
-import { SlidePage } from "./slide.ts";
-
-export type TaggedPage = PostPage | SlidePage;
diff --git a/services/nuldoc/nuldoc-src/main.ts b/services/nuldoc/nuldoc-src/main.ts
deleted file mode 100644
index af6acc2e..00000000
--- a/services/nuldoc/nuldoc-src/main.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { runBuildCommand } from "./commands/build.ts";
-import { runNewCommand } from "./commands/new.ts";
-import { runServeCommand } from "./commands/serve.ts";
-import { getDefaultConfigPath, loadConfig } from "./config.ts";
-
-const config = await loadConfig(getDefaultConfigPath());
-
-if (import.meta.main) {
- const command = Deno.args[0] ?? "build";
- if (command === "build") {
- await runBuildCommand(config);
- } else if (command === "new") {
- runNewCommand(config);
- } else if (command === "serve") {
- runServeCommand(config);
- } else {
- console.error(`Unknown command: ${command}`);
- }
-}
diff --git a/services/nuldoc/nuldoc-src/markdown/document.ts b/services/nuldoc/nuldoc-src/markdown/document.ts
deleted file mode 100644
index 1aee87b9..00000000
--- a/services/nuldoc/nuldoc-src/markdown/document.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import type { Root as MdastRoot } from "mdast";
-import { join } from "@std/path";
-import { z } from "zod/mod.ts";
-import { Config } from "../config.ts";
-import { Element } from "../dom.ts";
-import { Revision, stringToDate } from "../revision.ts";
-import { mdast2ndoc } from "./mdast2ndoc.ts";
-
-export const PostMetadataSchema = z.object({
- article: z.object({
- uuid: z.string(),
- title: z.string(),
- description: z.string(),
- tags: z.array(z.string()),
- toc: z.boolean().optional(),
- revisions: z.array(z.object({
- date: z.string(),
- remark: z.string(),
- isInternal: z.boolean().optional(),
- })),
- }),
-});
-
-export type PostMetadata = z.infer<typeof PostMetadataSchema>;
-
-export type TocEntry = {
- id: string;
- text: string;
- level: number;
- children: TocEntry[];
-};
-
-export type TocRoot = {
- entries: TocEntry[];
-};
-
-export type Document = {
- root: Element;
- sourceFilePath: string;
- uuid: string;
- link: string;
- title: string;
- description: string;
- tags: string[];
- revisions: Revision[];
- toc?: TocRoot;
- isTocEnabled: boolean;
-};
-
-export function createNewDocumentFromMdast(
- root: MdastRoot,
- meta: PostMetadata,
- sourceFilePath: string,
- config: Config,
-): Document {
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir);
- const link = sourceFilePath.replace(contentDir, "").replace(".xml", "/");
- return {
- root: mdast2ndoc(root),
- sourceFilePath,
- uuid: meta.article.uuid,
- link: link,
- title: meta.article.title,
- description: meta.article.description,
- tags: meta.article.tags,
- revisions: meta.article.revisions.map((r, i) => ({
- number: i,
- date: stringToDate(r.date),
- remark: r.remark,
- isInternal: !!r.isInternal,
- })),
- isTocEnabled: meta.article.toc !== false,
- };
-}
diff --git a/services/nuldoc/nuldoc-src/markdown/mdast2ndoc.ts b/services/nuldoc/nuldoc-src/markdown/mdast2ndoc.ts
deleted file mode 100644
index 626f1191..00000000
--- a/services/nuldoc/nuldoc-src/markdown/mdast2ndoc.ts
+++ /dev/null
@@ -1,589 +0,0 @@
-import type {
- Blockquote,
- Code,
- Definition,
- Delete,
- Emphasis,
- FootnoteDefinition,
- FootnoteReference,
- Heading,
- Html,
- Image,
- InlineCode,
- Link,
- List,
- ListItem,
- Paragraph,
- PhrasingContent,
- Root,
- RootContent,
- Strong,
- Table,
- TableCell,
- TableRow,
- Text as MdastText,
- ThematicBreak,
-} from "mdast";
-import type {
- ContainerDirective,
- LeafDirective,
- TextDirective,
-} from "mdast-util-directive";
-import {
- a,
- article,
- div,
- elem,
- Element,
- img,
- li,
- Node,
- ol,
- p,
- rawHTML,
- section,
- span,
- text,
- ul,
-} from "../dom.ts";
-
-type DirectiveNode = ContainerDirective | LeafDirective | TextDirective;
-
-function isDirective(node: RootContent): node is DirectiveNode {
- return (
- node.type === "containerDirective" ||
- node.type === "leafDirective" ||
- node.type === "textDirective"
- );
-}
-
-// Extract section ID and attributes from heading if present
-// Supports syntax like {#id} or {#id attr="value"}
-function extractSectionId(
- node: Heading,
-): {
- id: string | null;
- attributes: Record<string, string>;
- children: Heading["children"];
-} {
- if (node.children.length === 0) {
- return { id: null, attributes: {}, children: node.children };
- }
-
- const lastChild = node.children[node.children.length - 1];
- if (lastChild && lastChild.type === "text") {
- // Match {#id ...} or {#id attr="value" ...}
- const match = lastChild.value.match(/\s*\{#([^\s}]+)([^}]*)\}\s*$/);
- if (match) {
- const id = match[1];
- const attrString = match[2].trim();
- const attributes: Record<string, string> = {};
-
- // Parse attributes like toc="false" (supports smart quotes too)
- // U+0022 = ", U+201C = ", U+201D = "
- const attrRegex =
- /(\w+)=["\u201c\u201d]([^"\u201c\u201d]*)["\u201c\u201d]/g;
- let attrMatch;
- while ((attrMatch = attrRegex.exec(attrString)) !== null) {
- attributes[attrMatch[1]] = attrMatch[2];
- }
-
- const newValue = lastChild.value.replace(/\s*\{#[^}]+\}\s*$/, "");
- if (newValue === "") {
- return { id, attributes, children: node.children.slice(0, -1) };
- } else {
- const newChildren = [...node.children];
- newChildren[newChildren.length - 1] = { ...lastChild, value: newValue };
- return { id, attributes, children: newChildren };
- }
- }
- }
-
- return { id: null, attributes: {}, children: node.children };
-}
-
-function processBlock(node: RootContent): Element | Element[] | null {
- switch (node.type) {
- case "heading":
- // Headings are handled specially in mdast2ndoc
- return null;
- case "paragraph":
- return processParagraph(node);
- case "thematicBreak":
- return processThematicBreak(node);
- case "blockquote":
- return processBlockquote(node);
- case "code":
- return processCode(node);
- case "list":
- return processList(node);
- case "table":
- return processTable(node);
- case "html":
- return processHtmlBlock(node);
- case "definition":
- return processDefinition(node);
- case "footnoteDefinition":
- return processFootnoteDefinition(node);
- default:
- if (isDirective(node)) {
- return processDirective(node);
- }
- return null;
- }
-}
-
-function processParagraph(node: Paragraph): Element {
- return p({}, ...node.children.map(processInline));
-}
-
-function processThematicBreak(_node: ThematicBreak): Element {
- return elem("hr", {});
-}
-
-function processBlockquote(node: Blockquote): Element {
- const children: Node[] = [];
- for (const child of node.children) {
- const result = processBlock(child);
- if (result) {
- if (Array.isArray(result)) {
- children.push(...result);
- } else {
- children.push(result);
- }
- }
- }
- return elem("blockquote", {}, ...children);
-}
-
-function processCode(node: Code): Element {
- const attributes: Record<string, string> = {};
-
- if (node.lang) {
- attributes.language = node.lang;
- }
-
- // Parse meta string for filename and numbered attributes
- if (node.meta) {
- const filenameMatch = node.meta.match(/filename="([^"]+)"/);
- if (filenameMatch) {
- attributes.filename = filenameMatch[1];
- }
-
- if (node.meta.includes("numbered")) {
- attributes.numbered = "true";
- }
- }
-
- return elem("codeblock", attributes, text(node.value));
-}
-
-function processList(node: List): Element {
- const attributes: Record<string, string> = {};
- attributes.__tight = node.spread === false ? "true" : "false";
-
- const isTaskList = node.children.some(
- (item) => item.checked !== null && item.checked !== undefined,
- );
-
- if (isTaskList) {
- attributes.type = "task";
- }
-
- if (node.ordered && node.start !== null && node.start !== 1) {
- attributes.start = node.start!.toString();
- }
-
- const children = node.children.map((item) =>
- processListItem(item, isTaskList)
- );
-
- return node.ordered
- ? ol(attributes, ...children)
- : ul(attributes, ...children);
-}
-
-function processListItem(node: ListItem, isTaskList: boolean): Element {
- const attributes: Record<string, string> = {};
-
- if (isTaskList) {
- attributes.checked = node.checked ? "true" : "false";
- }
-
- const children: Node[] = [];
- for (const child of node.children) {
- const result = processBlock(child);
- if (result) {
- if (Array.isArray(result)) {
- children.push(...result);
- } else {
- children.push(result);
- }
- }
- }
-
- return li(attributes, ...children);
-}
-
-function processTable(node: Table): Element {
- const tableElement = elem("table", {});
- const headerRows: Element[] = [];
- const bodyRows: Element[] = [];
-
- node.children.forEach((row, rowIndex) => {
- const rowElement = processTableRow(row, rowIndex === 0, node.align);
- if (rowIndex === 0) {
- headerRows.push(rowElement);
- } else {
- bodyRows.push(rowElement);
- }
- });
-
- if (headerRows.length > 0) {
- tableElement.children.push(elem("thead", undefined, ...headerRows));
- }
-
- if (bodyRows.length > 0) {
- tableElement.children.push(elem("tbody", undefined, ...bodyRows));
- }
-
- return tableElement;
-}
-
-function processTableRow(
- node: TableRow,
- isHeader: boolean,
- alignments: (string | null)[] | null | undefined,
-): Element {
- const cells = node.children.map((cell, index) =>
- processTableCell(cell, isHeader, alignments?.[index])
- );
- return elem("tr", {}, ...cells);
-}
-
-function processTableCell(
- node: TableCell,
- isHeader: boolean,
- alignment: string | null | undefined,
-): Element {
- const attributes: Record<string, string> = {};
- if (alignment && alignment !== "none") {
- attributes.align = alignment;
- }
-
- return elem(
- isHeader ? "th" : "td",
- attributes,
- ...node.children.map(processInline),
- );
-}
-
-function processHtmlBlock(node: Html): Element {
- return div({ class: "raw-html" }, rawHTML(node.value));
-}
-
-function processDefinition(_node: Definition): null {
- // Link definitions are handled elsewhere
- return null;
-}
-
-function processFootnoteDefinition(node: FootnoteDefinition): Element {
- const children: Node[] = [];
- for (const child of node.children) {
- const result = processBlock(child);
- if (result) {
- if (Array.isArray(result)) {
- children.push(...result);
- } else {
- children.push(result);
- }
- }
- }
- return elem("footnote", { id: node.identifier }, ...children);
-}
-
-function processDirective(node: DirectiveNode): Element | null {
- const name = node.name;
-
- if (name === "note" || name === "edit") {
- const attributes: Record<string, string> = {};
-
- // Copy directive attributes
- if (node.attributes) {
- for (const [key, value] of Object.entries(node.attributes)) {
- if (value !== undefined && value !== null) {
- attributes[key] = String(value);
- }
- }
- }
-
- const children: Node[] = [];
- if ("children" in node && node.children) {
- for (const child of node.children as RootContent[]) {
- const result = processBlock(child);
- if (result) {
- if (Array.isArray(result)) {
- children.push(...result);
- } else {
- children.push(result);
- }
- }
- }
- }
-
- return elem("note", attributes, ...children);
- }
-
- // For other directives, treat as div
- const children: Node[] = [];
- if ("children" in node && node.children) {
- for (const child of node.children as RootContent[]) {
- const result = processBlock(child);
- if (result) {
- if (Array.isArray(result)) {
- children.push(...result);
- } else {
- children.push(result);
- }
- }
- }
- }
-
- return div(
- node.attributes as Record<string, string> || {},
- ...children,
- );
-}
-
-function processInline(node: PhrasingContent): Node {
- switch (node.type) {
- case "text":
- return processText(node);
- case "emphasis":
- return processEmphasis(node);
- case "strong":
- return processStrong(node);
- case "inlineCode":
- return processInlineCode(node);
- case "link":
- return processLink(node);
- case "image":
- return processImage(node);
- case "delete":
- return processDelete(node);
- case "break":
- return elem("br");
- case "html":
- return rawHTML(node.value);
- case "footnoteReference":
- return processFootnoteReference(node);
- default:
- // Handle any unexpected node types
- if ("value" in node) {
- return text(String(node.value));
- }
- if ("children" in node && Array.isArray(node.children)) {
- return span(
- {},
- ...node.children.map((c: PhrasingContent) => processInline(c)),
- );
- }
- return text("");
- }
-}
-
-function processText(node: MdastText): Node {
- return text(node.value);
-}
-
-function processEmphasis(node: Emphasis): Element {
- return elem("em", {}, ...node.children.map(processInline));
-}
-
-function processStrong(node: Strong): Element {
- return elem("strong", {}, ...node.children.map(processInline));
-}
-
-function processInlineCode(node: InlineCode): Element {
- return elem("code", {}, text(node.value));
-}
-
-function processLink(node: Link): Element {
- const attributes: Record<string, string> = {};
- if (node.url) {
- attributes.href = node.url;
- }
- if (node.title) {
- attributes.title = node.title;
- }
- // Detect autolinks (URL equals link text)
- const isAutolink = node.children.length === 1 &&
- node.children[0].type === "text" &&
- node.children[0].value === node.url;
- if (isAutolink) {
- attributes.class = "url";
- }
- return a(attributes, ...node.children.map(processInline));
-}
-
-function processImage(node: Image): Element {
- const attributes: Record<string, string> = {};
- if (node.url) {
- attributes.src = node.url;
- }
- if (node.alt) {
- attributes.alt = node.alt;
- }
- if (node.title) {
- attributes.title = node.title;
- }
- return img(attributes);
-}
-
-function processDelete(node: Delete): Element {
- return elem("del", {}, ...node.children.map(processInline));
-}
-
-function processFootnoteReference(node: FootnoteReference): Element {
- return elem("footnoteref", { reference: node.identifier });
-}
-
-// Build hierarchical section structure from flat mdast
-// This mimics Djot's section structure where headings create nested sections
-export function mdast2ndoc(root: Root): Element {
- const footnotes: Element[] = [];
- const nonFootnoteChildren: RootContent[] = [];
-
- // Separate footnotes from other content
- for (const child of root.children) {
- if (child.type === "footnoteDefinition") {
- const footnote = processFootnoteDefinition(child);
- footnotes.push(footnote);
- } else {
- nonFootnoteChildren.push(child);
- }
- }
-
- // Build hierarchical sections
- const articleContent = buildSectionHierarchy(nonFootnoteChildren);
-
- // Add footnotes section if any exist
- if (footnotes.length > 0) {
- const footnoteSection = section(
- { class: "footnotes" },
- ...footnotes,
- );
- articleContent.push(footnoteSection);
- }
-
- return elem(
- "__root__",
- undefined,
- article(undefined, ...articleContent),
- );
-}
-
-type SectionInfo = {
- id: string | null;
- attributes: Record<string, string>;
- level: number;
- heading: Element;
- children: Node[];
-};
-
-function buildSectionHierarchy(nodes: RootContent[]): Node[] {
- // Group nodes into sections based on headings
- // Each heading starts a new section at its level
- const result: Node[] = [];
- const sectionStack: SectionInfo[] = [];
-
- for (const node of nodes) {
- if (node.type === "heading") {
- const level = node.depth;
- const { id, attributes, children } = extractSectionId(node);
-
- // Create heading element
- const headingElement = elem(
- "h",
- {},
- ...children.map(processInline),
- );
-
- // Close sections that are at same or deeper level
- while (
- sectionStack.length > 0 &&
- sectionStack[sectionStack.length - 1].level >= level
- ) {
- const closedSection = sectionStack.pop()!;
- const sectionElement = createSectionElement(closedSection);
-
- if (sectionStack.length > 0) {
- // Add to parent section
- sectionStack[sectionStack.length - 1].children.push(sectionElement);
- } else {
- // Add to result
- result.push(sectionElement);
- }
- }
-
- // Start new section
- const newSection: SectionInfo = {
- id,
- attributes,
- level,
- heading: headingElement,
- children: [],
- };
- sectionStack.push(newSection);
- } else {
- // Non-heading content
- const processed = processBlock(node);
- if (processed) {
- if (sectionStack.length > 0) {
- // Add to current section
- if (Array.isArray(processed)) {
- sectionStack[sectionStack.length - 1].children.push(...processed);
- } else {
- sectionStack[sectionStack.length - 1].children.push(processed);
- }
- } else {
- // Content before any heading
- if (Array.isArray(processed)) {
- result.push(...processed);
- } else {
- result.push(processed);
- }
- }
- }
- }
- }
-
- // Close remaining sections
- while (sectionStack.length > 0) {
- const closedSection = sectionStack.pop()!;
- const sectionElement = createSectionElement(closedSection);
-
- if (sectionStack.length > 0) {
- // Add to parent section
- sectionStack[sectionStack.length - 1].children.push(sectionElement);
- } else {
- // Add to result
- result.push(sectionElement);
- }
- }
-
- return result;
-}
-
-function createSectionElement(sectionInfo: SectionInfo): Element {
- const attributes: Record<string, string> = { ...sectionInfo.attributes };
- if (sectionInfo.id) {
- attributes.id = sectionInfo.id;
- }
-
- return section(
- attributes,
- sectionInfo.heading,
- ...sectionInfo.children,
- );
-}
diff --git a/services/nuldoc/nuldoc-src/markdown/parse.ts b/services/nuldoc/nuldoc-src/markdown/parse.ts
deleted file mode 100644
index c0875a25..00000000
--- a/services/nuldoc/nuldoc-src/markdown/parse.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { Root as MdastRoot } from "mdast";
-import { unified } from "unified";
-import remarkParse from "remark-parse";
-import remarkGfm from "remark-gfm";
-import remarkDirective from "remark-directive";
-import remarkSmartypants from "remark-smartypants";
-import { parse as parseToml } from "@std/toml";
-import { Config } from "../config.ts";
-import {
- createNewDocumentFromMdast,
- Document,
- PostMetadata,
- PostMetadataSchema,
-} from "./document.ts";
-import toHtml from "./to_html.ts";
-
-export async function parseMarkdownFile(
- filePath: string,
- config: Config,
-): Promise<Document> {
- try {
- const fileContent = await Deno.readTextFile(filePath);
- const [, frontmatter, ...rest] = fileContent.split(/^---$/m);
- const meta = parseMetadata(frontmatter);
- const content = rest.join("---");
-
- const processor = unified()
- .use(remarkParse)
- .use(remarkGfm)
- .use(remarkDirective)
- .use(remarkSmartypants);
-
- const root = await processor.run(processor.parse(content)) as MdastRoot;
-
- const doc = createNewDocumentFromMdast(root, meta, filePath, config);
- return await toHtml(doc);
- } catch (e) {
- if (e instanceof Error) {
- e.message = `${e.message} in ${filePath}`;
- }
- throw e;
- }
-}
-
-function parseMetadata(s: string): PostMetadata {
- return PostMetadataSchema.parse(parseToml(s));
-}
diff --git a/services/nuldoc/nuldoc-src/markdown/to_html.ts b/services/nuldoc/nuldoc-src/markdown/to_html.ts
deleted file mode 100644
index 8758b0d3..00000000
--- a/services/nuldoc/nuldoc-src/markdown/to_html.ts
+++ /dev/null
@@ -1,496 +0,0 @@
-import { BundledLanguage, bundledLanguages, codeToHtml } from "shiki";
-import { Document, TocEntry } from "./document.ts";
-import { NuldocError } from "../errors.ts";
-import {
- a,
- addClass,
- div,
- Element,
- forEachChild,
- forEachChildRecursively,
- forEachChildRecursivelyAsync,
- forEachElementOfType,
- innerText,
- Node,
- processTextNodesInElement,
- RawHTML,
- rawHTML,
- Text,
- text,
-} from "../dom.ts";
-
-export default async function toHtml(doc: Document): Promise<Document> {
- mergeConsecutiveTextNodes(doc);
- removeUnnecessaryTextNode(doc);
- transformLinkLikeToAnchorElement(doc);
- transformSectionIdAttribute(doc);
- setSectionTitleAnchor(doc);
- transformSectionTitleElement(doc);
- transformNoteElement(doc);
- addAttributesToExternalLinkElement(doc);
- traverseFootnotes(doc);
- removeUnnecessaryParagraphNode(doc);
- await transformAndHighlightCodeBlockElement(doc);
- mergeConsecutiveTextNodes(doc);
- generateTableOfContents(doc);
- removeTocAttributes(doc);
- return doc;
-}
-
-function mergeConsecutiveTextNodes(doc: Document) {
- forEachChildRecursively(doc.root, (n) => {
- if (n.kind !== "element") {
- return;
- }
-
- const newChildren: Node[] = [];
- let currentTextContent = "";
-
- for (const child of n.children) {
- if (child.kind === "text") {
- currentTextContent += child.content;
- } else {
- if (currentTextContent !== "") {
- newChildren.push(text(currentTextContent));
- currentTextContent = "";
- }
- newChildren.push(child);
- }
- }
-
- if (currentTextContent !== "") {
- newChildren.push(text(currentTextContent));
- }
-
- n.children = newChildren;
- });
-}
-
-function removeUnnecessaryTextNode(doc: Document) {
- forEachChildRecursively(doc.root, (n) => {
- if (n.kind !== "element") {
- return;
- }
-
- let changed = true;
- while (changed) {
- changed = false;
- if (n.children.length === 0) {
- break;
- }
- const firstChild = n.children[0];
- if (firstChild.kind === "text" && firstChild.content.trim() === "") {
- n.children.shift();
- changed = true;
- }
- if (n.children.length === 0) {
- break;
- }
- const lastChild = n.children[n.children.length - 1];
- if (lastChild.kind === "text" && lastChild.content.trim() === "") {
- n.children.pop();
- changed = true;
- }
- }
- });
-}
-
-function transformLinkLikeToAnchorElement(doc: Document) {
- forEachChildRecursively(doc.root, (n) => {
- if (
- n.kind !== "element" || n.name === "a" || n.name === "code" ||
- n.name === "codeblock"
- ) {
- return;
- }
-
- processTextNodesInElement(n, (content) => {
- const nodes: Node[] = [];
- let restContent = content;
- while (restContent !== "") {
- const match = /^(.*?)(https?:\/\/[^ \n]+)(.*)$/s.exec(restContent);
- if (!match) {
- nodes.push(text(restContent));
- restContent = "";
- break;
- }
- const [_, prefix, url, suffix] = match;
- nodes.push(text(prefix));
- nodes.push(a({ href: url, class: "url" }, text(url)));
- restContent = suffix;
- }
- return nodes;
- });
- });
-}
-
-function transformSectionIdAttribute(doc: Document) {
- const sectionStack: string[] = [];
- const usedIds = new Set<string>();
-
- const processNode = (n: Node) => {
- if (n.kind !== "element") {
- return;
- }
-
- if (n.name === "section") {
- const idAttr = n.attributes.id;
- if (!idAttr) {
- return;
- }
-
- let newId: string;
- if (sectionStack.length === 0) {
- newId = `section--${idAttr}`;
- } else {
- newId = `section--${sectionStack.join("--")}--${idAttr}`;
- }
-
- if (usedIds.has(newId)) {
- throw new NuldocError(
- `[nuldoc.tohtml] Duplicate section ID: ${newId}`,
- );
- }
-
- usedIds.add(newId);
- n.attributes.id = newId;
- sectionStack.push(idAttr);
-
- forEachChild(n, processNode);
-
- sectionStack.pop();
- } else {
- forEachChild(n, processNode);
- }
- };
-
- forEachChild(doc.root, processNode);
-}
-
-function setSectionTitleAnchor(doc: Document) {
- const sectionStack: Element[] = [];
- const g = (c: Node) => {
- if (c.kind !== "element") {
- return;
- }
-
- if (c.name === "section") {
- sectionStack.push(c);
- }
- forEachChild(c, g);
- if (c.name === "section") {
- sectionStack.pop();
- }
- if (c.name === "h") {
- const currentSection = sectionStack[sectionStack.length - 1];
- if (!currentSection) {
- throw new NuldocError(
- "[nuldoc.tohtml] <h> element must be inside <section>",
- );
- }
- const sectionId = currentSection.attributes.id;
- const aElement = a(undefined, ...c.children);
- aElement.attributes.href = `#${sectionId}`;
- c.children = [aElement];
- }
- };
- forEachChild(doc.root, g);
-}
-
-function transformSectionTitleElement(doc: Document) {
- let sectionLevel = 1;
- const g = (c: Node) => {
- if (c.kind !== "element") {
- return;
- }
-
- if (c.name === "section") {
- sectionLevel += 1;
- c.attributes.__sectionLevel = sectionLevel.toString();
- }
- forEachChild(c, g);
- if (c.name === "section") {
- sectionLevel -= 1;
- }
- if (c.name === "h") {
- c.name = `h${sectionLevel}`;
- }
- };
- forEachChild(doc.root, g);
-}
-
-function transformNoteElement(doc: Document) {
- forEachElementOfType(doc.root, "note", (n) => {
- const editatAttr = n.attributes?.editat;
- const operationAttr = n.attributes?.operation;
- const isEditBlock = editatAttr && operationAttr;
-
- const labelElement = div(
- { class: "admonition-label" },
- text(isEditBlock ? `${editatAttr} ${operationAttr}` : "NOTE"),
- );
- const contentElement = div(
- { class: "admonition-content" },
- ...n.children,
- );
- n.name = "div";
- addClass(n, "admonition");
- n.children = [labelElement, contentElement];
- });
-}
-
-function addAttributesToExternalLinkElement(doc: Document) {
- forEachElementOfType(doc.root, "a", (n) => {
- const href = n.attributes.href ?? "";
- if (!href.startsWith("http")) {
- return;
- }
- n.attributes.target = "_blank";
- n.attributes.rel = "noreferrer";
- });
-}
-
-function traverseFootnotes(doc: Document) {
- let footnoteCounter = 0;
- const footnoteMap = new Map<string, number>();
-
- forEachElementOfType(doc.root, "footnoteref", (n) => {
- const reference = n.attributes.reference;
- if (!reference) {
- return;
- }
-
- let footnoteNumber: number;
- if (footnoteMap.has(reference)) {
- footnoteNumber = footnoteMap.get(reference)!;
- } else {
- footnoteNumber = ++footnoteCounter;
- footnoteMap.set(reference, footnoteNumber);
- }
-
- n.name = "sup";
- delete n.attributes.reference;
- n.attributes.class = "footnote";
- n.children = [
- a(
- {
- id: `footnoteref--${reference}`,
- class: "footnote",
- href: `#footnote--${reference}`,
- },
- text(`[${footnoteNumber}]`),
- ),
- ];
- });
-
- forEachElementOfType(doc.root, "footnote", (n) => {
- const id = n.attributes.id;
- if (!id || !footnoteMap.has(id)) {
- n.name = "span";
- n.children = [];
- return;
- }
-
- const footnoteNumber = footnoteMap.get(id)!;
-
- n.name = "div";
- delete n.attributes.id;
- n.attributes.class = "footnote";
- n.attributes.id = `footnote--${id}`;
-
- n.children = [
- a(
- { href: `#footnoteref--${id}` },
- text(`${footnoteNumber}. `),
- ),
- ...n.children,
- ];
- });
-}
-
-function removeUnnecessaryParagraphNode(doc: Document) {
- forEachChildRecursively(doc.root, (n) => {
- if (n.kind !== "element" || (n.name !== "ul" && n.name !== "ol")) {
- return;
- }
-
- const isTight = n.attributes.__tight === "true";
- if (!isTight) {
- return;
- }
-
- for (const child of n.children) {
- if (child.kind !== "element" || child.name !== "li") {
- continue;
- }
- const newGrandChildren: Node[] = [];
- for (const grandChild of child.children) {
- if (grandChild.kind === "element" && grandChild.name === "p") {
- newGrandChildren.push(...grandChild.children);
- } else {
- newGrandChildren.push(grandChild);
- }
- }
- child.children = newGrandChildren;
- }
- });
-}
-
-async function transformAndHighlightCodeBlockElement(doc: Document) {
- await forEachChildRecursivelyAsync(doc.root, async (n) => {
- if (n.kind !== "element" || n.name !== "codeblock") {
- return;
- }
-
- const language = n.attributes.language || "text";
- const filename = n.attributes.filename;
- const numbered = n.attributes.numbered;
- const sourceCodeNode = n.children[0] as Text | RawHTML;
- const sourceCode = sourceCodeNode.kind === "text"
- ? sourceCodeNode.content.trimEnd()
- : sourceCodeNode.html.trimEnd();
-
- const highlighted = await codeToHtml(sourceCode, {
- lang: language in bundledLanguages ? language as BundledLanguage : "text",
- theme: "github-light",
- colorReplacements: {
- "#fff": "#f5f5f5",
- },
- });
-
- n.name = "div";
- n.attributes.class = "codeblock";
- delete n.attributes.language;
-
- if (numbered === "true") {
- delete n.attributes.numbered;
- addClass(n, "numbered");
- }
- if (filename) {
- delete n.attributes.filename;
-
- n.children = [
- div({ class: "filename" }, text(filename)),
- rawHTML(highlighted),
- ];
- } else {
- if (sourceCodeNode.kind === "text") {
- n.children[0] = rawHTML(highlighted);
- } else {
- sourceCodeNode.html = highlighted;
- }
- }
- });
-}
-
-function generateTableOfContents(doc: Document) {
- if (!doc.isTocEnabled) {
- return;
- }
- const tocEntries: TocEntry[] = [];
- const stack: TocEntry[] = [];
- const excludedLevels: number[] = []; // Track levels to exclude
-
- const processNode = (node: Node) => {
- if (node.kind !== "element") {
- return;
- }
-
- const match = node.name.match(/^h(\d+)$/);
- if (match) {
- const level = parseInt(match[1]);
-
- let parentSection: Element | null = null;
- const findParentSection = (n: Node, target: Node): Element | null => {
- if (n.kind !== "element") return null;
-
- for (const child of n.children) {
- if (child === target && n.name === "section") {
- return n;
- }
- const result = findParentSection(child, target);
- if (result) return result;
- }
- return null;
- };
-
- parentSection = findParentSection(doc.root, node);
- if (!parentSection) return;
-
- // Check if this section has toc=false attribute
- const tocAttribute = parentSection.attributes.toc;
- if (tocAttribute === "false") {
- // Add this level to excluded levels and remove deeper levels
- excludedLevels.length = 0;
- excludedLevels.push(level);
- return;
- }
-
- // Check if this header should be excluded based on parent exclusion
- const shouldExclude = excludedLevels.some((excludedLevel) =>
- level > excludedLevel
- );
- if (shouldExclude) {
- return;
- }
-
- // Clean up excluded levels that are now at same or deeper level
- while (
- excludedLevels.length > 0 &&
- excludedLevels[excludedLevels.length - 1] >= level
- ) {
- excludedLevels.pop();
- }
-
- const sectionId = parentSection.attributes.id;
- if (!sectionId) return;
-
- let headingText = "";
- for (const child of node.children) {
- if (child.kind === "element" && child.name === "a") {
- headingText = innerText(child);
- }
- }
-
- const entry: TocEntry = {
- id: sectionId,
- text: headingText,
- level: level,
- children: [],
- };
-
- while (stack.length > 0 && stack[stack.length - 1].level >= level) {
- stack.pop();
- }
-
- if (stack.length === 0) {
- tocEntries.push(entry);
- } else {
- stack[stack.length - 1].children.push(entry);
- }
-
- stack.push(entry);
- }
-
- forEachChild(node, processNode);
- };
-
- forEachChild(doc.root, processNode);
-
- // Don't generate TOC if there's only one top-level section with no children
- if (tocEntries.length === 1 && tocEntries[0].children.length === 0) {
- return;
- }
-
- doc.toc = {
- entries: tocEntries,
- };
-}
-
-function removeTocAttributes(doc: Document) {
- forEachChildRecursively(doc.root, (node) => {
- if (node.kind === "element" && node.name === "section") {
- delete node.attributes.toc;
- }
- });
-}
diff --git a/services/nuldoc/nuldoc-src/page.ts b/services/nuldoc/nuldoc-src/page.ts
deleted file mode 100644
index 26cb4dee..00000000
--- a/services/nuldoc/nuldoc-src/page.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Element } from "./dom.ts";
-import { RendererType } from "./render.ts";
-
-export interface Page {
- root: Element;
- renderer: RendererType;
- site: "default" | "about" | "blog" | "slides";
- destFilePath: string;
- href: string;
-}
diff --git a/services/nuldoc/nuldoc-src/pages/AboutPage.ts b/services/nuldoc/nuldoc-src/pages/AboutPage.ts
deleted file mode 100644
index 5ae2ff52..00000000
--- a/services/nuldoc/nuldoc-src/pages/AboutPage.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import GlobalHeader from "../components/AboutGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import StaticScript from "../components/StaticScript.ts";
-import { Config } from "../config.ts";
-import { dateToString } from "../revision.ts";
-import { getPostPublishedDate } from "../generators/post.ts";
-import { SlidePage } from "../generators/slide.ts";
-import {
- a,
- article,
- div,
- elem,
- Element,
- h1,
- h2,
- header,
- img,
- li,
- p,
- section,
- ul,
-} from "../dom.ts";
-
-export default async function AboutPage(
- slides: SlidePage[],
- config: Config,
-): Promise<Element> {
- return await PageLayout({
- metaCopyrightYear: config.site.copyrightYear,
- metaDescription: "このサイトの著者について",
- metaTitle: `About|${config.sites.about.siteName}`,
- site: "about",
- config,
- children: elem(
- "body",
- { class: "single" },
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- article(
- { class: "post-single" },
- header(
- { class: "post-header" },
- h1({ class: "post-title" }, "nsfisis"),
- div(
- { class: "my-icon" },
- div(
- { id: "myIcon" },
- img({ src: "/favicon.svg" }),
- ),
- await StaticScript({
- site: "about",
- fileName: "/my-icon.js",
- defer: "true",
- config,
- }),
- ),
- ),
- div(
- { class: "post-content" },
- section(
- {},
- h2({}, "読み方"),
- p(
- {},
- "読み方は決めていません。音にする必要があるときは本名である「いまむら」をお使いください。",
- ),
- ),
- section(
- {},
- h2({}, "アカウント"),
- ul(
- {},
- li(
- {},
- a(
- {
- href: "https://twitter.com/nsfisis",
- target: "_blank",
- rel: "noreferrer",
- },
- "Twitter (現 𝕏): @nsfisis",
- ),
- ),
- li(
- {},
- a(
- {
- href: "https://github.com/nsfisis",
- target: "_blank",
- rel: "noreferrer",
- },
- "GitHub: @nsfisis",
- ),
- ),
- ),
- ),
- section(
- {},
- h2({}, "仕事"),
- ul(
- {},
- li(
- {},
- "2021-01~現在: ",
- a(
- {
- href: "https://www.dgcircus.com/",
- target: "_blank",
- rel: "noreferrer",
- },
- "デジタルサーカス株式会社",
- ),
- ),
- ),
- ),
- section(
- {},
- h2({}, "登壇"),
- ul(
- {},
- ...Array.from(slides)
- .sort((s1, s2) => {
- const ta = dateToString(getPostPublishedDate(s1));
- const tb = dateToString(getPostPublishedDate(s2));
- if (ta > tb) return -1;
- if (ta < tb) return 1;
- return 0;
- })
- .map((slide) =>
- li(
- {},
- a(
- {
- href:
- `https://${config.sites.slides.fqdn}${slide.href}`,
- },
- `${
- dateToString(getPostPublishedDate(slide))
- }: ${slide.event} (${slide.talkType})`,
- ),
- )
- ),
- ),
- ),
- ),
- ),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/pages/AtomPage.ts b/services/nuldoc/nuldoc-src/pages/AtomPage.ts
deleted file mode 100644
index b39902ee..00000000
--- a/services/nuldoc/nuldoc-src/pages/AtomPage.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Feed } from "../generators/atom.ts";
-import { elem, Element, link } from "../dom.ts";
-
-export default function AtomPage({ feed }: { feed: Feed }): Element {
- return elem(
- "feed",
- { xmlns: "http://www.w3.org/2005/Atom" },
- elem("id", {}, feed.id),
- elem("title", {}, feed.title),
- link({ rel: "alternate", href: feed.linkToAlternate }),
- link({ rel: "self", href: feed.linkToSelf }),
- elem("author", {}, elem("name", {}, feed.author)),
- elem("updated", {}, feed.updated),
- ...feed.entries.map((entry) =>
- elem(
- "entry",
- {},
- elem("id", {}, entry.id),
- link({ rel: "alternate", href: entry.linkToAlternate }),
- elem("title", {}, entry.title),
- elem("summary", {}, entry.summary),
- elem("published", {}, entry.published),
- elem("updated", {}, entry.updated),
- )
- ),
- );
-}
diff --git a/services/nuldoc/nuldoc-src/pages/HomePage.ts b/services/nuldoc/nuldoc-src/pages/HomePage.ts
deleted file mode 100644
index 6e984586..00000000
--- a/services/nuldoc/nuldoc-src/pages/HomePage.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import GlobalHeader from "../components/DefaultGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import { Config } from "../config.ts";
-import { a, article, elem, Element, h2, header } from "../dom.ts";
-
-export default async function HomePage(config: Config): Promise<Element> {
- return await PageLayout({
- metaCopyrightYear: config.site.copyrightYear,
- metaDescription: "nsfisis のサイト",
- metaTitle: config.sites.default.siteName,
- metaAtomFeedHref: `https://${config.sites.default.fqdn}/atom.xml`,
- site: "default",
- config,
- children: elem(
- "body",
- { class: "single" },
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- article(
- { class: "post-single" },
- article(
- { class: "post-entry" },
- a(
- { href: `https://${config.sites.about.fqdn}/` },
- header(
- { class: "entry-header" },
- h2({}, "About"),
- ),
- ),
- ),
- article(
- { class: "post-entry" },
- a(
- { href: `https://${config.sites.blog.fqdn}/posts/` },
- header({ class: "entry-header" }, h2({}, "Blog")),
- ),
- ),
- article(
- { class: "post-entry" },
- a(
- { href: `https://${config.sites.slides.fqdn}/slides/` },
- header(
- { class: "entry-header" },
- h2({}, "Slides"),
- ),
- ),
- ),
- article(
- { class: "post-entry" },
- a(
- { href: `https://repos.${config.sites.default.fqdn}/` },
- header(
- { class: "entry-header" },
- h2({}, "Repositories"),
- ),
- ),
- ),
- ),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/pages/NotFoundPage.ts b/services/nuldoc/nuldoc-src/pages/NotFoundPage.ts
deleted file mode 100644
index 62080665..00000000
--- a/services/nuldoc/nuldoc-src/pages/NotFoundPage.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import AboutGlobalHeader from "../components/AboutGlobalHeader.ts";
-import BlogGlobalHeader from "../components/BlogGlobalHeader.ts";
-import SlidesGlobalHeader from "../components/SlidesGlobalHeader.ts";
-import DefaultGlobalHeader from "../components/DefaultGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import { Config } from "../config.ts";
-import { article, div, elem, Element } from "../dom.ts";
-
-export default async function NotFoundPage(
- site: "default" | "about" | "blog" | "slides",
- config: Config,
-): Promise<Element> {
- const GlobalHeader = site === "about"
- ? AboutGlobalHeader
- : site === "blog"
- ? BlogGlobalHeader
- : site === "slides"
- ? SlidesGlobalHeader
- : DefaultGlobalHeader;
-
- return await PageLayout({
- metaCopyrightYear: config.site.copyrightYear,
- metaDescription: "リクエストされたページが見つかりません",
- metaTitle: `Page Not Found|${config.sites[site].siteName}`,
- site,
- config,
- children: elem(
- "body",
- { class: "single" },
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- article({}, div({ class: "not-found" }, "404")),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/pages/PostListPage.ts b/services/nuldoc/nuldoc-src/pages/PostListPage.ts
deleted file mode 100644
index ef7bfc57..00000000
--- a/services/nuldoc/nuldoc-src/pages/PostListPage.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import GlobalHeader from "../components/BlogGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import Pagination from "../components/Pagination.ts";
-import PostPageEntry from "../components/PostPageEntry.ts";
-import { Config } from "../config.ts";
-import { PostPage } from "../generators/post.ts";
-import { elem, Element, h1, header } from "../dom.ts";
-
-export default async function PostListPage(
- posts: PostPage[],
- config: Config,
- currentPage: number,
- totalPages: number,
-): Promise<Element> {
- const pageTitle = "投稿一覧";
-
- const pageInfoSuffix = ` (${currentPage}ページ目)`;
- const metaTitle =
- `${pageTitle}${pageInfoSuffix}|${config.sites.blog.siteName}`;
- const metaDescription = `投稿した記事の一覧${pageInfoSuffix}`;
-
- return await PageLayout({
- metaCopyrightYear: config.site.copyrightYear,
- metaDescription,
- metaTitle,
- metaAtomFeedHref: `https://${config.sites.blog.fqdn}/posts/atom.xml`,
- site: "blog",
- config,
- children: elem(
- "body",
- { class: "list" },
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- header(
- { class: "page-header" },
- h1({}, pageTitle + pageInfoSuffix),
- ),
- Pagination({ currentPage, totalPages, basePath: "/posts/" }),
- ...posts.map((post) => PostPageEntry({ post, config })),
- Pagination({ currentPage, totalPages, basePath: "/posts/" }),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/pages/PostPage.ts b/services/nuldoc/nuldoc-src/pages/PostPage.ts
deleted file mode 100644
index 20eec996..00000000
--- a/services/nuldoc/nuldoc-src/pages/PostPage.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import GlobalHeader from "../components/BlogGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import TableOfContents from "../components/TableOfContents.ts";
-import { Config, getTagLabel } from "../config.ts";
-import {
- a,
- article,
- div,
- elem,
- Element,
- h1,
- h2,
- header,
- li,
- ol,
- section,
- ul,
-} from "../dom.ts";
-import { Document } from "../markdown/document.ts";
-import { dateToString } from "../revision.ts";
-import { getPostPublishedDate } from "../generators/post.ts";
-
-export default async function PostPage(
- doc: Document,
- config: Config,
-): Promise<Element> {
- return await PageLayout({
- metaCopyrightYear: getPostPublishedDate(doc).year,
- metaDescription: doc.description,
- metaKeywords: doc.tags.map((slug) => getTagLabel(config, slug)),
- metaTitle: `${doc.title}|${config.sites.blog.siteName}`,
- requiresSyntaxHighlight: true,
- site: "blog",
- config,
- children: elem(
- "body",
- { class: "single" },
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- article(
- { class: "post-single" },
- header(
- { class: "post-header" },
- h1({ class: "post-title" }, doc.title),
- doc.tags.length !== 0
- ? ul(
- { class: "post-tags" },
- ...doc.tags.map((slug) =>
- li(
- { class: "tag" },
- a(
- { class: "tag-inner", href: `/tags/${slug}/` },
- getTagLabel(config, slug),
- ),
- )
- ),
- )
- : null,
- ),
- doc.toc && doc.toc.entries.length > 0
- ? TableOfContents({ toc: doc.toc })
- : null,
- div(
- { class: "post-content" },
- section(
- { id: "changelog" },
- h2({}, a({ href: "#changelog" }, "更新履歴")),
- ol(
- {},
- ...doc.revisions.map((rev) =>
- li(
- { class: "revision" },
- elem(
- "time",
- { datetime: dateToString(rev.date) },
- dateToString(rev.date),
- ),
- `: ${rev.remark}`,
- )
- ),
- ),
- ),
- // TODO: refactor
- ...(doc.root.children[0] as Element).children,
- ),
- ),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/pages/SlideListPage.ts b/services/nuldoc/nuldoc-src/pages/SlideListPage.ts
deleted file mode 100644
index f1b1a2a1..00000000
--- a/services/nuldoc/nuldoc-src/pages/SlideListPage.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import GlobalHeader from "../components/SlidesGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import SlidePageEntry from "../components/SlidePageEntry.ts";
-import { Config } from "../config.ts";
-import { dateToString } from "../revision.ts";
-import { getPostPublishedDate } from "../generators/post.ts";
-import { SlidePage } from "../generators/slide.ts";
-import { elem, Element, h1, header } from "../dom.ts";
-
-export default async function SlideListPage(
- slides: SlidePage[],
- config: Config,
-): Promise<Element> {
- const pageTitle = "スライド一覧";
-
- return await PageLayout({
- metaCopyrightYear: config.site.copyrightYear,
- metaDescription: "登壇したイベントで使用したスライドの一覧",
- metaTitle: `${pageTitle}|${config.sites.slides.siteName}`,
- metaAtomFeedHref: `https://${config.sites.slides.fqdn}/slides/atom.xml`,
- site: "slides",
- config,
- children: elem(
- "body",
- { class: "list" },
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- header({ class: "page-header" }, h1({}, pageTitle)),
- ...Array.from(slides)
- .sort((s1, s2) => {
- const ta = dateToString(getPostPublishedDate(s1));
- const tb = dateToString(getPostPublishedDate(s2));
- if (ta > tb) return -1;
- if (ta < tb) return 1;
- return 0;
- })
- .map((slide) => SlidePageEntry({ slide, config })),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/pages/SlidePage.ts b/services/nuldoc/nuldoc-src/pages/SlidePage.ts
deleted file mode 100644
index 149ecf8e..00000000
--- a/services/nuldoc/nuldoc-src/pages/SlidePage.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import GlobalHeader from "../components/SlidesGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import StaticStylesheet from "../components/StaticStylesheet.ts";
-import StaticScript from "../components/StaticScript.ts";
-import { Config, getTagLabel } from "../config.ts";
-import { dateToString } from "../revision.ts";
-import { Slide } from "../slide/slide.ts";
-import { getPostPublishedDate } from "../generators/post.ts";
-import {
- a,
- article,
- button,
- div,
- elem,
- Element,
- h1,
- h2,
- header,
- li,
- ol,
- section,
- ul,
-} from "../dom.ts";
-
-export default async function SlidePage(
- slide: Slide,
- config: Config,
-): Promise<Element> {
- return await PageLayout({
- metaCopyrightYear: getPostPublishedDate(slide).year,
- metaDescription: `「${slide.title}」(${slide.event} で登壇)`,
- metaKeywords: slide.tags.map((slug) => getTagLabel(config, slug)),
- metaTitle:
- `${slide.title} (${slide.event})|${config.sites.slides.siteName}`,
- requiresSyntaxHighlight: true,
- site: "slides",
- config,
- children: elem(
- "body",
- { class: "single" },
- await StaticStylesheet({
- site: "slides",
- fileName: "/slides.css",
- config,
- }),
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- article(
- { class: "post-single" },
- header(
- { class: "post-header" },
- h1({ class: "post-title" }, slide.title),
- slide.tags.length !== 0
- ? ul(
- { class: "post-tags" },
- ...slide.tags.map((slug) =>
- li(
- { class: "tag" },
- a(
- { class: "tag-inner", href: `/tags/${slug}/` },
- getTagLabel(config, slug),
- ),
- )
- ),
- )
- : null,
- ),
- div(
- { class: "post-content" },
- section(
- { id: "changelog" },
- h2({}, a({ href: "#changelog" }, "更新履歴")),
- ol(
- {},
- ...slide.revisions.map((rev) =>
- li(
- { class: "revision" },
- elem(
- "time",
- { datetime: dateToString(rev.date) },
- dateToString(rev.date),
- ),
- `: ${rev.remark}`,
- )
- ),
- ),
- ),
- elem("canvas", { id: "slide", "data-slide-link": slide.slideLink }),
- div(
- { class: "controllers" },
- div(
- { class: "controllers-buttons" },
- button(
- { id: "prev", type: "button" },
- elem(
- "svg",
- {
- width: "20",
- height: "20",
- viewBox: "0 0 24 24",
- fill: "none",
- stroke: "currentColor",
- "stroke-width": "2",
- },
- elem("path", { d: "M15 18l-6-6 6-6" }),
- ),
- ),
- button(
- { id: "next", type: "button" },
- elem(
- "svg",
- {
- width: "20",
- height: "20",
- viewBox: "0 0 24 24",
- fill: "none",
- stroke: "currentColor",
- "stroke-width": "2",
- },
- elem("path", { d: "M9 18l6-6-6-6" }),
- ),
- ),
- ),
- ),
- await StaticScript({
- site: "slides",
- fileName: "/slide.js",
- type: "module",
- config,
- }),
- ),
- ),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/pages/TagListPage.ts b/services/nuldoc/nuldoc-src/pages/TagListPage.ts
deleted file mode 100644
index cf1ec517..00000000
--- a/services/nuldoc/nuldoc-src/pages/TagListPage.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import BlogGlobalHeader from "../components/BlogGlobalHeader.ts";
-import SlidesGlobalHeader from "../components/SlidesGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import { Config } from "../config.ts";
-import { TagPage } from "../generators/tag.ts";
-import { a, article, elem, Element, footer, h1, h2, header } from "../dom.ts";
-
-export default async function TagListPage(
- tags: TagPage[],
- site: "blog" | "slides",
- config: Config,
-): Promise<Element> {
- const pageTitle = "タグ一覧";
-
- const GlobalHeader = site === "blog" ? BlogGlobalHeader : SlidesGlobalHeader;
-
- return await PageLayout({
- metaCopyrightYear: config.site.copyrightYear,
- metaDescription: "タグの一覧",
- metaTitle: `${pageTitle}|${config.sites[site].siteName}`,
- site,
- config,
- children: elem(
- "body",
- { class: "list" },
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- header({ class: "page-header" }, h1({}, pageTitle)),
- ...Array.from(tags)
- .sort((t1, t2) => {
- const ta = t1.tagSlug;
- const tb = t2.tagSlug;
- if (ta < tb) return -1;
- if (ta > tb) return 1;
- return 0;
- })
- .map((tag) => {
- const posts = tag.numOfPosts === 0
- ? ""
- : `${tag.numOfPosts}件の記事`;
- const slides = tag.numOfSlides === 0
- ? ""
- : `${tag.numOfSlides}件のスライド`;
- const footerText = `${posts}${
- posts && slides ? "、" : ""
- }${slides}`;
-
- return article(
- { class: "post-entry" },
- a(
- { href: tag.href },
- header(
- { class: "entry-header" },
- h2({}, tag.tagLabel),
- ),
- footer({ class: "entry-footer" }, footerText),
- ),
- );
- }),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/pages/TagPage.ts b/services/nuldoc/nuldoc-src/pages/TagPage.ts
deleted file mode 100644
index 1826219c..00000000
--- a/services/nuldoc/nuldoc-src/pages/TagPage.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import GlobalFooter from "../components/GlobalFooter.ts";
-import BlogGlobalHeader from "../components/BlogGlobalHeader.ts";
-import SlidesGlobalHeader from "../components/SlidesGlobalHeader.ts";
-import PageLayout from "../components/PageLayout.ts";
-import PostPageEntry from "../components/PostPageEntry.ts";
-import SlidePageEntry from "../components/SlidePageEntry.ts";
-import { Config, getTagLabel } from "../config.ts";
-import { getPostPublishedDate } from "../generators/post.ts";
-import { TaggedPage } from "../generators/tagged_page.ts";
-import { elem, Element, h1, header } from "../dom.ts";
-
-export default async function TagPage(
- tagSlug: string,
- pages: TaggedPage[],
- site: "blog" | "slides",
- config: Config,
-): Promise<Element> {
- const tagLabel = getTagLabel(config, tagSlug);
- const pageTitle = `タグ「${tagLabel}」一覧`;
-
- const GlobalHeader = site === "blog" ? BlogGlobalHeader : SlidesGlobalHeader;
-
- return await PageLayout({
- metaCopyrightYear: getPostPublishedDate(pages[pages.length - 1]).year,
- metaDescription: `タグ「${tagLabel}」のついた記事またはスライドの一覧`,
- metaKeywords: [tagLabel],
- metaTitle: `${pageTitle}|${config.sites[site].siteName}`,
- metaAtomFeedHref: `https://${
- config.sites[site].fqdn
- }/tags/${tagSlug}/atom.xml`,
- site,
- config,
- children: elem(
- "body",
- { class: "list" },
- GlobalHeader({ config }),
- elem(
- "main",
- { class: "main" },
- header({ class: "page-header" }, h1({}, pageTitle)),
- ...pages.map((page) =>
- "event" in page
- ? SlidePageEntry({ slide: page, config })
- : PostPageEntry({ post: page, config })
- ),
- ),
- GlobalFooter({ config }),
- ),
- });
-}
diff --git a/services/nuldoc/nuldoc-src/render.ts b/services/nuldoc/nuldoc-src/render.ts
deleted file mode 100644
index fbad25ab..00000000
--- a/services/nuldoc/nuldoc-src/render.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Node } from "./dom.ts";
-import { renderHtml } from "./renderers/html.ts";
-import { renderXml } from "./renderers/xml.ts";
-
-export type RendererType = "html" | "xml";
-
-export function render(root: Node, renderer: RendererType): string {
- if (renderer === "html") {
- return renderHtml(root);
- } else {
- return renderXml(root);
- }
-}
diff --git a/services/nuldoc/nuldoc-src/renderers/html.ts b/services/nuldoc/nuldoc-src/renderers/html.ts
deleted file mode 100644
index 0fa02d51..00000000
--- a/services/nuldoc/nuldoc-src/renderers/html.ts
+++ /dev/null
@@ -1,311 +0,0 @@
-import { Element, forEachChild, Node, Text } from "../dom.ts";
-import { NuldocError } from "../errors.ts";
-
-export function renderHtml(root: Node): string {
- return `<!DOCTYPE html>\n` + nodeToHtmlText(root, {
- indentLevel: 0,
- isInPre: false,
- });
-}
-
-type Context = {
- indentLevel: number;
- isInPre: boolean;
-};
-
-type Dtd = { type: "block" | "inline"; self_closing?: boolean };
-
-function getDtd(name: string): Dtd {
- switch (name) {
- case "a":
- return { type: "inline" };
- case "article":
- return { type: "block" };
- case "blockquote":
- return { type: "block" };
- case "body":
- return { type: "block" };
- case "br":
- return { type: "block", self_closing: true };
- case "button":
- return { type: "block" };
- case "canvas":
- return { type: "block" };
- case "caption":
- return { type: "block" };
- case "code":
- return { type: "inline" };
- case "del":
- return { type: "block" };
- case "div":
- return { type: "block" };
- case "em":
- return { type: "inline" };
- case "footer":
- return { type: "block" };
- case "h1":
- return { type: "inline" };
- case "h2":
- return { type: "inline" };
- case "h3":
- return { type: "inline" };
- case "h4":
- return { type: "inline" };
- case "h5":
- return { type: "inline" };
- case "h6":
- return { type: "inline" };
- case "head":
- return { type: "block" };
- case "header":
- return { type: "block" };
- case "hr":
- return { type: "block", self_closing: true };
- case "html":
- return { type: "block" };
- case "i":
- return { type: "inline" };
- case "li":
- return { type: "block" };
- case "link":
- return { type: "block", self_closing: true };
- case "img":
- return { type: "inline", self_closing: true };
- case "ins":
- return { type: "block" };
- case "main":
- return { type: "block" };
- case "mark":
- return { type: "inline" };
- case "meta":
- return { type: "block", self_closing: true };
- case "nav":
- return { type: "block" };
- case "noscript":
- return { type: "block" };
- case "ol":
- return { type: "block" };
- case "p":
- return { type: "block" };
- case "pre":
- return { type: "block" };
- case "script":
- return { type: "block" };
- case "section":
- return { type: "block" };
- case "span":
- return { type: "inline" };
- case "strong":
- return { type: "inline" };
- case "sub":
- return { type: "inline" };
- case "sup":
- return { type: "inline" };
- case "table":
- return { type: "block" };
- case "tbody":
- return { type: "block" };
- case "td": // TODO
- return { type: "block" };
- case "tfoot":
- return { type: "block" };
- case "th":
- return { type: "block" };
- case "thead":
- return { type: "block" };
- case "time":
- return { type: "inline" };
- case "title": // TODO
- return { type: "inline" };
- case "tr":
- return { type: "block" };
- case "ul":
- return { type: "block" };
- case "svg": // TODO
- case "path": // TODO
- return { type: "block" };
- default:
- throw new NuldocError(`[html.write] Unknown element name: ${name}`);
- }
-}
-
-function isInlineNode(n: Node): boolean {
- if (n.kind === "text" || n.kind === "raw") {
- return true;
- }
- if (n.name !== "a") {
- return getDtd(n.name).type === "inline";
- }
-
- // a tag: check if all children are inline elements.
- let allInline = true;
- forEachChild(n, (c) => allInline &&= isInlineNode(c));
- return allInline;
-}
-
-function isBlockNode(n: Node): boolean {
- return !isInlineNode(n);
-}
-
-function nodeToHtmlText(n: Node, ctx: Context): string {
- if (n.kind === "text") {
- return textNodeToHtmlText(n, ctx);
- } else if (n.kind === "raw") {
- return n.html;
- } else {
- return elementNodeToHtmlText(n, ctx);
- }
-}
-
-function textNodeToHtmlText(t: Text, ctx: Context): string {
- const s = encodeSpecialCharacters(t.content);
- if (ctx.isInPre) return s;
-
- return s.replaceAll(/\n */g, (_match, offset, subject) => {
- const last_char = subject[offset - 1];
- if (last_char === "。" || last_char === "、") {
- // 日本語で改行するときはスペースを入れない
- return "";
- } else {
- return " ";
- }
- });
-}
-
-function encodeSpecialCharacters(s: string): string {
- return s.replaceAll(/&(?!\w+;)/g, "&amp;")
- .replaceAll(/</g, "&lt;")
- .replaceAll(/>/g, "&gt;")
- .replaceAll(/'/g, "&apos;")
- .replaceAll(/"/g, "&quot;");
-}
-
-function elementNodeToHtmlText(e: Element, ctx: Context): string {
- const dtd = getDtd(e.name);
-
- let s = "";
-
- if (isBlockNode(e)) {
- s += indent(ctx);
- }
- s += `<${e.name}`;
- const attributes = getElementAttributes(e);
- if (attributes.length > 0) {
- s += " ";
- for (let i = 0; i < attributes.length; i++) {
- const [name, value] = attributes[i];
- if (name === "defer" && value === "true") {
- // TODO
- s += "defer";
- } else {
- s += `${name === "className" ? "class" : name}="${
- encodeSpecialCharacters(value)
- }"`;
- }
- if (i !== attributes.length - 1) {
- s += " ";
- }
- }
- }
- s += ">";
- if (isBlockNode(e) && e.name !== "pre") {
- s += "\n";
- }
-
- ctx.indentLevel += 1;
-
- let prevChild: Node | null = null;
- if (e.name === "pre") {
- ctx.isInPre = true;
- }
- forEachChild(e, (c) => {
- if (isBlockNode(e) && !ctx.isInPre) {
- if (isInlineNode(c)) {
- if (needsIndent(prevChild)) {
- s += indent(ctx);
- }
- } else {
- if (needsLineBreak(prevChild)) {
- s += "\n";
- }
- }
- }
- s += nodeToHtmlText(c, ctx);
- prevChild = c;
- });
- if (e.name === "pre") {
- ctx.isInPre = false;
- }
-
- ctx.indentLevel -= 1;
- if (!dtd.self_closing) {
- if (e.name !== "pre") {
- if (isBlockNode(e)) {
- if (needsLineBreak(prevChild)) {
- s += "\n";
- }
- s += indent(ctx);
- }
- }
- s += `</${e.name}>`;
- if (isBlockNode(e)) {
- s += "\n";
- }
- }
- return s;
-}
-
-function indent(ctx: Context): string {
- return " ".repeat(ctx.indentLevel);
-}
-
-function getElementAttributes(e: Element): [string, string][] {
- return [...Object.entries(e.attributes)]
- .filter((a) => !a[0].startsWith("__"))
- .filter((a) => a[1] !== undefined)
- .sort(
- (a, b) => {
- // Special rules:
- if (e.name === "meta") {
- if (a[0] === "content" && b[0] === "name") {
- return 1;
- }
- if (a[0] === "name" && b[0] === "content") {
- return -1;
- }
- if (a[0] === "content" && b[0] === "property") {
- return 1;
- }
- if (a[0] === "property" && b[0] === "content") {
- return -1;
- }
- }
- if (e.name === "link") {
- if (a[0] === "href" && b[0] === "rel") {
- return 1;
- }
- if (a[0] === "rel" && b[0] === "href") {
- return -1;
- }
- if (a[0] === "href" && b[0] === "type") {
- return 1;
- }
- if (a[0] === "type" && b[0] === "href") {
- return -1;
- }
- }
- // General rules:
- if (a[0] > b[0]) return 1;
- else if (a[0] < b[0]) return -1;
- else return 0;
- },
- );
-}
-
-function needsIndent(prevChild: Node | null): boolean {
- return !prevChild || isBlockNode(prevChild);
-}
-
-function needsLineBreak(prevChild: Node | null): boolean {
- return !needsIndent(prevChild);
-}
diff --git a/services/nuldoc/nuldoc-src/renderers/xml.ts b/services/nuldoc/nuldoc-src/renderers/xml.ts
deleted file mode 100644
index 523567ab..00000000
--- a/services/nuldoc/nuldoc-src/renderers/xml.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { Element, forEachChild, Node, Text } from "../dom.ts";
-
-export function renderXml(root: Node): string {
- return `<?xml version="1.0" encoding="utf-8"?>\n` + nodeToXmlText(root, {
- indentLevel: 0,
- });
-}
-
-type Context = {
- indentLevel: number;
-};
-
-type Dtd = { type: "block" | "inline" };
-
-function getDtd(name: string): Dtd {
- switch (name) {
- case "feed":
- case "entry":
- case "author":
- return { type: "block" };
- default:
- return { type: "inline" };
- }
-}
-
-function isInlineNode(n: Node): boolean {
- if (n.kind === "text" || n.kind === "raw") {
- return true;
- }
- return getDtd(n.name).type === "inline";
-}
-
-function isBlockNode(n: Node): boolean {
- return !isInlineNode(n);
-}
-
-function nodeToXmlText(n: Node, ctx: Context): string {
- if (n.kind === "text") {
- return textNodeToXmlText(n);
- } else if (n.kind === "raw") {
- return n.html;
- } else {
- return elementNodeToXmlText(n, ctx);
- }
-}
-
-function textNodeToXmlText(t: Text): string {
- const s = encodeSpecialCharacters(t.content);
-
- // TODO: 日本語で改行するときはスペースを入れない
- return s.replaceAll(/\n */g, " ");
-}
-
-function encodeSpecialCharacters(s: string): string {
- return s.replaceAll(/&(?!\w+;)/g, "&amp;")
- .replaceAll(/</g, "&lt;")
- .replaceAll(/>/g, "&gt;")
- .replaceAll(/'/g, "&apos;")
- .replaceAll(/"/g, "&quot;");
-}
-
-function elementNodeToXmlText(e: Element, ctx: Context): string {
- let s = "";
-
- s += indent(ctx);
- s += `<${e.name}`;
- const attributes = getElementAttributes(e);
- if (attributes.length > 0) {
- s += " ";
- for (let i = 0; i < attributes.length; i++) {
- const [name, value] = attributes[i];
- s += `${name}="${encodeSpecialCharacters(value)}"`;
- if (i !== attributes.length - 1) {
- s += " ";
- }
- }
- }
- s += ">";
- if (isBlockNode(e)) {
- s += "\n";
- }
- ctx.indentLevel += 1;
-
- forEachChild(e, (c) => {
- s += nodeToXmlText(c, ctx);
- });
-
- ctx.indentLevel -= 1;
- if (isBlockNode(e)) {
- s += indent(ctx);
- }
- s += `</${e.name}>`;
- s += "\n";
-
- return s;
-}
-
-function indent(ctx: Context): string {
- return " ".repeat(ctx.indentLevel);
-}
-
-function getElementAttributes(e: Element): [string, string][] {
- return [...Object.entries(e.attributes)]
- .filter((a) => !a[0].startsWith("__"))
- .sort(
- (a, b) => {
- // Special rules:
- if (e.name === "link") {
- if (a[0] === "href" && b[0] === "rel") {
- return 1;
- }
- if (a[0] === "rel" && b[0] === "href") {
- return -1;
- }
- if (a[0] === "href" && b[0] === "type") {
- return 1;
- }
- if (a[0] === "type" && b[0] === "href") {
- return -1;
- }
- }
- // General rules:
- if (a[0] > b[0]) return 1;
- else if (a[0] < b[0]) return -1;
- else return 0;
- },
- );
-}
diff --git a/services/nuldoc/nuldoc-src/revision.ts b/services/nuldoc/nuldoc-src/revision.ts
deleted file mode 100644
index a22b6bc4..00000000
--- a/services/nuldoc/nuldoc-src/revision.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-export type Date = {
- year: number;
- month: number;
- day: number;
-};
-
-export function stringToDate(s: string): Date {
- const match = s.match(/(\d{4})-(\d{2})-(\d{2})/);
- if (match === null) {
- throw new Error();
- }
- const [_, y, m, d] = match;
- return { year: parseInt(y), month: parseInt(m), day: parseInt(d) };
-}
-
-export function dateToString(date: Date): string {
- const y = `${date.year}`.padStart(4, "0");
- const m = `${date.month}`.padStart(2, "0");
- const d = `${date.day}`.padStart(2, "0");
- return `${y}-${m}-${d}`;
-}
-
-export function dateToRfc3339String(date: Date): string {
- // 2021-01-01T12:00:00+00:00
- // TODO: currently, time part is fixed to 00:00:00.
- const y = `${date.year}`.padStart(4, "0");
- const m = `${date.month}`.padStart(2, "0");
- const d = `${date.day}`.padStart(2, "0");
- return `${y}-${m}-${d}T00:00:00+09:00`;
-}
-
-export type Revision = {
- number: number;
- date: Date;
- remark: string;
- isInternal: boolean;
-};
diff --git a/services/nuldoc/nuldoc-src/slide/parse.ts b/services/nuldoc/nuldoc-src/slide/parse.ts
deleted file mode 100644
index c5a89675..00000000
--- a/services/nuldoc/nuldoc-src/slide/parse.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { parse as parseToml } from "@std/toml";
-import {
- createNewSlideFromMetadata,
- Slide,
- SlideMetadataSchema,
-} from "./slide.ts";
-
-export async function parseSlideFile(filePath: string): Promise<Slide> {
- try {
- const root = SlideMetadataSchema.parse(
- parseToml(await Deno.readTextFile(filePath)),
- );
- return createNewSlideFromMetadata(root, filePath);
- } catch (e) {
- if (e instanceof Error) {
- e.message = `${e.message} in ${filePath}`;
- }
- throw e;
- }
-}
diff --git a/services/nuldoc/nuldoc-src/slide/slide.ts b/services/nuldoc/nuldoc-src/slide/slide.ts
deleted file mode 100644
index 8fe99eab..00000000
--- a/services/nuldoc/nuldoc-src/slide/slide.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { SlideError } from "../errors.ts";
-import { Revision, stringToDate } from "../revision.ts";
-import { z } from "zod/mod.ts";
-
-export const SlideMetadataSchema = z.object({
- slide: z.object({
- uuid: z.string(),
- title: z.string(),
- event: z.string(),
- talkType: z.string(),
- link: z.string(),
- tags: z.array(z.string()),
- revisions: z.array(z.object({
- date: z.string(),
- remark: z.string(),
- isInternal: z.boolean().optional(),
- })),
- }),
-});
-
-export type SlideMetadata = z.infer<typeof SlideMetadataSchema>;
-
-export type Slide = {
- sourceFilePath: string;
- uuid: string;
- title: string;
- event: string;
- talkType: string;
- slideLink: string;
- tags: string[];
- revisions: Revision[];
-};
-
-export function createNewSlideFromMetadata(
- { slide }: SlideMetadata,
- sourceFilePath: string,
-): Slide {
- const revisions = slide.revisions.map(
- (rev, i) => {
- const date = rev.date;
- const remark = rev.remark;
- const isInternal = rev.isInternal ?? false;
- return {
- number: i + 1,
- date: stringToDate(date),
- remark,
- isInternal,
- };
- },
- );
- if (revisions.length === 0) {
- throw new SlideError(
- `[slide.new] 'slide.revisions' field is empty`,
- );
- }
-
- return {
- sourceFilePath: sourceFilePath,
- uuid: slide.uuid,
- title: slide.title,
- event: slide.event,
- talkType: slide.talkType,
- slideLink: slide.link,
- tags: slide.tags,
- revisions: revisions,
- };
-}