From 90f6b6a02e5e81d573ce5d86ce430c6eac78c41b Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Sun, 28 Jun 2026 16:56:47 -0400 Subject: [PATCH] Properly handle comments in arguments and other places, and handle spaces properly in macro templates. (mathjax/MathJax#3577) --- .../tests/input/tex/ConfigMacros.test.ts | 31 +-- testsuite/tests/input/tex/Newcommand.test.ts | 74 +++++- testsuite/tests/input/tex/Verb.test.ts | 2 +- .../__snapshots__/ConfigMacros.test.ts.snap | 55 +---- .../tex/__snapshots__/Newcommand.test.ts.snap | 217 ++++++++++++++++- .../tex/__snapshots__/Textmacros.test.ts.snap | 3 +- .../input/tex/__snapshots__/Verb.test.ts.snap | 2 +- ts/input/tex.ts | 2 + ts/input/tex/NodeUtil.ts | 9 +- ts/input/tex/TexParser.ts | 84 +++++-- ts/input/tex/Types.ts | 2 +- ts/input/tex/base/BaseItems.ts | 10 +- ts/input/tex/base/BaseMethods.ts | 46 ++-- ts/input/tex/bbox/BboxConfiguration.ts | 4 +- ts/input/tex/cases/CasesConfiguration.ts | 30 +-- .../configmacros/ConfigMacrosConfiguration.ts | 8 +- ts/input/tex/html/HtmlMethods.ts | 27 ++- ts/input/tex/newcommand/NewcommandMethods.ts | 35 +-- ts/input/tex/newcommand/NewcommandUtil.ts | 226 +++++++++--------- .../tex/setoptions/SetOptionsConfiguration.ts | 4 +- ts/input/tex/verb/VerbConfiguration.ts | 23 +- 21 files changed, 611 insertions(+), 283 deletions(-) diff --git a/testsuite/tests/input/tex/ConfigMacros.test.ts b/testsuite/tests/input/tex/ConfigMacros.test.ts index 395805c1d..38ed5d324 100644 --- a/testsuite/tests/input/tex/ConfigMacros.test.ts +++ b/testsuite/tests/input/tex/ConfigMacros.test.ts @@ -4,13 +4,11 @@ import '#js/input/tex/configmacros/ConfigMacrosConfiguration'; beforeEach(() => {}); -function runMacroTests( +function runMacroTest( macros: { [key: string]: any }, - control: string, macro: string ) { setupTex(['base', 'configmacros'], macros); - expect(tex2mml(control)).toMatchSnapshot(); expect(tex2mml(macro)).toMatchSnapshot(); } @@ -18,7 +16,7 @@ function runMacroTests( describe('Config Macros Active', () => { it('Macros Simple', () => { - runMacroTests({ active: { '@': '~' } }, 'A~a', 'A@a'); + runMacroTest({ active: { '@': '~' } }, 'A@a'); }); }); @@ -26,37 +24,42 @@ describe('Config Macros Active', () => { describe('Config Macros Commands', () => { it('Commands Simple', () => { - runMacroTests({ macros: { RR: '{\\bf R}' } }, '{\\bf R}', '\\RR'); + runMacroTest({ macros: { RR: '{\\bf R}' } }, '\\RR'); }); it('Commands Argument', () => { - runMacroTests( - { macros: { bold: ['{\\bf #1}', 1] } }, - '{\\bf bold}', - '\\bold{bold}' - ); + runMacroTest({ macros: { bold: ['{\\bf #1}', 1] } }, '\\bold{bold}'); }); it('Commands Aux Argument', () => { - runMacroTests( + runMacroTest( { macros: { foo: ['\\mbox{first } #1 \\mbox{ second } #2', 2, ['[', ']']], }, }, - '\\mbox{first } hi \\mbox{ second } there', '\\foo[hi]{there}' ); }); + + it('Commands Template', () => { + runMacroTest( + { + macros: { + foo: ['\\text{[#1]}', 1, [undefined , '+ \\oof']] + } + }, + '\\foo A + \\oof' + ); + }); }); /**********************************************************************************/ describe('Config Macros Environment', () => { it('Environment Simple', () => { - runMacroTests( + runMacroTest( { environments: { myHeartEnv: ['\\heartsuit', '\\spadesuit'] } }, - '\\begin{myHeartEnv}a\\end{myHeartEnv}', '\\begin{myHeartEnv}a\\end{myHeartEnv}' ); }); diff --git a/testsuite/tests/input/tex/Newcommand.test.ts b/testsuite/tests/input/tex/Newcommand.test.ts index 0ba47ba1a..75237993e 100644 --- a/testsuite/tests/input/tex/Newcommand.test.ts +++ b/testsuite/tests/input/tex/Newcommand.test.ts @@ -238,10 +238,76 @@ describe('Newcommand', () => { expectTexError('\\def\\x#1 #2 {[#1,#2]} \\x{a}{b}').toBe( 'Runaway argument for \\x?' ); - expectTexError('\\def\\x#1 #2 {[#1,#2]} \\x{a} {b}').toBe( - 'Runaway argument for \\x?' - ); - expect(tex2mml('\\def\\x#1 #2{[#1,#2]} \\x{a} {b} ')).toMatchSnapshot(); + expect(tex2mml('\\def\\x#1 #2 {[#1,#2]} \\x{a} {b}')).toMatchSnapshot(); + expect(tex2mml('\\def\\x#1 #2 {[#1,#2]} \\x{a} {b} ')).toMatchSnapshot(); + }); + + it('Def braces', () => { + expect(tex2mml('\\def\\x #1\\a{x^#1}\\x {23}\\a')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\\a{x^#1}\\x {23} \\a')).toMatchSnapshot(); + }); + + it('Def newlines, spaces, tabs', () => { + expect(tex2mml('\\def\\x #1\n#2{\\text{[#1][#2]}}\\x a\nb')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\n#2{\\text{[#1][#2]}}\\x a b')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\n#2{\\text{[#1][#2]}}\\x a\tb')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\n#2{\\text{[#1][#2]}}\\x a\t \nb')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1 #2{\\text{[#1][#2]}}\\x a\nb')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1 #2{\\text{[#1][#2]}}\\x a b')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1 #2{\\text{[#1][#2]}}\\x a\tb')).toMatchSnapshot(); + }); + + it('Def multiple spaces', () => { + expect(tex2mml('\\def\\x #1 #2{\\text{[#1][#2]}}\\x a b')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1 #2{\\text{[#1][#2]}}\\x a b')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\\a\\b#2\\c{\\text{[#1][#2]}}\\x a\\a\\b b\\c')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\\a\\b#2\\c{\\text{[#1][#2]}}\\x a\\a \\b b \\c')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\\a\\b#2\\c{\\text{[#1][#2]}}\\x a\\a \\b b \\c')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\\a b{\\text{[#1]}}\\x a\\a b')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\\a b{\\text{[#1]}}\\x a\\a\n b')).toMatchSnapshot(); + }); + + it('Def multiple arg template', () => { + expect(tex2mml('\\def\\x #1#2\\x{\\text{[#1][#2]}}\\x abc\\x')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1#2\\x{\\text{[#1][#2]}}\\x {abc}\\x')).toMatchSnapshot(); + }); + + it('Def partial template match', () => { + expect(tex2mml('\\def\\x #1ab{\\text{[#1]}}\\x axab')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1ab{\\text{[#1]}}\\x ax a b cab')).toMatchSnapshot(); + }); + + it('Def empty template arg', () => { + expect(tex2mml('\\def\\x #1ab{\\text{[#1]}}\\x ab')).toMatchSnapshot(); + }); + + it('Def comments', () => { + expect(tex2mml('\\def\\x #1{\\text{[#1]}}\\x%{}\n{ab}')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\\a{\\text{[#1]}}\\x ab%{}\ncd\\a')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1%{\n#2{\\text{[#1][#2]}}\\x ab')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1\\a#2\\b{\\text{[#1][#2]}}\\x %\\a\n a\\a %\\b\n b\\b')).toMatchSnapshot(); + expect(tex2mml('\\def\\x #1{%x}\n\\text{[#1]}}\\x a')).toMatchSnapshot(); + expectTexError('\\def\\x{\\text{%}}\\x').toBe('Missing close brace'); + expectTexError('\\def\\x #1X{\\text{[#1]}}\\x%aX').toBe('Runaway argument for \\x?'); + }); +}); + +/**********************************************************************************/ + +describe('Newcommand legacy', () => { + beforeEach(() => setupTex(['base', 'newcommand'], {legacyComments: true, legacyMacroTemplates: true})); + + it('Def legacy comments', () => { + expect(tex2mml('\\def\\x #1%X{\\text{[#1]}}\\x a%X')).toMatchSnapshot(); + expect(tex2mml('\\def\\x{\\text{%}}\\x')).toMatchSnapshot(); + }); + + it('Def legacy macro templates', () => { + expectTexError('\\def\\x #1X Y{\\text{[#1]}}\\x aX Y').toBe('Runaway argument for \\x?'); + expect(tex2mml('\\def\\x #1X Y{\\text{[#1]}}\\x aX Y')).toMatchSnapshot(); + expectTexError('\\def\\x #1X\nY{\\text{[#1]}}\\x aX Y').toBe('Runaway argument for \\x?'); + expectTexError('\\def\\x #1X\nY{\\text{[#1]}}\\x aX \nY').toBe('Runaway argument for \\x?'); + expect(tex2mml('\\def\\x #1X\nY{\\text{[#1]}}\\x aX\nY')).toMatchSnapshot(); }); }); diff --git a/testsuite/tests/input/tex/Verb.test.ts b/testsuite/tests/input/tex/Verb.test.ts index 1380689fc..e96be0985 100644 --- a/testsuite/tests/input/tex/Verb.test.ts +++ b/testsuite/tests/input/tex/Verb.test.ts @@ -7,7 +7,7 @@ beforeEach(async () => setupTex(['base', 'verb'])); /**********************************************************************************/ describe('Verb', () => { - it('Verb Plus ', () => { + it('Verb Plus', () => { expect(tex2mml('\\verb+{a}+')).toMatchSnapshot(); }); diff --git a/testsuite/tests/input/tex/__snapshots__/ConfigMacros.test.ts.snap b/testsuite/tests/input/tex/__snapshots__/ConfigMacros.test.ts.snap index 2520af291..aa49de38e 100644 --- a/testsuite/tests/input/tex/__snapshots__/ConfigMacros.test.ts.snap +++ b/testsuite/tests/input/tex/__snapshots__/ConfigMacros.test.ts.snap @@ -1,14 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Config Macros Active Macros Simple 1`] = ` -" - A -   - a -" -`; - -exports[`Config Macros Active Macros Simple 2`] = ` " A   @@ -17,17 +9,6 @@ exports[`Config Macros Active Macros Simple 2`] = ` `; exports[`Config Macros Commands Commands Argument 1`] = ` -" - - b - o - l - d - -" -`; - -exports[`Config Macros Commands Commands Argument 2`] = ` " b @@ -39,24 +20,6 @@ exports[`Config Macros Commands Commands Argument 2`] = ` `; exports[`Config Macros Commands Commands Aux Argument 1`] = ` -" - - first  - - h - i - -  second  - - t - h - e - r - e -" -`; - -exports[`Config Macros Commands Commands Aux Argument 2`] = ` " first  @@ -75,14 +38,6 @@ exports[`Config Macros Commands Commands Aux Argument 2`] = ` `; exports[`Config Macros Commands Commands Simple 1`] = ` -" - - R - -" -`; - -exports[`Config Macros Commands Commands Simple 2`] = ` " R @@ -90,15 +45,13 @@ exports[`Config Macros Commands Commands Simple 2`] = ` " `; -exports[`Config Macros Environment Environment Simple 1`] = ` -" - - a - +exports[`Config Macros Commands Commands Template 1`] = ` +" + [A ] " `; -exports[`Config Macros Environment Environment Simple 2`] = ` +exports[`Config Macros Environment Environment Simple 1`] = ` " a diff --git a/testsuite/tests/input/tex/__snapshots__/Newcommand.test.ts.snap b/testsuite/tests/input/tex/__snapshots__/Newcommand.test.ts.snap index a0b254542..5c82b8308 100644 --- a/testsuite/tests/input/tex/__snapshots__/Newcommand.test.ts.snap +++ b/testsuite/tests/input/tex/__snapshots__/Newcommand.test.ts.snap @@ -327,6 +327,69 @@ exports[`Newcommand Def Template Matching 1`] = ` " `; +exports[`Newcommand Def braces 1`] = ` +" + + x + 2 + + 3 +" +`; + +exports[`Newcommand Def braces 2`] = ` +" + + x + + 23 + + +" +`; + +exports[`Newcommand Def comments 1`] = ` +" + [ab] +" +`; + +exports[`Newcommand Def comments 2`] = ` +" + [abcd] +" +`; + +exports[`Newcommand Def comments 3`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def comments 4`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def comments 5`] = ` +" + [a] +" +`; + +exports[`Newcommand Def empty template arg 1`] = ` +" + [] +" +`; + exports[`Newcommand Def insignificant spaces 1`] = ` " [ @@ -335,6 +398,122 @@ exports[`Newcommand Def insignificant spaces 1`] = ` " `; +exports[`Newcommand Def multiple arg template 1`] = ` +" + [a][bc] +" +`; + +exports[`Newcommand Def multiple arg template 2`] = ` +" + [abc][] +" +`; + +exports[`Newcommand Def multiple spaces 1`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def multiple spaces 2`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def multiple spaces 3`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def multiple spaces 4`] = ` +" + [a][b ] +" +`; + +exports[`Newcommand Def multiple spaces 5`] = ` +" + [a][b ] +" +`; + +exports[`Newcommand Def multiple spaces 6`] = ` +" + [a] +" +`; + +exports[`Newcommand Def multiple spaces 7`] = ` +" + [a] +" +`; + +exports[`Newcommand Def newlines, spaces, tabs 1`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def newlines, spaces, tabs 2`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def newlines, spaces, tabs 3`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def newlines, spaces, tabs 4`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def newlines, spaces, tabs 5`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def newlines, spaces, tabs 6`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def newlines, spaces, tabs 7`] = ` +" + [a][b] +" +`; + +exports[`Newcommand Def partial template match 1`] = ` +" + [ax] +" +`; + +exports[`Newcommand Def partial template match 2`] = ` +" + [ax a b c] +" +`; + exports[`Newcommand Def significant spaces 1 1`] = ` " [ @@ -346,7 +525,17 @@ exports[`Newcommand Def significant spaces 1 1`] = ` `; exports[`Newcommand Def significant spaces 2 1`] = ` -" +" + [ + a + , + b + ] +" +`; + +exports[`Newcommand Def significant spaces 2 2`] = ` +" [ a , @@ -717,3 +906,29 @@ exports[`Newcommand Overrides Let overrides existing macro as delimiter 1`] = ` " `; + +exports[`Newcommand legacy Def legacy comments 1`] = ` +" + [a] +" +`; + +exports[`Newcommand legacy Def legacy comments 2`] = ` +" + % +" +`; + +exports[`Newcommand legacy Def legacy macro templates 1`] = ` +" + [a] +" +`; + +exports[`Newcommand legacy Def legacy macro templates 2`] = ` +" + [a] +" +`; diff --git a/testsuite/tests/input/tex/__snapshots__/Textmacros.test.ts.snap b/testsuite/tests/input/tex/__snapshots__/Textmacros.test.ts.snap index c876abb13..95ea8a011 100644 --- a/testsuite/tests/input/tex/__snapshots__/Textmacros.test.ts.snap +++ b/testsuite/tests/input/tex/__snapshots__/Textmacros.test.ts.snap @@ -479,8 +479,7 @@ exports[`Textmacros Specials Dollar 1`] = ` exports[`Textmacros Specials Percent 1`] = ` " - ab + ab " `; diff --git a/testsuite/tests/input/tex/__snapshots__/Verb.test.ts.snap b/testsuite/tests/input/tex/__snapshots__/Verb.test.ts.snap index 04ee38d2e..4c3b52c21 100644 --- a/testsuite/tests/input/tex/__snapshots__/Verb.test.ts.snap +++ b/testsuite/tests/input/tex/__snapshots__/Verb.test.ts.snap @@ -13,7 +13,7 @@ exports[`Verb Verb Minus Double 1`] = ` " `; -exports[`Verb Verb Plus 1`] = ` +exports[`Verb Verb Plus 1`] = ` " {a} " diff --git a/ts/input/tex.ts b/ts/input/tex.ts index 205ecd4ff..2749de126 100644 --- a/ts/input/tex.ts +++ b/ts/input/tex.ts @@ -74,6 +74,8 @@ export class TeX extends AbstractInputJax { maxTemplateSubtitutions: 10000, // math-style to use for Latin and Greek letters mathStyle: 'TeX', // one of TeX, ISO, French, or upright + legacyComments: false, // true to handle comments as ealier v4 versions (not properly processed in arguments) + legacyMacroTemplates: false, // true to handle spaces in macro as ealier v4 versions (not collapsed) formatError: (jax: TeX, err: TexError) => jax.formatError(err), }; diff --git a/ts/input/tex/NodeUtil.ts b/ts/input/tex/NodeUtil.ts index d04028da2..96f793f5c 100644 --- a/ts/input/tex/NodeUtil.ts +++ b/ts/input/tex/NodeUtil.ts @@ -30,7 +30,6 @@ import { } from '../../core/MmlTree/MmlNode.js'; import { MmlMo } from '../../core/MmlTree/MmlNodes/mo.js'; import { Property, PropertyList } from '../../core/Tree/Node.js'; -import { Args } from './Types.js'; import { OperatorDef } from '../../core/MmlTree/OperatorDictionary.js'; const NodeUtil = { @@ -95,9 +94,9 @@ const NodeUtil = { * * @param {MmlNode} node The node. * @param {string} attribute An attribute. - * @param {Args} value The attribute value. + * @param {Property} value The attribute value. */ - setAttribute(node: MmlNode, attribute: string, value: Args) { + setAttribute(node: MmlNode, attribute: string, value: Property) { node.attributes.set(attribute, value); }, @@ -106,9 +105,9 @@ const NodeUtil = { * * @param {MmlNode} node The node. * @param {string} property The property. - * @param {Args} value The property value. + * @param {Property} value The property value. */ - setProperty(node: MmlNode, property: string, value: Args) { + setProperty(node: MmlNode, property: string, value: Property) { node.setProperty(property, value); }, diff --git a/ts/input/tex/TexParser.ts b/ts/input/tex/TexParser.ts index 7e6117732..6d429447c 100644 --- a/ts/input/tex/TexParser.ts +++ b/ts/input/tex/TexParser.ts @@ -294,13 +294,48 @@ export default class TexParser { } /** + * @param {boolean} noComments True if comments are to be skipped * @returns {string} Get the next non-space character. */ - public GetNext(): string { - while (this.nextIsSpace()) { - this.i++; + public GetNext(noComments: boolean = true): string { + noComments &&= !this.configuration.options.legacyComments; + while (this.i < this.string.length) { + const c = this.getCodePoint(); + if (c === '%' && noComments) { + this.clipComment(); + continue; + } + if (c.match(/\s/)) { + this.i++; + continue; + } + return c; } - return this.getCodePoint(); + return ''; + } + + /** + * Remove a comment from the current string, if requested + * + * @param {boolean} noComments True if comments are to be skipped + * @param {number} di Offset from this.i for where the comment starts + */ + public clipComment(noComments: boolean = true, di: number = 0) { + if (!noComments || this.configuration.options.legacyComments) return; + this.i += di; + this.string = + this.string.slice(0, this.i) + + this.string.slice(this.i).replace(/^%.*?(?:\n\s*|$)/, ''); + } + + /** + * @param {string} text The string to unescape + * @returns {string} The string with \\ => \ and \% => % + */ + public unescapePercents(text: string): string { + return this.configuration.options.legacyComments + ? text + : text.replace(/\\([%\\])/g, '$1'); } /** @@ -325,10 +360,15 @@ export default class TexParser { * * @param {string} _name Name of the current control sequence. * @param {boolean} noneOK True if no argument is OK. + * @param {boolean} noComments True if comments are to be removed. * @returns {string} The next argument. */ - public GetArgument(_name: string, noneOK: boolean = false): string { - switch (this.GetNext()) { + public GetArgument( + _name: string, + noneOK: boolean = false, + noComments: boolean = true + ): string { + switch (this.GetNext(noComments)) { case '': if (!noneOK) { // @test MissingArgFor @@ -360,6 +400,9 @@ export default class TexParser { return this.string.slice(j, this.i - 1); } break; + case '%': + this.clipComment(noComments, -1); + break; } } // @test MissingCloseBrace @@ -376,15 +419,17 @@ export default class TexParser { * * @param {string} _name Name of the current control sequence. * @param {string?} def The default value for the optional argument. - * @param {boolean=} matchBrackets True if indernal brackets must match. + * @param {boolean=} matchBrackets True if internal brackets must match. + * @param {boolean} noComments True if comments are to be removed. * @returns {string} The optional argument. */ public GetBrackets( _name: string, def?: string, - matchBrackets: boolean = false + matchBrackets: boolean = false, + noComments: boolean = true ): string { - if (this.GetNext() !== '[') { + if (this.GetNext(noComments) !== '[') { return def; } const j = ++this.i; @@ -415,10 +460,13 @@ export default class TexParser { brackets--; } break; + case '%': + this.clipComment(noComments, -1); + break; } } // @test MissingCloseBracket - texError(COMPONENT, 'MissingCloseBracket', this.currentCS); + return texError(COMPONENT, 'MissingCloseBracket', this.currentCS); } /** @@ -443,7 +491,7 @@ export default class TexParser { } } // @test MissingOrUnrecognizedDelim1, MissingOrUnrecognizedDelim2 - texError(COMPONENT, 'MissingOrUnrecognizedDelim', this.currentCS); + return texError(COMPONENT, 'MissingOrUnrecognizedDelim', this.currentCS); } /** @@ -470,7 +518,7 @@ export default class TexParser { } } // @test MissingDimOrUnits - texError(COMPONENT, 'MissingDimOrUnits', this.currentCS); + return texError(COMPONENT, 'MissingDimOrUnits', this.currentCS); } /** @@ -504,13 +552,21 @@ export default class TexParser { } braces--; break; + case '%': + this.clipComment(true, -1); + break; } if (braces === 0 && c === token) { return this.string.slice(j, k); } } // @test TokenNotFoundForCommand - texError(COMPONENT, 'TokenNotFoundForCommand', token, this.currentCS); + return texError( + COMPONENT, + 'TokenNotFoundForCommand', + token, + this.currentCS + ); } /** @@ -557,7 +613,7 @@ export default class TexParser { return c; } // @test MissingOrUnrecognizedDelim - texError(COMPONENT, 'MissingOrUnrecognizedDelim', this.currentCS); + return texError(COMPONENT, 'MissingOrUnrecognizedDelim', this.currentCS); } /** diff --git a/ts/input/tex/Types.ts b/ts/input/tex/Types.ts index de98d39ca..1ff6f040f 100644 --- a/ts/input/tex/Types.ts +++ b/ts/input/tex/Types.ts @@ -25,7 +25,7 @@ import { StackItem } from './StackItem.js'; import { Token } from './Token.js'; import TexParser from './TexParser.js'; -export type Args = boolean | number | string | null; +export type Args = boolean | number | string | string[] | null; export type Attributes = Record; diff --git a/ts/input/tex/base/BaseItems.ts b/ts/input/tex/base/BaseItems.ts index 46c27a908..1814e2f15 100644 --- a/ts/input/tex/base/BaseItems.ts +++ b/ts/input/tex/base/BaseItems.ts @@ -1246,7 +1246,8 @@ export class ArrayItem extends BaseItem { */ protected getEntry(): [string, string, string, boolean] { const parser = this.parser; - const pattern = /^([^]*?)([&{}]|\\\\|\\(?:begin|end)\s*\{array\}|\\cr|\\)/; + const pattern = + /^([^]*?)([&{}]|\\\\|\\(?:begin|end)\s*\{array\}|\\cr|\\|%.*?(?:\n\s*|$))/; let braces = 0; let envs = 0; let i = parser.i; @@ -1277,9 +1278,12 @@ export class ArrayItem extends BaseItem { } // fall through if not closing a nested array environment default: { - if (braces || envs) continue; + if (braces || envs || match[2].charAt(0) === '%') continue; i -= match[2].length; - let entry = parser.string.slice(parser.i, i).trim(); + let entry = parser.string + .slice(parser.i, i) + .replace(/\\.|%.*?(?:\n|$)/g, (c) => (c.charAt(0) === '%' ? '' : c)) + .trim(); const prefix = entry.match( /^(?:\s*\\(?:h(?:dash)?line|hfil{1,3}|rowcolor\s*\{.*?\}))+/ ); diff --git a/ts/input/tex/base/BaseMethods.ts b/ts/input/tex/base/BaseMethods.ts index 675da5c92..ed78aae61 100644 --- a/ts/input/tex/base/BaseMethods.ts +++ b/ts/input/tex/base/BaseMethods.ts @@ -1020,7 +1020,9 @@ const BaseMethods: { [key: string]: ParseMethod } = { MmlToken(parser: TexParser, name: string) { // @test Modulo const kind = parser.GetArgument(name); - let attr = parser.GetBrackets(name, '').replace(/^\s+/, ''); + let attr = parser.unescapePercents( + parser.GetBrackets(name, '', false, false).replace(/^\s+/, '') + ); const text = parser.GetArgument(name); const def: EnvList = {}; const keep: string[] = []; @@ -1564,25 +1566,23 @@ const BaseMethods: { [key: string]: ParseMethod } = { // Make second column be in \text{...} (unless it is already // in a \text{...}, for backward compatibility). // - const str = parser.string; let braces = 0; let close = -1; - let i = parser.i; - let m = str.length; const end = env ? new RegExp(`^\\\\end\\s*\\{${env.replace(/\*/, '\\*')}\\}`) : null; // // Look through the string character by character... // - while (i < m) { - const c = str.charAt(i); + const i = parser.i; + let c; + while ((c = parser.GetNext())) { + parser.i += c.length; if (c === '{') { // // Increase the nested brace count and go on // braces++; - i++; } else if (c === '}') { // // If there are too many close braces, just end (we will get an @@ -1594,13 +1594,12 @@ const BaseMethods: { [key: string]: ParseMethod } = { // go on to the next character. // if (braces === 0) { - m = 0; - } else { - braces--; - if (braces === 0 && close < 0) { - close = i - parser.i; - } - i++; + parser.i--; + break; + } + braces--; + if (braces === 0 && close < 0) { + close = parser.i - i; } } else if (c === '&' && braces === 0) { // @@ -1614,17 +1613,11 @@ const BaseMethods: { [key: string]: ParseMethod } = { // (multi-letter names don't matter, as we will skip the rest of the // characters in the main loop) // - const rest = str.substring(i); + const rest = parser.string.substring(parser.i - 1); if (rest.match(/^((\\cr)[^a-zA-Z]|\\\\)/) || (end && rest.match(end))) { - m = 0; - } else { - i += 2; + parser.i--; + break; } - } else { - // - // Go on to the next character - // - i++; } } // @@ -1632,12 +1625,12 @@ const BaseMethods: { [key: string]: ParseMethod } = { // If not, process the second column as text and continue parsing from there, // (otherwise process the second column as normal, since it is in \text{} // - // i >= parser.i + // parser.i >= i // - const text = str.substring(parser.i, i); + const text = parser.string.substring(i, parser.i); if ( !text.match(/^\s*\\text[^a-zA-Z]/) || - close !== text.replace(/\s+$/, '').length - 1 + close !== text.replace(/\s+$/, '').length ) { const internal = ParseUtil.internalMath( parser, @@ -1645,6 +1638,7 @@ const BaseMethods: { [key: string]: ParseMethod } = { 0 ); parser.PushAll(internal); + } else { parser.i = i; } }, diff --git a/ts/input/tex/bbox/BboxConfiguration.ts b/ts/input/tex/bbox/BboxConfiguration.ts index f796732d5..44f89749b 100644 --- a/ts/input/tex/bbox/BboxConfiguration.ts +++ b/ts/input/tex/bbox/BboxConfiguration.ts @@ -39,7 +39,9 @@ const BboxMethods: { [key: string]: ParseMethod } = { * @param {string} name The name of the calling macro. */ BBox(parser: TexParser, name: string) { - const bbox = parser.GetBrackets(name, ''); + const bbox = parser.unescapePercents( + parser.GetBrackets(name, '', false, false) + ); let math = parser.ParseArg(name); const parts = bbox.split(/,/); let def, background, style; diff --git a/ts/input/tex/cases/CasesConfiguration.ts b/ts/input/tex/cases/CasesConfiguration.ts index 802e636d8..dd43ca60c 100644 --- a/ts/input/tex/cases/CasesConfiguration.ts +++ b/ts/input/tex/cases/CasesConfiguration.ts @@ -150,21 +150,19 @@ export const CasesMethods = { // // Make second column be in \text{...} // - const tex = parser.string; let braces = 0; - let i = parser.i; - const m = tex.length; // // Look through the string character by character... // - while (i < m) { - const c = tex.charAt(i); + const i = parser.i; + let c; + while ((c = parser.GetNext())) { + parser.i += c.length; if (c === '{') { // // Increase the nested brace count and go on // braces++; - i++; } else if (c === '}') { // // If there are too many close braces, just end (we will get an @@ -174,11 +172,10 @@ export const CasesMethods = { // go on to the next character. // if (braces === 0) { + parser.i--; break; - } else { - braces--; - i++; } + braces--; } else if (c === '&' && braces === 0) { // // Extra alignment tabs are not allowed in cases @@ -190,7 +187,7 @@ export const CasesMethods = { // (multi-letter names don't matter, as we will skip the rest of the // characters in the main loop) // - const cs = (tex.slice(i + 1).match(/^[a-z]+|./i) || [])[0]; + const cs = (parser.string.slice(parser.i).match(/^[a-z]+|./i) || [])[0]; if ( cs === '\\' || cs === 'cr' || @@ -198,24 +195,19 @@ export const CasesMethods = { cs === 'label' || cs === undefined ) { + parser.i--; break; } else { - i += cs.length; + parser.i += cs.length; } - } else { - // - // Go on to the next character - // - i++; } } // // Process the second column as text and continue parsing from there, // - // i >= parser.i - const text = tex.substring(parser.i, i).replace(/^\s*/, ''); + // parser.i >= i + const text = parser.string.substring(i, parser.i).replace(/^\s*/, ''); parser.PushAll(ParseUtil.internalMath(parser, text, 0)); - parser.i = i; return null; }, diff --git a/ts/input/tex/configmacros/ConfigMacrosConfiguration.ts b/ts/input/tex/configmacros/ConfigMacrosConfiguration.ts index c8aad8ae0..481dfbaf0 100644 --- a/ts/input/tex/configmacros/ConfigMacrosConfiguration.ts +++ b/ts/input/tex/configmacros/ConfigMacrosConfiguration.ts @@ -28,6 +28,7 @@ import { CommandMap, EnvironmentMap, MacroMap } from '../TokenMap.js'; import ParseMethods from '../ParseMethods.js'; import { Macro } from '../Token.js'; import NewcommandMethods from '../newcommand/NewcommandMethods.js'; +import { NewcommandUtil } from '../newcommand/NewcommandUtil.js'; import { BeginEnvItem } from '../newcommand/NewcommandItems.js'; import { TeX } from '../../tex.js'; @@ -91,13 +92,18 @@ function configmacrosConfig(_config: ParserConfiguration, jax: TEX) { function setMacros(name: string, map: string, jax: TEX) { const handler = jax.parseOptions.handlers.retrieve(map) as CommandMap; const macros = jax.parseOptions.options[name]; + const legacy = jax.parseOptions.options.legacyMacroTemplates; for (const cs of Object.keys(macros)) { const def = typeof macros[cs] === 'string' ? [macros[cs]] : macros[cs]; const macro = Array.isArray(def[2]) ? new Macro( cs, NewcommandMethods.MacroWithTemplate, - def.slice(0, 2).concat(def[2]) + def + .slice(0, 2) + .concat( + def[2].map((param) => NewcommandUtil.tokenize(param, legacy)) + ) ) : new Macro(cs, NewcommandMethods.Macro, def); handler.add(cs, macro); diff --git a/ts/input/tex/html/HtmlMethods.ts b/ts/input/tex/html/HtmlMethods.ts index 10c9b38e6..7221e2c2e 100644 --- a/ts/input/tex/html/HtmlMethods.ts +++ b/ts/input/tex/html/HtmlMethods.ts @@ -79,7 +79,7 @@ const HtmlMethods: { [key: string]: ParseMethod } = { * @param {string} name The macro name. */ Href(parser: TexParser, name: string) { - const url = parser.GetArgument(name); + const url = GetArgumentWithPercent(parser, name); const arg = GetArgumentMML(parser, name); NodeUtil.setAttribute(arg, 'href', url); parser.Push(arg); @@ -92,7 +92,7 @@ const HtmlMethods: { [key: string]: ParseMethod } = { * @param {string} name The macro name. */ Class(parser: TexParser, name: string) { - let CLASS = parser.GetArgument(name); + let CLASS = GetArgumentWithPercent(parser, name); const arg = GetArgumentMML(parser, name); const oldClass = NodeUtil.getAttribute(arg, 'class'); if (oldClass) { @@ -109,7 +109,7 @@ const HtmlMethods: { [key: string]: ParseMethod } = { * @param {string} name The macro name. */ Style(parser: TexParser, name: string) { - let style = parser.GetArgument(name); + let style = GetArgumentWithPercent(parser, name); const arg = GetArgumentMML(parser, name); // check that it looks like a style string let oldStyle = NodeUtil.getAttribute(arg, 'style') as string; @@ -130,7 +130,7 @@ const HtmlMethods: { [key: string]: ParseMethod } = { * @param {string} name The macro name. */ Id(parser: TexParser, name: string) { - const ID = parser.GetArgument(name); + const ID = parser.GetArgument(name, false, false); const arg = GetArgumentMML(parser, name); NodeUtil.setAttribute(arg, 'id', ID); parser.Push(arg); @@ -157,4 +157,23 @@ const GetArgumentMML = function (parser: TexParser, name: string): MmlNode { return mrow; }; +/** + * Gets a literal argument allowing percent signs (i.e., they are not + * treated as comments), and allows quoted percents to become + * percents, so if these commands are used inside the argument to + * another macro, or in a macro definition, you ca still use percent + * sign in them by quoting the percent with a backslash. Backslashes + * should also be doubled. + * + * @param {TexParser} parser The calling parser. + * @param {string} name The calling macro name. + * @returns {string} The argument + */ +const GetArgumentWithPercent = function ( + parser: TexParser, + name: string +): string { + return parser.unescapePercents(parser.GetArgument(name, false, false)); +}; + export default HtmlMethods; diff --git a/ts/input/tex/newcommand/NewcommandMethods.ts b/ts/input/tex/newcommand/NewcommandMethods.ts index d43ce9c8f..140b7dc93 100644 --- a/ts/input/tex/newcommand/NewcommandMethods.ts +++ b/ts/input/tex/newcommand/NewcommandMethods.ts @@ -87,19 +87,19 @@ const NewcommandMethods: { [key: string]: ParseMethod } = { const cs = NewcommandUtil.GetCSname(parser, name); const params = NewcommandUtil.GetTemplate(parser, name, '\\' + cs); const def = parser.GetArgument(name); - !(params instanceof Array) - ? // @test Def DoubleLet, DefReDef - NewcommandUtil.addMacro(parser, cs, NewcommandMethods.Macro, [ - def, - params, - ]) - : // @test Def Let - NewcommandUtil.addMacro( - parser, - cs, - NewcommandMethods.MacroWithTemplate, - [def].concat(params) - ); + if (params instanceof Array) { + // @test Def Let + NewcommandUtil.addMacro(parser, cs, NewcommandMethods.MacroWithTemplate, [ + def, + ...params, + ]); + } else { + // @test Def DoubleLet, DefReDef + NewcommandUtil.addMacro(parser, cs, NewcommandMethods.Macro, [ + def, + params, + ]); + } parser.Push(parser.itemFactory.create('null')); }, @@ -200,14 +200,14 @@ const NewcommandMethods: { [key: string]: ParseMethod } = { * @param {string} name The name of the calling command. * @param {string} text The text template of the macro. * @param {string} n The number of parameters. - * @param {string[]} params The parameter values. + * @param {string[][]} params The parameter token arrays. */ MacroWithTemplate( parser: TexParser, name: string, text: string, n: string, - ...params: string[] + ...params: string[][] ) { const argCount = parseInt(n, 10); // @test Def Let @@ -215,7 +215,10 @@ const NewcommandMethods: { [key: string]: ParseMethod } = { // @test Def Let const args = []; parser.GetNext(); - if (params[0] && !NewcommandUtil.MatchParam(parser, params[0])) { + if ( + params[0] && + NewcommandUtil.GetParameter(parser, name, params[0], true) !== '' + ) { // @test Missing Arguments texError(COMPONENT, 'MismatchUseDef', name); } diff --git a/ts/input/tex/newcommand/NewcommandUtil.ts b/ts/input/tex/newcommand/NewcommandUtil.ts index bb7ff956d..4a919a0f9 100644 --- a/ts/input/tex/newcommand/NewcommandUtil.ts +++ b/ts/input/tex/newcommand/NewcommandUtil.ts @@ -46,7 +46,48 @@ export enum NewcommandTables { */ export const NewcommandPriority = -100; +const tokenPattern = String.raw`\\[a-zA-Z]+\s*|\\.|[\n\s]+|%.*?(?:\n\s*|$)|.`; + export const NewcommandUtil = { + tokenPattern, + tokenMatch: new RegExp(`^(?:${tokenPattern})`, 'u'), + tokenTemplate: new RegExp(`^(?:#[1-9]?|${tokenPattern})`, 'u'), + + /** + * Convert a latex string into template tokens. + * + * @param {string} text The string to be tokenized + * @param {boolean} legacy True if legacy spacing is to be used. + * @returns {string[]|void} The array of tokens from the string + */ + tokenize(text: string, legacy: boolean): string[] | void { + if (text === undefined) return; + const tokens: string[] = []; + text = text.replace(/^\s+/, ''); + while (text) { + const token = text.match(NewcommandUtil.tokenMatch)[0]; + text = text.slice(token.length); + tokens.push(NewcommandUtil.trimToken(token, legacy)); + } + return tokens; + }, + + /** + * Collapse spaces and newlines into a single space. + * + * @param {string} token The token whose spaces are to be collapsed. + * @param {boolean} legacy True if legacy spacing is to be used. + * @returns {string} The trimmed token. + */ + trimToken(token: string, legacy: boolean): string { + return legacy + ? token + : token + .replace(/\n+/g, ' ') + .replace(/(.)\s+$/, '$1') + .replace(/\s$/, ' '); + }, + /** * Get the next CS name or give an error. * @@ -62,7 +103,7 @@ export const NewcommandUtil = { texError(COMPONENT, 'MissingCS', cmd); } const cs = UnitUtil.trimSpaces(parser.GetArgument(cmd)).substring(1); - this.checkProtectedMacros(parser, cs); + NewcommandUtil.checkProtectedMacros(parser, cs); return cs; }, @@ -83,7 +124,7 @@ export const NewcommandUtil = { // @test Illegal CS texError(COMPONENT, 'IllegalControlSequenceName', name); } - this.checkProtectedMacros(parser, cs); + NewcommandUtil.checkProtectedMacros(parser, cs); return cs; }, @@ -114,59 +155,54 @@ export const NewcommandUtil = { * @param {TexParser} parser The calling parser. * @param {string} cmd The string starting with the template. * @param {string} cs The control sequence of the \def. - * @returns {number | string[]} The number of parameters or a string array if - * there is an optional argument. + * @returns {number|(string|string[])[]} The number of parameters or a string array + * of the tokens that delimit the arguments. */ - GetTemplate(parser: TexParser, cmd: string, cs: string): number | string[] { - // @test Def Double Let, Def ReDef, Def Let - let c = parser.GetNext(); - const params: string[] = []; + GetTemplate( + parser: TexParser, + cmd: string, + cs: string + ): number | (string | string[])[] { + const { legacyComments, legacyMacroTemplates } = + parser.configuration.options; + parser.GetNext(); + const params: string[][] = []; + let arg: string[] = []; let n = 0; - let i = parser.i; while (parser.i < parser.string.length) { - c = parser.GetNext(); - if (c === '#') { - // @test Def ReDef, Def Let, Def Optional Brace - if (i !== parser.i) { - // @test Def Let, Def Optional Brace - // parser.i >= i! - params[n] = parser.string.substring(i, parser.i); + let token = parser.string + .slice(parser.i) + .match(NewcommandUtil.tokenTemplate)[0]; + parser.i += token.length; + if (token.charAt(0) === '%') { + if (!legacyComments) continue; + parser.i -= token.length - 1; + token = '%'; + } + if (token === '{') { + parser.i--; + if (arg.length && arg.join('') !== ' ') { + params.push(arg); } - c = parser.string.charAt(++parser.i); - if (!c.match(/^[1-9]$/)) { + return params.length ? [String(n), ...params] : n; + } + if (token.charAt(0) === '#') { + if (token === '#') { // @test Illegal Hash texError(COMPONENT, 'CantUseHash2', cs); } - if (parseInt(c) !== ++n) { + if (parseInt(token.charAt(1)) !== ++n) { // @test No Sequence texError(COMPONENT, 'SequentialParam', cs); } - i = parser.i + 1; - } else if (c === '{') { - // @test Def Double Let, Def ReDef, Def Let - if (i !== parser.i) { - // @test Optional Brace Error - // parser.i >= i! - params[n] = parser.string.substring(i, parser.i); - if ( - params[n].replace(/^ +/, '') === '' && - params.slice(0, n).join('') === '' - ) { - return n; - } - } - if (params.length > 0) { - // @test Def Let, Def Optional Brace - return [n.toString()].concat(params); - } else { - // @test Def Double Let, Def ReDef - return n; - } + params.push(arg.length ? arg : undefined); + arg = []; + continue; } - parser.i++; + arg.push(NewcommandUtil.trimToken(token, legacyMacroTemplates)); } // @test No Replacement - texError(COMPONENT, 'MissingReplacementString', cmd); + return texError(COMPONENT, 'MissingReplacementString', cmd); }, /** @@ -174,80 +210,56 @@ export const NewcommandUtil = { * * @param {TexParser} parser The calling parser. * @param {string} name The name of the calling command. - * @param {string} param The parameter for the macro. + * @param {string[]} tokens The tokens that end the argument. + * @param {boolean} initial True when this is the initial token list with no parameter * @returns {string} The parameter. */ - GetParameter(parser: TexParser, name: string, param: string): string { - if (param == null) { + GetParameter( + parser: TexParser, + name: string, + tokens: string[], + initial: boolean = false + ): string { + if (!tokens) { // @test Def Let, Def Optional Brace, Def Options CS return parser.GetArgument(name); } - let i = parser.i; - let j = 0; + const { legacyComments, legacyMacroTemplates } = + parser.configuration.options; + let t = 0; + const i = parser.i; let hasBraces = false; + const arg = []; while (parser.i < parser.string.length) { - const c = parser.string.charAt(parser.i); - // @test Def Let, Def Optional Brace, Def Options CS - if (c === '{') { - // @test Def Optional Brace, Def Options CS + let token = parser.string + .slice(parser.i) + .match(NewcommandUtil.tokenMatch)[0]; + parser.i += token.length; + if (token.charAt(0) === '%') { + if (!legacyComments) continue; + parser.i -= token.length - 1; + token = '%'; + } + if (NewcommandUtil.trimToken(token, legacyMacroTemplates) === tokens[t]) { + if (++t < tokens.length) continue; + return (hasBraces ? arg.slice(1, -1) : arg).join(''); + } + if (t) { + arg.push(...tokens.slice(0, t)); + t = 0; + } + if (token === '{') { + parser.i--; hasBraces = parser.i === i; - parser.GetArgument(name); - j = parser.i - i; - } else if (this.MatchParam(parser, param)) { - // @test Def Let, Def Optional Brace, Def Options CS - if (hasBraces) { - // @test Def Optional Brace - i++; - j -= 2; - } - return parser.string.substring(i, i + j); - } else if (c === '\\') { - // @test Def Options CS - parser.i++; - j++; - hasBraces = false; - const match = parser.string.substring(parser.i).match(/[a-z]+|./i); - if (match) { - // @test Def Options CS - parser.i += match[0].length; - j = parser.i - i; - } + arg.push('{', parser.GetArgument(name), '}'); } else { - // @test Def Let - parser.i++; - j++; hasBraces = false; + arg.push(token); } } + if (initial) return null; // @test Runaway Argument - texError(COMPONENT, 'RunawayArgument', name); - }, - - /** - * Check if a template is at the current location. - * (The match must be exact, with no spacing differences. TeX is - * a little more forgiving than this about spaces after macro names) - * - * @param {TexParser} parser The calling parser. - * @param {string} param Tries to match an optional parameter. - * @returns {number} The number of optional parameters, either 0 or 1. - */ - MatchParam(parser: TexParser, param: string): number { - // @test Def Let, Def Optional Brace, Def Options CS - if (parser.string.substring(parser.i, parser.i + param.length) !== param) { - // @test Def Let, Def Options CS - return 0; - } - if ( - param.match(/\\[a-z]+$/i) && - parser.string.charAt(parser.i + param.length).match(/[a-z]/i) - ) { - // @test (missing) - return 0; - } - // @test Def Let, Def Optional Brace, Def Options CS - parser.i += param.length; - return 1; + return texError(COMPONENT, 'RunawayArgument', name); }, /** @@ -298,7 +310,7 @@ export const NewcommandUtil = { */ addDelimiter(parser: TexParser, cs: string, char: string, attr: Attributes) { const name = cs.substring(1); - this.checkProtectedMacros(parser, name); + NewcommandUtil.checkProtectedMacros(parser, name); const [macros, delims] = NewcommandUtil.checkGlobal( parser, [name, cs], @@ -328,13 +340,13 @@ export const NewcommandUtil = { attr: Args[], token: string = '' ) { - this.checkProtectedMacros(parser, cs); + NewcommandUtil.checkProtectedMacros(parser, cs); const macros = NewcommandUtil.checkGlobal( parser, [cs], [NewcommandTables.NEW_COMMAND] )[0]; - this.undefineDelimiter(parser, '\\' + cs); + NewcommandUtil.undefineDelimiter(parser, '\\' + cs); macros.add(cs, new Macro(token ? token : cs, func, attr)); delete parser.stack.env.isGlobal; }, @@ -384,7 +396,7 @@ export const NewcommandUtil = { // handler. // macros.add(cs, new Macro(cs, () => SubHandler.FALLBACK, [])); - this.undefineDelimiter(parser, '\\' + cs); + NewcommandUtil.undefineDelimiter(parser, '\\' + cs); } delete parser.stack.env.isGlobal; }, diff --git a/ts/input/tex/setoptions/SetOptionsConfiguration.ts b/ts/input/tex/setoptions/SetOptionsConfiguration.ts index b33595ed1..1b6408a0f 100644 --- a/ts/input/tex/setoptions/SetOptionsConfiguration.ts +++ b/ts/input/tex/setoptions/SetOptionsConfiguration.ts @@ -127,7 +127,9 @@ export const SetOptionsUtil = { */ function SetOptions(parser: TexParser, name: string) { const extension = parser.GetBrackets(name) || 'tex'; - const options = ParseUtil.keyvalOptions(parser.GetArgument(name)); + const options = ParseUtil.keyvalOptions( + parser.GetArgument(name, false, false) + ); const config = parser.options.setoptions; if (!config.filterPackage(parser, extension)) return; for (const key of Object.keys(options)) { diff --git a/ts/input/tex/verb/VerbConfiguration.ts b/ts/input/tex/verb/VerbConfiguration.ts index 905193cb2..181e64620 100644 --- a/ts/input/tex/verb/VerbConfiguration.ts +++ b/ts/input/tex/verb/VerbConfiguration.ts @@ -41,22 +41,23 @@ const VerbMethods: { [key: string]: ParseMethod } = { * @param {string} name The name of the calling macro. */ Verb(parser: TexParser, name: string) { - const c = parser.GetNext(); - const start = ++parser.i; + const c = parser.GetNext(false); if (c === '') { texError(TEX_COMPONENT, 'MissingArgFor', name); } - while ( - parser.i < parser.string.length && - parser.string.charAt(parser.i) !== c - ) { - parser.i++; - } - if (parser.i === parser.string.length) { + parser.i += c.length; + const start = parser.i; + let C; + do { + C = parser.GetNext(false); + parser.i += C.length; + } while (C && C !== c); + if (!C) { texError(COMPONENT, 'NoClosingDelim', parser.currentCS); } - const text = parser.string.slice(start, parser.i).replace(/ /g, '\u00A0'); - parser.i++; + const text = parser.string + .slice(start, parser.i - c.length) + .replace(/ /g, '\u00A0'); parser.Push( parser.create( 'token',