Skip to content

Commit

Permalink
Infer slugs from tags and titles
Browse files Browse the repository at this point in the history
  • Loading branch information
zephraph authored and gitbutler-client committed Sep 12, 2024
1 parent 07cbabb commit f6e0c27
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 42 deletions.
69 changes: 48 additions & 21 deletions scripts/ob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,49 @@
*/
import { ulid } from "jsr:@std/ulid";
import { Command } from "jsr:@cliffy/[email protected]";
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("<path:string>")
.action(async (_, path) => {
// Ignore non-markdown files
if (!path.endsWith(".md")) return;
async function extractH1(path: string): Promise<string> {
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<string, any> = {};
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() : ""
Expand All @@ -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
Expand Down Expand Up @@ -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("<path:string>")
.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")
Expand Down
47 changes: 26 additions & 21 deletions src/pages/api/[...path].ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -21,27 +26,27 @@ const app = new Hono<{ Bindings: AstroContext }>()
.post("/notes/:id", async (c) => {
const id = c.req.param("id");
const props: Record<string, any> = 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") },
});
Expand Down

0 comments on commit f6e0c27

Please sign in to comment.