import { CompoundSelector , ParsedSelector , postcss , postcssSelectorParser as selectorParser } from "opticss" ; import { BLOCK_ALIAS , CLASS_NAME_IDENT } from "../../BlockSyntax" ; import { AttrValue , Block , BlockClass , Style } from "../../BlockTree" ; import { Configuration } from "../../configuration" ; import * as errors from "../../errors" ; import { selectorSourceRange as range , sourceRange } from "../../SourceLocation" ; import { BlockType , NodeAndType , blockTypeName , getStyleTargets , isAttributeNode , isClassLevelObject , isClassNode , isExternalBlock , isRootLevelObject , isRootNode , } from "../block-intermediates" ; import { stripQuotes } from "../utils" ; const SIBLING_COMBINATORS = new Set ( [ "+" , "~" ] ) ; const HIERARCHICAL_COMBINATORS = new Set ( [ " " , ">" ] ) ; const LEGAL_COMBINATORS = new Set ( [ "+" , "~" , " " , ">" ] ) ; param rule returns function shouldBeParsedAsBlockSelector ( rule ) { return ! ( rule . parent && rule . parent . type === "atrule" && ( rule . parent ) . name === "keyframes" ) ; } param block param rule param file returns function getParsedSelectors ( configuration , block , rule , file ) { let res ; try { res = block . getParsedSelectors ( rule ) ; } catch ( e ) { block . addError ( new errors . InvalidBlockSyntax ( e . message , sourceRange ( configuration , block . stylesheet , file , rule ) ) ) ; } return res || [ ] ; } export function constructBlock ( configuration , root , block , file ) { let styleRuleTuples = new Set ( ) ; root . walkRules ( ( rule ) => { if ( ! shouldBeParsedAsBlockSelector ( rule ) ) { return ; } let parsedSelectors = getParsedSelectors ( configuration , block , rule , file ) ; parsedSelectors . forEach ( ( iSel ) => { let keySel = iSel . key ; let sel = iSel . selector ; assertValidSelector ( configuration , block , rule , iSel , file ) ; while ( sel ) { let isKey = ( keySel === sel ) ; let foundStyles = getStyleTargets ( block , sel ) ; let blockName = sel . nodes . find ( n => isAttributeNode ( n ) && n . namespace ) ; if ( blockName ) { sel = sel . next && sel . next . selector ; continue ; } if ( isKey ) { if ( foundStyles . blockAttrs . length ) { foundStyles . blockAttrs . map ( s => addStyleRules ( configuration , block , rule , file , s , styleRuleTuples ) ) ; } else { foundStyles . blockClasses . map ( s => addStyleRules ( configuration , block , rule , file , s , styleRuleTuples ) ) ; } } sel = sel . next && sel . next . selector ; } } ) ; } ) ; for ( let [ style , rule ] of styleRuleTuples ) { style . rulesets . addRuleset ( configuration , file , rule ) ; } return block ; } function addStyleRules ( configuration , block , rule , file , style , tuple ) { rule . walkDecls ( BLOCK_ALIAS , decl => { let aliases = new Set ( ) ; decl . value . split ( /\s+/ ) . map ( alias => { let cleanedAlias = stripQuotes ( alias ) ; if ( ! CLASS_NAME_IDENT . test ( cleanedAlias ) ) { block . addError ( new errors . InvalidBlockSyntax ( `Illegal block-alias. "${ alias }" is not a legal CSS identifier.` , sourceRange ( configuration , block . stylesheet , file , rule ) ) ) ; } aliases . add ( cleanedAlias ) ; } ) ; style . setStyleAliases ( aliases ) ; } ) ; tuple . add ( [ style , rule ] ) ; } param block param rule param selector function assertValidSelector ( configuration , block , rule , selector , file ) { let keyObject = assertBlockObject ( configuration , block , selector . key , rule , file ) ; if ( keyObject && keyObject . blockName ) { block . addError ( new errors . InvalidBlockSyntax ( `Cannot style values from other blocks: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , keyObject . node ) ) ) ; } let currentCompoundSel = selector . selector ; let currentObject = selector . key === currentCompoundSel ? keyObject : assertBlockObject ( configuration , block , currentCompoundSel , rule , file ) ; let foundRootLevel = currentObject ? isRootLevelObject ( currentObject ) : false ; let foundClassLevel = currentObject ? isClassLevelObject ( currentObject ) : false ; let foundObjects = currentObject ? [ currentObject ] : [ ] ; let foundCombinators = [ ] ; while ( currentCompoundSel . next ) { let combinator = currentCompoundSel . next . combinator . value ; foundCombinators . push ( combinator ) ; let nextCompoundSel = currentCompoundSel . next . selector ; let nextObject = selector . key === nextCompoundSel ? keyObject : assertBlockObject ( configuration , block , nextCompoundSel , rule , file ) ; let nextLevelIsRoot = nextObject ? isRootLevelObject ( nextObject ) : false ; let nextLevelIsClass = nextObject ? isClassLevelObject ( nextObject ) : false ; if ( ! LEGAL_COMBINATORS . has ( combinator ) ) { block . addError ( new errors . InvalidBlockSyntax ( `Illegal Combinator '${ combinator }': ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , currentCompoundSel . next . combinator ) ) ) ; } if ( currentObject && isClassLevelObject ( currentObject ) && nextLevelIsRoot && SIBLING_COMBINATORS . has ( combinator ) ) { block . addError ( new errors . InvalidBlockSyntax ( `A class is never a sibling of a ${ blockTypeName ( ( nextObject as NodeAndType ) . blockType ) }: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , selector . selector . nodes [ 0 ] ) ) ) ; } if ( foundClassLevel && nextLevelIsRoot ) { block . addError ( new errors . InvalidBlockSyntax ( `Illegal scoping of a ${ blockTypeName ( ( currentObject as NodeAndType ) . blockType ) }: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , currentCompoundSel . next . combinator ) ) ) ; } if ( foundRootLevel && nextLevelIsRoot && foundCombinators . some ( c => HIERARCHICAL_COMBINATORS . has ( c ) ) ) { if ( ! foundObjects . every ( f => f . node . toString ( ) === ( nextObject as NodeAndType ) . node . toString ( ) ) ) { block . addError ( new errors . InvalidBlockSyntax ( `Illegal scoping of a ${ blockTypeName ( ( currentObject as NodeAndType ) . blockType ) }: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , currentCompoundSel . next . combinator ) ) ) ; } } if ( currentObject && isRootLevelObject ( currentObject ) && nextLevelIsClass && SIBLING_COMBINATORS . has ( combinator ) ) { block . addError ( new errors . InvalidBlockSyntax ( `A ${ blockTypeName ( ( nextObject as NodeAndType ) . blockType ) } cannot be a sibling with a ${ blockTypeName ( currentObject . blockType ) }: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , currentCompoundSel . next . combinator ) ) ) ; } if ( nextObject && isClassLevelObject ( nextObject ) ) { let conflictObj = foundObjects . find ( obj => isClassLevelObject ( obj ) && obj . node . toString ( ) !== ( nextObject as NodeAndType ) . node . toString ( ) ) ; if ( conflictObj ) { if ( conflictObj . blockType === nextObject . blockType ) { block . addError ( new errors . InvalidBlockSyntax ( `Distinct ${ blockTypeName ( conflictObj . blockType , { plural : true } ) } cannot be combined: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , nextObject . node ) ) ) ; } else { block . addError ( new errors . InvalidBlockSyntax ( `Cannot combine a ${ blockTypeName ( conflictObj . blockType ) } with a ${ blockTypeName ( nextObject . blockType ) }}: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , nextObject . node ) ) ) ; } } } if ( nextObject ) { foundObjects . push ( nextObject ) ; } foundRootLevel = foundRootLevel || nextLevelIsRoot ; foundClassLevel = foundClassLevel || nextLevelIsClass ; currentObject = nextObject ; currentCompoundSel = nextCompoundSel ; } } param block param sel param rule return function assertBlockObject ( configuration , block , sel , rule , file ) { let tagNode = sel . nodes . find ( selectorParser . isTag ) ; if ( tagNode ) { block . addError ( new errors . InvalidBlockSyntax ( `Tag name selectors are not allowed: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , tagNode ) ) ) ; } let nonStateAttribute = sel . nodes . find ( n => selectorParser . isAttribute ( n ) && ! isAttributeNode ( n ) ) ; if ( nonStateAttribute ) { block . addError ( new errors . InvalidBlockSyntax ( `Cannot select attributes in the \`${ selectorParser . isAttribute ( nonStateAttribute ) && nonStateAttribute . namespaceString }\` namespace: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , nonStateAttribute ) ) ) ; } sel . nodes . forEach ( n => { if ( selectorParser . isPseudoClass ( n ) ) { let pseudo = n ; if ( pseudo . value === ":not" || pseudo . value === ":matches" ) { block . addError ( new errors . InvalidBlockSyntax ( `The ${ pseudo . value }() pseudoclass cannot be used: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , pseudo ) ) ) ; } } } ) ; let result = sel . nodes . reduce < NodeAndType | null > ( ( found , n ) => { if ( isRootNode ( n ) ) { if ( found === null ) { found = { blockType : BlockType . root , node : n , } ; } else { if ( found . blockType === BlockType . class || found . blockType === BlockType . classAttribute ) { block . addError ( new errors . InvalidBlockSyntax ( `${ n } cannot be on the same element as ${ found . node }: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , sel . nodes [ 0 ] ) ) ) ; } } } else if ( isAttributeNode ( n ) ) { if ( n . value && n . operator !== "=" ) { block . addError ( new errors . InvalidBlockSyntax ( `A state with a value must use the = operator (found ${ n . operator } instead).` , range ( configuration , block . stylesheet , file , rule , n ) ) ) ; } if ( n . attribute === "scope" ) { block . addError ( new errors . InvalidBlockSyntax ( `A state cannot be named 'scope'.` , range ( configuration , block . stylesheet , file , rule , n ) ) ) ; } if ( ! found ) { block . addError ( new errors . InvalidBlockSyntax ( `States without an explicit :scope or class selector are not supported: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , n ) ) ) ; } else if ( found . blockType === BlockType . class || found . blockType === BlockType . classAttribute ) { found = { node : n , blockType : BlockType . classAttribute } ; } else if ( found . blockType === BlockType . root || found . blockType === BlockType . attribute ) { if ( n . namespace === true ) { block . addError ( new errors . InvalidBlockSyntax ( `The "any namespace" selector is not supported: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , n ) ) ) ; } else { found = { node : n , blockType : BlockType . attribute , blockName : n . namespace } ; } } } else if ( isClassNode ( n ) ) { if ( ! found ) { found = { node : n , blockType : BlockType . class , } ; } else { if ( found . blockType === BlockType . root ) { block . addError ( new errors . InvalidBlockSyntax ( `${ n } cannot be on the same element as ${ found . node }: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , sel . nodes [ 0 ] ) ) ) ; } else if ( found . blockType === BlockType . class ) { if ( n . toString ( ) !== found . node . toString ( ) ) { block . addError ( new errors . InvalidBlockSyntax ( `Two distinct classes cannot be selected on the same element: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , n ) ) ) ; } } else if ( found . blockType === BlockType . classAttribute || found . blockType === BlockType . attribute ) { block . addError ( new errors . InvalidBlockSyntax ( `The class must precede the state: ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , sel . nodes [ 0 ] ) ) ) ; } } } return found ; } , null ) ; if ( ! result ) { block . addError ( new errors . InvalidBlockSyntax ( `Missing block object in selector component '${ sel . nodes . join ( "" ) }': ${ rule . selector }` , range ( configuration , block . stylesheet , file , rule , sel . nodes [ 0 ] ) ) ) ; return null ; } else if ( isExternalBlock ( result ) ) { let blockName ; if ( result . blockType === BlockType . attribute ) { blockName = result . blockName ! ; } else { blockName = result . node . value ; } let external = block . getReferencedBlock ( blockName ) ; if ( ! external ) { block . addError ( new errors . InvalidBlockSyntax ( `A block named "${ blockName }" does not exist in this context.` , range ( configuration , block . stylesheet , file , rule , sel . nodes [ 0 ] ) ) ) ; } else { let globalStates = external . rootClass . allAttributeValues ( ) . filter ( ( a ) => a . isGlobal ) ; if ( ! globalStates . length ) { block . addError ( new errors . InvalidBlockSyntax ( `External Block '${ blockName }' has no global states.` , range ( configuration , block . stylesheet , file , rule , sel . nodes [ 0 ] ) ) ) ; } if ( result . blockType !== BlockType . attribute ) { block . addError ( new errors . InvalidBlockSyntax ( `Missing global state selector on external Block '${ blockName }'. Did you mean one of: ${ globalStates . map ( ( s ) => s . asSource ( ) ) . join ( " " ) }` , range ( configuration , block . stylesheet , file , rule , sel . nodes [ 0 ] ) ) ) ; } } return result ; } else { return result ; } }