diff --git a/components/mjs/a11y/complexity/config.json b/components/mjs/a11y/complexity/config.json index 77e22a0802..4ae2a8debe 100644 --- a/components/mjs/a11y/complexity/config.json +++ b/components/mjs/a11y/complexity/config.json @@ -3,8 +3,7 @@ "component": "a11y/complexity", "targets": [ "a11y/complexity.ts", - "a11y/complexity", - "a11y/semantic-enrich.ts" + "a11y/complexity" ] }, "webpack": { diff --git a/components/mjs/a11y/explorer/config.json b/components/mjs/a11y/explorer/config.json index 4fa9734572..5061295938 100644 --- a/components/mjs/a11y/explorer/config.json +++ b/components/mjs/a11y/explorer/config.json @@ -1,7 +1,11 @@ { "build": { "component": "a11y/explorer", - "targets": ["a11y/explorer.ts", "a11y/explorer"] + "targets": [ + "a11y/explorer.ts", + "a11y/explorer", + "a11y/aria/__locales__" + ] }, "copy": [ { diff --git a/components/mjs/a11y/semantic-enrich/config.json b/components/mjs/a11y/semantic-enrich/config.json index 925633eeed..1c4c01aa6d 100644 --- a/components/mjs/a11y/semantic-enrich/config.json +++ b/components/mjs/a11y/semantic-enrich/config.json @@ -2,7 +2,9 @@ "build": { "component": "a11y/semantic-enrich", "targets": [ - "a11y/semantic-enrich.ts" + "a11y/semantic-enrich.ts", + "a11y/semantic-enrich", + "a11y/speech/strings.ts" ] }, "copy": { diff --git a/components/mjs/a11y/speech/config.json b/components/mjs/a11y/speech/config.json index 80e5caf20f..f696fc9f3f 100644 --- a/components/mjs/a11y/speech/config.json +++ b/components/mjs/a11y/speech/config.json @@ -2,7 +2,10 @@ "build": { "component": "a11y/speech", "targets": ["a11y/speech.ts", "a11y/speech"], - "exclude": ["a11y/speech/SpeechMenu.ts"] + "exclude": [ + "a11y/speech/SpeechMenu.ts", + "a11y/speech/strings.ts" + ] }, "copy": { "to": "[bundle]/a11y/speech", diff --git a/components/mjs/adaptors/linkedom/config.json b/components/mjs/adaptors/linkedom/config.json index 9c4c1dbcd2..0fe6776c82 100644 --- a/components/mjs/adaptors/linkedom/config.json +++ b/components/mjs/adaptors/linkedom/config.json @@ -1,7 +1,10 @@ { "build": { "component": "adaptors/linkedom", - "targets": ["adaptors/linkedomAdaptor.ts"] + "targets": [ + "adaptors/linkedomAdaptor.ts", + "adaptors/linkedom" + ] }, "copy": { "to": "[bundle]/adaptors/linkedom", diff --git a/components/mjs/input/mml/config.json b/components/mjs/input/mml/config.json index 95cd7c5523..baf64cba3f 100644 --- a/components/mjs/input/mml/config.json +++ b/components/mjs/input/mml/config.json @@ -3,7 +3,8 @@ "component": "input/mml", "targets": [ "input/mathml.ts", - "input/mathml" + "input/mathml", + "input/mathml/__locales__" ], "excludeSubdirs": true }, diff --git a/components/mjs/input/tex/config.json b/components/mjs/input/tex/config.json index 551d42eb06..d4a34cad86 100644 --- a/components/mjs/input/tex/config.json +++ b/components/mjs/input/tex/config.json @@ -11,7 +11,8 @@ "input/tex/noundefined", "input/tex/require", "input/tex/autoload", - "input/tex/configmacros" + "input/tex/configmacros", + "input/tex/__locales__" ], "excludeSubdirs": true }, diff --git a/components/mjs/output/chtml/config.json b/components/mjs/output/chtml/config.json index 2890a1977c..c5d0ef5aa7 100644 --- a/components/mjs/output/chtml/config.json +++ b/components/mjs/output/chtml/config.json @@ -5,7 +5,9 @@ "output/chtml.ts", "output/chtml", "output/common.ts", - "output/common" + "output/common", + "a11y/semantic-enrich/strings.ts", + "a11y/semantic-enrich/maction.ts" ] }, "webpack": { diff --git a/components/mjs/output/svg/config.json b/components/mjs/output/svg/config.json index 660839c963..c293ddc3a6 100644 --- a/components/mjs/output/svg/config.json +++ b/components/mjs/output/svg/config.json @@ -5,7 +5,9 @@ "output/svg.ts", "output/svg", "output/common.ts", - "output/common" + "output/common", + "a11y/semantic-enrich/strings.ts", + "a11y/semantic-enrich/maction.ts" ] }, "webpack": { diff --git a/components/mjs/ui/menu/config.json b/components/mjs/ui/menu/config.json index 920cdb9604..e01b877c2c 100644 --- a/components/mjs/ui/menu/config.json +++ b/components/mjs/ui/menu/config.json @@ -1,7 +1,14 @@ { "build": { "component": "ui/menu", - "targets": ["ui/menu", "a11y/speech/SpeechMenu.ts"], + "targets": [ + "ui/menu", + "a11y/speech/SpeechMenu.ts", + "a11y/semantic-enrich/strings.ts", + "a11y/speech/strings.ts", + "ui/menu/__locales__", + "a11y/aria/__locales__" + ], "excludeSubdirs": true }, "copy": [ diff --git a/ts/a11y/complexity/collapse.ts b/ts/a11y/complexity/collapse.ts index 72854745a9..b297e74c0b 100644 --- a/ts/a11y/complexity/collapse.ts +++ b/ts/a11y/complexity/collapse.ts @@ -28,6 +28,10 @@ import { } from '../../core/MmlTree/MmlNode.js'; import { PropertyList } from '../../core/Tree/Node.js'; import { ComplexityVisitor } from './visitor.js'; +import { PROP, DATA } from './strings.js'; +import { SEM } from '../semantic-enrich/strings.js'; +import { SPEECH } from '../speech/strings.js'; +import { MACTION } from '../semantic-enrich/maction.js'; import type { SemanticMap } from '../speech/StructureUtil.js'; /*==========================================================================*/ @@ -156,7 +160,7 @@ export class Collapse { complexity = this.uncollapseChild(complexity, node, 1); if ( complexity > (this.cutoff.fenced as number) && - node.attributes.get('data-semantic-role') === 'leftright' + node.attributes.get(SEM.ROLE) === 'leftright' ) { complexity = this.recordCollapse( node, @@ -180,8 +184,7 @@ export class Collapse { complexity = this.complexity.visitNode(node, false); const marker = this.marker.appl as { [name: string]: string }; const text = - marker[node.attributes.get('data-semantic-role') as string] || - marker.value; + marker[node.attributes.get(SEM.ROLE) as string] || marker.value; complexity = this.recordCollapse(node, complexity, text); } return complexity; @@ -197,7 +200,7 @@ export class Collapse { (node, complexity) => { complexity = this.uncollapseChild(complexity, node, 0); if (complexity > (this.cutoff.sqrt as number)) { - node.setProperty('collapse-variant', true); + node.setProperty(PROP.VARIANT, true); complexity = this.recordCollapse( node, complexity, @@ -212,7 +215,7 @@ export class Collapse { (node, complexity) => { complexity = this.uncollapseChild(complexity, node, 0, 2); if (complexity > (this.cutoff.sqrt as number)) { - node.setProperty('collapse-variant', true); + node.setProperty(PROP.VARIANT, true); complexity = this.recordCollapse( node, complexity, @@ -232,7 +235,7 @@ export class Collapse { if (this.splitAttribute(node, 'children').length === 1) { const child = this.canUncollapse(node, 1); if (child) { - const marker = child.getProperty('collapse-marker') as string; + const marker = child.getProperty(PROP.MARKER) as string; this.unrecordCollapse(child); complexity = this.recordCollapse( node, @@ -369,7 +372,7 @@ export class Collapse { * @returns {number} The revised complexity */ public check(node: MmlNode, complexity: number): number { - const type = node.attributes.get('data-semantic-type') as string; + const type = node.attributes.get(SEM.TYPE) as string; if (this.collapse.has(type)) { return this.collapse.get(type).call(this, node, complexity); } @@ -392,7 +395,7 @@ export class Collapse { complexity: number, type: string ): number { - const role = node.attributes.get('data-semantic-role') as string; + const role = node.attributes.get(SEM.ROLE) as string; const check = this.cutoff[type]; const cutoff = typeof check === 'number' ? check : check[role] || check.value; @@ -417,8 +420,8 @@ export class Collapse { text: string ): number { text = '\u25C2' + text + '\u25B8'; - node.setProperty('collapse-marker', text); - node.setProperty('collapse-complexity', complexity); + node.setProperty(PROP.MARKER, text); + node.setProperty(PROP.COMPLEXITY, complexity); return text.length * this.complexity.complexity.text; } @@ -428,11 +431,11 @@ export class Collapse { * @param {MmlNode} node The node to uncollapse */ protected unrecordCollapse(node: MmlNode) { - const complexity = node.getProperty('collapse-complexity'); + const complexity = node.getProperty(PROP.COMPLEXITY); if (complexity != null) { - node.attributes.set('data-semantic-complexity', complexity); - node.removeProperty('collapse-complexity'); - node.removeProperty('collapse-marker'); + node.attributes.set(DATA.COMPLEXITY, complexity); + node.removeProperty(PROP.COMPLEXITY); + node.removeProperty(PROP.MARKER); } } @@ -454,7 +457,7 @@ export class Collapse { : node; if (mml && mml.childNodes[n]) { const child = mml.childNodes[n]; - if (child.getProperty('collapse-marker')) { + if (child.getProperty(PROP.MARKER)) { return child; } } @@ -479,7 +482,7 @@ export class Collapse { if (child) { this.unrecordCollapse(child); if (child.parent !== node) { - child.parent.attributes.set('data-semantic-complexity', undefined); + child.parent.attributes.set(DATA.COMPLEXITY, undefined); } complexity = this.complexity.visitNode(node, false) as number; } @@ -492,9 +495,7 @@ export class Collapse { * @returns {string[]} Array of ids in the attribute split at commas */ protected splitAttribute(node: MmlNode, id: string): string[] { - return ((node.attributes.get('data-semantic-' + id) as string) || '').split( - /,/ - ); + return ((node.attributes.get(SEM.PREFIX + id) as string) || '').split(/,/); } /** @@ -522,7 +523,7 @@ export class Collapse { * @returns {MmlNode|null} The child node (or null if not found) */ protected findChild(node: MmlNode, id: string): MmlNode | null { - if (!node || node.attributes.get('data-semantic-id') === id) return node; + if (!node || node.attributes.get(SEM.ID) === id) return node; if (!node.isToken) { for (const mml of node.childNodes) { const child = this.findChild(mml, id); @@ -554,7 +555,7 @@ export class Collapse { } const nodes: MmlNode[] = []; node.walkTree((child: MmlNode) => { - if (child.getProperty('collapse-marker')) { + if (child.getProperty(PROP.MARKER)) { nodes.push(child); } }); @@ -573,9 +574,7 @@ export class Collapse { public makeActions(root: MmlNode, nodes: MmlNode[], parts: SemanticMap) { for (const node of nodes) { const extra = - parts - .get(node.attributes.get('data-semantic-id') as string) - ?.slice(1) ?? []; + parts.get(node.attributes.get(SEM.ID) as string)?.slice(1) ?? []; this.makeAction(root, node, extra); } } @@ -597,10 +596,10 @@ export class Collapse { node = this.addMrow(node); } const factory = this.complexity.factory; - const marker = node.getProperty('collapse-marker') as string; + const marker = node.getProperty(PROP.MARKER) as string; const parent = node.parent; - const def = { 'data-mjx-collapsed': true } as PropertyList; - if (node.getProperty('collapse-variant')) { + const def = { [MACTION.COLLAPSED]: true } as PropertyList; + if (node.getProperty(PROP.VARIANT)) { def.mathvariant = '-tex-variant'; } const maction = factory.create( @@ -608,11 +607,9 @@ export class Collapse { { actiontype: 'toggle', selection: 2, - 'data-collapsible': true, + [MACTION.COLLAPSIBLE]: true, id: this.makeId(), - 'data-semantic-complexity': node.attributes.get( - 'data-semantic-complexity' - ), + [DATA.COMPLEXITY]: node.attributes.get(DATA.COMPLEXITY), }, [ factory.create('mtext', def, [ @@ -621,12 +618,9 @@ export class Collapse { ] ); maction.inheritAttributesFrom(node); - node.attributes.set( - 'data-semantic-complexity', - node.getProperty('collapse-complexity') - ); - node.removeProperty('collapse-marker'); - node.removeProperty('collapse-complexity'); + node.attributes.set(DATA.COMPLEXITY, node.getProperty(PROP.COMPLEXITY)); + node.removeProperty(PROP.MARKER); + node.removeProperty(PROP.COMPLEXITY); parent.replaceChild(maction, node); maction.appendChild(node); this.makeActionGroup(root, maction, extra); @@ -639,10 +633,10 @@ export class Collapse { */ public makeActionGroup(root: MmlNode, action: MmlNode, extra: string[]) { if (!extra.length) return; - action.attributes.set('data-collapse-group', true); + action.attributes.set(MACTION.GROUP, true); const nodes: MmlNode[] = []; root.walkTree((node) => { - if (extra.includes(node.attributes.get('data-semantic-id') as string)) { + if (extra.includes(node.attributes.get(SEM.ID) as string)) { nodes.push(node); } }); @@ -654,11 +648,9 @@ export class Collapse { { actiontype: 'toggle', selection: 2, - 'data-collapsible': true, - 'data-collapse-id': action.attributes.get('id'), - 'data-semantic-complexity': node.attributes.get( - 'data-semantic-complexity' - ), + [MACTION.COLLAPSIBLE]: true, + [MACTION.GROUPID]: action.attributes.get('id'), + [DATA.COMPLEXITY]: node.attributes.get(DATA.COMPLEXITY), }, [factory.create('mtext')] ); @@ -686,8 +678,8 @@ export class Collapse { const attributes = node.attributes.getAllAttributes(); for (const name of Object.keys(attributes)) { if ( - name.substring(0, 14) === 'data-semantic-' || - name.substring(0, 12) === 'data-speech-' || + name.substring(0, 14) === SEM.PREFIX || + name.substring(0, 12) === SPEECH.PREFIX || name.substring(0, 5) === 'aria-' || name === 'role' ) { @@ -696,13 +688,10 @@ export class Collapse { } } - mrow.setProperty('collapse-marker', node.getProperty('collapse-marker')); - mrow.setProperty( - 'collapse-complexity', - node.getProperty('collapse-complexity') - ); - node.removeProperty('collapse-marker'); - node.removeProperty('collapse-complexity'); + mrow.setProperty(PROP.MARKER, node.getProperty(PROP.MARKER)); + mrow.setProperty(PROP.COMPLEXITY, node.getProperty(PROP.COMPLEXITY)); + node.removeProperty(PROP.MARKER); + node.removeProperty(PROP.COMPLEXITY); return mrow; } } diff --git a/ts/a11y/complexity/strings.ts b/ts/a11y/complexity/strings.ts new file mode 100644 index 0000000000..1c66448973 --- /dev/null +++ b/ts/a11y/complexity/strings.ts @@ -0,0 +1,33 @@ +/************************************************************* + * + * Copyright (c) 2026 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. + */ + +/** + * @file The property and data attribute strings for collapsing + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +export const PROP = { + COMPLEXITY: 'collapse-complexity', + MARKER: 'collapse-marker', + VARIANT: 'collaspe-variant', +}; + +export const DATA = { + COMPLEXITY: 'data-collapse-complexity', + PREFIX: 'data-collapse-', +}; diff --git a/ts/a11y/complexity/visitor.ts b/ts/a11y/complexity/visitor.ts index db2be7695e..823501d81a 100644 --- a/ts/a11y/complexity/visitor.ts +++ b/ts/a11y/complexity/visitor.ts @@ -37,6 +37,7 @@ import { import { MmlVisitor } from '../../core/MmlTree/MmlVisitor.js'; import { MmlFactory } from '../../core/MmlTree/MmlFactory.js'; import { Collapse } from './collapse.js'; +import { DATA, PROP } from './strings.js'; import { OptionList, userOptions, defaultOptions } from '../../util/Options.js'; import type { SemanticMap } from '../speech/StructureUtil.js'; @@ -120,7 +121,7 @@ export class ComplexityVisitor extends MmlVisitor { * @override */ public visitNode(node: MmlNode, save: boolean) { - if (node.attributes.get('data-semantic-complexity')) return; + if (node.attributes.get(DATA.COMPLEXITY)) return; return super.visitNode(node, save); } @@ -172,8 +173,8 @@ export class ComplexityVisitor extends MmlVisitor { * (since it is counted in the children sum but is smaller) * * @param {MmlMroot} node The node whose complixity is being computed - * @param {boolean} save True if the complexity is to be saved or just returned - * @returns {number} The complexity of the node + * @param {boolean} save True if the complexity is to be saved or just returned + * @returns {number} The complexity of the node */ protected visitMrootNode(node: MmlMroot, save: boolean): number { const complexity = @@ -217,8 +218,8 @@ export class ComplexityVisitor extends MmlVisitor { * complexity for each child, and the subsup complexity. * * @param {MmlMsubsup} node The node whose complixity is being computed - * @param {boolean} save True if the complexity is to be saved or just returned - * @returns {number} The complexity of the node + * @param {boolean} save True if the complexity is to be saved or just returned + * @returns {number} The complexity of the node */ protected visitMsubsupNode(node: MmlMsubsup, save: boolean): number { super.visitDefault(node, true); @@ -259,8 +260,8 @@ export class ComplexityVisitor extends MmlVisitor { * underover amount. * * @param {MmlMunderover} node The node whose complixity is being computed - * @param {boolean} save True if the complexity is to be saved or just returned - * @returns {number} The complexity of the node + * @param {boolean} save True if the complexity is to be saved or just returned + * @returns {number} The complexity of the node */ protected visitMunderoverNode(node: MmlMunderover, save: boolean): number { super.visitDefault(node, true); @@ -283,16 +284,16 @@ export class ComplexityVisitor extends MmlVisitor { } /** * @param {MmlMunder} node The node whose complixity is being computed - * @param {boolean} save True if the complexity is to be saved or just returned - * @returns {number} The complexity of the node + * @param {boolean} save True if the complexity is to be saved or just returned + * @returns {number} The complexity of the node */ protected visitMunderNode(node: MmlMunder, save: boolean): number { return this.visitMunderoverNode(node, save); } /** * @param {MmlMover} node The node whose complixity is being computed - * @param {boolean} save True if the complexity is to be saved or just returned - * @returns {number} The complexity of the node + * @param {boolean} save True if the complexity is to be saved or just returned + * @returns {number} The complexity of the node */ protected visitMoverNode(node: MmlMover, save: boolean): number { return this.visitMunderoverNode(node, save); @@ -314,8 +315,8 @@ export class ComplexityVisitor extends MmlVisitor { * For actions, use the complexity of the selected child * * @param {MmlMaction} node The node whose complixity is being computed - * @param {boolean} save True if the complexity is to be saved or just returned - * @returns {number} The complexity of the node + * @param {boolean} save True if the complexity is to be saved or just returned + * @returns {number} The complexity of the node */ protected visitMactionNode(node: MmlMaction, save: boolean): number { this.childrenComplexity(node); @@ -377,14 +378,12 @@ export class ComplexityVisitor extends MmlVisitor { /** * @param {MmlNode} node The node whose complixity is needed - * @returns {number} The complexity of the node (if collapsable, then the collapsed complexity) + * @returns {number} The complexity of the node (if collapsable, then the collapsed complexity) */ public getComplexity(node: MmlNode): number { - const collapsed = node.getProperty('collapsedComplexity'); + const collapsed = node.getProperty(PROP.COMPLEXITY); return ( - collapsed != null - ? collapsed - : node.attributes.get('data-semantic-complexity') + collapsed != null ? collapsed : node.attributes.get(DATA.COMPLEXITY) ) as number; } @@ -403,14 +402,14 @@ export class ComplexityVisitor extends MmlVisitor { if (this.options.identifyCollapsible) { complexity = this.collapse.check(node, complexity); } - node.attributes.set('data-semantic-complexity', complexity); + node.attributes.set(DATA.COMPLEXITY, complexity); } return complexity; } /** * @param {MmlNode} node The node whose children complexities are to be added - * @returns {number} The sum of the complexities, plus child complexity for each one + * @returns {number} The sum of the complexities, plus child complexity for each one */ protected childrenComplexity(node: MmlNode): number { super.visitDefault(node, true); diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts index 3051521b6c..140786ee25 100644 --- a/ts/a11y/explorer.ts +++ b/ts/a11y/explorer.ts @@ -30,7 +30,9 @@ import { OptionList, expandable } from '../util/Options.js'; import { hasWindow } from '../util/context.js'; import { StyleJson } from '../util/StyleJson.js'; import { context } from '../util/context.js'; +import { SEM } from './semantic-enrich/strings.js'; +import { SAVED_HREF } from './explorer/KeyExplorer.js'; import { ExplorerPool, RegionPool } from './explorer/ExplorerPool.js'; import * as Sre from './sre.js'; @@ -294,15 +296,13 @@ export function ExplorerMathItemMixin>( * @returns {HTMLElement[]} All the nodes for the given id */ public getSplitNodes(node: HTMLElement): HTMLElement[] { - const id = node.getAttribute('data-semantic-id'); + const id = node.getAttribute(SEM.ID); if (!id) { return [node]; } const nodes = (this.semanticNodes.get(id) ?? [id]) .map((nid: string) => - Array.from( - this.typesetRoot.querySelectorAll(`[data-semantic-id="${nid}"]`) - ) + Array.from(this.typesetRoot.querySelectorAll(`[${SEM.ID}="${nid}"]`)) ) .flat() as HTMLElement[]; return nodes; @@ -423,11 +423,11 @@ export function ExplorerMathDocumentMixin< outline: '2px solid black', }, - 'mjx-container a[data-mjx-href]': { + [`mjx-container a[${SAVED_HREF}]`]: { color: 'LinkText', cursor: 'pointer', }, - 'mjx-container a[data-mjx-href].mjx-visited': { + [`mjx-container a[${SAVED_HREF}].mjx-visited`]: { color: 'VisitedText', }, diff --git a/ts/a11y/explorer/Explorer.ts b/ts/a11y/explorer/Explorer.ts index 1fdf2e3063..0e25614016 100644 --- a/ts/a11y/explorer/Explorer.ts +++ b/ts/a11y/explorer/Explorer.ts @@ -22,7 +22,8 @@ */ import { A11yDocument, Region } from './Region.js'; -import { Highlighter, ATTR } from './Highlighter.js'; +import { Highlighter } from './Highlighter.js'; +import { HILITE } from './strings.js'; import type { ExplorerPool } from './ExplorerPool.js'; @@ -329,7 +330,7 @@ export class AbstractExplorer implements Explorer { // if ( child.nodeName.charAt(0) === '#' || - child.hasAttribute?.(ATTR.ADDED) + child.hasAttribute?.(HILITE.ADDED) ) { continue; } diff --git a/ts/a11y/explorer/ExplorerPool.ts b/ts/a11y/explorer/ExplorerPool.ts index 381bd50911..a70e2a6777 100644 --- a/ts/a11y/explorer/ExplorerPool.ts +++ b/ts/a11y/explorer/ExplorerPool.ts @@ -23,6 +23,7 @@ import { LiveRegion, SpeechRegion, ToolTip, HoverRegion } from './Region.js'; import type { ExplorerMathDocument, ExplorerMathItem } from '../explorer.js'; +import { SEM } from '../semantic-enrich/strings.js'; import { Explorer } from './Explorer.js'; import { SpeechExplorer } from './KeyExplorer.js'; @@ -114,7 +115,7 @@ const allExplorers: { [options: string]: ExplorerInit } = { doc.explorerRegions.tooltip1, node, item, - 'data-semantic-type' + SEM.TYPE ), infoRole: (doc, pool, node, item) => ValueHoverer.create( @@ -123,7 +124,7 @@ const allExplorers: { [options: string]: ExplorerInit } = { doc.explorerRegions.tooltip2, node, item, - 'data-semantic-role' + SEM.ROLE ), infoPrefix: (doc, pool, node, item) => ValueHoverer.create( @@ -132,7 +133,7 @@ const allExplorers: { [options: string]: ExplorerInit } = { doc.explorerRegions.tooltip3, node, item, - 'data-semantic-prefix-none' + SEM.PREFIX_NONE ), flame: (doc, pool, node) => FlameColorer.create(doc, pool, null, node), treeColoring: (doc, pool, node, item) => diff --git a/ts/a11y/explorer/Highlighter.ts b/ts/a11y/explorer/Highlighter.ts index ba72b2c9ce..27dd8e67d8 100644 --- a/ts/a11y/explorer/Highlighter.ts +++ b/ts/a11y/explorer/Highlighter.ts @@ -19,6 +19,8 @@ */ import { LiveRegion } from './Region.js'; +import { HILITE } from './strings.js'; +import { MACTION } from '../semantic-enrich/maction.js'; export interface NamedColor { color: string; @@ -35,15 +37,6 @@ const DEFAULT_BACKGROUND: NamedColor = { color: 'blue', alpha: 0.2 }; */ const DEFAULT_FOREGROUND: NamedColor = { color: 'black', alpha: 1 }; -/** - * The attributes for various markers - */ -export const ATTR = { - ENCLOSED: 'data-sre-enclosed', - BBOX: 'data-sre-highlighter-bbox', - ADDED: 'data-sre-highlighter-added', -}; - export interface Highlighter { /** * Sets highlighting on a node. @@ -116,7 +109,7 @@ abstract class AbstractHighlighter implements Highlighter { /** * The Attribute for marking highlighted nodes. */ - protected ATTR: string; + protected SRE: string; /** * The CSS selector to use to find the line-box container. @@ -143,7 +136,7 @@ abstract class AbstractHighlighter implements Highlighter { */ constructor(priority: number) { this.priority = priority; - this.ATTR = 'data-sre-highlight-' + priority; + this.SRE = HILITE.PREFIX + priority; } /** @@ -177,7 +170,7 @@ abstract class AbstractHighlighter implements Highlighter { public highlightAll(node: HTMLElement) { const mactions = this.getMactionNodes(node); for (const maction of mactions) { - let parts: HTMLElement[] = maction.hasAttribute('data-collapse-group') + let parts: HTMLElement[] = maction.hasAttribute(MACTION.GROUP) ? this.getMactionGroup(node, maction) : [maction]; parts = this.encloseNodes([...parts], node); @@ -253,7 +246,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(ATTR.ENCLOSED, 'true'); + part.setAttribute(HILITE.ENCLOSED, 'true'); const { left, top, right, bottom } = part.getBoundingClientRect(); if (top === bottom && left === right) continue; if (left < L) L = left; @@ -312,7 +305,9 @@ abstract class AbstractHighlighter implements Highlighter { maction: HTMLElement ): HTMLElement[] { return Array.from( - node.querySelectorAll(`#${maction.id},[data-collapse-id="${maction.id}"]`) + node.querySelectorAll( + `#${maction.id},[${MACTION.GROUPID}="${maction.id}"]` + ) ); } @@ -323,7 +318,7 @@ abstract class AbstractHighlighter implements Highlighter { * @returns {boolean} True if already highlighted. */ public isHighlighted(node: HTMLElement): boolean { - return node.hasAttribute(this.ATTR); + return node.hasAttribute(this.SRE); } /** @@ -332,7 +327,7 @@ abstract class AbstractHighlighter implements Highlighter { * @param {HTMLElement} node The node. */ public setHighlighted(node: HTMLElement) { - node.setAttribute(this.ATTR, 'true'); + node.setAttribute(this.SRE, 'true'); } /** @@ -341,8 +336,8 @@ abstract class AbstractHighlighter implements Highlighter { * @param {HTMLElement} node The node. */ public unsetHighlighted(node: HTMLElement) { - node.removeAttribute(this.ATTR); - node.removeAttribute(ATTR.ENCLOSED); + node.removeAttribute(this.SRE); + node.removeAttribute(HILITE.ENCLOSED); } } @@ -358,8 +353,8 @@ class SvgHighlighter extends AbstractHighlighter { this.isHighlighted(node) || node.tagName === 'svg' || node.tagName === 'MJX-CONTAINER' || - node.hasAttribute(ATTR.BBOX) || - node.hasAttribute(ATTR.ENCLOSED) + node.hasAttribute(HILITE.BBOX) || + node.hasAttribute(HILITE.ENCLOSED) ) { return; } @@ -381,12 +376,12 @@ class SvgHighlighter extends AbstractHighlighter { * @override */ public unhighlightNode(node: HTMLElement) { - if (node.hasAttribute(ATTR.BBOX)) { + if (node.hasAttribute(HILITE.BBOX)) { node.remove(); return; } const previous = node.previousSibling as HTMLElement; - if (previous?.hasAttribute(ATTR.ADDED)) { + if (previous?.hasAttribute(HILITE.ADDED)) { previous.remove(); } } @@ -411,8 +406,8 @@ class SvgHighlighter extends AbstractHighlighter { y2 - y1, part.getAttribute('transform') ); - rect.setAttribute(ATTR.BBOX, 'true'); - rect.setAttribute(ATTR.ADDED, 'true'); + rect.setAttribute(HILITE.BBOX, 'true'); + rect.setAttribute(HILITE.ADDED, 'true'); part.parentNode.insertBefore(rect, part); return rect; } @@ -452,7 +447,7 @@ class SvgHighlighter extends AbstractHighlighter { ): HTMLElement { const padding = 40; const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); - rect.setAttribute(ATTR.ADDED, 'true'); // Mark highlighting rect. + rect.setAttribute(HILITE.ADDED, 'true'); // Mark highlighting rect. rect.setAttribute('x', String(x - padding)); rect.setAttribute('y', String(y - padding)); rect.setAttribute('width', String(w + 2 * padding)); @@ -476,7 +471,7 @@ class SvgHighlighter extends AbstractHighlighter { public getMactionNodes(node: HTMLElement): HTMLElement[] { return Array.from( node.querySelectorAll( - '[data-mml-node="maction"][data-collapsible]:not([data-collapse-id])' + `[data-mml-node="maction"][${MACTION.COLLAPSIBLE}]:not([${MACTION.GROUPID}])` ) ); } @@ -517,8 +512,8 @@ class ChtmlHighlighter extends AbstractHighlighter { enclosure.style.left = x - base.left + 'px'; enclosure.style.top = y - h - base.top + 'px'; enclosure.style.position = 'absolute'; - enclosure.setAttribute(ATTR.BBOX, 'true'); - enclosure.setAttribute(ATTR.ADDED, 'true'); + enclosure.setAttribute(HILITE.BBOX, 'true'); + enclosure.setAttribute(HILITE.ADDED, 'true'); node.prepend(enclosure); return enclosure; } @@ -536,7 +531,7 @@ class ChtmlHighlighter extends AbstractHighlighter { public getMactionNodes(node: HTMLElement): HTMLElement[] { return Array.from( node.querySelectorAll( - 'mjx-maction[data-collapsible]:not([data-collapse-id])' + `mjx-maction[${MACTION.COLLAPSIBLE}]:not([${MACTION.GROUPID}])` ) ); } diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts index 7cd3c51244..18533dfda1 100644 --- a/ts/a11y/explorer/KeyExplorer.ts +++ b/ts/a11y/explorer/KeyExplorer.ts @@ -26,7 +26,10 @@ import { STATE } from '../../core/MathItem.js'; import type { ExplorerMathItem, ExplorerMathDocument } from '../explorer.js'; import { Explorer, AbstractExplorer } from './Explorer.js'; import { ExplorerPool } from './ExplorerPool.js'; -import { ATTR } from './Highlighter.js'; +import { HILITE } from './strings.js'; +import { SEM } from '../semantic-enrich/strings.js'; +import { SPEECH } from '../speech/strings.js'; +import { MACTION } from '../semantic-enrich/maction.js'; import { MmlNode } from '../../core/MmlTree/MmlNode.js'; import { honk, SemAttr } from '../speech/SpeechUtil.js'; import { GeneratorPool } from '../speech/GeneratorPool.js'; @@ -40,6 +43,8 @@ const isWindows = context.os === 'Windows'; const BRAILLE_PADDING = Array(40).fill('\u2800').join(''); +export const SAVED_HREF = 'data-mjx-href'; + /** * Interface for keyboard explorers. Adds the necessary keyboard events. * @@ -87,7 +92,7 @@ export type keyMapping = ( /** * Selectors for walking. */ -const nav = '[data-speech-node]'; +const nav = `[${SPEECH.NODE}]`; /** * Predicate to check if element is a MJX container. @@ -447,7 +452,7 @@ export class SpeechExplorer // const clicked = this.nodeAtXY( event, - (node) => node.matches('[data-speech-node]'), + (node) => node.matches(nav), [this.speech, this.img], this.document.infoIcon ); @@ -464,7 +469,7 @@ export class SpeechExplorer // otherwise record the click for the focusin handler // document.getSelection()?.removeAllRanges(); - if ((event.target as HTMLElement).getAttribute(ATTR.ADDED)) { + if ((event.target as HTMLElement).getAttribute(HILITE.ADDED)) { this.refocus = clicked; } else { this.clicked = clicked; @@ -493,7 +498,7 @@ export class SpeechExplorer // const clicked = this.nodeAtXY( event, - (node) => node.matches('[data-speech-node]'), + (node) => node.matches(nav), [this.speech, this.img], this.document.infoIcon ); @@ -652,7 +657,7 @@ export class SpeechExplorer * @param {HTMLElement} node The node within the expression to receive the focus */ protected tabTo(node: HTMLElement) { - if (node.getAttribute('data-mjx-href')) { + if (node.getAttribute(SAVED_HREF)) { this.setCurrent(this.linkFor(node)); } else { node.focus(); @@ -698,7 +703,7 @@ export class SpeechExplorer return; } const tabs = this.getInternalTabs(this.current).filter( - (node) => !node.getAttribute('data-mjx-href') + (node) => !node.getAttribute(SAVED_HREF) ); if (tabs.length) { tabs[0].focus(); @@ -902,8 +907,8 @@ export class SpeechExplorer this.speechType = 'd'; const parts = [ [ - this.node.getAttribute('data-semantic-level') ?? 'Level', - this.current.getAttribute('data-semantic-level-number') ?? '0', + this.node.getAttribute(SEM.LEVEL) ?? 'Level', + this.current.getAttribute(SEM.LEVEL_NUMBER) ?? '0', ] .join(' ') .trim(), @@ -913,8 +918,8 @@ export class SpeechExplorer parts.unshift( this.node.getAttribute( action.getAttribute('toggle') === '1' - ? 'data-semantic-expandable' - : 'data-semantic-collapsible' + ? MACTION.EXPANDABLE + : MACTION.COLLAPSIBLE ) ?? '' ); } @@ -943,7 +948,7 @@ export class SpeechExplorer * the expression. */ public nextRules() { - this.node.removeAttribute('data-speech-attached'); + this.node.removeAttribute(SPEECH.ATTACHED); this.restartAfter(this.generators.nextRules(this.item)); } @@ -952,7 +957,7 @@ export class SpeechExplorer * speech for the expression. */ public nextStyle() { - this.node.removeAttribute('data-speech-attached'); + this.node.removeAttribute(SPEECH.ATTACHED); this.restartAfter(this.generators.nextStyle(this.current, this.item)); } @@ -966,7 +971,7 @@ export class SpeechExplorer const action = this.actionable(this.current); if ( !action || - !action.getAttribute('data-collapsible') || + !action.getAttribute(MACTION.COLLAPSIBLE) || action.getAttribute('toggle') !== '1' || this.speechType === 'z' ) { @@ -980,7 +985,7 @@ export class SpeechExplorer const id = this.nodeId(this.current); let current: MmlNode; this.item.root.walkTree((node) => { - if (node.attributes.get('data-semantic-id') === id) { + if (node.attributes.get(SEM.ID) === id) { current = node; } }); @@ -992,7 +997,10 @@ export class SpeechExplorer mml = `${mml}`; } mml = mml.replace( - / (?:data-semantic-|aria-|data-speech-|data-latex).*?=".*?"/g, + new RegExp( + ` (?:${SEM.PREFIX}|${SPEECH.PREFIX}|aria-|data-latex).*?=".*?"`, + 'g' + ), '' ); // @@ -1092,7 +1100,7 @@ export class SpeechExplorer const parts = [...this.item.getSplitNodes(this.current)]; this.highlighter.encloseNodes(parts, this.node); for (const part of parts) { - if (!part.getAttribute(ATTR.ENCLOSED)) { + if (!part.getAttribute(HILITE.ENCLOSED)) { part.classList.add('mjx-selected'); } } @@ -1303,14 +1311,14 @@ export class SpeechExplorer } /** - * Move all the href attributes to data-mjx-href attributes + * Move all the href attributes to SAVED_HREF attributes * (so they won't be focusable links, as they are aria-hidden). */ protected adjustAnchors() { this.anchors = Array.from(this.node.querySelectorAll('a[href]')); for (const anchor of this.anchors) { const href = anchor.getAttribute('href'); - anchor.setAttribute('data-mjx-href', href); + anchor.setAttribute(SAVED_HREF, href); anchor.removeAttribute('href'); } if (this.anchors.length) { @@ -1323,8 +1331,8 @@ export class SpeechExplorer */ protected restoreAnchors() { for (const anchor of this.anchors) { - anchor.setAttribute('href', anchor.getAttribute('data-mjx-href')); - anchor.removeAttribute('data-mjx-href'); + anchor.setAttribute('href', anchor.getAttribute(SAVED_HREF)); + anchor.removeAttribute(SAVED_HREF); } this.anchors = []; } @@ -1343,7 +1351,7 @@ export class SpeechExplorer protected getInternalTabs(node: HTMLElement): HTMLElement[] { return Array.from( node.querySelectorAll( - 'button, [data-mjx-href], input, select, textarea, [tabindex]:not([tabindex="-1"],mjx-speech)' + `button, [${SAVED_HREF}], input, select, textarea, [tabindex]:not([tabindex="-1"],mjx-speech)` ) ); } @@ -1365,7 +1373,7 @@ export class SpeechExplorer * @returns {string} The node's semantic ID */ protected nodeId(node: HTMLElement): string { - return node.getAttribute('data-semantic-id'); + return node.getAttribute(SEM.ID); } /** @@ -1373,7 +1381,7 @@ export class SpeechExplorer * @returns {string} The node's parent's semantic ID */ protected parentId(node: HTMLElement): string { - return node.getAttribute('data-semantic-parent'); + return node.getAttribute(SEM.PARENT); } /** @@ -1381,7 +1389,7 @@ export class SpeechExplorer * @returns {HTMLElement} The HTML node with that id */ protected getNode(id: string): HTMLElement { - return id ? this.node.querySelector(`[data-semantic-id="${id}"]`) : null; + return id ? this.node.querySelector(`[${SEM.ID}="${id}"]`) : null; } /** @@ -1397,7 +1405,7 @@ export class SpeechExplorer * @returns {string[]} The array of semantic IDs of its children */ protected childArray(node: HTMLElement): string[] { - return node ? node.getAttribute('data-semantic-children').split(/,/) : []; + return node ? node.getAttribute(SEM.CHILDREN).split(/,/) : []; } /** @@ -1405,9 +1413,7 @@ export class SpeechExplorer * @returns {boolean} True if the node is a cell node */ protected isCell(node: HTMLElement): boolean { - return ( - !!node && this.cellTypes.includes(node.getAttribute('data-semantic-type')) - ); + return !!node && this.cellTypes.includes(node.getAttribute(SEM.TYPE)); } /** @@ -1415,7 +1421,7 @@ export class SpeechExplorer * @returns {boolean} True if the node is a row node */ protected isRow(node: HTMLElement): boolean { - return !!node && node.getAttribute('data-semantic-type') === 'row'; + return !!node && node.getAttribute(SEM.TYPE) === 'row'; } /** @@ -1479,15 +1485,14 @@ export class SpeechExplorer * @returns {HTMLElement} The first speech child of the node */ protected firstNode(node: HTMLElement): HTMLElement { - const owns = node.getAttribute('data-semantic-owns'); - if (!owns) { - return node.querySelector(nav) as HTMLElement; - } - const ownsList = owns.split(/ /); - for (const id of ownsList) { - const node = this.getNode(id); - if (node?.hasAttribute('data-speech-node')) { - return node; + const owns = node.getAttribute(SEM.OWNS); + if (owns) { + const ownsList = owns.split(/ /); + for (const id of ownsList) { + const node = this.getNode(id); + if (node?.hasAttribute(SPEECH.NODE)) { + return node; + } } } return node.querySelector(nav) as HTMLElement; @@ -1505,14 +1510,11 @@ export class SpeechExplorer * @returns {HTMLElement} The semantic root or first speech node. */ protected rootNode(): HTMLElement { - const base = this.node.querySelector('[data-semantic-structure]'); + const base = this.node.querySelector(`[${SEM.STRUCTURE}]`); if (!base) { return this.node.querySelector(nav) as HTMLElement; } - const id = base - .getAttribute('data-semantic-structure') - .split(/ /)[0] - .replace('(', ''); + const id = base.getAttribute(SEM.STRUCTURE).split(/ /)[0].replace('(', ''); return this.getNode(id); } @@ -1525,15 +1527,13 @@ export class SpeechExplorer protected nextSibling(node: HTMLElement): HTMLElement { const id = this.parentId(node); if (!id) return null; - const owns = this.getNode(id) - .getAttribute('data-semantic-owns') - ?.split(/ /); + const owns = this.getNode(id).getAttribute(SEM.OWNS)?.split(/ /); if (!owns) return null; let i = owns.indexOf(this.nodeId(node)); let next; do { next = this.getNode(owns[++i]); - } while (next && !next.hasAttribute('data-speech-node')); + } while (next && !next.hasAttribute(SPEECH.NODE)); return next; } @@ -1546,15 +1546,13 @@ export class SpeechExplorer protected prevSibling(node: HTMLElement): HTMLElement { const id = this.parentId(node); if (!id) return null; - const owns = this.getNode(id) - .getAttribute('data-semantic-owns') - ?.split(/ /); + const owns = this.getNode(id).getAttribute(SEM.OWNS)?.split(/ /); if (!owns) return null; let i = owns.indexOf(this.nodeId(node)); let prev; do { prev = this.getNode(owns[--i]); - } while (prev && !prev.hasAttribute('data-speech-node')); + } while (prev && !prev.hasAttribute(SPEECH.NODE)); return prev; } @@ -1563,7 +1561,7 @@ export class SpeechExplorer * @returns {boolean} True if the node has a link, false otherwise */ protected isLink(node: HTMLElement = this.current): boolean { - return !!node?.getAttribute('data-semantic-attributes')?.includes('href:'); + return !!node?.getAttribute(SEM.ATTRIBUTES)?.includes('href:'); } /** @@ -1580,7 +1578,7 @@ export class SpeechExplorer * @returns {HTMLElement} The node for which the is handling the href */ protected linkFor(anchor: HTMLElement): HTMLElement { - return anchor?.querySelector('[data-semantic-attributes*="href:"]'); + return anchor?.querySelector(`[{SEM.ATTRIBUTES}*="href:"]`); } /** @@ -1588,9 +1586,7 @@ export class SpeechExplorer * @returns {HTMLElement} The parent node with an href that contains the given node */ protected parentLink(node: HTMLElement): HTMLElement { - const link = node?.closest( - '[data-semantic-attributes*="href:"]' - ) as HTMLElement; + const link = node?.closest(`[${SEM.ATTRIBUTES}*="href:"]`) as HTMLElement; return link && this.node.contains(link) ? link : null; } @@ -1877,7 +1873,7 @@ export class SpeechExplorer } /** - * Executiving the trigger the link action. + * Triggers a link action, if there is one. * * @param {HTMLElement} node The node with the link. * @returns {boolean} True if link was successfully triggered. @@ -1887,7 +1883,7 @@ export class SpeechExplorer const anchor = this.getAnchor(node); anchor.classList.add('mjx-visited'); setTimeout(() => this.FocusOut(null), 50); - window.location.href = anchor.getAttribute('data-mjx-href'); + window.location.href = anchor.getAttribute(SAVED_HREF); return true; } return false; @@ -1911,11 +1907,11 @@ export class SpeechExplorer */ public semanticFocus(): string { const focus = []; - let name = 'data-semantic-id'; + let name = SEM.ID; let node = this.current || this.refocus || this.node; const action = this.actionable(node); if (action) { - name = action.hasAttribute('data-maction-id') ? 'data-maction-id' : 'id'; + name = action.hasAttribute(MACTION.ID) ? MACTION.ID : 'id'; node = action; focus.push(nav); } diff --git a/ts/a11y/explorer/MouseExplorer.ts b/ts/a11y/explorer/MouseExplorer.ts index 5f3e4559b1..7008b95785 100644 --- a/ts/a11y/explorer/MouseExplorer.ts +++ b/ts/a11y/explorer/MouseExplorer.ts @@ -31,6 +31,8 @@ import { import { Explorer, AbstractExplorer } from './Explorer.js'; import { ExplorerPool } from './ExplorerPool.js'; import type { ExplorerMathItem } from '../explorer.js'; +import { SEM } from '../semantic-enrich/strings.js'; +import { MACTION } from '../semantic-enrich/maction.js'; import '../sre.js'; /** @@ -146,8 +148,7 @@ export abstract class Hoverer extends AbstractMouseExplorer { protected nodeAccess: (node: HTMLElement) => T ) { super(document, pool, region, node); - const top = - this.node.querySelector('[data-semantic-structure]') || this.node; + const top = this.node.querySelector(`[${SEM.STRUCTURE}]`) || this.node; this.topBBox = top.getBoundingClientRect(); this.nodeBBox = this.node.getBoundingClientRect(); } @@ -267,7 +268,7 @@ export class ContentHoverer extends Hoverer { region, node, item, - (x) => x.hasAttribute?.('data-semantic-id'), + (x) => x.hasAttribute?.(SEM.ID), (x) => x ); } @@ -296,17 +297,17 @@ export class FlameHoverer extends Hoverer { new DummyRegion(document), node, item, - (x) => x.hasAttribute('data-collapsible'), + (x) => x.hasAttribute(MACTION.COLLAPSIBLE), () => {} ); } display(node: HTMLElement) { - const id = node.getAttribute('data-collapse-id'); + const id = node.getAttribute(MACTION.GROUPID); if (id) { node = this.node.querySelector(`#${id}`); } - let parts: HTMLElement[] = node.hasAttribute('data-collapse-group') + let parts: HTMLElement[] = node.hasAttribute(MACTION.GROUP) ? this.highlighter.getMactionGroup(this.node, node) : [node]; parts = this.highlighter.encloseNodes([...parts], this.node); diff --git a/ts/a11y/explorer/Region.ts b/ts/a11y/explorer/Region.ts index 1902de0305..a8b701b29e 100644 --- a/ts/a11y/explorer/Region.ts +++ b/ts/a11y/explorer/Region.ts @@ -25,6 +25,14 @@ import { MathDocument } from '../../core/MathDocument.js'; import { StyleJsonSheet } from '../../util/StyleJson.js'; import { Highlighter } from './Highlighter.js'; import { SsmlElement, buildSpeech } from '../speech/SpeechUtil.js'; +import { SEM } from '../semantic-enrich/strings.js'; +import { MACTION } from '../semantic-enrich/maction.js'; +import { HILITE } from './strings.js'; + +export const MAG = { + CLONE: 'data-mjx-clone', + CONTAINER: 'data-mjx-clone-container', +}; export type A11yDocument = MathDocument; @@ -204,14 +212,12 @@ export abstract class AbstractRegion implements Region { this.CLASS.className + '_Show' ); // Get all the shown regions (one is this element!) and append at bottom. - for (let i = 0, region; (region = regions[i]); i++) { - if (region !== this.div) { - baseBottom = Math.max( - region.getBoundingClientRect().bottom, - baseBottom - ); - baseLeft = Math.min(region.getBoundingClientRect().left, baseLeft); + for (const region of Array.from(regions)) { + if (region == this.div) { + break; } + baseBottom = Math.max(region.getBoundingClientRect().bottom, baseBottom); + baseLeft = Math.min(region.getBoundingClientRect().left, baseLeft); } const bot = (baseBottom ? baseBottom : rect.bottom + 10) + window.scrollY; @@ -415,28 +421,28 @@ export class LiveRegion extends StringRegion { // // Primary highlighting colors // - 'mjx-container [data-sre-highlight-1]:not([data-mjx-collapsed], rect)': { + [`mjx-container [${HILITE.PREFIX}1]:not([${MACTION.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)', + `mjx-container:not([${MAG.CONTAINER}])`, + `[${HILITE.PREFIX}1]:not([${HILITE.ENCLOSED}], rect)`, ].join(' ')]: { 'background-color': 'var(--mjx-bg1-color) ! important', // // CHTML }, - 'mjx-container rect[data-sre-highlight-1]:not([data-sre-enclosed])': { + [`mjx-container rect[${HILITE.PREFIX}1]:not([${HILITE.ENCLOSED}])`]: { fill: 'var(--mjx-bg1-color) ! important', // // SVG }, // // Secondary highlighting colors // - 'mjx-container [data-sre-highlight-2]': { + [`mjx-container [${HILITE.PREFIX}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]': { + [`mjx-container rect[${HILITE.PREFIX}2]`]: { fill: 'var(--mjx-bg2-color) ! important', // // SVG }, }); @@ -652,9 +658,7 @@ export class SpeechRegion extends LiveRegion { */ private highlightNode(id: string, init: boolean = false) { this.highlighter.unhighlight(); - const nodes = Array.from( - this.node.querySelectorAll(`[data-semantic-id="${id}"]`) - ); + const nodes = Array.from(this.node.querySelectorAll(`[${SEM.ID}="${id}"]`)); if (!this.clear || init) { this.highlighter.highlight(nodes as HTMLElement[]); } @@ -707,10 +711,10 @@ export class HoverRegion extends AbstractRegion { border: '1px solid #7C7C7C', }, }, - 'mjx-container[data-mjx-clone-container]': { + [`mjx-container[${MAG.CONTAINER}]`]: { padding: '2px ! important', }, - 'mjx-container[data-mjx-clone-container][display] > mjx-math': { + [`mjx-container[${MAG.CONTAINER}][display] > mjx-math`]: { 'text-align': 'center', }, 'mjx-math > mjx-mlabeledtr': { @@ -730,7 +734,7 @@ export class HoverRegion extends AbstractRegion { */ protected position(node: HTMLElement) { const prev = node.previousSibling as HTMLElement; - if (prev?.getAttribute('data-sre-highlighter-added')) { + if (prev?.getAttribute(HILITE.ADDED)) { node = prev; } const nodeRect = node.getBoundingClientRect(); @@ -786,7 +790,7 @@ export class HoverRegion extends AbstractRegion { if (!this.div) return; this.Clear(); const mjx = this.cloneNode(node); - const selected = mjx.querySelector('[data-mjx-clone]') as HTMLElement; + const selected = mjx.querySelector(`[${MAG.CLONE}]`) as HTMLElement; this.inner.style.backgroundColor = node.style.backgroundColor; if (selected) { selected.style.backgroundColor = ''; @@ -805,7 +809,7 @@ export class HoverRegion extends AbstractRegion { */ protected cloneNode(node: HTMLElement): HTMLElement { let mjx = node.cloneNode(true) as HTMLElement; - mjx.setAttribute('data-mjx-clone', 'true'); + mjx.setAttribute(MAG.CLONE, 'true'); if (mjx.nodeName !== 'MJX-CONTAINER') { if (mjx.nodeName !== 'g') { mjx.style.marginLeft = mjx.style.marginRight = '0'; @@ -826,7 +830,7 @@ export class HoverRegion extends AbstractRegion { mjx.style.margin = '0'; mjx.style.minWidth = ''; } - mjx.setAttribute('data-mjx-clone-container', 'true'); + mjx.setAttribute(MAG.CONTAINER, 'true'); return mjx; } @@ -838,7 +842,7 @@ export class HoverRegion extends AbstractRegion { protected chtmlClone(node: Element, enclosed: Element[], mjx: HTMLElement) { const included = new Set(); for (const child of enclosed) { - const id = child.getAttribute('data-semantic-id'); + const id = child.getAttribute(SEM.ID); if (included.has(id)) { mjx.appendChild(document.createElement('br')); } @@ -846,7 +850,7 @@ export class HoverRegion extends AbstractRegion { const clone = mjx.appendChild(child.cloneNode(true)) as HTMLElement; clone.classList.remove('mjx-selected'); if (child === node) { - clone.setAttribute('data-mjx-clone', 'true'); + clone.setAttribute(MAG.CLONE, 'true'); clone.removeAttribute('space'); } } @@ -869,7 +873,7 @@ export class HoverRegion extends AbstractRegion { const g = container.querySelector('g').cloneNode(false); for (const child of enclosed) { const rect = child.previousSibling as SVGRectElement; - if (rect?.getAttribute('data-sre-highlighter-added')) { + if (rect?.getAttribute(HILITE.ADDED)) { const bbox = rect.getBBox(); const [X, Y] = this.xy(rect); [x, y] = [X, Y + bbox.y]; @@ -883,7 +887,7 @@ export class HoverRegion extends AbstractRegion { const clone = g.appendChild(child.cloneNode(true)) as HTMLElement; clone.classList.remove('mjx-selected'); if (child === node) { - clone.setAttribute('data-mjx-clone', 'true'); + clone.setAttribute(MAG.CLONE, 'true'); } const [cx, cy] = this.xy(child); clone.setAttribute('transform', `translate(${cx}, ${cy})`); @@ -900,10 +904,8 @@ export class HoverRegion extends AbstractRegion { ) { mjx.innerHTML = ''; mjx.appendChild(container.cloneNode(true).firstChild); - mjx - .querySelector('.mjx-selected') - ?.setAttribute('data-mjx-clone', 'true'); - mjx.querySelector('[data-sre-highlighter-added]')?.remove(); + mjx.querySelector('.mjx-selected')?.setAttribute(MAG.CLONE, 'true'); + mjx.querySelector(`[${HILITE.ADDED}]`)?.remove(); return; } // diff --git a/ts/a11y/explorer/TreeExplorer.ts b/ts/a11y/explorer/TreeExplorer.ts index 1192281211..e8b1a4b4f4 100644 --- a/ts/a11y/explorer/TreeExplorer.ts +++ b/ts/a11y/explorer/TreeExplorer.ts @@ -25,6 +25,7 @@ import { A11yDocument, Region } from './Region.js'; import { AbstractExplorer } from './Explorer.js'; import { ExplorerPool } from './ExplorerPool.js'; +import { SEM } from '../semantic-enrich/strings.js'; export class AbstractTreeExplorer extends AbstractExplorer { /** @@ -89,7 +90,7 @@ export class TreeColorer extends AbstractTreeExplorer { public contrast: ContrastPicker = new ContrastPicker(); private leaves: HTMLElement[] = []; - private modality: string = 'data-semantic-foreground'; + private modality: string = SEM.FOREGROUND; /** * @override @@ -120,9 +121,7 @@ export class TreeColorer extends AbstractTreeExplorer { */ private colorLeaves() { this.leaves = Array.from( - this.node.querySelectorAll( - '[data-semantic-id]:not([data-semantic-children])' - ) + this.node.querySelectorAll(`[${SEM.ID}]:not([${SEM.CHILDREN}])`) ); for (const leaf of this.leaves) { leaf.setAttribute(this.modality, this.contrast.generate()); diff --git a/ts/a11y/explorer/strings.ts b/ts/a11y/explorer/strings.ts new file mode 100644 index 0000000000..dfc127c62b --- /dev/null +++ b/ts/a11y/explorer/strings.ts @@ -0,0 +1,32 @@ +/************************************************************* + * + * Copyright (c) 2026 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. + */ + +/** + * @file The data attribute strings for the explorer + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +/** + * The attributes for various markers + */ +export const HILITE = { + ENCLOSED: 'data-sre-enclosed', + BBOX: 'data-sre-highlighter-bbox', + ADDED: 'data-sre-highlighter-added', + PREFIX: 'data-sre-highlight-', +}; diff --git a/ts/a11y/semantic-enrich.ts b/ts/a11y/semantic-enrich.ts index 11088d16d8..fe37082cdb 100644 --- a/ts/a11y/semantic-enrich.ts +++ b/ts/a11y/semantic-enrich.ts @@ -39,6 +39,9 @@ import { MathML } from '../input/mathml.js'; import { SerializedMmlVisitor } from '../core/MmlTree/SerializedMmlVisitor.js'; import { OptionList, expandable } from '../util/Options.js'; import * as Sre from './sre.js'; +import { SEM } from './semantic-enrich/strings.js'; +import { SPEECH } from './speech/strings.js'; +import { MACTION } from './semantic-enrich/maction.js'; import { StructureUtil, SemanticMap } from './speech/StructureUtil.js'; import { Locale } from '../util/Locale.js'; import { COMPONENT } from './semantic-enrich/__locales__/Component.js'; @@ -87,10 +90,10 @@ export class enrichVisitor extends SerializedMmlVisitor { // Add maction id and make sure selection is the next attribute // attributes = - ` data-maction-id="${id}" selection="${node.attributes.get('selection')}"` + + ` ${MACTION.ID}="${id}" selection="${node.attributes.get('selection')}"` + attributes .replace(/ selection="\d+"/, '') - .replace(/ data-maction-id="\d+"/, ''); + .replace(` ${MACTION.ID}="\\d+"`, ''); } return ( `${space}` + @@ -236,8 +239,8 @@ export function EnrichedMathItemMixin< // SRE is updated to do this itself. // math.math = math.math - .replace(/ role="treeitem"/g, ' data-speech-node="true"') - .replace(/ aria-level/g, ' data-semantic-level-number') + .replace(/ role="treeitem"/g, ` ${SPEECH.NODE}="true"`) + .replace(/ aria-level/g, ` ${SEM.LEVEL_NUMBER}`) .replace(/ aria-(?:posinset|owns|setsize)=".*?"/g, ''); math.display = this.display; math.compile(document); @@ -282,11 +285,11 @@ export function EnrichedMathItemMixin< const maction = [] as MmlNode[]; this.root.walkTree((node: MmlNode) => { if (node.isKind('maction')) { - maction[node.attributes.get('data-maction-id') as number] = node; + maction[node.attributes.get(MACTION.ID) as number] = node; } }); return mml.replace( - /(data-maction-id="(\d+)" selection=)"\d+"/g, + new RegExp(`(${MACTION.ID}="(\\d+)" selection=)"\\d+"`, 'g'), (_match: string, prefix: string, id: number) => `${prefix}"${maction[id].attributes.get('selection')}"` ); diff --git a/ts/a11y/semantic-enrich/maction.ts b/ts/a11y/semantic-enrich/maction.ts new file mode 100644 index 0000000000..3975ccd481 --- /dev/null +++ b/ts/a11y/semantic-enrich/maction.ts @@ -0,0 +1,31 @@ +/************************************************************* + * + * Copyright (c) 2026 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. + */ + +/** + * @file The data attribute strings for mactions + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +export const MACTION = { + COLLAPSED: 'data-mjx-collapsed', + COLLAPSIBLE: 'data-collapsible', + EXPANDABLE: 'data-expandable', + ID: 'data-maction-id', + GROUP: 'data-maction-group', + GROUPID: 'data-maction-group-id', +}; diff --git a/ts/a11y/semantic-enrich/strings.ts b/ts/a11y/semantic-enrich/strings.ts new file mode 100644 index 0000000000..2f4339f242 --- /dev/null +++ b/ts/a11y/semantic-enrich/strings.ts @@ -0,0 +1,47 @@ +/************************************************************* + * + * Copyright (c) 2026 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. + */ + +/** + * @file The data attribute strings for semantics + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +export const SEM = { + ADDED: 'data-semanitic-added', + ATTRIBUTES: 'data-semantic-attributes', + CHILDREN: 'data-semantic-children', + COLLAPSIBLE: 'data-semantic-collapsible', + DOMAIN: 'data-semantic-domain', + DOMAIN2STYLE: 'data-semantic-domain2style', + EXPANDABLE: 'data-semantic-expandable', + FENCEPOINTER: 'data-semantic-fencepointer', + FOREGROUND: 'data-semantic-foreground', + ID: 'data-semantic-id', + LEVEL: 'data-semantic-level', + LEVEL_NUMBER: 'data-semantic-level-number', + LOCALE: 'data-semantic-locale', + OWNS: 'data-semantic-owns', + PARENT: 'data-semantic-parent', + PREFIX: 'data-semantic-', + PREFIX_NONE: 'data-semantic-prefix-none', + ROLE: 'data-semantic-role', + SPEECH: 'data-semantic-speech', + STRUCTURE: 'data-semantic-structure', + STYLE: 'data-semantic-style', + TYPE: 'data-semantic-type', +}; diff --git a/ts/a11y/speech/GeneratorPool.ts b/ts/a11y/speech/GeneratorPool.ts index 82da3e344a..410a3c521f 100644 --- a/ts/a11y/speech/GeneratorPool.ts +++ b/ts/a11y/speech/GeneratorPool.ts @@ -27,6 +27,7 @@ import { buildLabel, SemAttr } from '../speech/SpeechUtil.js'; import { DOMAdaptor } from '../../core/DOMAdaptor.js'; import { SpeechMathItem } from '../speech.js'; import { WorkerHandler } from './WebWorker.js'; +import { SEM } from '../semantic-enrich/strings.js'; /** * @template N The HTMLElement node class @@ -160,11 +161,10 @@ export class GeneratorPool { */ private getOptions(node: N): OptionList { return { - locale: this.adaptor.getAttribute(node, 'data-semantic-locale') ?? '', - domain: this.adaptor.getAttribute(node, 'data-semantic-domain') ?? '', - style: this.adaptor.getAttribute(node, 'data-semantic-style') ?? '', - domain2style: - this.adaptor.getAttribute(node, 'data-semantic-domain2style') ?? '', + locale: this.adaptor.getAttribute(node, SEM.LOCALE) ?? '', + domain: this.adaptor.getAttribute(node, SEM.DOMAIN) ?? '', + style: this.adaptor.getAttribute(node, SEM.STYLE) ?? '', + domain2style: this.adaptor.getAttribute(node, SEM.DOMAIN2STYLE) ?? '', }; } @@ -197,7 +197,7 @@ export class GeneratorPool { return (this.promise = this.webworker.nextStyle( item.outputData.mml, Object.assign({}, this.options, { modality: 'speech' }), - this.adaptor.getAttribute(node, 'data-semantic-id'), + this.adaptor.getAttribute(node, SEM.ID), item )); } diff --git a/ts/a11y/speech/SpeechMenu.ts b/ts/a11y/speech/SpeechMenu.ts index 3aa47057ad..d7c07711b7 100644 --- a/ts/a11y/speech/SpeechMenu.ts +++ b/ts/a11y/speech/SpeechMenu.ts @@ -31,6 +31,7 @@ import { import { Submenu } from '../../ui/menu/mj-context-menu.js'; import { localize } from '../../ui/menu/__locales__/Component.js'; import * as Sre from '../sre.js'; +import { SEM } from '../semantic-enrich/strings.js'; /** * Values for the ClearSpeak preference variables. @@ -264,7 +265,7 @@ export async function clearspeakMenu( const previous = currentPreference(menu.settings.speechRules); items = items.concat(basePreferences(previous)); const focus = explorer?.refocus; - const semantic = focus?.getAttribute('data-semantic-id') ?? null; + const semantic = focus?.getAttribute(SEM.ID) ?? null; const count = counter++; await item.generatorPool.getRelevantPreferences( item, diff --git a/ts/a11y/speech/StructureUtil.ts b/ts/a11y/speech/StructureUtil.ts index af9edcafa0..17ce5a1de9 100644 --- a/ts/a11y/speech/StructureUtil.ts +++ b/ts/a11y/speech/StructureUtil.ts @@ -1,4 +1,5 @@ import { MmlNode } from '../../core/MmlTree/MmlNode.js'; +import { SEM } from '../semantic-enrich/strings.js'; /**********************************************************************/ /* @@ -55,7 +56,7 @@ export class StructureUtil { id: string = '', map: ParentMap = new Map() ): ParentMap { - const nid = node.attributes.get('data-semantic-id') as string; + const nid = node.attributes.get(SEM.ID) as string; if (nid) { map.set(nid, id); } @@ -76,7 +77,7 @@ export class StructureUtil { public static semanticNodes(root: MmlNode): SemanticMap { let sexp = ''; root.walkTree((node) => { - sexp = node.attributes?.get('data-semantic-structure') as string; + sexp = node.attributes?.get(SEM.STRUCTURE) as string; return !!sexp; }); const tree = this.parse(this.tokenize(sexp)); diff --git a/ts/a11y/speech/WebWorker.ts b/ts/a11y/speech/WebWorker.ts index fd5fca20a1..80482c0c19 100644 --- a/ts/a11y/speech/WebWorker.ts +++ b/ts/a11y/speech/WebWorker.ts @@ -26,6 +26,8 @@ import { OptionList } from '../../util/Options.js'; import { Message, ClientCommand, Structure } from './MessageTypes.js'; import { SpeechMathItem } from '../speech.js'; import { SemAttr } from './SpeechUtil.js'; +import { SEM } from '../semantic-enrich/strings.js'; +import { SPEECH, BRAILLE } from './strings.js'; import { Locale } from '../../util/Locale.js'; import { localize, COMPONENT } from './__locales__/Component.js'; @@ -317,14 +319,14 @@ export class WorkerHandler { const data = JSON.parse(structure) as Structure; const container = item.typesetRoot; if (!container) return; // Element is gone, maybe retypeset or removed. - this.setSpecialAttributes(container, data.options, 'data-semantic-', [ + this.setSpecialAttributes(container, data.options, SEM.PREFIX, [ 'locale', 'domain', 'style', 'domain2style', ]); const adaptor = this.adaptor; - this.setSpecialAttributes(container, data.translations, 'data-semantic-'); + this.setSpecialAttributes(container, data.translations, SEM.PREFIX); // Sort out Mactions for (const [id, sid] of Object.entries(data.mactions)) { let node = adaptor.getElement('#' + id, container); @@ -335,7 +337,7 @@ export class WorkerHandler { if (adaptor.kind(node) === 'rect') { node = adaptor.next(node) as N; } - adaptor.setAttribute(node, 'data-semantic-type', 'dummy'); + adaptor.setAttribute(node, SEM.TYPE, 'dummy'); this.setSpecialAttributes(node, sid, ''); } for (const child of adaptor.childNodes(container)) { @@ -349,7 +351,7 @@ export class WorkerHandler { adaptor.setAttribute(container, SemAttr.SPEECH_SSML, data.ssml); item.outputData.speech = data.label; } - adaptor.setAttribute(container, 'data-speech-attached', 'true'); + adaptor.setAttribute(container, SPEECH.ATTACHED, 'true'); } if (braille) { if (data.braillelabel) { @@ -357,7 +359,7 @@ export class WorkerHandler { item.outputData.braille = data.braillelabel; } if (data.braille) { - adaptor.setAttribute(container, 'data-braille-attached', 'true'); + adaptor.setAttribute(container, BRAILLE.ATTACHED, 'true'); } } } @@ -377,19 +379,19 @@ export class WorkerHandler { braille: boolean ) { const adaptor = this.adaptor; - const id = adaptor.getAttribute(node, 'data-semantic-id'); - adaptor.removeAttribute(node, 'data-speech-node'); + const id = adaptor.getAttribute(node, SEM.ID); + adaptor.removeAttribute(node, SPEECH.NODE); if (speech && data.speech[id]['speech-none']) { - adaptor.setAttribute(node, 'data-speech-node', 'true'); + adaptor.setAttribute(node, SPEECH.NODE, 'true'); for (let [key, value] of Object.entries(data.speech[id])) { key = key.replace(/-ssml$/, ''); if (value) { - adaptor.setAttribute(node, `data-semantic-${key}`, value as string); + adaptor.setAttribute(node, `${SEM.PREFIX}${key}`, value as string); } } } if (braille && data.braille?.[id]?.['braille-none']) { - adaptor.setAttribute(node, 'data-speech-node', 'true'); + adaptor.setAttribute(node, SPEECH.NODE, 'true'); const value = data.braille[id]['braille-none']; adaptor.setAttribute(node, SemAttr.BRAILLE, value); } @@ -421,10 +423,10 @@ export class WorkerHandler { return rootId; } root = root as N; - if (adaptor.hasAttribute(root, 'data-semantic-id')) { + if (adaptor.hasAttribute(root, SEM.ID)) { this.setSpeechAttribute(root, data, speech, braille); - if (!rootId && !adaptor.hasAttribute(root, 'data-semantic-parent')) { - rootId = adaptor.getAttribute(root, 'data-semantic-id'); + if (!rootId && !adaptor.hasAttribute(root, SEM.PARENT)) { + rootId = adaptor.getAttribute(root, SEM.ID); } } for (const child of Array.from(adaptor.childNodes(root))) { @@ -464,8 +466,8 @@ export class WorkerHandler { */ public Detach(item: SpeechMathItem) { const container = item.typesetRoot; - this.adaptor.removeAttribute(container, 'data-speech-attached'); - this.adaptor.removeAttribute(container, 'data-braille-attached'); + this.adaptor.removeAttribute(container, SPEECH.ATTACHED); + this.adaptor.removeAttribute(container, BRAILLE.ATTACHED); this.detachSpeech(container); } @@ -487,7 +489,7 @@ export class WorkerHandler { 'summary', 'braille', ]) { - adaptor.removeAttribute(node, `data-semantic-${key}`); + adaptor.removeAttribute(node, `${SEM.PREFIX}${key}`); } } for (const child of children) { diff --git a/ts/a11y/speech/strings.ts b/ts/a11y/speech/strings.ts new file mode 100644 index 0000000000..9e3385ab06 --- /dev/null +++ b/ts/a11y/speech/strings.ts @@ -0,0 +1,32 @@ +/************************************************************* + * + * Copyright (c) 2026 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. + */ + +/** + * @file The property and data attribute strings for collapsing + * + * @author dpvc@mathjax.org (Davide Cervone) + */ + +export const SPEECH = { + ATTACHED: 'data-speech-attached', + NODE: 'data-speech-node', + PREFIX: 'data-speech-', +}; + +export const BRAILLE = { + ATTACHED: 'data-braille-attached', +}; diff --git a/ts/output/chtml/Wrappers/maction.ts b/ts/output/chtml/Wrappers/maction.ts index 7c5ead16a1..f6bc688477 100644 --- a/ts/output/chtml/Wrappers/maction.ts +++ b/ts/output/chtml/Wrappers/maction.ts @@ -44,6 +44,7 @@ import { TextNode } from '../../../core/MmlTree/MmlNode.js'; import { StyleJson } from '../../../util/StyleJson.js'; import { STATE } from '../../../core/MathItem.js'; import { mathjax } from '../../../mathjax.js'; +import { MACTION } from '../../../a11y/semantic-enrich/maction.js'; /*****************************************************************/ /** @@ -191,7 +192,7 @@ export const ChtmlMaction = (function (): ChtmlMactionClass { 'background-color': '#F8F8F8', color: 'black', }, - 'mjx-container [data-mjx-collapsed]': { + [`mjx-container [${MACTION.COLLAPSED}]`]: { color: '#55F', }, @@ -206,7 +207,7 @@ export const ChtmlMaction = (function (): ChtmlMactionClass { 'background-color': '#303030', color: '#E0E0E0', }, - 'mjx-container [data-mjx-collapsed]': { + [`mjx-container [${MACTION.COLLAPSED}]`]: { color: '#88F', }, }, @@ -249,11 +250,11 @@ export const ChtmlMaction = (function (): ChtmlMactionClass { math.start.n = math.end.n = 0; } mml.nextToggleSelection(); - if (mml.attributes.get('data-collapse-group')) { + if (mml.attributes.get(MACTION.GROUP)) { const id = mml.attributes.get('id'); const selection = mml.attributes.get('selection'); math.root.walkTree((node) => { - if (node.attributes.get('data-collapse-id') === id) { + if (node.attributes.get(MACTION.GROUPID) === id) { node.attributes.set('selection', selection); } }); @@ -261,7 +262,7 @@ export const ChtmlMaction = (function (): ChtmlMactionClass { mathjax.handleRetriesFor(() => { math.rerender( document, - mml.attributes.get('data-maction-id') + mml.attributes.get(MACTION.ID) ? STATE.ENRICHED : STATE.RERENDER ); diff --git a/ts/output/common.ts b/ts/output/common.ts index d9b4a6167d..73541edb87 100644 --- a/ts/output/common.ts +++ b/ts/output/common.ts @@ -45,6 +45,7 @@ import { length2em } from '../util/lengths.js'; import { StyleList, Styles } from '../util/Styles.js'; import { StyleJson, StyleJsonSheet } from '../util/StyleJson.js'; import { BBox } from '../util/BBox.js'; +import { SEM } from '../a11y/semantic-enrich/strings.js'; /*****************************************************************/ @@ -592,10 +593,7 @@ export abstract class CommonOutputJax< child.childNodes[0].setProperty('inline-breaks', true); node.parent.setProperty('process-breaks', 'true'); } - } else if ( - child.isKind('mrow') && - child.attributes.get('data-semantic-added') - ) { + } else if (child.isKind('mrow') && child.attributes.get(SEM.ADDED)) { this.markInlineBreaks(child); if (child.getProperty('process-breaks')) { child.setProperty('inline-breaks', true); diff --git a/ts/output/common/Wrappers/scriptbase.ts b/ts/output/common/Wrappers/scriptbase.ts index 3b3220a718..b3318e1c67 100644 --- a/ts/output/common/Wrappers/scriptbase.ts +++ b/ts/output/common/Wrappers/scriptbase.ts @@ -47,6 +47,7 @@ import { MmlMo } from '../../../core/MmlTree/MmlNodes/mo.js'; import { BBox } from '../../../util/BBox.js'; import { LineBBox } from '../LineBBox.js'; import { DIRECTION } from '../FontData.js'; +import { SEM } from '../../../a11y/semantic-enrich/strings.js'; /*****************************************************************/ /** @@ -527,7 +528,7 @@ export function CommonScriptbaseMixin< */ public getSemanticBase(): WW { const fence = this.node.attributes.getExplicit( - 'data-semantic-fencepointer' + SEM.FENCEPOINTER ) as string; return this.getBaseFence(this.baseChild, fence); } @@ -539,7 +540,7 @@ export function CommonScriptbaseMixin< if (!fence || !fence.node.attributes || !id) { return null; } - if (fence.node.attributes.getExplicit('data-semantic-id') === id) { + if (fence.node.attributes.getExplicit(SEM.ID) === id) { return fence; } for (const child of fence.childNodes) { diff --git a/ts/output/svg/Wrappers/maction.ts b/ts/output/svg/Wrappers/maction.ts index e8f03d7f47..8ea22a1765 100644 --- a/ts/output/svg/Wrappers/maction.ts +++ b/ts/output/svg/Wrappers/maction.ts @@ -47,6 +47,7 @@ import { import { StyleJson } from '../../../util/StyleJson.js'; import { STATE } from '../../../core/MathItem.js'; import { mathjax } from '../../../mathjax.js'; +import { MACTION } from '../../../a11y/semantic-enrich/maction.js'; /*****************************************************************/ /** @@ -190,7 +191,7 @@ export const SvgMaction = (function (): SvgMactionClass { 'background-color': '#F8F8F8', color: 'black', }, - 'g[data-mjx-collapsed]': { + [`g[${MACTION.COLLAPSED}]`]: { fill: '#55F', }, @@ -204,7 +205,7 @@ export const SvgMaction = (function (): SvgMactionClass { 'background-color': '#303030', color: '#E0E0E0', }, - 'g[data-mjx-collapsed]': { + [`g[${MACTION.COLLAPSED}]`]: { fill: '#88F', }, }, @@ -247,11 +248,11 @@ export const SvgMaction = (function (): SvgMactionClass { math.start.n = math.end.n = 0; } mml.nextToggleSelection(); - if (mml.attributes.get('data-collapse-group')) { + if (mml.attributes.get(MACTION.GROUP)) { const id = mml.attributes.get('id'); const selection = mml.attributes.get('selection'); math.root.walkTree((node) => { - if (node.attributes.get('data-collapse-id') === id) { + if (node.attributes.get(MACTION.GROUPID) === id) { node.attributes.set('selection', selection); } }); @@ -259,7 +260,7 @@ export const SvgMaction = (function (): SvgMactionClass { mathjax.handleRetriesFor(() => { math.rerender( document, - mml.attributes.get('data-maction-id') + mml.attributes.get(MACTION.ID) ? STATE.ENRICHED : STATE.RERENDER ); diff --git a/ts/ui/menu/Menu.ts b/ts/ui/menu/Menu.ts index 899ecf55e5..dd52598a39 100644 --- a/ts/ui/menu/Menu.ts +++ b/ts/ui/menu/Menu.ts @@ -1670,7 +1670,7 @@ export class Menu { // Remove unwanted attributes // if (!this.settings.showSRE) { - svg = svg.replace(/ (?:data-semantic-.*?|data-speech-node)=".*?"/g, ''); + svg = svg.replace(/ (?:data-semantic-.*?|data-collapse-.*?|data-speech-node)=".*?"/g, ''); } if (!this.settings.showTex) { svg = svg.replace(/ data-latex(?:-item)?=".*?"/g, ''); diff --git a/ts/ui/menu/MmlVisitor.ts b/ts/ui/menu/MmlVisitor.ts index a4f9e4756e..91fc882f93 100644 --- a/ts/ui/menu/MmlVisitor.ts +++ b/ts/ui/menu/MmlVisitor.ts @@ -119,7 +119,7 @@ export class MmlVisitor extends SerializedMmlVisitor { if (this.options.filterSRE) { const keys = Object.keys(list).filter((id) => id.match( - /^(?:data-semantic-.*?|data-speech-node|role|aria-(?:level|posinset|setsize|owns))$/ + /^(?:data-semantic-.*?|data-collapse-.*?|data-speech-node|role|aria-(?:level|posinset|setsize|owns))$/ ) ); for (const key of keys) {