UNPKG

@vivliostyle/vfm

Version:

Custom Markdown syntax specialized in book authoring.

184 lines (183 loc) 6.09 kB
import { select } from 'hast-util-select'; import { findAndReplace } from 'mdast-util-find-and-replace'; import { u } from 'unist-builder'; import { visit } from 'unist-util-visit'; /** * Inline math format, e.g. `$...$`. * - OK: `$x = y$`, `$x = \$y$` * - NG: `$$x = y$`, `$x = y$$`, `$ x = y$`, `$x = y $`, `$x = y$7` */ const REGEXP_INLINE = /\$([^$\s].*?(?<=[^\\$\s]|[^\\](?:\\\\)+))\$(?!\$|\d)/gs; /** Display math format, e.g. `$$...$$`. */ const REGEXP_DISPLAY = /\$\$([^$].*?(?<=[^$]))\$\$(?!\$)/gs; /** Type of inline math in Markdown AST. */ const TYPE_INLINE = 'inlineMath'; /** Type of display math in Markdown AST. */ const TYPE_DISPLAY = 'displayMath'; /** URL of MathJax v2 supported by Vivliostyle. */ const MATH_URL = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.9/MathJax.js?config=TeX-MML-AM_CHTML'; /** * Create tokenizers for remark-parse. * @returns Tokenizers. */ const createTokenizers = () => { const tokenizerInlineMath = function (eat, value, silent) { if (!value.startsWith('$') || value.startsWith('$ ') || value.startsWith('$$')) { return; } const match = new RegExp(REGEXP_INLINE).exec(value); if (!match) { return; } if (silent) { return true; } const [eaten, valueText] = match; const now = eat.now(); now.column += 1; now.offset += 1; return eat(eaten)({ type: TYPE_INLINE, children: [], data: { hName: TYPE_INLINE, value: valueText }, }); }; tokenizerInlineMath.notInLink = true; tokenizerInlineMath.locator = function (value, fromIndex) { return value.indexOf('$', fromIndex); }; const tokenizerDisplayMath = function (eat, value, silent) { if (!value.startsWith('$$') || value.startsWith('$$$')) { return; } const match = new RegExp(REGEXP_DISPLAY).exec(value); if (!match) { return; } if (silent) { return true; } const [eaten, valueText] = match; const now = eat.now(); now.column += 1; now.offset += 1; return eat(eaten)({ type: TYPE_DISPLAY, children: [], data: { hName: TYPE_DISPLAY, value: valueText }, }); }; tokenizerDisplayMath.notInLink = true; tokenizerDisplayMath.locator = function (value, fromIndex) { return value.indexOf('$$', fromIndex); }; return { tokenizerInlineMath, tokenizerDisplayMath }; }; /** * Process Markdown AST. * @returns Transformer or undefined (less than remark 13). */ export const mdast = function () { // For less than remark 13 with exclusive other markdown syntax if (this.Parser && this.Parser.prototype.inlineTokenizers && this.Parser.prototype.inlineMethods) { const { inlineTokenizers, inlineMethods } = this.Parser.prototype; const tokenizers = createTokenizers(); inlineTokenizers[TYPE_INLINE] = tokenizers.tokenizerInlineMath; inlineTokenizers[TYPE_DISPLAY] = tokenizers.tokenizerDisplayMath; inlineMethods.splice(inlineMethods.indexOf('text'), 0, TYPE_INLINE); inlineMethods.splice(inlineMethods.indexOf('text'), 0, TYPE_DISPLAY); return; } return (tree) => { findAndReplace(tree, REGEXP_INLINE, (_, valueText) => { return { // NOTE: Although `type: "inlineMath"` is not a permitted type literal value within mdast, // it causes no issues because it is handled by the handlerInlineMath function via remark-rehype. // See also ../revive-rehype.ts. type: TYPE_INLINE, data: { hName: TYPE_INLINE, value: valueText, }, children: [], }; }); findAndReplace(tree, REGEXP_DISPLAY, (_, valueText) => { return { // NOTE: See the comment for inlineMath. type: TYPE_DISPLAY, data: { hName: TYPE_DISPLAY, value: valueText, }, children: [], }; }); }; }; /** * Handle inline math to Hypertext AST. * @param h Hypertext AST formatter. * @param node Node. * @returns Hypertext AST. */ export const handlerInlineMath = (h, node) => { if (!node.data) { node.data = {}; } return h({ type: 'element', }, 'span', { class: 'math inline', 'data-math-typeset': 'true', }, [u('text', `\\(${node.data.value}\\)`)]); }; /** * Handle display math to Hypertext AST. * @param h Hypertext AST formatter. * @param node Node. * @returns Hypertext AST. */ export const handlerDisplayMath = (h, node) => { if (!node.data) { node.data = {}; } return h({ type: 'element', }, 'span', { class: 'math display', 'data-math-typeset': 'true', }, [u('text', `$$${node.data.value}$$`)]); }; /** * Process math related Hypertext AST. * Set the `<script>` to load MathJax and `<body>` attribute that enable math typesetting. * * This function does the work even if it finds a `<math>` that it does not treat as a VFM. Therefore, call it only if the VFM option is `math: true`. */ export const hast = () => (tree) => { if (!(select('[data-math-typeset="true"]', tree) || select('math', tree))) { return; } visit(tree, 'element', (node) => { switch (node.tagName) { case 'head': node.children.push({ type: 'element', tagName: 'script', properties: { async: true, src: MATH_URL, }, children: [], }); node.children.push({ type: 'text', value: '\n' }); break; } }); };