kaneo (minimalist kanban) fork to experiment adding a tangled integration
github.com/usekaneo/kaneo
1type ProseMirrorNodeJSON = {
2 type: string;
3 attrs?: Record<string, unknown>;
4 content?: ProseMirrorNodeJSON[];
5 text?: string;
6};
7
8const TASK_LINE_REGEX = /^\s*[-*]\s+\[( |x|X)\]\s*(.*)$/;
9const HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
10
11function createParagraphNode(text: string): ProseMirrorNodeJSON {
12 const normalizedText = text.trim();
13 if (!normalizedText) {
14 return { type: "paragraph" };
15 }
16
17 return {
18 type: "paragraph",
19 content: [{ type: "text", text: normalizedText }],
20 };
21}
22
23export function parseTaskListMarkdownToNodes(
24 rawText: string,
25): ProseMirrorNodeJSON[] | null {
26 if (!/(?:^|\n)\s*[-*]\s+\[(?: |x|X)\]\s+/.test(rawText)) {
27 return null;
28 }
29
30 const lines = rawText.replace(/\r\n/g, "\n").split("\n");
31 const nodes: ProseMirrorNodeJSON[] = [];
32 let taskItems: ProseMirrorNodeJSON[] = [];
33 let hasTaskItems = false;
34
35 const flushTaskItems = () => {
36 if (!taskItems.length) return;
37 nodes.push({
38 type: "taskList",
39 content: taskItems,
40 });
41 taskItems = [];
42 };
43
44 for (const line of lines) {
45 const trimmedLine = line.trim();
46 if (!trimmedLine) {
47 flushTaskItems();
48 continue;
49 }
50
51 const taskMatch = TASK_LINE_REGEX.exec(line);
52 if (taskMatch) {
53 hasTaskItems = true;
54 taskItems.push({
55 type: "taskItem",
56 attrs: { checked: taskMatch[1].toLowerCase() === "x" },
57 content: [createParagraphNode(taskMatch[2] || "")],
58 });
59 continue;
60 }
61
62 flushTaskItems();
63
64 const headingMatch = HEADING_REGEX.exec(trimmedLine);
65 if (headingMatch) {
66 nodes.push({
67 type: "heading",
68 attrs: { level: headingMatch[1].length },
69 content: [{ type: "text", text: headingMatch[2].trim() }],
70 });
71 continue;
72 }
73
74 nodes.push(createParagraphNode(trimmedLine));
75 }
76
77 flushTaskItems();
78
79 if (!hasTaskItems || !nodes.length) {
80 return null;
81 }
82
83 return nodes;
84}