import { unified } from "npm:unified"; import remarkParse from "npm:remark-parse"; import remarkStringify from "https://esm.sh/remark-stringify@10.0.3"; import remarkFrontmatter, { Root, } from "https://esm.sh/remark-frontmatter@4.0.1"; import remarkRehype from "https://esm.sh/remark-rehype@10.1.0"; import rehypeSanitize from "https://esm.sh/rehype-sanitize@5.0.1"; import rehypeStringify from "https://esm.sh/rehype-stringify@9.0.3"; import { parse } from "https://deno.land/std@0.194.0/yaml/mod.ts"; import * as cache from "@lib/cache/documents.ts"; const SILVERBULLET_SERVER = Deno.env.get("SILVERBULLET_SERVER"); export type Document = { name: string; lastModified: number; contentType: string; size: number; perm: string; }; export function parseFrontmatter(yaml: string) { return parse(yaml); } export async function getDocuments(): Promise { const cachedDocuments = await cache.getDocuments(); if (cachedDocuments) return cachedDocuments; const headers = new Headers(); headers.append("Accept", "application/json"); const response = await fetch(SILVERBULLET_SERVER + "/index.json", { headers: headers, }); const documents = await response.json(); cache.setDocuments(documents); return documents; } export function createDocument( name: string, content: string | ArrayBuffer, mediaType?: string, ) { const headers = new Headers(); if (mediaType) { headers.append("Content-Type", mediaType); } return fetch(SILVERBULLET_SERVER + "/" + name, { body: content, method: "PUT", headers, }); } export async function getDocument(name: string): Promise { const cachedDocument = await cache.getDocument(name); if (cachedDocument) return cachedDocument; const response = await fetch(SILVERBULLET_SERVER + "/" + name); const text = await response.text(); cache.setDocument(name, text); 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 String(out) .replace("***\n", "---") .replace("----------------", "---") .replace("\n---", "---") .replace(/^(date:[^'\n]*)'|'/gm, (match, p1, p2) => { if (p1) { // This is a line starting with date: followed by single quotes return p1.replace(/'/gm, ""); } else if (p2) { return ""; } else { // This is a line with single quotes, but not starting with date: return match; } }); } export function parseDocument(doc: string) { return unified() .use(remarkParse).use(remarkFrontmatter, ["yaml", "toml"]) .parse(doc); } export function renderMarkdown(doc: string) { const out = unified() .use(remarkParse) .use(remarkRehype) .use(rehypeSanitize) .use(rehypeStringify) .processSync(doc); return String(out); } 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; }