fix/markdown-task-list-rendering: Render markdown checklists as task lists on update_page #12
@@ -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("<!DOCTYPE html><html><body></body></html>");
|
||||
@@ -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();
|
||||
|
||||
@@ -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 `<ul><li><input disabled type="checkbox"> foo</li></ul>`,
|
||||
* 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 `<input>` is dropped and the list degrades to a
|
||||
* plain bulletList. We walk each checkbox, tag its ancestor `<li>` and list, and
|
||||
* remove the now-redundant input.
|
||||
*/
|
||||
function normalizeTaskLists(html: string): string {
|
||||
const dom = new JSDOM(`<!DOCTYPE html><html><body>${html}</body></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<any> {
|
||||
const html = await marked.parse(markdown);
|
||||
return generateJSON(html, tiptapExtensions);
|
||||
const normalized = normalizeTaskLists(html);
|
||||
return generateJSON(normalized, tiptapExtensions);
|
||||
}
|
||||
|
||||
@@ -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 `<ul><li><input type="checkbox">`, 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 }),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user