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.
This commit is contained in:
Eddie Kohler
2017-06-30 13:50:28 -04:00
committed by Kevin Barabash
parent ebaa3feab3
commit f23bf3fe63
6 changed files with 127 additions and 131 deletions

View File

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

View File

@@ -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];
}
/**

View File

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

View File

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

View File

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

View File

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