commit 2c92a9a36880f6c7c68458f44ad8872326982331
parent 171e38f28a550f559d693eefc171ca55890ad9a8
Author: Kevin Barabash <kevinb7@gmail.com>
Date: Sat, 15 Apr 2017 20:39:01 -0400
Merge pull request #605 from gagern/overset
Builtin macros, macro arguments, \overset and \underset
Diffstat:
12 files changed, 131 insertions(+), 3 deletions(-)
diff --git a/.eslintrc b/.eslintrc
@@ -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"],
diff --git a/package.json b/package.json
@@ -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",
diff --git a/src/MacroExpander.js b/src/MacroExpander.js
@@ -4,16 +4,23 @@
*/
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 = [];
}
/**
* Recursively expand first token, then return first non-expandable token.
+ *
+ * At the moment, macro expansion doesn't handle delimited macros,
+ * i.e. things like those defined by \def\foo#1\end{…}.
+ * See the TeX book page 202ff. for details on how those should behave.
*/
MacroExpander.prototype.nextToken = function() {
for (;;) {
@@ -25,18 +32,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);
}
};
diff --git a/src/macros.js b/src/macros.js
@@ -0,0 +1,23 @@
+/**
+ * Predefined macros for KaTeX.
+ * This can be used to define some commands in terms of others.
+ */
+
+// This function might one day accept additional argument and do more things.
+function defineMacro(name, body) {
+ module.exports[name] = body;
+}
+
+//////////////////////////////////////////////////////////////////////
+// basics
+defineMacro("\\bgroup", "{");
+defineMacro("\\egroup", "}");
+defineMacro("\\begingroup", "{");
+defineMacro("\\endgroup", "}");
+
+//////////////////////////////////////////////////////////////////////
+// amsmath.sty
+
+// \def\overset#1#2{\binrel@{#2}\binrel@@{\mathop{\kern\z@#2}\limits^{#1}}}
+defineMacro("\\overset", "\\mathop{#2}\\limits^{#1}");
+defineMacro("\\underset", "\\mathop{#2}\\limits_{#1}");
diff --git a/test/katex-spec.js b/test/katex-spec.js
@@ -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() {
diff --git a/test/screenshotter/images/GroupMacros-chrome.png b/test/screenshotter/images/GroupMacros-chrome.png
Binary files differ.
diff --git a/test/screenshotter/images/GroupMacros-firefox.png b/test/screenshotter/images/GroupMacros-firefox.png
Binary files differ.
diff --git a/test/screenshotter/images/OverUnderset-chrome.png b/test/screenshotter/images/OverUnderset-chrome.png
Binary files differ.
diff --git a/test/screenshotter/images/OverUnderset-firefox.png b/test/screenshotter/images/OverUnderset-firefox.png
Binary files differ.
diff --git a/test/screenshotter/ss_data.js b/test/screenshotter/ss_data.js
@@ -26,5 +26,8 @@ for (var key in dict) {
}
});
itm.query = querystring.stringify(query);
+ if (itm.macros) {
+ itm.query += "&" + querystring.stringify(itm.macros);
+ }
}
module.exports = dict;
diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml
@@ -74,6 +74,11 @@ Exponents: a^{a^a_a}_{a^a_a}
FractionTest: \dfrac{a}{b}\frac{a}{b}\tfrac{a}{b}\;-\dfrac12\;1\tfrac12\;{1 \atop 2}
Functions: \sin\cos\tan\ln\log
GreekLetters: \alpha\beta\gamma\omega
+GroupMacros:
+ macros:
+ \startExp: e^\bgroup
+ \endExp: \egroup
+ tex: \startExp a+b\endExp
KaTeX: \KaTeX
Kern:
tex: \frac{a\kern{1em}b}{c}a\kern{1em}b\kern{1ex}c\kern{-0.25em}d
@@ -128,6 +133,13 @@ OpLimits: |
{\sin_2^2 \lim_2^2 \int_2^2 \sum_2^2}
{\displaystyle \lim_2^2 \int_2^2 \intop_2^2 \sum_2^2}
OverUnderline: x\underline{x}\underline{\underline{x}}\underline{x_{x_{x_x}}}\underline{x^{x^{x^x}}}\overline{x}\overline{x}\overline{x^{x^{x^x}}} \blue{\overline{\underline{x}}\underline{\overline{x}}}
+OverUnderset: |
+ \begin{array}{l}
+ x\overset?=1\\
+ {\displaystyle\lim_{t\underset{>0}\to0}}\\
+ a+b+c+d\overset{b+c=0}\longrightarrow a+d\\
+ \overset { x = y } { \sqrt { a b } }
+ \end{array}
Phantom: \dfrac{1+\phantom{x^{\blue{2}}} = x}{1+x^{\blue{2}} = x}
PrimeSpacing: f'+f_2'+f^{f'}
PrimeSuper: x'^2+x'''^2+x'^2_3+x_3'^2
diff --git a/test/screenshotter/test.html b/test/screenshotter/test.html
@@ -47,6 +47,12 @@
if (query["errorColor"]) {
settings.errorColor = query["errorColor"];
}
+ var macros = {};
+ var macroRegex = /(?:^\?|&)(?:\\|%5[Cc])([A-Za-z]+)=([^&]*)/g;
+ while ((match = macroRegex.exec(window.location.search)) !== null) {
+ settings.macros = macros;
+ macros["\\" + match[1]] = decodeURIComponent(match[2]);
+ }
katex.render(query["tex"], mathNode, settings);
document.getElementById("pre").innerHTML = query["pre"] || "";