@@ -421,6 +421,11 @@ export class SpeechExplorer
421421 [ 'dblclick' , this . DblClick . bind ( this ) ] ,
422422 ] ) ;
423423
424+ /**
425+ * Semantic id to subtree map.
426+ */
427+ private subtrees : Map < string , Set < string > > = null ;
428+
424429 /**
425430 * @override
426431 */
@@ -1029,6 +1034,8 @@ export class SpeechExplorer
10291034 this . node . removeAttribute ( 'aria-busy' ) ;
10301035 }
10311036
1037+ private cacheParts : Map < string , HTMLElement [ ] > = new Map ( ) ;
1038+
10321039 /**
10331040 * Get all nodes with the same semantic id (multiple nodes if there are line breaks).
10341041 *
@@ -1040,7 +1047,40 @@ export class SpeechExplorer
10401047 if ( ! id ) {
10411048 return [ node ] ;
10421049 }
1043- return Array . from ( this . node . querySelectorAll ( `[data-semantic-id="${ id } "]` ) ) ;
1050+ // Here we need to cache the subtrees.
1051+ if ( this . cacheParts . has ( id ) ) {
1052+ return this . cacheParts . get ( id ) ;
1053+ }
1054+ const parts = Array . from (
1055+ this . node . querySelectorAll ( `[data-semantic-id="${ id } "]` )
1056+ ) as HTMLElement [ ] ;
1057+ const subtree = this . subtree ( id , parts ) ;
1058+ this . cacheParts . set ( id , [ ...parts , ...subtree ] ) ;
1059+ return this . cacheParts . get ( id ) ;
1060+ }
1061+
1062+ /**
1063+ * Retrieve the elements in the semantic subtree that are not in the DOM subtree.
1064+ *
1065+ * @param {string } id The semantic id of the root node.
1066+ * @param {HTMLElement[] } nodes The list of nodes corresponding to that id
1067+ * (could be multiple for linebroken ones).
1068+ * @returns {HTMLElement[] } The list of nodes external to the DOM trees rooted
1069+ * by any of the input nodes.
1070+ */
1071+ private subtree ( id : string , nodes : HTMLElement [ ] ) : HTMLElement [ ] {
1072+ const sub = this . subtrees . get ( id ) ;
1073+ const children : Set < string > = new Set ( ) ;
1074+ for ( const node of nodes ) {
1075+ Array . from ( node . querySelectorAll ( `[data-semantic-id]` ) ) . forEach ( ( x ) =>
1076+ children . add ( x . getAttribute ( 'data-semantic-id' ) )
1077+ ) ;
1078+ }
1079+ const rest = setdifference ( sub , children ) ;
1080+ return [ ...rest ] . map ( ( child ) => {
1081+ const node = this . node . querySelector ( `[data-semantic-id="${ child } "]` ) ;
1082+ return node as HTMLElement ;
1083+ } ) ;
10441084 }
10451085
10461086 /**
@@ -1517,6 +1557,10 @@ export class SpeechExplorer
15171557 * @override
15181558 */
15191559 public async Start ( ) {
1560+ if ( ! this . subtrees ) {
1561+ this . subtrees = new Map ( ) ;
1562+ this . getSubtrees ( ) ;
1563+ }
15201564 //
15211565 // If we aren't attached or already active, return
15221566 //
@@ -1730,4 +1774,95 @@ export class SpeechExplorer
17301774 }
17311775 return focus . join ( ' ' ) ;
17321776 }
1777+
1778+ /**
1779+ * Populates the subtrees map from the data-semantic-structure attribute.
1780+ */
1781+ private getSubtrees ( ) {
1782+ const node = this . node . querySelector ( '[data-semantic-structure]' ) ;
1783+ if ( ! node ) return ;
1784+ const sexp = node . getAttribute ( 'data-semantic-structure' ) ;
1785+ const tokens = tokenize ( sexp ) ;
1786+ const tree = parse ( tokens ) ;
1787+ buildMap ( tree , this . subtrees ) ;
1788+ }
1789+ }
1790+
1791+ /**********************************************************************/
1792+ /*
1793+ * Some Aux functions for parsing the semantic structure sexpression
1794+ */
1795+ type SexpTree = string | SexpTree [ ] ;
1796+
1797+ /**
1798+ * Helper to tokenize input
1799+ *
1800+ * @param {string } str The semantic structure.
1801+ * @returns {string[] } The tokenized list.
1802+ */
1803+ function tokenize ( str : string ) : string [ ] {
1804+ return str . replace ( / \( / g, ' ( ' ) . replace ( / \) / g, ' ) ' ) . trim ( ) . split ( / \s + / ) ;
1805+ }
1806+
1807+ /**
1808+ * Recursive parser to convert tokens into a tree
1809+ *
1810+ * @param {string } tokens The tokens from the semantic structure.
1811+ * @returns {SexpTree } Array list for the semantic structure sexpression.
1812+ */
1813+ function parse ( tokens : string [ ] ) : SexpTree {
1814+ const stack : SexpTree [ ] [ ] = [ [ ] ] ;
1815+ for ( const token of tokens ) {
1816+ if ( token === '(' ) {
1817+ const newNode : SexpTree = [ ] ;
1818+ stack [ stack . length - 1 ] . push ( newNode ) ;
1819+ stack . push ( newNode ) ;
1820+ } else if ( token === ')' ) {
1821+ stack . pop ( ) ;
1822+ } else {
1823+ stack [ stack . length - 1 ] . push ( token ) ;
1824+ }
1825+ }
1826+ return stack [ 0 ] [ 0 ] ;
1827+ }
1828+
1829+ /**
1830+ * Flattens the tree and builds the map.
1831+ *
1832+ * @param {SexpTree } tree The sexpression tree.
1833+ * @param {Map<string, Set<string>> } map The map to populate.
1834+ */
1835+ function buildMap ( tree : SexpTree , map : Map < string , Set < string > > ) {
1836+ if ( typeof tree === 'string' ) {
1837+ if ( ! map . has ( tree ) ) map . set ( tree , new Set ( ) ) ;
1838+ return new Set ( ) ;
1839+ }
1840+ const [ root , ...children ] = tree ;
1841+ const rootId = root as string ;
1842+ const descendants : Set < string > = new Set ( ) ;
1843+ for ( const child of children ) {
1844+ const childRoot = typeof child === 'string' ? child : child [ 0 ] ;
1845+ const childDescendants = buildMap ( child , map ) ;
1846+ descendants . add ( childRoot as string ) ;
1847+ childDescendants . forEach ( ( d : string ) => descendants . add ( d ) ) ;
1848+ }
1849+ map . set ( rootId , descendants ) ;
1850+ return descendants ;
1851+ }
1852+
1853+ // Can be replaced with ES2024 implementation of Set.prototyp.difference
1854+ /**
1855+ * Set difference between two sets A and B: A\B.
1856+ *
1857+ * @param {Set<string> } a Initial set.
1858+ * @param {Set<string> } b Set to remove from A.
1859+ */
1860+ function setdifference ( a : Set < string > , b : Set < string > ) : Set < string > {
1861+ if ( ! a ) {
1862+ return new Set ( ) ;
1863+ }
1864+ if ( ! b ) {
1865+ return a ;
1866+ }
1867+ return new Set ( [ ...a ] . filter ( ( x ) => ! b . has ( x ) ) ) ;
17331868}
0 commit comments