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`] = `
-""
-`;
-
-exports[`Config Macros Active Macros Simple 2`] = `
""
`;
-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',