diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-01 00:49:15 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-01 00:49:19 +0900 |
| commit | 6dedddc545e2f1930bdc2256784eb1551bd4231d (patch) | |
| tree | 75fcb5a6043dc0f2c31b098bf3cfd17a2b938599 /services/nuldoc/nuldoc-src | |
| parent | d08e3edb65b215152aa26e3518fb2f2cd7071c4b (diff) | |
| download | nsfisis.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')
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" }, - `© ${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: `© ${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, "&") - .replaceAll(/</g, "<") - .replaceAll(/>/g, ">") - .replaceAll(/'/g, "'") - .replaceAll(/"/g, """); -} - -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, "&") - .replaceAll(/</g, "<") - .replaceAll(/>/g, ">") - .replaceAll(/'/g, "'") - .replaceAll(/"/g, """); -} - -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, - }; -} |
