diff options
| author | nsfisis <nsfisis@gmail.com> | 2023-09-20 19:56:52 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2023-09-20 19:56:57 +0900 |
| commit | a84908b7e8a0e2423afd6b836eccf27a420270b4 (patch) | |
| tree | 00204b62358f8c57fcb36f601db360626484cc1a /vhosts/blog/nuldoc-src/ndoc | |
| parent | 0b488f85380f964c40b0b9aae69c6611bc7978bc (diff) | |
| download | nsfisis.dev-a84908b7e8a0e2423afd6b836eccf27a420270b4.tar.gz nsfisis.dev-a84908b7e8a0e2423afd6b836eccf27a420270b4.tar.zst nsfisis.dev-a84908b7e8a0e2423afd6b836eccf27a420270b4.zip | |
feat(blog/nuldoc): change content format from DocBook to NulDoc
Diffstat (limited to 'vhosts/blog/nuldoc-src/ndoc')
| -rw-r--r-- | vhosts/blog/nuldoc-src/ndoc/document.ts | 56 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/ndoc/parse.ts | 47 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/ndoc/to_html.ts | 235 |
3 files changed, 338 insertions, 0 deletions
diff --git a/vhosts/blog/nuldoc-src/ndoc/document.ts b/vhosts/blog/nuldoc-src/ndoc/document.ts new file mode 100644 index 00000000..31bae616 --- /dev/null +++ b/vhosts/blog/nuldoc-src/ndoc/document.ts @@ -0,0 +1,56 @@ +import { join } from "std/path/mod.ts"; +import { Config } from "../config.ts"; +import { DocBookError } from "../errors.ts"; +import { Revision, stringToDate } from "../revision.ts"; +import { Element, findFirstChildElement } from "../dom.ts"; + +export type Document = { + root: Element; + sourceFilePath: string; + link: string; + title: string; + description: string; // TODO: should it be markup text? + tags: string[]; + revisions: Revision[]; +}; + +export function createNewDocumentFromRootElement( + root: Element, + meta: { + article: { + title: string; + description: string; + tags: string[]; + revisions: { + date: string; + remark: string; + }[]; + }; + }, + sourceFilePath: string, + config: Config, +): Document { + const article = findFirstChildElement(root, "article"); + if (!article) { + throw new DocBookError( + `[docbook.new] <article> element not found`, + ); + } + + const cwd = Deno.cwd(); + const contentDir = join(cwd, config.locations.contentDir); + const link = sourceFilePath.replace(contentDir, "").replace(".xml", "/"); + return { + root: root, + sourceFilePath: sourceFilePath, + 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, + })), + }; +} diff --git a/vhosts/blog/nuldoc-src/ndoc/parse.ts b/vhosts/blog/nuldoc-src/ndoc/parse.ts new file mode 100644 index 00000000..419d2630 --- /dev/null +++ b/vhosts/blog/nuldoc-src/ndoc/parse.ts @@ -0,0 +1,47 @@ +import { parse as parseToml } from "std/encoding/toml.ts"; +import { Config } from "../config.ts"; +import { parseXmlString } from "../xml.ts"; +import { createNewDocumentFromRootElement, Document } from "./document.ts"; +import toHtml from "./to_html.ts"; + +export async function parseNulDocFile( + filePath: string, + config: Config, +): Promise<Document> { + try { + const fileContent = await Deno.readTextFile(filePath); + const parts = fileContent.split(/^---$/m); + const meta = parseMetaInfo(parts[1]); + const root = parseXmlString("<?xml ?>" + parts[2]); + const doc = createNewDocumentFromRootElement(root, meta, filePath, config); + return toHtml(doc); + } catch (e) { + e.message = `${e.message} in ${filePath}`; + throw e; + } +} + +function parseMetaInfo(s: string): { + article: { + title: string; + description: string; + tags: string[]; + revisions: { + date: string; + remark: string; + }[]; + }; +} { + const root = parseToml(s) as { + article: { + title: string; + description: string; + tags: string[]; + revisions: { + date: string; + remark: string; + }[]; + }; + }; + return root; +} diff --git a/vhosts/blog/nuldoc-src/ndoc/to_html.ts b/vhosts/blog/nuldoc-src/ndoc/to_html.ts new file mode 100644 index 00000000..dc39919b --- /dev/null +++ b/vhosts/blog/nuldoc-src/ndoc/to_html.ts @@ -0,0 +1,235 @@ +// @deno-types="../types/highlight-js.d.ts" +import hljs from "npm:highlight.js"; +import { Document } from "./document.ts"; +import { DocBookError } from "../errors.ts"; +import { + addClass, + Element, + findFirstChildElement, + forEachChild, + forEachChildRecursively, + Node, + RawHTML, + Text, +} from "../dom.ts"; + +export default function toHtml(doc: Document): Document { + removeUnnecessaryTextNode(doc); + transformSectionIdAttribute(doc); + setSectionTitleAnchor(doc); + transformSectionTitleElement(doc); + transformCodeBlockElement(doc); + transformNoteElement(doc); + setDefaultLangAttribute(doc); + traverseFootnotes(doc); + highlightPrograms(doc); + return doc; +} + +function removeUnnecessaryTextNode(doc: Document) { + const g = (n: Node) => { + 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; + } + } + + forEachChild(n, g); + }; + forEachChild(doc.root, g); +} + +function transformSectionIdAttribute(doc: Document) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element" || n.name !== "section") { + return; + } + + const idAttr = n.attributes.get("id"); + n.attributes.set("id", `section--${idAttr}`); + }); +} + +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 DocBookError( + "[docbook.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 transformCodeBlockElement(doc: Document) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element" || n.name !== "codeblock") { + return; + } + + n.name = "pre"; + addClass(n, "highlight"); + const codeElement: Element = { + kind: "element", + name: "code", + attributes: new Map(), + children: n.children, + }; + n.children = [codeElement]; + }); +} + +function transformNoteElement(doc: Document) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element" || n.name !== "note") { + return; + } + + const labelElement: Element = { + kind: "element", + name: "div", + attributes: new Map([["class", "admonition-label"]]), + children: [{ + kind: "text", + content: "NOTE", + raw: false, + }], + }; + const contentElement: Element = { + kind: "element", + name: "div", + attributes: new Map([["class", "admonition-content"]]), + children: n.children, + }; + n.name = "div"; + addClass(n, "admonition"); + n.children = [ + labelElement, + contentElement, + ]; + }); +} + +function setDefaultLangAttribute(_doc: Document) { + // TODO + // if (!e.attributes.has("lang")) { + // e.attributes.set("lang", "ja-JP"); + // } +} + +function traverseFootnotes(doc: Document) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element" || n.name !== "footnote") { + return; + } + + // TODO + // <footnote>x</footnote> + // + // <sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1">1</a>]</sup> + // + // <div class="footnote" id="_footnotedef_1"> + // <a href="#_footnoteref_1">1</a>. RAS syndrome + // </div> + n.name = "span"; + n.children = []; + }); +} + +function highlightPrograms(doc: Document) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element" || n.name !== "pre") { + return; + } + const preClass = n.attributes.get("class") || ""; + if (!preClass.includes("highlight")) { + return; + } + const codeElement = findFirstChildElement(n, "code"); + if (!codeElement) { + return; + } + const language = n.attributes.get("language"); + if (!language) { + return; + } + const sourceCodeNode = codeElement.children[0] as Text | RawHTML; + const sourceCode = sourceCodeNode.content; + + if (!hljs.getLanguage(language)) { + return; + } + + const highlighted = + hljs.highlight(sourceCode, { language: language }).value; + + sourceCodeNode.content = highlighted; + sourceCodeNode.raw = true; + codeElement.attributes.set("class", "highlight"); + }); +} |
