memorium/lib/documents.ts

202 lines
5.4 KiB
TypeScript
Raw Normal View History

2023-08-01 18:35:35 +02:00
import { unified } from "https://esm.sh/unified@10.1.2";
2025-01-05 14:59:05 +01:00
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";
2023-08-01 18:35:35 +02:00
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";
2023-08-01 17:50:00 +02:00
import { SILVERBULLET_SERVER } from "@lib/env.ts";
2023-08-01 21:35:21 +02:00
import { fixRenderedMarkdown } from "@lib/helpers.ts";
import { createLogger } from "@lib/log/index.ts";
2025-01-06 16:14:29 +01:00
import { db } from "@lib/db/sqlite.ts";
import { documentTable } from "@lib/db/schema.ts";
2025-01-05 21:58:07 +01:00
import { eq } from "drizzle-orm/sql";
2023-07-26 13:47:01 +02:00
export type Document = {
name: string;
lastModified: number;
contentType: string;
2025-01-05 21:58:07 +01:00
content: string | null;
2023-07-26 13:47:01 +02:00
size: number;
perm: string;
};
2023-08-05 22:16:14 +02:00
const log = createLogger("documents");
2023-07-26 13:47:01 +02:00
export async function getDocuments(): Promise<Document[]> {
2025-01-05 21:58:07 +01:00
let documents = await db.select().from(documentTable).all();
if (documents.length) return documents;
2023-07-26 13:47:01 +02:00
const headers = new Headers();
headers.append("Accept", "application/json");
headers.append("X-Sync-Mode", "true");
2023-08-05 22:16:14 +02:00
log.debug("fetching all documents");
2023-08-01 17:50:00 +02:00
const response = await fetch(`${SILVERBULLET_SERVER}/index.json`, {
2023-07-26 13:47:01 +02:00
headers: headers,
});
2025-01-05 21:58:07 +01:00
documents = await response.json();
await db.delete(documentTable);
await db.insert(documentTable).values(documents);
2023-08-07 13:42:00 +02:00
return documents;
2023-07-26 13:47:01 +02:00
}
export function createDocument(
2023-07-31 17:21:17 +02:00
name: string,
content: string | ArrayBuffer,
mediaType?: string,
) {
const headers = new Headers();
if (mediaType) {
headers.append("Content-Type", mediaType);
}
2023-08-05 22:16:14 +02:00
log.info("creating document", { name });
2023-08-02 01:58:03 +02:00
2025-01-18 00:46:05 +01:00
if (typeof content === "string") {
updateDocument(name, content).catch(log.error);
}
return fetch(SILVERBULLET_SERVER + "/" + name, {
2023-07-31 17:21:17 +02:00
body: content,
method: "PUT",
headers,
});
}
2025-01-18 00:46:05 +01:00
async function fetchDocument(name: string) {
2023-08-05 22:16:14 +02:00
log.debug("fetching document", { name });
2023-08-19 23:29:39 +02:00
const headers = new Headers();
headers.append("X-Sync-Mode", "true");
const response = await fetch(SILVERBULLET_SERVER + "/" + name, { headers });
2025-01-18 00:46:05 +01:00
if (response.status === 404) {
return;
}
return response.text();
}
2025-01-18 00:46:05 +01:00
export async function getDocument(name: string): Promise<string | undefined> {
const documents = await db.select().from(documentTable).where(
eq(documentTable.name, name),
).limit(1);
// This updates the document in the background
fetchDocument(name).then((content) => {
if (content) {
updateDocument(name, content);
} else {
db.delete(documentTable).where(eq(documentTable.name, name));
}
}).catch(
log.error,
);
if (documents[0]?.content) return documents[0].content;
const text = await fetchDocument(name);
if (!text) {
db.delete(documentTable).where(eq(documentTable.name, name));
return;
}
await updateDocument(name, text);
2023-08-10 19:16:03 +02:00
return text;
2023-07-26 13:47:01 +02:00
}
2025-01-18 00:46:05 +01:00
export function updateDocument(name: string, content: string) {
return db.update(documentTable).set({
content,
}).where(eq(documentTable.name, name)).run();
2025-01-18 00:46:05 +01:00
}
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);
2023-08-01 21:35:21 +02:00
return fixRenderedMarkdown(String(out));
}
2023-07-26 13:47:01 +02:00
export function parseDocument(doc: string) {
return unified()
2023-08-01 21:35:21 +02:00
.use(remarkParse)
.use(remarkFrontmatter, ["yaml", "toml"])
2023-07-26 13:47:01 +02:00
.parse(doc);
}
function removeFrontmatter(doc: string) {
if (doc.trim().startsWith("---")) {
return doc.trim().split("---").filter((s) => s.length).slice(1).join("---");
}
return doc;
}
export function removeImage(doc: string, imageUrl?: string) {
if (!imageUrl) {
return doc;
}
// Remove image from content
const first = doc.slice(0, 500);
const second = doc.slice(500);
// Regex pattern to match the image Markdown syntax with the specific URL
const pattern = new RegExp(
`!\\[.*?\\]\\(${imageUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\)`,
"g",
);
// Remove the matched image
const updatedMarkdown = first.replace(pattern, "");
return updatedMarkdown + second;
}
2023-07-30 19:40:39 +02:00
export function renderMarkdown(doc: string) {
return render(removeFrontmatter(doc), {
baseUrl: SILVERBULLET_SERVER,
allowMath: true,
});
2023-07-30 19:40:39 +02:00
}
2023-07-26 13:47:01 +02:00
export type ParsedDocument = ReturnType<typeof parseDocument>;
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;
}