diff options
Diffstat (limited to 'vhosts/blog/nuldoc-src/docbook')
| -rw-r--r-- | vhosts/blog/nuldoc-src/docbook/document.ts | 108 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/docbook/parse.ts | 21 | ||||
| -rw-r--r-- | vhosts/blog/nuldoc-src/docbook/to_html.ts | 319 |
3 files changed, 448 insertions, 0 deletions
diff --git a/vhosts/blog/nuldoc-src/docbook/document.ts b/vhosts/blog/nuldoc-src/docbook/document.ts new file mode 100644 index 00000000..677c8275 --- /dev/null +++ b/vhosts/blog/nuldoc-src/docbook/document.ts @@ -0,0 +1,108 @@ +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, + findChildElements, + findFirstChildElement, + innerText, +} from "../dom.ts"; + +export type Document = { + root: Element; + sourceFilePath: string; + link: string; + title: string; + summary: string; // TODO: should it be markup text? + tags: string[]; + revisions: Revision[]; +}; + +export function createNewDocumentFromRootElement( + root: Element, + sourceFilePath: string, + config: Config, +): Document { + const article = findFirstChildElement(root, "article"); + if (!article) { + throw new DocBookError( + `[docbook.new] <article> element not found`, + ); + } + const info = findFirstChildElement(article, "info"); + if (!info) { + throw new DocBookError( + `[docbook.new] <info> element not found`, + ); + } + + const titleElement = findFirstChildElement(info, "title"); + if (!titleElement) { + throw new DocBookError( + `[docbook.new] <title> element not found`, + ); + } + const title = innerText(titleElement).trim(); + const abstractElement = findFirstChildElement(info, "abstract"); + if (!abstractElement) { + throw new DocBookError( + `[docbook.new] <abstract> element not found`, + ); + } + const summary = innerText(abstractElement).trim(); + const keywordsetElement = findFirstChildElement(info, "keywordset"); + let tags: string[]; + if (!keywordsetElement) { + tags = []; + } else { + tags = findChildElements(keywordsetElement, "keyword").map((x) => + innerText(x).trim() + ); + } + const revhistoryElement = findFirstChildElement(info, "revhistory"); + if (!revhistoryElement) { + throw new DocBookError( + `[docbook.new] <revhistory> element not found`, + ); + } + const revisions = findChildElements(revhistoryElement, "revision").map( + (x, i) => { + const dateElement = findFirstChildElement(x, "date"); + if (!dateElement) { + throw new DocBookError( + `[docbook.new] <date> element not found`, + ); + } + const revremarkElement = findFirstChildElement(x, "revremark"); + if (!revremarkElement) { + throw new DocBookError( + `[docbook.new] <revremark> element not found`, + ); + } + return { + number: i + 1, + date: stringToDate(innerText(dateElement).trim()), + remark: innerText(revremarkElement).trim(), + }; + }, + ); + if (revisions.length === 0) { + throw new DocBookError( + `[docbook.new] <revision> 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: title, + summary: summary, + tags: tags, + revisions: revisions, + }; +} diff --git a/vhosts/blog/nuldoc-src/docbook/parse.ts b/vhosts/blog/nuldoc-src/docbook/parse.ts new file mode 100644 index 00000000..bce317e6 --- /dev/null +++ b/vhosts/blog/nuldoc-src/docbook/parse.ts @@ -0,0 +1,21 @@ +import { Config } from "../config.ts"; +import { parseXmlFile } from "../xml.ts"; +import { DocBookError, XmlParseError } from "../errors.ts"; +import { createNewDocumentFromRootElement, Document } from "./document.ts"; +import toHtml from "./to_html.ts"; + +export async function parseDocBookFile( + filePath: string, + config: Config, +): Promise<Document> { + try { + const root = await parseXmlFile(filePath); + const doc = createNewDocumentFromRootElement(root, filePath, config); + return toHtml(doc); + } catch (e) { + if (e instanceof DocBookError || e instanceof XmlParseError) { + e.message = `${e.message} in ${filePath}`; + } + throw e; + } +} diff --git a/vhosts/blog/nuldoc-src/docbook/to_html.ts b/vhosts/blog/nuldoc-src/docbook/to_html.ts new file mode 100644 index 00000000..4add912c --- /dev/null +++ b/vhosts/blog/nuldoc-src/docbook/to_html.ts @@ -0,0 +1,319 @@ +// @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, + removeChildElements, + Text, +} from "../dom.ts"; + +export default function toHtml(doc: Document): Document { + removeArticleInfo(doc); + removeArticleAttributes(doc); + removeUnnecessaryTextNode(doc); + transformElementNames(doc, "emphasis", "em"); + transformElementNames(doc, "informaltable", "table"); + transformElementNames(doc, "itemizedlist", "ul"); + transformElementNames(doc, "link", "a"); + transformElementNames(doc, "listitem", "li"); + transformElementNames(doc, "literal", "code"); + transformElementNames(doc, "orderedlist", "ol"); + transformElementNames(doc, "para", "p"); + transformElementNames(doc, "superscript", "sup"); + transformAttributeNames(doc, "xml:id", "id"); + transformAttributeNames(doc, "xl:href", "href"); + transformSectionIdAttribute(doc); + setSectionTitleAnchor(doc); + transformSectionTitleElement(doc); + transformProgramListingElement(doc); + transformLiteralLayoutElement(doc); + transformNoteElement(doc); + setDefaultLangAttribute(doc); + traverseFootnotes(doc); + highlightPrograms(doc); + return doc; +} + +function removeArticleInfo(doc: Document) { + const article = findFirstChildElement(doc.root, "article"); + if (!article) { + throw new DocBookError( + `[docbook.tohtml] <article> element not found`, + ); + } + removeChildElements(article, "info"); +} + +function removeArticleAttributes(doc: Document) { + const article = findFirstChildElement(doc.root, "article"); + if (!article) { + throw new DocBookError( + `[docbook.tohtml] <article> element not found`, + ); + } + article.attributes.delete("xmlns"); + article.attributes.delete("xmlns:xl"); + article.attributes.delete("version"); +} + +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 transformElementNames( + doc: Document, + from: string, + to: string, +) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind === "element" && n.name === from) { + n.name = to; + } + }); +} + +function transformAttributeNames( + doc: Document, + from: string, + to: string, +) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element") { + return; + } + const value = n.attributes.get(from) as string; + if (value !== undefined) { + n.attributes.delete(from); + n.attributes.set(to, value); + } + }); +} + +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 === "title") { + const currentSection = sectionStack[sectionStack.length - 1]; + if (!currentSection) { + throw new DocBookError( + "[docbook.tohtml] <title> 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 === "title") { + c.name = `h${sectionLevel}`; + } + }; + forEachChild(doc.root, g); +} + +function transformProgramListingElement(doc: Document) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element" || n.name !== "programlisting") { + return; + } + + n.name = "pre"; + addClass(n, "highlight"); + const codeElement: Element = { + kind: "element", + name: "code", + attributes: new Map(), + children: n.children, + }; + n.children = [codeElement]; + }); +} + +function transformLiteralLayoutElement(doc: Document) { + forEachChildRecursively(doc.root, (n) => { + if (n.kind !== "element" || n.name !== "literallayout") { + 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"); + }); +} |
