146 lines
4.1 KiB
JavaScript
146 lines
4.1 KiB
JavaScript
/**
|
||
* @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 don’t 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
|
||
})
|
||
}
|
||
}
|