diff options
Diffstat (limited to 'services/blog/nuldoc-src')
| -rw-r--r-- | services/blog/nuldoc-src/djot/djot2ndoc.ts | 636 | ||||
| -rw-r--r-- | services/blog/nuldoc-src/djot/to_html.ts | 233 | ||||
| -rw-r--r-- | services/blog/nuldoc-src/dom.ts | 68 | ||||
| -rw-r--r-- | services/blog/nuldoc-src/jsx/render.ts | 12 | ||||
| -rw-r--r-- | services/blog/nuldoc-src/renderers/html.ts | 14 | ||||
| -rw-r--r-- | services/blog/nuldoc-src/renderers/xml.ts | 14 |
6 files changed, 358 insertions, 619 deletions
diff --git a/services/blog/nuldoc-src/djot/djot2ndoc.ts b/services/blog/nuldoc-src/djot/djot2ndoc.ts index 90b1289c..2abb6ce3 100644 --- a/services/blog/nuldoc-src/djot/djot2ndoc.ts +++ b/services/blog/nuldoc-src/djot/djot2ndoc.ts @@ -46,7 +46,7 @@ import { Url as DjotUrl, Verbatim as DjotVerbatim, } from "@djot/djot"; -import { Element, Node } from "../dom.ts"; +import { elem, Element, Node, rawHTML, text } from "../dom.ts"; function processBlock(node: DjotBlock): Element { switch (node.tag) { @@ -80,140 +80,93 @@ function processBlock(node: DjotBlock): Element { } function processSection(node: DjotSection): Element { - return { - kind: "element", - name: "section", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; + return elem( + "section", + node.attributes, + ...node.children.map(processBlock), + ); } function processPara(node: DjotPara): Element { - return { - kind: "element", - name: "p", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "p", + node.attributes, + ...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), - }; + return elem("h", node.attributes, ...node.children.map(processInline)); } function processThematicBreak(node: DjotThematicBreak): Element { - return { - kind: "element", - name: "hr", - attributes: convertAttributes(node.attributes), - children: [], - }; + return elem("hr", node.attributes); } function processBlockQuote(node: DjotBlockQuote): Element { - return { - kind: "element", - name: "blockquote", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; + return elem( + "blockquote", + node.attributes, + ...node.children.map(processBlock), + ); } function processCodeBlock(node: DjotCodeBlock): Element { - const attributes = convertAttributes(node.attributes); + const attributes = node.attributes || {}; if (node.lang) { - attributes.set("language", node.lang); + attributes.language = node.lang; } if (node.attributes?.filename) { - attributes.set("filename", node.attributes.filename); + attributes.filename = node.attributes.filename; } if (node.attributes?.numbered) { - attributes.set("numbered", "true"); + attributes.numbered = "true"; } - return { - kind: "element", - name: "codeblock", - attributes, - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; + return elem("codeblock", attributes, text(node.text)); } 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), - }; + const attributes = node.attributes || {}; + attributes.__tight = node.tight ? "true" : "false"; + return elem("ul", attributes, ...node.children.map(processListItem)); } function processOrderedList(node: DjotOrderedList): Element { - const attributes = convertAttributes(node.attributes); - attributes.set("--tight", node.tight ? "true" : "false"); + const attributes = node.attributes || {}; + attributes.__tight = node.tight ? "true" : "false"; if (node.start !== undefined && node.start !== 1) { - attributes.set("start", node.start.toString()); + attributes.start = node.start.toString(); } - return { - kind: "element", - name: "ol", - attributes, - children: node.children.map(processListItem), - }; + return elem("ol", attributes, ...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), - }; + const attributes = node.attributes || {}; + attributes.type = "task"; + attributes.__tight = node.tight ? "true" : "false"; + return elem("ul", attributes, ...node.children.map(processTaskListItem)); } function processListItem(node: DjotListItem): Element { - return { - kind: "element", - name: "li", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; + return elem( + "li", + node.attributes, + ...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), - }; + const attributes = node.attributes || {}; + attributes.checked = node.checkbox === "checked" ? "true" : "false"; + return elem("li", attributes, ...node.children.map(processBlock)); } function processDefinitionList(node: DjotDefinitionList): Element { - return { - kind: "element", - name: "dl", - attributes: convertAttributes(node.attributes), - children: node.children.flatMap(processDefinitionListItem), - }; + return elem( + "dl", + node.attributes, + ...node.children.flatMap(processDefinitionListItem), + ); } function processDefinitionListItem(node: DjotDefinitionListItem): Element[] { @@ -224,41 +177,33 @@ function processDefinitionListItem(node: DjotDefinitionListItem): Element[] { } function processTerm(node: DjotTerm): Element { - return { - kind: "element", - name: "dt", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "dt", + node.attributes, + ...node.children.map(processInline), + ); } function processDefinition(node: DjotDefinition): Element { - return { - kind: "element", - name: "dd", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; + return elem( + "dd", + node.attributes, + ...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: [], - }; + const tableElement = elem("table", node.attributes); // 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), - }; + const caption = elem( + "caption", + undefined, + ...node.children[0].children.map(processInline), + ); tableElement.children.push(caption); } @@ -270,26 +215,22 @@ function processTable(node: DjotTable): Element { 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), - }; - + const rowElement = elem( + "tr", + row.attributes, + ...row.children.map((cell) => { + const cellAttributes = cell.attributes || {}; // Set alignment attribute if needed if (cell.align !== "default") { - cellElement.attributes.set("align", cell.align); + cellAttributes.align = cell.align; } - - return cellElement; + return elem( + cell.head ? "th" : "td", + cellAttributes, + ...cell.children.map(processInline), + ); }), - }; + ); if (row.head) { headerRows.push(rowElement); @@ -301,21 +242,11 @@ function processTable(node: DjotTable): Element { // Add thead and tbody if needed if (headerRows.length > 0) { - tableElement.children.push({ - kind: "element", - name: "thead", - attributes: new Map(), - children: headerRows, - }); + tableElement.children.push(elem("thead", undefined, ...headerRows)); } if (bodyRows.length > 0) { - tableElement.children.push({ - kind: "element", - name: "tbody", - attributes: new Map(), - children: bodyRows, - }); + tableElement.children.push(elem("tbody", undefined, ...bodyRows)); } return tableElement; @@ -377,80 +308,49 @@ function processInline(node: DjotInline): Node { } function processStr(node: DjotStr): Node { - return { - kind: "text", - content: node.text, - raw: false, - }; + return text(node.text); } function processSoftBreak(_node: DjotSoftBreak): Node { - return { - kind: "text", - content: "\n", - raw: false, - }; + return text("\n"); } function processHardBreak(_node: DjotHardBreak): Node { - return { - kind: "element", - name: "br", - attributes: new Map(), - children: [], - }; + return elem("br"); } function processVerbatim(node: DjotVerbatim): Element { - return { - kind: "element", - name: "code", - attributes: convertAttributes(node.attributes), - children: [ - { - kind: "text", - content: node.text, - raw: false, - }, - ], - }; + return elem("code", node.attributes, text(node.text)); } function processEmph(node: DjotEmph): Element { - return { - kind: "element", - name: "em", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "em", + node.attributes, + ...node.children.map(processInline), + ); } function processStrong(node: DjotStrong): Element { - return { - kind: "element", - name: "strong", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "strong", + node.attributes, + ...node.children.map(processInline), + ); } function processLink(node: DjotLink): Element { - const attributes = convertAttributes(node.attributes); + const attributes = node.attributes || {}; if (node.destination !== undefined) { - attributes.set("href", node.destination); + attributes.href = node.destination; } - return { - kind: "element", - name: "a", - attributes, - children: node.children.map(processInline), - }; + return elem("a", attributes, ...node.children.map(processInline)); } function processImage(node: DjotImage): Element { - const attributes = convertAttributes(node.attributes); + const attributes = node.attributes || {}; if (node.destination !== undefined) { - attributes.set("src", node.destination); + attributes.src = node.destination; } // Alt text is derived from children in Djot @@ -464,157 +364,105 @@ function processImage(node: DjotImage): Element { .join(""); if (alt) { - attributes.set("alt", alt); + attributes.alt = alt; } - return { - kind: "element", - name: "img", - attributes, - children: [], - }; + return elem("img", attributes); } function processMark(node: DjotMark): Element { - return { - kind: "element", - name: "mark", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "mark", + node.attributes, + ...node.children.map(processInline), + ); } function processSuperscript(node: DjotSuperscript): Element { - return { - kind: "element", - name: "sup", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "sup", + node.attributes, + ...node.children.map(processInline), + ); } function processSubscript(node: DjotSubscript): Element { - return { - kind: "element", - name: "sub", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "sub", + node.attributes, + ...node.children.map(processInline), + ); } function processInsert(node: DjotInsert): Element { - return { - kind: "element", - name: "ins", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "ins", + node.attributes, + ...node.children.map(processInline), + ); } function processDelete(node: DjotDelete): Element { - return { - kind: "element", - name: "del", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "del", + node.attributes, + ...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, - }, - ], - }; + return elem("email", node.attributes, text(node.text)); } function processFootnoteReference(node: DjotFootnoteReference): Element { - return { - kind: "element", - name: "footnoteref", - attributes: new Map([["reference", node.text]]), - children: [], - }; + return elem("footnoteref", { reference: node.text }); } 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, - }, - ], - }; + return elem( + "a", + { + href: node.text, + ...node.attributes, + }, + text(node.text), + ); } function processSpan(node: DjotSpan): Element { - return { - kind: "element", - name: "span", - attributes: convertAttributes(node.attributes), - children: node.children.map(processInline), - }; + return elem( + "span", + node.attributes, + ...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, - }, - ], - }; + return elem( + "span", + { + class: "math inline", + ...node.attributes, + }, + text(node.text), + ); } 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, - }, - ], - }; + return elem( + "div", + { + class: "math display", + ...node.attributes, + }, + text(node.text), + ); } function processNonBreakingSpace(_node: DjotNonBreakingSpace): Node { - return { - kind: "text", - content: "\u00A0", // Unicode non-breaking space - raw: false, - }; + return text("\u00A0"); // Unicode non-breaking space } function processSymb(node: DjotSymb): Node { @@ -634,170 +482,98 @@ function processSymb(node: DjotSymb): Node { const symbolText = symbolMap[node.alias] || node.alias; - return { - kind: "text", - content: symbolText, - raw: false, - }; + return text(symbolText); } 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, - }; + return rawHTML(node.text); } // For other formats, just return as text - return { - kind: "text", - content: node.text, - raw: false, - }; + return text(node.text); } function processDoubleQuoted(node: DjotDoubleQuoted): Node { const children = node.children.map(processInline); - const attributes = convertAttributes(node.attributes); + const attributes = node.attributes || {}; if ( children.length === 1 && children[0].kind === "text" && - attributes.size === 0 + Object.keys(attributes).length === 0 ) { const content = children[0].content; - return { - kind: "text", - content: `\u201C${content}\u201D`, - raw: false, - }; + return text(`\u201C${content}\u201D`); } else { - return { - kind: "element", - name: "span", - attributes: convertAttributes(node.attributes), - children, - }; + return elem("span", node.attributes, ...children); } } function processSingleQuoted(node: DjotSingleQuoted): Node { const children = node.children.map(processInline); - const attributes = convertAttributes(node.attributes); + const attributes = node.attributes || {}; if ( children.length === 1 && children[0].kind === "text" && - attributes.size === 0 + Object.keys(attributes).length === 0 ) { const content = children[0].content; - return { - kind: "text", - content: `\u2018${content}\u2019`, - raw: false, - }; + return text(`\u2018${content}\u2019`); } else { - return { - kind: "element", - name: "span", - attributes: convertAttributes(node.attributes), - children, - }; + return elem("span", 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", // – + 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, - }; + return text(punctuationMap[node.type] || node.text); } 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 elem( + "note", + node.attributes, + ...node.children.map(processBlock), + ); } if (node.attributes?.class === "edit") { delete node.attributes.class; - return { - kind: "element", - name: "note", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; + return elem( + "note", + node.attributes, + ...node.children.map(processBlock), + ); } - return { - kind: "element", - name: "div", - attributes: convertAttributes(node.attributes), - children: node.children.map(processBlock), - }; + return elem( + "div", + node.attributes, + ...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, - }, - ], - }; + return elem("div", { class: "raw-html" }, rawHTML(node.text)); } // 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; + return elem("pre", { "data-format": node.format }, text(node.text)); } export function djot2ndoc(doc: DjotDoc): Element { @@ -808,35 +584,19 @@ export function djot2ndoc(doc: DjotDoc): Element { // 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: [], - }; + const footnoteSection = elem("section", { class: "footnotes" }); 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), - }; + const footnoteElement = elem( + "footnote", + { id }, + ...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, - }], - }; + return elem("__root__", undefined, elem("article", undefined, ...children)); } diff --git a/services/blog/nuldoc-src/djot/to_html.ts b/services/blog/nuldoc-src/djot/to_html.ts index 5d461ad9..c4939d5b 100644 --- a/services/blog/nuldoc-src/djot/to_html.ts +++ b/services/blog/nuldoc-src/djot/to_html.ts @@ -3,14 +3,19 @@ import { Document, TocEntry } from "./document.ts"; import { NuldocError } from "../errors.ts"; import { addClass, + elem, Element, forEachChild, forEachChildRecursively, forEachChildRecursivelyAsync, + forEachElementOfType, innerText, Node, + processTextNodesInElement, RawHTML, + rawHTML, Text, + text, } from "../dom.ts"; export default async function toHtml(doc: Document): Promise<Document> { @@ -41,15 +46,11 @@ function mergeConsecutiveTextNodes(doc: Document) { let currentTextContent = ""; for (const child of n.children) { - if (child.kind === "text" && !child.raw) { + if (child.kind === "text") { currentTextContent += child.content; } else { if (currentTextContent !== "") { - newChildren.push({ - kind: "text", - content: currentTextContent, - raw: false, - }); + newChildren.push(text(currentTextContent)); currentTextContent = ""; } newChildren.push(child); @@ -57,11 +58,7 @@ function mergeConsecutiveTextNodes(doc: Document) { } if (currentTextContent !== "") { - newChildren.push({ - kind: "text", - content: currentTextContent, - raw: false, - }); + newChildren.push(text(currentTextContent)); } n.children = newChildren; @@ -106,32 +103,23 @@ function transformLinkLikeToAnchorElement(doc: Document) { return; } - const newChildren: Node[] = []; - for (const child of n.children) { - if (child.kind !== "text") { - newChildren.push(child); - continue; - } - let restContent = child.content; + processTextNodesInElement(n, (content) => { + const nodes: Node[] = []; + let restContent = content; while (restContent !== "") { const match = /^(.*?)(https?:\/\/[^ \n]+)(.*)$/s.exec(restContent); if (!match) { - newChildren.push({ kind: "text", content: restContent, raw: false }); + nodes.push(text(restContent)); 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 }], - }); + nodes.push(text(prefix)); + nodes.push(elem("a", { href: url }, text(url))); restContent = suffix; } - } - n.children = newChildren; + return nodes; + }); }); } @@ -145,7 +133,7 @@ function transformSectionIdAttribute(doc: Document) { } if (n.name === "section") { - const idAttr = n.attributes.get("id"); + const idAttr = n.attributes.id; if (!idAttr) { return; } @@ -164,7 +152,7 @@ function transformSectionIdAttribute(doc: Document) { } usedIds.add(newId); - n.attributes.set("id", newId); + n.attributes.id = newId; sectionStack.push(idAttr); forEachChild(n, processNode); @@ -199,14 +187,9 @@ function setSectionTitleAnchor(doc: Document) { "[nuldoc.tohtml] <h> element must be inside <section>", ); } - const sectionId = currentSection.attributes.get("id"); - const aElement: Element = { - kind: "element", - name: "a", - attributes: new Map(), - children: c.children, - }; - aElement.attributes.set("href", `#${sectionId}`); + const sectionId = currentSection.attributes.id; + const aElement = elem("a", undefined, ...c.children); + aElement.attributes.href = `#${sectionId}`; c.children = [aElement]; } }; @@ -222,7 +205,7 @@ function transformSectionTitleElement(doc: Document) { if (c.name === "section") { sectionLevel += 1; - c.attributes.set("--section-level", sectionLevel.toString()); + c.attributes.__sectionLevel = sectionLevel.toString(); } forEachChild(c, g); if (c.name === "section") { @@ -236,53 +219,35 @@ function transformSectionTitleElement(doc: Document) { } function transformNoteElement(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "note") { - return; - } - - const editatAttr = n.attributes?.get("editat"); - const operationAttr = n.attributes?.get("operation"); + forEachElementOfType(doc.root, "note", (n) => { + const editatAttr = n.attributes?.editat; + const operationAttr = n.attributes?.operation; const isEditBlock = editatAttr && operationAttr; - const labelElement: Element = { - kind: "element", - name: "div", - attributes: new Map([["class", "admonition-label"]]), - children: [{ - kind: "text", - content: isEditBlock ? `${editatAttr} ${operationAttr}` : "NOTE", - raw: false, - }], - }; - const contentElement: Element = { - kind: "element", - name: "div", - attributes: new Map([["class", "admonition-content"]]), - children: n.children, - }; + const labelElement = elem( + "div", + { class: "admonition-label" }, + text(isEditBlock ? `${editatAttr} ${operationAttr}` : "NOTE"), + ); + const contentElement = elem( + "div", + { class: "admonition-content" }, + ...n.children, + ); n.name = "div"; addClass(n, "admonition"); - n.children = [ - labelElement, - contentElement, - ]; + 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") ?? ""; + forEachElementOfType(doc.root, "a", (n) => { + const href = n.attributes.href ?? ""; if (!href.startsWith("http")) { return; } - n.attributes - .set("target", "_blank") - .set("rel", "noreferrer"); + n.attributes.target = "_blank"; + n.attributes.rel = "noreferrer"; }); } @@ -290,12 +255,8 @@ function traverseFootnotes(doc: Document) { let footnoteCounter = 0; const footnoteMap = new Map<string, number>(); - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "footnoteref") { - return; - } - - const reference = n.attributes.get("reference"); + forEachElementOfType(doc.root, "footnoteref", (n) => { + const reference = n.attributes.reference; if (!reference) { return; } @@ -309,34 +270,23 @@ function traverseFootnotes(doc: Document) { } n.name = "sup"; - n.attributes.delete("reference"); - n.attributes.set("class", "footnote"); + delete n.attributes.reference; + n.attributes.class = "footnote"; n.children = [ - { - kind: "element", - name: "a", - attributes: new Map([ - ["id", `footnoteref--${reference}`], - ["class", "footnote"], - ["href", `#footnote--${reference}`], - ]), - children: [ - { - kind: "text", - content: `[${footnoteNumber}]`, - raw: false, - }, - ], - }, + elem( + "a", + { + id: `footnoteref--${reference}`, + class: "footnote", + href: `#footnote--${reference}`, + }, + text(`[${footnoteNumber}]`), + ), ]; }); - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "footnote") { - return; - } - - const id = n.attributes.get("id"); + forEachElementOfType(doc.root, "footnote", (n) => { + const id = n.attributes.id; if (!id || !footnoteMap.has(id)) { n.name = "span"; n.children = []; @@ -346,23 +296,16 @@ function traverseFootnotes(doc: Document) { const footnoteNumber = footnoteMap.get(id)!; n.name = "div"; - n.attributes.delete("id"); - n.attributes.set("class", "footnote"); - n.attributes.set("id", `footnote--${id}`); + delete n.attributes.id; + n.attributes.class = "footnote"; + n.attributes.id = `footnote--${id}`; n.children = [ - { - kind: "element", - name: "a", - attributes: new Map([["href", `#footnoteref--${id}`]]), - children: [ - { - kind: "text", - content: `${footnoteNumber}. `, - raw: false, - }, - ], - }, + elem( + "a", + { href: `#footnoteref--${id}` }, + text(`${footnoteNumber}. `), + ), ...n.children, ]; }); @@ -374,7 +317,7 @@ function removeUnnecessaryParagraphNode(doc: Document) { return; } - const isTight = n.attributes.get("--tight") === "true"; + const isTight = n.attributes.__tight === "true"; if (!isTight) { return; } @@ -402,11 +345,13 @@ async function transformAndHighlightCodeBlockElement(doc: Document) { return; } - const language = n.attributes.get("language") || "text"; - const filename = n.attributes.get("filename"); - const numbered = n.attributes.get("numbered"); + const language = n.attributes.language || "text"; + const filename = n.attributes.filename; + const numbered = n.attributes.numbered; const sourceCodeNode = n.children[0] as Text | RawHTML; - const sourceCode = sourceCodeNode.content.trimEnd(); + const sourceCode = sourceCodeNode.kind === "text" + ? sourceCodeNode.content.trimEnd() + : sourceCodeNode.html.trimEnd(); const highlighted = await codeToHtml(sourceCode, { lang: language in bundledLanguages ? language as BundledLanguage : "text", @@ -417,36 +362,26 @@ async function transformAndHighlightCodeBlockElement(doc: Document) { }); n.name = "div"; - n.attributes.set("class", "codeblock"); - n.attributes.delete("language"); + n.attributes.class = "codeblock"; + delete n.attributes.language; if (numbered === "true") { - n.attributes.delete("numbered"); + delete n.attributes.numbered; addClass(n, "numbered"); } if (filename) { - n.attributes.delete("filename"); + delete n.attributes.filename; n.children = [ - { - kind: "element", - name: "div", - attributes: new Map([["class", "filename"]]), - children: [{ - kind: "text", - content: filename, - raw: false, - }], - }, - { - kind: "text", - content: highlighted, - raw: true, - }, + elem("div", { class: "filename" }, text(filename)), + rawHTML(highlighted), ]; } else { - sourceCodeNode.content = highlighted; - sourceCodeNode.raw = true; + if (sourceCodeNode.kind === "text") { + n.children[0] = rawHTML(highlighted); + } else { + sourceCodeNode.html = highlighted; + } } }); } @@ -486,7 +421,7 @@ function generateTableOfContents(doc: Document) { if (!parentSection) return; // Check if this section has toc=false attribute - const tocAttribute = parentSection.attributes.get("toc"); + const tocAttribute = parentSection.attributes.toc; if (tocAttribute === "false") { // Add this level to excluded levels and remove deeper levels excludedLevels.length = 0; @@ -510,7 +445,7 @@ function generateTableOfContents(doc: Document) { excludedLevels.pop(); } - const sectionId = parentSection.attributes.get("id"); + const sectionId = parentSection.attributes.id; if (!sectionId) return; let headingText = ""; @@ -558,7 +493,7 @@ function generateTableOfContents(doc: Document) { function removeTocAttributes(doc: Document) { forEachChildRecursively(doc.root, (node) => { if (node.kind === "element" && node.name === "section") { - node.attributes.delete("toc"); + delete node.attributes.toc; } }); } diff --git a/services/blog/nuldoc-src/dom.ts b/services/blog/nuldoc-src/dom.ts index ed7ffd31..abe7ff89 100644 --- a/services/blog/nuldoc-src/dom.ts +++ b/services/blog/nuldoc-src/dom.ts @@ -1,33 +1,58 @@ export type Text = { kind: "text"; content: string; - raw: false; }; export type RawHTML = { - kind: "text"; - content: string; - raw: true; + kind: "raw"; + html: string; }; export type Element = { kind: "element"; name: string; - attributes: Map<string, string>; + attributes: Record<string, string>; children: Node[]; }; export type Node = Element | Text | RawHTML; +export function text(content: string): Text { + return { + kind: "text", + content, + }; +} + +export function rawHTML(html: string): RawHTML { + return { + kind: "raw", + html, + }; +} + +export function elem( + name: string, + attributes?: Record<string, string>, + ...children: Node[] +): Element { + return { + kind: "element", + name, + attributes: attributes || {}, + children, + }; +} + export function addClass(e: Element, klass: string) { - const classes = e.attributes.get("class"); + const classes = e.attributes.class; if (classes === undefined) { - e.attributes.set("class", klass); + e.attributes.class = klass; } else { const classList = classes.split(" "); classList.push(klass); classList.sort(); - e.attributes.set("class", classList.join(" ")); + e.attributes.class = classList.join(" "); } } @@ -100,3 +125,30 @@ export async function forEachChildRecursivelyAsync( }; await forEachChildAsync(e, g); } + +export function forEachElementOfType( + root: Element, + elementName: string, + f: (e: Element) => void, +) { + forEachChildRecursively(root, (n) => { + if (n.kind === "element" && n.name === elementName) { + f(n); + } + }); +} + +export function processTextNodesInElement( + e: Element, + f: (text: string) => Node[], +) { + const newChildren: Node[] = []; + for (const child of e.children) { + if (child.kind === "text") { + newChildren.push(...f(child.content)); + } else { + newChildren.push(child); + } + } + e.children = newChildren; +} diff --git a/services/blog/nuldoc-src/jsx/render.ts b/services/blog/nuldoc-src/jsx/render.ts index 8603f6c3..a72d9ad7 100644 --- a/services/blog/nuldoc-src/jsx/render.ts +++ b/services/blog/nuldoc-src/jsx/render.ts @@ -1,4 +1,5 @@ import type { Element, Node } from "../dom.ts"; +import { elem, text } from "../dom.ts"; import type { JSXNode, JSXNullableSimpleNode, @@ -16,7 +17,7 @@ function transformNode(node: JSXNode): Promise<Node[]> { .filter((c): c is JSXSimpleNode => c != null && c !== false) .map((c) => { if (typeof c === "string") { - return { kind: "text", content: c, raw: false }; + return text(c); } else if ("kind" in c) { return c; } else { @@ -32,13 +33,8 @@ export async function renderToDOM( const { tag, props } = element; if (typeof tag === "string") { const { children, ...attrs } = props; - const attrsMap = new Map(Object.entries(attrs)) as Map<string, string>; - return { - kind: "element", - name: tag, - attributes: attrsMap, - children: await transformNode(children), - }; + const attrsMap = attrs as Record<string, string>; + return elem(tag, attrsMap, ...(await transformNode(children))); } else { return renderToDOM(await tag(props)); } diff --git a/services/blog/nuldoc-src/renderers/html.ts b/services/blog/nuldoc-src/renderers/html.ts index 84b3ebaa..6e829f09 100644 --- a/services/blog/nuldoc-src/renderers/html.ts +++ b/services/blog/nuldoc-src/renderers/html.ts @@ -127,7 +127,7 @@ function getDtd(name: string): Dtd { } function isInlineNode(n: Node): boolean { - if (n.kind === "text") { + if (n.kind === "text" || n.kind === "raw") { return true; } if (n.name !== "a") { @@ -146,11 +146,9 @@ function isBlockNode(n: Node): boolean { function nodeToHtmlText(n: Node, ctx: Context): string { if (n.kind === "text") { - if (n.raw) { - return n.content; - } else { - return textNodeToHtmlText(n, ctx); - } + return textNodeToHtmlText(n, ctx); + } else if (n.kind === "raw") { + return n.html; } else { return elementNodeToHtmlText(n, ctx); } @@ -259,8 +257,8 @@ function indent(ctx: Context): string { } function getElementAttributes(e: Element): [string, string][] { - return [...e.attributes.entries()] - .filter((a) => !a[0].startsWith("--")) + return [...Object.entries(e.attributes)] + .filter((a) => !a[0].startsWith("__")) .filter((a) => a[1] !== undefined) .sort( (a, b) => { diff --git a/services/blog/nuldoc-src/renderers/xml.ts b/services/blog/nuldoc-src/renderers/xml.ts index 77cc1574..523567ab 100644 --- a/services/blog/nuldoc-src/renderers/xml.ts +++ b/services/blog/nuldoc-src/renderers/xml.ts @@ -24,7 +24,7 @@ function getDtd(name: string): Dtd { } function isInlineNode(n: Node): boolean { - if (n.kind === "text") { + if (n.kind === "text" || n.kind === "raw") { return true; } return getDtd(n.name).type === "inline"; @@ -36,11 +36,9 @@ function isBlockNode(n: Node): boolean { function nodeToXmlText(n: Node, ctx: Context): string { if (n.kind === "text") { - if (n.raw) { - return n.content; - } else { - return textNodeToXmlText(n); - } + return textNodeToXmlText(n); + } else if (n.kind === "raw") { + return n.html; } else { return elementNodeToXmlText(n, ctx); } @@ -102,8 +100,8 @@ function indent(ctx: Context): string { } function getElementAttributes(e: Element): [string, string][] { - return [...e.attributes.entries()] - .filter((a) => !a[0].startsWith("--")) + return [...Object.entries(e.attributes)] + .filter((a) => !a[0].startsWith("__")) .sort( (a, b) => { // Special rules: |
