UNPKG

myst-to-html

Version:
144 lines (143 loc) 5.14 kB
import { unified } from 'unified'; import rehypeParse from 'rehype-parse'; import rehypeRemark from 'rehype-remark'; import { all } from 'hast-util-to-mdast'; import { visit } from 'unist-util-visit'; import { select, selectAll } from 'unist-util-select'; import { findAfter } from 'unist-util-find-after'; import { remove } from 'unist-util-remove'; import { liftChildren, normalizeLabel, AdmonitionKind, admonitionKindToTitle } from 'myst-common'; import { enumerateTargets, resolveReferences } from './state.js'; const defaultOptions = { addAdmonitionHeaders: true, addContainerCaptionNumbers: true, disableHeadingEnumeration: false, disableContainerEnumeration: false, disableEquationEnumeration: false, }; const defaultHtmlToMdastOptions = { keepBreaks: true, htmlHandlers: { table(h, node) { return h(node, 'table', all(h, node)); }, th(h, node) { const result = h(node, 'tableCell', all(h, node)); result.header = true; return result; }, _brKeep(h, node) { return h(node, '_break'); }, }, }; // Visit all admonitions and add headers if necessary export function addAdmonitionHeaders(tree) { visit(tree, 'admonition', (node) => { if (!node.kind || node.kind === AdmonitionKind.admonition) return; node.children = [ { type: 'admonitionTitle', children: [{ type: 'text', value: admonitionKindToTitle(node.kind) }], }, ...(node.children ?? []), ]; }); } // Visit all containers and add captions export function addContainerCaptionNumbers(tree, state) { selectAll('container', tree) .filter((container) => container.enumerator !== false) .forEach((container) => { const enumerator = state.getTarget(container.identifier)?.node.enumerator; const para = select('caption > paragraph', container); if (enumerator && para) { para.children = [ { type: 'captionNumber', kind: container.kind, value: enumerator }, ...(para?.children ?? []), ]; } }); } /** * Propagate target identifier/value to subsequent node * * Note: While this propagation happens regardless of the * subsequent node type, references are only resolved to * the TargetKind nodes enumerated in state.ts. For example: * * (paragraph-target)= * Just a normal paragraph * * will add identifier/label to paragraph node, but the node * will still not be targetable. */ export function propagateTargets(tree) { visit(tree, 'mystTarget', (node, index) => { const nextNode = findAfter(tree, index); const normalized = normalizeLabel(node.label); if (nextNode && normalized) { nextNode.identifier = normalized.identifier; nextNode.label = normalized.label; } }); remove(tree, 'mystTarget'); } /** * Ensure caption content is nested in a paragraph. * * This function is idempotent! */ export function ensureCaptionIsParagraph(tree) { visit(tree, 'caption', (node) => { if (node.children && node.children[0].type !== 'paragraph') { node.children = [{ type: 'paragraph', children: node.children }]; } }); } export function convertHtmlToMdast(tree, opts) { const handlers = { ...defaultHtmlToMdastOptions.htmlHandlers, ...opts?.htmlHandlers }; const otherOptions = { ...defaultHtmlToMdastOptions, ...opts }; const htmlNodes = selectAll('html', tree); htmlNodes.forEach((node) => { const hast = unified() .use(rehypeParse, { fragment: true }) .parse(node.value); // hast-util-to-mdast removes breaks if they are the first/last children // and nests standalone breaks in paragraphs. // However, since HTML nodes may just be fragments in the middle of markdown text, // there is an option to `keepBreaks` which will simply convert `<br />` // tags to `break` nodes, without the special hast-util-to-mdast behavior. if (otherOptions.keepBreaks) { selectAll('[tagName=br]', hast).forEach((n) => { n.tagName = '_brKeep'; }); } const mdast = unified().use(rehypeRemark, { handlers }).runSync(hast); node.type = 'htmlParsed'; node.children = mdast.children; visit(node, (n) => delete n.position); }); liftChildren(tree, 'htmlParsed'); selectAll('_break', tree).forEach((n) => { n.type = 'break'; }); return tree; } export const transform = (state, o) => (tree) => { const opts = { ...defaultOptions, ...o, }; ensureCaptionIsParagraph(tree); propagateTargets(tree); enumerateTargets(state, tree, opts); resolveReferences(state, tree); liftChildren(tree, 'mystDirective'); liftChildren(tree, 'mystRole'); if (opts.addAdmonitionHeaders) addAdmonitionHeaders(tree); if (opts.addContainerCaptionNumbers) addContainerCaptionNumbers(tree, state); };