summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-01-12 22:17:44 +0900
committernsfisis <nsfisis@gmail.com>2025-01-12 22:17:44 +0900
commitb14dd149e1b5f75bc494623181b19970ef830dec (patch)
treedfff5ee9b9a9e16680fda5cba36e5c817eda1f9e
parentaecf316775c995f089012d8fec5c5cc77f6300be (diff)
parent16182acfcc1fad2885b9c1a96fe74d8ce56a50e0 (diff)
downloadnsfisis.dev-b14dd149e1b5f75bc494623181b19970ef830dec.tar.gz
nsfisis.dev-b14dd149e1b5f75bc494623181b19970ef830dec.tar.zst
nsfisis.dev-b14dd149e1b5f75bc494623181b19970ef830dec.zip
Merge branch 'feature/blog-nuldoc-jsx'
-rw-r--r--vhosts/blog/deno.jsonc7
-rw-r--r--vhosts/blog/nuldoc-src/atom/generate.ts7
-rw-r--r--vhosts/blog/nuldoc-src/commands/build.ts20
-rw-r--r--vhosts/blog/nuldoc-src/components/GlobalFooter.tsx9
-rw-r--r--vhosts/blog/nuldoc-src/components/GlobalHeader.tsx27
-rw-r--r--vhosts/blog/nuldoc-src/components/PageLayout.tsx63
-rw-r--r--vhosts/blog/nuldoc-src/components/PostPageEntry.tsx39
-rw-r--r--vhosts/blog/nuldoc-src/components/SlidePageEntry.tsx39
-rw-r--r--vhosts/blog/nuldoc-src/components/StaticScript.tsx17
-rw-r--r--vhosts/blog/nuldoc-src/components/StaticStylesheet.tsx11
-rw-r--r--vhosts/blog/nuldoc-src/components/global_footer.ts10
-rw-r--r--vhosts/blog/nuldoc-src/components/global_header.ts26
-rw-r--r--vhosts/blog/nuldoc-src/components/page_layout.ts82
-rw-r--r--vhosts/blog/nuldoc-src/components/post_page_entry.ts50
-rw-r--r--vhosts/blog/nuldoc-src/components/slide_page_entry.ts50
-rw-r--r--vhosts/blog/nuldoc-src/components/utils.ts24
-rw-r--r--vhosts/blog/nuldoc-src/generators/about.ts23
-rw-r--r--vhosts/blog/nuldoc-src/generators/home.ts19
-rw-r--r--vhosts/blog/nuldoc-src/generators/not_found.ts21
-rw-r--r--vhosts/blog/nuldoc-src/generators/post.ts63
-rw-r--r--vhosts/blog/nuldoc-src/generators/post_list.ts23
-rw-r--r--vhosts/blog/nuldoc-src/generators/slide.ts53
-rw-r--r--vhosts/blog/nuldoc-src/generators/slide_list.ts23
-rw-r--r--vhosts/blog/nuldoc-src/generators/tag.ts33
-rw-r--r--vhosts/blog/nuldoc-src/generators/tag_list.ts23
-rw-r--r--vhosts/blog/nuldoc-src/generators/tagged_page.ts (renamed from vhosts/blog/nuldoc-src/pages/tagged_page.ts)0
-rw-r--r--vhosts/blog/nuldoc-src/jsx/jsx-runtime.ts27
-rw-r--r--vhosts/blog/nuldoc-src/jsx/render.ts45
-rw-r--r--vhosts/blog/nuldoc-src/jsx/types.d.ts61
-rw-r--r--vhosts/blog/nuldoc-src/pages/AboutPage.tsx94
-rw-r--r--vhosts/blog/nuldoc-src/pages/HomePage.tsx53
-rw-r--r--vhosts/blog/nuldoc-src/pages/NotFoundPage.tsx27
-rw-r--r--vhosts/blog/nuldoc-src/pages/PostListPage.tsx41
-rw-r--r--vhosts/blog/nuldoc-src/pages/PostPage.tsx74
-rw-r--r--vhosts/blog/nuldoc-src/pages/SlideListPage.tsx42
-rw-r--r--vhosts/blog/nuldoc-src/pages/SlidePage.tsx70
-rw-r--r--vhosts/blog/nuldoc-src/pages/TagListPage.tsx57
-rw-r--r--vhosts/blog/nuldoc-src/pages/TagPage.tsx43
-rw-r--r--vhosts/blog/nuldoc-src/pages/about.ts156
-rw-r--r--vhosts/blog/nuldoc-src/pages/home.ts97
-rw-r--r--vhosts/blog/nuldoc-src/pages/not_found.ts47
-rw-r--r--vhosts/blog/nuldoc-src/pages/post.ts145
-rw-r--r--vhosts/blog/nuldoc-src/pages/post_list.ts61
-rw-r--r--vhosts/blog/nuldoc-src/pages/slide.ts137
-rw-r--r--vhosts/blog/nuldoc-src/pages/slide_list.ts62
-rw-r--r--vhosts/blog/nuldoc-src/pages/tag.ts65
-rw-r--r--vhosts/blog/nuldoc-src/pages/tag_list.ts85
-rw-r--r--vhosts/blog/nuldoc-src/renderers/html.ts46
-rw-r--r--vhosts/blog/nuldoc-src/renderers/xml.ts45
49 files changed, 1184 insertions, 1158 deletions
diff --git a/vhosts/blog/deno.jsonc b/vhosts/blog/deno.jsonc
index 1751a80c..1a7ee5cd 100644
--- a/vhosts/blog/deno.jsonc
+++ b/vhosts/blog/deno.jsonc
@@ -1,10 +1,17 @@
{
"imports": {
+ "myjsx/jsx-runtime": "./nuldoc-src/jsx/jsx-runtime.ts",
+ "myjsx-types/jsx-runtime": "./nuldoc-src/jsx/types.d.ts",
"checksum/": "https://deno.land/x/checksum@1.4.0/",
"highlight.js": "npm:highlight.js@11.9.0",
"std/": "https://deno.land/std@0.224.0/",
"zod/": "https://deno.land/x/zod@v3.23.8/",
},
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "myjsx",
+ "jsxImportSourceTypes": "myjsx-types",
+ },
"tasks": {
"check": "deno check nuldoc-src/main.ts && deno lint -- nuldoc-src/ && deno fmt --check -- nuldoc-src/",
"fmt": "deno fmt -- nuldoc-src",
diff --git a/vhosts/blog/nuldoc-src/atom/generate.ts b/vhosts/blog/nuldoc-src/atom/generate.ts
index ee659211..3c76f6c5 100644
--- a/vhosts/blog/nuldoc-src/atom/generate.ts
+++ b/vhosts/blog/nuldoc-src/atom/generate.ts
@@ -2,8 +2,8 @@ import { Config } from "../config.ts";
import { el } from "../dom.ts";
import { Page } from "../page.ts";
import { Entry, Feed } from "./types.ts";
-import { PostPage } from "../pages/post.ts";
-import { SlidePage } from "../pages/slide.ts";
+import { PostPage } from "../generators/post.ts";
+import { SlidePage } from "../generators/slide.ts";
import { dateToRfc3339String } from "../revision.ts";
const BASE_NAME = "atom.xml";
@@ -50,9 +50,8 @@ export function generateFeedPageFromEntries(
entries: entries_,
};
- const xml = buildXmlTree(feed);
return {
- root: el("__root__", {}, xml),
+ root: buildXmlTree(feed),
renderer: "xml",
destFilePath: feedPath,
href: feedPath,
diff --git a/vhosts/blog/nuldoc-src/commands/build.ts b/vhosts/blog/nuldoc-src/commands/build.ts
index 1ad08825..a596dc2a 100644
--- a/vhosts/blog/nuldoc-src/commands/build.ts
+++ b/vhosts/blog/nuldoc-src/commands/build.ts
@@ -7,20 +7,20 @@ import { parseNulDocFile } from "../ndoc/parse.ts";
import { Page } from "../page.ts";
import { render } from "../render.ts";
import { dateToString } from "../revision.ts";
-import { generateAboutPage } from "../pages/about.ts";
-import { generateHomePage } from "../pages/home.ts";
-import { generateNotFoundPage } from "../pages/not_found.ts";
+import { generateAboutPage } from "../generators/about.ts";
+import { generateHomePage } from "../generators/home.ts";
+import { generateNotFoundPage } from "../generators/not_found.ts";
import {
generatePostPage,
getPostPublishedDate,
PostPage,
-} from "../pages/post.ts";
-import { generatePostListPage } from "../pages/post_list.ts";
-import { generateSlidePage, SlidePage } from "../pages/slide.ts";
-import { generateSlideListPage } from "../pages/slide_list.ts";
-import { generateTagPage, TagPage } from "../pages/tag.ts";
-import { TaggedPage } from "../pages/tagged_page.ts";
-import { generateTagListPage } from "../pages/tag_list.ts";
+} from "../generators/post.ts";
+import { generatePostListPage } from "../generators/post_list.ts";
+import { generateSlidePage, SlidePage } from "../generators/slide.ts";
+import { generateSlideListPage } from "../generators/slide_list.ts";
+import { generateTagPage, TagPage } from "../generators/tag.ts";
+import { TaggedPage } from "../generators/tagged_page.ts";
+import { generateTagListPage } from "../generators/tag_list.ts";
import { parseSlideFile } from "../slide/parse.ts";
export async function runBuildCommand(config: Config) {
diff --git a/vhosts/blog/nuldoc-src/components/GlobalFooter.tsx b/vhosts/blog/nuldoc-src/components/GlobalFooter.tsx
new file mode 100644
index 00000000..757beced
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/components/GlobalFooter.tsx
@@ -0,0 +1,9 @@
+import { Config } from "../config.ts";
+
+export default function GlobalFooter({ config }: { config: Config }) {
+ return (
+ <footer className="footer">
+ {`&copy; ${config.blog.siteCopyrightYear} ${config.blog.author}`}
+ </footer>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/components/GlobalHeader.tsx b/vhosts/blog/nuldoc-src/components/GlobalHeader.tsx
new file mode 100644
index 00000000..c0fa7e8b
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/components/GlobalHeader.tsx
@@ -0,0 +1,27 @@
+import { Config } from "../config.ts";
+
+export default function GlobalHeader({ config }: { config: Config }) {
+ return (
+ <header className="header">
+ <div className="site-logo">
+ <a href="/">{config.blog.siteName}</a>
+ </div>
+ <nav className="nav">
+ <ul>
+ <li>
+ <a href="/about/">About</a>
+ </li>
+ <li>
+ <a href="/posts/">Posts</a>
+ </li>
+ <li>
+ <a href="/slides/">Slides</a>
+ </li>
+ <li>
+ <a href="/tags/">Tags</a>
+ </li>
+ </ul>
+ </nav>
+ </header>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/components/PageLayout.tsx b/vhosts/blog/nuldoc-src/components/PageLayout.tsx
new file mode 100644
index 00000000..c7145e0a
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/components/PageLayout.tsx
@@ -0,0 +1,63 @@
+import { Config } from "../config.ts";
+import { JSXNode } from "myjsx/jsx-runtime";
+import StaticStylesheet from "./StaticStylesheet.tsx";
+
+type Props = {
+ metaCopyrightYear: number;
+ metaDescription: string;
+ metaKeywords?: string[];
+ metaTitle: string;
+ metaAtomFeedHref?: string;
+ requiresSyntaxHighlight?: boolean;
+ config: Config;
+ children: JSXNode;
+};
+
+export default function PageLayout(
+ {
+ metaCopyrightYear,
+ metaDescription,
+ metaKeywords,
+ metaTitle,
+ metaAtomFeedHref,
+ requiresSyntaxHighlight,
+ config,
+ children,
+ }: Props,
+) {
+ return (
+ <html lang="ja-JP">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta name="author" content={config.blog.author} />
+ <meta
+ name="copyright"
+ content={`&copy; ${metaCopyrightYear} ${config.blog.author}`}
+ />
+ <meta name="description" content={metaDescription} />
+ {metaKeywords && metaKeywords.length !== 0 &&
+ <meta name="keywords" content={metaKeywords.join(",")} />}
+ <meta property="og:type" content="article" />
+ <meta property="og:title" content={metaTitle} />
+ <meta property="og:description" content={metaDescription} />
+ <meta property="og:site_name" content={config.blog.siteName} />
+ <meta property="og:locale" content="ja_JP" />
+ {metaAtomFeedHref &&
+ (
+ <link
+ rel="alternate"
+ href={metaAtomFeedHref}
+ type="application/atom+xml"
+ />
+ )}
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
+ <title>{metaTitle}</title>
+ <StaticStylesheet fileName="/style.css" config={config} />
+ {requiresSyntaxHighlight &&
+ <StaticStylesheet fileName="/hl.css" config={config} />}
+ </head>
+ {children}
+ </html>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/components/PostPageEntry.tsx b/vhosts/blog/nuldoc-src/components/PostPageEntry.tsx
new file mode 100644
index 00000000..2708b009
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/components/PostPageEntry.tsx
@@ -0,0 +1,39 @@
+import {
+ getPostPublishedDate,
+ getPostUpdatedDate,
+ postHasAnyUpdates,
+ PostPage,
+} from "../generators/post.ts";
+import { dateToString } from "../revision.ts";
+
+export default function PostPageEntry({ post }: { post: PostPage }) {
+ return (
+ <article className="post-entry">
+ <a href={post.href}>
+ <header className="entry-header">
+ <h2>{post.title}</h2>
+ </header>
+ <section className="entry-content">
+ <p>{post.description}</p>
+ </section>
+ <footer className="entry-footer">
+ <time datetime={dateToString(getPostPublishedDate(post))}>
+ {dateToString(getPostPublishedDate(post))}
+ </time>
+ {" 投稿"}
+ {
+ // TODO(jsx): support Fragment and merge them.
+ postHasAnyUpdates(post) && "、"
+ }
+ {postHasAnyUpdates(post) &&
+ (
+ <time datetime={dateToString(getPostUpdatedDate(post))}>
+ {dateToString(getPostUpdatedDate(post))}
+ </time>
+ )}
+ {postHasAnyUpdates(post) && " 更新"}
+ </footer>
+ </a>
+ </article>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/components/SlidePageEntry.tsx b/vhosts/blog/nuldoc-src/components/SlidePageEntry.tsx
new file mode 100644
index 00000000..d2cf9a17
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/components/SlidePageEntry.tsx
@@ -0,0 +1,39 @@
+import {
+ getPostPublishedDate,
+ getPostUpdatedDate,
+ postHasAnyUpdates,
+} from "../generators/post.ts";
+import { SlidePage } from "../generators/slide.ts";
+import { dateToString } from "../revision.ts";
+
+export default function SlidePageEntry({ slide }: { slide: SlidePage }) {
+ return (
+ <article className="post-entry">
+ <a href={slide.href}>
+ <header className="entry-header">
+ <h2>{slide.description}</h2>
+ </header>
+ <section className="entry-content">
+ <p>{slide.title}</p>
+ </section>
+ <footer className="entry-footer">
+ <time datetime={dateToString(getPostPublishedDate(slide))}>
+ {dateToString(getPostPublishedDate(slide))}
+ </time>
+ {" 登壇"}
+ {
+ // TODO(jsx): support Fragment and merge them.
+ postHasAnyUpdates(slide) && "、"
+ }
+ {postHasAnyUpdates(slide) &&
+ (
+ <time datetime={dateToString(getPostUpdatedDate(slide))}>
+ {dateToString(getPostUpdatedDate(slide))}
+ </time>
+ )}
+ {postHasAnyUpdates(slide) && " 更新"}
+ </footer>
+ </a>
+ </article>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/components/StaticScript.tsx b/vhosts/blog/nuldoc-src/components/StaticScript.tsx
new file mode 100644
index 00000000..eb26ada7
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/components/StaticScript.tsx
@@ -0,0 +1,17 @@
+import { join } from "std/path/mod.ts";
+import { Config } from "../config.ts";
+import { calculateFileHash } from "./utils.ts";
+
+export default async function StaticScript(
+ { fileName, type, config }: {
+ fileName: string;
+ type?: string;
+ config: Config;
+ },
+) {
+ const filePath = join(Deno.cwd(), config.locations.staticDir, fileName);
+ const hash = await calculateFileHash(filePath);
+ return (
+ <script src={`${fileName}?h=${hash}`} {...(type ? { type } : {})}></script>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/components/StaticStylesheet.tsx b/vhosts/blog/nuldoc-src/components/StaticStylesheet.tsx
new file mode 100644
index 00000000..5c2c2a0f
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/components/StaticStylesheet.tsx
@@ -0,0 +1,11 @@
+import { join } from "std/path/mod.ts";
+import { Config } from "../config.ts";
+import { calculateFileHash } from "./utils.ts";
+
+export default async function StaticStylesheet(
+ { fileName, config }: { fileName: string; config: Config },
+) {
+ const filePath = join(Deno.cwd(), config.locations.staticDir, fileName);
+ const hash = await calculateFileHash(filePath);
+ return <link rel="stylesheet" href={`${fileName}?h=${hash}`} />;
+}
diff --git a/vhosts/blog/nuldoc-src/components/global_footer.ts b/vhosts/blog/nuldoc-src/components/global_footer.ts
deleted file mode 100644
index 7fdc8797..00000000
--- a/vhosts/blog/nuldoc-src/components/global_footer.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Config } from "../config.ts";
-import { el, Element } from "../dom.ts";
-
-export function globalFooter(config: Config): Element {
- return el(
- "footer",
- { className: "footer" },
- `&copy; ${config.blog.siteCopyrightYear} ${config.blog.author}`,
- );
-}
diff --git a/vhosts/blog/nuldoc-src/components/global_header.ts b/vhosts/blog/nuldoc-src/components/global_header.ts
deleted file mode 100644
index 166c48ff..00000000
--- a/vhosts/blog/nuldoc-src/components/global_header.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Config } from "../config.ts";
-import { el, Element } from "../dom.ts";
-
-export function globalHeader(config: Config): Element {
- return el(
- "header",
- { className: "header" },
- el(
- "div",
- { className: "site-logo" },
- el("a", { href: "/" }, config.blog.siteName),
- ),
- el(
- "nav",
- { className: "nav" },
- el(
- "ul",
- {},
- el("li", {}, el("a", { href: "/about/" }, "About")),
- el("li", {}, el("a", { href: "/posts/" }, "Posts")),
- el("li", {}, el("a", { href: "/slides/" }, "Slides")),
- el("li", {}, el("a", { href: "/tags/" }, "Tags")),
- ),
- ),
- );
-}
diff --git a/vhosts/blog/nuldoc-src/components/page_layout.ts b/vhosts/blog/nuldoc-src/components/page_layout.ts
deleted file mode 100644
index a6b75d01..00000000
--- a/vhosts/blog/nuldoc-src/components/page_layout.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Config } from "../config.ts";
-import { el, Element } from "../dom.ts";
-import { stylesheetLinkElement } from "./utils.ts";
-
-type Params = {
- metaCopyrightYear: number;
- metaDescription: string;
- metaKeywords: string[];
- metaTitle: string;
- metaAtomFeedHref?: string;
- requiresSyntaxHighlight: boolean;
-};
-
-export async function pageLayout(
- {
- metaCopyrightYear,
- metaDescription,
- metaKeywords,
- metaTitle,
- metaAtomFeedHref,
- requiresSyntaxHighlight,
- }: Params,
- body: Element,
- config: Config,
-): Promise<Element> {
- 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: `&copy; ${metaCopyrightYear} ${config.blog.author}`,
- }),
- metaElement({ name: "description", content: metaDescription }),
- ...(metaKeywords.length === 0 ? [] : [
- metaElement({ name: "keywords", content: metaKeywords.join(",") }),
- ]),
- metaElement({ property: "og:type", content: "article" }),
- metaElement({ property: "og:title", content: metaTitle }),
- metaElement({ property: "og:description", content: metaDescription }),
- metaElement({ property: "og:site_name", content: config.blog.siteName }),
- metaElement({ property: "og:locale", content: "ja_JP" }),
- ...(metaAtomFeedHref
- ? [linkElement("alternate", metaAtomFeedHref, "application/atom+xml")]
- : []),
- linkElement("icon", "/favicon.svg", "image/svg+xml"),
- el("title", {}, metaTitle),
- await stylesheetLinkElement("/style.css", config),
- ...(
- requiresSyntaxHighlight
- ? [await stylesheetLinkElement("/hl.css", config)]
- : []
- ),
- );
- return el(
- "html",
- { lang: "ja-JP" },
- head,
- body,
- );
-}
-
-function metaElement(attrs: Record<string, string>): Element {
- return el("meta", attrs);
-}
-
-function linkElement(
- rel: string,
- href: string,
- type: string | null,
-): Element {
- const attrs: Record<string, string> = { rel: rel, href: href };
- if (type !== null) {
- attrs.type = type;
- }
- return el("link", attrs);
-}
diff --git a/vhosts/blog/nuldoc-src/components/post_page_entry.ts b/vhosts/blog/nuldoc-src/components/post_page_entry.ts
deleted file mode 100644
index bd82eb95..00000000
--- a/vhosts/blog/nuldoc-src/components/post_page_entry.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { el, Element } from "../dom.ts";
-import {
- getPostPublishedDate,
- getPostUpdatedDate,
- postHasAnyUpdates,
- PostPage,
-} from "../pages/post.ts";
-import { dateToString } from "../revision.ts";
-
-export function postPageEntry(post: PostPage): Element {
- return el(
- "article",
- { className: "post-entry" },
- el(
- "a",
- { href: post.href },
- el(
- "header",
- { className: "entry-header" },
- el("h2", {}, post.title),
- ),
- el(
- "section",
- { className: "entry-content" },
- el("p", {}, post.description),
- ),
- el(
- "footer",
- { className: "entry-footer" },
- el(
- "time",
- { datetime: dateToString(getPostPublishedDate(post)) },
- dateToString(getPostPublishedDate(post)),
- ),
- " 投稿",
- ...(postHasAnyUpdates(post)
- ? [
- "、",
- el(
- "time",
- { "datetime": dateToString(getPostUpdatedDate(post)) },
- dateToString(getPostUpdatedDate(post)),
- ),
- " 更新",
- ]
- : []),
- ),
- ),
- );
-}
diff --git a/vhosts/blog/nuldoc-src/components/slide_page_entry.ts b/vhosts/blog/nuldoc-src/components/slide_page_entry.ts
deleted file mode 100644
index e64a4022..00000000
--- a/vhosts/blog/nuldoc-src/components/slide_page_entry.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { el, Element } from "../dom.ts";
-import {
- getPostPublishedDate,
- getPostUpdatedDate,
- postHasAnyUpdates,
-} from "../pages/post.ts";
-import { SlidePage } from "../pages/slide.ts";
-import { dateToString } from "../revision.ts";
-
-export function slidePageEntry(slide: SlidePage): Element {
- return el(
- "article",
- { className: "post-entry" },
- el(
- "a",
- { href: slide.href },
- el(
- "header",
- { className: "entry-header" },
- el("h2", {}, slide.description),
- ),
- el(
- "section",
- { className: "entry-content" },
- el("p", {}, slide.title),
- ),
- el(
- "footer",
- { className: "entry-footer" },
- el(
- "time",
- { datetime: dateToString(getPostPublishedDate(slide)) },
- dateToString(getPostPublishedDate(slide)),
- ),
- " 登壇",
- ...(postHasAnyUpdates(slide)
- ? [
- "、",
- el(
- "time",
- { "datetime": dateToString(getPostUpdatedDate(slide)) },
- dateToString(getPostUpdatedDate(slide)),
- ),
- " 更新",
- ]
- : []),
- ),
- ),
- );
-}
diff --git a/vhosts/blog/nuldoc-src/components/utils.ts b/vhosts/blog/nuldoc-src/components/utils.ts
index ce693100..14059b5b 100644
--- a/vhosts/blog/nuldoc-src/components/utils.ts
+++ b/vhosts/blog/nuldoc-src/components/utils.ts
@@ -1,28 +1,6 @@
import { Hash } from "checksum/mod.ts";
-import { join } from "std/path/mod.ts";
-import { Config } from "../config.ts";
-import { el, Element } from "../dom.ts";
-export async function stylesheetLinkElement(
- fileName: string,
- config: Config,
-): Promise<Element> {
- const filePath = join(Deno.cwd(), config.locations.staticDir, fileName);
- const hash = await calculateFileHash(filePath);
- return el("link", { rel: "stylesheet", href: `${fileName}?h=${hash}` });
-}
-
-export async function staticScriptElement(
- fileName: string,
- attrs: Record<string, string>,
- config: Config,
-): Promise<Element> {
- const filePath = join(Deno.cwd(), config.locations.staticDir, fileName);
- const hash = await calculateFileHash(filePath);
- return el("script", { src: `${fileName}?h=${hash}`, ...attrs });
-}
-
-async function calculateFileHash(
+export async function calculateFileHash(
filePath: string,
): Promise<string> {
const content = await Deno.readFile(filePath);
diff --git a/vhosts/blog/nuldoc-src/generators/about.ts b/vhosts/blog/nuldoc-src/generators/about.ts
new file mode 100644
index 00000000..6663a190
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/about.ts
@@ -0,0 +1,23 @@
+import AboutPage from "../pages/AboutPage.tsx";
+import { Config } from "../config.ts";
+import { renderToDOM } from "../jsx/render.ts";
+import { Page } from "../page.ts";
+import { SlidePage } from "./slide.ts";
+
+export type AboutPage = Page;
+
+export async function generateAboutPage(
+ slides: SlidePage[],
+ config: Config,
+): Promise<AboutPage> {
+ const html = await renderToDOM(
+ AboutPage(slides, config),
+ );
+
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: "/about/index.html",
+ href: "/about/",
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/generators/home.ts b/vhosts/blog/nuldoc-src/generators/home.ts
new file mode 100644
index 00000000..679dd39a
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/home.ts
@@ -0,0 +1,19 @@
+import HomePage from "../pages/HomePage.tsx";
+import { renderToDOM } from "../jsx/render.ts";
+import { Config } from "../config.ts";
+import { Page } from "../page.ts";
+
+export type HomePage = Page;
+
+export async function generateHomePage(config: Config): Promise<HomePage> {
+ const html = await renderToDOM(
+ HomePage(config),
+ );
+
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: "/index.html",
+ href: "/",
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/generators/not_found.ts b/vhosts/blog/nuldoc-src/generators/not_found.ts
new file mode 100644
index 00000000..f5a81c86
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/not_found.ts
@@ -0,0 +1,21 @@
+import NotFoundPage from "../pages/NotFoundPage.tsx";
+import { renderToDOM } from "../jsx/render.ts";
+import { Config } from "../config.ts";
+import { Page } from "../page.ts";
+
+export type NotFoundPage = Page;
+
+export async function generateNotFoundPage(
+ config: Config,
+): Promise<NotFoundPage> {
+ const html = await renderToDOM(
+ NotFoundPage(config),
+ );
+
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: "/404.html",
+ href: "/404.html",
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/generators/post.ts b/vhosts/blog/nuldoc-src/generators/post.ts
new file mode 100644
index 00000000..f90ab193
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/post.ts
@@ -0,0 +1,63 @@
+import { join } from "std/path/mod.ts";
+import { renderToDOM } from "../jsx/render.ts";
+import PostPage from "../pages/PostPage.tsx";
+import { Config } from "../config.ts";
+import { Document } from "../ndoc/document.ts";
+import { Page } from "../page.ts";
+import { Date, Revision } from "../revision.ts";
+
+export interface PostPage extends Page {
+ title: string;
+ description: string;
+ tags: string[];
+ revisions: Revision[];
+ published: Date;
+ updated: Date;
+ uuid: string;
+}
+
+export function getPostPublishedDate(page: { revisions: Revision[] }): Date {
+ for (const rev of page.revisions) {
+ if (!rev.isInternal) {
+ return rev.date;
+ }
+ }
+ return page.revisions[0].date;
+}
+
+export function getPostUpdatedDate(page: { revisions: Revision[] }): Date {
+ return page.revisions[page.revisions.length - 1].date;
+}
+
+export function postHasAnyUpdates(page: { revisions: Revision[] }): boolean {
+ return 2 <= page.revisions.filter((rev) => !rev.isInternal).length;
+}
+
+export async function generatePostPage(
+ doc: Document,
+ config: Config,
+): Promise<PostPage> {
+ const html = await renderToDOM(
+ PostPage(doc, config),
+ );
+
+ const cwd = Deno.cwd();
+ const contentDir = join(cwd, config.locations.contentDir);
+ const destFilePath = join(
+ doc.sourceFilePath.replace(contentDir, "").replace(".ndoc", ""),
+ "index.html",
+ );
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: destFilePath,
+ href: destFilePath.replace("index.html", ""),
+ title: doc.title,
+ description: doc.description,
+ tags: doc.tags,
+ revisions: doc.revisions,
+ published: getPostPublishedDate(doc),
+ updated: getPostUpdatedDate(doc),
+ uuid: doc.uuid,
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/generators/post_list.ts b/vhosts/blog/nuldoc-src/generators/post_list.ts
new file mode 100644
index 00000000..67a4b996
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/post_list.ts
@@ -0,0 +1,23 @@
+import { renderToDOM } from "../jsx/render.ts";
+import PostListPage from "../pages/PostListPage.tsx";
+import { Config } from "../config.ts";
+import { Page } from "../page.ts";
+import { PostPage } from "./post.ts";
+
+export type PostListPage = Page;
+
+export async function generatePostListPage(
+ posts: PostPage[],
+ config: Config,
+): Promise<PostListPage> {
+ const html = await renderToDOM(
+ PostListPage(posts, config),
+ );
+
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: "/posts/index.html",
+ href: "/posts/",
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/generators/slide.ts b/vhosts/blog/nuldoc-src/generators/slide.ts
new file mode 100644
index 00000000..ab68d332
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/slide.ts
@@ -0,0 +1,53 @@
+import { join } from "std/path/mod.ts";
+import { renderToDOM } from "../jsx/render.ts";
+import SlidePage from "../pages/SlidePage.tsx";
+import { Config } from "../config.ts";
+import { Page } from "../page.ts";
+import { Date, Revision } from "../revision.ts";
+import { Slide } from "../slide/slide.ts";
+import { getPostPublishedDate, getPostUpdatedDate } from "./post.ts";
+
+export interface SlidePage extends Page {
+ title: string;
+ description: string;
+ event: string;
+ talkType: string;
+ slideLink: string;
+ tags: string[];
+ revisions: Revision[];
+ published: Date;
+ updated: Date;
+ uuid: string;
+}
+
+export async function generateSlidePage(
+ slide: Slide,
+ config: Config,
+): Promise<SlidePage> {
+ const html = await renderToDOM(
+ SlidePage(slide, config),
+ );
+
+ const cwd = Deno.cwd();
+ const contentDir = join(cwd, config.locations.contentDir);
+ const destFilePath = join(
+ slide.sourceFilePath.replace(contentDir, "").replace(".toml", ""),
+ "index.html",
+ );
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: destFilePath,
+ href: destFilePath.replace("index.html", ""),
+ title: slide.title,
+ description: `登壇: ${slide.event} (${slide.talkType})`,
+ event: slide.event,
+ talkType: slide.talkType,
+ slideLink: slide.slideLink,
+ tags: slide.tags,
+ revisions: slide.revisions,
+ published: getPostPublishedDate(slide),
+ updated: getPostUpdatedDate(slide),
+ uuid: slide.uuid,
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/generators/slide_list.ts b/vhosts/blog/nuldoc-src/generators/slide_list.ts
new file mode 100644
index 00000000..abebe109
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/slide_list.ts
@@ -0,0 +1,23 @@
+import { renderToDOM } from "../jsx/render.ts";
+import SlideListPage from "../pages/SlideListPage.tsx";
+import { Config } from "../config.ts";
+import { Page } from "../page.ts";
+import { SlidePage } from "./slide.ts";
+
+export type SlideListPage = Page;
+
+export async function generateSlideListPage(
+ slides: SlidePage[],
+ config: Config,
+): Promise<SlideListPage> {
+ const html = await renderToDOM(
+ SlideListPage(slides, config),
+ );
+
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: "/slides/index.html",
+ href: "/slides/",
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/generators/tag.ts b/vhosts/blog/nuldoc-src/generators/tag.ts
new file mode 100644
index 00000000..dbd8ef93
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/tag.ts
@@ -0,0 +1,33 @@
+import { renderToDOM } from "../jsx/render.ts";
+import TagPage from "../pages/TagPage.tsx";
+import { Config, getTagLabel } from "../config.ts";
+import { Page } from "../page.ts";
+import { TaggedPage } from "./tagged_page.ts";
+
+export interface TagPage extends Page {
+ tagSlug: string;
+ tagLabel: string;
+ numOfPosts: number;
+ numOfSlides: number;
+}
+
+export async function generateTagPage(
+ tagSlug: string,
+ pages: TaggedPage[],
+ config: Config,
+): Promise<TagPage> {
+ const html = await renderToDOM(
+ TagPage(tagSlug, pages, config),
+ );
+
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: `/tags/${tagSlug}/index.html`,
+ href: `/tags/${tagSlug}/`,
+ tagSlug: tagSlug,
+ tagLabel: getTagLabel(config, tagSlug),
+ numOfPosts: pages.filter((p) => !("event" in p)).length,
+ numOfSlides: pages.filter((p) => "event" in p).length,
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/generators/tag_list.ts b/vhosts/blog/nuldoc-src/generators/tag_list.ts
new file mode 100644
index 00000000..7baad8cf
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/generators/tag_list.ts
@@ -0,0 +1,23 @@
+import { renderToDOM } from "../jsx/render.ts";
+import TagListPage from "../pages/TagListPage.tsx";
+import { Config } from "../config.ts";
+import { Page } from "../page.ts";
+import { TagPage } from "./tag.ts";
+
+export type TagListPage = Page;
+
+export async function generateTagListPage(
+ tags: TagPage[],
+ config: Config,
+): Promise<TagListPage> {
+ const html = await renderToDOM(
+ TagListPage(tags, config),
+ );
+
+ return {
+ root: html,
+ renderer: "html",
+ destFilePath: "/tags/index.html",
+ href: "/tags/",
+ };
+}
diff --git a/vhosts/blog/nuldoc-src/pages/tagged_page.ts b/vhosts/blog/nuldoc-src/generators/tagged_page.ts
index 23de8cb4..23de8cb4 100644
--- a/vhosts/blog/nuldoc-src/pages/tagged_page.ts
+++ b/vhosts/blog/nuldoc-src/generators/tagged_page.ts
diff --git a/vhosts/blog/nuldoc-src/jsx/jsx-runtime.ts b/vhosts/blog/nuldoc-src/jsx/jsx-runtime.ts
new file mode 100644
index 00000000..9571e87d
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/jsx/jsx-runtime.ts
@@ -0,0 +1,27 @@
+import type { Node } from "../dom.ts";
+
+export type JSXElement = {
+ tag: string | FunctionComponent;
+ props: Props;
+};
+
+export type JSXNullNode = false | null | undefined;
+export type JSXSimpleNode = JSXElement | Node | string;
+export type JSXNullableSimpleNode = JSXSimpleNode | JSXNullNode;
+export type JSXNode = JSXNullableSimpleNode | JSXNode[];
+export type RenderableJSXNode = JSXElement;
+
+type Props = { children?: JSXNode } & Record<string, unknown>;
+export type FunctionComponentResult = JSXElement | Promise<JSXElement>;
+type FunctionComponent = (props: Props) => FunctionComponentResult;
+
+export function jsx(
+ tag: string | FunctionComponent,
+ props: Props,
+): JSXElement {
+ return { tag, props };
+}
+
+export { jsx as jsxs };
+
+// TODO: support Fragment
diff --git a/vhosts/blog/nuldoc-src/jsx/render.ts b/vhosts/blog/nuldoc-src/jsx/render.ts
new file mode 100644
index 00000000..8603f6c3
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/jsx/render.ts
@@ -0,0 +1,45 @@
+import type { Element, Node } from "../dom.ts";
+import type {
+ JSXNode,
+ JSXNullableSimpleNode,
+ JSXSimpleNode,
+ RenderableJSXNode,
+} from "myjsx/jsx-runtime";
+
+function transformNode(node: JSXNode): Promise<Node[]> {
+ const flattenNodes: JSXNullableSimpleNode[] = Array.isArray(node)
+ // @ts-ignore prevents infinite recursion
+ ? (node.flat(Infinity) as JSXNullableSimpleNode[])
+ : [node];
+ return Promise.all(
+ flattenNodes
+ .filter((c): c is JSXSimpleNode => c != null && c !== false)
+ .map((c) => {
+ if (typeof c === "string") {
+ return { kind: "text", content: c, raw: false };
+ } else if ("kind" in c) {
+ return c;
+ } else {
+ return renderToDOM(c);
+ }
+ }),
+ );
+}
+
+export async function renderToDOM(
+ element: RenderableJSXNode,
+): Promise<Element> {
+ const { tag, props } = element;
+ if (typeof tag === "string") {
+ const { children, ...attrs } = props;
+ const attrsMap = new Map(Object.entries(attrs)) as Map<string, string>;
+ return {
+ kind: "element",
+ name: tag,
+ attributes: attrsMap,
+ children: await transformNode(children),
+ };
+ } else {
+ return renderToDOM(await tag(props));
+ }
+}
diff --git a/vhosts/blog/nuldoc-src/jsx/types.d.ts b/vhosts/blog/nuldoc-src/jsx/types.d.ts
new file mode 100644
index 00000000..6a9d2b91
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/jsx/types.d.ts
@@ -0,0 +1,61 @@
+import type {
+ FunctionComponentResult,
+ JSXElement,
+ JSXNode,
+} from "myjsx/jsx-runtime";
+
+export { JSXNode };
+
+interface IntrinsicElementType {
+ children?: JSXNode;
+ className?: string;
+ id?: string;
+}
+
+declare global {
+ namespace JSX {
+ type Element = JSXElement;
+ type ElementType =
+ | string
+ // deno-lint-ignore no-explicit-any
+ | ((props: any) => FunctionComponentResult);
+
+ interface IntrinsicElements {
+ a: IntrinsicElementType & { href?: string };
+ article: IntrinsicElementType;
+ body: IntrinsicElementType;
+ button: IntrinsicElementType;
+ canvas: { id?: string; "data-slide-link"?: string };
+ div: IntrinsicElementType;
+ footer: IntrinsicElementType;
+ h1: IntrinsicElementType;
+ h2: IntrinsicElementType;
+ head: unknown;
+ header: IntrinsicElementType;
+ html: IntrinsicElementType & { lang?: string };
+ img: { src: string };
+ li: IntrinsicElementType;
+ link: { rel: string; href: string; type?: string };
+ main: IntrinsicElementType;
+ meta: {
+ charset?: string;
+ name?: string;
+ content?: string;
+ property?: string;
+ };
+ nav: IntrinsicElementType;
+ noscript: IntrinsicElementType;
+ ol: IntrinsicElementType;
+ p: IntrinsicElementType;
+ script: { src: string; type?: string };
+ section: IntrinsicElementType;
+ time: IntrinsicElementType & { datetime?: string };
+ title: IntrinsicElementType;
+ ul: IntrinsicElementType;
+ }
+
+ interface ElementChildrenAttribute {
+ children: unknown;
+ }
+ }
+}
diff --git a/vhosts/blog/nuldoc-src/pages/AboutPage.tsx b/vhosts/blog/nuldoc-src/pages/AboutPage.tsx
new file mode 100644
index 00000000..0000edf5
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/AboutPage.tsx
@@ -0,0 +1,94 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import StaticScript from "../components/StaticScript.tsx";
+import { Config } from "../config.ts";
+import { dateToString } from "../revision.ts";
+import { getPostPublishedDate } from "../generators/post.ts";
+import { SlidePage } from "../generators/slide.ts";
+
+export default function AboutPage(
+ slides: SlidePage[],
+ config: Config,
+) {
+ return (
+ <PageLayout
+ metaCopyrightYear={config.blog.siteCopyrightYear}
+ metaDescription="このサイトの著者について"
+ metaTitle={`About|${config.blog.siteName}`}
+ config={config}
+ >
+ <body className="single">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <article className="post-single">
+ <header className="post-header">
+ <h1 className="post-title">nsfisis</h1>
+ <div className="my-icon">
+ <StaticScript fileName="/p5.min.js" config={config} />
+ <StaticScript fileName="/my-icon.js" config={config} />
+ <div id="p5jsMyIcon" />
+ <noscript>
+ <img src="/favicon.svg" />
+ </noscript>
+ </div>
+ </header>
+ <div className="post-content">
+ <section>
+ <h2>読み方</h2>
+ <p>
+ 読み方は決めていません。音にする必要があるときは本名である「いまむら」をお使いください。
+ </p>
+ </section>
+ <section>
+ <h2>アカウント</h2>
+ <ul>
+ <li>
+ <a href="https://twitter.com/nsfisis">
+ Twitter (現 𝕏): @nsfisis
+ </a>
+ </li>
+ <li>
+ <a href="https://github.com/nsfisis">GitHub: @nsfisis</a>
+ </li>
+ </ul>
+ </section>
+ <section>
+ <h2>仕事</h2>
+ <ul>
+ <li>
+ {"2021-01~現在: "}
+ <a href="https://www.dgcircus.com/">
+ デジタルサーカス株式会社
+ </a>
+ </li>
+ </ul>
+ </section>
+ <section>
+ <h2>登壇</h2>
+ <ul>
+ {Array.from(slides).sort((a, b) => {
+ const ta = dateToString(getPostPublishedDate(a));
+ const tb = dateToString(getPostPublishedDate(b));
+ if (ta > tb) return -1;
+ if (ta < tb) return 1;
+ return 0;
+ }).map((slide) => (
+ <li>
+ <a href={slide.href}>
+ {`${
+ dateToString(getPostPublishedDate(slide))
+ }: ${slide.event} (${slide.talkType})`}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </section>
+ </div>
+ </article>
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/HomePage.tsx b/vhosts/blog/nuldoc-src/pages/HomePage.tsx
new file mode 100644
index 00000000..8850d039
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/HomePage.tsx
@@ -0,0 +1,53 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import { Config } from "../config.ts";
+
+export default function HomePage(config: Config) {
+ return (
+ <PageLayout
+ metaCopyrightYear={config.blog.siteCopyrightYear}
+ metaDescription="nsfisis のブログサイト"
+ metaTitle={config.blog.siteName}
+ metaAtomFeedHref={`https://${config.blog.fqdn}/atom.xml`}
+ config={config}
+ >
+ <body className="single">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <article className="post-single">
+ <article className="post-entry">
+ <a href="/about/">
+ <header className="entry-header">
+ <h2>About</h2>
+ </header>
+ </a>
+ </article>
+ <article className="post-entry">
+ <a href="/posts/">
+ <header className="entry-header">
+ <h2>Posts</h2>
+ </header>
+ </a>
+ </article>
+ <article className="post-entry">
+ <a href="/slides/">
+ <header className="entry-header">
+ <h2>Slides</h2>
+ </header>
+ </a>
+ </article>
+ <article className="post-entry">
+ <a href="/tags/">
+ <header className="entry-header">
+ <h2>Tags</h2>
+ </header>
+ </a>
+ </article>
+ </article>
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/NotFoundPage.tsx b/vhosts/blog/nuldoc-src/pages/NotFoundPage.tsx
new file mode 100644
index 00000000..9631fef2
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/NotFoundPage.tsx
@@ -0,0 +1,27 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import { Config } from "../config.ts";
+
+export default function NotFoundPage(
+ config: Config,
+) {
+ return (
+ <PageLayout
+ metaCopyrightYear={config.blog.siteCopyrightYear}
+ metaDescription="リクエストされたページが見つかりません"
+ metaTitle={`Page Not Found|${config.blog.siteName}`}
+ config={config}
+ >
+ <body className="single">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <article>
+ <div className="not-found">404</div>
+ </article>
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/PostListPage.tsx b/vhosts/blog/nuldoc-src/pages/PostListPage.tsx
new file mode 100644
index 00000000..3fcfbf1f
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/PostListPage.tsx
@@ -0,0 +1,41 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import PostPageEntry from "../components/PostPageEntry.tsx";
+import { Config } from "../config.ts";
+import { dateToString } from "../revision.ts";
+import { getPostPublishedDate, PostPage } from "../generators/post.ts";
+
+export default function PostListPage(
+ posts: PostPage[],
+ config: Config,
+) {
+ const pageTitle = "投稿一覧";
+
+ return (
+ <PageLayout
+ metaCopyrightYear={config.blog.siteCopyrightYear}
+ metaDescription="投稿した記事の一覧"
+ metaTitle={`${pageTitle}|${config.blog.siteName}`}
+ metaAtomFeedHref={`https://${config.blog.fqdn}/posts/atom.xml`}
+ config={config}
+ >
+ <body className="list">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <header className="page-header">
+ <h1>{pageTitle}</h1>
+ </header>
+ {Array.from(posts).sort((a, b) => {
+ const ta = dateToString(getPostPublishedDate(a));
+ const tb = dateToString(getPostPublishedDate(b));
+ if (ta > tb) return -1;
+ if (ta < tb) return 1;
+ return 0;
+ }).map((post) => <PostPageEntry post={post} />)}
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/PostPage.tsx b/vhosts/blog/nuldoc-src/pages/PostPage.tsx
new file mode 100644
index 00000000..e6aa83aa
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/PostPage.tsx
@@ -0,0 +1,74 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import { Config, getTagLabel } from "../config.ts";
+import { Element } from "../dom.ts";
+import { Document } from "../ndoc/document.ts";
+import { dateToString } from "../revision.ts";
+import { getPostPublishedDate } from "../generators/post.ts";
+
+export default function PostPage(
+ doc: Document,
+ config: Config,
+) {
+ return (
+ <PageLayout
+ metaCopyrightYear={getPostPublishedDate(doc).year}
+ metaDescription={doc.description}
+ metaKeywords={doc.tags.map((slug) => getTagLabel(config, slug))}
+ metaTitle={`${doc.title}|${config.blog.siteName}`}
+ requiresSyntaxHighlight
+ config={config}
+ >
+ <body className="single">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <article className="post-single">
+ <header className="post-header">
+ <h1 className="post-title">{doc.title}</h1>
+ {doc.tags.length !== 0 && (
+ <ul className="post-tags">
+ {doc.tags.map((slug) => (
+ <li className="tag">
+ <a href={`/tags/${slug}/`}>{getTagLabel(config, slug)}</a>
+ </li>
+ ))}
+ </ul>
+ )}
+ </header>
+ <div className="post-content">
+ <section>
+ <h2 id="changelog">更新履歴</h2>
+ <ol>
+ {doc.revisions.map((rev) => (
+ <li className="revision">
+ <time datetime={dateToString(rev.date)}>
+ {dateToString(rev.date)}
+ </time>
+ {`: ${rev.remark}`}
+ </li>
+ ))}
+ </ol>
+ </section>
+ {
+ // TODO: refactor
+ (doc.root.children[0] as Element).children
+ }
+ {
+ // TODO: footnotes
+ // <div id="footnotes">
+ // <% for footnote in footnotes %>
+ // <div class="footnote" id="_footnotedef_<%= footnote.index %>">
+ // <a href="#_footnoteref_<%= footnote.index %>"><%= footnote.index %></a>. <%= footnote.text %>
+ // </div>
+ // <% end %>
+ // </div>
+ }
+ </div>
+ </article>
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/SlideListPage.tsx b/vhosts/blog/nuldoc-src/pages/SlideListPage.tsx
new file mode 100644
index 00000000..44d6afff
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/SlideListPage.tsx
@@ -0,0 +1,42 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import SlidePageEntry from "../components/SlidePageEntry.tsx";
+import { Config } from "../config.ts";
+import { dateToString } from "../revision.ts";
+import { getPostPublishedDate } from "../generators/post.ts";
+import { SlidePage } from "../generators/slide.ts";
+
+export default function SlideListPage(
+ slides: SlidePage[],
+ config: Config,
+) {
+ const pageTitle = "スライド一覧";
+
+ return (
+ <PageLayout
+ metaCopyrightYear={config.blog.siteCopyrightYear}
+ metaDescription="登壇したイベントで使用したスライドの一覧"
+ metaTitle={`${pageTitle}|${config.blog.siteName}`}
+ metaAtomFeedHref={`https://${config.blog.fqdn}/slides/atom.xml`}
+ config={config}
+ >
+ <body className="list">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <header className="page-header">
+ <h1>{pageTitle}</h1>
+ </header>
+ {Array.from(slides).sort((a, b) => {
+ const ta = dateToString(getPostPublishedDate(a));
+ const tb = dateToString(getPostPublishedDate(b));
+ if (ta > tb) return -1;
+ if (ta < tb) return 1;
+ return 0;
+ }).map((slide) => <SlidePageEntry slide={slide} />)}
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/SlidePage.tsx b/vhosts/blog/nuldoc-src/pages/SlidePage.tsx
new file mode 100644
index 00000000..6d167036
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/SlidePage.tsx
@@ -0,0 +1,70 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import StaticScript from "../components/StaticScript.tsx";
+import { Config, getTagLabel } from "../config.ts";
+import { dateToString } from "../revision.ts";
+import { Slide } from "../slide/slide.ts";
+import { getPostPublishedDate } from "../generators/post.ts";
+
+export default function SlidePage(
+ slide: Slide,
+ config: Config,
+) {
+ return (
+ <PageLayout
+ metaCopyrightYear={getPostPublishedDate(slide).year}
+ metaDescription={slide.title}
+ metaKeywords={slide.tags.map((slug) => getTagLabel(config, slug))}
+ metaTitle={`${slide.event} (${slide.talkType})|${config.blog.siteName}`}
+ requiresSyntaxHighlight
+ config={config}
+ >
+ <body className="single">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <article className="post-single">
+ <header className="post-header">
+ <h1 className="post-title">{slide.title}</h1>
+ {slide.tags.length !== 0 && (
+ <ul className="post-tags">
+ {slide.tags.map((slug) => (
+ <li className="tag">
+ <a href={`/tags/${slug}/`}>{getTagLabel(config, slug)}</a>
+ </li>
+ ))}
+ </ul>
+ )}
+ </header>
+ <div className="post-content">
+ <section>
+ <h2 id="changelog">更新履歴</h2>
+ <ol>
+ {slide.revisions.map((rev) => (
+ <li className="revision">
+ <time datetime={dateToString(rev.date)}>
+ {dateToString(rev.date)}
+ </time>
+ {`: ${rev.remark}`}
+ </li>
+ ))}
+ </ol>
+ </section>
+ <canvas id="slide" data-slide-link={slide.slideLink} />
+ <div>
+ <button id="prev">Prev</button>
+ <button id="next">Next</button>
+ </div>
+ <StaticScript
+ fileName="/slide.js"
+ type="module"
+ config={config}
+ />
+ </div>
+ </article>
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/TagListPage.tsx b/vhosts/blog/nuldoc-src/pages/TagListPage.tsx
new file mode 100644
index 00000000..cdb83ea5
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/TagListPage.tsx
@@ -0,0 +1,57 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import { Config } from "../config.ts";
+import { TagPage } from "../generators/tag.ts";
+
+export default function TagListPage(
+ tags: TagPage[],
+ config: Config,
+) {
+ const pageTitle = "タグ一覧";
+
+ return (
+ <PageLayout
+ metaCopyrightYear={config.blog.siteCopyrightYear}
+ metaDescription="タグの一覧"
+ metaTitle={`${pageTitle}|${config.blog.siteName}`}
+ config={config}
+ >
+ <body className="list">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <header className="page-header">
+ <h1>{pageTitle}</h1>
+ </header>
+ {Array.from(tags).sort((a, b) => {
+ const ta = a.tagSlug;
+ const tb = b.tagSlug;
+ if (ta < tb) return -1;
+ if (ta > tb) return 1;
+ return 0;
+ }).map((tag) => (
+ <article className="post-entry">
+ <a href={tag.href}>
+ <header className="entry-header">
+ <h2>{tag.tagLabel}</h2>
+ </header>
+ <footer className="entry-footer">
+ {(() => {
+ const posts = tag.numOfPosts === 0
+ ? ""
+ : `${tag.numOfPosts}件の記事`;
+ const slides = tag.numOfSlides === 0
+ ? ""
+ : `${tag.numOfSlides}件のスライド`;
+ return `${posts}${posts && slides ? "、" : ""}${slides}`;
+ })()}
+ </footer>
+ </a>
+ </article>
+ ))}
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/TagPage.tsx b/vhosts/blog/nuldoc-src/pages/TagPage.tsx
new file mode 100644
index 00000000..568dfaa6
--- /dev/null
+++ b/vhosts/blog/nuldoc-src/pages/TagPage.tsx
@@ -0,0 +1,43 @@
+import GlobalFooter from "../components/GlobalFooter.tsx";
+import GlobalHeader from "../components/GlobalHeader.tsx";
+import PageLayout from "../components/PageLayout.tsx";
+import PostPageEntry from "../components/PostPageEntry.tsx";
+import SlidePageEntry from "../components/SlidePageEntry.tsx";
+import { Config, getTagLabel } from "../config.ts";
+import { getPostPublishedDate } from "../generators/post.ts";
+import { TaggedPage } from "../generators/tagged_page.ts";
+
+export default function TagPage(
+ tagSlug: string,
+ pages: TaggedPage[],
+ config: Config,
+) {
+ const tagLabel = getTagLabel(config, tagSlug);
+ const pageTitle = `タグ「${tagLabel}」一覧`;
+
+ return (
+ <PageLayout
+ metaCopyrightYear={getPostPublishedDate(pages[pages.length - 1]).year}
+ metaDescription={`タグ「${tagLabel}」のついた記事またはスライドの一覧`}
+ metaKeywords={[tagLabel]}
+ metaTitle={`${pageTitle}|${config.blog.siteName}`}
+ metaAtomFeedHref={`https://${config.blog.fqdn}/tags/${tagSlug}/atom.xml`}
+ config={config}
+ >
+ <body className="list">
+ <GlobalHeader config={config} />
+ <main className="main">
+ <header className="page-header">
+ <h1>{pageTitle}</h1>
+ </header>
+ {pages.map((page) =>
+ "event" in page
+ ? <SlidePageEntry slide={page} />
+ : <PostPageEntry post={page} />
+ )}
+ </main>
+ <GlobalFooter config={config} />
+ </body>
+ </PageLayout>
+ );
+}
diff --git a/vhosts/blog/nuldoc-src/pages/about.ts b/vhosts/blog/nuldoc-src/pages/about.ts
deleted file mode 100644
index b5411eb7..00000000
--- a/vhosts/blog/nuldoc-src/pages/about.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { staticScriptElement } from "../components/utils.ts";
-import { Config } from "../config.ts";
-import { el } from "../dom.ts";
-import { Page } from "../page.ts";
-import { dateToString } from "../revision.ts";
-import { getPostPublishedDate } from "./post.ts";
-import { SlidePage } from "./slide.ts";
-
-export type AboutPage = Page;
-
-export async function generateAboutPage(
- slides: SlidePage[],
- config: Config,
-): Promise<AboutPage> {
- const body = el(
- "body",
- { className: "single" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el(
- "article",
- { className: "post-single" },
- el(
- "header",
- { className: "post-header" },
- el("h1", { className: "post-title" }, "nsfisis"),
- el(
- "div",
- { className: "my-icon" },
- await staticScriptElement("/p5.min.js", {}, config),
- await staticScriptElement("/my-icon.js", {}, config),
- el("div", { id: "p5jsMyIcon" }),
- el(
- "noscript",
- {},
- el("img", { src: "/favicon.svg" }),
- ),
- ),
- ),
- el(
- "div",
- { className: "post-content" },
- el(
- "section",
- {},
- el("h2", {}, "読み方"),
- el(
- "p",
- {},
- "読み方は決めていません。音にする必要があるときは本名である「いまむら」をお使いください。",
- ),
- ),
- el(
- "section",
- {},
- el("h2", {}, "アカウント"),
- el(
- "ul",
- {},
- el(
- "li",
- {},
- el(
- "a",
- { href: "https://twitter.com/nsfisis" },
- "Twitter (現 𝕏): @nsfisis",
- ),
- ),
- el(
- "li",
- {},
- el(
- "a",
- { href: "https://github.com/nsfisis" },
- "GitHub: @nsfisis",
- ),
- ),
- ),
- ),
- el(
- "section",
- {},
- el("h2", {}, "仕事"),
- el(
- "ul",
- {},
- el(
- "li",
- {},
- "2021-01~現在: ",
- el(
- "a",
- { href: "https://www.dgcircus.com/" },
- "デジタルサーカス株式会社",
- ),
- ),
- ),
- ),
- el(
- "section",
- {},
- el("h2", {}, "登壇"),
- el(
- "ul",
- {},
- ...Array.from(slides).sort((a, b) => {
- const ta = dateToString(getPostPublishedDate(a));
- const tb = dateToString(getPostPublishedDate(b));
- if (ta > tb) return -1;
- if (ta < tb) return 1;
- return 0;
- }).map((slide) =>
- el(
- "li",
- {},
- el(
- "a",
- { href: slide.href },
- `${
- dateToString(getPostPublishedDate(slide))
- }: ${slide.event} (${slide.talkType})`,
- ),
- )
- ),
- ),
- ),
- ),
- ),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: config.blog.siteCopyrightYear,
- metaDescription: "このサイトの著者について",
- metaKeywords: [],
- metaTitle: `About|${config.blog.siteName}`,
- requiresSyntaxHighlight: false,
- },
- body,
- config,
- );
-
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: "/about/index.html",
- href: "/about/",
- };
-}
diff --git a/vhosts/blog/nuldoc-src/pages/home.ts b/vhosts/blog/nuldoc-src/pages/home.ts
deleted file mode 100644
index 86a767c0..00000000
--- a/vhosts/blog/nuldoc-src/pages/home.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { Config } from "../config.ts";
-import { el } from "../dom.ts";
-import { Page } from "../page.ts";
-
-export type HomePage = Page;
-
-export async function generateHomePage(config: Config): Promise<HomePage> {
- const body = el(
- "body",
- { className: "single" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el(
- "article",
- { className: "post-single" },
- el(
- "article",
- { className: "post-entry" },
- el(
- "a",
- { href: "/about/" },
- el(
- "header",
- { className: "entry-header" },
- el("h2", {}, "About"),
- ),
- ),
- ),
- el(
- "article",
- { className: "post-entry" },
- el(
- "a",
- { href: "/posts/" },
- el(
- "header",
- { className: "entry-header" },
- el("h2", {}, "Posts"),
- ),
- ),
- ),
- el(
- "article",
- { className: "post-entry" },
- el(
- "a",
- { href: "/slides/" },
- el(
- "header",
- { className: "entry-header" },
- el("h2", {}, "Slides"),
- ),
- ),
- ),
- el(
- "article",
- { className: "post-entry" },
- el(
- "a",
- { href: "/tags/" },
- el(
- "header",
- { className: "entry-header" },
- el("h2", {}, "Tags"),
- ),
- ),
- ),
- ),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: config.blog.siteCopyrightYear,
- metaDescription: "nsfisis のブログサイト",
- metaKeywords: [],
- metaTitle: config.blog.siteName,
- metaAtomFeedHref: `https://${config.blog.fqdn}/atom.xml`,
- requiresSyntaxHighlight: false,
- },
- body,
- config,
- );
-
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: "/index.html",
- href: "/",
- };
-}
diff --git a/vhosts/blog/nuldoc-src/pages/not_found.ts b/vhosts/blog/nuldoc-src/pages/not_found.ts
deleted file mode 100644
index bb70d8ac..00000000
--- a/vhosts/blog/nuldoc-src/pages/not_found.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { Config } from "../config.ts";
-import { el } from "../dom.ts";
-import { Page } from "../page.ts";
-
-export type NotFoundPage = Page;
-
-export async function generateNotFoundPage(
- config: Config,
-): Promise<NotFoundPage> {
- const body = el(
- "body",
- { className: "single" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el(
- "article",
- {},
- el("div", { className: "not-found" }, "404"),
- ),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: config.blog.siteCopyrightYear,
- metaDescription: "リクエストされたページが見つかりません",
- metaKeywords: [],
- metaTitle: `Page Not Found|${config.blog.siteName}`,
- requiresSyntaxHighlight: false,
- },
- body,
- config,
- );
-
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: "/404.html",
- href: "/404.html",
- };
-}
diff --git a/vhosts/blog/nuldoc-src/pages/post.ts b/vhosts/blog/nuldoc-src/pages/post.ts
deleted file mode 100644
index c0c3bb2e..00000000
--- a/vhosts/blog/nuldoc-src/pages/post.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { join } from "std/path/mod.ts";
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { Config, getTagLabel } from "../config.ts";
-import { el, Element } from "../dom.ts";
-import { Document } from "../ndoc/document.ts";
-import { Page } from "../page.ts";
-import { Date, dateToString, Revision } from "../revision.ts";
-
-export interface PostPage extends Page {
- title: string;
- description: string;
- tags: string[];
- revisions: Revision[];
- published: Date;
- updated: Date;
- uuid: string;
-}
-
-export function getPostPublishedDate(page: { revisions: Revision[] }): Date {
- for (const rev of page.revisions) {
- if (!rev.isInternal) {
- return rev.date;
- }
- }
- return page.revisions[0].date;
-}
-
-export function getPostUpdatedDate(page: { revisions: Revision[] }): Date {
- return page.revisions[page.revisions.length - 1].date;
-}
-
-export function postHasAnyUpdates(page: { revisions: Revision[] }): boolean {
- return 2 <= page.revisions.filter((rev) => !rev.isInternal).length;
-}
-
-export async function generatePostPage(
- doc: Document,
- config: Config,
-): Promise<PostPage> {
- const body = el(
- "body",
- { className: "single" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el(
- "article",
- { className: "post-single" },
- el(
- "header",
- { className: "post-header" },
- el("h1", { className: "post-title" }, doc.title),
- ...(doc.tags.length === 0 ? [] : [
- el(
- "ul",
- { className: "post-tags" },
- ...doc.tags.map((slug) =>
- el(
- "li",
- { className: "tag" },
- el(
- "a",
- { href: `/tags/${slug}/` },
- getTagLabel(config, slug),
- ),
- )
- ),
- ),
- ]),
- ),
- el(
- "div",
- { className: "post-content" },
- el(
- "section",
- {},
- el("h2", { id: "changelog" }, "更新履歴"),
- el(
- "ol",
- {},
- ...doc.revisions.map((rev) =>
- el(
- "li",
- { className: "revision" },
- el(
- "time",
- { datetime: dateToString(rev.date) },
- dateToString(rev.date),
- ),
- `: ${rev.remark}`,
- )
- ),
- ),
- ),
- // TODO: refactor
- ...(doc.root.children[0] as Element).children,
- // TODO: footnotes
- // <div id="footnotes">
- // <% for footnote in footnotes %>
- // <div class="footnote" id="_footnotedef_<%= footnote.index %>">
- // <a href="#_footnoteref_<%= footnote.index %>"><%= footnote.index %></a>. <%= footnote.text %>
- // </div>
- // <% end %>
- // </div>
- ),
- ),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: getPostPublishedDate(doc).year,
- metaDescription: doc.description,
- metaKeywords: doc.tags.map((slug) => getTagLabel(config, slug)),
- metaTitle: `${doc.title}|${config.blog.siteName}`,
- requiresSyntaxHighlight: true,
- },
- body,
- config,
- );
-
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir);
- const destFilePath = join(
- doc.sourceFilePath.replace(contentDir, "").replace(".ndoc", ""),
- "index.html",
- );
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: destFilePath,
- href: destFilePath.replace("index.html", ""),
- title: doc.title,
- description: doc.description,
- tags: doc.tags,
- revisions: doc.revisions,
- published: getPostPublishedDate(doc),
- updated: getPostUpdatedDate(doc),
- uuid: doc.uuid,
- };
-}
diff --git a/vhosts/blog/nuldoc-src/pages/post_list.ts b/vhosts/blog/nuldoc-src/pages/post_list.ts
deleted file mode 100644
index 40df8ed9..00000000
--- a/vhosts/blog/nuldoc-src/pages/post_list.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { postPageEntry } from "../components/post_page_entry.ts";
-import { Config } from "../config.ts";
-import { el } from "../dom.ts";
-import { Page } from "../page.ts";
-import { dateToString } from "../revision.ts";
-import { getPostPublishedDate, PostPage } from "./post.ts";
-
-export type PostListPage = Page;
-
-export async function generatePostListPage(
- posts: PostPage[],
- config: Config,
-): Promise<PostListPage> {
- const pageTitle = "投稿一覧";
-
- const body = el(
- "body",
- { className: "list" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el(
- "header",
- { className: "page-header" },
- el("h1", {}, pageTitle),
- ),
- ...Array.from(posts).sort((a, b) => {
- const ta = dateToString(getPostPublishedDate(a));
- const tb = dateToString(getPostPublishedDate(b));
- if (ta > tb) return -1;
- if (ta < tb) return 1;
- return 0;
- }).map((post) => postPageEntry(post)),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: config.blog.siteCopyrightYear,
- metaDescription: "投稿した記事の一覧",
- metaKeywords: [],
- metaTitle: `${pageTitle}|${config.blog.siteName}`,
- metaAtomFeedHref: `https://${config.blog.fqdn}/posts/atom.xml`,
- requiresSyntaxHighlight: false,
- },
- body,
- config,
- );
-
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: "/posts/index.html",
- href: "/posts/",
- };
-}
diff --git a/vhosts/blog/nuldoc-src/pages/slide.ts b/vhosts/blog/nuldoc-src/pages/slide.ts
deleted file mode 100644
index 15a18bb1..00000000
--- a/vhosts/blog/nuldoc-src/pages/slide.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import { join } from "std/path/mod.ts";
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { staticScriptElement } from "../components/utils.ts";
-import { Config, getTagLabel } from "../config.ts";
-import { el } from "../dom.ts";
-import { Page } from "../page.ts";
-import { Date, dateToString, Revision } from "../revision.ts";
-import { Slide } from "../slide/slide.ts";
-import { getPostPublishedDate, getPostUpdatedDate } from "./post.ts";
-
-export interface SlidePage extends Page {
- title: string;
- description: string;
- event: string;
- talkType: string;
- slideLink: string;
- tags: string[];
- revisions: Revision[];
- published: Date;
- updated: Date;
- uuid: string;
-}
-
-export async function generateSlidePage(
- slide: Slide,
- config: Config,
-): Promise<SlidePage> {
- const body = el(
- "body",
- { className: "single" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el(
- "article",
- { className: "post-single" },
- el(
- "header",
- { className: "post-header" },
- el("h1", { className: "post-title" }, slide.title),
- ...(slide.tags.length === 0 ? [] : [
- el(
- "ul",
- { className: "post-tags" },
- ...slide.tags.map((slug) =>
- el(
- "li",
- { className: "tag" },
- el(
- "a",
- { href: `/tags/${slug}/` },
- getTagLabel(config, slug),
- ),
- )
- ),
- ),
- ]),
- ),
- el(
- "div",
- { className: "post-content" },
- el(
- "section",
- {},
- el("h2", { id: "changelog" }, "更新履歴"),
- el(
- "ol",
- {},
- ...slide.revisions.map((rev) =>
- el(
- "li",
- { className: "revision" },
- el(
- "time",
- { datetime: dateToString(rev.date) },
- dateToString(rev.date),
- ),
- `: ${rev.remark}`,
- )
- ),
- ),
- ),
- el(
- "canvas",
- { id: "slide", "data-slide-link": slide.slideLink },
- ),
- el(
- "div",
- {},
- el("button", { id: "prev" }, "Prev"),
- el("button", { id: "next" }, "Next"),
- ),
- await staticScriptElement("/slide.js", { type: "module" }, config),
- ),
- ),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: getPostPublishedDate(slide).year,
- metaDescription: slide.title,
- metaKeywords: slide.tags.map((slug) => getTagLabel(config, slug)),
- metaTitle: `${slide.event} (${slide.talkType})|${config.blog.siteName}`,
- requiresSyntaxHighlight: true,
- },
- body,
- config,
- );
-
- const cwd = Deno.cwd();
- const contentDir = join(cwd, config.locations.contentDir);
- const destFilePath = join(
- slide.sourceFilePath.replace(contentDir, "").replace(".toml", ""),
- "index.html",
- );
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: destFilePath,
- href: destFilePath.replace("index.html", ""),
- title: slide.title,
- description: `登壇: ${slide.event} (${slide.talkType})`,
- event: slide.event,
- talkType: slide.talkType,
- slideLink: slide.slideLink,
- tags: slide.tags,
- revisions: slide.revisions,
- published: getPostPublishedDate(slide),
- updated: getPostUpdatedDate(slide),
- uuid: slide.uuid,
- };
-}
diff --git a/vhosts/blog/nuldoc-src/pages/slide_list.ts b/vhosts/blog/nuldoc-src/pages/slide_list.ts
deleted file mode 100644
index 53b2d2b1..00000000
--- a/vhosts/blog/nuldoc-src/pages/slide_list.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { slidePageEntry } from "../components/slide_page_entry.ts";
-import { Config } from "../config.ts";
-import { el } from "../dom.ts";
-import { Page } from "../page.ts";
-import { dateToString } from "../revision.ts";
-import { getPostPublishedDate } from "./post.ts";
-import { SlidePage } from "./slide.ts";
-
-export type SlideListPage = Page;
-
-export async function generateSlideListPage(
- slides: SlidePage[],
- config: Config,
-): Promise<SlideListPage> {
- const pageTitle = "スライド一覧";
-
- const body = el(
- "body",
- { className: "list" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el(
- "header",
- { className: "page-header" },
- el("h1", {}, pageTitle),
- ),
- ...Array.from(slides).sort((a, b) => {
- const ta = dateToString(getPostPublishedDate(a));
- const tb = dateToString(getPostPublishedDate(b));
- if (ta > tb) return -1;
- if (ta < tb) return 1;
- return 0;
- }).map((slide) => slidePageEntry(slide)),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: config.blog.siteCopyrightYear,
- metaDescription: "登壇したイベントで使用したスライドの一覧",
- metaKeywords: [],
- metaTitle: `${pageTitle}|${config.blog.siteName}`,
- metaAtomFeedHref: `https://${config.blog.fqdn}/slides/atom.xml`,
- requiresSyntaxHighlight: false,
- },
- body,
- config,
- );
-
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: "/slides/index.html",
- href: "/slides/",
- };
-}
diff --git a/vhosts/blog/nuldoc-src/pages/tag.ts b/vhosts/blog/nuldoc-src/pages/tag.ts
deleted file mode 100644
index 32711f7e..00000000
--- a/vhosts/blog/nuldoc-src/pages/tag.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { postPageEntry } from "../components/post_page_entry.ts";
-import { slidePageEntry } from "../components/slide_page_entry.ts";
-import { Config, getTagLabel } from "../config.ts";
-import { el } from "../dom.ts";
-import { Page } from "../page.ts";
-import { getPostPublishedDate } from "./post.ts";
-import { TaggedPage } from "./tagged_page.ts";
-
-export interface TagPage extends Page {
- tagSlug: string;
- tagLabel: string;
- numOfPosts: number;
- numOfSlides: number;
-}
-
-export async function generateTagPage(
- tagSlug: string,
- pages: TaggedPage[],
- config: Config,
-): Promise<TagPage> {
- const tagLabel = getTagLabel(config, tagSlug);
- const pageTitle = `タグ「${tagLabel}」一覧`;
-
- const body = el(
- "body",
- { className: "list" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el("header", { className: "page-header" }, el("h1", {}, pageTitle)),
- ...pages.map((page) =>
- "event" in page ? slidePageEntry(page) : postPageEntry(page)
- ),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: getPostPublishedDate(pages[pages.length - 1]).year,
- metaDescription: `タグ「${tagLabel}」のついた記事またはスライドの一覧`,
- metaKeywords: [tagLabel],
- metaTitle: `${pageTitle}|${config.blog.siteName}`,
- metaAtomFeedHref: `https://${config.blog.fqdn}/tags/${tagSlug}/atom.xml`,
- requiresSyntaxHighlight: false,
- },
- body,
- config,
- );
-
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: `/tags/${tagSlug}/index.html`,
- href: `/tags/${tagSlug}/`,
- tagSlug: tagSlug,
- tagLabel: tagLabel,
- numOfPosts: pages.filter((p) => !("event" in p)).length,
- numOfSlides: pages.filter((p) => "event" in p).length,
- };
-}
diff --git a/vhosts/blog/nuldoc-src/pages/tag_list.ts b/vhosts/blog/nuldoc-src/pages/tag_list.ts
deleted file mode 100644
index 57f6a293..00000000
--- a/vhosts/blog/nuldoc-src/pages/tag_list.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { globalFooter } from "../components/global_footer.ts";
-import { globalHeader } from "../components/global_header.ts";
-import { pageLayout } from "../components/page_layout.ts";
-import { Config } from "../config.ts";
-import { el } from "../dom.ts";
-import { Page } from "../page.ts";
-import { TagPage } from "./tag.ts";
-
-export type TagListPage = Page;
-
-export async function generateTagListPage(
- tags: TagPage[],
- config: Config,
-): Promise<TagListPage> {
- const pageTitle = "タグ一覧";
-
- const body = el(
- "body",
- { className: "list" },
- globalHeader(config),
- el(
- "main",
- { className: "main" },
- el(
- "header",
- { className: "page-header" },
- el("h1", {}, pageTitle),
- ),
- ...Array.from(tags).sort((a, b) => {
- const ta = a.tagSlug;
- const tb = b.tagSlug;
- if (ta < tb) return -1;
- if (ta > tb) return 1;
- return 0;
- }).map((tag) =>
- el(
- "article",
- { className: "post-entry" },
- el(
- "a",
- { href: tag.href },
- el(
- "header",
- { className: "entry-header" },
- el("h2", {}, tag.tagLabel),
- ),
- el(
- "footer",
- { className: "entry-footer" },
- (() => {
- const posts = tag.numOfPosts === 0
- ? ""
- : `${tag.numOfPosts}件の記事`;
- const slides = tag.numOfSlides === 0
- ? ""
- : `${tag.numOfSlides}件のスライド`;
- return `${posts}${posts && slides ? "、" : ""}${slides}`;
- })(),
- ),
- ),
- )
- ),
- ),
- globalFooter(config),
- );
-
- const html = await pageLayout(
- {
- metaCopyrightYear: config.blog.siteCopyrightYear,
- metaDescription: "タグの一覧",
- metaKeywords: [],
- metaTitle: `${pageTitle}|${config.blog.siteName}`,
- requiresSyntaxHighlight: false,
- },
- body,
- config,
- );
-
- return {
- root: el("__root__", {}, html),
- renderer: "html",
- destFilePath: "/tags/index.html",
- href: "/tags/",
- };
-}
diff --git a/vhosts/blog/nuldoc-src/renderers/html.ts b/vhosts/blog/nuldoc-src/renderers/html.ts
index 70d097b2..5f1342f1 100644
--- a/vhosts/blog/nuldoc-src/renderers/html.ts
+++ b/vhosts/blog/nuldoc-src/renderers/html.ts
@@ -3,7 +3,7 @@ import { DocBookError } from "../errors.ts";
export function renderHtml(root: Node): string {
return `<!DOCTYPE html>\n` + nodeToHtmlText(root, {
- indentLevel: -1,
+ indentLevel: 0,
isInPre: false,
});
}
@@ -17,8 +17,6 @@ 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":
@@ -164,29 +162,29 @@ function elementNodeToHtmlText(e: Element, ctx: Context): string {
const dtd = getDtd(e.name);
let s = "";
- if (e.name !== "__root__") {
- if (isBlockNode(e)) {
- 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 === "className" ? "class" : name}="${
- encodeSpecialCharacters(value)
- }"`;
- if (i !== attributes.length - 1) {
- s += " ";
- }
+
+ if (isBlockNode(e)) {
+ 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 === "className" ? "class" : name}="${
+ encodeSpecialCharacters(value)
+ }"`;
+ if (i !== attributes.length - 1) {
+ s += " ";
}
}
- s += ">";
- if (isBlockNode(e) && e.name !== "pre") {
- s += "\n";
- }
}
+ s += ">";
+ if (isBlockNode(e) && e.name !== "pre") {
+ s += "\n";
+ }
+
ctx.indentLevel += 1;
let prevChild: Node | null = null;
@@ -213,7 +211,7 @@ function elementNodeToHtmlText(e: Element, ctx: Context): string {
}
ctx.indentLevel -= 1;
- if (e.name !== "__root__" && !dtd.auto_closing) {
+ if (!dtd.auto_closing) {
if (e.name !== "pre") {
if (isBlockNode(e)) {
if (needsLineBreak(prevChild)) {
diff --git a/vhosts/blog/nuldoc-src/renderers/xml.ts b/vhosts/blog/nuldoc-src/renderers/xml.ts
index 69b8266c..77cc1574 100644
--- a/vhosts/blog/nuldoc-src/renderers/xml.ts
+++ b/vhosts/blog/nuldoc-src/renderers/xml.ts
@@ -2,7 +2,7 @@ import { Element, forEachChild, Node, Text } from "../dom.ts";
export function renderXml(root: Node): string {
return `<?xml version="1.0" encoding="utf-8"?>\n` + nodeToXmlText(root, {
- indentLevel: -1,
+ indentLevel: 0,
});
}
@@ -14,7 +14,6 @@ type Dtd = { type: "block" | "inline" };
function getDtd(name: string): Dtd {
switch (name) {
- case "__root__":
case "feed":
case "entry":
case "author":
@@ -64,24 +63,23 @@ function encodeSpecialCharacters(s: string): string {
function elementNodeToXmlText(e: Element, ctx: Context): string {
let s = "";
- if (e.name !== "__root__") {
- 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}="${encodeSpecialCharacters(value)}"`;
- if (i !== attributes.length - 1) {
- s += " ";
- }
+
+ 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}="${encodeSpecialCharacters(value)}"`;
+ if (i !== attributes.length - 1) {
+ s += " ";
}
}
- s += ">";
- if (isBlockNode(e)) {
- s += "\n";
- }
+ }
+ s += ">";
+ if (isBlockNode(e)) {
+ s += "\n";
}
ctx.indentLevel += 1;
@@ -90,13 +88,12 @@ function elementNodeToXmlText(e: Element, ctx: Context): string {
});
ctx.indentLevel -= 1;
- if (e.name !== "__root__") {
- if (isBlockNode(e)) {
- s += indent(ctx);
- }
- s += `</${e.name}>`;
- s += "\n";
+ if (isBlockNode(e)) {
+ s += indent(ctx);
}
+ s += `</${e.name}>`;
+ s += "\n";
+
return s;
}