import { Attr , AttributeValue , isAbsent , isChoice , isConstant , isEndsWith , isSet , isStartsAndEndsWith , isStartsWith , isUnknown , isUnknownIdentifier , } from "@opticss/element-analysis" ; import { assertNever } from "@opticss/util" ; import * as SelectorParser from "postcss-selector-parser" ; import { Memoize } from "typescript-memoize" ; import { Match , boolToMatch , matches } from "./Match" ; import { HasSelectorNodes , Matcher } from "./Matcher" ; export function isAttrNode ( node ) { if ( node . type === SelectorParser . ATTRIBUTE ) { return true ; } else { return false ; } } export class AttributeMatcher extends Matcher < Attr > { private constructor ( ) { super ( ) ; } private static _instance = new AttributeMatcher ( ) ; public static get instance ( ) { return AttributeMatcher . _instance ; } matchSelectorComponent ( attr , selector ) { let no = false ; let maybe = false ; selector . nodes . forEach ( node => { let match = this . matchSelectorNode ( attr , node ) ; switch ( match ) { case Match . no : no = true ; break ; case Match . maybe : maybe = true ; break ; case Match . yes : case Match . pass : break ; default : assertNever ( match ) ; } } ) ; if ( no ) { return Match . no ; } else if ( maybe ) { return Match . maybe ; } else { return Match . yes ; } } matchSelectorNode ( attr , node ) { switch ( node . type ) { case "string" : case "selector" : case "root" : case "comment" : case "combinator" : case "nesting" : case "pseudo" : return Match . pass ; case "tag" : return Match . no ; case "id" : if ( attr . name === "id" && attr . namespaceURL === null ) { return this . matchIdent ( attr , node . value ) ; } else { return Match . no ; } case "class" : if ( attr . name === "class" && attr . namespaceURL === null ) { return boolToMatch ( this . matchWhitespaceDelimited ( attr , node . value ) ) ; } else { return Match . no ; } case "attribute" : let a = node ; if ( a . attribute === attr . name && attr . sameNamespace ( a . namespaceString ) ) { return this . matchAttributeNode ( attr , node ) ; } else { return Match . no ; } case "universal" : return Match . yes ; default : return assertNever ( node ) ; } } param identifier param value returns matchIdent ( attr , identifier , value = attr . value , whitespaceDelimited = false ) { if ( isAbsent ( value ) ) { return Match . no ; } else if ( isUnknown ( value ) ) { return Match . maybe ; } else if ( isUnknownIdentifier ( value ) ) { return Match . maybe ; } else if ( isConstant ( value ) ) { if ( value . constant === identifier ) { return Match . yes ; } else { return Match . no ; } } else if ( isStartsWith ( value ) ) { return boolToMatch ( identifier . startsWith ( value . startsWith ) ) ; } else if ( isEndsWith ( value ) ) { return boolToMatch ( identifier . endsWith ( value . endsWith ) ) ; } else if ( isStartsAndEndsWith ( value ) ) { return boolToMatch ( identifier . startsWith ( value . startsWith ) && identifier . endsWith ( value . endsWith ) ) ; } else if ( isSet ( value ) ) { if ( whitespaceDelimited ) { return boolToMatch ( value . allOf . some ( v => matches ( this . matchIdent ( attr , identifier , v , whitespaceDelimited ) ) ) ) ; } else { return Match . no ; } } else if ( isChoice ( value ) ) { return boolToMatch ( value . oneOf . some ( v => matches ( this . matchIdent ( attr , identifier , v , whitespaceDelimited ) ) ) ) ; } else { return assertNever ( value ) ; } } param identifier param value returns matchWhitespaceDelimited ( attr , identifier , value ? ) { if ( ! value ) { value = attr . value ; } if ( isAbsent ( value ) ) { return false ; } else if ( isUnknown ( value ) ) { return true ; } else if ( isUnknownIdentifier ( value ) ) { return true ; } else if ( isConstant ( value ) ) { return ( value . constant === identifier ) ; } else if ( isStartsWith ( value ) || isEndsWith ( value ) || isStartsAndEndsWith ( value ) ) { if ( value . whitespace ) { return true ; } if ( ( isStartsWith ( value ) || isStartsAndEndsWith ( value ) ) && ! identifier . startsWith ( value . startsWith ) ) { return false ; } if ( ( isEndsWith ( value ) || isStartsAndEndsWith ( value ) ) && ! identifier . endsWith ( value . endsWith ) ) { return false ; } return true ; } else if ( isSet ( value ) ) { return value . allOf . some ( v => matches ( this . matchIdent ( attr , identifier , v , true ) ) ) ; } else if ( isChoice ( value ) ) { return value . oneOf . some ( v => matches ( this . matchIdent ( attr , identifier , v , true ) ) ) ; } else { return assertNever ( value ) ; } } matchAttributeNode ( attr , node ) { if ( ! isAttrNode ( node ) ) { return Match . no ; } let caseInsensitive = ! ! node . insensitive ; if ( node . operator === undefined ) { return Match . yes ; } else { if ( attr . value === undefined ) return Match . no ; switch ( node . operator ) { case "=" : let attrRegex = this . regexForAttrEq ( attr , caseInsensitive ) ; if ( attrRegex . test ( node . value ! ) ) { if ( attr . isAmbiguous ( ) ) { return Match . maybe ; } else { return Match . yes ; } } else { return Match . no ; } default : throw new Error ( `Unsupported Attribute selector: ${ node . toString ( ) }` ) ; } } } @ Memoize ( ) regexForAttrEq ( attr , caseInsensitive ) { return new RegExp ( "^" + this . regexPatternForAttr ( attr . value ) + "$" , caseInsensitive ? "i" : undefined ) ; } @ Memoize ( ) regexPatternForAttr ( condition ) { if ( isUnknown ( condition ) ) { return "*" ; } else if ( isUnknownIdentifier ( condition ) ) { return "[^\\s]+" ; } else if ( isAbsent ( condition ) ) { return "" ; } else if ( isConstant ( condition ) ) { return condition . constant ; } else if ( isStartsWith ( condition ) ) { let unknownPattern = condition . whitespace ? "*" : "[^\\s]*" ; return `${ condition . startsWith }${ unknownPattern }` ; } else if ( isEndsWith ( condition ) ) { let unknownPattern = condition . whitespace ? "*" : "[^\\s]*" ; return `${ unknownPattern }${ condition . endsWith }` ; } else if ( isStartsAndEndsWith ( condition ) ) { let unknownPattern = condition . whitespace ? "*" : "[^\\s]*" ; return `${ condition . startsWith }${ unknownPattern }${ condition . endsWith }` ; } else if ( isChoice ( condition ) ) { return `(?:${ condition . oneOf . map ( c => this . regexPatternForAttr ( c ) ) . join ( "|" ) })` ; } else if ( isSet ( condition ) ) { return condition . allOf . map ( c => this . regexPatternForAttr ( c ) ) . join ( "\\s+" ) ; } else { return assertNever ( condition ) ; } } }