summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--services/blog/nuldoc-src/djot/djot2ndoc.ts636
-rw-r--r--services/blog/nuldoc-src/djot/to_html.ts233
-rw-r--r--services/blog/nuldoc-src/dom.ts68
-rw-r--r--services/blog/nuldoc-src/jsx/render.ts12
-rw-r--r--services/blog/nuldoc-src/renderers/html.ts14
-rw-r--r--services/blog/nuldoc-src/renderers/xml.ts14
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: