ParseUtil.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2009-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview A namespace for utility functions for the TeX Parser.
  19. *
  20. * @author v.sorge@mathjax.org (Volker Sorge)
  21. */
  22. import {TEXCLASS, MmlNode} from '../../core/MmlTree/MmlNode.js';
  23. import {EnvList} from './StackItem.js';
  24. import {ArrayItem} from './base/BaseItems.js';
  25. import ParseOptions from './ParseOptions.js';
  26. import NodeUtil from './NodeUtil.js';
  27. import TexParser from './TexParser.js';
  28. import TexError from './TexError.js';
  29. import {entities} from '../../util/Entities.js';
  30. import {MmlMunderover} from '../../core/MmlTree/MmlNodes/munderover.js';
  31. namespace ParseUtil {
  32. // TODO (VS): Combine some of this with lengths in util.
  33. const emPerInch = 7.2;
  34. const pxPerInch = 72;
  35. // Note, the following are TeX CM font values.
  36. const UNIT_CASES: {[key: string]: ((m: number) => number)} = {
  37. 'em': m => m,
  38. 'ex': m => m * .43,
  39. 'pt': m => m / 10, // 10 pt to an em
  40. 'pc': m => m * 1.2, // 12 pt to a pc
  41. 'px': m => m * emPerInch / pxPerInch,
  42. 'in': m => m * emPerInch,
  43. 'cm': m => m * emPerInch / 2.54, // 2.54 cm to an inch
  44. 'mm': m => m * emPerInch / 25.4, // 10 mm to a cm
  45. 'mu': m => m / 18,
  46. };
  47. const num = '([-+]?([.,]\\d+|\\d+([.,]\\d*)?))';
  48. const unit = '(pt|em|ex|mu|px|mm|cm|in|pc)';
  49. const dimenEnd = RegExp('^\\s*' + num + '\\s*' + unit + '\\s*$');
  50. const dimenRest = RegExp('^\\s*' + num + '\\s*' + unit + ' ?');
  51. /**
  52. * Matches for a dimension argument.
  53. * @param {string} dim The argument.
  54. * @param {boolean} rest Allow for trailing garbage in the dimension string.
  55. * @return {[string, string, number]} The match result as (Anglosaxon) value,
  56. * unit name, length of matched string. The latter is interesting in the
  57. * case of trailing garbage.
  58. */
  59. export function matchDimen(
  60. dim: string, rest: boolean = false): [string, string, number] {
  61. let match = dim.match(rest ? dimenRest : dimenEnd);
  62. return match ?
  63. muReplace([match[1].replace(/,/, '.'), match[4], match[0].length]) :
  64. [null, null, 0];
  65. }
  66. /**
  67. * Transforms mu dimension to em if necessary.
  68. * @param {[string, string, number]} [value, unit, length] The dimension triple.
  69. * @return {[string, string, number]} [value, unit, length] The transformed triple.
  70. */
  71. function muReplace([value, unit, length]: [string, string, number]): [string, string, number] {
  72. if (unit !== 'mu') {
  73. return [value, unit, length];
  74. }
  75. let em = Em(UNIT_CASES[unit](parseFloat(value || '1')));
  76. return [em.slice(0, -2), 'em', length];
  77. }
  78. /**
  79. * Convert a dimension string into standard em dimension.
  80. * @param {string} dim The attribute string.
  81. * @return {number} The numerical value.
  82. */
  83. export function dimen2em(dim: string): number {
  84. let [value, unit] = matchDimen(dim);
  85. let m = parseFloat(value || '1');
  86. let func = UNIT_CASES[unit];
  87. return func ? func(m) : 0;
  88. }
  89. /**
  90. * Turns a number into an em value.
  91. * @param {number} m The number.
  92. * @return {string} The em dimension string.
  93. */
  94. export function Em(m: number): string {
  95. if (Math.abs(m) < .0006) {
  96. return '0em';
  97. }
  98. return m.toFixed(3).replace(/\.?0+$/, '') + 'em';
  99. }
  100. /**
  101. * Takes an array of numbers and returns a space-separated string of em values.
  102. * @param {number[]} W The widths to be turned into em values
  103. * @return {string} The numbers with em units, separated by spaces.
  104. */
  105. export function cols(...W: number[]): string {
  106. return W.map(n => Em(n)).join(' ');
  107. }
  108. /**
  109. * Create an mrow that has stretchy delimiters at either end, as needed
  110. * @param {ParseOptions} configuration Current parse options.
  111. * @param {string} open The opening fence.
  112. * @param {MmlNode} mml The enclosed node.
  113. * @param {string} close The closing fence.
  114. * @param {string=} big Bigg command.
  115. */
  116. export function fenced(configuration: ParseOptions, open: string, mml: MmlNode,
  117. close: string, big: string = '', color: string = '') {
  118. // @test Fenced, Fenced3
  119. let nf = configuration.nodeFactory;
  120. let mrow = nf.create('node', 'mrow', [],
  121. {open: open, close: close, texClass: TEXCLASS.INNER});
  122. let mo;
  123. if (big) {
  124. mo = new TexParser('\\' + big + 'l' + open, configuration.parser.stack.env, configuration).mml();
  125. } else {
  126. let openNode = nf.create('text', open);
  127. mo = nf.create('node', 'mo', [],
  128. {fence: true, stretchy: true, symmetric: true, texClass: TEXCLASS.OPEN},
  129. openNode);
  130. }
  131. NodeUtil.appendChildren(mrow, [mo, mml]);
  132. if (big) {
  133. mo = new TexParser('\\' + big + 'r' + close, configuration.parser.stack.env, configuration).mml();
  134. } else {
  135. let closeNode = nf.create('text', close);
  136. mo = nf.create('node', 'mo', [],
  137. {fence: true, stretchy: true, symmetric: true, texClass: TEXCLASS.CLOSE},
  138. closeNode);
  139. }
  140. color && mo.attributes.set('mathcolor', color);
  141. NodeUtil.appendChildren(mrow, [mo]);
  142. return mrow;
  143. }
  144. /**
  145. * Create an mrow that has \\mathchoice using \\bigg and \\big for the delimiters.
  146. * @param {ParseOptions} configuration The current parse options.
  147. * @param {string} open The opening fence.
  148. * @param {MmlNode} mml The enclosed node.
  149. * @param {string} close The closing fence.
  150. * @return {MmlNode} The mrow node.
  151. */
  152. export function fixedFence(configuration: ParseOptions, open: string,
  153. mml: MmlNode, close: string): MmlNode {
  154. // @test Choose, Over With Delims, Above with Delims
  155. let mrow = configuration.nodeFactory.create('node',
  156. 'mrow', [], {open: open, close: close, texClass: TEXCLASS.ORD});
  157. if (open) {
  158. NodeUtil.appendChildren(mrow, [mathPalette(configuration, open, 'l')]);
  159. }
  160. if (NodeUtil.isType(mml, 'mrow')) {
  161. NodeUtil.appendChildren(mrow, NodeUtil.getChildren(mml));
  162. } else {
  163. NodeUtil.appendChildren(mrow, [mml]);
  164. }
  165. if (close) {
  166. NodeUtil.appendChildren(mrow, [mathPalette(configuration, close, 'r')]);
  167. }
  168. return mrow;
  169. }
  170. /**
  171. * Generates a mathchoice element for fences. These will be resolved later,
  172. * once the position, and therefore size, of the of the fenced expression is
  173. * known.
  174. * @param {ParseOptions} configuration The current parse otpions.
  175. * @param {string} fence The fence.
  176. * @param {string} side The side of the fence (l or r).
  177. * @return {MmlNode} The mathchoice node.
  178. */
  179. export function mathPalette(configuration: ParseOptions, fence: string,
  180. side: string): MmlNode {
  181. if (fence === '{' || fence === '}') {
  182. fence = '\\' + fence;
  183. }
  184. let D = '{\\bigg' + side + ' ' + fence + '}';
  185. let T = '{\\big' + side + ' ' + fence + '}';
  186. return new TexParser('\\mathchoice' + D + T + T + T, {}, configuration).mml();
  187. }
  188. /**
  189. * If the initial child, skipping any initial space or
  190. * empty braces (TeXAtom with child being an empty inferred row),
  191. * is an <mo>, precede it by an empty <mi> to force the <mo> to
  192. * be infix.
  193. * @param {ParseOptions} configuration The current parse options.
  194. * @param {MmlNode[]} nodes The row of nodes to scan for an initial <mo>
  195. */
  196. export function fixInitialMO(configuration: ParseOptions, nodes: MmlNode[]) {
  197. for (let i = 0, m = nodes.length; i < m; i++) {
  198. let child = nodes[i];
  199. if (child && (!NodeUtil.isType(child, 'mspace') &&
  200. (!NodeUtil.isType(child, 'TeXAtom') ||
  201. (NodeUtil.getChildren(child)[0] &&
  202. NodeUtil.getChildren(NodeUtil.getChildren(child)[0]).length)))) {
  203. if (NodeUtil.isEmbellished(child) ||
  204. (NodeUtil.isType(child, 'TeXAtom') && NodeUtil.getTexClass(child) === TEXCLASS.REL)) {
  205. let mi = configuration.nodeFactory.create('node', 'mi');
  206. nodes.unshift(mi);
  207. }
  208. break;
  209. }
  210. }
  211. }
  212. /**
  213. * Break up a string into text and math blocks.
  214. * @param {TexParser} parser The calling parser.
  215. * @param {string} text The text in the math expression to parse.
  216. * @param {number|string=} level The scriptlevel.
  217. * @param {string} font The mathvariant to use
  218. * @return {MmlNode[]} The nodes corresponding to the internal math expression.
  219. */
  220. export function internalMath(parser: TexParser, text: string,
  221. level?: number | string, font?: string): MmlNode[] {
  222. if (parser.configuration.options.internalMath) {
  223. return parser.configuration.options.internalMath(parser, text, level, font);
  224. }
  225. let mathvariant = font || parser.stack.env.font;
  226. let def = (mathvariant ? {mathvariant} : {});
  227. let mml: MmlNode[] = [], i = 0, k = 0, c, node, match = '', braces = 0;
  228. if (text.match(/\\?[${}\\]|\\\(|\\(eq)?ref\s*\{/)) {
  229. while (i < text.length) {
  230. c = text.charAt(i++);
  231. if (c === '$') {
  232. if (match === '$' && braces === 0) {
  233. // @test Interspersed Text
  234. node = parser.create(
  235. 'node', 'TeXAtom',
  236. [(new TexParser(text.slice(k, i - 1), {}, parser.configuration)).mml()]);
  237. mml.push(node);
  238. match = '';
  239. k = i;
  240. } else if (match === '') {
  241. // @test Interspersed Text
  242. if (k < i - 1) {
  243. // @test Interspersed Text
  244. mml.push(internalText(parser, text.slice(k, i - 1), def));
  245. }
  246. match = '$';
  247. k = i;
  248. }
  249. } else if (c === '{' && match !== '') {
  250. // @test Mbox Mbox, Mbox Math
  251. braces++;
  252. } else if (c === '}') {
  253. // @test Mbox Mbox, Mbox Math
  254. if (match === '}' && braces === 0) {
  255. // @test Mbox Eqref, Mbox Math
  256. let atom = (new TexParser(text.slice(k, i), {}, parser.configuration)).mml();
  257. node = parser.create('node', 'TeXAtom', [atom], def);
  258. mml.push(node);
  259. match = '';
  260. k = i;
  261. } else if (match !== '') {
  262. // @test Mbox Math, Mbox Mbox
  263. if (braces) {
  264. // @test Mbox Math, Mbox Mbox
  265. braces--;
  266. }
  267. }
  268. } else if (c === '\\') {
  269. // @test Mbox Eqref, Mbox CR
  270. if (match === '' && text.substr(i).match(/^(eq)?ref\s*\{/)) {
  271. // @test Mbox Eqref
  272. let len = ((RegExp as any)['$&'] as string).length;
  273. if (k < i - 1) {
  274. // @test Mbox Eqref
  275. mml.push(internalText(parser, text.slice(k, i - 1), def));
  276. }
  277. match = '}';
  278. k = i - 1;
  279. i += len;
  280. } else {
  281. // @test Mbox CR, Mbox Mbox
  282. c = text.charAt(i++);
  283. if (c === '(' && match === '') {
  284. // @test Mbox Internal Display
  285. if (k < i - 2) {
  286. // @test Mbox Internal Display
  287. mml.push(internalText(parser, text.slice(k, i - 2), def));
  288. }
  289. match = ')'; k = i;
  290. } else if (c === ')' && match === ')' && braces === 0) {
  291. // @test Mbox Internal Display
  292. node = parser.create(
  293. 'node', 'TeXAtom',
  294. [(new TexParser(text.slice(k, i - 2), {}, parser.configuration)).mml()]);
  295. mml.push(node);
  296. match = '';
  297. k = i;
  298. } else if (c.match(/[${}\\]/) && match === '') {
  299. // @test Mbox CR
  300. i--;
  301. text = text.substr(0, i - 1) + text.substr(i); // remove \ from \$, \{, \}, or \\
  302. }
  303. }
  304. }
  305. }
  306. if (match !== '') {
  307. // @test Internal Math Error
  308. throw new TexError('MathNotTerminated', 'Math not terminated in text box');
  309. }
  310. }
  311. if (k < text.length) {
  312. // @test Interspersed Text, Mbox Mbox
  313. mml.push(internalText(parser, text.slice(k), def));
  314. }
  315. if (level != null) {
  316. // @test Label, Fbox, Hbox
  317. mml = [parser.create('node', 'mstyle', mml, {displaystyle: false, scriptlevel: level})];
  318. } else if (mml.length > 1) {
  319. // @test Interspersed Text
  320. mml = [parser.create('node', 'mrow', mml)];
  321. }
  322. return mml;
  323. }
  324. /**
  325. * Parses text internal to boxes or labels.
  326. * @param {TexParser} parser The current tex parser.
  327. * @param {string} text The text to parse.
  328. * @param {EnvList} def The attributes of the text node.
  329. * @return {MmlNode} The text node.
  330. */
  331. export function internalText(parser: TexParser, text: string, def: EnvList): MmlNode {
  332. // @test Label, Fbox, Hbox
  333. text = text.replace(/^\s+/, entities.nbsp).replace(/\s+$/, entities.nbsp);
  334. let textNode = parser.create('text', text);
  335. return parser.create('node', 'mtext', [], def, textNode);
  336. }
  337. /**
  338. * Create an munderover node with the given script position.
  339. * @param {TexParser} parser The current TeX parser.
  340. * @param {MmlNode} base The base node.
  341. * @param {MmlNode} script The under- or over-script.
  342. * @param {string} pos Either 'over' or 'under'.
  343. * @param {boolean} stack True if super- or sub-scripts should stack.
  344. * @return {MmlNode} The generated node (MmlMunderover or TeXAtom)
  345. */
  346. export function underOver(parser: TexParser, base: MmlNode, script: MmlNode, pos: string, stack: boolean): MmlNode {
  347. // @test Overline
  348. ParseUtil.checkMovableLimits(base);
  349. if (NodeUtil.isType(base, 'munderover') && NodeUtil.isEmbellished(base)) {
  350. // @test Overline Limits
  351. NodeUtil.setProperties(NodeUtil.getCoreMO(base), {lspace: 0, rspace: 0});
  352. const mo = parser.create('node', 'mo', [], {rspace: 0});
  353. base = parser.create('node', 'mrow', [mo, base]);
  354. // TODO? add an empty <mi> so it's not embellished any more
  355. }
  356. const mml = parser.create('node', 'munderover', [base]) as MmlMunderover;
  357. NodeUtil.setChild(mml, pos === 'over' ? mml.over : mml.under, script);
  358. let node: MmlNode = mml;
  359. if (stack) {
  360. // @test Overbrace 1 2 3, Underbrace, Overbrace Op 1 2
  361. node = parser.create('node', 'TeXAtom', [mml], {texClass: TEXCLASS.OP, movesupsub: true});
  362. }
  363. NodeUtil.setProperty(node, 'subsupOK', true);
  364. return node;
  365. }
  366. /**
  367. * Set movablelimits to false if necessary.
  368. * @param {MmlNode} base The base node being tested.
  369. */
  370. export function checkMovableLimits(base: MmlNode) {
  371. const symbol = (NodeUtil.isType(base, 'mo') ? NodeUtil.getForm(base) : null);
  372. if (NodeUtil.getProperty(base, 'movablelimits') || (symbol && symbol[3] && symbol[3].movablelimits)) {
  373. // @test Overline Sum
  374. NodeUtil.setProperties(base, {movablelimits: false});
  375. }
  376. }
  377. /**
  378. * Trim spaces from a string.
  379. * @param {string} text The string to clean.
  380. * @return {string} The string with leading and trailing whitespace removed.
  381. */
  382. export function trimSpaces(text: string): string {
  383. if (typeof(text) !== 'string') {
  384. return text;
  385. }
  386. let TEXT = text.trim();
  387. if (TEXT.match(/\\$/) && text.match(/ $/)) {
  388. TEXT += ' ';
  389. }
  390. return TEXT;
  391. }
  392. /**
  393. * Sets alignment in array definitions.
  394. * @param {ArrayItem} array The array item.
  395. * @param {string} align The alignment string.
  396. * @return {ArrayItem} The altered array item.
  397. */
  398. export function setArrayAlign(array: ArrayItem, align: string): ArrayItem {
  399. // @test Array1, Array2, Array Test
  400. align = ParseUtil.trimSpaces(align || '');
  401. if (align === 't') {
  402. array.arraydef.align = 'baseline 1';
  403. } else if (align === 'b') {
  404. array.arraydef.align = 'baseline -1';
  405. } else if (align === 'c') {
  406. array.arraydef.align = 'axis';
  407. } else if (align) {
  408. array.arraydef.align = align;
  409. } // FIXME: should be an error?
  410. return array;
  411. }
  412. /**
  413. * Replace macro parameters with their values.
  414. * @param {TexParser} parser The current TeX parser.
  415. * @param {string[]} args A list of arguments for macro parameters.
  416. * @param {string} str The macro parameter string.
  417. * @return {string} The string with all parameters replaced by arguments.
  418. */
  419. export function substituteArgs(parser: TexParser, args: string[],
  420. str: string): string {
  421. let text = '';
  422. let newstring = '';
  423. let i = 0;
  424. while (i < str.length) {
  425. let c = str.charAt(i++);
  426. if (c === '\\') {
  427. text += c + str.charAt(i++);
  428. }
  429. else if (c === '#') {
  430. c = str.charAt(i++);
  431. if (c === '#') {
  432. text += c;
  433. } else {
  434. if (!c.match(/[1-9]/) || parseInt(c, 10) > args.length) {
  435. throw new TexError('IllegalMacroParam',
  436. 'Illegal macro parameter reference');
  437. }
  438. newstring = addArgs(parser, addArgs(parser, newstring, text),
  439. args[parseInt(c, 10) - 1]);
  440. text = '';
  441. }
  442. } else {
  443. text += c;
  444. }
  445. }
  446. return addArgs(parser, newstring, text);
  447. }
  448. /**
  449. * Adds a new expanded argument to an already macro parameter string. Makes
  450. * sure that macros are followed by a space if their names could accidentally
  451. * be continued into the following text.
  452. * @param {TexParser} parser The current TeX parser.
  453. * @param {string} s1 The already expanded string.
  454. * @param {string} s2 The string to add.
  455. * @return {string} The combined string.
  456. */
  457. export function addArgs(parser: TexParser, s1: string, s2: string): string {
  458. if (s2.match(/^[a-z]/i) && s1.match(/(^|[^\\])(\\\\)*\\[a-z]+$/i)) {
  459. s1 += ' ';
  460. }
  461. if (s1.length + s2.length > parser.configuration.options['maxBuffer']) {
  462. throw new TexError('MaxBufferSize',
  463. 'MathJax internal buffer size exceeded; is there a' +
  464. ' recursive macro call?');
  465. }
  466. return s1 + s2;
  467. }
  468. /**
  469. * Report an error if there are too many macro substitutions.
  470. * @param {TexParser} parser The current TeX parser.
  471. * @param {boolean} isMacro True if we are substituting a macro, false for environment.
  472. */
  473. export function checkMaxMacros(parser: TexParser, isMacro: boolean = true) {
  474. if (++parser.macroCount <= parser.configuration.options['maxMacros']) {
  475. return;
  476. }
  477. if (isMacro) {
  478. throw new TexError('MaxMacroSub1',
  479. 'MathJax maximum macro substitution count exceeded; ' +
  480. 'is here a recursive macro call?');
  481. } else {
  482. throw new TexError('MaxMacroSub2',
  483. 'MathJax maximum substitution count exceeded; ' +
  484. 'is there a recursive latex environment?');
  485. }
  486. }
  487. /**
  488. * Check for bad nesting of equation environments
  489. */
  490. export function checkEqnEnv(parser: TexParser) {
  491. if (parser.stack.global.eqnenv) {
  492. // @test ErroneousNestingEq
  493. throw new TexError('ErroneousNestingEq', 'Erroneous nesting of equation structures');
  494. }
  495. parser.stack.global.eqnenv = true;
  496. }
  497. /**
  498. * Copy an MmlNode and add it (and its children) to the proper lists.
  499. *
  500. * @param {MmlNode} node The MmlNode to copy
  501. * @param {TexParser} parser The active tex parser
  502. * @return {MmlNode} The duplicate tree
  503. */
  504. export function copyNode(node: MmlNode, parser: TexParser): MmlNode {
  505. const tree = node.copy() as MmlNode;
  506. const options = parser.configuration;
  507. tree.walkTree((n: MmlNode) => {
  508. options.addNode(n.kind, n);
  509. const lists = (n.getProperty('in-lists') as string || '').split(/,/);
  510. for (const list of lists) {
  511. list && options.addNode(list, n);
  512. }
  513. });
  514. return tree;
  515. }
  516. /**
  517. * This is a placeholder for future security filtering of attributes.
  518. * @param {TexParser} parser The current parser.
  519. * @param {string} name The attribute name.
  520. * @param {string} value The attribute value to filter.
  521. * @return {string} The filtered value.
  522. */
  523. export function MmlFilterAttribute(_parser: TexParser, _name: string, value: string): string {
  524. // TODO: Implement in security package.
  525. return value;
  526. }
  527. /**
  528. * Initialises an stack environment with current font definition in the parser.
  529. * @param {TexParser} parser The current tex parser.
  530. * @return {EnvList} The initialised environment list.
  531. */
  532. export function getFontDef(parser: TexParser): EnvList {
  533. const font = parser.stack.env['font'];
  534. return (font ? {mathvariant: font} : {});
  535. }
  536. /**
  537. * Splits a package option list of the form [x=y,z=1] into an attribute list
  538. * of the form {x: y, z: 1}.
  539. * @param {string} attrib The attributes of the package.
  540. * @param {{[key: string]: number}?} allowed A list of allowed options. If
  541. * given only allowed arguments are returned.
  542. * @param {boolean?} error If true, raises an exception if not allowed options
  543. * are found.
  544. * @return {EnvList} The attribute list.
  545. */
  546. export function keyvalOptions(attrib: string,
  547. allowed: {[key: string]: number} = null,
  548. error: boolean = false): EnvList {
  549. let def: EnvList = readKeyval(attrib);
  550. if (allowed) {
  551. for (let key of Object.keys(def)) {
  552. if (!allowed.hasOwnProperty(key)) {
  553. if (error) {
  554. throw new TexError('InvalidOption', 'Invalid option: %1', key);
  555. }
  556. delete def[key];
  557. }
  558. }
  559. }
  560. return def;
  561. }
  562. /**
  563. * Implementation of the keyval function from https://www.ctan.org/pkg/keyval
  564. * @param {string} text The optional parameter string for a package or
  565. * command.
  566. * @return {EnvList} Set of options as key/value pairs.
  567. */
  568. function readKeyval(text: string): EnvList {
  569. let options: EnvList = {};
  570. let rest = text;
  571. let end, key, val;
  572. while (rest) {
  573. [key, end, rest] = readValue(rest, ['=', ',']);
  574. if (end === '=') {
  575. [val, end, rest] = readValue(rest, [',']);
  576. val = (val === 'false' || val === 'true') ?
  577. JSON.parse(val) : val;
  578. options[key] = val;
  579. } else if (key) {
  580. options[key] = true;
  581. }
  582. }
  583. return options;
  584. }
  585. /**
  586. * Removes pairs of outer braces.
  587. * @param {string} text The string to clean.
  588. * @param {number} count The number of outer braces to slice off.
  589. * @return {string} The cleaned string.
  590. */
  591. function removeBraces(text: string, count: number): string {
  592. while (count > 0) {
  593. text = text.trim().slice(1, -1);
  594. count--;
  595. }
  596. return text.trim();
  597. }
  598. /**
  599. * Read a value from the given string until an end parameter is reached or
  600. * string is exhausted.
  601. * @param {string} text The string to process.
  602. * @param {string[]} end List of possible end characters.
  603. * @return {[string, string, string]} The collected value, the actual end
  604. * character, and the rest of the string still to parse.
  605. */
  606. function readValue(text: string, end: string[]): [string, string, string] {
  607. let length = text.length;
  608. let braces = 0;
  609. let value = '';
  610. let index = 0;
  611. let start = 0; // Counter for the starting left braces.
  612. let startCount = true; // Flag for counting starting left braces.
  613. let stopCount = false; // If true right braces are found directly
  614. // after starting braces, but no other char yet.
  615. while (index < length) {
  616. let c = text[index++];
  617. switch (c) {
  618. case ' ': // Ignore spaces.
  619. break;
  620. case '{':
  621. if (startCount) { // Count start left braces at start.
  622. start++;
  623. } else {
  624. stopCount = false;
  625. if (start > braces) { // Some start left braces have been closed.
  626. start = braces;
  627. }
  628. }
  629. braces++;
  630. break;
  631. case '}':
  632. if (braces) { // Closing braces.
  633. braces--;
  634. }
  635. if (startCount || stopCount) { // Closing braces at the start.
  636. start--;
  637. stopCount = true; // Continue to close braces.
  638. }
  639. startCount = false; // Stop counting start left braces.
  640. break;
  641. default:
  642. if (!braces && end.indexOf(c) !== -1) { // End character reached.
  643. return [stopCount ? 'true' : // If Stop count is true we
  644. // have balanced braces, only.
  645. removeBraces(value, start), c, text.slice(index)];
  646. }
  647. startCount = false;
  648. stopCount = false;
  649. }
  650. value += c;
  651. }
  652. if (braces) {
  653. throw new TexError('ExtraOpenMissingClose',
  654. 'Extra open brace or missing close brace');
  655. }
  656. return [stopCount ? 'true' : removeBraces(value, start), '', text.slice(index)];
  657. }
  658. }
  659. export default ParseUtil;