MathMLCompile.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2017-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 Implementation of the Compile function for the MathML input jax
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {MmlFactory} from '../../core/MmlTree/MmlFactory.js';
  23. import {MmlNode, TextNode, XMLNode, AbstractMmlNode, AbstractMmlTokenNode, TEXCLASS}
  24. from '../../core/MmlTree/MmlNode.js';
  25. import {userOptions, defaultOptions, OptionList} from '../../util/Options.js';
  26. import * as Entities from '../../util/Entities.js';
  27. import {DOMAdaptor} from '../../core/DOMAdaptor.js';
  28. /********************************************************************/
  29. /**
  30. * The class for performing the MathML DOM node to
  31. * internal MmlNode conversion.
  32. *
  33. * @template N The HTMLElement node class
  34. * @template T The Text node class
  35. * @template D The Document class
  36. */
  37. export class MathMLCompile<N, T, D> {
  38. /**
  39. * The default options for this object
  40. */
  41. public static OPTIONS: OptionList = {
  42. MmlFactory: null, // The MmlFactory to use (defaults to a new MmlFactory)
  43. fixMisplacedChildren: true, // True if we want to use heuristics to try to fix
  44. // problems with the tree based on HTML not handling
  45. // self-closing tags properly
  46. verify: { // Options to pass to verifyTree() controlling MathML verification
  47. ...AbstractMmlNode.verifyDefaults
  48. },
  49. translateEntities: true // True means translate entities in text nodes
  50. };
  51. /**
  52. * The DOMAdaptor for the document being processed
  53. */
  54. public adaptor: DOMAdaptor<N, T, D>;
  55. /**
  56. * The instance of the MmlFactory object and
  57. */
  58. protected factory: MmlFactory;
  59. /**
  60. * The options (the defaults with the user options merged in)
  61. */
  62. protected options: OptionList;
  63. /**
  64. * Merge the user options into the defaults, and save them
  65. * Create the MmlFactory object
  66. *
  67. * @param {OptionList} options The options controlling the conversion
  68. */
  69. constructor(options: OptionList = {}) {
  70. const Class = this.constructor as typeof MathMLCompile;
  71. this.options = userOptions(defaultOptions({}, Class.OPTIONS), options);
  72. }
  73. /**
  74. * @param{MmlFactory} mmlFactory The MathML factory to use for new nodes
  75. */
  76. public setMmlFactory(mmlFactory: MmlFactory) {
  77. this.factory = mmlFactory;
  78. }
  79. /**
  80. * Convert a MathML DOM tree to internal MmlNodes
  81. *
  82. * @param {N} node The <math> node to convert to MmlNodes
  83. * @return {MmlNode} The MmlNode at the root of the converted tree
  84. */
  85. public compile(node: N): MmlNode {
  86. let mml = this.makeNode(node);
  87. mml.verifyTree(this.options['verify']);
  88. mml.setInheritedAttributes({}, false, 0, false);
  89. mml.walkTree(this.markMrows);
  90. return mml;
  91. }
  92. /**
  93. * Recursively convert nodes and their children, taking MathJax classes
  94. * into account.
  95. *
  96. * FIXME: we should use data-* attributes rather than classes for these
  97. *
  98. * @param {N} node The node to convert to an MmlNode
  99. * @return {MmlNode} The converted MmlNode
  100. */
  101. public makeNode(node: N): MmlNode {
  102. const adaptor = this.adaptor;
  103. let limits = false;
  104. let kind = adaptor.kind(node).replace(/^.*:/, '');
  105. let texClass = adaptor.getAttribute(node, 'data-mjx-texclass') || '';
  106. if (texClass) {
  107. texClass = this.filterAttribute('data-mjx-texclass', texClass) || '';
  108. }
  109. let type = texClass && kind === 'mrow' ? 'TeXAtom' : kind;
  110. for (const name of this.filterClassList(adaptor.allClasses(node))) {
  111. if (name.match(/^MJX-TeXAtom-/) && kind === 'mrow') {
  112. texClass = name.substr(12);
  113. type = 'TeXAtom';
  114. } else if (name === 'MJX-fixedlimits') {
  115. limits = true;
  116. }
  117. }
  118. this.factory.getNodeClass(type) || this.error('Unknown node type "' + type + '"');
  119. let mml = this.factory.create(type);
  120. if (type === 'TeXAtom' && texClass === 'OP' && !limits) {
  121. mml.setProperty('movesupsub', true);
  122. mml.attributes.setInherited('movablelimits', true);
  123. }
  124. if (texClass) {
  125. mml.texClass = (TEXCLASS as {[name: string]: number})[texClass];
  126. mml.setProperty('texClass', mml.texClass);
  127. }
  128. this.addAttributes(mml, node);
  129. this.checkClass(mml, node);
  130. this.addChildren(mml, node);
  131. return mml;
  132. }
  133. /**
  134. * Copy the attributes from a MathML node to an MmlNode.
  135. *
  136. * @param {MmlNode} mml The MmlNode to which attributes will be added
  137. * @param {N} node The MathML node whose attributes to copy
  138. */
  139. protected addAttributes(mml: MmlNode, node: N) {
  140. let ignoreVariant = false;
  141. for (const attr of this.adaptor.allAttributes(node)) {
  142. let name = attr.name;
  143. let value = this.filterAttribute(name, attr.value);
  144. if (value === null || name === 'xmlns') {
  145. continue;
  146. }
  147. if (name.substr(0, 9) === 'data-mjx-') {
  148. switch (name.substr(9)) {
  149. case 'alternate':
  150. mml.setProperty('variantForm', true);
  151. break;
  152. case 'variant':
  153. mml.attributes.set('mathvariant', value);
  154. ignoreVariant = true;
  155. break;
  156. case 'smallmatrix':
  157. mml.setProperty('scriptlevel', 1);
  158. mml.setProperty('useHeight', false);
  159. break;
  160. case 'accent':
  161. mml.setProperty('mathaccent', value === 'true');
  162. break;
  163. case 'auto-op':
  164. mml.setProperty('autoOP', value === 'true');
  165. break;
  166. case 'script-align':
  167. mml.setProperty('scriptalign', value);
  168. break;
  169. }
  170. } else if (name !== 'class') {
  171. let val = value.toLowerCase();
  172. if (val === 'true' || val === 'false') {
  173. mml.attributes.set(name, val === 'true');
  174. } else if (!ignoreVariant || name !== 'mathvariant') {
  175. mml.attributes.set(name, value);
  176. }
  177. }
  178. }
  179. }
  180. /**
  181. * Provide a hook for the Safe extension to filter attribute values.
  182. *
  183. * @param {string} name The name of an attribute to filter
  184. * @param {string} value The value to filter
  185. */
  186. protected filterAttribute(_name: string, value: string) {
  187. return value;
  188. }
  189. /**
  190. * Provide a hook for the Safe extension to filter class names.
  191. *
  192. * @param {string[]} list The list of class names to filter
  193. */
  194. protected filterClassList(list: string[]) {
  195. return list;
  196. }
  197. /**
  198. * Convert the children of the MathML node and add them to the MmlNode
  199. *
  200. * @param {MmlNode} mml The MmlNode to which children will be added
  201. * @param {N} node The MathML node whose children are to be copied
  202. */
  203. protected addChildren(mml: MmlNode, node: N) {
  204. if (mml.arity === 0) {
  205. return;
  206. }
  207. const adaptor = this.adaptor;
  208. for (const child of adaptor.childNodes(node) as N[]) {
  209. const name = adaptor.kind(child);
  210. if (name === '#comment') {
  211. continue;
  212. }
  213. if (name === '#text') {
  214. this.addText(mml, child);
  215. } else if (mml.isKind('annotation-xml')) {
  216. mml.appendChild((this.factory.create('XML') as XMLNode).setXML(child, adaptor));
  217. } else {
  218. let childMml = mml.appendChild(this.makeNode(child)) as MmlNode;
  219. if (childMml.arity === 0 && adaptor.childNodes(child).length) {
  220. if (this.options['fixMisplacedChildren']) {
  221. this.addChildren(mml, child);
  222. } else {
  223. childMml.mError('There should not be children for ' + childMml.kind + ' nodes',
  224. this.options['verify'], true);
  225. }
  226. }
  227. }
  228. }
  229. }
  230. /**
  231. * Add text to a token node
  232. *
  233. * @param {MmlNode} mml The MmlNode to which text will be added
  234. * @param {N} child The text node whose contents is to be copied
  235. */
  236. protected addText(mml: MmlNode, child: N) {
  237. let text = this.adaptor.value(child);
  238. if ((mml.isToken || mml.getProperty('isChars')) && mml.arity) {
  239. if (mml.isToken) {
  240. text = Entities.translate(text);
  241. text = this.trimSpace(text);
  242. }
  243. mml.appendChild((this.factory.create('text') as TextNode).setText(text));
  244. } else if (text.match(/\S/)) {
  245. this.error('Unexpected text node "' + text + '"');
  246. }
  247. }
  248. /**
  249. * Check for special MJX values in the class and process them
  250. *
  251. * @param {MmlNode} mml The MmlNode to be modified according to the class markers
  252. * @param {N} node The MathML node whose class is to be processed
  253. */
  254. protected checkClass(mml: MmlNode, node: N) {
  255. let classList = [];
  256. for (const name of this.filterClassList(this.adaptor.allClasses(node))) {
  257. if (name.substr(0, 4) === 'MJX-') {
  258. if (name === 'MJX-variant') {
  259. mml.setProperty('variantForm', true);
  260. } else if (name.substr(0, 11) !== 'MJX-TeXAtom') {
  261. mml.attributes.set('mathvariant', this.fixCalligraphic(name.substr(3)));
  262. }
  263. } else {
  264. classList.push(name);
  265. }
  266. }
  267. if (classList.length) {
  268. mml.attributes.set('class', classList.join(' '));
  269. }
  270. }
  271. /**
  272. * Fix the old incorrect spelling of calligraphic.
  273. *
  274. * @param {string} variant The mathvariant name
  275. * @return {string} The corrected variant
  276. */
  277. protected fixCalligraphic(variant: string): string {
  278. return variant.replace(/caligraphic/, 'calligraphic');
  279. }
  280. /**
  281. * Check to see if an mrow has delimiters at both ends (so looks like an mfenced structure).
  282. *
  283. * @param {MmlNode} mml The node to check for mfenced structure
  284. */
  285. protected markMrows(mml: MmlNode) {
  286. if (mml.isKind('mrow') && !mml.isInferred && mml.childNodes.length >= 2) {
  287. let first = mml.childNodes[0] as MmlNode;
  288. let last = mml.childNodes[mml.childNodes.length - 1] as MmlNode;
  289. if (first.isKind('mo') && first.attributes.get('fence') && first.attributes.get('stretchy') &&
  290. last.isKind('mo') && last.attributes.get('fence') && last.attributes.get('stretchy')) {
  291. if (first.childNodes.length) {
  292. mml.setProperty('open', (first as AbstractMmlTokenNode).getText());
  293. }
  294. if (last.childNodes.length) {
  295. mml.setProperty('close', (last as AbstractMmlTokenNode).getText());
  296. }
  297. }
  298. }
  299. }
  300. /**
  301. * @param {string} text The text to have leading/trailing spaced removed
  302. * @return {string} The trimmed text
  303. */
  304. protected trimSpace(text: string): string {
  305. return text.replace(/[\t\n\r]/g, ' ') // whitespace to spaces
  306. .replace(/^ +/, '') // initial whitespace
  307. .replace(/ +$/, '') // trailing whitespace
  308. .replace(/ +/g, ' '); // internal multiple whitespace
  309. }
  310. /**
  311. * @param {string} message The error message to produce
  312. */
  313. protected error(message: string) {
  314. throw new Error(message);
  315. }
  316. }