diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index fa0964636f..6b8d3333d7 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -107,7 +107,8 @@ const css = { 'invalid-css-global-selector-list': () => `:global(...) must not contain type or universal selectors when used in a compound selector`, 'invalid-css-selector': () => `Invalid selector`, - 'invalid-css-identifier': () => 'Expected a valid CSS identifier' + 'invalid-css-identifier': () => 'Expected a valid CSS identifier', + 'nesting-selector-not-allowed': () => 'Nesting selector is not allowed in top level rules', }; /** @satisfies {Errors} */ 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 810b308ad6..914ce4aa40 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -25,8 +25,8 @@ export default class Selector { /** @type {import('./Stylesheet.js').default} */ stylesheet; - /** @type {Block[]} */ - blocks; + /** @type {BlockUse[][]} */ + block_groups; /** @type {Block[]} */ local_blocks; @@ -37,12 +37,12 @@ export default class Selector { /** * @param {import('#compiler').Css.Selector} node * @param {import('./Stylesheet.js').default} stylesheet - * @param {Selector | null} parent_selector + * @param {BlockUse[][] | null} parent_blocks */ - constructor(node, stylesheet, parent_selector) { + constructor(node, stylesheet, parent_blocks) { this.node = node; this.stylesheet = stylesheet; - this.blocks = group_selectors(node, parent_selector); + this.block_groups = group_selectors(node, parent_blocks); // take trailing :global(...) selectors out of consideration let i = this.blocks.length; while (i > 0) { @@ -846,13 +846,30 @@ class Block { } /** @type {import('#compiler').Css.Combinator} */ -const InvisibleCombinator = { +const FakeCombinator = { type: 'Combinator', name: ' ', start: -1, end: -1 }; +class BlockUse { + /** @type {Block} */ + block; + + /** @type {boolean} */ + visible; + + /** + * @param {Block} block + * @param {boolean} visible + */ + constructor(block, visible) { + this.block = block; + this.visible = visible; + } +} + /** * Groups selectors by combinator into blocks * @@ -861,54 +878,74 @@ const InvisibleCombinator = { * - Then insert the parent_selector's blocks at that position * * @param {import('#compiler').Css.Selector} selector - * @param {Selector | null} parent_selector + * @param {BlockUse[][] | null} parent_blocks_group */ -function group_selectors(selector, parent_selector) { - let block = new Block(null); +function group_selectors(selector, parent_blocks_group) { + // If it isn't a nested rule, then we add an empty block group + if (parent_blocks_group === null) { + return [ + selector_to_blocks(selector, null, false).map((block) => new BlockUse(block, true)) + ]; + } - const blocks = [block]; + // This is a nested rule, so we need to insert the parent selector's blocks at the position of the `&` selector + // or at the front if there is no `&` selector + // TODO: handle multiple `&` selectors + let nested_rule_index = selector.children.findIndex((child) => child.type === 'NestingSelector'); + + // if there is no `&` selector, we need to add a fake combinator at the start + if (nested_rule_index === -1) { + nested_rule_index = 0; + } else { + // if there is a `&` selector, we need to delete it + // and insert the parent's blocks there + selector.children.splice(nested_rule_index, 1); + } - const real_selectors_start = parent_selector?.node.children.length || 0; + // Create the new blocks for the nested rule, visible by default + const blocks = selector_to_blocks( + selector, + nested_rule_index === 0 ? null : FakeCombinator, + false + ); - if (parent_selector) { - const nested_rule_indices = selector.children - .map((child, index) => (child.type === 'NestingSelector' ? index : -1)) - .filter((index) => index !== -1); + return parent_blocks_group.map(parent_blocks => { + // create a new block use for each parent block and set them to invisible + let parent_block_uses = parent_blocks.map((block_use) => new BlockUse(block_use.block, false)); - const parent_children = parent_selector.node.children.map((child) => ({ - ...child, - invisible: true - })); + // Create a new block use for each block in the nested rule, set them to visible + let block_uses = blocks.map((block) => new BlockUse(block, true)); - if (nested_rule_indices.length === 0) { - // if the next selector is a combinator, we must not unshift a child combinator - const next_is_combinator = selector.children[0]?.type === 'Combinator'; - if (!next_is_combinator) { - selector.children.unshift(InvisibleCombinator); - } - selector.children.unshift(...parent_children); - } else { - // There's an & nesting selectors somewhere - // so we delete it and insert invisible parent's children there - nested_rule_indices.forEach((nested_rule_index) => - selector.children.splice(nested_rule_index, 1, ...parent_children) - ); - } - } + // insert the parent blocks at the position of the `&` selector + block_uses.splice(nested_rule_index, 0, ...parent_block_uses); + + return block_uses; + }) +} - selector.children.forEach((child, i) => { + +/** + * @param {import('#compiler').Css.Selector} selector + * @param {import('../../../types/css.js').Combinator | null} combinator + * @param {boolean} allow_nesting + */ +function selector_to_blocks(selector, combinator, allow_nesting) { + let block = new Block(combinator); + const blocks = [block]; + selector.children.forEach((child) => { if (child.type === 'Combinator') { block = new Block(child); blocks.push(block); } else if (child.type === 'NestingSelector') { - // Don't think we need to add it here + if (!allow_nesting) { + error(child, 'nesting-selector-not-allowed'); + } else { + // We should've already removed the `&` selector + throw new Error("Unexpected nesting selector"); + } } else { block.add(child); } - if (real_selectors_start > i) { - block.invisible = true; - } }); - return blocks; -} +} \ No newline at end of file diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js index 7d4247ef15..4ca39900ed 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -74,8 +74,8 @@ class Rule { this.parent = parent; /** - * We need to add selectors for each parent rule's selectors - * because of CSS nesting. For example: + * If there's a parent, we need to pass that parent's block_groups into the child + * selector because of CSS nesting. For example: * ```css * .a, .b { * .c { @@ -88,11 +88,8 @@ class Rule { * - .b .c */ if (parent && parent.node.type === 'Rule') { - this.selectors = /** @type {Rule} **/ (parent).selectors - .map((parent_selector) => - node.prelude.children.map((node) => new Selector(node, stylesheet, parent_selector)) - ) - .flat(); + let block_groups = /** @type {Rule} **/ (parent).selectors.map(selector => selector.block_groups).flat(); + this.selectors = node.prelude.children.map(node => new Selector(node, stylesheet, block_groups)); } else { this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet, null)); }