UNPKG

myst-parser

Version:
629 lines (628 loc) 21.3 kB
import he from 'he'; import { liftChildren, normalizeLabel, setTextAsChild } from 'myst-common'; import { visit } from 'unist-util-visit'; import { remove } from 'unist-util-remove'; import { selectAll } from 'unist-util-select'; import { u } from 'unist-builder'; import { MarkdownParseState, withoutTrailingNewline } from './fromMarkdown.js'; export function computeAmsmathTightness(src, map) { var _a; const lines = src.split('\n'); const tightBefore = typeof (map === null || map === void 0 ? void 0 : map[0]) === 'number' && map[0] > 0 ? lines[map[0] - 1].trim() !== '' : false; // Note: The end line might be different/wrong for AMS math. If that is updated, remove the `+1` that shifts the index const last = typeof (map === null || map === void 0 ? void 0 : map[1]) === 'number' ? (map === null || map === void 0 ? void 0 : map[1]) + 1 : undefined; const tightAfter = typeof last === 'number' && last < lines.length ? ((_a = lines[last]) === null || _a === void 0 ? void 0 : _a.trim()) !== '' : false; const tight = tightBefore && tightAfter ? true : tightBefore ? 'before' : tightAfter ? 'after' : false; return tight; } const NUMBERED_CLASS = /^numbered$/; const ALIGN_CLASS = /(?:(?:align-)|^)(left|right|center)/; function getClassName(token, exclude) { var _a, _b, _c; const allClasses = new Set([ // Grab the trimmed classes from the token ...((_a = token.attrGet('class')) !== null && _a !== void 0 ? _a : '') .split(' ') .map((c) => c.trim()) .filter((c) => c), // Add any from the meta information (these are often repeated) ...((_c = (_b = token.meta) === null || _b === void 0 ? void 0 : _b.class) !== null && _c !== void 0 ? _c : []), ]); const className = [...allClasses].join(' '); if (!className) return undefined; return (className .split(' ') .map((c) => c.trim()) .filter((c) => { if (!c) return false; if (!exclude) return true; return !exclude.reduce((doExclude, test) => doExclude || !!c.match(test), false); }) .join(' ') || undefined); } function hasClassName(token, matcher) { const className = getClassName(token); if (!className) return false; const matches = className .split(' ') .map((c) => c.match(matcher)) .filter((c) => c); if (matches.length === 0) return false; return matches[0]; } function getLang(t) { return he.decode(t.info).trim().split(' ')[0].replace('\\', ''); } function getColAlign(t) { var _a; if ((_a = t.attrs) === null || _a === void 0 ? void 0 : _a.length) { for (const attrPair of t.attrs) { if (attrPair[0] === 'style') { const match = attrPair[1].match(/text-align:(left|right|center)/); if (match) { return match[1]; } } } } } const defaultMdast = { heading: { type: 'heading', getAttrs(token) { var _a; return { depth: Number(token.tag[1]), enumerated: (_a = token.meta) === null || _a === void 0 ? void 0 : _a.enumerated, }; }, }, s: { type: 'delete' }, hr: { type: 'thematicBreak', noCloseToken: true, isLeaf: true, }, paragraph: { type: 'paragraph', }, blockquote: { type: 'blockquote', }, ordered_list: { type: 'list', getAttrs(token, tokens, index) { var _a, _b; const info = (_a = tokens[index + 1]) === null || _a === void 0 ? void 0 : _a.info; const start = Number((_b = tokens[index + 1]) === null || _b === void 0 ? void 0 : _b.info); return { ordered: true, start: isNaN(start) || !info ? 1 : start, spread: false, }; }, }, bullet_list: { type: 'list', attrs: { ordered: false, spread: false, }, }, list_item: { type: 'listItem', getAttrs(t) { const attrs = { spread: true }; if (t.attrGet('class') === 'task-list-item') { // This is a temporary property used to clean up the AST in a subsequent pass attrs.__taskList = true; } return attrs; }, }, em: { type: 'emphasis', }, strong: { type: 'strong', }, colon_fence: { type: 'code', isLeaf: true, noCloseToken: true, getAttrs(t) { return { lang: getLang(t), value: withoutTrailingNewline(t.content) }; }, }, fence: { type: 'code', isLeaf: true, getAttrs(t) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const name = ((_a = t.meta) === null || _a === void 0 ? void 0 : _a.name) || undefined; const showLineNumbers = !!(((_b = t.meta) === null || _b === void 0 ? void 0 : _b.linenos) || ((_c = t.meta) === null || _c === void 0 ? void 0 : _c.linenos) === null || // Weird docutils implementation ((_d = t.meta) === null || _d === void 0 ? void 0 : _d['number-lines']) || ( // If lineno-start is present, linenos option is also automatically activated // https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-option-code-block-lineno-start (_e = t.meta) === null || _e === void 0 ? void 0 : _e['lineno-start'])); const lineno = (_g = (_f = t.meta) === null || _f === void 0 ? void 0 : _f['lineno-start']) !== null && _g !== void 0 ? _g : (_h = t.meta) === null || _h === void 0 ? void 0 : _h['number-lines']; const startingLineNumber = lineno && lineno !== 1 && !isNaN(Number(lineno)) ? Number(lineno) : undefined; const emphasizeLines = ((_j = t.meta) === null || _j === void 0 ? void 0 : _j['emphasize-lines']) ? (_k = t.meta) === null || _k === void 0 ? void 0 : _k['emphasize-lines'].split(',').map((n) => Number(n.trim())).filter((n) => !isNaN(n) && n > 0) : undefined; return { lang: getLang(t), ...normalizeLabel(name), class: getClassName(t), showLineNumbers: showLineNumbers || undefined, // Only add to MDAST if true startingLineNumber: showLineNumbers ? startingLineNumber : undefined, // Only if showing line numbers! emphasizeLines, value: withoutTrailingNewline(t.content), }; }, }, code_block: { type: 'code', isLeaf: true, getAttrs(t) { return { lang: getLang(t), value: withoutTrailingNewline(t.content) }; }, }, code_inline: { type: 'inlineCode', noCloseToken: true, isText: true, }, hardbreak: { type: 'break', noCloseToken: true, isLeaf: true, }, link: { type: 'link', getAttrs(token) { var _a; return { url: token.attrGet('href'), title: (_a = token.attrGet('title')) !== null && _a !== void 0 ? _a : undefined, }; }, }, image: { type: 'image', noCloseToken: true, isLeaf: true, getAttrs(token) { var _a; const alt = token.attrGet('alt') || ((_a = token.children) === null || _a === void 0 ? void 0 : _a.reduce((i, t) => i + (t === null || t === void 0 ? void 0 : t.content), '')); const alignMatch = hasClassName(token, ALIGN_CLASS); const align = alignMatch ? alignMatch[1] : undefined; return { url: token.attrGet('src'), alt: alt || undefined, title: token.attrGet('title') || undefined, class: getClassName(token, [ALIGN_CLASS]), width: token.attrGet('width') || undefined, height: token.attrGet('height') || undefined, align, }; }, }, dl: { type: 'definitionList', }, dt: { type: 'definitionTerm', }, dd: { type: 'definitionDescription', }, table: { type: 'table', getAttrs(token) { var _a, _b, _c; const name = ((_a = token.meta) === null || _a === void 0 ? void 0 : _a.name) || undefined; return { kind: undefined, ...normalizeLabel(name), enumerated: (_b = token.meta) === null || _b === void 0 ? void 0 : _b.enumerated, class: getClassName(token, [NUMBERED_CLASS, ALIGN_CLASS]), align: ((_c = token.meta) === null || _c === void 0 ? void 0 : _c.align) || undefined, }; }, }, // table_caption: { // type: 'caption', // }, thead: { type: '_lift', }, tbody: { type: '_lift', }, tr: { type: 'tableRow', }, th: { type: 'tableCell', getAttrs(t) { return { header: true, align: getColAlign(t) || undefined }; }, }, td: { type: 'tableCell', getAttrs(t) { return { align: getColAlign(t) || undefined }; }, }, math_inline: { type: 'inlineMath', noCloseToken: true, isText: true, }, math_inline_double: { type: 'math', noCloseToken: true, isLeaf: true, getAttrs(t) { var _a; return { value: t.content.trim(), enumerated: (_a = t.meta) === null || _a === void 0 ? void 0 : _a.enumerated, }; }, }, math_block: { type: 'math', noCloseToken: true, isLeaf: true, getAttrs(t) { var _a; const name = t.info || undefined; return { value: t.content.trim(), ...normalizeLabel(name), enumerated: (_a = t.meta) === null || _a === void 0 ? void 0 : _a.enumerated, }; }, }, math_block_label: { type: 'math', noCloseToken: true, isLeaf: true, getAttrs(t) { var _a; const name = t.info || undefined; return { value: t.content.trim(), ...normalizeLabel(name), enumerated: (_a = t.meta) === null || _a === void 0 ? void 0 : _a.enumerated, }; }, }, amsmath: { type: 'math', noCloseToken: true, isLeaf: true, getAttrs(t, tokens, index, state) { var _a; const tight = computeAmsmathTightness(state.src, t.map); const attrs = { value: t.content.trim(), enumerated: (_a = t.meta) === null || _a === void 0 ? void 0 : _a.enumerated, }; if (tight) attrs.tight = tight; return attrs; }, }, footnote_ref: { type: 'footnoteReference', noCloseToken: true, isLeaf: true, getAttrs(t) { var _a; const { identifier, label } = normalizeLabel((_a = t === null || t === void 0 ? void 0 : t.meta) === null || _a === void 0 ? void 0 : _a.label) || {}; return { identifier, label }; }, }, footnote_anchor: { type: '_remove', noCloseToken: true, }, footnote_block: { // The footnote block is a view concern, not AST // Lift footnotes out of the tree type: '_lift', }, footnote: { type: 'footnoteDefinition', getAttrs(t) { var _a; const { identifier, label } = normalizeLabel((_a = t === null || t === void 0 ? void 0 : t.meta) === null || _a === void 0 ? void 0 : _a.label) || {}; return { identifier, label }; }, }, cite: { type: 'cite', noCloseToken: true, isLeaf: true, getAttrs(t) { var _a, _b, _c; const { identifier, label } = normalizeLabel((_a = t === null || t === void 0 ? void 0 : t.meta) === null || _a === void 0 ? void 0 : _a.label) || {}; return { identifier, label, kind: t.meta.kind, partial: t.meta.partial, // author or year // TODO: can use the parsed version in the future prefix: (_b = t.meta.prefix) === null || _b === void 0 ? void 0 : _b[0].content, suffix: (_c = t.meta.suffix) === null || _c === void 0 ? void 0 : _c[0].content, }; }, }, cite_group: { type: 'citeGroup', getAttrs(t) { return { kind: t.meta.kind }; }, }, parsed_directive: { type: 'mystDirective', getAttrs(t) { var _a, _b, _c; return { name: t.info, args: (_a = t.meta) === null || _a === void 0 ? void 0 : _a.arg, options: (_b = t.meta) === null || _b === void 0 ? void 0 : _b.options, value: t.content || undefined, tight: ((_c = t.meta) === null || _c === void 0 ? void 0 : _c.tight) || undefined, processed: false, }; }, }, directive_arg: { type: 'mystDirectiveArg', getAttrs(t) { return { value: t.content, }; }, }, directive_body: { type: 'mystDirectiveBody', getAttrs(t) { return { value: t.content, }; }, }, directive_error: { type: 'mystDirectiveError', noCloseToken: true, getAttrs(t) { var _a; return { message: (_a = t.meta) === null || _a === void 0 ? void 0 : _a.error_message, }; }, }, myst_option: { type: 'mystOption', getAttrs(t) { return { name: t.info, location: t.meta.location, value: t.meta.value, }; }, }, parsed_role: { type: 'mystRole', getAttrs(t) { return { name: t.info, value: t.content, processed: false, }; }, }, role_body: { type: 'mystRoleBody', getAttrs(t) { return { value: t.content, }; }, }, role_error: { type: 'mystRoleError', noCloseToken: true, isLeaf: true, getAttrs(t) { return { value: t.content, }; }, }, myst_target: { type: 'mystTarget', noCloseToken: true, isLeaf: true, getAttrs(t) { return { label: t.content, }; }, }, html_inline: { type: 'html', noCloseToken: true, isText: true, }, html_block: { type: 'html', noCloseToken: true, isText: true, }, myst_block_break: { type: 'blockBreak', noCloseToken: true, isLeaf: true, getAttrs(t) { return { meta: t.content || undefined, }; }, }, myst_line_comment: { type: 'comment', noCloseToken: true, isLeaf: true, getAttrs(t) { return { value: t.content.trim() || undefined, }; }, }, }; function hoistSingleImagesOutofParagraphs(tree) { // Hoist up all paragraphs with a single image visit(tree, 'paragraph', (node) => { var _a, _b; if (!(((_a = node.children) === null || _a === void 0 ? void 0 : _a.length) === 1 && ((_b = node.children) === null || _b === void 0 ? void 0 : _b[0].type) === 'image')) return; const child = node.children[0]; Object.keys(node).forEach((k) => { delete node[k]; }); Object.assign(node, child); }); } function nestSingleImagesIntoParagraphs(tree) { tree.children = tree.children.map((node) => { if (node.type === 'image') { return { type: 'paragraph', children: [node] }; } else { return node; } }); } const defaultOptions = { handlers: defaultMdast, hoistSingleImagesOutofParagraphs: true, nestBlocks: true, }; export function tokensToMyst(src, tokens, options = defaultOptions) { var _a; const opts = { ...defaultOptions, ...options, handlers: { ...defaultOptions.handlers, ...options === null || options === void 0 ? void 0 : options.handlers }, }; const state = new MarkdownParseState(src, opts.handlers); state.parseTokens(tokens); let tree; do { tree = state.closeNode(); } while (state.stack.length); // Remove all redundant nodes marked for removal remove(tree, '_remove'); // Lift up all nodes that are named "lift" liftChildren(tree, '_lift'); // Remove unnecessary admonition titles from AST // These are up to the serializer to put in // visit(tree, 'admonition', (node: Admonition) => { // const { kind, children } = node; // if (!kind || !children || kind === AdmonitionKind.admonition) return; // const expectedTitle = admonitionKindToTitle(kind); // const titleNode = children[0]; // if (titleNode.type === 'admonitionTitle' && titleNode.children?.[0]?.value === expectedTitle) // node.children = children.slice(1); // }); // Change all task lists to checked selectAll('listItem[__taskList=true]', tree).forEach((node) => { var _a, _b, _c, _d; delete node.__taskList; const html = (_a = node.children) === null || _a === void 0 ? void 0 : _a.slice(0, 1); if (html && html[0].type === 'html' && ((_b = html[0].value) === null || _b === void 0 ? void 0 : _b.includes('task-list-item-checkbox'))) { node.checked = html[0].value.includes('checked=""'); // Remove the inline html (_c = node.children) === null || _c === void 0 ? void 0 : _c.splice(0, 1); if (((_d = node.children) === null || _d === void 0 ? void 0 : _d[0].type) === 'text') { // Ensure that the parsing is the same as other text lists, that strip leading whitespace node.children[0].value = node.children[0].value.replace(/^\s/, ''); } } }); // Move crossReference text value to children visit(tree, 'crossReference', (node) => { delete node.children; if (node.value) { setTextAsChild(node, node.value); delete node.value; } }); // Nest block content inside of a block if (opts.nestBlocks) { const newTree = u('root', []); let lastBlock; const pushBlock = () => { if (!lastBlock) return; newTree.children.push(lastBlock); }; (_a = tree.children) === null || _a === void 0 ? void 0 : _a.forEach((node) => { var _a, _b; if (node.type === 'blockBreak') { pushBlock(); lastBlock = node; node.type = 'block'; node.children = (_a = node.children) !== null && _a !== void 0 ? _a : []; return; } const stack = lastBlock ? lastBlock : newTree; (_b = stack.children) === null || _b === void 0 ? void 0 : _b.push(node); }); pushBlock(); tree = newTree; } // ensureCaptionIsParagraph(tree); // Replace "table node with caption" with "figure node with table and caption" // TODO: Clean up when we update to typescript > 4.6.2 and we have access to // parent in visitor function. // i.e. visit(tree, 'table', (node, index parent) => {...}) // https://github.com/microsoft/TypeScript/issues/46900 // selectAll('table', tree).forEach((node: GenericNode) => { // const captionChildren = node.children?.filter((n: GenericNode) => n.type === 'caption'); // if (captionChildren?.length) { // const tableChildren = node.children?.filter((n: GenericNode) => n.type !== 'caption'); // const newTableNode: GenericNode = { // type: 'table', // align: node.align, // children: tableChildren, // }; // node.type = 'container'; // node.kind = 'table'; // node.children = [...captionChildren, newTableNode]; // delete node.align; // } else { // delete node.enumerated; // } // }); if (opts.hoistSingleImagesOutofParagraphs) { hoistSingleImagesOutofParagraphs(tree); } else { nestSingleImagesIntoParagraphs(tree); } return tree; }