@vivliostyle/vfm
Version:
Custom Markdown syntax specialized in book authoring.
184 lines (183 loc) • 6.09 kB
JavaScript
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;
}
});
};