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
@@ -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 {
|
||||||
|
117
src/delimiter.js
@@ -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(
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 6.8 KiB |