diff --git a/src/environments/array.js b/src/environments/array.js index 9462f8fe..61929c23 100644 --- a/src/environments/array.js +++ b/src/environments/array.js @@ -30,8 +30,20 @@ export type ArrayEnvNodeData = { // initialization. body?: ParseNode<*>[][], // List of rows in the (2D) array. rowGaps?: number[], + numHLinesBeforeRow?: number[], }; +function getNumHLines(parser: Parser): number { + let n = 0; + parser.consumeSpaces(); + while (parser.nextToken.text === "\\hline") { + parser.consume(); + n++; + parser.consumeSpaces(); + } + return n; +} + /** * Parse the body of the environment, with rows delimited by \\ and * columns delimited by &, and create a nested list in row-major order @@ -46,6 +58,11 @@ function parseArray( let row = []; const body = [row]; const rowGaps = []; + const numHLinesBeforeRow = []; + + // Test for \hline at the top of the array. + numHLinesBeforeRow.push(getNumHLines(parser)); + while (true) { // eslint-disable-line no-constant-condition let cell = parser.parseExpression(false, undefined); cell = new ParseNode("ordgroup", cell, parser.mode); @@ -76,6 +93,10 @@ function parseArray( throw new ParseError(`Failed to parse function after ${next}`); } rowGaps.push(cr.value.size); + + // check for \hline(s) following the row separator + numHLinesBeforeRow.push(getNumHLines(parser)); + row = []; body.push(row); } else { @@ -85,6 +106,7 @@ function parseArray( } result.body = body; result.rowGaps = rowGaps; + result.numHLinesBeforeRow = numHLinesBeforeRow; return new ParseNode("array", result, parser.mode); } @@ -110,8 +132,10 @@ const htmlBuilder = function(group, options) { let r; let c; const nr = group.value.body.length; + const numHLinesBeforeRow = group.value.numHLinesBeforeRow; let nc = 0; let body = new Array(nr); + const hlinePos = []; // Horizontal spacing const pt = 1 / options.fontMetrics().ptPerEm; @@ -130,6 +154,15 @@ const htmlBuilder = function(group, options) { const arstrutDepth = 0.3 * arrayskip; // \@arstrutbox in lttab.dtx let totalHeight = 0; + + // Set a position for \hline(s) at the top of the array, if any. + for (let i = 1; i <= numHLinesBeforeRow[0]; i++) { + if (i > 1) { // The first \hline doesn't add to height. + totalHeight += 0.25; + } + hlinePos.push(totalHeight); + } + for (r = 0; r < group.value.body.length; ++r) { const inrow = group.value.body[r]; let height = arstrutHeight; // \@array adds an \@arstrut @@ -175,6 +208,14 @@ const htmlBuilder = function(group, options) { outrow.pos = totalHeight; totalHeight += depth + gap; // \@yargarraycr body[r] = outrow; + + // Set a position for \hline(s), if any. + for (let i = 1; i <= numHLinesBeforeRow[r + 1]; i++) { + if (i > 1) { // the first \hline doesn't add height + totalHeight += 0.25; + } + hlinePos.push(totalHeight); + } } const offset = totalHeight / 2 + options.fontMetrics().axisHeight; @@ -266,6 +307,21 @@ const htmlBuilder = function(group, options) { } } body = buildCommon.makeSpan(["mtable"], cols); + + // Add \hline(s), if any. + if (hlinePos.length > 0) { + const line = buildCommon.makeLineSpan("hline", options, 0.05); + const vListChildren = [{type: "elem", elem: body, shift: 0}]; + while (hlinePos.length > 0) { + const lineShift = hlinePos.pop() - offset; + vListChildren.push({type: "elem", elem: line, shift: lineShift}); + } + body = buildCommon.makeVList({ + positionType: "individualShift", + children: vListChildren, + }, options); + } + return buildCommon.makeSpan(["mord"], [body], options); }; diff --git a/src/katex.less b/src/katex.less index b199a4d6..b62c5acf 100644 --- a/src/katex.less +++ b/src/katex.less @@ -11,7 +11,8 @@ .mfrac .frac-line, .overline .overline-line, - .underline .underline-line { + .underline .underline-line, + .hline { min-height: 1px; } } @@ -317,7 +318,8 @@ } .overline .overline-line, - .underline .underline-line { + .underline .underline-line, + .hline { display: inline-block; width: 100%; border-bottom-style: solid; diff --git a/test/katex-spec.js b/test/katex-spec.js index adc65f3b..e629dc62 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -1356,6 +1356,10 @@ describe("A begin/end parser", function() { expect("\\begin{array}{cc}a&b\\\\c&d\\end{array}").toParse(); }); + it("should parse an environment with hlines", function() { + expect("\\begin{matrix}\\hline a&b\\\\ \\hline c&d\\end{matrix}").toParse(); + }); + it("should error when name is mismatched", function() { expect("\\begin{matrix}a&b\\\\c&d\\end{pmatrix}").toNotParse(); }); diff --git a/test/screenshotter/images/Hline-chrome.png b/test/screenshotter/images/Hline-chrome.png new file mode 100644 index 00000000..6a8ebb06 Binary files /dev/null and b/test/screenshotter/images/Hline-chrome.png differ diff --git a/test/screenshotter/images/Hline-firefox.png b/test/screenshotter/images/Hline-firefox.png new file mode 100644 index 00000000..6f3e5656 Binary files /dev/null and b/test/screenshotter/images/Hline-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 64d318be..99f18d1a 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -124,6 +124,7 @@ GroupMacros: \startExp: e^\bgroup \endExp: \egroup tex: \startExp a+b\endExp +Hline: \begin{array}{c|c}\hline \hline \hline a & b \\ \hline \hline \hline c & d \end{array} \quad \frac{\begin{pmatrix} a & b \\ \hline c & d \\ \hline \hline \hline\end{pmatrix}} 2 \quad \begin{bmatrix}\hline a & b \\ \hline c & d \\ \hline e & f \end{bmatrix} HorizontalBraces: \overbrace{\displaystyle{\oint_S{\vec E\cdot\hat n\,\mathrm d a}}}^\text{emf} = \underbrace{\frac{q_{\text{enc}}}{\varepsilon_0}}_{\text{charge}} KaTeX: tex: \KaTeX