import { assertNever } from "@opticss/util" ; import { CompoundSelector , ParsedSelector , parseSelector , postcss , postcssSelectorParser as selectorParser } from "opticss" ; import { isAttributeNode , isClassNode , isRootNode , toAttrToken } from "../BlockParser" ; import { Resolution , getResolution , isResolution } from "../BlockSyntax" ; import { Block , BlockClass , Style } from "../BlockTree" ; import { ResolvedConfiguration } from "../configuration" ; import * as errors from "../errors" ; import { QueryKeySelector } from "../query" ; import { SourceFile , SourceRange , sourceRange } from "../SourceLocation" ; import { expandProp } from "../util/propertyParser" ; import { Conflicts , detectConflicts } from "./conflictDetection" ; enum ConflictType { conflict , noConflict , sameValues } const SIBLING_COMBINATORS = new Set ( [ "+" , "~" ] ) ; const HIERARCHICAL_COMBINATORS = new Set ( [ " " , ">" ] ) ; const CONTIGUOUS_COMBINATORS = new Set ( [ "+" , ">" ] ) ; const NONCONTIGUOUS_COMBINATORS = new Set ( [ "~" , " " ] ) ; function updateConflict ( t1 , t2 ) { switch ( t1 ) { case ConflictType . conflict : return ConflictType . conflict ; case ConflictType . noConflict : return t2 ; case ConflictType . sameValues : switch ( t2 ) { case ConflictType . conflict : return ConflictType . conflict ; default : return ConflictType . sameValues ; } default : return assertNever ( t1 ) ; } } interface ResolutionDecls { decl : postcss . Declaration ; resolution : Resolution ; isOverride : boolean ; localDecls : SimpleDecl [ ] ; } interface SimpleDecl { prop : string ; value : string ; } /**
 * `ConflictResolver` is a utility class that crawls a Block, the block it inherits from,
 * and any other explicitly referenced blocks where resolution rules are applied, and
 * resolves property values accordingly.
 */ export class ConflictResolver { readonly config : ResolvedConfiguration ; private _reservedClassNames : Set < string > ; constructor ( config , reservedClassNames ) { this . config = config ; this . _reservedClassNames = reservedClassNames ; } param root param block resolveInheritance ( root , block ) { let blockBase = block . base ; let blockBaseName = block . getReferencedBlockLocalName ( block . base ) ; if ( blockBase && blockBaseName ) { root . walkRules ( ( rule ) => { let handledConflicts = new Conflicts < string > ( ) ; let handledObjects = new Conflicts < Style > ( ) ; let parsedSelectors = block . getParsedSelectors ( rule ) ; parsedSelectors . forEach ( ( sel ) => { let key = sel . key ; let obj = null ; let container ; for ( let node of key . nodes ) { if ( isRootNode ( node ) ) { container = obj = block . rootClass ; } if ( isClassNode ( node ) ) { container = obj = block . getClass ( node . value ) ; } else if ( isAttributeNode ( node ) ) { obj = container ! . getAttributeValue ( toAttrToken ( node ) ) ; } } if ( ! obj ) { return ; } let objectConflicts = handledObjects . getConflictSet ( key . pseudoelement && key . pseudoelement . value ) ; if ( objectConflicts . has ( obj ) ) { return ; } objectConflicts . add ( obj ) ; let base = obj . base ; if ( ! base ) { return ; } let baseSource = base . asSource ( ) ; let conflicts = detectConflicts ( obj , base ) ; let handledConflictSet = handledConflicts . getConflictSet ( key . pseudoelement && key . pseudoelement . value ) ; let conflictingProps = conflicts . getConflictSet ( key . pseudoelement && key . pseudoelement . value ) ; if ( ! conflictingProps || conflictingProps . size === 0 ) { return ; } let ruleProps = new Set < string > ( ) ; rule . walkDecls ( ( decl ) => { ruleProps . add ( decl . prop ) ; } ) ; conflictingProps . forEach ( ( [ thisProp , _ ] ) => { if ( ruleProps . has ( thisProp ) && ! handledConflictSet . has ( thisProp ) ) { handledConflictSet . add ( thisProp ) ; rule . prepend ( postcss . decl ( { prop : thisProp , value : `resolve-inherited("${ blockBaseName }${ baseSource }")` } ) ) ; } } ) ; } ) ; } ) ; } } param root param block resolve ( root , block ) { const resolutions = new Set ( ) ; root . walkDecls ( ( decl ) => { if ( ! isResolution ( decl . value ) ) { return ; } resolutions . add ( { decl , resolution : getResolution ( decl . value ) , isOverride : false , localDecls : [ ] , } ) ; } ) ; resolutions . forEach ( ( res ) => { const { decl , resolution , localDecls } = res ; let propExpansion = expandProp ( decl . prop , decl . value ) ; let foundRes = false ; decl . parent . walkDecls ( ( { prop , value } ) => { if ( value === decl . value ) { foundRes = ( localDecls . length === 0 ) ? false : true ; res . isOverride = ( localDecls . length === 0 ) ; } if ( isResolution ( value ) || ! propExpansion [ prop ] ) { return ; } if ( localDecls . length && foundRes ) { throw new errors . InvalidBlockSyntax ( `Resolving ${ decl . prop } must happen either before or after all other values for ${ decl . prop }.` , this . sourceRange ( block , decl ) ) ; } localDecls . push ( { prop , value } ) ; } ) ; if ( ! localDecls . length ) { throw new errors . InvalidBlockSyntax ( `Cannot resolve ${ decl . prop } without a concrete value.` , this . sourceRange ( block , decl ) ) ; } let other = block . lookup ( resolution . path ) ; if ( ! other ) { throw new errors . InvalidBlockSyntax ( `Cannot find ${ resolution . path }` , this . sourceRange ( block , decl ) ) ; } if ( block . equal ( other && other . block ) ) { throw new errors . InvalidBlockSyntax ( `Cannot resolve conflicts with your own block.` , this . sourceRange ( block , decl ) ) ; } else if ( ! resolution . isInherited && other && other . block . isAncestorOf ( block ) ) { throw new errors . InvalidBlockSyntax ( `Cannot resolve conflicts with ancestors of your own block.` , this . sourceRange ( block , decl ) ) ; } let foundConflict = ConflictType . noConflict ; do { foundConflict = this . resolveConflictWith ( resolution . path , other , res ) ; other = other . base ; } while ( other && foundConflict === ConflictType . noConflict ) ; if ( ! resolution . isInherited && foundConflict === ConflictType . noConflict ) { throw new errors . InvalidBlockSyntax ( `There are no conflicting values for ${ decl . prop } found in any selectors targeting ${ resolution . path }.` , this . sourceRange ( block , decl ) ) ; } decl . remove ( ) ; } ) ; } private resolveConflictWith ( referenceStr , other , resolution ) { const { decl , localDecls , isOverride } = resolution ; const root = other . block . stylesheet ; const curSel = parseSelector ( ( < postcss . Rule > decl . parent ) ) ; if ( root === undefined ) { throw new TypeError ( `Cannot resolve. The block for ${ referenceStr } is missing a stylesheet root` ) ; } const query = new QueryKeySelector ( other ) ; const result = query . execute ( root , other . block ) ; let foundConflict = ConflictType . noConflict ; for ( let cs of curSel ) { let resultSelectors = cs . key . pseudoelement ? result . other [ cs . key . pseudoelement . value ] : result . main ; if ( ! resultSelectors || resultSelectors . length === 0 ) continue ; for ( let s of resultSelectors . reverse ( ) ) { let newSelectors = this . mergeKeySelectors ( other . block . rewriteSelector ( s . parsedSelector , this . config , this . _reservedClassNames ) , cs ) ; if ( newSelectors === null ) { continue ; } let newSelStr = newSelectors . join ( ",\n" ) ; let newRule = postcss . rule ( { selector : newSelStr } ) ; let newRuleContext = reproduceContext ( s . rule , newRule ) ; const remoteDecls = [ ] ; s . rule . walkDecls ( ( overrideDecl ) : true | void => { if ( isResolution ( overrideDecl . value ) ) { return true ; } let propExpansion = expandProp ( overrideDecl . prop , overrideDecl . value ) ; if ( ! propExpansion [ decl . prop ] ) { let localPropExpansion = expandProp ( decl . prop , decl . value ) ; let discovered = false ; for ( let prop of Object . keys ( localPropExpansion ) ) { discovered = discovered || ! ! propExpansion [ prop ] ; } if ( ! discovered ) { return true ; } } remoteDecls . push ( { prop : decl . prop , value : propExpansion [ decl . prop ] } ) ; } ) ; if ( ! remoteDecls . length ) { continue ; } if ( localDecls . length === remoteDecls . length ) { if ( localDecls . reduce ( ( c , d ) => `${ c }${ d . prop }:${ d . value };` , "" ) === remoteDecls . reduce ( ( c , d ) => `${ c }${ d . prop }:${ d . value };` , "" ) ) { foundConflict = updateConflict ( foundConflict , ConflictType . sameValues ) ; continue ; } } foundConflict = updateConflict ( foundConflict , ConflictType . conflict ) ; for ( let { prop , value } of isOverride ? localDecls : remoteDecls ) { newRule . append ( postcss . decl ( { prop , value } ) ) ; } if ( newRule . nodes && newRule . nodes . length > 0 ) { let parent = decl . parent . parent ; if ( parent ) { let rule = decl . parent as postcss . Rule ; parent . insertAfter ( rule , newRuleContext ) ; } } } } return foundConflict ; } param s returns private splitSelector ( s ) { s = s . clone ( ) ; let last = s . removeLast ( ) ; if ( last ) { return [ s , last . combinator , last . selector ] ; } else { return [ undefined , undefined , s ] ; } } param s1 param s2 returns private mergeKeySelectors ( s1 , s2 ) { if ( s1 . length > 2 && s2 . length > 2 ) { throw new errors . InvalidBlockSyntax ( `Cannot resolve selectors with more than 1 combinator at this time [FIXME].` ) ; } let [ context1 , combinator1 , key1 ] = this . splitSelector ( s1 . selector ) ; let [ context2 , combinator2 , key2 ] = this . splitSelector ( s2 . selector ) ; let mergedKey = key1 . clone ( ) . mergeNodes ( key2 ) ; let mergedSelectors = [ ] ; if ( context1 && context2 && combinator1 && combinator2 ) { if ( CONTIGUOUS_COMBINATORS . has ( combinator1 . value ) && combinator1 . value === combinator2 . value ) { mergedSelectors . push ( context1 . clone ( ) . mergeNodes ( context2 ) . append ( combinator1 , mergedKey ) ) ; } else if ( SIBLING_COMBINATORS . has ( combinator1 . value ) && HIERARCHICAL_COMBINATORS . has ( combinator2 . value ) ) { mergedSelectors . push ( context2 . clone ( ) . append ( combinator2 , context1 ) . append ( combinator1 , mergedKey ) ) ; } else if ( HIERARCHICAL_COMBINATORS . has ( combinator1 . value ) && SIBLING_COMBINATORS . has ( combinator2 . value ) ) { mergedSelectors . push ( context1 . clone ( ) . append ( combinator1 , context2 ) . append ( combinator2 , mergedKey ) ) ; } else if ( NONCONTIGUOUS_COMBINATORS . has ( combinator1 . value ) && NONCONTIGUOUS_COMBINATORS . has ( combinator2 . value ) ) { mergedSelectors . push ( context1 . clone ( ) . mergeNodes ( context2 ) . append ( combinator2 , mergedKey ) ) ; mergedSelectors . push ( context1 . clone ( ) . append ( combinator1 , context2 . clone ( ) ) . append ( combinator2 , mergedKey . clone ( ) ) ) ; mergedSelectors . push ( context2 . clone ( ) . append ( combinator1 , context1 . clone ( ) ) . append ( combinator2 , mergedKey . clone ( ) ) ) ; } else if ( NONCONTIGUOUS_COMBINATORS . has ( combinator1 . value ) && CONTIGUOUS_COMBINATORS . has ( combinator2 . value ) && ( ( HIERARCHICAL_COMBINATORS . has ( combinator1 . value ) && HIERARCHICAL_COMBINATORS . has ( combinator2 . value ) ) || ( SIBLING_COMBINATORS . has ( combinator1 . value ) && SIBLING_COMBINATORS . has ( combinator2 . value ) ) ) ) { mergedSelectors . push ( context1 . clone ( ) . mergeNodes ( context2 ) . append ( combinator2 , mergedKey ) ) ; mergedSelectors . push ( context1 . clone ( ) . append ( combinator1 , context2 . clone ( ) ) . append ( combinator2 , mergedKey . clone ( ) ) ) ; } else if ( NONCONTIGUOUS_COMBINATORS . has ( combinator2 . value ) && CONTIGUOUS_COMBINATORS . has ( combinator1 . value ) && ( ( HIERARCHICAL_COMBINATORS . has ( combinator2 . value ) && HIERARCHICAL_COMBINATORS . has ( combinator1 . value ) ) || ( SIBLING_COMBINATORS . has ( combinator2 . value ) && SIBLING_COMBINATORS . has ( combinator1 . value ) ) ) ) { mergedSelectors . push ( context1 . clone ( ) . mergeNodes ( context2 ) . append ( combinator1 , mergedKey ) ) ; mergedSelectors . push ( context2 . clone ( ) . append ( combinator2 , context1 . clone ( ) ) . append ( combinator1 , mergedKey . clone ( ) ) ) ; } else { throw new errors . InvalidBlockSyntax ( `Cannot merge selectors with combinators: '${ combinator1 . value }' and '${ combinator2 . value }' [FIXME?].` ) ; } } else if ( context1 && combinator1 ) { mergedSelectors . push ( context1 . clone ( ) . append ( combinator1 , mergedKey ) ) ; } else if ( context2 && combinator2 ) { mergedSelectors . push ( context2 . clone ( ) . append ( combinator2 , mergedKey ) ) ; } else { mergedSelectors . push ( mergedKey ) ; } return mergedSelectors . map ( sel => new ParsedSelector ( sel , sel . toString ( ) ) ) ; } sourceRange ( block , node ) { let blockPath = this . config . importer . debugIdentifier ( block . identifier , this . config ) ; return sourceRange ( this . config , block . stylesheet , blockPath , node ) ; } } interface InstanceOf < T > { constructor : { new ( ) : T ; } ; } function shallowClone ( node ) { let cloned = new node . constructor ( ) ; for ( let i in node ) { if ( ! node . hasOwnProperty ( i ) ) continue ; let value = node [ i ] ; let type = typeof value ; if ( i === "parent" && type === "object" ) { } else if ( i === "source" ) { cloned [ i ] = value ; } else if ( value instanceof Array ) { } else if ( type === "object" && value !== null ) { } else { cloned [ i ] = value ; } } return cloned ; } function reproduceContext ( hasContext , needsContext ) { if ( ! hasContext . parent || hasContext . parent . type === "root" ) { return needsContext ; } let parent = hasContext . parent ; let newParent = shallowClone ( < InstanceOf < postcss . Container > > < unknown > parent ) ; newParent . append ( needsContext ) ; return reproduceContext ( parent , newParent ) ; }