From dbc3e0dfd893435f31cca39873f7ba9bf13b93a6 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Wed, 9 Apr 2025 20:27:54 +0900 Subject: feat(blog/nuldoc): change format of nuldoc builder from .ndoc to .dj --- vhosts/blog/deno.jsonc | 1 + vhosts/blog/deno.lock | 5 + vhosts/blog/nuldoc-src/commands/build.ts | 6 +- vhosts/blog/nuldoc-src/commands/new.ts | 14 +- vhosts/blog/nuldoc-src/djot/djot2ndoc.ts | 826 ++++++++++++++++++++++++++++++ vhosts/blog/nuldoc-src/djot/document.ts | 60 +++ vhosts/blog/nuldoc-src/djot/parse.ts | 33 ++ vhosts/blog/nuldoc-src/djot/to_html.ts | 320 ++++++++++++ vhosts/blog/nuldoc-src/generators/post.ts | 4 +- vhosts/blog/nuldoc-src/ndoc/document.ts | 66 --- vhosts/blog/nuldoc-src/ndoc/parse.ts | 33 -- vhosts/blog/nuldoc-src/ndoc/to_html.ts | 292 ----------- vhosts/blog/nuldoc-src/pages/PostPage.tsx | 2 +- vhosts/blog/nuldoc-src/renderers/html.ts | 2 + vhosts/blog/nuldoc-src/xml.ts | 265 ---------- vhosts/blog/nuldoc-src/xml_test.ts | 17 - 16 files changed, 1258 insertions(+), 688 deletions(-) create mode 100644 vhosts/blog/nuldoc-src/djot/djot2ndoc.ts create mode 100644 vhosts/blog/nuldoc-src/djot/document.ts create mode 100644 vhosts/blog/nuldoc-src/djot/parse.ts create mode 100644 vhosts/blog/nuldoc-src/djot/to_html.ts delete mode 100644 vhosts/blog/nuldoc-src/ndoc/document.ts delete mode 100644 vhosts/blog/nuldoc-src/ndoc/parse.ts delete mode 100644 vhosts/blog/nuldoc-src/ndoc/to_html.ts delete mode 100644 vhosts/blog/nuldoc-src/xml.ts delete mode 100644 vhosts/blog/nuldoc-src/xml_test.ts (limited to 'vhosts/blog') 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 { async function collectPostFiles(sourceDir: string): Promise { 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 = "公開" --- -
-
- TODO -

- TODO -

-
-
+{#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 = { + "->": "→", + "<-": "←", + "<->": "↔", + "=>": "⇒", + "<=": "⇐", + "<=>": "⇔", + "--": "–", // 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 = { + "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, +): Map { + const result = new Map(); + 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 new file mode 100644 index 00000000..be9c08d5 --- /dev/null +++ b/vhosts/blog/nuldoc-src/djot/document.ts @@ -0,0 +1,60 @@ +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; + +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 new file mode 100644 index 00000000..884ab154 --- /dev/null +++ b/vhosts/blog/nuldoc-src/djot/parse.ts @@ -0,0 +1,33 @@ +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 { + try { + const fileContent = await Deno.readTextFile(filePath); + const parts = fileContent.split(/^---$/m); + const meta = parseMetadata(parts[1]); + const root = parseDjot(parts[2]); + 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 new file mode 100644 index 00000000..5ee76023 --- /dev/null +++ b/vhosts/blog/nuldoc-src/djot/to_html.ts @@ -0,0 +1,320 @@ +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 { + removeUnnecessaryTextNode(doc); + transformLinkLikeToAnchorElement(doc); + transformSectionIdAttribute(doc); + setSectionTitleAnchor(doc); + transformSectionTitleElement(doc); + transformNoteElement(doc); + addAttributesToExternalLinkElement(doc); + setDefaultLangAttribute(doc); + traverseFootnotes(doc); + removeUnnecessaryParagraphNode(doc); + await transformAndHighlightCodeBlockElement(doc); + return doc; +} + +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(); + + 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] element must be inside
", + ); + } + 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 labelElement: Element = { + kind: "element", + name: "div", + attributes: new Map([["class", "admonition-label"]]), + children: [{ + kind: "text", + content: "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 setDefaultLangAttribute(_doc: Document) { + // TODO + // if (!e.attributes.has("lang")) { + // e.attributes.set("lang", "ja-JP"); + // } +} + +function traverseFootnotes(doc: Document) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element" || n.name !== "footnote") { + return; + } + + // TODO + // x + // + // [1] + // + //
+ // 1. RAS syndrome + //
+ n.name = "span"; + 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; + } + 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") { + return; + } + + const language = n.attributes.get("language") || "text"; + 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", + }, + }); + + sourceCodeNode.content = highlighted; + sourceCodeNode.raw = true; + n.name = "div"; + n.attributes.set("class", "codeblock"); + }); +} 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/ndoc/document.ts b/vhosts/blog/nuldoc-src/ndoc/document.ts deleted file mode 100644 index dfb6d03b..00000000 --- a/vhosts/blog/nuldoc-src/ndoc/document.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { join } from "@std/path"; -import { Config } from "../config.ts"; -import { NuldocError } from "../errors.ts"; -import { Revision, stringToDate } from "../revision.ts"; -import { Element, findFirstChildElement } from "../dom.ts"; -import { z } from "zod/mod.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; - -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 createNewDocumentFromRootElement( - root: Element, - meta: PostMetadata, - sourceFilePath: string, - config: Config, -): Document { - const article = findFirstChildElement(root, "article"); - if (!article) { - throw new NuldocError( - `[nuldoc.new]
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, - 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/ndoc/parse.ts b/vhosts/blog/nuldoc-src/ndoc/parse.ts deleted file mode 100644 index 4bb96f4d..00000000 --- a/vhosts/blog/nuldoc-src/ndoc/parse.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { parse as parseToml } from "@std/toml"; -import { Config } from "../config.ts"; -import { parseXmlString } from "../xml.ts"; -import { - createNewDocumentFromRootElement, - Document, - PostMetadata, - PostMetadataSchema, -} from "./document.ts"; -import toHtml from "./to_html.ts"; - -export async function parseNulDocFile( - filePath: string, - config: Config, -): Promise { - try { - const fileContent = await Deno.readTextFile(filePath); - const parts = fileContent.split(/^---$/m); - const meta = parseMetadata(parts[1]); - const root = parseXmlString("" + parts[2]); - const doc = createNewDocumentFromRootElement(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/ndoc/to_html.ts b/vhosts/blog/nuldoc-src/ndoc/to_html.ts deleted file mode 100644 index a82f0333..00000000 --- a/vhosts/blog/nuldoc-src/ndoc/to_html.ts +++ /dev/null @@ -1,292 +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 { - removeUnnecessaryTextNode(doc); - transformLinkLikeToAnchorElement(doc); - transformSectionIdAttribute(doc); - setSectionTitleAnchor(doc); - transformSectionTitleElement(doc); - transformNoteElement(doc); - addAttributesToExternalLinkElement(doc); - setDefaultLangAttribute(doc); - traverseFootnotes(doc); - await transformAndHighlightCodeBlockElement(doc); - return doc; -} - -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(); - - 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] element must be inside
", - ); - } - 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 labelElement: Element = { - kind: "element", - name: "div", - attributes: new Map([["class", "admonition-label"]]), - children: [{ - kind: "text", - content: "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 setDefaultLangAttribute(_doc: Document) { - // TODO - // if (!e.attributes.has("lang")) { - // e.attributes.set("lang", "ja-JP"); - // } -} - -function traverseFootnotes(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "footnote") { - return; - } - - // TODO - // x - // - // [1] - // - //
- // 1. RAS syndrome - //
- n.name = "span"; - n.children = []; - }); -} - -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 sourceCodeNode = n.children[0] as Text | RawHTML; - const sourceCode = sourceCodeNode.content; - - const highlighted = await codeToHtml(sourceCode, { - lang: language in bundledLanguages ? language as BundledLanguage : "text", - theme: "github-light", - colorReplacements: { - "#fff": "#f5f5f5", - }, - }); - - sourceCodeNode.content = highlighted; - sourceCodeNode.raw = true; - n.name = "div"; - n.attributes.set("class", "codeblock"); - }); -} 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, ""); - 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 === "[") { - // "); - next(p, "]]>".length); - return { - kind: "text", - content: formatCdata(content), - raw: false, - }; -} - -function skipComment(p: Parser) { - expect(p, ""); - next(p, "-->".length); -} - -function formatCdata(s: string): string { - // - // => "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; 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, ``); -} - -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( - ` - - - - - -`, - ).name, - ); -}); -- cgit v1.2.3-70-g09d2