mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-13 06:58:40 +00:00
feat: support {equation}, {equation*}, and {split} (#2369)
* Support {equation}, {equation*}, and {split} * Update screenshots * Allow {split} at top level * Move equation column number check to to ParseArray * Add token to ParseError * Sharpen parameters passed to parseArray * Add token information * Update an {array} spec in screenshotter * Adjust {array} screenshotter spec * Make a non-strict error call when {array} argument specifies too few columns. * Move context checks to a helper function.
This commit is contained in:
@@ -368,7 +368,8 @@ use `\ce` instead|
|
||||
|\eqcirc|$\eqcirc$||
|
||||
|\Eqcolon|$\Eqcolon$||
|
||||
|\eqcolon|$\eqcolon$||
|
||||
|{equation}|<span style="color:firebrick;">Not supported</span>|[Issue #445](https://github.com/KaTeX/KaTeX/issues/445)|
|
||||
|{equation}|$$\begin{equation}a = b + c\end{equation}$$|`\begin{equation}`<br> `a = b + c`<br>`\end{equation}`|
|
||||
|{equation*}|$$\begin{equation*}a = b + c\end{equation*}$$|`\begin{equation*}`<br> `a = b + c`<br>`\end{equation*}`|
|
||||
|{eqnarray}|<span style="color:firebrick;">Not supported</span>||
|
||||
|\Eqqcolon|$\Eqqcolon$||
|
||||
|\eqqcolon|$\eqqcolon$||
|
||||
@@ -965,7 +966,7 @@ use `\ce` instead|
|
||||
|\spades|$\spades$||
|
||||
|\spadesuit|$\spadesuit$||
|
||||
|\sphericalangle|$\sphericalangle$||
|
||||
|{split}|<span style="color:firebrick;">Not supported</span>|[Issue #1345](https://github.com/KaTeX/KaTeX/issues/1345)|
|
||||
|{split}|$$\begin{equation}\begin{split}a &=b+c\\&=e+f\end{split}\end{equation}$$|`\begin{equation}`<br>`\begin{split}`<br> `a &=b+c\\`<br> `&=e+f`<br>`\end{split}`<br>`\end{equation}`|
|
||||
|\sqcap|$\sqcap$||
|
||||
|\sqcup|$\sqcup$||
|
||||
|\square|$\square$||
|
||||
|
@@ -83,6 +83,7 @@ $( \big( \Big( \bigg( \Bigg($ `( \big( \Big( \bigg( \Bigg(`
|
||||
|$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$ |`\begin{pmatrix}`<br> `a & b \\`<br> `c & d`<br>`\end{pmatrix}` |$\begin{bmatrix} a & b \\ c & d \end{bmatrix}$ | `\begin{bmatrix}`<br> `a & b \\`<br> `c & d`<br>`\end{bmatrix}`
|
||||
|$\begin{vmatrix} a & b \\ c & d \end{vmatrix}$ |`\begin{vmatrix}`<br> `a & b \\`<br> `c & d`<br>`\end{vmatrix}` |$\begin{Vmatrix} a & b \\ c & d \end{Vmatrix}$ |`\begin{Vmatrix}`<br> `a & b \\`<br> `c & d`<br>`\end{Vmatrix}`
|
||||
|$\begin{Bmatrix} a & b \\ c & d \end{Bmatrix}$ |`\begin{Bmatrix}`<br> `a & b \\`<br> `c & d`<br>`\end{Bmatrix}`|$\def\arraystretch{1.5}\begin{array}{c:c:c} a & b & c \\ \hline d & e & f \\ \hdashline g & h & i \end{array}$|`\def\arraystretch{1.5}`<br> `\begin{array}{c:c:c}`<br> `a & b & c \\ \hline`<br> `d & e & f \\`<br> `\hdashline`<br> `g & h & i`<br>`\end{array}`
|
||||
|$$\begin{equation}\begin{split}a &=b+c\\&=e+f\end{split}\end{equation}$$ |`\begin{equation}`<br>`\begin{split}` `a &=b+c\\`<br> `&=e+f`<br>`\end{split}`<br>`\end{equation}` |$$\begin{equation*}\begin{split}a &=b+c\\&=e+f\end{split}\end{equation*}$$ |`\begin{equation*}`<br>`\begin{split}` `a &=b+c\\`<br> `&=e+f`<br>`\end{split}`<br>`\end{equation*}`
|
||||
|$$\begin{align} a&=b+c \\ d+e&=f \end{align}$$ |`\begin{align}`<br> `a&=b+c \\`<br> `d+e&=f`<br>`\end{align}`|$$\begin{alignat}{2}10&x+&3&y=2\\3&x+&13&y=4\end{alignat}$$ |
|
||||
|$$\begin{align*} a&=b+c \\ d+e&=f \end{align*}$$ |`\begin{align*}`<br> `a&=b+c \\`<br> `d+e&=f`<br>`\end{align*}`|$\begin{aligned} a&=b+c \\ d+e&=f \end{aligned}$ |`\begin{aligned}`<br> `a&=b+c \\`<br> `d+e&=f`<br>`\end{aligned}`|
|
||||
`\begin{alignedat}{2}`<br> `10&x+ &3&y = 2 \\`<br> ` 3&x+&13&y = 4`<br>`\end{alignedat}`|$\begin{alignedat}{2}10&x+&3&y=2\\3&x+&13&y=4\end{alignedat}$ |`\begin{alignedat}{2}`<br> `10&x+ &3&y = 2 \\`<br> ` 3&x+&13&y = 4`<br>`\end{alignedat}`
|
||||
|
@@ -29,6 +29,7 @@ export type AlignSpec = { type: "separator", separator: string } | {
|
||||
// Type to indicate column separation in MathML
|
||||
export type ColSeparationType = "align" | "alignat" | "gather" | "small";
|
||||
|
||||
// Helper functions
|
||||
function getHLines(parser: Parser): boolean[] {
|
||||
// Return an array. The array length = number of hlines.
|
||||
// Each element in the array tells if the line is dashed.
|
||||
@@ -44,6 +45,16 @@ function getHLines(parser: Parser): boolean[] {
|
||||
return hlineInfo;
|
||||
}
|
||||
|
||||
const validateAmsEnvironmentContext = context => {
|
||||
const settings = context.parser.settings;
|
||||
if (!settings.displayMode) {
|
||||
throw new ParseError(`{${context.envName}} cannot be used inline.`);
|
||||
} else if (settings.strict && !settings.topEnv) {
|
||||
settings.reportNonstrict("textEnv",
|
||||
`{${context.envName}} called from math mode.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse the body of the environment, with rows delimited by \\ and
|
||||
* columns delimited by &, and create a nested list in row-major order
|
||||
@@ -59,6 +70,8 @@ function parseArray(
|
||||
arraystretch,
|
||||
colSeparationType,
|
||||
addEqnNum,
|
||||
singleRow,
|
||||
maxNumCols,
|
||||
leqno,
|
||||
}: {|
|
||||
hskipBeforeAndAfter?: boolean,
|
||||
@@ -67,13 +80,19 @@ function parseArray(
|
||||
arraystretch?: number,
|
||||
colSeparationType?: ColSeparationType,
|
||||
addEqnNum?: boolean,
|
||||
singleRow?: boolean,
|
||||
maxNumCols?: number,
|
||||
leqno?: boolean,
|
||||
|},
|
||||
style: StyleStr,
|
||||
): ParseNode<"array"> {
|
||||
// Parse body of array with \\ temporarily mapped to \cr
|
||||
parser.gullet.beginGroup();
|
||||
parser.gullet.macros.set("\\\\", "\\cr");
|
||||
if (singleRow) {
|
||||
parser.gullet.macros.set("\\\\", ""); // {equation} acts this way.
|
||||
} else {
|
||||
parser.gullet.macros.set("\\\\", "\\cr");
|
||||
}
|
||||
|
||||
// Get current arraystretch if it's not set by the environment
|
||||
if (!arraystretch) {
|
||||
@@ -122,6 +141,17 @@ function parseArray(
|
||||
row.push(cell);
|
||||
const next = parser.fetch().text;
|
||||
if (next === "&") {
|
||||
if (maxNumCols && row.length === maxNumCols) {
|
||||
if (singleRow || colSeparationType) {
|
||||
// {equation} or {split}
|
||||
throw new ParseError("Too many tab characters: &",
|
||||
parser.nextToken);
|
||||
} else {
|
||||
// {array} environment
|
||||
parser.settings.reportNonstrict("textEnv", "Too few columns " +
|
||||
"specified in the {array} column argument.");
|
||||
}
|
||||
}
|
||||
parser.consume();
|
||||
} else if (next === "\\end") {
|
||||
// Arrays terminate newlines with `\crcr` which consumes a `\cr` if
|
||||
@@ -136,6 +166,9 @@ function parseArray(
|
||||
}
|
||||
break;
|
||||
} else if (next === "\\cr") {
|
||||
if (singleRow) {
|
||||
throw new ParseError("Misplaced \\cr.", parser.nextToken);
|
||||
}
|
||||
const cr = assertNodeType(parser.parseFunction(), "cr");
|
||||
rowGaps.push(cr.size);
|
||||
|
||||
@@ -580,14 +613,7 @@ const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
|
||||
// Convenience function for align, align*, aligned, alignat, alignat*, alignedat.
|
||||
const alignedHandler = function(context, args) {
|
||||
if (context.envName.indexOf("ed") === -1) {
|
||||
// Check if this environment call is allowed.
|
||||
const settings = context.parser.settings;
|
||||
if (!settings.displayMode) {
|
||||
throw new ParseError(`{${context.envName}} cannot be used inline.`);
|
||||
} else if (settings.strict && !settings.topEnv) {
|
||||
settings.reportNonstrict("textEnv",
|
||||
`{${context.envName}} called from math mode.`);
|
||||
}
|
||||
validateAmsEnvironmentContext(context);
|
||||
}
|
||||
const cols = [];
|
||||
const separationType = context.envName.indexOf("at") > -1 ? "alignat" : "align";
|
||||
@@ -597,6 +623,7 @@ const alignedHandler = function(context, args) {
|
||||
addJot: true,
|
||||
addEqnNum: context.envName === "align" || context.envName === "alignat",
|
||||
colSeparationType: separationType,
|
||||
maxNumCols: context.envName === "split" ? 2 : undefined,
|
||||
leqno: context.parser.settings.leqno,
|
||||
},
|
||||
"display"
|
||||
@@ -712,6 +739,7 @@ defineEnvironment({
|
||||
const res = {
|
||||
cols,
|
||||
hskipBeforeAndAfter: true, // \@preamble in lttab.dtx
|
||||
maxNumCols: cols.length,
|
||||
};
|
||||
return parseArray(context.parser, res, dCellStyle(context.envName));
|
||||
},
|
||||
@@ -876,7 +904,7 @@ defineEnvironment({
|
||||
// so that \strut@ is the same as \strut.
|
||||
defineEnvironment({
|
||||
type: "array",
|
||||
names: ["align", "align*", "aligned"],
|
||||
names: ["align", "align*", "aligned", "split"],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
},
|
||||
@@ -896,13 +924,7 @@ defineEnvironment({
|
||||
},
|
||||
handler(context) {
|
||||
if (utils.contains(["gather", "gather*"], context.envName)) {
|
||||
const settings = context.parser.settings;
|
||||
if (!settings.displayMode) {
|
||||
throw new ParseError(`{${context.envName}} cannot be used inline.`);
|
||||
} else if (settings.strict && !settings.topEnv) {
|
||||
settings.reportNonstrict("textEnv",
|
||||
`{${context.envName}} called from math mode.`);
|
||||
}
|
||||
validateAmsEnvironmentContext(context);
|
||||
}
|
||||
const res = {
|
||||
cols: [{
|
||||
@@ -934,6 +956,26 @@ defineEnvironment({
|
||||
mathmlBuilder,
|
||||
});
|
||||
|
||||
defineEnvironment({
|
||||
type: "array",
|
||||
names: ["equation", "equation*"],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
},
|
||||
handler(context) {
|
||||
validateAmsEnvironmentContext(context);
|
||||
const res = {
|
||||
addEqnNum: context.envName === "equation",
|
||||
singleRow: true,
|
||||
maxNumCols: 1,
|
||||
leqno: context.parser.settings.leqno,
|
||||
};
|
||||
return parseArray(context.parser, res, "display");
|
||||
},
|
||||
htmlBuilder,
|
||||
mathmlBuilder,
|
||||
});
|
||||
|
||||
// Catch \hline outside array environment
|
||||
defineFunction({
|
||||
type: "text", // Doesn't matter what this is.
|
||||
|
@@ -2727,8 +2727,6 @@ describe("An aligned environment", function() {
|
||||
});
|
||||
|
||||
describe("AMS environments", function() {
|
||||
const nonStrictDisplay = new Settings({displayMode: true, strict: false});
|
||||
|
||||
it("should fail outside display mode", () => {
|
||||
expect`\begin{gather}a+b\\c+d\end{gather}`.not.toParse(nonstrictSettings);
|
||||
expect`\begin{gather*}a+b\\c+d\end{gather*}`.not.toParse(nonstrictSettings);
|
||||
@@ -2736,8 +2734,11 @@ describe("AMS environments", function() {
|
||||
expect`\begin{align*}a&=b+c\\d+e&=f\end{align*}`.not.toParse(nonstrictSettings);
|
||||
expect`\begin{alignat}{2}10&x+ &3&y = 2\\3&x+&13&y = 4\end{alignat}`.not.toParse(nonstrictSettings);
|
||||
expect`\begin{alignat*}{2}10&x+ &3&y = 2\\3&x+&13&y = 4\end{alignat*}`.not.toParse(nonstrictSettings);
|
||||
expect`\begin{equation}a=b+c\end{equation}`.not.toParse(nonstrictSettings);
|
||||
expect`\begin{split}a &=b+c\\&=e+f\end{split}`.not.toParse(nonstrictSettings);
|
||||
});
|
||||
|
||||
const nonStrictDisplay = new Settings({displayMode: true, strict: false});
|
||||
it("should build if in non-strict display mode", () => {
|
||||
expect`\begin{gather}a+b\\c+d\end{gather}`.toBuild(nonStrictDisplay);
|
||||
expect`\begin{gather*}a+b\\c+d\end{gather*}`.toBuild(nonStrictDisplay);
|
||||
@@ -2745,6 +2746,22 @@ describe("AMS environments", function() {
|
||||
expect`\begin{align*}a&=b+c\\d+e&=f\end{align*}`.toBuild(nonStrictDisplay);
|
||||
expect`\begin{alignat}{2}10&x+ &3&y = 2\\3&x+&13&y = 4\end{alignat}`.toBuild(nonStrictDisplay);
|
||||
expect`\begin{alignat*}{2}10&x+ &3&y = 2\\3&x+&13&y = 4\end{alignat*}`.toBuild(nonStrictDisplay);
|
||||
expect`\begin{equation}a=b+c\end{equation}`.toBuild(nonStrictDisplay);
|
||||
expect`\begin{equation}\begin{split}a &=b+c\\&=e+f\end{split}\end{equation}`.toBuild(nonStrictDisplay);
|
||||
expect`\begin{split}a &=b+c\\&=e+f\end{split}`.toBuild(nonStrictDisplay);
|
||||
});
|
||||
|
||||
it("{equation} should fail if argument contains two rows.", () => {
|
||||
expect`\begin{equation}a=\cr b+c\end{equation}`.not.toParse(nonStrictDisplay);
|
||||
});
|
||||
it("{equation} should fail if argument contains two columns.", () => {
|
||||
expect`\begin{equation}a &=b+c\end{equation}`.not.toBuild(nonStrictDisplay);
|
||||
});
|
||||
it("{split} should fail if argument contains three columns.", () => {
|
||||
expect`\begin{equation}\begin{split}a &=b &+c\\&=e &+f\end{split}\end{equation}`.not.toBuild(nonStrictDisplay);
|
||||
});
|
||||
it("{array} should fail if body contains more columns than specification.", () => {
|
||||
expect`\begin{array}{2}a & b & c\\d & e f\end{array}`.not.toBuild(nonStrictDisplay);
|
||||
});
|
||||
});
|
||||
|
||||
|
BIN
test/screenshotter/images/Equation-chrome.png
Normal file
BIN
test/screenshotter/images/Equation-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
test/screenshotter/images/Equation-firefox.png
Normal file
BIN
test/screenshotter/images/Equation-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@@ -15,7 +15,7 @@
|
||||
|
||||
Accents: \vec{A}\vec{x}\vec x^2\vec{x}_2^2\vec{A}^2\vec{xA}^2
|
||||
AccentsText: |
|
||||
\begin{array}{l}
|
||||
\begin{array}{lccccc}
|
||||
\text{\'\i} & \text{\.\i} & \text{\`\i} & \text{\"\i} & \text{\H\i} & \text{\r\i} \\
|
||||
\text{\'\j} & \text{\.\j} & \text{\`\j} & \text{\"\j} & \text{\H\j} & \text{\r\j} \\
|
||||
\text{\'a} & \text{\.a} & \text{\`a} & \text{\"a} & \text{\H{a}} & \text{\r{a}} \\
|
||||
@@ -129,6 +129,9 @@ Dots: |
|
||||
\cdots;\dots+\dots\int\dots,\dots \\
|
||||
\cdots{};\ldots+\ldots\int\ldots,\ldots
|
||||
\end{array}
|
||||
Equation:
|
||||
tex: \begin{equation}\begin{split}a& =b+c-d \\ & \quad +e-f \\ & =g+h \\ & =i \end{split}\end{equation}
|
||||
display: 1
|
||||
Exponents: a^{a^a_a}_{a^a_a}
|
||||
ExtensibleArrows: |
|
||||
\begin{array}{l}
|
||||
|
Reference in New Issue
Block a user