Move markdown to prosemirror parser in packages
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -3262,6 +3262,10 @@
|
|||||||
"resolved": "packages/n8n-node",
|
"resolved": "packages/n8n-node",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@probo/prosemirror": {
|
||||||
|
"resolved": "packages/prosemirror",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@probo/react-lazy": {
|
"node_modules/@probo/react-lazy": {
|
||||||
"resolved": "packages/react-lazy",
|
"resolved": "packages/react-lazy",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -18215,7 +18219,6 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
}
|
}
|
||||||
@@ -19701,6 +19704,17 @@
|
|||||||
"n8n-workflow": "*"
|
"n8n-workflow": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/prosemirror": {
|
||||||
|
"name": "@probo/prosemirror",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/pm": "^3.20.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "~5.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/react-lazy": {
|
"packages/react-lazy": {
|
||||||
"name": "@probo/react-lazy",
|
"name": "@probo/react-lazy",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -19755,6 +19769,7 @@
|
|||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@probo/helpers": "1.0.0",
|
"@probo/helpers": "1.0.0",
|
||||||
"@probo/i18n": "1.0.0",
|
"@probo/i18n": "1.0.0",
|
||||||
|
"@probo/prosemirror": "1.0.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
|||||||
18
packages/prosemirror/package.json
Normal file
18
packages/prosemirror/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@probo/prosemirror",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"sideEffects": false,
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"license": "ISC",
|
||||||
|
"scripts": {
|
||||||
|
"check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/pm": "^3.20.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "~5.8.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/prosemirror/src/index.ts
Normal file
5
packages/prosemirror/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
// Use of this source code is governed by the ISC license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
export { hasMarkdown, parseInlineContent, parseMarkdown } from "./markdown";
|
||||||
288
packages/prosemirror/src/markdown.ts
Normal file
288
packages/prosemirror/src/markdown.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
// Use of this source code is governed by the ISC license
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import type { Node as ProseMirrorNode, Schema } from "@tiptap/pm/model";
|
||||||
|
|
||||||
|
const codeBlockOpenPattern = /^```(\w*)$/;
|
||||||
|
const tableSeparatorPattern
|
||||||
|
= /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/m;
|
||||||
|
const markdownLinePattern
|
||||||
|
= /(?:^```\w*$|^#{1,6}\s|^>\s|^[-+*]\s|^\d+\.\s|^(?:---|___|\*\*\*)\s*$)/m;
|
||||||
|
|
||||||
|
function parseCells(line: string): string[] {
|
||||||
|
return line
|
||||||
|
.replace(/^\|/, "")
|
||||||
|
.replace(/\|\s*$/, "")
|
||||||
|
.split("|")
|
||||||
|
.map(cell => cell.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMarks(
|
||||||
|
schema: Schema,
|
||||||
|
markNames: string[],
|
||||||
|
inner: string,
|
||||||
|
): ProseMirrorNode[] {
|
||||||
|
const marks = markNames.map(name => schema.marks[name].create());
|
||||||
|
return parseInlineContent(schema, inner).map((node) => {
|
||||||
|
let combined = node.marks;
|
||||||
|
for (const mark of marks) {
|
||||||
|
combined = mark.addToSet(combined);
|
||||||
|
}
|
||||||
|
return node.mark(combined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseInlineContent(
|
||||||
|
schema: Schema,
|
||||||
|
text: string,
|
||||||
|
): ProseMirrorNode[] {
|
||||||
|
const nodes: ProseMirrorNode[] = [];
|
||||||
|
const pattern
|
||||||
|
= /`([^`]+)`|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|~~(.+?)~~|<u>(.+?)<\/u>|\[([^\]]+)\]\(([^)]+)\)|\*(.+?)\*/g;
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = pattern.exec(text)) !== null) {
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
nodes.push(schema.text(text.slice(lastIndex, match.index)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match[1] !== undefined) {
|
||||||
|
nodes.push(schema.text(match[1], [schema.marks.code.create()]));
|
||||||
|
} else if (match[2] !== undefined) {
|
||||||
|
nodes.push(...applyMarks(schema, ["bold", "italic"], match[2]));
|
||||||
|
} else if (match[3] !== undefined) {
|
||||||
|
nodes.push(...applyMarks(schema, ["bold"], match[3]));
|
||||||
|
} else if (match[4] !== undefined) {
|
||||||
|
nodes.push(...applyMarks(schema, ["strike"], match[4]));
|
||||||
|
} else if (match[5] !== undefined) {
|
||||||
|
nodes.push(...applyMarks(schema, ["underline"], match[5]));
|
||||||
|
} else if (match[6] !== undefined) {
|
||||||
|
const linkMark = schema.marks.link.create({ href: match[7] });
|
||||||
|
for (const node of parseInlineContent(schema, match[6])) {
|
||||||
|
nodes.push(node.mark(linkMark.addToSet(node.marks)));
|
||||||
|
}
|
||||||
|
} else if (match[8] !== undefined) {
|
||||||
|
nodes.push(...applyMarks(schema, ["italic"], match[8]));
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIndex = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < text.length) {
|
||||||
|
nodes.push(schema.text(text.slice(lastIndex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTable(
|
||||||
|
schema: Schema,
|
||||||
|
headerLine: string,
|
||||||
|
dataLines: string[],
|
||||||
|
): ProseMirrorNode {
|
||||||
|
const headers = parseCells(headerLine);
|
||||||
|
const columnCount = headers.length;
|
||||||
|
|
||||||
|
const headerCells = headers.map((h) => {
|
||||||
|
const content = h ? parseInlineContent(schema, h) : [];
|
||||||
|
return schema.nodes.tableHeader.create(
|
||||||
|
null,
|
||||||
|
schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableRows: ProseMirrorNode[] = [
|
||||||
|
schema.nodes.tableRow.create(null, headerCells),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const line of dataLines) {
|
||||||
|
const cells = parseCells(line);
|
||||||
|
const rowCells: ProseMirrorNode[] = [];
|
||||||
|
for (let c = 0; c < columnCount; c++) {
|
||||||
|
const value = cells[c]?.trim() ?? "";
|
||||||
|
const content = value ? parseInlineContent(schema, value) : [];
|
||||||
|
rowCells.push(
|
||||||
|
schema.nodes.tableCell.create(
|
||||||
|
null,
|
||||||
|
schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tableRows.push(schema.nodes.tableRow.create(null, rowCells));
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.nodes.table.create(null, tableRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasMarkdown(text: string): boolean {
|
||||||
|
return markdownLinePattern.test(text) || tableSeparatorPattern.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMarkdown(
|
||||||
|
text: string,
|
||||||
|
schema: Schema,
|
||||||
|
): ProseMirrorNode[] {
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const nodes: ProseMirrorNode[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i];
|
||||||
|
|
||||||
|
const codeMatch = line.match(codeBlockOpenPattern);
|
||||||
|
if (codeMatch) {
|
||||||
|
const language = codeMatch[1] || null;
|
||||||
|
const contentLines: string[] = [];
|
||||||
|
i++;
|
||||||
|
while (i < lines.length && !lines[i].startsWith("```")) {
|
||||||
|
contentLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
|
||||||
|
const codeContent = contentLines.join("\n").replace(/\n$/, "");
|
||||||
|
nodes.push(
|
||||||
|
schema.nodes.codeBlock.create(
|
||||||
|
{ language },
|
||||||
|
codeContent ? schema.text(codeContent) : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
line.includes("|")
|
||||||
|
&& i + 1 < lines.length
|
||||||
|
&& tableSeparatorPattern.test(lines[i + 1].trim())
|
||||||
|
) {
|
||||||
|
const headerLine = line;
|
||||||
|
i += 2;
|
||||||
|
const dataLines: string[] = [];
|
||||||
|
while (i < lines.length) {
|
||||||
|
const row = lines[i].trim();
|
||||||
|
if (!row || !row.includes("|")) break;
|
||||||
|
dataLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
nodes.push(buildTable(schema, headerLine, dataLines));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed === "---" || trimmed === "___" || trimmed === "***") {
|
||||||
|
nodes.push(schema.nodes.horizontalRule.create());
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headingMatch = line.match(/^(#{1,6})\s(.+)$/);
|
||||||
|
if (headingMatch) {
|
||||||
|
const level = headingMatch[1].length;
|
||||||
|
const content = parseInlineContent(schema, headingMatch[2]);
|
||||||
|
nodes.push(
|
||||||
|
schema.nodes.heading.create(
|
||||||
|
{ level },
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("> ")) {
|
||||||
|
const paragraphs: ProseMirrorNode[] = [];
|
||||||
|
while (i < lines.length) {
|
||||||
|
const bqMatch = lines[i].match(/^>\s(.*)$/);
|
||||||
|
if (!bqMatch) break;
|
||||||
|
const bqText = bqMatch[1];
|
||||||
|
const content = bqText
|
||||||
|
? parseInlineContent(schema, bqText)
|
||||||
|
: [];
|
||||||
|
paragraphs.push(
|
||||||
|
schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
nodes.push(schema.nodes.blockquote.create(null, paragraphs));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
line.startsWith("- ")
|
||||||
|
|| line.startsWith("+ ")
|
||||||
|
|| line.startsWith("* ")
|
||||||
|
) {
|
||||||
|
const items: ProseMirrorNode[] = [];
|
||||||
|
while (i < lines.length) {
|
||||||
|
const blMatch = lines[i].match(/^[-+*]\s(.*)$/);
|
||||||
|
if (!blMatch) break;
|
||||||
|
const blText = blMatch[1];
|
||||||
|
const content = blText
|
||||||
|
? parseInlineContent(schema, blText)
|
||||||
|
: [];
|
||||||
|
items.push(
|
||||||
|
schema.nodes.listItem.create(
|
||||||
|
null,
|
||||||
|
schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
nodes.push(schema.nodes.bulletList.create(null, items));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const olMatch = line.match(/^(\d+)\.\s(.*)$/);
|
||||||
|
if (olMatch) {
|
||||||
|
const start = +olMatch[1];
|
||||||
|
const items: ProseMirrorNode[] = [];
|
||||||
|
while (i < lines.length) {
|
||||||
|
const itemMatch = lines[i].match(/^\d+\.\s(.*)$/);
|
||||||
|
if (!itemMatch) break;
|
||||||
|
const olText = itemMatch[1];
|
||||||
|
const content = olText
|
||||||
|
? parseInlineContent(schema, olText)
|
||||||
|
: [];
|
||||||
|
items.push(
|
||||||
|
schema.nodes.listItem.create(
|
||||||
|
null,
|
||||||
|
schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
nodes.push(schema.nodes.orderedList.create({ start }, items));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.trim()) {
|
||||||
|
const content = parseInlineContent(schema, line);
|
||||||
|
nodes.push(
|
||||||
|
schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
20
packages/prosemirror/tsconfig.json
Normal file
20
packages/prosemirror/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": false,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
"@probo/helpers": "1.0.0",
|
"@probo/helpers": "1.0.0",
|
||||||
"@probo/i18n": "1.0.0",
|
"@probo/i18n": "1.0.0",
|
||||||
|
"@probo/prosemirror": "1.0.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
|||||||
@@ -2,308 +2,13 @@
|
|||||||
// Use of this source code is governed by the ISC license
|
// Use of this source code is governed by the ISC license
|
||||||
// that can be found in the LICENSE file.
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import { hasMarkdown, parseMarkdown } from "@probo/prosemirror";
|
||||||
import { Extension } from "@tiptap/core";
|
import { Extension } from "@tiptap/core";
|
||||||
import {
|
import { Fragment, Slice } from "@tiptap/pm/model";
|
||||||
Fragment,
|
|
||||||
type Node as ProseMirrorNode,
|
|
||||||
type Schema,
|
|
||||||
Slice,
|
|
||||||
} from "@tiptap/pm/model";
|
|
||||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||||
|
|
||||||
const markdownPasteKey = new PluginKey("markdownPaste");
|
const markdownPasteKey = new PluginKey("markdownPaste");
|
||||||
|
|
||||||
const codeBlockOpenPattern = /^```(\w*)$/;
|
|
||||||
const tableSeparatorPattern
|
|
||||||
= /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/m;
|
|
||||||
const markdownLinePattern
|
|
||||||
= /(?:^```\w*$|^#{1,6}\s|^>\s|^[-+*]\s|^\d+\.\s|^(?:---|___|\*\*\*)\s*$)/m;
|
|
||||||
|
|
||||||
function parseCells(line: string): string[] {
|
|
||||||
return line
|
|
||||||
.replace(/^\|/, "")
|
|
||||||
.replace(/\|\s*$/, "")
|
|
||||||
.split("|")
|
|
||||||
.map(cell => cell.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyMarks(
|
|
||||||
schema: Schema,
|
|
||||||
markNames: string[],
|
|
||||||
inner: string,
|
|
||||||
): ProseMirrorNode[] {
|
|
||||||
const marks = markNames.map(name => schema.marks[name].create());
|
|
||||||
return parseInlineContent(schema, inner).map((node) => {
|
|
||||||
let combined = node.marks;
|
|
||||||
for (const mark of marks) {
|
|
||||||
combined = mark.addToSet(combined);
|
|
||||||
}
|
|
||||||
return node.mark(combined);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseInlineContent(
|
|
||||||
schema: Schema,
|
|
||||||
text: string,
|
|
||||||
): ProseMirrorNode[] {
|
|
||||||
const nodes: ProseMirrorNode[] = [];
|
|
||||||
const pattern
|
|
||||||
= /`([^`]+)`|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|~~(.+?)~~|<u>(.+?)<\/u>|\[([^\]]+)\]\(([^)]+)\)|\*(.+?)\*/g;
|
|
||||||
let lastIndex = 0;
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = pattern.exec(text)) !== null) {
|
|
||||||
if (match.index > lastIndex) {
|
|
||||||
nodes.push(schema.text(text.slice(lastIndex, match.index)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match[1] !== undefined) {
|
|
||||||
nodes.push(schema.text(match[1], [schema.marks.code.create()]));
|
|
||||||
} else if (match[2] !== undefined) {
|
|
||||||
nodes.push(...applyMarks(schema, ["bold", "italic"], match[2]));
|
|
||||||
} else if (match[3] !== undefined) {
|
|
||||||
nodes.push(...applyMarks(schema, ["bold"], match[3]));
|
|
||||||
} else if (match[4] !== undefined) {
|
|
||||||
nodes.push(...applyMarks(schema, ["strike"], match[4]));
|
|
||||||
} else if (match[5] !== undefined) {
|
|
||||||
nodes.push(...applyMarks(schema, ["underline"], match[5]));
|
|
||||||
} else if (match[6] !== undefined) {
|
|
||||||
const linkMark = schema.marks.link.create({ href: match[7] });
|
|
||||||
for (const node of parseInlineContent(schema, match[6])) {
|
|
||||||
nodes.push(node.mark(linkMark.addToSet(node.marks)));
|
|
||||||
}
|
|
||||||
} else if (match[8] !== undefined) {
|
|
||||||
nodes.push(...applyMarks(schema, ["italic"], match[8]));
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIndex = match.index + match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastIndex < text.length) {
|
|
||||||
nodes.push(schema.text(text.slice(lastIndex)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTable(
|
|
||||||
schema: Schema,
|
|
||||||
headerLine: string,
|
|
||||||
dataLines: string[],
|
|
||||||
): ProseMirrorNode {
|
|
||||||
const headers = parseCells(headerLine);
|
|
||||||
const columnCount = headers.length;
|
|
||||||
|
|
||||||
const headerCells = headers.map((h) => {
|
|
||||||
const content = h ? parseInlineContent(schema, h) : [];
|
|
||||||
return schema.nodes.tableHeader.create(
|
|
||||||
null,
|
|
||||||
schema.nodes.paragraph.create(
|
|
||||||
null,
|
|
||||||
content.length > 0 ? content : undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const tableRows: ProseMirrorNode[] = [
|
|
||||||
schema.nodes.tableRow.create(null, headerCells),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const line of dataLines) {
|
|
||||||
const cells = parseCells(line);
|
|
||||||
const rowCells: ProseMirrorNode[] = [];
|
|
||||||
for (let c = 0; c < columnCount; c++) {
|
|
||||||
const value = cells[c]?.trim() ?? "";
|
|
||||||
const content = value ? parseInlineContent(schema, value) : [];
|
|
||||||
rowCells.push(
|
|
||||||
schema.nodes.tableCell.create(
|
|
||||||
null,
|
|
||||||
schema.nodes.paragraph.create(
|
|
||||||
null,
|
|
||||||
content.length > 0 ? content : undefined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
tableRows.push(schema.nodes.tableRow.create(null, rowCells));
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema.nodes.table.create(null, tableRows);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasMarkdown(text: string): boolean {
|
|
||||||
return markdownLinePattern.test(text) || tableSeparatorPattern.test(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseMarkdown(
|
|
||||||
text: string,
|
|
||||||
schema: Schema,
|
|
||||||
): ProseMirrorNode[] {
|
|
||||||
const lines = text.split("\n");
|
|
||||||
const nodes: ProseMirrorNode[] = [];
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (i < lines.length) {
|
|
||||||
const line = lines[i];
|
|
||||||
|
|
||||||
// Code blocks
|
|
||||||
const codeMatch = line.match(codeBlockOpenPattern);
|
|
||||||
if (codeMatch) {
|
|
||||||
const language = codeMatch[1] || null;
|
|
||||||
const contentLines: string[] = [];
|
|
||||||
i++;
|
|
||||||
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
||||||
contentLines.push(lines[i]);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
|
|
||||||
const codeContent = contentLines.join("\n").replace(/\n$/, "");
|
|
||||||
nodes.push(
|
|
||||||
schema.nodes.codeBlock.create(
|
|
||||||
{ language },
|
|
||||||
codeContent ? schema.text(codeContent) : undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tables
|
|
||||||
if (
|
|
||||||
line.includes("|")
|
|
||||||
&& i + 1 < lines.length
|
|
||||||
&& tableSeparatorPattern.test(lines[i + 1].trim())
|
|
||||||
) {
|
|
||||||
const headerLine = line;
|
|
||||||
i += 2;
|
|
||||||
const dataLines: string[] = [];
|
|
||||||
while (i < lines.length) {
|
|
||||||
const row = lines[i].trim();
|
|
||||||
if (!row || !row.includes("|")) break;
|
|
||||||
dataLines.push(lines[i]);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
nodes.push(buildTable(schema, headerLine, dataLines));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Horizontal rules (before bullet lists since --- / *** could overlap)
|
|
||||||
const trimmed = line.trim();
|
|
||||||
if (trimmed === "---" || trimmed === "___" || trimmed === "***") {
|
|
||||||
nodes.push(schema.nodes.horizontalRule.create());
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Headings
|
|
||||||
const headingMatch = line.match(/^(#{1,6})\s(.+)$/);
|
|
||||||
if (headingMatch) {
|
|
||||||
const level = headingMatch[1].length;
|
|
||||||
const content = parseInlineContent(schema, headingMatch[2]);
|
|
||||||
nodes.push(
|
|
||||||
schema.nodes.heading.create(
|
|
||||||
{ level },
|
|
||||||
content.length > 0 ? content : undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blockquotes (group consecutive lines)
|
|
||||||
if (line.startsWith("> ")) {
|
|
||||||
const paragraphs: ProseMirrorNode[] = [];
|
|
||||||
while (i < lines.length) {
|
|
||||||
const bqMatch = lines[i].match(/^>\s(.*)$/);
|
|
||||||
if (!bqMatch) break;
|
|
||||||
const bqText = bqMatch[1];
|
|
||||||
const content = bqText
|
|
||||||
? parseInlineContent(schema, bqText)
|
|
||||||
: [];
|
|
||||||
paragraphs.push(
|
|
||||||
schema.nodes.paragraph.create(
|
|
||||||
null,
|
|
||||||
content.length > 0 ? content : undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
nodes.push(schema.nodes.blockquote.create(null, paragraphs));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bullet lists (group consecutive items)
|
|
||||||
if (
|
|
||||||
line.startsWith("- ")
|
|
||||||
|| line.startsWith("+ ")
|
|
||||||
|| line.startsWith("* ")
|
|
||||||
) {
|
|
||||||
const items: ProseMirrorNode[] = [];
|
|
||||||
while (i < lines.length) {
|
|
||||||
const blMatch = lines[i].match(/^[-+*]\s(.*)$/);
|
|
||||||
if (!blMatch) break;
|
|
||||||
const blText = blMatch[1];
|
|
||||||
const content = blText
|
|
||||||
? parseInlineContent(schema, blText)
|
|
||||||
: [];
|
|
||||||
items.push(
|
|
||||||
schema.nodes.listItem.create(
|
|
||||||
null,
|
|
||||||
schema.nodes.paragraph.create(
|
|
||||||
null,
|
|
||||||
content.length > 0 ? content : undefined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
nodes.push(schema.nodes.bulletList.create(null, items));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordered lists (group consecutive items)
|
|
||||||
const olMatch = line.match(/^(\d+)\.\s(.*)$/);
|
|
||||||
if (olMatch) {
|
|
||||||
const start = +olMatch[1];
|
|
||||||
const items: ProseMirrorNode[] = [];
|
|
||||||
while (i < lines.length) {
|
|
||||||
const itemMatch = lines[i].match(/^\d+\.\s(.*)$/);
|
|
||||||
if (!itemMatch) break;
|
|
||||||
const olText = itemMatch[1];
|
|
||||||
const content = olText
|
|
||||||
? parseInlineContent(schema, olText)
|
|
||||||
: [];
|
|
||||||
items.push(
|
|
||||||
schema.nodes.listItem.create(
|
|
||||||
null,
|
|
||||||
schema.nodes.paragraph.create(
|
|
||||||
null,
|
|
||||||
content.length > 0 ? content : undefined,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
nodes.push(schema.nodes.orderedList.create({ start }, items));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plain paragraph
|
|
||||||
if (line.trim()) {
|
|
||||||
const content = parseInlineContent(schema, line);
|
|
||||||
nodes.push(
|
|
||||||
schema.nodes.paragraph.create(
|
|
||||||
null,
|
|
||||||
content.length > 0 ? content : undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MarkdownPasteExtension = Extension.create({
|
export const MarkdownPasteExtension = Extension.create({
|
||||||
name: "markdownPaste",
|
name: "markdownPaste",
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user