From c417c4978122bfe8dbcbc50cec054cb0822cabe8 Mon Sep 17 00:00:00 2001 From: zorkow Date: Tue, 9 Sep 2025 16:55:12 +0200 Subject: [PATCH 1/2] ensures highlighting of all nodes that are spoken --- ts/a11y/explorer/KeyExplorer.ts | 125 +++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts index 943def94d..ef9c2db1b 100644 --- a/ts/a11y/explorer/KeyExplorer.ts +++ b/ts/a11y/explorer/KeyExplorer.ts @@ -421,6 +421,11 @@ export class SpeechExplorer ['dblclick', this.DblClick.bind(this)], ]); + /** + * Semantic id to subtee map. + */ + private subtrees: Map> = new Map(); + /** * @override */ @@ -1040,7 +1045,35 @@ export class SpeechExplorer if (!id) { return [node]; } - return Array.from(this.node.querySelectorAll(`[data-semantic-id="${id}"]`)); + const parts = Array.from( + this.node.querySelectorAll(`[data-semantic-id="${id}"]`) + ) as HTMLElement[]; + const subtree = this.subtree(id, parts); + return [...parts, ...subtree]; + } + + /** + * Retrieve the elements in the semantic subtree that are not in the DOM subtree. + * + * @param {string} id The semantic id of the root node. + * @param {HTMLElement[]} nodes The list of nodes corresponding to that id + * (could be multiple for linebroken ones). + * @returns {HTMLElement[]} The list of nodes external to the DOM trees rooted + * by any of the input nodes. + */ + private subtree(id: string, nodes: HTMLElement[]): HTMLElement[] { + const sub = this.subtrees.get(id); + const children: Set = new Set(); + for (const node of nodes) { + Array.from(node.querySelectorAll(`[data-semantic-id]`)).forEach((x) => + children.add(x.getAttribute('data-semantic-id')) + ); + } + const rest = setdifference(sub, children); + return [...rest].map((child) => { + const node = this.node.querySelector(`[data-semantic-id="${child}"]`); + return node as HTMLElement; + }); } /** @@ -1496,6 +1529,7 @@ export class SpeechExplorer public item: ExplorerMathItem ) { super(document, pool, null, node); + this.getSubtrees(); } /** @@ -1730,4 +1764,93 @@ export class SpeechExplorer } return focus.join(' '); } + + private getSubtrees() { + const node = this.node.querySelector('[data-semantic-structure]'); + if (!node) return; + const sexp = node.getAttribute('data-semantic-structure'); + const tokens = tokenize(sexp); + const tree = parse(tokens); + buildMap(tree, this.subtrees); + } +} + +// Some Aux functions +// +type SexpTree = string | SexpTree[]; + +// Helper to tokenize input +/** + * + * @param str + */ +function tokenize(str: string): string[] { + return str.replace(/\(/g, ' ( ').replace(/\)/g, ' ) ').trim().split(/\s+/); +} + +// Recursive parser to convert tokens into a tree +/** + * + * @param tokens + */ +function parse(tokens: string[]): SexpTree { + if (!tokens.length) return null; + + const token = tokens.shift(); + + if (token === '(') { + const node = []; + while (tokens[0] !== ')') { + node.push(parse(tokens)); + } + tokens.shift(); // remove ')' + return node; + } else { + return token; + } +} + +// Flatten tree and build the map +/** + * + * @param tree + * @param map + */ +function buildMap(tree: SexpTree, map = new Map()) { + if (typeof tree === 'string') { + if (!map.has(tree)) map.set(tree, new Set()); + return new Set(); + } + + const [root, ...children] = tree; + const rootId = root; + const descendants = new Set(); + + for (const child of children) { + const childRoot = typeof child === 'string' ? child : child[0]; + if (!map.has(rootId)) map.set(rootId, new Set()); + + const childDescendants = buildMap(child, map); + descendants.add(childRoot); + childDescendants.forEach((d) => descendants.add(d)); + } + + map.set(rootId, descendants); + return descendants; +} + +// Can be replaced with ES2024 +/** + * + * @param a + * @param b + */ +function setdifference(a: Set, b: Set): Set { + if (!a) { + return new Set(); + } + if (!b) { + return a; + } + return new Set([...a].filter((x) => !b.has(x))); } From 1c2bbef99dac29e62c855049eaab1f841db53aa3 Mon Sep 17 00:00:00 2001 From: zorkow Date: Wed, 10 Sep 2025 11:30:59 +0200 Subject: [PATCH 2/2] Add comments --- ts/a11y/explorer/KeyExplorer.ts | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts index ef9c2db1b..7a4f54496 100644 --- a/ts/a11y/explorer/KeyExplorer.ts +++ b/ts/a11y/explorer/KeyExplorer.ts @@ -422,7 +422,7 @@ export class SpeechExplorer ]); /** - * Semantic id to subtee map. + * Semantic id to subtree map. */ private subtrees: Map> = new Map(); @@ -1765,6 +1765,9 @@ export class SpeechExplorer return focus.join(' '); } + /** + * Populates the subtrees map from the data-semantic-structure attribute. + */ private getSubtrees() { const node = this.node.querySelector('[data-semantic-structure]'); if (!node) return; @@ -1775,23 +1778,23 @@ export class SpeechExplorer } } -// Some Aux functions +// Some Aux functions for parsing the semantic structure sexpression // type SexpTree = string | SexpTree[]; -// Helper to tokenize input /** + * Helper to tokenize input * - * @param str + * @param str The semantic structure. */ function tokenize(str: string): string[] { return str.replace(/\(/g, ' ( ').replace(/\)/g, ' ) ').trim().split(/\s+/); } -// Recursive parser to convert tokens into a tree /** + * Recursive parser to convert tokens into a tree * - * @param tokens + * @param tokens The tokens from the semantic structure. */ function parse(tokens: string[]): SexpTree { if (!tokens.length) return null; @@ -1810,11 +1813,11 @@ function parse(tokens: string[]): SexpTree { } } -// Flatten tree and build the map /** + * Flattens the tree and builds the map. * - * @param tree - * @param map + * @param tree The sexpression tree. + * @param map The map to populate. */ function buildMap(tree: SexpTree, map = new Map()) { if (typeof tree === 'string') { @@ -1839,11 +1842,12 @@ function buildMap(tree: SexpTree, map = new Map()) { return descendants; } -// Can be replaced with ES2024 +// Can be replaced with ES2024 implementation of Set.prototyp.difference /** + * Set difference between two sets A and B: A\B. * - * @param a - * @param b + * @param a Initial set. + * @param b Set to remove from A. */ function setdifference(a: Set, b: Set): Set { if (!a) {