| 
 | 1 | +#!/usr/bin/env -S deno run --allow-write=./_site,./tmp --allow-read=/tmp,./ --allow-net --allow-run=./main.ts,lua  | 
 | 2 | +import { std } from "./deps.ts";  | 
 | 3 | +import * as templates from "./templates.ts";  | 
 | 4 | +import * as djot from "./djot.ts";  | 
 | 5 | +import { HtmlString } from "./templates.ts";  | 
 | 6 | + | 
 | 7 | +let build_id = 0;  | 
 | 8 | +async function watch() {  | 
 | 9 | +  async function rebuild() {  | 
 | 10 | +    try {  | 
 | 11 | +      console.log(`rebuild #${build_id}`);  | 
 | 12 | +      build_id += 1;  | 
 | 13 | +      await Deno.run({ cmd: ["./main.ts", "build", "--update"] }).status();  | 
 | 14 | +    } catch {  | 
 | 15 | +      // ignore  | 
 | 16 | +    }  | 
 | 17 | +  }  | 
 | 18 | + | 
 | 19 | +  await std.fs.emptyDir("./_site");  | 
 | 20 | +  await rebuild();  | 
 | 21 | + | 
 | 22 | +  const rebuild_debounced = std.async.debounce(  | 
 | 23 | +    rebuild,  | 
 | 24 | +    16,  | 
 | 25 | +  );  | 
 | 26 | + | 
 | 27 | +  outer:  | 
 | 28 | +  for await (const event of Deno.watchFs("./", { recursive: true })) {  | 
 | 29 | +    for (const path of event.paths) {  | 
 | 30 | +      if (path.match(/\.\/(tmp|_site)/)) {  | 
 | 31 | +        continue outer;  | 
 | 32 | +      }  | 
 | 33 | +    }  | 
 | 34 | +    if (event.kind == "access") continue outer;  | 
 | 35 | +    rebuild_debounced();  | 
 | 36 | +  }  | 
 | 37 | +}  | 
 | 38 | + | 
 | 39 | +async function build() {  | 
 | 40 | +  const start = performance.now();  | 
 | 41 | + | 
 | 42 | +  if (Deno.args.includes("--update")) {  | 
 | 43 | +    await Deno.mkdir("_site", { recursive: true });  | 
 | 44 | +  } else {  | 
 | 45 | +    await std.fs.emptyDir("./_site");  | 
 | 46 | +  }  | 
 | 47 | + | 
 | 48 | +  const posts = await collect_posts();  | 
 | 49 | + | 
 | 50 | +  await update_file("_site/index.html", templates.post_list(posts).value);  | 
 | 51 | +  await update_file("_site/feed.xml", templates.feed(posts).value);  | 
 | 52 | +  await update_file("_site/about.html", templates.about().value);  | 
 | 53 | +  for (const post of posts) {  | 
 | 54 | +    await update_file(`_site${post.path}`, templates.post(post).value);  | 
 | 55 | +  }  | 
 | 56 | + | 
 | 57 | +  const paths = [  | 
 | 58 | +    "favicon.ico",  | 
 | 59 | +    "css/*",  | 
 | 60 | +    "assets/*",  | 
 | 61 | +  ];  | 
 | 62 | +  for (const path of paths) {  | 
 | 63 | +    await update_path(path);  | 
 | 64 | +  }  | 
 | 65 | + | 
 | 66 | +  const end = performance.now();  | 
 | 67 | +  console.log(`${end - start}ms`);  | 
 | 68 | +}  | 
 | 69 | + | 
 | 70 | +async function update_file(path: string, contents: Uint8Array | string) {  | 
 | 71 | +  if (!contents) return;  | 
 | 72 | +  await std.fs.ensureFile(path);  | 
 | 73 | +  await std.fs.ensureDir("./tmp");  | 
 | 74 | +  const temp = await Deno.makeTempFile({ dir: "./tmp" });  | 
 | 75 | +  if (contents instanceof Uint8Array) {  | 
 | 76 | +    await Deno.writeFile(temp, contents);  | 
 | 77 | +  } else {  | 
 | 78 | +    await Deno.writeTextFile(temp, contents);  | 
 | 79 | +  }  | 
 | 80 | +  await Deno.rename(temp, path);  | 
 | 81 | +}  | 
 | 82 | + | 
 | 83 | +async function update_path(path: string) {  | 
 | 84 | +  if (path.endsWith("*")) {  | 
 | 85 | +    const dir = path.replace("*", "");  | 
 | 86 | +    const futs = [];  | 
 | 87 | +    for await (const entry of Deno.readDir(`src/${dir}`)) {  | 
 | 88 | +      futs.push(update_path(`${dir}/${entry.name}`));  | 
 | 89 | +    }  | 
 | 90 | +    await Promise.all(futs);  | 
 | 91 | +  } else {  | 
 | 92 | +    await update_file(  | 
 | 93 | +      `_site/${path}`,  | 
 | 94 | +      await Deno.readFile(`src/${path}`),  | 
 | 95 | +    );  | 
 | 96 | +  }  | 
 | 97 | +}  | 
 | 98 | + | 
 | 99 | +export type Post = {  | 
 | 100 | +  year: number;  | 
 | 101 | +  month: number;  | 
 | 102 | +  day: number;  | 
 | 103 | +  slug: string;  | 
 | 104 | +  date: Date;  | 
 | 105 | +  title: HtmlString;  | 
 | 106 | +  path: string;  | 
 | 107 | +  src: string;  | 
 | 108 | +  content: HtmlString;  | 
 | 109 | +  summary: HtmlString;  | 
 | 110 | +};  | 
 | 111 | + | 
 | 112 | +async function collect_posts(): Promise<Post[]> {  | 
 | 113 | +  const post_walk = std.fs.walk("./src/posts", { includeDirs: false });  | 
 | 114 | +  const work = std.async.pooledMap(8, post_walk, async (entry) => {  | 
 | 115 | +    if (!entry.name.endsWith(".djot")) return undefined;  | 
 | 116 | +    const [, y, m, d, slug] = entry.name.match(  | 
 | 117 | +      /^(\d\d\d\d)-(\d\d)-(\d\d)-(.*)\.djot$/,  | 
 | 118 | +    )!;  | 
 | 119 | +    const [year, month, day] = [y, m, d].map((it) => parseInt(it, 10));  | 
 | 120 | +    const date = new Date(Date.UTC(year, month - 1, day));  | 
 | 121 | + | 
 | 122 | +    const text = await Deno.readTextFile(entry.path);  | 
 | 123 | +    const ast = await djot.parse(text);  | 
 | 124 | +    const ctx = { date };  | 
 | 125 | +    const html = djot.render(ast, ctx);  | 
 | 126 | + | 
 | 127 | +    const title = ast.child("heading")?.content ?? "untitled";  | 
 | 128 | +    return {  | 
 | 129 | +      year,  | 
 | 130 | +      month,  | 
 | 131 | +      day,  | 
 | 132 | +      slug,  | 
 | 133 | +      date,  | 
 | 134 | +      title,  | 
 | 135 | +      content: html,  | 
 | 136 | +      summary: ctx.summary,  | 
 | 137 | +      path: `/${y}/${m}/${d}/${slug}.html`,  | 
 | 138 | +      src: `/src/posts/${y}-${m}-${d}-${slug}.djot`,  | 
 | 139 | +    };  | 
 | 140 | +  });  | 
 | 141 | + | 
 | 142 | +  const posts = [];  | 
 | 143 | +  for await (const it of work) if (it) posts.push(it);  | 
 | 144 | +  posts.sort((l, r) => l.path < r.path ? 1 : -1);  | 
 | 145 | +  return posts;  | 
 | 146 | +}  | 
 | 147 | + | 
 | 148 | +export const commands: { [key: string]: () => Promise<void> } = {  | 
 | 149 | +  watch,  | 
 | 150 | +  build,  | 
 | 151 | +};  | 
0 commit comments