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:
Ron Kok
2020-12-27 10:45:31 -08:00
committed by GitHub
parent b34175bd92
commit 75a3af9725
17 changed files with 652 additions and 14 deletions

View File

@@ -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.

View File

@@ -215,7 +215,7 @@ table td {
|\cap|$\cap$||
|{cases}|$\begin{cases}a&\text{if }b\\c&\text{if }d\end{cases}$|`\begin{cases}`<br>&nbsp;&nbsp;&nbsp;`a &\text{if } b \\`<br>&nbsp;&nbsp;&nbsp;`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>&nbsp;&nbsp;&nbsp;`A @>a>> B \\`<br>`@VbVV @AAcA \\`<br>&nbsp;&nbsp;&nbsp;`C @= D`<br>`\end{CD}`|
|\cdot|$\cdot$||
|\cdotp|$\cdotp$||
|\cdots|$\cdots$||

View File

@@ -83,17 +83,24 @@ $( \big( \Big( \bigg( \Bigg($ `( \big( \Big( \bigg( \Bigg(`
|$\begin{pmatrix} a & b \\ c & d \end{pmatrix}$ |`\begin{pmatrix}`<br>&nbsp;&nbsp;&nbsp;`a & b \\`<br>&nbsp;&nbsp;&nbsp;`c & d`<br>`\end{pmatrix}` |$\begin{bmatrix} a & b \\ c & d \end{bmatrix}$ | `\begin{bmatrix}`<br>&nbsp;&nbsp;&nbsp;`a & b \\`<br>&nbsp;&nbsp;&nbsp;`c & d`<br>`\end{bmatrix}`
|$\begin{vmatrix} a & b \\ c & d \end{vmatrix}$ |`\begin{vmatrix}`<br>&nbsp;&nbsp;&nbsp;`a & b \\`<br>&nbsp;&nbsp;&nbsp;`c & d`<br>`\end{vmatrix}` |$\begin{Vmatrix} a & b \\ c & d \end{Vmatrix}$ |`\begin{Vmatrix}`<br>&nbsp;&nbsp;&nbsp;`a & b \\`<br>&nbsp;&nbsp;&nbsp;`c & d`<br>`\end{Vmatrix}`
|$\begin{Bmatrix} a & b \\ c & d \end{Bmatrix}$ |`\begin{Bmatrix}`<br>&nbsp;&nbsp;&nbsp;`a & b \\`<br>&nbsp;&nbsp;&nbsp;`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>&nbsp;&nbsp;&nbsp;`\begin{array}{c:c:c}`<br>&nbsp;&nbsp;&nbsp;`a & b & c \\ \hline`<br>&nbsp;&nbsp;&nbsp;`d & e & f \\`<br>&nbsp;&nbsp;&nbsp;`\hdashline`<br>&nbsp;&nbsp;&nbsp;`g & h & i`<br>`\end{array}`
|$$\begin{equation}\begin{split}a &=b+c\\&=e+f\end{split}\end{equation}$$ |`\begin{equation}`<br>`\begin{split}`&nbsp;&nbsp;&nbsp;`a &=b+c\\`<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`&=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}`&nbsp;&nbsp;&nbsp;`a &=b+c\\`<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`&=e+f`<br>`\end{split}`<br>`\end{equation*}`
|$$\begin{align} a&=b+c \\ d+e&=f \end{align}$$ |`\begin{align}`<br>&nbsp;&nbsp;&nbsp;`a&=b+c \\`<br>&nbsp;&nbsp;&nbsp;`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>&nbsp;&nbsp;&nbsp;`a&=b+c \\`<br>&nbsp;&nbsp;&nbsp;`d+e&=f`<br>`\end{align*}`|$\begin{aligned} a&=b+c \\ d+e&=f \end{aligned}$ |`\begin{aligned}`<br>&nbsp;&nbsp;&nbsp;`a&=b+c \\`<br>&nbsp;&nbsp;&nbsp;`d+e&=f`<br>`\end{aligned}`|
`\begin{alignedat}{2}`<br>&nbsp;&nbsp;&nbsp;`10&x+ &3&y = 2 \\`<br>&nbsp;&nbsp;&nbsp;` 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>&nbsp;&nbsp;&nbsp;`10&x+ &3&y = 2 \\`<br>&nbsp;&nbsp;&nbsp;` 3&x+&13&y = 4`<br>`\end{alignedat}`
|$$\begin{gather} a=b \\ e=b+c \end{gather}$$ |`\begin{gather}`<br>&nbsp;&nbsp;&nbsp;`a=b \\ `<br>&nbsp;&nbsp;&nbsp;`e=b+c`<br>`\end{gather}`|$\begin{gathered} a=b \\ e=b+c \end{gathered}$ |`\begin{gathered}`<br>&nbsp;&nbsp;&nbsp;`a=b \\ `<br>&nbsp;&nbsp;&nbsp;`e=b+c`<br>`\end{gathered}`|
|$x = \begin{cases} a &\text{if } b \\ c &\text{if } d \end{cases}$ |`x = \begin{cases}`<br>&nbsp;&nbsp;&nbsp;`a &\text{if } b \\`<br>&nbsp;&nbsp;&nbsp;`c &\text{if } d`<br>`\end{cases}`|$\begin{rcases} a &\text{if } b \\ c &\text{if } d \end{rcases}⇒…$ |`\begin{rcases}`<br>&nbsp;&nbsp;&nbsp;`a &\text{if } b \\`<br>&nbsp;&nbsp;&nbsp;`c &\text{if } d`<br>`\end{rcases}⇒…`|
|$\begin{smallmatrix} a & b \\ c & d \end{smallmatrix}$ | `\begin{smallmatrix}`<br>&nbsp;&nbsp;&nbsp;`a & b \\`<br>&nbsp;&nbsp;&nbsp;`c & d`<br>`\end{smallmatrix}` |||
|$\begin{smallmatrix} a & b \\ c & d \end{smallmatrix}$ | `\begin{smallmatrix}`<br>&nbsp;&nbsp;&nbsp;`a & b \\`<br>&nbsp;&nbsp;&nbsp;`c & d`<br>`\end{smallmatrix}` |$$\begin{CD}A @>a>> B \\@VbVV @AAcA\\C @= D\end{CD}$$ |`\begin{CD}`<br>&nbsp;&nbsp;&nbsp;`A @>a>> B \\`<br>`@VbVV @AAcA \\`<br>&nbsp;&nbsp;&nbsp;`C @= D`<br>`\\end{CD}`|
|||||
|:---------------------|:---------------------|
|$$\begin{align} a&=b+c \\ d+e&=f \end{align}$$ |`\begin{align}`<br>&nbsp;&nbsp;&nbsp;`a&=b+c \\`<br>&nbsp;&nbsp;&nbsp;`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>&nbsp;&nbsp;&nbsp;`10&x+ &3&y = 2 \\`<br>&nbsp;&nbsp;&nbsp;` 3&x+&13&y = 4`<br>`\end{alignedat}`
|`\begin{equation}`<br>`\begin{split}`&nbsp;&nbsp;&nbsp;`a &=b+c\\`<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`&=e+f`<br>`\end{split}`<br>`\end{equation}`
|$$\begin{gather} a=b \\ e=b+c \end{gather}$$ |`\begin{gather}`<br>&nbsp;&nbsp;&nbsp;`a=b \\ `<br>&nbsp;&nbsp;&nbsp;`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

View File

@@ -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
View 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)]);
},
});

View File

@@ -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";

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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",

View File

@@ -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"],

View File

@@ -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 @&gt;a&gt;&gt; B\\\\ @VVbV @VVcV\\\\ C @&gt;d&gt;&gt; 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>

View File

@@ -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();

View File

@@ -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})),

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -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