@curvenote/schema
Version:
Schema and markdown parser for @curvenote/editor
311 lines • 9.59 kB
JavaScript
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