Reimplement ts converter using marked parser
Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
3
package-lock.json
generated
3
package-lock.json
generated
@@ -19709,7 +19709,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/pm": "^3.20.2"
|
"@tiptap/pm": "^3.20.2",
|
||||||
|
"marked": "^15.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "~5.8.3"
|
"typescript": "~5.8.3"
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"check": "tsc --noEmit"
|
"check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tiptap/pm": "^3.20.2"
|
"@tiptap/pm": "^3.20.2",
|
||||||
|
"marked": "^15.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "~5.8.3"
|
"typescript": "~5.8.3"
|
||||||
|
|||||||
@@ -2,287 +2,453 @@
|
|||||||
// 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 type { Node as ProseMirrorNode, Schema } from "@tiptap/pm/model";
|
import { lexer, type Token, type Tokens } from "marked";
|
||||||
|
import type {
|
||||||
|
Mark as ProseMirrorMark,
|
||||||
|
Node as ProseMirrorNode,
|
||||||
|
Schema,
|
||||||
|
} from "@tiptap/pm/model";
|
||||||
|
|
||||||
const codeBlockOpenPattern = /^```(\w*)$/;
|
const tableSeparatorPattern =
|
||||||
const tableSeparatorPattern
|
/^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/m;
|
||||||
= /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)*\|?\s*$/m;
|
const markdownLinePattern =
|
||||||
const markdownLinePattern
|
/(?:^```\w*$|^#{1,6}\s|^>\s|^[-+*]\s|^\d+\.\s|^(?:---|___|\*\*\*)\s*$)/m;
|
||||||
= /(?:^```\w*$|^#{1,6}\s|^>\s|^[-+*]\s|^\d+\.\s|^(?:---|___|\*\*\*)\s*$)/m;
|
|
||||||
|
|
||||||
function parseCells(line: string): string[] {
|
function unescapeHtml(html: string): string {
|
||||||
return line
|
return html
|
||||||
.replace(/^\|/, "")
|
.replace(/</g, "<")
|
||||||
.replace(/\|\s*$/, "")
|
.replace(/>/g, ">")
|
||||||
.split("|")
|
.replace(/"/g, '"')
|
||||||
.map(cell => cell.trim());
|
.replace(/'/g, "'")
|
||||||
|
.replace(/&/g, "&");
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyMarks(
|
function safeLinkHref(href: string): string {
|
||||||
schema: Schema,
|
href = href.trim();
|
||||||
markNames: string[],
|
if (!href) return "#";
|
||||||
inner: string,
|
if (href[0] === "#") return href;
|
||||||
): ProseMirrorNode[] {
|
if (href.startsWith("/")) {
|
||||||
const marks = markNames.map(name => schema.marks[name].create());
|
if (href.length > 1 && (href[1] === "/" || href[1] === "\\")) return "#";
|
||||||
return parseInlineContent(schema, inner).map((node) => {
|
return href;
|
||||||
let combined = node.marks;
|
}
|
||||||
for (const mark of marks) {
|
try {
|
||||||
combined = mark.addToSet(combined);
|
const url = new URL(href);
|
||||||
|
const scheme = url.protocol.slice(0, -1).toLowerCase();
|
||||||
|
switch (scheme) {
|
||||||
|
case "http":
|
||||||
|
case "https":
|
||||||
|
case "mailto":
|
||||||
|
case "tel":
|
||||||
|
return href;
|
||||||
|
default:
|
||||||
|
return "#";
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return href;
|
||||||
}
|
}
|
||||||
return node.mark(combined);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseInlineContent(
|
type HtmlTagInfo = {
|
||||||
schema: Schema,
|
closing: boolean;
|
||||||
text: string,
|
markName: string;
|
||||||
): ProseMirrorNode[] {
|
attrs?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTagPattern = /^<(u|b|i|s|em|strong|del|code|a)(\s[^>]*)?\s*\/?>$/i;
|
||||||
|
const closeTagPattern = /^<\/(u|b|i|s|em|strong|del|code|a)\s*>$/i;
|
||||||
|
|
||||||
|
function tagToMarkName(tag: string): string | null {
|
||||||
|
switch (tag.toLowerCase()) {
|
||||||
|
case "u":
|
||||||
|
return "underline";
|
||||||
|
case "b":
|
||||||
|
case "strong":
|
||||||
|
return "bold";
|
||||||
|
case "i":
|
||||||
|
case "em":
|
||||||
|
return "italic";
|
||||||
|
case "s":
|
||||||
|
case "del":
|
||||||
|
return "strike";
|
||||||
|
case "code":
|
||||||
|
return "code";
|
||||||
|
case "a":
|
||||||
|
return "link";
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHtmlTag(html: string): HtmlTagInfo | null {
|
||||||
|
const closeMatch = html.match(closeTagPattern);
|
||||||
|
if (closeMatch) {
|
||||||
|
const markName = tagToMarkName(closeMatch[1]);
|
||||||
|
if (markName) return { closing: true, markName };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openMatch = html.match(openTagPattern);
|
||||||
|
if (openMatch) {
|
||||||
|
const markName = tagToMarkName(openMatch[1]);
|
||||||
|
if (!markName) return null;
|
||||||
|
|
||||||
|
const info: HtmlTagInfo = { closing: false, markName };
|
||||||
|
|
||||||
|
if (openMatch[1].toLowerCase() === "a" && openMatch[2]) {
|
||||||
|
const hrefMatch = openMatch[2].match(/href=["']([^"']*)["']/i);
|
||||||
|
if (hrefMatch) {
|
||||||
|
info.attrs = { href: safeLinkHref(hrefMatch[1]) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Converter {
|
||||||
|
private schema: Schema;
|
||||||
|
private marks: ProseMirrorMark[];
|
||||||
|
|
||||||
|
constructor(schema: Schema) {
|
||||||
|
this.schema = schema;
|
||||||
|
this.marks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private withMark(
|
||||||
|
mark: ProseMirrorMark,
|
||||||
|
fn: () => ProseMirrorNode[],
|
||||||
|
): ProseMirrorNode[] {
|
||||||
|
this.marks.push(mark);
|
||||||
|
const result = fn();
|
||||||
|
this.marks.pop();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentMarks(): readonly ProseMirrorMark[] {
|
||||||
|
let result: readonly ProseMirrorMark[] = [];
|
||||||
|
for (const mark of this.marks) {
|
||||||
|
result = mark.addToSet(result as ProseMirrorMark[]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeTextNode(text: string): ProseMirrorNode[] {
|
||||||
|
const unescaped = unescapeHtml(text);
|
||||||
|
if (!unescaped) return [];
|
||||||
|
const marks = this.currentMarks();
|
||||||
|
return [this.schema.text(unescaped, marks.length > 0 ? marks : undefined)];
|
||||||
|
}
|
||||||
|
|
||||||
|
convertBlockTokens(tokens: Token[]): ProseMirrorNode[] {
|
||||||
const nodes: ProseMirrorNode[] = [];
|
const nodes: ProseMirrorNode[] = [];
|
||||||
const pattern
|
for (const token of tokens) {
|
||||||
= /`([^`]+)`|\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|~~(.+?)~~|<u>(.+?)<\/u>|\[([^\]]+)\]\(([^)]+)\)|\*(.+?)\*/g;
|
nodes.push(...this.convertBlockToken(token));
|
||||||
let lastIndex = 0;
|
}
|
||||||
let match;
|
return nodes;
|
||||||
|
|
||||||
while ((match = pattern.exec(text)) !== null) {
|
|
||||||
if (match.index > lastIndex) {
|
|
||||||
nodes.push(schema.text(text.slice(lastIndex, match.index)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match[1] !== undefined) {
|
private convertBlockToken(token: Token): ProseMirrorNode[] {
|
||||||
nodes.push(schema.text(match[1], [schema.marks.code.create()]));
|
switch (token.type) {
|
||||||
} else if (match[2] !== undefined) {
|
case "heading":
|
||||||
nodes.push(...applyMarks(schema, ["bold", "italic"], match[2]));
|
return this.convertHeading(token as Tokens.Heading);
|
||||||
} else if (match[3] !== undefined) {
|
case "paragraph":
|
||||||
nodes.push(...applyMarks(schema, ["bold"], match[3]));
|
return this.convertParagraph(token as Tokens.Paragraph);
|
||||||
} else if (match[4] !== undefined) {
|
case "blockquote":
|
||||||
nodes.push(...applyMarks(schema, ["strike"], match[4]));
|
return this.convertBlockquote(token as Tokens.Blockquote);
|
||||||
} else if (match[5] !== undefined) {
|
case "code":
|
||||||
nodes.push(...applyMarks(schema, ["underline"], match[5]));
|
return this.convertCodeBlock(token as Tokens.Code);
|
||||||
} else if (match[6] !== undefined) {
|
case "list":
|
||||||
const linkMark = schema.marks.link.create({ href: match[7] });
|
return this.convertList(token as Tokens.List);
|
||||||
for (const node of parseInlineContent(schema, match[6])) {
|
case "hr":
|
||||||
nodes.push(node.mark(linkMark.addToSet(node.marks)));
|
return [this.schema.nodes.horizontalRule.create()];
|
||||||
|
case "table":
|
||||||
|
return this.convertTable(token as Tokens.Table);
|
||||||
|
case "text":
|
||||||
|
return this.convertBlockText(token as Tokens.Text);
|
||||||
|
case "html":
|
||||||
|
case "space":
|
||||||
|
case "def":
|
||||||
|
return [];
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
} else if (match[8] !== undefined) {
|
|
||||||
nodes.push(...applyMarks(schema, ["italic"], match[8]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastIndex = match.index + match[0].length;
|
private convertHeading(token: Tokens.Heading): ProseMirrorNode[] {
|
||||||
|
const content = this.convertInlineTokens(token.tokens);
|
||||||
|
return [
|
||||||
|
this.schema.nodes.heading.create(
|
||||||
|
{ level: token.depth },
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex < text.length) {
|
private convertParagraph(token: Tokens.Paragraph): ProseMirrorNode[] {
|
||||||
nodes.push(schema.text(text.slice(lastIndex)));
|
const content = this.convertInlineTokens(token.tokens);
|
||||||
|
return [
|
||||||
|
this.schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertBlockquote(token: Tokens.Blockquote): ProseMirrorNode[] {
|
||||||
|
const children = this.convertBlockTokens(token.tokens);
|
||||||
|
return [
|
||||||
|
this.schema.nodes.blockquote.create(
|
||||||
|
null,
|
||||||
|
children.length > 0 ? children : undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertCodeBlock(token: Tokens.Code): ProseMirrorNode[] {
|
||||||
|
const language = token.lang || null;
|
||||||
|
return [
|
||||||
|
this.schema.nodes.codeBlock.create(
|
||||||
|
{ language },
|
||||||
|
token.text ? this.schema.text(token.text) : undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertList(token: Tokens.List): ProseMirrorNode[] {
|
||||||
|
const children = token.items.flatMap(item => this.convertListItem(item));
|
||||||
|
|
||||||
|
if (token.ordered) {
|
||||||
|
const start = typeof token.start === "number" ? token.start : 1;
|
||||||
|
return [
|
||||||
|
this.schema.nodes.orderedList.create(
|
||||||
|
{ start },
|
||||||
|
children.length > 0 ? children : undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
this.schema.nodes.bulletList.create(
|
||||||
|
null,
|
||||||
|
children.length > 0 ? children : undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertListItem(token: Tokens.ListItem): ProseMirrorNode[] {
|
||||||
|
const children: ProseMirrorNode[] = [];
|
||||||
|
|
||||||
|
for (const child of token.tokens) {
|
||||||
|
if (child.type === "text") {
|
||||||
|
const textToken = child as Tokens.Text;
|
||||||
|
const inlineContent = textToken.tokens
|
||||||
|
? this.convertInlineTokens(textToken.tokens)
|
||||||
|
: this.makeTextNode(textToken.text);
|
||||||
|
children.push(
|
||||||
|
this.schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
inlineContent.length > 0 ? inlineContent : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
children.push(...this.convertBlockToken(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children.length === 0) {
|
||||||
|
children.push(this.schema.nodes.paragraph.create());
|
||||||
|
}
|
||||||
|
|
||||||
|
return [this.schema.nodes.listItem.create(null, children)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertBlockText(token: Tokens.Text): ProseMirrorNode[] {
|
||||||
|
const content = token.tokens
|
||||||
|
? this.convertInlineTokens(token.tokens)
|
||||||
|
: this.makeTextNode(token.text);
|
||||||
|
return [
|
||||||
|
this.schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertTable(token: Tokens.Table): ProseMirrorNode[] {
|
||||||
|
const rows: ProseMirrorNode[] = [];
|
||||||
|
|
||||||
|
const headerCells = token.header.map(cell =>
|
||||||
|
this.convertTableCell(cell, true),
|
||||||
|
);
|
||||||
|
rows.push(this.schema.nodes.tableRow.create(null, headerCells));
|
||||||
|
|
||||||
|
for (const row of token.rows) {
|
||||||
|
const dataCells = row.map(cell => this.convertTableCell(cell, false));
|
||||||
|
rows.push(this.schema.nodes.tableRow.create(null, dataCells));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [this.schema.nodes.table.create(null, rows)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertTableCell(
|
||||||
|
cell: Tokens.TableCell,
|
||||||
|
isHeader: boolean,
|
||||||
|
): ProseMirrorNode {
|
||||||
|
const content = this.convertInlineTokens(cell.tokens);
|
||||||
|
const nodeType = isHeader
|
||||||
|
? this.schema.nodes.tableHeader
|
||||||
|
: this.schema.nodes.tableCell;
|
||||||
|
return nodeType.create(
|
||||||
|
null,
|
||||||
|
this.schema.nodes.paragraph.create(
|
||||||
|
null,
|
||||||
|
content.length > 0 ? content : undefined,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
convertInlineTokens(tokens: Token[]): ProseMirrorNode[] {
|
||||||
|
const nodes: ProseMirrorNode[] = [];
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
switch (token.type) {
|
||||||
|
case "text": {
|
||||||
|
const textToken = token as Tokens.Text;
|
||||||
|
if (textToken.tokens && textToken.tokens.length > 0) {
|
||||||
|
nodes.push(...this.convertInlineTokens(textToken.tokens));
|
||||||
|
} else {
|
||||||
|
nodes.push(...this.makeTextNode(textToken.text));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "strong":
|
||||||
|
nodes.push(
|
||||||
|
...this.withMark(this.schema.marks.bold.create(), () =>
|
||||||
|
this.convertInlineTokens((token as Tokens.Strong).tokens),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "em":
|
||||||
|
nodes.push(
|
||||||
|
...this.withMark(this.schema.marks.italic.create(), () =>
|
||||||
|
this.convertInlineTokens((token as Tokens.Em).tokens),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "del":
|
||||||
|
nodes.push(
|
||||||
|
...this.withMark(this.schema.marks.strike.create(), () =>
|
||||||
|
this.convertInlineTokens((token as Tokens.Del).tokens),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "codespan": {
|
||||||
|
const codeText = unescapeHtml((token as Tokens.Codespan).text);
|
||||||
|
if (codeText) {
|
||||||
|
const codeMark = this.schema.marks.code.create();
|
||||||
|
const marks = codeMark.addToSet(
|
||||||
|
this.currentMarks() as ProseMirrorMark[],
|
||||||
|
);
|
||||||
|
nodes.push(this.schema.text(codeText, marks));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "link": {
|
||||||
|
const linkToken = token as Tokens.Link;
|
||||||
|
const attrs: Record<string, unknown> = {
|
||||||
|
href: safeLinkHref(linkToken.href),
|
||||||
|
};
|
||||||
|
if (linkToken.title) {
|
||||||
|
attrs.title = linkToken.title;
|
||||||
|
}
|
||||||
|
nodes.push(
|
||||||
|
...this.withMark(this.schema.marks.link.create(attrs), () =>
|
||||||
|
this.convertInlineTokens(linkToken.tokens),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "image": {
|
||||||
|
const imgToken = token as Tokens.Image;
|
||||||
|
const attrs: Record<string, unknown> = { src: imgToken.href };
|
||||||
|
if (imgToken.title) attrs.title = imgToken.title;
|
||||||
|
if (imgToken.text) attrs.alt = unescapeHtml(imgToken.text);
|
||||||
|
nodes.push(this.schema.nodes.image.create(attrs));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "br":
|
||||||
|
nodes.push(this.schema.nodes.hardBreak.create());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "escape":
|
||||||
|
nodes.push(...this.makeTextNode((token as Tokens.Escape).text));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "html": {
|
||||||
|
const tagInfo = parseHtmlTag((token as Tokens.Tag).text);
|
||||||
|
if (tagInfo) {
|
||||||
|
if (tagInfo.closing) {
|
||||||
|
for (let j = this.marks.length - 1; j >= 0; j--) {
|
||||||
|
if (this.marks[j].type.name === tagInfo.markName) {
|
||||||
|
this.marks.splice(j, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const markType = this.schema.marks[tagInfo.markName];
|
||||||
|
if (markType) {
|
||||||
|
this.marks.push(markType.create(tagInfo.attrs || null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes;
|
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 {
|
export function hasMarkdown(text: string): boolean {
|
||||||
return markdownLinePattern.test(text) || tableSeparatorPattern.test(text);
|
return markdownLinePattern.test(text) || tableSeparatorPattern.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseInlineContent(
|
||||||
|
schema: Schema,
|
||||||
|
text: string,
|
||||||
|
): ProseMirrorNode[] {
|
||||||
|
const tokens = lexer(text);
|
||||||
|
const converter = new Converter(schema);
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (token.type === "paragraph") {
|
||||||
|
return converter.convertInlineTokens(
|
||||||
|
(token as Tokens.Paragraph).tokens,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
export function parseMarkdown(
|
export function parseMarkdown(
|
||||||
text: string,
|
text: string,
|
||||||
schema: Schema,
|
schema: Schema,
|
||||||
): ProseMirrorNode[] {
|
): ProseMirrorNode[] {
|
||||||
const lines = text.split("\n");
|
const tokens = lexer(text);
|
||||||
const nodes: ProseMirrorNode[] = [];
|
const converter = new Converter(schema);
|
||||||
let i = 0;
|
return converter.convertBlockTokens(tokens);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user