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",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@probo/prosemirror": {
|
||||
"resolved": "packages/prosemirror",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@probo/react-lazy": {
|
||||
"resolved": "packages/react-lazy",
|
||||
"link": true
|
||||
@@ -18215,7 +18219,6 @@
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
@@ -19701,6 +19704,17 @@
|
||||
"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": {
|
||||
"name": "@probo/react-lazy",
|
||||
"version": "1.0.0",
|
||||
@@ -19755,6 +19769,7 @@
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@probo/helpers": "1.0.0",
|
||||
"@probo/i18n": "1.0.0",
|
||||
"@probo/prosemirror": "1.0.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@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",
|
||||
"@probo/helpers": "1.0.0",
|
||||
"@probo/i18n": "1.0.0",
|
||||
"@probo/prosemirror": "1.0.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
|
||||
@@ -2,308 +2,13 @@
|
||||
// Use of this source code is governed by the ISC license
|
||||
// that can be found in the LICENSE file.
|
||||
|
||||
import { hasMarkdown, parseMarkdown } from "@probo/prosemirror";
|
||||
import { Extension } from "@tiptap/core";
|
||||
import {
|
||||
Fragment,
|
||||
type Node as ProseMirrorNode,
|
||||
type Schema,
|
||||
Slice,
|
||||
} from "@tiptap/pm/model";
|
||||
import { Fragment, Slice } from "@tiptap/pm/model";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
|
||||
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({
|
||||
name: "markdownPaste",
|
||||
|
||||
|
||||
Reference in New Issue
Block a user