diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js index aa5e5f0e02..bb2cea8718 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -77,24 +77,53 @@ export default class Selector { } - /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ + /** + * Determines whether the given selector is used within the component's nodes + * and marks the corresponding blocks for encapsulation if so. + * + * In CSS nesting, the selector might be used in one nested rule, but not in another + * e.g: + * ```css + * a, b { + * c { + * color: red; + * } + * ``` + * + * ```svelte + * + * ... + * + * + * No 'c' here + * + * ``` + * + * In the above example, the selector `a c` is used, but `b c` is not. + * However, we must not mark the `c` block as encapsulated because it's used in `a`. + * Even though it's not used in `b`, it's still used in the component. + * + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node - The node to apply the selector to. + * @returns {void} + */ apply(node) { - /** @type {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} */ - const to_encapsulate = []; + /** + * Create a map of blocks to their nodes to know whether they should be encapsulated + * @type {Map>} + * */ + const used_blocks = new Map(); + this.local_block_groups.map(group => { const blocks = group.map(block_use => block_use.block); - apply_selector(blocks, node, to_encapsulate); + apply_selector(blocks, node, used_blocks); }); - if (to_encapsulate.length > 0) { - to_encapsulate.forEach(({ node, block }) => { - // This block might've been encapsulated by a previous selector - // so we make sure to not encapsulate it again - if (!block.should_encapsulate) { - block.should_encapsulate = true; - this.stylesheet.nodes_with_css_class.add(node); - } - }); + // Iterate over used_blocks + for (const [block, nodes] of used_blocks) { + block.should_encapsulate = true; + for (const node of nodes) { + this.stylesheet.nodes_with_css_class.add(node); + } this.used = true; } } @@ -105,9 +134,49 @@ export default class Selector { * @param {number} max_amount_class_specificity_increased */ transform(code, attr, max_amount_class_specificity_increased) { - const amount_class_specificity_to_increase = - max_amount_class_specificity_increased - - this.blocks.filter((block) => !block.invisible && block.should_encapsulate).length; + /** + * @param {BlockUse} block_use + * @param {string} attr + */ + function encapsulate_block(block_use, attr) { + let block = block_use.block + if (block_use.visible && !block.has_been_encapsulated) { + // Similar encapsulation logic as before + // Ensure we mark the block as encapsulated to avoid re-processing + block.has_been_encapsulated = true; + + for (const selector of block.selectors) { + if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { + remove_global_pseudo_class(selector); + } + } + let i = block.selectors.length; + + while (i--) { + const selector = block.selectors[i]; + + // We don't make any changes to the invisible selectors + // because they don't exist in reality in css nesting + // and changing them would affect the nested rules parent rule selectors + if (selector.invisible) { + continue; + } + + if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { + if (!block.root && !block.host) { + if (i === 0) code.prependRight(selector.start, attr); + } + continue; + } + if (selector.type === 'TypeSelector' && selector.name === '*') { + code.update(selector.start, selector.end, attr); + } else { + code.appendLeft(selector.end, attr); + } + break; + } + } + }; /** @param {import('#compiler').Css.SimpleSelector} selector */ function remove_global_pseudo_class(selector) { @@ -116,91 +185,74 @@ export default class Selector { .remove(selector.end - 1, selector.end); } - /** - * @param {Block} block - * @param {string} attr - */ - function encapsulate_block(block, attr) { - for (const selector of block.selectors) { - if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { - remove_global_pseudo_class(selector); - } - } - let i = block.selectors.length; - - while (i--) { - const selector = block.selectors[i]; + for (const group of this.block_groups) { + const amount_class_specificity_to_increase = max_amount_class_specificity_increased - + group.filter(block_use => block_use.visible && block_use.block.should_encapsulate).length; - // We don't make any changes to the invisible selectors - // because they don't exist in reality in css nesting - // and changing them would affect the nested rules parent rule selectors - if (selector.invisible) { - continue; + group.map((block_use, index) => { + const block = block_use.block; + if (block.global) { + // Remove the global pseudo class from the selector + remove_global_pseudo_class(block.selectors[0]); } - if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { - if (!block.root && !block.host) { - if (i === 0) code.prependRight(selector.start, attr); - } - continue; - } - if (selector.type === 'TypeSelector' && selector.name === '*') { - code.update(selector.start, selector.end, attr); - } else { - code.appendLeft(selector.end, attr); + if(block.should_encapsulate) { + encapsulate_block( + block_use, + index === group.length - 1 + ? attr.repeat(amount_class_specificity_to_increase + 1) + : attr + ); } - break; - } + }); } - this.blocks.forEach((block, index) => { - if (block.global) { - remove_global_pseudo_class(block.selectors[0]); - } - if (block.should_encapsulate) { - encapsulate_block( - block, - index === this.blocks.filter((block) => !block.invisible).length - 1 - ? attr.repeat(amount_class_specificity_to_increase + 1) - : attr - ); - } - }); } /** @param {import('../../types.js').ComponentAnalysis} analysis */ validate(analysis) { - let start = 0; - let end = this.blocks.length; - for (; start < end; start += 1) { - if (!this.blocks[start].global) break; - } - for (; end > start; end -= 1) { - if (!this.blocks[end - 1].global) break; - } - for (let i = start; i < end; i += 1) { - if (this.blocks[i].global) { - error(this.blocks[i].selectors[0], 'invalid-css-global-placement'); - } - } + this.validate_invalid_css_global_placement(); this.validate_global_with_multiple_selectors(); this.validate_global_compound_selector(); this.validate_invalid_combinator_without_selector(analysis); } - validate_global_with_multiple_selectors() { - if (this.blocks.length === 1 && this.blocks[0].selectors.length === 1) { - // standalone :global() with multiple selectors is OK - return; + + validate_invalid_css_global_placement() { + for (let group of this.block_groups) { + let start = 0; + let end = group.length; + for (; start < end; start += 1) { + if (!group[start].block.global) break; + } + for (; end > start; end -= 1) { + if (!group[end - 1].block.global) break; + } + for (let i = start; i < end; i += 1) { + if (group[i].block.global) { + error(group[i].block.selectors[0], 'invalid-css-global-placement'); + } + } } - for (const block of this.blocks) { - for (const selector of block.selectors) { - if ( - selector.type === 'PseudoClassSelector' && - selector.name === 'global' && - selector.args !== null && - selector.args.children.length > 1 - ) { - error(selector, 'invalid-css-global-selector'); + } + + + validate_global_with_multiple_selectors() { + for (const group of this.block_groups) { + if (group.length === 1 && group[0].block.selectors.length === 1) { + // standalone :global() with multiple selectors is OK + return; + } + for (const block_use of group) { + const block = block_use.block; + for (const selector of block.selectors) { + if ( + selector.type === 'PseudoClassSelector' && + selector.name === 'global' && + selector.args !== null && + selector.args.children.length > 1 + ) { + error(selector, 'invalid-css-global-selector'); + } } } } @@ -208,34 +260,39 @@ export default class Selector { /** @param {import('../../types.js').ComponentAnalysis} analysis */ validate_invalid_combinator_without_selector(analysis) { - for (let i = 0; i < this.blocks.length; i++) { - const block = this.blocks[i]; - if (block.selectors.length === 0) { - error(this.node, 'invalid-css-selector'); + for (const group of this.block_groups) { + for (const block_use of group) { + const block = block_use.block; + if (block.combinator && block.selectors.length === 0) { + error(block.combinator, 'invalid-css-selector'); + } } } } validate_global_compound_selector() { - for (const block of this.blocks) { - if (block.selectors.length === 1) continue; - - for (let i = 0; i < block.selectors.length; i++) { - const selector = block.selectors[i]; - - if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { - const child = selector.args?.children[0].children[0]; - if ( - child?.type === 'TypeSelector' && - !/[.:#]/.test(child.name[0]) && - (i !== 0 || - block.selectors - .slice(1) - .some( - (s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector' - )) - ) { - error(selector, 'invalid-css-global-selector-list'); + for (const group of this.block_groups) { + for (const block_use of group) { + const block = block_use.block; + if (block.selectors.length === 1) continue; + + for (let i = 0; i < block.selectors.length; i++) { + const selector = block.selectors[i]; + + if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { + const child = selector.args?.children[0].children[0]; + if ( + child?.type === 'TypeSelector' && + !/[.:#]/.test(child.name[0]) && + (i !== 0 || + block.selectors + .slice(1) + .some( + (s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector' + )) + ) { + error(selector, 'invalid-css-global-selector-list'); + } } } } @@ -243,14 +300,29 @@ export default class Selector { } get_amount_class_specificity_increased() { - return this.blocks.filter((block) => block.should_encapsulate).length; + // Is this right? Should we be counting the amount of blocks that are visible? + // Or should we be counting the amount of selectors that are visible? + return this.block_groups[0].filter(block_use => block_use.block.should_encapsulate).length; + } + +} + +/** + * @param {Map>} map + * @param {Block} block + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node + */ +function add_node(map, block, node) { + if (!map.has(block)) { + map.set(block, new Set()); } + map.get(block)?.add(node); } /** * @param {Block[]} blocks * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node - * @param {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} to_encapsulate + * @param {Map>} to_encapsulate * @returns {boolean} */ function apply_selector(blocks, node, to_encapsulate) { @@ -268,7 +340,7 @@ function apply_selector(blocks, node, to_encapsulate) { } if (applies === UNKNOWN_SELECTOR) { - to_encapsulate.push({ node, block }); + add_node(to_encapsulate, block, node); return true; } @@ -279,30 +351,30 @@ function apply_selector(blocks, node, to_encapsulate) { continue; } if (ancestor_block.host) { - to_encapsulate.push({ node, block }); + add_node(to_encapsulate, block, node); return true; } /** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */ let parent = node; while ((parent = get_element_parent(parent))) { if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) { - to_encapsulate.push({ node: parent, block: ancestor_block }); + add_node(to_encapsulate, ancestor_block, parent); } } - if (to_encapsulate.length) { - to_encapsulate.push({ node, block }); + if (to_encapsulate.size) { + add_node(to_encapsulate, block, node); return true; } } if (blocks.every((block) => block.global)) { - to_encapsulate.push({ node, block }); + add_node(to_encapsulate, block, node); return true; } return false; } else if (block.combinator.name === '>') { const has_global_parent = blocks.every((block) => block.global); if (has_global_parent || apply_selector(blocks, get_element_parent(node), to_encapsulate)) { - to_encapsulate.push({ node, block }); + add_node(to_encapsulate, block, node); return true; } return false; @@ -317,22 +389,22 @@ function apply_selector(blocks, node, to_encapsulate) { if (siblings.size === 0 && get_element_parent(node) !== null) { return false; } - to_encapsulate.push({ node, block }); + add_node(to_encapsulate, block, node); return true; } for (const possible_sibling of siblings.keys()) { if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) { - to_encapsulate.push({ node, block }); + add_node(to_encapsulate, block, node); has_match = true; } } return has_match; } // TODO other combinators - to_encapsulate.push({ node, block }); + add_node(to_encapsulate, block, node); return true; } - to_encapsulate.push({ node, block }); + add_node(to_encapsulate, block, node); return true; }