import '../../../globals' ; import { isCssVariable } from '../../core/properties' ; import { isNullOrUndefined } from '../../../utils/types' ; import * as cssParser from '../../../css' ; import * as parser from '../../../css/parser' ; /**
 * An interface describing the shape of a type on which the selectors may apply.
 * Note, the ui/core/view.View implements Node.
 */ export interface Node { parent ? : Node ; id ? : string ; nodeName ? : string ; cssType ? : string ; cssClasses ? : Set < string > ; cssPseudoClasses ? : Set < string > ; getChildIndex ? ( node ) : number ; getChildAt ? ( index ) : Node ; } export interface Declaration { property : string ; value : string ; } export type ChangeMap < T extends Node > = Map < T , Changes > ; export interface Changes { attributes ? : Set < string > ; pseudoClasses ? : Set < string > ; } const enum Specificity { Inline = 1000 , Id = 100 , Attribute = 10 , Class = 10 , PseudoClass = 10 , Type = 1 , Universal = 0 , Invalid = 0 } const enum Rarity { Invalid = 4 , Id = 3 , Class = 2 , Type = 1 , PseudoClass = 0 , Attribute = 0 , Universal = 0 , Inline = 0 } interface LookupSorter { sortById ( id , sel ) ; sortByClass ( cssClass , sel ) ; sortByType ( cssType , sel ) ; sortAsUniversal ( sel ) ; } namespace Match { /**
     * Depends on attributes or pseudoclasses state;
     */ export const Dynamic = true ; /**
     * Depends only on the tree structure.
     */ export const Static = false ; } function getNodeDirectSibling ( node ) { if ( ! node . parent || ! node . parent . getChildIndex || ! node . parent . getChildAt ) { return null ; } const nodeIndex = node . parent . getChildIndex ( node ) ; if ( nodeIndex === 0 ) { return null ; } return node . parent . getChildAt ( nodeIndex - 1 ) ; } function SelectorProperties ( specificity , rarity , dynamic = false ) { return ( cls ) => { cls . prototype . specificity = specificity ; cls . prototype . rarity = rarity ; cls . prototype . combinator = undefined ; cls . prototype . dynamic = dynamic ; return cls ; } ; } declare type Combinator = '+' | '>' | '~' | ' ' ; @ SelectorProperties ( Specificity . Universal , Rarity . Universal , Match . Static ) export abstract class SelectorCore { public pos : number ; public specificity : number ; public rarity : Rarity ; public combinator : Combinator ; public ruleset : RuleSet ; /**
     * Dynamic selectors depend on attributes and pseudo classes.
     */ public dynamic : boolean ; public abstract match ( node ) ; /**
     * If the selector is static returns if it matches the node.
     * If the selector is dynamic returns if it may match the node, and accumulates any changes that may affect its state.
     */ public abstract accumulateChanges ( node , map ) ; public lookupSort ( sorter , base ? ) { sorter . sortAsUniversal ( base || this ) ; } } export abstract class SimpleSelector extends SelectorCore { public accumulateChanges ( node , map ? ) { if ( ! this . dynamic ) { return this . match ( node ) ; } else if ( this . mayMatch ( node ) ) { this . trackChanges ( node , map ) ; return true ; } return false ; } public mayMatch ( node ) { return this . match ( node ) ; } public trackChanges ( node , map ) { } } function wrap ( text ) { return text ? ` ${ text } ` : '' ; } @ SelectorProperties ( Specificity . Invalid , Rarity . Invalid , Match . Static ) export class InvalidSelector extends SimpleSelector { constructor ( public e ) { super ( ) ; } public toString ( ) { return `<error: ${ this . e }>` ; } public match ( node ) { return false ; } public lookupSort ( sorter , base ? ) { } } @ SelectorProperties ( Specificity . Universal , Rarity . Universal , Match . Static ) export class UniversalSelector extends SimpleSelector { public toString ( ) { return `*${ wrap ( this . combinator ) }` ; } public match ( node ) { return true ; } } @ SelectorProperties ( Specificity . Id , Rarity . Id , Match . Static ) export class IdSelector extends SimpleSelector { constructor ( public id ) { super ( ) ; } public toString ( ) { return `#${ this . id }${ wrap ( this . combinator ) }` ; } public match ( node ) { return node . id === this . id ; } public lookupSort ( sorter , base ? ) { sorter . sortById ( this . id , base || this ) ; } } @ SelectorProperties ( Specificity . Type , Rarity . Type , Match . Static ) export class TypeSelector extends SimpleSelector { constructor ( public cssType ) { super ( ) ; } public toString ( ) { return `${ this . cssType }${ wrap ( this . combinator ) }` ; } public match ( node ) { return node . cssType === this . cssType ; } public lookupSort ( sorter , base ? ) { sorter . sortByType ( this . cssType , base || this ) ; } } @ SelectorProperties ( Specificity . Class , Rarity . Class , Match . Static ) export class ClassSelector extends SimpleSelector { constructor ( public cssClass ) { super ( ) ; } public toString ( ) { return `.${ this . cssClass }${ wrap ( this . combinator ) }` ; } public match ( node ) { return node . cssClasses && node . cssClasses . has ( this . cssClass ) ; } public lookupSort ( sorter , base ? ) { sorter . sortByClass ( this . cssClass , base || this ) ; } } declare type AttributeTest = '=' | '^=' | '$=' | '*=' | '=' | '~=' | '|=' ; @ SelectorProperties ( Specificity . Attribute , Rarity . Attribute , Match . Dynamic ) export class AttributeSelector extends SimpleSelector { constructor ( public attribute , public test ? , public value ? ) { super ( ) ; if ( ! test ) { this . match = ( node ) => ! isNullOrUndefined ( node [ attribute ] ) ; return ; } if ( ! value ) { this . match = ( node ) => false ; } this . match = ( node ) => { const attr = node [ attribute ] + '' ; if ( test === '=' ) { return attr === value ; } if ( test === '^=' ) { return attr . startsWith ( value ) ; } if ( test === '$=' ) { return attr . endsWith ( value ) ; } if ( test === '*=' ) { return attr . indexOf ( value ) !== - 1 ; } if ( test === '~=' ) { const words = attr . split ( ' ' ) ; return words && words . indexOf ( value ) !== - 1 ; } if ( test === '|=' ) { return attr === value || attr . startsWith ( value + '-' ) ; } } ; } public toString ( ) { return `[${ this . attribute }${ wrap ( this . test ) }${ ( this . test && this . value ) || '' }]${ wrap ( this . combinator ) }` ; } public match ( node ) { return false ; } public mayMatch ( node ) { return true ; } public trackChanges ( node , map ) { map . addAttribute ( node , this . attribute ) ; } } @ SelectorProperties ( Specificity . PseudoClass , Rarity . PseudoClass , Match . Dynamic ) export class PseudoClassSelector extends SimpleSelector { constructor ( public cssPseudoClass ) { super ( ) ; } public toString ( ) { return `:${ this . cssPseudoClass }${ wrap ( this . combinator ) }` ; } public match ( node ) { return node . cssPseudoClasses && node . cssPseudoClasses . has ( this . cssPseudoClass ) ; } public mayMatch ( node ) { return true ; } public trackChanges ( node , map ) { map . addPseudoClass ( node , this . cssPseudoClass ) ; } } export class SimpleSelectorSequence extends SimpleSelector { private head : SimpleSelector ; constructor ( public selectors ) { super ( ) ; this . specificity = selectors . reduce ( ( sum , sel ) => sel . specificity + sum , 0 ) ; this . head = this . selectors . reduce ( ( prev , curr ) => ( ! prev || curr . rarity > prev . rarity ? curr : prev ) , null ) ; this . dynamic = selectors . some ( ( sel ) => sel . dynamic ) ; } public toString ( ) { return `${ this . selectors . join ( '' ) }${ wrap ( this . combinator ) }` ; } public match ( node ) { return this . selectors . every ( ( sel ) => sel . match ( node ) ) ; } public mayMatch ( node ) { return this . selectors . every ( ( sel ) => sel . mayMatch ( node ) ) ; } public trackChanges ( node , map ) { this . selectors . forEach ( ( sel ) => sel . trackChanges ( node , map ) ) ; } public lookupSort ( sorter , base ? ) { this . head . lookupSort ( sorter , base || this ) ; } } export class Selector extends SelectorCore { private groups : Selector . ChildGroup [ ] ; private last : SelectorCore ; constructor ( public selectors ) { super ( ) ; const supportedCombinator = [ undefined , ' ' , '>' , '+' ] ; let siblingGroup ; let lastGroup ; const groups = [ ] ; this . specificity = 0 ; this . dynamic = false ; for ( let i = selectors . length - 1 ; i > - 1 ; i -- ) { const sel = selectors [ i ] ; if ( supportedCombinator . indexOf ( sel . combinator ) === - 1 ) { throw new Error ( `Unsupported combinator "${ sel . combinator }".` ) ; } if ( sel . combinator === undefined || sel . combinator === ' ' ) { groups . push ( ( lastGroup = [ ( siblingGroup = [ ] ) ] ) ) ; } if ( sel . combinator === '>' ) { lastGroup . push ( ( siblingGroup = [ ] ) ) ; } this . specificity += sel . specificity ; if ( sel . dynamic ) { this . dynamic = true ; } siblingGroup . push ( sel ) ; } this . groups = groups . map ( ( g ) => new Selector . ChildGroup ( g . map ( ( sg ) => new Selector . SiblingGroup ( sg ) ) ) ) ; this . last = selectors [ selectors . length - 1 ] ; } public toString ( ) { return this . selectors . join ( '' ) ; } public match ( node ) { return this . groups . every ( ( group , i ) => { if ( i === 0 ) { node = group . match ( node ) ; return ! ! node ; } else { let ancestor = node ; while ( ( ancestor = ancestor . parent ) ) { if ( ( node = group . match ( ancestor ) ) ) { return true ; } } return false ; } } ) ; } public lookupSort ( sorter , base ? ) { this . last . lookupSort ( sorter , this ) ; } public accumulateChanges ( node , map ? ) { if ( ! this . dynamic ) { return this . match ( node ) ; } const bounds = [ ] ; const mayMatch = this . groups . every ( ( group , i ) => { if ( i === 0 ) { const nextNode = group . mayMatch ( node ) ; bounds . push ( { left : node , right : node } ) ; node = nextNode ; return ! ! node ; } else { let ancestor = node ; while ( ( ancestor = ancestor . parent ) ) { const nextNode = group . mayMatch ( ancestor ) ; if ( nextNode ) { bounds . push ( { left : ancestor , right : null } ) ; node = nextNode ; return true ; } } return false ; } } ) ; if ( ! mayMatch ) { return false ; } if ( ! map ) { return mayMatch ; } for ( let i = 0 ; i < this . groups . length ; i ++ ) { const group = this . groups [ i ] ; if ( ! group . dynamic ) { continue ; } const bound = bounds [ i ] ; let node = bound . left ; do { if ( group . mayMatch ( node ) ) { group . trackChanges ( node , map ) ; } } while ( node !== bound . right && ( node = node . parent ) ) ; } return mayMatch ; } } export namespace Selector { export class ChildGroup { public dynamic : boolean ; constructor ( private selectors ) { this . dynamic = selectors . some ( ( sel ) => sel . dynamic ) ; } public match ( node ) { return this . selectors . every ( ( sel , i ) => ( node = i === 0 ? node : node . parent ) && sel . match ( node ) ) ? node : null ; } public mayMatch ( node ) { return this . selectors . every ( ( sel , i ) => ( node = i === 0 ? node : node . parent ) && sel . mayMatch ( node ) ) ? node : null ; } public trackChanges ( node , map ) { this . selectors . forEach ( ( sel , i ) => ( node = i === 0 ? node : node . parent ) && sel . trackChanges ( node , map ) ) ; } } export class SiblingGroup { public dynamic : boolean ; constructor ( private selectors ) { this . dynamic = selectors . some ( ( sel ) => sel . dynamic ) ; } public match ( node ) { return this . selectors . every ( ( sel , i ) => ( node = i === 0 ? node : getNodeDirectSibling ( node ) ) && sel . match ( node ) ) ? node : null ; } public mayMatch ( node ) { return this . selectors . every ( ( sel , i ) => ( node = i === 0 ? node : getNodeDirectSibling ( node ) ) && sel . mayMatch ( node ) ) ? node : null ; } public trackChanges ( node , map ) { this . selectors . forEach ( ( sel , i ) => ( node = i === 0 ? node : getNodeDirectSibling ( node ) ) && sel . trackChanges ( node , map ) ) ; } } export interface Bound { left : Node ; right : Node ; } } export class RuleSet { tag : string | number ; scopedTag : string ; constructor ( public selectors , public declarations ) { this . selectors . forEach ( ( sel ) => ( sel . ruleset = this ) ) ; } public toString ( ) { return `${ this . selectors . join ( ', ' ) } {${ this . declarations . map ( ( d , i ) => `${ i === 0 ? ' ' : '' }${ d . property }: ${ d . value }` ) . join ( '; ' ) } }` ; } public lookupSort ( sorter ) { this . selectors . forEach ( ( sel ) => sel . lookupSort ( sorter ) ) ; } } export function fromAstNodes ( astRules ) { return ( < cssParser . Rule [ ] > astRules . filter ( isRule ) ) . map ( ( rule ) => { const declarations = rule . declarations . filter ( isDeclaration ) . map ( createDeclaration ) ; const selectors = rule . selectors . map ( createSelector ) ; return new RuleSet ( selectors , declarations ) ; } ) ; } function createDeclaration ( decl ) { return { property : isCssVariable ( decl . property ) ? decl . property : decl . property . toLowerCase ( ) , value : decl . value } ; } function createSimpleSelectorFromAst ( ast ) { if ( ast . type === '.' ) { return new ClassSelector ( ast . identifier ) ; } if ( ast . type === '' ) { return new TypeSelector ( ast . identifier . replace ( '-' , '' ) . toLowerCase ( ) ) ; } if ( ast . type === '#' ) { return new IdSelector ( ast . identifier ) ; } if ( ast . type === '[]' ) { return new AttributeSelector ( ast . property , ast . test , ast . test && ast . value ) ; } if ( ast . type === ':' ) { return new PseudoClassSelector ( ast . identifier ) ; } if ( ast . type === '*' ) { return new UniversalSelector ( ) ; } } function createSimpleSelectorSequenceFromAst ( ast ) { if ( ast . length === 0 ) { return new InvalidSelector ( new Error ( 'Empty simple selector sequence.' ) ) ; } else if ( ast . length === 1 ) { return createSimpleSelectorFromAst ( ast [ 0 ] ) ; } else { return new SimpleSelectorSequence ( ast . map ( createSimpleSelectorFromAst ) ) ; } } function createSelectorFromAst ( ast ) { if ( ast . length === 0 ) { return new InvalidSelector ( new Error ( 'Empty selector.' ) ) ; } else if ( ast . length === 1 ) { return createSimpleSelectorSequenceFromAst ( ast [ 0 ] [ 0 ] ) ; } else { const simpleSelectorSequences = [ ] ; let simpleSelectorSequence ; let combinator ; for ( let i = 0 ; i < ast . length ; i ++ ) { simpleSelectorSequence = createSimpleSelectorSequenceFromAst ( < parser . SimpleSelectorSequence > ast [ i ] [ 0 ] ) ; combinator = < parser . Combinator > ast [ i ] [ 1 ] ; if ( combinator ) { simpleSelectorSequence . combinator = combinator ; } simpleSelectorSequences . push ( simpleSelectorSequence ) ; } return new Selector ( simpleSelectorSequences ) ; } } export function createSelector ( sel ) { try { const parsedSelector = parser . parseSelector ( sel ) ; if ( ! parsedSelector ) { return new InvalidSelector ( new Error ( 'Empty selector' ) ) ; } return createSelectorFromAst ( parsedSelector . value ) ; } catch ( e ) { return new InvalidSelector ( e ) ; } } function isRule ( node ) { return node . type === 'rule' ; } function isDeclaration ( node ) { return node . type === 'declaration' ; } interface SelectorMap { [ key ] : SelectorCore [ ] ; } export class SelectorsMap < T extends Node > implements LookupSorter { private id : SelectorMap = { } ; private class : SelectorMap = { } ; private type : SelectorMap = { } ; private universal : SelectorCore [ ] = [ ] ; private position = 0 ; constructor ( rulesets ) { rulesets . forEach ( ( rule ) => rule . lookupSort ( this ) ) ; } query ( node ) { const selectorsMatch = new SelectorsMatch < T > ( ) ; const { cssClasses , id , cssType } = node ; const selectorClasses = [ this . universal , this . id [ id ] , this . type [ cssType ] ] ; if ( cssClasses && cssClasses . size ) { cssClasses . forEach ( ( c ) => selectorClasses . push ( this . class [ c ] ) ) ; } const selectors = selectorClasses . reduce ( ( cur , next ) => cur . concat ( next || [ ] ) , [ ] ) ; selectorsMatch . selectors = selectors . filter ( ( sel ) => sel . accumulateChanges ( node , selectorsMatch ) ) . sort ( ( a , b ) => a . specificity - b . specificity || a . pos - b . pos ) ; return selectorsMatch ; } sortById ( id , sel ) { this . addToMap ( this . id , id , sel ) ; } sortByClass ( cssClass , sel ) { this . addToMap ( this . class , cssClass , sel ) ; } sortByType ( cssType , sel ) { this . addToMap ( this . type , cssType , sel ) ; } sortAsUniversal ( sel ) { this . universal . push ( this . makeDocSelector ( sel ) ) ; } private addToMap ( map , head , sel ) { if ( ! map [ head ] ) { map [ head ] = [ ] ; } map [ head ] . push ( this . makeDocSelector ( sel ) ) ; } private makeDocSelector ( sel ) { sel . pos = this . position ++ ; return sel ; } } interface ChangeAccumulator { addAttribute ( node , attribute ) : void ; addPseudoClass ( node , pseudoClass ) : void ; } export class SelectorsMatch < T extends Node > implements ChangeAccumulator { public changeMap : ChangeMap < T > = new Map < T , Changes > ( ) ; public selectors ; public addAttribute ( node , attribute ) { const deps = this . properties ( node ) ; if ( ! deps . attributes ) { deps . attributes = new Set ( ) ; } deps . attributes . add ( attribute ) ; } public addPseudoClass ( node , pseudoClass ) { const deps = this . properties ( node ) ; if ( ! deps . pseudoClasses ) { deps . pseudoClasses = new Set ( ) ; } deps . pseudoClasses . add ( pseudoClass ) ; } public properties ( node ) { let set = this . changeMap . get ( node ) ; if ( ! set ) { this . changeMap . set ( node , ( set = { } ) ) ; } return set ; } } export const CSSHelper = { createSelector , SelectorCore , SimpleSelector , InvalidSelector , UniversalSelector , TypeSelector , ClassSelector , AttributeSelector , PseudoClassSelector , SimpleSelectorSequence , Selector , RuleSet , SelectorsMap , fromAstNodes , SelectorsMatch , } ;