Wrapper.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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 Implements the CHTMLWrapper class
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {OptionList} from '../../util/Options.js';
  23. import * as LENGTHS from '../../util/lengths.js';
  24. import {CommonWrapper, AnyWrapperClass, Constructor, StringMap} from '../common/Wrapper.js';
  25. import {CHTML} from '../chtml.js';
  26. import {CHTMLWrapperFactory} from './WrapperFactory.js';
  27. import {BBox} from '../../util/BBox.js';
  28. import {CHTMLFontData, CHTMLCharOptions, CHTMLDelimiterData} from './FontData.js';
  29. export {Constructor, StringMap} from '../common/Wrapper.js';
  30. /*****************************************************************/
  31. /**
  32. * Some standard sizes to use in predefind CSS properties
  33. */
  34. export const FONTSIZE: StringMap = {
  35. '70.7%': 's',
  36. '70%': 's',
  37. '50%': 'ss',
  38. '60%': 'Tn',
  39. '85%': 'sm',
  40. '120%': 'lg',
  41. '144%': 'Lg',
  42. '173%': 'LG',
  43. '207%': 'hg',
  44. '249%': 'HG'
  45. };
  46. export const SPACE: StringMap = {
  47. /* tslint:disable:whitespace */
  48. [LENGTHS.em(2/18)]: '1',
  49. [LENGTHS.em(3/18)]: '2',
  50. [LENGTHS.em(4/18)]: '3',
  51. [LENGTHS.em(5/18)]: '4',
  52. [LENGTHS.em(6/18)]: '5'
  53. /* tslint:enable */
  54. };
  55. /**
  56. * Shorthand for making a CHTMLWrapper constructor
  57. */
  58. export type CHTMLConstructor<N, T, D> = Constructor<CHTMLWrapper<N, T, D>>;
  59. /*****************************************************************/
  60. /**
  61. * The type of the CHTMLWrapper class (used when creating the wrapper factory for this class)
  62. */
  63. export interface CHTMLWrapperClass extends AnyWrapperClass {
  64. kind: string;
  65. /**
  66. * If true, this causes a style for the node type to be generated automatically
  67. * that sets display:inline-block (as needed for the output for MmlNodes).
  68. */
  69. autoStyle: boolean;
  70. }
  71. /*****************************************************************/
  72. /**
  73. * The base CHTMLWrapper class
  74. *
  75. * @template N The HTMLElement node class
  76. * @template T The Text node class
  77. * @template D The Document class
  78. */
  79. export class CHTMLWrapper<N, T, D> extends
  80. CommonWrapper<
  81. CHTML<N, T, D>,
  82. CHTMLWrapper<N, T, D>,
  83. CHTMLWrapperClass,
  84. CHTMLCharOptions,
  85. CHTMLDelimiterData,
  86. CHTMLFontData
  87. > {
  88. /**
  89. * The wrapper type
  90. */
  91. public static kind: string = 'unknown';
  92. /**
  93. * If true, this causes a style for the node type to be generated automatically
  94. * that sets display:inline-block (as needed for the output for MmlNodes).
  95. */
  96. public static autoStyle = true;
  97. /**
  98. * @override
  99. */
  100. protected factory: CHTMLWrapperFactory<N, T, D>;
  101. /**
  102. * @override
  103. */
  104. public parent: CHTMLWrapper<N, T, D>;
  105. /**
  106. * @override
  107. */
  108. public childNodes: CHTMLWrapper<N, T, D>[];
  109. /**
  110. * The HTML element generated for this wrapped node
  111. */
  112. public chtml: N = null;
  113. /*******************************************************************/
  114. /**
  115. * Create the HTML for the wrapped node.
  116. *
  117. * @param {N} parent The HTML node where the output is added
  118. */
  119. public toCHTML(parent: N) {
  120. const chtml = this.standardCHTMLnode(parent);
  121. for (const child of this.childNodes) {
  122. child.toCHTML(chtml);
  123. }
  124. }
  125. /*******************************************************************/
  126. /**
  127. * Create the standard CHTML element for the given wrapped node.
  128. *
  129. * @param {N} parent The HTML element in which the node is to be created
  130. * @returns {N} The root of the HTML tree for the wrapped node's output
  131. */
  132. protected standardCHTMLnode(parent: N): N {
  133. this.markUsed();
  134. const chtml = this.createCHTMLnode(parent);
  135. this.handleStyles();
  136. this.handleVariant();
  137. this.handleScale();
  138. this.handleColor();
  139. this.handleSpace();
  140. this.handleAttributes();
  141. this.handlePWidth();
  142. return chtml;
  143. }
  144. /**
  145. * Mark this class as having been typeset (so its styles will be output)
  146. */
  147. public markUsed() {
  148. this.jax.wrapperUsage.add(this.kind);
  149. }
  150. /**
  151. * @param {N} parent The HTML element in which the node is to be created
  152. * @returns {N} The root of the HTML tree for the wrapped node's output
  153. */
  154. protected createCHTMLnode(parent: N): N {
  155. const href = this.node.attributes.get('href');
  156. if (href) {
  157. parent = this.adaptor.append(parent, this.html('a', {href: href})) as N;
  158. }
  159. this.chtml = this.adaptor.append(parent, this.html('mjx-' + this.node.kind)) as N;
  160. return this.chtml;
  161. }
  162. /**
  163. * Set the CSS styles for the chtml element
  164. */
  165. protected handleStyles() {
  166. if (!this.styles) return;
  167. const styles = this.styles.cssText;
  168. if (styles) {
  169. this.adaptor.setAttribute(this.chtml, 'style', styles);
  170. const family = this.styles.get('font-family');
  171. if (family) {
  172. this.adaptor.setStyle(this.chtml, 'font-family', 'MJXZERO, ' + family);
  173. }
  174. }
  175. }
  176. /**
  177. * Set the CSS for the math variant
  178. */
  179. protected handleVariant() {
  180. if (this.node.isToken && this.variant !== '-explicitFont') {
  181. this.adaptor.setAttribute(this.chtml, 'class',
  182. (this.font.getVariant(this.variant) || this.font.getVariant('normal')).classes);
  183. }
  184. }
  185. /**
  186. * Set the (relative) scaling factor for the node
  187. */
  188. protected handleScale() {
  189. this.setScale(this.chtml, this.bbox.rscale);
  190. }
  191. /**
  192. * @param {N} chtml The HTML node to scale
  193. * @param {number} rscale The relatie scale to apply
  194. * @return {N} The HTML node (for chaining)
  195. */
  196. protected setScale(chtml: N, rscale: number): N {
  197. const scale = (Math.abs(rscale - 1) < .001 ? 1 : rscale);
  198. if (chtml && scale !== 1) {
  199. const size = this.percent(scale);
  200. if (FONTSIZE[size]) {
  201. this.adaptor.setAttribute(chtml, 'size', FONTSIZE[size]);
  202. } else {
  203. this.adaptor.setStyle(chtml, 'fontSize', size);
  204. }
  205. }
  206. return chtml;
  207. }
  208. /**
  209. * Add the proper spacing
  210. */
  211. protected handleSpace() {
  212. for (const data of [[this.bbox.L, 'space', 'marginLeft'],
  213. [this.bbox.R, 'rspace', 'marginRight']]) {
  214. const [dimen, name, margin] = data as [number, string, string];
  215. if (dimen) {
  216. const space = this.em(dimen);
  217. if (SPACE[space]) {
  218. this.adaptor.setAttribute(this.chtml, name, SPACE[space]);
  219. } else {
  220. this.adaptor.setStyle(this.chtml, margin, space);
  221. }
  222. }
  223. }
  224. }
  225. /**
  226. * Add the foreground and background colors
  227. * (Only look at explicit attributes, since inherited ones will
  228. * be applied to a parent element, and we will inherit from that)
  229. */
  230. protected handleColor() {
  231. const attributes = this.node.attributes;
  232. const mathcolor = attributes.getExplicit('mathcolor') as string;
  233. const color = attributes.getExplicit('color') as string;
  234. const mathbackground = attributes.getExplicit('mathbackground') as string;
  235. const background = attributes.getExplicit('background') as string;
  236. if (mathcolor || color) {
  237. this.adaptor.setStyle(this.chtml, 'color', mathcolor || color);
  238. }
  239. if (mathbackground || background) {
  240. this.adaptor.setStyle(this.chtml, 'backgroundColor', mathbackground || background);
  241. }
  242. }
  243. /**
  244. * Copy RDFa, aria, and other tags from the MathML to the CHTML output nodes.
  245. * Don't copy those in the skipAttributes list, or anything that already exists
  246. * as a property of the node (e.g., no "onlick", etc.). If a name in the
  247. * skipAttributes object is set to false, then the attribute WILL be copied.
  248. * Add the class to any other classes already in use.
  249. */
  250. protected handleAttributes() {
  251. const attributes = this.node.attributes;
  252. const defaults = attributes.getAllDefaults();
  253. const skip = CHTMLWrapper.skipAttributes;
  254. for (const name of attributes.getExplicitNames()) {
  255. if (skip[name] === false || (!(name in defaults) && !skip[name] &&
  256. !this.adaptor.hasAttribute(this.chtml, name))) {
  257. this.adaptor.setAttribute(this.chtml, name, attributes.getExplicit(name) as string);
  258. }
  259. }
  260. if (attributes.get('class')) {
  261. const names = (attributes.get('class') as string).trim().split(/ +/);
  262. for (const name of names) {
  263. this.adaptor.addClass(this.chtml, name);
  264. }
  265. }
  266. }
  267. /**
  268. * Handle the attributes needed for percentage widths
  269. */
  270. protected handlePWidth() {
  271. if (this.bbox.pwidth) {
  272. if (this.bbox.pwidth === BBox.fullWidth) {
  273. this.adaptor.setAttribute(this.chtml, 'width', 'full');
  274. } else {
  275. this.adaptor.setStyle(this.chtml, 'width', this.bbox.pwidth);
  276. }
  277. }
  278. }
  279. /*******************************************************************/
  280. /**
  281. * @param {N} chtml The HTML node whose indentation is to be adjusted
  282. * @param {string} align The alignment for the node
  283. * @param {number} shift The indent (positive or negative) for the node
  284. */
  285. protected setIndent(chtml: N, align: string, shift: number) {
  286. const adaptor = this.adaptor;
  287. if (align === 'center' || align === 'left') {
  288. const L = this.getBBox().L;
  289. adaptor.setStyle(chtml, 'margin-left', this.em(shift + L));
  290. }
  291. if (align === 'center' || align === 'right') {
  292. const R = this.getBBox().R;
  293. adaptor.setStyle(chtml, 'margin-right', this.em(-shift + R));
  294. }
  295. }
  296. /*******************************************************************/
  297. /**
  298. * For debugging
  299. */
  300. public drawBBox() {
  301. let {w, h, d, R} = this.getBBox();
  302. const box = this.html('mjx-box', {style: {
  303. opacity: .25, 'margin-left': this.em(-w - R)
  304. }}, [
  305. this.html('mjx-box', {style: {
  306. height: this.em(h),
  307. width: this.em(w),
  308. 'background-color': 'red'
  309. }}),
  310. this.html('mjx-box', {style: {
  311. height: this.em(d),
  312. width: this.em(w),
  313. 'margin-left': this.em(-w),
  314. 'vertical-align': this.em(-d),
  315. 'background-color': 'green'
  316. }})
  317. ] as N[]);
  318. const node = this.chtml || this.parent.chtml;
  319. const size = this.adaptor.getAttribute(node, 'size');
  320. if (size) {
  321. this.adaptor.setAttribute(box, 'size', size);
  322. }
  323. const fontsize = this.adaptor.getStyle(node, 'fontSize');
  324. if (fontsize) {
  325. this.adaptor.setStyle(box, 'fontSize', fontsize);
  326. }
  327. this.adaptor.append(this.adaptor.parent(node), box);
  328. this.adaptor.setStyle(node, 'backgroundColor', '#FFEE00');
  329. }
  330. /*******************************************************************/
  331. /*
  332. * Easy access to some utility routines
  333. */
  334. /**
  335. * @param {string} type The tag name of the HTML node to be created
  336. * @param {OptionList} def The properties to set for the created node
  337. * @param {(N|T)[]} content The child nodes for the created HTML node
  338. * @return {N} The generated HTML tree
  339. */
  340. public html(type: string, def: OptionList = {}, content: (N | T)[] = []): N {
  341. return this.jax.html(type, def, content);
  342. }
  343. /**
  344. * @param {string} text The text from which to create an HTML text node
  345. * @return {T} The generated text node with the given text
  346. */
  347. public text(text: string): T {
  348. return this.jax.text(text);
  349. }
  350. /**
  351. * @param {number} n A unicode code point to be converted to a character className reference.
  352. * @return {string} The className for the character
  353. */
  354. protected char(n: number): string {
  355. return this.font.charSelector(n).substr(1);
  356. }
  357. }