site/node_modules/mathjax-full/ts/output/common/Wrapper.ts

846 lines
27 KiB
TypeScript
Raw Normal View History

2024-10-14 06:09:33 +00:00
/*************************************************************
*
* Copyright (c) 2017-2022 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 CommonWrapper class
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {AbstractWrapper, WrapperClass} from '../../core/Tree/Wrapper.js';
import {PropertyList} from '../../core/Tree/Node.js';
import {MmlNode, TextNode, AbstractMmlNode, indentAttributes} from '../../core/MmlTree/MmlNode.js';
import {MmlMo} from '../../core/MmlTree/MmlNodes/mo.js';
import {Property} from '../../core/Tree/Node.js';
import {unicodeChars} from '../../util/string.js';
import * as LENGTHS from '../../util/lengths.js';
import {Styles} from '../../util/Styles.js';
import {StyleList} from '../../util/StyleList.js';
import {CommonOutputJax} from './OutputJax.js';
import {CommonWrapperFactory} from './WrapperFactory.js';
import {BBox} from '../../util/BBox.js';
import {FontData, DelimiterData, CharData, CharOptions, DIRECTION, NOSTRETCH} from './FontData.js';
/*****************************************************************/
/**
* Shorthand for a dictionary object (an object of key:value pairs)
*/
export type StringMap = {[key: string]: string};
/**
* MathML spacing rules
*/
/* tslint:disable-next-line:whitespace */
const SMALLSIZE = 2/18;
/**
* @param {boolean} script The scriptlevel
* @param {number} size The space size
* @return {number} The size clamped to SMALLSIZE when scriptlevel > 0
*/
function MathMLSpace(script: boolean, size: number): number {
return (script ? size < SMALLSIZE ? 0 : SMALLSIZE : size);
}
export type Constructor<T> = new(...args: any[]) => T;
/**
* Shorthands for wrappers and their constructors
*/
export type AnyWrapper = CommonWrapper<any, any, any, any, any, any>;
export type AnyWrapperClass = CommonWrapperClass<any, any, any, any, any, any>;
export type WrapperConstructor = Constructor<AnyWrapper>;
/*********************************************************/
/**
* The CommonWrapper class interface
*
* @template J The OutputJax type
* @template W The Wrapper type
* @template C The WrapperClass type
* @template CC The CharOptions type
* @template FD The FontData type
*/
export interface CommonWrapperClass<
J extends CommonOutputJax<any, any, any, W, CommonWrapperFactory<J, W, C, CC, DD, FD>, FD, any>,
W extends CommonWrapper<J, W, C, CC, DD, FD>,
C extends CommonWrapperClass<J, W, C, CC, DD, FD>,
CC extends CharOptions,
DD extends DelimiterData,
FD extends FontData<CC, any, DD>
> extends WrapperClass<MmlNode, CommonWrapper<J, W, C, CC, DD, FD>> {
/**
* @override
*/
new(factory: CommonWrapperFactory<J, W, C, CC, DD, FD>, node: MmlNode, ...args: any[]): W;
}
/*****************************************************************/
/**
* The base CommonWrapper class
*
* @template J The OutputJax type
* @template W The Wrapper type
* @template C The WrapperClass type
* @template CC The CharOptions type
* @template FD The FontData type
*/
export class CommonWrapper<
J extends CommonOutputJax<any, any, any, W, CommonWrapperFactory<J, W, C, CC, DD, FD>, FD, any>,
W extends CommonWrapper<J, W, C, CC, DD, FD>,
C extends CommonWrapperClass<J, W, C, CC, DD, FD>,
CC extends CharOptions,
DD extends DelimiterData,
FD extends FontData<CC, any, DD>
> extends AbstractWrapper<MmlNode, CommonWrapper<J, W, C, CC, DD, FD>> {
/**
* The wrapper kind
*/
public static kind: string = 'unknown';
/**
* Any styles needed for the class
*/
public static styles: StyleList = {};
/**
* Styles that should not be passed on from style attribute
*/
public static removeStyles: string[] = [
'fontSize', 'fontFamily', 'fontWeight',
'fontStyle', 'fontVariant', 'font'
];
/**
* Non-MathML attributes on MathML elements NOT to be copied to the
* corresponding DOM elements. If set to false, then the attribute
* WILL be copied. Most of these (like the font attributes) are handled
* in other ways.
*/
public static skipAttributes: {[name: string]: boolean} = {
fontfamily: true, fontsize: true, fontweight: true, fontstyle: true,
color: true, background: true,
'class': true, href: true, style: true,
xmlns: true
};
/**
* The translation of mathvariant to bold styles, or to remove
* bold from a mathvariant.
*/
public static BOLDVARIANTS: {[name: string]: StringMap} = {
bold: {
normal: 'bold',
italic: 'bold-italic',
fraktur: 'bold-fraktur',
script: 'bold-script',
'sans-serif': 'bold-sans-serif',
'sans-serif-italic': 'sans-serif-bold-italic'
},
normal: {
bold: 'normal',
'bold-italic': 'italic',
'bold-fraktur': 'fraktur',
'bold-script': 'script',
'bold-sans-serif': 'sans-serif',
'sans-serif-bold-italic': 'sans-serif-italic'
}
};
/**
* The translation of mathvariant to italic styles, or to remove
* italic from a mathvariant.
*/
public static ITALICVARIANTS: {[name: string]: StringMap} = {
italic: {
normal: 'italic',
bold: 'bold-italic',
'sans-serif': 'sans-serif-italic',
'bold-sans-serif': 'sans-serif-bold-italic'
},
normal: {
italic: 'normal',
'bold-italic': 'bold',
'sans-serif-italic': 'sans-serif',
'sans-serif-bold-italic': 'bold-sans-serif'
}
};
/**
* The factory used to create more wrappers
*/
protected factory: CommonWrapperFactory<J, W, C, CC, DD, FD>;
/**
* The parent of this node
*/
public parent: W = null;
/**
* The children of this node
*/
public childNodes: W[];
/**
* Styles that must be handled directly by the wrappers (mostly having to do with fonts)
*/
protected removedStyles: StringMap = null;
/**
* The explicit styles set by the node
*/
protected styles: Styles = null;
/**
* The mathvariant for this node
*/
public variant: string = '';
/**
* The bounding box for this node
*/
public bbox: BBox;
/**
* Whether the bounding box has been computed yet
*/
protected bboxComputed: boolean = false;
/**
* Delimiter data for stretching this node (NOSTRETCH means not yet determined)
*/
public stretch: DD = NOSTRETCH as DD;
/**
* Easy access to the font parameters
*/
public font: FD = null;
/**
* Easy access to the output jax for this node
*/
get jax() {
return this.factory.jax;
}
/**
* Easy access to the DOMAdaptor object
*/
get adaptor() {
return this.factory.jax.adaptor;
}
/**
* Easy access to the metric data for this node
*/
get metrics() {
return this.factory.jax.math.metrics;
}
/**
* True if children with percentage widths should be resolved by this container
*/
get fixesPWidth() {
return !this.node.notParent && !this.node.isToken;
}
/*******************************************************************/
/**
* @override
*/
constructor(factory: CommonWrapperFactory<J, W, C, CC, DD, FD>, node: MmlNode, parent: W = null) {
super(factory, node);
this.parent = parent;
this.font = factory.jax.font;
this.bbox = BBox.zero();
this.getStyles();
this.getVariant();
this.getScale();
this.getSpace();
this.childNodes = node.childNodes.map((child: MmlNode) => {
const wrapped = this.wrap(child);
if (wrapped.bbox.pwidth && (node.notParent || node.isKind('math'))) {
this.bbox.pwidth = BBox.fullWidth;
}
return wrapped;
});
}
/**
* @param {MmlNode} node The node to the wrapped
* @param {W} parent The wrapped parent node
* @return {W} The newly wrapped node
*/
public wrap(node: MmlNode, parent: W = null): W {
const wrapped = this.factory.wrap(node, parent || this);
if (parent) {
parent.childNodes.push(wrapped);
}
this.jax.nodeMap.set(node, wrapped);
return wrapped;
}
/*******************************************************************/
/**
* Return the wrapped node's bounding box, either the cached one, if it exists,
* or computed directly if not.
*
* @param {boolean} save Whether to cache the bbox or not (used for stretchy elements)
* @return {BBox} The computed bounding box
*/
public getBBox(save: boolean = true): BBox {
if (this.bboxComputed) {
return this.bbox;
}
const bbox = (save ? this.bbox : BBox.zero());
this.computeBBox(bbox);
this.bboxComputed = save;
return bbox;
}
/**
* Return the wrapped node's bounding box that includes borders and padding
*
* @param {boolean} save Whether to cache the bbox or not (used for stretchy elements)
* @return {BBox} The computed bounding box
*/
public getOuterBBox(save: boolean = true): BBox {
const bbox = this.getBBox(save);
if (!this.styles) return bbox;
const obox = new BBox();
Object.assign(obox, bbox);
for (const [name, side] of BBox.StyleAdjust) {
const x = this.styles.get(name);
if (x) {
(obox as any)[side] += this.length2em(x, 1, obox.rscale);
}
}
return obox;
}
/**
* @param {BBox} bbox The bounding box to modify (either this.bbox, or an empty one)
* @param {boolean} recompute True if we are recomputing due to changes in children that have percentage widths
*/
protected computeBBox(bbox: BBox, recompute: boolean = false) {
bbox.empty();
for (const child of this.childNodes) {
bbox.append(child.getOuterBBox());
}
bbox.clean();
if (this.fixesPWidth && this.setChildPWidths(recompute)) {
this.computeBBox(bbox, true);
}
}
/**
* Recursively resolve any percentage widths in the child nodes using the given
* container width (or the child width, if none was passed).
* Overriden for mtables in order to compute the width.
*
* @param {boolean} recompute True if we are recomputing due to changes in children
* @param {(number|null)=} w The width of the container (from which percentages are computed)
* @param {boolean=} clear True if pwidth marker is to be cleared
* @return {boolean} True if a percentage width was found
*/
public setChildPWidths(recompute: boolean, w: (number | null) = null, clear: boolean = true): boolean {
if (recompute) {
return false;
}
if (clear) {
this.bbox.pwidth = '';
}
let changed = false;
for (const child of this.childNodes) {
const cbox = child.getOuterBBox();
if (cbox.pwidth && child.setChildPWidths(recompute, w === null ? cbox.w : w, clear)) {
changed = true;
}
}
return changed;
}
/**
* Mark BBox to be computed again (e.g., when an mo has stretched)
*/
public invalidateBBox() {
if (this.bboxComputed) {
this.bboxComputed = false;
if (this.parent) {
this.parent.invalidateBBox();
}
}
}
/**
* Copy child skew and italic correction
*
* @param {BBox} bbox The bounding box to modify
*/
protected copySkewIC(bbox: BBox) {
const first = this.childNodes[0];
if (first?.bbox.sk) {
bbox.sk = first.bbox.sk;
}
if (first?.bbox.dx) {
bbox.dx = first.bbox.dx;
}
const last = this.childNodes[this.childNodes.length - 1];
if (last?.bbox.ic) {
bbox.ic = last.bbox.ic;
bbox.w += bbox.ic;
}
}
/*******************************************************************/
/**
* Add the style attribute, but remove any font-related styles
* (since these are handled separately by the variant)
*/
protected getStyles() {
const styleString = this.node.attributes.getExplicit('style') as string;
if (!styleString) return;
const style = this.styles = new Styles(styleString);
for (let i = 0, m = CommonWrapper.removeStyles.length; i < m; i++) {
const id = CommonWrapper.removeStyles[i];
if (style.get(id)) {
if (!this.removedStyles) this.removedStyles = {};
this.removedStyles[id] = style.get(id);
style.set(id, '');
}
}
}
/**
* Get the mathvariant (or construct one, if needed).
*/
protected getVariant() {
if (!this.node.isToken) return;
const attributes = this.node.attributes;
let variant = attributes.get('mathvariant') as string;
if (!attributes.getExplicit('mathvariant')) {
const values = attributes.getList('fontfamily', 'fontweight', 'fontstyle') as StringMap;
if (this.removedStyles) {
const style = this.removedStyles;
if (style.fontFamily) values.family = style.fontFamily;
if (style.fontWeight) values.weight = style.fontWeight;
if (style.fontStyle) values.style = style.fontStyle;
}
if (values.fontfamily) values.family = values.fontfamily;
if (values.fontweight) values.weight = values.fontweight;
if (values.fontstyle) values.style = values.fontstyle;
if (values.weight && values.weight.match(/^\d+$/)) {
values.weight = (parseInt(values.weight) > 600 ? 'bold' : 'normal');
}
if (values.family) {
variant = this.explicitVariant(values.family, values.weight, values.style);
} else {
if (this.node.getProperty('variantForm')) variant = '-tex-variant';
variant = (CommonWrapper.BOLDVARIANTS[values.weight] || {})[variant] || variant;
variant = (CommonWrapper.ITALICVARIANTS[values.style] || {})[variant] || variant;
}
}
this.variant = variant;
}
/**
* Set the CSS for a token element having an explicit font (rather than regular mathvariant).
*
* @param {string} fontFamily The font family to use
* @param {string} fontWeight The font weight to use
* @param {string} fontStyle The font style to use
*/
protected explicitVariant(fontFamily: string, fontWeight: string, fontStyle: string) {
let style = this.styles;
if (!style) style = this.styles = new Styles();
style.set('fontFamily', fontFamily);
if (fontWeight) style.set('fontWeight', fontWeight);
if (fontStyle) style.set('fontStyle', fontStyle);
return '-explicitFont';
}
/**
* Determine the scaling factor to use for this wrapped node, and set the styles for it.
*/
protected getScale() {
let scale = 1, parent = this.parent;
let pscale = (parent ? parent.bbox.scale : 1);
let attributes = this.node.attributes;
let scriptlevel = Math.min(attributes.get('scriptlevel') as number, 2);
let fontsize = attributes.get('fontsize');
let mathsize = (this.node.isToken || this.node.isKind('mstyle') ?
attributes.get('mathsize') : attributes.getInherited('mathsize'));
//
// If scriptsize is non-zero, set scale based on scriptsizemultiplier
//
if (scriptlevel !== 0) {
scale = Math.pow(attributes.get('scriptsizemultiplier') as number, scriptlevel);
let scriptminsize = this.length2em(attributes.get('scriptminsize'), .8, 1);
if (scale < scriptminsize) scale = scriptminsize;
}
//
// If there is style="font-size:...", and not fontsize attribute, use that as fontsize
//
if (this.removedStyles && this.removedStyles.fontSize && !fontsize) {
fontsize = this.removedStyles.fontSize;
}
//
// If there is a fontsize and no mathsize attribute, is that
//
if (fontsize && !attributes.getExplicit('mathsize')) {
mathsize = fontsize;
}
//
// Incorporate the mathsize, if any
//
if (mathsize !== '1') {
scale *= this.length2em(mathsize, 1, 1);
}
//
// Record the scaling factors and set the element's CSS
//
this.bbox.scale = scale;
this.bbox.rscale = scale / pscale;
}
/**
* Sets the spacing based on TeX or MathML algorithm
*/
protected getSpace() {
const isTop = this.isTopEmbellished();
const hasSpacing = this.node.hasSpacingAttributes();
if (this.jax.options.mathmlSpacing || hasSpacing) {
isTop && this.getMathMLSpacing();
} else {
this.getTeXSpacing(isTop, hasSpacing);
}
}
/**
* Get the spacing using MathML rules based on the core MO
*/
protected getMathMLSpacing() {
const node = this.node.coreMO() as MmlMo;
//
// If the mo is not within a multi-node mrow, don't add space
//
const child = node.coreParent();
const parent = child.parent;
if (!parent || !parent.isKind('mrow') || parent.childNodes.length === 1) return;
//
// Get the lspace and rspace
//
const attributes = node.attributes;
const isScript = (attributes.get('scriptlevel') > 0);
this.bbox.L = (attributes.isSet('lspace') ?
Math.max(0, this.length2em(attributes.get('lspace'))) :
MathMLSpace(isScript, node.lspace));
this.bbox.R = (attributes.isSet('rspace') ?
Math.max(0, this.length2em(attributes.get('rspace'))) :
MathMLSpace(isScript, node.rspace));
//
// If there are two adjacent <mo>, use enough left space to make it
// the maximum of the rspace of the first and lspace of the second
//
const n = parent.childIndex(child);
if (n === 0) return;
const prev = parent.childNodes[n - 1] as AbstractMmlNode;
if (!prev.isEmbellished) return;
const bbox = this.jax.nodeMap.get(prev).getBBox();
if (bbox.R) {
this.bbox.L = Math.max(0, this.bbox.L - bbox.R);
}
}
/**
* Get the spacing using the TeX rules
*
* @parm {boolean} isTop True when this is a top-level embellished operator
* @parm {boolean} hasSpacing True when there is an explicit or inherited 'form' attribute
*/
protected getTeXSpacing(isTop: boolean, hasSpacing: boolean) {
if (!hasSpacing) {
const space = this.node.texSpacing();
if (space) {
this.bbox.L = this.length2em(space);
}
}
if (isTop || hasSpacing) {
const attributes = this.node.coreMO().attributes;
if (attributes.isSet('lspace')) {
this.bbox.L = Math.max(0, this.length2em(attributes.get('lspace')));
}
if (attributes.isSet('rspace')) {
this.bbox.R = Math.max(0, this.length2em(attributes.get('rspace')));
}
}
}
/**
* @return {boolean} True if this is the top-most container of an embellished operator that is
* itself an embellished operator (the maximal embellished operator for its core)
*/
protected isTopEmbellished(): boolean {
return (this.node.isEmbellished &&
!(this.node.parent && this.node.parent.isEmbellished));
}
/*******************************************************************/
/**
* @return {CommonWrapper} The wrapper for this node's core node
*/
public core(): CommonWrapper<J, W, C, CC, DD, FD> {
return this.jax.nodeMap.get(this.node.core());
}
/**
* @return {CommonWrapper} The wrapper for this node's core <mo> node
*/
public coreMO(): CommonWrapper<J, W, C, CC, DD, FD> {
return this.jax.nodeMap.get(this.node.coreMO());
}
/**
* @return {string} For a token node, the combined text content of the node's children
*/
public getText(): string {
let text = '';
if (this.node.isToken) {
for (const child of this.node.childNodes) {
if (child instanceof TextNode) {
text += child.getText();
}
}
}
return text;
}
/**
* @param {DIRECTION} direction The direction to stretch this node
* @return {boolean} Whether the node can stretch in that direction
*/
public canStretch(direction: DIRECTION): boolean {
this.stretch = NOSTRETCH as DD;
if (this.node.isEmbellished) {
let core = this.core();
if (core && core.node !== this.node) {
if (core.canStretch(direction)) {
this.stretch = core.stretch;
}
}
}
return this.stretch.dir !== DIRECTION.None;
}
/**
* @return {[string, number]} The alignment and indentation shift for the expression
*/
protected getAlignShift(): [string, number] {
let {indentalign, indentshift, indentalignfirst, indentshiftfirst} =
this.node.attributes.getList(...indentAttributes) as StringMap;
if (indentalignfirst !== 'indentalign') {
indentalign = indentalignfirst;
}
if (indentalign === 'auto') {
indentalign = this.jax.options.displayAlign;
}
if (indentshiftfirst !== 'indentshift') {
indentshift = indentshiftfirst;
}
if (indentshift === 'auto') {
indentshift = this.jax.options.displayIndent;
if (indentalign === 'right' && !indentshift.match(/^\s*0[a-z]*\s*$/)) {
indentshift = ('-' + indentshift.trim()).replace(/^--/, '');
}
}
const shift = this.length2em(indentshift, this.metrics.containerWidth);
return [indentalign, shift] as [string, number];
}
/**
* @param {number} W The total width
* @param {BBox} bbox The bbox to be aligned
* @param {string} align How to align (left, center, right)
* @return {number} The x position of the aligned width
*/
protected getAlignX(W: number, bbox: BBox, align: string): number {
return (align === 'right' ? W - (bbox.w + bbox.R) * bbox.rscale :
align === 'left' ? bbox.L * bbox.rscale :
(W - bbox.w * bbox.rscale) / 2);
}
/**
* @param {number} H The total height
* @param {number} D The total depth
* @param {number} h The height to be aligned
* @param {number} d The depth to be aligned
* @param {string} align How to align (top, bottom, center, axis, baseline)
* @return {number} The y position of the aligned baseline
*/
protected getAlignY(H: number, D: number, h: number, d: number, align: string): number {
return (align === 'top' ? H - h :
align === 'bottom' ? d - D :
align === 'center' ? ((H - h) - (D - d)) / 2 :
0); // baseline and axis
}
/**
* @param {number} i The index of the child element whose container is needed
* @return {number} The inner width as a container (for percentage widths)
*/
public getWrapWidth(i: number): number {
return this.childNodes[i].getBBox().w;
}
/**
* @param {number} i The index of the child element whose container is needed
* @return {string} The alignment child element
*/
public getChildAlign(_i: number): string {
return 'left';
}
/*******************************************************************/
/*
* Easy access to some utility routines
*/
/**
* @param {number} m A number to be shown as a percent
* @return {string} The number m as a percent
*/
protected percent(m: number): string {
return LENGTHS.percent(m);
}
/**
* @param {number} m A number to be shown in ems
* @return {string} The number with units of ems
*/
protected em(m: number): string {
return LENGTHS.em(m);
}
/**
* @param {number} m A number of em's to be shown as pixels
* @param {number} M The minimum number of pixels to allow
* @return {string} The number with units of px
*/
protected px(m: number, M: number = -LENGTHS.BIGDIMEN): string {
return LENGTHS.px(m, M, this.metrics.em);
}
/**
* @param {Property} length A dimension (giving number and units) or number to be converted to ems
* @param {number} size The default size of the dimension (for percentage values)
* @param {number} scale The current scaling factor (to handle absolute units)
* @return {number} The dimension converted to ems
*/
protected length2em(length: Property, size: number = 1, scale: number = null): number {
if (scale === null) {
scale = this.bbox.scale;
}
return LENGTHS.length2em(length as string, size, scale, this.jax.pxPerEm);
}
/**
* @param {string} text The text to turn into unicode locations
* @param {string} name The name of the variant for the characters
* @return {number[]} Array of numbers represeting the string's unicode character positions
*/
protected unicodeChars(text: string, name: string = this.variant): number[] {
let chars = unicodeChars(text);
//
// Remap to Math Alphanumerics block
//
const variant = this.font.getVariant(name);
if (variant && variant.chars) {
const map = variant.chars;
//
// Is map[n] doesn't exist, (map[n] || []) still gives an CharData array.
// If the array doesn't have a CharOptions element use {} instead.
// Then check if the options has an smp property, which gives
// the Math Alphabet mapping for this character.
// Otherwise use the original code point, n.
//
chars = chars.map((n) => ((map[n] || [])[3] || {}).smp || n);
}
return chars;
}
/**
* @param {number[]} chars The array of unicode character numbers to remap
* @return {number[]} The converted array
*/
public remapChars(chars: number[]): number[] {
return chars;
}
/**
* @param {string} text The text from which to create a TextNode object
* @return {TextNode} The TextNode with the given text
*/
public mmlText(text: string): TextNode {
return ((this.node as AbstractMmlNode).factory.create('text') as TextNode).setText(text);
}
/**
* @param {string} kind The kind of MmlNode to create
* @param {ProperyList} properties The properties to set initially
* @param {MmlNode[]} children The child nodes to add to the created node
* @return {MmlNode} The newly created MmlNode
*/
public mmlNode(kind: string, properties: PropertyList = {}, children: MmlNode[] = []): MmlNode {
return (this.node as AbstractMmlNode).factory.create(kind, properties, children);
}
/**
* Create an mo wrapper with the given text,
* link it in, and give it the right defaults.
*
* @param {string} text The text for the wrapped element
* @return {CommonWrapper} The wrapped MmlMo node
*/
protected createMo(text: string): CommonWrapper<J, W, C, CC, DD, FD> {
const mmlFactory = (this.node as AbstractMmlNode).factory;
const textNode = (mmlFactory.create('text') as TextNode).setText(text);
const mml = mmlFactory.create('mo', {stretchy: true}, [textNode]);
mml.inheritAttributesFrom(this.node);
const node = this.wrap(mml);
node.parent = this as any as W;
return node;
}
/**
* @param {string} variant The variant in which to look for the character
* @param {number} n The number of the character to look up
* @return {CharData} The full CharData object, with CharOptions guaranteed to be defined
*/
protected getVariantChar(variant: string, n: number): CharData<CC> {
const char = this.font.getChar(variant, n) || [0, 0, 0, {unknown: true} as CC];
if (char.length === 3) {
(char as any)[3] = {};
}
return char as [number, number, number, CC];
}
}