mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-06 03:38:39 +00:00
feat: Support {CD} (#2396)
* Support {CD} * Edit screenshotter test to fit on one page * Update screenshots * Remove bogus Safari screenshot * Edit documentation to avoid tag conflicts and explain delimiters * Add type annotations * Add bogus safari screenshot * Update with real Safari screenshot * Set label vertical alignment * Revise call to parseExpression() per PR 2085 changes to macro parsing * Update Firefox screenshot * Pick up review comments * Add unit tests and snapshot. * Tighten up label collection. * Better loop index * remove extra space * Picked up comments. Added a parse check. Added a test. Co-authored-by: ylemkimon <y@ylem.kim> Co-authored-by: Kevin Barabash <kevinb@khanacademy.org> Co-authored-by: Kevin Barabash <kevinb7@gmail.com>
This commit is contained in:
@@ -647,6 +647,14 @@ const handleObject = (
|
||||
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.
|
||||
|
@@ -215,7 +215,7 @@ table td {
|
||||
|\cap|$\cap$||
|
||||
|{cases}|$\begin{cases}a&\text{if }b\\c&\text{if }d\end{cases}$|`\begin{cases}`<br> `a &\text{if } b \\`<br> `c &\text{if } d`<br>`\end{cases}`|
|
||||
|\cases|<span style="color:firebrick;">Not supported</span>|see `{cases}`|
|
||||
|{CD}|<span style="color:firebrick;">Not supported</span>||
|
||||
|{CD}|$$\begin{CD}A @>a>> B \\@VbVV @AAcA\\C @= D\end{CD}$$|`\begin{CD}`<br> `A @>a>> B \\`<br>`@VbVV @AAcA \\`<br> `C @= D`<br>`\end{CD}`|
|
||||
|\cdot|$\cdot$||
|
||||
|\cdotp|$\cdotp$||
|
||||
|\cdots|$\cdots$||
|
||||
|
@@ -83,17 +83,24 @@ $( \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}`
|
||||
|$$\begin{gather} a=b \\ e=b+c \end{gather}$$ |`\begin{gather}`<br> `a=b \\ `<br> `e=b+c`<br>`\end{gather}`|$\begin{gathered} a=b \\ e=b+c \end{gathered}$ |`\begin{gathered}`<br> `a=b \\ `<br> `e=b+c`<br>`\end{gathered}`|
|
||||
|$x = \begin{cases} a &\text{if } b \\ c &\text{if } d \end{cases}$ |`x = \begin{cases}`<br> `a &\text{if } b \\`<br> `c &\text{if } d`<br>`\end{cases}`|$\begin{rcases} a &\text{if } b \\ c &\text{if } d \end{rcases}⇒…$ |`\begin{rcases}`<br> `a &\text{if } b \\`<br> `c &\text{if } d`<br>`\end{rcases}⇒…`|
|
||||
|$\begin{smallmatrix} a & b \\ c & d \end{smallmatrix}$ | `\begin{smallmatrix}`<br> `a & b \\`<br> `c & d`<br>`\end{smallmatrix}` |||
|
||||
|$\begin{smallmatrix} a & b \\ c & d \end{smallmatrix}$ | `\begin{smallmatrix}`<br> `a & b \\`<br> `c & d`<br>`\end{smallmatrix}` |$$\begin{CD}A @>a>> B \\@VbVV @AAcA\\C @= D\end{CD}$$ |`\begin{CD}`<br> `A @>a>> B \\`<br>`@VbVV @AAcA \\`<br> `C @= D`<br>`\\end{CD}`|
|
||||
|
||||
|||||
|
||||
|:---------------------|:---------------------|
|
||||
|$$\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{alignedat}{2}`<br> `10&x+ &3&y = 2 \\`<br> ` 3&x+&13&y = 4`<br>`\end{alignedat}`
|
||||
|`\begin{equation}`<br>`\begin{split}` `a &=b+c\\`<br> `&=e+f`<br>`\end{split}`<br>`\end{equation}`
|
||||
|$$\begin{gather} a=b \\ e=b+c \end{gather}$$ |`\begin{gather}`<br> `a=b \\ `<br> `e=b+c`<br>`\end{gather}`
|
||||
|
||||
</div>
|
||||
|
||||
KaTeX also supports `darray`, `dcases`, and `drcases`, which apply `displaystyle`.
|
||||
KaTeX also supports:
|
||||
* `darray`, `dcases`, and `drcases`, which render in `displaystyle`
|
||||
* `align*`, `alignat*`, `gather*`, and `equation*`, which omit equation numbers
|
||||
* `aligned`, `alignedat`, and `gathered`, which LaTeX will render while in math mode
|
||||
|
||||
Environments `align`, `alignat`, `CD`, `equation`, and `gather` (and their starred versions) may be called from display mode. This usually means that they may be called from within `$$…$$` delimiters or `\[…\]` delimiters. They may not be called from within `$…$` or `\(…\)` delimiters. In the KaTeX auto-render extension, as in LaTeX, these environments are themselves are a way to change from text mode to math display mode, and `\[…\]` delimiters are not necessary.
|
||||
|
||||
And KaTeX supports `matrix*`, `pmatrix*`, `bmatrix*`, `Bmatrix*`, `vmatrix*`, and `Vmatrix*`, which take an optional argument to set column alignment, as in `\begin{matrix}[1]`, `\begin{matrix}[c]`, or `\begin{matrix}[r]`.
|
||||
|
||||
@@ -103,6 +110,8 @@ The `{array}` environment supports `|` and `:` vertical separators.
|
||||
|
||||
The `{array}` environment does not yet support `\cline` or `\multicolumn`.
|
||||
|
||||
`\tag` can not yet be applied to individual environment rows.
|
||||
|
||||
<div class="katex-hopscotch">
|
||||
|
||||
## HTML
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import buildCommon from "../buildCommon";
|
||||
import Style from "../Style";
|
||||
import defineEnvironment from "../defineEnvironment";
|
||||
import {parseCD} from "./cd";
|
||||
import defineFunction from "../defineFunction";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
import ParseError from "../ParseError";
|
||||
@@ -27,7 +28,7 @@ export type AlignSpec = { type: "separator", separator: string } | {
|
||||
};
|
||||
|
||||
// Type to indicate column separation in MathML
|
||||
export type ColSeparationType = "align" | "alignat" | "gather" | "small";
|
||||
export type ColSeparationType = "align" | "alignat" | "gather" | "small" | "CD";
|
||||
|
||||
// Helper functions
|
||||
function getHLines(parser: Parser): boolean[] {
|
||||
@@ -256,7 +257,9 @@ const htmlBuilder: HtmlBuilder<"array"> = function(group, options) {
|
||||
}
|
||||
|
||||
// Vertical spacing
|
||||
const baselineskip = 12 * pt; // see size10.clo
|
||||
const baselineskip = group.colSeparationType === "CD"
|
||||
? calculateSize({number: 3, unit: "ex"}, options)
|
||||
: 12 * pt; // see size10.clo
|
||||
// Default \jot from ltmath.dtx
|
||||
// TODO(edemaine): allow overriding \jot via \setlength (#687)
|
||||
const jot = 3 * pt;
|
||||
@@ -516,7 +519,7 @@ const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
|
||||
const gap = (group.arraystretch === 0.5)
|
||||
? 0.1 // {smallmatrix}, {subarray}
|
||||
: 0.16 + group.arraystretch - 1 + (group.addJot ? 0.09 : 0);
|
||||
table.setAttribute("rowspacing", gap + "em");
|
||||
table.setAttribute("rowspacing", gap.toFixed(4) + "em");
|
||||
|
||||
// MathML table lines go only between cells.
|
||||
// To place a line on an edge we'll use <menclose>, if necessary.
|
||||
@@ -580,6 +583,8 @@ const mathmlBuilder: MathMLBuilder<"array"> = function(group, options) {
|
||||
table.setAttribute("columnspacing", "0em");
|
||||
} else if (group.colSeparationType === "small") {
|
||||
table.setAttribute("columnspacing", "0.2778em");
|
||||
} else if (group.colSeparationType === "CD") {
|
||||
table.setAttribute("columnspacing", "0.5em");
|
||||
} else {
|
||||
table.setAttribute("columnspacing", "1em");
|
||||
}
|
||||
@@ -1016,6 +1021,20 @@ defineEnvironment({
|
||||
mathmlBuilder,
|
||||
});
|
||||
|
||||
defineEnvironment({
|
||||
type: "array",
|
||||
names: ["CD"],
|
||||
props: {
|
||||
numArgs: 0,
|
||||
},
|
||||
handler(context) {
|
||||
validateAmsEnvironmentContext(context);
|
||||
return parseCD(context.parser);
|
||||
},
|
||||
htmlBuilder,
|
||||
mathmlBuilder,
|
||||
});
|
||||
|
||||
// Catch \hline outside array environment
|
||||
defineFunction({
|
||||
type: "text", // Doesn't matter what this is.
|
||||
|
312
src/environments/cd.js
Normal file
312
src/environments/cd.js
Normal file
@@ -0,0 +1,312 @@
|
||||
// @flow
|
||||
import buildCommon from "../buildCommon";
|
||||
import defineFunction from "../defineFunction";
|
||||
import mathMLTree from "../mathMLTree";
|
||||
import * as html from "../buildHTML";
|
||||
import * as mml from "../buildMathML";
|
||||
import {assertSymbolNodeType} from "../parseNode";
|
||||
import ParseError from "../ParseError";
|
||||
|
||||
import type Parser from "../Parser";
|
||||
import type {ParseNode, AnyParseNode} from "../parseNode";
|
||||
|
||||
const cdArrowFunctionName = {
|
||||
">": "\\\\cdrightarrow",
|
||||
"<": "\\\\cdleftarrow",
|
||||
"=": "\\\\cdlongequal",
|
||||
"A": "\\uparrow",
|
||||
"V": "\\downarrow",
|
||||
"|": "\\Vert",
|
||||
".": "no arrow",
|
||||
};
|
||||
|
||||
const newCell = () => {
|
||||
// Create an empty cell, to be filled below with parse nodes.
|
||||
// The parseTree from this module must be constructed like the
|
||||
// one created by parseArray(), so an empty CD cell must
|
||||
// be a ParseNode<"styling">. And CD is always displaystyle.
|
||||
// So these values are fixed and flow can do implicit typing.
|
||||
return {type: "styling", body: [], mode: "math", style: "display"};
|
||||
};
|
||||
|
||||
const isStartOfArrow = (node: AnyParseNode) => {
|
||||
return (node.type === "textord" && node.text === "@");
|
||||
};
|
||||
|
||||
const isLabelEnd = (node: AnyParseNode, endChar: string): boolean => {
|
||||
return ((node.type === "mathord" || node.type === "atom") &&
|
||||
node.text === endChar);
|
||||
};
|
||||
|
||||
function cdArrow(
|
||||
arrowChar: string,
|
||||
labels: ParseNode<"ordgroup">[],
|
||||
parser: Parser
|
||||
): AnyParseNode {
|
||||
// Return a parse tree of an arrow and its labels.
|
||||
// This acts in a way similar to a macro expansion.
|
||||
const funcName = cdArrowFunctionName[arrowChar];
|
||||
switch (funcName) {
|
||||
case "\\\\cdrightarrow":
|
||||
case "\\\\cdleftarrow":
|
||||
return parser.callFunction(
|
||||
funcName, [labels[0]], [labels[1]]
|
||||
);
|
||||
case "\\uparrow":
|
||||
case "\\downarrow": {
|
||||
const leftLabel = parser.callFunction(
|
||||
"\\\\cdleft", [labels[0]], []
|
||||
);
|
||||
const bareArrow = {
|
||||
type: "atom",
|
||||
text: funcName,
|
||||
mode: "math",
|
||||
family: "rel",
|
||||
};
|
||||
const sizedArrow = parser.callFunction("\\Big", [bareArrow], []);
|
||||
const rightLabel = parser.callFunction(
|
||||
"\\\\cdright", [labels[1]], []
|
||||
);
|
||||
const arrowGroup = {
|
||||
type: "ordgroup",
|
||||
mode: "math",
|
||||
body: [leftLabel, sizedArrow, rightLabel],
|
||||
};
|
||||
return parser.callFunction("\\\\cdparent", [arrowGroup], []);
|
||||
}
|
||||
case "\\\\cdlongequal":
|
||||
return parser.callFunction("\\\\cdlongequal", [], []);
|
||||
case "\\Vert": {
|
||||
const arrow = {type: "textord", text: "\\Vert", mode: "math"};
|
||||
return parser.callFunction("\\Big", [arrow], []);
|
||||
}
|
||||
default:
|
||||
return {type: "textord", text: " ", mode: "math"};
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCD(parser: Parser): ParseNode<"array"> {
|
||||
// Get the array's parse nodes with \\ temporarily mapped to \cr.
|
||||
const parsedRows: AnyParseNode[][] = [];
|
||||
parser.gullet.beginGroup();
|
||||
parser.gullet.macros.set("\\cr", "\\\\\\relax");
|
||||
parser.gullet.beginGroup();
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
// Get the parse nodes for the next row.
|
||||
parsedRows.push(parser.parseExpression(false, "\\\\"));
|
||||
parser.gullet.endGroup();
|
||||
parser.gullet.beginGroup();
|
||||
const next = parser.fetch().text;
|
||||
if (next === "&" || next === "\\\\") {
|
||||
parser.consume();
|
||||
} else if (next === "\\end") {
|
||||
if (parsedRows[parsedRows.length - 1].length === 0) {
|
||||
parsedRows.pop(); // final row ended in \\
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
throw new ParseError("Expected \\\\ or \\cr or \\end",
|
||||
parser.nextToken);
|
||||
}
|
||||
}
|
||||
|
||||
let row = [];
|
||||
const body = [row];
|
||||
|
||||
// Loop thru the parse nodes. Collect them into cells and arrows.
|
||||
for (let i = 0; i < parsedRows.length; i++) {
|
||||
// Start a new row.
|
||||
const rowNodes = parsedRows[i];
|
||||
// Create the first cell.
|
||||
let cell = newCell();
|
||||
|
||||
for (let j = 0; j < rowNodes.length; j++) {
|
||||
if (!isStartOfArrow(rowNodes[j])) {
|
||||
// If a parseNode is not an arrow, it goes into a cell.
|
||||
cell.body.push(rowNodes[j]);
|
||||
} else {
|
||||
// Parse node j is an "@", the start of an arrow.
|
||||
// Before starting on the arrow, push the cell into `row`.
|
||||
row.push(cell);
|
||||
|
||||
// Now collect parseNodes into an arrow.
|
||||
// The character after "@" defines the arrow type.
|
||||
j += 1;
|
||||
const arrowChar = assertSymbolNodeType(rowNodes[j]).text;
|
||||
|
||||
// Create two empty label nodes. We may or may not use them.
|
||||
const labels: ParseNode<"ordgroup">[] = new Array(2);
|
||||
labels[0] = {type: "ordgroup", mode: "math", body: []};
|
||||
labels[1] = {type: "ordgroup", mode: "math", body: []};
|
||||
|
||||
// Process the arrow.
|
||||
if ("=|.".indexOf(arrowChar) > -1) {
|
||||
// Three "arrows", ``@=`, `@|`, and `@.`, do not take labels.
|
||||
// Do nothing here.
|
||||
} else if ("<>AV".indexOf(arrowChar) > -1) {
|
||||
// Four arrows, `@>>>`, `@<<<`, `@AAA`, and `@VVV`, each take
|
||||
// two optional labels. E.g. the right-point arrow syntax is
|
||||
// really: @>{optional label}>{optional label}>
|
||||
// Collect parseNodes into labels.
|
||||
for (let labelNum = 0; labelNum < 2; labelNum++) {
|
||||
let inLabel = true;
|
||||
for (let k = j + 1; k < rowNodes.length; k++) {
|
||||
if (isLabelEnd(rowNodes[k], arrowChar)) {
|
||||
inLabel = false;
|
||||
j = k;
|
||||
break;
|
||||
}
|
||||
if (isStartOfArrow(rowNodes[k])) {
|
||||
throw new ParseError("Missing a " + arrowChar +
|
||||
" character to complete a CD arrow.", rowNodes[k]);
|
||||
}
|
||||
|
||||
labels[labelNum].body.push(rowNodes[k]);
|
||||
}
|
||||
if (inLabel) {
|
||||
// isLabelEnd never returned a true.
|
||||
throw new ParseError("Missing a " + arrowChar +
|
||||
" character to complete a CD arrow.", rowNodes[j]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new ParseError(`Expected one of "<>AV=|." after @`,
|
||||
rowNodes[j]);
|
||||
}
|
||||
|
||||
// Now join the arrow to its labels.
|
||||
const arrow: AnyParseNode = cdArrow(arrowChar, labels, parser);
|
||||
|
||||
// Wrap the arrow in ParseNode<"styling">.
|
||||
// This is done to match parseArray() behavior.
|
||||
const wrappedArrow = {
|
||||
type: "styling",
|
||||
body: [arrow],
|
||||
mode: "math",
|
||||
style: "display", // CD is always displaystyle.
|
||||
};
|
||||
row.push(wrappedArrow);
|
||||
// In CD's syntax, cells are implicit. That is, everything that
|
||||
// is not an arrow gets collected into a cell. So create an empty
|
||||
// cell now. It will collect upcoming parseNodes.
|
||||
cell = newCell();
|
||||
}
|
||||
}
|
||||
if (i % 2 === 0) {
|
||||
// Even-numbered rows consist of: cell, arrow, cell, arrow, ... cell
|
||||
// The last cell is not yet pushed into `row`, so:
|
||||
row.push(cell);
|
||||
} else {
|
||||
// Odd-numbered rows consist of: vert arrow, empty cell, ... vert arrow
|
||||
// Remove the empty cell that was placed at the beginning of `row`.
|
||||
row.shift();
|
||||
}
|
||||
row = [];
|
||||
body.push(row);
|
||||
}
|
||||
|
||||
// End row group
|
||||
parser.gullet.endGroup();
|
||||
// End array group defining \\
|
||||
parser.gullet.endGroup();
|
||||
|
||||
// define column separation.
|
||||
const cols = new Array(body[0].length).fill({
|
||||
type: "align",
|
||||
align: "c",
|
||||
pregap: 0.25, // CD package sets \enskip between columns.
|
||||
postgap: 0.25, // So pre and post each get half an \enskip, i.e. 0.25em.
|
||||
});
|
||||
|
||||
return {
|
||||
type: "array",
|
||||
mode: "math",
|
||||
body,
|
||||
arraystretch: 1,
|
||||
addJot: true,
|
||||
rowGaps: [null],
|
||||
cols,
|
||||
colSeparationType: "CD",
|
||||
hLinesBeforeRow: new Array(body.length + 1).fill([]),
|
||||
};
|
||||
}
|
||||
|
||||
// The functions below are not available for general use.
|
||||
// They are here only for internal use by the {CD} environment in placing labels
|
||||
// next to vertical arrows.
|
||||
|
||||
// We don't need any such functions for horizontal arrows because we can reuse
|
||||
// the functionality that already exists for extensible arrows.
|
||||
|
||||
defineFunction({
|
||||
type: "cdlabel",
|
||||
names: ["\\\\cdleft", "\\\\cdright"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
},
|
||||
handler({parser, funcName}, args) {
|
||||
return {
|
||||
type: "cdlabel",
|
||||
mode: parser.mode,
|
||||
side: funcName.slice(4),
|
||||
label: args[0],
|
||||
};
|
||||
},
|
||||
htmlBuilder(group, options) {
|
||||
const newOptions = options.havingStyle(options.style.sup());
|
||||
const label = buildCommon.wrapFragment(
|
||||
html.buildGroup(group.label, newOptions, options), options);
|
||||
label.classes.push("cd-label-" + group.side);
|
||||
label.style.bottom = (0.8 - label.depth) + "em";
|
||||
// Zero out label height & depth, so vertical align of arrow is set
|
||||
// by the arrow height, not by the label.
|
||||
label.height = 0;
|
||||
label.depth = 0;
|
||||
return label;
|
||||
},
|
||||
mathmlBuilder(group, options) {
|
||||
let label = new mathMLTree.MathNode("mrow",
|
||||
[mml.buildGroup(group.label, options)]);
|
||||
label = new mathMLTree.MathNode("mpadded", [label]);
|
||||
label.setAttribute("width", "0");
|
||||
if (group.side === "left") {
|
||||
label.setAttribute("lspace", "-1width");
|
||||
}
|
||||
// We have to guess at vertical alignment. We know the arrow is 1.8em tall,
|
||||
// But we don't know the height or depth of the label.
|
||||
label.setAttribute("voffset", "0.7em");
|
||||
label = new mathMLTree.MathNode("mstyle", [label]);
|
||||
label.setAttribute("displaystyle", "false");
|
||||
label.setAttribute("scriptlevel", "1");
|
||||
return label;
|
||||
},
|
||||
});
|
||||
|
||||
defineFunction({
|
||||
type: "cdlabelparent",
|
||||
names: ["\\\\cdparent"],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
},
|
||||
handler({parser}, args) {
|
||||
return {
|
||||
type: "cdlabelparent",
|
||||
mode: parser.mode,
|
||||
fragment: args[0],
|
||||
};
|
||||
},
|
||||
htmlBuilder(group, options) {
|
||||
// Wrap the vertical arrow and its labels.
|
||||
// The parent gets position: relative. The child gets position: absolute.
|
||||
// So CSS can locate the label correctly.
|
||||
const parent = buildCommon.wrapFragment(
|
||||
html.buildGroup(group.fragment, options), options
|
||||
);
|
||||
parent.classes.push("cd-vert-arrow");
|
||||
return parent;
|
||||
},
|
||||
mathmlBuilder(group, options) {
|
||||
return new mathMLTree.MathNode("mrow",
|
||||
[mml.buildGroup(group.fragment, options)]);
|
||||
},
|
||||
});
|
@@ -10,6 +10,7 @@ export default functions;
|
||||
import "./functions/accent";
|
||||
import "./functions/accentunder";
|
||||
import "./functions/arrow";
|
||||
import "./environments/cd";
|
||||
import "./functions/char";
|
||||
import "./functions/color";
|
||||
import "./functions/cr";
|
||||
|
@@ -30,6 +30,8 @@ defineFunction({
|
||||
// The next 3 functions are here to support the mhchem extension.
|
||||
// Direct use of these functions is discouraged and may break someday.
|
||||
"\\xrightleftarrows", "\\xrightequilibrium", "\\xleftequilibrium",
|
||||
// The next 3 functions are here only to support the {CD} environment.
|
||||
"\\\\cdrightarrow", "\\\\cdleftarrow", "\\\\cdlongequal",
|
||||
],
|
||||
props: {
|
||||
numArgs: 1,
|
||||
@@ -57,7 +59,8 @@ defineFunction({
|
||||
let newOptions = options.havingStyle(style.sup());
|
||||
const upperGroup = buildCommon.wrapFragment(
|
||||
html.buildGroup(group.body, newOptions, options), options);
|
||||
upperGroup.classes.push("x-arrow-pad");
|
||||
const arrowPrefix = group.label.slice(0, 2) === "\\x" ? "x" : "cd";
|
||||
upperGroup.classes.push(arrowPrefix + "-arrow-pad");
|
||||
|
||||
let lowerGroup;
|
||||
if (group.below) {
|
||||
@@ -65,7 +68,7 @@ defineFunction({
|
||||
newOptions = options.havingStyle(style.sub());
|
||||
lowerGroup = buildCommon.wrapFragment(
|
||||
html.buildGroup(group.below, newOptions, options), options);
|
||||
lowerGroup.classes.push("x-arrow-pad");
|
||||
lowerGroup.classes.push(arrowPrefix + "-arrow-pad");
|
||||
}
|
||||
|
||||
const arrowBody = stretchy.svgSpan(group, options);
|
||||
@@ -112,6 +115,9 @@ defineFunction({
|
||||
},
|
||||
mathmlBuilder(group, options) {
|
||||
const arrowNode = stretchy.mathMLnode(group.label);
|
||||
arrowNode.setAttribute(
|
||||
"minsize", group.label.charAt(0) === "x" ? "1.75em" : "3.0em"
|
||||
);
|
||||
let node;
|
||||
|
||||
if (group.body) {
|
||||
|
@@ -555,6 +555,10 @@
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
.cd-arrow-pad {
|
||||
padding: 0 0.55556em 0 0.27778em; // \;{#1}\;\;
|
||||
}
|
||||
|
||||
.x-arrow,
|
||||
.mover,
|
||||
.munder {
|
||||
@@ -610,6 +614,25 @@
|
||||
.mtr-glue {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.cd-vert-arrow {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cd-label-left {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
right: calc(50% + 0.3em);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.cd-label-right {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: calc(50% + 0.3em);
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.katex-display {
|
||||
|
@@ -41,6 +41,20 @@ type ParseNodeTypes = {
|
||||
hLinesBeforeRow: Array<boolean[]>,
|
||||
addEqnNum?: boolean,
|
||||
leqno?: boolean,
|
||||
isCD?: boolean,
|
||||
|},
|
||||
"cdlabel": {|
|
||||
type: "cdlabel",
|
||||
mode: Mode,
|
||||
loc?: ?SourceLocation,
|
||||
side: string,
|
||||
label: AnyParseNode,
|
||||
|},
|
||||
"cdlabelparent": {|
|
||||
type: "cdlabelparent",
|
||||
mode: Mode,
|
||||
loc?: ?SourceLocation,
|
||||
fragment: AnyParseNode,
|
||||
|},
|
||||
"color": {|
|
||||
type: "color",
|
||||
|
@@ -54,11 +54,14 @@ const stretchyCodePoint: {[string]: string} = {
|
||||
xrightleftarrows: "\u21c4",
|
||||
xrightequilibrium: "\u21cc", // Not a perfect match.
|
||||
xleftequilibrium: "\u21cb", // None better available.
|
||||
"\\\\cdrightarrow": "\u2192",
|
||||
"\\\\cdleftarrow": "\u2190",
|
||||
"\\\\cdlongequal": "=",
|
||||
};
|
||||
|
||||
const mathMLnode = function(label: string): mathMLTree.MathNode {
|
||||
const node = new mathMLTree.MathNode(
|
||||
"mo", [new mathMLTree.TextNode(stretchyCodePoint[label.substr(1)])]);
|
||||
"mo", [new mathMLTree.TextNode(stretchyCodePoint[label])]);
|
||||
node.setAttribute("stretchy", "true");
|
||||
return node;
|
||||
};
|
||||
@@ -118,7 +121,9 @@ const katexImagesData: {
|
||||
underrightarrow: [["rightarrow"], 0.888, 522, "xMaxYMin"],
|
||||
underleftarrow: [["leftarrow"], 0.888, 522, "xMinYMin"],
|
||||
xrightarrow: [["rightarrow"], 1.469, 522, "xMaxYMin"],
|
||||
"\\cdrightarrow": [["rightarrow"], 3.0, 522, "xMaxYMin"], // CD minwwidth2.5pc
|
||||
xleftarrow: [["leftarrow"], 1.469, 522, "xMinYMin"],
|
||||
"\\cdleftarrow": [["leftarrow"], 3.0, 522, "xMinYMin"],
|
||||
Overrightarrow: [["doublerightarrow"], 0.888, 560, "xMaxYMin"],
|
||||
xRightarrow: [["doublerightarrow"], 1.526, 560, "xMaxYMin"],
|
||||
xLeftarrow: [["doubleleftarrow"], 1.526, 560, "xMinYMin"],
|
||||
@@ -129,6 +134,7 @@ const katexImagesData: {
|
||||
xrightharpoonup: [["rightharpoon"], 0.888, 522, "xMaxYMin"],
|
||||
xrightharpoondown: [["rightharpoondown"], 0.888, 522, "xMaxYMin"],
|
||||
xlongequal: [["longequal"], 0.888, 334, "xMinYMin"],
|
||||
"\\cdlongequal": [["longequal"], 3.0, 334, "xMinYMin"],
|
||||
xtwoheadleftarrow: [["twoheadleftarrow"], 0.888, 334, "xMinYMin"],
|
||||
xtwoheadrightarrow: [["twoheadrightarrow"], 0.888, 334, "xMaxYMin"],
|
||||
|
||||
|
@@ -204,6 +204,211 @@ exports[`A MathML builder normal spaces render normally 1`] = `
|
||||
</math>
|
||||
`;
|
||||
|
||||
exports[`A MathML builder should build the CD environment properly 1`] = `
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML"
|
||||
display="block"
|
||||
>
|
||||
<semantics>
|
||||
<mtable rowspacing="0.2500em"
|
||||
columnalign="center center center"
|
||||
columnspacing="0.5em"
|
||||
>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
<mi>
|
||||
A
|
||||
</mi>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
<munderover>
|
||||
<mo stretchy="true"
|
||||
minsize="3.0em"
|
||||
>
|
||||
→
|
||||
</mo>
|
||||
<mpadded width="+0.6em"
|
||||
lspace="0.3em"
|
||||
>
|
||||
<mrow>
|
||||
</mrow>
|
||||
</mpadded>
|
||||
<mpadded width="+0.6em"
|
||||
lspace="0.3em"
|
||||
>
|
||||
<mi>
|
||||
a
|
||||
</mi>
|
||||
</mpadded>
|
||||
</munderover>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
<mi>
|
||||
B
|
||||
</mi>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
<mrow>
|
||||
<mrow>
|
||||
<mstyle displaystyle="false"
|
||||
scriptlevel="1"
|
||||
>
|
||||
<mpadded width="0"
|
||||
lspace="-1width"
|
||||
voffset="0.7em"
|
||||
>
|
||||
<mrow>
|
||||
<mrow>
|
||||
</mrow>
|
||||
</mrow>
|
||||
</mpadded>
|
||||
</mstyle>
|
||||
<mo fence="false"
|
||||
stretchy="true"
|
||||
minsize="1.8em"
|
||||
maxsize="1.8em"
|
||||
>
|
||||
↓
|
||||
</mo>
|
||||
<mstyle displaystyle="false"
|
||||
scriptlevel="1"
|
||||
>
|
||||
<mpadded width="0"
|
||||
voffset="0.7em"
|
||||
>
|
||||
<mrow>
|
||||
<mi>
|
||||
b
|
||||
</mi>
|
||||
</mrow>
|
||||
</mpadded>
|
||||
</mstyle>
|
||||
</mrow>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
<mrow>
|
||||
<mrow>
|
||||
<mstyle displaystyle="false"
|
||||
scriptlevel="1"
|
||||
>
|
||||
<mpadded width="0"
|
||||
lspace="-1width"
|
||||
voffset="0.7em"
|
||||
>
|
||||
<mrow>
|
||||
<mrow>
|
||||
</mrow>
|
||||
</mrow>
|
||||
</mpadded>
|
||||
</mstyle>
|
||||
<mo fence="false"
|
||||
stretchy="true"
|
||||
minsize="1.8em"
|
||||
maxsize="1.8em"
|
||||
>
|
||||
↓
|
||||
</mo>
|
||||
<mstyle displaystyle="false"
|
||||
scriptlevel="1"
|
||||
>
|
||||
<mpadded width="0"
|
||||
voffset="0.7em"
|
||||
>
|
||||
<mrow>
|
||||
<mi>
|
||||
c
|
||||
</mi>
|
||||
</mrow>
|
||||
</mpadded>
|
||||
</mstyle>
|
||||
</mrow>
|
||||
</mrow>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
<mi>
|
||||
C
|
||||
</mi>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
<munderover>
|
||||
<mo stretchy="true"
|
||||
minsize="3.0em"
|
||||
>
|
||||
→
|
||||
</mo>
|
||||
<mpadded width="+0.6em"
|
||||
lspace="0.3em"
|
||||
>
|
||||
<mrow>
|
||||
</mrow>
|
||||
</mpadded>
|
||||
<mpadded width="+0.6em"
|
||||
lspace="0.3em"
|
||||
>
|
||||
<mi>
|
||||
d
|
||||
</mi>
|
||||
</mpadded>
|
||||
</munderover>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
<mtd>
|
||||
<mstyle scriptlevel="0"
|
||||
displaystyle="true"
|
||||
>
|
||||
<mi>
|
||||
D
|
||||
</mi>
|
||||
</mstyle>
|
||||
</mtd>
|
||||
</mtr>
|
||||
<mtr>
|
||||
</mtr>
|
||||
</mtable>
|
||||
<annotation encoding="application/x-tex">
|
||||
\\begin{CD} A @>a>> B\\\\ @VVbV @VVcV\\\\ C @>d>> D \\end{CD}
|
||||
</annotation>
|
||||
</semantics>
|
||||
</math>
|
||||
`;
|
||||
|
||||
exports[`A MathML builder should concatenate digits into single <mn> 1`] = `
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<semantics>
|
||||
|
@@ -2778,6 +2778,7 @@ describe("AMS environments", function() {
|
||||
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);
|
||||
expect`\begin{CD}A @>a>> B \\@VbVV @AAcA\\C @= D\end{CD}`.not.toParse(nonstrictSettings);
|
||||
});
|
||||
|
||||
const nonStrictDisplay = new Settings({displayMode: true, strict: false});
|
||||
@@ -2791,6 +2792,7 @@ describe("AMS environments", function() {
|
||||
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);
|
||||
expect`\begin{CD}A @<a<< B @>>b> C @>>> D\\@. @| @AcAA @VVdV \\@. E @= F @>>> G\end{CD}`.toBuild(nonStrictDisplay);
|
||||
});
|
||||
|
||||
it("{equation} should fail if argument contains two rows.", () => {
|
||||
@@ -2807,6 +2809,29 @@ describe("AMS environments", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("The CD environment", function() {
|
||||
it("should fail if not is display mode", function() {
|
||||
expect(`\\begin{CD}A @<a<< B @>>b> C @>>> D\\\\@. @| @AcAA @VVdV \\\\@. E @= F @>>> G\\end{CD}`).not.toParse(
|
||||
new Settings({displayMode: false, strict: false})
|
||||
);
|
||||
});
|
||||
const displaySettings = new Settings({displayMode: true, strict: false});
|
||||
it("should fail if the character after '@' is not in <>AV=|.", function() {
|
||||
expect(`\\begin{CD}A @X<a<< B @>>b> C @>>> D\\\\@. @| @AcAA @VVdV \\\\@. E @= F @>>> G\\end{CD}`).not.toParse(displaySettings);
|
||||
});
|
||||
it("should fail if an arrow does not have its final character.", function() {
|
||||
expect(`\\begin{CD}A @<a< B @>>b> C @>>> D\\\\@. @| @AcAA @VVdV \\\\@. E @= F @>>> G\\end{CD}`).not.toParse(displaySettings);
|
||||
expect(`\\begin{CD}A @<a<< B @>>b C @>>> D\\\\@. @| @AcAA @VVdV \\\\@. E @= F @>>> G\\end{CD}`).not.toParse(displaySettings);
|
||||
});
|
||||
it("should fail without an \\\\end.", function() {
|
||||
expect(`\\begin{CD}A @<a<< B @>>b> C @>>> D\\\\@. @| @AcAA @VVdV \\\\@. E @= F @>>> G`).not.toParse(displaySettings);
|
||||
});
|
||||
|
||||
it("should succeed without the flaws noted above.", function() {
|
||||
expect(`\\begin{CD}A @<a<< B @>>b> C @>>> D\\\\@. @| @AcAA @VVdV \\\\@. E @= F @>>> G\\end{CD}`).toBuild(displaySettings);
|
||||
});
|
||||
});
|
||||
|
||||
describe("operatorname support", function() {
|
||||
it("should not fail", function() {
|
||||
expect("\\operatorname{x*Π∑\\Pi\\sum\\frac a b}").toBuild();
|
||||
|
@@ -79,6 +79,13 @@ describe("A MathML builder", function() {
|
||||
expect(getMathML("\\colorbox{red}{b}")).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should build the CD environment properly', () => {
|
||||
const displaySettings = new Settings({displayMode: true, strict: false});
|
||||
const mathml = getMathML("\\begin{CD} A @>a>> B\\\\ @VVbV @VVcV\\\\" +
|
||||
" C @>d>> D \\end{CD}", displaySettings);
|
||||
expect(mathml).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set href attribute for href appropriately', () => {
|
||||
expect(
|
||||
getMathML("\\href{http://example.org}{\\alpha}", new Settings({trust: true})),
|
||||
|
BIN
test/screenshotter/images/CD-chrome.png
Normal file
BIN
test/screenshotter/images/CD-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
test/screenshotter/images/CD-firefox.png
Normal file
BIN
test/screenshotter/images/CD-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
test/screenshotter/images/CD-safari.png
Normal file
BIN
test/screenshotter/images/CD-safari.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
@@ -86,6 +86,9 @@ Cases: |
|
||||
a &\text{if } b \\
|
||||
c &\text{if } d
|
||||
\end{rcases}⇒…
|
||||
CD:
|
||||
tex: \begin{CD} A @<a<< B @>>b> C \\ @| @AcAA @VVdV \\ D @= E @>>> F \end{CD}
|
||||
display: 1
|
||||
Colors:
|
||||
tex: \blue{a}\textcolor{#0f0}{b}\textcolor{red}{c}
|
||||
nolatex: different syntax and different scope
|
||||
|
Reference in New Issue
Block a user