import { isAST , isAtoms } from './types' ; import type { AST , Atoms , AtomsOrStrings , Attribute , Class , Combinator , Comma , Id , ParserOptions , PseudoClass , PseudoElement , Strings , TokenType , Type , } from './types' ; export const RECURSIVE_PSEUDO_CLASSES = new Set ( [ 'any' , 'dir' , 'has' , 'host-context' , 'if' , 'if-not' , 'is' , 'matches' , 'not' , 'where' , ] ) ; const TOKENS = { attribute : /\[\s*(?:(?<namespace>\*|[-\w]*)\|)?(?<name>[-\w\u{0080}-\u{FFFF}]+)\s*(?:(?<operator>\W?=)\s*(?<value>.+?)\s*(?<caseSensitive>[iIsS])?\s*)?\]/gu , id : /#(?<name>(?:[-\w\u{0080}-\u{FFFF}]|\\.)+)/gu , class : /\.(?<name>(?:[-\w\u{0080}-\u{FFFF}]|\\.)+)/gu , comma : /\s*,\s*/g , combinator : /\s*[\s>+~]\s*/g , 'pseudo-element' : /::(?<name>[-\w\u{0080}-\u{FFFF}]+)(?:\((?:¶*)\))?/gu , 'pseudo-class' : /:(?<name>[-\w\u{0080}-\u{FFFF}]+)(?:\((?<argument>¶*)\))?/gu , type : /(?:(?<namespace>\*|[-\w]*)\|)?(?<name>[-\w\u{0080}-\u{FFFF}]+)|\*/gu , } ; const TOKENS_WITH_PARENS = new Set ( [ 'pseudo-class' , 'pseudo-element' ] ) ; const TOKENS_WITH_STRINGS = new Set ( [ ... TOKENS_WITH_PARENS , 'attribute' ] ) ; const TRIM_TOKENS = new Set ( [ 'combinator' , 'comma' ] ) ; const TOKENS_FOR_RESTORE = Object . assign ( { } , TOKENS ) ; TOKENS_FOR_RESTORE [ 'pseudo-element' ] = RegExp ( TOKENS [ 'pseudo-element' ] . source . replace ( '(?<argument>¶*)' , '(?<argument>.*?)' ) , 'gu' ) ; TOKENS_FOR_RESTORE [ 'pseudo-class' ] = RegExp ( TOKENS [ 'pseudo-class' ] . source . replace ( '(?<argument>¶*)' , '(?<argument>.*)' ) , 'gu' ) ; function splitOnMatch ( pattern , str ) { pattern . lastIndex = 0 ; const match = pattern . exec ( str ) ; if ( match === null ) { return undefined ; } const from = match . index - 1 ; const content = match [ 0 ] ; const before = str . slice ( 0 , from + 1 ) ; const after = str . slice ( from + content . length + 1 ) ; return [ before , [ content , match . groups || { } ] , after ] ; } const GRAMMAR = [ ( str ) : [ string , Attribute , string ] | undefined => { const match = splitOnMatch ( TOKENS . attribute , str ) ; if ( match === undefined ) { return undefined ; } const [ before , [ content , { name , operator , value , namespace , caseSensitive } ] , after ] = match ; if ( name === undefined ) { return undefined ; } return [ before , { type : 'attribute' , content , length : content . length , namespace , caseSensitive , pos : [ ] , name , operator , value , } , after , ] ; } , ( str ) : [ string , Id , string ] | undefined => { const match = splitOnMatch ( TOKENS . id , str ) ; if ( match === undefined ) { return undefined ; } const [ before , [ content , { name } ] , after ] = match ; if ( name === undefined ) { return undefined ; } return [ before , { type : 'id' , content , length : content . length , pos : [ ] , name , } , after , ] ; } , ( str ) : [ string , Class , string ] | undefined => { const match = splitOnMatch ( TOKENS . class , str ) ; if ( match === undefined ) { return undefined ; } const [ before , [ content , { name } ] , after ] = match ; if ( name === undefined ) { return undefined ; } return [ before , { type : 'class' , content , length : content . length , pos : [ ] , name , } , after , ] ; } , ( str ) : [ string , Comma , string ] | undefined => { const match = splitOnMatch ( TOKENS . comma , str ) ; if ( match === undefined ) { return undefined ; } const [ before , [ content ] , after ] = match ; return [ before , { type : 'comma' , content , length : content . length , pos : [ ] , } , after , ] ; } , ( str ) : [ string , Combinator , string ] | undefined => { const match = splitOnMatch ( TOKENS . combinator , str ) ; if ( match === undefined ) { return undefined ; } const [ before , [ content ] , after ] = match ; return [ before , { type : 'combinator' , content , length : content . length , pos : [ ] , } , after , ] ; } , ( str ) : [ string , PseudoElement , string ] | undefined => { const match = splitOnMatch ( TOKENS [ 'pseudo-element' ] , str ) ; if ( match === undefined ) { return undefined ; } const [ before , [ content , { name } ] , after ] = match ; if ( name === undefined ) { return undefined ; } return [ before , { type : 'pseudo-element' , content , length : content . length , pos : [ ] , name , } , after , ] ; } , ( str ) : [ string , PseudoClass , string ] | undefined => { const match = splitOnMatch ( TOKENS [ 'pseudo-class' ] , str ) ; if ( match === undefined ) { return undefined ; } const [ before , [ content , { name , argument } ] , after ] = match ; if ( name === undefined ) { return undefined ; } return [ before , { type : 'pseudo-class' , content , length : content . length , pos : [ ] , name , argument , subtree : undefined , } , after , ] ; } , ( str ) : [ string , Type , string ] | undefined => { const match = splitOnMatch ( TOKENS . type , str ) ; if ( match === undefined ) { return undefined ; } const [ before , [ content , { name , namespace } ] , after ] = match ; return [ before , { type : 'type' , content , length : content . length , namespace , pos : [ ] , name , } , after , ] ; } , ] ; function tokenizeBy ( text ) { if ( ! text ) { return [ ] ; } const strarr = [ text ] ; for ( const tokenizer of GRAMMAR ) { for ( let i = 0 ; i < strarr . length ; i ++ ) { const str = strarr [ i ] ; if ( typeof str === 'string' ) { const match = tokenizer ( str ) ; if ( match !== undefined ) { strarr . splice ( i , 1 , ... ( match as AtomsOrStrings ) . filter ( ( a ) => a . length !== 0 ) ) ; } } } } let offset = 0 ; for ( const token of strarr ) { if ( typeof token !== 'string' ) { token . pos = [ offset , offset + token . length ] ; if ( TRIM_TOKENS . has ( token . type ) ) { token . content = token . content . trim ( ) || ' ' ; } } offset += token . length ; } if ( isAtoms ( strarr ) ) { return strarr ; } return [ ] ; } function restoreNested ( tokens , strings , regex , types ) { for ( const str of strings ) { for ( const token of tokens ) { if ( types . has ( token . type ) && token . pos [ 0 ] < str . start && str . start < token . pos [ 1 ] ) { const content = token . content ; token . content = token . content . replace ( regex , str . str ) ; if ( token . content !== content ) { TOKENS_FOR_RESTORE [ token . type ] . lastIndex = 0 ; const match = TOKENS_FOR_RESTORE [ token . type ] . exec ( token . content ) ; if ( match !== null ) { Object . assign ( token , match . groups ) ; } } } } } } export function isEscaped ( str , index ) { let backslashes = 0 ; index -= 1 ; while ( index >= 0 && str [ index ] === '\\' ) { backslashes += 1 ; index -= 1 ; } return backslashes % 2 !== 0 ; } export function gobbleQuotes ( text , quote , start ) { let end = start + 1 ; while ( ( end = text . indexOf ( quote , end ) ) !== - 1 && isEscaped ( text , end ) === true ) { end += 1 ; } if ( end === - 1 ) { return undefined ; } return text . slice ( start , end + 1 ) ; } export function gobbleParens ( text , start ) { let stack = 0 ; for ( let i = start ; i < text . length ; i ++ ) { const char = text [ i ] ; if ( char === '(' ) { stack += 1 ; } else if ( char === ')' ) { if ( stack > 0 ) { stack -= 1 ; } else { return undefined ; } } if ( stack === 0 ) { return text . slice ( start , i + 1 ) ; } } return undefined ; } export function replace ( selector , replacement , opening , gobble ) { const strings = [ ] ; let offset = 0 ; while ( ( offset = selector . indexOf ( opening , offset ) ) !== - 1 ) { const str = gobble ( selector , offset ) ; if ( str === undefined ) { break ; } strings . push ( { str , start : offset } ) ; selector = `${ selector . slice ( 0 , offset + 1 ) }${ replacement . repeat ( str . length - 2 ) }${ selector . slice ( offset + str . length - 1 ) }` ; offset += str . length ; } return [ strings , selector ] ; } export function tokenize ( selector ) { if ( typeof selector !== 'string' ) { return [ ] ; } selector = selector . trim ( ) ; if ( selector . length === 0 ) { return [ ] ; } const [ doubleQuotes , selectorWithoutDoubleQuotes ] = replace ( selector , '§' , '"' , ( text , start ) => gobbleQuotes ( text , '"' , start ) ) ; const [ singleQuotes , selectorWithoutQuotes ] = replace ( selectorWithoutDoubleQuotes , '§' , "'" , ( text , start ) => gobbleQuotes ( text , "'" , start ) ) ; const [ parens , selectorWithoutParens ] = replace ( selectorWithoutQuotes , '¶' , '(' , gobbleParens ) ; const tokens = tokenizeBy ( selectorWithoutParens ) ; restoreNested ( tokens , parens , /\(¶*\)/ , TOKENS_WITH_PARENS ) ; restoreNested ( tokens , doubleQuotes , /"§*"/ , TOKENS_WITH_STRINGS ) ; restoreNested ( tokens , singleQuotes , /'§*'/ , TOKENS_WITH_STRINGS ) ; return tokens ; } function nestTokens ( tokens , { list = true } = { } ) { if ( list === true && tokens . some ( ( t ) => t . type === 'comma' ) ) { const selectors = [ ] ; const temp = [ ] ; for ( let i = 0 ; i < tokens . length ; i += 1 ) { const token = tokens [ i ] ; if ( token . type === 'comma' ) { if ( temp . length === 0 ) { throw new Error ( 'Incorrect comma at ' + i ) ; } const sub = nestTokens ( temp , { list : false } ) ; if ( sub !== undefined ) { selectors . push ( sub ) ; } temp . length = 0 ; } else { temp . push ( token ) ; } } if ( temp . length === 0 ) { throw new Error ( 'Trailing comma' ) ; } else { const sub = nestTokens ( temp , { list : false } ) ; if ( sub !== undefined ) { selectors . push ( sub ) ; } } return { type : 'list' , list : selectors } ; } for ( let i = tokens . length - 1 ; i >= 0 ; i -- ) { const token = tokens [ i ] ; if ( token . type === 'combinator' ) { const left = nestTokens ( tokens . slice ( 0 , i ) ) ; const right = nestTokens ( tokens . slice ( i + 1 ) ) ; if ( right === undefined ) { return undefined ; } if ( token . content !== ' ' && token . content !== '~' && token . content !== '+' && token . content !== '>' ) { return undefined ; } return { type : 'complex' , combinator : token . content , left , right , } ; } } if ( tokens . length === 0 ) { return undefined ; } if ( isAST ( tokens ) ) { if ( tokens . length === 1 ) { return tokens [ 0 ] ; } return { type : 'compound' , compound : [ ... tokens ] , } ; } return undefined ; } function walk ( node , callback , o ? , parent ? ) { if ( node === undefined ) { return ; } if ( node . type === 'complex' ) { walk ( node . left , callback , o , node ) ; walk ( node . right , callback , o , node ) ; } else if ( node . type === 'compound' ) { for ( const n of node . compound ) { walk ( n , callback , o , node ) ; } } else if ( node . type === 'pseudo-class' && node . subtree !== undefined && o !== undefined && o . type === 'pseudo-class' && o . subtree !== undefined ) { walk ( node . subtree , callback , o , node ) ; } callback ( node , parent ) ; } param selector { String } param options . recursive { Boolean } param options . list { Boolean } export function parse ( selector , { recursive = true , list = true } = { } ) { const tokens = tokenize ( selector ) ; if ( tokens . length === 0 ) { return undefined ; } const ast = nestTokens ( tokens , { list } ) ; if ( recursive === true ) { walk ( ast , ( node ) => { if ( node . type === 'pseudo-class' && node . argument && node . name !== undefined && RECURSIVE_PSEUDO_CLASSES . has ( node . name ) ) { node . subtree = parse ( node . argument , { recursive : true , list : true } ) ; } } ) ; } return ast ; }