diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-06-27 23:39:31 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-06-27 23:39:31 +0900 |
| commit | 674fe965550444db87edc7937ff6932e1a918d9d (patch) | |
| tree | e8a80dd958d3e082485286bf5785a7992b6e6b0e /vhosts/blog/nuldoc-src/djot | |
| parent | fe4d1d625b53796c5f20399790e5ff8c7a7e1608 (diff) | |
| download | nsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.tar.gz nsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.tar.zst nsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.zip | |
feat(meta): rename vhosts/ directory to services/
Diffstat (limited to 'vhosts/blog/nuldoc-src/djot')
| -rw-r--r-- | vhosts/blog/nuldoc-src/djot/djot2ndoc.ts | 842 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/djot/document.ts | 60 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/djot/parse.ts | 33 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/djot/to_html.ts | 449 |
4 files changed, 0 insertions, 1384 deletions
diff --git a/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts b/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts deleted file mode 100644 index 90b1289c..00000000 --- a/vhosts/blog/nuldoc-src/djot/djot2ndoc.ts +++ /dev/null @@ -1,842 +0,0 @@ -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); - } - if (node.attributes?.filename) { - attributes.set("filename", node.attributes.filename); - } - if (node.attributes?.numbered) { - attributes.set("numbered", "true"); - } - 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), - }; - } - - if (node.attributes?.class === "edit") { - 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/djot/document.ts b/vhosts/blog/nuldoc-src/djot/document.ts deleted file mode 100644 index be9c08d5..00000000 --- a/vhosts/blog/nuldoc-src/djot/document.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Doc as DjotDoc } from "@djot/djot"; -import { join } from "@std/path"; -import { z } from "zod/mod.ts"; -import { Config } from "../config.ts"; -import { Element } from "../dom.ts"; -import { Revision, stringToDate } from "../revision.ts"; -import { djot2ndoc } from "./djot2ndoc.ts"; - -export const PostMetadataSchema = z.object({ - article: z.object({ - uuid: z.string(), - title: z.string(), - description: z.string(), - tags: z.array(z.string()), - revisions: z.array(z.object({ - date: z.string(), - remark: z.string(), - isInternal: z.boolean().optional(), - })), - }), -}); - -export type PostMetadata = z.infer<typeof PostMetadataSchema>; - -export type Document = { - root: Element; - sourceFilePath: string; - uuid: string; - link: string; - title: string; - description: string; // TODO: should it be markup text? - tags: string[]; - revisions: Revision[]; -}; - -export function createNewDocumentFromDjotDocument( - root: DjotDoc, - meta: PostMetadata, - sourceFilePath: string, - config: Config, -): Document { - const cwd = Deno.cwd(); - const contentDir = join(cwd, config.locations.contentDir); - const link = sourceFilePath.replace(contentDir, "").replace(".xml", "/"); - return { - root: djot2ndoc(root), - sourceFilePath, - uuid: meta.article.uuid, - link: link, - title: meta.article.title, - description: meta.article.description, - tags: meta.article.tags, - revisions: meta.article.revisions.map((r, i) => ({ - number: i, - date: stringToDate(r.date), - remark: r.remark, - isInternal: !!r.isInternal, - })), - }; -} diff --git a/vhosts/blog/nuldoc-src/djot/parse.ts b/vhosts/blog/nuldoc-src/djot/parse.ts deleted file mode 100644 index c79a6708..00000000 --- a/vhosts/blog/nuldoc-src/djot/parse.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { parse as parseDjot } from "@djot/djot"; -import { parse as parseToml } from "@std/toml"; -import { Config } from "../config.ts"; -import { - createNewDocumentFromDjotDocument, - Document, - PostMetadata, - PostMetadataSchema, -} from "./document.ts"; -import toHtml from "./to_html.ts"; - -export async function parseDjotFile( - filePath: string, - config: Config, -): Promise<Document> { - try { - const fileContent = await Deno.readTextFile(filePath); - const [, frontmatter, ...rest] = fileContent.split(/^---$/m); - const meta = parseMetadata(frontmatter); - const root = parseDjot(rest.join("\n")); - const doc = createNewDocumentFromDjotDocument(root, meta, filePath, config); - return await toHtml(doc); - } catch (e) { - if (e instanceof Error) { - e.message = `${e.message} in ${filePath}`; - } - throw e; - } -} - -function parseMetadata(s: string): PostMetadata { - return PostMetadataSchema.parse(parseToml(s)); -} diff --git a/vhosts/blog/nuldoc-src/djot/to_html.ts b/vhosts/blog/nuldoc-src/djot/to_html.ts deleted file mode 100644 index 5ea9b57d..00000000 --- a/vhosts/blog/nuldoc-src/djot/to_html.ts +++ /dev/null @@ -1,449 +0,0 @@ -import { BundledLanguage, bundledLanguages, codeToHtml } from "shiki"; -import { Document } from "./document.ts"; -import { NuldocError } from "../errors.ts"; -import { - addClass, - Element, - forEachChild, - forEachChildRecursively, - forEachChildRecursivelyAsync, - Node, - RawHTML, - Text, -} from "../dom.ts"; - -export default async function toHtml(doc: Document): Promise<Document> { - mergeConsecutiveTextNodes(doc); - removeUnnecessaryTextNode(doc); - transformLinkLikeToAnchorElement(doc); - transformSectionIdAttribute(doc); - setSectionTitleAnchor(doc); - transformSectionTitleElement(doc); - transformNoteElement(doc); - addAttributesToExternalLinkElement(doc); - traverseFootnotes(doc); - removeUnnecessaryParagraphNode(doc); - await transformAndHighlightCodeBlockElement(doc); - mergeConsecutiveTextNodes(doc); - return doc; -} - -function mergeConsecutiveTextNodes(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element") { - return; - } - - const newChildren: Node[] = []; - let currentTextContent = ""; - - for (const child of n.children) { - if (child.kind === "text" && !child.raw) { - currentTextContent += child.content; - } else { - if (currentTextContent !== "") { - newChildren.push({ - kind: "text", - content: currentTextContent, - raw: false, - }); - currentTextContent = ""; - } - newChildren.push(child); - } - } - - if (currentTextContent !== "") { - newChildren.push({ - kind: "text", - content: currentTextContent, - raw: false, - }); - } - - n.children = newChildren; - }); -} - -function removeUnnecessaryTextNode(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element") { - return; - } - - let changed = true; - while (changed) { - changed = false; - if (n.children.length === 0) { - break; - } - const firstChild = n.children[0]; - if (firstChild.kind === "text" && firstChild.content.trim() === "") { - n.children.shift(); - changed = true; - } - if (n.children.length === 0) { - break; - } - const lastChild = n.children[n.children.length - 1]; - if (lastChild.kind === "text" && lastChild.content.trim() === "") { - n.children.pop(); - changed = true; - } - } - }); -} - -function transformLinkLikeToAnchorElement(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if ( - n.kind !== "element" || n.name === "a" || n.name === "code" || - n.name === "codeblock" - ) { - return; - } - - const newChildren: Node[] = []; - for (const child of n.children) { - if (child.kind !== "text") { - newChildren.push(child); - continue; - } - let restContent = child.content; - while (restContent !== "") { - const match = /^(.*?)(https?:\/\/[^ \n]+)(.*)$/s.exec(restContent); - if (!match) { - newChildren.push({ kind: "text", content: restContent, raw: false }); - restContent = ""; - break; - } - const [_, prefix, url, suffix] = match; - newChildren.push({ kind: "text", content: prefix, raw: false }); - newChildren.push({ - kind: "element", - name: "a", - attributes: new Map([["href", url]]), - children: [{ kind: "text", content: url, raw: false }], - }); - restContent = suffix; - } - } - n.children = newChildren; - }); -} - -function transformSectionIdAttribute(doc: Document) { - const sectionStack: string[] = []; - const usedIds = new Set<string>(); - - const processNode = (n: Node) => { - if (n.kind !== "element") { - return; - } - - if (n.name === "section") { - const idAttr = n.attributes.get("id"); - if (!idAttr) { - return; - } - - let newId: string; - if (sectionStack.length === 0) { - newId = `section--${idAttr}`; - } else { - newId = `section--${sectionStack.join("--")}--${idAttr}`; - } - - if (usedIds.has(newId)) { - throw new NuldocError( - `[nuldoc.tohtml] Duplicate section ID: ${newId}`, - ); - } - - usedIds.add(newId); - n.attributes.set("id", newId); - sectionStack.push(idAttr); - - forEachChild(n, processNode); - - sectionStack.pop(); - } else { - forEachChild(n, processNode); - } - }; - - forEachChild(doc.root, processNode); -} - -function setSectionTitleAnchor(doc: Document) { - const sectionStack: Element[] = []; - const g = (c: Node) => { - if (c.kind !== "element") { - return; - } - - if (c.name === "section") { - sectionStack.push(c); - } - forEachChild(c, g); - if (c.name === "section") { - sectionStack.pop(); - } - if (c.name === "h") { - const currentSection = sectionStack[sectionStack.length - 1]; - if (!currentSection) { - throw new NuldocError( - "[nuldoc.tohtml] <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}`); - c.children = [aElement]; - } - }; - forEachChild(doc.root, g); -} - -function transformSectionTitleElement(doc: Document) { - let sectionLevel = 1; - const g = (c: Node) => { - if (c.kind !== "element") { - return; - } - - if (c.name === "section") { - sectionLevel += 1; - c.attributes.set("--section-level", sectionLevel.toString()); - } - forEachChild(c, g); - if (c.name === "section") { - sectionLevel -= 1; - } - if (c.name === "h") { - c.name = `h${sectionLevel}`; - } - }; - forEachChild(doc.root, g); -} - -function transformNoteElement(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "note") { - return; - } - - const editatAttr = n.attributes?.get("editat"); - const operationAttr = n.attributes?.get("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, - }; - n.name = "div"; - addClass(n, "admonition"); - n.children = [ - labelElement, - contentElement, - ]; - }); -} - -function addAttributesToExternalLinkElement(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "a") { - return; - } - - const href = n.attributes.get("href") ?? ""; - if (!href.startsWith("http")) { - return; - } - n.attributes - .set("target", "_blank") - .set("rel", "noreferrer"); - }); -} - -function 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"); - if (!reference) { - return; - } - - let footnoteNumber: number; - if (footnoteMap.has(reference)) { - footnoteNumber = footnoteMap.get(reference)!; - } else { - footnoteNumber = ++footnoteCounter; - footnoteMap.set(reference, footnoteNumber); - } - - n.name = "sup"; - n.attributes.delete("reference"); - n.attributes.set("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, - }, - ], - }, - ]; - }); - - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || n.name !== "footnote") { - return; - } - - const id = n.attributes.get("id"); - if (!id || !footnoteMap.has(id)) { - n.name = "span"; - n.children = []; - return; - } - - const footnoteNumber = footnoteMap.get(id)!; - - n.name = "div"; - n.attributes.delete("id"); - n.attributes.set("class", "footnote"); - n.attributes.set("id", `footnote--${id}`); - - n.children = [ - { - kind: "element", - name: "a", - attributes: new Map([["href", `#footnoteref--${id}`]]), - children: [ - { - kind: "text", - content: `${footnoteNumber}. `, - raw: false, - }, - ], - }, - ...n.children, - ]; - }); -} - -function removeUnnecessaryParagraphNode(doc: Document) { - forEachChildRecursively(doc.root, (n) => { - if (n.kind !== "element" || (n.name !== "ul" && n.name !== "ol")) { - return; - } - - const isTight = n.attributes.get("--tight") === "true"; - if (!isTight) { - return; - } - - for (const child of n.children) { - if (child.kind !== "element" || child.name !== "li") { - continue; - } - const newGrandChildren: Node[] = []; - for (const grandChild of child.children) { - if (grandChild.kind === "element" && grandChild.name === "p") { - newGrandChildren.push(...grandChild.children); - } else { - newGrandChildren.push(grandChild); - } - } - child.children = newGrandChildren; - } - }); -} - -async function transformAndHighlightCodeBlockElement(doc: Document) { - await forEachChildRecursivelyAsync(doc.root, async (n) => { - if (n.kind !== "element" || n.name !== "codeblock") { - return; - } - - const language = n.attributes.get("language") || "text"; - const filename = n.attributes.get("filename"); - const numbered = n.attributes.get("numbered"); - const sourceCodeNode = n.children[0] as Text | RawHTML; - const sourceCode = sourceCodeNode.content.trimEnd(); - - const highlighted = await codeToHtml(sourceCode, { - lang: language in bundledLanguages ? language as BundledLanguage : "text", - theme: "github-light", - colorReplacements: { - "#fff": "#f5f5f5", - }, - }); - - n.name = "div"; - n.attributes.set("class", "codeblock"); - n.attributes.delete("language"); - - if (numbered === "true") { - n.attributes.delete("numbered"); - addClass(n, "numbered"); - } - if (filename) { - n.attributes.delete("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, - }, - ]; - } else { - sourceCodeNode.content = highlighted; - sourceCodeNode.raw = true; - } - }); -} |
