/** * @typedef {import('hast').ElementContent} ElementContent * @typedef {import('hast').Root} Root * * @typedef {import('katex').KatexOptions} KatexOptions * * @typedef {import('vfile').VFile} VFile */ /** * @typedef {Omit} Options */ import {fromHtmlIsomorphic} from 'hast-util-from-html-isomorphic' import {toText} from 'hast-util-to-text' import katex from 'katex' import {SKIP, visitParents} from 'unist-util-visit-parents' /** @type {Readonly} */ const emptyOptions = {} /** @type {ReadonlyArray} */ const emptyClasses = [] /** * Render elements with a `language-math` (or `math-display`, `math-inline`) * class with KaTeX. * * @param {Readonly | null | undefined} [options] * Configuration (optional). * @returns * Transform. */ export default function rehypeKatex(options) { const settings = options || emptyOptions /** * Transform. * * @param {Root} tree * Tree. * @param {VFile} file * File. * @returns {undefined} * Nothing. */ return function (tree, file) { visitParents(tree, 'element', function (element, parents) { const classes = Array.isArray(element.properties.className) ? element.properties.className : emptyClasses // 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 // 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]
        displayMode = true
      }

      /* c8 ignore next -- verbose to test. */
      if (!parent) return

      const value = toText(scope, {whitespace: 'pre'})

      /** @type {Array | string | undefined} */
      let result

      try {
        result = katex.renderToString(value, {
          ...settings,
          displayMode,
          throwOnError: true
        })
      } catch (error) {
        const cause = /** @type {Error} */ (error)
        const ruleId = cause.name.toLowerCase()

        file.message('Could not render math with KaTeX', {
          ancestors: [...parents, element],
          cause,
          place: element.position,
          ruleId,
          source: 'rehype-katex'
        })

        // KaTeX can handle `ParseError` itself, but not others.
        if (ruleId === 'parseerror') {
          result = katex.renderToString(value, {
            ...settings,
            displayMode,
            strict: 'ignore',
            throwOnError: false
          })
        }
        // Generate similar markup if this is an other error.
        // See: .
        else {
          result = [
            {
              type: 'element',
              tagName: 'span',
              properties: {
                className: ['katex-error'],
                style: 'color:' + (settings.errorColor || '#cc0000'),
                title: String(error)
              },
              children: [{type: 'text', value}]
            }
          ]
        }
      }

      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
    })
  }
}