From f23bf3fe6341c2b862e565717783ae95e4b13b44 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 30 Jun 2017 13:50:28 -0400 Subject: [PATCH] Associate font metrics with Options, not Style. (#743) * Associate font metrics with Options, not Style. Font metrics are associated with a given font and size combination. Before KaTeX understood sizing commands, sizes were associated with a Style. That's not true now. So instead of `style.metrics`, use `options.fontMetrics()`, since `options` knows the font and the size. This is a cleanup commit with no visible effects on most tests (there could be some small effect on size + style combinations). It will make other changes possible later. --- src/Options.js | 15 ++++++ src/Style.js | 15 ------ src/buildHTML.js | 119 +++++++++++++++++++++----------------------- src/delimiter.js | 8 +-- src/environments.js | 3 +- src/fontMetrics.js | 98 ++++++++++++++++++------------------ 6 files changed, 127 insertions(+), 131 deletions(-) diff --git a/src/Options.js b/src/Options.js index 32dce189..a3bc7172 100644 --- a/src/Options.js +++ b/src/Options.js @@ -5,6 +5,8 @@ * `.reset` functions. */ +const fontMetrics = require("./fontMetrics"); + const BASESIZE = 6; const sizeStyleMap = [ @@ -24,6 +26,8 @@ const sizeStyleMap = [ ]; const sizeMultipliers = [ + // fontMetrics.js:getFontMetrics also uses size indexes, so if + // you change size indexes, change that function. 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488, ]; @@ -42,6 +46,7 @@ function Options(data) { this.phantom = data.phantom; this.font = data.font; this.sizeMultiplier = sizeMultipliers[this.size - 1]; + this._fontMetrics = null; } /** @@ -180,6 +185,16 @@ Options.prototype.baseSizingClasses = function() { } }; +/** + * Return the font metrics for this size. + */ +Options.prototype.fontMetrics = function() { + if (!this._fontMetrics) { + this._fontMetrics = fontMetrics.getFontMetrics(this.size); + } + return this._fontMetrics; +}; + /** * A map of color names to CSS colors. * TODO(emily): Remove this when we have real macros diff --git a/src/Style.js b/src/Style.js index 06ff910f..7a5cd85b 100644 --- a/src/Style.js +++ b/src/Style.js @@ -6,20 +6,6 @@ * information about them. */ -const sigmas = require("./fontMetrics.js").sigmas; - -const metrics = [{}, {}, {}]; -for (const key in sigmas) { - if (sigmas.hasOwnProperty(key)) { - for (let i = 0; i < 3; i++) { - metrics[i][key] = sigmas[key][i]; - } - } -} -for (let i = 0; i < 3; i++) { - metrics[i].emPerEx = sigmas.xHeight[i] / sigmas.quad[i]; -} - /** * The main style class. Contains a unique id for the style, a size (which is * the same for cramped and uncramped version of a style), and a cramped flag. @@ -28,7 +14,6 @@ function Style(id, size, cramped) { this.id = id; this.size = size; this.cramped = cramped; - this.metrics = metrics[size > 0 ? size - 1 : 0]; } /** diff --git a/src/buildHTML.js b/src/buildHTML.js index 07691260..270a3ba6 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -12,7 +12,6 @@ const Style = require("./Style"); const buildCommon = require("./buildCommon"); const delimiter = require("./delimiter"); const domTree = require("./domTree"); -const fontMetrics = require("./fontMetrics"); const utils = require("./utils"); const stretchy = require("./stretchy"); @@ -312,7 +311,7 @@ groupTypes.supsub = function(group, options) { let supm; let subm; - const style = options.style; + const metrics = options.fontMetrics(); let newOptions; // Rule 18a @@ -320,45 +319,45 @@ groupTypes.supsub = function(group, options) { let subShift = 0; if (group.value.sup) { - newOptions = options.havingStyle(style.sup()); + newOptions = options.havingStyle(options.style.sup()); supm = buildGroup(group.value.sup, newOptions, options); if (!isCharacterBox(group.value.base)) { - supShift = base.height - newOptions.style.metrics.supDrop + supShift = base.height - newOptions.fontMetrics().supDrop * newOptions.sizeMultiplier / options.sizeMultiplier; } } if (group.value.sub) { - newOptions = options.havingStyle(style.sub()); + newOptions = options.havingStyle(options.style.sub()); subm = buildGroup(group.value.sub, newOptions, options); if (!isCharacterBox(group.value.base)) { - subShift = base.depth + newOptions.style.metrics.subDrop + subShift = base.depth + newOptions.fontMetrics().subDrop * newOptions.sizeMultiplier / options.sizeMultiplier; } } // Rule 18c let minSupShift; - if (style === Style.DISPLAY) { - minSupShift = style.metrics.sup1; - } else if (style.cramped) { - minSupShift = style.metrics.sup3; + if (options.style === Style.DISPLAY) { + minSupShift = metrics.sup1; + } else if (options.style.cramped) { + minSupShift = metrics.sup3; } else { - minSupShift = style.metrics.sup2; + minSupShift = metrics.sup2; } // scriptspace is a font-size-independent size, so scale it // appropriately const multiplier = options.sizeMultiplier; const scriptspace = - (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em"; + (0.5 / metrics.ptPerEm) / multiplier + "em"; let supsub; if (!group.value.sup) { // Rule 18b subShift = Math.max( - subShift, style.metrics.sub1, - subm.height - 0.8 * style.metrics.xHeight); + subShift, metrics.sub1, + subm.height - 0.8 * metrics.xHeight); supsub = buildCommon.makeVList([ {type: "elem", elem: subm}, @@ -375,7 +374,7 @@ groupTypes.supsub = function(group, options) { } else if (!group.value.sub) { // Rule 18c, d supShift = Math.max(supShift, minSupShift, - supm.depth + 0.25 * style.metrics.xHeight); + supm.depth + 0.25 * metrics.xHeight); supsub = buildCommon.makeVList([ {type: "elem", elem: supm}, @@ -384,16 +383,16 @@ groupTypes.supsub = function(group, options) { supsub.children[0].style.marginRight = scriptspace; } else { supShift = Math.max( - supShift, minSupShift, supm.depth + 0.25 * style.metrics.xHeight); - subShift = Math.max(subShift, style.metrics.sub2); + supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight); + subShift = Math.max(subShift, metrics.sub2); - const ruleWidth = fontMetrics.metrics.defaultRuleThickness; + const ruleWidth = metrics.defaultRuleThickness; // Rule 18e if ((supShift - supm.depth) - (subm.height - subShift) < 4 * ruleWidth) { subShift = 4 * ruleWidth - (supShift - supm.depth) + subm.height; - const psi = 0.8 * style.metrics.xHeight - (supShift - supm.depth); + const psi = 0.8 * metrics.xHeight - (supShift - supm.depth); if (psi > 0) { supShift += psi; subShift -= psi; @@ -455,22 +454,22 @@ groupTypes.genfrac = function(group, options) { let clearance; let denomShift; if (style.size === Style.DISPLAY.size) { - numShift = style.metrics.num1; + numShift = options.fontMetrics().num1; if (ruleWidth > 0) { clearance = 3 * ruleWidth; } else { - clearance = 7 * fontMetrics.metrics.defaultRuleThickness; + clearance = 7 * options.fontMetrics().defaultRuleThickness; } - denomShift = style.metrics.denom1; + denomShift = options.fontMetrics().denom1; } else { if (ruleWidth > 0) { - numShift = style.metrics.num2; + numShift = options.fontMetrics().num2; clearance = ruleWidth; } else { - numShift = style.metrics.num3; - clearance = 3 * fontMetrics.metrics.defaultRuleThickness; + numShift = options.fontMetrics().num3; + clearance = 3 * options.fontMetrics().defaultRuleThickness; } - denomShift = style.metrics.denom2; + denomShift = options.fontMetrics().denom2; } let frac; @@ -489,7 +488,7 @@ groupTypes.genfrac = function(group, options) { ], "individualShift", null, options); } else { // Rule 15d - const axisHeight = style.metrics.axisHeight; + const axisHeight = options.fontMetrics().axisHeight; if ((numShift - numerm.depth) - (axisHeight + 0.5 * ruleWidth) < clearance) { @@ -523,9 +522,9 @@ groupTypes.genfrac = function(group, options) { // Rule 15e let delimSize; if (style.size === Style.DISPLAY.size) { - delimSize = style.metrics.delim1; + delimSize = options.fontMetrics().delim1; } else { - delimSize = style.metrics.delim2; + delimSize = options.fontMetrics().delim2; } let leftDelim; @@ -551,10 +550,10 @@ groupTypes.genfrac = function(group, options) { options); }; -const calculateSize = function(sizeValue, style) { +const calculateSize = function(sizeValue, options) { let x = sizeValue.number; if (sizeValue.unit === "ex") { - x *= style.metrics.emPerEx; + x *= options.fontMetrics().emPerEx; } else if (sizeValue.unit === "mu") { x /= 18; } @@ -568,10 +567,8 @@ groupTypes.array = function(group, options) { let nc = 0; let body = new Array(nr); - const style = options.style; - // Horizontal spacing - const pt = 1 / fontMetrics.metrics.ptPerEm; + const pt = 1 / options.fontMetrics().ptPerEm; const arraycolsep = 5 * pt; // \arraycolsep in article.cls // Vertical spacing @@ -610,7 +607,7 @@ groupTypes.array = function(group, options) { let gap = 0; if (group.value.rowGaps[r]) { - gap = calculateSize(group.value.rowGaps[r].value, style); + gap = calculateSize(group.value.rowGaps[r].value, options); if (gap > 0) { // \@argarraycr gap += arstrutDepth; if (depth < gap) { @@ -634,7 +631,7 @@ groupTypes.array = function(group, options) { body[r] = outrow; } - const offset = totalHeight / 2 + style.metrics.axisHeight; + const offset = totalHeight / 2 + options.fontMetrics().axisHeight; const colDescriptions = group.value.cols || []; const cols = []; let colSep; @@ -654,7 +651,7 @@ groupTypes.array = function(group, options) { if (!firstSeparator) { colSep = makeSpan(["arraycolsep"], []); colSep.style.width = - fontMetrics.metrics.doubleRuleSep + "em"; + options.fontMetrics().doubleRuleSep + "em"; cols.push(colSep); } @@ -829,7 +826,8 @@ groupTypes.op = function(group, options) { // almost on the axis, so these numbers are very small. Note we // don't actually apply this here, but instead it is used either in // the vlist creation or separately when there are no limits. - baseShift = (base.height - base.depth) / 2 - style.metrics.axisHeight; + baseShift = (base.height - base.depth) / 2 - + options.fontMetrics().axisHeight; // The slant of the symbol is just its italic correction. slant = base.italic; @@ -852,8 +850,8 @@ groupTypes.op = function(group, options) { supm = buildGroup(supGroup, newOptions, options); supKern = Math.max( - fontMetrics.metrics.bigOpSpacing1, - fontMetrics.metrics.bigOpSpacing3 - supm.depth); + options.fontMetrics().bigOpSpacing1, + options.fontMetrics().bigOpSpacing3 - supm.depth); } if (subGroup) { @@ -861,8 +859,8 @@ groupTypes.op = function(group, options) { subm = buildGroup(subGroup, newOptions, options); subKern = Math.max( - fontMetrics.metrics.bigOpSpacing2, - fontMetrics.metrics.bigOpSpacing4 - subm.height); + options.fontMetrics().bigOpSpacing2, + options.fontMetrics().bigOpSpacing4 - subm.height); } // Build the final group as a vlist of the possible subscript, base, @@ -874,7 +872,7 @@ groupTypes.op = function(group, options) { top = base.height - baseShift; finalGroup = buildCommon.makeVList([ - {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "kern", size: options.fontMetrics().bigOpSpacing5}, {type: "elem", elem: subm}, {type: "kern", size: subKern}, {type: "elem", elem: base}, @@ -892,7 +890,7 @@ groupTypes.op = function(group, options) { {type: "elem", elem: base}, {type: "kern", size: supKern}, {type: "elem", elem: supm}, - {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "kern", size: options.fontMetrics().bigOpSpacing5}, ], "bottom", bottom, options); // See comment above about slants @@ -903,19 +901,19 @@ groupTypes.op = function(group, options) { // subscript) but be safe. return base; } else { - bottom = fontMetrics.metrics.bigOpSpacing5 + + bottom = options.fontMetrics().bigOpSpacing5 + subm.height + subm.depth + subKern + base.depth + baseShift; finalGroup = buildCommon.makeVList([ - {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "kern", size: options.fontMetrics().bigOpSpacing5}, {type: "elem", elem: subm}, {type: "kern", size: subKern}, {type: "elem", elem: base}, {type: "kern", size: supKern}, {type: "elem", elem: supm}, - {type: "kern", size: fontMetrics.metrics.bigOpSpacing5}, + {type: "kern", size: options.fontMetrics().bigOpSpacing5}, ], "bottom", bottom, options); // See comment above about slants @@ -1019,7 +1017,7 @@ const makeLineSpan = function(className, options) { const line = makeSpan( [className].concat(baseOptions.sizingClasses(options)), [], options); - line.height = fontMetrics.metrics.defaultRuleThickness / + line.height = options.fontMetrics().defaultRuleThickness / options.sizeMultiplier; line.maxFontSize = 1.0; return line; @@ -1077,7 +1075,7 @@ groupTypes.sqrt = function(group, options) { let phi = ruleWidth; if (options.style.id < Style.TEXT.id) { - phi = options.style.metrics.xHeight * options.sizeMultiplier; + phi = options.fontMetrics().xHeight * options.sizeMultiplier; } // Calculate the clearance between the body and line @@ -1310,16 +1308,15 @@ groupTypes.middle = function(group, options) { groupTypes.rule = function(group, options) { // Make an empty span for the rule const rule = makeSpan(["mord", "rule"], [], options); - const style = options.style; // Calculate the shift, width, and height of the rule, and account for units let shift = 0; if (group.value.shift) { - shift = calculateSize(group.value.shift, style); + shift = calculateSize(group.value.shift, options); } - let width = calculateSize(group.value.width, style); - let height = calculateSize(group.value.height, style); + let width = calculateSize(group.value.width, options); + let height = calculateSize(group.value.height, options); // The sizes of rules are absolute, so make it larger if we are in a // smaller style. @@ -1343,11 +1340,10 @@ groupTypes.rule = function(group, options) { groupTypes.kern = function(group, options) { // Make an empty span for the rule const rule = makeSpan(["mord", "rule"], [], options); - const style = options.style; let dimension = 0; if (group.value.dimension) { - dimension = calculateSize(group.value.dimension, style); + dimension = calculateSize(group.value.dimension, options); } dimension /= options.sizeMultiplier; @@ -1360,7 +1356,6 @@ groupTypes.kern = function(group, options) { groupTypes.accent = function(group, options) { // Accents are handled in the TeXbook pg. 443, rule 12. let base = group.value.base; - const style = options.style; let supsubGroup; if (group.type === "supsub") { @@ -1415,7 +1410,7 @@ groupTypes.accent = function(group, options) { // calculate the amount of space between the body and the accent const clearance = Math.min( body.height, - style.metrics.xHeight); + options.fontMetrics().xHeight); // Build the accent let accentBody; @@ -1587,9 +1582,9 @@ groupTypes.enclose = function(group, options) { if (label === "sout") { img = makeSpan(["stretchy", "sout"]); - img.height = fontMetrics.metrics.defaultRuleThickness / scale; + img.height = options.fontMetrics().defaultRuleThickness / scale; img.maxFontSize = 1.0; - imgShift = -0.5 * options.style.metrics.xHeight; + imgShift = -0.5 * options.fontMetrics().xHeight; } else { // Add horizontal padding inner.classes.push((label === "fbox" ? "boxpad" : "cancel-pad")); @@ -1646,14 +1641,14 @@ groupTypes.xArrow = function(group, options) { const arrowBody = stretchy.svgSpan(group, options); - const arrowShift = -style.metrics.axisHeight + arrowBody.depth; - const upperShift = -style.metrics.axisHeight - arrowBody.height - + const arrowShift = -options.fontMetrics().axisHeight + arrowBody.depth; + const upperShift = -options.fontMetrics().axisHeight - arrowBody.height - 0.111; // 2 mu. Ref: amsmath.dtx: #7\if0#2\else\mkern#2mu\fi // Generate the vlist let vlist; if (group.value.below) { - const lowerShift = -style.metrics.axisHeight + const lowerShift = -options.fontMetrics().axisHeight + lowerGroup.height + arrowBody.height + 0.111; vlist = buildCommon.makeVList([ diff --git a/src/delimiter.js b/src/delimiter.js index 636fca7d..98a24e38 100644 --- a/src/delimiter.js +++ b/src/delimiter.js @@ -66,7 +66,7 @@ const centerSpan = function(span, options, style) { const newOptions = options.havingBaseStyle(style); const shift = (1 - options.sizeMultiplier / newOptions.sizeMultiplier) * - options.style.metrics.axisHeight; + options.fontMetrics().axisHeight; span.classes.push("delimcenter"); span.style.top = shift + "em"; @@ -275,7 +275,7 @@ const makeStackedDelim = function(delim, heightTotal, center, options, mode, // that in this context, "center" means that the delimiter should be // centered around the axis in the current style, while normally it is // centered around the axis in textstyle. - let axisHeight = options.style.metrics.axisHeight; + let axisHeight = options.fontMetrics().axisHeight; if (center) { axisHeight *= options.sizeMultiplier; } @@ -510,11 +510,11 @@ const makeLeftRightDelim = function(delim, height, depth, options, mode, classes) { // We always center \left/\right delimiters, so the axis is always shifted const axisHeight = - options.style.metrics.axisHeight * options.sizeMultiplier; + options.fontMetrics().axisHeight * options.sizeMultiplier; // Taken from TeX source, tex.web, function make_left_right const delimiterFactor = 901; - const delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm; + const delimiterExtend = 5.0 / options.fontMetrics().ptPerEm; const maxDistFromAxis = Math.max( height - axisHeight, depth + axisHeight); diff --git a/src/environments.js b/src/environments.js index 55dc797d..9ee204bc 100644 --- a/src/environments.js +++ b/src/environments.js @@ -1,7 +1,6 @@ /* eslint no-constant-condition:0 */ const parseData = require("./parseData"); const ParseError = require("./ParseError"); -const Style = require("./Style"); const ParseNode = parseData.ParseNode; @@ -190,7 +189,7 @@ defineEnvironment([ // For now we use the metrics for TEXT style which is what we were // doing before. Before attempting to get the current style we // should look at TeX's behavior especially for \over and matrices. - postgap: Style.TEXT.metrics.quad, + postgap: 1.0, /* 1em quad */ }, { type: "align", align: "l", diff --git a/src/fontMetrics.js b/src/fontMetrics.js index 51a22a00..39ae0647 100644 --- a/src/fontMetrics.js +++ b/src/fontMetrics.js @@ -1,6 +1,3 @@ -/* eslint no-unused-vars:0 */ - -const Style = require("./Style"); const cjkRegex = require("./unicodeRegexes").cjkRegex; /** @@ -11,8 +8,9 @@ const cjkRegex = require("./unicodeRegexes").cjkRegex; */ // In TeX, there are actually three sets of dimensions, one for each of -// textstyle, scriptstyle, and scriptscriptstyle. These are provided in the -// the arrays below, in that order. +// textstyle (size index 5 and higher: >=9pt), scriptstyle (size index 3 and 4: +// 7-8pt), and scriptscriptstyle (size index 1 and 2: 5-6pt). These are +// provided in the the arrays below, in that order. // // The font metrics are stored in fonts cmsy10, cmsy7, and cmsy5 respsectively. // This was determined by running the following script: @@ -32,7 +30,7 @@ const cjkRegex = require("./unicodeRegexes").cjkRegex; // // The output of each of these commands is quite lengthy. The only part we // care about is the FONTDIMEN section. Each value is measured in EMs. -const sigmas = { +const sigmasAndXis = { slant: [0.250, 0.250, 0.250], // sigma1 space: [0.000, 0.000, 0.000], // sigma2 stretch: [0.000, 0.000, 0.000], // sigma3 @@ -55,49 +53,28 @@ const sigmas = { delim1: [2.390, 1.700, 1.980], // sigma20 delim2: [1.010, 1.157, 1.420], // sigma21 axisHeight: [0.250, 0.250, 0.250], // sigma22 -}; -// These font metrics are extracted from TeX by using -// \font\a=cmex10 -// \showthe\fontdimenX\a -// where X is the corresponding variable number. These correspond to the font -// parameters of the extension fonts (family 3). See the TeXbook, page 441. -const xi1 = 0; -const xi2 = 0; -const xi3 = 0; -const xi4 = 0; -const xi5 = 0.431; -const xi6 = 1; -const xi7 = 0; -const xi8 = 0.04; -const xi9 = 0.111; -const xi10 = 0.166; -const xi11 = 0.2; -const xi12 = 0.6; -const xi13 = 0.1; + // These font metrics are extracted from TeX by using tftopl on cmex10.tfm; + // they correspond to the font parameters of the extension fonts (family 3). + // See the TeXbook, page 441. In AMSTeX, the extension fonts scale; to + // match cmex7, we'd use cmex7.tfm values for script and scriptscript + // values. + defaultRuleThickness: [0.04, 0.04, 0.04], // xi8; cmex7: 0.049 + bigOpSpacing1: [0.111, 0.111, 0.111], // xi9 + bigOpSpacing2: [0.166, 0.166, 0.166], // xi10 + bigOpSpacing3: [0.2, 0.2, 0.2], // xi11 + bigOpSpacing4: [0.6, 0.6, 0.6], // xi12; cmex7: 0.611 + bigOpSpacing5: [0.1, 0.1, 0.1], // xi13; cmex7: 0.143 -// This value determines how large a pt is, for metrics which are defined in -// terms of pts. -// This value is also used in katex.less; if you change it make sure the values -// match. -const ptPerEm = 10.0; + // This value determines how large a pt is, for metrics which are defined + // in terms of pts. + // This value is also used in katex.less; if you change it make sure the + // values match. + ptPerEm: [10.0, 10.0, 10.0], -// The space between adjacent `|` columns in an array definition. From -// `\showthe\doublerulesep` in LaTeX. -const doubleRuleSep = 2.0 / ptPerEm; - -/** - * This is just a mapping from common names to real metrics - */ -const metrics = { - defaultRuleThickness: xi8, - bigOpSpacing1: xi9, - bigOpSpacing2: xi10, - bigOpSpacing3: xi11, - bigOpSpacing4: xi12, - bigOpSpacing5: xi13, - ptPerEm: ptPerEm, - doubleRuleSep: doubleRuleSep, + // The space between adjacent `|` columns in an array definition. From + // `\showthe\doublerulesep` in LaTeX. Equals 2.0 / ptPerEm. + doubleRuleSep: [0.2, 0.2, 0.2], }; // This map contains a mapping from font name and character code to character @@ -271,8 +248,33 @@ const getCharacterMetrics = function(character, style) { } }; +const fontMetricsBySizeIndex = {}; + +/** + * Get the font metrics for a given size. + */ +const getFontMetrics = function(size) { + let sizeIndex; + if (size >= 5) { + sizeIndex = 0; + } else if (size >= 3) { + sizeIndex = 1; + } else { + sizeIndex = 2; + } + if (!fontMetricsBySizeIndex[sizeIndex]) { + const metrics = fontMetricsBySizeIndex[sizeIndex] = {}; + for (const key in sigmasAndXis) { + if (sigmasAndXis.hasOwnProperty(key)) { + metrics[key] = sigmasAndXis[key][sizeIndex]; + } + } + metrics.emPerEx = metrics.xHeight / metrics.quad; + } + return fontMetricsBySizeIndex[sizeIndex]; +}; + module.exports = { - metrics: metrics, - sigmas: sigmas, + getFontMetrics: getFontMetrics, getCharacterMetrics: getCharacterMetrics, };