@@ -81,29 +81,24 @@ export type TraversalType =
81
81
82
82
const reName = / ^ [ ^ \\ # ] ? (?: \\ (?: [ \d a - f ] { 1 , 6 } \s ? | .) | [ \w \- \u00b0 - \uFFFF ] ) + / ;
83
83
const reEscape = / \\ ( [ \d a - f ] { 1 , 6 } \s ? | ( \s ) | .) / gi;
84
- // Modified version of https://github.com/jquery/sizzle/blob/master/src/sizzle.js#L87
85
- const reAttr =
86
- / ^ \s * (?: ( \* | [ - \w ] * ) \| ) ? ( (?: \\ .| [ \w \u00b0 - \uFFFF - ] ) + ) \s * (?: ( \S ? ) = \s * (?: ( [ ' " ] ) ( (?: [ ^ \\ ] | \\ [ ^ ] ) * ?) \4| ( # ? (?: \\ .| [ \w \u00b0 - \uFFFF - ] ) * ) | ) | ) \s * ( [ i I s S ] ) ? \s * \] / ;
87
-
88
- const actionTypes : { [ key : string ] : AttributeAction } = {
89
- undefined : "exists" ,
90
- "" : "equals" ,
91
- "~" : "element" ,
92
- "^" : "start" ,
93
- $ : "end" ,
94
- "*" : "any" ,
95
- "!" : "not" ,
96
- "|" : "hyphen" ,
97
- } ;
98
84
99
- const Traversals : { [ key : string ] : TraversalType } = {
85
+ const actionTypes = new Map < string , AttributeAction > ( [
86
+ [ "~" , "element" ] ,
87
+ [ "^" , "start" ] ,
88
+ [ "$" , "end" ] ,
89
+ [ "*" , "any" ] ,
90
+ [ "!" , "not" ] ,
91
+ [ "|" , "hyphen" ] ,
92
+ ] ) ;
93
+
94
+ const Traversals : Record < string , TraversalType > = {
100
95
">" : "child" ,
101
96
"<" : "parent" ,
102
97
"~" : "sibling" ,
103
98
"+" : "adjacent" ,
104
99
} ;
105
100
106
- const attribSelectors : { [ key : string ] : [ string , AttributeAction ] } = {
101
+ const attribSelectors : Record < string , [ string , AttributeAction ] > = {
107
102
"#" : [ "id" , "equals" ] ,
108
103
"." : [ "class" , "element" ] ,
109
104
} ;
@@ -302,10 +297,7 @@ function parseSelector(
302
297
tokens = [ ] ;
303
298
sawWS = false ;
304
299
stripWhitespace ( 1 ) ;
305
- } else if (
306
- firstChar === "/" &&
307
- selector . charAt ( selectorIndex + 1 ) === "*"
308
- ) {
300
+ } else if ( selector . startsWith ( "/*" , selectorIndex ) ) {
309
301
const endIndex = selector . indexOf ( "*/" , selectorIndex + 2 ) ;
310
302
311
303
if ( endIndex < 0 ) {
@@ -332,51 +324,134 @@ function parseSelector(
332
324
ignoreCase : options . xmlMode ? null : false ,
333
325
} ) ;
334
326
} else if ( firstChar === "[" ) {
335
- const attributeMatch = selector
336
- . slice ( selectorIndex + 1 )
337
- . match ( reAttr ) ;
338
-
339
- if ( ! attributeMatch ) {
340
- throw new Error (
341
- `Malformed attribute selector: ${ selector . slice (
342
- selectorIndex
343
- ) } `
344
- ) ;
327
+ stripWhitespace ( 1 ) ;
328
+
329
+ // Determine attribute name and namespace
330
+
331
+ let name ;
332
+ let namespace : string | null = null ;
333
+
334
+ if ( selector . charAt ( selectorIndex ) === "|" ) {
335
+ namespace = "" ;
336
+ selectorIndex += 1 ;
337
+ }
338
+
339
+ if ( selector . startsWith ( "*|" , selectorIndex ) ) {
340
+ namespace = "*" ;
341
+ selectorIndex += 2 ;
345
342
}
346
343
347
- const [
348
- completeSelector ,
349
- namespace = null ,
350
- baseName ,
351
- actionType ,
352
- ,
353
- quotedValue = "" ,
354
- value = quotedValue ,
355
- forceIgnore ,
356
- ] = attributeMatch ;
344
+ name = getName ( 0 ) ;
357
345
358
- selectorIndex += completeSelector . length + 1 ;
359
- let name = unescapeCSS ( baseName ) ;
346
+ if (
347
+ namespace === null &&
348
+ selector . charAt ( selectorIndex ) === "|" &&
349
+ selector . charAt ( selectorIndex + 1 ) !== "="
350
+ ) {
351
+ namespace = name ;
352
+ name = getName ( 1 ) ;
353
+ }
360
354
361
355
if ( options . lowerCaseAttributeNames ?? ! options . xmlMode ) {
362
356
name = name . toLowerCase ( ) ;
363
357
}
364
358
365
- const ignoreCase =
359
+ stripWhitespace ( 0 ) ;
360
+
361
+ // Determine comparison operation
362
+
363
+ let action : AttributeAction = "exists" ;
364
+ const possibleAction = actionTypes . get (
365
+ selector . charAt ( selectorIndex )
366
+ ) ;
367
+
368
+ if ( possibleAction ) {
369
+ action = possibleAction ;
370
+
371
+ if ( selector . charAt ( selectorIndex + 1 ) !== "=" ) {
372
+ throw new Error ( "Expected `=`" ) ;
373
+ }
374
+
375
+ stripWhitespace ( 2 ) ;
376
+ } else if ( selector . charAt ( selectorIndex ) === "=" ) {
377
+ action = "equals" ;
378
+ stripWhitespace ( 1 ) ;
379
+ }
380
+
381
+ // Determine value
382
+
383
+ let value = "" ;
384
+ let ignoreCase : boolean | null = null ;
385
+
386
+ if ( action !== "exists" ) {
387
+ if ( quotes . has ( selector . charAt ( selectorIndex ) ) ) {
388
+ const quote = selector . charAt ( selectorIndex ) ;
389
+ let sectionEnd = selectorIndex + 1 ;
390
+ while (
391
+ sectionEnd < selector . length &&
392
+ ( selector . charAt ( sectionEnd ) !== quote ||
393
+ isEscaped ( sectionEnd ) )
394
+ ) {
395
+ sectionEnd += 1 ;
396
+ }
397
+
398
+ if ( selector . charAt ( sectionEnd ) !== quote ) {
399
+ throw new Error ( "Attribute value didn't end" ) ;
400
+ }
401
+
402
+ value = unescapeCSS (
403
+ selector . slice ( selectorIndex + 1 , sectionEnd )
404
+ ) ;
405
+ selectorIndex = sectionEnd + 1 ;
406
+ } else {
407
+ const valueStart = selectorIndex ;
408
+
409
+ while (
410
+ selectorIndex < selector . length &&
411
+ ( ( ! isWhitespace ( selector . charAt ( selectorIndex ) ) &&
412
+ selector . charAt ( selectorIndex ) !== "]" ) ||
413
+ isEscaped ( selectorIndex ) )
414
+ ) {
415
+ selectorIndex += 1 ;
416
+ }
417
+
418
+ value = unescapeCSS (
419
+ selector . slice ( valueStart , selectorIndex )
420
+ ) ;
421
+ }
422
+
423
+ stripWhitespace ( 0 ) ;
424
+
425
+ // See if we have a force ignore flag
426
+
427
+ const forceIgnore = selector . charAt ( selectorIndex ) ;
366
428
// If the forceIgnore flag is set (either `i` or `s`), use that value
367
- forceIgnore
368
- ? forceIgnore . toLowerCase ( ) === "i"
369
- : // If `xmlMode` is set, there are no rules; return `null`.
370
- options . xmlMode
371
- ? null
372
- : // Otherwise, use the `caseInsensitiveAttributes` list.
373
- caseInsensitiveAttributes . has ( name ) ;
429
+ if ( forceIgnore === "s" || forceIgnore === "S" ) {
430
+ ignoreCase = false ;
431
+ stripWhitespace ( 1 ) ;
432
+ } else if ( forceIgnore === "i" || forceIgnore === "I" ) {
433
+ ignoreCase = true ;
434
+ stripWhitespace ( 1 ) ;
435
+ }
436
+ }
437
+
438
+ // If `xmlMode` is set, there are no rules; otherwise, use the `caseInsensitiveAttributes` list.
439
+ if ( ! options . xmlMode ) {
440
+ // TODO: Skip this for `exists`, as there is no value to compare to.
441
+ ignoreCase ??= caseInsensitiveAttributes . has ( name ) ;
442
+ }
443
+
444
+ if ( selector . charAt ( selectorIndex ) !== "]" ) {
445
+ throw new Error ( "Attribute selector didn't terminate" ) ;
446
+ }
447
+
448
+ selectorIndex += 1 ;
374
449
375
450
const attributeSelector : AttributeSelector = {
376
451
type : "attribute" ,
377
452
name,
378
- action : actionTypes [ actionType ] ,
379
- value : unescapeCSS ( value ) ,
453
+ action,
454
+ value,
380
455
namespace,
381
456
ignoreCase,
382
457
} ;
0 commit comments