Add support for \expandafter, \noexpand, \edef, \let, and \long (#2122)

* Add support for \expandafter

* Add support for \noexpand

* Add support for \edef

* Update comments

* Allow \long before macro definition

* Update documentation

* Update comments

* Fix defPrefix

* Add support for \let

* Update documentation

* Print error token

* Update documentation

* Check whether command is expandable

* Add tests

* Fix token order

* Make noexpand a Token property

* Throw error if control sequence is undefined when expanding

* Rename expandableOnly to expandOnly

* Make unexpandable macro property

* Move \expandafter to macros.js

* Add TODO

* Fix merge conflict

* Update a test case

* Remove unused functions in MacroContextInterface

* Update comments

* Refactor code

* Move \noexpand to macros

* Update MacroExpander.js

* Add a test case

* Separate control sequence check to a function

* Add support for \futurelet

* Separate RHS getter to a function

* Update documentation

* Move expandOnly logic to expandOnce

* Refactor code and update comments

Co-authored-by: Kevin Barabash <kevinb@khanacademy.org>
This commit is contained in:
ylemkimon
2020-03-11 12:14:34 +09:00
committed by GitHub
parent d6a4379b49
commit 9917d1ce84
9 changed files with 319 additions and 27 deletions

View File

@@ -562,6 +562,42 @@ exports[`A font parser \\boldsymbol should inherit mbin/mrel from argument 1`] =
]
`;
exports[`A macro expander \\let should consume one optional space after equals sign 1`] = `
[
{
"type": "ordgroup",
"body": [
{
"type": "font",
"body": {
"type": "ordgroup",
"body": [
{
"type": "mathord",
"loc": {
"end": 39,
"lexer": {
"input": "\\\\def\\\\bold{\\\\bgroup\\\\bf\\\\let\\\\next= }\\\\bold{a}",
"lastIndex": 40
},
"start": 38
},
"mode": "math",
"text": "a"
}
],
"mode": "math"
},
"font": "mathbf",
"mode": "math"
}
],
"loc": null,
"mode": "math"
}
]
`;
exports[`A parse tree generator generates a tree 1`] = `
[
{

View File

@@ -2975,6 +2975,30 @@ describe("A macro expander", function() {
}}));
});
it("should delay expansion if preceded by \\expandafter", function() {
expect`\expandafter\foo\bar`.toParseLike("x+y", new Settings({macros: {
"\\foo": "#1+#2",
"\\bar": "xy",
}}));
expect`\def\foo{x}\def\bar{\def\foo{y}}\expandafter\bar\foo`.toParseLike`x`;
// \def is not expandable, i.e., \expandafter doesn't define the macro
expect`\expandafter\foo\def\foo{x}`.not.toParse();
});
it("should not expand if preceded by \\noexpand", function() {
// \foo is not expanded and interpreted as if its meaning were \relax
expect`\noexpand\foo y`.toParseLike("y",
new Settings({macros: {"\\foo": "x"}}));
// \noexpand is expandable, so the second \foo is not expanded
expect`\expandafter\foo\noexpand\foo`.toParseLike("x",
new Settings({macros: {"\\foo": "x"}}));
// \frac is a macro and therefore expandable
expect`\noexpand\frac xy`.toParseLike`xy`;
// TODO(ylem): #2085
// \def is not expandable, so is not affected by \noexpand
// expect`\noexpand\def\foo{xy}\foo`.toParseLike`xy`;
});
it("should allow for space macro argument (text version)", function() {
expect`\text{\foo\bar}`.toParseLike(r`\text{( )}`, new Settings({macros: {
"\\foo": "(#1)",
@@ -3156,6 +3180,16 @@ describe("A macro expander", function() {
//expect`\gdef{\foo}{}`.not.toParse();
});
it("\\xdef should expand definition", function() {
expect`\def\foo{a}\xdef\bar{\foo}\def\foo{}\bar`.toParseLike`a`;
// \def\noexpand\foo{} expands into \def\foo{}
expect`\def\foo{a}\xdef\bar{\def\noexpand\foo{}}\foo\bar\foo`.toParseLike`a`;
// \foo\noexpand\foo expands into a\foo
expect`\def\foo{a}\xdef\bar{\foo\noexpand\foo}\def\foo{b}\bar`.toParseLike`ab`;
// \foo is not defined
expect`\xdef\bar{\foo}`.not.toParse();
});
it("\\def should be handled in Parser", () => {
expect`\gdef\foo{1}`.toParse(new Settings({maxExpand: 0}));
expect`2^\def\foo{1}2`.not.toParse();
@@ -3177,13 +3211,23 @@ describe("A macro expander", function() {
"\\x\\def\\x{5}\\x}\\x").toParseLike`1{2{34}35}3`;
});
it("\\global needs to followed by \\def", () => {
it("\\global needs to followed by macro prefixes, \\def or \\edef", () => {
expect`\global\def\foo{}\foo`.toParseLike``;
expect`\global\edef\foo{}\foo`.toParseLike``;
expect`\def\DEF{\def}\global\DEF\foo{}\foo`.toParseLike``;
expect`\global\global\def\foo{}\foo`.toParseLike``;
expect`\global\long\def\foo{}\foo`.toParseLike``;
expect`\global\foo`.not.toParse();
expect`\global\bar x`.not.toParse();
});
it("\\long needs to followed by macro prefixes, \\def or \\edef", () => {
expect`\long\def\foo{}\foo`.toParseLike``;
expect`\long\edef\foo{}\foo`.toParseLike``;
expect`\long\global\def\foo{}\foo`.toParseLike``;
expect`\long\foo`.not.toParse();
});
it("Macro arguments do not generate groups", () => {
expect("\\def\\x{1}\\x\\def\\foo#1{#1}\\foo{\\x\\def\\x{2}\\x}\\x")
.toParseLike`1122`;
@@ -3225,6 +3269,30 @@ describe("A macro expander", function() {
expect(macros["\\foo"]).toBeTruthy();
});
it("\\let copies the definition", () => {
expect`\let\foo=\frac\def\frac{}\foo12`.toParseLike`\frac12`;
expect`\def\foo{1}\let\bar\foo\def\foo{2}\bar`.toParseLike`1`;
expect`\let\foo=\kern\edef\bar{\foo1em}\let\kern=\relax\bar`.toParseLike`\kern1em`;
// \foo = { (left brace)
expect`\let\foo{\frac\foo1}{2}`.toParseLike`\frac{1}{2}`;
// \equals = = (equal sign)
expect`\let\equals==a\equals b`.toParseLike`a=b`;
// \foo should not be expandable and not affected by \noexpand or \edef
expect`\let\foo=x\noexpand\foo`.toParseLike`x`;
expect`\let\foo=x\edef\bar{\foo}\def\foo{y}\bar`.toParseLike`y`;
});
it("\\let should consume one optional space after equals sign", () => {
// https://tex.stackexchange.com/questions/141166/let-foo-bar-vs-let-foo-bar-let-with-equals-sign
expect`\def\:{\let\space= }\: \text{\space}`.toParseLike`\text{ }`;
const tree = getParsed`\def\bold{\bgroup\bf\let\next= }\bold{a}`;
expect(tree).toMatchSnapshot();
});
it("\\futurelet should parse correctly", () => {
expect`\futurelet\foo\frac1{2+\foo}`.toParseLike`\frac1{2+1}`;
});
it("\\newcommand doesn't change settings.macros", () => {
const macros = {};
expect`\newcommand\foo{x^2}\foo+\foo`.toParse(new Settings({macros}));