Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion ts/a11y/complexity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,11 @@ export function ComplexityMathDocumentMixin<N, T, D, B extends EMDocC<N, T, D>>(
visitorOptions
);
const computeComplexity = (math: ComplexityMathItem<N, T, D>) => {
math.parseSemanticNodes();
math.initialID = this.complexityVisitor.visitTree(
math.root,
math.initialID
math.initialID,
math.semanticNodes
);
};
this.options.MathItem = ComplexityMathItemMixin<
Expand Down
95 changes: 73 additions & 22 deletions ts/a11y/complexity/collapse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from '../../core/MmlTree/MmlNode.js';
import { PropertyList } from '../../core/Tree/Node.js';
import { ComplexityVisitor } from './visitor.js';
import type { SemanticMap } from '../speech/StructureUtil.js';

/*==========================================================================*/

Expand Down Expand Up @@ -365,7 +366,7 @@ export class Collapse {
*
* @param {MmlNode} node The node to check
* @param {number} complexity The current complexity of the node
* @returns {number} The revised complexity
* @returns {number} The revised complexity
*/
public check(node: MmlNode, complexity: number): number {
const type = node.attributes.get('data-semantic-type') as string;
Expand All @@ -384,7 +385,7 @@ export class Collapse {
* @param {MmlNode} node The node to check
* @param {number} complexity The current complexity of the node
* @param {string} type The semantic type of the node
* @returns {number} The revised complexity
* @returns {number} The revised complexity
*/
protected defaultCheck(
node: MmlNode,
Expand All @@ -408,7 +409,7 @@ export class Collapse {
* @param {MmlNode} node The node to check
* @param {number} complexity The current complexity of the node
* @param {string} text The text to use for the collapsed node
* @returns {number} The revised complexity for the collapsed node
* @returns {number} The revised complexity for the collapsed node
*/
protected recordCollapse(
node: MmlNode,
Expand Down Expand Up @@ -439,7 +440,7 @@ export class Collapse {
* @param {MmlNode} node The node to check if its child is collapsible
* @param {number} n The position of the child node to check
* @param {number=} m The number of children node must have
* @returns {MmlNode|null} The child node that was collapsed (or null)
* @returns {MmlNode|null} The child node that was collapsed (or null)
*/
protected canUncollapse(
node: MmlNode,
Expand All @@ -466,7 +467,7 @@ export class Collapse {
* @param {MmlNode} node The node to check
* @param {number} n The position of the child node to check
* @param {number=} m The number of children the node must have
* @returns {number} The updated complexity
* @returns {number} The updated complexity
*/
protected uncollapseChild(
complexity: number,
Expand All @@ -488,7 +489,7 @@ export class Collapse {
/**
* @param {MmlNode} node The node whose attribute is to be split
* @param {string} id The name of the data-semantic attribute to split
* @returns {string[]} Array of ids in the attribute split at commas
* @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(
Expand All @@ -508,7 +509,7 @@ export class Collapse {
/**
* @param {MmlNode} node The node whose child text is needed
* @param {string} id The (semantic) id of the child needed
* @returns {string} The text of the specified child node
* @returns {string} The text of the specified child node
*/
protected findChildText(node: MmlNode, id: string): string {
const child = this.findChild(node, id);
Expand All @@ -518,7 +519,7 @@ export class Collapse {
/**
* @param {MmlNode} node The node whose child is to be located
* @param {string} id The (semantic) id of the child to be found
* @returns {MmlNode|null} The child node (or null if not found)
* @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;
Expand All @@ -534,11 +535,16 @@ export class Collapse {
/**
* Add maction nodes to the nodes in the tree that can collapse
*
* @param {MmlNode} node The root of the tree to check
* @param {number|null} id The initial id to use
* @returns {number} The initial id used
* @param {MmlNode} node The root of the tree to check
* @param {number|null} id The initial id to use
* @param {SemanticMap} parts The map of ids to extra nodes
* @returns {number} The initial id used
*/
public makeCollapse(node: MmlNode, id: number | null): number {
public makeCollapse(
node: MmlNode,
id: number | null,
parts: SemanticMap
): number {
let oldCount = null;
if (id === null) {
id = this.idCount;
Expand All @@ -552,19 +558,25 @@ export class Collapse {
nodes.push(child);
}
});
this.makeActions(nodes);
this.makeActions(node, nodes, parts);
if (oldCount !== null) {
this.idCount = oldCount;
}
return id;
}

/**
* @param {MmlNode[]} nodes The list of nodes to replace by maction nodes
* @param {MmlNode} root The top of the MathML tree
* @param {MmlNode[]} nodes The list of nodes to replace by maction nodes
* @param {SemanticMap} parts The map of ids to extra nodes
*/
public makeActions(nodes: MmlNode[]) {
public makeActions(root: MmlNode, nodes: MmlNode[], parts: SemanticMap) {
for (const node of nodes) {
this.makeAction(node);
const extra =
parts
.get(node.attributes.get('data-semantic-id') as string)
?.slice(1) ?? [];
this.makeAction(root, node, extra);
}
}

Expand All @@ -576,18 +588,20 @@ export class Collapse {
}

/**
* @param {MmlNode} node The node to make collapsible by replacing with an maction
* @param {MmlNode} root The top of the MathML tree
* @param {MmlNode} node The node to make collapsible by replacing with an maction
* @param {string[]} extra The extra nodes (if any) that need to be included in linked mactions
*/
public makeAction(node: MmlNode) {
public makeAction(root: MmlNode, node: MmlNode, extra: string[]) {
if (node.isKind('math')) {
node = this.addMrow(node);
}
const factory = this.complexity.factory;
const marker = node.getProperty('collapse-marker') as string;
const parent = node.parent;
const variant = { 'data-mjx-collapsed': true } as PropertyList;
const def = { 'data-mjx-collapsed': true } as PropertyList;
if (node.getProperty('collapse-variant')) {
variant.mathvariant = '-tex-variant';
def.mathvariant = '-tex-variant';
}
const maction = factory.create(
'maction',
Expand All @@ -601,7 +615,7 @@ export class Collapse {
),
},
[
factory.create('mtext', variant, [
factory.create('mtext', def, [
(factory.create('text') as TextNode).setText(marker),
]),
]
Expand All @@ -615,14 +629,51 @@ export class Collapse {
node.removeProperty('collapse-complexity');
parent.replaceChild(maction, node);
maction.appendChild(node);
this.makeActionGroup(root, maction, extra);
}

/**
* @param {MmlNode} root The root of the MathML tree
* @param {MmlNode} action The maction node that controls the potential group
* @param {string[]} extra The list of extra nodes for this group (if non-empty)
*/
public makeActionGroup(root: MmlNode, action: MmlNode, extra: string[]) {
if (!extra.length) return;
action.attributes.set('data-collapse-group', true);
const nodes: MmlNode[] = [];
root.walkTree((node) => {
if (extra.includes(node.attributes.get('data-semantic-id') as string)) {
nodes.push(node);
}
});
const factory = this.complexity.factory;
for (const node of nodes) {
const parent = node.parent;
const maction = factory.create(
'maction',
{
actiontype: 'toggle',
selection: 2,
'data-collapsible': true,
'data-collapse-id': action.attributes.get('id'),
'data-semantic-complexity': node.attributes.get(
'data-semantic-complexity'
),
},
[factory.create('mtext')]
);
maction.inheritAttributesFrom(node);
parent.replaceChild(maction, node);
maction.appendChild(node);
}
}

/**
* If the <math> node is to be collapsible, add an mrow to it instead so that we can wrap it
* in an maction (can't put one around the <math> node).
*
* @param {MmlNode} node The math node to create an mrow for
* @returns {MmlNode} The newly created mrow
* @returns {MmlNode} The newly created mrow
*/
public addMrow(node: MmlNode): MmlNode {
const mrow = this.complexity.factory.create(
Expand Down
5 changes: 3 additions & 2 deletions ts/a11y/complexity/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { MmlVisitor } from '../../core/MmlTree/MmlVisitor.js';
import { MmlFactory } from '../../core/MmlTree/MmlFactory.js';
import { Collapse } from './collapse.js';
import { OptionList, userOptions, defaultOptions } from '../../util/Options.js';
import type { SemanticMap } from '../speech/StructureUtil.js';

/*==========================================================================*/

Expand Down Expand Up @@ -107,10 +108,10 @@ export class ComplexityVisitor extends MmlVisitor {
/**
* @override
*/
public visitTree(node: MmlNode, id: number) {
public visitTree(node: MmlNode, id: number, parts: SemanticMap) {
super.visitTree(node, true);
if (this.options.makeCollapsible) {
id = this.collapse.makeCollapse(node, id);
id = this.collapse.makeCollapse(node, id, parts);
}
return id;
}
Expand Down
46 changes: 34 additions & 12 deletions ts/a11y/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@
*/

import { Handler } from '../core/Handler.js';
import { MmlNode } from '../core/MmlTree/MmlNode.js';
import { MathML } from '../input/mathml.js';
import { STATE, newState } from '../core/MathItem.js';
import { SpeechMathItem, SpeechMathDocument, SpeechHandler } from './speech.js';
import { MathDocumentConstructor } from '../core/MathDocument.js';
import { OptionList, expandable } from '../util/Options.js';
import { SerializedMmlVisitor } from '../core/MmlTree/SerializedMmlVisitor.js';
import { hasWindow } from '../util/context.js';
import { StyleJson } from '../util/StyleJson.js';
import { context } from '../util/context.js';
Expand Down Expand Up @@ -112,20 +110,27 @@ export interface ExplorerMathItem extends HTMLMATHITEM {
* @param {HTMLElement} focus The temporary focus element, if any
*/
clearTemporaryFocus(focus: HTMLElement): void;

/**
* Get all nodes with the same semantic id (multiple nodes if there
* are line breaks).
*
* @param {HTMLElement} node The node to check if it is split
* @returns {HTMLElement[]} All the nodes for the given id
*/
getSplitNodes(node: HTMLElement): HTMLElement[];
}

/**
* The mixin for adding the Explorer to MathItems
*
* @param {B} BaseMathItem The MathItem class to be extended
* @param {Function} toMathML The function to serialize the internal MathML
* @returns {ExplorerMathItem} The Explorer MathItem class
*
* @template B The MathItem class to extend
*/
export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
BaseMathItem: B,
toMathML: (node: MmlNode) => string
BaseMathItem: B
): Constructor<ExplorerMathItem> & B {
return class BaseClass extends BaseMathItem {
/**
Expand Down Expand Up @@ -214,11 +219,10 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
if (this.state() >= STATE.EXPLORER) return;
if (!this.isEscaped && (document.options.enableExplorer || force)) {
const node = this.typesetRoot;
const mml = toMathML(this.root);
if (!this.explorers) {
this.explorers = new ExplorerPool();
}
this.explorers.init(document, node, mml, this);
this.explorers.init(document, node, this);
}
this.state(STATE.EXPLORER);
}
Expand Down Expand Up @@ -282,6 +286,27 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
promise.then(() => setTimeout(() => focus.remove(), 100));
}
}

/**
* Get all nodes with the same semantic id (multiple nodes if there are line breaks).
*
* @param {HTMLElement} node The node to check if it is split
* @returns {HTMLElement[]} All the nodes for the given id
*/
public getSplitNodes(node: HTMLElement): HTMLElement[] {
const id = node.getAttribute('data-semantic-id');
if (!id) {
return [node];
}
const nodes = (this.semanticNodes.get(id) ?? [id])
.map((nid: string) =>
Array.from(
this.typesetRoot.querySelectorAll(`[data-semantic-id="${nid}"]`)
)
)
.flat() as HTMLElement[];
return nodes;
}
};
}

Expand Down Expand Up @@ -379,7 +404,7 @@ export function ExplorerMathDocumentMixin<
* Styles to add for speech
*/
public static speechStyles: StyleJson = {
'mjx-container[has-speech="true"]': {
'mjx-container /* explorers */': {
position: 'relative',
cursor: 'default',
},
Expand Down Expand Up @@ -499,15 +524,12 @@ export function ExplorerMathDocumentMixin<
if (!ProcessBits.has('explorer')) {
ProcessBits.allocate('explorer');
}
const visitor = new SerializedMmlVisitor(this.mmlFactory);
const toMathML = (node: MmlNode) => visitor.visitTree(node);
const options = this.options;
if (!options.a11y.speechRules) {
options.a11y.speechRules = `${options.sre.domain}-${options.sre.style}`;
}
const mathItem = (options.MathItem = ExplorerMathItemMixin(
options.MathItem,
toMathML
options.MathItem
));
mathItem.roleDescription = options.roleDescription;
this.explorerRegions = new RegionPool(this);
Expand Down
Loading