/************************************************************* * * Copyright (c) 2017 The MathJax Consortium * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Implements the CHTMLFontData class and AddCSS() function. * * @author dpvc@mathjax.org (Davide Cervone) */ import {CharMap, CharOptions, CharData, VariantData, DelimiterData, FontData, DIRECTION} from '../common/FontData.js'; import {StringMap} from './Wrapper.js'; import {StyleList, StyleData} from '../common/CssStyles.js'; import {em} from '../../util/lengths.js'; import {OptionList, defaultOptions, userOptions} from '../../util/Options.js'; export * from '../common/FontData.js'; /****************************************************************************/ /** * Add the extra data needed for CharOptions in CHTML */ export interface CHTMLCharOptions extends CharOptions { c?: string; // the content value (for css) f?: string; // the font postfix (for css) used?: boolean; // true when the character has been used on the page } /** * Shorthands for CHTML char maps and char data */ export type CHTMLCharMap = CharMap; export type CHTMLCharData = CharData; /** * temporary record of which characters are used */ export type UsedMap = Map; /** * The extra data needed for a Variant in CHTML output */ export interface CHTMLVariantData extends VariantData { classes?: string; // the classes to use for this variant }; /** * The extra data needed for a Delimiter in CHTML output */ export interface CHTMLDelimiterData extends DelimiterData { used?: boolean; // true when this delimiter has been used on the page }; /****************************************************************************/ /** * The CHTML FontData class */ export class CHTMLFontData extends FontData { /** * Default options */ public static OPTIONS = { fontURL: 'mathjax3-ts/output/chtml/fonts/tex-woff-v2' }; /** * The default class names to use for each variant */ protected static defaultVariantClasses: StringMap = {}; /** * The CSS styles needed for this font. */ protected static defaultStyles = { 'mjx-c::before': { display: 'inline-block', width: 0 } }; /** * The default @font-face declarations with %%URL%% where the font path should go */ protected static defaultFonts = { '@font-face /* 0 */': { 'font-family': 'MJXZERO', src: 'url("%%URL%%/MathJax_Zero.woff") format("woff")' } }; /** * The font options */ protected options: OptionList; /** * The root class for this font (e.g., '.MJX-TEX ') including the following space */ protected cssRoot: string = ''; /***********************************************************************/ /** * @param {OptionList} options The options for this font * * @override * @constructor */ constructor(options: OptionList = null) { super(); let CLASS = (this.constructor as CHTMLFontDataClass); this.options = userOptions(defaultOptions({}, CLASS.OPTIONS), options); for (const name of Object.keys(CLASS.defaultVariantClasses)) { this.variant[name].classes = CLASS.defaultVariantClasses[name]; } } /** * @param {boolean} adapt Whether to use adaptive CSS or not */ public adaptiveCSS(adapt: boolean) { this.options.adaptiveCSS = adapt; } /** * Clear the cache of which characters have been used */ public clearCache() { if (!this.options.adaptiveCSS) return; // // Clear delimiter usage // for (const n of Object.keys(this.delimiters)) { this.delimiters[parseInt(n)].used = false; } // // Clear the character usage // for (const name of Object.keys(this.variant)) { const chars = this.variant[name].chars; for (const n of Object.keys(chars)) { const options = chars[parseInt(n)][3] as CHTMLCharOptions; if (options) { options.used = false; } } } } /***********************************************************************/ /** * @return {StyleList} The (computed) styles for this font */ get styles() { const CLASS = this.constructor as typeof CHTMLFontData; // // Include the default styles // let styles: StyleList = {...CLASS.defaultStyles}; // // Add fonts with proper URL // this.addFontURLs(styles, CLASS.defaultFonts, this.options.fontURL); // // Create styles needed for the delimiters // for (const n of Object.keys(this.delimiters)) { const N = parseInt(n); this.addDelimiterStyles(styles, N, this.delimiters[N]); } // // Create styles needed for the characters in each variant // this.addVariantChars(styles); // // Return the final style sheet // return styles; } /** * @param {StyleList} styles The style list to add characters to */ protected addVariantChars(styles: StyleList) { const charUsed: UsedMap = new Map(); for (const name of Object.keys(this.variant)) { const variant = this.variant[name]; const vclass = (name === 'normal' ? '' : '.' + variant.classes.replace(/ /g, '.')); for (const n of Object.keys(variant.chars)) { const N = parseInt(n); if (variant.chars[N].length === 4) { this.addCharStyles(styles, vclass, N, variant.chars[N], charUsed); } } } } /** * @param {StyleList} styles The style object to add styles to * @param {StyleList} fonts The default font-face directives with %%URL%% where the url should go * @param {string} url The actual URL to insert into the src strings */ protected addFontURLs(styles: StyleList, fonts: StyleList, url: string) { for (const name of Object.keys(fonts)) { const font = {...fonts[name]}; font.src = (font.src as string).replace(/%%URL%%/, url); styles[name] = font; } } /*******************************************************/ /** * @param {StyleList} styles The style object to add styles to * @param {number} n The unicode character number of the delimiter * @param {CHTMLDelimiterData} data The data for the delimiter whose CSS is to be added */ protected addDelimiterStyles(styles: StyleList, n: number, data: CHTMLDelimiterData) { if (this.options.adaptiveCSS && !data.used) return; const c = this.charSelector(n); if (data.c && data.c !== n) { styles[this.cssRoot + '.mjx-stretched mjx-c' + c + '::before'] = { content: this.charContent(data.c) }; } if (!data.stretch) return; if (data.dir === DIRECTION.Vertical) { this.addDelimiterVStyles(styles, c, data); } else { this.addDelimiterHStyles(styles, c, data); } } /*******************************************************/ /** * @param {StyleList} styles The style object to add styles to * @param {string} c The delimiter character string * @param {CHTMLDelimiterData} data The data for the delimiter whose CSS is to be added */ protected addDelimiterVStyles(styles: StyleList, c: string, data: CHTMLDelimiterData) { const W = data.HDW[2]; const [beg, ext, end, mid] = data.stretch; const Hb = this.addDelimiterVPart(styles, c, W, 'beg', beg); this.addDelimiterVPart(styles, c, W, 'ext', ext); const He = this.addDelimiterVPart(styles, c, W, 'end', end); const css: StyleData = {}; const root = this.cssRoot; if (mid) { const Hm = this.addDelimiterVPart(styles, c, W, 'mid', mid); css.height = '50%'; styles[root + 'mjx-stretchy-v' + c + ' > mjx-mid'] = { 'margin-top': this.em(-Hm/2), 'margin-bottom': this.em(-Hm/2) }; } if (Hb) { css['border-top-width'] = this.em0(Hb - .03); } if (He) { css['border-bottom-width'] = this.em0(He - .03); styles[root + 'mjx-stretchy-v' + c + ' > mjx-end'] = {'margin-top': this.em(-He)}; } if (Object.keys(css).length) { styles[root + 'mjx-stretchy-v' + c + ' > mjx-ext'] = css; } } /** * @param {StyleList} styles The style object to add styles to * @param {string} c The vertical character whose part is being added * @param {number} W The width for the stretchy delimiter as a whole * @param {string} part The name of the part (beg, ext, end, mid) that is being added * @param {number} n The unicode character to use for the part * @return {number} The total height of the character */ protected addDelimiterVPart(styles: StyleList, c: string, W: number, part: string, n: number) { if (!n) return 0; const data = this.getDelimiterData(n); const dw = (W - data[2]) / 2 const css: StyleData = {content: this.charContent(n)}; if (part !== 'ext') { css.padding = this.padding(data, dw); } else if (dw) { css['padding-left'] = this.em0(dw); } styles[this.cssRoot + 'mjx-stretchy-v' + c + ' mjx-' + part + ' mjx-c::before'] = css; return data[0] + data[1]; } /*******************************************************/ /** * @param {StyleList} styles The style object to add styles to * @param {string} c The delimiter character string * @param {CHTMLDelimiterData} data The data for the delimiter whose CSS is to be added */ protected addDelimiterHStyles(styles: StyleList, c: string, data: CHTMLDelimiterData) { const [beg, ext, end, mid] = data.stretch; this.addDelimiterHPart(styles, c, 'beg', beg); this.addDelimiterHPart(styles, c, 'ext', ext, !(beg || end)); this.addDelimiterHPart(styles, c, 'end', end); if (mid) { this.addDelimiterHPart(styles, c, 'mid', mid); styles[this.cssRoot + 'mjx-stretchy-h' + c + ' > mjx-ext'] = {width: '50%'}; } } /** * @param {StyleList} styles The style object to add styles to * @param {string} c The vertical character whose part is being added * @param {string} part The name of the part (beg, ext, end, mid) that is being added * @param {number} n The unicode character to use for the part */ protected addDelimiterHPart(styles: StyleList, c: string, part: string, n: number, force: boolean = false) { if (!n) { return 0; } const data = this.getDelimiterData(n); const options = data[3] as CHTMLCharOptions; const css: StyleData = {content: (options && options.c ? '"' + options.c + '"' : this.charContent(n))}; if (part !== 'ext' || force) { css.padding = this.padding(data, 0, -data[2]); } styles[this.cssRoot + 'mjx-stretchy-h' + c + ' mjx-' + part + ' mjx-c::before'] = css; } /*******************************************************/ /** * @param {StyleList} styles The style object to add styles to * @param {string} vclass The variant class string (e.g., .mjx-b) where this character is being defined * @param {number} n The unicode character being defined * @param {CharData} data The bounding box data and options for the character * @param {UsedMap} charUsed The map of characters whose content has been included in the styles */ protected addCharStyles(styles: StyleList, vclass: string, n: number, data: CHTMLCharData, charUsed: UsedMap) { const [h, d, w, options] = data as [number, number, number, CHTMLCharOptions]; if (this.options.adaptiveCSS && !options.used) return; const css: StyleData = {}; const selector = 'mjx-c' + this.charSelector(n); const root = this.cssRoot; css.padding = this.padding(data, 0, options.ic || 0); const content = (options.c ? '"' + options.c + '"' : this.charContent(n)); if (charUsed.get(n) !== content) { if (!charUsed.has(n) && !options.c) { styles[root + selector + '::before'] = {content}; charUsed.set(n, content); } else { styles[root + vclass + ' ' + selector + '::before'] = {content}; } } if (options.f !== undefined) { css['font-family'] = 'MJXZERO, MJXTEX' + (options.f ? '-' + options.f : ''); } const char = (vclass ? vclass + ' ': '') + selector; styles[root + char] = css; if (options.ic) { const [MJX, noIC] = [root + 'mjx-', '[noIC]' + char + ':last-child']; styles[MJX + 'mi' + noIC] = styles[MJX + 'mo' + noIC] = { 'padding-right': this.em(w) }; } } /***********************************************************************/ /** * @param {number} n The character number to find * @return {CharData} The data for that character to be used for stretchy delimiters */ protected getDelimiterData(n: number) { return this.getChar('-smallop', n); } /** * @override */ public static charOptions(font: CHTMLCharMap, n: number) { return super.charOptions(font, n) as CHTMLCharOptions; } /** * @param {number} n The number of ems * @return {string} The string representing the number with units of "em" */ public em(n: number) { return em(n); } /** * @param {number} n The number of ems (will be restricted to non-negative values) * @return {string} The string representing the number with units of "em" */ public em0(n: number) { return em(Math.max(0, n)); } /** * @param {CHTMLCharData} data The [h, d, w] data for the character * @param {number} dw The (optional) left offset of the glyph * @param {number} ic The (optional) italic correction value * @return {string} The padding string for the h, d, w. */ public padding([h, d, w]: CHTMLCharData, dw: number = 0, ic: number = 0) { return [h, w + ic, d, dw].map(this.em0).join(' '); } /** * @param {number} n A unicode code point to be converted to character content for use with the * CSS rules for fonts (either a literal character for most ASCII values, or \nnnn * for higher values, or for the double quote and backslash characters). * @return {string} The character as a properly encoded string in quotes. */ public charContent(n: number) { return '"' + (n >= 0x20 && n <= 0x7E && n !== 0x22 && n !== 0x27 && n !== 0x5C ? String.fromCharCode(n) : '\\' + n.toString(16).toUpperCase()) + '"'; } /** * @param {number} n A unicode code point to be converted to a selector for use with the * CSS rules for fonts * @return {string} The character as a selector value. */ public charSelector(n: number) { return '.mjx-c' + n.toString(16).toUpperCase(); } } /** * The CHTMLFontData constructor class */ export type CHTMLFontDataClass = typeof CHTMLFontData; /****************************************************************************/ /** * Data needed for AddCSS() */ export type CharOptionsMap = {[name: number]: CHTMLCharOptions}; export type CssMap = {[name: number]: number}; /** * @param {CHTMLCharMap} font The font to augment * @param {CharOptionsMap} options Any additional options for characters in the font * @return {CharMap} The augmented font */ export function AddCSS(font: CHTMLCharMap, options: CharOptionsMap) { for (const c of Object.keys(options)) { const n = parseInt(c); Object.assign(FontData.charOptions(font, n), options[n]); } return font; }