diff --git a/src/lib/collaboration.ts b/src/lib/collaboration.ts
index 452e340..75a592b 100644
--- a/src/lib/collaboration.ts
+++ b/src/lib/collaboration.ts
@@ -2,10 +2,9 @@ import { HocuspocusProvider } from "@hocuspocus/provider";
import { TiptapTransformer } from "@hocuspocus/transformer";
import * as Y from "yjs";
import WebSocket from "ws";
-import { marked } from "marked";
-import { generateJSON } from "@tiptap/html";
import { JSDOM } from "jsdom";
import { tiptapExtensions } from "./tiptap-extensions.js";
+import { markdownToTiptapJson } from "./markdown-to-json.js";
// Setup DOM environment for Tiptap HTML parsing in Node.js
const dom = new JSDOM("
");
@@ -29,11 +28,9 @@ export async function updatePageContentRealtime(
`Token prefix: ${collabToken ? collabToken.substring(0, 5) : "NONE"}...`,
);
- // 1. Convert Markdown to HTML
- const html = await marked.parse(markdownContent);
-
- // 2. Convert HTML to ProseMirror JSON
- const tiptapJson = generateJSON(html, tiptapExtensions);
+ // 1. Convert Markdown to ProseMirror JSON
+ // (handles GFM task lists; see markdown-to-json.ts)
+ const tiptapJson = await markdownToTiptapJson(markdownContent);
// 3. Setup Hocuspocus Provider
const ydoc = new Y.Doc();
diff --git a/src/lib/markdown-to-json.ts b/src/lib/markdown-to-json.ts
index d1cfda9..8cf74f9 100644
--- a/src/lib/markdown-to-json.ts
+++ b/src/lib/markdown-to-json.ts
@@ -16,10 +16,47 @@ if (typeof global.document === "undefined") {
global.Element = dom.window.Element;
}
+/**
+ * Convert marked's GFM task-list HTML into the markup TipTap's TaskList/TaskItem
+ * extensions expect.
+ *
+ * marked renders `- [ ] foo` as `
foo
`,
+ * but TipTap's parseHTML rules only match `ul[data-type="taskList"]` and
+ * `li[data-type="taskItem"]` (with the checked state held in `data-checked`).
+ * Without this normalization the `` is dropped and the list degrades to a
+ * plain bulletList. We walk each checkbox, tag its ancestor `
` and list, and
+ * remove the now-redundant input.
+ */
+function normalizeTaskLists(html: string): string {
+ const dom = new JSDOM(`${html}`);
+ const doc = dom.window.document;
+
+ const checkboxes = doc.querySelectorAll('input[type="checkbox"]');
+ checkboxes.forEach((input) => {
+ const li = input.closest("li");
+ if (!li) return;
+
+ const checked =
+ (input as HTMLInputElement).checked || input.hasAttribute("checked");
+ li.setAttribute("data-type", "taskItem");
+ li.setAttribute("data-checked", checked ? "true" : "false");
+
+ const list = li.closest("ul, ol");
+ if (list) {
+ list.setAttribute("data-type", "taskList");
+ }
+
+ input.remove();
+ });
+
+ return doc.body.innerHTML;
+}
+
/**
* Convert markdown string to TipTap-compatible ProseMirror JSON
*/
export async function markdownToTiptapJson(markdown: string): Promise {
const html = await marked.parse(markdown);
- return generateJSON(html, tiptapExtensions);
+ const normalized = normalizeTaskLists(html);
+ return generateJSON(normalized, tiptapExtensions);
}
diff --git a/src/lib/tiptap-extensions.ts b/src/lib/tiptap-extensions.ts
index 380818c..ca15212 100644
--- a/src/lib/tiptap-extensions.ts
+++ b/src/lib/tiptap-extensions.ts
@@ -2,6 +2,7 @@ import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import Link from "@tiptap/extension-link";
import { TableKit } from "@tiptap/extension-table";
+import { TaskList, TaskItem } from "@tiptap/extension-list";
// Define extensions compatible with standard Markdown features
// We use the default Tiptap extensions to handle basic content
@@ -19,4 +20,10 @@ export const tiptapExtensions = [
openOnClick: false,
}),
TableKit,
+ // Task lists (GFM checkboxes: `- [ ]` / `- [x]`).
+ // marked emits `
`, which is normalized to the
+ // `data-type="taskList"`/`data-type="taskItem"` markup these expect by
+ // normalizeTaskLists() in markdown-to-json.ts before generateJSON runs.
+ TaskList,
+ TaskItem.configure({ nested: true }),
];