diff --git a/package.json b/package.json index b337678..0409100 100644 --- a/package.json +++ b/package.json @@ -28,16 +28,13 @@ "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "remark-cli": "^11.0.0", - "remark-html": "^15.0.0", "remark-parse": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "remark-rehype": "^10.0.0", "remark-stringify": "^11.0.0", - "to-vfile": "^8.0.0", "type-coverage": "^2.0.0", "typescript": "^5.0.0", "unified": "^11.0.0", - "unist-builder": "^4.0.0", "unist-util-remove-position": "^5.0.0", "xo": "^0.56.0" }, diff --git a/packages/rehype-katex/index.js b/packages/rehype-katex/index.js index def3bbd..7f0fe78 100644 --- a/packages/rehype-katex/index.js +++ b/packages/rehype-katex/index.js @@ -1,33 +1,50 @@ /** + * @typedef {import('hast').ElementContent} ElementContent * @typedef {import('hast').Root} Root + * * @typedef {import('katex').KatexOptions} Options + * + * @typedef {import('vfile').VFile} VFile */ +import {fromHtmlIsomorphic} from 'hast-util-from-html-isomorphic' +import {toText} from 'hast-util-to-text' import katex from 'katex' import {visit} from 'unist-util-visit' -import {toText} from 'hast-util-to-text' -import {fromHtmlIsomorphic} from 'hast-util-from-html-isomorphic' - -const assign = Object.assign -const source = 'rehype-katex' +/** @type {Readonly} */ +const emptyOptions = {} +/** @type {ReadonlyArray} */ +const emptyClasses = [] /** * Plugin to transform `` and `
` * with KaTeX. * - * @type {import('unified').Plugin<[Options?]|void[], Root>} + * @param {Readonly | null | undefined} [options] + * Configuration (optional). + * @returns + * Transform. */ export default function rehypeKatex(options) { - const settings = options || {} + const settings = options || emptyOptions const throwOnError = settings.throwOnError || false - return (tree, file) => { - visit(tree, 'element', (element) => { - const classes = - element.properties && Array.isArray(element.properties.className) - ? element.properties.className - : [] + /** + * Transform. + * + * @param {Root} tree + * Tree. + * @param {VFile} file + * File. + * @returns {undefined} + * Nothing. + */ + return function (tree, file) { + visit(tree, 'element', function (element) { + const classes = Array.isArray(element.properties.className) + ? element.properties.className + : emptyClasses const inline = classes.includes('math-inline') const displayMode = classes.includes('math-display') @@ -41,29 +58,30 @@ export default function rehypeKatex(options) { let result try { - result = katex.renderToString( - value, - assign({}, settings, {displayMode, throwOnError: true}) - ) - } catch (error_) { - const error = /** @type {Error} */ (error_) + result = katex.renderToString(value, { + ...settings, + displayMode, + throwOnError: true + }) + } catch (error) { + const exception = /** @type {Error} */ (error) const fn = throwOnError ? 'fail' : 'message' - const origin = [source, error.name.toLowerCase()].join(':') + const origin = ['rehype-katex', exception.name.toLowerCase()].join(':') - file[fn](error.message, element.position, origin) + file[fn](exception.message, element.position, origin) // KaTeX can handle `ParseError` itself, but not others. // Generate similar markup if this is an other error. // See: . - if (error.name !== 'ParseError') { + if (exception.name !== 'ParseError') { element.children = [ { type: 'element', tagName: 'span', properties: { className: ['katex-error'], - title: String(error), - style: 'color:' + (settings.errorColor || '#cc0000') + style: 'color:' + (settings.errorColor || '#cc0000'), + title: String(error) }, children: [{type: 'text', value}] } @@ -71,20 +89,18 @@ export default function rehypeKatex(options) { return } - result = katex.renderToString( - value, - assign({}, settings, { - displayMode, - throwOnError: false, - strict: 'ignore' - }) - ) + result = katex.renderToString(value, { + ...settings, + displayMode, + strict: 'ignore', + throwOnError: false + }) } const root = fromHtmlIsomorphic(result, {fragment: true}) - // To do: cast content. - // @ts-expect-error: assume no `doctypes` in KaTeX result. - element.children = root.children + // Cast because there will not be `doctypes` in KaTeX result. + const content = /** @type {Array} */ (root.children) + element.children = content }) } } diff --git a/packages/rehype-katex/package.json b/packages/rehype-katex/package.json index d6326f9..d030eb7 100644 --- a/packages/rehype-katex/package.json +++ b/packages/rehype-katex/package.json @@ -43,7 +43,8 @@ "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, "scripts": { "test-api": "node --conditions development test.js", diff --git a/packages/rehype-katex/test.js b/packages/rehype-katex/test.js index f2833c4..9c5f40b 100644 --- a/packages/rehype-katex/test.js +++ b/packages/rehype-katex/test.js @@ -1,83 +1,93 @@ import assert from 'node:assert/strict' import test from 'node:test' import katex from 'katex' -import {unified} from 'unified' -import remarkParse from 'remark-parse' -import remarkRehype from 'remark-rehype' +import rehypeKatex from 'rehype-katex' import rehypeParse from 'rehype-parse' import rehypeStringify from 'rehype-stringify' -import remarkMath from '../remark-math/index.js' -import rehypeKatex from './index.js' +import remarkMath from 'remark-math' +import remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' +import {unified} from 'unified' test('rehype-katex', async function (t) { + await t.test('should expose the public api', async function () { + assert.deepEqual(Object.keys(await import('rehype-katex')).sort(), [ + 'default' + ]) + }) + await t.test('should transform math with katex', async function () { assert.deepEqual( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeKatex) - .use(rehypeStringify) - .processSync( - [ - '

Inline math \\alpha.

', - '

Block math:

', - '
\\gamma
' - ].join('\n') - ) - .toString(), - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - .processSync( - [ - '

Inline math ' + - katex.renderToString('\\alpha') + - '.

', - '

Block math:

', - '
' + - katex.renderToString('\\gamma', {displayMode: true}) + - '
' - ].join('\n') - ) - .toString() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeKatex) + .use(rehypeStringify) + .process( + [ + '

Inline math \\alpha.

', + '

Block math:

', + '
\\gamma
' + ].join('\n') + ) + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process( + [ + '

Inline math ' + + katex.renderToString('\\alpha') + + '.

', + '

Block math:

', + '
' + + katex.renderToString('\\gamma', {displayMode: true}) + + '
' + ].join('\n') + ) + ) ) }) await t.test('should integrate with `remark-math`', async function () { assert.deepEqual( - unified() - .use(remarkParse) - .use(remarkMath) - // @ts-expect-error: to do: remove when `remark-rehype` is released. - .use(remarkRehype) - .use(rehypeKatex) - .use(rehypeStringify) - .processSync( - [ - 'Inline math $\\alpha$.', - '', - 'Block math:', - '', - '$$', - '\\gamma', - '$$' - ].join('\n') - ) - .toString(), - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - .processSync( - [ - '

Inline math ' + - katex.renderToString('\\alpha') + - '.

', - '

Block math:

', - '
' +
-              katex.renderToString('\\gamma', {displayMode: true}) +
-              '
' - ].join('\n') - ) - .toString() + String( + await unified() + .use(remarkParse) + .use(remarkMath) + // @ts-expect-error: to do: remove when `remark-rehype` is released. + .use(remarkRehype) + .use(rehypeKatex) + .use(rehypeStringify) + .process( + [ + 'Inline math $\\alpha$.', + '', + 'Block math:', + '', + '$$', + '\\gamma', + '$$' + ].join('\n') + ) + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process( + [ + '

Inline math ' + + katex.renderToString('\\alpha') + + '.

', + '

Block math:

', + '
' +
+                katex.renderToString('\\gamma', {displayMode: true}) +
+                '
' + ].join('\n') + ) + ) ) }) @@ -85,23 +95,25 @@ test('rehype-katex', async function (t) { 'should transform `.math-inline.math-display` math with `displayMode: true`', async function () { assert.deepEqual( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeKatex) - .use(rehypeStringify) - .processSync( - '

Double math \\alpha.

' - ) - .toString(), - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - .processSync( - '

Double math ' + - katex.renderToString('\\alpha', {displayMode: true}) + - '.

' - ) - .toString() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeKatex) + .use(rehypeStringify) + .process( + '

Double math \\alpha.

' + ) + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process( + '

Double math ' + + katex.renderToString('\\alpha', {displayMode: true}) + + '.

' + ) + ) ) } ) @@ -110,79 +122,80 @@ test('rehype-katex', async function (t) { const macros = {'\\RR': '\\mathbb{R}'} assert.deepEqual( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeKatex, {macros}) - .use(rehypeStringify) - .processSync('\\RR') - .toString(), - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - .processSync( - '' + - katex.renderToString('\\RR', {macros}) + - '' - ) - .toString() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeKatex, {macros}) + .use(rehypeStringify) + .process('\\RR') + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process( + '' + + katex.renderToString('\\RR', {macros}) + + '' + ) + ) ) }) await t.test('should support `errorColor`', async function () { assert.deepEqual( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeKatex, {errorColor: 'orange'}) - .use(rehypeStringify) - .processSync('\\alpa') - .toString(), - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - .processSync( - '' + - katex.renderToString('\\alpa', { - throwOnError: false, - errorColor: 'orange' - }) + - '' - ) - .toString() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeKatex, {errorColor: 'orange'}) + .use(rehypeStringify) + .process('\\alpa') + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process( + '' + + katex.renderToString('\\alpa', { + errorColor: 'orange', + throwOnError: false + }) + + '' + ) + ) ) }) await t.test('should create a message for errors', async function () { - assert.deepEqual( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeKatex) - .use(rehypeStringify) - .processSync( - '

Lorem

\n

\\alpa

' - ) - .messages.map(String), - [ - '2:4-2:42: KaTeX parse error: Undefined control sequence: \\alpa at position 1: \\̲a̲l̲p̲a̲' - ] - ) + const file = await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeKatex) + .use(rehypeStringify) + .process('

Lorem

\n

\\alpa

') + + assert.deepEqual(file.messages.map(String), [ + '2:4-2:42: KaTeX parse error: Undefined control sequence: \\alpa at position 1: \\̲a̲l̲p̲a̲' + ]) }) await t.test( 'should throw an error if `throwOnError: true`', async function () { try { - unified() + await unified() .use(rehypeParse, {fragment: true}) .use(rehypeKatex, {throwOnError: true}) .use(rehypeStringify) - .processSync( + .process( '

Lorem

\n

\\alpa

' ) - } catch (error_) { - const error = /** @type {Error} */ (error_) - assert.equal( - error.message, - 'KaTeX parse error: Undefined control sequence: \\alpa at position 1: \\̲a̲l̲p̲a̲' + /* c8 ignore next 2 -- some c8 bug. */ + assert.fail() + } catch (error) { + assert.match( + String(error), + /KaTeX parse error: Undefined control sequence: \\alpa at position 1: \\̲a̲l̲p̲a̲/ ) } } @@ -190,64 +203,70 @@ test('rehype-katex', async function (t) { await t.test('should support `strict: ignore`', async function () { assert.deepEqual( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeKatex, {errorColor: 'orange', strict: 'ignore'}) - .use(rehypeStringify) - .processSync('ê&') - .toString(), - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - .processSync( - 'ê&' - ) - .toString() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeKatex, {errorColor: 'orange', strict: 'ignore'}) + .use(rehypeStringify) + .process('ê&') + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process( + 'ê&' + ) + ) ) }) await t.test('should support comments', async function () { assert.deepEqual( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeKatex, {errorColor: 'orange', strict: 'ignore'}) - .use(rehypeStringify) - .processSync( - '
\\begin{split}\n f(-2) &= \\sqrt{-2+4} \\\\\n &= x % Test Comment\n\\end{split}
' - ) - .toString(), - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - .processSync( - '
' + - katex.renderToString( - '\\begin{split}\n f(-2) &= \\sqrt{-2+4} \\\\\n &= x % Test Comment\n\\end{split}', - {displayMode: true} - ) + - '
' - ) - .toString() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeKatex, {errorColor: 'orange', strict: 'ignore'}) + .use(rehypeStringify) + .process( + '
\\begin{split}\n f(-2) &= \\sqrt{-2+4} \\\\\n &= x % Test Comment\n\\end{split}
' + ) + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process( + '
' + + katex.renderToString( + '\\begin{split}\n f(-2) &= \\sqrt{-2+4} \\\\\n &= x % Test Comment\n\\end{split}', + {displayMode: true} + ) + + '
' + ) + ) ) }) await t.test('should not crash on non-parse errors', async function () { assert.deepEqual( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeKatex) - .use(rehypeStringify) - .processSync( - '\\begin{split}\n\\end{{split}}\n' - ) - .toString(), - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeStringify) - .processSync( - '\\begin{split}\n\\end{{split}}\n' - ) - .toString() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeKatex) + .use(rehypeStringify) + .process( + '\\begin{split}\n\\end{{split}}\n' + ) + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process( + '\\begin{split}\n\\end{{split}}\n' + ) + ) ) }) }) diff --git a/packages/rehype-mathjax/browser.js b/packages/rehype-mathjax/browser.js index 5655c4f..6481d87 100644 --- a/packages/rehype-mathjax/browser.js +++ b/packages/rehype-mathjax/browser.js @@ -1,11 +1,15 @@ /** * @typedef {import('./lib/create-plugin.js').Options} Options + * @typedef {import('./lib/create-plugin.js').InputTexOptions} InputTexOptions */ import {createPlugin} from './lib/create-plugin.js' -const rehypeMathJaxBrowser = createPlugin((options) => { - const tex = options.tex || {} +/** @type {Readonly} */ +const emptyTexOptions = {} + +const rehypeMathJaxBrowser = createPlugin(function (options) { + const tex = options.tex || emptyTexOptions const display = tex.displayMath || [['\\[', '\\]']] const inline = tex.inlineMath || [['\\(', '\\)']] diff --git a/packages/rehype-mathjax/chtml.js b/packages/rehype-mathjax/chtml.js index 9ea9653..75946ad 100644 --- a/packages/rehype-mathjax/chtml.js +++ b/packages/rehype-mathjax/chtml.js @@ -3,10 +3,10 @@ */ import {CHTML} from 'mathjax-full/js/output/chtml.js' -import {createRenderer} from './lib/create-renderer.js' import {createPlugin} from './lib/create-plugin.js' +import {createRenderer} from './lib/create-renderer.js' -const rehypeMathJaxCHtml = createPlugin((options) => { +const rehypeMathJaxCHtml = createPlugin(function (options) { if (!options.chtml || !options.chtml.fontURL) { throw new Error( 'rehype-mathjax: missing `fontURL` in options, which must be set to a URL to reach MathJaX fonts' diff --git a/packages/rehype-mathjax/lib/create-plugin.js b/packages/rehype-mathjax/lib/create-plugin.js index 0ac2e1a..e631849 100644 --- a/packages/rehype-mathjax/lib/create-plugin.js +++ b/packages/rehype-mathjax/lib/create-plugin.js @@ -1,98 +1,201 @@ /** - * @typedef {import('hast').Root} Root * @typedef {import('hast').Element} Element + * @typedef {import('hast').Root} Root + */ + +/** + * @callback CreateRenderer + * Create a renderer. + * @param {Readonly} options + * Configuration. + * @returns {Renderer} + * Rendeder. + * + * @callback FormatError + * Format an error. + * @param {any} jax + * MathJax object. + * @param {any} error + * Error. + * @returns {string} + * Formatted error. + * + * @typedef InputTexOptions + * Configuration for input tex math. + * + * @property {string | null | undefined} [baseURL] + * URL for use with links to tags, when there is a `` tag in effect + * (optional). + * @property {RegExp | null | undefined} [digits] + * Pattern for recognizing numbers (optional). + * @property {ReadonlyArray | null | undefined} [displayMath] + * Start/end delimiter pairs for display math (optional). + * @property {FormatError | null | undefined} [formatError] + * Function called when TeX syntax errors occur (optional). + * @property {ReadonlyArray | null | undefined} [inlineMath] + * Start/end delimiter pairs for in-line math (optional). + * @property {number | null | undefined} [maxBuffer] + * Max size for the internal TeX string (5K) (optional). + * @property {number | null | undefined} [maxMacros] + * Max number of macro substitutions per expression (optional). + * @property {ReadonlyArray | null | undefined} [packages] + * Extensions to use (optional). + * @property {boolean | null | undefined} [processEnvironments] + * Process `\begin{xxx}...\end{xxx}` outside math mode (optional). + * @property {boolean | null | undefined} [processEscapes] + * Use `\$` to produce a literal dollar sign (optional). + * @property {boolean | null | undefined} [processRefs] + * Process `\ref{...}` outside of math mode (optional). + * @property {string | null | undefined} [tagIndent] + * Amount to indent tags (optional). + * @property {'left' | 'right' | null | undefined} [tagSide] + * Side for `\tag` macros (optional). + * @property {'all' | 'ams' | 'none' | null | undefined} [tags] + * Optional. + * @property {boolean | null | undefined} [useLabelIds] + * Use label name rather than tag for ids (optional). * * @typedef {[string, string]} MathNotation * Markers to use for math. * See: * - * @typedef OutputSvgOptions - * - * @property {number} [scale] - * @property {number} [minScale] - * @property {boolean} [mtextInheritFont] - * @property {boolean} [merrorInheritFont] - * @property {boolean} [mathmlSpacing] - * @property {Record} [skipAttributes] - * @property {number} [exFactor] - * @property {'left'|'center'|'right'} [displayAlign] - * @property {string} [displayIndent] - * @property {'local'|'global'} [fontCache] - * @property {string|null} [localID] - * @property {boolean} [internalSpeechTitles] - * @property {number} [titleID] + * @typedef Options + * Configuration. + * @property {Readonly | null | undefined} [chtml] + * Configuration for the output, when CHTML (optional). + * @property {Readonly | null | undefined} [svg] + * Configuration for the output, when SVG (optional). + * @property {Readonly | null | undefined} [tex] + * Configuration for the input TeX (optional). * * @typedef OutputCHtmlOptions + * Configuration for output CHTML. * - * @property {number} [scale] - * @property {number} [minScale] - * @property {boolean} [matchFontHeight] - * @property {boolean} [mtextInheritFont] - * @property {boolean} [merrorInheritFont] - * @property {boolean} [mathmlSpacing] - * @property {Record} [skipAttributes] - * @property {number} [exFactor] - * @property {'left'|'center'|'right'} [displayAlign] - * @property {string} [displayIndent] + * @property {boolean | null | undefined} [adaptiveCSS] + * `true` means only produce CSS that is used in the processed equations (optional). + * @property {'center' | 'left' | 'right' | null | undefined} [displayAlign] + * Default for indentalign when set to `'auto'` (optional). + * @property {string | null | undefined} [displayIndent] + * Default for indentshift when set to `'auto'` (optional). + * @property {number | null | undefined} [exFactor] + * Default size of ex in em units (optional). * @property {string} fontURL - * @property {boolean} [adaptiveCSS] + * The URL where the fonts are found (**required**). + * @property {boolean | null | undefined} [matchFontHeight] + * `true` to match ex-height of surrounding font (optional). + * @property {boolean | null | undefined} [mathmlSpacing] + * `true` for MathML spacing rules, false for TeX rules (optional). + * @property {boolean | null | undefined} [merrorInheritFont] + * `true` to make merror text use surrounding font (optional). + * @property {number | null | undefined} [minScale] + * Smallest scaling factor to use (optional). + * @property {boolean | null | undefined} [mtextInheritFont] + * `true` to make mtext elements use surrounding font (optional). + * @property {number | null | undefined} [scale] + * Global scaling factor for all expressions (optional). + * @property {Readonly> | null | undefined} [skipAttributes] + * RFDa and other attributes NOT to copy to the output (optional). * - * @typedef InputTexOptions - * - * @property {string[]} [packages] - * @property {MathNotation[]} [inlineMath] - * @property {MathNotation[]} [displayMath] - * @property {boolean} [processEscapes] - * @property {boolean} [processEnvironments] - * @property {boolean} [processRefs] - * @property {RegExp} [digits] - * @property {'none'|'ams'|'all'} [tags] - * @property {'left'|'right'} [tagSide] - * @property {string} [tagIndent] - * @property {boolean} [useLabelIds] - * @property {string} [multlineWidth] - * @property {number} [maxMacros] - * @property {number} [maxBuffer] - * @property {string} [baseURL] - * @property {(jax: any, error: any) => string} [formatError] + * @typedef OutputSvgOptions + * Configuration for output SVG. + * + * @property {'center' | 'left' | 'right' | null | undefined} [displayAlign] + * Default for indentalign when set to `'auto'` (optional). + * @property {string | null | undefined} [displayIndent] + * Default for indentshift when set to `'auto'` (optional). + * @property {number | null | undefined} [exFactor] + * Default size of ex in em units (optional). + * @property {'global' | 'local' | 'none' | null | undefined} [fontCache] + * Or `'global'` or `'none'` (optional). + * @property {boolean | null | undefined} [internalSpeechTitles] + * Insert `` tags with speech content (optional). + * @property {string | null | undefined} [localID] + * ID to use for local font cache, for single equation processing (optional). + * @property {boolean | null | undefined} [mathmlSpacing] + * `true` for MathML spacing rules, `false` for TeX rules (optional). + * @property {boolean | null | undefined} [merrorInheritFont] + * `true` to make merror text use surrounding font (optional). + * @property {number | null | undefined} [minScale] + * Smallest scaling factor to use (optional). + * @property {boolean | null | undefined} [mtextInheritFont] + * `true` to make mtext elements use surrounding font (optional). + * @property {number | null | undefined} [scale] + * Global scaling factor for all expressions (optional). + * @property {Readonly<Record<string, boolean>> | null | undefined} [skipAttributes] + * RFDa and other attributes *not* to copy to the output (optional). + * @property {number | null | undefined} [titleID] + * Initial ID number to use for `aria-labeledby` titles (optional). * - * @typedef Options + * @callback Render + * Render a math node. + * @param {Element} element + * Math node. + * @param {Readonly<RenderOptions>} options * Configuration. - * @property {InputTexOptions} [tex] - * Configuration for the input TeX. - * @property {OutputCHtmlOptions} [chtml] - * Configuration for the output (when CHTML). - * @property {OutputSvgOptions} [svg] - * Configuration for the output (when SVG). + * @returns {undefined} + * Nothing. + * + * @typedef RenderOptions + * Configuration. + * @property {boolean} display + * Whether to render display math. * * @typedef Renderer - * @property {(node: Element, options: {display: boolean}) => void} render - * @property {() => Element} [styleSheet] + * Renderer. + * @property {Render} render + * Render a math node. + * @property {StyleSheet | null | undefined} [styleSheet] + * Render a style sheet (optional). * - * @callback CreateRenderer - * @param {Options} options - * @returns {Renderer} + * @callback StyleSheet + * Render a style sheet. + * @returns {Element} + * Style sheet. */ -import {visit, SKIP} from 'unist-util-visit' +import {SKIP, visit} from 'unist-util-visit' + +/** @type {Readonly<Options>} */ +const emptyOptions = {} +/** @type {ReadonlyArray<unknown>} */ +const emptyClasses = [] /** + * Create a plugin. + * * @param {CreateRenderer} createRenderer + * Create a renderer. + * @returns + * Plugin. */ export function createPlugin(createRenderer) { - /** @type {import('unified').Plugin<[Options?]|void[], Root>} */ - return (options = {}) => - (tree) => { - const renderer = createRenderer(options) + /** + * Plugin. + * + * @param {Readonly<Options> | null | undefined} [options] + * Configuration (optional). + * @returns + * Transform. + */ + return function (options) { + /** + * Transform. + * + * @param {Root} tree + * Tree. + * @returns {undefined} + * Nothing. + */ + return function (tree) { + const renderer = createRenderer(options || emptyOptions) let found = false - /** @type {Root|Element} */ + /** @type {Element | Root} */ let context = tree - visit(tree, 'element', (node) => { - const classes = - node.properties && Array.isArray(node.properties.className) - ? node.properties.className - : [] + visit(tree, 'element', function (node) { + const classes = Array.isArray(node.properties.className) + ? node.properties.className + : emptyClasses const inline = classes.includes('math-inline') const display = classes.includes('math-display') @@ -114,4 +217,5 @@ export function createPlugin(createRenderer) { context.children.push(renderer.styleSheet()) } } + } } diff --git a/packages/rehype-mathjax/lib/create-renderer.js b/packages/rehype-mathjax/lib/create-renderer.js index af90027..cb38b5c 100644 --- a/packages/rehype-mathjax/lib/create-renderer.js +++ b/packages/rehype-mathjax/lib/create-renderer.js @@ -1,18 +1,17 @@ /** * @typedef {import('hast').Element} Element - * @typedef {import('mathjax-full/js/core/OutputJax.js').OutputJax<HTMLElement, Text, Document>} OutputJax * @typedef {import('mathjax-full/js/core/MathDocument.js').MathDocument<HTMLElement, Text, Document>} MathDocument - * @typedef {import('mathjax-full/js/input/tex.js').TeX<HTMLElement, Text, Document>} TeX_ + * @typedef {import('mathjax-full/js/core/OutputJax.js').OutputJax<HTMLElement, Text, Document>} OutputJax * @typedef {import('./create-plugin.js').Options} Options * @typedef {import('./create-plugin.js').Renderer} Renderer */ -import {mathjax} from 'mathjax-full/js/mathjax.js' +import {fromDom} from 'hast-util-from-dom' +import {toText} from 'hast-util-to-text' import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js' import {TeX} from 'mathjax-full/js/input/tex.js' import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js' -import {fromDom} from 'hast-util-from-dom' -import {toText} from 'hast-util-to-text' +import {mathjax} from 'mathjax-full/js/mathjax.js' import {createAdaptor} from './create-adaptor.js' const adaptor = createAdaptor() @@ -31,24 +30,30 @@ const adaptor = createAdaptor() RegisterHTMLHandler(adaptor) /** + * Create a renderer. + * * @param {Options} options + * Configuration. * @param {OutputJax} output + * Output jax. * @returns {Renderer} + * Rendeder. */ export function createRenderer(options, output) { - const input = new TeX(Object.assign({packages: AllPackages}, options.tex)) + const input = new TeX({packages: AllPackages, ...options.tex}) /** @type {MathDocument} */ const doc = mathjax.document('', {InputJax: input, OutputJax: output}) return { render(node, options) { - const domNode = fromDom( - // @ts-expect-error: assume mathml nodes can be handled by - // `hast-util-from-dom`. - doc.convert(toText(node, {whitespace: 'pre'}), options) + const mathText = toText(node, {whitespace: 'pre'}) + // Cast as this practically results in `HTMLElement`. + const domNode = /** @type {HTMLElement} */ ( + doc.convert(mathText, options) ) - // @ts-expect-error: `fromDom` returns an element for a given element. - node.children = [domNode] + // Cast as `HTMLElement` results in an `Element`. + const hastNode = /** @type {Element} */ (fromDom(domNode)) + node.children = [hastNode] }, styleSheet() { const value = adaptor.textContent(output.styleSheet(doc)) diff --git a/packages/rehype-mathjax/svg.js b/packages/rehype-mathjax/svg.js index dd16ab8..27e6fb2 100644 --- a/packages/rehype-mathjax/svg.js +++ b/packages/rehype-mathjax/svg.js @@ -3,11 +3,12 @@ */ import {SVG} from 'mathjax-full/js/output/svg.js' -import {createRenderer} from './lib/create-renderer.js' import {createPlugin} from './lib/create-plugin.js' +import {createRenderer} from './lib/create-renderer.js' -const rehypeMathJaxSvg = createPlugin((options) => - createRenderer(options, new SVG(options.svg)) -) +const rehypeMathJaxSvg = createPlugin(function (options) { + // `mathjax-types` do not allow `null`. + return createRenderer(options, new SVG(options.svg || undefined)) +}) export default rehypeMathJaxSvg diff --git a/packages/rehype-mathjax/test/index.js b/packages/rehype-mathjax/test/index.js index a016119..eeccf24 100644 --- a/packages/rehype-mathjax/test/index.js +++ b/packages/rehype-mathjax/test/index.js @@ -1,81 +1,128 @@ import assert from 'node:assert/strict' +import fs from 'node:fs/promises' import test from 'node:test' -import path from 'node:path' -import {readSync} from 'to-vfile' -import {unified} from 'unified' -import remarkParse from 'remark-parse' -import remarkRehype from 'remark-rehype' +import rehypeMathJaxBrowser from 'rehype-mathjax/browser.js' +import rehypeMathJaxChtml from 'rehype-mathjax/chtml.js' +import rehypeMathJaxSvg from 'rehype-mathjax/svg.js' import rehypeParse from 'rehype-parse' import rehypeStringify from 'rehype-stringify' -import remarkMath from '../../remark-math/index.js' -import rehypeMathJaxSvg from '../svg.js' -import rehypeMathJaxChtml from '../chtml.js' -import rehypeMathJaxBrowser from '../browser.js' +import remarkMath from 'remark-math' +import remarkParse from 'remark-parse' +import remarkRehype from 'remark-rehype' +import {unified} from 'unified' -const fixtures = path.join('test', 'fixture') +const base = new URL('fixture/', import.meta.url) test('rehype-mathjax', async function (t) { + await t.test( + 'should expose the public api for `rehype-mathjax`', + async function () { + assert.deepEqual(Object.keys(await import('rehype-mathjax')).sort(), [ + 'default' + ]) + } + ) + + await t.test( + 'should expose the public api for `rehype-mathjax/browser.js`', + async function () { + assert.deepEqual( + Object.keys(await import('rehype-mathjax/browser.js')).sort(), + ['default'] + ) + } + ) + + await t.test( + 'should expose the public api for `rehype-mathjax/chtml.js`', + async function () { + assert.deepEqual( + Object.keys(await import('rehype-mathjax/chtml.js')).sort(), + ['default'] + ) + } + ) + + await t.test( + 'should expose the public api for `rehype-mathjax/svg.js`', + async function () { + assert.deepEqual( + Object.keys(await import('rehype-mathjax/svg.js')).sort(), + ['default'] + ) + } + ) + await t.test('should render SVG', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxSvg) - .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'small.html'})) - .toString(), - String(readSync({dirname: fixtures, basename: 'small-svg.html'})).trim() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeMathJaxSvg) + .use(rehypeStringify) + .process(await fs.readFile(new URL('small.html', base))) + ), + String(await fs.readFile(new URL('small-svg.html', base))).trim() ) }) await t.test('should crash for CHTML w/o `fontURL`', async function () { - assert.throws(function () { - unified() + try { + await unified() .use(rehypeParse, {fragment: true}) .use(rehypeMathJaxChtml) .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'small.html'})) - .toString() - }, /rehype-mathjax: missing `fontURL` in options/) + .process( + await fs.readFile(new URL('equation-numbering-2-svg.html', base)) + ) + assert.fail() + } catch (error) { + assert.match( + String(error), + /rehype-mathjax: missing `fontURL` in options/ + ) + } }) await t.test('should render CHTML', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxChtml, {chtml: {fontURL: 'place/to/fonts'}}) - .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'small.html'})) - .toString(), - String(readSync({dirname: fixtures, basename: 'small-chtml.html'})).trim() + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeMathJaxChtml, {chtml: {fontURL: 'place/to/fonts'}}) + .use(rehypeStringify) + .process(await fs.readFile(new URL('small.html', base))) + ), + String(await fs.readFile(new URL('small-chtml.html', base))).trim() ) }) await t.test('should render browser', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxBrowser) - .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'small.html'})) - .toString(), - String(readSync({dirname: fixtures, basename: 'small-browser.html'})) + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeMathJaxBrowser) + .use(rehypeStringify) + .process(await fs.readFile(new URL('small.html', base))) + ), + String(await fs.readFile(new URL('small-browser.html', base))) ) }) await t.test('should integrate with `remark-math`', async function () { assert.equal( - unified() - .use(remarkParse) - .use(remarkMath) - // @ts-expect-error: to do: remove when `remark-rehype` is released. - .use(remarkRehype) - .use(rehypeMathJaxSvg) - .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'markdown.md'})) - .toString(), String( - readSync({dirname: fixtures, basename: 'markdown-svg.html'}) - ).trim() + await unified() + .use(remarkParse) + .use(remarkMath) + // @ts-expect-error: to do: remove when `remark-rehype` is released. + .use(remarkRehype) + .use(rehypeMathJaxSvg) + .use(rehypeStringify) + .process(await fs.readFile(new URL('markdown.md', base))) + ), + String(await fs.readFile(new URL('markdown-svg.html', base))).trim() ) }) @@ -83,42 +130,41 @@ test('rehype-mathjax', async function (t) { 'should transform `.math-inline.math-display`', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxSvg) - .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'double.html'})) - .toString(), String( - readSync({dirname: fixtures, basename: 'double-svg.html'}) - ).trim() + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeMathJaxSvg) + .use(rehypeStringify) + .process(await fs.readFile(new URL('double.html', base))) + ), + String(await fs.readFile(new URL('double-svg.html', base))).trim() ) } ) await t.test('should transform documents without math', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxSvg) - .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'none.html'})) - .toString(), - String(readSync({dirname: fixtures, basename: 'none-svg.html'})) + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeMathJaxSvg) + .use(rehypeStringify) + .process(await fs.readFile(new URL('none.html', base))) + ), + String(await fs.readFile(new URL('none-svg.html', base))) ) }) await t.test('should transform complete documents', async function () { assert.equal( - unified() - .use(rehypeParse) - .use(rehypeMathJaxSvg) - .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'document.html'})) - .toString(), String( - readSync({dirname: fixtures, basename: 'document-svg.html'}) - ).trim() + await unified() + .use(rehypeParse) + .use(rehypeMathJaxSvg) + .use(rehypeStringify) + .process(await fs.readFile(new URL('document.html', base))) + ), + String(await fs.readFile(new URL('document-svg.html', base))).trim() ) }) @@ -126,22 +172,20 @@ test('rehype-mathjax', async function (t) { 'should support custom `inlineMath` and `displayMath` delimiters for browser', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxBrowser, { - tex: { - inlineMath: [['$', '$']], - displayMath: [['$$', '$$']] - } - }) - .use(rehypeStringify) - .processSync(readSync({dirname: fixtures, basename: 'small.html'})) - .toString(), String( - readSync({ - dirname: fixtures, - basename: 'small-browser-delimiters.html' - }) + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeMathJaxBrowser, { + tex: { + displayMath: [['$$', '$$']], + inlineMath: [['$', '$']] + } + }) + .use(rehypeStringify) + .process(await fs.readFile(new URL('small.html', base))) + ), + String( + await fs.readFile(new URL('small-browser-delimiters.html', base)) ) ) } @@ -149,22 +193,17 @@ test('rehype-mathjax', async function (t) { await t.test('should render SVG with equation numbers', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxSvg, {tex: {tags: 'ams'}}) - .use(rehypeStringify) - .processSync( - readSync({ - dirname: fixtures, - basename: 'equation-numbering-1.html' - }) - ) - .toString(), String( - readSync({ - dirname: fixtures, - basename: 'equation-numbering-1-svg.html' - }) + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeMathJaxSvg, {tex: {tags: 'ams'}}) + .use(rehypeStringify) + .process( + await fs.readFile(new URL('equation-numbering-1.html', base)) + ) + ), + String( + await fs.readFile(new URL('equation-numbering-1-svg.html', base)) ).trim() ) }) @@ -173,22 +212,17 @@ test('rehype-mathjax', async function (t) { 'should render SVG with reference to an undefined equation', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxSvg, {tex: {tags: 'ams'}}) - .use(rehypeStringify) - .processSync( - readSync({ - dirname: fixtures, - basename: 'equation-numbering-2.html' - }) - ) - .toString(), String( - readSync({ - dirname: fixtures, - basename: 'equation-numbering-2-svg.html' - }) + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeMathJaxSvg, {tex: {tags: 'ams'}}) + .use(rehypeStringify) + .process( + await fs.readFile(new URL('equation-numbering-2.html', base)) + ) + ), + String( + await fs.readFile(new URL('equation-numbering-2-svg.html', base)) ).trim() ) } @@ -196,63 +230,21 @@ test('rehype-mathjax', async function (t) { await t.test('should render CHTML with equation numbers', async function () { assert.equal( - unified() - .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxChtml, { - chtml: {fontURL: 'place/to/fonts'}, - tex: {tags: 'ams'} - }) - .use(rehypeStringify) - .processSync( - readSync({ - dirname: fixtures, - basename: 'equation-numbering-1.html' - }) - ) - .toString(), String( - readSync({ - dirname: fixtures, - basename: 'equation-numbering-1-chtml.html' - }) - ).trim() - ) - }) - - await t.test('should render SVG with equation numbers', async function () { - assert.equal( - (function () { - const processor = unified() + await unified() .use(rehypeParse, {fragment: true}) - .use(rehypeMathJaxSvg, {tex: {tags: 'ams'}}) - .use(rehypeStringify) - return ['equation-numbering-1.html', 'equation-numbering-2.html'] - .map(function (basename) { - return processor - .processSync( - readSync({ - dirname: fixtures, - basename - }) - ) - .toString() + .use(rehypeMathJaxChtml, { + chtml: {fontURL: 'place/to/fonts'}, + tex: {tags: 'ams'} }) - .join('') - })(), - [ - String( - readSync({ - dirname: fixtures, - basename: 'equation-numbering-1-svg.html' - }) - ).trim(), - String( - readSync({ - dirname: fixtures, - basename: 'equation-numbering-2-svg.html' - }) - ).trim() - ].join('') + .use(rehypeStringify) + .process( + await fs.readFile(new URL('equation-numbering-1.html', base)) + ) + ), + String( + await fs.readFile(new URL('equation-numbering-1-chtml.html', base)) + ).trim() ) }) }) diff --git a/packages/remark-math/index.js b/packages/remark-math/index.js index 71a7ae2..a2cc9a8 100644 --- a/packages/remark-math/index.js +++ b/packages/remark-math/index.js @@ -1,38 +1,42 @@ +/// <reference types="mdast-util-math" /> +/// <reference types="remark-parse" /> +/// <reference types="remark-stringify" /> + /** * @typedef {import('mdast').Root} Root * @typedef {import('mdast-util-math').ToOptions} Options - * - * @typedef {import('mdast-util-math')} DoNotTouchAsThisImportIncludesMathInTree + * @typedef {import('unified').Processor<Root>} Processor */ -import {math} from 'micromark-extension-math' import {mathFromMarkdown, mathToMarkdown} from 'mdast-util-math' +import {math} from 'micromark-extension-math' + +/** @type {Readonly<Options>} */ +const emptyOptions = {} /** * Plugin to support math. * - * @this {import('unified').Processor} - * @type {import('unified').Plugin<[Options?] | void[], Root, Root>} + * @param {Readonly<Options> | null | undefined} [options] + * Configuration (optional). + * @returns {undefined} + * Nothing. */ -export default function remarkMath(options = {}) { - const data = this.data() - - add('micromarkExtensions', math(options)) - add('fromMarkdownExtensions', mathFromMarkdown()) - add('toMarkdownExtensions', mathToMarkdown(options)) +export default function remarkMath(options) { + // @ts-expect-error: TS is wrong about `this`. + // eslint-disable-next-line unicorn/no-this-assignment + const self = /** @type {Processor} */ (this) + const settings = options || emptyOptions + const data = self.data() - /** - * @param {string} field - * @param {unknown} value - */ - function add(field, value) { - const list = /** @type {Array<unknown>} */ ( - // Other extensions - /* c8 ignore next 2 */ - // @ts-expect-error: to do: refactor. - data[field] || (data[field] = []) - ) + const micromarkExtensions = + data.micromarkExtensions || (data.micromarkExtensions = []) + const fromMarkdownExtensions = + data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []) + const toMarkdownExtensions = + data.toMarkdownExtensions || (data.toMarkdownExtensions = []) - list.push(value) - } + micromarkExtensions.push(math(settings)) + fromMarkdownExtensions.push(mathFromMarkdown()) + toMarkdownExtensions.push(mathToMarkdown(settings)) } diff --git a/packages/remark-math/readme.md b/packages/remark-math/readme.md index b3f21e9..35d4359 100644 --- a/packages/remark-math/readme.md +++ b/packages/remark-math/readme.md @@ -191,9 +191,9 @@ somewhere in your types, as that registers the new node types in the tree. import {visit} from 'unist-util-visit' /** @type {import('unified').Plugin<[], import('mdast').Root>} */ -export default function myRemarkPlugin() => { - return (tree) => { - visit(tree, (node) => { +export default function myRemarkPlugin() { + return function (tree) { + visit(tree, function(node) { // `node` can now be one of the nodes for math. }) } diff --git a/packages/remark-math/test.js b/packages/remark-math/test.js index 3c4249c..3db31a1 100644 --- a/packages/remark-math/test.js +++ b/packages/remark-math/test.js @@ -1,12 +1,11 @@ import assert from 'node:assert/strict' import test from 'node:test' -import {u} from 'unist-builder' -import {removePosition} from 'unist-util-remove-position' -import {unified} from 'unified' +import rehypeStringify from 'rehype-stringify' import remarkParse from 'remark-parse' import remarkRehype from 'remark-rehype' -import rehypeStringify from 'rehype-stringify' import remarkStringify from 'remark-stringify' +import {unified} from 'unified' +import {removePosition} from 'unist-util-remove-position' import remarkMath from './index.js' test('remarkMath', async function (t) { @@ -17,6 +16,12 @@ test('remarkMath', async function (t) { .use(remarkRehype) .use(rehypeStringify) + await t.test('should expose the public api', async function () { + assert.deepEqual(Object.keys(await import('remark-math')).sort(), [ + 'default' + ]) + }) + await t.test('should parse inline and block math', async function () { const tree = unified() .use(remarkParse) @@ -25,43 +30,42 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u('paragraph', [ - u('text', 'Math '), - u( - 'inlineMath', + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + {type: 'text', value: 'Math '}, { + type: 'inlineMath', data: { hName: 'code', hProperties: {className: ['language-math', 'math-inline']}, - hChildren: [u('text', '\\alpha')] - } - }, - '\\alpha' - ) - ]), - u( - 'math', - { - meta: null, - data: { - hName: 'pre', - hChildren: [ - { - type: 'element', - tagName: 'code', - properties: {className: ['language-math', 'math-display']}, - children: [{type: 'text', value: '\\beta+\\gamma'}] - } - ] + hChildren: [{type: 'text', value: '\\alpha'}] + }, + value: '\\alpha' } + ] + }, + { + type: 'math', + meta: null, + data: { + hName: 'pre', + hChildren: [ + { + type: 'element', + tagName: 'code', + properties: {className: ['language-math', 'math-display']}, + children: [{type: 'text', value: '\\beta+\\gamma'}] + } + ] }, - '\\beta+\\gamma' - ) - ]) - ) + value: '\\beta+\\gamma' + } + ] + }) }) await t.test( @@ -74,10 +78,12 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [u('paragraph', [u('text', '$\\alpha$')])]) - ) + assert.deepEqual(tree, { + type: 'root', + children: [ + {type: 'paragraph', children: [{type: 'text', value: '$\\alpha$'}]} + ] + }) } ) @@ -91,24 +97,26 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u('paragraph', [ - u( - 'inlineMath', + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ { + type: 'inlineMath', + data: { hName: 'code', hProperties: {className: ['language-math', 'math-inline']}, - hChildren: [u('text', '\\alpha\\')] - } - }, - '\\alpha\\' - ) - ]) - ]) - ) + hChildren: [{type: 'text', value: '\\alpha\\'}] + }, + value: '\\alpha\\' + } + ] + } + ] + }) } ) @@ -122,25 +130,26 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u('paragraph', [ - u('text', '\\'), - u( - 'inlineMath', + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + {type: 'text', value: '\\'}, { + type: 'inlineMath', data: { hName: 'code', hProperties: {className: ['language-math', 'math-inline']}, - hChildren: [u('text', '\\alpha')] - } - }, - '\\alpha' - ) - ]) - ]) - ) + hChildren: [{type: 'text', value: '\\alpha'}] + }, + value: '\\alpha' + } + ] + } + ] + }) } ) @@ -154,12 +163,18 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u('paragraph', [u('inlineCode', '$'), u('text', '\\alpha$')]) - ]) - ) + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + {type: 'inlineCode', value: '$'}, + {type: 'text', value: '\\alpha$'} + ] + } + ] + }) } ) @@ -168,25 +183,26 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u('paragraph', [ - u( - 'inlineMath', + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ { + type: 'inlineMath', data: { hName: 'code', hProperties: {className: ['language-math', 'math-inline']}, - hChildren: [u('text', '\\alpha`')] - } + hChildren: [{type: 'text', value: '\\alpha`'}] + }, + value: '\\alpha`' }, - '\\alpha`' - ), - u('text', '`') - ]) - ]) - ) + {type: 'text', value: '`'} + ] + } + ] + }) }) await t.test('should support backticks in inline math', async function () { @@ -197,21 +213,25 @@ test('remarkMath', async function (t) { assert.deepEqual( tree, - u('root', [ - u('paragraph', [ - u( - 'inlineMath', - { - data: { - hName: 'code', - hProperties: {className: ['language-math', 'math-inline']}, - hChildren: [u('text', '`\\alpha`')] + { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + { + type: 'inlineMath', + data: { + hName: 'code', + hProperties: {className: ['language-math', 'math-inline']}, + hChildren: [{type: 'text', value: '`\\alpha`'}] + }, + value: '`\\alpha`' } - }, - '`\\alpha`' - ) - ]) - ]) + ] + } + ] + } ) }) @@ -225,24 +245,25 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u('paragraph', [ - u( - 'inlineMath', + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ { + type: 'inlineMath', data: { hName: 'code', hProperties: {className: ['language-math', 'math-inline']}, - hChildren: [u('text', '\\alpha$')] - } - }, - '\\alpha$' - ) - ]) - ]) - ) + hChildren: [{type: 'text', value: '\\alpha$'}] + }, + value: '\\alpha$' + } + ] + } + ] + }) } ) @@ -256,29 +277,28 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u( - 'math', - { - meta: null, - data: { - hName: 'pre', - hChildren: [ - { - type: 'element', - tagName: 'code', - properties: {className: ['language-math', 'math-display']}, - children: [{type: 'text', value: '\\alpha\\$'}] - } - ] - } + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'math', + + meta: null, + data: { + hName: 'pre', + hChildren: [ + { + type: 'element', + tagName: 'code', + properties: {className: ['language-math', 'math-display']}, + children: [{type: 'text', value: '\\alpha\\$'}] + } + ] }, - '\\alpha\\$' - ) - ]) - ) + value: '\\alpha\\$' + } + ] + }) } ) @@ -295,11 +315,13 @@ test('remarkMath', async function (t) { assert.deepEqual( tree, - u('root', [ - u('paragraph', [u('text', 'tango')]), - u( - 'math', + { + type: 'root', + children: [ + {type: 'paragraph', children: [{type: 'text', value: 'tango'}]}, { + type: 'math', + meta: null, data: { hName: 'pre', @@ -307,15 +329,17 @@ test('remarkMath', async function (t) { { type: 'element', tagName: 'code', - properties: {className: ['language-math', 'math-display']}, + properties: { + className: ['language-math', 'math-display'] + }, children: [{type: 'text', value: '\\alpha'}] } ] - } - }, - '\\alpha' - ) - ]) + }, + value: '\\alpha' + } + ] + } ) } ) @@ -330,24 +354,26 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u('paragraph', [ - u( - 'inlineMath', + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ { + type: 'inlineMath', + data: { hName: 'code', hProperties: {className: ['language-math', 'math-inline']}, - hChildren: [u('text', '\\alpha')] - } - }, - '\\alpha' - ) - ]) - ]) - ) + hChildren: [{type: 'text', value: '\\alpha'}] + }, + value: '\\alpha' + } + ] + } + ] + }) } ) @@ -361,29 +387,28 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u( - 'math', - { - meta: null, - data: { - hName: 'pre', - hChildren: [ - { - type: 'element', - tagName: 'code', - properties: {className: ['language-math', 'math-display']}, - children: [{type: 'text', value: '\\alpha'}] - } - ] - } + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'math', + + meta: null, + data: { + hName: 'pre', + hChildren: [ + { + type: 'element', + tagName: 'code', + properties: {className: ['language-math', 'math-display']}, + children: [{type: 'text', value: '\\alpha'}] + } + ] }, - '\\alpha' - ) - ]) - ) + value: '\\alpha' + } + ] + }) } ) @@ -395,39 +420,38 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u( - 'math', - { - meta: null, - data: { - hName: 'pre', - hChildren: [ - { - type: 'element', - tagName: 'code', - properties: {className: ['language-math', 'math-display']}, - children: [{type: 'text', value: ' \\alpha'}] - } - ] - } + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'math', + meta: null, + data: { + hName: 'pre', + hChildren: [ + { + type: 'element', + tagName: 'code', + properties: {className: ['language-math', 'math-display']}, + children: [{type: 'text', value: ' \\alpha'}] + } + ] }, - ' \\alpha' - ) - ]) - ) + value: ' \\alpha' + } + ] + }) }) await t.test('should stringify inline and block math', async function () { assert.deepEqual( - unified() - .use(remarkParse) - .use(remarkStringify) - .use(remarkMath) - .processSync('Math $\\alpha$\n\n$$\n\\beta+\\gamma\n$$\n') - .toString(), + String( + await unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkMath) + .process('Math $\\alpha$\n\n$$\n\\beta+\\gamma\n$$\n') + ), 'Math $\\alpha$\n\n$$\n\\beta+\\gamma\n$$\n' ) }) @@ -436,12 +460,13 @@ test('remarkMath', async function (t) { 'should support `singleDollarTextMath: false` (1)', async function () { assert.deepEqual( - unified() - .use(remarkParse) - .use(remarkStringify) - .use(remarkMath, {singleDollarTextMath: false}) - .processSync('Math $\\alpha$\n\n$$\\beta+\\gamma$$\n') - .toString(), + String( + await unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkMath, {singleDollarTextMath: false}) + .process('Math $\\alpha$\n\n$$\\beta+\\gamma$$\n') + ), 'Math $\\alpha$\n\n$$\\beta+\\gamma$$\n' ) } @@ -451,14 +476,15 @@ test('remarkMath', async function (t) { 'should support `singleDollarTextMath: false` (2)', async function () { assert.deepEqual( - unified() - .use(remarkParse) - .use(remarkMath, {singleDollarTextMath: false}) - // @ts-expect-error: to do: remove when `remark-rehype` is released. - .use(remarkRehype) - .use(rehypeStringify) - .processSync('Math $\\alpha$\n\n$$\\beta+\\gamma$$\n') - .toString(), + String( + await unified() + .use(remarkParse) + .use(remarkMath, {singleDollarTextMath: false}) + // @ts-expect-error: to do: remove when `remark-rehype` is released. + .use(remarkRehype) + .use(rehypeStringify) + .process('Math $\\alpha$\n\n$$\\beta+\\gamma$$\n') + ), '<p>Math $\\alpha$</p>\n<p><code class="language-math math-inline">\\beta+\\gamma</code></p>' ) } @@ -466,12 +492,13 @@ test('remarkMath', async function (t) { await t.test('should stringify math in a blockquote', async function () { assert.deepEqual( - unified() - .use(remarkParse) - .use(remarkStringify) - .use(remarkMath) - .processSync('> $$\n> \\alpha\\beta\n> $$\n') - .toString(), + String( + await unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkMath) + .process('> $$\n> \\alpha\\beta\n> $$\n') + ), '> $$\n> \\alpha\\beta\n> $$\n' ) }) @@ -480,7 +507,7 @@ test('remarkMath', async function (t) { 'should support an opening fence w/ meta, w/o closing fence', async function () { assert.deepEqual( - String(toHtml.processSync('$$just two dollars')), + String(await toHtml.process('$$just two dollars')), '<pre><code class="language-math math-display"></code></pre>' ) } @@ -488,7 +515,7 @@ test('remarkMath', async function (t) { await t.test('should support `meta`', async function () { assert.deepEqual( - String(toHtml.processSync('$$ must\n\\alpha\n$$')), + String(await toHtml.process('$$ must\n\\alpha\n$$')), '<pre><code class="language-math math-display">\\alpha</code></pre>' ) }) @@ -497,7 +524,7 @@ test('remarkMath', async function (t) { 'should include values after the opening fence', async function () { assert.deepEqual( - String(toHtml.processSync('$$ \n\\alpha\n$$')), + String(await toHtml.process('$$ \n\\alpha\n$$')), '<pre><code class="language-math math-display">\\alpha</code></pre>' ) } @@ -507,7 +534,7 @@ test('remarkMath', async function (t) { 'should not support values before the closing fence', async function () { assert.deepEqual( - String(toHtml.processSync('$$\n\\alpha\nmust $$')), + String(await toHtml.process('$$\n\\alpha\nmust $$')), '<pre><code class="language-math math-display">\\alpha\nmust $$</code></pre>' ) } @@ -517,7 +544,7 @@ test('remarkMath', async function (t) { 'should include values before the closing fence (except for spacing #2)', async function () { assert.deepEqual( - String(toHtml.processSync('$$\n\\alpha\n $$')), + String(await toHtml.process('$$\n\\alpha\n $$')), '<pre><code class="language-math math-display">\\alpha</code></pre>' ) } @@ -527,7 +554,7 @@ test('remarkMath', async function (t) { 'should exclude spacing after the closing fence', async function () { assert.deepEqual( - String(toHtml.processSync('$$\n\\alpha\n$$ ')), + String(await toHtml.process('$$\n\\alpha\n$$ ')), '<pre><code class="language-math math-display">\\alpha</code></pre>' ) } @@ -541,30 +568,28 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u( - 'math', - { - meta: null, - data: { - hName: 'pre', - hChildren: [ - { - type: 'element', - tagName: 'code', - properties: {className: ['language-math', 'math-display']}, - children: [{type: 'text', value: '\\alpha'}] - } - ] - } + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'math', + meta: null, + data: { + hName: 'pre', + hChildren: [ + { + type: 'element', + tagName: 'code', + properties: {className: ['language-math', 'math-display']}, + children: [{type: 'text', value: '\\alpha'}] + } + ] }, - '\\alpha' - ), - u('code', {lang: null, meta: null}, 'bravo') - ]) - ) + value: '\\alpha' + }, + {type: 'code', lang: null, meta: null, value: 'bravo'} + ] + }) }) await t.test( @@ -577,39 +602,48 @@ test('remarkMath', async function (t) { removePosition(tree, {force: true}) - assert.deepEqual( - tree, - u('root', [ - u('paragraph', [ - u( - 'inlineMath', + assert.deepEqual(tree, { + type: 'root', + children: [ + { + type: 'paragraph', + children: [ { + type: 'inlineMath', data: { hName: 'code', hProperties: {className: ['language-math', 'math-inline']}, - hChildren: [u('text', '\\alpha')] - } - }, - '\\alpha' - ) - ]) - ]) - ) + hChildren: [{type: 'text', value: '\\alpha'}] + }, + value: '\\alpha' + } + ] + } + ] + }) } ) await t.test('should stringify a tree', async function () { assert.deepEqual( - unified() - .use(remarkStringify) - .use(remarkMath) - .stringify( - u('root', [ - u('paragraph', [u('text', 'Math '), u('inlineMath', '\\alpha')]), - u('math', '\\beta+\\gamma') - ]) - ) - .toString(), + String( + await unified() + .use(remarkStringify) + .use(remarkMath) + .stringify({ + type: 'root', + children: [ + { + type: 'paragraph', + children: [ + {type: 'text', value: 'Math '}, + {type: 'inlineMath', value: '\\alpha'} + ] + }, + {type: 'math', value: '\\beta+\\gamma'} + ] + }) + ), 'Math $\\alpha$\n\n$$\n\\beta+\\gamma\n$$\n' ) }) @@ -618,12 +652,13 @@ test('remarkMath', async function (t) { 'should stringify inline math with double dollars using one dollar by default', async function () { assert.deepEqual( - unified() - .use(remarkParse) - .use(remarkStringify) - .use(remarkMath) - .processSync('$$\\alpha$$') - .toString(), + String( + await unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkMath) + .process('$$\\alpha$$') + ), '$\\alpha$\n' ) } @@ -633,12 +668,13 @@ test('remarkMath', async function (t) { 'should stringify inline math with double dollars using one dollar', async function () { assert.deepEqual( - unified() - .use(remarkParse) - .use(remarkStringify) - .use(remarkMath) - .processSync('$$\\alpha$$') - .toString(), + String( + await unified() + .use(remarkParse) + .use(remarkStringify) + .use(remarkMath) + .process('$$\\alpha$$') + ), '$\\alpha$\n' ) } @@ -646,14 +682,14 @@ test('remarkMath', async function (t) { await t.test('should do markdown-it-katex#01', async function () { assert.deepEqual( - String(toHtml.processSync('$1+1 = 2$')), + String(await toHtml.process('$1+1 = 2$')), '<p><code class="language-math math-inline">1+1 = 2</code></p>' ) }) await t.test('should do markdown-it-katex#02 (deviation)', async function () { assert.deepEqual( - String(toHtml.processSync('$$1+1 = 2$$')), + String(await toHtml.process('$$1+1 = 2$$')), '<p><code class="language-math math-inline">1+1 = 2</code></p>' ) }) @@ -662,7 +698,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#03: no whitespace before and after is fine', async function () { assert.deepEqual( - String(toHtml.processSync('foo$1+1 = 2$bar')), + String(await toHtml.process('foo$1+1 = 2$bar')), '<p>foo<code class="language-math math-inline">1+1 = 2</code>bar</p>' ) } @@ -672,7 +708,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#04: even when it starts with a negative sign', async function () { assert.deepEqual( - String(toHtml.processSync('foo$-1+1 = 2$bar')), + String(await toHtml.process('foo$-1+1 = 2$bar')), '<p>foo<code class="language-math math-inline">-1+1 = 2</code>bar</p>' ) } @@ -682,7 +718,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#05: shouldn’t render empty content', async function () { assert.deepEqual( - String(toHtml.processSync('aaa $$ bbb')), + String(await toHtml.process('aaa $$ bbb')), '<p>aaa $$ bbb</p>' ) } @@ -692,7 +728,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#06: should require a closing delimiter', async function () { assert.deepEqual( - String(toHtml.processSync('aaa $5.99 bbb')), + String(await toHtml.process('aaa $5.99 bbb')), '<p>aaa $5.99 bbb</p>' ) } @@ -702,7 +738,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#07: paragraph break in inline math is not allowed', async function () { assert.deepEqual( - String(toHtml.processSync('foo $1+1\n\n= 2$ bar')), + String(await toHtml.process('foo $1+1\n\n= 2$ bar')), '<p>foo $1+1</p>\n<p>= 2$ bar</p>' ) } @@ -712,7 +748,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#08: inline math with apparent markup should not be processed', async function () { assert.deepEqual( - String(toHtml.processSync('foo $1 *i* 1$ bar')), + String(await toHtml.process('foo $1 *i* 1$ bar')), '<p>foo <code class="language-math math-inline">1 *i* 1</code> bar</p>' ) } @@ -722,7 +758,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#09: block math can be indented up to 3 spaces', async function () { assert.deepEqual( - String(toHtml.processSync(' $$\n 1+1 = 2\n $$')), + String(await toHtml.process(' $$\n 1+1 = 2\n $$')), '<pre><code class="language-math math-display">1+1 = 2</code></pre>' ) } @@ -732,7 +768,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#10: …but 4 means a code block', async function () { assert.deepEqual( - String(toHtml.processSync(' $$\n 1+1 = 2\n $$')), + String(await toHtml.process(' $$\n 1+1 = 2\n $$')), '<pre><code>$$\n1+1 = 2\n$$\n</code></pre>' ) } @@ -742,7 +778,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#11: multiline inline math', async function () { assert.deepEqual( - String(toHtml.processSync('foo $1 + 1\n= 2$ bar')), + String(await toHtml.process('foo $1 + 1\n= 2$ bar')), '<p>foo <code class="language-math math-inline">1 + 1\n= 2</code> bar</p>' ) } @@ -752,7 +788,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#12: multiline display math', async function () { assert.deepEqual( - String(toHtml.processSync('$$\n\n 1\n+ 1\n\n= 2\n\n$$')), + String(await toHtml.process('$$\n\n 1\n+ 1\n\n= 2\n\n$$')), '<pre><code class="language-math math-display">\n 1\n+ 1\n\n= 2\n</code></pre>' ) } @@ -762,7 +798,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#13: text can immediately follow inline math', async function () { assert.deepEqual( - String(toHtml.processSync('$n$-th order')), + String(await toHtml.process('$n$-th order')), '<p><code class="language-math math-inline">n</code>-th order</p>' ) } @@ -772,7 +808,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#14: display math self-closes at the end of document', async function () { assert.deepEqual( - String(toHtml.processSync('$$\n1+1 = 2')), + String(await toHtml.process('$$\n1+1 = 2')), '<pre><code class="language-math math-display">1+1 = 2</code></pre>' ) } @@ -782,7 +818,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#15: display and inline math can appear in lists', async function () { assert.deepEqual( - String(toHtml.processSync('* $1+1 = 2$\n* $$\n 1+1 = 2\n $$')), + String(await toHtml.process('* $1+1 = 2$\n* $$\n 1+1 = 2\n $$')), '<ul>\n<li><code class="language-math math-inline">1+1 = 2</code></li>\n<li>\n<pre><code class="language-math math-display">1+1 = 2</code></pre>\n</li>\n</ul>' ) } @@ -792,7 +828,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#16: display math can be written in one line (deviation)', async function () { assert.deepEqual( - String(toHtml.processSync('$$1+1 = 2$$')), + String(await toHtml.process('$$1+1 = 2$$')), '<p><code class="language-math math-inline">1+1 = 2</code></p>' ) } @@ -803,7 +839,7 @@ test('remarkMath', async function (t) { async function () { // To do: this is broken. assert.deepEqual( - String(toHtml.processSync('$$\n[\n[1, 2]\n[3, 4]\n]\n$$')), + String(await toHtml.process('$$\n[\n[1, 2]\n[3, 4]\n]\n$$')), '<pre><code class="language-math math-display">[\n[1, 2]\n[3, 4]\n]</code></pre>' ) } @@ -813,7 +849,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#18: escaped delimiters should not render math (deviated)', async function () { assert.deepEqual( - String(toHtml.processSync('Foo \\$1$ bar\n\\$\\$\n1\n\\$\\$')), + String(await toHtml.process('Foo \\$1$ bar\n\\$\\$\n1\n\\$\\$')), '<p>Foo $1<code class="language-math math-inline"> bar\n\\</code>$\n1\n$$</p>' ) } @@ -824,7 +860,7 @@ test('remarkMath', async function (t) { async function () { assert.deepEqual( String( - toHtml.processSync( + await toHtml.process( 'Thus, $20,000 and USD$30,000 won’t parse as math.' ) ), @@ -837,7 +873,7 @@ test('remarkMath', async function (t) { 'should do markdown-it-katex#20: require non whitespace to right of opening inline math (deviated)', async function () { assert.deepEqual( - String(toHtml.processSync('It is 2$ for a can of soda, not 1$.')), + String(await toHtml.process('It is 2$ for a can of soda, not 1$.')), '<p>It is 2<code class="language-math math-inline"> for a can of soda, not 1</code>.</p>' ) } @@ -848,7 +884,7 @@ test('remarkMath', async function (t) { async function () { assert.deepEqual( String( - toHtml.processSync( + await toHtml.process( 'I’ll give $20 today, if you give me more $ tomorrow.' ) ), @@ -862,7 +898,7 @@ test('remarkMath', async function (t) { async function () { // #22 “inline blockmath is not (currently) registered” <-- we do support it! assert.deepEqual( - String(toHtml.processSync('Money adds: $\\$X + \\$Y = \\$Z$.')), + String(await toHtml.process('Money adds: $\\$X + \\$Y = \\$Z$.')), '<p>Money adds: <code class="language-math math-inline">\\</code>X + $Y = $Z$.</p>' ) } @@ -873,7 +909,7 @@ test('remarkMath', async function (t) { async function () { assert.deepEqual( String( - toHtml.processSync( + await toHtml.process( 'Weird-o: $\\displaystyle{\\begin{pmatrix} \\$ & 1\\\\\\$ \\end{pmatrix}}$.' ) ), diff --git a/tsconfig.json b/tsconfig.json index 93d2959..617fe63 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "exactOptionalPropertyTypes": true, "lib": ["es2020"], "module": "node16", - // To do: remove when `hast-util-from-parse5` is updated. + // To do: remove when `mathjax-full` types are fixed. "skipLibCheck": true, "strict": true, "target": "es2020"