From 2428ea512cdbac4b86189c654814c5eeca54a704 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 18 Mar 2023 15:54:53 +0900 Subject: refactor: add Page type to represent single web page --- nuldoc-src/commands/build.ts | 23 +++- nuldoc-src/html.ts | 260 ------------------------------------------- nuldoc-src/page.ts | 8 ++ nuldoc-src/render.ts | 13 +++ nuldoc-src/renderers/html.ts | 255 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 294 insertions(+), 265 deletions(-) delete mode 100644 nuldoc-src/html.ts create mode 100644 nuldoc-src/page.ts create mode 100644 nuldoc-src/render.ts create mode 100644 nuldoc-src/renderers/html.ts diff --git a/nuldoc-src/commands/build.ts b/nuldoc-src/commands/build.ts index 8765802..90aaab9 100644 --- a/nuldoc-src/commands/build.ts +++ b/nuldoc-src/commands/build.ts @@ -3,8 +3,9 @@ import { ensureDir } from "std/fs/mod.ts"; import { expandGlob } from "std/fs/expand_glob.ts"; import { Config } from "../config.ts"; import { parseDocBookFile } from "../docbook/parse.ts"; -import { writeHtmlFile } from "../html.ts"; import { Document } from "../docbook/document.ts"; +import { Page } from "../page.ts"; +import { render } from "../render.ts"; import convertPost from "../templates/post.ts"; import convertPostList from "../templates/post_list.ts"; import convertTag from "../templates/tag.ts"; @@ -58,7 +59,7 @@ async function outputPosts(posts: Document[], config: Config) { "index.html", ); await ensureDir(dirname(destFilePath)); - await writeHtmlFile(post, destFilePath); + await writePage(docToPage(post, destFilePath)); } } @@ -79,7 +80,7 @@ async function outputPostList(postList: Document, config: Config) { const destDir = join(cwd, config.locations.destDir); const destFilePath = join(destDir, "posts", "index.html"); await ensureDir(dirname(destFilePath)); - await writeHtmlFile(postList, destFilePath); + await writePage(docToPage(postList, destFilePath)); } async function generateAboutPage(config: Config) { @@ -92,7 +93,7 @@ async function outputAboutPage(about: Document, config: Config) { const destDir = join(cwd, config.locations.destDir); const destFilePath = join(destDir, "about", "index.html"); await ensureDir(dirname(destFilePath)); - await writeHtmlFile(about, destFilePath); + await writePage(docToPage(about, destFilePath)); } async function generateTags(posts: Document[], config: Config) { @@ -153,7 +154,7 @@ async function outputTags(tagDocs: [string, Document][], config: Config) { for (const [tag, tagDoc] of tagDocs) { const destFilePath = join(destDir, "tags", tag, "index.html"); await ensureDir(dirname(destFilePath)); - await writeHtmlFile(tagDoc, destFilePath); + await writePage(docToPage(tagDoc, destFilePath)); } } @@ -168,3 +169,15 @@ async function copyStaticFiles(config: Config) { await Deno.copyFile(src, dst); } } + +async function writePage(page: Page) { + await Deno.writeTextFile(page.destFilePath, render(page.root, page.renderer)); +} + +function docToPage(d: Document, p: string): Page { + return { + root: d.root, + renderer: "html", + destFilePath: p, + }; +} diff --git a/nuldoc-src/html.ts b/nuldoc-src/html.ts deleted file mode 100644 index b94877a..0000000 --- a/nuldoc-src/html.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { Document } from "./docbook/document.ts"; -import { Element, forEachChild, Node, Text } from "./dom.ts"; -import { DocBookError } from "./errors.ts"; - -export async function writeHtmlFile(dom: Document, filePath: string) { - await Deno.writeTextFile(filePath, toHtmlText(dom)); -} - -type Context = { - indentLevel: number; - isInPre: boolean; -}; - -type Dtd = { type: "block" | "inline"; auto_closing?: boolean }; - -function getDtd(name: string): Dtd { - switch (name) { - case "__root__": - return { type: "block" }; - case "a": - return { type: "inline" }; - case "article": - return { type: "block" }; - case "blockquote": - return { type: "block" }; - case "body": - return { type: "block" }; - case "br": - return { type: "block", auto_closing: true }; - case "code": - return { type: "inline" }; - case "div": - return { type: "block" }; - case "em": - return { type: "inline" }; - case "footer": - return { type: "block" }; - case "h1": - return { type: "inline" }; - case "h2": - return { type: "inline" }; - case "h3": - return { type: "inline" }; - case "h4": - return { type: "inline" }; - case "h5": - return { type: "inline" }; - case "h6": - return { type: "inline" }; - case "head": - return { type: "block" }; - case "header": - return { type: "block" }; - case "hr": - return { type: "block" }; - case "html": - return { type: "block" }; - case "li": - return { type: "block" }; - case "link": - return { type: "block", auto_closing: true }; - case "main": - return { type: "block" }; - case "meta": - return { type: "block", auto_closing: true }; - case "nav": - return { type: "block" }; - case "ol": - return { type: "block" }; - case "p": - return { type: "block" }; - case "pre": - return { type: "block" }; - case "section": - return { type: "block" }; - case "span": - return { type: "inline" }; - case "table": - return { type: "block" }; - case "tbody": - return { type: "block" }; - case "td": // TODO - return { type: "block" }; - case "tfoot": - return { type: "block" }; - case "thead": - return { type: "block" }; - case "time": - return { type: "inline" }; - case "title": // TODO - return { type: "inline" }; - case "tr": - return { type: "block" }; - case "ul": - return { type: "block" }; - default: - throw new DocBookError(`[html.write] Unknown element name: ${name}`); - } -} - -function isInlineNode(n: Node): boolean { - return n.kind === "text" || getDtd(n.name).type === "inline"; -} - -function isBlockNode(n: Node): boolean { - return !isInlineNode(n); -} - -function toHtmlText(dom: Document): string { - return `\n` + nodeToHtmlText(dom.root, { - indentLevel: -1, - isInPre: false, - }); -} - -function nodeToHtmlText(n: Node, ctx: Context): string { - if (n.kind === "text") { - if (n.raw) { - return n.content; - } else { - return textNodeToHtmlText(n, ctx); - } - } else { - return elementNodeToHtmlText(n, ctx); - } -} - -function textNodeToHtmlText(t: Text, ctx: Context): string { - const s = encodeSpecialCharacters(t.content); - if (ctx.isInPre) return s; - - // TODO: 日本語で改行するときはスペースを入れない - return s - .trim() - .replaceAll(/\n */g, " "); -} - -function encodeSpecialCharacters(s: string): string { - return s.replaceAll(/&(?!\w+;)/g, "&") - .replaceAll(//g, ">") - .replaceAll(/'/g, "'") - .replaceAll(/"/g, """); -} - -function elementNodeToHtmlText(e: Element, ctx: Context): string { - const dtd = getDtd(e.name); - - let s = ""; - if (e.name !== "__root__") { - if (dtd.type === "block") { - s += indent(ctx); - } - s += `<${e.name}`; - const attributes = getElementAttributes(e); - if (attributes.length > 0) { - s += " "; - for (let i = 0; i < attributes.length; i++) { - const [name, value] = attributes[i]; - s += `${name}="${value}"`; - if (i !== attributes.length - 1) { - s += " "; - } - } - } - s += ">"; - if (dtd.type === "block" && e.name !== "pre") { - s += "\n"; - } - } - ctx.indentLevel += 1; - - let prevChild: Node | null = null; - if (e.name === "pre") { - ctx.isInPre = true; - } - forEachChild(e, (c) => { - if (dtd.type === "block" && !ctx.isInPre) { - if (isInlineNode(c)) { - if (needsIndent(prevChild)) { - s += indent(ctx); - } - } else { - if (needsLineBreak(prevChild)) { - s += "\n"; - } - } - } - s += nodeToHtmlText(c, ctx); - prevChild = c; - }); - if (e.name === "pre") { - ctx.isInPre = false; - } - - ctx.indentLevel -= 1; - if (e.name !== "__root__" && !dtd.auto_closing) { - if (e.name !== "pre") { - if (dtd.type === "block") { - if (needsLineBreak(prevChild)) { - s += "\n"; - } - s += indent(ctx); - } - } - s += ``; - if (dtd.type === "block") { - s += "\n"; - } - } - return s; -} - -function indent(ctx: Context): string { - return " ".repeat(ctx.indentLevel); -} - -function getElementAttributes(e: Element): [string, string][] { - return [...e.attributes.entries()] - .filter((a) => !a[0].startsWith("--")) - .sort( - (a, b) => { - // Special rules: - if (e.name === "meta") { - if (a[0] === "content" && b[0] === "name") { - return 1; - } - if (a[0] === "name" && b[0] === "content") { - return -1; - } - } - if (e.name === "link") { - if (a[0] === "href" && b[0] === "rel") { - return 1; - } - if (a[0] === "rel" && b[0] === "href") { - return -1; - } - if (a[0] === "href" && b[0] === "type") { - return 1; - } - if (a[0] === "type" && b[0] === "href") { - return -1; - } - } - // General rules: - if (a[0] > b[0]) return 1; - else if (a[0] < b[0]) return -1; - else return 0; - }, - ); -} - -function needsIndent(prevChild: Node | null): boolean { - return !prevChild || isBlockNode(prevChild); -} - -function needsLineBreak(prevChild: Node | null): boolean { - return !needsIndent(prevChild); -} diff --git a/nuldoc-src/page.ts b/nuldoc-src/page.ts new file mode 100644 index 0000000..970265e --- /dev/null +++ b/nuldoc-src/page.ts @@ -0,0 +1,8 @@ +import { Element } from "./dom.ts"; +import { RendererType } from "./render.ts"; + +export type Page = { + root: Element; + renderer: RendererType; + destFilePath: string; +}; diff --git a/nuldoc-src/render.ts b/nuldoc-src/render.ts new file mode 100644 index 0000000..feb72a4 --- /dev/null +++ b/nuldoc-src/render.ts @@ -0,0 +1,13 @@ +import { Node } from "./dom.ts"; +import { renderHtml } from "./renderers/html.ts"; + +// export type RendererType = "html" | "xml"; +export type RendererType = "html"; + +export function render(root: Node, renderer: RendererType): string { + if (renderer === "html") { + return renderHtml(root); + } else { + return renderHtml(root); + } +} diff --git a/nuldoc-src/renderers/html.ts b/nuldoc-src/renderers/html.ts new file mode 100644 index 0000000..3c49174 --- /dev/null +++ b/nuldoc-src/renderers/html.ts @@ -0,0 +1,255 @@ +import { Element, forEachChild, Node, Text } from "../dom.ts"; +import { DocBookError } from "../errors.ts"; + +export function renderHtml(root: Node): string { + return `\n` + nodeToHtmlText(root, { + indentLevel: -1, + isInPre: false, + }); +} + +type Context = { + indentLevel: number; + isInPre: boolean; +}; + +type Dtd = { type: "block" | "inline"; auto_closing?: boolean }; + +function getDtd(name: string): Dtd { + switch (name) { + case "__root__": + return { type: "block" }; + case "a": + return { type: "inline" }; + case "article": + return { type: "block" }; + case "blockquote": + return { type: "block" }; + case "body": + return { type: "block" }; + case "br": + return { type: "block", auto_closing: true }; + case "code": + return { type: "inline" }; + case "div": + return { type: "block" }; + case "em": + return { type: "inline" }; + case "footer": + return { type: "block" }; + case "h1": + return { type: "inline" }; + case "h2": + return { type: "inline" }; + case "h3": + return { type: "inline" }; + case "h4": + return { type: "inline" }; + case "h5": + return { type: "inline" }; + case "h6": + return { type: "inline" }; + case "head": + return { type: "block" }; + case "header": + return { type: "block" }; + case "hr": + return { type: "block" }; + case "html": + return { type: "block" }; + case "li": + return { type: "block" }; + case "link": + return { type: "block", auto_closing: true }; + case "main": + return { type: "block" }; + case "meta": + return { type: "block", auto_closing: true }; + case "nav": + return { type: "block" }; + case "ol": + return { type: "block" }; + case "p": + return { type: "block" }; + case "pre": + return { type: "block" }; + case "section": + return { type: "block" }; + case "span": + return { type: "inline" }; + case "table": + return { type: "block" }; + case "tbody": + return { type: "block" }; + case "td": // TODO + return { type: "block" }; + case "tfoot": + return { type: "block" }; + case "thead": + return { type: "block" }; + case "time": + return { type: "inline" }; + case "title": // TODO + return { type: "inline" }; + case "tr": + return { type: "block" }; + case "ul": + return { type: "block" }; + default: + throw new DocBookError(`[html.write] Unknown element name: ${name}`); + } +} + +function isInlineNode(n: Node): boolean { + return n.kind === "text" || getDtd(n.name).type === "inline"; +} + +function isBlockNode(n: Node): boolean { + return !isInlineNode(n); +} + +function nodeToHtmlText(n: Node, ctx: Context): string { + if (n.kind === "text") { + if (n.raw) { + return n.content; + } else { + return textNodeToHtmlText(n, ctx); + } + } else { + return elementNodeToHtmlText(n, ctx); + } +} + +function textNodeToHtmlText(t: Text, ctx: Context): string { + const s = encodeSpecialCharacters(t.content); + if (ctx.isInPre) return s; + + // TODO: 日本語で改行するときはスペースを入れない + return s + .trim() + .replaceAll(/\n */g, " "); +} + +function encodeSpecialCharacters(s: string): string { + return s.replaceAll(/&(?!\w+;)/g, "&") + .replaceAll(//g, ">") + .replaceAll(/'/g, "'") + .replaceAll(/"/g, """); +} + +function elementNodeToHtmlText(e: Element, ctx: Context): string { + const dtd = getDtd(e.name); + + let s = ""; + if (e.name !== "__root__") { + if (dtd.type === "block") { + s += indent(ctx); + } + s += `<${e.name}`; + const attributes = getElementAttributes(e); + if (attributes.length > 0) { + s += " "; + for (let i = 0; i < attributes.length; i++) { + const [name, value] = attributes[i]; + s += `${name}="${value}"`; + if (i !== attributes.length - 1) { + s += " "; + } + } + } + s += ">"; + if (dtd.type === "block" && e.name !== "pre") { + s += "\n"; + } + } + ctx.indentLevel += 1; + + let prevChild: Node | null = null; + if (e.name === "pre") { + ctx.isInPre = true; + } + forEachChild(e, (c) => { + if (dtd.type === "block" && !ctx.isInPre) { + if (isInlineNode(c)) { + if (needsIndent(prevChild)) { + s += indent(ctx); + } + } else { + if (needsLineBreak(prevChild)) { + s += "\n"; + } + } + } + s += nodeToHtmlText(c, ctx); + prevChild = c; + }); + if (e.name === "pre") { + ctx.isInPre = false; + } + + ctx.indentLevel -= 1; + if (e.name !== "__root__" && !dtd.auto_closing) { + if (e.name !== "pre") { + if (dtd.type === "block") { + if (needsLineBreak(prevChild)) { + s += "\n"; + } + s += indent(ctx); + } + } + s += ``; + if (dtd.type === "block") { + s += "\n"; + } + } + return s; +} + +function indent(ctx: Context): string { + return " ".repeat(ctx.indentLevel); +} + +function getElementAttributes(e: Element): [string, string][] { + return [...e.attributes.entries()] + .filter((a) => !a[0].startsWith("--")) + .sort( + (a, b) => { + // Special rules: + if (e.name === "meta") { + if (a[0] === "content" && b[0] === "name") { + return 1; + } + if (a[0] === "name" && b[0] === "content") { + return -1; + } + } + if (e.name === "link") { + if (a[0] === "href" && b[0] === "rel") { + return 1; + } + if (a[0] === "rel" && b[0] === "href") { + return -1; + } + if (a[0] === "href" && b[0] === "type") { + return 1; + } + if (a[0] === "type" && b[0] === "href") { + return -1; + } + } + // General rules: + if (a[0] > b[0]) return 1; + else if (a[0] < b[0]) return -1; + else return 0; + }, + ); +} + +function needsIndent(prevChild: Node | null): boolean { + return !prevChild || isBlockNode(prevChild); +} + +function needsLineBreak(prevChild: Node | null): boolean { + return !needsIndent(prevChild); +} -- cgit v1.2.3-70-g09d2