fix: Use SVGs to avoid gaps in tall delimiters. (#2698)

* fix: Use SVGs to avoid gaps in tall delimiters.

* Add \vert and \Vert

* Update screenshots

* Define verts array outside of function.

* Revert height-finding logic.

* Update screenshots

* Pick up review comments.

* Update CD screen shots
This commit is contained in:
Ron Kok
2020-12-28 13:50:09 -08:00
committed by GitHub
parent a829fa10f8
commit 4a8a6a38ce
31 changed files with 89 additions and 64 deletions

View File

@@ -728,8 +728,6 @@ const svgData: {
oiintSize2: ["oiintSize2", 1.472, 0.659], oiintSize2: ["oiintSize2", 1.472, 0.659],
oiiintSize1: ["oiiintSize1", 1.304, 0.499], oiiintSize1: ["oiiintSize1", 1.304, 0.499],
oiiintSize2: ["oiiintSize2", 1.98, 0.659], oiiintSize2: ["oiiintSize2", 1.98, 0.659],
leftParenInner: ["leftParenInner", 0.875, 0.3],
rightParenInner: ["rightParenInner", 0.875, 0.3],
}; };
const staticSvg = function(value: string, options: Options): SvgSpan { const staticSvg = function(value: string, options: Options): SvgSpan {

View File

@@ -25,11 +25,12 @@ import ParseError from "./ParseError";
import Style from "./Style"; import Style from "./Style";
import {PathNode, SvgNode, SymbolNode} from "./domTree"; import {PathNode, SvgNode, SymbolNode} from "./domTree";
import {sqrtPath} from "./svgGeometry"; import {sqrtPath, innerPath} from "./svgGeometry";
import buildCommon from "./buildCommon"; import buildCommon from "./buildCommon";
import {getCharacterMetrics} from "./fontMetrics"; import {getCharacterMetrics} from "./fontMetrics";
import symbols from "./symbols"; import symbols from "./symbols";
import utils from "./utils"; import utils from "./utils";
import fontMetricsData from "../submodules/katex-fonts/fontMetricsData";
import type Options from "./Options"; import type Options from "./Options";
import type {CharacterMetrics} from "./fontMetrics"; import type {CharacterMetrics} from "./fontMetrics";
@@ -153,10 +154,10 @@ const makeLargeDelim = function(delim,
}; };
/** /**
* Make an inner span with the given offset and in the given font. This is used * Make a span from a font glyph with the given offset and in the given font.
* in `makeStackedDelim` to make the stacking pieces for the delimiter. * This is used in makeStackedDelim to make the stacking pieces for the delimiter.
*/ */
const makeInner = function( const makeGlyphSpan = function(
symbol: string, symbol: string,
font: "Size1-Regular" | "Size4-Regular", font: "Size1-Regular" | "Size4-Regular",
mode: Mode, mode: Mode,
@@ -169,17 +170,45 @@ const makeInner = function(
sizeClass = "delim-size4"; sizeClass = "delim-size4";
} }
const inner = buildCommon.makeSpan( const corner = buildCommon.makeSpan(
["delimsizinginner", sizeClass], ["delimsizinginner", sizeClass],
[buildCommon.makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]); [buildCommon.makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
// Since this will be passed into `makeVList` in the end, wrap the element // Since this will be passed into `makeVList` in the end, wrap the element
// in the appropriate tag that VList uses. // in the appropriate tag that VList uses.
return {type: "elem", elem: inner}; return {type: "elem", elem: corner};
}; };
// Helper for makeStackedDelim const makeInner = function(
const lap = {type: "kern", size: -0.005}; ch: string,
height: number,
options: Options
): VListElem {
// Create a span with inline SVG for the inner part of a tall stacked delimiter.
const width = fontMetricsData['Size4-Regular'][ch.charCodeAt(0)]
? fontMetricsData['Size4-Regular'][ch.charCodeAt(0)][4].toFixed(3)
: fontMetricsData['Size1-Regular'][ch.charCodeAt(0)][4].toFixed(3);
const path = new PathNode("inner", innerPath(ch, Math.round(1000 * height)));
const svgNode = new SvgNode([path], {
"width": width + "em",
"height": height + "em",
// Override CSS rule `.katex svg { width: 100% }`
"style": "width:" + width + "em",
"viewBox": "0 0 " + 1000 * width + " " + Math.round(1000 * height),
"preserveAspectRatio": "xMinYMin",
});
const span = buildCommon.makeSvgSpan([], [svgNode], options);
span.height = height;
span.style.height = height + "em";
span.style.width = width + "em";
return {type: "elem", elem: span};
};
// Helpers for makeStackedDelim
const lapInEms = 0.008;
const lap = {type: "kern", size: -1 * lapInEms};
const verts = ["|", "\\lvert", "\\rvert", "\\vert"];
const doubleVerts = ["\\|", "\\lVert", "\\rVert", "\\Vert"];
/** /**
* Make a stacked delimiter out of a given delimiter, with the total height at * Make a stacked delimiter out of a given delimiter, with the total height at
@@ -223,6 +252,10 @@ const makeStackedDelim = function(
top = "\\Uparrow"; top = "\\Uparrow";
repeat = "\u2016"; repeat = "\u2016";
bottom = "\\Downarrow"; bottom = "\\Downarrow";
} else if (utils.contains(verts, delim)) {
repeat = "\u2223";
} else if (utils.contains(doubleVerts, delim)) {
repeat = "\u2225";
} else if (delim === "[" || delim === "\\lbrack") { } else if (delim === "[" || delim === "\\lbrack") {
top = "\u23a1"; top = "\u23a1";
repeat = "\u23a2"; repeat = "\u23a2";
@@ -331,74 +364,44 @@ const makeStackedDelim = function(
// Calculate the depth // Calculate the depth
const depth = realHeightTotal / 2 - axisHeight; const depth = realHeightTotal / 2 - axisHeight;
// This function differs from the TeX procedure in one way.
// We shift each repeat element downwards by 0.005em, to prevent a gap
// due to browser floating point rounding error.
// Then, at the last element-to element joint, we add one extra repeat
// element to cover the gap created by the shifts.
// Find the shift needed to align the upper end of the extra element at a point
// 0.005em above the lower end of the top element.
const shiftOfExtraElement = (repeatCount + 1) * 0.005 - repeatHeightTotal;
// Now, we start building the pieces that will go into the vlist // Now, we start building the pieces that will go into the vlist
// Keep a list of the pieces of the stacked delimiter
// Keep a list of the inner pieces const stack = [];
const inners = [];
// Add the bottom symbol // Add the bottom symbol
inners.push(makeInner(bottom, font, mode)); stack.push(makeGlyphSpan(bottom, font, mode));
stack.push(lap); // overlap
if (middle === null) { if (middle === null) {
// Add that many symbols // The middle section will be an SVG. Make it an extra 0.016em tall.
for (let i = 0; i < repeatCount; i++) { // We'll overlap by 0.008em at top and bottom.
inners.push(lap); // overlap const innerHeight = realHeightTotal - topHeightTotal - bottomHeightTotal
inners.push(makeInner(repeat, font, mode)); + 2 * lapInEms;
} stack.push(makeInner(repeat, innerHeight, options));
} else { } else {
// When there is a middle bit, we need the middle part and two repeated // When there is a middle bit, we need the middle part and two repeated
// sections // sections
for (let i = 0; i < repeatCount; i++) { const innerHeight = (realHeightTotal - topHeightTotal - bottomHeightTotal -
inners.push(lap); middleHeightTotal) / 2 + 2 * lapInEms;
inners.push(makeInner(repeat, font, mode)); stack.push(makeInner(repeat, innerHeight, options));
}
// Insert one extra repeat element.
inners.push({type: "kern", size: shiftOfExtraElement});
inners.push(makeInner(repeat, font, mode));
inners.push(lap);
// Now insert the middle of the brace. // Now insert the middle of the brace.
inners.push(makeInner(middle, font, mode)); stack.push(lap);
for (let i = 0; i < repeatCount; i++) { stack.push(makeGlyphSpan(middle, font, mode));
inners.push(lap); stack.push(lap);
inners.push(makeInner(repeat, font, mode)); stack.push(makeInner(repeat, innerHeight, options));
}
}
// To cover the gap create by the overlaps, insert one more repeat element,
// at a position that juts 0.005 above the bottom of the top element.
if ((repeat === "\u239c" || repeat === "\u239f") && repeatCount === 0) {
// Parentheses need a short repeat element in order to avoid an overrun.
// We'll make a 0.3em tall element from a SVG.
const overlap = buildCommon.svgData.leftParenInner[2] / 2;
inners.push({type: "kern", size: -overlap});
const pathName = repeat === "\u239c" ? "leftParenInner" : "rightParenInner";
const innerSpan = buildCommon.staticSvg(pathName, options);
inners.push({type: "elem", elem: innerSpan});
inners.push({type: "kern", size: -overlap});
} else {
inners.push({type: "kern", size: shiftOfExtraElement});
inners.push(makeInner(repeat, font, mode));
inners.push(lap);
} }
// Add the top symbol // Add the top symbol
inners.push(makeInner(top, font, mode)); stack.push(lap);
stack.push(makeGlyphSpan(top, font, mode));
// Finally, build the vlist // Finally, build the vlist
const newOptions = options.havingBaseStyle(Style.TEXT); const newOptions = options.havingBaseStyle(Style.TEXT);
const inner = buildCommon.makeVList({ const inner = buildCommon.makeVList({
positionType: "bottom", positionType: "bottom",
positionData: depth, positionData: depth,
children: inners, children: stack,
}, newOptions); }, newOptions);
return styleWrap( return styleWrap(

View File

@@ -533,7 +533,7 @@ export class PathNode implements VirtualNode {
constructor(pathName: string, alternate?: string) { constructor(pathName: string, alternate?: string) {
this.pathName = pathName; this.pathName = pathName;
this.alternate = alternate; // Used only for \sqrt and \phase this.alternate = alternate; // Used only for \sqrt, \phase, & tall delims
} }
toNode(): Node { toNode(): Node {

View File

@@ -151,11 +151,35 @@ export const sqrtPath = function(
return path; return path;
}; };
export const path: {[string]: string} = { export const innerPath = function(name: string, height: number): string {
// Two paths that cover gaps in built-up parentheses. // The inner part of stretchy tall delimiters
leftParenInner: `M291 0 H417 V300 H291 z`, switch (name) {
rightParenInner: `M457 0 H583 V300 H457 z`, case "\u239c":
return `M291 0 H417 V${height} H291z M291 0 H417 V${height} H291z`;
case "\u2223":
return `M145 0 H188 V${height} H145z M145 0 H188 V${height} H145z`;
case "\u2225":
return `M145 0 H188 V${height} H145z M145 0 H188 V${height} H145z` +
`M367 0 H410 V${height} H367z M367 0 H410 V${height} H367z`;
case "\u239f":
return `M457 0 H583 V${height} H457z M457 0 H583 V${height} H457z`;
case "\u23a2":
return `M319 0 H403 V${height} H319z M319 0 H403 V${height} H319z`;
case "\u23a5":
return `M263 0 H347 V${height} H263z M263 0 H347 V${height} H263z`;
case "\u23aa":
return `M384 0 H504 V${height} H384z M384 0 H504 V${height} H384z`;
case "\u23d0":
return `M312 0 H355 V${height} H312z M312 0 H355 V${height} H312z`;
case "\u2016":
return `M257 0 H300 V${height} H257z M257 0 H300 V${height} H257z` +
`M478 0 H521 V${height} H478z M478 0 H521 V${height} H478z`;
default:
return "";
}
};
export const path: {[string]: string} = {
// The doubleleftarrow geometry is from glyph U+21D0 in the font KaTeX Main // The doubleleftarrow geometry is from glyph U+21D0 in the font KaTeX Main
doubleleftarrow: `M262 157 doubleleftarrow: `M262 157
l10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3 l10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB