To @flow: fontMetrics, fontMetricsData, Options, Settings, Style (#848)

*     To @flow: fontMetrics, fontMetricsData, Options, Settings, Style

* Don't overuse $Shape, improve type checking for fontMetrics*, make maxSize required in OptionsData and update callers.

* Remove eslintrc globals change, since eslint-plugin-flowtype makes it redundant.

* Remove extra ?s in Options and Settings

* Undo removal of width in fontMetrics and switch to `T | void` for nullable types in Options

* fix typing of FontMetrics
This commit is contained in:
Xuming Zeng
2017-09-09 16:21:52 -05:00
committed by Kevin Barabash
parent ccf09786cc
commit 0f9fb0a1ce
8 changed files with 139 additions and 54 deletions

View File

@@ -1,3 +1,4 @@
// @flow
/** /**
* This file contains information about the options that the Parser carries * This file contains information about the options that the Parser carries
* around with it while parsing. Data is held in an `Options` object, and when * around with it while parsing. Data is held in an `Options` object, and when
@@ -6,8 +7,8 @@
*/ */
import fontMetrics from "./fontMetrics"; import fontMetrics from "./fontMetrics";
import type {FontMetrics} from "./fontMetrics";
const BASESIZE = 6; import type {StyleInterface} from "./Style";
const sizeStyleMap = [ const sizeStyleMap = [
// Each element contains [textsize, scriptsize, scriptscriptsize]. // Each element contains [textsize, scriptsize, scriptscriptsize].
@@ -31,10 +32,20 @@ const sizeMultipliers = [
0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.2, 1.44, 1.728, 2.074, 2.488,
]; ];
const sizeAtStyle = function(size, style) { const sizeAtStyle = function(size: number, style: StyleInterface): number {
return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1]; return style.size < 2 ? size : sizeStyleMap[size - 1][style.size - 1];
}; };
export type OptionsData = {
style: StyleInterface;
color?: string | void;
size?: number;
textSize?: number;
phantom?: boolean;
font?: string | void;
maxSize: number;
};
/** /**
* This is the main options class. It contains the current style, size, color, * This is the main options class. It contains the current style, size, color,
* and font. * and font.
@@ -43,23 +54,38 @@ const sizeAtStyle = function(size, style) {
* different properties, call a `.having*` method. * different properties, call a `.having*` method.
*/ */
class Options { class Options {
constructor(data) { style: StyleInterface;
color: string | void;
size: number;
textSize: number;
phantom: boolean;
font: string | void;
sizeMultiplier: number;
maxSize: number;
_fontMetrics: FontMetrics | void;
/**
* The base size index.
*/
static BASESIZE = 6;
constructor(data: OptionsData) {
this.style = data.style; this.style = data.style;
this.color = data.color; this.color = data.color;
this.size = data.size || BASESIZE; this.size = data.size || Options.BASESIZE;
this.textSize = data.textSize || this.size; this.textSize = data.textSize || this.size;
this.phantom = data.phantom; this.phantom = !!data.phantom;
this.font = data.font; this.font = data.font;
this.sizeMultiplier = sizeMultipliers[this.size - 1]; this.sizeMultiplier = sizeMultipliers[this.size - 1];
this.maxSize = data.maxSize; this.maxSize = data.maxSize;
this._fontMetrics = null; this._fontMetrics = undefined;
} }
/** /**
* Returns a new options object with the same properties as "this". Properties * Returns a new options object with the same properties as "this". Properties
* from "extension" will be copied to the new options object. * from "extension" will be copied to the new options object.
*/ */
extend(extension) { extend(extension: $Shape<OptionsData>): Options {
const data = { const data = {
style: this.style, style: this.style,
size: this.size, size: this.size,
@@ -83,7 +109,7 @@ class Options {
* Return an options object with the given style. If `this.style === style`, * Return an options object with the given style. If `this.style === style`,
* returns `this`. * returns `this`.
*/ */
havingStyle(style) { havingStyle(style: StyleInterface): Options {
if (this.style === style) { if (this.style === style) {
return this; return this;
} else { } else {
@@ -98,7 +124,7 @@ class Options {
* Return an options object with a cramped version of the current style. If * Return an options object with a cramped version of the current style. If
* the current style is cramped, returns `this`. * the current style is cramped, returns `this`.
*/ */
havingCrampedStyle() { havingCrampedStyle(): Options {
return this.havingStyle(this.style.cramp()); return this.havingStyle(this.style.cramp());
} }
@@ -106,7 +132,7 @@ class Options {
* Return an options object with the given size and in at least `\textstyle`. * Return an options object with the given size and in at least `\textstyle`.
* Returns `this` if appropriate. * Returns `this` if appropriate.
*/ */
havingSize(size) { havingSize(size: number): Options {
if (this.size === size && this.textSize === size) { if (this.size === size && this.textSize === size) {
return this; return this;
} else { } else {
@@ -122,17 +148,16 @@ class Options {
* Like `this.havingSize(BASESIZE).havingStyle(style)`. If `style` is omitted, * Like `this.havingSize(BASESIZE).havingStyle(style)`. If `style` is omitted,
* changes to at least `\textstyle`. * changes to at least `\textstyle`.
*/ */
havingBaseStyle(style) { havingBaseStyle(style: StyleInterface): Options {
style = style || this.style.text(); style = style || this.style.text();
const wantSize = sizeAtStyle(BASESIZE, style); const wantSize = sizeAtStyle(Options.BASESIZE, style);
if (this.size === wantSize && this.textSize === BASESIZE if (this.size === wantSize && this.textSize === Options.BASESIZE
&& this.style === style) { && this.style === style) {
return this; return this;
} else { } else {
return this.extend({ return this.extend({
style: style, style: style,
size: wantSize, size: wantSize,
baseSize: BASESIZE,
}); });
} }
} }
@@ -140,7 +165,7 @@ class Options {
/** /**
* Create a new options object with the given color. * Create a new options object with the given color.
*/ */
withColor(color) { withColor(color: string): Options {
return this.extend({ return this.extend({
color: color, color: color,
}); });
@@ -149,7 +174,7 @@ class Options {
/** /**
* Create a new options object with "phantom" set to true. * Create a new options object with "phantom" set to true.
*/ */
withPhantom() { withPhantom(): Options {
return this.extend({ return this.extend({
phantom: true, phantom: true,
}); });
@@ -158,7 +183,7 @@ class Options {
/** /**
* Create a new options objects with the give font. * Create a new options objects with the give font.
*/ */
withFont(font) { withFont(font: ?string): Options {
return this.extend({ return this.extend({
font: font || this.font, font: font || this.font,
}); });
@@ -168,7 +193,7 @@ class Options {
* Return the CSS sizing classes required to switch from enclosing options * Return the CSS sizing classes required to switch from enclosing options
* `oldOptions` to `this`. Returns an array of classes. * `oldOptions` to `this`. Returns an array of classes.
*/ */
sizingClasses(oldOptions) { sizingClasses(oldOptions: Options): Array<string> {
if (oldOptions.size !== this.size) { if (oldOptions.size !== this.size) {
return ["sizing", "reset-size" + oldOptions.size, "size" + this.size]; return ["sizing", "reset-size" + oldOptions.size, "size" + this.size];
} else { } else {
@@ -180,9 +205,9 @@ class Options {
* Return the CSS sizing classes required to switch to the base size. Like * Return the CSS sizing classes required to switch to the base size. Like
* `this.havingSize(BASESIZE).sizingClasses(this)`. * `this.havingSize(BASESIZE).sizingClasses(this)`.
*/ */
baseSizingClasses() { baseSizingClasses(): Array<string> {
if (this.size !== BASESIZE) { if (this.size !== Options.BASESIZE) {
return ["sizing", "reset-size" + this.size, "size" + BASESIZE]; return ["sizing", "reset-size" + this.size, "size" + Options.BASESIZE];
} else { } else {
return []; return [];
} }
@@ -191,7 +216,7 @@ class Options {
/** /**
* Return the font metrics for this size. * Return the font metrics for this size.
*/ */
fontMetrics() { fontMetrics(): FontMetrics {
if (!this._fontMetrics) { if (!this._fontMetrics) {
this._fontMetrics = fontMetrics.getFontMetrics(this.size); this._fontMetrics = fontMetrics.getFontMetrics(this.size);
} }
@@ -265,18 +290,18 @@ class Options {
* Gets the CSS color of the current options object, accounting for the * Gets the CSS color of the current options object, accounting for the
* `colorMap`. * `colorMap`.
*/ */
getColor() { getColor(): string | void {
if (this.phantom) { if (this.phantom) {
return "transparent"; return "transparent";
} else if (
this.color != null &&
Options.colorMap.hasOwnProperty(this.color)
) {
return Options.colorMap[this.color];
} else { } else {
return Options.colorMap[this.color] || this.color; return this.color;
} }
} }
} }
/**
* The base size index.
*/
Options.BASESIZE = BASESIZE;
module.exports = Options; module.exports = Options;

View File

@@ -1,3 +1,4 @@
// @flow
/** /**
* This is a module for storing settings passed into KaTeX. It correctly handles * This is a module for storing settings passed into KaTeX. It correctly handles
* default settings. * default settings.
@@ -5,6 +6,15 @@
import utils from "./utils"; import utils from "./utils";
type SettingsOptions = {
displayMode?: boolean;
throwOnError?: boolean;
errorColor?: string;
macros?: {[macroName: string]: string};
colorIsTextColor?: boolean;
maxSize?: number;
};
/** /**
* The main Settings object * The main Settings object
* *
@@ -16,7 +26,14 @@ import utils from "./utils";
* and is placed in a block with vertical margin. * and is placed in a block with vertical margin.
*/ */
class Settings { class Settings {
constructor(options) { displayMode: boolean;
throwOnError: boolean;
errorColor: string;
macros: {[macroName: string]: string};
colorIsTextColor: boolean;
maxSize: number;
constructor(options: SettingsOptions) {
// allow null options // allow null options
options = options || {}; options = options || {};
this.displayMode = utils.deflt(options.displayMode, false); this.displayMode = utils.deflt(options.displayMode, false);

View File

@@ -1,3 +1,4 @@
// @flow
/** /**
* This file contains information and classes for the various kinds of styles * This file contains information and classes for the various kinds of styles
* used in TeX. It provides a generic `Style` class, which holds information * used in TeX. It provides a generic `Style` class, which holds information
@@ -10,8 +11,12 @@
* The main style class. Contains a unique id for the style, a size (which is * 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. * the same for cramped and uncramped version of a style), and a cramped flag.
*/ */
class Style { class Style implements StyleInterface {
constructor(id, size, cramped) { id: number;
size: number;
cramped: boolean;
constructor(id: number, size: number, cramped: boolean) {
this.id = id; this.id = id;
this.size = size; this.size = size;
this.cramped = cramped; this.cramped = cramped;
@@ -20,14 +25,14 @@ class Style {
/** /**
* Get the style of a superscript given a base in the current style. * Get the style of a superscript given a base in the current style.
*/ */
sup() { sup(): Style {
return styles[sup[this.id]]; return styles[sup[this.id]];
} }
/** /**
* Get the style of a subscript given a base in the current style. * Get the style of a subscript given a base in the current style.
*/ */
sub() { sub(): Style {
return styles[sub[this.id]]; return styles[sub[this.id]];
} }
@@ -35,7 +40,7 @@ class Style {
* Get the style of a fraction numerator given the fraction in the current * Get the style of a fraction numerator given the fraction in the current
* style. * style.
*/ */
fracNum() { fracNum(): Style {
return styles[fracNum[this.id]]; return styles[fracNum[this.id]];
} }
@@ -43,7 +48,7 @@ class Style {
* Get the style of a fraction denominator given the fraction in the current * Get the style of a fraction denominator given the fraction in the current
* style. * style.
*/ */
fracDen() { fracDen(): Style {
return styles[fracDen[this.id]]; return styles[fracDen[this.id]];
} }
@@ -51,25 +56,41 @@ class Style {
* Get the cramped version of a style (in particular, cramping a cramped style * Get the cramped version of a style (in particular, cramping a cramped style
* doesn't change the style). * doesn't change the style).
*/ */
cramp() { cramp(): Style {
return styles[cramp[this.id]]; return styles[cramp[this.id]];
} }
/** /**
* Get a text or display version of this style. * Get a text or display version of this style.
*/ */
text() { text(): Style {
return styles[text[this.id]]; return styles[text[this.id]];
} }
/** /**
* Return if this style is tightly spaced (scriptstyle/scriptscriptstyle) * Return true if this style is tightly spaced (scriptstyle/scriptscriptstyle)
*/ */
isTight() { isTight(): boolean {
return this.size >= 2; return this.size >= 2;
} }
} }
// Export an interface for type checking, but don't expose the implementation.
// This way, no more styles can be generated.
export interface StyleInterface {
id: number;
size: number;
cramped: boolean;
sup(): StyleInterface;
sub(): StyleInterface;
fracNum(): StyleInterface;
fracDen(): StyleInterface;
cramp(): StyleInterface;
text(): StyleInterface;
isTight(): boolean;
}
// IDs of the different styles // IDs of the different styles
const D = 0; const D = 0;
const Dc = 1; const Dc = 1;
@@ -100,8 +121,7 @@ const fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc];
const cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]; const cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc];
const text = [D, Dc, T, Tc, T, Tc, T, Tc]; const text = [D, Dc, T, Tc, T, Tc, T, Tc];
// We only export some of the styles. Also, we don't export the `Style` class so // We only export some of the styles.
// no more styles can be generated.
module.exports = { module.exports = {
DISPLAY: styles[D], DISPLAY: styles[D],
TEXT: styles[T], TEXT: styles[T],

View File

@@ -3,8 +3,7 @@ import functions from "./functions";
import {groupTypes as htmlGroupTypes} from "./buildHTML"; import {groupTypes as htmlGroupTypes} from "./buildHTML";
import {groupTypes as mathmlGroupTypes} from "./buildMathML"; import {groupTypes as mathmlGroupTypes} from "./buildMathML";
// TODO(kevinb) use flow to define a proper type for Options import type Options from "./Options";
type Options = any;
type FunctionSpec<T> = { type FunctionSpec<T> = {
// Unique string to differentiate parse nodes. // Unique string to differentiate parse nodes.

View File

@@ -1,3 +1,4 @@
// @flow
import { cjkRegex } from "./unicodeRegexes"; import { cjkRegex } from "./unicodeRegexes";
/** /**
@@ -226,21 +227,32 @@ const extraCharacterMap = {
'я': 'r', 'я': 'r',
}; };
export type CharacterMetrics = {
depth: number;
height: number;
italic: number;
skew: number;
width: number;
};
/** /**
* This function is a convenience function for looking up information in the * This function is a convenience function for looking up information in the
* metricMap table. It takes a character as a string, and a style. * metricMap table. It takes a character as a string, and a font.
* *
* Note: the `width` property may be undefined if fontMetricsData.js wasn't * Note: the `width` property may be undefined if fontMetricsData.js wasn't
* built using `Make extended_metrics`. * built using `Make extended_metrics`.
*/ */
const getCharacterMetrics = function(character, style) { const getCharacterMetrics = function(
character: string,
font: string,
): ?CharacterMetrics {
let ch = character.charCodeAt(0); let ch = character.charCodeAt(0);
if (character[0] in extraCharacterMap) { if (character[0] in extraCharacterMap) {
ch = extraCharacterMap[character[0]].charCodeAt(0); ch = extraCharacterMap[character[0]].charCodeAt(0);
} else if (cjkRegex.test(character[0])) { } else if (cjkRegex.test(character[0])) {
ch = 'M'.charCodeAt(0); ch = 'M'.charCodeAt(0);
} }
const metrics = metricMap[style][ch]; const metrics = metricMap[font]['' + ch];
if (metrics) { if (metrics) {
return { return {
depth: metrics[0], depth: metrics[0],
@@ -252,13 +264,19 @@ const getCharacterMetrics = function(character, style) {
} }
}; };
const fontMetricsBySizeIndex = {}; type FontSizeIndex = 0 | 1 | 2;
export type FontMetrics = {
cssEmPerMu: number,
[string]: number,
};
const fontMetricsBySizeIndex: {[FontSizeIndex]: FontMetrics} = {};
/** /**
* Get the font metrics for a given size. * Get the font metrics for a given size.
*/ */
const getFontMetrics = function(size) { const getFontMetrics = function(size: number): FontMetrics {
let sizeIndex; let sizeIndex: FontSizeIndex;
if (size >= 5) { if (size >= 5) {
sizeIndex = 0; sizeIndex = 0;
} else if (size >= 3) { } else if (size >= 3) {
@@ -267,13 +285,14 @@ const getFontMetrics = function(size) {
sizeIndex = 2; sizeIndex = 2;
} }
if (!fontMetricsBySizeIndex[sizeIndex]) { if (!fontMetricsBySizeIndex[sizeIndex]) {
const metrics = fontMetricsBySizeIndex[sizeIndex] = {}; const metrics = fontMetricsBySizeIndex[sizeIndex] = {
cssEmPerMu: sigmasAndXis.quad[sizeIndex] / 18,
};
for (const key in sigmasAndXis) { for (const key in sigmasAndXis) {
if (sigmasAndXis.hasOwnProperty(key)) { if (sigmasAndXis.hasOwnProperty(key)) {
metrics[key] = sigmasAndXis[key][sizeIndex]; metrics[key] = sigmasAndXis[key][sizeIndex];
} }
} }
metrics.cssEmPerMu = metrics.quad / 18;
} }
return fontMetricsBySizeIndex[sizeIndex]; return fontMetricsBySizeIndex[sizeIndex];
}; };

View File

@@ -1,4 +1,5 @@
module.exports = { // @flow
const fontMetricsData = {
"AMS-Regular": { "AMS-Regular": {
"65": [0, 0.68889, 0, 0], "65": [0, 0.68889, 0, 0],
"66": [0, 0.68889, 0, 0], "66": [0, 0.68889, 0, 0],
@@ -1750,3 +1751,5 @@ module.exports = {
"8242": [0, 0.61111, 0, 0], "8242": [0, 0.61111, 0, 0],
}, },
}; };
module.exports = fontMetricsData;

View File

@@ -17,6 +17,7 @@ const defaultSettings = new Settings({});
const defaultOptions = new Options({ const defaultOptions = new Options({
style: Style.TEXT, style: Style.TEXT,
size: 5, size: 5,
maxSize: Infinity,
}); });
const _getBuilt = function(expr, settings) { const _getBuilt = function(expr, settings) {

View File

@@ -21,6 +21,7 @@ const getMathML = function(expr, settings) {
// Setup the default options // Setup the default options
const options = new Options({ const options = new Options({
style: startStyle, style: startStyle,
maxSize: Infinity,
}); });
const built = buildMathML(parseTree(expr, usedSettings), expr, options); const built = buildMathML(parseTree(expr, usedSettings), expr, options);