diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-04-09 20:27:54 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-04-09 22:24:37 +0900 |
| commit | dbc3e0dfd893435f31cca39873f7ba9bf13b93a6 (patch) | |
| tree | 2cee4227ec6a995a9ddc99894dce604523833dc7 | |
| parent | bba1212ab46ed85c2ed3b646f2362bdbb1f45b63 (diff) | |
| download | nsfisis.dev-dbc3e0dfd893435f31cca39873f7ba9bf13b93a6.tar.gz nsfisis.dev-dbc3e0dfd893435f31cca39873f7ba9bf13b93a6.tar.zst nsfisis.dev-dbc3e0dfd893435f31cca39873f7ba9bf13b93a6.zip | |
feat(blog/nuldoc): change format of nuldoc builder from .ndoc to .dj
| -rw-r--r-- | vhosts/blog/deno.jsonc | 1 | ||||
| -rw-r--r-- | vhosts/blog/deno.lock | 5 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/commands/build.ts | 6 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/commands/new.ts | 14 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/djot/djot2ndoc.ts | 826 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/djot/document.ts (renamed from vhosts/blog/nuldoc-src/ndoc/document.ts) | 22 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/djot/parse.ts (renamed from vhosts/blog/nuldoc-src/ndoc/parse.ts) | 10 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/djot/to_html.ts (renamed from vhosts/blog/nuldoc-src/ndoc/to_html.ts) | 30 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/generators/post.ts | 4 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/pages/PostPage.tsx | 2 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/renderers/html.ts | 2 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/xml.ts | 265 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/xml_test.ts | 17 |
13 files changed, 887 insertions, 317 deletions
diff --git a/vhosts/blog/deno.jsonc b/vhosts/blog/deno.jsonc index b0550a5b..55c2fc2e 100644 --- a/vhosts/blog/deno.jsonc +++ b/vhosts/blog/deno.jsonc @@ -1,5 +1,6 @@ { "imports": { + "@djot/djot": "npm:@djot/djot@^0.3.2", "@std/assert": "jsr:@std/assert@^1.0.12", "@std/cli": "jsr:@std/cli@^1.0.15", "@std/fs": "jsr:@std/fs@^1.0.15", diff --git a/vhosts/blog/deno.lock b/vhosts/blog/deno.lock index 300594ba..f9eda6fb 100644 --- a/vhosts/blog/deno.lock +++ b/vhosts/blog/deno.lock @@ -16,6 +16,7 @@ "jsr:@std/path@^1.0.8": "1.0.8", "jsr:@std/streams@^1.0.9": "1.0.9", "jsr:@std/toml@^1.0.3": "1.0.3", + "npm:@djot/djot@~0.3.2": "0.3.2", "npm:shiki@^3.2.1": "3.2.1" }, "jsr": { @@ -85,6 +86,9 @@ } }, "npm": { + "@djot/djot@0.3.2": { + "integrity": "sha512-joMKR24B8rxueyFiJbpZAqEiypjvOyzTxzkhyr0q5mM/sUBaOD3unna/9IxtOotFugViyYlkIRaiXg3xM//zxg==" + }, "@shikijs/core@3.2.1": { "integrity": "sha512-FhsdxMWYu/C11sFisEp7FMGBtX/OSSbnXZDMBhGuUDBNTdsoZlMSgQv5f90rwvzWAdWIW6VobD+G3IrazxA6dQ==", "dependencies": [ @@ -549,6 +553,7 @@ "jsr:@std/http@^1.0.13", "jsr:@std/path@^1.0.8", "jsr:@std/toml@^1.0.3", + "npm:@djot/djot@~0.3.2", "npm:shiki@^3.2.1" ] } diff --git a/vhosts/blog/nuldoc-src/commands/build.ts b/vhosts/blog/nuldoc-src/commands/build.ts index 4702fd0a..52aca1f7 100644 --- a/vhosts/blog/nuldoc-src/commands/build.ts +++ b/vhosts/blog/nuldoc-src/commands/build.ts @@ -2,7 +2,7 @@ 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 { parseNulDocFile } from "../ndoc/parse.ts"; +import { parseDjotFile } from "../djot/parse.ts"; import { Page } from "../page.ts"; import { render } from "../render.ts"; import { dateToString } from "../revision.ts"; @@ -49,7 +49,7 @@ async function buildPostPages(config: Config): Promise<PostPage[]> { async function collectPostFiles(sourceDir: string): Promise<string[]> { const filePaths = []; - const globPattern = joinGlobs([sourceDir, "**", "*.ndoc"]); + const globPattern = joinGlobs([sourceDir, "**", "*.dj"]); for await (const entry of expandGlob(globPattern)) { filePaths.push(entry.path); } @@ -63,7 +63,7 @@ async function parsePosts( const posts = []; for (const postFile of postFiles) { posts.push( - await generatePostPage(await parseNulDocFile(postFile, config), config), + await generatePostPage(await parseDjotFile(postFile, config), config), ); } return posts; diff --git a/vhosts/blog/nuldoc-src/commands/new.ts b/vhosts/blog/nuldoc-src/commands/new.ts index e694ce20..651c59e6 100644 --- a/vhosts/blog/nuldoc-src/commands/new.ts +++ b/vhosts/blog/nuldoc-src/commands/new.ts @@ -50,7 +50,7 @@ OPTIONS: } function getFilename(type: "post" | "slide"): string { - return type === "post" ? "TODO.ndoc" : "TODO.toml"; + return type === "post" ? "TODO.dj" : "TODO.toml"; } function getDirPath(type: "post" | "slide"): string { @@ -73,14 +73,10 @@ tags = [ date = "${date}" remark = "公開" --- -<article> - <section id="TODO"> - <h>TODO</h> - <p> - TODO - </p> - </section> -</article> +{#TODO} +# TODO + +TODO `; } else { return `[slide] diff --git a/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts b/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts new file mode 100644 index 00000000..07071441 --- /dev/null +++ b/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts @@ -0,0 +1,826 @@ +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); + } + 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), + }; + } + + 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/ndoc/document.ts b/vhosts/blog/nuldoc-src/djot/document.ts index dfb6d03b..be9c08d5 100644 --- a/vhosts/blog/nuldoc-src/ndoc/document.ts +++ b/vhosts/blog/nuldoc-src/djot/document.ts @@ -1,9 +1,10 @@ +import { Doc as DjotDoc } from "@djot/djot"; import { join } from "@std/path"; +import { z } from "zod/mod.ts"; import { Config } from "../config.ts"; -import { NuldocError } from "../errors.ts"; +import { Element } from "../dom.ts"; import { Revision, stringToDate } from "../revision.ts"; -import { Element, findFirstChildElement } from "../dom.ts"; -import { z } from "zod/mod.ts"; +import { djot2ndoc } from "./djot2ndoc.ts"; export const PostMetadataSchema = z.object({ article: z.object({ @@ -32,25 +33,18 @@ export type Document = { revisions: Revision[]; }; -export function createNewDocumentFromRootElement( - root: Element, +export function createNewDocumentFromDjotDocument( + root: DjotDoc, meta: PostMetadata, sourceFilePath: string, config: Config, ): Document { - const article = findFirstChildElement(root, "article"); - if (!article) { - throw new NuldocError( - `[nuldoc.new] <article> element not found`, - ); - } - const cwd = Deno.cwd(); const contentDir = join(cwd, config.locations.contentDir); const link = sourceFilePath.replace(contentDir, "").replace(".xml", "/"); return { - root: root, - sourceFilePath: sourceFilePath, + root: djot2ndoc(root), + sourceFilePath, uuid: meta.article.uuid, link: link, title: meta.article.title, diff --git a/vhosts/blog/nuldoc-src/ndoc/parse.ts b/vhosts/blog/nuldoc-src/djot/parse.ts index 4bb96f4d..884ab154 100644 --- a/vhosts/blog/nuldoc-src/ndoc/parse.ts +++ b/vhosts/blog/nuldoc-src/djot/parse.ts @@ -1,15 +1,15 @@ +import { parse as parseDjot } from "@djot/djot"; import { parse as parseToml } from "@std/toml"; import { Config } from "../config.ts"; -import { parseXmlString } from "../xml.ts"; import { - createNewDocumentFromRootElement, + createNewDocumentFromDjotDocument, Document, PostMetadata, PostMetadataSchema, } from "./document.ts"; import toHtml from "./to_html.ts"; -export async function parseNulDocFile( +export async function parseDjotFile( filePath: string, config: Config, ): Promise<Document> { @@ -17,8 +17,8 @@ export async function parseNulDocFile( const fileContent = await Deno.readTextFile(filePath); const parts = fileContent.split(/^---$/m); const meta = parseMetadata(parts[1]); - const root = parseXmlString("<?xml ?>" + parts[2]); - const doc = createNewDocumentFromRootElement(root, meta, filePath, config); + const root = parseDjot(parts[2]); + const doc = createNewDocumentFromDjotDocument(root, meta, filePath, config); return await toHtml(doc); } catch (e) { if (e instanceof Error) { diff --git a/vhosts/blog/nuldoc-src/ndoc/to_html.ts b/vhosts/blog/nuldoc-src/djot/to_html.ts index a82f0333..5ee76023 100644 --- a/vhosts/blog/nuldoc-src/ndoc/to_html.ts +++ b/vhosts/blog/nuldoc-src/djot/to_html.ts @@ -22,6 +22,7 @@ export default async function toHtml(doc: Document): Promise<Document> { addAttributesToExternalLinkElement(doc); setDefaultLangAttribute(doc); traverseFootnotes(doc); + removeUnnecessaryParagraphNode(doc); await transformAndHighlightCodeBlockElement(doc); return doc; } @@ -266,6 +267,33 @@ function traverseFootnotes(doc: Document) { }); } +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; + } + if (child.children.length !== 1) { + continue; + } + const grandChild = child.children[0]; + if (grandChild.kind !== "element" || grandChild.name !== "p") { + continue; + } + child.children = grandChild.children; + } + }); +} + async function transformAndHighlightCodeBlockElement(doc: Document) { await forEachChildRecursivelyAsync(doc.root, async (n) => { if (n.kind !== "element" || n.name !== "codeblock") { @@ -274,7 +302,7 @@ async function transformAndHighlightCodeBlockElement(doc: Document) { const language = n.attributes.get("language") || "text"; const sourceCodeNode = n.children[0] as Text | RawHTML; - const sourceCode = sourceCodeNode.content; + const sourceCode = sourceCodeNode.content.trimEnd(); const highlighted = await codeToHtml(sourceCode, { lang: language in bundledLanguages ? language as BundledLanguage : "text", diff --git a/vhosts/blog/nuldoc-src/generators/post.ts b/vhosts/blog/nuldoc-src/generators/post.ts index ae96c573..0e2a9553 100644 --- a/vhosts/blog/nuldoc-src/generators/post.ts +++ b/vhosts/blog/nuldoc-src/generators/post.ts @@ -2,7 +2,7 @@ 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 "../ndoc/document.ts"; +import { Document } from "../djot/document.ts"; import { Page } from "../page.ts"; import { Date, Revision } from "../revision.ts"; @@ -44,7 +44,7 @@ export async function generatePostPage( const cwd = Deno.cwd(); const contentDir = join(cwd, config.locations.contentDir); const destFilePath = join( - doc.sourceFilePath.replace(contentDir, "").replace(".ndoc", ""), + doc.sourceFilePath.replace(contentDir, "").replace(".dj", ""), "index.html", ); return { diff --git a/vhosts/blog/nuldoc-src/pages/PostPage.tsx b/vhosts/blog/nuldoc-src/pages/PostPage.tsx index e6aa83aa..37755ae3 100644 --- a/vhosts/blog/nuldoc-src/pages/PostPage.tsx +++ b/vhosts/blog/nuldoc-src/pages/PostPage.tsx @@ -3,7 +3,7 @@ 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 "../ndoc/document.ts"; +import { Document } from "../djot/document.ts"; import { dateToString } from "../revision.ts"; import { getPostPublishedDate } from "../generators/post.ts"; diff --git a/vhosts/blog/nuldoc-src/renderers/html.ts b/vhosts/blog/nuldoc-src/renderers/html.ts index 19e6af08..ec77eb4d 100644 --- a/vhosts/blog/nuldoc-src/renderers/html.ts +++ b/vhosts/blog/nuldoc-src/renderers/html.ts @@ -89,6 +89,8 @@ function getDtd(name: string): Dtd { return { type: "inline" }; case "strong": return { type: "inline" }; + case "sub": + return { type: "inline" }; case "sup": return { type: "inline" }; case "table": diff --git a/vhosts/blog/nuldoc-src/xml.ts b/vhosts/blog/nuldoc-src/xml.ts deleted file mode 100644 index 9f53ef8c..00000000 --- a/vhosts/blog/nuldoc-src/xml.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { Element, Node, Text } from "./dom.ts"; -import { XmlParseError } from "./errors.ts"; - -export function parseXmlString(source: string): Element { - return parse({ source: source, index: 0 }); -} - -type Parser = { - source: string; - index: number; -}; - -function parse(p: Parser): Element { - parseXmlDeclaration(p); - skipWhitespaces(p); - const e = parseXmlElement(p); - const root: Element = { - kind: "element", - name: "__root__", - attributes: new Map(), - children: [e], - }; - return root; -} - -function parseXmlDeclaration(p: Parser) { - expect(p, "<?xml "); - skipTo(p, "?>"); - next(p, 2); -} - -function parseXmlElement(p: Parser): Element { - const { name, attributes, closed } = parseStartTag(p); - if (closed) { - return { - kind: "element", - name: name, - attributes: attributes, - children: [], - }; - } - const children = parseChildNodes(p); - parseEndTag(p, name); - - const thisElement: Element = { - kind: "element", - name: name, - attributes: attributes, - children: children, - }; - return thisElement; -} - -function parseChildNodes(p: Parser): Node[] { - const nodes = []; - while (true) { - const c = peek(p); - const c2 = peekN(p, 2); - const c3 = peekN(p, 3); - if (c === "<") { - if (c2 === "/") { - break; - } else if (c2 === "!") { - if (c3 === "[") { - // <![CDATA[ - nodes.push(parseCdata(p)); - } else { - // <!-- - skipComment(p); - } - } else { - nodes.push(parseXmlElement(p)); - } - } else { - nodes.push(parseTextNode(p)); - } - } - return nodes; -} - -function parseTextNode(p: Parser): Text { - const content = skipTo(p, "<"); - return { - kind: "text", - content: replaceEntityReferences(content), - raw: false, - }; -} - -function parseCdata(p: Parser): Text { - expect(p, "<![CDATA["); - const content = skipTo(p, "]]>"); - next(p, "]]>".length); - return { - kind: "text", - content: formatCdata(content), - raw: false, - }; -} - -function skipComment(p: Parser) { - expect(p, "<!--"); - skipTo(p, "-->"); - next(p, "-->".length); -} - -function formatCdata(s: string): string { - // <![CDATA[ - // foo - // bar - // baz - // ]]> - // => "foo\n bar\nbaz" - s = s.replace(/^\n(.*)\n *$/s, "$1"); - const ls = s.split("\n"); - const n = Math.min( - ...ls.filter((l) => l !== "").map((l) => - l.match(/^( *)/)?.[0]?.length ?? 0 - ), - ); - let z = ""; - for (const p of s.split("\n")) { - z += p.slice(n) + "\n"; - } - return z.slice(0, -1); -} - -function parseStartTag( - p: Parser, -): { name: string; attributes: Map<string, string>; closed: boolean } { - expect(p, "<"); - const name = parseIdentifier(p); - skipWhitespaces(p); - if (peek(p) === "/") { - expect(p, "/>"); - return { name: name, attributes: new Map(), closed: true }; - } - if (peek(p) === ">") { - next(p); - return { name: name, attributes: new Map(), closed: false }; - } - const attributes = new Map(); - while (peek(p) !== ">" && peek(p) !== "/") { - const { name, value } = parseAttribute(p); - attributes.set(name, value); - } - let closed = false; - if (peek(p) === "/") { - next(p); - closed = true; - } - expect(p, ">"); - return { name: name, attributes: attributes, closed: closed }; -} - -function parseEndTag(p: Parser, name: string) { - expect(p, `</${name}>`); -} - -function parseAttribute(p: Parser): { name: string; value: string } { - skipWhitespaces(p); - let name = parseIdentifier(p); - if (peek(p) === ":") { - next(p); - const name2 = parseIdentifier(p); - name += ":" + name2; - } - expect(p, "="); - const value = parseQuotedString(p); - skipWhitespaces(p); - return { name: name, value: replaceEntityReferences(value) }; -} - -function parseQuotedString(p: Parser): string { - expect(p, '"'); - const content = skipTo(p, '"'); - next(p); - return content; -} - -function parseIdentifier(p: Parser): string { - let id = ""; - while (p.index < p.source.length) { - const c = peek(p); - if (!c || !/[A-Za-z]/.test(c)) { - break; - } - id += c; - next(p); - } - return id; -} - -function expect(p: Parser, expected: string) { - let actual = ""; - for (let i = 0; i < expected.length; i++) { - actual += peek(p); - next(p); - } - if (actual !== expected) { - throw new XmlParseError( - `[parse.expect] expected ${expected}, but actually got ${ - escapeForHuman(actual) - } (pos: ${p.index})`, - ); - } -} - -function skipTo(p: Parser, delimiter: string): string { - const indexStart = p.index; - let i = 0; - while (i < delimiter.length) { - if (peek(p) === delimiter[i]) { - i++; - } else { - i = 0; - } - next(p); - } - back(p, delimiter.length); - return p.source.substring(indexStart, p.index); -} - -function skipWhitespaces(p: Parser) { - while (p.index < p.source.length) { - const c = peek(p); - if (!c || !/[ \n\t]/.test(c)) { - break; - } - next(p); - } -} - -function peek(p: Parser): string | null { - return peekN(p, 1); -} - -function peekN(p: Parser, n: number): string | null { - return (p.index + n - 1 < p.source.length) ? p.source[p.index + n - 1] : null; -} - -function next(p: Parser, n = 1) { - p.index += n; -} - -function back(p: Parser, n = 1) { - p.index -= n; -} - -function replaceEntityReferences(s: string): string { - return s - .replaceAll(/&/g, "&") - .replaceAll(/</g, "<") - .replaceAll(/>/g, ">") - .replaceAll(/'/g, "'") - .replaceAll(/"/g, '"'); -} - -function escapeForHuman(s: string): string { - // support more characters? - return s - .replaceAll("\n", "\\n") - .replaceAll("\t", "\\t") - .replaceAll("\r", "\\r"); -} diff --git a/vhosts/blog/nuldoc-src/xml_test.ts b/vhosts/blog/nuldoc-src/xml_test.ts deleted file mode 100644 index c423800e..00000000 --- a/vhosts/blog/nuldoc-src/xml_test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { parseXmlString } from "./xml.ts"; - -Deno.test("Parse XML", () => { - assertEquals( - "__root__", - parseXmlString( - `<?xml version="1.0" encoding="UTF-8"?> -<hoge> - <piyo> - <!-- comment --> - </piyo> -</hoge> -`, - ).name, - ); -}); |
