mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-04 18:58:39 +00:00
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:
committed by
Kevin Barabash
parent
3613885da1
commit
5a4aedd882
2
katex.js
2
katex.js
@@ -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);
|
||||
};
|
||||
|
@@ -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;
|
||||
|
251
src/ParseNode.js
251
src/ParseNode.js
@@ -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<*>,
|
||||
|},
|
||||
};
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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.
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
};
|
||||
},
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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";
|
||||
|
@@ -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 = "]" | "}" | "$" | "\\)";
|
||||
|
@@ -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();
|
||||
|
Reference in New Issue
Block a user