Skip to content

Commit 8202b4a

Browse files
authored
Merge pull request #1165 from mathjax/fix/newcommand
Update newcommand to handle delimiters vs macros better, and add tests
2 parents 45c07db + 8785c6c commit 8202b4a

6 files changed

Lines changed: 242 additions & 25 deletions

File tree

testsuite/tests/input/tex/Newcommand.test.ts

Lines changed: 163 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ describe('Newcommand', () => {
6969
tex2mml(
7070
'\\newenvironment{argument}[1][a]{\\textbf{Argument #1:}}{aa}\\begin{argument}b\\end{argument}'
7171
),
72-
`<math xmlns=\"http://www.w3.org/1998/Math/MathML\" data-latex=\"\\newenvironment{argument}[1][a]{\\textbf{Argument #1:}}{aa}\\begin{argument}b\\end{argument}\" display=\"block\">
73-
<mtext mathvariant=\"bold\" data-latex=\"\\textbf{Argument a:}\">Argument a:</mtext>
74-
<mi data-latex=\"b\">b</mi>
75-
<mi data-latex=\"a\">a</mi>
76-
<mi data-latex=\"a\">a</mi>
72+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\newenvironment{argument}[1][a]{\\textbf{Argument #1:}}{aa}\\begin{argument}b\\end{argument}" display="block">
73+
<mtext mathvariant="bold" data-latex="\\textbf{Argument a:}">Argument a:</mtext>
74+
<mi data-latex="b">b</mi>
75+
<mi data-latex="a">a</mi>
76+
<mi data-latex="a">a</mi>
7777
</math>`
7878
));
7979
it('Newenvironment Arg Optional', () =>
@@ -773,12 +773,168 @@ describe('NewcommandError', () => {
773773
it('Missing End Error', () =>
774774
toXmlMatch(
775775
tex2mml('\\newenvironment{env}{aa}{bb}\\begin{env}cc'),
776-
`<math xmlns=\"http://www.w3.org/1998/Math/MathML\" data-latex=\"\\newenvironment{env}{aa}{bb}\\begin{env}cc\" display=\"block\">
777-
<merror data-mjx-error=\"Missing \\end{env}\">
776+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\newenvironment{env}{aa}{bb}\\begin{env}cc" display="block">
777+
<merror data-mjx-error="Missing \\end{env}">
778778
<mtext>Missing \\end{env}</mtext>
779779
</merror>
780780
</math>`
781781
));
782782
});
783783

784+
describe('Newcommand Overrides', () => {
785+
beforeEach(() => setupTex(['base', 'newcommand']));
786+
it('Let def macro be undefined', () =>
787+
toXmlMatch(
788+
tex2mml('\\def\\test{error} \\let\\test=\\undefined \\test'),
789+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\def\\test{error} \\let\\test=\\undefined \\test" display="block">
790+
<merror data-mjx-error="Undefined control sequence \\test">
791+
<mtext>Undefined control sequence \\test</mtext>
792+
</merror>
793+
</math>`
794+
));
795+
it('Let existing macro be undefined', () =>
796+
toXmlMatch(
797+
tex2mml('\\let\\sqrt=\\undefined \\sqrt{x}'),
798+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\sqrt=\\undefined \\sqrt{x}" display="block">
799+
<merror data-mjx-error="Undefined control sequence \\sqrt">
800+
<mtext>Undefined control sequence \\sqrt</mtext>
801+
</merror>
802+
</math>`
803+
));
804+
it('Let existing delimiter be undefined', () =>
805+
toXmlMatch(
806+
tex2mml('\\let\\|=\\undefined \\left\\| X \\right\\|'),
807+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\|=\\undefined \\left\\| X \\right\\|" display="block">
808+
<merror data-mjx-error="Missing or unrecognized delimiter for \\left">
809+
<mtext>Missing or unrecognized delimiter for \\left</mtext>
810+
</merror>
811+
</math>`
812+
));
813+
it('Let after def of existing macro be undefined', () =>
814+
toXmlMatch(
815+
tex2mml('\\def\\sqrt{X} \\let\\sqrt=\\undefined \\sqrt{x}'),
816+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\def\\sqrt{X} \\let\\sqrt=\\undefined \\sqrt{x}" display="block">
817+
<merror data-mjx-error="Undefined control sequence \\sqrt">
818+
<mtext>Undefined control sequence \\sqrt</mtext>
819+
</merror>
820+
</math>`
821+
));
822+
it('Def overrides let delimiter', () =>
823+
toXmlMatch(
824+
tex2mml('\\let\\test=\\| \\def\\test{x} \\left\\test X \\right\\test'),
825+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\test=\\| \\def\\test{x} \\left\\test X \\right\\test" display="block">
826+
<merror data-mjx-error="Missing or unrecognized delimiter for \\left">
827+
<mtext>Missing or unrecognized delimiter for \\left</mtext>
828+
</merror>
829+
</math>`
830+
));
831+
it('Def overrides let delimiter as macro', () =>
832+
toXmlMatch(
833+
tex2mml('\\let\\test=\\| \\def\\test{x} \\test'),
834+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\test=\\| \\def\\test{x} \\test" display="block">
835+
<mi data-latex="x">x</mi>
836+
</math>`
837+
));
838+
it('Def overrides existing delimiter', () =>
839+
toXmlMatch(
840+
tex2mml('\\def\\|{x} \\left\\| X \\right\\|'),
841+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\def\\|{x} \\left\\| X \\right\\|" display="block">
842+
<merror data-mjx-error="Missing or unrecognized delimiter for \\left">
843+
<mtext>Missing or unrecognized delimiter for \\left</mtext>
844+
</merror>
845+
</math>`
846+
));
847+
it('Def overrides existing delimiter as macro', () =>
848+
toXmlMatch(
849+
tex2mml('\\def\\|{x} \\|'),
850+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\def\\|{x} \\|" display="block">
851+
<mi data-latex="x">x</mi>
852+
</math>`
853+
));
854+
it('Let overrides def macro', () =>
855+
toXmlMatch(
856+
tex2mml('\\def\\test{x} \\let\\test=\\| \\test X \\test'),
857+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\def\\test{x} \\let\\test=\\| \\test X \\test" display="block">
858+
<mo data-mjx-texclass="ORD" fence="false" stretchy="false" data-latex="\\test">&#x2016;</mo>
859+
<mi data-latex="X">X</mi>
860+
<mo data-mjx-texclass="ORD" fence="false" stretchy="false" data-latex="\\test">&#x2016;</mo>
861+
</math>`
862+
));
863+
it('Let overrides def macro as delimiter', () =>
864+
toXmlMatch(
865+
tex2mml('\\def\\test{x} \\let\\test=\\| \\left\\test X \\right\\test'),
866+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\def\\test{x} \\let\\test=\\| \\left\\test X \\right\\test" display="block">
867+
<mrow data-mjx-texclass="INNER" data-latex-item="\\left\\test X \\right\\test" data-latex="\\def\\test{x} \\let\\test=\\| \\left\\test X \\right\\test">
868+
<mo data-mjx-texclass="OPEN" symmetric="true" data-latex-item="\\left\\test " data-latex="\\left\\test ">&#x2016;</mo>
869+
<mi data-latex="X">X</mi>
870+
<mo data-mjx-texclass="CLOSE" symmetric="true" data-latex-item="\\right\\test" data-latex="\\right\\test">&#x2016;</mo>
871+
</mrow>
872+
</math>`
873+
));
874+
it('Let overrides existing macro', () =>
875+
toXmlMatch(
876+
tex2mml('\\let\\sqrt=\\| \\sqrt X'),
877+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\sqrt=\\| \\sqrt X" display="block">
878+
<mo data-mjx-texclass="ORD" fence="false" stretchy="false" data-latex="\\sqrt">&#x2016;</mo>
879+
<mi data-latex="X">X</mi>
880+
</math>`
881+
));
882+
it('Let overrides existing macro as delimiter', () =>
883+
toXmlMatch(
884+
tex2mml('\\let\\sqrt=\\| \\left\\sqrt X \\right\\sqrt'),
885+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\sqrt=\\| \\left\\sqrt X \\right\\sqrt" display="block">
886+
<mrow data-mjx-texclass="INNER" data-latex-item="\\left\\sqrt X \\right\\sqrt" data-latex="\\let\\sqrt=\\| \\left\\sqrt X \\right\\sqrt">
887+
<mo data-mjx-texclass="OPEN" symmetric="true" data-latex-item="\\left\\sqrt " data-latex="\\left\\sqrt ">&#x2016;</mo>
888+
<mi data-latex="X">X</mi>
889+
<mo data-mjx-texclass="CLOSE" symmetric="true" data-latex-item="\\right\\sqrt" data-latex="\\right\\sqrt">&#x2016;</mo>
890+
</mrow>
891+
</math>`
892+
));
893+
it('Let overrides delimiter', () =>
894+
toXmlMatch(
895+
tex2mml('\\let\\|=\\sqrt \\left\\| X \\right\\|'),
896+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\|=\\sqrt \\left\\| X \\right\\|" display="block">
897+
<merror data-mjx-error="Missing or unrecognized delimiter for \\left">
898+
<mtext>Missing or unrecognized delimiter for \\left</mtext>
899+
</merror>
900+
</math>`
901+
));
902+
it('Let overrides delimiter as macro', () =>
903+
toXmlMatch(
904+
tex2mml('\\let\\|=\\sqrt \\| X'),
905+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\|=\\sqrt \\| X" display="block">
906+
<msqrt data-latex="\\let\\|=\\sqrt \\| X">
907+
<mi data-latex="X">X</mi>
908+
</msqrt>
909+
</math>`
910+
));
911+
it('Let of character macro overrides delimiter', () =>
912+
toXmlMatch(
913+
tex2mml('\\let\\|=\\alpha \\left\\| X \\right\\|'),
914+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\|=\\alpha \\left\\| X \\right\\|" display="block">
915+
<merror data-mjx-error="Missing or unrecognized delimiter for \\left">
916+
<mtext>Missing or unrecognized delimiter for \\left</mtext>
917+
</merror>
918+
</math>`
919+
));
920+
it('Let of character creates delimiter', () =>
921+
toXmlMatch(
922+
tex2mml('\\let\\test=< \\left\\test X \\right\\test'),
923+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\let\\test=&lt; \\left\\test X \\right\\test" display="block">
924+
<mrow data-mjx-texclass="INNER" data-latex-item="\\left\\test X \\right\\test" data-latex="\\let\\test=&lt; \\left\\test X \\right\\test">
925+
<mo data-mjx-texclass="OPEN" data-latex-item="\\left\\test " data-latex="\\left\\test ">&#x27E8;</mo>
926+
<mi data-latex="X">X</mi>
927+
<mo data-mjx-texclass="CLOSE" data-latex-item="\\right\\test" data-latex="\\right\\test">&#x27E8;</mo>
928+
</mrow>
929+
</math>`
930+
));
931+
it('Let of character overrides def', () =>
932+
toXmlMatch(
933+
tex2mml('\\def\\test{X}\\let\\test=< \\test'),
934+
`<math xmlns="http://www.w3.org/1998/Math/MathML" data-latex="\\def\\test{X}\\let\\test=&lt; \\test" display="block">
935+
<mo fence="false" stretchy="false" data-latex="\\def\\test{X}\\let\\test=&lt; \\test">&#x27E8;</mo>
936+
</math>`
937+
));
938+
});
939+
784940
afterAll(() => getTokens('newcommand'));

testsuite/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"compilerOptions": {
44
"rootDir": ".",
55
"outDir": "./js",
6-
"moduleResolution": "node",
6+
"moduleResolution": "nodenext",
77
"paths": {
88
"#js/*": ["../mjs/*"],
99
"#source/*": ["../components/mjs/*"],

ts/input/tex/MapHandler.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
*/
2323

2424
import { HandlerType } from './HandlerTypes.js';
25-
import { AbstractTokenMap, TokenMap } from './TokenMap.js';
25+
import { AbstractTokenMap, TokenMap, CharacterMap } from './TokenMap.js';
2626
import { ParseInput, ParseResult, ParseMethod } from './Types.js';
2727
import { PrioritizedList } from '../../util/PrioritizedList.js';
2828
import { FunctionList } from '../../util/FunctionList.js';
@@ -58,6 +58,8 @@ export const MapHandler = {
5858
* Class of token mappings that are active in a configuration.
5959
*/
6060
export class SubHandler {
61+
public static FALLBACK = Symbol('fallback');
62+
6163
private _configuration: PrioritizedList<TokenMap> =
6264
new PrioritizedList<TokenMap>();
6365
private _fallback: FunctionList = new FunctionList();
@@ -96,6 +98,9 @@ export class SubHandler {
9698
public parse(input: ParseInput): ParseResult {
9799
for (const { item: map } of this._configuration) {
98100
const result = map.parse(input);
101+
if (result === SubHandler.FALLBACK) {
102+
break;
103+
}
99104
if (result) {
100105
return result;
101106
}
@@ -149,6 +154,9 @@ export class SubHandler {
149154
public applicable(token: string): TokenMap {
150155
for (const { item: map } of this._configuration) {
151156
if (map.contains(token)) {
157+
if (map instanceof CharacterMap && map.lookup(token).char === null) {
158+
return null;
159+
}
152160
return map;
153161
}
154162
}

ts/input/tex/Types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export type Attributes = Record<string, Args>;
3232
export type Environment = Record<string, Args>;
3333

3434
export type ParseInput = [TexParser, string];
35-
export type ParseResult = void | boolean | StackItem;
35+
export type ParseResult = void | boolean | StackItem | symbol;
3636

3737
export type ParseMethod = (
3838
parser: TexParser,

ts/input/tex/newcommand/NewcommandMethods.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { ParseResult, ParseMethod } from '../Types.js';
2626
import TexError from '../TexError.js';
2727
import TexParser from '../TexParser.js';
2828
import * as sm from '../TokenMap.js';
29-
import { Token, Macro } from '../Token.js';
29+
import { Token } from '../Token.js';
3030
import BaseMethods from '../base/BaseMethods.js';
3131
import { ParseUtil } from '../ParseUtil.js';
3232
import { UnitUtil } from '../UnitUtil.js';
@@ -131,14 +131,13 @@ const NewcommandMethods: { [key: string]: ParseMethod } = {
131131
if (c === '\\') {
132132
// @test Let Bar, Let Brace Equal Stretchy
133133
name = NewcommandUtil.GetCSname(parser, name);
134-
const map = handlers.get(HandlerType.MACRO).applicable(name);
135-
if (!map) {
136-
// @test Let Undefined CS
134+
if (cs === name) {
137135
return;
138136
}
137+
const map = handlers.get(HandlerType.MACRO).applicable(name);
139138
if (map instanceof sm.MacroMap) {
140139
// @test Def Let, Newcommand Let
141-
const macro = (map as sm.CommandMap).lookup(name) as Macro;
140+
const macro = map.lookup(name);
142141
NewcommandUtil.addMacro(
143142
parser,
144143
cs,
@@ -148,7 +147,14 @@ const NewcommandMethods: { [key: string]: ParseMethod } = {
148147
);
149148
return;
150149
}
151-
let macro = handlers
150+
if (map instanceof sm.CharacterMap && !(map instanceof sm.DelimiterMap)) {
151+
const macro = map.lookup(name);
152+
// @test Let Relet, Let Let, Let Circular Macro
153+
const method = (p: TexParser) => map.parser(p, macro);
154+
NewcommandUtil.addMacro(parser, cs, method, [cs, macro.char]);
155+
return;
156+
}
157+
const macro = handlers
152158
.get(HandlerType.DELIMITER)
153159
.lookup('\\' + name) as Token;
154160
if (macro) {
@@ -161,10 +167,9 @@ const NewcommandMethods: { [key: string]: ParseMethod } = {
161167
);
162168
return;
163169
}
164-
macro = (map as sm.CharacterMap).lookup(name) as Token;
165-
// @test Let Relet, Let Let, Let Circular Macro
166-
const method = (p: TexParser) => map.parser(p, macro);
167-
NewcommandUtil.addMacro(parser, cs, method, [cs, macro.char]);
170+
// @test Let Undefined CS
171+
NewcommandUtil.undefineMacro(parser, cs);
172+
NewcommandUtil.undefineDelimiter(parser, '\\' + cs);
168173
return;
169174
}
170175
// @test Let Brace Equal, Let Caret

ts/input/tex/newcommand/NewcommandUtil.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
* @author v.sorge@mathjax.org (Volker Sorge)
2222
*/
2323

24+
import { HandlerType } from '../HandlerTypes.js';
25+
import { SubHandler } from '../MapHandler.js';
2426
import { UnitUtil } from '../UnitUtil.js';
2527
import TexError from '../TexError.js';
2628
import TexParser from '../TexParser.js';
@@ -270,17 +272,19 @@ export const NewcommandUtil = {
270272
*/
271273
addDelimiter(parser: TexParser, cs: string, char: string, attr: Attributes) {
272274
const handlers = parser.configuration.handlers;
273-
const handler = handlers.retrieve(
274-
NewcommandTables.NEW_DELIMITER
275-
) as sm.DelimiterMap;
276-
handler.add(cs, new Token(cs, char, attr));
275+
if (char !== null && cs.charAt(0) === '\\') {
276+
const macros = handlers.retrieve(NewcommandTables.NEW_COMMAND);
277+
(macros as sm.CommandMap).remove(cs.slice(1));
278+
}
279+
const handler = handlers.retrieve(NewcommandTables.NEW_DELIMITER);
280+
(handler as sm.DelimiterMap).add(cs, new Token(cs, char, attr));
277281
},
278282

279283
/**
280284
* Adds a new macro as extension to the parser.
281285
*
282286
* @param {TexParser} parser The current parser.
283-
* @param {string} cs The control sequence of the delimiter.
287+
* @param {string} cs The control sequence of the macro.
284288
* @param {ParseMethod} func The parse method for this macro.
285289
* @param {Args[]} attr The attributes needed for parsing.
286290
* @param {string=} token Optionally original token for macro, in case it is
@@ -293,6 +297,7 @@ export const NewcommandUtil = {
293297
attr: Args[],
294298
token: string = ''
295299
) {
300+
this.undefineDelimiter(parser, '\\' + cs);
296301
const handlers = parser.configuration.handlers;
297302
const handler = handlers.retrieve(
298303
NewcommandTables.NEW_COMMAND
@@ -320,4 +325,47 @@ export const NewcommandUtil = {
320325
) as sm.EnvironmentMap;
321326
handler.add(env, new Macro(env, func, attr));
322327
},
328+
329+
/**
330+
* Removes a user-defined macro, if there is one, and
331+
* Adds an undefined macro (to block ones in later maps),
332+
* if needed.
333+
*
334+
* @param {TexParser} parser The current parser.
335+
* @param {string} cs The control sequence to undefine.
336+
*/
337+
undefineMacro(parser: TexParser, cs: string) {
338+
const handlers = parser.configuration.handlers;
339+
const macros = handlers.retrieve(NewcommandTables.NEW_COMMAND);
340+
(macros as sm.CommandMap).remove(cs);
341+
if (handlers.get(HandlerType.MACRO).applicable(cs)) {
342+
//
343+
// This will hide the macro that is in a later mapping
344+
// by forcing the parser to jump directly to the fallback
345+
// handler.
346+
//
347+
this.addMacro(parser, cs, () => SubHandler.FALLBACK, []);
348+
}
349+
},
350+
351+
/**
352+
* Removes a user-defined delimiter, if there is one, and
353+
* Adds an undefined one (to block ones in later maps),
354+
* if needed.
355+
*
356+
* @param {TexParser} parser The current parser.
357+
* @param {string} cs The control sequence to undefine.
358+
*/
359+
undefineDelimiter(parser: TexParser, cs: string) {
360+
const handlers = parser.configuration.handlers;
361+
const delims = handlers.retrieve(NewcommandTables.NEW_DELIMITER);
362+
(delims as sm.DelimiterMap).remove(cs);
363+
if (handlers.get(HandlerType.DELIMITER).applicable(cs)) {
364+
//
365+
// This will hide the delimiter that is in a later mapping
366+
// by forcing the parser to skip any additional maps.
367+
//
368+
this.addDelimiter(parser, cs, null);
369+
}
370+
},
323371
};

0 commit comments

Comments
 (0)