UNPKG

@curvenote/schema

Version:

Schema and markdown parser for @curvenote/editor

311 lines 9.59 kB
import { Mark } from 'prosemirror-model'; import { select, selectAll, remove } from 'mystjs'; import { getSchema } from '../../schemas'; import { markNames, nodeNames } from '../../types'; export var ignoreNames; (function (ignoreNames) { ignoreNames["directive"] = "directive"; ignoreNames["role"] = "role"; })(ignoreNames || (ignoreNames = {})); function maybeMerge(a, b) { if (!a || !b) return undefined; if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks)) return undefined; // a.withText(a.text + b.text); return undefined; } /** MarkdownParseState tracks the context of a running token stream. * * Loosly based on prosemirror-markdown */ export class MarkdownParseState { constructor(schema, handlers) { this.schema = schema; this.stack = [{ type: schema.topNodeType, attrs: {}, content: [] }]; this.marks = []; this.handlers = handlers; } top() { return this.stack[this.stack.length - 1]; } addNode(type, attrs, content) { const top = this.top(); const node = type.createAndFill(attrs, content, this.marks); if (this.stack.length && node && 'content' in top) top.content.push(node); return node; } addText(text) { var _a, _b; const top = this.top(); const value = text; if (!value || !this.stack.length || !('content' in top)) return; const last = (_a = top.content) === null || _a === void 0 ? void 0 : _a[top.content.length - 1]; const node = this.schema.text(text, this.marks); const merged = maybeMerge(last, node); (_b = top.content) === null || _b === void 0 ? void 0 : _b.push(merged || node); } // : (Mark) // Adds the given mark to the set of active marks. openMark(mark) { this.marks = mark.addToSet(this.marks); } // : (Mark) // Removes the given mark from the set of active marks. closeMark(mark) { this.marks = mark.removeFromSet(this.marks); } openNode(type, attrs) { this.stack.push({ type, attrs, content: [] }); } closeNode() { const node = this.stack.pop(); if (!node) return undefined; return this.addNode(node.type, node.attrs, node.content); } parseTokens(tokens) { tokens === null || tokens === void 0 ? void 0 : tokens.forEach((token) => { if (token.hidden) return; const handler = this.handlers[token.type]; if (!handler) { console.warn(`Token type \`${token.type}\` not supported by myst parser`); return; } const { name, children } = handler(token, tokens); if (name in ignoreNames && children && typeof children !== 'string') { this.parseTokens(children); } else if (name === nodeNames.text) { if (typeof children === 'string') { this.addText(children); } else { throw new Error(`Invalid children of type ${typeof children} for node ${name}`); } } else if (name in nodeNames) { const nodeType = this.schema.nodes[name]; const nodeAttrs = nodeType.spec.attrsFromMyst(token, tokens); this.openNode(nodeType, nodeAttrs); if (typeof children === 'string') { this.addText(children); } else { this.parseTokens(children); } this.closeNode(); } else if (name in markNames) { const markType = this.schema.marks[name]; const mark = markType.create(markType.spec.attrsFromMyst(token, tokens)); this.openMark(mark); if (typeof children === 'string') { this.addText(children); } else { this.parseTokens(children); } this.closeMark(mark); } }); } } const handlers = { text: (token) => ({ name: nodeNames.text, children: token.value, }), abbreviation: (token) => ({ name: markNames.abbr, children: token.children, }), emphasis: (token) => ({ name: markNames.em, children: token.children, }), inlineCode: (token) => ({ name: markNames.code, children: token.value, }), link: (token) => ({ name: markNames.link, children: token.children, }), strong: (token) => ({ name: markNames.strong, children: token.children, }), subscript: (token) => ({ name: markNames.subscript, children: token.children, }), superscript: (token) => ({ name: markNames.superscript, children: token.children, }), paragraph: (token) => ({ name: nodeNames.paragraph, children: token.children, }), thematicBreak: () => ({ name: nodeNames.horizontal_rule, }), break: () => ({ name: nodeNames.hard_break, }), heading: (token) => ({ name: nodeNames.heading, children: token.children, }), blockquote: (token) => ({ name: nodeNames.blockquote, children: token.children, }), code: (token) => ({ name: nodeNames.code_block, children: token.value, }), list: (token) => ({ name: token.ordered ? nodeNames.ordered_list : nodeNames.bullet_list, children: token.children, }), listItem: (token) => { var _a; let { children } = token; if (((_a = token.children) === null || _a === void 0 ? void 0 : _a.length) === 1 && token.children[0].type === 'text') { children = [{ type: 'paragraph', children }]; } return { name: nodeNames.list_item, children, }; }, inlineMath: (token) => ({ name: nodeNames.math, children: token.value, }), math: (token) => ({ name: nodeNames.equation, children: token.value, }), container: (token) => ({ name: nodeNames.figure, children: token.children, }), caption: (token) => ({ name: nodeNames.figcaption, children: token.children, }), image: () => ({ name: nodeNames.image, }), table: (token) => ({ name: nodeNames.table, children: token.children, }), tableRow: (token) => ({ name: nodeNames.table_row, children: token.children, }), tableCell: (token) => ({ name: token.header ? nodeNames.table_header : nodeNames.table_cell, children: token.children, }), admonition: (token) => ({ name: nodeNames.callout, children: token.children, }), mystDirective: (token) => ({ name: ignoreNames.directive, children: token.children, }), mystRole: (token) => ({ name: ignoreNames.role, children: token.children, }), inlineFootnote: (token) => ({ name: nodeNames.footnote, children: token.children, }), reactiveButton: () => ({ name: nodeNames.button, }), reactiveDisplay: () => ({ name: nodeNames.display, }), reactiveDynamic: () => ({ name: nodeNames.dynamic, }), reactiveRange: () => ({ name: nodeNames.range, }), reactiveSwitch: () => ({ name: nodeNames.switch, }), reactiveVariable: () => ({ name: nodeNames.variable, }), delete: (token) => ({ name: markNames.strikethrough, children: token.children, }), mention: () => ({ name: nodeNames.mention, }), iframe: () => ({ name: nodeNames.iframe, }), citeGroup: (token) => ({ name: nodeNames.cite_group, children: token.children, }), cite: () => ({ name: nodeNames.cite, }), margin: (token) => ({ name: nodeNames.aside, children: token.children, }), time: () => ({ name: nodeNames.time, }), linkBlock: () => ({ name: nodeNames.link_block, }), }; export function fromMdast(tree, useSchema) { const schema = getSchema(useSchema); const state = new MarkdownParseState(schema, handlers); // Change the structure of footnotes: const footnoteDefinitions = selectAll('footnoteDefinition', tree); const footnotes = {}; footnoteDefinitions.forEach((node) => { if (node.identifier) footnotes[node.identifier] = node; }); // The any here is a typescript bug, that causes delays ... remove(tree, 'footnoteDefinition'); const footnoteReferences = selectAll('footnoteReference', tree); footnoteReferences.forEach((node) => { const footnote = footnotes[node.identifier]; if (!footnote) { node.type = 'skip'; return; } const ourFootnote = node; ourFootnote.type = 'inlineFootnote'; // Note: our footnotes only support content from a single paragraph ourFootnote.children = select('paragraph', footnote).children; }); remove(tree, 'skip'); state.parseTokens(tree.children); let doc; do { doc = state.closeNode(); } while (state.stack.length); return doc; } //# sourceMappingURL=index.js.map