UNPKG

@firecms/core

Version:

Awesome Firebase/Firestore-based headless open-source CMS

173 lines (161 loc) 6.47 kB
import { MarkdownParser, MarkdownSerializer, defaultMarkdownParser, defaultMarkdownSerializer } from "prosemirror-markdown"; import markdownIt from "markdown-it"; // @ts-ignore import markdownItTaskLists from "markdown-it-task-lists"; // @ts-ignore import markdownItMark from "markdown-it-mark"; // @ts-ignore import markdownItIns from "markdown-it-ins"; import { schema } from "./schema"; const parserTokens: any = { ...defaultMarkdownParser.tokens, em: { mark: "italic" }, strong: { mark: "bold" }, html_inline: { ignore: true, noCloseToken: true }, html_block: { ignore: true, noCloseToken: true }, s: { mark: "strike", }, task_list: { block: "task_list", }, task_item: { block: "task_item", getAttrs: (tok: any) => ({ checked: tok.attrGet("checked") === "true" }), }, mark: { mark: "highlight" }, ins: { mark: "underline" }, table: { block: "table" }, thead: { ignore: true }, tbody: { ignore: true }, tr: { block: "table_row" }, th: { block: "table_header" }, td: { block: "table_cell" } }; const md = markdownIt({ html: false }) .use(markdownItTaskLists) .use(markdownItMark) .use(markdownItIns); // Unwrap images from paragraphs so they can be parsed as block nodes by ProseMirror md.core.ruler.after("inline", "image-to-block", (state: any) => { const tokens = state.tokens; for (let i = tokens.length - 2; i >= 1; i--) { if ( tokens[i - 1] && tokens[i - 1].type === "paragraph_open" && tokens[i] && tokens[i].type === "inline" && tokens[i + 1] && tokens[i + 1].type === "paragraph_close" ) { const inlineTokens = tokens[i].children || []; if (inlineTokens.length === 1 && inlineTokens[0].type === "image") { state.tokens.splice(i - 1, 3, inlineTokens[0]); // No need to adjust index when looping backward! } } } }); // Wrap inline tokens inside table cells into paragraphs to satisfy ProseMirror table cell schema (block+) md.core.ruler.after("inline", "tables-wrap-paragraphs", (state: any) => { const tokens = state.tokens; for (let i = tokens.length - 1; i >= 0; i--) { if (tokens[i].type === "td_open" || tokens[i].type === "th_open") { let closeIndex = i + 1; while (closeIndex < tokens.length && tokens[closeIndex].type !== "td_close" && tokens[closeIndex].type !== "th_close") { closeIndex++; } if (closeIndex < tokens.length) { const pOpen = new state.Token("paragraph_open", "p", 1); pOpen.block = true; const pClose = new state.Token("paragraph_close", "p", -1); pClose.block = true; state.tokens.splice(closeIndex, 0, pClose); state.tokens.splice(i + 1, 0, pOpen); } } } }); export const markdownParser = new MarkdownParser(schema, md, parserTokens); export const markdownSerializer = new MarkdownSerializer( { ...defaultMarkdownSerializer.nodes, // Add custom serialization for task lists task_list(state, node) { state.renderList(node, " ", () => "- "); }, task_item(state, node) { state.write(`[${node.attrs.checked ? "x" : " "}] `); state.renderContent(node); }, horizontal_rule(state, node) { state.write(node.attrs.markup || "---"); state.closeBlock(node); }, image(state, node) { const rawSrc = node.attrs.src || ""; const src = rawSrc.replace(/ /g, "%20"); state.write("![" + state.esc(node.attrs.alt || "") + "](" + src.replace(/[\(\)]/g, "\\$&") + (node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : "") + ")"); state.closeBlock(node); }, table(state, node) { node.forEach((row, _, i) => { row.forEach((cell, _, j) => { state.write(j === 0 ? "| " : " "); let cellContent = ""; const oldWrite = state.write.bind(state); state.write = (s: string) => { cellContent += s; }; let first = true; cell.forEach((block: any) => { if (!first) cellContent += "<br>"; state.renderInline(block); first = false; }); state.write = oldWrite; state.write(cellContent.replace(/\|/g, "\\|")); state.write(" |"); }); state.write("\n"); if (i === 0) { row.forEach((cell, _, j) => { state.write(j === 0 ? "|---|" : "---|"); }); state.write("\n"); } }); state.closeBlock(node); }, table_row() {}, table_cell() {}, table_header() {} }, { ...defaultMarkdownSerializer.marks, bold: defaultMarkdownSerializer.marks.strong, italic: defaultMarkdownSerializer.marks.em, strike: { open: "~~", close: "~~", mixable: true, expelEnclosingWhitespace: true }, highlight: { open: "==", close: "==", mixable: true, expelEnclosingWhitespace: true }, underline: { open: "++", close: "++", mixable: true, expelEnclosingWhitespace: true }, link: { ...defaultMarkdownSerializer.marks.link, close(state: any, mark, parent, index) { const inAutolink = state.inAutolink; state.inAutolink = undefined; const href = mark.attrs.href.replace(/ /g, "%20"); return inAutolink ? ">" : "](" + href.replace(/[\(\)"]/g, "\\$&") + (mark.attrs.title ? ` "${mark.attrs.title.replace(/"/g, '\\"')}"` : "") + ")"; } }, // textStyle (colored text from HTML) has no markdown equivalent — emit content as-is textStyle: { open: "", close: "", mixable: true, expelEnclosingWhitespace: true }, } ); export const parser = markdownParser; export const serializer = markdownSerializer;