import * as util from "util" ; import * as cssWhat from "css-what" ; import * as ld from "lodash" ; import { DomPath } from "./dom" ; import { InternalError } from "./error" ; import { BuildId } from "./handle" ; import { AdaptElement , AdaptElementOrNull , AnyProps , AnyState , BuiltinProps , childrenToArray , Component , createElement , isElement , isMountedElement , SFC , } from "./jsx" ; /**
 * An array of  StyleRule s
 *  public export type StyleList = StyleRule [ ] ; /**
 * Informational object provided to the callback of a
 *  rule .
 *  public export interface StyleBuildInfo extends BuildId { origBuild : SFC ; origElement : any ; } internal export const $matchInfoReg = Symbol . for ( "$matchInfoReg" ) ; internal export interface StyleBuildInfoInternal extends StyleBuildInfo { [ $matchInfoReg ] : MatchInfoReg ; } function isStyleBuildInfoInternal ( val ) { return ( val as any ) [ $matchInfoReg ] != null ; } export type BuildOverride < P = AnyProps > = ( props : P & BuiltinProps , info : StyleBuildInfo ) => AdaptElementOrNull ; export interface StyleRule { selector : string ; sfc : BuildOverride ; match ( path ) : boolean ; } export interface RawStyle { selector : string ; build : BuildOverride ; } type SelFrag = cssWhat . Selector ; type ParsedSelectorBlock = SelFrag [ ] ; type ParsedSelector = ParsedSelectorBlock [ ] ; interface Matched { newPath : DomPath ; matched : boolean ; } interface MatchConfigType { [ type ] : ( frag : SelFrag , path : DomPath ) => Matched ; } const matchConfig = { attribute : matchAttribute , child : matchChild , descendant : ( ) => { throw new InternalError ( "should not get here" ) ; } , pseudo : matchPseudo , tag : matchTag } ; function last < T > ( arr ) { if ( arr . length <= 0 ) { return { prefix : [ ] , elem : null } ; } const lastElem = arr [ arr . length - 1 ] ; return { prefix : arr . slice ( 0 , - 1 ) , elem : lastElem } ; } function getPropValue ( elem , prop , ignoreCase ) { let val ; if ( ignoreCase ) { prop = prop . toLowerCase ( ) ; for ( const key of Object . keys ( elem . props ) ) { if ( key . toLowerCase ( ) === prop ) val = elem . props [ key ] ; } } else { val = elem . props [ prop ] ; } return typeof val === "string" ? val : undefined ; } function fragToString ( frag ) { return util . inspect ( frag ) ; } const matchPseudoConfig = { root : matchRoot , not : matchNot , } ; function matchPseudo ( frag , path ) { if ( frag . type !== "pseudo" ) throw new InternalError ( util . inspect ( frag ) ) ; const matcher = matchPseudoConfig [ frag . name ] ; if ( matcher == null ) throw new Error ( `Unsupported CSS pseudo-class :${ frag . name }` ) ; return matcher ( frag , path ) ; } function matchNot ( frag , path ) { if ( frag . type !== "pseudo" ) throw new InternalError ( util . inspect ( frag ) ) ; if ( frag . data == null || frag . data . length === 0 || frag . data [ 0 ] . length === 0 ) { throw new Error ( `CSS ":not" requires at least one selector argument in parentheses` ) ; } if ( typeof frag . data === "string" ) { throw new InternalError ( `CSS ":not" - unexpected string data` ) ; } return { newPath : path , matched : ! matchWithSelector ( frag . data , path ) } ; } function matchRoot ( frag , path ) { return { newPath : path , matched : path . length === 1 } ; } function matchAttribute ( frag , path ) { if ( frag . type !== "attribute" ) throw new InternalError ( util . inspect ( frag ) ) ; const { elem } = last ( path ) ; if ( elem == null ) throw new InternalError ( "null element" ) ; const value = getPropValue ( elem , frag . name , frag . ignoreCase ) ; if ( value === undefined ) return { newPath : path , matched : false } ; let matched ; switch ( frag . action ) { case "exists" : matched = true ; break ; case "equals" : matched = value === frag . value ; break ; case "start" : matched = value . startsWith ( frag . value ) ; break ; case "any" : matched = value . includes ( frag . value ) ; break ; case "end" : matched = value . endsWith ( frag . value ) ; break ; case "element" : matched = value . split ( /\s+/ ) . indexOf ( frag . value ) !== - 1 ; break ; default : throw new Error ( `CSS attribute selector action '${ frag . action }' not supported` ) ; } return { newPath : path , matched } ; } function matchTag ( frag , path ) { if ( frag . type !== "tag" ) throw new InternalError ( util . inspect ( frag ) ) ; const { elem } = last ( path ) ; if ( elem == null ) throw new InternalError ( "null element" ) ; return { newPath : path , matched : uniqueName ( elem . componentType ) === frag . name } ; } function matchChild ( frag , path ) { if ( frag . type !== "child" ) throw new InternalError ( util . inspect ( frag ) ) ; if ( path . length < 1 ) return { newPath : path , matched : false } ; return { newPath : path . slice ( 0 , - 1 ) , matched : true } ; } function matchDescendant ( selector , path ) { if ( selector . length <= 0 ) { throw new InternalError ( `validated but malformed CSS ${ util . inspect ( selector ) }` ) ; } for ( let i = 1 ; i < path . length ; i ++ ) { const tryPath = path . slice ( 0 , - i ) ; if ( matchWithSelector ( [ selector ] , tryPath ) ) { return true ; } } return false ; } function matchFrag ( selFrag , path ) { const matcher = matchConfig [ selFrag . type ] ; if ( matcher === undefined ) { throw new Error ( "Unsupported selector fragment: " + fragToString ( selFrag ) ) ; } return matcher ( selFrag , path ) ; } function matchWithSelector ( selector , path ) { for ( const block of selector ) { if ( matchWithBlock ( block , path ) ) { return true ; } } return false ; } function matchWithBlock ( selBlock , path ) { const { prefix , elem : selFrag } = last ( selBlock ) ; if ( selFrag == null ) { return true ; } if ( selFrag . type === "descendant" ) { return matchDescendant ( prefix , path ) ; } else { const { newPath , matched } = matchFrag ( selFrag , path ) ; if ( ! matched ) return false ; if ( newPath . length === 0 ) { return false ; } return matchWithBlock ( prefix , newPath ) ; } } function validateSelector ( _selector ) { return ; } function buildStyle ( rawStyle ) { const selector = cssWhat . parse ( rawStyle . selector , { xmlMode : true } ) ; validateSelector ( selector ) ; return { selector : rawStyle . selector , sfc : rawStyle . build , match : ( path ) => matchWithSelector ( selector , path ) } ; } function makeStyle ( selector , build ) { return { selector , build } ; } function parseStyles ( styles ) { const ret = [ ] ; for ( const style of styles ) { ret . push ( buildStyle ( style ) ) ; } return ret ; } export type AbstractComponentCtor < P extends object = AnyProps , S extends object = AnyState , T extends Component < P , S > = Component < P , S > > = Function & { prototype : T ; } ; export type AdaptComponentConstructor = new ( props ) => Component < AnyProps , AnyState > ; export interface StyleProps { children : ( AbstractComponentCtor | SFC | string | AdaptComponentConstructor | Rule ) [ ] ; } export class Rule < P = AnyProps > { constructor ( readonly override ) { } } export function rule < P = AnyProps > ( override ? ) { if ( override === undefined ) { override = ( _ , i ) => i . origElement ; } return new Rule < P > ( override ) ; } function isRule ( x ) { return ( typeof x === "object" ) && ( x instanceof Rule ) ; } internal export interface MatchInfo { matched ? : Set < StyleRule > ; neverMatch ? : true ; } export type MatchInfoReg = Map < AdaptElement , MatchInfo > ; export function createMatchInfoReg ( ) { return new Map < AdaptElement , MatchInfo > ( ) ; } function getCssMatched ( reg , el ) { let mi = reg . get ( el ) ; if ( mi === undefined ) { mi = { } ; reg . set ( el , mi ) ; } return mi ; } export function ruleHasMatched ( reg , el , r ) { const m = getCssMatched ( reg , el ) ; return ( m . matched && m . matched . has ( r ) ) === true ; } export function ruleMatches ( reg , el , r ) { const m = getCssMatched ( reg , el ) ; if ( ! m . matched ) m . matched = new Set < StyleRule > ( ) ; m . matched . add ( r ) ; } export function neverMatch ( reg , el ) { const m = getCssMatched ( reg , el ) ; m . neverMatch = true ; } export function canMatch ( reg , el ) { const m = getCssMatched ( reg , el ) ; return m . neverMatch !== true ; } export function copyRuleMatches ( reg , fromEl , toEl ) { const from = getCssMatched ( reg , fromEl ) ; const to = getCssMatched ( reg , toEl ) ; if ( from . neverMatch ) { to . neverMatch = true ; } else if ( from . matched ) { if ( ! to . matched ) to . matched = new Set < StyleRule > ( ) ; for ( const r of from . matched ) { to . matched . add ( r ) ; } } } param info param elem returns remarks example public export function ruleNoRematch ( info , elem ) { if ( isMountedElement ( elem ) ) { throw new Error ( `elem has already been mounted. elem must be a newly created element` ) ; } if ( ! isStyleBuildInfoInternal ( info ) ) { throw new Error ( `Unable to find $matchInfoReg symbol on StyleBuildInfo object` ) ; } copyRuleMatches ( info [ $matchInfoReg ] , info . origElement , elem ) ; return elem ; } function isStylesComponent ( componentType ) { return componentType === Style ; } const objToName = new WeakMap < object , string > ( ) ; const uniqueNamePrefix = "UniqueName" ; let nextUniqueNameIndex = 0 ; function hasName ( o ) { if ( Object . hasOwnProperty . apply ( o , [ "name" ] ) ) { return ld . isString ( o . name ) ; } return false ; } function uniqueName ( o ) { let ret = objToName . get ( o ) ; if ( ret === undefined ) { const objName = hasName ( o ) ? o . name : "" ; ret = uniqueNamePrefix + nextUniqueNameIndex + objName ; objToName . set ( o , ret ) ; nextUniqueNameIndex ++ ; } return ret ; } export function buildStyles ( styleElem ) { if ( styleElem == null ) { return [ ] ; } const stylesConstructor = styleElem . componentType ; if ( ! isStylesComponent ( stylesConstructor ) ) { throw new Error ( "Invalid Styles element: " + util . inspect ( styleElem ) ) ; } let curSelector = "" ; const rawStyles = [ ] ; for ( const child of childrenToArray ( styleElem . props . children ) ) { if ( typeof child === "function" ) { curSelector = curSelector + uniqueName ( child ) ; } else if ( typeof child === "string" ) { curSelector += child ; } else if ( isRule ( child ) ) { rawStyles . push ( makeStyle ( curSelector . trim ( ) , child . override ) ) ; curSelector = "" ; } else { throw new Error ( `Unsupported child type in Styles: "${ typeof child }" (value: ${ util . inspect ( child ) })` ) ; } } if ( curSelector !== "" ) { throw new Error ( "Missing rule in final style" ) ; } return parseStyles ( rawStyles ) ; } function findInDomImpl ( styles , path ) { const elem = ld . last ( path ) ; if ( elem == null ) return [ ] ; const matches = [ ] ; for ( const style of styles ) { if ( style . match ( path ) ) { matches . push ( path ) ; break ; } } const children = childrenToArray ( elem . props . children ) ; for ( const child of children ) { if ( isElement ( child ) ) { matches . push ( ... findInDomImpl ( styles , [ ... path , child ] ) ) ; } } return matches ; } export function findElementsInDom ( stylesIn , dom ) { return ld . compact ( findPathsInDom ( stylesIn , dom ) . map ( ( path ) => ld . last ( path ) ) ) ; } export function findPathsInDom ( stylesIn , dom ) { if ( stylesIn == null ) return [ ] ; const styles = isElement ( stylesIn ) ? buildStyles ( stylesIn ) : stylesIn ; if ( dom === null ) return [ ] ; return findInDomImpl ( styles , [ dom ] ) ; } export class Style extends Component < StyleProps > { build ( ) { return null ; } } param styles returns public export function concatStyles ( ... styles ) { const rules = [ ] ; for ( const styleElem of styles ) { if ( ! isStylesComponent ( styleElem . componentType ) ) { throw new Error ( "Invalid Styles element: " + util . inspect ( styleElem ) ) ; } const kids = styleElem . props . children ; if ( kids == null ) continue ; if ( ! Array . isArray ( kids ) ) { throw new Error ( `Invalid type for children of a Style ` + `element: ${ typeof kids }` ) ; } rules . push ( ... styleElem . props . children ) ; } return createElement ( Style , { } , rules ) ; }