diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-06-27 23:39:31 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-06-27 23:39:31 +0900 |
| commit | 674fe965550444db87edc7937ff6932e1a918d9d (patch) | |
| tree | e8a80dd958d3e082485286bf5785a7992b6e6b0e /services/blog/nuldoc-src/commands | |
| parent | fe4d1d625b53796c5f20399790e5ff8c7a7e1608 (diff) | |
| download | nsfisis.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.ts | 260 | ||||
| -rw-r--r-- | services/blog/nuldoc-src/commands/new.ts | 97 | ||||
| -rw-r--r-- | services/blog/nuldoc-src/commands/serve.ts | 45 |
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", + }, + }); + }); +} |
