Builtin macros, macro arguments, overset and underset
* Ship predefined macros with the library, in macros.js. * Allow macro arguments #1 and so on, with argument count deduced from string. * Use these features to implement \overset and \underset, fixes #484.
This commit is contained in:
parent
40ec1b92b8
commit
7ec455083f
|
@ -39,7 +39,7 @@
|
|||
"no-with": 2,
|
||||
"one-var": [2, "never"],
|
||||
"prefer-const": 2,
|
||||
"prefer-spread": 2,
|
||||
"prefer-spread": 0, // re-enable once we use es6
|
||||
"semi": [2, "always"],
|
||||
"space-before-blocks": 2,
|
||||
"space-before-function-paren": [2, "never"],
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"less": "~2.7.1",
|
||||
"morgan": "^1.7.0",
|
||||
"nomnom": "^1.8.1",
|
||||
"object-assign": "^4.1.0",
|
||||
"pako": "1.0.4",
|
||||
"selenium-webdriver": "^2.48.2",
|
||||
"sri-toolbox": "^0.2.0",
|
||||
|
|
|
@ -4,10 +4,13 @@
|
|||
*/
|
||||
|
||||
const Lexer = require("./Lexer");
|
||||
const builtinMacros = require("./macros");
|
||||
const ParseError = require("./ParseError");
|
||||
const objectAssign = require("object-assign");
|
||||
|
||||
function MacroExpander(input, macros) {
|
||||
this.lexer = new Lexer(input);
|
||||
this.macros = macros;
|
||||
this.macros = objectAssign({}, builtinMacros, macros);
|
||||
this.stack = []; // contains tokens in REVERSE order
|
||||
this.discardedWhiteSpace = [];
|
||||
}
|
||||
|
@ -25,18 +28,87 @@ MacroExpander.prototype.nextToken = function() {
|
|||
if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) {
|
||||
return topToken;
|
||||
}
|
||||
let tok;
|
||||
let expansion = this.macros[name];
|
||||
if (typeof expansion === "string") {
|
||||
let numArgs = 0;
|
||||
if (expansion.indexOf("#") !== -1) {
|
||||
const stripped = expansion.replace(/##/g, "");
|
||||
while (stripped.indexOf("#" + (numArgs + 1)) !== -1) {
|
||||
++numArgs;
|
||||
}
|
||||
}
|
||||
const bodyLexer = new Lexer(expansion);
|
||||
expansion = [];
|
||||
let tok = bodyLexer.lex();
|
||||
tok = bodyLexer.lex();
|
||||
while (tok.text !== "EOF") {
|
||||
expansion.push(tok);
|
||||
tok = bodyLexer.lex();
|
||||
}
|
||||
expansion.reverse(); // to fit in with stack using push and pop
|
||||
expansion.numArgs = numArgs;
|
||||
this.macros[name] = expansion;
|
||||
}
|
||||
if (expansion.numArgs) {
|
||||
const args = [];
|
||||
let i;
|
||||
// obtain arguments, either single token or balanced {…} group
|
||||
for (i = 0; i < expansion.numArgs; ++i) {
|
||||
const startOfArg = this.get(true);
|
||||
if (startOfArg.text === "{") {
|
||||
const arg = [];
|
||||
let depth = 1;
|
||||
while (depth !== 0) {
|
||||
tok = this.get(false);
|
||||
arg.push(tok);
|
||||
if (tok.text === "{") {
|
||||
++depth;
|
||||
} else if (tok.text === "}") {
|
||||
--depth;
|
||||
} else if (tok.text === "EOF") {
|
||||
throw new ParseError(
|
||||
"End of input in macro argument",
|
||||
startOfArg);
|
||||
}
|
||||
}
|
||||
arg.pop(); // remove last }
|
||||
arg.reverse(); // like above, to fit in with stack order
|
||||
args[i] = arg;
|
||||
} else if (startOfArg.text === "EOF") {
|
||||
throw new ParseError(
|
||||
"End of input expecting macro argument", topToken);
|
||||
} else {
|
||||
args[i] = [startOfArg];
|
||||
}
|
||||
}
|
||||
// paste arguments in place of the placeholders
|
||||
expansion = expansion.slice(); // make a shallow copy
|
||||
for (i = expansion.length - 1; i >= 0; --i) {
|
||||
tok = expansion[i];
|
||||
if (tok.text === "#") {
|
||||
if (i === 0) {
|
||||
throw new ParseError(
|
||||
"Incomplete placeholder at end of macro body",
|
||||
tok);
|
||||
}
|
||||
tok = expansion[--i]; // next token on stack
|
||||
if (tok.text === "#") { // ## → #
|
||||
expansion.splice(i + 1, 1); // drop first #
|
||||
} else if (/^[1-9]$/.test(tok.text)) {
|
||||
// expansion.splice(i, 2, arg[0], arg[1], …)
|
||||
// to replace placeholder with the indicated argument.
|
||||
// TODO: use spread once we move to ES2015
|
||||
expansion.splice.apply(
|
||||
expansion,
|
||||
[i, 2].concat(args[tok.text - 1]));
|
||||
} else {
|
||||
throw new ParseError(
|
||||
"Not a valid argument number",
|
||||
tok);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.stack = this.stack.concat(expansion);
|
||||
}
|
||||
};
|
||||
|
|
15
src/macros.js
Normal file
15
src/macros.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Predefined macros for KaTeX.
|
||||
* This can be used to define some commands in terms of others.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// amsmath.sty
|
||||
|
||||
// \def\overset#1#2{\binrel@{#2}\binrel@@{\mathop{\kern\z@#2}\limits^{#1}}}
|
||||
"\\overset": "\\mathop{#2}\\limits^{#1}",
|
||||
"\\underset": "\\mathop{#2}\\limits_{#1}",
|
||||
|
||||
};
|
|
@ -2011,6 +2011,13 @@ describe("A macro expander", function() {
|
|||
"\\bar": "a",
|
||||
});
|
||||
});
|
||||
|
||||
it("should expand the \overset macro as expected", function() {
|
||||
expect("\\overset?=").toParseLike("\\mathop{=}\\limits^{?}");
|
||||
expect("\\overset{x=y}{\sqrt{ab}}")
|
||||
.toParseLike("\\mathop{\sqrt{ab}}\\limits^{x=y}");
|
||||
expect("\\overset {?} =").toParseLike("\\mathop{=}\\limits^{?}");
|
||||
});
|
||||
});
|
||||
|
||||
describe("A parser taking String objects", function() {
|
||||
|
|
Loading…
Reference in New Issue
Block a user