New calling convention for functions and environments
Fixes issue #255. Mixing the variable number of arguments a function receives from TeX code with the fixed arguments which the parser provides can cause some confusion. After this change, a handler will receive exactly two arguments: one is a context object from which things provided by the parser can be accessed by name, which allows for simple extensions in the future. The other is the list of TeX arguments, passed as an array. If we ever switch to EcmaScript 2015, we might want to use its destructuring features to name the elements of the args array in the function head. Until then, destructuring that array manually immediately at the beginning of the function seems like a useful convention to easily find the meaning of these arguments.
This commit is contained in:
parent
a81c4fe78d
commit
30f7a1c5bf
|
@ -195,7 +195,8 @@ Parser.prototype.handleInfixNodes = function (body, mode) {
|
|||
denomNode = new ParseNode("ordgroup", denomBody, mode);
|
||||
}
|
||||
|
||||
var value = func.handler(funcName, numerNode, denomNode);
|
||||
var value = this.callFunction(
|
||||
funcName, [numerNode, denomNode], null);
|
||||
return [new ParseNode(value.type, value, mode)];
|
||||
} else {
|
||||
return body;
|
||||
|
@ -430,11 +431,18 @@ Parser.prototype.parseImplicitGroup = function(pos, mode) {
|
|||
// Build the environment object. Arguments and other information will
|
||||
// be made available to the begin and end methods using properties.
|
||||
var env = environments[envName];
|
||||
var args = [null, mode, envName];
|
||||
var args = [];
|
||||
var newPos = this.parseArguments(
|
||||
begin.position, mode, "\\begin{" + envName + "}", env, args);
|
||||
args[0] = newPos;
|
||||
var result = env.handler.apply(this, args);
|
||||
var context = {
|
||||
pos: newPos,
|
||||
mode: mode,
|
||||
envName: envName,
|
||||
parser: this,
|
||||
lexer: this.lexer,
|
||||
positions: args.pop()
|
||||
};
|
||||
var result = env.handler(context, args);
|
||||
var endLex = this.lexer.lex(result.position, mode);
|
||||
this.expect(endLex, "\\end");
|
||||
var end = this.parseFunction(result.position, mode);
|
||||
|
@ -491,10 +499,10 @@ Parser.prototype.parseFunction = function(pos, mode) {
|
|||
this.lexer, baseGroup.position);
|
||||
}
|
||||
|
||||
var args = [func];
|
||||
var args = [];
|
||||
var newPos = this.parseArguments(
|
||||
baseGroup.result.position, mode, func, funcData, args);
|
||||
var result = functions[func].handler.apply(this, args);
|
||||
var result = this.callFunction(func, args, args.pop());
|
||||
return new ParseResult(
|
||||
new ParseNode(result.type, result, mode),
|
||||
newPos);
|
||||
|
@ -506,6 +514,18 @@ Parser.prototype.parseFunction = function(pos, mode) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Call a function handler with a suitable context and arguments.
|
||||
*/
|
||||
Parser.prototype.callFunction = function(name, args, positions) {
|
||||
var context = {
|
||||
funcName: name,
|
||||
parser: this,
|
||||
lexer: this.lexer,
|
||||
positions: positions
|
||||
};
|
||||
return functions[name].handler(context, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the arguments of a function or environment
|
||||
|
|
|
@ -50,14 +50,17 @@ function parseArray(parser, pos, mode, result) {
|
|||
* - numOptionalArgs: (optional) Just like for a function
|
||||
* A bare number instead of that object indicates the numArgs value.
|
||||
*
|
||||
* The handler function will receive the following arguments:
|
||||
* The handler function will receive two arguments
|
||||
* - context: information and references provided by the parser
|
||||
* - args: an array of arguments passed to \begin{name}
|
||||
* The context contains the following properties:
|
||||
* - pos: the current position of the parser.
|
||||
* - mode: the current parsing mode.
|
||||
* - envName: the name of the environment, one of the listed names.
|
||||
* - [args]: the arguments passed to \begin.
|
||||
* - positions: the positions associated with these arguments.
|
||||
* The handler is called with `this` referring to the parser.
|
||||
* It must return a ParseResult.
|
||||
* - parser: the parser object
|
||||
* - lexer: the lexer object
|
||||
* - positions: the positions associated with these arguments from args.
|
||||
* The handler must return a ParseResult.
|
||||
*/
|
||||
|
||||
function defineEnvironment(names, props, handler) {
|
||||
|
@ -85,8 +88,10 @@ function defineEnvironment(names, props, handler) {
|
|||
// is part of the source2e.pdf file of LaTeX2e source documentation.
|
||||
defineEnvironment("array", {
|
||||
numArgs: 1
|
||||
}, function(pos, mode, envName, colalign, positions) {
|
||||
var parser = this;
|
||||
}, function(context, args) {
|
||||
var colalign = args[0];
|
||||
var lexer = context.lexer;
|
||||
var positions = context.positions;
|
||||
colalign = colalign.value.map ? colalign.value : [colalign];
|
||||
var cols = colalign.map(function(node) {
|
||||
var ca = node.value;
|
||||
|
@ -103,14 +108,14 @@ defineEnvironment("array", {
|
|||
}
|
||||
throw new ParseError(
|
||||
"Unknown column alignment: " + node.value,
|
||||
parser.lexer, positions[1]);
|
||||
lexer, positions[1]);
|
||||
});
|
||||
var res = {
|
||||
type: "array",
|
||||
cols: cols,
|
||||
hskipBeforeAndAfter: true // \@preamble in lttab.dtx
|
||||
};
|
||||
res = parseArray(parser, pos, mode, res);
|
||||
res = parseArray(context.parser, context.pos, context.mode, res);
|
||||
return res;
|
||||
});
|
||||
|
||||
|
@ -124,7 +129,7 @@ defineEnvironment([
|
|||
"vmatrix",
|
||||
"Vmatrix"
|
||||
], {
|
||||
}, function(pos, mode, envName) {
|
||||
}, function(context) {
|
||||
var delimiters = {
|
||||
"matrix": null,
|
||||
"pmatrix": ["(", ")"],
|
||||
|
@ -132,18 +137,18 @@ defineEnvironment([
|
|||
"Bmatrix": ["\\{", "\\}"],
|
||||
"vmatrix": ["|", "|"],
|
||||
"Vmatrix": ["\\Vert", "\\Vert"]
|
||||
}[envName];
|
||||
}[context.envName];
|
||||
var res = {
|
||||
type: "array",
|
||||
hskipBeforeAndAfter: false // \hskip -\arraycolsep in amsmath
|
||||
};
|
||||
res = parseArray(this, pos, mode, res);
|
||||
res = parseArray(context.parser, context.pos, context.mode, res);
|
||||
if (delimiters) {
|
||||
res.result = new ParseNode("leftright", {
|
||||
body: [res.result],
|
||||
left: delimiters[0],
|
||||
right: delimiters[1]
|
||||
}, mode);
|
||||
}, context.mode);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
@ -152,7 +157,7 @@ defineEnvironment([
|
|||
// \def\arraystretch{1.2}%
|
||||
// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right.
|
||||
defineEnvironment("cases", {
|
||||
}, function(pos, mode, envName) {
|
||||
}, function(context) {
|
||||
var res = {
|
||||
type: "array",
|
||||
arraystretch: 1.2,
|
||||
|
@ -168,11 +173,11 @@ defineEnvironment("cases", {
|
|||
postgap: 0
|
||||
}]
|
||||
};
|
||||
res = parseArray(this, pos, mode, res);
|
||||
res = parseArray(context.parser, context.pos, context.mode, res);
|
||||
res.result = new ParseNode("leftright", {
|
||||
body: [res.result],
|
||||
left: "\\{",
|
||||
right: "."
|
||||
}, mode);
|
||||
}, context.mode);
|
||||
return res;
|
||||
});
|
||||
|
|
115
src/functions.js
115
src/functions.js
|
@ -2,9 +2,9 @@ var utils = require("./utils");
|
|||
var ParseError = require("./ParseError");
|
||||
|
||||
/* This file contains a list of functions that we parse, identified by
|
||||
* the calls to declareFunction.
|
||||
* the calls to defineFunction.
|
||||
*
|
||||
* The first argument to declareFunction is a single name or a list of names.
|
||||
* 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.
|
||||
*
|
||||
* Each declared function can have associated properties, which
|
||||
|
@ -58,14 +58,17 @@ var ParseError = require("./ParseError");
|
|||
*
|
||||
* The last argument is that implementation, the handler for the function(s).
|
||||
* It is called to handle these functions and their arguments.
|
||||
* Its own arguments are:
|
||||
* - func: the text of the function
|
||||
* - [args]: the next arguments are the arguments to the function,
|
||||
* of which there are numArgs of them
|
||||
* It receives two arguments:
|
||||
* - context contains information and references provided by the parser
|
||||
* - args is an array of arguments obtained from TeX input
|
||||
* The context contains the following properties:
|
||||
* - funcName: the text (i.e. name) of the function, including \
|
||||
* - parser: the parser object
|
||||
* - lexer: the lexer object
|
||||
* - positions: the positions in the overall string of the function
|
||||
* and the arguments. Should only be used to produce
|
||||
* error messages
|
||||
* The handler is called with `this` referring to the parser.
|
||||
* and the arguments.
|
||||
* The latter three should only be used to produce error messages.
|
||||
*
|
||||
* 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
|
||||
|
@ -99,7 +102,9 @@ function defineFunction(names, props, handler) {
|
|||
defineFunction("\\sqrt", {
|
||||
numArgs: 1,
|
||||
numOptionalArgs: 1
|
||||
}, function(func, index, body, positions) {
|
||||
}, function(context, args) {
|
||||
var index = args[0];
|
||||
var body = args[1];
|
||||
return {
|
||||
type: "sqrt",
|
||||
body: body,
|
||||
|
@ -112,7 +117,8 @@ defineFunction("\\text", {
|
|||
numArgs: 1,
|
||||
argTypes: ["text"],
|
||||
greediness: 2
|
||||
}, function(func, body) {
|
||||
}, function(context, args) {
|
||||
var body = args[0];
|
||||
// Since the corresponding buildHTML/buildMathML function expects a
|
||||
// list of elements, we normalize for different kinds of arguments
|
||||
// TODO(emily): maybe this should be done somewhere else
|
||||
|
@ -135,7 +141,9 @@ defineFunction("\\color", {
|
|||
allowedInText: true,
|
||||
greediness: 3,
|
||||
argTypes: ["color", "original"]
|
||||
}, function(func, color, body) {
|
||||
}, function(context, args) {
|
||||
var color = args[0];
|
||||
var body = args[1];
|
||||
// Normalize the different kinds of bodies (see \text above)
|
||||
var inner;
|
||||
if (body.type === "ordgroup") {
|
||||
|
@ -154,7 +162,8 @@ defineFunction("\\color", {
|
|||
// An overline
|
||||
defineFunction("\\overline", {
|
||||
numArgs: 1
|
||||
}, function(func, body) {
|
||||
}, function(context, args) {
|
||||
var body = args[0];
|
||||
return {
|
||||
type: "overline",
|
||||
body: body
|
||||
|
@ -166,7 +175,10 @@ defineFunction("\\rule", {
|
|||
numArgs: 2,
|
||||
numOptionalArgs: 1,
|
||||
argTypes: ["size", "size", "size"]
|
||||
}, function(func, shift, width, height) {
|
||||
}, function(context, args) {
|
||||
var shift = args[0];
|
||||
var width = args[1];
|
||||
var height = args[2];
|
||||
return {
|
||||
type: "rule",
|
||||
shift: shift && shift.value,
|
||||
|
@ -178,7 +190,7 @@ defineFunction("\\rule", {
|
|||
// A KaTeX logo
|
||||
defineFunction("\\KaTeX", {
|
||||
numArgs: 0
|
||||
}, function(func) {
|
||||
}, function(context) {
|
||||
return {
|
||||
type: "katex"
|
||||
};
|
||||
|
@ -186,7 +198,8 @@ defineFunction("\\KaTeX", {
|
|||
|
||||
defineFunction("\\phantom", {
|
||||
numArgs: 1
|
||||
}, function(func, body) {
|
||||
}, function(context, args) {
|
||||
var body = args[0];
|
||||
var inner;
|
||||
if (body.type === "ordgroup") {
|
||||
inner = body.value;
|
||||
|
@ -260,7 +273,8 @@ defineFunction([
|
|||
numArgs: 1,
|
||||
allowedInText: true,
|
||||
greediness: 3
|
||||
}, function(func, body) {
|
||||
}, function(context, args) {
|
||||
var body = args[0];
|
||||
var atoms;
|
||||
if (body.type === "ordgroup") {
|
||||
atoms = body.value;
|
||||
|
@ -270,7 +284,7 @@ defineFunction([
|
|||
|
||||
return {
|
||||
type: "color",
|
||||
color: "katex-" + func.slice(1),
|
||||
color: "katex-" + context.funcName.slice(1),
|
||||
value: atoms
|
||||
};
|
||||
});
|
||||
|
@ -287,12 +301,12 @@ defineFunction([
|
|||
"\\tan","\\tanh"
|
||||
], {
|
||||
numArgs: 0
|
||||
}, function(func) {
|
||||
}, function(context) {
|
||||
return {
|
||||
type: "op",
|
||||
limits: false,
|
||||
symbol: false,
|
||||
body: func
|
||||
body: context.funcName
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -302,12 +316,12 @@ defineFunction([
|
|||
"\\min", "\\Pr", "\\sup"
|
||||
], {
|
||||
numArgs: 0
|
||||
}, function(func) {
|
||||
}, function(context) {
|
||||
return {
|
||||
type: "op",
|
||||
limits: true,
|
||||
symbol: false,
|
||||
body: func
|
||||
body: context.funcName
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -316,12 +330,12 @@ defineFunction([
|
|||
"\\int", "\\iint", "\\iiint", "\\oint"
|
||||
], {
|
||||
numArgs: 0
|
||||
}, function(func) {
|
||||
}, function(context) {
|
||||
return {
|
||||
type: "op",
|
||||
limits: false,
|
||||
symbol: true,
|
||||
body: func
|
||||
body: context.funcName
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -332,12 +346,12 @@ defineFunction([
|
|||
"\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint"
|
||||
], {
|
||||
numArgs: 0
|
||||
}, function(func) {
|
||||
}, function(context) {
|
||||
return {
|
||||
type: "op",
|
||||
limits: true,
|
||||
symbol: true,
|
||||
body: func
|
||||
body: context.funcName
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -348,13 +362,15 @@ defineFunction([
|
|||
], {
|
||||
numArgs: 2,
|
||||
greediness: 2
|
||||
}, function(func, numer, denom) {
|
||||
}, function(context, args) {
|
||||
var numer = args[0];
|
||||
var denom = args[1];
|
||||
var hasBarLine;
|
||||
var leftDelim = null;
|
||||
var rightDelim = null;
|
||||
var size = "auto";
|
||||
|
||||
switch (func) {
|
||||
switch (context.funcName) {
|
||||
case "\\dfrac":
|
||||
case "\\frac":
|
||||
case "\\tfrac":
|
||||
|
@ -371,7 +387,7 @@ defineFunction([
|
|||
throw new Error("Unrecognized genfrac command");
|
||||
}
|
||||
|
||||
switch (func) {
|
||||
switch (context.funcName) {
|
||||
case "\\dfrac":
|
||||
case "\\dbinom":
|
||||
size = "display";
|
||||
|
@ -397,9 +413,10 @@ defineFunction([
|
|||
defineFunction(["\\llap", "\\rlap"], {
|
||||
numArgs: 1,
|
||||
allowedInText: true
|
||||
}, function(func, body) {
|
||||
}, function(context, args) {
|
||||
var body = args[0];
|
||||
return {
|
||||
type: func.slice(1),
|
||||
type: context.funcName.slice(1),
|
||||
body: body
|
||||
};
|
||||
});
|
||||
|
@ -413,17 +430,18 @@ defineFunction([
|
|||
"\\left", "\\right"
|
||||
], {
|
||||
numArgs: 1
|
||||
}, function(func, delim, positions) {
|
||||
}, function(context, args) {
|
||||
var delim = args[0];
|
||||
if (!utils.contains(delimiters, delim.value)) {
|
||||
throw new ParseError(
|
||||
"Invalid delimiter: '" + delim.value + "' after '" +
|
||||
func + "'",
|
||||
this.lexer, positions[1]);
|
||||
context.funcName + "'",
|
||||
context.lexer, context.positions[1]);
|
||||
}
|
||||
|
||||
// \left and \right are caught somewhere in Parser.js, which is
|
||||
// why this data doesn't match what is in buildHTML.
|
||||
if (func === "\\left" || func === "\\right") {
|
||||
if (context.funcName === "\\left" || context.funcName === "\\right") {
|
||||
return {
|
||||
type: "leftright",
|
||||
value: delim.value
|
||||
|
@ -431,8 +449,8 @@ defineFunction([
|
|||
} else {
|
||||
return {
|
||||
type: "delimsizing",
|
||||
size: delimiterSizes[func].size,
|
||||
delimType: delimiterSizes[func].type,
|
||||
size: delimiterSizes[context.funcName].size,
|
||||
delimType: delimiterSizes[context.funcName].type,
|
||||
value: delim.value
|
||||
};
|
||||
}
|
||||
|
@ -464,7 +482,9 @@ defineFunction([
|
|||
], {
|
||||
numArgs: 1,
|
||||
greediness: 2
|
||||
}, function (func, body) {
|
||||
}, function (context, args) {
|
||||
var body = args[0];
|
||||
var func = context.funcName;
|
||||
if (func in fontAliases) {
|
||||
func = fontAliases[func];
|
||||
}
|
||||
|
@ -483,10 +503,11 @@ defineFunction([
|
|||
// "\\widetilde", "\\widehat"
|
||||
], {
|
||||
numArgs: 1
|
||||
}, function(func, base) {
|
||||
}, function(context, args) {
|
||||
var base = args[0];
|
||||
return {
|
||||
type: "accent",
|
||||
accent: func,
|
||||
accent: context.funcName,
|
||||
base: base
|
||||
};
|
||||
});
|
||||
|
@ -494,9 +515,9 @@ defineFunction([
|
|||
// Infix generalized fractions
|
||||
defineFunction(["\\over", "\\choose"], {
|
||||
numArgs: 0
|
||||
}, function (func) {
|
||||
}, function (context) {
|
||||
var replaceWith;
|
||||
switch (func) {
|
||||
switch (context.funcName) {
|
||||
case "\\over":
|
||||
replaceWith = "\\frac";
|
||||
break;
|
||||
|
@ -517,7 +538,8 @@ defineFunction(["\\\\", "\\cr"], {
|
|||
numArgs: 0,
|
||||
numOptionalArgs: 1,
|
||||
argTypes: ["size"]
|
||||
}, function(func, size) {
|
||||
}, function(context, args) {
|
||||
var size = args[0];
|
||||
return {
|
||||
type: "cr",
|
||||
size: size
|
||||
|
@ -528,11 +550,12 @@ defineFunction(["\\\\", "\\cr"], {
|
|||
defineFunction(["\\begin", "\\end"], {
|
||||
numArgs: 1,
|
||||
argTypes: ["text"]
|
||||
}, function(func, nameGroup, positions) {
|
||||
}, function(context, args) {
|
||||
var nameGroup = args[0];
|
||||
if (nameGroup.type !== "ordgroup") {
|
||||
throw new ParseError(
|
||||
"Invalid environment name",
|
||||
this.lexer, positions[1]);
|
||||
context.lexer, context.positions[1]);
|
||||
}
|
||||
var name = "";
|
||||
for (var i = 0; i < nameGroup.value.length; ++i) {
|
||||
|
@ -541,6 +564,6 @@ defineFunction(["\\begin", "\\end"], {
|
|||
return {
|
||||
type: "environment",
|
||||
name: name,
|
||||
namepos: positions[1]
|
||||
namepos: context.positions[1]
|
||||
};
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user