semantic-enrich.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2018-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 Mixin that adds semantic enrichment to internal MathML
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {mathjax} from '../mathjax.js';
  23. import {Handler} from '../core/Handler.js';
  24. import {MathDocument, AbstractMathDocument, MathDocumentConstructor} from '../core/MathDocument.js';
  25. import {MathItem, AbstractMathItem, STATE, newState} from '../core/MathItem.js';
  26. import {MmlNode} from '../core/MmlTree/MmlNode.js';
  27. import {MathML} from '../input/mathml.js';
  28. import {SerializedMmlVisitor} from '../core/MmlTree/SerializedMmlVisitor.js';
  29. import {OptionList, expandable} from '../util/Options.js';
  30. import Sre from './sre.js';
  31. /*==========================================================================*/
  32. /**
  33. * The current speech setting for Sre
  34. */
  35. let currentSpeech = 'none';
  36. /**
  37. * Generic constructor for Mixins
  38. */
  39. export type Constructor<T> = new(...args: any[]) => T;
  40. /*==========================================================================*/
  41. /**
  42. * Add STATE value for being enriched (after COMPILED and before TYPESET)
  43. */
  44. newState('ENRICHED', 30);
  45. /**
  46. * Add STATE value for adding speech (after TYPESET)
  47. */
  48. newState('ATTACHSPEECH', 155);
  49. /**
  50. * The functions added to MathItem for enrichment
  51. *
  52. * @template N The HTMLElement node class
  53. * @template T The Text node class
  54. * @template D The Document class
  55. */
  56. export interface EnrichedMathItem<N, T, D> extends MathItem<N, T, D> {
  57. /**
  58. * @param {MathDocument} document The document where enrichment is occurring
  59. * @param {boolean} force True to force the enrichment even if not enabled
  60. */
  61. enrich(document: MathDocument<N, T, D>, force?: boolean): void;
  62. /**
  63. * @param {MathDocument} document The document where enrichment is occurring
  64. */
  65. attachSpeech(document: MathDocument<N, T, D>): void;
  66. }
  67. /**
  68. * The mixin for adding enrichment to MathItems
  69. *
  70. * @param {B} BaseMathItem The MathItem class to be extended
  71. * @param {MathML} MmlJax The MathML input jax used to convert the enriched MathML
  72. * @param {Function} toMathML The function to serialize the internal MathML
  73. * @return {EnrichedMathItem} The enriched MathItem class
  74. *
  75. * @template N The HTMLElement node class
  76. * @template T The Text node class
  77. * @template D The Document class
  78. * @template B The MathItem class to extend
  79. */
  80. export function EnrichedMathItemMixin<N, T, D, B extends Constructor<AbstractMathItem<N, T, D>>>(
  81. BaseMathItem: B,
  82. MmlJax: MathML<N, T, D>,
  83. toMathML: (node: MmlNode) => string
  84. ): Constructor<EnrichedMathItem<N, T, D>> & B {
  85. return class extends BaseMathItem {
  86. /**
  87. * @param {any} node The node to be serialized
  88. * @return {string} The serialized version of node
  89. */
  90. protected serializeMml(node: any): string {
  91. if ('outerHTML' in node) {
  92. return node.outerHTML;
  93. }
  94. //
  95. // For IE11
  96. //
  97. if (typeof Element !== 'undefined' && typeof window !== 'undefined' && node instanceof Element) {
  98. const div = window.document.createElement('div');
  99. div.appendChild(node);
  100. return div.innerHTML;
  101. }
  102. //
  103. // For NodeJS version of Sre
  104. //
  105. return node.toString();
  106. }
  107. /**
  108. * @param {MathDocument} document The MathDocument for the MathItem
  109. * @param {boolean} force True to force the enrichment even if not enabled
  110. */
  111. public enrich(document: MathDocument<N, T, D>, force: boolean = false) {
  112. if (this.state() >= STATE.ENRICHED) return;
  113. if (!this.isEscaped && (document.options.enableEnrichment || force)) {
  114. if (document.options.sre.speech !== currentSpeech) {
  115. currentSpeech = document.options.sre.speech;
  116. mathjax.retryAfter(
  117. Sre.setupEngine(document.options.sre).then(
  118. () => Sre.sreReady()));
  119. }
  120. const math = new document.options.MathItem('', MmlJax);
  121. try {
  122. const mml = this.inputData.originalMml = toMathML(this.root);
  123. math.math = this.serializeMml(Sre.toEnriched(mml));
  124. math.display = this.display;
  125. math.compile(document);
  126. this.root = math.root;
  127. this.inputData.enrichedMml = math.math;
  128. } catch (err) {
  129. document.options.enrichError(document, this, err);
  130. }
  131. }
  132. this.state(STATE.ENRICHED);
  133. }
  134. /**
  135. * @param {MathDocument} document The MathDocument for the MathItem
  136. */
  137. public attachSpeech(document: MathDocument<N, T, D>) {
  138. if (this.state() >= STATE.ATTACHSPEECH) return;
  139. const attributes = this.root.attributes;
  140. const speech = (attributes.get('aria-label') ||
  141. this.getSpeech(this.root)) as string;
  142. if (speech) {
  143. const adaptor = document.adaptor;
  144. const node = this.typesetRoot;
  145. adaptor.setAttribute(node, 'aria-label', speech);
  146. for (const child of adaptor.childNodes(node) as N[]) {
  147. adaptor.setAttribute(child, 'aria-hidden', 'true');
  148. }
  149. }
  150. this.state(STATE.ATTACHSPEECH);
  151. }
  152. /**
  153. * Retrieves the actual speech element that should be used as aria label.
  154. * @param {MmlNode} node The root node to search from.
  155. * @return {string} The speech content.
  156. */
  157. private getSpeech(node: MmlNode): string {
  158. const attributes = node.attributes;
  159. if (!attributes) return '';
  160. const speech = attributes.getExplicit('data-semantic-speech') as string;
  161. if (!attributes.getExplicit('data-semantic-parent') && speech) {
  162. return speech;
  163. }
  164. for (let child of node.childNodes) {
  165. let value = this.getSpeech(child as MmlNode);
  166. if (value != null) {
  167. return value;
  168. }
  169. }
  170. return '';
  171. }
  172. };
  173. }
  174. /*==========================================================================*/
  175. /**
  176. * The functions added to MathDocument for enrichment
  177. *
  178. * @template N The HTMLElement node class
  179. * @template T The Text node class
  180. * @template D The Document class
  181. */
  182. export interface EnrichedMathDocument<N, T, D> extends AbstractMathDocument<N, T, D> {
  183. /**
  184. * Perform enrichment on the MathItems in the MathDocument
  185. *
  186. * @return {EnrichedMathDocument} The MathDocument (so calls can be chained)
  187. */
  188. enrich(): EnrichedMathDocument<N, T, D>;
  189. /**
  190. * Attach speech to the MathItems in the MathDocument
  191. *
  192. * @return {EnrichedMathDocument} The MathDocument (so calls can be chained)
  193. */
  194. attachSpeech(): EnrichedMathDocument<N, T, D>;
  195. /**
  196. * @param {EnrichedMathDocument} doc The MathDocument for the error
  197. * @paarm {EnrichedMathItem} math The MathItem causing the error
  198. * @param {Error} err The error being processed
  199. */
  200. enrichError(doc: EnrichedMathDocument<N, T, D>, math: EnrichedMathItem<N, T, D>, err: Error): void;
  201. }
  202. /**
  203. * The mixin for adding enrichment to MathDocuments
  204. *
  205. * @param {B} BaseDocument The MathDocument class to be extended
  206. * @param {MathML} MmlJax The MathML input jax used to convert the enriched MathML
  207. * @return {EnrichedMathDocument} The enriched MathDocument class
  208. *
  209. * @template N The HTMLElement node class
  210. * @template T The Text node class
  211. * @template D The Document class
  212. * @template B The MathDocument class to extend
  213. */
  214. export function EnrichedMathDocumentMixin<N, T, D, B extends MathDocumentConstructor<AbstractMathDocument<N, T, D>>>(
  215. BaseDocument: B,
  216. MmlJax: MathML<N, T, D>,
  217. ): MathDocumentConstructor<EnrichedMathDocument<N, T, D>> & B {
  218. return class extends BaseDocument {
  219. /**
  220. * @override
  221. */
  222. public static OPTIONS: OptionList = {
  223. ...BaseDocument.OPTIONS,
  224. enableEnrichment: true,
  225. enrichError: (doc: EnrichedMathDocument<N, T, D>,
  226. math: EnrichedMathItem<N, T, D>,
  227. err: Error) => doc.enrichError(doc, math, err),
  228. renderActions: expandable({
  229. ...BaseDocument.OPTIONS.renderActions,
  230. enrich: [STATE.ENRICHED],
  231. attachSpeech: [STATE.ATTACHSPEECH]
  232. }),
  233. sre: expandable({
  234. speech: 'none', // by default no speech is included
  235. domain: 'mathspeak', // speech rules domain
  236. style: 'default', // speech rules style
  237. locale: 'en' // switch the locale
  238. }),
  239. };
  240. /**
  241. * Enrich the MathItem class used for this MathDocument, and create the
  242. * temporary MathItem used for enrchment
  243. *
  244. * @override
  245. * @constructor
  246. */
  247. constructor(...args: any[]) {
  248. super(...args);
  249. MmlJax.setMmlFactory(this.mmlFactory);
  250. const ProcessBits = (this.constructor as typeof AbstractMathDocument).ProcessBits;
  251. if (!ProcessBits.has('enriched')) {
  252. ProcessBits.allocate('enriched');
  253. ProcessBits.allocate('attach-speech');
  254. }
  255. const visitor = new SerializedMmlVisitor(this.mmlFactory);
  256. const toMathML = ((node: MmlNode) => visitor.visitTree(node));
  257. this.options.MathItem =
  258. EnrichedMathItemMixin<N, T, D, Constructor<AbstractMathItem<N, T, D>>>(
  259. this.options.MathItem, MmlJax, toMathML
  260. );
  261. }
  262. /**
  263. * Attach speech from a MathItem to a node
  264. */
  265. public attachSpeech() {
  266. if (!this.processed.isSet('attach-speech')) {
  267. for (const math of this.math) {
  268. (math as EnrichedMathItem<N, T, D>).attachSpeech(this);
  269. }
  270. this.processed.set('attach-speech');
  271. }
  272. return this;
  273. }
  274. /**
  275. * Enrich the MathItems in this MathDocument
  276. */
  277. public enrich() {
  278. if (!this.processed.isSet('enriched')) {
  279. if (this.options.enableEnrichment) {
  280. for (const math of this.math) {
  281. (math as EnrichedMathItem<N, T, D>).enrich(this);
  282. }
  283. }
  284. this.processed.set('enriched');
  285. }
  286. return this;
  287. }
  288. /**
  289. */
  290. public enrichError(_doc: EnrichedMathDocument<N, T, D>, _math: EnrichedMathItem<N, T, D>, err: Error) {
  291. console.warn('Enrichment error:', err);
  292. }
  293. /**
  294. * @override
  295. */
  296. public state(state: number, restore: boolean = false) {
  297. super.state(state, restore);
  298. if (state < STATE.ENRICHED) {
  299. this.processed.clear('enriched');
  300. }
  301. return this;
  302. }
  303. };
  304. }
  305. /*==========================================================================*/
  306. /**
  307. * Add enrichment a Handler instance
  308. *
  309. * @param {Handler} handler The Handler instance to enhance
  310. * @param {MathML} MmlJax The MathML input jax to use for reading the enriched MathML
  311. * @return {Handler} The handler that was modified (for purposes of chainging extensions)
  312. *
  313. * @template N The HTMLElement node class
  314. * @template T The Text node class
  315. * @template D The Document class
  316. */
  317. export function EnrichHandler<N, T, D>(handler: Handler<N, T, D>, MmlJax: MathML<N, T, D>): Handler<N, T, D> {
  318. MmlJax.setAdaptor(handler.adaptor);
  319. handler.documentClass =
  320. EnrichedMathDocumentMixin<N, T, D, MathDocumentConstructor<AbstractMathDocument<N, T, D>>>(
  321. handler.documentClass, MmlJax
  322. );
  323. return handler;
  324. }