import { unified } from "https://esm.sh/unified@10.1.2"; import { render } from "gfm"; import "https://esm.sh/prismjs@1.29.0/components/prism-typescript?no-check"; import "https://esm.sh/prismjs@1.29.0/components/prism-bash?no-check"; import "https://esm.sh/prismjs@1.29.0/components/prism-rust?no-check"; import remarkParse from "https://esm.sh/remark-parse@10.0.2"; import remarkStringify from "https://esm.sh/remark-stringify@10.0.3"; import remarkFrontmatter, { Root, } from "https://esm.sh/remark-frontmatter@4.0.1"; import { SILVERBULLET_SERVER } from "@lib/env.ts"; import { fixRenderedMarkdown } from "@lib/helpers.ts"; import { createLogger } from "@lib/log.ts"; import { db } from "@lib/db/sqlite.ts"; import { documentTable } from "@lib/db/schema.ts"; import { eq } from "drizzle-orm/sql"; export type Document = { name: string; lastModified: number; contentType: string; content: string | null; size: number; perm: string; }; const log = createLogger("documents"); export async function getDocuments(): Promise { let documents = await db.select().from(documentTable).all(); if (documents.length) return documents; const headers = new Headers(); headers.append("Accept", "application/json"); headers.append("X-Sync-Mode", "true"); log.debug("fetching all documents"); const response = await fetch(`${SILVERBULLET_SERVER}/index.json`, { headers: headers, }); documents = await response.json(); await db.delete(documentTable); await db.insert(documentTable).values(documents); return documents; } export function createDocument( name: string, content: string | ArrayBuffer, mediaType?: string, ) { const headers = new Headers(); if (mediaType) { headers.append("Content-Type", mediaType); } log.info("creating document", { name }); return fetch(SILVERBULLET_SERVER + "/" + name, { body: content, method: "PUT", headers, }); } export async function getDocument(name: string): Promise { const documents = await db.select().from(documentTable).where( eq(documentTable.name, name), ).limit(1); if (documents[0]?.content) return documents[0].content; log.debug("fetching document", { name }); const headers = new Headers(); headers.append("X-Sync-Mode", "true"); const response = await fetch(SILVERBULLET_SERVER + "/" + name, { headers }); const text = await response.text(); await db.update(documentTable).set({ content: text, }).where(eq(documentTable.name, name)); return text; } export function transformDocument(input: string, cb: (r: Root) => Root) { const out = unified() .use(remarkParse) .use(remarkFrontmatter, ["yaml"]) .use(() => (tree) => { return cb(tree); }) .use(remarkStringify) .processSync(input); return fixRenderedMarkdown(String(out)); } export function parseDocument(doc: string) { return unified() .use(remarkParse) .use(remarkFrontmatter, ["yaml", "toml"]) .parse(doc); } export function renderMarkdown(doc: string) { return render(doc, { allowMath: true, }); } export type ParsedDocument = ReturnType; export type DocumentChild = ParsedDocument["children"][number]; export function findRangeOfChildren(children: DocumentChild[]) { const firstChild = children[0]; const lastChild = children.length > 1 ? children[children.length - 1] : firstChild; const start = firstChild.position?.start.offset; const end = lastChild.position?.end.offset; if (typeof start !== "number" || typeof end !== "number") return; return [start, end]; } export function getTextOfRange(children: DocumentChild[], text: string) { if (!children || children.length === 0) { return; } const range = findRangeOfChildren(children); if (!range) return; return text.substring(range[0], range[1]); } export function getTextOfChild(child: DocumentChild): string | undefined { if ("value" in child) return child.value; if ("children" in child) { return getTextOfChild(child.children[0]); } return; }