FontData.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  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 CHTMLFontData class and AddCSS() function.
  19. *
  20. * @author dpvc@mathjax.org (Davide Cervone)
  21. */
  22. import {CharMap, CharOptions, CharData, VariantData, DelimiterData, FontData, DIRECTION} from '../common/FontData.js';
  23. import {Usage} from './Usage.js';
  24. import {StringMap} from './Wrapper.js';
  25. import {StyleList, StyleData} from '../../util/StyleList.js';
  26. import {em} from '../../util/lengths.js';
  27. export * from '../common/FontData.js';
  28. /****************************************************************************/
  29. /**
  30. * Add the extra data needed for CharOptions in CHTML
  31. */
  32. export interface CHTMLCharOptions extends CharOptions {
  33. c?: string; // the content value (for css)
  34. f?: string; // the font postfix (for css)
  35. }
  36. /**
  37. * Shorthands for CHTML char maps and char data
  38. */
  39. export type CHTMLCharMap = CharMap<CHTMLCharOptions>;
  40. export type CHTMLCharData = CharData<CHTMLCharOptions>;
  41. /**
  42. * The extra data needed for a Variant in CHTML output
  43. */
  44. export interface CHTMLVariantData extends VariantData<CHTMLCharOptions> {
  45. classes?: string; // the classes to use for this variant
  46. letter: string; // the font letter(s) for the default font for this variant
  47. }
  48. /**
  49. * The extra data needed for a Delimiter in CHTML output
  50. */
  51. export interface CHTMLDelimiterData extends DelimiterData {
  52. }
  53. /****************************************************************************/
  54. /**
  55. * The CHTML FontData class
  56. */
  57. export class CHTMLFontData extends FontData<CHTMLCharOptions, CHTMLVariantData, CHTMLDelimiterData> {
  58. /**
  59. * Default options
  60. */
  61. public static OPTIONS = {
  62. ...FontData.OPTIONS,
  63. fontURL: 'js/output/chtml/fonts/tex-woff-v2'
  64. };
  65. /**
  66. * @override
  67. */
  68. public static JAX = 'CHTML';
  69. /**
  70. * The default class names to use for each variant
  71. */
  72. protected static defaultVariantClasses: StringMap = {};
  73. /**
  74. * The default font letter to use for each variant
  75. */
  76. protected static defaultVariantLetters: StringMap = {};
  77. /**
  78. * The CSS styles needed for this font.
  79. */
  80. protected static defaultStyles = {
  81. 'mjx-c::before': {
  82. display: 'block',
  83. width: 0
  84. }
  85. };
  86. /**
  87. * The default @font-face declarations with %%URL%% where the font path should go
  88. */
  89. protected static defaultFonts = {
  90. '@font-face /* 0 */': {
  91. 'font-family': 'MJXZERO',
  92. src: 'url("%%URL%%/MathJax_Zero.woff") format("woff")'
  93. }
  94. };
  95. /***********************************************************************/
  96. /**
  97. * Data about the characters used (for adaptive CSS)
  98. */
  99. public charUsage: Usage<[string, number]> = new Usage<[string, number]>();
  100. /**
  101. * Data about the delimiters used (for adpative CSS)
  102. */
  103. public delimUsage: Usage<number> = new Usage<number>();
  104. /***********************************************************************/
  105. /**
  106. * @override
  107. */
  108. public static charOptions(font: CHTMLCharMap, n: number) {
  109. return super.charOptions(font, n) as CHTMLCharOptions;
  110. }
  111. /***********************************************************************/
  112. /**
  113. * @param {boolean} adapt Whether to use adaptive CSS or not
  114. */
  115. public adaptiveCSS(adapt: boolean) {
  116. this.options.adaptiveCSS = adapt;
  117. }
  118. /**
  119. * Clear the cache of which characters have been used
  120. */
  121. public clearCache() {
  122. if (this.options.adaptiveCSS) {
  123. this.charUsage.clear();
  124. this.delimUsage.clear();
  125. }
  126. }
  127. /**
  128. * @override
  129. */
  130. public createVariant(name: string, inherit: string = null, link: string = null) {
  131. super.createVariant(name, inherit, link);
  132. let CLASS = (this.constructor as CHTMLFontDataClass);
  133. this.variant[name].classes = CLASS.defaultVariantClasses[name];
  134. this.variant[name].letter = CLASS.defaultVariantLetters[name];
  135. }
  136. /**
  137. * @override
  138. */
  139. public defineChars(name: string, chars: CHTMLCharMap) {
  140. super.defineChars(name, chars);
  141. const letter = this.variant[name].letter;
  142. for (const n of Object.keys(chars)) {
  143. const options = CHTMLFontData.charOptions(chars, parseInt(n));
  144. if (options.f === undefined) {
  145. options.f = letter;
  146. }
  147. }
  148. }
  149. /***********************************************************************/
  150. /**
  151. * @return {StyleList} The (computed) styles for this font
  152. */
  153. get styles(): StyleList {
  154. const CLASS = this.constructor as typeof CHTMLFontData;
  155. //
  156. // Include the default styles
  157. //
  158. const styles: StyleList = {...CLASS.defaultStyles};
  159. //
  160. // Add fonts with proper URL
  161. //
  162. this.addFontURLs(styles, CLASS.defaultFonts, this.options.fontURL);
  163. //
  164. // Add the styles for delimiters and characters
  165. //
  166. if (this.options.adaptiveCSS) {
  167. this.updateStyles(styles);
  168. } else {
  169. this.allStyles(styles);
  170. }
  171. //
  172. // Return the final style sheet
  173. //
  174. return styles;
  175. }
  176. /**
  177. * Get the styles for any newly used characters and delimiters
  178. *
  179. * @param {StyleList} styles The style list to add delimiter styles to.
  180. * @return {StyleList} The modified style list.
  181. */
  182. public updateStyles(styles: StyleList): StyleList {
  183. for (const N of this.delimUsage.update()) {
  184. this.addDelimiterStyles(styles, N, this.delimiters[N]);
  185. }
  186. for (const [name, N] of this.charUsage.update()) {
  187. const variant = this.variant[name];
  188. this.addCharStyles(styles, variant.letter, N, variant.chars[N]);
  189. }
  190. return styles;
  191. }
  192. /**
  193. * @param {StyleList} styles The style list to add characters to
  194. */
  195. protected allStyles(styles: StyleList) {
  196. //
  197. // Create styles needed for the delimiters
  198. //
  199. for (const n of Object.keys(this.delimiters)) {
  200. const N = parseInt(n);
  201. this.addDelimiterStyles(styles, N, this.delimiters[N]);
  202. }
  203. //
  204. // Add style for all character data
  205. //
  206. for (const name of Object.keys(this.variant)) {
  207. const variant = this.variant[name];
  208. const vletter = variant.letter;
  209. for (const n of Object.keys(variant.chars)) {
  210. const N = parseInt(n);
  211. const char = variant.chars[N];
  212. if ((char[3] || {}).smp) continue;
  213. if (char.length < 4) {
  214. (char as CHTMLCharData)[3] = {};
  215. }
  216. this.addCharStyles(styles, vletter, N, char);
  217. }
  218. }
  219. }
  220. /**
  221. * @param {StyleList} styles The style object to add styles to
  222. * @param {StyleList} fonts The default font-face directives with %%URL%% where the url should go
  223. * @param {string} url The actual URL to insert into the src strings
  224. */
  225. protected addFontURLs(styles: StyleList, fonts: StyleList, url: string) {
  226. for (const name of Object.keys(fonts)) {
  227. const font = {...fonts[name]};
  228. font.src = (font.src as string).replace(/%%URL%%/, url);
  229. styles[name] = font;
  230. }
  231. }
  232. /*******************************************************/
  233. /**
  234. * @param {StyleList} styles The style object to add styles to
  235. * @param {number} n The unicode character number of the delimiter
  236. * @param {CHTMLDelimiterData} data The data for the delimiter whose CSS is to be added
  237. */
  238. protected addDelimiterStyles(styles: StyleList, n: number, data: CHTMLDelimiterData) {
  239. let c = this.charSelector(n);
  240. if (data.c && data.c !== n) {
  241. c = this.charSelector(data.c);
  242. styles['.mjx-stretched mjx-c' + c + '::before'] = {
  243. content: this.charContent(data.c)
  244. };
  245. }
  246. if (!data.stretch) return;
  247. if (data.dir === DIRECTION.Vertical) {
  248. this.addDelimiterVStyles(styles, c, data);
  249. } else {
  250. this.addDelimiterHStyles(styles, c, data);
  251. }
  252. }
  253. /*******************************************************/
  254. /**
  255. * @param {StyleList} styles The style object to add styles to
  256. * @param {string} c The delimiter character string
  257. * @param {CHTMLDelimiterData} data The data for the delimiter whose CSS is to be added
  258. */
  259. protected addDelimiterVStyles(styles: StyleList, c: string, data: CHTMLDelimiterData) {
  260. const HDW = data.HDW as CHTMLCharData;
  261. const [beg, ext, end, mid] = data.stretch;
  262. const Hb = this.addDelimiterVPart(styles, c, 'beg', beg, HDW);
  263. this.addDelimiterVPart(styles, c, 'ext', ext, HDW);
  264. const He = this.addDelimiterVPart(styles, c, 'end', end, HDW);
  265. const css: StyleData = {};
  266. if (mid) {
  267. const Hm = this.addDelimiterVPart(styles, c, 'mid', mid, HDW);
  268. css.height = '50%';
  269. styles['mjx-stretchy-v' + c + ' > mjx-mid'] = {
  270. 'margin-top': this.em(-Hm / 2),
  271. 'margin-bottom': this.em(-Hm / 2)
  272. };
  273. }
  274. if (Hb) {
  275. css['border-top-width'] = this.em0(Hb - .03);
  276. }
  277. if (He) {
  278. css['border-bottom-width'] = this.em0(He - .03);
  279. styles['mjx-stretchy-v' + c + ' > mjx-end'] = {'margin-top': this.em(-He)};
  280. }
  281. if (Object.keys(css).length) {
  282. styles['mjx-stretchy-v' + c + ' > mjx-ext'] = css;
  283. }
  284. }
  285. /**
  286. * @param {StyleList} styles The style object to add styles to
  287. * @param {string} c The vertical character whose part is being added
  288. * @param {string} part The name of the part (beg, ext, end, mid) that is being added
  289. * @param {number} n The unicode character to use for the part
  290. * @param {number} HDW The height-depth-width data for the stretchy delimiter
  291. * @return {number} The total height of the character
  292. */
  293. protected addDelimiterVPart(styles: StyleList, c: string, part: string, n: number, HDW: CHTMLCharData): number {
  294. if (!n) return 0;
  295. const data = this.getDelimiterData(n);
  296. const dw = (HDW[2] - data[2]) / 2;
  297. const css: StyleData = {content: this.charContent(n)};
  298. if (part !== 'ext') {
  299. css.padding = this.padding(data, dw);
  300. } else {
  301. css.width = this.em0(HDW[2]);
  302. if (dw) {
  303. css['padding-left'] = this.em0(dw);
  304. }
  305. }
  306. styles['mjx-stretchy-v' + c + ' mjx-' + part + ' mjx-c::before'] = css;
  307. return data[0] + data[1];
  308. }
  309. /*******************************************************/
  310. /**
  311. * @param {StyleList} styles The style object to add styles to
  312. * @param {string} c The delimiter character string
  313. * @param {CHTMLDelimiterData} data The data for the delimiter whose CSS is to be added
  314. */
  315. protected addDelimiterHStyles(styles: StyleList, c: string, data: CHTMLDelimiterData) {
  316. const [beg, ext, end, mid] = data.stretch;
  317. const HDW = data.HDW as CHTMLCharData;
  318. this.addDelimiterHPart(styles, c, 'beg', beg, HDW);
  319. this.addDelimiterHPart(styles, c, 'ext', ext, HDW);
  320. this.addDelimiterHPart(styles, c, 'end', end, HDW);
  321. if (mid) {
  322. this.addDelimiterHPart(styles, c, 'mid', mid, HDW);
  323. styles['mjx-stretchy-h' + c + ' > mjx-ext'] = {width: '50%'};
  324. }
  325. }
  326. /**
  327. * @param {StyleList} styles The style object to add styles to
  328. * @param {string} c The vertical character whose part is being added
  329. * @param {string} part The name of the part (beg, ext, end, mid) that is being added
  330. * @param {number} n The unicode character to use for the part
  331. * @param {CHTMLCharData} HDW The height-depth-width data for the stretchy character
  332. */
  333. protected addDelimiterHPart(styles: StyleList, c: string, part: string, n: number, HDW: CHTMLCharData) {
  334. if (!n) return;
  335. const data = this.getDelimiterData(n);
  336. const options = data[3] as CHTMLCharOptions;
  337. const css: StyleData = {content: (options && options.c ? '"' + options.c + '"' : this.charContent(n))};
  338. css.padding = this.padding(HDW as CHTMLCharData, 0, -HDW[2]);
  339. styles['mjx-stretchy-h' + c + ' mjx-' + part + ' mjx-c::before'] = css;
  340. }
  341. /*******************************************************/
  342. /**
  343. * @param {StyleList} styles The style object to add styles to
  344. * @param {string} vletter The variant class letter (e.g., `B`, `SS`) where this character is being defined
  345. * @param {number} n The unicode character being defined
  346. * @param {CHTMLCharData} data The bounding box data and options for the character
  347. */
  348. protected addCharStyles(styles: StyleList, vletter: string, n: number, data: CHTMLCharData) {
  349. const options = data[3] as CHTMLCharOptions;
  350. const letter = (options.f !== undefined ? options.f : vletter);
  351. const selector = 'mjx-c' + this.charSelector(n) + (letter ? '.TEX-' + letter : '');
  352. styles[selector + '::before'] = {
  353. padding: this.padding(data, 0, options.ic || 0),
  354. content: (options.c != null ? '"' + options.c + '"' : this.charContent(n))
  355. };
  356. }
  357. /***********************************************************************/
  358. /**
  359. * @param {number} n The character number to find
  360. * @return {CHTMLCharData} The data for that character to be used for stretchy delimiters
  361. */
  362. protected getDelimiterData(n: number): CHTMLCharData {
  363. return this.getChar('-smallop', n);
  364. }
  365. /**
  366. * @param {number} n The number of ems
  367. * @return {string} The string representing the number with units of "em"
  368. */
  369. public em(n: number): string {
  370. return em(n);
  371. }
  372. /**
  373. * @param {number} n The number of ems (will be restricted to non-negative values)
  374. * @return {string} The string representing the number with units of "em"
  375. */
  376. public em0(n: number): string {
  377. return em(Math.max(0, n));
  378. }
  379. /**
  380. * @param {CHTMLCharData} data The [h, d, w] data for the character
  381. * @param {number} dw The (optional) left offset of the glyph
  382. * @param {number} ic The (optional) italic correction value
  383. * @return {string} The padding string for the h, d, w.
  384. */
  385. public padding([h, d, w]: CHTMLCharData, dw: number = 0, ic: number = 0): string {
  386. return [h, w + ic, d, dw].map(this.em0).join(' ');
  387. }
  388. /**
  389. * @param {number} n A unicode code point to be converted to character content for use with the
  390. * CSS rules for fonts (either a literal character for most ASCII values, or \nnnn
  391. * for higher values, or for the double quote and backslash characters).
  392. * @return {string} The character as a properly encoded string in quotes.
  393. */
  394. public charContent(n: number): string {
  395. return '"' + (n >= 0x20 && n <= 0x7E && n !== 0x22 && n !== 0x27 && n !== 0x5C ?
  396. String.fromCharCode(n) : '\\' + n.toString(16).toUpperCase()) + '"';
  397. }
  398. /**
  399. * @param {number} n A unicode code point to be converted to a selector for use with the
  400. * CSS rules for fonts
  401. * @return {string} The character as a selector value.
  402. */
  403. public charSelector(n: number): string {
  404. return '.mjx-c' + n.toString(16).toUpperCase();
  405. }
  406. }
  407. /**
  408. * The CHTMLFontData constructor class
  409. */
  410. export type CHTMLFontDataClass = typeof CHTMLFontData;
  411. /****************************************************************************/
  412. /**
  413. * Data needed for AddCSS()
  414. */
  415. export type CharOptionsMap = {[name: number]: CHTMLCharOptions};
  416. export type CssMap = {[name: number]: number};
  417. /**
  418. * @param {CHTMLCharMap} font The font to augment
  419. * @param {CharOptionsMap} options Any additional options for characters in the font
  420. * @return {CHTMLCharMap} The augmented font
  421. */
  422. export function AddCSS(font: CHTMLCharMap, options: CharOptionsMap): CHTMLCharMap {
  423. for (const c of Object.keys(options)) {
  424. const n = parseInt(c);
  425. Object.assign(FontData.charOptions(font, n), options[n]);
  426. }
  427. return font;
  428. }