Improve markdown parser for editor

This commit is contained in:
Hakan Shehu
2026-01-08 12:41:08 +01:00
parent 3ccde245e9
commit 98fe12f90c
6 changed files with 131 additions and 0 deletions

30
package-lock.json generated
View File

@@ -13918,6 +13918,23 @@
"@tiptap/pm": "^3.15.3"
}
},
"node_modules/@tiptap/markdown": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/@tiptap/markdown/-/markdown-3.15.3.tgz",
"integrity": "sha512-JjjZ/X7H2+/Jeapk8GurbncJVyG9ai5YD/eJLBKDyWqsoQwsrHbDlYBx26q9J5VwWXzxe9G6fmHNlAfw0Pokow==",
"license": "MIT",
"dependencies": {
"marked": "^15.0.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.15.3",
"@tiptap/pm": "^3.15.3"
}
},
"node_modules/@tiptap/pm": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.15.3.tgz",
@@ -23969,6 +23986,18 @@
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/marked": {
"version": "15.0.12",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/marky": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
@@ -32679,6 +32708,7 @@
"@tiptap/extension-text": "^3.15.3",
"@tiptap/extension-underline": "^3.15.3",
"@tiptap/extensions": "^3.15.3",
"@tiptap/markdown": "^3.15.3",
"@tiptap/pm": "^3.15.3",
"@tiptap/react": "^3.15.3",
"@tiptap/suggestion": "^3.15.3",

View File

@@ -66,6 +66,7 @@
"@tiptap/extension-text": "^3.15.3",
"@tiptap/extension-underline": "^3.15.3",
"@tiptap/extensions": "^3.15.3",
"@tiptap/markdown": "^3.15.3",
"@tiptap/pm": "^3.15.3",
"@tiptap/react": "^3.15.3",
"@tiptap/suggestion": "^3.15.3",

View File

@@ -83,6 +83,8 @@ import {
DatabaseNode,
AutoJoiner,
HardBreakNode,
ParserExtension,
Markdown,
} from '@colanode/ui/editor/extensions';
import { ToolbarMenu, ActionMenu } from '@colanode/ui/editor/menus';
@@ -170,6 +172,8 @@ export const DocumentEditor = ({
{
extensions: [
IdExtension,
ParserExtension,
Markdown,
DocumentNode,
PageNode,
FolderNode,

View File

@@ -27,11 +27,13 @@ import { IdExtension } from '@colanode/ui/editor/extensions/id';
import { LinkMark } from '@colanode/ui/editor/extensions/link';
import { ListItemNode } from '@colanode/ui/editor/extensions/list-item';
import { ListKeymapExtension } from '@colanode/ui/editor/extensions/list-keymap';
import { Markdown } from '@colanode/ui/editor/extensions/markdown';
import { MentionExtension } from '@colanode/ui/editor/extensions/mention';
import { MessageNode } from '@colanode/ui/editor/extensions/message';
import { OrderedListNode } from '@colanode/ui/editor/extensions/ordered-list';
import { PageNode } from '@colanode/ui/editor/extensions/page';
import { ParagraphNode } from '@colanode/ui/editor/extensions/paragraph';
import { ParserExtension } from '@colanode/ui/editor/extensions/parser';
import { PlaceholderExtension } from '@colanode/ui/editor/extensions/placeholder';
import { TabKeymapExtension } from '@colanode/ui/editor/extensions/tab-keymap';
import { TableNode } from '@colanode/ui/editor/extensions/table';
@@ -87,4 +89,6 @@ export {
AutoJoiner,
MentionExtension,
HardBreakNode,
ParserExtension,
Markdown,
};

View File

@@ -0,0 +1,3 @@
import { Markdown } from '@tiptap/markdown';
export { Markdown };

View File

@@ -0,0 +1,89 @@
import { Plugin, PluginKey } from '@tiptap/pm/state';
import { Extension, JSONContent } from '@tiptap/react';
export const ParserExtension = Extension.create({
name: 'parser',
addOptions() {
return {
nodes: [],
};
},
addProseMirrorPlugins() {
const editor = this.editor;
const markdown = editor.markdown;
if (!markdown) {
console.error('Markdown extension not found');
return [];
}
return [
new Plugin({
key: new PluginKey('parser'),
props: {
handlePaste(view, event) {
const html = event.clipboardData?.getData('text/html');
if (html) {
return false;
}
const text = event.clipboardData?.getData('text/plain');
if (!text) {
return false;
}
const parsedContent = markdown.parse(text);
if (!parsedContent || !parsedContent.content) {
return false;
}
const blocks = parsedContent.content;
if (!blocks || blocks.length === 0) {
return false;
}
const normalizedBlocks = normalizeHeadings(blocks);
editor.commands.insertContent(normalizedBlocks);
return true;
},
},
}),
];
},
});
const normalizeHeadings = (content: JSONContent[]): JSONContent[] => {
return content.map(normalizeNode);
};
const normalizeNode = (node: JSONContent): JSONContent => {
if (node.type === 'heading' && node.attrs?.level) {
const level = node.attrs.level;
let newType: string;
if (level === 1) {
newType = 'heading1';
} else if (level === 2) {
newType = 'heading2';
} else {
newType = 'heading3';
}
const { level: _, ...restAttrs } = node.attrs;
return {
...node,
type: newType,
attrs: Object.keys(restAttrs).length > 0 ? restAttrs : undefined,
content: node.content ? node.content.map(normalizeNode) : undefined,
};
}
if (node.content) {
return {
...node,
content: node.content.map(normalizeNode),
};
}
return node;
};