Merge pull request 'fix/markdown-task-list-rendering: Render markdown checklists as task lists on update_page' (#12) from fix/markdown-task-list-rendering into main
Some checks are pending
Docker Build & Push / build-and-push (push) Has started running
Some checks are pending
Docker Build & Push / build-and-push (push) Has started running
This commit was merged in pull request #12.
This commit is contained in:
@@ -2,10 +2,9 @@ import { HocuspocusProvider } from "@hocuspocus/provider";
|
|||||||
import { TiptapTransformer } from "@hocuspocus/transformer";
|
import { TiptapTransformer } from "@hocuspocus/transformer";
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import WebSocket from "ws";
|
import WebSocket from "ws";
|
||||||
import { marked } from "marked";
|
|
||||||
import { generateJSON } from "@tiptap/html";
|
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import { tiptapExtensions } from "./tiptap-extensions.js";
|
import { tiptapExtensions } from "./tiptap-extensions.js";
|
||||||
|
import { markdownToTiptapJson } from "./markdown-to-json.js";
|
||||||
|
|
||||||
// Setup DOM environment for Tiptap HTML parsing in Node.js
|
// Setup DOM environment for Tiptap HTML parsing in Node.js
|
||||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
|
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"}...`,
|
`Token prefix: ${collabToken ? collabToken.substring(0, 5) : "NONE"}...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1. Convert Markdown to HTML
|
// 1. Convert Markdown to ProseMirror JSON
|
||||||
const html = await marked.parse(markdownContent);
|
// (handles GFM task lists; see markdown-to-json.ts)
|
||||||
|
const tiptapJson = await markdownToTiptapJson(markdownContent);
|
||||||
// 2. Convert HTML to ProseMirror JSON
|
|
||||||
const tiptapJson = generateJSON(html, tiptapExtensions);
|
|
||||||
|
|
||||||
// 3. Setup Hocuspocus Provider
|
// 3. Setup Hocuspocus Provider
|
||||||
const ydoc = new Y.Doc();
|
const ydoc = new Y.Doc();
|
||||||
|
|||||||
@@ -16,10 +16,47 @@ if (typeof global.document === "undefined") {
|
|||||||
global.Element = dom.window.Element;
|
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
|
* Convert markdown string to TipTap-compatible ProseMirror JSON
|
||||||
*/
|
*/
|
||||||
export async function markdownToTiptapJson(markdown: string): Promise<any> {
|
export async function markdownToTiptapJson(markdown: string): Promise<any> {
|
||||||
const html = await marked.parse(markdown);
|
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 Image from "@tiptap/extension-image";
|
||||||
import Link from "@tiptap/extension-link";
|
import Link from "@tiptap/extension-link";
|
||||||
import { TableKit } from "@tiptap/extension-table";
|
import { TableKit } from "@tiptap/extension-table";
|
||||||
|
import { TaskList, TaskItem } from "@tiptap/extension-list";
|
||||||
|
|
||||||
// Define extensions compatible with standard Markdown features
|
// Define extensions compatible with standard Markdown features
|
||||||
// We use the default Tiptap extensions to handle basic content
|
// We use the default Tiptap extensions to handle basic content
|
||||||
@@ -19,4 +20,10 @@ export const tiptapExtensions = [
|
|||||||
openOnClick: false,
|
openOnClick: false,
|
||||||
}),
|
}),
|
||||||
TableKit,
|
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