1717import { CSSComplexSelector , CSSSimpleSelector , CSSComplexSelectorList , CSSFunctionArgument } from '../common/cssParser' ;
1818
1919export type QueryContext = {
20- scope : Element | ShadowRoot | Document ;
21- // Place for more options, e.g. normalizing whitespace or piercing shadow.
20+ scope : Element | Document ;
21+ pierceShadow : boolean ;
22+ // Place for more options, e.g. normalizing whitespace.
2223} ;
2324export type Selector = any ; // Opaque selector type.
2425export interface SelectorEvaluator {
@@ -42,6 +43,11 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
4243 this . _engines . set ( 'where' , isEngine ) ;
4344 this . _engines . set ( 'has' , hasEngine ) ;
4445 this . _engines . set ( 'scope' , scopeEngine ) ;
46+ this . _engines . set ( 'text' , textEngine ) ;
47+ this . _engines . set ( 'matches-text' , matchesTextEngine ) ;
48+ this . _engines . set ( 'xpath' , xpathEngine ) ;
49+ for ( const attr of [ 'id' , 'data-testid' , 'data-test-id' , 'data-test' ] )
50+ this . _engines . set ( attr , createAttributeEngine ( attr ) ) ;
4551 // TODO: host
4652 // TODO: host-context?
4753 }
@@ -154,40 +160,40 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
154160 return true ;
155161 const { selector : simple , combinator } = complex . simples [ index ] ;
156162 if ( combinator === '>' ) {
157- const parent = parentElementOrShadowHostInScope ( element , context . scope ) ;
163+ const parent = parentElementOrShadowHostInContext ( element , context ) ;
158164 if ( ! parent || ! this . _matchesSimple ( parent , simple , context ) )
159165 return false ;
160166 return this . _matchesParents ( parent , complex , index - 1 , context ) ;
161167 }
162168 if ( combinator === '+' ) {
163- const previousSibling = element === context . scope ? null : element . previousElementSibling ;
169+ const previousSibling = previousSiblingInContext ( element , context ) ;
164170 if ( ! previousSibling || ! this . _matchesSimple ( previousSibling , simple , context ) )
165171 return false ;
166172 return this . _matchesParents ( previousSibling , complex , index - 1 , context ) ;
167173 }
168174 if ( combinator === '' ) {
169- let parent = parentElementOrShadowHostInScope ( element , context . scope ) ;
175+ let parent = parentElementOrShadowHostInContext ( element , context ) ;
170176 while ( parent ) {
171177 if ( this . _matchesSimple ( parent , simple , context ) ) {
172178 if ( this . _matchesParents ( parent , complex , index - 1 , context ) )
173179 return true ;
174180 if ( complex . simples [ index - 1 ] . combinator === '' )
175181 break ;
176182 }
177- parent = parentElementOrShadowHostInScope ( parent , context . scope ) ;
183+ parent = parentElementOrShadowHostInContext ( parent , context ) ;
178184 }
179185 return false ;
180186 }
181187 if ( combinator === '~' ) {
182- let previousSibling = element === context . scope ? null : element . previousElementSibling ;
188+ let previousSibling = previousSiblingInContext ( element , context ) ;
183189 while ( previousSibling ) {
184190 if ( this . _matchesSimple ( previousSibling , simple , context ) ) {
185191 if ( this . _matchesParents ( previousSibling , complex , index - 1 , context ) )
186192 return true ;
187193 if ( complex . simples [ index - 1 ] . combinator === '~' )
188194 break ;
189195 }
190- previousSibling = previousSibling === context . scope ? null : previousSibling . previousElementSibling ;
196+ previousSibling = previousSiblingInContext ( previousSibling , context ) ;
191197 }
192198 return false ;
193199 }
@@ -212,13 +218,13 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
212218 }
213219
214220 private _callMatches ( engine : SelectorEngine , element : Element , args : CSSFunctionArgument [ ] , context : QueryContext ) : boolean {
215- return this . _cached < boolean > ( element , [ '_callMatches' , engine , args , context . scope ] , ( ) => {
221+ return this . _cached < boolean > ( element , [ '_callMatches' , engine , args , context . scope , context . pierceShadow ] , ( ) => {
216222 return engine . matches ! ( element , args , context , this ) ;
217223 } ) ;
218224 }
219225
220226 private _callQuery ( engine : SelectorEngine , args : CSSFunctionArgument [ ] , context : QueryContext ) : Element [ ] {
221- return this . _cached < Element [ ] > ( args , [ '_callQuery' , engine , context . scope ] , ( ) => {
227+ return this . _cached < Element [ ] > ( args , [ '_callQuery' , engine , context . scope , context . pierceShadow ] , ( ) => {
222228 return engine . query ! ( context , args , this ) ;
223229 } ) ;
224230 }
@@ -229,11 +235,13 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
229235 } ) ;
230236 }
231237
232- private _queryCSS ( context : QueryContext , css : string ) : Element [ ] {
238+ _queryCSS ( context : QueryContext , css : string ) : Element [ ] {
233239 return this . _cached < Element [ ] > ( css , [ '_queryCSS' , context ] , ( ) => {
234240 const result : Element [ ] = [ ] ;
235241 function query ( root : Element | ShadowRoot | Document ) {
236242 result . push ( ...root . querySelectorAll ( css ) ) ;
243+ if ( ! context . pierceShadow )
244+ return ;
237245 if ( ( root as Element ) . shadowRoot )
238246 query ( ( root as Element ) . shadowRoot ! ) ;
239247 for ( const element of root . querySelectorAll ( '*' ) ) {
@@ -255,13 +263,13 @@ export class SelectorEvaluatorImpl implements SelectorEvaluator {
255263}
256264
257265const isEngine : SelectorEngine = {
258- matches ( element : Element , args : any [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
266+ matches ( element : Element , args : ( string | number | Selector ) [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
259267 if ( args . length === 0 )
260268 throw new Error ( `"is" engine expects non-empty selector list` ) ;
261269 return args . some ( selector => evaluator . matches ( element , selector , context ) ) ;
262270 } ,
263271
264- query ( context : QueryContext , args : any [ ] , evaluator : SelectorEvaluator ) : Element [ ] {
272+ query ( context : QueryContext , args : ( string | number | Selector ) [ ] , evaluator : SelectorEvaluator ) : Element [ ] {
265273 if ( args . length === 0 )
266274 throw new Error ( `"is" engine expects non-empty selector list` ) ;
267275 const elements : Element [ ] = [ ] ;
@@ -273,7 +281,7 @@ const isEngine: SelectorEngine = {
273281} ;
274282
275283const hasEngine : SelectorEngine = {
276- matches ( element : Element , args : any [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
284+ matches ( element : Element , args : ( string | number | Selector ) [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
277285 if ( args . length === 0 )
278286 throw new Error ( `"has" engine expects non-empty selector list` ) ;
279287 return evaluator . query ( { ...context , scope : element } , args ) . length > 0 ;
@@ -284,15 +292,15 @@ const hasEngine: SelectorEngine = {
284292} ;
285293
286294const scopeEngine : SelectorEngine = {
287- matches ( element : Element , args : any [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
295+ matches ( element : Element , args : ( string | number | Selector ) [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
288296 if ( args . length !== 0 )
289297 throw new Error ( `"scope" engine expects no arguments` ) ;
290298 if ( context . scope . nodeType === 9 /* Node.DOCUMENT_NODE */ )
291299 return element === ( context . scope as Document ) . documentElement ;
292300 return element === context . scope ;
293301 } ,
294302
295- query ( context : QueryContext , args : any [ ] , evaluator : SelectorEvaluator ) : Element [ ] {
303+ query ( context : QueryContext , args : ( string | number | Selector ) [ ] , evaluator : SelectorEvaluator ) : Element [ ] {
296304 if ( args . length !== 0 )
297305 throw new Error ( `"scope" engine expects no arguments` ) ;
298306 if ( context . scope . nodeType === 9 /* Node.DOCUMENT_NODE */ ) {
@@ -306,13 +314,102 @@ const scopeEngine: SelectorEngine = {
306314} ;
307315
308316const notEngine : SelectorEngine = {
309- matches ( element : Element , args : any [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
317+ matches ( element : Element , args : ( string | number | Selector ) [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
310318 if ( args . length === 0 )
311319 throw new Error ( `"not" engine expects non-empty selector list` ) ;
312320 return ! evaluator . matches ( element , args , context ) ;
313321 } ,
314322} ;
315323
324+ const textEngine : SelectorEngine = {
325+ matches ( element : Element , args : ( string | number | Selector ) [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
326+ if ( args . length === 0 || typeof args [ 0 ] !== 'string' || args . length > 2 || ( args . length === 2 && typeof args [ 1 ] !== 'string' ) )
327+ throw new Error ( `"text" engine expects a string and an optional flags string` ) ;
328+ const text = args [ 0 ] ;
329+ const flags = args . length === 2 ? args [ 1 ] : '' ;
330+ const matcher = textMatcher ( text , flags ) ;
331+ return elementMatchesText ( element , context , matcher ) ;
332+ } ,
333+ } ;
334+
335+ const matchesTextEngine : SelectorEngine = {
336+ matches ( element : Element , args : ( string | number | Selector ) [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
337+ if ( args . length === 0 || typeof args [ 0 ] !== 'string' || args . length > 2 || ( args . length === 2 && typeof args [ 1 ] !== 'string' ) )
338+ throw new Error ( `"matches-text" engine expects a regexp body and optional regexp flags` ) ;
339+ const re = new RegExp ( args [ 0 ] , args . length === 2 ? args [ 1 ] : undefined ) ;
340+ return elementMatchesText ( element , context , s => re . test ( s ) ) ;
341+ } ,
342+ } ;
343+
344+ function textMatcher ( text : string , flags : string ) : ( s : string ) => boolean {
345+ const normalizeSpace = flags . includes ( 's' ) ;
346+ const lowerCase = flags . includes ( 'i' ) ;
347+ const substring = flags . includes ( 'g' ) ;
348+ if ( normalizeSpace )
349+ text = text . trim ( ) . replace ( / \s + / g, ' ' ) ;
350+ if ( lowerCase )
351+ text = text . toLowerCase ( ) ;
352+ return ( s : string ) => {
353+ if ( normalizeSpace )
354+ s = s . trim ( ) . replace ( / \s + / g, ' ' ) ;
355+ if ( lowerCase )
356+ s = s . toLowerCase ( ) ;
357+ return substring ? s . includes ( text ) : s === text ;
358+ } ;
359+ }
360+
361+ function elementMatchesText ( element : Element , context : QueryContext , matcher : ( s : string ) => boolean ) {
362+ if ( element . nodeName === 'SCRIPT' || element . nodeName === 'STYLE' || document . head && document . head . contains ( element ) )
363+ return false ;
364+ if ( ( element instanceof HTMLInputElement ) && ( element . type === 'submit' || element . type === 'button' ) && matcher ( element . value ) )
365+ return true ;
366+ let lastText = '' ;
367+ for ( let child = element . firstChild ; child ; child = child . nextSibling ) {
368+ if ( child . nodeType === 3 /* Node.TEXT_NODE */ ) {
369+ lastText += child . nodeValue ;
370+ } else {
371+ if ( lastText && matcher ( lastText ) )
372+ return true ;
373+ lastText = '' ;
374+ }
375+ }
376+ return ! ! lastText && matcher ( lastText ) ;
377+ }
378+
379+ const xpathEngine : SelectorEngine = {
380+ query ( context : QueryContext , args : ( string | number | Selector ) [ ] , evaluator : SelectorEvaluator ) : Element [ ] {
381+ if ( args . length !== 1 || typeof args [ 0 ] !== 'string' )
382+ throw new Error ( `"xpath" engine expects a single string` ) ;
383+ const document = context . scope . nodeType === 9 /* Node.DOCUMENT_NODE */ ? context . scope as Document : context . scope . ownerDocument ;
384+ if ( ! document )
385+ return [ ] ;
386+ const result : Element [ ] = [ ] ;
387+ const it = document . evaluate ( args [ 0 ] , context . scope , null , XPathResult . ORDERED_NODE_ITERATOR_TYPE ) ;
388+ for ( let node = it . iterateNext ( ) ; node ; node = it . iterateNext ( ) ) {
389+ if ( node . nodeType === 1 /* Node.ELEMENT_NODE */ )
390+ result . push ( node as Element ) ;
391+ }
392+ return result ;
393+ } ,
394+ } ;
395+
396+ function createAttributeEngine ( attr : string ) : SelectorEngine {
397+ return {
398+ matches ( element : Element , args : ( string | number | Selector ) [ ] , context : QueryContext , evaluator : SelectorEvaluator ) : boolean {
399+ if ( args . length === 0 || typeof args [ 0 ] !== 'string' )
400+ throw new Error ( `"${ attr } " engine expects a single string` ) ;
401+ return element . getAttribute ( attr ) === args [ 0 ] ;
402+ } ,
403+
404+ query ( context : QueryContext , args : ( string | number | Selector ) [ ] , evaluator : SelectorEvaluator ) : Element [ ] {
405+ if ( args . length !== 1 || typeof args [ 0 ] !== 'string' )
406+ throw new Error ( `"${ attr } " engine expects a single string` ) ;
407+ const css = `[${ attr } =${ CSS . escape ( args [ 0 ] ) } ]` ;
408+ return ( evaluator as SelectorEvaluatorImpl ) . _queryCSS ( context , css ) ;
409+ } ,
410+ } ;
411+ }
412+
316413function parentElementOrShadowHost ( element : Element ) : Element | undefined {
317414 if ( element . parentElement )
318415 return element . parentElement ;
@@ -322,8 +419,18 @@ function parentElementOrShadowHost(element: Element): Element | undefined {
322419 return ( element . parentNode as ShadowRoot ) . host ;
323420}
324421
325- function parentElementOrShadowHostInScope ( element : Element , scope : Element | ShadowRoot | Document ) : Element | undefined {
326- return element === scope ? undefined : parentElementOrShadowHost ( element ) ;
422+ function parentElementOrShadowHostInContext ( element : Element , context : QueryContext ) : Element | undefined {
423+ if ( element === context . scope )
424+ return ;
425+ if ( ! context . pierceShadow )
426+ return element . parentElement || undefined ;
427+ return parentElementOrShadowHost ( element ) ;
428+ }
429+
430+ function previousSiblingInContext ( element : Element , context : QueryContext ) : Element | undefined {
431+ if ( element === context . scope )
432+ return ;
433+ return element . previousElementSibling || undefined ;
327434}
328435
329436function sortInDOMOrder ( elements : Element [ ] ) : Element [ ] {
0 commit comments