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:
Martin von Gagern 2015-06-21 10:14:03 +02:00
parent a81c4fe78d
commit 30f7a1c5bf
3 changed files with 116 additions and 68 deletions

View File

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

View File

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

View File

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