diff --git a/ts/a11y/complexity/collapse.ts b/ts/a11y/complexity/collapse.ts index 24ce13109..eb2a5abf3 100644 --- a/ts/a11y/complexity/collapse.ts +++ b/ts/a11y/complexity/collapse.ts @@ -26,6 +26,7 @@ import { AbstractMmlTokenNode, TextNode, } from '../../core/MmlTree/MmlNode.js'; +import { PropertyList } from '../../core/Tree/Node.js'; import { ComplexityVisitor } from './visitor.js'; /*==========================================================================*/ @@ -584,9 +585,10 @@ export class Collapse { const factory = this.complexity.factory; const marker = node.getProperty('collapse-marker') as string; const parent = node.parent; - const variant = node.getProperty('collapse-variant') - ? { mathvariant: '-tex-variant' } - : {}; + const variant = { 'data-mjx-collapsed': true } as PropertyList; + if (node.getProperty('collapse-variant')) { + variant.mathvariant = '-tex-variant'; + } const maction = factory.create( 'maction', { diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts index 89c03b0d3..475548cfd 100644 --- a/ts/a11y/explorer.ts +++ b/ts/a11y/explorer.ts @@ -413,7 +413,7 @@ export function ExplorerMathDocumentMixin< fill: 'white', }, 'mjx-help > svg > circle:nth-child(2)': { - fill: 'rgba(0, 0, 255, 0.2)', + fill: 'var(--mjx-bg1-color)', r: '7px', }, 'mjx-help > svg > line': { diff --git a/ts/a11y/explorer/ExplorerPool.ts b/ts/a11y/explorer/ExplorerPool.ts index f497f1575..e631eb8fc 100644 --- a/ts/a11y/explorer/ExplorerPool.ts +++ b/ts/a11y/explorer/ExplorerPool.ts @@ -30,7 +30,6 @@ import * as me from './MouseExplorer.js'; import { TreeColorer, FlameColorer } from './TreeExplorer.js'; import { Highlighter, getHighlighter } from './Highlighter.js'; -// import * as Sre from '../sre.js'; /** * The regions objects needed for the explorers. @@ -360,6 +359,7 @@ export class ExplorerPool { protected setPrimaryHighlighter() { const [foreground, background] = this.colorOptions(); this._highlighter = getHighlighter( + LiveRegion.priority.primary, background, foreground, this.document.outputJax.name @@ -371,6 +371,7 @@ export class ExplorerPool { */ protected setSecondaryHighlighter() { this.secondaryHighlighter = getHighlighter( + LiveRegion.priority.secondary, { color: 'red' }, { color: 'black' }, this.document.outputJax.name diff --git a/ts/a11y/explorer/Highlighter.ts b/ts/a11y/explorer/Highlighter.ts index c3e00869b..82c1074e9 100644 --- a/ts/a11y/explorer/Highlighter.ts +++ b/ts/a11y/explorer/Highlighter.ts @@ -18,36 +18,31 @@ * @author v.sorge@mathjax.org (Volker Sorge) */ -interface NamedColor { +import { LiveRegion } from './Region.js'; + +export interface NamedColor { color: string; alpha?: number; - type?: string; } /** - * Turns a named color into a channel color. - * - * @param {NamedColor} color The definition. - * @param {NamedColor} deflt The default color name if the named color does not exist. - * @returns {string} The channel color. + * The default background color if a none existing color is provided. */ -function getColorString(color: NamedColor, deflt: NamedColor): string { - const type = deflt.type; - const name = color.color ?? deflt.color; - const opacity = color.alpha ?? deflt.alpha; - const alpha = opacity === 1 ? 1 : `var(--mjx-${type}-alpha)`; - return `rgba(var(--mjx-${type}-${name}), ${alpha})`; -} +const DEFAULT_BACKGROUND: NamedColor = { color: 'blue', alpha: 0.2 }; /** - * The default background color if a none existing color is provided. + * The default color if a none existing color is provided. */ -const DEFAULT_BACKGROUND: NamedColor = { color: 'blue', alpha: 1, type: 'bg' }; +const DEFAULT_FOREGROUND: NamedColor = { color: 'black', alpha: 1 }; /** - * The default color if a none existing color is provided. + * The attributes for various markers */ -const DEFAULT_FOREGROUND: NamedColor = { color: 'black', alpha: 1, type: 'fg' }; +export const ATTR = { + ENCLOSED: 'data-sre-enclosed', + BBOX: 'data-sre-highlighter-bbox', + ADDED: 'data-sre-highlighter-added', +}; export interface Highlighter { /** @@ -92,14 +87,12 @@ export interface Highlighter { isMactionNode(node: Element): boolean; /** - * @returns {string} The foreground color as rgba string. - */ - get foreground(): string; - - /** - * @returns {string} The background color as rgba string. + * Returns the maction sub nodes of a given node. + * + * @param {HTMLElement} node The root node. + * @returns {HTMLElement[]} The list of maction sub nodes. */ - get background(): string; + getMactionNodes(node: HTMLElement): HTMLElement[]; /** * Sets of the color the highlighter is using. @@ -110,86 +103,63 @@ export interface Highlighter { setColor(background: NamedColor, foreground: NamedColor): void; } -/** - * Highlight information consisting of node, fore and background color. - */ -interface Highlight { - node: HTMLElement; - background?: string; - foreground?: string; -} - -let counter = 0; - abstract class AbstractHighlighter implements Highlighter { - /** - * This counter creates a unique highlighter name. This is important in case - * we have more than a single highlighter on a node, e.g., during auto voicing - * with synchronised highlighting. - */ - public counter = counter++; - /** * The Attribute for marking highlighted nodes. */ - protected ATTR = 'data-sre-highlight-' + this.counter.toString(); - - /** - * The foreground color. - */ - private _foreground: string; + protected ATTR: string; /** - * The background color. + * The CSS selector to use to find the line-box container. */ - private _background: string; + protected static lineSelector: string; /** - * The maction name/class for a highlighter. + * The attribute name for the line number. */ - protected mactionName = ''; + protected static lineAttr: string; /** - * The CSS selector to use to find the line-box container. + * Primary highlighter = 1, secondary highlighter = 2 */ - protected static lineSelector = ''; + protected priority: number; /** - * The attribute name for the line number. + * List of currently highlighted nodes and their original background color. */ - protected static lineAttr = ''; + private currentHighlights: HTMLElement[][] = []; /** - * List of currently highlighted nodes and their original background color. + * @param {number} priority 1 = primary, 2 = secondary */ - private currentHighlights: Highlight[][] = []; + constructor(priority: number) { + this.priority = priority; + this.ATTR = 'data-sre-highlight-' + priority; + } /** * Highlights a single node. * - * @param node The node to be highlighted. - * @returns The old node information. + * @param {HTMLElement} node The node to be highlighted. */ - protected abstract highlightNode(node: HTMLElement): Highlight; + protected abstract highlightNode(node: HTMLElement): void; /** * Unhighlights a single node. * - * @param highlight The highlight info for the node to be unhighlighted. + * @param {HTMLElement} node The highlight info for the node to be unhighlighted. */ - protected abstract unhighlightNode(highlight: Highlight): void; + protected abstract unhighlightNode(node: HTMLElement): void; /** * @override */ public highlight(nodes: HTMLElement[]) { - this.currentHighlights.push( - nodes.map((node) => { - const info = this.highlightNode(node); - this.setHighlighted(node); - return info; - }) - ); + this.currentHighlights.push(nodes); + for (const node of nodes) { + this.highlightNode(node); + this.setHighlighted(node); + } } /** @@ -197,7 +167,7 @@ abstract class AbstractHighlighter implements Highlighter { */ public highlightAll(node: HTMLElement) { const mactions = this.getMactionNodes(node); - for (let i = 0, maction; (maction = mactions[i]); i++) { + for (const maction of mactions) { this.highlight([maction]); } } @@ -210,10 +180,10 @@ abstract class AbstractHighlighter implements Highlighter { if (!nodes) { return; } - nodes.forEach((highlight: Highlight) => { - if (this.isHighlighted(highlight.node)) { - this.unhighlightNode(highlight); - this.unsetHighlighted(highlight.node); + nodes.forEach((node: HTMLElement) => { + if (this.isHighlighted(node)) { + this.unhighlightNode(node); + this.unsetHighlighted(node); } }); } @@ -270,7 +240,7 @@ abstract class AbstractHighlighter implements Highlighter { if (list.length > 1) { let [L, T, R, B] = [Infinity, Infinity, -Infinity, -Infinity]; for (const part of list) { - part.setAttribute('data-mjx-enclosed', 'true'); + part.setAttribute(ATTR.ENCLOSED, 'true'); const { left, top, right, bottom } = part.getBoundingClientRect(); if (top === bottom && left === right) continue; if (left < L) L = left; @@ -293,25 +263,22 @@ abstract class AbstractHighlighter implements Highlighter { } /** - * @override - */ - public setColor(background: NamedColor, foreground: NamedColor) { - this._foreground = getColorString(foreground, DEFAULT_FOREGROUND); - this._background = getColorString(background, DEFAULT_BACKGROUND); - } - - /** - * @override + * @param {string} type fg or bg + * @param {NamedColor} color The color to set + * @param {NamedColor} def The defaults to use for missing parts of the color */ - public get foreground(): string { - return this._foreground; + protected setColorCSS(type: string, color: NamedColor, def: NamedColor) { + const name = color.color ?? def.color; + const alpha = color.alpha ?? def.alpha; + LiveRegion.setColor(type, this.priority, name, alpha); } /** * @override */ - public get background(): string { - return this._background; + public setColor(background: NamedColor, foreground: NamedColor) { + this.setColorCSS('fg', foreground, DEFAULT_FOREGROUND); + this.setColorCSS('bg', background, DEFAULT_BACKGROUND); } /** @@ -320,19 +287,12 @@ abstract class AbstractHighlighter implements Highlighter { * @param {HTMLElement} node The root node. * @returns {HTMLElement[]} The list of maction sub nodes. */ - public getMactionNodes(node: HTMLElement): HTMLElement[] { - return Array.from( - node.getElementsByClassName(this.mactionName) - ) as HTMLElement[]; - } + public abstract getMactionNodes(node: HTMLElement): HTMLElement[]; /** * @override */ - public isMactionNode(node: Element): boolean { - const className = node.className || node.getAttribute('class'); - return className ? !!className.match(new RegExp(this.mactionName)) : false; - } + public abstract isMactionNode(node: Element): boolean; /** * Check if a node is already highlighted. @@ -360,6 +320,7 @@ abstract class AbstractHighlighter implements Highlighter { */ public unsetHighlighted(node: HTMLElement) { node.removeAttribute(this.ATTR); + node.removeAttribute(ATTR.ENCLOSED); } } @@ -367,103 +328,45 @@ class SvgHighlighter extends AbstractHighlighter { protected static lineSelector = '[data-mjx-linebox]'; protected static lineAttr = 'data-mjx-lineno'; - /** - * @override - */ - constructor() { - super(); - this.mactionName = 'maction'; - } - /** * @override */ public highlightNode(node: HTMLElement) { - let info: Highlight; - if (this.isHighlighted(node)) { - info = { - node: node, - background: this.background, - foreground: this.foreground, - }; - return info; - } - if (node.tagName === 'svg' || node.tagName === 'MJX-CONTAINER') { - info = { - node: node, - background: node.style.backgroundColor, - foreground: node.style.color, - }; - if (!node.hasAttribute('data-mjx-enclosed')) { - node.style.backgroundColor = this.background; - } - node.style.color = this.foreground; - return info; - } - if (node.hasAttribute('data-sre-highlighter-bbox')) { - node.setAttribute(this.ATTR, 'true'); - node.setAttribute('fill', this.background); - return { node: node, foreground: 'none' }; - } - if (!node.hasAttribute('data-mjx-enclosed')) { - const { x, y, width, height } = ( - node as any as SVGGraphicsElement - ).getBBox(); - const rect = this.createRect( - x, - y, - width, - height, - node.getAttribute('transform') - ); - rect.setAttribute('fill', this.background); - node.parentNode.insertBefore(rect, node); - } - node.setAttribute(this.ATTR, 'true'); - info = { node: node, foreground: node.getAttribute('fill') }; - if (node.nodeName !== 'rect') { - // We currently do not change foreground of collapsed nodes. - node.setAttribute('fill', this.foreground); - } - return info; - } - - /** - * @override - */ - public setHighlighted(node: HTMLElement) { - if (node.tagName === 'svg') { - super.setHighlighted(node); + if ( + this.isHighlighted(node) || + node.tagName === 'svg' || + node.tagName === 'MJX-CONTAINER' || + node.hasAttribute(ATTR.BBOX) || + node.hasAttribute(ATTR.ENCLOSED) + ) { + return; } + const { x, y, width, height } = ( + node as any as SVGGraphicsElement + ).getBBox(); + const rect = this.createRect( + x, + y, + width, + height, + node.getAttribute('transform') + ); + this.setHighlighted(rect); + node.parentNode.insertBefore(rect, node); } /** * @override */ - public unhighlightNode(info: Highlight) { - const node = info.node; - if (node.hasAttribute('data-sre-highlighter-bbox')) { + public unhighlightNode(node: HTMLElement) { + if (node.hasAttribute(ATTR.BBOX)) { node.remove(); return; } - if (node.tagName === 'svg' || node.tagName === 'MJX-CONTAINER') { - if (!node.hasAttribute('data-mjx-enclosed')) { - node.style.backgroundColor = info.background; - } - node.removeAttribute('data-mjx-enclosed'); - node.style.color = info.foreground; - return; - } const previous = node.previousSibling as HTMLElement; - if (previous?.hasAttribute('data-sre-highlighter-added')) { + if (previous?.hasAttribute(ATTR.ADDED)) { previous.remove(); } - node.removeAttribute('data-mjx-enclosed'); - if (info.foreground) { - node.setAttribute('fill', info.foreground); - } else { - node.removeAttribute('fill'); - } } /** @@ -486,7 +389,7 @@ class SvgHighlighter extends AbstractHighlighter { y2 - y1, part.getAttribute('transform') ); - rect.setAttribute('data-sre-highlighter-bbox', 'true'); + rect.setAttribute(ATTR.BBOX, 'true'); part.parentNode.insertBefore(rect, part); return rect; } @@ -526,10 +429,7 @@ class SvgHighlighter extends AbstractHighlighter { ): HTMLElement { const padding = 40; const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - rect.setAttribute( - 'data-sre-highlighter-added', // Mark highlighting rect. - 'true' - ); + rect.setAttribute(ATTR.ADDED, 'true'); // Mark highlighting rect. rect.setAttribute('x', String(x - padding)); rect.setAttribute('y', String(y - padding)); rect.setAttribute('width', String(w + 2 * padding)); @@ -544,16 +444,14 @@ class SvgHighlighter extends AbstractHighlighter { * @override */ public isMactionNode(node: HTMLElement) { - return node.getAttribute('data-mml-node') === this.mactionName; + return node.getAttribute('data-mml-node') === 'maction'; } /** * @override */ - public getMactionNodes(node: HTMLElement) { - return Array.from( - node.querySelectorAll(`[data-mml-node="${this.mactionName}"]`) - ) as HTMLElement[]; + public getMactionNodes(node: HTMLElement): HTMLElement[] { + return Array.from(node.querySelectorAll('[data-mml-node="maction"]')); } } @@ -564,37 +462,12 @@ class ChtmlHighlighter extends AbstractHighlighter { /** * @override */ - constructor() { - super(); - this.mactionName = 'mjx-maction'; - } - - /** - * @override - */ - public highlightNode(node: HTMLElement) { - const info = { - node: node, - background: node.style.backgroundColor, - foreground: node.style.color, - }; - if (!this.isHighlighted(node)) { - if (!node.hasAttribute('data-mjx-enclosed')) { - node.style.backgroundColor = this.background; - } - node.style.color = this.foreground; - } - return info; - } + public highlightNode(_node: HTMLElement) {} /** * @override */ - public unhighlightNode(info: Highlight) { - const node = info.node; - node.style.backgroundColor = info.background; - node.style.color = info.foreground; - node.removeAttribute('data-mjx-enclosed'); + public unhighlightNode(node: HTMLElement) { if (node.tagName.toLowerCase() === 'mjx-bbox') { node.remove(); } @@ -625,16 +498,14 @@ class ChtmlHighlighter extends AbstractHighlighter { * @override */ public isMactionNode(node: HTMLElement) { - return node.tagName?.toUpperCase() === this.mactionName.toUpperCase(); + return node.tagName?.toLowerCase() === 'mjx-maction'; } /** * @override */ - public getMactionNodes(node: HTMLElement) { - return Array.from( - node.getElementsByTagName(this.mactionName) - ) as HTMLElement[]; + public getMactionNodes(node: HTMLElement): HTMLElement[] { + return Array.from(node.querySelectorAll('mjx-maction')); } } @@ -642,17 +513,19 @@ class ChtmlHighlighter extends AbstractHighlighter { * Highlighter factory that returns the highlighter that goes with the current * Mathjax renderer. * + * @param {number} priority 1 = primary, 2 = secondary highlighter. * @param {NamedColor} back A background color specification. * @param {NamedColor} fore A foreground color specification. * @param {string} renderer The renderer name. * @returns {Highlighter} A new highlighter. */ export function getHighlighter( + priority: number, back: NamedColor, fore: NamedColor, renderer: string ): Highlighter { - const highlighter = new highlighterMapping[renderer](); + const highlighter = new highlighterMapping[renderer](priority); highlighter.setColor(back, fore); return highlighter; } @@ -660,7 +533,9 @@ export function getHighlighter( /** * Mapping renderer names to highlighter constructor. */ -const highlighterMapping: { [key: string]: new () => Highlighter } = { +const highlighterMapping: { + [key: string]: new (priority: number) => Highlighter; +} = { SVG: SvgHighlighter, CHTML: ChtmlHighlighter, generic: ChtmlHighlighter, diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts index 106ea23ba..559199ec1 100644 --- a/ts/a11y/explorer/KeyExplorer.ts +++ b/ts/a11y/explorer/KeyExplorer.ts @@ -1090,12 +1090,14 @@ export class SpeechExplorer const parts = [...this.getSplitNodes(this.current)]; this.highlighter.encloseNodes(parts, this.node); for (const part of parts) { - if (!part.getAttribute('data-mjx-enclosed')) { + if (!part.getAttribute('data-sre-enclosed')) { part.classList.add('mjx-selected'); } } this.pool.highlight(parts); this.addSpeech(node, addDescription); + this.node.setAttribute('tabindex', '-1'); + this.Update(); } // // Done making changes @@ -1159,6 +1161,12 @@ export class SpeechExplorer * @param {boolean} describe True if the description should be added */ protected addSpeech(node: HTMLElement, describe: boolean) { + if ( + !this.document.options.enableSpeech && + !this.document.options.enableBraille + ) { + return; + } if (this.anchors.length) { setTimeout(() => this.img?.remove(), 10); } else { @@ -1187,7 +1195,6 @@ export class SpeechExplorer node.getAttribute(SemAttr.BRAILLE), this.SsmlAttributes(node, SemAttr.SPEECH_SSML) ); - this.node.setAttribute('tabindex', '-1'); } /** @@ -1754,13 +1761,13 @@ export class SpeechExplorer const options = this.document.options; const a11y = options.a11y; if (a11y.subtitles && a11y.speech && options.enableSpeech) { - this.region.Show(this.node, this.highlighter); + this.region.Show(this.node); } if (a11y.viewBraille && a11y.braille && options.enableBraille) { - this.brailleRegion.Show(this.node, this.highlighter); + this.brailleRegion.Show(this.node); } if (a11y.keyMagnifier) { - this.magnifyRegion.Show(this.current, this.highlighter); + this.magnifyRegion.Show(this.current); } this.Update(); } @@ -1792,11 +1799,16 @@ export class SpeechExplorer public Update() { if (!this.active) return; this.region.node = this.node; - this.generators.updateRegions( - this.speech || this.node, - this.region, - this.brailleRegion - ); + if ( + this.document.options.enableSpeech || + this.document.options.enableBraille + ) { + this.generators.updateRegions( + this.speech || this.node, + this.region, + this.brailleRegion + ); + } this.magnifyRegion.Update(this.current); } diff --git a/ts/a11y/explorer/MouseExplorer.ts b/ts/a11y/explorer/MouseExplorer.ts index 056b4b7a5..59c8f361e 100644 --- a/ts/a11y/explorer/MouseExplorer.ts +++ b/ts/a11y/explorer/MouseExplorer.ts @@ -136,7 +136,7 @@ export abstract class Hoverer extends AbstractMouseExplorer { this.highlighter.unhighlight(); this.highlighter.highlight([node]); this.region.Update(kind); - this.region.Show(node, this.highlighter); + this.region.Show(node); } /** diff --git a/ts/a11y/explorer/Region.ts b/ts/a11y/explorer/Region.ts index 1303fb7f2..170b7285b 100644 --- a/ts/a11y/explorer/Region.ts +++ b/ts/a11y/explorer/Region.ts @@ -23,7 +23,7 @@ import { MathDocument } from '../../core/MathDocument.js'; import { StyleJsonSheet } from '../../util/StyleJson.js'; -import { Highlighter, getHighlighter } from './Highlighter.js'; +import { Highlighter } from './Highlighter.js'; import { SsmlElement, buildSpeech } from '../speech/SpeechUtil.js'; export type A11yDocument = MathDocument; @@ -43,9 +43,8 @@ export interface Region { * Shows the live region in the document. * * @param {HTMLElement} node - * @param {Highlighter} highlighter */ - Show(node: HTMLElement, highlighter: Highlighter): void; + Show(node: HTMLElement): void; /** * Takes the element out of the document flow. @@ -117,6 +116,13 @@ export abstract class AbstractRegion implements Region { return 'MJX-' + this.name + '-styles'; } + /** + * @returns {HTMLStyleElement} The stylesheet for this region + */ + public static get styleSheet(): HTMLStyleElement { + return document.head.querySelector('#' + this.sheetId) as HTMLStyleElement; + } + /** * @override */ @@ -151,10 +157,9 @@ export abstract class AbstractRegion implements Region { /** * @override */ - public Show(node: HTMLElement, highlighter: Highlighter) { + public Show(node: HTMLElement) { this.AddElement(); this.position(node); - this.highlight(highlighter); this.div.classList.add(this.CLASS.className + '_Show'); } @@ -165,13 +170,6 @@ export abstract class AbstractRegion implements Region { */ protected abstract position(node: HTMLElement): void; - /** - * Highlights the region. - * - * @param {Highlighter} highlighter The Sre highlighter. - */ - protected abstract highlight(highlighter: Highlighter): void; - /** * @override */ @@ -260,11 +258,6 @@ export class DummyRegion extends AbstractRegion { * @override */ public position() {} - - /** - * @override - */ - public highlight(_highlighter: Highlighter) {} } export class StringRegion extends AbstractRegion { @@ -297,15 +290,6 @@ export class StringRegion extends AbstractRegion { protected position(node: HTMLElement) { this.stackRegions(node); } - - /** - * @override - */ - protected highlight(highlighter: Highlighter) { - if (!this.div) return; - this.inner.style.backgroundColor = highlighter.background; - this.inner.style.color = highlighter.foreground; - } } export class ToolTip extends StringRegion { @@ -318,7 +302,7 @@ export class ToolTip extends StringRegion { * @override */ protected static style: StyleJsonSheet = new StyleJsonSheet({ - ['.' + ToolTip.className]: { + [`.${ToolTip.className}`]: { width: 'auto', height: 'auto', opacity: 1, @@ -331,7 +315,7 @@ export class ToolTip extends StringRegion { 'background-color': 'white', 'z-index': 202, }, - ['.' + ToolTip.className + ' > div']: { + [`.${ToolTip.className} > div`]: { 'border-radius': 'inherit', padding: '0 2px', }, @@ -351,6 +335,11 @@ export class LiveRegion extends StringRegion { */ protected static className = 'MJX_LiveRegion'; + public static priority = { + primary: 1, + secondary: 2, + }; + /** * @override */ @@ -375,8 +364,14 @@ export class LiveRegion extends StringRegion { '--mjx-live-bg-color': 'white', '--mjx-live-shadow-color': '#888', '--mjx-live-border-color': '#CCCCCC', - '--mjx-bg-alpha': 0.2, - '--mjx-fg-alpha': 1, + '--mjx-bg1-color': 'rgba(var(--mjx-bg-blue), var(--mjx-bg-alpha))', + '--mjx-fg1-color': 'rgba(var(--mjx-fg-black), 1)', + '--mjx-bg2-color': 'rgba(var(--mjx-bg-red), 1)', + '--mjx-fg2-color': 'rgba(var(--mjx-fg-black), 1)', + '--mjx-bg1-alpha': 0.2, + '--mjx-fg1-alpha': 1, + '--mjx-bg2-alpha': 1, + '--mjx-fg2-alpha': 1, }, '@media (prefers-color-scheme: dark)': { ':root': { @@ -388,11 +383,13 @@ export class LiveRegion extends StringRegion { '--mjx-live-bg-color': '#222025', '--mjx-live-shadow-color': 'black', '--mjx-live-border-color': '#7C7C7C', - '--mjx-bg-alpha': 0.3, - '--mjx-fg-alpha': 1, + '--mjx-bg1-alpha': 0.3, + '--mjx-fg1-alpha': 1, + '--mjx-bg2-alpha': 1, + '--mjx-fg2-alpha': 1, }, }, - ['.' + LiveRegion.className]: { + [`.${LiveRegion.className}`]: { position: 'absolute', top: 0, display: 'none', @@ -408,27 +405,73 @@ export class LiveRegion extends StringRegion { 'box-shadow': '0px 5px 20px var(--mjx-live-shadow-color)', border: '2px solid var(--mjx-live-border-color)', }, - ['.' + LiveRegion.className + '_Show']: { + [`.${LiveRegion.className}_Show`]: { display: 'block', }, + [`.${LiveRegion.className} > div`]: { + color: 'var(--mjx-fg1-color)', + 'background-color': 'var(--mjx-bg1-color)', + }, + // + // Primary highlighting colors + // + 'mjx-container [data-sre-highlight-1]:not([data-mjx-collapsed], rect)': { + color: 'var(--mjx-fg1-color) ! important', // // CHTML + fill: 'var(--mjx-fg1-color) ! important', // // SVG + }, + [[ + 'mjx-container:not([data-mjx-clone-container])', + '[data-sre-highlight-1]:not([data-sre-enclosed], rect)', + ].join(' ')]: { + 'background-color': 'var(--mjx-bg1-color) ! important', // // CHTML + }, + 'mjx-container rect[data-sre-highlight-1]:not([data-sre-enclosed])': { + fill: 'var(--mjx-bg1-color) ! important', // // SVG + }, + // + // Secondary highlighting colors + // + 'mjx-container [data-sre-highlight-2]': { + color: 'var(--mjx-fg2-color) ! important', // // CHTML + 'background-color': 'var(--mjx-bg2-color) ! important', // // CHTML + fill: 'var(--mjx-fg2-color) ! important', // // SVG + }, + 'mjx-container rect[data-sre-highlight-2]': { + fill: 'var(--mjx-bg2-color) ! important', // // SVG + }, }); /** - * @param {string} type The type of alpha to set (fg or bg) - * @param {number} alpha The alpha value to use - * @param {Document} document The document whose CSS styles are to be adjusted - */ - public static setAlpha(type: string, alpha: number, document: Document) { - const style = document.head.querySelector( - '#' + this.sheetId - ) as HTMLStyleElement; + * Set the CSS styles for a given color type and priority + * + * @param {string} type The color type (fg or bg) + * @param {number} priority 1 = primary, 2 = secondary + * @param {string} color The color name (blue, red, black, etc.) + * @param {number} opacity The alpha channel for the color + */ + public static setColor( + type: string, + priority: number, + color: string, + opacity: number + ) { + const style = this.styleSheet; if (style) { - const name = `--mjx-${type}-alpha`; - (style.sheet.cssRules[0] as any).style.setProperty(name, alpha); - (style.sheet.cssRules[1] as any).cssRules[0].style.setProperty( - name, - alpha ** 0.7071 - ); + const css = (style.sheet.cssRules[0] as any).style; + const alpha = opacity === 1 ? 1 : `var(--mjx-${type}${priority}-alpha)`; + const name = `--mjx-${type}${priority}-color`; + const value = `rgba(var(--mjx-${type}-${color}), ${alpha})`; + if (css.getPropertyValue(name) !== value) { + css.setProperty(name, value); + } + const oname = `--mjx-${type}${priority}-alpha`; + if (css.getPropertyValue(name) !== String(opacity)) { + css.setProperty(oname, opacity); + (style.sheet.cssRules[1] as any).cssRules[0].style.setProperty( + oname, + opacity ** 0.7071 + ); + } } } } @@ -458,21 +501,17 @@ export class SpeechRegion extends LiveRegion { private clear: boolean = false; /** - * The highlighter to use. + * The highlighter to use. (Set by ExplorerPool) */ - public highlighter: Highlighter = getHighlighter( - { color: 'red' }, - { color: 'black' }, - this.document.outputJax.name - ); + public highlighter: Highlighter; /** * @override */ - public Show(node: HTMLElement, highlighter: Highlighter) { + public Show(node: HTMLElement) { super.Update('\u00a0'); // Ensures region shown and cannot be overwritten. this.node = node; - super.Show(node, highlighter); + super.Show(node); } /** @@ -513,6 +552,9 @@ export class SpeechRegion extends LiveRegion { promise.then(() => this.makeVoice(speech)); } + /** + * @param {string} speech The speech string to voice + */ private makeVoice(speech: string) { this.active = this.document.options.a11y.voicing && @@ -631,7 +673,7 @@ export class HoverRegion extends AbstractRegion { * @override */ protected static style: StyleJsonSheet = new StyleJsonSheet({ - ['.' + HoverRegion.className]: { + [`.${HoverRegion.className}`]: { display: 'block', position: 'absolute', width: 'max-content', @@ -645,8 +687,10 @@ export class HoverRegion extends AbstractRegion { 'box-shadow': '0px 10px 20px #888', border: '2px solid #CCCCCC', }, - ['.' + HoverRegion.className + ' > div']: { + [`.${HoverRegion.className} > div`]: { overflow: 'hidden', + color: 'var(--mjx-fg1-color)', + 'background-color': 'var(--mjx-bg1-color)', }, '@media (prefers-color-scheme: dark)': { ['.' + HoverRegion.className]: { @@ -655,6 +699,9 @@ export class HoverRegion extends AbstractRegion { border: '1px solid #7C7C7C', }, }, + 'mjx-container[data-mjx-clone-container]': { + padding: '2px ! important', + }, }); /** @@ -693,27 +740,11 @@ export class HoverRegion extends AbstractRegion { /** * @override */ - protected highlight(highlighter: Highlighter) { - if (!this.div) return; - // TODO Do this with styles to avoid the interaction of SVG/CHTML. - if ( - this.inner.firstChild && - !(this.inner.firstChild as HTMLElement).hasAttribute('sre-highlight') - ) { - return; - } - this.inner.style.backgroundColor = highlighter.background; - this.inner.style.color = highlighter.foreground; - } - - /** - * @override - */ - public Show(node: HTMLElement, highlighter: Highlighter) { + public Show(node: HTMLElement) { this.AddElement(); this.div.style.fontSize = this.document.options.a11y.magnify; this.Update(node); - super.Show(node, highlighter); + super.Show(node); } /** @@ -747,47 +778,101 @@ export class HoverRegion extends AbstractRegion { * @param {HTMLElement} node The original node. * @returns {HTMLElement} The cloned node. */ - private cloneNode(node: HTMLElement): HTMLElement { + protected cloneNode(node: HTMLElement): HTMLElement { let mjx = node.cloneNode(true) as HTMLElement; mjx.setAttribute('data-mjx-clone', 'true'); if (mjx.nodeName !== 'MJX-CONTAINER') { - // remove element spacing (could be done in CSS) if (mjx.nodeName !== 'g') { mjx.style.marginLeft = mjx.style.marginRight = '0'; } - let container = node; - while (container && container.nodeName !== 'MJX-CONTAINER') { - container = container.parentNode as HTMLElement; - } + const container = node.closest('mjx-container'); if (mjx.nodeName !== 'MJX-MATH' && mjx.nodeName !== 'svg') { - const child = container.firstChild; - mjx = child.cloneNode(false).appendChild(mjx).parentNode as HTMLElement; - // - // SVG specific - // - if (mjx.nodeName === 'svg') { - (mjx.firstChild as HTMLElement).setAttribute( - 'transform', - 'matrix(1 0 0 -1 0 0)' - ); - const W = parseFloat(mjx.getAttribute('viewBox').split(/ /)[2]); - const w = parseFloat(mjx.getAttribute('width')); - const { x, y, width, height } = (node as any).getBBox(); - mjx.setAttribute( - 'viewBox', - [x, -(y + height), width, height].join(' ') - ); - mjx.removeAttribute('style'); - mjx.setAttribute('width', (w / W) * width + 'ex'); - mjx.setAttribute('height', (w / W) * height + 'ex'); - container.setAttribute('sre-highlight', 'false'); + let math = container.firstChild; + if (math.nodeName === 'MJX-BBOX') { + math = math.nextSibling; } + mjx = math.cloneNode(false).appendChild(mjx).parentElement; + const enclosed = Array.from( + container.querySelectorAll('[data-sre-enclosed]') + ); + math.nodeName === 'svg' + ? this.svgClone(node, enclosed, mjx, container) + : this.chtmlClone(node, enclosed, mjx); } - mjx = container.cloneNode(false).appendChild(mjx) - .parentNode as HTMLElement; - // remove displayed math margins (could be done in CSS) + mjx = container.cloneNode(false).appendChild(mjx).parentElement; mjx.style.margin = '0'; } + mjx.setAttribute('data-mjx-clone-container', 'true'); return mjx; } + + /** + * @param {HTMLElement} node The main node being shown + * @param {Element[]} enclosed The elements to be cloned + * @param {HTMLElement} mjx The container for the clones + */ + protected chtmlClone( + node: HTMLElement, + enclosed: Element[], + mjx: HTMLElement + ) { + for (const child of enclosed) { + if (child !== node) { + mjx.appendChild(child.cloneNode(true)); + } + } + } + + /** + * @param {HTMLElement} node The main node being shown + * @param {Element[]} enclosed The elements to be cloned + * @param {HTMLElement} mjx The container for the clones + * @param {Element} container The container for node + */ + protected svgClone( + node: Element, + enclosed: Element[], + mjx: HTMLElement, + container: Element + ) { + let { x, y, width, height } = (node as SVGGraphicsElement).getBBox(); + if (enclosed.length) { + mjx.firstChild.remove(); + const g = container.querySelector('g').cloneNode(false); + for (const child of enclosed) { + const clone = g.appendChild(child.cloneNode(true)) as HTMLElement; + if (child === node) { + clone.setAttribute('data-mjx-clone', 'true'); + } + const [cx, cy] = this.xy(child); + clone.setAttribute('transform', `translate(${cx}, ${cy})`); + } + mjx.appendChild(g); + const rect = node.previousSibling as SVGRectElement; + const bbox = rect.getBBox(); + width = bbox.width; + height = bbox.height; + const [X, Y] = this.xy(rect); + x = X; + y = Y + bbox.y; + } + (mjx.firstChild as HTMLElement).setAttribute('transform', 'scale(1, -1)'); + const W = parseFloat(mjx.getAttribute('viewBox').split(/ /)[2]); + const w = parseFloat(mjx.getAttribute('width')); + mjx.setAttribute('viewBox', [x, -(y + height), width, height].join(' ')); + mjx.removeAttribute('style'); + mjx.setAttribute('width', (w / W) * width + 'ex'); + mjx.setAttribute('height', (w / W) * height + 'ex'); + } + + /** + * @param {Element} node The node whose position is needed + * @returns {[number, number]} The position in viewport coordinates + */ + protected xy(node: Element): number[] { + const P = DOMPoint.fromPoint({ x: 0, y: 0 }).matrixTransform( + (node as SVGGraphicsElement).getCTM().inverse() + ); + return [-P.x, -P.y]; + } } diff --git a/ts/a11y/speech/WebWorker.ts b/ts/a11y/speech/WebWorker.ts index be0a65db5..72a54c5aa 100644 --- a/ts/a11y/speech/WebWorker.ts +++ b/ts/a11y/speech/WebWorker.ts @@ -328,6 +328,9 @@ export class WorkerHandler { continue; } node = adaptor.childNodes(node)[0] as N; + if (adaptor.kind(node) === 'rect') { + node = adaptor.next(node) as N; + } adaptor.setAttribute(node, 'data-semantic-type', 'dummy'); this.setSpecialAttributes(node, sid, ''); } diff --git a/ts/handlers/html/HTMLMathItem.ts b/ts/handlers/html/HTMLMathItem.ts index 6e8f96b6c..b8042411c 100644 --- a/ts/handlers/html/HTMLMathItem.ts +++ b/ts/handlers/html/HTMLMathItem.ts @@ -83,7 +83,9 @@ export class HTMLMathItem extends AbstractMathItem { if (this.start.n) { node = this.adaptor.split(this.start.node as T, this.start.n); } - this.adaptor.replace(this.typesetRoot, node); + if (this.adaptor.parent(node)) { + this.adaptor.replace(this.typesetRoot, node); + } } else { if (this.start.n) { node = this.adaptor.split(node, this.start.n); diff --git a/ts/output/chtml/Wrappers/maction.ts b/ts/output/chtml/Wrappers/maction.ts index 9f66e54a8..05913c1c7 100644 --- a/ts/output/chtml/Wrappers/maction.ts +++ b/ts/output/chtml/Wrappers/maction.ts @@ -188,7 +188,7 @@ export const ChtmlMaction = (function (): ChtmlMactionClass { 'background-color': '#F8F8F8', color: 'black', }, - 'mjx-maction[data-collapsible][toggle="1"]': { + 'mjx-container [data-mjx-collapsed]': { color: '#55F', }, @@ -203,7 +203,7 @@ export const ChtmlMaction = (function (): ChtmlMactionClass { 'background-color': '#303030', color: '#E0E0E0', }, - 'mjx-maction[data-collapsible][toggle="1"]': { + 'mjx-container [data-mjx-collapsed]': { color: '#88F', }, }, diff --git a/ts/output/svg/Wrappers/maction.ts b/ts/output/svg/Wrappers/maction.ts index 298c40f11..9dc227202 100644 --- a/ts/output/svg/Wrappers/maction.ts +++ b/ts/output/svg/Wrappers/maction.ts @@ -187,7 +187,7 @@ export const SvgMaction = (function (): SvgMactionClass { 'background-color': '#F8F8F8', color: 'black', }, - 'g[data-mml-node="maction"][data-collapsible][data-toggle="1"]': { + 'g[data-mjx-collapsed]': { fill: '#55F', }, @@ -201,7 +201,7 @@ export const SvgMaction = (function (): SvgMactionClass { 'background-color': '#303030', color: '#E0E0E0', }, - 'g[data-mml-node="maction"][data-collapsible][data-toggle="1"]': { + 'g[data-mjx-collapsed]': { fill: '#88F', }, }, diff --git a/ts/ui/menu/Menu.ts b/ts/ui/menu/Menu.ts index fa8dd9d4d..e858bdba5 100644 --- a/ts/ui/menu/Menu.ts +++ b/ts/ui/menu/Menu.ts @@ -620,13 +620,17 @@ export class Menu { this.setBrailleCode(code) ), this.a11yVar('highlight', (value) => this.setHighlight(value)), - this.a11yVar('backgroundColor'), - this.a11yVar('backgroundOpacity', (value) => - this.setAlpha('bg', value) + this.a11yVar('backgroundColor', (color) => + this.setColor('bg', color) ), - this.a11yVar('foregroundColor'), - this.a11yVar('foregroundOpacity', (value) => - this.setAlpha('fg', value) + this.a11yVar('backgroundOpacity', (opacity) => + this.setColor('bg', null, opacity) + ), + this.a11yVar('foregroundColor', (color) => + this.setColor('fg', color) + ), + this.a11yVar('foregroundOpacity', (opacity) => + this.setColor('fg', null, opacity) ), this.a11yVar('subtitles'), this.a11yVar('viewBraille'), @@ -1078,8 +1082,6 @@ export class Menu { if (renderer !== this.defaultSettings.renderer) { this.document.whenReady(() => this.setRenderer(renderer, false)); } - this.setAlpha('fg', this.settings.foregroundOpacity ?? '100'); - this.setAlpha('bg', this.settings.backgroundOpacity ?? '20'); }); } @@ -1308,9 +1310,6 @@ export class Menu { this.rerender(STATE.COMPILED); } else { this.loadA11y('complexity'); - if (!MathJax._?.a11y?.explorer) { - this.loadA11y('explorer'); - } } } @@ -1333,18 +1332,24 @@ export class Menu { } /** - * @param {string} type The type of alpha to set (fg or bg) - * @param {string} value The value to set it to + * @param {string} type 'fg' or 'bg' + * @param {string} name The color name + * @param {string} opacity The color's opacity percentage */ - protected setAlpha(type: string, value: string) { - if (MathJax._?.a11y?.explorer) { - const alpha = parseInt(value) / 100; - MathJax._.a11y.explorer.Region.LiveRegion.setAlpha( - type, - alpha, - this.document.document - ); + protected setColor(type: string, name: string, opacity?: string) { + const a11y = this.document.options.a11y; + if (!name) { + name = a11y[type === 'fg' ? 'foregroundColor' : 'backgroundColor']; } + if (!opacity) { + opacity = a11y[type === 'fg' ? 'foregroundOpacity' : 'backgroundOpacity']; + } + MathJax._.a11y.explorer.Region.LiveRegion.setColor( + type, + 1, + name.toLowerCase(), + parseInt(opacity) / 100 + ); } /** @@ -1682,9 +1687,8 @@ export class Menu { */ protected rerender(start: number = STATE.TYPESET) { this.rerenderStart = Math.min(start, this.rerenderStart); - const startup = MathJax.startup; - if (!Menu.loading && startup.hasTypeset) { - startup.document.whenReady(async () => { + if (!Menu.loading && MathJax.startup.hasTypeset) { + this.document.whenReady(async () => { if (this.rerenderStart <= STATE.COMPILED) { this.document.reset({ inputJax: [] }); }