summaryrefslogtreecommitdiffhomepage
path: root/services/blog/nuldoc-src/commands
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-06-27 23:39:31 +0900
committernsfisis <nsfisis@gmail.com>2025-06-27 23:39:31 +0900
commit674fe965550444db87edc7937ff6932e1a918d9d (patch)
treee8a80dd958d3e082485286bf5785a7992b6e6b0e /services/blog/nuldoc-src/commands
parentfe4d1d625b53796c5f20399790e5ff8c7a7e1608 (diff)
downloadnsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.tar.gz
nsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.tar.zst
nsfisis.dev-674fe965550444db87edc7937ff6932e1a918d9d.zip
feat(meta): rename vhosts/ directory to services/
Diffstat (limited to 'services/blog/nuldoc-src/commands')
-rw-r--r--services/blog/nuldoc-src/commands/build.ts260
-rw-r--r--services/blog/nuldoc-src/commands/new.ts97
-rw-r--r--services/blog/nuldoc-src/commands/serve.ts45
3 files changed, 402 insertions, 0 deletions
diff --git a/services/blog/nuldoc-src/commands/build.ts b/services/blog/nuldoc-src/commands/build.ts
new file mode 100644
index 00000000..3f765441
--- /dev/null
+++ b/services/blog/nuldoc-src/commands/build.ts
@@ -0,0 +1,260 @@
+import { dirname, join, joinGlobs } from "@std/path";
+import { ensureDir, expandGlob } from "@std/fs";
+import { generateFeedPageFromEntries } from "../generators/atom.ts";
+import { Config, getTagLabel } from "../config.ts";
+import { parseDjotFile } from "../djot/parse.ts";
+import { Page } from "../page.ts";
+import { render } from "../render.ts";
+import { dateToString } from "../revision.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 "../generators/post.ts";
+import { generatePostListPages } 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) {
+ const posts = await buildPostPages(config);
+ await buildPostListPage(posts, config);
+ const slides = await buildSlidePages(config);
+ await buildSlideListPage(slides, config);
+ const tags = await buildTagPages(posts, slides, config);
+ await buildTagListPage(tags, config);
+ await buildHomePage(config);
+ await buildAboutPage(slides, config);
+ await buildNotFoundPage(config);
+ await buildFeedOfAllContents(posts, slides, config);
+ await copyStaticFiles(config);
+ await copyAssetFiles(slides, config);
+}
+
+async function buildPostPages(config: Config): Promise<PostPage[]> {
+ const sourceDir = join(Deno.cwd(), config.locations.contentDir, "posts");
+ const postFiles = await collectPostFiles(sourceDir);
+ const posts = await parsePosts(postFiles, config);
+ for (const post of posts) {
+ await writePage(post, config);
+ }
+ return posts;
+}
+
+async function collectPostFiles(sourceDir: string): Promise<string[]> {
+ const filePaths = [];
+ const globPattern = joinGlobs([sourceDir, "**", "*.dj"]);
+ for await (const entry of expandGlob(globPattern)) {
+ filePaths.push(entry.path);
+ }
+ return filePaths;
+}
+
+async function parsePosts(
+ postFiles: string[],
+ config: Config,
+): Promise<PostPage[]> {
+ const posts = [];
+ for (const postFile of postFiles) {
+ posts.push(
+ await generatePostPage(await parseDjotFile(postFile, config), config),
+ );
+ }
+ return posts;
+}
+
+async function buildPostListPage(posts: PostPage[], config: Config) {
+ // Sort posts by published date (newest first)
+ const sortedPosts = [...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;
+ });
+
+ const postListPages = await generatePostListPages(sortedPosts, config);
+ for (const page of postListPages) {
+ await writePage(page, config);
+ }
+
+ const postFeedPage = await generateFeedPageFromEntries(
+ "/posts/",
+ "posts",
+ `投稿一覧|${config.blog.siteName}`,
+ posts,
+ config,
+ );
+ await writePage(postFeedPage, config);
+}
+
+async function buildSlidePages(config: Config): Promise<SlidePage[]> {
+ const sourceDir = join(Deno.cwd(), config.locations.contentDir, "slides");
+ const slideFiles = await collectSlideFiles(sourceDir);
+ const slides = await parseSlides(slideFiles, config);
+ for (const slide of slides) {
+ await writePage(slide, config);
+ }
+ return slides;
+}
+
+async function collectSlideFiles(sourceDir: string): Promise<string[]> {
+ const filePaths = [];
+ const globPattern = joinGlobs([sourceDir, "**", "*.toml"]);
+ for await (const entry of expandGlob(globPattern)) {
+ filePaths.push(entry.path);
+ }
+ return filePaths;
+}
+
+async function parseSlides(
+ slideFiles: string[],
+ config: Config,
+): Promise<SlidePage[]> {
+ const slides = [];
+ for (const slideFile of slideFiles) {
+ slides.push(
+ await generateSlidePage(await parseSlideFile(slideFile), config),
+ );
+ }
+ return slides;
+}
+
+async function buildSlideListPage(slides: SlidePage[], config: Config) {
+ const slideListPage = await generateSlideListPage(slides, config);
+ await writePage(slideListPage, config);
+ const slideFeedPage = await generateFeedPageFromEntries(
+ slideListPage.href,
+ "slides",
+ `スライド一覧|${config.blog.siteName}`,
+ slides,
+ config,
+ );
+ await writePage(slideFeedPage, config);
+}
+
+async function buildHomePage(config: Config) {
+ const homePage = await generateHomePage(config);
+ await writePage(homePage, config);
+}
+
+async function buildAboutPage(slides: SlidePage[], config: Config) {
+ const aboutPage = await generateAboutPage(slides, config);
+ await writePage(aboutPage, config);
+}
+
+async function buildNotFoundPage(config: Config) {
+ const notFoundPage = await generateNotFoundPage(config);
+ await writePage(notFoundPage, config);
+}
+
+async function buildFeedOfAllContents(
+ posts: PostPage[],
+ slides: SlidePage[],
+ config: Config,
+) {
+ const feed = await generateFeedPageFromEntries(
+ "/",
+ "all",
+ config.blog.siteName,
+ [...posts, ...slides],
+ config,
+ );
+ await writePage(feed, config);
+}
+
+async function buildTagPages(
+ posts: PostPage[],
+ slides: SlidePage[],
+ config: Config,
+): Promise<TagPage[]> {
+ const tagsAndPages = collectTags([...posts, ...slides]);
+ const tags = [];
+ for (const [tag, pages] of tagsAndPages) {
+ const tagPage = await generateTagPage(tag, pages, config);
+ await writePage(tagPage, config);
+ const tagFeedPage = await generateFeedPageFromEntries(
+ tagPage.href,
+ `tag-${tag}`,
+ `タグ「${getTagLabel(config, tag)}」一覧|${config.blog.siteName}`,
+ pages,
+ config,
+ );
+ await writePage(tagFeedPage, config);
+ tags.push(tagPage);
+ }
+ return tags;
+}
+
+async function buildTagListPage(tags: TagPage[], config: Config) {
+ const tagListPage = await generateTagListPage(tags, config);
+ await writePage(tagListPage, config);
+}
+
+function collectTags(taggedPages: TaggedPage[]): [string, TaggedPage[]][] {
+ const tagsAndPages = new Map();
+ for (const page of taggedPages) {
+ for (const tag of page.tags) {
+ if (!tagsAndPages.has(tag)) {
+ tagsAndPages.set(tag, []);
+ }
+ tagsAndPages.get(tag).push(page);
+ }
+ }
+
+ const result: [string, TaggedPage[]][] = [];
+ for (const tag of Array.from(tagsAndPages.keys()).sort()) {
+ result.push([
+ tag,
+ tagsAndPages.get(tag).sort((a: TaggedPage, b: TaggedPage) => {
+ const ta = dateToString(getPostPublishedDate(a));
+ const tb = dateToString(getPostPublishedDate(b));
+ if (ta > tb) return -1;
+ if (ta < tb) return 1;
+ return 0;
+ }),
+ ]);
+ }
+ return result;
+}
+
+async function copyStaticFiles(config: Config) {
+ const globPattern = joinGlobs([Deno.cwd(), config.locations.staticDir, "*"]);
+ for await (const entry of expandGlob(globPattern)) {
+ const src = entry.path;
+ const dst = src.replace(
+ config.locations.staticDir,
+ config.locations.destDir,
+ );
+ await Deno.copyFile(src, dst);
+ }
+}
+
+async function copyAssetFiles(slides: SlidePage[], config: Config) {
+ const cwd = Deno.cwd();
+ const contentDir = join(cwd, config.locations.contentDir);
+ const destDir = join(cwd, config.locations.destDir);
+
+ for (const slide of slides) {
+ const src = join(contentDir, slide.slideLink);
+ const dst = join(destDir, slide.slideLink);
+ await ensureDir(dirname(dst));
+ await Deno.copyFile(src, dst);
+ }
+}
+
+async function writePage(page: Page, config: Config) {
+ const destFilePath = join(
+ Deno.cwd(),
+ config.locations.destDir,
+ page.destFilePath,
+ );
+ await ensureDir(dirname(destFilePath));
+ await Deno.writeTextFile(destFilePath, render(page.root, page.renderer));
+}
diff --git a/services/blog/nuldoc-src/commands/new.ts b/services/blog/nuldoc-src/commands/new.ts
new file mode 100644
index 00000000..651c59e6
--- /dev/null
+++ b/services/blog/nuldoc-src/commands/new.ts
@@ -0,0 +1,97 @@
+import { dirname, join } from "@std/path";
+import { ensureDir } from "@std/fs";
+import { parseArgs } from "@std/cli";
+import { Config } from "../config.ts";
+
+export async function runNewCommand(config: Config) {
+ const parsedArgs = parseArgs(Deno.args, {
+ string: ["date"],
+ });
+
+ const type = parsedArgs._[1];
+ if (type !== "post" && type !== "slide") {
+ console.log(`Usage: nuldoc new <type>
+
+<type> must be either "post" or "slide".
+
+OPTIONS:
+ --date <DATE>
+`);
+ Deno.exit(1);
+ }
+
+ const ymd = (() => {
+ if (parsedArgs.date) {
+ return parsedArgs.date;
+ }
+
+ const now = new Date();
+ const y = now.getFullYear();
+ const d = (now.getMonth() + 1).toString().padStart(2, "0");
+ const m = now.getDate().toString().padStart(2, "0");
+ return `${y}-${d}-${m}`;
+ })();
+
+ const destFilePath = join(
+ Deno.cwd(),
+ config.locations.contentDir,
+ getDirPath(type),
+ ymd,
+ getFilename(type),
+ );
+
+ await ensureDir(dirname(destFilePath));
+ await Deno.writeTextFile(destFilePath, getTemplate(type, ymd));
+ console.log(
+ `New file ${
+ destFilePath.replace(Deno.cwd(), "")
+ } was successfully created.`,
+ );
+}
+
+function getFilename(type: "post" | "slide"): string {
+ return type === "post" ? "TODO.dj" : "TODO.toml";
+}
+
+function getDirPath(type: "post" | "slide"): string {
+ return type === "post" ? "posts" : "slides";
+}
+
+function getTemplate(type: "post" | "slide", date: string): string {
+ const uuid = crypto.randomUUID();
+ if (type === "post") {
+ return `---
+[article]
+uuid = "${uuid}"
+title = "TODO"
+description = "TODO"
+tags = [
+ "TODO",
+]
+
+[[article.revisions]]
+date = "${date}"
+remark = "公開"
+---
+{#TODO}
+# TODO
+
+TODO
+`;
+ } else {
+ return `[slide]
+uuid = "${uuid}"
+title = "TODO"
+event = "TODO"
+talkType = "TODO"
+link = "TODO"
+tags = [
+ "TODO",
+]
+
+[[slide.revisions]]
+date = "${date}"
+remark = "登壇"
+`;
+ }
+}
diff --git a/services/blog/nuldoc-src/commands/serve.ts b/services/blog/nuldoc-src/commands/serve.ts
new file mode 100644
index 00000000..e944aaf0
--- /dev/null
+++ b/services/blog/nuldoc-src/commands/serve.ts
@@ -0,0 +1,45 @@
+import { serveDir, STATUS_CODE, STATUS_TEXT } from "@std/http";
+import { join } from "@std/path";
+import { Config } from "../config.ts";
+import { runBuildCommand } from "./build.ts";
+
+function isResourcePath(pathname: string): boolean {
+ const EXTENSIONS = [
+ ".css",
+ ".gif",
+ ".ico",
+ ".jpeg",
+ ".jpg",
+ ".js",
+ ".png",
+ ".svg",
+ ];
+ return EXTENSIONS.some((ext) => pathname.endsWith(ext));
+}
+
+export function runServeCommand(config: Config) {
+ const rootDir = join(Deno.cwd(), config.locations.destDir);
+ Deno.serve({ hostname: "127.0.0.1" }, async (req) => {
+ const pathname = new URL(req.url).pathname;
+ if (!isResourcePath(pathname)) {
+ await runBuildCommand(config);
+ console.log("rebuild");
+ }
+ const res = await serveDir(req, {
+ fsRoot: rootDir,
+ showIndex: true,
+ });
+ if (res.status !== STATUS_CODE.NotFound) {
+ return res;
+ }
+
+ const notFoundHtml = await Deno.readTextFile(join(rootDir, "404.html"));
+ return new Response(notFoundHtml, {
+ status: STATUS_CODE.NotFound,
+ statusText: STATUS_TEXT[STATUS_CODE.NotFound],
+ headers: {
+ "content-type": "text/html",
+ },
+ });
+ });
+}