@@ -66,13 +66,13 @@ SideTab.prototype = {
6666    const  titleWrapper  =  document . createElement ( "div" ) ; 
6767    titleWrapper . className  =  "tab-title-wrapper" ; 
6868
69-     const  title  =  document . createElement ( "span " ) ; 
70-     title . className  =  "tab-title" ; 
69+     const  title  =  document . createElement ( "div " ) ; 
70+     title . className  =  "tab-title search-highlight-container " ; 
7171    titleWrapper . appendChild ( title ) ; 
7272    this . _titleView  =  title ; 
7373
74-     const  host  =  document . createElement ( "span " ) ; 
75-     host . className  =  "tab-host" ; 
74+     const  host  =  document . createElement ( "div " ) ; 
75+     host . className  =  "tab-host search-highlight-container " ; 
7676    titleWrapper . appendChild ( host ) ; 
7777    this . _hostView  =  host ; 
7878
@@ -94,13 +94,60 @@ SideTab.prototype = {
9494    tab . appendChild ( pin ) ; 
9595    tab . appendChild ( close ) ; 
9696  } , 
97+   matches ( tokens )  { 
98+     if  ( tokens . length  ===  0 )  { 
99+       return  true ; 
100+     } 
101+     let  title  =  normalizeStr ( this . title ) ; 
102+     let  url  =  normalizeStr ( this . url ) ; 
103+     for  ( let  token  of  tokens )  { 
104+       token  =  normalizeStr ( token ) ; 
105+       if  ( title . includes ( token ) )  { 
106+         return  true ; 
107+       } 
108+       if  ( url . includes ( token ) )  { 
109+         return  true ; 
110+       } 
111+     } 
112+     return  false ; 
113+   } , 
114+   _highlightSearchResults ( node ,  text ,  searchTokens )  { 
115+     let  ranges  =  findHighlightedRanges ( text ,  searchTokens ) ; 
116+ 
117+     // Clear out the node before we fill it with new stuff. 
118+     while  ( node . firstChild )  { 
119+       node . removeChild ( node . firstChild ) ; 
120+     } 
121+ 
122+     for  ( let  { text,  highlight}  of  ranges )  { 
123+       if  ( highlight )  { 
124+         let  span  =  document . createElement ( "span" ) ; 
125+         span . className  =  "search-highlight" ; 
126+         span . textContent  =  text ; 
127+         node . appendChild ( span ) ; 
128+       }  else  { 
129+         node . appendChild ( document . createTextNode ( text ) ) ; 
130+       } 
131+     } 
132+   } , 
133+   highlightMatches ( tokens )  { 
134+     if  ( ! this . visible )  { 
135+       // Reset these to the 'no matches' state (Not calling 
136+       // _highlightSearchResult is just an optimization). 
137+       this . updateTitle ( this . title ) ; 
138+       this . updateURL ( this . url ) ; 
139+     }  else  { 
140+       this . _highlightSearchResults ( this . _titleView ,  this . title ,  tokens ) ; 
141+       this . _highlightSearchResults ( this . _hostView ,  getHost ( this . url ) ,  tokens ) ; 
142+     } 
143+   } , 
97144  updateTitle ( title )  { 
98145    this . title  =  title ; 
99146    this . _titleView . innerText  =  title ; 
100147    this . view . title  =  title ; 
101148  } , 
102149  updateURL ( url )  { 
103-     const  host  =  new   URL ( url ) . host   ||   url ; 
150+     const  host  =  getHost ( url ) ; 
104151    this . url  =  url ; 
105152    this . _hostView . innerText  =  host ; 
106153  } , 
@@ -282,4 +329,88 @@ function toggleClass(node, className, boolean) {
282329  boolean  ? node . classList . add ( className )  : node . classList . remove ( className ) ; 
283330} 
284331
332+ function  normalizeStr ( str )  { 
333+   return  str  ? str . toLowerCase ( ) . normalize ( "NFD" ) . replace ( / [ \u0300 - \u036f ] / g,  "" )  : "" ; 
334+ } 
335+ 
336+ function  getHost ( url )  { 
337+   return  new  URL ( url ) . host  ||  url ; 
338+ } 
339+ 
340+ // This function takes as input a text string and an array of "search tokens" 
341+ // and returns what we should render in an abstract sense. e.g. an array of 
342+ // `{text: string, highlighted: bool}`, such that `result.map(r => 
343+ // r.text).join('')` should equal what was provided as the first argument, and 
344+ // that the sections with `highlighted: true` correspond to ranges that match 
345+ // the members of searchTokens. 
346+ // 
347+ // (It's complex enough to arguably warrant unit tests, but oh well, it's split 
348+ // out so that I could more easily test it manually). 
349+ function  findHighlightedRanges ( text ,  searchTokens )  { 
350+   // Trivial case 
351+   if  ( searchTokens . length  ===  0 )  { 
352+     return  [ { text,  highlighted : false } ] ; 
353+   } 
354+   // Potentially surprisingly, changing case doesn't preserve length. If we 
355+   // can't do this without messing up the indices in the given text, we fail. 
356+   // This function is just for highlighting the matching parts in searches in 
357+   // the UI, so it's not a big deal if it doesn't highlight something. 
358+   let  canLowercaseText  =  text . toLowerCase ( ) . length  ===  text . length  && 
359+                          searchTokens . every ( t  => 
360+                            t . toLowerCase ( ) . length  ===  t . length ) ; 
361+   let  normalize  =  s  =>  canLowercaseText  ? s . toLowerCase ( )  : s ; 
362+   let  normText  =  normalize ( text ) ; 
363+ 
364+   // Build an array of the start/end indices of each result. 
365+   let  ranges  =  [ ] ; 
366+   for  ( let  token  of  searchTokens )  { 
367+     token  =  normalize ( token ) ; 
368+     if  ( ! token . length )  { 
369+       continue ; 
370+     } 
371+     for  ( let  index  =  normText . indexOf ( token ) ; 
372+          index  >=  0 ; 
373+          index  =  normText . indexOf ( token ,  index  +  1 ) )  { 
374+       ranges . push ( { start : index ,  end : index  +  token . length } ) ; 
375+     } 
376+   } 
377+   if  ( ranges . length  ===  0 )  { 
378+     return  [ { text,  highlighted : false } ] ; 
379+   } 
380+ 
381+   // Order them in the order they appear in the text (as it is they're ordered 
382+   // first by the order of the tokens in searchTokens, and then by the 
383+   // position in the text). 
384+   ranges . sort ( ( a ,  b )  =>  a . start  -  b . start ) ; 
385+ 
386+   let  coalesced  =  [ ranges [ 0 ] ] ; 
387+   for  ( let  i  =  1 ;  i  <  ranges . length ;  ++ i )  { 
388+     let  prev  =  coalesced [ coalesced . length  -  1 ] ; 
389+     let  curr  =  ranges [ i ] ; 
390+     if  ( curr . start  <  prev . end )  { 
391+       // Overlap, update prev, but don't add curr. 
392+       if  ( curr . end  >  prev . end )  { 
393+         prev . end  =  curr . end ; 
394+       } 
395+     }  else  { 
396+       coalesced . push ( curr ) ; 
397+     } 
398+   } 
399+ 
400+   let  result  =  [ ] ; 
401+   let  pos  =  0 ; 
402+   for  ( let  range  of  coalesced )  { 
403+     if  ( pos  <  range . start )  { 
404+       result . push ( { text : text . slice ( pos ,  range . start ) ,  highlight : false } ) ; 
405+     } 
406+     result . push ( { text : text . slice ( range . start ,  range . end ) ,  highlight : true } ) ; 
407+     pos  =  range . end ; 
408+   } 
409+   if  ( pos  <  text . length )  { 
410+     result . push ( { text : text . slice ( pos ) ,  highlight : false } ) ; 
411+   } 
412+ 
413+   return  result ; 
414+ } 
415+ 
285416module . exports  =  SideTab ; 
0 commit comments