From f52666633b9f8a21756afe1ccb7bc0239a02b468 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Thu, 2 Nov 2023 07:58:57 -0400 Subject: [PATCH 1/5] Fix rendering of embedded HTML to work with scaling and mathsize changes. (mathjax/MathJax#3116) --- ts/a11y/semantic-enrich.ts | 5 + ts/core/MmlTree/MmlNodes/HtmlNode.ts | 40 ++--- ts/output/chtml.ts | 7 - ts/output/chtml/Wrappers/HtmlNode.ts | 53 +----- ts/output/chtml/Wrappers/semantics.ts | 84 ++++++--- ts/output/common.ts | 29 ---- ts/output/common/Wrapper.ts | 13 ++ ts/output/common/Wrappers/HtmlNode.ts | 238 -------------------------- ts/output/svg.ts | 8 + ts/output/svg/Wrappers/HtmlNode.ts | 65 +------ ts/output/svg/Wrappers/semantics.ts | 95 ++++++---- 11 files changed, 169 insertions(+), 468 deletions(-) delete mode 100644 ts/output/common/Wrappers/HtmlNode.ts diff --git a/ts/a11y/semantic-enrich.ts b/ts/a11y/semantic-enrich.ts index 6dcfa748d..ccc77e3dc 100644 --- a/ts/a11y/semantic-enrich.ts +++ b/ts/a11y/semantic-enrich.ts @@ -26,6 +26,7 @@ import {Handler} from '../core/Handler.js'; import {MathDocument, AbstractMathDocument, MathDocumentConstructor} from '../core/MathDocument.js'; import {MathItem, AbstractMathItem, STATE, newState} from '../core/MathItem.js'; import {MmlNode} from '../core/MmlTree/MmlNode.js'; +import {HtmlNode} from '../core/MmlTree/MmlNodes/HtmlNode.js'; import {MathML} from '../input/mathml.js'; import {SerializedMmlVisitor} from '../core/MmlTree/SerializedMmlVisitor.js'; import {OptionList, expandable} from '../util/Options.js'; @@ -70,6 +71,10 @@ export class enrichVisitor extends SerializedMmlVisitor { return mml; } + public visitHtmlNode(node: HtmlNode, _space: string): string { + return node.getSerializedXML(); + } + public visitMactionNode(node: MmlNode, space: string) { let [nl, endspace] = (node.childNodes.length === 0 ? ['', ''] : ['\n', space]); const children = this.childNodeMml(node, space + ' ', nl); diff --git a/ts/core/MmlTree/MmlNodes/HtmlNode.ts b/ts/core/MmlTree/MmlNodes/HtmlNode.ts index 76df58bae..2917e0365 100644 --- a/ts/core/MmlTree/MmlNodes/HtmlNode.ts +++ b/ts/core/MmlTree/MmlNodes/HtmlNode.ts @@ -21,27 +21,18 @@ * @author dpvc@mathjax.org (Davide Cervone) */ -import {AbstractMmlEmptyNode} from '../MmlNode.js'; +import {XMLNode} from '../MmlNode.js'; import {DOMAdaptor} from '../../DOMAdaptor.js'; import {PropertyList} from '../../Tree/Node.js'; /******************************************************************/ /** - * The HtmlNode calss for storing HTML within token elements + * The HtmlNode class for storing HTML within token elements * * @template N The HTMLElement class */ -export class HtmlNode extends AbstractMmlEmptyNode { - /** - * The HTML content for this node - */ - protected html: N = null; - - /** - * DOM adaptor for the content - */ - protected adaptor: DOMAdaptor = null; +export class HtmlNode extends XMLNode { /** * @override @@ -51,14 +42,14 @@ export class HtmlNode extends AbstractMmlEmptyNode { } /** - * @return {Object} Return the node's HTML content + * @return {N} Return the node's HTML content */ - public getHTML(): Object { - return this.html; + public getHTML(): N { + return this.getXML() as any as N; } /** - * @param {object} html The HTML content to be saved + * @param {N} html The HTML content to be saved * @param {DOMAdaptor} adaptor DOM adaptor for the content * @return {HTMLNode} The HTML node (for chaining of method calls) */ @@ -71,37 +62,28 @@ export class HtmlNode extends AbstractMmlEmptyNode { } catch (error) { html = adaptor.node('span', {}, [html]); } - this.html = html; - this.adaptor = adaptor; - return this; + return this.setXML(html, adaptor) as HtmlNode; } /** * @return {string} The serialized HTML content */ public getSerializedHTML(): string { - return this.adaptor.outerHTML(this.html); + return this.adaptor.outerHTML(this.xml); } /** * @return {string} The text of the HTML content */ public textContent(): string { - return this.adaptor.textContent(this.html); - } - - /** - * @override - */ - public copy(): HtmlNode { - return (this.factory.create(this.kind) as HtmlNode).setHTML(this.adaptor.clone(this.html)); + return this.adaptor.textContent(this.xml); } /** * Just indicate that this is HTML data */ public toString() { - const kind = this.adaptor.kind(this.html); + const kind = this.adaptor.kind(this.xml); return `HTML=<${kind}>...` ; } diff --git a/ts/output/chtml.ts b/ts/output/chtml.ts index 9f41a5d15..d2ed4f10b 100644 --- a/ts/output/chtml.ts +++ b/ts/output/chtml.ts @@ -255,13 +255,6 @@ CommonOutputJax< this.clearCache(); } - /** - * @override - */ - protected getInitialScale() { - return this.math.metrics.scale; - } - /*****************************************************************/ /** diff --git a/ts/output/chtml/Wrappers/HtmlNode.ts b/ts/output/chtml/Wrappers/HtmlNode.ts index d4e23cc3b..f368eda5c 100644 --- a/ts/output/chtml/Wrappers/HtmlNode.ts +++ b/ts/output/chtml/Wrappers/HtmlNode.ts @@ -21,15 +21,11 @@ * @author dpvc@mathjax.org (Davide Cervone) */ -import {CHTML} from '../../chtml.js'; -import {ChtmlWrapper, ChtmlWrapperClass} from '../Wrapper.js'; +import {ChtmlWrapper} from '../Wrapper.js'; import {ChtmlWrapperFactory} from '../WrapperFactory.js'; -import {ChtmlCharOptions, ChtmlVariantData, ChtmlDelimiterData, - ChtmlFontData, ChtmlFontDataClass} from '../FontData.js'; -import {CommonHtmlNode, CommonHtmlNodeClass, CommonHtmlNodeMixin} from '../../common/Wrappers/HtmlNode.js'; +import {ChtmlXmlNode, ChtmlXmlNodeNTD, ChtmlXmlNodeClass} from './semantics.js'; import {MmlNode} from '../../../core/MmlTree/MmlNode.js'; import {HtmlNode} from '../../../core/MmlTree/MmlNodes/HtmlNode.js'; -import {StyleList as StyleList} from '../../../util/StyleList.js'; /*****************************************************************/ /** @@ -39,11 +35,7 @@ import {StyleList as StyleList} from '../../../util/StyleList.js'; * @template T The Text node class * @template D The Document class */ -export interface ChtmlHtmlNodeNTD extends ChtmlWrapper, CommonHtmlNode< - N, T, D, - CHTML, ChtmlWrapper, ChtmlWrapperFactory, ChtmlWrapperClass, - ChtmlCharOptions, ChtmlVariantData, ChtmlDelimiterData, ChtmlFontData, ChtmlFontDataClass -> {} +export interface ChtmlHtmlNodeNTD extends ChtmlXmlNodeNTD {} /** * The ChtmlHtmlNodeClass interface for the CHTML HtmlNode wrapper @@ -52,11 +44,7 @@ export interface ChtmlHtmlNodeNTD extends ChtmlWrapper, Common * @template T The Text node class * @template D The Document class */ -export interface ChtmlHtmlNodeClass extends ChtmlWrapperClass, CommonHtmlNodeClass< - N, T, D, - CHTML, ChtmlWrapper, ChtmlWrapperFactory, ChtmlWrapperClass, - ChtmlCharOptions, ChtmlVariantData, ChtmlDelimiterData, ChtmlFontData, ChtmlFontDataClass -> { +export interface ChtmlHtmlNodeClass extends ChtmlXmlNodeClass { new(factory: ChtmlWrapperFactory, node: MmlNode, parent?: ChtmlWrapper): ChtmlHtmlNodeNTD; } @@ -68,47 +56,16 @@ export interface ChtmlHtmlNodeClass extends ChtmlWrapperClass, */ export const ChtmlHtmlNode = (function (): ChtmlHtmlNodeClass { - const Base = CommonHtmlNodeMixin< - N, T, D, - CHTML, ChtmlWrapper, ChtmlWrapperFactory, ChtmlWrapperClass, - ChtmlCharOptions, ChtmlVariantData, ChtmlDelimiterData, ChtmlFontData, ChtmlFontDataClass, - ChtmlHtmlNodeClass - >(ChtmlWrapper); - // Avoid message about base constructors not having the same type // (they should both be ChtmlWrapper, but are thought of as different by typescript) // @ts-ignore - return class ChtmlHtmlNode extends Base implements ChtmlHtmlNodeNTD { + return class ChtmlHtmlNode extends ChtmlXmlNode implements ChtmlHtmlNodeNTD { /** * @override */ public static kind = HtmlNode.prototype.kind; - /** - * @override - */ - public static styles: StyleList = { - 'mjx-html': { - 'line-height': 'normal', - 'text-align': 'initial' - }, - 'mjx-html-holder': { - display: 'block', - position: 'absolute', - width: '100%', - height: '100%' - } - }; - - /** - * @override - */ - public toCHTML(parents: N[]) { - this.markUsed(); - this.dom = [this.adaptor.append(parents[0], this.getHTML()) as N]; - } - }; })(); diff --git a/ts/output/chtml/Wrappers/semantics.ts b/ts/output/chtml/Wrappers/semantics.ts index df0eafa13..0305dd2ee 100644 --- a/ts/output/chtml/Wrappers/semantics.ts +++ b/ts/output/chtml/Wrappers/semantics.ts @@ -28,11 +28,13 @@ import {ChtmlWrapperFactory} from '../WrapperFactory.js'; import {ChtmlCharOptions, ChtmlVariantData, ChtmlDelimiterData, ChtmlFontData, ChtmlFontDataClass} from '../FontData.js'; import {CommonSemantics, CommonSemanticsClass, CommonSemanticsMixin} from '../../common/Wrappers/semantics.js'; +import {CommonXmlNode, CommonXmlNodeClass, CommonXmlNodeMixin} from '../../common/Wrappers/XmlNode.js'; import {MmlNode} from '../../../core/MmlTree/MmlNode.js'; import {MmlSemantics, MmlAnnotation, MmlAnnotationXML} from '../../../core/MmlTree/MmlNodes/semantics.js'; import {XMLNode} from '../../../core/MmlTree/MmlNode.js'; -import {BBox} from '../../../util/BBox.js'; import {StyleList} from '../../../util/StyleList.js'; +import {StyleList as Styles} from '../../../util/Styles.js'; + /*****************************************************************/ /** @@ -167,55 +169,81 @@ export const ChtmlAnnotationXML = (function (): ChtmlWrapperClass extends ChtmlWrapper, CommonXmlNode< + N, T, D, + CHTML, ChtmlWrapper, ChtmlWrapperFactory, ChtmlWrapperClass, + ChtmlCharOptions, ChtmlVariantData, ChtmlDelimiterData, ChtmlFontData, ChtmlFontDataClass +> {} + +/** + * The ChtmlXmlNodeClass interface for the CHTML XmlNode wrapper + * + * @template N The HTMLElement node class + * @template T The Text node class + * @template D The Document class + */ +export interface ChtmlXmlNodeClass extends ChtmlWrapperClass, CommonXmlNodeClass< + N, T, D, + CHTML, ChtmlWrapper, ChtmlWrapperFactory, ChtmlWrapperClass, + ChtmlCharOptions, ChtmlVariantData, ChtmlDelimiterData, ChtmlFontData, ChtmlFontDataClass +> { + new(factory: ChtmlWrapperFactory, node: MmlNode, parent?: ChtmlWrapper): ChtmlXmlNodeNTD; +} + /** * The ChtmlXmlNode wrapper for the XMLNode class */ export const ChtmlXmlNode = (function (): ChtmlWrapperClass { - return class ChtmlXmlNode extends ChtmlWrapper { + const Base = CommonXmlNodeMixin< + N, T, D, + CHTML, ChtmlWrapper, ChtmlWrapperFactory, ChtmlWrapperClass, + ChtmlCharOptions, ChtmlVariantData, ChtmlDelimiterData, ChtmlFontData, ChtmlFontDataClass, + ChtmlXmlNodeClass + >(ChtmlWrapper); + + // Avoid message about base constructors not having the same type + // (they should both be ChtmlWrapper, but are thought of as different by typescript) + // @ts-ignore + return class ChtmlXmlNode extends Base implements ChtmlXmlNodeNTD { /** * @override */ public static kind = XMLNode.prototype.kind; - /** - * Don't set up inline-block styles for this - */ - public static autoStyle = false; - /** * @override */ public toCHTML(parents: N[]) { - this.dom = [this.adaptor.append(parents[0], this.adaptor.clone((this.node as XMLNode).getXML() as N)) as N]; + this.markUsed(); + this.dom = [this.adaptor.append(parents[0], this.getHTML()) as N]; } /** * @override */ - public computeBBox(bbox: BBox, _recompute: boolean = false) { - const {w, h, d} = this.jax.measureXMLnode((this.node as XMLNode).getXML() as N); - bbox.w = w; - bbox.h = h; - bbox.d = d; + public addHDW(html: N, styles: Styles): N { + const scale = this.jax.options.scale; + const {h, d, w} = this.bbox; + const rscale = scale * this.metrics.scale; + styles.width = this.em(w * rscale); + styles.height = this.em((h + d) * rscale); + styles['vertical-align'] = this.em(-d * rscale); + styles.position = 'relative'; + return this.html('mjx-html-holder', {style: { + transform: `scale(${this.jax.fixed(scale)})`, + 'transform-origin': 'top left', + }}, [html]); } - /** - * @override - */ - protected getStyles() {} - - /** - * @override - */ - protected getScale() {} - - /** - * @override - */ - protected getVariant() {} - }; })(); diff --git a/ts/output/common.ts b/ts/output/common.ts index 290f4f008..7620e7c5a 100644 --- a/ts/output/common.ts +++ b/ts/output/common.ts @@ -805,35 +805,6 @@ export abstract class CommonOutputJax< */ public abstract measureTextNode(text: N): UnknownBBox; - /** - * Measure the width, height and depth of an annotation-xml node's content - * - * @param{N} xml The xml content node to be measured - * @return {UnknownBBox} The width, height, and depth of the content - */ - public measureXMLnode(xml: N): UnknownBBox { - const adaptor = this.adaptor; - const content = this.html('mjx-xml-block', {style: {display: 'inline-block'}}, [adaptor.clone(xml)]); - const base = this.html('mjx-baseline', {style: {display: 'inline-block', width: 0, height: 0}}); - const style = { - position: 'absolute', - display: 'inline-block', - 'font-family': 'initial', - 'line-height': 'normal' - }; - const node = this.html('mjx-measure-xml', {style}, [base, content]); - adaptor.append(adaptor.parent(this.math.start.node), this.container); - adaptor.append(this.container, node); - const em = this.math.metrics.em * this.math.metrics.scale; - const {left, right, bottom, top} = adaptor.nodeBBox(content); - const w = (right - left) / em; - const h = (adaptor.nodeBBox(base).top - top) / em; - const d = (bottom - top) / em - h; - adaptor.remove(this.container); - adaptor.remove(node); - return {w, h, d}; - } - /** * @param {CssFontData} font The family, style, and weight for the given font * @param {StyleList} styles The style object to add the font data to diff --git a/ts/output/common/Wrapper.ts b/ts/output/common/Wrapper.ts index 2ab30d1e8..1c0de1287 100644 --- a/ts/output/common/Wrapper.ts +++ b/ts/output/common/Wrapper.ts @@ -1024,6 +1024,19 @@ export class CommonWrapper< return rscale; } + /** + * @return {number} The cumulative relative scale from the root to the current node + */ + public getRScale(): number { + let rscale = 1; + let node = this as any as WW; + while (node) { + rscale *= node.bbox.rscale; + node = node.parent; + } + return rscale; + } + /** * @return {string} For a token node, the combined text content of the node's children */ diff --git a/ts/output/common/Wrappers/HtmlNode.ts b/ts/output/common/Wrappers/HtmlNode.ts deleted file mode 100644 index 70bef9f47..000000000 --- a/ts/output/common/Wrappers/HtmlNode.ts +++ /dev/null @@ -1,238 +0,0 @@ -/************************************************************* - * - * Copyright (c) 2022-2023 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 CommonHtmlNode wrapper mixin for the HtmlNode object - * - * @author dpvc@mathjax.org (Davide Cervone) - */ - -import {CommonWrapper, CommonWrapperClass, CommonWrapperConstructor} from '../Wrapper.js'; -import {CommonWrapperFactory} from '../WrapperFactory.js'; -import {CharOptions, VariantData, DelimiterData, FontData, FontDataClass} from '../FontData.js'; -import {CommonOutputJax} from '../../common.js'; -import {HtmlNode} from '../../../core/MmlTree/MmlNodes/HtmlNode.js'; -import {BBox} from '../../../util/BBox.js'; -import {StyleList} from '../../../util/Styles.js'; -import {ExtendedMetrics, UnknownBBox} from '../../common.js'; -import {split} from '../../../util/string.js'; - -/*****************************************************************/ -/** - * The CommonHtmlNode interface - * - * @template N The DOM node type - * @template T The DOM text node type - * @template D The DOM document type - * @template JX The OutputJax type - * @template WW The Wrapper type - * @template WF The WrapperFactory type - * @template WC The WrapperClass type - * @template CC The CharOptions type - * @template VV The VariantData type - * @template DD The DelimiterData type - * @template FD The FontData type - * @template FC The FontDataClass type - */ -export interface CommonHtmlNode< - N, T, D, - JX extends CommonOutputJax, - WW extends CommonWrapper, - WF extends CommonWrapperFactory, - WC extends CommonWrapperClass, - CC extends CharOptions, - VV extends VariantData, - DD extends DelimiterData, - FD extends FontData, - FC extends FontDataClass -> extends CommonWrapper { - - /** - * @return {N} The HTML for the node - */ - getHTML(): N; - - /** - * @param {N} html The html to adjust if using or forcing HDW - * @param {StyleList} styles The styles object to add to, as needed - */ - addHDW(html: N, styles: StyleList): N; - - /** - * @param {N} html The HTML tree to check - * @param {string} use The first htmlHDW value to check - * @param {string} force The second (optional) htmlHDW value to check - * @return {string} The data-mjx-hdw value, if the options are met - */ - getHDW(html: N, use: string, force?: string): string; - - /** - * @param {string} hdw The data-mjx-hdw string to split - * @return {UnknownBBox} The h, d, w values (in em units) as an object - */ - splitHDW(hdw: string): UnknownBBox; - -} - -/** - * The CommonHtmlNodeClass interface - * - * @template N The DOM node type - * @template T The DOM text node type - * @template D The DOM document type - * @template JX The OutputJax type - * @template WW The Wrapper type - * @template WF The WrapperFactory type - * @template WC The WrapperClass type - * @template CC The CharOptions type - * @template VV The VariantData type - * @template DD The DelimiterData type - * @template FD The FontData type - * @template FC The FontDataClass type - */ -export interface CommonHtmlNodeClass< - N, T, D, - JX extends CommonOutputJax, - WW extends CommonWrapper, - WF extends CommonWrapperFactory, - WC extends CommonWrapperClass, - CC extends CharOptions, - VV extends VariantData, - DD extends DelimiterData, - FD extends FontData, - FC extends FontDataClass -> extends CommonWrapperClass {} - -/*****************************************************************/ -/** - * The CommonHtmlNode wrapper mixin for the HtmlNode object - * - * @template N The DOM node type - * @template T The DOM text node type - * @template D The DOM document type - * @template JX The OutputJax type - * @template WW The Wrapper type - * @template WF The WrapperFactory type - * @template WC The WrapperClass type - * @template CC The CharOptions type - * @template VV The VariantData type - * @template DD The DelimiterData type - * @template FD The FontData type - * @template FC The FontDataClass type - * - * @template B The Mixin interface to create - */ -export function CommonHtmlNodeMixin< - N, T, D, - JX extends CommonOutputJax, - WW extends CommonWrapper, - WF extends CommonWrapperFactory, - WC extends CommonWrapperClass, - CC extends CharOptions, - VV extends VariantData, - DD extends DelimiterData, - FD extends FontData, - FC extends FontDataClass, - B extends CommonWrapperClass ->(Base: CommonWrapperConstructor): B { - - return class CommonHtmlNodeMixin extends Base - implements CommonHtmlNode { - - /** - * @override - */ - public computeBBox(bbox: BBox, _recompute: boolean = false) { - const hdw = this.getHDW((this.node as HtmlNode).getHTML() as N, 'use', 'force'); - const {h, d, w} = (hdw ? this.splitHDW(hdw) : this.jax.measureXMLnode(this.getHTML())); - bbox.w = w; - bbox.h = h; - bbox.d = d; - } - - /** - * @return {N} The HTML for the node - */ - public getHTML(): N { - const adaptor = this.adaptor; - const jax = this.jax; - const styles: StyleList = {}; - const html = this.addHDW(adaptor.clone((this.node as HtmlNode).getHTML() as N), styles); - const metrics = jax.math.metrics as ExtendedMetrics; - if (metrics.scale !== 1) { - styles['font-size'] = jax.fixed(100 / metrics.scale, 1) + '%'; - } - const parent = adaptor.parent(jax.math.start.node); - styles['font-family'] = this.parent.styles?.get('font-family') || - metrics.family || adaptor.fontFamily(parent) || 'initial'; - return this.html('mjx-html', {variant: this.parent.variant, style: styles}, [html]); - } - - /** - * @param {N} html The html to adjust if using or forcing HDW - * @param {StyleList} styles The styles object to add to, as needed - */ - public addHDW(html: N, styles: StyleList): N { - const hdw = this.getHDW(html, 'force'); - if (!hdw) return html; - const {h, d, w} = this.splitHDW(hdw); - styles.height = this.em(h + d); - styles.width = this.em(w); - styles['vertical-align'] = this.em(-d); - styles.position = 'relative'; - return this.html('mjx-html-holder', {}, [html]); - } - - /** - * @param {N} html The HTML tree to check - * @param {string} use The first htmlHDW value to check - * @param {string} force The second (optional) htmlHDW value to check - * @return {string} The data-mjx-hdw value, if the options are met - */ - public getHDW(html: N, use: string, force: string = use): string { - const option = this.jax.options.htmlHDW; - const hdw = this.adaptor.getAttribute(html, 'data-mjx-hdw') as string; - return (hdw && (option === use || option === force) ? hdw : null); - } - - /** - * @param {string} hdw The data-mjx-hdw string to split - * @return {UnknownBBox} The h, d, w values (in em units) as an object - */ - public splitHDW(hdw: string): UnknownBBox { - const [h, d, w] = split(hdw).map(x => this.length2em(x || '0')); - return {h, d, w}; - } - - /** - * @override - */ - protected getStyles() {} - - /** - * @override - */ - protected getScale() {} - - /** - * @override - */ - protected getVariant() {} - - } as any as B; - -} diff --git a/ts/output/svg.ts b/ts/output/svg.ts index fdd58ff54..d3c00192b 100644 --- a/ts/output/svg.ts +++ b/ts/output/svg.ts @@ -143,6 +143,7 @@ CommonOutputJax< constructor(options: OptionList = null) { super(options, SvgWrapperFactory as any, DefaultFont); this.fontCache = new FontCache(this); + this.options.matchFontHeight = true; } /** @@ -215,6 +216,13 @@ CommonOutputJax< return false; } + /** + * @override + */ + protected getInitialScale() { + return 1; + } + /** * @param {SvgWrapper} wrapper The MML node wrapper whose SVG is to be produced * @param {N} parent The HTML node to contain the SVG diff --git a/ts/output/svg/Wrappers/HtmlNode.ts b/ts/output/svg/Wrappers/HtmlNode.ts index 9254d7db7..7c70e4366 100644 --- a/ts/output/svg/Wrappers/HtmlNode.ts +++ b/ts/output/svg/Wrappers/HtmlNode.ts @@ -21,14 +21,11 @@ * @author dpvc@mathjax.org (Davide Cervone) */ -import {SVG} from '../../svg.js'; -import {SvgWrapper, SvgWrapperClass} from '../Wrapper.js'; +import {SvgWrapper} from '../Wrapper.js'; import {SvgWrapperFactory} from '../WrapperFactory.js'; -import {SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass} from '../FontData.js'; -import {CommonHtmlNode, CommonHtmlNodeClass, CommonHtmlNodeMixin} from '../../common/Wrappers/HtmlNode.js'; +import {SvgXmlNode, SvgXmlNodeNTD, SvgXmlNodeClass} from './semantics.js'; import {MmlNode} from '../../../core/MmlTree/MmlNode.js'; import {HtmlNode} from '../../../core/MmlTree/MmlNodes/HtmlNode.js'; -import {StyleList} from '../../../util/StyleList.js'; /*****************************************************************/ /** @@ -38,11 +35,7 @@ import {StyleList} from '../../../util/StyleList.js'; * @template T The Text node class * @template D The Document class */ -export interface SvgHtmlNodeNTD extends SvgWrapper, CommonHtmlNode< - N, T, D, - SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, - SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass -> {} +export interface SvgHtmlNodeNTD extends SvgXmlNodeNTD {} /** * The SvgHtmlNodeClass interface for the SVG HtmlNode wrapper @@ -51,11 +44,7 @@ export interface SvgHtmlNodeNTD extends SvgWrapper, CommonHtml * @template T The Text node class * @template D The Document class */ -export interface SvgHtmlNodeClass extends SvgWrapperClass, CommonHtmlNodeClass< - N, T, D, - SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, - SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass -> { +export interface SvgHtmlNodeClass extends SvgXmlNodeClass { new(factory: SvgWrapperFactory, node: MmlNode, parent?: SvgWrapper): SvgHtmlNodeNTD; } @@ -67,60 +56,16 @@ export interface SvgHtmlNodeClass extends SvgWrapperClass, Com */ export const SvgHtmlNode = (function (): SvgHtmlNodeClass { - const Base = CommonHtmlNodeMixin< - N, T, D, - SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, - SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass, - SvgHtmlNodeClass - >(SvgWrapper); - // Avoid message about base constructors not having the same type // (they should both be SvgWrapper, but are thought of as different by typescript) // @ts-ignore - return class SvgHtmlNode extends Base implements SvgHtmlNodeNTD { + return class SvgHtmlNode extends SvgXmlNode implements SvgHtmlNodeNTD { /** * @override */ public static kind = HtmlNode.prototype.kind; - /** - * @override - */ - public static styles: StyleList = { - 'foreignObject[data-mjx-html]': { - overflow: 'visible' - }, - 'mjx-html': { - display: 'inline-block', - 'line-height': 'normal', - 'text-align': 'initial', - }, - 'mjx-html-holder': { - display: 'block', - position: 'absolute', - width: '100%', - height: '100%' - } - }; - - /** - * @override - */ - public toSVG(parents: N[]) { - const metrics = this.jax.math.metrics; - const em = metrics.em * metrics.scale; - const scale = this.fixed(1 / em); - const {w, h, d} = this.getBBox(); - this.dom = [this.adaptor.append(parents[0], this.svg('foreignObject', { - 'data-mjx-html': true, - y: this.jax.fixed(-h * em) + 'px', - width: this.jax.fixed(w * em) + 'px', - height: this.jax.fixed((h + d) * em) + 'px', - transform: `scale(${scale}) matrix(1 0 0 -1 0 0)` - }, [this.getHTML()])) as N]; - } - }; })(); diff --git a/ts/output/svg/Wrappers/semantics.ts b/ts/output/svg/Wrappers/semantics.ts index 4ddd85a54..386e8360e 100644 --- a/ts/output/svg/Wrappers/semantics.ts +++ b/ts/output/svg/Wrappers/semantics.ts @@ -27,11 +27,13 @@ import {SvgWrapper, SvgWrapperClass} from '../Wrapper.js'; import {SvgWrapperFactory} from '../WrapperFactory.js'; import {SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass} from '../FontData.js'; import {CommonSemantics, CommonSemanticsClass, CommonSemanticsMixin} from '../../common/Wrappers/semantics.js'; +import {CommonXmlNode, CommonXmlNodeClass, CommonXmlNodeMixin} from '../../common/Wrappers/XmlNode.js'; import {MmlNode} from '../../../core/MmlTree/MmlNode.js'; -import {BBox} from '../../../util/BBox.js'; import {MmlSemantics, MmlAnnotation, MmlAnnotationXML} from '../../../core/MmlTree/MmlNodes/semantics.js'; import {XMLNode} from '../../../core/MmlTree/MmlNode.js'; import {StyleList} from '../../../util/StyleList.js'; +import {StyleList as Styles} from '../../../util/Styles.js'; + /*****************************************************************/ /** @@ -166,11 +168,51 @@ export const SvgAnnotationXML = (function (): SvgWrapperClass /*****************************************************************/ +/** + * The SvgXmlNode interface for the SVG XmlNode wrapper + * + * @template N The HTMLElement node class + * @template T The Text node class + * @template D The Document class + */ +export interface SvgXmlNodeNTD extends SvgWrapper, CommonXmlNode< + N, T, D, + SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, + SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass +> {} + +/** + * The SvgXmlNodeClass interface for the SVG XmlNode wrapper + * + * @template N The HTMLElement node class + * @template T The Text node class + * @template D The Document class + */ +export interface SvgXmlNodeClass extends SvgWrapperClass, CommonXmlNodeClass< + N, T, D, + SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, + SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass +> { + new(factory: SvgWrapperFactory, node: MmlNode, parent?: SvgWrapper): SvgXmlNodeNTD; +} + /** * The SvgXmlNode wrapper for the XMLNode object */ -export const SvgXmlNode = (function (): SvgWrapperClass { - return class SvgXmlNode extends SvgWrapper { + +export const SvgXmlNode = (function (): SvgXmlNodeClass { + + const Base = CommonXmlNodeMixin< + N, T, D, + SVG, SvgWrapper, SvgWrapperFactory, SvgWrapperClass, + SvgCharOptions, SvgVariantData, SvgDelimiterData, SvgFontData, SvgFontDataClass, + SvgXmlNodeClass + >(SvgWrapper); + + // Avoid message about base constructors not having the same type + // (they should both be SvgWrapper, but are thought of as different by typescript) + // @ts-ignore + return class SvgXmlNode extends Base implements SvgXmlNodeNTD { /** * The XMLNode wrapper @@ -178,17 +220,22 @@ export const SvgXmlNode = (function (): SvgWrapperClass { public static kind = XMLNode.prototype.kind; /** - * Don't include inline-block CSS for this element + * @override */ - public static autoStyle = false; + public static styles: StyleList = { + 'foreignObject[data-mjx-html]': { + overflow: 'visible' + }, + ...Base.styles + }; /** * @override */ public toSVG(parents: N[]) { - const xml = this.adaptor.clone((this.node as XMLNode).getXML() as N); - const em = this.jax.math.metrics.em * this.jax.math.metrics.scale; - const scale = this.fixed(1 / em); + const metrics = this.jax.math.metrics; + const em = metrics.em * metrics.scale * this.rscale; + const scale = this.fixed(1 / em, 3); const {w, h, d} = this.getBBox(); this.dom = [this.adaptor.append(parents[0], this.svg('foreignObject', { 'data-mjx-xml': true, @@ -196,34 +243,24 @@ export const SvgXmlNode = (function (): SvgWrapperClass { width: this.jax.fixed(w * em) + 'px', height: this.jax.fixed((h + d) * em) + 'px', transform: `scale(${scale}) matrix(1 0 0 -1 0 0)` - }, [xml])) as N]; + }, [this.getHTML()])) as N]; } /** * @override */ - public computeBBox(bbox: BBox, _recompute: boolean = false) { - const {w, h, d} = this.jax.measureXMLnode((this.node as XMLNode).getXML() as N); - bbox.w = w; - bbox.h = h; - bbox.d = d; + public addHDW(html: N, styles: Styles): N { + html = this.html('mjx-html-holder', {style: styles}, [html]); + const {h, d, w} = this.getBBox(); + const scale = this.metrics.scale; + styles.height = this.em((h + d) * scale); + styles.width = this.em(w * scale); + styles['vertical-align'] = this.em(-d * scale); + styles.position = 'relative'; + delete styles['font-size'], styles['font-family']; + return html; } - /** - * @override - */ - protected getStyles() {} - - /** - * @override - */ - protected getScale() {} - - /** - * @override - */ - protected getVariant() {} - }; })(); From 70ae074107c98cb111b68611611332ae128aca4f Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Sat, 4 Nov 2023 15:55:01 -0400 Subject: [PATCH 2/5] Fix SVG output for scaling less than 100% --- ts/output/svg/Wrappers/semantics.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/output/svg/Wrappers/semantics.ts b/ts/output/svg/Wrappers/semantics.ts index 386e8360e..0d9960ed3 100644 --- a/ts/output/svg/Wrappers/semantics.ts +++ b/ts/output/svg/Wrappers/semantics.ts @@ -256,7 +256,6 @@ export const SvgXmlNode = (function (): SvgXmlNodeClass { styles.height = this.em((h + d) * scale); styles.width = this.em(w * scale); styles['vertical-align'] = this.em(-d * scale); - styles.position = 'relative'; delete styles['font-size'], styles['font-family']; return html; } From e6d6e1ac64aaac96e84bfe758551a8bcc9ab43ea Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Sun, 5 Nov 2023 08:35:19 -0500 Subject: [PATCH 3/5] Add the new XmlNode common wrapper file that was left out of the initial commit. --- ts/output/common/Wrappers/XmlNode.ts | 313 +++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 ts/output/common/Wrappers/XmlNode.ts diff --git a/ts/output/common/Wrappers/XmlNode.ts b/ts/output/common/Wrappers/XmlNode.ts new file mode 100644 index 000000000..821c0f100 --- /dev/null +++ b/ts/output/common/Wrappers/XmlNode.ts @@ -0,0 +1,313 @@ +/************************************************************* + * + * Copyright (c) 2023 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 CommonXmlNode wrapper mixin for the XmlNode object + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +import {CommonWrapper, CommonWrapperClass, CommonWrapperConstructor} from '../Wrapper.js'; +import {CommonWrapperFactory} from '../WrapperFactory.js'; +import {CharOptions, VariantData, DelimiterData, FontData, FontDataClass} from '../FontData.js'; +import {CommonOutputJax, ExtendedMetrics, UnknownBBox} from '../../common.js'; +import {MmlNode, XMLNode} from '../../../core/MmlTree/MmlNode.js'; +import {BBox} from '../../../util/BBox.js'; +import {StyleList as Styles} from '../../../util/Styles.js'; +import {StyleList} from '../../../util/StyleList.js'; +import {split} from '../../../util/string.js'; + +/*****************************************************************/ +/** + * The CommonXmlNode interface + * + * @template N The DOM node type + * @template T The DOM text node type + * @template D The DOM document type + * @template JX The OutputJax type + * @template WW The Wrapper type + * @template WF The WrapperFactory type + * @template WC The WrapperClass type + * @template CC The CharOptions type + * @template VV The VariantData type + * @template DD The DelimiterData type + * @template FD The FontData type + * @template FC The FontDataClass type + */ +export interface CommonXmlNode< + N, T, D, + JX extends CommonOutputJax, + WW extends CommonWrapper, + WF extends CommonWrapperFactory, + WC extends CommonWrapperClass, + CC extends CharOptions, + VV extends VariantData, + DD extends DelimiterData, + FD extends FontData, + FC extends FontDataClass +> extends CommonWrapper { + + /** + * The relative scaling from the root to this node + */ + rscale: number; + + /** + * @return {N} The HTML for the node + * @param {boolean?} forBBox True when the HTML is for computing the bounding box + */ + getHTML(forBBox?: boolean): N; + + /** + * @param {N} html The html to adjust if using or forcing HDW + * @param {Styles} styles The styles object to add to, as needed + * @param {boolean} forBBox True when the HTML is for computing the bounding box + */ + addHDW(html: N, styles: Styles, forBBox: boolean): N; + + /** + * @param {N} xml The XML tree to check + * @param {string} use The first xmlHDW value to check + * @param {string} force The second (optional) xmlHDW value to check + * @return {string} The data-mjx-hdw value, if the options are met + */ + getHDW(xml: N, use: string, force?: string): string; + + /** + * @param {string} hdw The data-mjx-hdw string to split + * @return {UnknownBBox} The h, d, w values (in em units) as an object + */ + splitHDW(hdw: string): UnknownBBox; + +} + +/** + * The CommonXmlNodeClass interface + * + * @template N The DOM node type + * @template T The DOM text node type + * @template D The DOM document type + * @template JX The OutputJax type + * @template WW The Wrapper type + * @template WF The WrapperFactory type + * @template WC The WrapperClass type + * @template CC The CharOptions type + * @template VV The VariantData type + * @template DD The DelimiterData type + * @template FD The FontData type + * @template FC The FontDataClass type + */ +export interface CommonXmlNodeClass< + N, T, D, + JX extends CommonOutputJax, + WW extends CommonWrapper, + WF extends CommonWrapperFactory, + WC extends CommonWrapperClass, + CC extends CharOptions, + VV extends VariantData, + DD extends DelimiterData, + FD extends FontData, + FC extends FontDataClass +> extends CommonWrapperClass {} + +/*****************************************************************/ +/** + * The CommonXmlNode wrapper mixin for the XmlNode object + * + * @template N The DOM node type + * @template T The DOM text node type + * @template D The DOM document type + * @template JX The OutputJax type + * @template WW The Wrapper type + * @template WF The WrapperFactory type + * @template WC The WrapperClass type + * @template CC The CharOptions type + * @template VV The VariantData type + * @template DD The DelimiterData type + * @template FD The FontData type + * @template FC The FontDataClass type + * + * @template B The Mixin interface to create + */ +export function CommonXmlNodeMixin< + N, T, D, + JX extends CommonOutputJax, + WW extends CommonWrapper, + WF extends CommonWrapperFactory, + WC extends CommonWrapperClass, + CC extends CharOptions, + VV extends VariantData, + DD extends DelimiterData, + FD extends FontData, + FC extends FontDataClass, + B extends CommonWrapperClass +>(Base: CommonWrapperConstructor): B { + + return class CommonXmlNodeMixin extends Base + implements CommonXmlNode { + + /** + * Don't set up inline-block styles for this + */ + public static autoStyle = false; + + /** + * @override + */ + public static styles: StyleList = { + 'mjx-measure-xml': { + position: 'absolute', + left: 0, top: 0, + display: 'inline-block', + 'line-height': 'normal', + 'white-space': 'normal' + }, + 'mjx-html': { + display: 'inline-block', + 'line-height': 'normal', + 'text-align': 'initial', + 'white-space': 'initial' + }, + 'mjx-html-holder': { + display: 'block', + position: 'absolute', + top: 0, left: 0, bottom: 0, right: 0 + } + }; + + /** + * @override + */ + public rscale: number; + + /** + * @override + */ + public constructor(factory: WF, node: MmlNode, parent: WW = null) { + super(factory, node, parent); + this.rscale = this.getRScale(); + } + + /** + * @override + */ + public computeBBox(bbox: BBox, _recompute: boolean = false) { + const xml = (this.node as XMLNode).getXML() as N; + const hdw = this.getHDW(xml, 'use', 'force'); + const {h, d, w} = (hdw ? this.splitHDW(hdw) : this.measureXmlNode(xml)); + bbox.w = w; + bbox.h = h; + bbox.d = d; + } + + /** + * @override + */ + public getHTML(): N { + const adaptor = this.adaptor; + let html = adaptor.clone((this.node as XMLNode).getXML() as N); + const styles = this.getFontStyles(); + const hdw = this.getHDW(html, 'force'); + if (hdw || this.jax.options.scale !== 1) { + html = this.addHDW(html, styles); + } + return this.html('mjx-html', {variant: this.parent.variant, style: styles}, [html]); + } + + /** + * Implemented in subclasses + * @override + */ + public addHDW(html: N, _styles: Styles): N { + return html; + } + + /** + * @override + */ + public getHDW(xml: N, use: string, force: string = use): string { + const option = this.jax.options.htmlHDW; + const hdw = this.adaptor.getAttribute(xml, 'data-mjx-hdw') as string; + return (hdw && (option === use || option === force) ? hdw : null); + } + + /** + * @override + */ + public splitHDW(hdw: string): UnknownBBox { + const scale = 1 / this.metrics.scale; + const [h, d, w] = split(hdw).map(x => this.length2em(x || '0') * scale); + return {h, d, w}; + } + + /** + * The font-size and font-family values to use for the XML + */ + public getFontStyles() { + const adaptor = this.adaptor; + const metrics = this.metrics as ExtendedMetrics; + return { + 'font-family': this.parent.styles?.get('font-family') || + metrics.family || adaptor.fontFamily(adaptor.parent(this.jax.math.start.node)) || 'initial', + 'font-size': this.jax.fixed(metrics.em * this.rscale) + 'px' + }; + } + + /** + * Measure the width, height and depth of an annotation-xml node's content + * + * @param{N} xml The xml content node to be measured + * @return {UnknownBBox} The width, height, and depth of the content + */ + public measureXmlNode(xml: N): UnknownBBox { + const adaptor = this.adaptor; + const content = this.html('mjx-xml-block', {style: {display: 'inline-block'}}, [adaptor.clone(xml)]); + const base = this.html('mjx-baseline', {style: {display: 'inline-block', width: 0, height: 0}}); + const style = this.getFontStyles(); + const node = this.html('mjx-measure-xml', {style}, [base, content]); + const container = this.jax.container; + adaptor.append(adaptor.parent(this.jax.math.start.node), container); + adaptor.append(container, node); + const metrics = this.metrics; + const em = metrics.em * metrics.scale * this.rscale; + const {left, right, bottom, top} = adaptor.nodeBBox(content); + const w = (right - left) / em; + const h = (adaptor.nodeBBox(base).top - top) / em; + const d = (bottom - top) / em - h; + adaptor.remove(container); + adaptor.remove(node); + return {w, h, d}; + } + + /** + * @override + */ + protected getStyles() {} + + /** + * @override + */ + protected getScale() {} + + /** + * @override + */ + protected getVariant() {} + + } as any as B; + +} From e2acc7f8a9c820d9ac57e7f7da13038428e14a2a Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Sun, 5 Nov 2023 11:18:44 -0500 Subject: [PATCH 4/5] Remove unused argument from interface for addHDW() and getHTML() --- ts/output/common/Wrappers/XmlNode.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ts/output/common/Wrappers/XmlNode.ts b/ts/output/common/Wrappers/XmlNode.ts index 821c0f100..9d6145c7e 100644 --- a/ts/output/common/Wrappers/XmlNode.ts +++ b/ts/output/common/Wrappers/XmlNode.ts @@ -68,16 +68,14 @@ export interface CommonXmlNode< /** * @return {N} The HTML for the node - * @param {boolean?} forBBox True when the HTML is for computing the bounding box */ - getHTML(forBBox?: boolean): N; + getHTML(): N; /** * @param {N} html The html to adjust if using or forcing HDW * @param {Styles} styles The styles object to add to, as needed - * @param {boolean} forBBox True when the HTML is for computing the bounding box */ - addHDW(html: N, styles: Styles, forBBox: boolean): N; + addHDW(html: N, styles: Styles): N; /** * @param {N} xml The XML tree to check From 7c8792791d9d1deef540d64ca3a91c8600abed50 Mon Sep 17 00:00:00 2001 From: "Davide P. Cervone" Date: Wed, 20 Dec 2023 16:52:27 -0500 Subject: [PATCH 5/5] Make XmlNode mixin be an abstract class --- ts/output/common/Wrappers/XmlNode.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ts/output/common/Wrappers/XmlNode.ts b/ts/output/common/Wrappers/XmlNode.ts index 9d6145c7e..d310d468e 100644 --- a/ts/output/common/Wrappers/XmlNode.ts +++ b/ts/output/common/Wrappers/XmlNode.ts @@ -155,7 +155,7 @@ export function CommonXmlNodeMixin< B extends CommonWrapperClass >(Base: CommonWrapperConstructor): B { - return class CommonXmlNodeMixin extends Base + abstract class CommonXmlNodeMixin extends Base implements CommonXmlNode { /** @@ -227,12 +227,9 @@ export function CommonXmlNodeMixin< } /** - * Implemented in subclasses * @override */ - public addHDW(html: N, _styles: Styles): N { - return html; - } + abstract addHDW(html: N, _styles: Styles): N; /** * @override @@ -306,6 +303,8 @@ export function CommonXmlNodeMixin< */ protected getVariant() {} - } as any as B; + }; + + return CommonXmlNodeMixin as any as B; }