diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-06-27 23:39:31 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-06-27 23:39:31 +0900 |
| commit | 674fe965550444db87edc7937ff6932e1a918d9d (patch) | |
| tree | e8a80dd958d3e082485286bf5785a7992b6e6b0e /vhosts/blog/nuldoc-src | |
| parent | fe4d1d625b53796c5f20399790e5ff8c7a7e1608 (diff) | |
| download | nsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.tar.gz nsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.tar.zst nsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.zip | |
feat(meta): rename vhosts/ directory to services/
Diffstat (limited to 'vhosts/blog/nuldoc-src')
51 files changed, 0 insertions, 3917 deletions
diff --git a/vhosts/blog/nuldoc-src/commands/build.ts b/vhosts/blog/nuldoc-src/commands/build.ts deleted file mode 100644 index 3f765441..00000000 --- a/vhosts/blog/nuldoc-src/commands/build.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { dirname, join, joinGlobs } from "@std/path"; -import { ensureDir, expandGlob } from "@std/fs"; -import { generateFeedPageFromEntries } from "../generators/atom.ts"; -import { Config, getTagLabel } from "../config.ts"; -import { parseDjotFile } from "../djot/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 tags = await buildTagPages(posts, slides, config); - await buildTagListPage(tags, config); - await buildHomePage(config); - await buildAboutPage(slides, config); - await buildNotFoundPage(config); - await buildFeedOfAllContents(posts, slides, config); - await copyStaticFiles(config); - await copyAssetFiles(slides, 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, "**", "*.dj"]); - 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 parseDjotFile(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 = await generateFeedPageFromEntries( - "/posts/", - "posts", - `投稿一覧|${config.blog.siteName}`, - posts, - 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 = await generateFeedPageFromEntries( - slideListPage.href, - "slides", - `スライド一覧|${config.blog.siteName}`, - 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(config: Config) { - const notFoundPage = await generateNotFoundPage(config); - await writePage(notFoundPage, config); -} - -async function buildFeedOfAllContents( - posts: PostPage[], - slides: SlidePage[], - config: Config, -) { - const feed = await generateFeedPageFromEntries( - "/", - "all", - config.blog.siteName, - [...posts, ...slides], - config, - ); - await writePage(feed, config); -} - -async function buildTagPages( - posts: PostPage[], - slides: SlidePage[], - config: Config, -): Promise<TagPage[]> { - const tagsAndPages = collectTags([...posts, ...slides]); - const tags = []; - for (const [tag, pages] of tagsAndPages) { - const tagPage = await generateTagPage(tag, pages, config); - await writePage(tagPage, config); - const tagFeedPage = await generateFeedPageFromEntries( - tagPage.href, - `tag-${tag}`, - `タグ「${getTagLabel(config, tag)}」一覧|${config.blog.siteName}`, - pages, - config, - ); - await writePage(tagFeedPage, config); - tags.push(tagPage); - } - return tags; -} - -async function buildTagListPage(tags: TagPage[], config: Config) { - const tagListPage = await generateTagListPage(tags, 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 globPattern = joinGlobs([Deno.cwd(), config.locations.staticDir, "*"]); - for await (const entry of expandGlob(globPattern)) { - const src = entry.path; - const dst = src.replace( - config.locations.staticDir, - config.locations.destDir, - ); - await Deno.copyFile(src, dst); - } -} - -async function copyAssetFiles(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, slide.slideLink); - 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.destFilePath, - ); - await ensureDir(dirname(destFilePath)); - await Deno.writeTextFile(destFilePath, render(page.root, page.renderer)); -} diff --git a/vhosts/blog/nuldoc-src/commands/new.ts b/vhosts/blog/nuldoc-src/commands/new.ts deleted file mode 100644 index 651c59e6..00000000 --- a/vhosts/blog/nuldoc-src/commands/new.ts +++ /dev/null @@ -1,97 +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.dj" : "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 = "公開" ---- -{#TODO} -# TODO - -TODO -`; - } else { - return `[slide] -uuid = "${uuid}" -title = "TODO" -event = "TODO" -talkType = "TODO" -link = "TODO" -tags = [ - "TODO", -] - -[[slide.revisions]] -date = "${date}" -remark = "登壇" -`; - } -} diff --git a/vhosts/blog/nuldoc-src/commands/serve.ts b/vhosts/blog/nuldoc-src/commands/serve.ts deleted file mode 100644 index e944aaf0..00000000 --- a/vhosts/blog/nuldoc-src/commands/serve.ts +++ /dev/null @@ -1,45 +0,0 @@ -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", - ".png", - ".svg", - ]; - return EXTENSIONS.some((ext) => pathname.endsWith(ext)); -} - -export function runServeCommand(config: Config) { - const rootDir = join(Deno.cwd(), config.locations.destDir); - Deno.serve({ hostname: "127.0.0.1" }, async (req) => { - const pathname = new URL(req.url).pathname; - if (!isResourcePath(pathname)) { - 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/vhosts/blog/nuldoc-src/components/GlobalFooter.tsx b/vhosts/blog/nuldoc-src/components/GlobalFooter.tsx deleted file mode 100644 index 757beced..00000000 --- a/vhosts/blog/nuldoc-src/components/GlobalFooter.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Config } from "../config.ts"; - -export default function GlobalFooter({ config }: { config: Config }) { - return ( - <footer className="footer"> - {`© ${config.blog.siteCopyrightYear} ${config.blog.author}`} - </footer> - ); -} diff --git a/vhosts/blog/nuldoc-src/components/GlobalHeader.tsx b/vhosts/blog/nuldoc-src/components/GlobalHeader.tsx deleted file mode 100644 index c0fa7e8b..00000000 --- a/vhosts/blog/nuldoc-src/components/GlobalHeader.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Config } from "../config.ts"; - -export default function GlobalHeader({ config }: { config: Config }) { - return ( - <header className="header"> - <div className="site-logo"> - <a href="/">{config.blog.siteName}</a> - </div> - <nav className="nav"> - <ul> - <li> - <a href="/about/">About</a> - </li> - <li> - <a href="/posts/">Posts</a> - </li> - <li> - <a href="/slides/">Slides</a> - </li> - <li> - <a href="/tags/">Tags</a> - </li> - </ul> - </nav> - </header> - ); -} diff --git a/vhosts/blog/nuldoc-src/components/PageLayout.tsx b/vhosts/blog/nuldoc-src/components/PageLayout.tsx deleted file mode 100644 index 1cd0aebf..00000000 --- a/vhosts/blog/nuldoc-src/components/PageLayout.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Config } from "../config.ts"; -import { JSXNode } from "myjsx/jsx-runtime"; -import StaticStylesheet from "./StaticStylesheet.tsx"; - -type Props = { - metaCopyrightYear: number; - metaDescription: string; - metaKeywords?: string[]; - metaTitle: string; - metaAtomFeedHref?: string; - requiresSyntaxHighlight?: boolean; - config: Config; - children: JSXNode; -}; - -export default function PageLayout( - { - metaCopyrightYear, - metaDescription, - metaKeywords, - metaTitle, - metaAtomFeedHref, - requiresSyntaxHighlight: _, - config, - children, - }: Props, -) { - return ( - <html lang="ja-JP"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <meta name="author" content={config.blog.author} /> - <meta - name="copyright" - content={`© ${metaCopyrightYear} ${config.blog.author}`} - /> - <meta name="description" content={metaDescription} /> - {metaKeywords && metaKeywords.length !== 0 && - <meta name="keywords" content={metaKeywords.join(",")} />} - <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.blog.siteName} /> - <meta property="og:locale" content="ja_JP" /> - {metaAtomFeedHref && - ( - <link - rel="alternate" - href={metaAtomFeedHref} - type="application/atom+xml" - /> - )} - <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> - <title>{metaTitle}</title> - <StaticStylesheet fileName="/style.css" config={config} /> - </head> - {children} - </html> - ); -} diff --git a/vhosts/blog/nuldoc-src/components/Pagination.tsx b/vhosts/blog/nuldoc-src/components/Pagination.tsx deleted file mode 100644 index 5527c924..00000000 --- a/vhosts/blog/nuldoc-src/components/Pagination.tsx +++ /dev/null @@ -1,45 +0,0 @@ -type Props = { - currentPage: number; - totalPages: number; - basePath: string; -}; - -export default function Pagination( - { currentPage, totalPages, basePath }: Props, -) { - if (totalPages <= 1) { - return <div></div>; - } - - const prevPage = currentPage > 1 ? currentPage - 1 : null; - const nextPage = currentPage < totalPages ? currentPage + 1 : null; - - const prevHref = prevPage === 1 ? basePath : `${basePath}${prevPage}/`; - const nextHref = `${basePath}${nextPage}/`; - - return ( - <nav className="pagination"> - <div className="pagination-prev"> - {prevPage - ? ( - <a href={prevHref}> - 前のページ - </a> - ) - : null} - </div> - <div className="pagination-info"> - {String(currentPage)} / {String(totalPages)} - </div> - <div className="pagination-next"> - {nextPage - ? ( - <a href={nextHref}> - 次のページ - </a> - ) - : null} - </div> - </nav> - ); -} diff --git a/vhosts/blog/nuldoc-src/components/PostPageEntry.tsx b/vhosts/blog/nuldoc-src/components/PostPageEntry.tsx deleted file mode 100644 index 2708b009..00000000 --- a/vhosts/blog/nuldoc-src/components/PostPageEntry.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { - getPostPublishedDate, - getPostUpdatedDate, - postHasAnyUpdates, - PostPage, -} from "../generators/post.ts"; -import { dateToString } from "../revision.ts"; - -export default function PostPageEntry({ post }: { post: PostPage }) { - return ( - <article className="post-entry"> - <a href={post.href}> - <header className="entry-header"> - <h2>{post.title}</h2> - </header> - <section className="entry-content"> - <p>{post.description}</p> - </section> - <footer className="entry-footer"> - <time datetime={dateToString(getPostPublishedDate(post))}> - {dateToString(getPostPublishedDate(post))} - </time> - {" 投稿"} - { - // TODO(jsx): support Fragment and merge them. - postHasAnyUpdates(post) && "、" - } - {postHasAnyUpdates(post) && - ( - <time datetime={dateToString(getPostUpdatedDate(post))}> - {dateToString(getPostUpdatedDate(post))} - </time> - )} - {postHasAnyUpdates(post) && " 更新"} - </footer> - </a> - </article> - ); -} diff --git a/vhosts/blog/nuldoc-src/components/SlidePageEntry.tsx b/vhosts/blog/nuldoc-src/components/SlidePageEntry.tsx deleted file mode 100644 index d2cf9a17..00000000 --- a/vhosts/blog/nuldoc-src/components/SlidePageEntry.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { - getPostPublishedDate, - getPostUpdatedDate, - postHasAnyUpdates, -} from "../generators/post.ts"; -import { SlidePage } from "../generators/slide.ts"; -import { dateToString } from "../revision.ts"; - -export default function SlidePageEntry({ slide }: { slide: SlidePage }) { - return ( - <article className="post-entry"> - <a href={slide.href}> - <header className="entry-header"> - <h2>{slide.description}</h2> - </header> - <section className="entry-content"> - <p>{slide.title}</p> - </section> - <footer className="entry-footer"> - <time datetime={dateToString(getPostPublishedDate(slide))}> - {dateToString(getPostPublishedDate(slide))} - </time> - {" 登壇"} - { - // TODO(jsx): support Fragment and merge them. - postHasAnyUpdates(slide) && "、" - } - {postHasAnyUpdates(slide) && - ( - <time datetime={dateToString(getPostUpdatedDate(slide))}> - {dateToString(getPostUpdatedDate(slide))} - </time> - )} - {postHasAnyUpdates(slide) && " 更新"} - </footer> - </a> - </article> - ); -} diff --git a/vhosts/blog/nuldoc-src/components/StaticScript.tsx b/vhosts/blog/nuldoc-src/components/StaticScript.tsx deleted file mode 100644 index 0e3ab194..00000000 --- a/vhosts/blog/nuldoc-src/components/StaticScript.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { join } from "@std/path"; -import { Config } from "../config.ts"; -import { calculateFileHash } from "./utils.ts"; - -export default async function StaticScript( - { fileName, type, defer, config }: { - fileName: string; - type?: string; - defer?: "true"; - config: Config; - }, -) { - const filePath = join(Deno.cwd(), config.locations.staticDir, fileName); - const hash = await calculateFileHash(filePath); - return ( - <script src={`${fileName}?h=${hash}`} type={type} defer={defer}></script> - ); -} diff --git a/vhosts/blog/nuldoc-src/components/StaticStylesheet.tsx b/vhosts/blog/nuldoc-src/components/StaticStylesheet.tsx deleted file mode 100644 index 52b695e5..00000000 --- a/vhosts/blog/nuldoc-src/components/StaticStylesheet.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { join } from "@std/path"; -import { Config } from "../config.ts"; -import { calculateFileHash } from "./utils.ts"; - -export default async function StaticStylesheet( - { fileName, config }: { fileName: string; config: Config }, -) { - const filePath = join(Deno.cwd(), config.locations.staticDir, fileName); - const hash = await calculateFileHash(filePath); - return <link rel="stylesheet" href={`${fileName}?h=${hash}`} />; -} diff --git a/vhosts/blog/nuldoc-src/components/utils.ts b/vhosts/blog/nuldoc-src/components/utils.ts deleted file mode 100644 index 14059b5b..00000000 --- a/vhosts/blog/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/vhosts/blog/nuldoc-src/config.ts b/vhosts/blog/nuldoc-src/config.ts deleted file mode 100644 index adcb5632..00000000 --- a/vhosts/blog/nuldoc-src/config.ts +++ /dev/null @@ -1,41 +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(), - }), - rendering: z.object({ - html: z.object({ - indentWidth: z.number(), - }), - }), - blog: z.object({ - author: z.string(), - fqdn: z.string(), - siteName: z.string(), - siteCopyrightYear: z.number(), - postsPerPage: z.number().default(10), - 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.blog.tagLabels)) { - throw new Error(`Unknown tag: ${slug}`); - } - return c.blog.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/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts b/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts deleted file mode 100644 index 90b1289c..00000000 --- a/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts +++ /dev/null @@ -1,842 +0,0 @@ -import { - Block as DjotBlock, - BlockQuote as DjotBlockQuote, - BulletList as DjotBulletList, - CodeBlock as DjotCodeBlock, - Definition as DjotDefinition, - DefinitionList as DjotDefinitionList, - DefinitionListItem as DjotDefinitionListItem, - Delete as DjotDelete, - DisplayMath as DjotDisplayMath, - Div as DjotDiv, - Doc as DjotDoc, - DoubleQuoted as DjotDoubleQuoted, - Email as DjotEmail, - Emph as DjotEmph, - FootnoteReference as DjotFootnoteReference, - HardBreak as DjotHardBreak, - Heading as DjotHeading, - Image as DjotImage, - Inline as DjotInline, - InlineMath as DjotInlineMath, - Insert as DjotInsert, - Link as DjotLink, - ListItem as DjotListItem, - Mark as DjotMark, - NonBreakingSpace as DjotNonBreakingSpace, - OrderedList as DjotOrderedList, - Para as DjotPara, - RawBlock as DjotRawBlock, - RawInline as DjotRawInline, - Section as DjotSection, - SingleQuoted as DjotSingleQuoted, - SmartPunctuation as DjotSmartPunctuation, - SoftBreak as DjotSoftBreak, - Span as DjotSpan, - Str as DjotStr, - Strong as DjotStrong, - Subscript as DjotSubscript, - Superscript as DjotSuperscript, - Symb as DjotSymb, - Table as DjotTable, - TaskList as DjotTaskList, - TaskListItem as DjotTaskListItem, - Term as DjotTerm, - ThematicBreak as DjotThematicBreak, - Url as DjotUrl, - Verbatim as DjotVerbatim, -} from "@djot/djot"; -import { Element, Node } from "../dom.ts"; - -function processBlock(node: DjotBlock): Element { - switch (node.tag) { - case "section": - return processSection(node); - case "para": - return processPara(node); - case "heading": - return processHeading(node); - case "thematic_break": - return processThematicBreak(node); - case "block_quote": - return processBlockQuote(node); - case "code_block": - return processCodeBlock(node); - case "bullet_list": - return processBulletList(node); - case "ordered_list": - return processOrderedList(node); - case "task_list": - return processTaskList(node); - case "definition_list": - return processDefinitionList(node); - case "table": - return processTable(node); - case "div": - return processDiv(node); - case "raw_block": - return processRawBlock(node); - } -} - -function processSection(node: DjotSection): Element { - return { - kind: "element", - name: "section", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; -} - -function processPara(node: DjotPara): Element { - return { - kind: "element", - name: "p", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processHeading(node: DjotHeading): Element { - const attributes = convertAttributes(node.attributes); - return { - kind: "element", - name: "h", - attributes, - children: node.children.map(processInline), - }; -} - -function processThematicBreak(node: DjotThematicBreak): Element { - return { - kind: "element", - name: "hr", - attributes: convertAttributes(node.attributes), - children: [], - }; -} - -function processBlockQuote(node: DjotBlockQuote): Element { - return { - kind: "element", - name: "blockquote", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; -} - -function processCodeBlock(node: DjotCodeBlock): Element { - const attributes = convertAttributes(node.attributes); - if (node.lang) { - attributes.set("language", node.lang); - } - if (node.attributes?.filename) { - attributes.set("filename", node.attributes.filename); - } - if (node.attributes?.numbered) { - attributes.set("numbered", "true"); - } - return { - kind: "element", - name: "codeblock", - attributes, - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; -} - -function processBulletList(node: DjotBulletList): Element { - const attributes = convertAttributes(node.attributes); - attributes.set("--tight", node.tight ? "true" : "false"); - return { - kind: "element", - name: "ul", - attributes, - children: node.children.map(processListItem), - }; -} - -function processOrderedList(node: DjotOrderedList): Element { - const attributes = convertAttributes(node.attributes); - attributes.set("--tight", node.tight ? "true" : "false"); - if (node.start !== undefined && node.start !== 1) { - attributes.set("start", node.start.toString()); - } - return { - kind: "element", - name: "ol", - attributes, - children: node.children.map(processListItem), - }; -} - -function processTaskList(node: DjotTaskList): Element { - const attributes = convertAttributes(node.attributes); - attributes.set("type", "task"); - attributes.set("--tight", node.tight ? "true" : "false"); - return { - kind: "element", - name: "ul", - attributes, - children: node.children.map(processTaskListItem), - }; -} - -function processListItem(node: DjotListItem): Element { - return { - kind: "element", - name: "li", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; -} - -function processTaskListItem(node: DjotTaskListItem): Element { - const attributes = convertAttributes(node.attributes); - attributes.set("checked", node.checkbox === "checked" ? "true" : "false"); - return { - kind: "element", - name: "li", - attributes, - children: node.children.map(processBlock), - }; -} - -function processDefinitionList(node: DjotDefinitionList): Element { - return { - kind: "element", - name: "dl", - attributes: convertAttributes(node.attributes), - children: node.children.flatMap(processDefinitionListItem), - }; -} - -function processDefinitionListItem(node: DjotDefinitionListItem): Element[] { - return [ - processTerm(node.children[0]), - processDefinition(node.children[1]), - ]; -} - -function processTerm(node: DjotTerm): Element { - return { - kind: "element", - name: "dt", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processDefinition(node: DjotDefinition): Element { - return { - kind: "element", - name: "dd", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; -} - -function processTable(node: DjotTable): Element { - // Tables in Djot have a caption as first child and then rows - // For now, we'll create a basic table structure and ignore caption - const tableElement: Element = { - kind: "element", - name: "table", - attributes: convertAttributes(node.attributes), - children: [], - }; - - // Process caption if it exists (first child) - if (node.children.length > 0 && node.children[0].tag === "caption") { - const caption: Element = { - kind: "element", - name: "caption", - attributes: new Map(), - children: node.children[0].children.map(processInline), - }; - tableElement.children.push(caption); - } - - // Group rows into thead, tbody based on head property - const headerRows: Element[] = []; - const bodyRows: Element[] = []; - - // Start from index 1 to skip caption - for (let i = 1; i < node.children.length; i++) { - const row = node.children[i]; - if (row.tag === "row") { - const rowElement: Element = { - kind: "element", - name: "tr", - attributes: convertAttributes(row.attributes), - children: row.children.map((cell) => { - const cellElement: Element = { - kind: "element", - name: cell.head ? "th" : "td", - attributes: convertAttributes(cell.attributes), - children: cell.children.map(processInline), - }; - - // Set alignment attribute if needed - if (cell.align !== "default") { - cellElement.attributes.set("align", cell.align); - } - - return cellElement; - }), - }; - - if (row.head) { - headerRows.push(rowElement); - } else { - bodyRows.push(rowElement); - } - } - } - - // Add thead and tbody if needed - if (headerRows.length > 0) { - tableElement.children.push({ - kind: "element", - name: "thead", - attributes: new Map(), - children: headerRows, - }); - } - - if (bodyRows.length > 0) { - tableElement.children.push({ - kind: "element", - name: "tbody", - attributes: new Map(), - children: bodyRows, - }); - } - - return tableElement; -} - -function processInline(node: DjotInline): Node { - switch (node.tag) { - case "str": - return processStr(node); - case "soft_break": - return processSoftBreak(node); - case "hard_break": - return processHardBreak(node); - case "verbatim": - return processVerbatim(node); - case "emph": - return processEmph(node); - case "strong": - return processStrong(node); - case "link": - return processLink(node); - case "image": - return processImage(node); - case "mark": - return processMark(node); - case "superscript": - return processSuperscript(node); - case "subscript": - return processSubscript(node); - case "insert": - return processInsert(node); - case "delete": - return processDelete(node); - case "email": - return processEmail(node); - case "footnote_reference": - return processFootnoteReference(node); - case "url": - return processUrl(node); - case "span": - return processSpan(node); - case "inline_math": - return processInlineMath(node); - case "display_math": - return processDisplayMath(node); - case "non_breaking_space": - return processNonBreakingSpace(node); - case "symb": - return processSymb(node); - case "raw_inline": - return processRawInline(node); - case "double_quoted": - return processDoubleQuoted(node); - case "single_quoted": - return processSingleQuoted(node); - case "smart_punctuation": - return processSmartPunctuation(node); - } -} - -function processStr(node: DjotStr): Node { - return { - kind: "text", - content: node.text, - raw: false, - }; -} - -function processSoftBreak(_node: DjotSoftBreak): Node { - return { - kind: "text", - content: "\n", - raw: false, - }; -} - -function processHardBreak(_node: DjotHardBreak): Node { - return { - kind: "element", - name: "br", - attributes: new Map(), - children: [], - }; -} - -function processVerbatim(node: DjotVerbatim): Element { - return { - kind: "element", - name: "code", - attributes: convertAttributes(node.attributes), - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; -} - -function processEmph(node: DjotEmph): Element { - return { - kind: "element", - name: "em", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processStrong(node: DjotStrong): Element { - return { - kind: "element", - name: "strong", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processLink(node: DjotLink): Element { - const attributes = convertAttributes(node.attributes); - if (node.destination !== undefined) { - attributes.set("href", node.destination); - } - return { - kind: "element", - name: "a", - attributes, - children: node.children.map(processInline), - }; -} - -function processImage(node: DjotImage): Element { - const attributes = convertAttributes(node.attributes); - if (node.destination !== undefined) { - attributes.set("src", node.destination); - } - - // Alt text is derived from children in Djot - const alt = node.children - .map((child) => { - if (child.tag === "str") { - return child.text; - } - return ""; - }) - .join(""); - - if (alt) { - attributes.set("alt", alt); - } - - return { - kind: "element", - name: "img", - attributes, - children: [], - }; -} - -function processMark(node: DjotMark): Element { - return { - kind: "element", - name: "mark", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processSuperscript(node: DjotSuperscript): Element { - return { - kind: "element", - name: "sup", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processSubscript(node: DjotSubscript): Element { - return { - kind: "element", - name: "sub", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processInsert(node: DjotInsert): Element { - return { - kind: "element", - name: "ins", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processDelete(node: DjotDelete): Element { - return { - kind: "element", - name: "del", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processEmail(node: DjotEmail): Element { - return { - kind: "element", - name: "email", - attributes: convertAttributes(node.attributes), - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; -} - -function processFootnoteReference(node: DjotFootnoteReference): Element { - return { - kind: "element", - name: "footnoteref", - attributes: new Map([["reference", node.text]]), - children: [], - }; -} - -function processUrl(node: DjotUrl): Element { - return { - kind: "element", - name: "a", - attributes: new Map([ - ["href", node.text], - ...Object.entries(node.attributes || {}), - ]), - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; -} - -function processSpan(node: DjotSpan): Element { - return { - kind: "element", - name: "span", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; -} - -function processInlineMath(node: DjotInlineMath): Element { - // For inline math, we'll wrap it in a span with a class - return { - kind: "element", - name: "span", - attributes: new Map([ - ["class", "math inline"], - ...Object.entries(node.attributes || {}), - ]), - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; -} - -function processDisplayMath(node: DjotDisplayMath): Element { - // For display math, we'll wrap it in a div with a class - return { - kind: "element", - name: "div", - attributes: new Map([ - ["class", "math display"], - ...Object.entries(node.attributes || {}), - ]), - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; -} - -function processNonBreakingSpace(_node: DjotNonBreakingSpace): Node { - return { - kind: "text", - content: "\u00A0", // Unicode non-breaking space - raw: false, - }; -} - -function processSymb(node: DjotSymb): Node { - // Map symbol aliases to their Unicode characters - const symbolMap: Record<string, string> = { - "->": "→", - "<-": "←", - "<->": "↔", - "=>": "⇒", - "<=": "⇐", - "<=>": "⇔", - "--": "–", // en dash - "---": "—", // em dash - "...": "…", // ellipsis - // Add more symbol mappings as needed - }; - - const symbolText = symbolMap[node.alias] || node.alias; - - return { - kind: "text", - content: symbolText, - raw: false, - }; -} - -function processRawInline(node: DjotRawInline): Node { - // If the format is HTML, return as raw HTML - if (node.format === "html" || node.format === "HTML") { - return { - kind: "text", - content: node.text, - raw: true, - }; - } - - // For other formats, just return as text - return { - kind: "text", - content: node.text, - raw: false, - }; -} - -function processDoubleQuoted(node: DjotDoubleQuoted): Node { - const children = node.children.map(processInline); - const attributes = convertAttributes(node.attributes); - - if ( - children.length === 1 && children[0].kind === "text" && - attributes.size === 0 - ) { - const content = children[0].content; - return { - kind: "text", - content: `\u201C${content}\u201D`, - raw: false, - }; - } else { - return { - kind: "element", - name: "span", - attributes: convertAttributes(node.attributes), - children, - }; - } -} - -function processSingleQuoted(node: DjotSingleQuoted): Node { - const children = node.children.map(processInline); - const attributes = convertAttributes(node.attributes); - - if ( - children.length === 1 && children[0].kind === "text" && - attributes.size === 0 - ) { - const content = children[0].content; - return { - kind: "text", - content: `\u2018${content}\u2019`, - raw: false, - }; - } else { - return { - kind: "element", - name: "span", - attributes: convertAttributes(node.attributes), - children, - }; - } -} - -function processSmartPunctuation(node: DjotSmartPunctuation): Node { - // Map smart punctuation types to Unicode characters - const punctuationMap: Record<string, string> = { - "left_single_quote": "\u2018", // ' - "right_single_quote": "\u2019", // ' - "left_double_quote": "\u201C", // " - "right_double_quote": "\u201D", // " - "ellipses": "\u2026", // … - "em_dash": "\u2014", // — - "en_dash": "\u2013", // – - }; - - return { - kind: "text", - content: punctuationMap[node.type] || node.text, - raw: false, - }; -} - -function processDiv(node: DjotDiv): Element { - if (node.attributes?.class === "note") { - delete node.attributes.class; - return { - kind: "element", - name: "note", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; - } - - if (node.attributes?.class === "edit") { - delete node.attributes.class; - return { - kind: "element", - name: "note", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; - } - - return { - kind: "element", - name: "div", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; -} - -function processRawBlock(node: DjotRawBlock): Element { - // If the format is HTML, wrap the HTML content in a div - if (node.format === "html" || node.format === "HTML") { - return { - kind: "element", - name: "div", - attributes: new Map([["class", "raw-html"]]), - children: [ - { - kind: "text", - content: node.text, - raw: true, - }, - ], - }; - } - - // For other formats, wrap in a pre tag - return { - kind: "element", - name: "pre", - attributes: new Map([["data-format", node.format]]), - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; -} - -// Helper function to convert Djot attributes to Nuldoc attributes -function convertAttributes( - attrs?: Record<string, string>, -): Map<string, string> { - const result = new Map<string, string>(); - if (attrs) { - for (const [key, value] of Object.entries(attrs)) { - result.set(key, value); - } - } - return result; -} - -export function djot2ndoc(doc: DjotDoc): Element { - const children: Node[] = []; - for (const child of doc.children) { - children.push(processBlock(child)); - } - - // Process footnotes if any exist - if (doc.footnotes && Object.keys(doc.footnotes).length > 0) { - const footnoteSection: Element = { - kind: "element", - name: "section", - attributes: new Map([["class", "footnotes"]]), - children: [], - }; - - for (const [id, footnote] of Object.entries(doc.footnotes)) { - const footnoteElement: Element = { - kind: "element", - name: "footnote", - attributes: new Map([["id", id]]), - children: footnote.children.map(processBlock), - }; - footnoteSection.children.push(footnoteElement); - } - - children.push(footnoteSection); - } - - return { - kind: "element", - name: "__root__", - attributes: new Map(), - children: [{ - kind: "element", - name: "article", - attributes: new Map(), - children, - }], - }; -} diff --git a/vhosts/blog/nuldoc-src/djot/document.ts b/vhosts/blog/nuldoc-src/djot/document.ts deleted file mode 100644 index be9c08d5..00000000 --- a/vhosts/blog/nuldoc-src/djot/document.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Doc as DjotDoc } from "@djot/djot"; -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 { djot2ndoc } from "./djot2ndoc.ts"; - -export const PostMetadataSchema = z.object({ - article: z.object({ - uuid: z.string(), - title: z.string(), - description: z.string(), - tags: z.array(z.string()), - revisions: z.array(z.object({ - date: z.string(), - remark: z.string(), - isInternal: z.boolean().optional(), - })), - }), -}); - -export type PostMetadata = z.infer<typeof PostMetadataSchema>; - -export type Document = { - root: Element; - sourceFilePath: string; - uuid: string; - link: string; - title: string; - description: string; // TODO: should it be markup text? - tags: string[]; - revisions: Revision[]; -}; - -export function createNewDocumentFromDjotDocument( - root: DjotDoc, - 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: djot2ndoc(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, - })), - }; -} diff --git a/vhosts/blog/nuldoc-src/djot/parse.ts b/vhosts/blog/nuldoc-src/djot/parse.ts deleted file mode 100644 index c79a6708..00000000 --- a/vhosts/blog/nuldoc-src/djot/parse.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { parse as parseDjot } from "@djot/djot"; -import { parse as parseToml } from "@std/toml"; -import { Config } from "../config.ts"; -import { - createNewDocumentFromDjotDocument, - Document, - PostMetadata, - PostMetadataSchema, -} from "./document.ts"; -import toHtml from "./to_html.ts"; - -export async function parseDjotFile( - filePath: string, - config: Config, -): Promise<Document> { - try { - const fileContent = await Deno.readTextFile(filePath); - const [, frontmatter, ...rest] = fileContent.split(/^---$/m); - const meta = parseMetadata(frontmatter); - const root = parseDjot(rest.join("\n")); - const doc = createNewDocumentFromDjotDocument(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/vhosts/blog/nuldoc-src/djot/to_html.ts b/vhosts/blog/nuldoc-src/djot/to_html.ts deleted file mode 100644 index 5ea9b57d..00000000 --- a/vhosts/blog/nuldoc-src/djot/to_html.ts +++ /dev/null @@ -1,449 +0,0 @@ -import { BundledLanguage, bundledLanguages, codeToHtml } from "shiki"; -import { Document } from "./document.ts"; -import { NuldocError } from "../errors.ts"; -import { - addClass, - Element, - forEachChild, - forEachChildRecursively, - forEachChildRecursivelyAsync, - Node, - RawHTML, - 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); - 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" && !child.raw) { - currentTextContent += child.content; - } else { - if (currentTextContent !== "") { - newChildren.push({ - kind: "text", - content: currentTextContent, - raw: false, - }); - currentTextContent = ""; - } - newChildren.push(child); - } - } - - if (currentTextContent !== "") { - newChildren.push({ - kind: "text", - content: currentTextContent, - raw: false, - }); - } - - 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; - } - - const newChildren: Node[] = []; - for (const child of n.children) { - if (child.kind !== "text") { - newChildren.push(child); - continue; - } - let restContent = child.content; - while (restContent !== "") { - const match = /^(.*?)(https?:\/\/[^ \n]+)(.*)$/s.exec(restContent); - if (!match) { - newChildren.push({ kind: "text", content: restContent, raw: false }); - restContent = ""; - break; - } - const [_, prefix, url, suffix] = match; - newChildren.push({ kind: "text", content: prefix, raw: false }); - newChildren.push({ - kind: "element", - name: "a", - attributes: new Map([["href", url]]), - children: [{ kind: "text", content: url, raw: false }], - }); - restContent = suffix; - } - } - n.children = newChildren; - }); -} - -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.get("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.set("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.get("id"); - const aElement: Element = { - kind: "element", - name: "a", - attributes: new Map(), - children: c.children, - }; - aElement.attributes.set("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.set("--section-level", 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) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "note") { - return; - } - - const editatAttr = n.attributes?.get("editat"); - const operationAttr = n.attributes?.get("operation"); - const isEditBlock = editatAttr && operationAttr; - - const labelElement: Element = { - kind: "element", - name: "div", - attributes: new Map([["class", "admonition-label"]]), - children: [{ - kind: "text", - content: isEditBlock ? `${editatAttr} ${operationAttr}` : "NOTE", - raw: false, - }], - }; - const contentElement: Element = { - kind: "element", - name: "div", - attributes: new Map([["class", "admonition-content"]]), - children: n.children, - }; - n.name = "div"; - addClass(n, "admonition"); - n.children = [ - labelElement, - contentElement, - ]; - }); -} - -function addAttributesToExternalLinkElement(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "a") { - return; - } - - const href = n.attributes.get("href") ?? ""; - if (!href.startsWith("http")) { - return; - } - n.attributes - .set("target", "_blank") - .set("rel", "noreferrer"); - }); -} - -function traverseFootnotes(doc: Document) { - let footnoteCounter = 0; - const footnoteMap = new Map<string, number>(); - - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "footnoteref") { - return; - } - - const reference = n.attributes.get("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"; - n.attributes.delete("reference"); - n.attributes.set("class", "footnote"); - n.children = [ - { - kind: "element", - name: "a", - attributes: new Map([ - ["id", `footnoteref--${reference}`], - ["class", "footnote"], - ["href", `#footnote--${reference}`], - ]), - children: [ - { - kind: "text", - content: `[${footnoteNumber}]`, - raw: false, - }, - ], - }, - ]; - }); - - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "footnote") { - return; - } - - const id = n.attributes.get("id"); - if (!id || !footnoteMap.has(id)) { - n.name = "span"; - n.children = []; - return; - } - - const footnoteNumber = footnoteMap.get(id)!; - - n.name = "div"; - n.attributes.delete("id"); - n.attributes.set("class", "footnote"); - n.attributes.set("id", `footnote--${id}`); - - n.children = [ - { - kind: "element", - name: "a", - attributes: new Map([["href", `#footnoteref--${id}`]]), - children: [ - { - kind: "text", - content: `${footnoteNumber}. `, - raw: false, - }, - ], - }, - ...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.get("--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.get("language") || "text"; - const filename = n.attributes.get("filename"); - const numbered = n.attributes.get("numbered"); - const sourceCodeNode = n.children[0] as Text | RawHTML; - const sourceCode = sourceCodeNode.content.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.set("class", "codeblock"); - n.attributes.delete("language"); - - if (numbered === "true") { - n.attributes.delete("numbered"); - addClass(n, "numbered"); - } - if (filename) { - n.attributes.delete("filename"); - - n.children = [ - { - kind: "element", - name: "div", - attributes: new Map([["class", "filename"]]), - children: [{ - kind: "text", - content: filename, - raw: false, - }], - }, - { - kind: "text", - content: highlighted, - raw: true, - }, - ]; - } else { - sourceCodeNode.content = highlighted; - sourceCodeNode.raw = true; - } - }); -} diff --git a/vhosts/blog/nuldoc-src/dom.ts b/vhosts/blog/nuldoc-src/dom.ts deleted file mode 100644 index ed7ffd31..00000000 --- a/vhosts/blog/nuldoc-src/dom.ts +++ /dev/null @@ -1,102 +0,0 @@ -export type Text = { - kind: "text"; - content: string; - raw: false; -}; - -export type RawHTML = { - kind: "text"; - content: string; - raw: true; -}; - -export type Element = { - kind: "element"; - name: string; - attributes: Map<string, string>; - children: Node[]; -}; - -export type Node = Element | Text | RawHTML; - -export function addClass(e: Element, klass: string) { - const classes = e.attributes.get("class"); - if (classes === undefined) { - e.attributes.set("class", klass); - } else { - const classList = classes.split(" "); - classList.push(klass); - classList.sort(); - e.attributes.set("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); -} diff --git a/vhosts/blog/nuldoc-src/errors.ts b/vhosts/blog/nuldoc-src/errors.ts deleted file mode 100644 index 1692a4c8..00000000 --- a/vhosts/blog/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/vhosts/blog/nuldoc-src/generators/about.ts b/vhosts/blog/nuldoc-src/generators/about.ts deleted file mode 100644 index 6663a190..00000000 --- a/vhosts/blog/nuldoc-src/generators/about.ts +++ /dev/null @@ -1,23 +0,0 @@ -import AboutPage from "../pages/AboutPage.tsx"; -import { Config } from "../config.ts"; -import { renderToDOM } from "../jsx/render.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 renderToDOM( - AboutPage(slides, config), - ); - - return { - root: html, - renderer: "html", - destFilePath: "/about/index.html", - href: "/about/", - }; -} diff --git a/vhosts/blog/nuldoc-src/generators/atom.ts b/vhosts/blog/nuldoc-src/generators/atom.ts deleted file mode 100644 index 6ad07b46..00000000 --- a/vhosts/blog/nuldoc-src/generators/atom.ts +++ /dev/null @@ -1,79 +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.tsx"; -import { renderToDOM } from "../jsx/render.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 async function generateFeedPageFromEntries( - alternateLink: string, - feedSlug: string, - feedTitle: string, - entries: Array<PostPage | SlidePage>, - config: Config, -): Promise<Page> { - const entries_: Entry[] = []; - for (const entry of entries) { - entries_.push({ - id: `urn:uuid:${entry.uuid}`, - linkToAlternate: `https://${config.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.blog.author, - icon: `https://${config.blog.fqdn}/favicon.svg`, - id: `tag:${config.blog.fqdn},${config.blog.siteCopyrightYear}:${feedSlug}`, - linkToSelf: `https://${config.blog.fqdn}${feedPath}`, - linkToAlternate: `https://${config.blog.fqdn}${alternateLink}`, - title: feedTitle, - updated: entries_.reduce( - (latest, entry) => entry.updated > latest ? entry.updated : latest, - entries_[0].updated, - ), - entries: entries_, - }; - - return { - root: await renderToDOM(AtomPage({ feed: feed })), - renderer: "xml", - destFilePath: feedPath, - href: feedPath, - }; -} diff --git a/vhosts/blog/nuldoc-src/generators/home.ts b/vhosts/blog/nuldoc-src/generators/home.ts deleted file mode 100644 index 679dd39a..00000000 --- a/vhosts/blog/nuldoc-src/generators/home.ts +++ /dev/null @@ -1,19 +0,0 @@ -import HomePage from "../pages/HomePage.tsx"; -import { renderToDOM } from "../jsx/render.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 renderToDOM( - HomePage(config), - ); - - return { - root: html, - renderer: "html", - destFilePath: "/index.html", - href: "/", - }; -} diff --git a/vhosts/blog/nuldoc-src/generators/not_found.ts b/vhosts/blog/nuldoc-src/generators/not_found.ts deleted file mode 100644 index f5a81c86..00000000 --- a/vhosts/blog/nuldoc-src/generators/not_found.ts +++ /dev/null @@ -1,21 +0,0 @@ -import NotFoundPage from "../pages/NotFoundPage.tsx"; -import { renderToDOM } from "../jsx/render.ts"; -import { Config } from "../config.ts"; -import { Page } from "../page.ts"; - -export type NotFoundPage = Page; - -export async function generateNotFoundPage( - config: Config, -): Promise<NotFoundPage> { - const html = await renderToDOM( - NotFoundPage(config), - ); - - return { - root: html, - renderer: "html", - destFilePath: "/404.html", - href: "/404.html", - }; -} diff --git a/vhosts/blog/nuldoc-src/generators/post.ts b/vhosts/blog/nuldoc-src/generators/post.ts deleted file mode 100644 index 0e2a9553..00000000 --- a/vhosts/blog/nuldoc-src/generators/post.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { join } from "@std/path"; -import { renderToDOM } from "../jsx/render.ts"; -import PostPage from "../pages/PostPage.tsx"; -import { Config } from "../config.ts"; -import { Document } from "../djot/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; -} - -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 renderToDOM( - PostPage(doc, config), - ); - - const cwd = Deno.cwd(); - const contentDir = join(cwd, config.locations.contentDir); - const destFilePath = join( - doc.sourceFilePath.replace(contentDir, "").replace(".dj", ""), - "index.html", - ); - return { - root: html, - renderer: "html", - 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, - }; -} diff --git a/vhosts/blog/nuldoc-src/generators/post_list.ts b/vhosts/blog/nuldoc-src/generators/post_list.ts deleted file mode 100644 index b05f7ee6..00000000 --- a/vhosts/blog/nuldoc-src/generators/post_list.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { renderToDOM } from "../jsx/render.ts"; -import PostListPage from "../pages/PostListPage.tsx"; -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.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 renderToDOM( - 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", - destFilePath, - href, - }; -} diff --git a/vhosts/blog/nuldoc-src/generators/slide.ts b/vhosts/blog/nuldoc-src/generators/slide.ts deleted file mode 100644 index cd28879f..00000000 --- a/vhosts/blog/nuldoc-src/generators/slide.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { join } from "@std/path"; -import { renderToDOM } from "../jsx/render.ts"; -import SlidePage from "../pages/SlidePage.tsx"; -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 renderToDOM( - 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", - 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/vhosts/blog/nuldoc-src/generators/slide_list.ts b/vhosts/blog/nuldoc-src/generators/slide_list.ts deleted file mode 100644 index abebe109..00000000 --- a/vhosts/blog/nuldoc-src/generators/slide_list.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { renderToDOM } from "../jsx/render.ts"; -import SlideListPage from "../pages/SlideListPage.tsx"; -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 renderToDOM( - SlideListPage(slides, config), - ); - - return { - root: html, - renderer: "html", - destFilePath: "/slides/index.html", - href: "/slides/", - }; -} diff --git a/vhosts/blog/nuldoc-src/generators/tag.ts b/vhosts/blog/nuldoc-src/generators/tag.ts deleted file mode 100644 index dbd8ef93..00000000 --- a/vhosts/blog/nuldoc-src/generators/tag.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { renderToDOM } from "../jsx/render.ts"; -import TagPage from "../pages/TagPage.tsx"; -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[], - config: Config, -): Promise<TagPage> { - const html = await renderToDOM( - TagPage(tagSlug, pages, config), - ); - - return { - root: html, - renderer: "html", - 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/vhosts/blog/nuldoc-src/generators/tag_list.ts b/vhosts/blog/nuldoc-src/generators/tag_list.ts deleted file mode 100644 index 7baad8cf..00000000 --- a/vhosts/blog/nuldoc-src/generators/tag_list.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { renderToDOM } from "../jsx/render.ts"; -import TagListPage from "../pages/TagListPage.tsx"; -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[], - config: Config, -): Promise<TagListPage> { - const html = await renderToDOM( - TagListPage(tags, config), - ); - - return { - root: html, - renderer: "html", - destFilePath: "/tags/index.html", - href: "/tags/", - }; -} diff --git a/vhosts/blog/nuldoc-src/generators/tagged_page.ts b/vhosts/blog/nuldoc-src/generators/tagged_page.ts deleted file mode 100644 index 23de8cb4..00000000 --- a/vhosts/blog/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/vhosts/blog/nuldoc-src/jsx/jsx-runtime.ts b/vhosts/blog/nuldoc-src/jsx/jsx-runtime.ts deleted file mode 100644 index 9571e87d..00000000 --- a/vhosts/blog/nuldoc-src/jsx/jsx-runtime.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Node } from "../dom.ts"; - -export type JSXElement = { - tag: string | FunctionComponent; - props: Props; -}; - -export type JSXNullNode = false | null | undefined; -export type JSXSimpleNode = JSXElement | Node | string; -export type JSXNullableSimpleNode = JSXSimpleNode | JSXNullNode; -export type JSXNode = JSXNullableSimpleNode | JSXNode[]; -export type RenderableJSXNode = JSXElement; - -type Props = { children?: JSXNode } & Record<string, unknown>; -export type FunctionComponentResult = JSXElement | Promise<JSXElement>; -type FunctionComponent = (props: Props) => FunctionComponentResult; - -export function jsx( - tag: string | FunctionComponent, - props: Props, -): JSXElement { - return { tag, props }; -} - -export { jsx as jsxs }; - -// TODO: support Fragment diff --git a/vhosts/blog/nuldoc-src/jsx/render.ts b/vhosts/blog/nuldoc-src/jsx/render.ts deleted file mode 100644 index 8603f6c3..00000000 --- a/vhosts/blog/nuldoc-src/jsx/render.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Element, Node } from "../dom.ts"; -import type { - JSXNode, - JSXNullableSimpleNode, - JSXSimpleNode, - RenderableJSXNode, -} from "myjsx/jsx-runtime"; - -function transformNode(node: JSXNode): Promise<Node[]> { - const flattenNodes: JSXNullableSimpleNode[] = Array.isArray(node) - // @ts-ignore prevents infinite recursion - ? (node.flat(Infinity) as JSXNullableSimpleNode[]) - : [node]; - return Promise.all( - flattenNodes - .filter((c): c is JSXSimpleNode => c != null && c !== false) - .map((c) => { - if (typeof c === "string") { - return { kind: "text", content: c, raw: false }; - } else if ("kind" in c) { - return c; - } else { - return renderToDOM(c); - } - }), - ); -} - -export async function renderToDOM( - element: RenderableJSXNode, -): Promise<Element> { - const { tag, props } = element; - if (typeof tag === "string") { - const { children, ...attrs } = props; - const attrsMap = new Map(Object.entries(attrs)) as Map<string, string>; - return { - kind: "element", - name: tag, - attributes: attrsMap, - children: await transformNode(children), - }; - } else { - return renderToDOM(await tag(props)); - } -} diff --git a/vhosts/blog/nuldoc-src/jsx/types.d.ts b/vhosts/blog/nuldoc-src/jsx/types.d.ts deleted file mode 100644 index 0e5b223f..00000000 --- a/vhosts/blog/nuldoc-src/jsx/types.d.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { - FunctionComponentResult, - JSXElement, - JSXNode, -} from "myjsx/jsx-runtime"; - -export { JSXNode }; - -interface IntrinsicElementType { - children?: JSXNode; - className?: string; - id?: string; - // My JSX runtime does not use key. It is only for linter that complains about missing key. - key?: string; -} - -declare global { - namespace JSX { - type Element = JSXElement; - type ElementType = - | string - // deno-lint-ignore no-explicit-any - | ((props: any) => FunctionComponentResult); - - // TODO: HTML 用の element と XML 用の element を分ける - interface IntrinsicElements { - // XML (Atom) - author: IntrinsicElementType; - entry: IntrinsicElementType; - feed: IntrinsicElementType & { xmlns: string }; - id: IntrinsicElementType; - name: IntrinsicElementType; - published: IntrinsicElementType; - summary: IntrinsicElementType; - updated: IntrinsicElementType; - // HTML - a: IntrinsicElementType & { - href?: string; - rel?: "noreferrer"; - target?: "_blank"; - }; - article: IntrinsicElementType; - body: IntrinsicElementType; - button: IntrinsicElementType & { type: string }; - canvas: { id?: string; "data-slide-link"?: string }; - div: IntrinsicElementType; - footer: IntrinsicElementType; - h1: IntrinsicElementType; - h2: IntrinsicElementType; - head: unknown; - header: IntrinsicElementType; - html: IntrinsicElementType & { lang?: string }; - img: { src: string }; - li: IntrinsicElementType; - link: { rel: string; href: string; type?: string }; - main: IntrinsicElementType; - meta: { - charset?: string; - name?: string; - content?: string; - property?: string; - }; - nav: IntrinsicElementType; - noscript: IntrinsicElementType; - ol: IntrinsicElementType; - p: IntrinsicElementType; - script: { src: string; type?: string; defer?: "true" }; - section: IntrinsicElementType; - time: IntrinsicElementType & { datetime?: string }; - title: IntrinsicElementType; - ul: IntrinsicElementType; - } - - interface ElementChildrenAttribute { - children: unknown; - } - - type LibraryManagedAttributes<_F, P> = P & { - // My JSX runtime does not use key. It is only for linter that complains about missing key. - key?: string; - }; - } -} diff --git a/vhosts/blog/nuldoc-src/main.ts b/vhosts/blog/nuldoc-src/main.ts deleted file mode 100644 index af6acc2e..00000000 --- a/vhosts/blog/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/vhosts/blog/nuldoc-src/page.ts b/vhosts/blog/nuldoc-src/page.ts deleted file mode 100644 index f4a6166b..00000000 --- a/vhosts/blog/nuldoc-src/page.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Element } from "./dom.ts"; -import { RendererType } from "./render.ts"; - -export interface Page { - root: Element; - renderer: RendererType; - destFilePath: string; - href: string; -} diff --git a/vhosts/blog/nuldoc-src/pages/AboutPage.tsx b/vhosts/blog/nuldoc-src/pages/AboutPage.tsx deleted file mode 100644 index 3d6583a4..00000000 --- a/vhosts/blog/nuldoc-src/pages/AboutPage.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import StaticScript from "../components/StaticScript.tsx"; -import { Config } from "../config.ts"; -import { dateToString } from "../revision.ts"; -import { getPostPublishedDate } from "../generators/post.ts"; -import { SlidePage } from "../generators/slide.ts"; - -export default function AboutPage( - slides: SlidePage[], - config: Config, -) { - return ( - <PageLayout - metaCopyrightYear={config.blog.siteCopyrightYear} - metaDescription="このサイトの著者について" - metaTitle={`About|${config.blog.siteName}`} - config={config} - > - <body className="single"> - <GlobalHeader config={config} /> - <main className="main"> - <article className="post-single"> - <header className="post-header"> - <h1 className="post-title">nsfisis</h1> - <div className="my-icon"> - <div id="myIcon"> - <img src="/favicon.svg" /> - </div> - <StaticScript - fileName="/my-icon.js" - defer="true" - config={config} - /> - </div> - </header> - <div className="post-content"> - <section> - <h2>読み方</h2> - <p> - 読み方は決めていません。音にする必要があるときは本名である「いまむら」をお使いください。 - </p> - </section> - <section> - <h2>アカウント</h2> - <ul> - <li> - <a - href="https://twitter.com/nsfisis" - target="_blank" - rel="noreferrer" - > - Twitter (現 𝕏): @nsfisis - </a> - </li> - <li> - <a - href="https://github.com/nsfisis" - target="_blank" - rel="noreferrer" - > - GitHub: @nsfisis - </a> - </li> - </ul> - </section> - <section> - <h2>仕事</h2> - <ul> - <li> - {"2021-01~現在: "} - <a - href="https://www.dgcircus.com/" - target="_blank" - rel="noreferrer" - > - デジタルサーカス株式会社 - </a> - </li> - </ul> - </section> - <section> - <h2>登壇</h2> - <ul> - {Array.from(slides).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; - }).map((slide) => ( - <li> - <a href={slide.href}> - {`${ - dateToString(getPostPublishedDate(slide)) - }: ${slide.event} (${slide.talkType})`} - </a> - </li> - ))} - </ul> - </section> - </div> - </article> - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/AtomPage.tsx b/vhosts/blog/nuldoc-src/pages/AtomPage.tsx deleted file mode 100644 index 21c3bfaf..00000000 --- a/vhosts/blog/nuldoc-src/pages/AtomPage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Feed } from "../generators/atom.ts"; - -export default function AtomPage({ feed }: { feed: Feed }) { - return ( - <feed xmlns="http://www.w3.org/2005/Atom"> - <id>{feed.id}</id> - <title>{feed.title}</title> - <link rel="alternate" href={feed.linkToAlternate} /> - <link rel="self" href={feed.linkToSelf} /> - <author> - <name>{feed.author}</name> - </author> - <updated>{feed.updated}</updated> - {feed.entries.map((entry) => ( - <entry> - <id>{entry.id}</id> - <link rel="alternate" href={entry.linkToAlternate} /> - <title>{entry.title}</title> - <summary>{entry.summary}</summary> - <published>{entry.published}</published> - <updated>{entry.updated}</updated> - </entry> - ))} - </feed> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/HomePage.tsx b/vhosts/blog/nuldoc-src/pages/HomePage.tsx deleted file mode 100644 index 8850d039..00000000 --- a/vhosts/blog/nuldoc-src/pages/HomePage.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import { Config } from "../config.ts"; - -export default function HomePage(config: Config) { - return ( - <PageLayout - metaCopyrightYear={config.blog.siteCopyrightYear} - metaDescription="nsfisis のブログサイト" - metaTitle={config.blog.siteName} - metaAtomFeedHref={`https://${config.blog.fqdn}/atom.xml`} - config={config} - > - <body className="single"> - <GlobalHeader config={config} /> - <main className="main"> - <article className="post-single"> - <article className="post-entry"> - <a href="/about/"> - <header className="entry-header"> - <h2>About</h2> - </header> - </a> - </article> - <article className="post-entry"> - <a href="/posts/"> - <header className="entry-header"> - <h2>Posts</h2> - </header> - </a> - </article> - <article className="post-entry"> - <a href="/slides/"> - <header className="entry-header"> - <h2>Slides</h2> - </header> - </a> - </article> - <article className="post-entry"> - <a href="/tags/"> - <header className="entry-header"> - <h2>Tags</h2> - </header> - </a> - </article> - </article> - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/NotFoundPage.tsx b/vhosts/blog/nuldoc-src/pages/NotFoundPage.tsx deleted file mode 100644 index 9631fef2..00000000 --- a/vhosts/blog/nuldoc-src/pages/NotFoundPage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import { Config } from "../config.ts"; - -export default function NotFoundPage( - config: Config, -) { - return ( - <PageLayout - metaCopyrightYear={config.blog.siteCopyrightYear} - metaDescription="リクエストされたページが見つかりません" - metaTitle={`Page Not Found|${config.blog.siteName}`} - config={config} - > - <body className="single"> - <GlobalHeader config={config} /> - <main className="main"> - <article> - <div className="not-found">404</div> - </article> - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/PostListPage.tsx b/vhosts/blog/nuldoc-src/pages/PostListPage.tsx deleted file mode 100644 index 054955e6..00000000 --- a/vhosts/blog/nuldoc-src/pages/PostListPage.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import Pagination from "../components/Pagination.tsx"; -import PostPageEntry from "../components/PostPageEntry.tsx"; -import { Config } from "../config.ts"; -import { PostPage } from "../generators/post.ts"; - -export default function PostListPage( - posts: PostPage[], - config: Config, - currentPage: number, - totalPages: number, -) { - const pageTitle = "投稿一覧"; - - const pageInfoSuffix = ` (${currentPage}ページ目)`; - const metaTitle = `${pageTitle}${pageInfoSuffix}|${config.blog.siteName}`; - const metaDescription = `投稿した記事の一覧${pageInfoSuffix}`; - - return ( - <PageLayout - metaCopyrightYear={config.blog.siteCopyrightYear} - metaDescription={metaDescription} - metaTitle={metaTitle} - metaAtomFeedHref={`https://${config.blog.fqdn}/posts/atom.xml`} - config={config} - > - <body className="list"> - <GlobalHeader config={config} /> - <main className="main"> - <header className="page-header"> - <h1>{pageTitle}{pageInfoSuffix}</h1> - </header> - - <Pagination - currentPage={currentPage} - totalPages={totalPages} - basePath="/posts/" - /> - - {posts.map((post) => <PostPageEntry post={post} key={post.uuid} />)} - - <Pagination - currentPage={currentPage} - totalPages={totalPages} - basePath="/posts/" - /> - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/PostPage.tsx b/vhosts/blog/nuldoc-src/pages/PostPage.tsx deleted file mode 100644 index 97a24048..00000000 --- a/vhosts/blog/nuldoc-src/pages/PostPage.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import { Config, getTagLabel } from "../config.ts"; -import { Element } from "../dom.ts"; -import { Document } from "../djot/document.ts"; -import { dateToString } from "../revision.ts"; -import { getPostPublishedDate } from "../generators/post.ts"; - -export default function PostPage( - doc: Document, - config: Config, -) { - return ( - <PageLayout - metaCopyrightYear={getPostPublishedDate(doc).year} - metaDescription={doc.description} - metaKeywords={doc.tags.map((slug) => getTagLabel(config, slug))} - metaTitle={`${doc.title}|${config.blog.siteName}`} - requiresSyntaxHighlight - config={config} - > - <body className="single"> - <GlobalHeader config={config} /> - <main className="main"> - <article className="post-single"> - <header className="post-header"> - <h1 className="post-title">{doc.title}</h1> - {doc.tags.length !== 0 && ( - <ul className="post-tags"> - {doc.tags.map((slug) => ( - <li className="tag"> - <a href={`/tags/${slug}/`}>{getTagLabel(config, slug)}</a> - </li> - ))} - </ul> - )} - </header> - <div className="post-content"> - <section id="changelog"> - <h2> - <a href="#changelog">更新履歴</a> - </h2> - <ol> - {doc.revisions.map((rev) => ( - <li className="revision"> - <time datetime={dateToString(rev.date)}> - {dateToString(rev.date)} - </time> - {`: ${rev.remark}`} - </li> - ))} - </ol> - </section> - { - // TODO: refactor - (doc.root.children[0] as Element).children - } - </div> - </article> - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/SlideListPage.tsx b/vhosts/blog/nuldoc-src/pages/SlideListPage.tsx deleted file mode 100644 index 3d87d492..00000000 --- a/vhosts/blog/nuldoc-src/pages/SlideListPage.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import SlidePageEntry from "../components/SlidePageEntry.tsx"; -import { Config } from "../config.ts"; -import { dateToString } from "../revision.ts"; -import { getPostPublishedDate } from "../generators/post.ts"; -import { SlidePage } from "../generators/slide.ts"; - -export default function SlideListPage( - slides: SlidePage[], - config: Config, -) { - const pageTitle = "スライド一覧"; - - return ( - <PageLayout - metaCopyrightYear={config.blog.siteCopyrightYear} - metaDescription="登壇したイベントで使用したスライドの一覧" - metaTitle={`${pageTitle}|${config.blog.siteName}`} - metaAtomFeedHref={`https://${config.blog.fqdn}/slides/atom.xml`} - config={config} - > - <body className="list"> - <GlobalHeader config={config} /> - <main className="main"> - <header className="page-header"> - <h1>{pageTitle}</h1> - </header> - {Array.from(slides).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; - }).map((slide) => <SlidePageEntry slide={slide} key={slide.uuid} />)} - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/SlidePage.tsx b/vhosts/blog/nuldoc-src/pages/SlidePage.tsx deleted file mode 100644 index fc11072d..00000000 --- a/vhosts/blog/nuldoc-src/pages/SlidePage.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import StaticScript from "../components/StaticScript.tsx"; -import { Config, getTagLabel } from "../config.ts"; -import { dateToString } from "../revision.ts"; -import { Slide } from "../slide/slide.ts"; -import { getPostPublishedDate } from "../generators/post.ts"; - -export default function SlidePage( - slide: Slide, - config: Config, -) { - return ( - <PageLayout - metaCopyrightYear={getPostPublishedDate(slide).year} - metaDescription={slide.title} - metaKeywords={slide.tags.map((slug) => getTagLabel(config, slug))} - metaTitle={`${slide.event} (${slide.talkType})|${config.blog.siteName}`} - requiresSyntaxHighlight - config={config} - > - <body className="single"> - <GlobalHeader config={config} /> - <main className="main"> - <article className="post-single"> - <header className="post-header"> - <h1 className="post-title">{slide.title}</h1> - {slide.tags.length !== 0 && ( - <ul className="post-tags"> - {slide.tags.map((slug) => ( - <li className="tag"> - <a href={`/tags/${slug}/`}>{getTagLabel(config, slug)}</a> - </li> - ))} - </ul> - )} - </header> - <div className="post-content"> - <section id="changelog"> - <h2> - <a href="#changelog">更新履歴</a> - </h2> - <ol> - {slide.revisions.map((rev) => ( - <li className="revision"> - <time datetime={dateToString(rev.date)}> - {dateToString(rev.date)} - </time> - {`: ${rev.remark}`} - </li> - ))} - </ol> - </section> - <canvas id="slide" data-slide-link={slide.slideLink} /> - <div> - <button id="prev" type="button">Prev</button> - <button id="next" type="button">Next</button> - </div> - <StaticScript - fileName="/slide.js" - type="module" - config={config} - /> - </div> - </article> - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/TagListPage.tsx b/vhosts/blog/nuldoc-src/pages/TagListPage.tsx deleted file mode 100644 index cdb83ea5..00000000 --- a/vhosts/blog/nuldoc-src/pages/TagListPage.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import { Config } from "../config.ts"; -import { TagPage } from "../generators/tag.ts"; - -export default function TagListPage( - tags: TagPage[], - config: Config, -) { - const pageTitle = "タグ一覧"; - - return ( - <PageLayout - metaCopyrightYear={config.blog.siteCopyrightYear} - metaDescription="タグの一覧" - metaTitle={`${pageTitle}|${config.blog.siteName}`} - config={config} - > - <body className="list"> - <GlobalHeader config={config} /> - <main className="main"> - <header className="page-header"> - <h1>{pageTitle}</h1> - </header> - {Array.from(tags).sort((a, b) => { - const ta = a.tagSlug; - const tb = b.tagSlug; - if (ta < tb) return -1; - if (ta > tb) return 1; - return 0; - }).map((tag) => ( - <article className="post-entry"> - <a href={tag.href}> - <header className="entry-header"> - <h2>{tag.tagLabel}</h2> - </header> - <footer className="entry-footer"> - {(() => { - const posts = tag.numOfPosts === 0 - ? "" - : `${tag.numOfPosts}件の記事`; - const slides = tag.numOfSlides === 0 - ? "" - : `${tag.numOfSlides}件のスライド`; - return `${posts}${posts && slides ? "、" : ""}${slides}`; - })()} - </footer> - </a> - </article> - ))} - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/pages/TagPage.tsx b/vhosts/blog/nuldoc-src/pages/TagPage.tsx deleted file mode 100644 index 02c484af..00000000 --- a/vhosts/blog/nuldoc-src/pages/TagPage.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import GlobalFooter from "../components/GlobalFooter.tsx"; -import GlobalHeader from "../components/GlobalHeader.tsx"; -import PageLayout from "../components/PageLayout.tsx"; -import PostPageEntry from "../components/PostPageEntry.tsx"; -import SlidePageEntry from "../components/SlidePageEntry.tsx"; -import { Config, getTagLabel } from "../config.ts"; -import { getPostPublishedDate } from "../generators/post.ts"; -import { TaggedPage } from "../generators/tagged_page.ts"; - -export default function TagPage( - tagSlug: string, - pages: TaggedPage[], - config: Config, -) { - const tagLabel = getTagLabel(config, tagSlug); - const pageTitle = `タグ「${tagLabel}」一覧`; - - return ( - <PageLayout - metaCopyrightYear={getPostPublishedDate(pages[pages.length - 1]).year} - metaDescription={`タグ「${tagLabel}」のついた記事またはスライドの一覧`} - metaKeywords={[tagLabel]} - metaTitle={`${pageTitle}|${config.blog.siteName}`} - metaAtomFeedHref={`https://${config.blog.fqdn}/tags/${tagSlug}/atom.xml`} - config={config} - > - <body className="list"> - <GlobalHeader config={config} /> - <main className="main"> - <header className="page-header"> - <h1>{pageTitle}</h1> - </header> - {pages.map((page) => - "event" in page - ? <SlidePageEntry slide={page} key={page.uuid} /> - : <PostPageEntry post={page} key={page.uuid} /> - )} - </main> - <GlobalFooter config={config} /> - </body> - </PageLayout> - ); -} diff --git a/vhosts/blog/nuldoc-src/render.ts b/vhosts/blog/nuldoc-src/render.ts deleted file mode 100644 index fbad25ab..00000000 --- a/vhosts/blog/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/vhosts/blog/nuldoc-src/renderers/html.ts b/vhosts/blog/nuldoc-src/renderers/html.ts deleted file mode 100644 index 84b3ebaa..00000000 --- a/vhosts/blog/nuldoc-src/renderers/html.ts +++ /dev/null @@ -1,310 +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" }; - default: - throw new NuldocError(`[html.write] Unknown element name: ${name}`); - } -} - -function isInlineNode(n: Node): boolean { - if (n.kind === "text") { - 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") { - if (n.raw) { - return n.content; - } else { - return textNodeToHtmlText(n, ctx); - } - } 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 [...e.attributes.entries()] - .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/vhosts/blog/nuldoc-src/renderers/xml.ts b/vhosts/blog/nuldoc-src/renderers/xml.ts deleted file mode 100644 index 77cc1574..00000000 --- a/vhosts/blog/nuldoc-src/renderers/xml.ts +++ /dev/null @@ -1,130 +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") { - 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") { - if (n.raw) { - return n.content; - } else { - return textNodeToXmlText(n); - } - } 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 [...e.attributes.entries()] - .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/vhosts/blog/nuldoc-src/revision.ts b/vhosts/blog/nuldoc-src/revision.ts deleted file mode 100644 index a22b6bc4..00000000 --- a/vhosts/blog/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/vhosts/blog/nuldoc-src/slide/parse.ts b/vhosts/blog/nuldoc-src/slide/parse.ts deleted file mode 100644 index c5a89675..00000000 --- a/vhosts/blog/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/vhosts/blog/nuldoc-src/slide/slide.ts b/vhosts/blog/nuldoc-src/slide/slide.ts deleted file mode 100644 index 8fe99eab..00000000 --- a/vhosts/blog/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, - }; -} |
