From 12035272d44d92cd2360aeff88d499db67fe1949 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 18 Mar 2023 18:09:50 +0900 Subject: refactor: use Page instead of Document --- nuldoc-src/commands/build.ts | 145 ++++++++------------------ nuldoc-src/config.ts | 1 - nuldoc-src/docbook/document.ts | 56 ++--------- nuldoc-src/page.ts | 5 +- nuldoc-src/pages/about.ts | 113 +++++++++++++++++++++ nuldoc-src/pages/post.ts | 207 ++++++++++++++++++++++++++++++++++++++ nuldoc-src/pages/post_list.ts | 152 ++++++++++++++++++++++++++++ nuldoc-src/pages/tag.ts | 146 +++++++++++++++++++++++++++ nuldoc-src/pages/utils.ts | 51 ++++++++++ nuldoc-src/templates/about.ts | 115 --------------------- nuldoc-src/templates/post.ts | 174 -------------------------------- nuldoc-src/templates/post_list.ts | 153 ---------------------------- nuldoc-src/templates/tag.ts | 147 --------------------------- nuldoc-src/templates/utils.ts | 51 ---------- 14 files changed, 725 insertions(+), 791 deletions(-) create mode 100644 nuldoc-src/pages/about.ts create mode 100644 nuldoc-src/pages/post.ts create mode 100644 nuldoc-src/pages/post_list.ts create mode 100644 nuldoc-src/pages/tag.ts create mode 100644 nuldoc-src/pages/utils.ts delete mode 100644 nuldoc-src/templates/about.ts delete mode 100644 nuldoc-src/templates/post.ts delete mode 100644 nuldoc-src/templates/post_list.ts delete mode 100644 nuldoc-src/templates/tag.ts delete mode 100644 nuldoc-src/templates/utils.ts (limited to 'nuldoc-src') diff --git a/nuldoc-src/commands/build.ts b/nuldoc-src/commands/build.ts index 90aaab9..ef529e0 100644 --- a/nuldoc-src/commands/build.ts +++ b/nuldoc-src/commands/build.ts @@ -3,27 +3,32 @@ 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 { 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"; -import generateAbout from "../templates/about.ts"; +import { generateAboutPage } from "../pages/about.ts"; +import { + generatePostPage, + getPostCreatedDate, + PostPage, +} from "../pages/post.ts"; +import { generatePostListPage } from "../pages/post_list.ts"; +import { generateTagPage } from "../pages/tag.ts"; export async function runBuildCommand(config: Config) { - const posts = await generatePosts(config); - await generateTags(posts, config); - await generatePostList(posts, config); - await generateAboutPage(config); + const posts = await buildPostPages(config); + await buildPostListPage(posts, config); + await buildTagPages(posts, config); + await buildAboutPage(config); await copyStaticFiles(config); } -async function generatePosts(config: Config) { +async function buildPostPages(config: Config) { const sourceDir = join(Deno.cwd(), config.locations.contentDir, "posts"); const postFiles = await collectPostFiles(sourceDir); const posts = await parsePosts(postFiles, config); - await outputPosts(posts, config); + for (const post of posts) { + await writePage(post, config); + } return posts; } @@ -39,70 +44,35 @@ async function collectPostFiles(sourceDir: string): Promise { async function parsePosts( postFiles: string[], config: Config, -): Promise { +): Promise { const posts = []; for (const postFile of postFiles) { posts.push( - await convertPost(await parseDocBookFile(postFile, config), config), + await generatePostPage(await parseDocBookFile(postFile, config), config), ); } return posts; } -async function outputPosts(posts: Document[], config: Config) { - const cwd = Deno.cwd(); - const contentDir = join(cwd, config.locations.contentDir); - const destDir = join(cwd, config.locations.destDir); - for (const post of posts) { - const destFilePath = join( - post.sourceFilePath.replace(contentDir, destDir).replace(".xml", ""), - "index.html", - ); - await ensureDir(dirname(destFilePath)); - await writePage(docToPage(post, destFilePath)); - } -} - -async function generatePostList(posts: Document[], config: Config) { - const postList = await buildPostListDoc(posts, config); - await outputPostList(postList, config); -} - -async function buildPostListDoc( - posts: Document[], - config: Config, -): Promise { - return await convertPostList(posts, config); -} - -async function outputPostList(postList: Document, config: Config) { - const cwd = Deno.cwd(); - const destDir = join(cwd, config.locations.destDir); - const destFilePath = join(destDir, "posts", "index.html"); - await ensureDir(dirname(destFilePath)); - await writePage(docToPage(postList, destFilePath)); -} - -async function generateAboutPage(config: Config) { - const aboutDoc = await generateAbout(config); - await outputAboutPage(aboutDoc, config); +async function buildPostListPage(posts: PostPage[], config: Config) { + const postListPage = await generatePostListPage(posts, config); + await writePage(postListPage, config); } -async function outputAboutPage(about: Document, config: Config) { - const cwd = Deno.cwd(); - const destDir = join(cwd, config.locations.destDir); - const destFilePath = join(destDir, "about", "index.html"); - await ensureDir(dirname(destFilePath)); - await writePage(docToPage(about, destFilePath)); +async function buildAboutPage(config: Config) { + const aboutPage = await generateAboutPage(config); + await writePage(aboutPage, config); } -async function generateTags(posts: Document[], config: Config) { +async function buildTagPages(posts: PostPage[], config: Config) { const tagsAndPosts = collectTags(posts); - const tagDocs = await buildTagDocs(tagsAndPosts, config); - await outputTags(tagDocs, config); + for (const [tag, posts] of tagsAndPosts) { + const tagPage = await generateTagPage(tag, posts, config); + await writePage(tagPage, config); + } } -function collectTags(posts: Document[]): [string, Document[]][] { +function collectTags(posts: PostPage[]): [string, PostPage[]][] { const tagsAndPosts = new Map(); for (const post of posts) { for (const tag of post.tags) { @@ -113,13 +83,13 @@ function collectTags(posts: Document[]): [string, Document[]][] { } } - const result: [string, Document[]][] = []; + const result: [string, PostPage[]][] = []; for (const tag of Array.from(tagsAndPosts.keys()).sort()) { result.push([ tag, - tagsAndPosts.get(tag).sort((a: Document, b: Document) => { - const ta = a.getCreatedDate(); - const tb = b.getCreatedDate(); + tagsAndPosts.get(tag).sort((a: PostPage, b: PostPage) => { + const ta = getPostCreatedDate(a); + const tb = getPostCreatedDate(b); if (ta > tb) return -1; if (ta < tb) return 1; return 0; @@ -129,35 +99,6 @@ function collectTags(posts: Document[]): [string, Document[]][] { return result; } -async function buildTagDocs( - tagsAndPosts: [string, Document[]][], - config: Config, -): Promise<[string, Document][]> { - const docs: [string, Document][] = []; - for (const [tag, posts] of tagsAndPosts) { - docs.push([tag, await buildTagDoc(tag, posts, config)]); - } - return docs; -} - -async function buildTagDoc( - tag: string, - posts: Document[], - config: Config, -): Promise { - return await convertTag(tag, posts, config); -} - -async function outputTags(tagDocs: [string, Document][], config: Config) { - const cwd = Deno.cwd(); - const destDir = join(cwd, config.locations.destDir); - for (const [tag, tagDoc] of tagDocs) { - const destFilePath = join(destDir, "tags", tag, "index.html"); - await ensureDir(dirname(destFilePath)); - await writePage(docToPage(tagDoc, destFilePath)); - } -} - async function copyStaticFiles(config: Config) { const globPattern = joinGlobs([Deno.cwd(), config.locations.staticDir, "*"]); for await (const entry of expandGlob(globPattern)) { @@ -170,14 +111,12 @@ async function copyStaticFiles(config: Config) { } } -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, - }; +async function writePage(page: Page, config: Config) { + const destFilePath = join( + Deno.cwd(), + config.locations.destDir, + page.destFilePath, + ); + await ensureDir(dirname(destFilePath)); + await Deno.writeTextFile(destFilePath, render(page.root, page.renderer)); } diff --git a/nuldoc-src/config.ts b/nuldoc-src/config.ts index 74521b3..91b4844 100644 --- a/nuldoc-src/config.ts +++ b/nuldoc-src/config.ts @@ -3,7 +3,6 @@ export const config = { contentDir: "/content", destDir: "/public", staticDir: "/static", - templateDir: "/templates", }, rendering: { html: { diff --git a/nuldoc-src/docbook/document.ts b/nuldoc-src/docbook/document.ts index ae3159a..9f3221b 100644 --- a/nuldoc-src/docbook/document.ts +++ b/nuldoc-src/docbook/document.ts @@ -9,7 +9,7 @@ import { innerText, } from "../dom.ts"; -export class Document { +export type Document = { root: Element; sourceFilePath: string; link: string; @@ -17,41 +17,7 @@ export class Document { summary: string; // TODO: should it be markup text? tags: string[]; revisions: Revision[]; - - constructor( - root: Element, - sourceFilePath: string, - link: string, - title: string, - summary: string, - tags: string[], - revisions: Revision[], - ) { - this.root = root; - this.sourceFilePath = sourceFilePath; - this.link = link; - this.title = title; - this.summary = summary; - this.tags = tags; - this.revisions = revisions; - } - - getLatestRevision(): Revision { - return this.revisions[this.revisions.length - 1]; - } - - getOldestRevision(): Revision { - return this.revisions[0]; - } - - getUpdatedDate(): string { - return this.getLatestRevision().date; - } - - getCreatedDate(): string { - return this.getOldestRevision().date; - } -} +}; export function createNewDocumentFromRootElement( root: Element, @@ -130,13 +96,13 @@ export function createNewDocumentFromRootElement( const cwd = Deno.cwd(); const contentDir = join(cwd, config.locations.contentDir); const link = sourceFilePath.replace(contentDir, "").replace(".xml", "/"); - return new Document( - root, - sourceFilePath, - link, - title, - summary, - tags, - revisions, - ); + return { + root: root, + sourceFilePath: sourceFilePath, + link: link, + title: title, + summary: summary, + tags: tags, + revisions: revisions, + }; } diff --git a/nuldoc-src/page.ts b/nuldoc-src/page.ts index 970265e..f4a6166 100644 --- a/nuldoc-src/page.ts +++ b/nuldoc-src/page.ts @@ -1,8 +1,9 @@ import { Element } from "./dom.ts"; import { RendererType } from "./render.ts"; -export type Page = { +export interface Page { root: Element; renderer: RendererType; destFilePath: string; -}; + href: string; +} diff --git a/nuldoc-src/pages/about.ts b/nuldoc-src/pages/about.ts new file mode 100644 index 0000000..bba4031 --- /dev/null +++ b/nuldoc-src/pages/about.ts @@ -0,0 +1,113 @@ +import { Config } from "../config.ts"; +import { Page } from "../page.ts"; +import { + el, + linkElement, + metaElement, + stylesheetLinkElement, + text, +} from "./utils.ts"; + +export type AboutPage = Page; + +export async function generateAboutPage(config: Config): Promise { + const head = el( + "head", + [], + metaElement([["charset", "UTF-8"]]), + metaElement([["name", "viewport"], [ + "content", + "width=device-width, initial-scale=1.0", + ]]), + metaElement([["name", "author"], ["content", config.blog.author]]), + metaElement([["name", "copyright"], [ + "content", + `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, + ]]), + metaElement([["name", "description"], [ + "content", + "このサイトの著者について", + ]]), + linkElement("icon", "/favicon.svg", "image/svg+xml"), + el("title", [], text(`About | ${config.blog.siteName}`)), + await stylesheetLinkElement("/style.css", config), + ); + const body = el( + "body", + [["class", "single"]], + el( + "header", + [["class", "header"]], + el( + "nav", + [["class", "nav"]], + el( + "ul", + [], + el( + "li", + [["class", "logo"]], + el("a", [["href", "/"]], text(config.blog.siteName)), + ), + el( + "li", + [], + el("a", [["href", "/about"]], text("About")), + ), + el( + "li", + [], + el("a", [["href", "/posts"]], text("Posts")), + ), + el( + "li", + [], + el("a", [["href", "/slides"]], text("Slides")), + ), + ), + ), + ), + el( + "main", + [["class", "main"]], + el( + "article", + [["class", "post-single"]], + el( + "header", + [["class", "post-header"]], + el( + "h1", + [["class", "post-title"]], + text("About"), + ), + ), + el( + "div", + [["class", "post-content"]], + text("WIP"), + ), + ), + ), + el( + "footer", + [["class", "footer"]], + text( + `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, + ), + ), + ); + const html = el( + "html", + [["lang", "ja-JP"]], + head, + body, + ); + + return { + root: el("__root__", [], html), + renderer: "html", + destFilePath: "/about/index.html", + href: "/about/", + }; +} diff --git a/nuldoc-src/pages/post.ts b/nuldoc-src/pages/post.ts new file mode 100644 index 0000000..a431c37 --- /dev/null +++ b/nuldoc-src/pages/post.ts @@ -0,0 +1,207 @@ +import { join } from "std/path/mod.ts"; +import { Config } from "../config.ts"; +import { Element } from "../dom.ts"; +import { Document } from "../docbook/document.ts"; +import { Page } from "../page.ts"; +import { Revision } from "../revision.ts"; +import { + el, + linkElement, + metaElement, + stylesheetLinkElement, + text, +} from "./utils.ts"; + +export interface PostPage extends Page { + title: string; + summary: string; + tags: string[]; + revisions: Revision[]; +} + +export function getPostCreatedDate(page: { revisions: Revision[] }): string { + return page.revisions[0].date; +} + +export function getPostUpdatedDate(page: { revisions: Revision[] }): string { + return page.revisions[page.revisions.length - 1].date; +} + +export async function generatePostPage( + doc: Document, + config: Config, +): Promise { + const headChildren = [ + metaElement([["charset", "UTF-8"]]), + metaElement([["name", "viewport"], [ + "content", + "width=device-width, initial-scale=1.0", + ]]), + metaElement([["name", "author"], ["content", config.blog.author]]), + metaElement([["name", "copyright"], [ + "content", + `© ${getPostCreatedDate(doc).substring(0, 4)} ${config.blog.author}`, + ]]), + metaElement([["name", "description"], ["content", doc.summary]]), + ]; + if (doc.tags.length !== 0) { + headChildren.push( + metaElement([["name", "keywords"], [ + "content", + doc.tags.map((slug) => + (config.blog.tagLabels as { [key: string]: string })[slug] + ).join(","), + ]]), + ); + } + headChildren.push(linkElement("icon", "/favicon.svg", "image/svg+xml")); + headChildren.push( + el("title", [], text(`${doc.title} | ${config.blog.siteName}`)), + ); + headChildren.push(await stylesheetLinkElement("/style.css", config)); + headChildren.push(await stylesheetLinkElement("/hl.css", config)); + const head = el("head", [], ...headChildren); + const body = el( + "body", + [["class", "single"]], + el( + "header", + [["class", "header"]], + el( + "nav", + [["class", "nav"]], + el( + "ul", + [], + el( + "li", + [["class", "logo"]], + el("a", [["href", "/"]], text(config.blog.siteName)), + ), + el( + "li", + [], + el("a", [["href", "/about"]], text("About")), + ), + el( + "li", + [], + el("a", [["href", "/posts"]], text("Posts")), + ), + el( + "li", + [], + el("a", [["href", "/slides"]], text("Slides")), + ), + ), + ), + ), + el( + "main", + [["class", "main"]], + el( + "article", + [["class", "post-single"]], + el( + "header", + [["class", "post-header"]], + el( + "h1", + [["class", "post-title"]], + text(doc.title), + ), + ...(doc.tags.length === 0 ? [] : [ + el( + "ul", + [["class", "post-tags"]], + ...doc.tags.map((slug) => + el( + "li", + [["class", "tag"]], + el( + "a", + [["href", `/tags/${slug}`]], + text( + (config.blog.tagLabels as { + [key: string]: string; + })[slug], + ), + ), + ) + ), + ), + ]), + ), + el( + "div", + [["class", "post-content"]], + el( + "section", + [], + el( + "h2", + [["id", "changelog"]], + text("更新履歴"), + ), + el( + "ol", + [], + ...doc.revisions.map((rev) => + el( + "li", + [["class", "revision"]], + el( + "time", + [["datetime", rev.date]], + text(rev.date), + ), + text(`: ${rev.remark}`), + ) + ), + ), + ), + // TODO: refactor + ...(doc.root.children[0] as Element).children, + // TODO: footnotes + //
+ // <% for footnote in footnotes %> + //
+ // <%= footnote.index %>. <%= footnote.text %> + //
+ // <% end %> + //
+ ), + ), + ), + el( + "footer", + [["class", "footer"]], + text( + `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, + ), + ), + ); + const html = el( + "html", + [["lang", "ja-JP"]], + head, + body, + ); + + const cwd = Deno.cwd(); + const contentDir = join(cwd, config.locations.contentDir); + const destFilePath = join( + doc.sourceFilePath.replace(contentDir, "").replace(".xml", ""), + "index.html", + ); + return { + root: el("__root__", [], html), + renderer: "html", + destFilePath: destFilePath, + href: destFilePath.replace("index.html", ""), + title: doc.title, + summary: doc.summary, + tags: doc.tags, + revisions: doc.revisions, + }; +} diff --git a/nuldoc-src/pages/post_list.ts b/nuldoc-src/pages/post_list.ts new file mode 100644 index 0000000..e22316e --- /dev/null +++ b/nuldoc-src/pages/post_list.ts @@ -0,0 +1,152 @@ +import { Config } from "../config.ts"; +import { Page } from "../page.ts"; +import { getPostCreatedDate, getPostUpdatedDate, PostPage } from "./post.ts"; +import { + el, + linkElement, + metaElement, + stylesheetLinkElement, + text, +} from "./utils.ts"; + +export type PostListPage = Page; + +export async function generatePostListPage( + posts: PostPage[], + config: Config, +): Promise { + const pageTitle = "投稿一覧"; + + const head = el( + "head", + [], + metaElement([["charset", "UTF-8"]]), + metaElement([["name", "viewport"], [ + "content", + "width=device-width, initial-scale=1.0", + ]]), + metaElement([["name", "author"], ["content", config.blog.author]]), + metaElement([["name", "copyright"], [ + "content", + `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, + ]]), + metaElement([["name", "description"], ["content", "投稿した記事の一覧"]]), + linkElement("icon", "/favicon.svg", "image/svg+xml"), + el("title", [], text(`${pageTitle} | ${config.blog.siteName}`)), + await stylesheetLinkElement("/style.css", config), + ); + const body = el( + "body", + [["class", "list"]], + el( + "header", + [["class", "header"]], + el( + "nav", + [["class", "nav"]], + el( + "ul", + [], + el( + "li", + [["class", "logo"]], + el("a", [["href", "/"]], text(config.blog.siteName)), + ), + el( + "li", + [], + el("a", [["href", "/about"]], text("About")), + ), + el( + "li", + [], + el("a", [["href", "/posts"]], text("Posts")), + ), + el( + "li", + [], + el("a", [["href", "/slides"]], text("Slides")), + ), + ), + ), + ), + el( + "main", + [["class", "main"]], + el( + "header", + [["class", "page-header"]], + el( + "h1", + [], + text(pageTitle), + ), + ), + ...Array.from(posts).sort((a, b) => { + const ta = getPostCreatedDate(a); + const tb = getPostCreatedDate(b); + if (ta > tb) return -1; + if (ta < tb) return 1; + return 0; + }).map((post) => + el( + "article", + [["class", "post-entry"]], + el( + "a", + [["href", post.href]], + el( + "header", + [["class", "entry-header"]], + el("h2", [], text(post.title)), + ), + el( + "section", + [["class", "entry-content"]], + el("p", [], text(post.summary)), + ), + el( + "footer", + [["class", "entry-footer"]], + text("Posted on"), + el( + "time", + [["datetime", getPostCreatedDate(post)]], + text(getPostCreatedDate(post)), + ), + ...(post.revisions.length > 1 + ? [ + text(", updated on "), + el("time", [[ + "datetime", + getPostUpdatedDate(post), + ]], text(getPostUpdatedDate(post))), + ] + : []), + ), + ), + ) + ), + ), + el( + "footer", + [["class", "footer"]], + text( + `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, + ), + ), + ); + const html = el( + "html", + [["lang", "ja-JP"]], + head, + body, + ); + + return { + root: el("__root__", [], html), + renderer: "html", + destFilePath: "/posts/index.html", + href: "/posts/", + }; +} diff --git a/nuldoc-src/pages/tag.ts b/nuldoc-src/pages/tag.ts new file mode 100644 index 0000000..51aedcd --- /dev/null +++ b/nuldoc-src/pages/tag.ts @@ -0,0 +1,146 @@ +import { Config } from "../config.ts"; +import { Page } from "../page.ts"; +import { getPostCreatedDate, getPostUpdatedDate, PostPage } from "./post.ts"; +import { + el, + linkElement, + metaElement, + stylesheetLinkElement, + text, +} from "./utils.ts"; + +export type TagPage = Page; + +export async function generateTagPage( + tagSlug: string, + posts: PostPage[], + config: Config, +): Promise { + const tagLabel = + (config.blog.tagLabels as { [key: string]: string })[tagSlug]; + const pageTitle = `タグ「${tagLabel}」一覧`; + + const headChildren = [ + metaElement([["charset", "UTF-8"]]), + metaElement([["name", "viewport"], [ + "content", + "width=device-width, initial-scale=1.0", + ]]), + metaElement([["name", "author"], ["content", config.blog.author]]), + metaElement([["name", "copyright"], [ + "content", + `© ${ + getPostCreatedDate(posts[posts.length - 1]).substring(0, 4) + } ${config.blog.author}`, + ]]), + metaElement([["name", "description"], [ + "content", + `タグ「${tagLabel}」のついた記事一覧`, + ]]), + metaElement([["name", "keywords"], ["content", tagLabel]]), + linkElement("icon", "/favicon.svg", "image/svg+xml"), + el("title", [], text(`${pageTitle} | ${config.blog.siteName}`)), + await stylesheetLinkElement("/style.css", config), + ]; + const head = el("head", [], ...headChildren); + const body = el( + "body", + [["class", "list"]], + el( + "header", + [["class", "header"]], + el( + "nav", + [["class", "nav"]], + el( + "ul", + [], + el( + "li", + [["class", "logo"]], + el("a", [["href", "/"]], text(config.blog.siteName)), + ), + el( + "li", + [], + el("a", [["href", "/about"]], text("About")), + ), + el( + "li", + [], + el("a", [["href", "/posts"]], text("Posts")), + ), + el( + "li", + [], + el("a", [["href", "/slides"]], text("Slides")), + ), + ), + ), + ), + el( + "main", + [["class", "main"]], + el("header", [["class", "page-header"]], el("h1", [], text(pageTitle))), + ...posts.map((post) => + el( + "article", + [["class", "post-entry"]], + el( + "a", + [["href", post.href]], + el( + "header", + [["class", "entry-header"]], + el("h2", [], text(post.title)), + ), + el( + "section", + [["class", "entry-content"]], + el("p", [], text(post.summary)), + ), + el( + "footer", + [["class", "entry-footer"]], + text("Posted on"), + el( + "time", + [["datetime", getPostCreatedDate(post)]], + text(getPostCreatedDate(post)), + ), + ...(post.revisions.length > 1 + ? [ + text(", updated on "), + el("time", [[ + "datetime", + getPostUpdatedDate(post), + ]], text(getPostUpdatedDate(post))), + ] + : []), + ), + ), + ) + ), + ), + el( + "footer", + [["class", "footer"]], + text( + `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, + ), + ), + ); + const html = el( + "html", + [["lang", "ja-JP"]], + head, + body, + ); + + return { + root: el("__root__", [], html), + renderer: "html", + destFilePath: `/tags/${tagSlug}/index.html`, + href: `/tags/${tagSlug}/`, + }; +} diff --git a/nuldoc-src/pages/utils.ts b/nuldoc-src/pages/utils.ts new file mode 100644 index 0000000..018c460 --- /dev/null +++ b/nuldoc-src/pages/utils.ts @@ -0,0 +1,51 @@ +import { join } from "std/path/mod.ts"; +import { crypto, toHashString } from "std/crypto/mod.ts"; +import { Element, Node, Text } from "../dom.ts"; +import { Config } from "../config.ts"; + +export function text(content: string): Text { + return { + kind: "text", + content: content, + raw: false, + }; +} + +export function el( + name: string, + attrs: [string, string][], + ...children: Node[] +): Element { + return { + kind: "element", + name: name, + attributes: new Map(attrs), + children: children, + }; +} + +export async function stylesheetLinkElement( + fileName: string, + config: Config, +): Promise { + const filePath = join(Deno.cwd(), config.locations.staticDir, fileName); + const content = (await Deno.readFile(filePath)).buffer; + const hash = toHashString(await crypto.subtle.digest("MD5", content), "hex"); + return el("link", [["rel", "stylesheet"], ["href", `${fileName}?h=${hash}`]]); +} + +export function metaElement(attrs: [string, string][]): Element { + return el("meta", attrs); +} + +export function linkElement( + rel: string, + href: string, + type: string | null, +): Element { + const attrs: [string, string][] = [["rel", rel], ["href", href]]; + if (type !== null) { + attrs.push(["type", type]); + } + return el("link", attrs); +} diff --git a/nuldoc-src/templates/about.ts b/nuldoc-src/templates/about.ts deleted file mode 100644 index 2f20aa9..0000000 --- a/nuldoc-src/templates/about.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Document } from "../docbook/document.ts"; -import { Config } from "../config.ts"; -import { - el, - linkElement, - metaElement, - stylesheetLinkElement, - text, -} from "./utils.ts"; - -export default async function generateAbout( - config: Config, -): Promise { - const head = el( - "head", - [], - metaElement([["charset", "UTF-8"]]), - metaElement([["name", "viewport"], [ - "content", - "width=device-width, initial-scale=1.0", - ]]), - metaElement([["name", "author"], ["content", config.blog.author]]), - metaElement([["name", "copyright"], [ - "content", - `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, - ]]), - metaElement([["name", "description"], [ - "content", - "このサイトの著者について", - ]]), - linkElement("icon", "/favicon.svg", "image/svg+xml"), - el("title", [], text(`About | ${config.blog.siteName}`)), - await stylesheetLinkElement("/style.css", config), - ); - const body = el( - "body", - [["class", "single"]], - el( - "header", - [["class", "header"]], - el( - "nav", - [["class", "nav"]], - el( - "ul", - [], - el( - "li", - [["class", "logo"]], - el("a", [["href", "/"]], text(config.blog.siteName)), - ), - el( - "li", - [], - el("a", [["href", "/about"]], text("About")), - ), - el( - "li", - [], - el("a", [["href", "/posts"]], text("Posts")), - ), - el( - "li", - [], - el("a", [["href", "/slides"]], text("Slides")), - ), - ), - ), - ), - el( - "main", - [["class", "main"]], - el( - "article", - [["class", "post-single"]], - el( - "header", - [["class", "post-header"]], - el( - "h1", - [["class", "post-title"]], - text("About"), - ), - ), - el( - "div", - [["class", "post-content"]], - text("WIP"), - ), - ), - ), - el( - "footer", - [["class", "footer"]], - text( - `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, - ), - ), - ); - const html = el( - "html", - [["lang", "ja-JP"]], - head, - body, - ); - return new Document( - el("__root__", [], html), - "", - "/about/", - "About", - "このサイトの著者について", - [], - [], - ); -} diff --git a/nuldoc-src/templates/post.ts b/nuldoc-src/templates/post.ts deleted file mode 100644 index 51b9aa9..0000000 --- a/nuldoc-src/templates/post.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Element } from "../dom.ts"; -import { Document } from "../docbook/document.ts"; -import { Config } from "../config.ts"; -import { - el, - linkElement, - metaElement, - stylesheetLinkElement, - text, -} from "./utils.ts"; - -export default async function convertPost( - doc: Document, - config: Config, -): Promise { - const headChildren = [ - metaElement([["charset", "UTF-8"]]), - metaElement([["name", "viewport"], [ - "content", - "width=device-width, initial-scale=1.0", - ]]), - metaElement([["name", "author"], ["content", config.blog.author]]), - metaElement([["name", "copyright"], [ - "content", - `© ${doc.getCreatedDate().substring(0, 4)} ${config.blog.author}`, - ]]), - metaElement([["name", "description"], ["content", doc.summary]]), - ]; - if (doc.tags.length !== 0) { - headChildren.push( - metaElement([["name", "keywords"], [ - "content", - doc.tags.map((slug) => - (config.blog.tagLabels as { [key: string]: string })[slug] - ).join(","), - ]]), - ); - } - headChildren.push(linkElement("icon", "/favicon.svg", "image/svg+xml")); - headChildren.push( - el("title", [], text(`${doc.title} | ${config.blog.siteName}`)), - ); - headChildren.push(await stylesheetLinkElement("/style.css", config)); - headChildren.push(await stylesheetLinkElement("/hl.css", config)); - const head = el("head", [], ...headChildren); - const body = el( - "body", - [["class", "single"]], - el( - "header", - [["class", "header"]], - el( - "nav", - [["class", "nav"]], - el( - "ul", - [], - el( - "li", - [["class", "logo"]], - el("a", [["href", "/"]], text(config.blog.siteName)), - ), - el( - "li", - [], - el("a", [["href", "/about"]], text("About")), - ), - el( - "li", - [], - el("a", [["href", "/posts"]], text("Posts")), - ), - el( - "li", - [], - el("a", [["href", "/slides"]], text("Slides")), - ), - ), - ), - ), - el( - "main", - [["class", "main"]], - el( - "article", - [["class", "post-single"]], - el( - "header", - [["class", "post-header"]], - el( - "h1", - [["class", "post-title"]], - text(doc.title), - ), - ...(doc.tags.length === 0 ? [] : [ - el( - "ul", - [["class", "post-tags"]], - ...doc.tags.map((slug) => - el( - "li", - [["class", "tag"]], - el( - "a", - [["href", `/tags/${slug}`]], - text( - (config.blog.tagLabels as { - [key: string]: string; - })[slug], - ), - ), - ) - ), - ), - ]), - ), - el( - "div", - [["class", "post-content"]], - el( - "section", - [], - el( - "h2", - [["id", "changelog"]], - text("更新履歴"), - ), - el( - "ol", - [], - ...doc.revisions.map((rev) => - el( - "li", - [["class", "revision"]], - el( - "time", - [["datetime", rev.date]], - text(rev.date), - ), - text(`: ${rev.remark}`), - ) - ), - ), - ), - // TODO: refactor - ...(doc.root.children[0] as Element).children, - // TODO: footnotes - //
- // <% for footnote in footnotes %> - //
- // <%= footnote.index %>. <%= footnote.text %> - //
- // <% end %> - //
- ), - ), - ), - el( - "footer", - [["class", "footer"]], - text( - `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, - ), - ), - ); - const html = el( - "html", - [["lang", "ja-JP"]], - head, - body, - ); - doc.root = el("__root__", [], html); - return doc; -} diff --git a/nuldoc-src/templates/post_list.ts b/nuldoc-src/templates/post_list.ts deleted file mode 100644 index f75b8e1..0000000 --- a/nuldoc-src/templates/post_list.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Document } from "../docbook/document.ts"; -import { Config } from "../config.ts"; -import { - el, - linkElement, - metaElement, - stylesheetLinkElement, - text, -} from "./utils.ts"; - -export default async function convertPostList( - posts: Document[], - config: Config, -): Promise { - const doc = new Document( - el("__root__", []), - "", - "/posts/", - "投稿一覧", - "投稿した記事の一覧", - [], - [], - ); - - const head = el( - "head", - [], - metaElement([["charset", "UTF-8"]]), - metaElement([["name", "viewport"], [ - "content", - "width=device-width, initial-scale=1.0", - ]]), - metaElement([["name", "author"], ["content", config.blog.author]]), - metaElement([["name", "copyright"], [ - "content", - `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, - ]]), - metaElement([["name", "description"], ["content", doc.summary]]), - linkElement("icon", "/favicon.svg", "image/svg+xml"), - el("title", [], text(`${doc.title} | ${config.blog.siteName}`)), - await stylesheetLinkElement("/style.css", config), - ); - const body = el( - "body", - [["class", "list"]], - el( - "header", - [["class", "header"]], - el( - "nav", - [["class", "nav"]], - el( - "ul", - [], - el( - "li", - [["class", "logo"]], - el("a", [["href", "/"]], text(config.blog.siteName)), - ), - el( - "li", - [], - el("a", [["href", "/about"]], text("About")), - ), - el( - "li", - [], - el("a", [["href", "/posts"]], text("Posts")), - ), - el( - "li", - [], - el("a", [["href", "/slides"]], text("Slides")), - ), - ), - ), - ), - el( - "main", - [["class", "main"]], - el( - "header", - [["class", "page-header"]], - el( - "h1", - [], - text(doc.title), - ), - ), - ...Array.from(posts).sort((a, b) => { - const ta = a.getCreatedDate(); - const tb = b.getCreatedDate(); - if (ta > tb) return -1; - if (ta < tb) return 1; - return 0; - }).map((post) => - el( - "article", - [["class", "post-entry"]], - el( - "a", - [["href", post.link]], - el( - "header", - [["class", "entry-header"]], - el("h2", [], text(post.title)), - ), - el( - "section", - [["class", "entry-content"]], - el("p", [], text(post.summary)), - ), - el( - "footer", - [["class", "entry-footer"]], - text("Posted on"), - el( - "time", - [["datetime", post.getCreatedDate()]], - text(post.getCreatedDate()), - ), - ...(post.revisions.length > 1 - ? [ - text(", updated on "), - el("time", [[ - "datetime", - post.getUpdatedDate(), - ]], text(post.getUpdatedDate())), - ] - : []), - ), - ), - ) - ), - ), - el( - "footer", - [["class", "footer"]], - text( - `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, - ), - ), - ); - const html = el( - "html", - [["lang", "ja-JP"]], - head, - body, - ); - - doc.root.children = [html]; - return doc; -} diff --git a/nuldoc-src/templates/tag.ts b/nuldoc-src/templates/tag.ts deleted file mode 100644 index 8aeac58..0000000 --- a/nuldoc-src/templates/tag.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Document } from "../docbook/document.ts"; -import { Config } from "../config.ts"; -import { - el, - linkElement, - metaElement, - stylesheetLinkElement, - text, -} from "./utils.ts"; - -export default async function convertTag( - tag: string, - posts: Document[], - config: Config, -): Promise { - const tagLabel = (config.blog.tagLabels as { [key: string]: string })[tag]; - - const doc = new Document( - el("__root__", []), - ``, - `/tags/${tag}/`, - `タグ「${tagLabel}」一覧`, - `タグ「${tagLabel}」のついた記事一覧`, - [], - [], - ); - - const headChildren = [ - metaElement([["charset", "UTF-8"]]), - metaElement([["name", "viewport"], [ - "content", - "width=device-width, initial-scale=1.0", - ]]), - metaElement([["name", "author"], ["content", config.blog.author]]), - metaElement([["name", "copyright"], [ - "content", - `© ${ - posts[posts.length - 1].getCreatedDate().substring(0, 4) - } ${config.blog.author}`, - ]]), - metaElement([["name", "description"], [ - "content", - doc.summary, - ]]), - metaElement([["name", "keywords"], ["content", tagLabel]]), - linkElement("icon", "/favicon.svg", "image/svg+xml"), - el("title", [], text(`${doc.title} | ${config.blog.siteName}`)), - await stylesheetLinkElement("/style.css", config), - ]; - const head = el("head", [], ...headChildren); - const body = el( - "body", - [["class", "list"]], - el( - "header", - [["class", "header"]], - el( - "nav", - [["class", "nav"]], - el( - "ul", - [], - el( - "li", - [["class", "logo"]], - el("a", [["href", "/"]], text(config.blog.siteName)), - ), - el( - "li", - [], - el("a", [["href", "/about"]], text("About")), - ), - el( - "li", - [], - el("a", [["href", "/posts"]], text("Posts")), - ), - el( - "li", - [], - el("a", [["href", "/slides"]], text("Slides")), - ), - ), - ), - ), - el( - "main", - [["class", "main"]], - el("header", [["class", "page-header"]], el("h1", [], text(doc.title))), - ...posts.map((post) => - el( - "article", - [["class", "post-entry"]], - el( - "a", - [["href", post.link]], - el( - "header", - [["class", "entry-header"]], - el("h2", [], text(post.title)), - ), - el( - "section", - [["class", "entry-content"]], - el("p", [], text(post.summary)), - ), - el( - "footer", - [["class", "entry-footer"]], - text("Posted on"), - el( - "time", - [["datetime", post.getCreatedDate()]], - text(post.getCreatedDate()), - ), - ...(post.revisions.length > 1 - ? [ - text(", updated on "), - el("time", [[ - "datetime", - post.getUpdatedDate(), - ]], text(post.getUpdatedDate())), - ] - : []), - ), - ), - ) - ), - ), - el( - "footer", - [["class", "footer"]], - text( - `© ${config.blog.siteCopyrightYear} ${config.blog.author}`, - ), - ), - ); - const html = el( - "html", - [["lang", "ja-JP"]], - head, - body, - ); - - doc.root.children = [html]; - return doc; -} diff --git a/nuldoc-src/templates/utils.ts b/nuldoc-src/templates/utils.ts deleted file mode 100644 index 018c460..0000000 --- a/nuldoc-src/templates/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { join } from "std/path/mod.ts"; -import { crypto, toHashString } from "std/crypto/mod.ts"; -import { Element, Node, Text } from "../dom.ts"; -import { Config } from "../config.ts"; - -export function text(content: string): Text { - return { - kind: "text", - content: content, - raw: false, - }; -} - -export function el( - name: string, - attrs: [string, string][], - ...children: Node[] -): Element { - return { - kind: "element", - name: name, - attributes: new Map(attrs), - children: children, - }; -} - -export async function stylesheetLinkElement( - fileName: string, - config: Config, -): Promise { - const filePath = join(Deno.cwd(), config.locations.staticDir, fileName); - const content = (await Deno.readFile(filePath)).buffer; - const hash = toHashString(await crypto.subtle.digest("MD5", content), "hex"); - return el("link", [["rel", "stylesheet"], ["href", `${fileName}?h=${hash}`]]); -} - -export function metaElement(attrs: [string, string][]): Element { - return el("meta", attrs); -} - -export function linkElement( - rel: string, - href: string, - type: string | null, -): Element { - const attrs: [string, string][] = [["rel", rel], ["href", href]]; - if (type !== null) { - attrs.push(["type", type]); - } - return el("link", attrs); -} -- cgit v1.2.3-70-g09d2