From f1733469de2f86d1173637cb35b8077dee6d908f Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 19 Sep 2023 12:15:28 +0200 Subject: [PATCH] Change to replace wrapping elements Previously, spans and divs were generated by `remark-math`. They were left as-is by `rehype-katex` and `rehype-mathjax`. With fc32531, (`
` and) `` elements are generated by
`remark-math`.
That is to allow folks to use normal markdown, as in ` ```math `,
to generate math blocks.
However, `` and `
` do not contribute to how a document is displayed by browsers, but `
` and `` do.
To solve that, this commit *replaces* those elements instead of
changing their contents, when using `rehype-katex` or
`rehype-mathjax`.
---
 packages/rehype-katex/lib/index.js            |  59 ++++++---
 packages/rehype-katex/package.json            |   7 +-
 packages/rehype-katex/test.js                 |  84 +++++++------
 packages/rehype-mathjax/lib/browser.js        |   5 +-
 packages/rehype-mathjax/lib/create-plugin.js  |  61 ++++++---
 .../rehype-mathjax/lib/create-renderer.js     |  10 +-
 packages/rehype-mathjax/package.json          |   5 +-
 .../test/fixture/document-svg.html            |   2 +-
 .../test/fixture/double-svg.html              |   2 +-
 .../fixture/equation-numbering-1-chtml.html   |   6 +-
 .../fixture/equation-numbering-1-svg.html     |   4 +-
 .../fixture/equation-numbering-2-svg.html     |   6 +-
 .../fixture/markdown-code-fenced-svg.html     | 117 ++++++++++++++++++
 .../test/fixture/markdown-svg.html            |   4 +-
 .../fixture/small-browser-delimiters.html     |   4 +-
 .../test/fixture/small-browser.html           |   4 +-
 .../test/fixture/small-chtml.html             |   6 +-
 .../test/fixture/small-svg.html               |   4 +-
 packages/rehype-mathjax/test/index.js         |  17 +++
 packages/remark-math/lib/index.js             |   2 +-
 20 files changed, 304 insertions(+), 105 deletions(-)
 create mode 100644 packages/rehype-mathjax/test/fixture/markdown-code-fenced-svg.html

diff --git a/packages/rehype-katex/lib/index.js b/packages/rehype-katex/lib/index.js
index d60ab01..8272e2d 100644
--- a/packages/rehype-katex/lib/index.js
+++ b/packages/rehype-katex/lib/index.js
@@ -14,7 +14,7 @@
 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 {SKIP, visitParents} from 'unist-util-visit-parents'
 
 /** @type {Readonly} */
 const emptyOptions = {}
@@ -44,20 +44,46 @@ export default function rehypeKatex(options) {
    *   Nothing.
    */
   return function (tree, file) {
-    visit(tree, 'element', function (element, _, parent) {
+    visitParents(tree, 'element', function (element, parents) {
       const classes = Array.isArray(element.properties.className)
         ? element.properties.className
         : emptyClasses
-      const inline = classes.includes('math-inline')
-      const displayMode = classes.includes('math-display')
+      // This class can be generated from markdown with ` ```math `.
+      const languageMath = classes.includes('language-math')
+      // This class is used by `remark-math` for flow math (block, `$$\nmath\n$$`).
+      const mathDisplay = classes.includes('math-display')
+      // This class is used by `remark-math` for text math (inline, `$math$`).
+      const mathInline = classes.includes('math-inline')
+      let displayMode = mathDisplay
 
-      if (!inline && !displayMode) {
+      // Any class is fine.
+      if (!languageMath && !mathDisplay && !mathInline) {
         return
       }
 
-      const value = toText(element, {whitespace: 'pre'})
+      let parent = parents[parents.length - 1]
+      let scope = element
 
-      /** @type {string} */
+      // If this was generated with ` ```math `, replace the `
` and use
+      // display.
+      if (
+        element.tagName === 'code' &&
+        languageMath &&
+        parent &&
+        parent.type === 'element' &&
+        parent.tagName === 'pre'
+      ) {
+        scope = parent
+        parent = parents[parents.length - 2]
+        displayMode = true
+      }
+
+      /* c8 ignore next -- verbose to test. */
+      if (!parent) return
+
+      const value = toText(scope, {whitespace: 'pre'})
+
+      /** @type {Array | string | undefined} */
       let result
 
       try {
@@ -71,8 +97,7 @@ export default function rehypeKatex(options) {
         const ruleId = cause.name.toLowerCase()
 
         file.message('Could not render math with KaTeX', {
-          /* c8 ignore next -- verbose to test */
-          ancestors: parent ? [parent, element] : [element],
+          ancestors: [...parents, element],
           cause,
           place: element.position,
           ruleId,
@@ -91,7 +116,7 @@ export default function rehypeKatex(options) {
         // Generate similar markup if this is an other error.
         // See: .
         else {
-          element.children = [
+          result = [
             {
               type: 'element',
               tagName: 'span',
@@ -103,14 +128,18 @@ export default function rehypeKatex(options) {
               children: [{type: 'text', value}]
             }
           ]
-          return
         }
       }
 
-      const root = fromHtmlIsomorphic(result, {fragment: true})
-      // Cast because there will not be `doctypes` in KaTeX result.
-      const content = /** @type {Array} */ (root.children)
-      element.children = content
+      if (typeof result === 'string') {
+        const root = fromHtmlIsomorphic(result, {fragment: true})
+        // Cast as we don’t expect `doctypes` in KaTeX result.
+        result = /** @type {Array} */ (root.children)
+      }
+
+      const index = parent.children.indexOf(scope)
+      parent.children.splice(index, 1, ...result)
+      return SKIP
     })
   }
 }
diff --git a/packages/rehype-katex/package.json b/packages/rehype-katex/package.json
index 8eaec3f..55b6c3e 100644
--- a/packages/rehype-katex/package.json
+++ b/packages/rehype-katex/package.json
@@ -43,7 +43,7 @@
     "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-parents": "^6.0.0",
     "vfile": "^6.0.0"
   },
   "scripts": {
@@ -51,6 +51,9 @@
     "test": "npm run build && npm run test-api"
   },
   "xo": {
-    "prettier": true
+    "prettier": true,
+    "rules": {
+      "unicorn/prefer-at": "off"
+    }
   }
 }
diff --git a/packages/rehype-katex/test.js b/packages/rehype-katex/test.js
index f7bbb08..77b8406 100644
--- a/packages/rehype-katex/test.js
+++ b/packages/rehype-katex/test.js
@@ -25,7 +25,7 @@ test('rehype-katex', async function (t) {
           .use(rehypeStringify)
           .process(
             [
-              '

Inline math \\alpha.

', + '

Inline math \\alpha.

', '

Block math:

', '
\\gamma
' ].join('\n') @@ -37,19 +37,35 @@ test('rehype-katex', async function (t) { .use(rehypeStringify) .process( [ - '

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

', + '

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

', '

Block math:

', - '
' + - katex.renderToString('\\gamma', {displayMode: true}) + - '
' + katex.renderToString('\\gamma', {displayMode: true}) ].join('\n') ) ) ) }) + await t.test('should support markdown fenced code', async function () { + assert.deepEqual( + String( + await unified() + .use(remarkParse) + // @ts-expect-error: to do: remove when `remark-rehype` is released. + .use(remarkRehype) + .use(rehypeKatex) + .use(rehypeStringify) + .process('```math\n\\gamma\n```') + ), + String( + await unified() + .use(rehypeParse, {fragment: true}) + .use(rehypeStringify) + .process(katex.renderToString('\\gamma\n', {displayMode: true})) + ) + ) + }) + await t.test('should integrate with `remark-math`', async function () { assert.deepEqual( String( @@ -78,13 +94,9 @@ test('rehype-katex', async function (t) { .use(rehypeStringify) .process( [ - '

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

', + '

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

', '

Block math:

', - '
' +
-                katex.renderToString('\\gamma', {displayMode: true}) +
-                '
' + katex.renderToString('\\gamma', {displayMode: true}) ].join('\n') ) ) @@ -109,9 +121,9 @@ test('rehype-katex', async function (t) { .use(rehypeParse, {fragment: true}) .use(rehypeStringify) .process( - '

Double math ' + + '

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

' + '.

' ) ) ) @@ -127,17 +139,13 @@ test('rehype-katex', async function (t) { .use(rehypeParse, {fragment: true}) .use(rehypeKatex, {macros}) .use(rehypeStringify) - .process('\\RR') + .process('\\RR') ), String( await unified() .use(rehypeParse, {fragment: true}) .use(rehypeStringify) - .process( - '' + - katex.renderToString('\\RR', {macros}) + - '' - ) + .process(katex.renderToString('\\RR', {macros})) ) ) }) @@ -147,7 +155,7 @@ test('rehype-katex', async function (t) { .use(rehypeParse, {fragment: true}) .use(rehypeKatex, {errorColor: 'orange'}) .use(rehypeStringify) - .process('\\alpa') + .process('\\alpa') assert.deepEqual( String(file), @@ -156,12 +164,10 @@ test('rehype-katex', async function (t) { .use(rehypeParse, {fragment: true}) .use(rehypeStringify) .process( - '' + - katex.renderToString('\\alpa', { - errorColor: 'orange', - throwOnError: false - }) + - '' + katex.renderToString('\\alpa', { + errorColor: 'orange', + throwOnError: false + }) ) ) ) @@ -170,11 +176,11 @@ test('rehype-katex', async function (t) { const message = file.messages[0] assert(message) assert(message.cause) - assert(message.ancestors) assert.match( String(message.cause), /KaTeX parse error: Undefined control sequence/ ) + assert(message.ancestors) assert.equal(message.ancestors.length, 2) assert.deepEqual( {...file.messages[0], cause: undefined, ancestors: []}, @@ -204,14 +210,14 @@ test('rehype-katex', async function (t) { .use(rehypeParse, {fragment: true}) .use(rehypeKatex, {errorColor: 'orange', strict: 'ignore'}) .use(rehypeStringify) - .process('ê&') + .process('ê&') ), String( await unified() .use(rehypeParse, {fragment: true}) .use(rehypeStringify) .process( - 'ê&' + 'ê&' ) ) ) @@ -225,7 +231,7 @@ test('rehype-katex', async function (t) { .use(rehypeKatex, {errorColor: 'orange', strict: 'ignore'}) .use(rehypeStringify) .process( - '
\\begin{split}\n f(-2) &= \\sqrt{-2+4} \\\\\n &= x % Test Comment\n\\end{split}
' + '
\\begin{split}\n f(-2) &= \\sqrt{-2+4} \\\\\n &= x % Test Comment\n\\end{split}
' ) ), String( @@ -233,12 +239,10 @@ test('rehype-katex', async function (t) { .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} - ) + - '
' + katex.renderToString( + '\\begin{split}\n f(-2) &= \\sqrt{-2+4} \\\\\n &= x % Test Comment\n\\end{split}', + {displayMode: true} + ) ) ) ) @@ -252,7 +256,7 @@ test('rehype-katex', async function (t) { .use(rehypeKatex) .use(rehypeStringify) .process( - '\\begin{split}\n\\end{{split}}\n' + '\\begin{split}\n\\end{{split}}\n' ) ), String( @@ -260,7 +264,7 @@ test('rehype-katex', async function (t) { .use(rehypeParse, {fragment: true}) .use(rehypeStringify) .process( - '\\begin{split}\n\\end{{split}}\n' + '\\begin{split}\n\\end{{split}}\n' ) ) ) diff --git a/packages/rehype-mathjax/lib/browser.js b/packages/rehype-mathjax/lib/browser.js index 09b25ae..91cdebd 100644 --- a/packages/rehype-mathjax/lib/browser.js +++ b/packages/rehype-mathjax/lib/browser.js @@ -13,10 +13,9 @@ const rehypeMathJaxBrowser = createPlugin(function (options) { const inline = tex.inlineMath || [['\\(', '\\)']] return { - render(node, options) { + render(value, options) { const delimiters = (options.display ? display : inline)[0] - node.children.unshift({type: 'text', value: delimiters[0]}) - node.children.push({type: 'text', value: delimiters[1]}) + return [{type: 'text', value: delimiters[0] + value + delimiters[1]}] } } }) diff --git a/packages/rehype-mathjax/lib/create-plugin.js b/packages/rehype-mathjax/lib/create-plugin.js index e631849..9f8130e 100644 --- a/packages/rehype-mathjax/lib/create-plugin.js +++ b/packages/rehype-mathjax/lib/create-plugin.js @@ -1,5 +1,6 @@ /** * @typedef {import('hast').Element} Element + * @typedef {import('hast').ElementContent} ElementContent * @typedef {import('hast').Root} Root */ @@ -128,12 +129,12 @@ * * @callback Render * Render a math node. - * @param {Element} element - * Math node. + * @param {string} value + * Math value. * @param {Readonly} options * Configuration. - * @returns {undefined} - * Nothing. + * @returns {Array} + * Content. * * @typedef RenderOptions * Configuration. @@ -153,7 +154,8 @@ * Style sheet. */ -import {SKIP, visit} from 'unist-util-visit' +import {toText} from 'hast-util-to-text' +import {SKIP, visitParents} from 'unist-util-visit-parents' /** @type {Readonly} */ const emptyOptions = {} @@ -192,23 +194,54 @@ export function createPlugin(createRenderer) { /** @type {Element | Root} */ let context = tree - visit(tree, 'element', function (node) { - const classes = Array.isArray(node.properties.className) - ? node.properties.className + visitParents(tree, 'element', function (element, parents) { + const classes = Array.isArray(element.properties.className) + ? element.properties.className : emptyClasses - const inline = classes.includes('math-inline') - const display = classes.includes('math-display') + // This class can be generated from markdown with ` ```math `. + const languageMath = classes.includes('language-math') + // This class is used by `remark-math` for flow math (block, `$$\nmath\n$$`). + const mathDisplay = classes.includes('math-display') + // This class is used by `remark-math` for text math (inline, `$math$`). + const mathInline = classes.includes('math-inline') + let display = mathDisplay - if (node.tagName === 'head') { - context = node + // Find ``. + if (element.tagName === 'head') { + context = element } - if (!inline && !display) { + // Any class is fine. + if (!languageMath && !mathDisplay && !mathInline) { return } + let parent = parents[parents.length - 1] + let scope = element + + // If this was generated with ` ```math `, replace the `
` and use
+        // display.
+        if (
+          element.tagName === 'code' &&
+          languageMath &&
+          parent &&
+          parent.type === 'element' &&
+          parent.tagName === 'pre'
+        ) {
+          scope = parent
+          parent = parents[parents.length - 2]
+          display = true
+        }
+
+        /* c8 ignore next -- verbose to test. */
+        if (!parent) return
+
         found = true
-        renderer.render(node, {display})
+        const text = toText(scope, {whitespace: 'pre'})
+        const result = renderer.render(text, {display})
+
+        const index = parent.children.indexOf(scope)
+        parent.children.splice(index, 1, ...result)
 
         return SKIP
       })
diff --git a/packages/rehype-mathjax/lib/create-renderer.js b/packages/rehype-mathjax/lib/create-renderer.js
index 2f4d486..ff1c493 100644
--- a/packages/rehype-mathjax/lib/create-renderer.js
+++ b/packages/rehype-mathjax/lib/create-renderer.js
@@ -7,7 +7,6 @@
  */
 
 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'
@@ -45,15 +44,12 @@ export function createRenderer(options, output) {
   const doc = mathjax.document('', {InputJax: input, OutputJax: output})
 
   return {
-    render(node, options) {
-      const mathText = toText(node, {whitespace: 'pre'})
+    render(value, options) {
       // Cast as this practically results in `HTMLElement`.
-      const domNode = /** @type {HTMLElement} */ (
-        doc.convert(mathText, options)
-      )
+      const domNode = /** @type {HTMLElement} */ (doc.convert(value, options))
       // Cast as `HTMLElement` results in an `Element`.
       const hastNode = /** @type {Element} */ (fromDom(domNode))
-      node.children = [hastNode]
+      return [hastNode]
     },
     styleSheet() {
       const value = adapter.textContent(output.styleSheet(doc))
diff --git a/packages/rehype-mathjax/package.json b/packages/rehype-mathjax/package.json
index 9bc9ce2..6cce96c 100644
--- a/packages/rehype-mathjax/package.json
+++ b/packages/rehype-mathjax/package.json
@@ -63,7 +63,7 @@
     "jsdom": "^22.0.0",
     "mathjax-full": "^3.0.0",
     "unified": "^11.0.0",
-    "unist-util-visit": "^5.0.0"
+    "unist-util-visit-parents": "^6.0.0"
   },
   "devDependencies": {
     "@types/jsdom": "^21.0.0"
@@ -75,7 +75,8 @@
   "xo": {
     "prettier": true,
     "rules": {
-      "n/file-extension-in-import": "off"
+      "n/file-extension-in-import": "off",
+      "unicorn/prefer-at": "off"
     }
   }
 }
diff --git a/packages/rehype-mathjax/test/fixture/document-svg.html b/packages/rehype-mathjax/test/fixture/document-svg.html
index 3d1dbc5..765aa2f 100644
--- a/packages/rehype-mathjax/test/fixture/document-svg.html
+++ b/packages/rehype-mathjax/test/fixture/document-svg.html
@@ -120,7 +120,7 @@
 }
 
 
-

Hello, !

+

Hello, !

diff --git a/packages/rehype-mathjax/test/fixture/double-svg.html b/packages/rehype-mathjax/test/fixture/double-svg.html index 1497526..50174f5 100644 --- a/packages/rehype-mathjax/test/fixture/double-svg.html +++ b/packages/rehype-mathjax/test/fixture/double-svg.html @@ -1,4 +1,4 @@ -

Double math .

+

Double math .

\ No newline at end of file + diff --git a/packages/rehype-mathjax/test/fixture/equation-numbering-1-svg.html b/packages/rehype-mathjax/test/fixture/equation-numbering-1-svg.html index 3e753a8..4ff3036 100644 --- a/packages/rehype-mathjax/test/fixture/equation-numbering-1-svg.html +++ b/packages/rehype-mathjax/test/fixture/equation-numbering-1-svg.html @@ -1,6 +1,6 @@

Block math:

-
-

See equation .

+ +

See equation .

diff --git a/packages/rehype-mathjax/test/fixture/markdown-svg.html b/packages/rehype-mathjax/test/fixture/markdown-svg.html index 10a53c3..d45a34b 100644 --- a/packages/rehype-mathjax/test/fixture/markdown-svg.html +++ b/packages/rehype-mathjax/test/fixture/markdown-svg.html @@ -1,6 +1,6 @@ -

Inline math .

+

Inline math .

Block math:

-
\ No newline at end of file + diff --git a/packages/rehype-mathjax/test/fixture/small-svg.html b/packages/rehype-mathjax/test/fixture/small-svg.html index b2d7d2b..1c10fdf 100644 --- a/packages/rehype-mathjax/test/fixture/small-svg.html +++ b/packages/rehype-mathjax/test/fixture/small-svg.html @@ -1,6 +1,6 @@ -

Inline math .

+

Inline math .

Block math:

-
+