NewcommandUtil.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  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 Utility functions for the newcommand package.
  19. *
  20. * @author v.sorge@mathjax.org (Volker Sorge)
  21. */
  22. import ParseUtil from '../ParseUtil.js';
  23. import TexError from '../TexError.js';
  24. import TexParser from '../TexParser.js';
  25. import {Macro, Symbol} from '../Symbol.js';
  26. import {Args, Attributes, ParseMethod} from '../Types.js';
  27. import * as sm from '../SymbolMap.js';
  28. namespace NewcommandUtil {
  29. /**
  30. * Transforms the attributes of a symbol into the arguments of a macro. E.g.,
  31. * Symbol('ell', 'l', {mathvariant: "italic"}) is turned into Macro arguments:
  32. * ['ell', 'l', 'mathvariant', 'italic'].
  33. *
  34. * @param {string} name The command name for the symbol.
  35. * @param {Symbol} symbol The symbol associated with name.
  36. * @return {Args[]} Arguments for a macro.
  37. */
  38. export function disassembleSymbol(name: string, symbol: Symbol): Args[] {
  39. let newArgs = [name, symbol.char] as Args[];
  40. // @test Let Relet, Let Let, Let Circular Macro
  41. if (symbol.attributes) {
  42. // @test Let Relet
  43. for (let key in symbol.attributes) {
  44. newArgs.push(key);
  45. newArgs.push(symbol.attributes[key] as Args);
  46. }
  47. }
  48. return newArgs;
  49. }
  50. /**
  51. * Assembles a symbol from a list of macro arguments. This is the inverse
  52. * method of the one above.
  53. *
  54. * @param {Args[]} args The arguments of the macro.
  55. * @return {Symbol} The Symbol generated from the arguments..
  56. */
  57. export function assembleSymbol(args: Args[]): Symbol {
  58. // @test Let Relet, Let Let, Let Circular Macro
  59. let name = args[0] as string;
  60. let char = args[1] as string;
  61. let attrs: Attributes = {};
  62. for (let i = 2; i < args.length; i = i + 2) {
  63. // @test Let Relet
  64. attrs[args[i] as string] = args[i + 1];
  65. }
  66. return new Symbol(name, char, attrs);
  67. }
  68. /**
  69. * Get the next CS name or give an error.
  70. * @param {TexParser} parser The calling parser.
  71. * @param {string} cmd The string starting with a control sequence.
  72. * @return {string} The control sequence.
  73. */
  74. export function GetCSname(parser: TexParser, cmd: string): string {
  75. // @test Def ReDef, Let Bar, Let Brace Equal
  76. let c = parser.GetNext();
  77. if (c !== '\\') {
  78. // @test No CS
  79. throw new TexError('MissingCS',
  80. '%1 must be followed by a control sequence', cmd);
  81. }
  82. let cs = ParseUtil.trimSpaces(parser.GetArgument(cmd));
  83. return cs.substr(1);
  84. }
  85. /**
  86. * Get a control sequence name as an argument (doesn't require the backslash)
  87. * @param {TexParser} parser The calling parser.
  88. * @param {string} name The macro that is getting the name.
  89. * @return {string} The control sequence.
  90. */
  91. export function GetCsNameArgument(parser: TexParser, name: string): string {
  92. let cs = ParseUtil.trimSpaces(parser.GetArgument(name));
  93. if (cs.charAt(0) === '\\') {
  94. // @test Newcommand Simple
  95. cs = cs.substr(1);
  96. }
  97. if (!cs.match(/^(.|[a-z]+)$/i)) {
  98. // @test Illegal CS
  99. throw new TexError('IllegalControlSequenceName',
  100. 'Illegal control sequence name for %1', name);
  101. }
  102. return cs;
  103. }
  104. /**
  105. * Get the number of arguments for a macro definition
  106. * @param {TexParser} parser The calling parser.
  107. * @param {string} name The macro that is getting the argument count.
  108. * @return {string} The number of arguments (or blank).
  109. */
  110. export function GetArgCount(parser: TexParser, name: string): string {
  111. let n = parser.GetBrackets(name);
  112. if (n) {
  113. // @test Newcommand Optional, Newcommand Arg, Newcommand Arg Optional
  114. // @test Newenvironment Optional, Newenvironment Arg Optional
  115. n = ParseUtil.trimSpaces(n);
  116. if (!n.match(/^[0-9]+$/)) {
  117. // @test Illegal Argument Number
  118. throw new TexError('IllegalParamNumber',
  119. 'Illegal number of parameters specified in %1', name);
  120. }
  121. }
  122. return n;
  123. }
  124. /**
  125. * Get a \def parameter template.
  126. * @param {TexParser} parser The calling parser.
  127. * @param {string} cmd The string starting with the template.
  128. * @param {string} cs The control sequence of the \def.
  129. * @return {number | string[]} The number of parameters or a string array if
  130. * there is an optional argument.
  131. */
  132. export function GetTemplate(parser: TexParser, cmd: string, cs: string): number | string[] {
  133. // @test Def Double Let, Def ReDef, Def Let
  134. let c = parser.GetNext();
  135. let params: string[] = [];
  136. let n = 0;
  137. let i = parser.i;
  138. while (parser.i < parser.string.length) {
  139. c = parser.GetNext();
  140. if (c === '#') {
  141. // @test Def ReDef, Def Let, Def Optional Brace
  142. if (i !== parser.i) {
  143. // @test Def Let, Def Optional Brace
  144. params[n] = parser.string.substr(i, parser.i - i);
  145. }
  146. c = parser.string.charAt(++parser.i);
  147. if (!c.match(/^[1-9]$/)) {
  148. // @test Illegal Hash
  149. throw new TexError('CantUseHash2',
  150. 'Illegal use of # in template for %1', cs);
  151. }
  152. if (parseInt(c) !== ++n) {
  153. // @test No Sequence
  154. throw new TexError('SequentialParam',
  155. 'Parameters for %1 must be numbered sequentially', cs);
  156. }
  157. i = parser.i + 1;
  158. } else if (c === '{') {
  159. // @test Def Double Let, Def ReDef, Def Let
  160. if (i !== parser.i) {
  161. // @test Optional Brace Error
  162. params[n] = parser.string.substr(i, parser.i - i);
  163. }
  164. if (params.length > 0) {
  165. // @test Def Let, Def Optional Brace
  166. return [n.toString()].concat(params);
  167. } else {
  168. // @test Def Double Let, Def ReDef
  169. return n;
  170. }
  171. }
  172. parser.i++;
  173. }
  174. // @test No Replacement
  175. throw new TexError('MissingReplacementString',
  176. 'Missing replacement string for definition of %1', cmd);
  177. }
  178. /**
  179. * Find a single parameter delimited by a trailing template.
  180. * @param {TexParser} parser The calling parser.
  181. * @param {string} name The name of the calling command.
  182. * @param {string} param The parameter for the macro.
  183. */
  184. export function GetParameter(parser: TexParser, name: string, param: string) {
  185. if (param == null) {
  186. // @test Def Let, Def Optional Brace, Def Options CS
  187. return parser.GetArgument(name);
  188. }
  189. let i = parser.i;
  190. let j = 0;
  191. let hasBraces = 0;
  192. while (parser.i < parser.string.length) {
  193. let c = parser.string.charAt(parser.i);
  194. // @test Def Let, Def Optional Brace, Def Options CS
  195. if (c === '{') {
  196. // @test Def Optional Brace, Def Options CS
  197. if (parser.i === i) {
  198. // @test Def Optional Brace
  199. hasBraces = 1;
  200. }
  201. parser.GetArgument(name);
  202. j = parser.i - i;
  203. } else if (MatchParam(parser, param)) {
  204. // @test Def Let, Def Optional Brace, Def Options CS
  205. if (hasBraces) {
  206. // @test Def Optional Brace
  207. i++;
  208. j -= 2;
  209. }
  210. return parser.string.substr(i, j);
  211. } else if (c === '\\') {
  212. // @test Def Options CS
  213. parser.i++;
  214. j++;
  215. hasBraces = 0;
  216. let match = parser.string.substr(parser.i).match(/[a-z]+|./i);
  217. if (match) {
  218. // @test Def Options CS
  219. parser.i += match[0].length;
  220. j = parser.i - i;
  221. }
  222. } else {
  223. // @test Def Let
  224. parser.i++;
  225. j++;
  226. hasBraces = 0;
  227. }
  228. }
  229. // @test Runaway Argument
  230. throw new TexError('RunawayArgument', 'Runaway argument for %1?', name);
  231. }
  232. /**
  233. * Check if a template is at the current location.
  234. * (The match must be exact, with no spacing differences. TeX is
  235. * a little more forgiving than this about spaces after macro names)
  236. * @param {TexParser} parser The calling parser.
  237. * @param {string} param Tries to match an optional parameter.
  238. * @return {number} The number of optional parameters, either 0 or 1.
  239. */
  240. export function MatchParam(parser: TexParser, param: string): number {
  241. // @test Def Let, Def Optional Brace, Def Options CS
  242. if (parser.string.substr(parser.i, param.length) !== param) {
  243. // @test Def Let, Def Options CS
  244. return 0;
  245. }
  246. if (param.match(/\\[a-z]+$/i) &&
  247. parser.string.charAt(parser.i + param.length).match(/[a-z]/i)) {
  248. // @test (missing)
  249. return 0;
  250. }
  251. // @test Def Let, Def Optional Brace, Def Options CS
  252. parser.i += param.length;
  253. return 1;
  254. }
  255. /**
  256. * Adds a new delimiter as extension to the parser.
  257. * @param {TexParser} parser The current parser.
  258. * @param {string} cs The control sequence of the delimiter.
  259. * @param {string} char The corresponding character.
  260. * @param {Attributes} attr The attributes needed for parsing.
  261. */
  262. export function addDelimiter(parser: TexParser, cs: string, char: string, attr: Attributes) {
  263. const handlers = parser.configuration.handlers;
  264. const handler = handlers.retrieve(NEW_DELIMITER) as sm.DelimiterMap;
  265. handler.add(cs, new Symbol(cs, char, attr));
  266. }
  267. /**
  268. * Adds a new macro as extension to the parser.
  269. * @param {TexParser} parser The current parser.
  270. * @param {string} cs The control sequence of the delimiter.
  271. * @param {ParseMethod} func The parse method for this macro.
  272. * @param {Args[]} attr The attributes needed for parsing.
  273. * @param {string=} symbol Optionally original symbol for macro, in case it is
  274. * different from the control sequence.
  275. */
  276. export function addMacro(parser: TexParser, cs: string, func: ParseMethod, attr: Args[],
  277. symbol: string = '') {
  278. const handlers = parser.configuration.handlers;
  279. const handler = handlers.retrieve(NEW_COMMAND) as sm.CommandMap;
  280. handler.add(cs, new Macro(symbol ? symbol : cs, func, attr));
  281. }
  282. /**
  283. * Adds a new environment as extension to the parser.
  284. * @param {TexParser} parser The current parser.
  285. * @param {string} env The environment name.
  286. * @param {ParseMethod} func The parse method for this macro.
  287. * @param {Args[]} attr The attributes needed for parsing.
  288. */
  289. export function addEnvironment(parser: TexParser, env: string, func: ParseMethod, attr: Args[]) {
  290. const handlers = parser.configuration.handlers;
  291. const handler = handlers.retrieve(NEW_ENVIRONMENT) as sm.EnvironmentMap;
  292. handler.add(env, new Macro(env, func, attr));
  293. }
  294. /**
  295. * Naming constants for the extension mappings.
  296. */
  297. export const NEW_DELIMITER = 'new-Delimiter';
  298. export const NEW_COMMAND = 'new-Command';
  299. export const NEW_ENVIRONMENT = 'new-Environment';
  300. }
  301. export default NewcommandUtil;