summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-04-09 20:27:54 +0900
committernsfisis <nsfisis@gmail.com>2025-04-09 22:24:37 +0900
commitdbc3e0dfd893435f31cca39873f7ba9bf13b93a6 (patch)
tree2cee4227ec6a995a9ddc99894dce604523833dc7
parentbba1212ab46ed85c2ed3b646f2362bdbb1f45b63 (diff)
downloadnsfisis.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.jsonc1
-rw-r--r--vhosts/blog/deno.lock5
-rw-r--r--vhosts/blog/nuldoc-src/commands/build.ts6
-rw-r--r--vhosts/blog/nuldoc-src/commands/new.ts14
-rw-r--r--vhosts/blog/nuldoc-src/djot/djot2ndoc.ts826
-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.ts4
-rw-r--r--vhosts/blog/nuldoc-src/pages/PostPage.tsx2
-rw-r--r--vhosts/blog/nuldoc-src/renderers/html.ts2
-rw-r--r--vhosts/blog/nuldoc-src/xml.ts265
-rw-r--r--vhosts/blog/nuldoc-src/xml_test.ts17
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(/&amp;/g, "&")
- .replaceAll(/&lt;/g, "<")
- .replaceAll(/&gt;/g, ">")
- .replaceAll(/&apos;/g, "'")
- .replaceAll(/&quot;/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,
- );
-});