@ -1,6 +1,7 @@
import { walk } from 'zimmerframe' ;
import { get _possible _values } from './utils.js' ;
import { regex _ends _with _whitespace , regex _starts _with _whitespace } from '../../patterns.js' ;
import { error } from '../../../errors.js' ;
/ * *
* @ typedef { {
@ -8,254 +9,379 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../
* element : import ( '#compiler' ) . RegularElement | import ( '#compiler' ) . SvelteElement ;
* } } State
* /
/** @typedef { typeof NodeExist[keyof typeof NodeExist] } NodeExistsValue */
/** @typedef { NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS } NodeExistsValue */
const NO _MATCH = 'NO_MATCH' ;
const POSSIBLE _MATCH = 'POSSIBLE_MATCH' ;
const UNKNOWN _SELECTOR = 'UNKNOWN_SELECTOR' ;
const NodeExist = /** @type {const} */ ( {
Probably : 0 ,
Definitely : 1
} ) ;
const NODE _PROBABLY _EXISTS = 0 ;
const NODE _DEFINITELY _EXISTS = 1 ;
const whitelist _attribute _selector = new Map ( [
[ 'details' , [ 'open' ] ] ,
[ 'dialog' , [ 'open' ] ]
] ) ;
/** @type {import('#compiler').Css.Combinator} */
const descendant _combinator = {
type : 'Combinator' ,
name : ' ' ,
start : - 1 ,
end : - 1
} ;
/** @type {import('#compiler').Css.RelativeSelector} */
const nesting _selector = {
type : 'RelativeSelector' ,
start : - 1 ,
end : - 1 ,
combinator : null ,
selectors : [
{
type : 'NestingSelector' ,
name : '&' ,
start : - 1 ,
end : - 1
}
] ,
metadata : {
is _global : false ,
is _host : false ,
is _root : false ,
scoped : false
}
} ;
/ * *
*
* @ param { import ( '#compiler' ) . Css . StyleSheet } stylesheet
* @ param { import ( '#compiler' ) . RegularElement | import ( '#compiler' ) . SvelteElement } element
* /
export function prune ( stylesheet , element ) {
/** @type {State} */
const state = { stylesheet , element } ;
walk ( stylesheet , state , visitors ) ;
walk ( stylesheet , { stylesheet , element } , visitors ) ;
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = {
ComplexSelector ( node , context ) {
context . next ( ) ;
const selectors = truncate ( node ) ;
const inner = selectors [ selectors . length - 1 ] ;
const i = node . children . findLastIndex ( ( child ) => {
return ! child . metadata . is _global && ! child . metadata . is _host && ! child . metadata . is _root ;
} ) ;
if ( node . metadata . rule ? . metadata . parent _rule ) {
const has _explicit _nesting _selector = selectors . some ( ( selector ) =>
selector . selectors . some ( ( s ) => s . type === 'NestingSelector' )
) ;
const relative _selectors = node . children . slice ( 0 , i + 1 ) ;
if ( ! has _explicit _nesting _selector ) {
selectors [ 0 ] = {
... selectors [ 0 ] ,
combinator : descendant _combinator
} ;
if ( apply _selector ( relative _selectors , context . state . element , context . state . stylesheet ) ) {
selectors . unshift ( nesting _selector ) ;
}
}
if (
apply _selector (
selectors ,
/** @type {import('#compiler').Css.Rule} */ ( node . metadata . rule ) ,
context . state . element ,
context . state . stylesheet
)
) {
mark ( inner , context . state . element ) ;
node . metadata . used = true ;
}
} ,
RelativeSelector ( node , context ) {
// for now, don't visit children (i.e. inside `:foo(...)`)
// this will likely change when we implement `:is(...)` etc
// note: we don't call context.next() here, we only recurse into
// selectors that don't belong to rules (i.e. inside `:is(...)` etc )
// when we encounter them below
}
} ;
/ * *
* Discard trailing ` :global(...) ` selectors , these are unused for scoping purposes
* @ param { import ( '#compiler' ) . Css . ComplexSelector } node
* /
function truncate ( node ) {
const i = node . children . findLastIndex ( ( { metadata } ) => {
return ! metadata . is _global && ! metadata . is _host && ! metadata . is _root ;
} ) ;
return node . children . slice ( 0 , i + 1 ) ;
}
/ * *
* @ param { import ( '#compiler' ) . Css . RelativeSelector [ ] } relative _selectors
* @ param { import ( '#compiler' ) . RegularElement | import ( '#compiler' ) . SvelteElement | null } element
* @ param { import ( '#compiler' ) . Css . Rule } rule
* @ param { import ( '#compiler' ) . RegularElement | import ( '#compiler' ) . SvelteElement } element
* @ param { import ( '#compiler' ) . Css . StyleSheet } stylesheet
* @ returns { boolean }
* /
function apply _selector ( relative _selectors , element , stylesheet ) {
if ( ! element ) {
return relative _selectors . every ( ( { metadata } ) => metadata . is _global || metadata . is _host ) ;
}
function apply _selector ( relative _selectors , rule , element , stylesheet ) {
const parent _selectors = relative _selectors . slice ( ) ;
const relative _selector = parent _selectors . pop ( ) ;
const relative _selector = relative _selectors . pop ( ) ;
if ( ! relative _selector ) return false ;
const applies = relative _selector _might _apply _to _node ( relative _selector , element ) ;
const possible _match = relative _selector _might _apply _to _node (
relative _selector ,
rule ,
element ,
stylesheet
) ;
if ( applies === NO _MATCH ) {
if ( ! possible _match ) {
return false ;
}
/ * *
* Mark both the compound selector and the node it selects as encapsulated ,
* for transformation in a later step
* @ param { import ( '#compiler' ) . Css . RelativeSelector } relative _selector
* @ param { import ( '#compiler' ) . RegularElement | import ( '#compiler' ) . SvelteElement } element
* /
function mark ( relative _selector , element ) {
relative _selector . metadata . scoped = true ;
element . metadata . scoped = true ;
return true ;
}
if ( relative _selector . combinator ) {
const name = relative _selector . combinator . name ;
if ( applies === UNKNOWN _SELECTOR ) {
return mark ( relative _selector , element ) ;
}
switch ( name ) {
case ' ' :
case '>' : {
let parent = /** @type {import('#compiler').TemplateNode | null} */ ( element . parent ) ;
if ( relative _selector . combinator ) {
if (
relative _selector . combinator . type === 'Combinator' &&
relative _selector . combinator . name === ' '
) {
for ( const ancestor _selector of relative _selectors ) {
if ( ancestor _selector . metadata . is _global ) {
continue ;
}
let parent _matched = false ;
let crossed _component _boundary = false ;
if ( ancestor _selector . metadata . is _host ) {
return mark ( relative _selector , element ) ;
}
while ( parent ) {
if ( parent . type === 'Component' || parent . type === 'SvelteComponent' ) {
crossed _component _boundary = true ;
}
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */
let parent = element ;
let matched = false ;
while ( ( parent = get _element _parent ( parent ) ) ) {
if ( relative _selector _might _apply _to _node ( ancestor _selector , parent ) !== NO _MATCH ) {
mark ( ancestor _selector , parent ) ;
matched = true ;
if ( parent . type === 'RegularElement' || parent . type === 'SvelteElement' ) {
if ( apply _selector ( parent _selectors , rule , parent , stylesheet ) ) {
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
if ( name === ' ' || crossed _component _boundary ) {
mark ( parent _selectors [ parent _selectors . length - 1 ] , parent ) ;
}
parent _matched = true ;
}
if ( name === '>' ) return parent _matched ;
}
}
if ( matched ) {
return mark ( relative _selector , element ) ;
parent = /** @type {import('#compiler').TemplateNode | null} */ ( parent . parent ) ;
}
}
if ( relative _selectors . every ( ( relative _selector ) => relative _selector . metadata . is _global ) ) {
return mark ( relative _selector , element ) ;
return parent _matched || parent _selectors . every ( ( selector ) => is _global ( selector , rule ) ) ;
}
return false ;
}
case '+' :
case '~' : {
const siblings = get _possible _element _siblings ( element , name === '+' ) ;
if ( relative _selector . combinator . name === '>' ) {
const has _global _parent = relative _selectors . every (
( relative _selector ) => relative _selector . metadata . is _global
) ;
let sibling _matched = false ;
if (
has _global _parent ||
apply _selector ( relative _selectors , get _element _parent ( element ) , stylesheet )
) {
return mark ( relative _selector , element ) ;
for ( const possible _sibling of siblings . keys ( ) ) {
if ( apply _selector ( parent _selectors , rule , possible _sibling , stylesheet ) ) {
mark ( relative _selector , element ) ;
sibling _matched = true ;
}
}
return (
sibling _matched ||
( get _element _parent ( element ) === null &&
parent _selectors . every ( ( selector ) => is _global ( selector , rule ) ) )
) ;
}
return false ;
default :
// TODO other combinators
return true ;
}
}
if ( relative _selector . combinator . name === '+' || relative _selector . combinator . name === '~' ) {
const siblings = get _possible _element _siblings (
element ,
relative _selector . combinator . name === '+'
) ;
// if this is the left-most non-global selector, mark it — we want
// `x y z {...}` to become `x.blah y z.blah {...}`
const parent = parent _selectors [ parent _selectors . length - 1 ] ;
if ( ! parent || is _global ( parent , rule ) ) {
mark ( relative _selector , element ) ;
}
let has _match = false ;
// NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the
// css-tree limitation that does not parse the inner selector of :global
// so unless we are sure there will be no sibling to match, we will consider it as matched
const has _global = relative _selectors . some (
( relative _selector ) => relative _selector . metadata . is _global
) ;
return true ;
}
if ( has _global ) {
if ( siblings . size === 0 && get _element _parent ( element ) !== null ) {
return false ;
}
return mark ( relative _selector , element ) ;
}
/ * *
* Mark both the compound selector and the node it selects as encapsulated ,
* for transformation in a later step
* @ param { import ( '#compiler' ) . Css . RelativeSelector } relative _selector
* @ param { import ( '#compiler' ) . RegularElement | import ( '#compiler' ) . SvelteElement } element
* /
function mark ( relative _selector , element ) {
relative _selector . metadata . scoped = true ;
element . metadata . scoped = true ;
}
for ( const possible _sibling of siblings . keys ( ) ) {
if ( apply _selector ( relative _selectors . slice ( ) , possible _sibling , stylesheet ) ) {
mark ( relative _selector , element ) ;
has _match = true ;
}
/ * *
* Returns ` true ` if the relative selector is global , meaning
* it ' s a ` :global(...) ` or ` :host ` or ` :root ` selector , or
* is an ` :is(...) ` or ` :where(...) ` selector that contains
* a global selector
* @ param { import ( '#compiler' ) . Css . RelativeSelector } selector
* @ param { import ( '#compiler' ) . Css . Rule } rule
* /
function is _global ( selector , rule ) {
if ( selector . metadata . is _global || selector . metadata . is _host || selector . metadata . is _root ) {
return true ;
}
for ( const s of selector . selectors ) {
/** @type {import('#compiler').Css.SelectorList | null} */
let selector _list = null ;
let owner = rule ;
if ( s . type === 'PseudoClassSelector' ) {
if ( ( s . name === 'is' || s . name === 'where' ) && s . args ) {
selector _list = s . args ;
}
}
return has _match ;
if ( s . type === 'NestingSelector' ) {
owner = /** @type {import('#compiler').Css.Rule} */ ( rule . metadata . parent _rule ) ;
selector _list = owner . prelude ;
}
// TODO other combinators
return mark ( relative _selector , element ) ;
const has _global _selectors = selector _list ? . children . some ( ( complex _selector ) => {
return complex _selector . children . every ( ( relative _selector ) =>
is _global ( relative _selector , owner )
) ;
} ) ;
if ( ! has _global _selectors ) {
return false ;
}
}
return mark ( relative _selector , element ) ;
return true ;
}
const regex _backslash _and _following _character = /\\(.)/g ;
/ * *
* Ensure that ` element ` satisfies each simple selector in ` relative_selector `
*
* @ param { import ( '#compiler' ) . Css . RelativeSelector } relative _selector
* @ param { import ( '#compiler' ) . RegularElement | import ( '#compiler' ) . SvelteElement } node
* @ returns { NO _MATCH | POSSIBLE _MATCH | UNKNOWN _SELECTOR }
* @ param { import ( '#compiler' ) . Css . Rule } rule
* @ param { import ( '#compiler' ) . RegularElement | import ( '#compiler' ) . SvelteElement } element
* @ param { import ( '#compiler' ) . Css . StyleSheet } stylesheet
* @ returns { boolean }
* /
function relative _selector _might _apply _to _node ( relative _selector , node ) {
if ( relative _selector . metadata . is _host || relative _selector . metadata . is _root ) return NO _MATCH ;
let i = relative _selector . selectors . length ;
while ( i -- ) {
const selector = relative _selector . selectors [ i ] ;
function relative _selector _might _apply _to _node ( relative _selector , rule , element , stylesheet ) {
for ( const selector of relative _selector . selectors ) {
if ( selector . type === 'Percentage' || selector . type === 'Nth' ) continue ;
const name = selector . name . replace ( regex _backslash _and _following _character , '$1' ) ;
if ( selector . type === 'PseudoClassSelector' && ( name === 'host' || name === 'root' ) ) {
return NO _MATCH ;
}
if (
relative _selector . selectors . length === 1 &&
selector . type === 'PseudoClassSelector' &&
name === 'global'
) {
return NO _MATCH ;
}
switch ( selector . type ) {
case 'PseudoClassSelector' : {
if ( name === 'host' || name === 'root' ) {
return false ;
}
if ( selector . type === 'PseudoClassSelector' || selector . type === 'PseudoElementSelector' ) {
continue ;
}
if ( name === 'global' && relative _selector . selectors . length === 1 ) {
const args = /** @type {import('#compiler').Css.SelectorList} */ ( selector . args ) ;
const complex _selector = args . children [ 0 ] ;
return apply _selector ( complex _selector . children , rule , element , stylesheet ) ;
}
if ( selector . type === 'AttributeSelector' ) {
const whitelisted = whitelist _attribute _selector . get ( node . name . toLowerCase ( ) ) ;
if (
! whitelisted ? . includes ( selector . name . toLowerCase ( ) ) &&
! attribute _matches (
node ,
selector . name ,
selector . value && unquote ( selector . value ) ,
selector . matcher ,
selector . flags ? . includes ( 'i' ) ? ? false
)
) {
return NO _MATCH ;
if ( ( name === 'is' || name === 'where' ) && selector . args ) {
let matched = false ;
for ( const complex _selector of selector . args . children ) {
if ( apply _selector ( truncate ( complex _selector ) , rule , element , stylesheet ) ) {
complex _selector . metadata . used = true ;
matched = true ;
}
}
if ( ! matched ) {
return false ;
}
}
break ;
}
case 'PseudoElementSelector' : {
break ;
}
} else {
if ( selector . type === 'ClassSelector' ) {
case 'AttributeSelector' : {
const whitelisted = whitelist _attribute _selector . get ( element . name . toLowerCase ( ) ) ;
if (
! whitelisted ? . includes ( selector . name . toLowerCase ( ) ) &&
! attribute _matches (
element ,
selector . name ,
selector . value && unquote ( selector . value ) ,
selector . matcher ,
selector . flags ? . includes ( 'i' ) ? ? false
)
) {
return false ;
}
break ;
}
case 'ClassSelector' : {
if (
! attribute _matches ( node , 'class' , name , '~=' , false ) &&
! node . attributes . some (
! attribute _matches ( element , 'class' , name , '~=' , false ) &&
! element . attributes . some (
( attribute ) => attribute . type === 'ClassDirective' && attribute . name === name
)
) {
return NO _MATCH ;
return false ;
}
} else if ( selector . type === 'IdSelector' ) {
if ( ! attribute _matches ( node , 'id' , name , '=' , false ) ) return NO _MATCH ;
} else if ( selector . type === 'TypeSelector' ) {
break ;
}
case 'IdSelector' : {
if ( ! attribute _matches ( element , 'id' , name , '=' , false ) ) {
return false ;
}
break ;
}
case 'TypeSelector' : {
if (
node . name . toLowerCase ( ) !== name . toLowerCase ( ) &&
element . name . toLowerCase ( ) !== name . toLowerCase ( ) &&
name !== '*' &&
node . type !== 'SvelteElement'
element . type !== 'SvelteElement'
) {
return NO _MATCH ;
return false ;
}
break ;
}
case 'NestingSelector' : {
let matched = false ;
const parent = /** @type {import('#compiler').Css.Rule} */ ( rule . metadata . parent _rule ) ;
for ( const complex _selector of parent . prelude . children ) {
if ( apply _selector ( truncate ( complex _selector ) , parent , element , stylesheet ) ) {
complex _selector . metadata . used = true ;
matched = true ;
}
}
} else {
return UNKNOWN _SELECTOR ;
if ( ! matched ) {
return false ;
}
break ;
}
}
}
return POSSIBLE _MATCH ;
// possible match
return true ;
}
/ * *
@ -481,7 +607,7 @@ function get_possible_element_siblings(node, adjacent_only) {
( attr ) => attr . type === 'Attribute' && attr . name . toLowerCase ( ) === 'slot'
)
) {
result . set ( prev , N odeExist. Definitely ) ;
result . set ( prev , N ODE_DEFINITELY _EXISTS ) ;
}
if ( adjacent _only ) {
break ;
@ -600,7 +726,7 @@ function get_possible_last_child(relative_selector, adjacent_only) {
function has _definite _elements ( result ) {
if ( result . size === 0 ) return false ;
for ( const exist of result . values ( ) ) {
if ( exist === N odeExist. Definitely ) {
if ( exist === N ODE_DEFINITELY _EXISTS ) {
return true ;
}
}
@ -632,7 +758,7 @@ function higher_existence(exist1, exist2) {
/** @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result */
function mark _as _probably ( result ) {
for ( const key of result . keys ( ) ) {
result . set ( key , N odeExist. Probably ) ;
result . set ( key , N ODE_PROBABLY _EXISTS ) ;
}
}
@ -646,7 +772,7 @@ function loop_child(children, adjacent_only) {
for ( let i = children . length - 1 ; i >= 0 ; i -- ) {
const child = children [ i ] ;
if ( child . type === 'RegularElement' ) {
result . set ( child , N odeExist. Definitely ) ;
result . set ( child , N ODE_DEFINITELY _EXISTS ) ;
if ( adjacent _only ) {
break ;
}