UNPKG

myst-parser

Version:
135 lines (134 loc) 4.8 kB
import { u } from 'unist-builder'; const UNHIDDEN_TOKENS = new Set([ 'parsed_directive_open', 'parsed_directive_close', 'directive_arg_open', 'directive_arg_close', 'directive_body_open', 'directive_body_close', 'myst_option_open', 'myst_option_close', 'parsed_role_open', 'parsed_role_close', 'role_body_open', 'role_body_close', ]); export function withoutTrailingNewline(str) { return str[str.length - 1] == '\n' ? str.slice(0, str.length - 1) : str; } /** MarkdownParseState tracks the context of a running token stream. * * Loosely based on prosemirror-markdown */ export class MarkdownParseState { src; stack; handlers; constructor(src, handlers) { this.src = src; this.stack = [u('root', [])]; this.handlers = getTokenHandlers(handlers); } top() { return this.stack[this.stack.length - 1]; } addNode(node) { const top = this.top(); if (this.stack.length && node && 'children' in top) top.children?.push(node); return node; } addText(text, token, type = 'text', attrs) { const top = this.top(); const value = text; if (!value || !this.stack.length || !type || !('children' in top)) return; const last = top.children?.[top.children.length - 1]; if (type === 'text' && last?.type === type) { // The last node is also text, merge it with a space last.value += `${value}`; return last; } const node = { type: type, ...attrs, value }; top.children?.push(node); this.addPositionsToNode(node, token); return node; } openNode(type, token, attrs, isLeaf = false) { const node = { type, ...attrs }; this.addPositionsToNode(node, token); if (!isLeaf) node.children = []; this.stack.push(node); } closeNode() { const node = this.stack.pop(); return this.addNode(node); } parseTokens(tokens) { tokens?.forEach((token, index) => { if (token.hidden && !UNHIDDEN_TOKENS.has(token.type)) return; const handler = this.handlers[token.type]; if (!handler) { throw new Error(`Token type ${token.type} not supported by tokensToMyst parser`); } handler(this, token, tokens, index); }); } _lastPosition; addPositionsToNode(node, token) { const col = token.col ?? [0, 0]; if (token.map) { node.position = { start: { line: token.map[0] + 1, column: col[0] + 1 }, end: { line: token.map[1], column: col[1] + 1 }, }; } else if (this._lastPosition) { node.position = { start: { line: this._lastPosition.start.line, column: col[0] + 1 }, end: { line: this._lastPosition.start.line, column: col[1] + 1 }, }; } if (node.position) { this._lastPosition = node.position; } } } function getAttrs(state, spec, token, tokens, index) { const attrs = spec.getAttrs?.(token, tokens, index, state) || spec.attrs || {}; if ('type' in attrs) throw new Error('You can not have "type" as attrs.'); if ('children' in attrs) throw new Error('You can not have "children" as attrs.'); return attrs; } function noCloseToken(spec, type) { return spec.noCloseToken || type == 'code_inline' || type == 'code_block' || type == 'fence'; } function getTokenHandlers(specHandlers) { const handlers = {}; Object.entries(specHandlers).forEach(([type, spec]) => { const nodeType = spec.type; if (noCloseToken(spec, type)) { handlers[type] = (state, tok, tokens, i) => { if (spec.isText) { state.addText(withoutTrailingNewline(tok.content), tok, spec.type, getAttrs(state, spec, tok, tokens, i)); return; } state.openNode(nodeType, tok, getAttrs(state, spec, tok, tokens, i), spec.isLeaf); state.addText(withoutTrailingNewline(tok.content), tok); state.closeNode(); }; } else { handlers[type + '_open'] = (state, tok, tokens, i) => state.openNode(nodeType, tok, getAttrs(state, spec, tok, tokens, i)); handlers[type + '_close'] = (state) => state.closeNode(); } }); handlers.text = (state, tok) => state.addText(tok.content, tok); handlers.inline = (state, tok) => state.parseTokens(tok.children); handlers.softbreak = handlers.softbreak || ((state, tok) => state.addText('\n', tok)); return handlers; }