@ -1,16 +1,9 @@
/** @import { Visitors } from 'zimmerframe' */
/** @import * as Compiler from '#compiler' */
/** @import * as Compiler from '#compiler' */
import { walk } from 'zimmerframe' ;
import { walk } from 'zimmerframe' ;
import { get _parent _rules , get _possible _values , is _outer _global } from './utils.js' ;
import { get _parent _rules , get _possible _values , is _outer _global } from './utils.js' ;
import { regex _ends _with _whitespace , regex _starts _with _whitespace } from '../../patterns.js' ;
import { regex _ends _with _whitespace , regex _starts _with _whitespace } from '../../patterns.js' ;
import { get _attribute _chunks , is _text _attribute } from '../../../utils/ast.js' ;
import { get _attribute _chunks , is _text _attribute } from '../../../utils/ast.js' ;
/ * *
* @ typedef { {
* element : Compiler . AST . RegularElement | Compiler . AST . SvelteElement ;
* from _render _tag : boolean ;
* } } State
* /
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
const NODE _PROBABLY _EXISTS = 0 ;
const NODE _PROBABLY _EXISTS = 0 ;
@ -53,78 +46,32 @@ const nesting_selector = {
/ * *
/ * *
*
*
* @ param { Compiler . Css . StyleSheet } stylesheet
* @ param { Compiler . Css . StyleSheet } stylesheet
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement | Compiler . AST . RenderTag } element
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement } element
* /
* /
export function prune ( stylesheet , element ) {
export function prune ( stylesheet , element ) {
if ( element . type === 'RenderTag' ) {
walk ( /** @type {Compiler.Css.Node} */ ( stylesheet ) , null , {
const parent = get _element _parent ( element ) ;
Rule ( node , context ) {
if ( ! parent ) return ;
if ( node . metadata . is _global _block ) {
context . visit ( node . prelude ) ;
walk ( stylesheet , { element : parent , from _render _tag : true } , visitors ) ;
} else {
} else {
context . next ( ) ;
walk ( stylesheet , { element , from _render _tag : false } , visitors ) ;
}
}
} ,
}
ComplexSelector ( node ) {
const selectors = get _relative _selectors ( node ) ;
/** @type {Visitors<Compiler.Css.Node, State>} */
const visitors = {
if (
Rule ( node , context ) {
apply _selector ( selectors , /** @type {Compiler.Css.Rule} */ ( node . metadata . rule ) , element )
if ( node . metadata . is _global _block ) {
) {
context . visit ( node . prelude ) ;
node . metadata . used = true ;
} else {
context . next ( ) ;
}
} ,
ComplexSelector ( node , context ) {
const selectors = get _relative _selectors ( node ) ;
const inner = selectors [ selectors . length - 1 ] ;
if ( context . state . from _render _tag ) {
// We're searching for a match that crosses a render tag boundary. That means we have to both traverse up
// the element tree (to see if we find an entry point) but also remove selectors from the end (assuming
// they are part of the render tag we don't see). We do all possible combinations of both until we find a match.
/** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */
let element = context . state . element ;
while ( element ) {
const selectors _to _check = selectors . slice ( ) ;
while ( selectors _to _check . length > 0 ) {
selectors _to _check . pop ( ) ;
if (
apply _selector (
selectors _to _check ,
/** @type {Compiler.Css.Rule} */ ( node . metadata . rule ) ,
element ,
context . state
)
) {
mark ( inner , element ) ;
node . metadata . used = true ;
return ;
}
}
element = get _element _parent ( element ) ;
}
}
} else if (
apply _selector (
selectors ,
/** @type {Compiler.Css.Rule} */ ( node . metadata . rule ) ,
context . state . element ,
context . state
)
) {
mark ( inner , context . state . element ) ;
node . metadata . used = true ;
}
// note: we don't call context.next() here, we only recurse into
// note: we don't call context.next() here, we only recurse into
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// when we encounter them below
// when we encounter them below
}
}
} ;
} ) ;
}
/ * *
/ * *
* Retrieves the relative selectors ( minus the trailing globals ) from a complex selector .
* Retrieves the relative selectors ( minus the trailing globals ) from a complex selector .
@ -147,6 +94,7 @@ function get_relative_selectors(node) {
has _explicit _nesting _selector = true ;
has _explicit _nesting _selector = true ;
}
}
} ) ;
} ) ;
// if we found one we can break from the others
// if we found one we can break from the others
if ( has _explicit _nesting _selector ) break ;
if ( has _explicit _nesting _selector ) break ;
}
}
@ -199,89 +147,63 @@ function truncate(node) {
* @ param { Compiler . Css . RelativeSelector [ ] } relative _selectors
* @ param { Compiler . Css . RelativeSelector [ ] } relative _selectors
* @ param { Compiler . Css . Rule } rule
* @ param { Compiler . Css . Rule } rule
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement } element
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement } element
* @ param { State } state
* @ returns { boolean }
* @ returns { boolean }
* /
* /
function apply _selector ( relative _selectors , rule , element , state ) {
function apply _selector ( relative _selectors , rule , element ) {
const parent _selectors = relative _selectors . slice ( ) ;
const parent _selectors = relative _selectors . slice ( ) ;
const relative _selector = parent _selectors . pop ( ) ;
const relative _selector = parent _selectors . pop ( ) ;
if ( ! relative _selector ) return false ;
const matched =
! ! relative _selector &&
relative _selector _might _apply _to _node ( relative _selector , rule , element ) &&
apply _combinator ( relative _selector , parent _selectors , rule , element ) ;
const possible _match = relative _selector _might _apply _to _node (
if ( matched ) {
relative _selector ,
if ( ! is _outer _global ( relative _selector ) ) {
rule ,
relative _selector . metadata . scoped = true ;
element ,
}
state
) ;
if ( ! possible _match ) {
return false ;
}
if ( relative _selector . combinator ) {
return apply _combinator (
relative _selector . combinator ,
relative _selector ,
parent _selectors ,
rule ,
element ,
state
) ;
}
// if this is the left-most non-global selector, mark it — we want
element . metadata . scoped = true ;
// `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 ) ;
}
}
return true ;
return matched ;
}
}
/ * *
/ * *
* @ param { Compiler . Css . Combinator } combinator
* @ param { Compiler . Css . RelativeSelector } relative _selector
* @ param { Compiler . Css . RelativeSelector } relative _selector
* @ param { Compiler . Css . RelativeSelector [ ] } parent _selectors
* @ param { Compiler . Css . RelativeSelector [ ] } parent _selectors
* @ param { Compiler . Css . Rule } rule
* @ param { Compiler . Css . Rule } rule
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement } element
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement | Compiler . AST . RenderTag | Compiler . AST . Component | Compiler . AST . SvelteComponent | Compiler . AST . SvelteSelf } node
* @ param { State } state
* @ returns { boolean }
* @ returns { boolean }
* /
* /
function apply _combinator ( combinator , relative _selector , parent _selectors , rule , element , state ) {
function apply _combinator ( relative _selector , parent _selectors , rule , node ) {
const name = combinator . name ;
if ( ! relative _selector . combinator ) return true ;
const name = relative _selector . combinator . name ;
switch ( name ) {
switch ( name ) {
case ' ' :
case ' ' :
case '>' : {
case '>' : {
let parent _matched = false ;
let parent _matched = false ;
let crossed _component _boundary = false ;
const path = element . metadata . path ;
const path = node . metadata . path ;
let i = path . length ;
let i = path . length ;
while ( i -- ) {
while ( i -- ) {
const parent = path [ i ] ;
const parent = path [ i ] ;
if ( parent . type === 'Component' || parent . type === 'SvelteComponent' ) {
crossed _component _boundary = true ;
}
if ( parent . type === 'SnippetBlock' ) {
if ( parent . type === 'SnippetBlock' ) {
// We assume the snippet might be rendered in a place where the parent selectors match.
for ( const site of parent . metadata . sites ) {
// (We could do more static analysis and check the render tag reference to see if this snippet block continues
if ( apply _combinator ( relative _selector , parent _selectors , rule , site ) ) {
// with elements that actually match the selector, but that would be a lot of work for little gain)
return true ;
return true ;
}
}
return false ;
}
}
if ( parent . type === 'RegularElement' || parent . type === 'SvelteElement' ) {
if ( parent . type === 'RegularElement' || parent . type === 'SvelteElement' ) {
if ( apply _selector ( parent _selectors , rule , parent , state ) ) {
if ( apply _selector ( parent _selectors , rule , parent ) ) {
// 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 ;
parent _matched = true ;
}
}
@ -294,7 +216,7 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
case '+' :
case '+' :
case '~' : {
case '~' : {
const siblings = get _possible _element _siblings ( element , name === '+' ) ;
const siblings = get _possible _element _siblings ( node , name === '+' ) ;
let sibling _matched = false ;
let sibling _matched = false ;
@ -302,18 +224,16 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
if ( possible _sibling . type === 'RenderTag' || possible _sibling . type === 'SlotElement' ) {
if ( possible _sibling . type === 'RenderTag' || possible _sibling . type === 'SlotElement' ) {
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
// `{@render foo()}<p>foo</p>` with `:global(.x) + p` is a match
if ( parent _selectors . length === 1 && parent _selectors [ 0 ] . metadata . is _global ) {
if ( parent _selectors . length === 1 && parent _selectors [ 0 ] . metadata . is _global ) {
mark ( relative _selector , element ) ;
sibling _matched = true ;
sibling _matched = true ;
}
}
} else if ( apply _selector ( parent _selectors , rule , possible _sibling , state ) ) {
} else if ( apply _selector ( parent _selectors , rule , possible _sibling ) ) {
mark ( relative _selector , element ) ;
sibling _matched = true ;
sibling _matched = true ;
}
}
}
}
return (
return (
sibling _matched ||
sibling _matched ||
( get _element _parent ( element ) === null &&
( get _element _parent ( node ) === null &&
parent _selectors . every ( ( selector ) => is _global ( selector , rule ) ) )
parent _selectors . every ( ( selector ) => is _global ( selector , rule ) ) )
) ;
) ;
}
}
@ -324,19 +244,6 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule,
}
}
}
}
/ * *
* Mark both the compound selector and the node it selects as encapsulated ,
* for transformation in a later step
* @ param { Compiler . Css . RelativeSelector } relative _selector
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement } element
* /
function mark ( relative _selector , element ) {
if ( ! is _outer _global ( relative _selector ) ) {
relative _selector . metadata . scoped = true ;
}
element . metadata . scoped = true ;
}
/ * *
/ * *
* Returns ` true ` if the relative selector is global , meaning
* Returns ` true ` if the relative selector is global , meaning
* it ' s a ` :global(...) ` or unscopeable selector , or
* it ' s a ` :global(...) ` or unscopeable selector , or
@ -388,10 +295,9 @@ const regex_backslash_and_following_character = /\\(.)/g;
* @ param { Compiler . Css . RelativeSelector } relative _selector
* @ param { Compiler . Css . RelativeSelector } relative _selector
* @ param { Compiler . Css . Rule } rule
* @ param { Compiler . Css . Rule } rule
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement } element
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement } element
* @ param { State } state
* @ returns { boolean }
* @ returns { boolean }
* /
* /
function relative _selector _might _apply _to _node ( relative _selector , rule , element , state ) {
function relative _selector _might _apply _to _node ( relative _selector , rule , element ) {
// Sort :has(...) selectors in one bucket and everything else into another
// Sort :has(...) selectors in one bucket and everything else into another
const has _selectors = [ ] ;
const has _selectors = [ ] ;
const other _selectors = [ ] ;
const other _selectors = [ ] ;
@ -416,7 +322,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
// If this is a :has inside a global selector, we gotta include the element itself, too,
// If this is a :has inside a global selector, we gotta include the element itself, too,
// because the global selector might be for an element that's outside the component (e.g. :root).
// because the global selector might be for an element that's outside the component (e.g. :root).
const rules = [ rule , ... get _parent _rules ( rule ) ] ;
const rules = get _parent _rules ( rule ) ;
const include _self =
const include _self =
rules . some ( ( r ) => r . prelude . children . some ( ( c ) => c . children . some ( ( s ) => is _global ( s , r ) ) ) ) ||
rules . some ( ( r ) => r . prelude . children . some ( ( c ) => c . children . some ( ( s ) => is _global ( s , r ) ) ) ) ||
rules [ rules . length - 1 ] . prelude . children . some ( ( c ) =>
rules [ rules . length - 1 ] . prelude . children . some ( ( c ) =>
@ -429,10 +335,12 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
descendant _elements . push ( element ) ;
descendant _elements . push ( element ) ;
}
}
walk (
/ * *
/** @type {Compiler.SvelteNode} */ ( element . fragment ) ,
* @ param { Compiler . SvelteNode } node
{ is _child : true } ,
* @ param { { is _child : boolean } } state
{
* /
function walk _children ( node , state ) {
walk ( node , state , {
_ ( node , context ) {
_ ( node , context ) {
if ( node . type === 'RegularElement' || node . type === 'SvelteElement' ) {
if ( node . type === 'RegularElement' || node . type === 'SvelteElement' ) {
descendant _elements . push ( node ) ;
descendant _elements . push ( node ) ;
@ -445,12 +353,18 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
} else {
} else {
context . next ( ) ;
context . next ( ) ;
}
}
} else if ( node . type === 'RenderTag' ) {
for ( const snippet of node . metadata . snippets ) {
walk _children ( snippet . body , context . state ) ;
}
} else {
} else {
context . next ( ) ;
context . next ( ) ;
}
}
}
}
}
} ) ;
) ;
}
walk _children ( element . fragment , { is _child : true } ) ;
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
// upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the
@ -486,7 +400,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if (
if (
selectors . length === 0 /* is :global(...) */ ||
selectors . length === 0 /* is :global(...) */ ||
( element . metadata . scoped && selector _matched ) ||
( element . metadata . scoped && selector _matched ) ||
apply _selector ( selectors , rule , element , state )
apply _selector ( selectors , rule , element )
) {
) {
complex _selector . metadata . used = true ;
complex _selector . metadata . used = true ;
selector _matched = matched = true ;
selector _matched = matched = true ;
@ -516,7 +430,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
) {
) {
const args = selector . args ;
const args = selector . args ;
const complex _selector = args . children [ 0 ] ;
const complex _selector = args . children [ 0 ] ;
return apply _selector ( complex _selector . children , rule , element , state );
return apply _selector ( complex _selector . children , rule , element );
}
}
// We came across a :global, everything beyond it is global and therefore a potential match
// We came across a :global, everything beyond it is global and therefore a potential match
@ -565,7 +479,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
if ( is _global ) {
if ( is _global ) {
complex _selector . metadata . used = true ;
complex _selector . metadata . used = true ;
matched = true ;
matched = true ;
} else if ( apply _selector ( relative , rule , element , state )) {
} else if ( apply _selector ( relative , rule , element )) {
complex _selector . metadata . used = true ;
complex _selector . metadata . used = true ;
matched = true ;
matched = true ;
} else if ( complex _selector . children . length > 1 && ( name == 'is' || name == 'where' ) ) {
} else if ( complex _selector . children . length > 1 && ( name == 'is' || name == 'where' ) ) {
@ -649,7 +563,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
for ( const complex _selector of parent . prelude . children ) {
for ( const complex _selector of parent . prelude . children ) {
if (
if (
apply _selector ( get _relative _selectors ( complex _selector ) , parent , element , state ) ||
apply _selector ( get _relative _selectors ( complex _selector ) , parent , element ) ||
complex _selector . children . every ( ( s ) => is _global ( s , parent ) )
complex _selector . children . every ( ( s ) => is _global ( s , parent ) )
) {
) {
complex _selector . metadata . used = true ;
complex _selector . metadata . used = true ;
@ -703,17 +617,28 @@ function get_following_sibling_elements(element, include_self) {
// ...then walk them, starting from the node after the one
// ...then walk them, starting from the node after the one
// containing the element in question
// containing the element in question
for ( const node of nodes . slice ( nodes . indexOf ( start ) + 1 ) ) {
/** @param {Compiler.SvelteNode} node */
function get _siblings ( node ) {
walk ( node , null , {
walk ( node , null , {
RegularElement ( node ) {
RegularElement ( node ) {
siblings . push ( node ) ;
siblings . push ( node ) ;
} ,
} ,
SvelteElement ( node ) {
SvelteElement ( node ) {
siblings . push ( node ) ;
siblings . push ( node ) ;
} ,
RenderTag ( node ) {
for ( const snippet of node . metadata . snippets ) {
get _siblings ( snippet . body ) ;
}
}
}
} ) ;
} ) ;
}
}
for ( const node of nodes . slice ( nodes . indexOf ( start ) + 1 ) ) {
get _siblings ( node ) ;
}
if ( include _self ) {
if ( include _self ) {
siblings . push ( element ) ;
siblings . push ( element ) ;
}
}
@ -858,7 +783,7 @@ function unquote(str) {
}
}
/ * *
/ * *
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement | Compiler . AST . RenderTag } node
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement | Compiler . AST . RenderTag | Compiler . AST . Component | Compiler . AST . SvelteComponent | Compiler . AST . SvelteSelf } node
* @ returns { Compiler . AST . RegularElement | Compiler . AST . SvelteElement | null }
* @ returns { Compiler . AST . RegularElement | Compiler . AST . SvelteElement | null }
* /
* /
function get _element _parent ( node ) {
function get _element _parent ( node ) {
@ -877,7 +802,7 @@ function get_element_parent(node) {
}
}
/ * *
/ * *
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement } node
* @ param { Compiler . AST . RegularElement | Compiler . AST . SvelteElement | Compiler . AST . RenderTag | Compiler . AST . Component | Compiler . AST . SvelteComponent | Compiler . AST . SvelteSelf } node
* @ param { boolean } adjacent _only
* @ param { boolean } adjacent _only
* @ returns { Map < Compiler . AST . RegularElement | Compiler . AST . SvelteElement | Compiler . AST . SlotElement | Compiler . AST . RenderTag , NodeExistsValue > }
* @ returns { Map < Compiler . AST . RegularElement | Compiler . AST . SvelteElement | Compiler . AST . SlotElement | Compiler . AST . RenderTag , NodeExistsValue > }
* /
* /
@ -929,7 +854,28 @@ function get_possible_element_siblings(node, adjacent_only) {
current = path [ i ] ;
current = path [ i ] ;
if ( ! current || ! is _block ( current ) ) break ;
if ( ! current ) break ;
if (
current . type === 'Component' ||
current . type === 'SvelteComponent' ||
current . type === 'SvelteSelf'
) {
continue ;
}
if ( current . type === 'SnippetBlock' ) {
for ( const site of current . metadata . sites ) {
const siblings = get _possible _element _siblings ( site , adjacent _only ) ;
add _to _map ( siblings , result ) ;
if ( adjacent _only && current . metadata . sites . size === 1 && has _definite _elements ( siblings ) ) {
return result ;
}
}
}
if ( ! is _block ( current ) ) break ;
if ( current . type === 'EachBlock' && fragment === current . body ) {
if ( current . type === 'EachBlock' && fragment === current . body ) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`