site/node_modules/rehype-katex/lib/index.js

146 lines
4.1 KiB
JavaScript
Raw Normal View History

2024-10-14 06:09:33 +00:00
/**
* @typedef {import('hast').ElementContent} ElementContent
* @typedef {import('hast').Root} Root
*
* @typedef {import('katex').KatexOptions} KatexOptions
*
* @typedef {import('vfile').VFile} VFile
*/
/**
* @typedef {Omit<KatexOptions, 'displayMode' | 'throwOnError'>} 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<Options>} */
const emptyOptions = {}
/** @type {ReadonlyArray<unknown>} */
const emptyClasses = []
/**
* Render elements with a `language-math` (or `math-display`, `math-inline`)
* class with KaTeX.
*
* @param {Readonly<Options> | 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 `<pre>` 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<ElementContent> | 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: <https://github.com/KaTeX/KaTeX/blob/5dc7af0/docs/error.md>.
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 dont expect `doctypes` in KaTeX result.
result = /** @type {Array<ElementContent>} */ (root.children)
}
const index = parent.children.indexOf(scope)
parent.children.splice(index, 1, ...result)
return SKIP
})
}
}