mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-08 20:48:41 +00:00
Merge pull request #605 from gagern/overset
Builtin macros, macro arguments, \overset and \underset
This commit is contained in:
@@ -39,7 +39,7 @@
|
|||||||
"no-with": 2,
|
"no-with": 2,
|
||||||
"one-var": [2, "never"],
|
"one-var": [2, "never"],
|
||||||
"prefer-const": 2,
|
"prefer-const": 2,
|
||||||
"prefer-spread": 2,
|
"prefer-spread": 0, // re-enable once we use es6
|
||||||
"semi": [2, "always"],
|
"semi": [2, "always"],
|
||||||
"space-before-blocks": 2,
|
"space-before-blocks": 2,
|
||||||
"space-before-function-paren": [2, "never"],
|
"space-before-function-paren": [2, "never"],
|
||||||
|
@@ -30,6 +30,7 @@
|
|||||||
"less": "~2.7.1",
|
"less": "~2.7.1",
|
||||||
"morgan": "^1.7.0",
|
"morgan": "^1.7.0",
|
||||||
"nomnom": "^1.8.1",
|
"nomnom": "^1.8.1",
|
||||||
|
"object-assign": "^4.1.0",
|
||||||
"pako": "1.0.4",
|
"pako": "1.0.4",
|
||||||
"selenium-webdriver": "^2.48.2",
|
"selenium-webdriver": "^2.48.2",
|
||||||
"sri-toolbox": "^0.2.0",
|
"sri-toolbox": "^0.2.0",
|
||||||
|
@@ -4,16 +4,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const Lexer = require("./Lexer");
|
const Lexer = require("./Lexer");
|
||||||
|
const builtinMacros = require("./macros");
|
||||||
|
const ParseError = require("./ParseError");
|
||||||
|
const objectAssign = require("object-assign");
|
||||||
|
|
||||||
function MacroExpander(input, macros) {
|
function MacroExpander(input, macros) {
|
||||||
this.lexer = new Lexer(input);
|
this.lexer = new Lexer(input);
|
||||||
this.macros = macros;
|
this.macros = objectAssign({}, builtinMacros, macros);
|
||||||
this.stack = []; // contains tokens in REVERSE order
|
this.stack = []; // contains tokens in REVERSE order
|
||||||
this.discardedWhiteSpace = [];
|
this.discardedWhiteSpace = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively expand first token, then return first non-expandable token.
|
* 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() {
|
MacroExpander.prototype.nextToken = function() {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
@@ -25,18 +32,87 @@ MacroExpander.prototype.nextToken = function() {
|
|||||||
if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) {
|
if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) {
|
||||||
return topToken;
|
return topToken;
|
||||||
}
|
}
|
||||||
|
let tok;
|
||||||
let expansion = this.macros[name];
|
let expansion = this.macros[name];
|
||||||
if (typeof expansion === "string") {
|
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);
|
const bodyLexer = new Lexer(expansion);
|
||||||
expansion = [];
|
expansion = [];
|
||||||
let tok = bodyLexer.lex();
|
tok = bodyLexer.lex();
|
||||||
while (tok.text !== "EOF") {
|
while (tok.text !== "EOF") {
|
||||||
expansion.push(tok);
|
expansion.push(tok);
|
||||||
tok = bodyLexer.lex();
|
tok = bodyLexer.lex();
|
||||||
}
|
}
|
||||||
expansion.reverse(); // to fit in with stack using push and pop
|
expansion.reverse(); // to fit in with stack using push and pop
|
||||||
|
expansion.numArgs = numArgs;
|
||||||
this.macros[name] = expansion;
|
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);
|
this.stack = this.stack.concat(expansion);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
23
src/macros.js
Normal file
23
src/macros.js
Normal file
@@ -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}");
|
@@ -2011,6 +2011,13 @@ describe("A macro expander", function() {
|
|||||||
"\\bar": "a",
|
"\\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() {
|
describe("A parser taking String objects", function() {
|
||||||
|
BIN
test/screenshotter/images/GroupMacros-chrome.png
Normal file
BIN
test/screenshotter/images/GroupMacros-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
test/screenshotter/images/GroupMacros-firefox.png
Normal file
BIN
test/screenshotter/images/GroupMacros-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
test/screenshotter/images/OverUnderset-chrome.png
Normal file
BIN
test/screenshotter/images/OverUnderset-chrome.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
test/screenshotter/images/OverUnderset-firefox.png
Normal file
BIN
test/screenshotter/images/OverUnderset-firefox.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@@ -26,5 +26,8 @@ for (var key in dict) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
itm.query = querystring.stringify(query);
|
itm.query = querystring.stringify(query);
|
||||||
|
if (itm.macros) {
|
||||||
|
itm.query += "&" + querystring.stringify(itm.macros);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = dict;
|
module.exports = dict;
|
||||||
|
@@ -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}
|
FractionTest: \dfrac{a}{b}\frac{a}{b}\tfrac{a}{b}\;-\dfrac12\;1\tfrac12\;{1 \atop 2}
|
||||||
Functions: \sin\cos\tan\ln\log
|
Functions: \sin\cos\tan\ln\log
|
||||||
GreekLetters: \alpha\beta\gamma\omega
|
GreekLetters: \alpha\beta\gamma\omega
|
||||||
|
GroupMacros:
|
||||||
|
macros:
|
||||||
|
\startExp: e^\bgroup
|
||||||
|
\endExp: \egroup
|
||||||
|
tex: \startExp a+b\endExp
|
||||||
KaTeX: \KaTeX
|
KaTeX: \KaTeX
|
||||||
Kern:
|
Kern:
|
||||||
tex: \frac{a\kern{1em}b}{c}a\kern{1em}b\kern{1ex}c\kern{-0.25em}d
|
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}
|
{\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}
|
{\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}}}
|
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}
|
Phantom: \dfrac{1+\phantom{x^{\blue{2}}} = x}{1+x^{\blue{2}} = x}
|
||||||
PrimeSpacing: f'+f_2'+f^{f'}
|
PrimeSpacing: f'+f_2'+f^{f'}
|
||||||
PrimeSuper: x'^2+x'''^2+x'^2_3+x_3'^2
|
PrimeSuper: x'^2+x'''^2+x'^2_3+x_3'^2
|
||||||
|
@@ -47,6 +47,12 @@
|
|||||||
if (query["errorColor"]) {
|
if (query["errorColor"]) {
|
||||||
settings.errorColor = 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);
|
katex.render(query["tex"], mathNode, settings);
|
||||||
document.getElementById("pre").innerHTML = query["pre"] || "";
|
document.getElementById("pre").innerHTML = query["pre"] || "";
|
||||||
|
Reference in New Issue
Block a user