Commit node_modules

This commit is contained in:
Will Faught
2023-03-27 20:36:01 -07:00
parent 53a30a4c3e
commit 18ea53bb81
2693 changed files with 193960 additions and 70 deletions

View File

@@ -0,0 +1,8 @@
# Auto-render extension
This is an extension to automatically render all of the math inside of text. It
searches all of the text nodes in a given element for the given delimiters, and
renders the math in place.
See [Auto-render extension documentation](https://katex.org/docs/autorender.html)
for more information.

View File

@@ -0,0 +1,142 @@
/* eslint no-console:0 */
import katex from "katex";
import splitAtDelimiters from "./splitAtDelimiters";
/* Note: optionsCopy is mutated by this method. If it is ever exposed in the
* API, we should copy it before mutating.
*/
const renderMathInText = function(text, optionsCopy) {
const data = splitAtDelimiters(text, optionsCopy.delimiters);
if (data.length === 1 && data[0].type === 'text') {
// There is no formula in the text.
// Let's return null which means there is no need to replace
// the current text node with a new one.
return null;
}
const fragment = document.createDocumentFragment();
for (let i = 0; i < data.length; i++) {
if (data[i].type === "text") {
fragment.appendChild(document.createTextNode(data[i].data));
} else {
const span = document.createElement("span");
let math = data[i].data;
// Override any display mode defined in the settings with that
// defined by the text itself
optionsCopy.displayMode = data[i].display;
try {
if (optionsCopy.preProcess) {
math = optionsCopy.preProcess(math);
}
katex.render(math, span, optionsCopy);
} catch (e) {
if (!(e instanceof katex.ParseError)) {
throw e;
}
optionsCopy.errorCallback(
"KaTeX auto-render: Failed to parse `" + data[i].data +
"` with ",
e
);
fragment.appendChild(document.createTextNode(data[i].rawData));
continue;
}
fragment.appendChild(span);
}
}
return fragment;
};
const renderElem = function(elem, optionsCopy) {
for (let i = 0; i < elem.childNodes.length; i++) {
const childNode = elem.childNodes[i];
if (childNode.nodeType === 3) {
// Text node
// Concatenate all sibling text nodes.
// Webkit browsers split very large text nodes into smaller ones,
// so the delimiters may be split across different nodes.
let textContentConcat = childNode.textContent;
let sibling = childNode.nextSibling;
let nSiblings = 0;
while (sibling && (sibling.nodeType === Node.TEXT_NODE)) {
textContentConcat += sibling.textContent;
sibling = sibling.nextSibling;
nSiblings++;
}
const frag = renderMathInText(textContentConcat, optionsCopy);
if (frag) {
// Remove extra text nodes
for (let j = 0; j < nSiblings; j++) {
childNode.nextSibling.remove();
}
i += frag.childNodes.length - 1;
elem.replaceChild(frag, childNode);
} else {
// If the concatenated text does not contain math
// the siblings will not either
i += nSiblings;
}
} else if (childNode.nodeType === 1) {
// Element node
const className = ' ' + childNode.className + ' ';
const shouldRender = optionsCopy.ignoredTags.indexOf(
childNode.nodeName.toLowerCase()) === -1 &&
optionsCopy.ignoredClasses.every(
x => className.indexOf(' ' + x + ' ') === -1);
if (shouldRender) {
renderElem(childNode, optionsCopy);
}
}
// Otherwise, it's something else, and ignore it.
}
};
const renderMathInElement = function(elem, options) {
if (!elem) {
throw new Error("No element provided to render");
}
const optionsCopy = {};
// Object.assign(optionsCopy, option)
for (const option in options) {
if (options.hasOwnProperty(option)) {
optionsCopy[option] = options[option];
}
}
// default options
optionsCopy.delimiters = optionsCopy.delimiters || [
{left: "$$", right: "$$", display: true},
{left: "\\(", right: "\\)", display: false},
// LaTeX uses $…$, but it ruins the display of normal `$` in text:
// {left: "$", right: "$", display: false},
// $ must come after $$
// Render AMS environments even if outside $$…$$ delimiters.
{left: "\\begin{equation}", right: "\\end{equation}", display: true},
{left: "\\begin{align}", right: "\\end{align}", display: true},
{left: "\\begin{alignat}", right: "\\end{alignat}", display: true},
{left: "\\begin{gather}", right: "\\end{gather}", display: true},
{left: "\\begin{CD}", right: "\\end{CD}", display: true},
{left: "\\[", right: "\\]", display: true},
];
optionsCopy.ignoredTags = optionsCopy.ignoredTags || [
"script", "noscript", "style", "textarea", "pre", "code", "option",
];
optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || [];
optionsCopy.errorCallback = optionsCopy.errorCallback || console.error;
// Enable sharing of global macros defined via `\gdef` between different
// math elements within a single call to `renderMathInElement`.
optionsCopy.macros = optionsCopy.macros || {};
renderElem(elem, optionsCopy);
};
export default renderMathInElement;

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<!--To run this example from a clone of the repository, run `yarn start`
in the root KaTeX directory and then visit with your web browser:
http://localhost:7936/contrib/auto-render/index.html
-->
<html>
<head>
<meta charset="UTF-8">
<title>Auto-render test</title>
<script src="/katex.js" type="text/javascript"></script>
<script src="/contrib/auto-render.js" type="text/javascript"></script>
<style type="text/css">
body {
margin: 0px;
padding: 0px;
font-size: 36px;
}
#test > .blue {
color: blue;
}
</style>
</head>
<body>
<div id="test">
This is some text $math \frac12$ other text $\unsupported$
<span class="blue">
Other node \[ \text{displaymath} \frac{1}{2} \] blah $$ \int_2^3 $$
</span>
and some <!-- comment --> more text \(and math\) blah. And $math with a
\$ sign$.
<pre>
Stuff in a $pre tag$
</pre>
<p>An AMS environment without <code>$$…$$</code> delimiters.</p>
<p>\begin{equation} \begin{split} a &=b+c\\ &=e+f \end{split} \end{equation}</p>
</div>
<script>
renderMathInElement(
document.getElementById("test"),
{
delimiters: [
{left: "$$", right: "$$", display: true},
{left: "$", right: "$", display: false},
{left: "\\begin{equation}", right: "\\end{equation}", display: true},
{left: "\\begin{align}", right: "\\end{align}", display: true},
{left: "\\begin{alignat}", right: "\\end{alignat}", display: true},
{left: "\\begin{gather}", right: "\\end{gather}", display: true},
{left: "\\(", right: "\\)", display: false},
{left: "\\[", right: "\\]", display: true}
]
}
);
</script>
</body>
</html>

View File

@@ -0,0 +1,85 @@
/* eslint no-constant-condition:0 */
const findEndOfMath = function(delimiter, text, startIndex) {
// Adapted from
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
let index = startIndex;
let braceLevel = 0;
const delimLength = delimiter.length;
while (index < text.length) {
const character = text[index];
if (braceLevel <= 0 &&
text.slice(index, index + delimLength) === delimiter) {
return index;
} else if (character === "\\") {
index++;
} else if (character === "{") {
braceLevel++;
} else if (character === "}") {
braceLevel--;
}
index++;
}
return -1;
};
const escapeRegex = function(string) {
return string.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
};
const amsRegex = /^\\begin{/;
const splitAtDelimiters = function(text, delimiters) {
let index;
const data = [];
const regexLeft = new RegExp(
"(" + delimiters.map((x) => escapeRegex(x.left)).join("|") + ")"
);
while (true) {
index = text.search(regexLeft);
if (index === -1) {
break;
}
if (index > 0) {
data.push({
type: "text",
data: text.slice(0, index),
});
text = text.slice(index); // now text starts with delimiter
}
// ... so this always succeeds:
const i = delimiters.findIndex((delim) => text.startsWith(delim.left));
index = findEndOfMath(delimiters[i].right, text, delimiters[i].left.length);
if (index === -1) {
break;
}
const rawData = text.slice(0, index + delimiters[i].right.length);
const math = amsRegex.test(rawData)
? rawData
: text.slice(delimiters[i].left.length, index);
data.push({
type: "math",
data: math,
rawData,
display: delimiters[i].display,
});
text = text.slice(index + delimiters[i].right.length);
}
if (text !== "") {
data.push({
type: "text",
data: text,
});
}
return data;
};
export default splitAtDelimiters;

View File

@@ -0,0 +1,363 @@
/**
* @jest-environment jsdom
*/
import splitAtDelimiters from "../splitAtDelimiters";
import renderMathInElement from "../auto-render";
beforeEach(function() {
expect.extend({
toSplitInto: function(actual, result, delimiters) {
const message = {
pass: true,
message: () => "'" + actual + "' split correctly",
};
const split =
splitAtDelimiters(actual, delimiters);
if (split.length !== result.length) {
message.pass = false;
message.message = () => "Different number of splits: " +
split.length + " vs. " + result.length + " (" +
JSON.stringify(split) + " vs. " +
JSON.stringify(result) + ")";
return message;
}
for (let i = 0; i < split.length; i++) {
const real = split[i];
const correct = result[i];
let good = true;
let diff;
if (real.type !== correct.type) {
good = false;
diff = "type";
} else if (real.data !== correct.data) {
good = false;
diff = "data";
} else if (real.display !== correct.display) {
good = false;
diff = "display";
}
if (!good) {
message.pass = false;
message.message = () => "Difference at split " +
(i + 1) + ": " + JSON.stringify(real) +
" vs. " + JSON.stringify(correct) +
" (" + diff + " differs)";
break;
}
}
return message;
},
});
});
describe("A delimiter splitter", function() {
it("doesn't split when there are no delimiters", function() {
expect("hello").toSplitInto(
[
{type: "text", data: "hello"},
],
[
{left: "(", right: ")", display: false},
]);
});
it("doesn't create a math node with only one left delimiter", function() {
expect("hello ( world").toSplitInto(
[
{type: "text", data: "hello "},
{type: "text", data: "( world"},
],
[
{left: "(", right: ")", display: false},
]);
});
it("doesn't split when there's only a right delimiter", function() {
expect("hello ) world").toSplitInto(
[
{type: "text", data: "hello ) world"},
],
[
{left: "(", right: ")", display: false},
]);
});
it("splits when there are both delimiters", function() {
expect("hello ( world ) boo").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: " world ",
rawData: "( world )", display: false},
{type: "text", data: " boo"},
],
[
{left: "(", right: ")", display: false},
]);
});
it("splits on multi-character delimiters", function() {
expect("hello [[ world ]] boo").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: " world ",
rawData: "[[ world ]]", display: false},
{type: "text", data: " boo"},
],
[
{left: "[[", right: "]]", display: false},
]);
expect("hello \\begin{equation} world \\end{equation} boo").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: "\\begin{equation} world \\end{equation}",
rawData: "\\begin{equation} world \\end{equation}",
display: false},
{type: "text", data: " boo"},
],
[
{left: "\\begin{equation}", right: "\\end{equation}",
display: false},
]);
});
it("splits mutliple times", function() {
expect("hello ( world ) boo ( more ) stuff").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: " world ",
rawData: "( world )", display: false},
{type: "text", data: " boo "},
{type: "math", data: " more ",
rawData: "( more )", display: false},
{type: "text", data: " stuff"},
],
[
{left: "(", right: ")", display: false},
]);
});
it("leaves the ending when there's only a left delimiter", function() {
expect("hello ( world ) boo ( left").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: " world ",
rawData: "( world )", display: false},
{type: "text", data: " boo "},
{type: "text", data: "( left"},
],
[
{left: "(", right: ")", display: false},
]);
});
it("doesn't split when close delimiters are in {}s", function() {
expect("hello ( world { ) } ) boo").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: " world { ) } ",
rawData: "( world { ) } )", display: false},
{type: "text", data: " boo"},
],
[
{left: "(", right: ")", display: false},
]);
expect("hello ( world { { } ) } ) boo").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: " world { { } ) } ",
rawData: "( world { { } ) } )", display: false},
{type: "text", data: " boo"},
],
[
{left: "(", right: ")", display: false},
]);
});
it("correctly processes sequences of $..$", function() {
expect("$hello$$world$$boo$").toSplitInto(
[
{type: "math", data: "hello",
rawData: "$hello$", display: false},
{type: "math", data: "world",
rawData: "$world$", display: false},
{type: "math", data: "boo",
rawData: "$boo$", display: false},
],
[
{left: "$", right: "$", display: false},
]);
});
it("doesn't split at escaped delimiters", function() {
expect("hello ( world \\) ) boo").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: " world \\) ",
rawData: "( world \\) )", display: false},
{type: "text", data: " boo"},
],
[
{left: "(", right: ")", display: false},
]);
/* TODO(emily): make this work maybe?
expect("hello \\( ( world ) boo").toSplitInto(
"(", ")",
[
{type: "text", data: "hello \\( "},
{type: "math", data: " world ",
rawData: "( world )", display: false},
{type: "text", data: " boo"},
]);
*/
});
it("splits when the right and left delimiters are the same", function() {
expect("hello $ world $ boo").toSplitInto(
[
{type: "text", data: "hello "},
{type: "math", data: " world ",
rawData: "$ world $", display: false},
{type: "text", data: " boo"},
],
[
{left: "$", right: "$", display: false},
]);
});
it("ignores \\$", function() {
expect("$x = \\$5$").toSplitInto(
[
{type: "math", data: "x = \\$5",
rawData: "$x = \\$5$", display: false},
],
[
{left: "$", right: "$", display: false},
]);
});
it("remembers which delimiters are display-mode", function() {
const startData = "hello ( world ) boo";
expect(splitAtDelimiters(startData,
[{left:"(", right:")", display:true}])).toEqual(
[
{type: "text", data: "hello "},
{type: "math", data: " world ",
rawData: "( world )", display: true},
{type: "text", data: " boo"},
]);
});
it("handles nested delimiters irrespective of order", function() {
expect(splitAtDelimiters("$\\fbox{\\(hi\\)}$",
[
{left:"\\(", right:"\\)", display:false},
{left:"$", right:"$", display:false},
])).toEqual(
[
{type: "math", data: "\\fbox{\\(hi\\)}",
rawData: "$\\fbox{\\(hi\\)}$", display: false},
]);
expect(splitAtDelimiters("\\(\\fbox{$hi$}\\)",
[
{left:"\\(", right:"\\)", display:false},
{left:"$", right:"$", display:false},
])).toEqual(
[
{type: "math", data: "\\fbox{$hi$}",
rawData: "\\(\\fbox{$hi$}\\)", display: false},
]);
});
it("handles a mix of $ and $$", function() {
expect(splitAtDelimiters("$hello$world$$boo$$",
[
{left:"$$", right:"$$", display:true},
{left:"$", right:"$", display:false},
])).toEqual(
[
{type: "math", data: "hello",
rawData: "$hello$", display: false},
{type: "text", data: "world"},
{type: "math", data: "boo",
rawData: "$$boo$$", display: true},
]);
expect(splitAtDelimiters("$hello$$world$$$boo$$",
[
{left:"$$", right:"$$", display:true},
{left:"$", right:"$", display:false},
])).toEqual(
[
{type: "math", data: "hello",
rawData: "$hello$", display: false},
{type: "math", data: "world",
rawData: "$world$", display: false},
{type: "math", data: "boo",
rawData: "$$boo$$", display: true},
]);
});
});
describe("Pre-process callback", function() {
it("replace `-squared` with `^2 `", function() {
const el1 = document.createElement('div');
el1.textContent = 'Circle equation: $x-squared + y-squared = r-squared$.';
const el2 = document.createElement('div');
el2.textContent = 'Circle equation: $x^2 + y^2 = r^2$.';
const delimiters = [{left: "$", right: "$", display: false}];
renderMathInElement(el1, {
delimiters,
preProcess: math => math.replace(/-squared/g, '^2'),
});
renderMathInElement(el2, {delimiters});
expect(el1.innerHTML).toEqual(el2.innerHTML);
});
});
describe("Parse adjacent text nodes", function() {
it("parse adjacent text nodes with math", function() {
const textNodes = ['\\[',
'x^2 + y^2 = r^2',
'\\]'];
const el = document.createElement('div');
for (let i = 0; i < textNodes.length; i++) {
const txt = document.createTextNode(textNodes[i]);
el.appendChild(txt);
}
const el2 = document.createElement('div');
const txt = document.createTextNode(textNodes.join(''));
el2.appendChild(txt);
const delimiters = [{left: "\\[", right: "\\]", display: true}];
renderMathInElement(el, {delimiters});
renderMathInElement(el2, {delimiters});
expect(el).toStrictEqual(el2);
});
it("parse adjacent text nodes without math", function() {
const textNodes = ['Lorem ipsum dolor',
'sit amet',
'consectetur adipiscing elit'];
const el = document.createElement('div');
for (let i = 0; i < textNodes.length; i++) {
const txt = document.createTextNode(textNodes[i]);
el.appendChild(txt);
}
const el2 = document.createElement('div');
for (let i = 0; i < textNodes.length; i++) {
const txt = document.createTextNode(textNodes[i]);
el2.appendChild(txt);
}
const delimiters = [{left: "\\[", right: "\\]", display: true}];
renderMathInElement(el, {delimiters});
expect(el).toStrictEqual(el2);
});
});

39
paige/node_modules/katex/contrib/copy-tex/README.md generated vendored Normal file
View File

@@ -0,0 +1,39 @@
# Copy-tex extension
This extension modifies the copy/paste behavior in any browser supporting the
[Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent)
so that, when selecting and copying KaTeX-rendered elements, the text
content of the resulting clipboard renders KaTeX elements as their LaTeX source
surrounded by specified delimiters. (The HTML content of the resulting
clipboard remains the selected HTML content, as it normally would.)
The default delimiters are `$...$` for inline math and `$$...$$` for display
math, but you can easy switch them to e.g. `\(...\)` and `\[...\]` by
modifying `copyDelimiters` in [the source code](copy-tex.js).
Note that a selection containing part of a KaTeX formula gets extended to
include the entire KaTeX formula.
## Usage
This extension isn't part of KaTeX proper, so the script should be separately
included in the page.
```html
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/copy-tex.min.js" integrity="sha384-ww/583aHhxWkz5DEVn6OKtNiIaLi2iBRNZXfJRiY1Ai7tnJ9UXpEsyvOITVpTl4A" crossorigin="anonymous"></script>
```
(Note that, as of KaTeX 0.16.0, there is no longer a corresponding CSS file.)
See [index.html](index.html) for an example.
(To run this example from a clone of the repository, run `yarn start`
in the root KaTeX directory, and then visit
http://localhost:7936/contrib/copy-tex/index.html
with your web browser.)
If you want to build your own custom copy handler based on this one,
copy the `copy-tex.js` into your codebase and replace the `require`
statement with `require('katex/contrib/copy-tex/katex2tex.js')`.
ECMAScript module is also available:
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/copy-tex.mjs" integrity="sha384-bVEnwt0PtX+1EuJoOEcm4rgTUWvb2ILTdjHfI1gUe/r5fdqrTcQaUuRdHG2DciuQ" crossorigin="anonymous"></script>
```

51
paige/node_modules/katex/contrib/copy-tex/copy-tex.js generated vendored Normal file
View File

@@ -0,0 +1,51 @@
// @flow
import katexReplaceWithTex from './katex2tex';
// Return <div class="katex"> element containing node, or null if not found.
function closestKatex(node: Node): ?Element {
// If node is a Text Node, for example, go up to containing Element,
// where we can apply the `closest` method.
const element: ?Element =
(node instanceof Element ? node : node.parentElement);
return element && element.closest('.katex');
}
// Global copy handler to modify behavior on/within .katex elements.
document.addEventListener('copy', function(event: ClipboardEvent) {
const selection = window.getSelection();
if (selection.isCollapsed || !event.clipboardData) {
return; // default action OK if selection is empty or unchangeable
}
const clipboardData = event.clipboardData;
const range = selection.getRangeAt(0);
// When start point is within a formula, expand to entire formula.
const startKatex = closestKatex(range.startContainer);
if (startKatex) {
range.setStartBefore(startKatex);
}
// Similarly, when end point is within a formula, expand to entire formula.
const endKatex = closestKatex(range.endContainer);
if (endKatex) {
range.setEndAfter(endKatex);
}
const fragment = range.cloneContents();
if (!fragment.querySelector('.katex-mathml')) {
return; // default action OK if no .katex-mathml elements
}
const htmlContents = Array.prototype.map.call(fragment.childNodes,
(el) => (el instanceof Text ? el.textContent : el.outerHTML)
).join('');
// Preserve usual HTML copy/paste behavior.
clipboardData.setData('text/html', htmlContents);
// Rewrite plain-text version.
clipboardData.setData('text/plain',
katexReplaceWithTex(fragment).textContent);
// Prevent normal copy handling.
event.preventDefault();
});

38
paige/node_modules/katex/contrib/copy-tex/index.html generated vendored Normal file
View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<!--To run this example from a clone of the repository, run `yarn start`
in the root KaTeX directory and then visit with your web browser:
http://localhost:7936/contrib/copy-tex/index.html
-->
<html>
<head>
<meta charset="UTF-8">
<title>Copy-tex test</title>
<script src="/katex.js" type="text/javascript"></script>
<script src="/contrib/auto-render.js" type="text/javascript"></script>
<script src="/contrib/copy-tex.js" type="text/javascript"></script>
<style type="text/css">
body {
margin: 0px;
padding: 0px;
font-size: 36px;
}
#test > .blue {
color: blue;
}
</style>
</head>
<body>
<h1>Copy-tex test</h1>
<h2>Try copy/pasting some of the text below!</h2>
<p>
Here is some \(\KaTeX\) math: $$ x^2+y^2=z^2 $$
The variables are \(x\), \(y\), and \(z\),
which are all in \(\mathbb{R}^+\).
Q.E.D.
</p>
<script>
renderMathInElement(document.body);
</script>
</body>
</html>

61
paige/node_modules/katex/contrib/copy-tex/katex2tex.js generated vendored Normal file
View File

@@ -0,0 +1,61 @@
// @flow
export interface CopyDelimiters {
inline: [string, string],
display: [string, string],
}
// Set these to how you want inline and display math to be delimited.
export const defaultCopyDelimiters: CopyDelimiters = {
inline: ['$', '$'], // alternative: ['\(', '\)']
display: ['$$', '$$'], // alternative: ['\[', '\]']
};
// Replace .katex elements with their TeX source (<annotation> element).
// Modifies fragment in-place. Useful for writing your own 'copy' handler,
// as in copy-tex.js.
export function katexReplaceWithTex(
fragment: DocumentFragment,
copyDelimiters: CopyDelimiters = defaultCopyDelimiters
): DocumentFragment {
// Remove .katex-html blocks that are preceded by .katex-mathml blocks
// (which will get replaced below).
const katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html');
for (let i = 0; i < katexHtml.length; i++) {
const element = katexHtml[i];
if (element.remove) {
element.remove();
} else if (element.parentNode) {
element.parentNode.removeChild(element);
}
}
// Replace .katex-mathml elements with their annotation (TeX source)
// descendant, with inline delimiters.
const katexMathml = fragment.querySelectorAll('.katex-mathml');
for (let i = 0; i < katexMathml.length; i++) {
const element = katexMathml[i];
const texSource = element.querySelector('annotation');
if (texSource) {
if (element.replaceWith) {
element.replaceWith(texSource);
} else if (element.parentNode) {
element.parentNode.replaceChild(texSource, element);
}
texSource.innerHTML = copyDelimiters.inline[0] +
texSource.innerHTML + copyDelimiters.inline[1];
}
}
// Switch display math to display delimiters.
const displays = fragment.querySelectorAll('.katex-display annotation');
for (let i = 0; i < displays.length; i++) {
const element = displays[i];
element.innerHTML = copyDelimiters.display[0] +
element.innerHTML.substr(copyDelimiters.inline[0].length,
element.innerHTML.length - copyDelimiters.inline[0].length
- copyDelimiters.inline[1].length)
+ copyDelimiters.display[1];
}
return fragment;
}
export default katexReplaceWithTex;

View File

@@ -0,0 +1,38 @@
# `math/tex` Custom Script Type Extension
This is an extension to automatically display code inside `script` tags with `type=math/tex` using KaTeX.
This script type is commonly used by MathJax, so this can be used to support compatibility with MathJax.
### Usage
This extension isn't part of KaTeX proper, so the script should be separately
included in the page, in addition to KaTeX.
Load the extension by adding the following line to your HTML file.
```html
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/mathtex-script-type.min.js" integrity="sha384-jiBVvJ8NGGj5n7kJaiWwWp9AjC+Yh8rhZY3GtAX8yU28azcLgoRo4oukO87g7zDT" crossorigin="anonymous"></script>
```
You can download the script and use it locally, or from a local KaTeX installation instead.
For example, in the following simple page, we first load KaTeX as usual.
Then, in the body, we use a `math/tex` script to typeset the equation `x+\sqrt{1-x^2}`.
```html
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css" integrity="sha384-vKruj+a13U8yHIkAyGgK1J3ArTLzrFGBbBc0tDp4ad/EyewESeXE/Iv67Aj8gKZ0" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.js" integrity="sha384-PwRUT/YqbnEjkZO0zZxNqcxACrXe+j766U2amXcgMg5457rve2Y7I6ZJSm2A0mS4" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/mathtex-script-type.min.js" integrity="sha384-jiBVvJ8NGGj5n7kJaiWwWp9AjC+Yh8rhZY3GtAX8yU28azcLgoRo4oukO87g7zDT" crossorigin="anonymous"></script>
</head>
<body>
<script type="math/tex">x+\sqrt{1-x^2}</script>
</body>
</html>
```
ECMAScript module is also available:
```html
<script type="module" src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/mathtex-script-type.mjs" integrity="sha384-4EJvC5tvqq9XJxXvdD4JutBokuFw/dCe2AB4gZ9sRpwFFXECpL3qT43tmE0PkpVg" crossorigin="anonymous"></script>

View File

@@ -0,0 +1,22 @@
import katex from "katex";
let scripts = document.body.getElementsByTagName("script");
scripts = Array.prototype.slice.call(scripts);
scripts.forEach(function(script) {
if (!script.type || !script.type.match(/math\/tex/i)) {
return -1;
}
const display =
(script.type.match(/mode\s*=\s*display(;|\s|\n|$)/) != null);
const katexElement = document.createElement(display ? "div" : "span");
katexElement.setAttribute("class",
display ? "equation" : "inline-equation");
try {
katex.render(script.text, katexElement, {displayMode: display});
} catch (err) {
//console.error(err); linter doesn't like this
katexElement.textContent = script.text;
}
script.parentNode.replaceChild(katexElement, script);
});

23
paige/node_modules/katex/contrib/mhchem/README.md generated vendored Normal file
View File

@@ -0,0 +1,23 @@
# mhchem extension
This extension adds to KaTeX the `\ce` and `\pu` functions from the [mhchem](https://mhchem.github.io/MathJax-mhchem/) package.
### Usage
This extension isn't part of core KaTeX, so the script should be separately included. Write the following line into the HTML page's `<head>`. Place it *after* the line that calls `katex.js`, and if you make use of the [auto-render](https://katex.org/docs/autorender.html) extension, place it *before* the line that calls `auto-render.js`.
```html
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/mhchem.min.js" integrity="sha384-RTN08a0AXIioPBcVosEqPUfKK+rPp+h1x/izR7xMkdMyuwkcZCWdxO+RSwIFtJXN" crossorigin="anonymous"></script>
```
If you remove the `defer` attribute from this tag, then you must also remove the `defer` attribute from the `<script src="https://../katex.min.js">` tag.
### Syntax
See the [mhchem Manual](https://mhchem.github.io/MathJax-mhchem/) for a full explanation of the input syntax, with working examples. The manual also includes a demonstration box.
Note that old versions of `mhchem.sty` used `\cf` for chemical formula and `\ce` for chemical equations, but `\cf` has been deprecated in place of `\ce`. This extension supports only `\ce`. You can define a macro mapping `\cf` to `\ce` if needed.
### Browser Support
This extension has been tested on Chrome, Firefox, Opera, and Edge.

1695
paige/node_modules/katex/contrib/mhchem/mhchem.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,746 @@
// @flow
/**
* renderA11yString returns a readable string.
*
* In some cases the string will have the proper semantic math
* meaning,:
* renderA11yString("\\frac{1}{2}"")
* -> "start fraction, 1, divided by, 2, end fraction"
*
* However, other cases do not:
* renderA11yString("f(x) = x^2")
* -> "f, left parenthesis, x, right parenthesis, equals, x, squared"
*
* The commas in the string aim to increase ease of understanding
* when read by a screenreader.
*/
// NOTE: since we're importing types here these files won't actually be
// included in the build.
import type {Atom} from "../../src/symbols";
import type {AnyParseNode} from "../../src/parseNode";
import type {SettingsOptions} from "../../src/Settings";
// $FlowIgnore: we import the types directly anyways
import katex from "katex";
const stringMap = {
"(": "left parenthesis",
")": "right parenthesis",
"[": "open bracket",
"]": "close bracket",
"\\{": "left brace",
"\\}": "right brace",
"\\lvert": "open vertical bar",
"\\rvert": "close vertical bar",
"|": "vertical bar",
"\\uparrow": "up arrow",
"\\Uparrow": "up arrow",
"\\downarrow": "down arrow",
"\\Downarrow": "down arrow",
"\\updownarrow": "up down arrow",
"\\leftarrow": "left arrow",
"\\Leftarrow": "left arrow",
"\\rightarrow": "right arrow",
"\\Rightarrow": "right arrow",
"\\langle": "open angle",
"\\rangle": "close angle",
"\\lfloor": "open floor",
"\\rfloor": "close floor",
"\\int": "integral",
"\\intop": "integral",
"\\lim": "limit",
"\\ln": "natural log",
"\\log": "log",
"\\sin": "sine",
"\\cos": "cosine",
"\\tan": "tangent",
"\\cot": "cotangent",
"\\sum": "sum",
"/": "slash",
",": "comma",
".": "point",
"-": "negative",
"+": "plus",
"~": "tilde",
":": "colon",
"?": "question mark",
"'": "apostrophe",
"\\%": "percent",
" ": "space",
"\\ ": "space",
"\\$": "dollar sign",
"\\angle": "angle",
"\\degree": "degree",
"\\circ": "circle",
"\\vec": "vector",
"\\triangle": "triangle",
"\\pi": "pi",
"\\prime": "prime",
"\\infty": "infinity",
"\\alpha": "alpha",
"\\beta": "beta",
"\\gamma": "gamma",
"\\omega": "omega",
"\\theta": "theta",
"\\sigma": "sigma",
"\\lambda": "lambda",
"\\tau": "tau",
"\\Delta": "delta",
"\\delta": "delta",
"\\mu": "mu",
"\\rho": "rho",
"\\nabla": "del",
"\\ell": "ell",
"\\ldots": "dots",
// TODO: add entries for all accents
"\\hat": "hat",
"\\acute": "acute",
};
const powerMap = {
"prime": "prime",
"degree": "degrees",
"circle": "degrees",
"2": "squared",
"3": "cubed",
};
const openMap = {
"|": "open vertical bar",
".": "",
};
const closeMap = {
"|": "close vertical bar",
".": "",
};
const binMap = {
"+": "plus",
"-": "minus",
"\\pm": "plus minus",
"\\cdot": "dot",
"*": "times",
"/": "divided by",
"\\times": "times",
"\\div": "divided by",
"\\circ": "circle",
"\\bullet": "bullet",
};
const relMap = {
"=": "equals",
"\\approx": "approximately equals",
"≠": "does not equal",
"\\geq": "is greater than or equal to",
"\\ge": "is greater than or equal to",
"\\leq": "is less than or equal to",
"\\le": "is less than or equal to",
">": "is greater than",
"<": "is less than",
"\\leftarrow": "left arrow",
"\\Leftarrow": "left arrow",
"\\rightarrow": "right arrow",
"\\Rightarrow": "right arrow",
":": "colon",
};
const accentUnderMap = {
"\\underleftarrow": "left arrow",
"\\underrightarrow": "right arrow",
"\\underleftrightarrow": "left-right arrow",
"\\undergroup": "group",
"\\underlinesegment": "line segment",
"\\utilde": "tilde",
};
type NestedArray<T> = Array<T | NestedArray<T>>;
const buildString = (
str: string,
type: Atom | "normal",
a11yStrings: NestedArray<string>,
) => {
if (!str) {
return;
}
let ret;
if (type === "open") {
ret = str in openMap ? openMap[str] : stringMap[str] || str;
} else if (type === "close") {
ret = str in closeMap ? closeMap[str] : stringMap[str] || str;
} else if (type === "bin") {
ret = binMap[str] || str;
} else if (type === "rel") {
ret = relMap[str] || str;
} else {
ret = stringMap[str] || str;
}
// If the text to add is a number and there is already a string
// in the list and the last string is a number then we should
// combine them into a single number
if (
/^\d+$/.test(ret) &&
a11yStrings.length > 0 &&
// TODO(kevinb): check that the last item in a11yStrings is a string
// I think we might be able to drop the nested arrays, which would make
// this easier to type
// $FlowFixMe
/^\d+$/.test(a11yStrings[a11yStrings.length - 1])
) {
a11yStrings[a11yStrings.length - 1] += ret;
} else if (ret) {
a11yStrings.push(ret);
}
};
const buildRegion = (
a11yStrings: NestedArray<string>,
callback: (regionStrings: NestedArray<string>) => void,
) => {
const regionStrings: NestedArray<string> = [];
a11yStrings.push(regionStrings);
callback(regionStrings);
};
const handleObject = (
tree: AnyParseNode,
a11yStrings: NestedArray<string>,
atomType: Atom | "normal",
) => {
// Everything else is assumed to be an object...
switch (tree.type) {
case "accent": {
buildRegion(a11yStrings, (a11yStrings) => {
buildA11yStrings(tree.base, a11yStrings, atomType);
a11yStrings.push("with");
buildString(tree.label, "normal", a11yStrings);
a11yStrings.push("on top");
});
break;
}
case "accentUnder": {
buildRegion(a11yStrings, (a11yStrings) => {
buildA11yStrings(tree.base, a11yStrings, atomType);
a11yStrings.push("with");
buildString(accentUnderMap[tree.label], "normal", a11yStrings);
a11yStrings.push("underneath");
});
break;
}
case "accent-token": {
// Used internally by accent symbols.
break;
}
case "atom": {
const {text} = tree;
switch (tree.family) {
case "bin": {
buildString(text, "bin", a11yStrings);
break;
}
case "close": {
buildString(text, "close", a11yStrings);
break;
}
// TODO(kevinb): figure out what should be done for inner
case "inner": {
buildString(tree.text, "inner", a11yStrings);
break;
}
case "open": {
buildString(text, "open", a11yStrings);
break;
}
case "punct": {
buildString(text, "punct", a11yStrings);
break;
}
case "rel": {
buildString(text, "rel", a11yStrings);
break;
}
default: {
(tree.family: empty);
throw new Error(`"${tree.family}" is not a valid atom type`);
}
}
break;
}
case "color": {
const color = tree.color.replace(/katex-/, "");
buildRegion(a11yStrings, (regionStrings) => {
regionStrings.push("start color " + color);
buildA11yStrings(tree.body, regionStrings, atomType);
regionStrings.push("end color " + color);
});
break;
}
case "color-token": {
// Used by \color, \colorbox, and \fcolorbox but not directly rendered.
// It's a leaf node and has no children so just break.
break;
}
case "delimsizing": {
if (tree.delim && tree.delim !== ".") {
buildString(tree.delim, "normal", a11yStrings);
}
break;
}
case "genfrac": {
buildRegion(a11yStrings, (regionStrings) => {
// genfrac can have unbalanced delimiters
const {leftDelim, rightDelim} = tree;
// NOTE: Not sure if this is a safe assumption
// hasBarLine true -> fraction, false -> binomial
if (tree.hasBarLine) {
regionStrings.push("start fraction");
leftDelim && buildString(leftDelim, "open", regionStrings);
buildA11yStrings(tree.numer, regionStrings, atomType);
regionStrings.push("divided by");
buildA11yStrings(tree.denom, regionStrings, atomType);
rightDelim && buildString(rightDelim, "close", regionStrings);
regionStrings.push("end fraction");
} else {
regionStrings.push("start binomial");
leftDelim && buildString(leftDelim, "open", regionStrings);
buildA11yStrings(tree.numer, regionStrings, atomType);
regionStrings.push("over");
buildA11yStrings(tree.denom, regionStrings, atomType);
rightDelim && buildString(rightDelim, "close", regionStrings);
regionStrings.push("end binomial");
}
});
break;
}
case "hbox": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "kern": {
// No op: we don't attempt to present kerning information
// to the screen reader.
break;
}
case "leftright": {
buildRegion(a11yStrings, (regionStrings) => {
buildString(tree.left, "open", regionStrings);
buildA11yStrings(tree.body, regionStrings, atomType);
buildString(tree.right, "close", regionStrings);
});
break;
}
case "leftright-right": {
// TODO: double check that this is a no-op
break;
}
case "lap": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "mathord": {
buildString(tree.text, "normal", a11yStrings);
break;
}
case "op": {
const {body, name} = tree;
if (body) {
buildA11yStrings(body, a11yStrings, atomType);
} else if (name) {
buildString(name, "normal", a11yStrings);
}
break;
}
case "op-token": {
// Used internally by operator symbols.
buildString(tree.text, atomType, a11yStrings);
break;
}
case "ordgroup": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "overline": {
buildRegion(a11yStrings, function(a11yStrings) {
a11yStrings.push("start overline");
buildA11yStrings(tree.body, a11yStrings, atomType);
a11yStrings.push("end overline");
});
break;
}
case "pmb": {
a11yStrings.push("bold");
break;
}
case "phantom": {
a11yStrings.push("empty space");
break;
}
case "raisebox": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "rule": {
a11yStrings.push("rectangle");
break;
}
case "sizing": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "spacing": {
a11yStrings.push("space");
break;
}
case "styling": {
// We ignore the styling and just pass through the contents
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "sqrt": {
buildRegion(a11yStrings, (regionStrings) => {
const {body, index} = tree;
if (index) {
const indexString = flatten(
buildA11yStrings(index, [], atomType)).join(",");
if (indexString === "3") {
regionStrings.push("cube root of");
buildA11yStrings(body, regionStrings, atomType);
regionStrings.push("end cube root");
return;
}
regionStrings.push("root");
regionStrings.push("start index");
buildA11yStrings(index, regionStrings, atomType);
regionStrings.push("end index");
return;
}
regionStrings.push("square root of");
buildA11yStrings(body, regionStrings, atomType);
regionStrings.push("end square root");
});
break;
}
case "supsub": {
const {base, sub, sup} = tree;
let isLog = false;
if (base) {
buildA11yStrings(base, a11yStrings, atomType);
isLog = base.type === "op" && base.name === "\\log";
}
if (sub) {
const regionName = isLog ? "base" : "subscript";
buildRegion(a11yStrings, function(regionStrings) {
regionStrings.push(`start ${regionName}`);
buildA11yStrings(sub, regionStrings, atomType);
regionStrings.push(`end ${regionName}`);
});
}
if (sup) {
buildRegion(a11yStrings, function(regionStrings) {
const supString = flatten(
buildA11yStrings(sup, [], atomType)).join(",");
if (supString in powerMap) {
regionStrings.push(powerMap[supString]);
return;
}
regionStrings.push("start superscript");
buildA11yStrings(sup, regionStrings, atomType);
regionStrings.push("end superscript");
});
}
break;
}
case "text": {
// TODO: handle other fonts
if (tree.font === "\\textbf") {
buildRegion(a11yStrings, function(regionStrings) {
regionStrings.push("start bold text");
buildA11yStrings(tree.body, regionStrings, atomType);
regionStrings.push("end bold text");
});
break;
}
buildRegion(a11yStrings, function(regionStrings) {
regionStrings.push("start text");
buildA11yStrings(tree.body, regionStrings, atomType);
regionStrings.push("end text");
});
break;
}
case "textord": {
buildString(tree.text, atomType, a11yStrings);
break;
}
case "smash": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "enclose": {
// TODO: create a map for these.
// TODO: differentiate between a body with a single atom, e.g.
// "cancel a" instead of "start cancel, a, end cancel"
if (/cancel/.test(tree.label)) {
buildRegion(a11yStrings, function(regionStrings) {
regionStrings.push("start cancel");
buildA11yStrings(tree.body, regionStrings, atomType);
regionStrings.push("end cancel");
});
break;
} else if (/box/.test(tree.label)) {
buildRegion(a11yStrings, function(regionStrings) {
regionStrings.push("start box");
buildA11yStrings(tree.body, regionStrings, atomType);
regionStrings.push("end box");
});
break;
} else if (/sout/.test(tree.label)) {
buildRegion(a11yStrings, function(regionStrings) {
regionStrings.push("start strikeout");
buildA11yStrings(tree.body, regionStrings, atomType);
regionStrings.push("end strikeout");
});
break;
} else if (/phase/.test(tree.label)) {
buildRegion(a11yStrings, function(regionStrings) {
regionStrings.push("start phase angle");
buildA11yStrings(tree.body, regionStrings, atomType);
regionStrings.push("end phase angle");
});
break;
}
throw new Error(
`KaTeX-a11y: enclose node with ${tree.label} not supported yet`);
}
case "vcenter": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "vphantom": {
throw new Error("KaTeX-a11y: vphantom not implemented yet");
}
case "hphantom": {
throw new Error("KaTeX-a11y: hphantom not implemented yet");
}
case "operatorname": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "array": {
throw new Error("KaTeX-a11y: array not implemented yet");
}
case "raw": {
throw new Error("KaTeX-a11y: raw not implemented yet");
}
case "size": {
// Although there are nodes of type "size" in the parse tree, they have
// no semantic meaning and should be ignored.
break;
}
case "url": {
throw new Error("KaTeX-a11y: url not implemented yet");
}
case "tag": {
throw new Error("KaTeX-a11y: tag not implemented yet");
}
case "verb": {
buildString(`start verbatim`, "normal", a11yStrings);
buildString(tree.body, "normal", a11yStrings);
buildString(`end verbatim`, "normal", a11yStrings);
break;
}
case "environment": {
throw new Error("KaTeX-a11y: environment not implemented yet");
}
case "horizBrace": {
buildString(`start ${tree.label.slice(1)}`, "normal", a11yStrings);
buildA11yStrings(tree.base, a11yStrings, atomType);
buildString(`end ${tree.label.slice(1)}`, "normal", a11yStrings);
break;
}
case "infix": {
// All infix nodes are replace with other nodes.
break;
}
case "includegraphics": {
throw new Error("KaTeX-a11y: includegraphics not implemented yet");
}
case "font": {
// TODO: callout the start/end of specific fonts
// TODO: map \BBb{N} to "the naturals" or something like that
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "href": {
throw new Error("KaTeX-a11y: href not implemented yet");
}
case "cr": {
// This is used by environments.
throw new Error("KaTeX-a11y: cr not implemented yet");
}
case "underline": {
buildRegion(a11yStrings, function(a11yStrings) {
a11yStrings.push("start underline");
buildA11yStrings(tree.body, a11yStrings, atomType);
a11yStrings.push("end underline");
});
break;
}
case "xArrow": {
throw new Error("KaTeX-a11y: xArrow not implemented yet");
}
case "cdlabel": {
throw new Error("KaTeX-a11y: cdlabel not implemented yet");
}
case "cdlabelparent": {
throw new Error("KaTeX-a11y: cdlabelparent not implemented yet");
}
case "mclass": {
// \neq and \ne are macros so we let "htmlmathml" render the mathmal
// side of things and extract the text from that.
const atomType = tree.mclass.slice(1);
// $FlowFixMe: drop the leading "m" from the values in mclass
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
case "mathchoice": {
// TODO: track which which style we're using, e.g. dispaly, text, etc.
// default to text style if even that may not be the correct style
buildA11yStrings(tree.text, a11yStrings, atomType);
break;
}
case "htmlmathml": {
buildA11yStrings(tree.mathml, a11yStrings, atomType);
break;
}
case "middle": {
buildString(tree.delim, atomType, a11yStrings);
break;
}
case "internal": {
// internal nodes are never included in the parse tree
break;
}
case "html": {
buildA11yStrings(tree.body, a11yStrings, atomType);
break;
}
default:
(tree.type: empty);
throw new Error("KaTeX a11y un-recognized type: " + tree.type);
}
};
const buildA11yStrings = (
tree: AnyParseNode | AnyParseNode[],
a11yStrings: NestedArray<string> = [],
atomType: Atom | "normal",
) => {
if (tree instanceof Array) {
for (let i = 0; i < tree.length; i++) {
buildA11yStrings(tree[i], a11yStrings, atomType);
}
} else {
handleObject(tree, a11yStrings, atomType);
}
return a11yStrings;
};
const flatten = function(array) {
let result = [];
array.forEach(function(item) {
if (item instanceof Array) {
result = result.concat(flatten(item));
} else {
result.push(item);
}
});
return result;
};
const renderA11yString = function(
text: string,
settings?: SettingsOptions,
): string {
const tree = katex.__parse(text, settings);
const a11yStrings = buildA11yStrings(tree, [], "normal");
return flatten(a11yStrings).join(", ");
};
export default renderA11yString;

View File

@@ -0,0 +1,549 @@
/* eslint-disable max-len */
// @flow
import renderA11yString from "../render-a11y-string";
describe("renderA11yString", () => {
describe("basic expressions", () => {
test("simple addition", () => {
const result = renderA11yString("1 + 2");
expect(result).toMatchInlineSnapshot(`"1, plus, 2"`);
});
});
describe("accent", () => {
test("\\vec", () => {
const result = renderA11yString("\\vec{a}");
expect(result).toMatchInlineSnapshot(`"a, with, vector, on top"`);
});
test("\\acute{a}", () => {
const result = renderA11yString("\\acute{a}");
expect(result).toMatchInlineSnapshot(`"a, with, acute, on top"`);
});
test("\\hat{a}", () => {
const result = renderA11yString("\\hat{a}");
expect(result).toMatchInlineSnapshot(`"a, with, hat, on top"`);
});
});
describe("accentUnder", () => {
test("\\underleftarrow", () => {
const result = renderA11yString("\\underleftarrow{1+2}");
expect(result).toMatchInlineSnapshot(
`"1, plus, 2, with, left arrow, underneath"`,
);
});
test("\\underlinesegment", () => {
const result = renderA11yString("\\underlinesegment{1+2}");
expect(result).toMatchInlineSnapshot(
`"1, plus, 2, with, line segment, underneath"`,
);
});
});
describe("atom", () => {
test("punct", () => {
const result = renderA11yString("1, 2, 3");
expect(result).toMatchInlineSnapshot(`"1, comma, 2, comma, 3"`);
});
});
describe("color", () => {
test("\\color{red}", () => {
const result = renderA11yString("\\color{red}1+2");
expect(result).toMatchInlineSnapshot(
`"start color red, 1, plus, 2, end color red"`,
);
});
test("\\color{FF0000}", () => {
const result = renderA11yString("\\color{FF0000}1+2");
expect(result).toMatchInlineSnapshot(
`"start color #FF0000, 1, plus, 2, end color #FF0000"`,
);
});
// colorIsTextColor is an option added in KaTeX 0.9.0 for backward
// compatibility. It makes \color parse like \textcolor. We use it
// in the KA webapp, and need it here because the tests are written
// assuming it is set.
test("\\color{red} with {colorIsTextColor: true}", () => {
const result = renderA11yString("\\color{red}1+2", {
colorIsTextColor: true,
});
expect(result).toMatchInlineSnapshot(
`"start color red, 1, end color red, plus, 2"`,
);
});
test("\\textcolor{red}", () => {
const result = renderA11yString("\\textcolor{red}1+2");
expect(result).toMatchInlineSnapshot(
`"start color red, 1, end color red, plus, 2"`,
);
});
});
describe("delimiters", () => {
test("simple parens", () => {
const result = renderA11yString("(1 + 3)");
expect(result).toMatchInlineSnapshot(
`"left parenthesis, 1, plus, 3, right parenthesis"`,
);
});
test("simple brackets", () => {
const result = renderA11yString("[1 + 3]");
expect(result).toMatchInlineSnapshot(
`"open bracket, 1, plus, 3, close bracket"`,
);
});
test("nested parens", () => {
const result = renderA11yString("(a + (b + c))");
expect(result).toMatchInlineSnapshot(
`"left parenthesis, a, plus, left parenthesis, b, plus, c, right parenthesis, right parenthesis"`,
);
});
test("stretchy parens around fractions", () => {
const result = renderA11yString("\\left(\\frac{1}{x}\\right)");
expect(result).toMatchInlineSnapshot(
`"left parenthesis, start fraction, 1, divided by, x, end fraction, right parenthesis"`,
);
});
});
describe("delimsizing", () => {
test("\\bigl(1+2\\bigr)", () => {
const result = renderA11yString("\\bigl(1+2\\bigr)");
expect(result).toMatchInlineSnapshot(
`"left parenthesis, 1, plus, 2, right parenthesis"`,
);
});
});
describe("enclose", () => {
test("\\cancel", () => {
const result = renderA11yString("\\cancel{a}");
expect(result).toMatchInlineSnapshot(
`"start cancel, a, end cancel"`,
);
});
test("\\fbox", () => {
const result = renderA11yString("\\fbox{a}");
expect(result).toMatchInlineSnapshot(`"start box, a, end box"`);
});
test("\\boxed", () => {
const result = renderA11yString("\\boxed{a}");
expect(result).toMatchInlineSnapshot(`"start box, a, end box"`);
});
test("\\sout", () => {
const result = renderA11yString("\\sout{a}");
expect(result).toMatchInlineSnapshot(
`"start strikeout, a, end strikeout"`,
);
});
});
describe("phase", () => {
test("\\phase", () => {
const result = renderA11yString("\\phase{a}");
expect(result).toMatchInlineSnapshot(
`"start phase angle, a, end phase angle"`,
);
});
});
describe("exponents", () => {
test("simple exponent", () => {
const result = renderA11yString("e^x");
expect(result).toMatchInlineSnapshot(
`"e, start superscript, x, end superscript"`,
);
});
test("^{\\circ} => degrees", () => {
const result = renderA11yString("90^{\\circ}");
expect(result).toMatchInlineSnapshot(`"90, degrees"`);
});
test("^{\\degree} => degrees", () => {
const result = renderA11yString("90^{\\degree}");
expect(result).toMatchInlineSnapshot(`"90, degrees"`);
});
test("^{\\prime} => prime", () => {
const result = renderA11yString("f^{\\prime}");
expect(result).toMatchInlineSnapshot(`"f, prime"`);
});
test("^2 => squared", () => {
const result = renderA11yString("x^2");
expect(result).toMatchInlineSnapshot(`"x, squared"`);
});
test("^3 => cubed", () => {
const result = renderA11yString("x^3");
expect(result).toMatchInlineSnapshot(`"x, cubed"`);
});
test("log_2", () => {
const result = renderA11yString("\\log_2{x+1}");
expect(result).toMatchInlineSnapshot(
`"log, start base, 2, end base, x, plus, 1"`,
);
});
test("a_{n+1}", () => {
const result = renderA11yString("a_{n+1}");
expect(result).toMatchInlineSnapshot(
`"a, start subscript, n, plus, 1, end subscript"`,
);
});
});
describe("genfrac", () => {
test("simple fractions", () => {
const result = renderA11yString("\\frac{2}{3}");
expect(result).toMatchInlineSnapshot(
`"start fraction, 2, divided by, 3, end fraction"`,
);
});
test("nested fractions", () => {
const result = renderA11yString("\\frac{1}{1+\\frac{1}{x}}");
// TODO: this result is ambiguous, we need to fix this
expect(result).toMatchInlineSnapshot(
`"start fraction, 1, divided by, 1, plus, start fraction, 1, divided by, x, end fraction, end fraction"`,
);
});
test("binomials", () => {
const result = renderA11yString("\\binom{n}{k}");
// TODO: drop the parenthesis as they're not normally read
expect(result).toMatchInlineSnapshot(
`"start binomial, left parenthesis, n, over, k, right parenthesis, end binomial"`,
);
});
});
describe("horizBrace", () => {
test("\\overbrace", () => {
const result = renderA11yString("\\overbrace{1+2}");
expect(result).toMatchInlineSnapshot(
`"start overbrace, 1, plus, 2, end overbrace"`,
);
});
test("\\underbrace", () => {
const result = renderA11yString("\\underbrace{1+2}");
expect(result).toMatchInlineSnapshot(
`"start underbrace, 1, plus, 2, end underbrace"`,
);
});
});
describe("infix", () => {
test("\\over", () => {
const result = renderA11yString("a \\over b");
expect(result).toMatchInlineSnapshot(
`"start fraction, a, divided by, b, end fraction"`,
);
});
test("\\choose", () => {
const result = renderA11yString("a \\choose b");
expect(result).toMatchInlineSnapshot(
`"start binomial, left parenthesis, a, over, b, right parenthesis, end binomial"`,
);
});
test("\\above", () => {
const result = renderA11yString("a \\above{2pt} b");
expect(result).toMatchInlineSnapshot(
`"start fraction, a, divided by, b, end fraction"`,
);
});
});
describe("hbox", () => {
test("\\hbox", () => {
const result = renderA11yString("x+\\hbox{y}");
expect(result).toMatchInlineSnapshot(`"x, plus, y"`);
});
});
describe("inner", () => {
test("\\ldots", () => {
const result = renderA11yString("\\ldots");
expect(result).toMatchInlineSnapshot(`"dots"`);
});
});
describe("lap", () => {
test("\\llap", () => {
const result = renderA11yString("a\\llap{b}");
expect(result).toMatchInlineSnapshot(
`"a, start text, b, end text"`,
);
});
test("\\rlap", () => {
const result = renderA11yString("a\\rlap{b}");
expect(result).toMatchInlineSnapshot(
`"a, start text, b, end text"`,
);
});
});
describe("middle", () => {
test("\\middle", () => {
const result = renderA11yString("\\left(a\\middle|b\\right)");
expect(result).toMatchInlineSnapshot(
`"left parenthesis, a, vertical bar, b, right parenthesis"`,
);
});
});
describe("mod", () => {
test("\\mod", () => {
const result = renderA11yString("\\mod{23}");
// TODO: drop the "space"
// TODO: collate m, o, d... we should fix this inside of KaTeX since
// this affects the HTML and MathML output as well
expect(result).toMatchInlineSnapshot(`"space, m, o, d, 23"`);
});
});
describe("op", () => {
test("\\lim", () => {
const result = renderA11yString("\\lim{x+1}");
// TODO: add begin/end to track argument of operators
expect(result).toMatchInlineSnapshot(`"limit, x, plus, 1"`);
});
test("\\sin 2\\pi", () => {
const result = renderA11yString("\\sin{2\\pi}");
// TODO: add begin/end to track argument of operators
expect(result).toMatchInlineSnapshot(`"sine, 2, pi"`);
});
test("\\sum_{i=0}", () => {
const result = renderA11yString("\\sum_{i=0}");
expect(result).toMatchInlineSnapshot(
`"sum, start subscript, i, equals, 0, end subscript"`,
);
});
test("\u2211_{i=0}", () => {
const result = renderA11yString("\u2211_{i=0}");
expect(result).toMatchInlineSnapshot(
`"sum, start subscript, i, equals, 0, end subscript"`,
);
});
});
describe("operatorname", () => {
test("\\limsup", () => {
const result = renderA11yString("\\limsup");
// TODO: collate strings so that this is "lim, sup"
// NOTE: this also affect HTML and MathML output
expect(result).toMatchInlineSnapshot(`"l, i, m, s, u, p"`);
});
test("\\liminf", () => {
const result = renderA11yString("\\liminf");
expect(result).toMatchInlineSnapshot(`"l, i, m, i, n, f"`);
});
test("\\argmin", () => {
const result = renderA11yString("\\argmin");
expect(result).toMatchInlineSnapshot(`"a, r, g, m, i, n"`);
});
});
describe("overline", () => {
test("\\overline", () => {
const result = renderA11yString("\\overline{1+2}");
expect(result).toMatchInlineSnapshot(
`"start overline, 1, plus, 2, end overline"`,
);
});
});
describe("phantom", () => {
test("\\phantom", () => {
const result = renderA11yString("1+\\phantom{2}");
expect(result).toMatchInlineSnapshot(`"1, plus, empty space"`);
});
});
describe("raisebox", () => {
test("\\raisebox", () => {
const result = renderA11yString("x+\\raisebox{1em}{y}");
expect(result).toMatchInlineSnapshot(`"x, plus, y"`);
});
});
describe("relations", () => {
test("1 \\neq 2", () => {
const result = renderA11yString("1 \\neq 2");
expect(result).toMatchInlineSnapshot(`"1, does not equal, 2"`);
});
test("1 \\ne 2", () => {
const result = renderA11yString("1 \\ne 2");
expect(result).toMatchInlineSnapshot(`"1, does not equal, 2"`);
});
test("1 \\geq 2", () => {
const result = renderA11yString("1 \\geq 2");
expect(result).toMatchInlineSnapshot(
`"1, is greater than or equal to, 2"`,
);
});
test("1 \\ge 2", () => {
const result = renderA11yString("1 \\ge 2");
expect(result).toMatchInlineSnapshot(
`"1, is greater than or equal to, 2"`,
);
});
test("1 \\leq 2", () => {
const result = renderA11yString("1 \\leq 3");
expect(result).toMatchInlineSnapshot(
`"1, is less than or equal to, 3"`,
);
});
test("1 \\le 2", () => {
const result = renderA11yString("1 \\le 3");
expect(result).toMatchInlineSnapshot(
`"1, is less than or equal to, 3"`,
);
});
});
describe("rule", () => {
test("\\rule", () => {
const result = renderA11yString("\\rule{1em}{1em}");
expect(result).toMatchInlineSnapshot(`"rectangle"`);
});
});
describe("smash", () => {
test("1 + \\smash{2}", () => {
const result = renderA11yString("1 + \\smash{2}");
expect(result).toMatchInlineSnapshot(`"1, plus, 2"`);
});
});
describe("sqrt", () => {
test("square root", () => {
const result = renderA11yString("\\sqrt{x + 1}");
expect(result).toMatchInlineSnapshot(
`"square root of, x, plus, 1, end square root"`,
);
});
test("nest square root", () => {
const result = renderA11yString("\\sqrt{x + \\sqrt{y}}");
// TODO: this sounds ambiguous as well... we should probably say "start square root"
expect(result).toMatchInlineSnapshot(
`"square root of, x, plus, square root of, y, end square root, end square root"`,
);
});
test("cube root", () => {
const result = renderA11yString("\\sqrt[3]{x + 1}");
expect(result).toMatchInlineSnapshot(
`"cube root of, x, plus, 1, end cube root"`,
);
});
test("nth root", () => {
const result = renderA11yString("\\sqrt[n]{x + 1}");
expect(result).toMatchInlineSnapshot(
`"root, start index, n, end index"`,
);
});
});
describe("sizing", () => {
test("\\Huge is ignored", () => {
const result = renderA11yString("\\Huge{a+b}");
expect(result).toMatchInlineSnapshot(`"a, plus, b"`);
});
test("\\small is ignored", () => {
const result = renderA11yString("\\small{a+b}");
expect(result).toMatchInlineSnapshot(`"a, plus, b"`);
});
// We don't need to test all sizing commands since all style
// nodes are treated in the same way.
});
describe("styling", () => {
test("\\displaystyle is ignored", () => {
const result = renderA11yString("\\displaystyle{a+b}");
expect(result).toMatchInlineSnapshot(`"a, plus, b"`);
});
test("\\textstyle is ignored", () => {
const result = renderA11yString("\\textstyle{a+b}");
expect(result).toMatchInlineSnapshot(`"a, plus, b"`);
});
// We don't need to test all styling commands since all style
// nodes are treated in the same way.
});
describe("text", () => {
test("\\text", () => {
const result = renderA11yString("\\text{hello}");
expect(result).toMatchInlineSnapshot(
`"start text, h, e, l, l, o, end text"`,
);
});
test("\\textbf", () => {
const result = renderA11yString("\\textbf{hello}");
expect(result).toMatchInlineSnapshot(
`"start bold text, h, e, l, l, o, end bold text"`,
);
});
});
describe("underline", () => {
test("\\underline", () => {
const result = renderA11yString("\\underline{1+2}");
expect(result).toMatchInlineSnapshot(
`"start underline, 1, plus, 2, end underline"`,
);
});
});
describe("vcenter", () => {
test("\\vcenter", () => {
const result = renderA11yString("x+\\vcenter{y}");
expect(result).toMatchInlineSnapshot(`"x, plus, y"`);
});
});
describe("verb", () => {
test("\\verb", () => {
const result = renderA11yString("\\verb|hello|");
expect(result).toMatchInlineSnapshot(
`"start verbatim, hello, end verbatim"`,
);
});
});
});