From f6e0c276464a2f87c73ed9bd36b68ae935a06035 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Thu, 12 Sep 2024 16:00:02 -0400 Subject: [PATCH] Infer slugs from tags and titles --- scripts/ob.ts | 69 ++++++++++++++++++++++++++------------ src/pages/api/[...path].ts | 47 ++++++++++++++------------ 2 files changed, 74 insertions(+), 42 deletions(-) diff --git a/scripts/ob.ts b/scripts/ob.ts index b9ef1d8..3d25a21 100644 --- a/scripts/ob.ts +++ b/scripts/ob.ts @@ -3,44 +3,49 @@ */ import { ulid } from "jsr:@std/ulid"; import { Command } from "jsr:@cliffy/command@1.0.0-rc.5"; -import { basename, dirname, join, extname } from "jsr:@std/path"; +import { basename, dirname, extname, join } from "jsr:@std/path"; import { move } from "jsr:@std/fs"; import { extractYaml } from "jsr:@std/front-matter"; import { contentType } from "jsr:@std/media-types"; import { isULID, slugify } from "../src/utils.ts"; +import { crypto } from "jsr:@std/crypto"; -const rename = new Command() - .description("Renames a note with a ULID") - .arguments("") - .action(async (_, path) => { - // Ignore non-markdown files - if (!path.endsWith(".md")) return; +async function extractH1(path: string): Promise { + using file = await Deno.open(path, { read: true }); - const file = basename(path, ".md"); - const dir = dirname(path); - const targetDir = "logs"; - if (/\d{4}/.test(file)) { - if (dir.endsWith(targetDir)) return; - const targetFile = join(dir, targetDir, `${file}.md`); - await move(path, targetFile, { - overwrite: false, - }); - } - if (!isULID(file)) { - return Deno.rename(path, join(dir, `${ulid()}.md`)); + let content = ""; + + const buf = new Uint8Array(100); + while (true) { + const bytesRead = await file.read(buf); + if (bytesRead === null) break; + + content += new TextDecoder().decode(buf); + + const h1Match = content.match(/^# (.+)$/m); + if (h1Match) { + return h1Match[1].trim(); } - }); + + if (content.length > 1000) break; + } + + return ""; +} async function publishNote(path: string) { const file = basename(path, ".md"); if (!isULID(file)) { throw new Error("File must have a ULID name to be published"); } + const content = await Deno.readTextFile(path); - let fm = {}; + let fm: Record = {}; if (content.startsWith("---")) { fm = extractYaml(content).attrs; } + fm.title = await extractH1(path); + return fetch( `${Deno.env.get("SITE")}/api/notes/${file}${ fm ? "?" + new URLSearchParams(fm).toString() : "" @@ -59,6 +64,7 @@ async function publishNote(path: string) { : console.error("Error publishing", res.status + " " + (await res.text())) ); } + async function publishAsset(path: string) { // Calculate the SHA256 hash of the file const hash = await crypto.subtle @@ -96,6 +102,27 @@ const publish = new Command() return path.endsWith(".md") ? publishNote(path) : publishAsset(path); }); +const rename = new Command() + .description("Renames a note with a ULID") + .arguments("") + .action(async (_, path) => { + // Ignore non-markdown files + if (!path.endsWith(".md")) return; + const file = basename(path, ".md"); + const dir = dirname(path); + const targetDir = "logs"; + if (/\d{4}/.test(file)) { + if (dir.endsWith(targetDir)) return; + const targetFile = join(dir, targetDir, `${file}.md`); + await move(path, targetFile, { + overwrite: false, + }); + } + if (!isULID(file)) { + return Deno.rename(path, join(dir, `${ulid()}.md`)); + } + }); + await new Command() .name("ob") .version("0.0.1") diff --git a/src/pages/api/[...path].ts b/src/pages/api/[...path].ts index 6bb9595..a90b728 100644 --- a/src/pages/api/[...path].ts +++ b/src/pages/api/[...path].ts @@ -5,7 +5,12 @@ import { Hono } from "hono"; import type { APIContext, APIRoute } from "astro"; import { bearerAuth } from "hono/bearer-auth"; -import { isValidSha256, slugify } from "~/utils"; +import { isULID, isValidSha256, slugify } from "~/utils"; + +// A way to alias tags to different slugs +const tagMap = { + recurse: "rc", +}; // Dump cf env in top level of context type AstroContext = APIContext & APIContext["locals"]["runtime"]["env"]; @@ -21,27 +26,27 @@ const app = new Hono<{ Bindings: AstroContext }>() .post("/notes/:id", async (c) => { const id = c.req.param("id"); const props: Record = c.req.query(); - // Alises (as stored in obsidian) are treated as slugs for navigation - if (props.aliases) { - const slugs: string[] = props.aliases - .split(",") - .filter(Boolean) - .map(slugify); - await Promise.all( - slugs.map((slug) => - c.env.KV_MAPPINGS.get(slug).then((currentId) => { - if (currentId) { - // Mappings are immutable. If a slug is already mapped to a different id, throw an error. - if (currentId !== id) { - throw new Error(`Slug ${slug} is mapped to ${currentId}`); - } - return; - } - return c.env.KV_MAPPINGS.put(slug, id); - }) - ) - ); + let slug = props.slug || slugify(props.title); + if (isULID(slug)) { + throw new Error("Title or H1s should not be a ULID"); } + + const primaryTag = props.tags.split(",")[0]; + // Set the prefix URL to the first tag (or it's mapped value) if it exists + if (primaryTag && primaryTag !== slug) { + slug = `${ + tagMap[primaryTag as keyof typeof tagMap] ?? props.tags[0] + }/${slug}`; + } + await c.env.KV_MAPPINGS.get(slug).then((mappedID) => { + if (mappedID && mappedID !== id) { + throw new Error(`Slug ${slug} is already mapped to ${mappedID}`); + } + return Promise.all([ + c.env.KV_MAPPINGS.put(slug, id), + c.env.KV_MAPPINGS.put(id, slug), + ]); + }); const result = await c.env.R2_BUCKET.put(id, await c.req.blob(), { onlyIf: { etagDoesNotMatch: c.req.header("If-None-Match") }, });