Skip to content

Commit c417c49

Browse files
committed
ensures highlighting of all nodes that are spoken
1 parent afaf1ea commit c417c49

File tree

1 file changed

+124
-1
lines changed

1 file changed

+124
-1
lines changed

ts/a11y/explorer/KeyExplorer.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,11 @@ export class SpeechExplorer
421421
['dblclick', this.DblClick.bind(this)],
422422
]);
423423

424+
/**
425+
* Semantic id to subtee map.
426+
*/
427+
private subtrees: Map<string, Set<string>> = new Map();
428+
424429
/**
425430
* @override
426431
*/
@@ -1040,7 +1045,35 @@ export class SpeechExplorer
10401045
if (!id) {
10411046
return [node];
10421047
}
1043-
return Array.from(this.node.querySelectorAll(`[data-semantic-id="${id}"]`));
1048+
const parts = Array.from(
1049+
this.node.querySelectorAll(`[data-semantic-id="${id}"]`)
1050+
) as HTMLElement[];
1051+
const subtree = this.subtree(id, parts);
1052+
return [...parts, ...subtree];
1053+
}
1054+
1055+
/**
1056+
* Retrieve the elements in the semantic subtree that are not in the DOM subtree.
1057+
*
1058+
* @param {string} id The semantic id of the root node.
1059+
* @param {HTMLElement[]} nodes The list of nodes corresponding to that id
1060+
* (could be multiple for linebroken ones).
1061+
* @returns {HTMLElement[]} The list of nodes external to the DOM trees rooted
1062+
* by any of the input nodes.
1063+
*/
1064+
private subtree(id: string, nodes: HTMLElement[]): HTMLElement[] {
1065+
const sub = this.subtrees.get(id);
1066+
const children: Set<string> = new Set();
1067+
for (const node of nodes) {
1068+
Array.from(node.querySelectorAll(`[data-semantic-id]`)).forEach((x) =>
1069+
children.add(x.getAttribute('data-semantic-id'))
1070+
);
1071+
}
1072+
const rest = setdifference(sub, children);
1073+
return [...rest].map((child) => {
1074+
const node = this.node.querySelector(`[data-semantic-id="${child}"]`);
1075+
return node as HTMLElement;
1076+
});
10441077
}
10451078

10461079
/**
@@ -1496,6 +1529,7 @@ export class SpeechExplorer
14961529
public item: ExplorerMathItem
14971530
) {
14981531
super(document, pool, null, node);
1532+
this.getSubtrees();
14991533
}
15001534

15011535
/**
@@ -1730,4 +1764,93 @@ export class SpeechExplorer
17301764
}
17311765
return focus.join(' ');
17321766
}
1767+
1768+
private getSubtrees() {
1769+
const node = this.node.querySelector('[data-semantic-structure]');
1770+
if (!node) return;
1771+
const sexp = node.getAttribute('data-semantic-structure');
1772+
const tokens = tokenize(sexp);
1773+
const tree = parse(tokens);
1774+
buildMap(tree, this.subtrees);
1775+
}
1776+
}
1777+
1778+
// Some Aux functions
1779+
//
1780+
type SexpTree = string | SexpTree[];
1781+
1782+
// Helper to tokenize input
1783+
/**
1784+
*
1785+
* @param str
1786+
*/
1787+
function tokenize(str: string): string[] {
1788+
return str.replace(/\(/g, ' ( ').replace(/\)/g, ' ) ').trim().split(/\s+/);
1789+
}
1790+
1791+
// Recursive parser to convert tokens into a tree
1792+
/**
1793+
*
1794+
* @param tokens
1795+
*/
1796+
function parse(tokens: string[]): SexpTree {
1797+
if (!tokens.length) return null;
1798+
1799+
const token = tokens.shift();
1800+
1801+
if (token === '(') {
1802+
const node = [];
1803+
while (tokens[0] !== ')') {
1804+
node.push(parse(tokens));
1805+
}
1806+
tokens.shift(); // remove ')'
1807+
return node;
1808+
} else {
1809+
return token;
1810+
}
1811+
}
1812+
1813+
// Flatten tree and build the map
1814+
/**
1815+
*
1816+
* @param tree
1817+
* @param map
1818+
*/
1819+
function buildMap(tree: SexpTree, map = new Map()) {
1820+
if (typeof tree === 'string') {
1821+
if (!map.has(tree)) map.set(tree, new Set());
1822+
return new Set();
1823+
}
1824+
1825+
const [root, ...children] = tree;
1826+
const rootId = root;
1827+
const descendants = new Set();
1828+
1829+
for (const child of children) {
1830+
const childRoot = typeof child === 'string' ? child : child[0];
1831+
if (!map.has(rootId)) map.set(rootId, new Set());
1832+
1833+
const childDescendants = buildMap(child, map);
1834+
descendants.add(childRoot);
1835+
childDescendants.forEach((d) => descendants.add(d));
1836+
}
1837+
1838+
map.set(rootId, descendants);
1839+
return descendants;
1840+
}
1841+
1842+
// Can be replaced with ES2024
1843+
/**
1844+
*
1845+
* @param a
1846+
* @param b
1847+
*/
1848+
function setdifference(a: Set<string>, b: Set<string>): Set<string> {
1849+
if (!a) {
1850+
return new Set();
1851+
}
1852+
if (!b) {
1853+
return a;
1854+
}
1855+
return new Set([...a].filter((x) => !b.has(x)));
17331856
}

0 commit comments

Comments
 (0)