diff --git a/src/lib/diff.ts b/src/lib/diff.ts
new file mode 100644
index 0000000..abc9043
--- /dev/null
+++ b/src/lib/diff.ts
@@ -0,0 +1,18 @@
+import DiffMatchPatch from "diff-match-patch";
+
+const dmp = new DiffMatchPatch();
+
+export function createDiff(oldText: string, newText: string): string {
+ const patches = dmp.patch_make(oldText, newText);
+ return dmp.patch_toText(patches);
+}
+
+export function applyDiff(text: string, patchText: string): string {
+ const patches = dmp.patch_fromText(patchText);
+ const [result, applied] = dmp.patch_apply(patches, text);
+ const failed = applied.filter((ok) => !ok);
+ if (failed.length > 0) {
+ throw new Error(`Failed to apply ${failed.length} patch(es)`);
+ }
+ return result;
+}
diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts
new file mode 100644
index 0000000..9ce505f
--- /dev/null
+++ b/src/lib/markdown.ts
@@ -0,0 +1,89 @@
+import MarkdownIt from "markdown-it";
+import type StateInline from "markdown-it/lib/rules_inline/state_inline.mjs";
+import { type VizPluginEnv, vizPlugin } from "./viz/plugin.ts";
+
+export interface RenderResult {
+ html: string;
+ hasViz: boolean;
+}
+
+export interface MarkdownEnv extends VizPluginEnv {
+ wikiSlug?: string;
+}
+
+const md = new MarkdownIt({ html: false, linkify: true });
+
+// Wikilink plugin: transforms [[slug]] and [[slug|label]] into links
+function wikilinkPlugin(mdi: MarkdownIt): void {
+ mdi.inline.ruler.push("wikilink", (state: StateInline, silent: boolean) => {
+ const src = state.src;
+ const pos = state.pos;
+
+ if (src[pos] !== "[" || src[pos + 1] !== "[") return false;
+
+ const closeIdx = src.indexOf("]]", pos + 2);
+ if (closeIdx === -1) return false;
+
+ if (!silent) {
+ const content = src.slice(pos + 2, closeIdx);
+ let slug: string;
+ let label: string;
+ if (content.includes("|")) {
+ const parts = content.split("|");
+ slug = parts[0] ?? content;
+ label = parts.slice(1).join("|");
+ } else {
+ slug = content;
+ label = content;
+ }
+
+ const env = state.env as MarkdownEnv;
+ const wikiSlug = env.wikiSlug;
+ const href = wikiSlug ? `/wiki/${wikiSlug}/${slug.trim()}` : slug.trim();
+
+ const token = state.push("html_inline", "", 0);
+ token.content = `${label.trim()}`;
+ }
+
+ state.pos = closeIdx + 2;
+ return true;
+ });
+}
+
+md.use(wikilinkPlugin);
+md.use(vizPlugin);
+
+export function renderMarkdown(
+ source: string,
+ wikiSlug?: string,
+): RenderResult {
+ const env: MarkdownEnv = { wikiSlug };
+ const html = md.render(source, env);
+ return { html, hasViz: env.hasViz === true };
+}
+
+/**
+ * Extract unique wikilink target slugs from markdown content.
+ * Matches [[slug]] and [[slug|label]], returns deduplicated trimmed slugs.
+ */
+export function extractWikilinks(content: string): string[] {
+ const seen = new Set();
+ let pos = 0;
+ while (pos < content.length) {
+ if (content[pos] === "[" && content[pos + 1] === "[") {
+ const closeIdx = content.indexOf("]]", pos + 2);
+ if (closeIdx === -1) break;
+ const inner = content.slice(pos + 2, closeIdx);
+ const slug = (inner.includes("|") ? inner.split("|")[0] : inner)?.trim();
+ if (slug) {
+ seen.add(slug);
+ }
+ pos = closeIdx + 2;
+ } else {
+ pos++;
+ }
+ }
+ return [...seen];
+}
+
+export { md };
diff --git a/src/lib/slug.ts b/src/lib/slug.ts
new file mode 100644
index 0000000..220c83d
--- /dev/null
+++ b/src/lib/slug.ts
@@ -0,0 +1,7 @@
+export { default as slugify } from "@sindresorhus/slugify";
+
+const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
+
+export function isValidSlug(slug: string): boolean {
+ return SLUG_RE.test(slug) && slug.length >= 1 && slug.length <= 128;
+}
diff --git a/tests/lib/diff.test.ts b/tests/lib/diff.test.ts
new file mode 100644
index 0000000..b9e1a69
--- /dev/null
+++ b/tests/lib/diff.test.ts
@@ -0,0 +1,63 @@
+import { describe, expect, test } from "bun:test";
+import { applyDiff, createDiff } from "../../src/lib/diff.ts";
+
+describe("createDiff and applyDiff", () => {
+ test("roundtrips a simple edit", () => {
+ const oldText = "Hello world";
+ const newText = "Hello brave new world";
+ const diff = createDiff(oldText, newText);
+ expect(applyDiff(oldText, diff)).toBe(newText);
+ });
+
+ test("handles first revision (diff from empty)", () => {
+ const content = "# My First Note\n\nSome content here.";
+ const diff = createDiff("", content);
+ expect(applyDiff("", diff)).toBe(content);
+ });
+
+ test("handles deletion", () => {
+ const oldText = "line one\nline two\nline three";
+ const newText = "line one\nline three";
+ const diff = createDiff(oldText, newText);
+ expect(applyDiff(oldText, diff)).toBe(newText);
+ });
+
+ test("handles no change", () => {
+ const text = "unchanged content";
+ const diff = createDiff(text, text);
+ expect(applyDiff(text, diff)).toBe(text);
+ });
+
+ test("handles multiline markdown", () => {
+ const oldText = "# Title\n\nParagraph one.\n\n## Section\n\nParagraph two.";
+ const newText =
+ "# Title\n\nParagraph one, edited.\n\n## Section\n\nParagraph two.\n\n## New Section\n\nNew content.";
+ const diff = createDiff(oldText, newText);
+ expect(applyDiff(oldText, diff)).toBe(newText);
+ });
+
+ test("chains multiple diffs", () => {
+ const v1 = "";
+ const v2 = "Hello";
+ const v3 = "Hello world";
+ const v4 = "Hello brave world";
+
+ const diff1 = createDiff(v1, v2);
+ const diff2 = createDiff(v2, v3);
+ const diff3 = createDiff(v3, v4);
+
+ let current = v1;
+ current = applyDiff(current, diff1);
+ expect(current).toBe(v2);
+ current = applyDiff(current, diff2);
+ expect(current).toBe(v3);
+ current = applyDiff(current, diff3);
+ expect(current).toBe(v4);
+ });
+
+ test("diff output is a string", () => {
+ const diff = createDiff("a", "b");
+ expect(typeof diff).toBe("string");
+ expect(diff.length).toBeGreaterThan(0);
+ });
+});
diff --git a/tests/lib/markdown.test.ts b/tests/lib/markdown.test.ts
new file mode 100644
index 0000000..07802b9
--- /dev/null
+++ b/tests/lib/markdown.test.ts
@@ -0,0 +1,156 @@
+import { describe, expect, test } from "bun:test";
+import { extractWikilinks, renderMarkdown } from "../../src/lib/markdown.ts";
+
+describe("renderMarkdown", () => {
+ test("renders basic markdown", () => {
+ const { html } = renderMarkdown("# Hello\n\nWorld");
+ expect(html).toContain("Hello
");
+ expect(html).toContain("World
");
+ });
+
+ test("renders bold and italic", () => {
+ const { html } = renderMarkdown("**bold** and *italic*");
+ expect(html).toContain("bold");
+ expect(html).toContain("italic");
+ });
+
+ test("renders code blocks", () => {
+ const { html } = renderMarkdown("```js\nconsole.log('hi');\n```");
+ expect(html).toContain(" {
+ const { html } = renderMarkdown("use `const` here");
+ expect(html).toContain("const");
+ });
+
+ test("renders links", () => {
+ const { html } = renderMarkdown("[click](https://example.com)");
+ expect(html).toContain('href="https://example.com"');
+ expect(html).toContain("click");
+ });
+
+ test("does not render raw HTML", () => {
+ const { html } = renderMarkdown('');
+ expect(html).not.toContain("