Make ParseNode value payload and defineFunction handler functions type-safe (#1276)

* Make ParseNode `value` payload type-safe.

* Make defineFunction handlers aware of ParseNode data types.

* Add `type` to all function definitions to help determine handler return type.

* Added unit test for case caught only in screenshot test and fixed issue.

* Rename some symbol `Group`s to avoid conflicts with `ParseNode` groups.

Symbol `Group`s are also used as `ParseNode` types. However, `ParseNode`s of
these types always contain a raw text token as opposed to any structured
content. These `ParseNode`s are passed as arguments into function handlers to
create more semantical `ParseNode`s with more structure.

Before this change, "accent" and "op" were both symbol `Group`s and `ParseNode`
types. With this change, these two types (the raw accent token `ParseNode`, and
the structured semantical `ParseNode` are separated for better type-safety on
the `ParseNode` payload).

* stretchy: Remove FlowFixMe for a forced typecast that's no longer needed.
This commit is contained in:
Ashish Myles
2018-05-09 20:13:31 -04:00
committed by Kevin Barabash
parent 3613885da1
commit 5a4aedd882
18 changed files with 370 additions and 87 deletions

View File

@@ -69,7 +69,7 @@ const renderToString = function(
const generateParseTree = function(
expression: string,
options: SettingsOptions,
): ParseNode[] {
): ParseNode<*>[] {
const settings = new Settings(options);
return parseTree(expression, settings);
};

View File

@@ -15,8 +15,8 @@ class ParseError {
// Error position based on passed-in Token or ParseNode.
constructor(
message: string, // The error message
token?: Token | ParseNode, // An object providing position information
message: string, // The error message
token?: Token | ParseNode<*>, // An object providing position information
) {
let error = "KaTeX parse error: " + message;
let start;

View File

@@ -1,6 +1,9 @@
// @flow
import SourceLocation from "./SourceLocation";
import type {Mode} from "./types";
import type {ArrayEnvNodeData} from "./environments/array.js";
import type {Mode, StyleStr} from "./types";
import type {Token} from "./Token.js";
import type {Measurement} from "./units.js";
/**
* The resulting parse tree nodes of the parse tree.
@@ -10,15 +13,15 @@ import type {Mode} from "./types";
* For details on the corresponding properties see `Token` constructor.
* Providing such information can lead to better error reporting.
*/
export default class ParseNode {
type: *;
value: *;
export default class ParseNode<TYPE: NodeType> {
type: TYPE;
value: NodeValue<TYPE>;
mode: Mode;
loc: ?SourceLocation;
constructor(
type: string, // type of node, like e.g. "ordgroup"
value: mixed, // type-specific representation of the node
type: TYPE, // type of node, like e.g. "ordgroup"
value: NodeValue<TYPE>, // type-specific representation of the node
mode: Mode, // parse mode in action for this node, "math" or "text"
first?: {loc: ?SourceLocation}, // first token or node of the input for
// this node, will omit position information if unset
@@ -31,3 +34,239 @@ export default class ParseNode {
this.loc = SourceLocation.range(first, last);
}
}
export type NodeType = $Keys<ParseNodeTypes>;
export type NodeValue<TYPE: NodeType> = $ElementType<ParseNodeTypes, TYPE>;
export type AccentStructType = {|
type: "accent",
label: string,
isStretchy: boolean,
isShifty: boolean,
base: ParseNode<*>,
|};
// Map from `type` field value to corresponding `value` type.
type ParseNodeTypes = {
"array": ArrayEnvNodeData,
"accent": AccentStructType,
"color": {|
type: "color",
color: string,
value: ParseNode<*>[],
|},
"leftright": {|
body: [{|
type: "array",
hskipBeforeAndAfter: boolean,
|} | ParseNode<*>],
left: string,
right: string,
|},
"op": {|
type: "op",
limits: boolean,
symbol: boolean,
alwaysHandleSupSub?: boolean,
body?: string,
value?: ParseNode<*>[],
|},
"ordgroup": ParseNode<*>[],
"size": {|
number: number,
unit: string,
|},
"styling": {|
type: "styling",
style: StyleStr,
value: ParseNode<*>[],
|},
"supsub": {|
base: ?ParseNode<*>,
sup?: ?ParseNode<*>,
sub?: ?ParseNode<*>,
|},
"text": {|
type: "text",
body: ParseNode<*>[],
font?: string,
|},
"textord": string,
"url": string,
"verb": {|
body: string,
star: boolean,
|},
// From symbol groups, constructed in Parser.js via `symbols` lookup.
// (Some of these have "-token" suffix to distinguish them from existing
// `ParseNode` types.)
"accent-token": string,
"bin": string,
"close": string,
"inner": string,
"mathord": string,
"op-token": string,
"open": string,
"punct": string,
"rel": string,
"spacing": string,
"textord": string,
// From functions.js and functions/*.js. See also "accent", "color", "op",
// "styling", and "text" above.
"accentUnder": {|
type: "accentUnder",
label: string,
base: ParseNode<*>,
|},
"cr": {|
type: "cr",
size: ?ParseNode<*>,
|},
"delimsizing": {|
type: "delimsizing",
size: 1 | 2 | 3 | 4,
mclass: "mopen" | "mclose" | "mrel" | "mord",
value: string,
|},
"enclose": {|
type: "enclose",
label: string,
backgroundColor?: ParseNode<*>,
borderColor?: ParseNode<*>,
body: ParseNode<*>,
|},
"environment": {|
type: "environment",
name: string,
nameGroup: ParseNode<*>,
|},
"font": {|
type: "font",
font: string,
body: ParseNode<*>,
|},
"genfrac": {|
type: "genfrac",
numer: ParseNode<*>,
denom: ParseNode<*>,
hasBarLine: boolean,
leftDelim: ?string,
rightDelim: ?string,
size: StyleStr | "auto",
|},
"horizBrace": {|
type: "horizBrace",
label: string,
isOver: boolean,
base: ParseNode<*>,
|},
"href": {|
type: "href",
href: string,
body: ParseNode<*>[],
|},
"infix": {|
type: "infix",
replaceWith: string,
token: ?Token,
|},
"kern": {|
type: "kern",
dimension: Measurement,
|},
"lap": {|
type: "lap",
alignment: string,
body: ParseNode<*>,
|},
"leftright": {|
type?: "leftright",
body?: ParseNode<*>[],
left: string,
right: string,
|} | {|
type: "leftright",
value: string,
|},
"mathchoice": {|
type: "mathchoice",
display: ParseNode<*>[],
text: ParseNode<*>[],
script: ParseNode<*>[],
scriptscript: ParseNode<*>[],
|},
"middle": {|
type: "middle",
value: string,
|},
"mclass": {|
type: "mclass",
mclass: string,
value: ParseNode<*>[],
|},
"mod": {|
type: "mod",
modType: string,
value: ?ParseNode<*>[],
|},
"operatorname": {|
type: "operatorname",
value: ParseNode<*>[],
|},
"overline": {|
type: "overline",
body: ParseNode<*>,
|},
"phantom": {|
type: "phantom",
value: ParseNode<*>[],
|},
"hphantom": {|
type: "hphantom",
body: ParseNode<*>,
value: ParseNode<*>[],
|},
"vphantom": {|
type: "vphantom",
body: ParseNode<*>,
value: ParseNode<*>[],
|},
"raisebox": {|
type: "raisebox",
dy: ParseNode<*>,
body: ParseNode<*>,
value: ParseNode<*>[],
|},
"rule": {|
type: "rule",
shift: ?Measurement,
width: ParseNode<*>,
height: ParseNode<*>,
|},
"sizing": {|
type: "sizing",
size: number,
value: ParseNode<*>[],
|},
"smash": {|
type: "smash",
body: ParseNode<*>,
smashHeight: boolean,
smashDepth: boolean,
|},
"sqrt": {|
type: "sqrt",
body: ParseNode<*>,
index: ?ParseNode<*>,
|},
"underline": {|
type: "underline",
body: ParseNode<*>,
|},
"xArrow": {|
type: "xArrow",
label: string,
body: ParseNode<*>,
below: ?ParseNode<*>,
|},
};

View File

@@ -60,12 +60,12 @@ type ParsedFunc = {|
|};
type ParsedArg = {|
type: "arg",
result: ParseNode,
result: ParseNode<*>,
token: Token,
|};
type ParsedFuncOrArg = ParsedFunc | ParsedArg;
function newArgument(result: ParseNode, token: Token): ParsedArg {
function newArgument(result: ParseNode<*>, token: Token): ParsedArg {
return {type: "arg", result, token};
}
@@ -132,7 +132,7 @@ export default class Parser {
/**
* Main parsing function, which parses an entire input.
*/
parse(): ParseNode[] {
parse(): ParseNode<*>[] {
// Try to parse the input
this.consume();
const parse = this.parseInput();
@@ -142,7 +142,7 @@ export default class Parser {
/**
* Parses an entire input tree.
*/
parseInput(): ParseNode[] {
parseInput(): ParseNode<*>[] {
// Parse an expression
const expression = this.parseExpression(false);
// If we succeeded, make sure there's an EOF at the end
@@ -166,7 +166,7 @@ export default class Parser {
parseExpression(
breakOnInfix: boolean,
breakOnTokenText?: BreakToken,
): ParseNode[] {
): ParseNode<*>[] {
const body = [];
// Keep adding atoms to the body until we can't parse any more atoms (either
// we reached the end, a }, or a \right)
@@ -207,7 +207,7 @@ export default class Parser {
* There can only be one infix operator per group. If there's more than one
* then the expression is ambiguous. This can be resolved by adding {}.
*/
handleInfixNodes(body: ParseNode[]): ParseNode[] {
handleInfixNodes(body: ParseNode<*>[]): ParseNode<*>[] {
let overIndex = -1;
let funcName;
@@ -258,7 +258,7 @@ export default class Parser {
*/
handleSupSubscript(
name: string, // For error reporting.
): ParseNode {
): ParseNode<*> {
const symbolToken = this.nextToken;
const symbol = symbolToken.text;
this.consume();
@@ -296,7 +296,7 @@ export default class Parser {
* Converts the textual input of an unsupported command into a text node
* contained within a color node whose color is determined by errorColor
*/
handleUnsupportedCmd(): ParseNode {
handleUnsupportedCmd(): ParseNode<*> {
const text = this.nextToken.text;
const textordArray = [];
@@ -328,7 +328,7 @@ export default class Parser {
/**
* Parses a group with optional super/subscripts.
*/
parseAtom(breakOnTokenText?: BreakToken): ?ParseNode {
parseAtom(breakOnTokenText?: BreakToken): ?ParseNode<*> {
// The body of an atom is an implicit group, so that things like
// \left(x\right)^2 work correctly.
const base = this.parseImplicitGroup(breakOnTokenText);
@@ -402,6 +402,8 @@ export default class Parser {
}
}
// Base must be set if superscript or subscript are set per logic above,
// but need to check here for type check to pass.
if (superscript || subscript) {
// If we got either a superscript or subscript, create a supsub
return new ParseNode("supsub", {
@@ -423,7 +425,7 @@ export default class Parser {
* implicit grouping after it until the end of the group. E.g.
* small text {\Large large text} small text again
*/
parseImplicitGroup(breakOnTokenText?: BreakToken): ?ParseNode {
parseImplicitGroup(breakOnTokenText?: BreakToken): ?ParseNode<*> {
const start = this.parseSymbol();
if (start == null) {
@@ -477,7 +479,7 @@ export default class Parser {
* Parses an entire function, including its base and all of its arguments.
* It also handles the case where the parsed node is not a function.
*/
parseFunction(): ?ParseNode {
parseFunction(): ?ParseNode<*> {
const baseGroup = this.parseGroup();
return baseGroup ? this.parseGivenFunction(baseGroup) : null;
}
@@ -489,7 +491,7 @@ export default class Parser {
parseGivenFunction(
baseGroup: ParsedFuncOrArg,
breakOnTokenText?: BreakToken,
): ParseNode {
): ParseNode<*> {
if (baseGroup.type === "fn") {
const func = baseGroup.result;
const funcData = functions[func];
@@ -530,8 +532,8 @@ export default class Parser {
*/
callFunction(
name: string,
args: ParseNode[],
optArgs: (?ParseNode)[],
args: ParseNode<*>[],
optArgs: (?ParseNode<*>)[],
token?: Token,
breakOnTokenText?: BreakToken,
): * {
@@ -554,10 +556,10 @@ export default class Parser {
*/
parseArguments(
func: string, // Should look like "\name" or "\begin{name}".
funcData: FunctionSpec | EnvSpec,
funcData: FunctionSpec<*> | EnvSpec,
): {
args: ParseNode[],
optArgs: (?ParseNode)[],
args: ParseNode<*>[],
optArgs: (?ParseNode<*>)[],
} {
const totalArgs = funcData.numArgs + funcData.numOptionalArgs;
if (totalArgs === 0) {
@@ -604,7 +606,7 @@ export default class Parser {
"Expected group after '" + func + "'", nextToken);
}
}
let argNode: ParseNode;
let argNode: ParseNode<*>;
if (arg.type === "fn") {
const argGreediness =
functions[arg.result].greediness;
@@ -880,7 +882,7 @@ export default class Parser {
* characters in its value. The representation is still ASCII source.
* The group will be modified in place.
*/
formLigatures(group: ParseNode[]) {
formLigatures(group: ParseNode<*>[]) {
let n = group.length - 1;
for (let i = 0; i < n; ++i) {
const a = group[i];
@@ -977,7 +979,7 @@ export default class Parser {
// Transform combining characters into accents
if (match) {
for (let i = 0; i < match[0].length; i++) {
const accent = match[0][i];
const accent: string = match[0][i];
if (!unicodeAccents[accent]) {
throw new ParseError(`Unknown accent ' ${accent}'`, nucleus);
}

View File

@@ -15,6 +15,7 @@ import {calculateSize} from "./units";
import type Options from "./Options";
import type ParseNode from "./ParseNode";
import type {NodeType} from "./ParseNode";
import type {CharacterMetrics} from "./fontMetrics";
import type {Mode} from "./types";
import type {HtmlDomNode, DomSpan, SvgSpan, CssStyle} from "./domTree";
@@ -138,7 +139,7 @@ const mathDefault = function(
mode: Mode,
options: Options,
classes: string[],
type: string, // TODO(#892): Use ParseNode type here.
type: NodeType,
): domTree.symbolNode {
if (type === "mathord") {
const fontLookup = mathit(value, mode, options, classes);
@@ -223,9 +224,9 @@ const boldsymbol = function(
* Makes either a mathord or textord in the correct font and color.
*/
const makeOrd = function(
group: ParseNode,
group: ParseNode<*>,
options: Options,
type: string, // TODO(#892): Use ParseNode type here.
type: NodeType,
): domTree.symbolNode {
const mode = group.mode;
const value = group.value;
@@ -591,9 +592,7 @@ const makeVList = function(params: VListParam, options: Options): DomSpan {
};
// Converts verb group into body string, dealing with \verb* form
const makeVerb = function(group: ParseNode, options: Options): string {
// TODO(#892): Make ParseNode type-safe and confirm `group.type` to guarantee
// that `group.value.body` is of type string.
const makeVerb = function(group: ParseNode<"verb">, options: Options): string {
let text = group.value.body;
if (group.value.star) {
text = text.replace(/ /g, '\u2423'); // Open Box

View File

@@ -17,7 +17,7 @@ const optionsFromSettings = function(settings: Settings) {
};
export const buildTree = function(
tree: ParseNode[],
tree: ParseNode<*>[],
expression: string,
settings: Settings,
): DomSpan {
@@ -39,7 +39,7 @@ export const buildTree = function(
};
export const buildHTMLTree = function(
tree: ParseNode[],
tree: ParseNode<*>[],
expression: string,
settings: Settings,
): DomSpan {

View File

@@ -27,9 +27,9 @@ type EnvContext = {|
*/
type EnvHandler = (
context: EnvContext,
args: ParseNode[],
optArgs: (?ParseNode)[],
) => ParseNode;
args: ParseNode<*>[],
optArgs: (?ParseNode<*>)[],
) => ParseNode<*>;
/**
* - numArgs: (default 0) The number of arguments after the \begin{name} function.

View File

@@ -4,6 +4,7 @@ import {groupTypes as mathmlGroupTypes} from "./buildMathML";
import type Parser from "./Parser";
import type ParseNode from "./ParseNode";
import type {NodeType, NodeValue} from "./ParseNode";
import type Options from "./Options";
import type {ArgType, BreakToken, Mode} from "./types";
import type {Token} from "./Token";
@@ -16,12 +17,11 @@ export type FunctionContext = {|
breakOnTokenText?: BreakToken,
|};
// TODO: Enumerate all allowed output types.
export type FunctionHandler = (
export type FunctionHandler<NODETYPE: NodeType> = (
context: FunctionContext,
args: ParseNode[],
optArgs: (?ParseNode)[],
) => *;
args: ParseNode<*>[],
optArgs: (?ParseNode<*>)[],
) => NodeValue<NODETYPE>;
export type FunctionPropSpec = {
// The number of arguments the function takes.
@@ -80,9 +80,10 @@ export type FunctionPropSpec = {
consumeMode?: ?Mode,
};
type FunctionDefSpec = {|
type FunctionDefSpec<NODETYPE: NodeType> = {|
// Unique string to differentiate parse nodes.
type?: string,
// Also determines the type of the value returned by `handler`.
type: NODETYPE,
// The first argument to defineFunction is a single name or a list of names.
// All functions named in such a list will share a single implementation.
@@ -92,23 +93,22 @@ type FunctionDefSpec = {|
props: FunctionPropSpec,
// The handler is called to handle these functions and their arguments.
//
// The function should return an object with the following keys:
// - type: The type of element that this is. This is then used in
// buildHTML/buildMathML to determine which function
// should be called to build this node into a DOM node
// Any other data can be added to the object, which will be passed
// in to the function in buildHTML/buildMathML as `group.value`.
handler: ?FunctionHandler,
handler: ?FunctionHandler<NODETYPE>,
// This function returns an object representing the DOM structure to be
// created when rendering the defined LaTeX function.
// TODO: Port buildHTML to flow and make the group and return types explicit.
// TODO: Change `group` to ParseNode<NODETYPE> and make return type explicit.
htmlBuilder?: (group: *, options: Options) => *,
// This function returns an object representing the MathML structure to be
// created when rendering the defined LaTeX function.
// TODO: Port buildMathML to flow and make the group and return types explicit.
// TODO: Change `group` to ParseNode<NODETYPE> and make return type explicit.
mathmlBuilder?: (group: *, options: Options) => *,
|};
@@ -119,7 +119,8 @@ type FunctionDefSpec = {|
* 2. requires all arguments except argTypes.
* It is generated by `defineFunction()` below.
*/
export type FunctionSpec = {|
export type FunctionSpec<NODETYPE: NodeType> = {|
type: NODETYPE, // Need to use the type to avoid error. See NOTES below.
numArgs: number,
argTypes?: ArgType[],
greediness: number,
@@ -128,8 +129,23 @@ export type FunctionSpec = {|
numOptionalArgs: number,
infix: boolean,
consumeMode: ?Mode,
// FLOW TYPE NOTES: Doing either one of the following two
//
// - removing the NOTETYPE type parameter in FunctionSpec above;
// - using ?FunctionHandler<NODETYPE> below;
//
// results in a confusing flow typing error:
// "string literal `styling`. This type is incompatible with..."
// pointing to the definition of `defineFunction` and finishing with
// "some incompatible instantiation of `NODETYPE`"
//
// Having FunctionSpec<NODETYPE> above and FunctionHandler<*> below seems to
// circumvent this error. This is not harmful for catching errors since
// _functions is typed FunctionSpec<*> (it stores all TeX function specs).
// Must be specified unless it's handled directly in the parser.
handler: ?FunctionHandler,
handler: ?FunctionHandler<*>,
|};
/**
@@ -137,18 +153,20 @@ export type FunctionSpec = {|
* `functions.js` just exports this same dictionary again and makes it public.
* `Parser.js` requires this dictionary.
*/
export const _functions: {[string]: FunctionSpec} = {};
export const _functions: {[string]: FunctionSpec<*>} = {};
export default function defineFunction({
export default function defineFunction<NODETYPE: NodeType>({
type,
nodeType,
names,
props,
handler,
htmlBuilder,
mathmlBuilder,
}: FunctionDefSpec) {
}: FunctionDefSpec<NODETYPE>) {
// Set default values of functions
const data = {
type,
numArgs: props.numArgs,
argTypes: props.argTypes,
greediness: (props.greediness === undefined) ? 1 : props.greediness,
@@ -176,7 +194,7 @@ export default function defineFunction({
// Since the corresponding buildHTML/buildMathML function expects a
// list of elements, we normalize for different kinds of arguments
export const ordargument = function(arg: ParseNode): ParseNode[] {
export const ordargument = function(arg: ParseNode<*>): ParseNode<*>[] {
if (arg.type === "ordgroup") {
return arg.value;
} else {

View File

@@ -21,7 +21,7 @@ type AlignSpec = { type: "separator", separator: string } | {
pregap?: number,
postgap?: number,
};
type ArrayEnvNodeData = {
export type ArrayEnvNodeData = {
type: "array",
hskipBeforeAndAfter?: boolean,
arraystretch?: number,
@@ -29,7 +29,7 @@ type ArrayEnvNodeData = {
cols?: AlignSpec[],
// These fields are always set, but not on struct construction
// initialization.
body?: ParseNode[][], // List of rows in the (2D) array.
body?: ParseNode<*>[][], // List of rows in the (2D) array.
rowGaps?: number[],
};
@@ -43,7 +43,7 @@ function parseArray(
parser: Parser,
result: ArrayEnvNodeData,
style: StyleStr,
): ParseNode {
): ParseNode<*> {
let row = [];
const body = [row];
const rowGaps = [];
@@ -52,6 +52,7 @@ function parseArray(
cell = new ParseNode("ordgroup", cell, parser.mode);
if (style) {
cell = new ParseNode("styling", {
type: "styling",
style: style,
value: [cell],
}, parser.mode);
@@ -85,7 +86,7 @@ function parseArray(
}
result.body = body;
result.rowGaps = rowGaps;
return new ParseNode(result.type, result, parser.mode);
return new ParseNode("array", result, parser.mode);
}

View File

@@ -9,6 +9,7 @@ import {
} from "./defineFunction";
import type {FunctionPropSpec, FunctionHandler} from "./defineFunction";
import type {NodeType} from "./ParseNode";
// WARNING: New functions should be added to src/functions and imported here.
@@ -17,12 +18,15 @@ export default functions;
// Define a convenience function that mimcs the old semantics of defineFunction
// to support existing code so that we can migrate it a little bit at a time.
const defineFunction = function(
const defineFunction = function<NODETYPE: NodeType>(
// Type of node data output by the function handler. This is required to aid
// type inference of the actual function output.
type: NODETYPE,
names: string[],
props: FunctionPropSpec,
handler: ?FunctionHandler, // null only if handled in parser
handler: ?FunctionHandler<NODETYPE>, // null only if handled in parser
) {
_defineFunction({names, props, handler});
_defineFunction({type, names, props, handler});
};
// TODO(kevinb): have functions return an object and call defineFunction with
@@ -48,7 +52,7 @@ import "./functions/kern";
import "./functions/phantom";
// Math class commands except \mathop
defineFunction([
defineFunction("mclass", [
"\\mathord", "\\mathbin", "\\mathrel", "\\mathopen",
"\\mathclose", "\\mathpunct", "\\mathinner",
], {
@@ -63,7 +67,7 @@ defineFunction([
});
// Build a relation or stacked op by placing one symbol on top of another
defineFunction(["\\stackrel", "\\overset", "\\underset"], {
defineFunction("mclass", ["\\stackrel", "\\overset", "\\underset"], {
numArgs: 2,
}, function(context, args) {
const mathAxisArg = args[1];
@@ -104,7 +108,7 @@ const singleCharIntegrals: {[string]: string} = {
// displaystyle. These four groups cover the four possible choices.
// No limits, not symbols
defineFunction([
defineFunction("op", [
"\\arcsin", "\\arccos", "\\arctan", "\\arctg", "\\arcctg",
"\\arg", "\\ch", "\\cos", "\\cosec", "\\cosh", "\\cot", "\\cotg",
"\\coth", "\\csc", "\\ctg", "\\cth", "\\deg", "\\dim", "\\exp",
@@ -122,7 +126,7 @@ defineFunction([
});
// Limits, not symbols
defineFunction([
defineFunction("op", [
"\\det", "\\gcd", "\\inf", "\\lim", "\\max", "\\min", "\\Pr", "\\sup",
], {
numArgs: 0,
@@ -136,7 +140,7 @@ defineFunction([
});
// No limits, symbols
defineFunction([
defineFunction("op", [
"\\int", "\\iint", "\\iiint", "\\oint", "\u222b", "\u222c",
"\u222d", "\u222e",
], {
@@ -175,7 +179,7 @@ import "./functions/font";
import "./functions/accent";
// Horizontal stretchy braces
defineFunction([
defineFunction("horizBrace", [
"\\overbrace", "\\underbrace",
], {
numArgs: 1,
@@ -193,7 +197,7 @@ defineFunction([
import "./functions/accentunder";
// Stretchy arrows with an optional argument
defineFunction([
defineFunction("xArrow", [
"\\xleftarrow", "\\xrightarrow", "\\xLeftarrow", "\\xRightarrow",
"\\xleftrightarrow", "\\xLeftrightarrow", "\\xhookleftarrow",
"\\xhookrightarrow", "\\xmapsto", "\\xrightharpoondown",
@@ -219,7 +223,7 @@ defineFunction([
});
// Infix generalized fractions
defineFunction(["\\over", "\\choose", "\\atop"], {
defineFunction("infix", ["\\over", "\\choose", "\\atop"], {
numArgs: 0,
infix: true,
}, function(context) {
@@ -245,7 +249,7 @@ defineFunction(["\\over", "\\choose", "\\atop"], {
});
// Row breaks for aligned data
defineFunction(["\\\\", "\\cr"], {
defineFunction("cr", ["\\\\", "\\cr"], {
numArgs: 0,
numOptionalArgs: 1,
argTypes: ["size"],
@@ -258,7 +262,7 @@ defineFunction(["\\\\", "\\cr"], {
});
// Environment delimiters
defineFunction(["\\begin", "\\end"], {
defineFunction("environment", ["\\begin", "\\end"], {
numArgs: 1,
argTypes: ["text"],
}, function(context, args) {
@@ -278,7 +282,7 @@ defineFunction(["\\begin", "\\end"], {
});
// Box manipulation
defineFunction(["\\raisebox"], {
defineFunction("raisebox", ["\\raisebox"], {
numArgs: 2,
argTypes: ["size", "text"],
allowedInText: true,

View File

@@ -50,7 +50,10 @@ const delimiters = [
];
// Delimiter functions
function checkDelimiter(delim: ParseNode, context: FunctionContext): ParseNode {
function checkDelimiter(
delim: ParseNode<*>,
context: FunctionContext,
): ParseNode<*> {
if (utils.contains(delimiters, delim.value)) {
return delim;
} else {

View File

@@ -4,6 +4,7 @@ import ParseError from "../ParseError";
// Switching from text mode back to math mode
defineFunction({
type: "styling",
names: ["\\(", "$"],
props: {
numArgs: 0,
@@ -32,6 +33,7 @@ defineFunction({
// Check for extra closing math delimiters
defineFunction({
type: "text", // Doesn't matter what this is.
names: ["\\)", "\\]"],
props: {
numArgs: 0,

View File

@@ -30,11 +30,15 @@ defineFunction({
parser.consumeSpaces();
const body = parser.parseExpression(true, breakOnTokenText);
// TODO: Refactor to avoid duplicating styleMap in multiple places (e.g.
// here and in buildHTML and de-dupe the enumeration of all the styles).
// $FlowFixMe: The names above exactly match the styles.
const style: StyleStr = funcName.slice(1, funcName.length - 5);
return {
type: "styling",
// Figure out what style to use by pulling out the style from
// the function name
style: funcName.slice(1, funcName.length - 5),
style,
value: body,
};
},

View File

@@ -12,7 +12,7 @@ import type Settings from "./Settings";
/**
* Parses an expression using a Parser, then returns the parsed result.
*/
const parseTree = function(toParse: string, settings: Settings): ParseNode[] {
const parseTree = function(toParse: string, settings: Settings): ParseNode<*>[] {
if (!(typeof toParse === 'string' || toParse instanceof String)) {
throw new TypeError('KaTeX can only parse string typed expression');
}

View File

@@ -107,7 +107,7 @@ const mathMLnode = function(label: string): mathMLTree.MathNode {
// corresponds to 0.522 em inside the document.
const katexImagesData: {
[string]: ([string[], number, number] | [string[], number, number, string])
[string]: ([string[], number, number] | [[string], number, number, string])
} = {
// path(s), minWidth, height, align
overrightarrow: [["rightarrow"], 0.888, 522, "xMaxYMin"],
@@ -159,7 +159,7 @@ const katexImagesData: {
"shortrightharpoonabovebar"], 1.75, 716],
};
const groupLength = function(arg: ParseNode): number {
const groupLength = function(arg: ParseNode<*>): number {
if (arg.type === "ordgroup") {
return arg.value.length;
} else {
@@ -167,7 +167,10 @@ const groupLength = function(arg: ParseNode): number {
}
};
const svgSpan = function(group: ParseNode, options: Options): DomSpan | SvgSpan {
const svgSpan = function(
group: ParseNode<"accent">,
options: Options,
): DomSpan | SvgSpan {
// Create a span with inline SVG for the element.
function buildSvgSpan_(): {
span: DomSpan | SvgSpan,
@@ -219,13 +222,16 @@ const svgSpan = function(group: ParseNode, options: Options): DomSpan | SvgSpan
} else {
const spans = [];
const [paths, minWidth, viewBoxHeight, align1] = katexImagesData[label];
const data = katexImagesData[label];
const [paths, minWidth, viewBoxHeight] = data;
const height = viewBoxHeight / 1000;
const numSvgChildren = paths.length;
let widthClasses;
let aligns;
if (numSvgChildren === 1) {
// $FlowFixMe: All these cases must be of the 4-tuple type.
const align1: string = data[3];
widthClasses = ["hide-tail"];
aligns = [align1];
} else if (numSvgChildren === 2) {

View File

@@ -20,9 +20,13 @@
import type {Mode} from "./types";
type Font = "main" | "ams"
// Some of these have a "-token" suffix since these are also used as `ParseNode`
// types for raw text tokens, and we want to avoid conflicts with higher-level
// `ParseNode` types. These `ParseNode`s are constructed within `Parser` by
// looking up the `symbols` map.
type Group =
"accent" | "bin" | "close" | "inner" | "mathord" | "op" | "open" | "punct" |
"rel" | "spacing" | "textord";
"accent-token" | "bin" | "close" | "inner" | "mathord" |
"op-token" | "open" | "punct" | "rel" | "spacing" | "textord";
type CharInfoMap = {[string]: {font: Font, group: Group, replace: ?string}};
const symbols: {[Mode]: CharInfoMap} = {
@@ -59,12 +63,12 @@ const main = "main";
const ams = "ams";
// groups:
const accent = "accent";
const accent = "accent-token";
const bin = "bin";
const close = "close";
const inner = "inner";
const mathord = "mathord";
const op = "op";
const op = "op-token";
const open = "open";
const punct = "punct";
const rel = "rel";

View File

@@ -21,7 +21,7 @@ export type Mode = "math" | "text";
export type ArgType = "color" | "size" | "url" | "original" | Mode;
// LaTeX display style.
export type StyleStr = "text" | "display";
export type StyleStr = "text" | "display" | "script" | "scriptscript";
// Allowable token text for "break" arguments in parser
export type BreakToken = "]" | "}" | "$" | "\\)";

View File

@@ -407,6 +407,7 @@ describe("A subscript and superscript parser", function() {
it("should not fail when there is no nucleus", function() {
expect("^3").toParse();
expect("^3+").toParse();
expect("_2").toParse();
expect("^3_2").toParse();
expect("_2^3").toParse();