summaryrefslogtreecommitdiffhomepage
path: root/vhosts/blog/nuldoc-src/ndoc
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2023-09-20 19:56:52 +0900
committernsfisis <nsfisis@gmail.com>2023-09-20 19:56:57 +0900
commita84908b7e8a0e2423afd6b836eccf27a420270b4 (patch)
tree00204b62358f8c57fcb36f601db360626484cc1a /vhosts/blog/nuldoc-src/ndoc
parent0b488f85380f964c40b0b9aae69c6611bc7978bc (diff)
downloadnsfisis.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.ts56
-rw-r--r--vhosts/blog/nuldoc-src/ndoc/parse.ts47
-rw-r--r--vhosts/blog/nuldoc-src/ndoc/to_html.ts235
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");
+ });
+}