@@ -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