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