mystjs
Version:
Markdown parser for MyST markdown in JavaScript
159 lines • 6.05 kB
JavaScript
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 { map } from 'unist-util-map';
import { AdmonitionKind } from './types';
import { admonitionKindToTitle, normalizeLabel } from './utils';
import { enumerateTargets, resolveReferences } from './state';
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) => {
var _a;
if (!node.kind || node.kind === AdmonitionKind.admonition)
return;
node.children = [
{
type: 'admonitionTitle',
children: [{ type: 'text', value: admonitionKindToTitle(node.kind) }],
},
...((_a = node.children) !== null && _a !== void 0 ? _a : []),
];
});
}
// Visit all containers and add captions
export function addContainerCaptionNumbers(tree, state) {
selectAll('container', tree)
.filter((container) => container.enumerator !== false)
.forEach((container) => {
var _a, _b;
const enumerator = (_a = state.getTarget(container.identifier)) === null || _a === void 0 ? void 0 : _a.node.enumerator;
const para = select('caption > paragraph', container);
if (enumerator && para) {
para.children = [
{ type: 'captionNumber', kind: container.kind, value: enumerator },
...((_b = para === null || para === void 0 ? void 0 : para.children) !== null && _b !== void 0 ? _b : []),
];
}
});
}
/** @deprecated use myst-common */
export function liftChildren(tree, nodeType) {
map(tree, (node) => {
var _a, _b;
const children = (_b = (_a = node.children) === null || _a === void 0 ? void 0 : _a.map((child) => {
if (child.type === nodeType && child.children)
return child.children;
return child;
})) === null || _b === void 0 ? void 0 : _b.flat();
if (children !== undefined)
node.children = children;
return node;
});
}
/**
* 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 = Object.assign(Object.assign({}, defaultHtmlToMdastOptions.htmlHandlers), opts === null || opts === void 0 ? void 0 : opts.htmlHandlers);
const otherOptions = Object.assign(Object.assign({}, 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 = Object.assign(Object.assign({}, 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);
};
//# sourceMappingURL=transforms.js.map