diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-01-12 22:17:44 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-01-12 22:17:44 +0900 |
| commit | b14dd149e1b5f75bc494623181b19970ef830dec (patch) | |
| tree | dfff5ee9b9a9e16680fda5cba36e5c817eda1f9e | |
| parent | aecf316775c995f089012d8fec5c5cc77f6300be (diff) | |
| parent | 16182acfcc1fad2885b9c1a96fe74d8ce56a50e0 (diff) | |
| download | nsfisis.dev-b14dd149e1b5f75bc494623181b19970ef830dec.tar.gz nsfisis.dev-b14dd149e1b5f75bc494623181b19970ef830dec.tar.zst nsfisis.dev-b14dd149e1b5f75bc494623181b19970ef830dec.zip | |
Merge branch 'feature/blog-nuldoc-jsx'
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"> + {`© ${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={`© ${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" }, - `© ${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: `© ${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; } |
