From 4ded50fa789c07f002fb7497197f66997472d94e Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 20 Nov 2023 10:25:48 +1030 Subject: [PATCH 01/69] Semi working --- .../src/compiler/phases/1-parse/index.js | 2 +- .../src/compiler/phases/1-parse/read/style.js | 26 +++++++++++++++++-- .../compiler/phases/2-analyze/css/Selector.js | 13 ++++++++-- .../phases/2-analyze/css/Stylesheet.js | 15 +++++++---- packages/svelte/src/compiler/types/css.d.ts | 6 +++++ .../tests/css/samples/nested-css/expected.css | 14 ++++++++++ .../tests/css/samples/nested-css/input.svelte | 20 ++++++++++++++ 7 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css/input.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 8c89b4b99b8b..f91e94c6b9e9 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -129,7 +129,7 @@ export class Parser { * @param {string} str * @param {boolean} [required] */ - eat(str, required) { + eat(str, required = false) { if (this.match(str)) { this.index += str.length; return true; diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 34d22bc7c3fd..21c3018903e6 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -189,7 +189,14 @@ function read_selector(parser) { while (parser.index < parser.template.length) { const start = parser.index; - if (parser.eat('*')) { + if (parser.eat('&')){ + children.push({ + type: 'NestedSelector', + name: '&', + start, + end: parser.index + }); + } else if (parser.eat('*')) { children.push({ type: 'TypeSelector', name: '*', @@ -337,7 +344,7 @@ function read_block(parser) { if (parser.match('}')) { break; } else { - children.push(read_declaration(parser)); + children.push(read_declaration_or_rule(parser)); } } @@ -380,6 +387,21 @@ function read_declaration(parser) { }; } +/** + * @param {import('../index.js').Parser} parser + * @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule} + */ +function read_declaration_or_rule(parser) { + // We will only allow css nesting using & selector + // due to complexities with https://bugs.chromium.org/p/chromium/issues/detail?id=1427259 + // as most browsers as of 17/11/2023 do not support nesting without & selector prefix + if (parser.match('&')) { + return read_rule(parser) + } else { + return read_declaration(parser); + } +} + /** * @param {import('../index.js').Parser} parser * @returns {string} 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 25e44b4e4025..8e453f68596e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -105,7 +105,10 @@ export default class Selector { } continue; } - if (selector.type === 'TypeSelector' && selector.name === '*') { + if (selector.type === "NestedSelector") { + // do we want to add the attr to the nested selector? + // it's kind of implied that it's a child of the parent selector + } else if (selector.type === 'TypeSelector' && selector.name === '*') { code.update(selector.start, selector.end, attr); } else { code.appendLeft(selector.end, attr); @@ -114,16 +117,18 @@ export default class Selector { } } this.blocks.forEach((block, index) => { + console.log(block) if (block.global) { remove_global_pseudo_class(block.selectors[0]); } - if (block.should_encapsulate) + if (block.should_encapsulate) { encapsulate_block( block, index === this.blocks.length - 1 ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); + } }); } @@ -309,6 +314,10 @@ function block_might_apply_to_node(block, node) { const name = selector.name.replace(regex_backslash_and_following_character, '$1'); + // if(name === "&") { + // return POSSIBLE_MATCH; + // } + if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) { return NO_MATCH; } 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 9d45692ed5b3..eecab0c16e46 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -58,22 +58,25 @@ class Rule { /** @type {import('#compiler').Css.Rule} */ node; - /** @type {Atrule | undefined} */ + /** @type {Atrule | Rule | undefined} */ parent; /** * @param {import('#compiler').Css.Rule} node * @param {any} stylesheet - * @param {Atrule | undefined} parent + * @param {Atrule | Rule | undefined} parent */ constructor(node, stylesheet, parent) { this.node = node; this.parent = parent; this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet)); - this.declarations = /** @type {import('#compiler').Css.Declaration[]} */ ( - node.block.children - ).map((node) => new Declaration(node)); + this.nested_rules = node.block.children + .filter((node) => node.type === 'Rule') + .map(node => new Rule(/** @type {import('#compiler').Css.Rule}*/ (node), stylesheet, this)); + this.declarations = node.block.children + .filter((node) => node.type === 'Declaration') + .map((node) => new Declaration(/** @type {import('#compiler').Css.Declaration} */ (node))); } /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ @@ -109,6 +112,7 @@ class Rule { selector.transform(code, attr, max_amount_class_specificity_increased) ); this.declarations.forEach((declaration) => declaration.transform(code, keyframes)); + this.nested_rules.forEach((rule) => rule.transform(code, id, keyframes, max_amount_class_specificity_increased)); } /** @param {import('../../types.js').ComponentAnalysis} analysis */ @@ -381,6 +385,7 @@ export default class Stylesheet { css: ast.content.styles, hash }); + this.has_styles = true; const state = { diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index e4d587ef6585..c1adee00b9a8 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -67,6 +67,11 @@ export interface Percentage extends BaseNode { value: string; } +export interface NestedSelector extends BaseNode { + type: 'NestedSelector'; + name: "&"; +} + export type SimpleSelector = | TypeSelector | IdSelector @@ -74,6 +79,7 @@ export type SimpleSelector = | AttributeSelector | PseudoElementSelector | PseudoClassSelector + | NestedSelector | Percentage; export interface Combinator extends BaseNode { diff --git a/packages/svelte/tests/css/samples/nested-css/expected.css b/packages/svelte/tests/css/samples/nested-css/expected.css new file mode 100644 index 000000000000..077d0196c299 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css/expected.css @@ -0,0 +1,14 @@ +button.svelte-xyz { + color: red; + & > div.svelte-xyz { + color: blue; + } + & .foo.svelte-xyz { + color: yellow; + } + &:last-child { + color: green; + } + + color: black; +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css/input.svelte b/packages/svelte/tests/css/samples/nested-css/input.svelte new file mode 100644 index 000000000000..1e5399ecd719 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css/input.svelte @@ -0,0 +1,20 @@ + + \ No newline at end of file From a3d4eafbecc096b924f359c3bc3da35afc77ab43 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 20 Nov 2023 13:04:21 +1030 Subject: [PATCH 02/69] Working CSS nesting implementation --- .../src/compiler/phases/1-parse/read/style.js | 34 ++++++++-------- .../compiler/phases/2-analyze/css/Selector.js | 9 ++--- .../phases/2-analyze/css/Stylesheet.js | 39 ++++++++++++++++--- .../tests/css/samples/nested-css/expected.css | 6 +++ .../tests/css/samples/nested-css/input.svelte | 10 ++++- 5 files changed, 69 insertions(+), 29 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 21c3018903e6..ba000981d504 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -189,21 +189,21 @@ function read_selector(parser) { while (parser.index < parser.template.length) { const start = parser.index; - if (parser.eat('&')){ - children.push({ - type: 'NestedSelector', - name: '&', - start, - end: parser.index - }); - } else if (parser.eat('*')) { - children.push({ - type: 'TypeSelector', - name: '*', - start, - end: parser.index - }); - } else if (parser.eat('#')) { + if (parser.eat('*')) { + children.push({ + type: 'TypeSelector', + name: '*', + start, + end: parser.index + }); + } else if (parser.eat('&')){ + children.push({ + type: 'NestedSelector', + name: '&', + start, + end: parser.index + }); + } else if (parser.eat('#')) { children.push({ type: 'IdSelector', name: read_identifier(parser), @@ -392,9 +392,9 @@ function read_declaration(parser) { * @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule} */ function read_declaration_or_rule(parser) { - // We will only allow css nesting using & selector + // We will only allow css nesting using & selector for now // due to complexities with https://bugs.chromium.org/p/chromium/issues/detail?id=1427259 - // as most browsers as of 17/11/2023 do not support nesting without & selector prefix + // as most browsers as of 17/11/2023 do not support nesting without & selector if (parser.match('&')) { return read_rule(parser) } else { 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 8e453f68596e..686685059846 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -117,14 +117,13 @@ export default class Selector { } } this.blocks.forEach((block, index) => { - console.log(block) if (block.global) { remove_global_pseudo_class(block.selectors[0]); } if (block.should_encapsulate) { encapsulate_block( block, - index === this.blocks.length - 1 + index === this.blocks.filter(block => block.nested !== true).length - 1 ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); @@ -314,10 +313,6 @@ function block_might_apply_to_node(block, node) { const name = selector.name.replace(regex_backslash_and_following_character, '$1'); - // if(name === "&") { - // return POSSIBLE_MATCH; - // } - if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) { return NO_MATCH; } @@ -806,6 +801,7 @@ class Block { this.combinator = combinator; this.host = false; this.root = false; + this.nested = false; this.selectors = []; this.start = -1; this.end = -1; @@ -845,6 +841,7 @@ function group_selectors(selector) { block = new Block(child); blocks.push(block); } else { + if (child.type === "NestedSelector") block.nested = true; block.add(child); } }); 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 eecab0c16e46..e2074ddbfbcf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -6,6 +6,7 @@ import hash from '../utils/hash.js'; // import { extract_ignores_above_position } from '../utils/extract_svelte_ignore.js'; import { push_array } from '../utils/push_array.js'; import { create_attribute } from '../../nodes.js'; +import assert from 'assert'; const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/; @@ -61,12 +62,17 @@ class Rule { /** @type {Atrule | Rule | undefined} */ parent; + /** @type {Rule[]} */ + nested_rules; + /** * @param {import('#compiler').Css.Rule} node - * @param {any} stylesheet + * @param {Stylesheet} stylesheet * @param {Atrule | Rule | undefined} parent */ constructor(node, stylesheet, parent) { + // console.log('...............') + // console.log(JSON.stringify(node, null, 4)) this.node = node; this.parent = parent; this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet)); @@ -82,9 +88,13 @@ class Rule { /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ apply(node) { this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here? + this.nested_rules.forEach((rule) => rule.apply(node)); } - /** @param {boolean} dev */ + /** + * @param {boolean} dev + * @returns {boolean} + */ is_used(dev) { if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) return true; @@ -93,7 +103,7 @@ class Rule { // see them in devtools if (this.declarations.length === 0) return dev; - return this.selectors.some((s) => s.used); + return [this.selectors.some((s) => s.used), this.nested_rules.some(r => r.is_used(dev))].some(Boolean); } /** @@ -120,6 +130,9 @@ class Rule { this.selectors.forEach((selector) => { selector.validate(analysis); }); + this.nested_rules.forEach((rule) => { + rule.validate(analysis); + }); } /** @param {(selector: import('./Selector.js').default) => void} handler */ @@ -127,6 +140,9 @@ class Rule { this.selectors.forEach((selector) => { if (!selector.used) handler(selector); }); + this.nested_rules.forEach((rule) => { + rule.warn_on_unused_selector(handler); + }); } /** @returns number */ @@ -134,6 +150,7 @@ class Rule { return Math.max( ...this.selectors.map((selector) => selector.get_amount_class_specificity_increased()) ); + // do we need to check nested rules? } /** @@ -147,7 +164,7 @@ class Rule { // keep empty rules in dev, because it's convenient to // see them in devtools - if (this.declarations.length === 0) { + if (this.declarations.length === 0 && this.nested_rules.length === 0) { if (!dev) { code.prependRight(this.node.start, '/* (empty) '); code.appendLeft(this.node.end, '*/'); @@ -198,6 +215,10 @@ class Rule { code.appendLeft(last, '*/'); } } + + this.nested_rules.forEach((rule) => { + rule.prune(code, dev); + }); } } @@ -390,9 +411,12 @@ export default class Stylesheet { const state = { /** @type {Atrule | undefined} */ - atrule: undefined + atrule: undefined, }; + /** @type {import('#compiler').Css.Node}*/ + let prev_node; + walk(/** @type {import('#compiler').Css.Node} */ (ast), state, { Atrule: (node, context) => { const atrule = new Atrule(node); @@ -429,12 +453,17 @@ export default class Stylesheet { }, Rule: (node, context) => { const rule = new Rule(node, this, context.state.atrule); + if (context.state.atrule) { context.state.atrule.children.push(rule); } else { this.children.push(rule); } + if (rule.nested_rules.length > 0) { + // Skip nested rules as they are instantiated in the Rule constructor + return node + } context.next(); } }); diff --git a/packages/svelte/tests/css/samples/nested-css/expected.css b/packages/svelte/tests/css/samples/nested-css/expected.css index 077d0196c299..5bd89686706f 100644 --- a/packages/svelte/tests/css/samples/nested-css/expected.css +++ b/packages/svelte/tests/css/samples/nested-css/expected.css @@ -10,5 +10,11 @@ button.svelte-xyz { color: green; } + & .bar.svelte-xyz { + & .hello.svelte-xyz { + color: orange; + } + } + color: black; } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css/input.svelte b/packages/svelte/tests/css/samples/nested-css/input.svelte index 1e5399ecd719..8e4d3bead9bd 100644 --- a/packages/svelte/tests/css/samples/nested-css/input.svelte +++ b/packages/svelte/tests/css/samples/nested-css/input.svelte @@ -1,6 +1,8 @@ \ No newline at end of file From 8756de3fe0da44f8682049eec96516eec8564a8e Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 20 Nov 2023 13:17:22 +1030 Subject: [PATCH 03/69] Add changeset --- .changeset/small-kids-switch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/small-kids-switch.md diff --git a/.changeset/small-kids-switch.md b/.changeset/small-kids-switch.md new file mode 100644 index 000000000000..3d3d81b72d99 --- /dev/null +++ b/.changeset/small-kids-switch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +Implement CSS nesting with & prefix requirement From 2661f4ea7aedae5903bb632f5e39d3ad22afbee2 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 20 Nov 2023 14:10:33 +1030 Subject: [PATCH 04/69] Remove some unused testing code --- .../svelte/src/compiler/phases/2-analyze/css/Stylesheet.js | 3 --- 1 file changed, 3 deletions(-) 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 e2074ddbfbcf..6e162c356bd8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -6,7 +6,6 @@ import hash from '../utils/hash.js'; // import { extract_ignores_above_position } from '../utils/extract_svelte_ignore.js'; import { push_array } from '../utils/push_array.js'; import { create_attribute } from '../../nodes.js'; -import assert from 'assert'; const regex_css_browser_prefix = /^-((webkit)|(moz)|(o)|(ms))-/; @@ -71,8 +70,6 @@ class Rule { * @param {Atrule | Rule | undefined} parent */ constructor(node, stylesheet, parent) { - // console.log('...............') - // console.log(JSON.stringify(node, null, 4)) this.node = node; this.parent = parent; this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet)); From 0e67006d4b1a6fd20a9e8fbeb9342b82a835ca41 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 20 Nov 2023 14:51:55 +1030 Subject: [PATCH 05/69] Remove unused testing code --- .../svelte/src/compiler/phases/2-analyze/css/Stylesheet.js | 3 --- 1 file changed, 3 deletions(-) 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 6e162c356bd8..5c6a98d8a780 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -411,9 +411,6 @@ export default class Stylesheet { atrule: undefined, }; - /** @type {import('#compiler').Css.Node}*/ - let prev_node; - walk(/** @type {import('#compiler').Css.Node} */ (ast), state, { Atrule: (node, context) => { const atrule = new Atrule(node); From d9174c4fbf8ea4f715b490ad9b4bdb96c03502e2 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 20 Nov 2023 15:41:01 +1030 Subject: [PATCH 06/69] Handle rule nesting, remove NestedSelector from SimpleSelector, improve logic for encapsulation --- .../src/compiler/phases/1-parse/read/style.js | 47 ++++++++++--------- .../compiler/phases/2-analyze/css/Selector.js | 21 +++++---- packages/svelte/src/compiler/types/css.d.ts | 13 +++-- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index ba000981d504..cf49b5d8daa4 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -129,14 +129,15 @@ function read_at_rule(parser) { /** * @param {import('../index.js').Parser} parser + * @param {boolean} nested Whether this rule is nested inside another rule * @returns {import('#compiler').Css.Rule} */ -function read_rule(parser) { +function read_rule(parser, nested = false) { const start = parser.index; return { type: 'Rule', - prelude: read_selector_list(parser), + prelude: read_selector_list(parser, nested), block: read_block(parser), start, end: parser.index @@ -145,16 +146,17 @@ function read_rule(parser) { /** * @param {import('../index.js').Parser} parser + * @param {boolean} nested Whether this selector list is nested inside another rule * @returns {import('#compiler').Css.SelectorList} */ -function read_selector_list(parser) { +function read_selector_list(parser, nested) { /** @type {import('#compiler').Css.Selector[]} */ const children = []; const start = parser.index; while (parser.index < parser.template.length) { - children.push(read_selector(parser)); + children.push(read_selector(parser, nested)); const end = parser.index; @@ -178,32 +180,33 @@ function read_selector_list(parser) { /** * @param {import('../index.js').Parser} parser + * @param {boolean} nested Whether this selector is nested inside another rule * @returns {import('#compiler').Css.Selector} */ -function read_selector(parser) { +function read_selector(parser, nested) { const list_start = parser.index; - /** @type {Array} */ + /** @type {Array} */ const children = []; while (parser.index < parser.template.length) { const start = parser.index; - if (parser.eat('*')) { - children.push({ - type: 'TypeSelector', - name: '*', - start, - end: parser.index - }); - } else if (parser.eat('&')){ - children.push({ - type: 'NestedSelector', - name: '&', - start, - end: parser.index - }); - } else if (parser.eat('#')) { + if (nested && parser.eat('&')){ + children.push({ + type: 'NestedSelector', + name: '&', + start, + end: parser.index + }); + } else if (parser.eat('*')) { + children.push({ + type: 'TypeSelector', + name: '*', + start, + end: parser.index + }); + } else if (parser.eat('#')) { children.push({ type: 'IdSelector', name: read_identifier(parser), @@ -396,7 +399,7 @@ function read_declaration_or_rule(parser) { // due to complexities with https://bugs.chromium.org/p/chromium/issues/detail?id=1427259 // as most browsers as of 17/11/2023 do not support nesting without & selector if (parser.match('&')) { - return read_rule(parser) + return read_rule(parser, true) } else { return read_declaration(parser); } 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 686685059846..8ca686976599 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -100,15 +100,12 @@ export default class Selector { while (i--) { const selector = block.selectors[i]; if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { - if (selector.name !== 'root' && selector.name !== 'host') { + if (!block.root && !block.host && !block.nested) { if (i === 0) code.prependRight(selector.start, attr); } continue; } - if (selector.type === "NestedSelector") { - // do we want to add the attr to the nested selector? - // it's kind of implied that it's a child of the parent selector - } else if (selector.type === 'TypeSelector' && selector.name === '*') { + if (selector.type === 'TypeSelector' && selector.name === '*') { code.update(selector.start, selector.end, attr); } else { code.appendLeft(selector.end, attr); @@ -123,7 +120,7 @@ export default class Selector { if (block.should_encapsulate) { encapsulate_block( block, - index === this.blocks.filter(block => block.nested !== true).length - 1 + index === this.blocks.length - 1 + (block.nested ? 1 : 0) ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); @@ -838,12 +835,18 @@ function group_selectors(selector) { selector.children.forEach((child) => { if (child.type === 'Combinator') { - block = new Block(child); - blocks.push(block); + if(block.nested && !block.combinator) { + block.combinator = child; + } else { + block = new Block(child); + blocks.push(block); + } + } else if (child.type === "NestedSelector") { + block.nested = true; } else { - if (child.type === "NestedSelector") block.nested = true; block.add(child); } }); + return blocks; } diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index c1adee00b9a8..cfbfd973815e 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -25,7 +25,7 @@ export interface SelectorList extends BaseNode { export interface Selector extends BaseNode { type: 'Selector'; - children: Array; + children: Array; } export interface TypeSelector extends BaseNode { @@ -67,11 +67,6 @@ export interface Percentage extends BaseNode { value: string; } -export interface NestedSelector extends BaseNode { - type: 'NestedSelector'; - name: "&"; -} - export type SimpleSelector = | TypeSelector | IdSelector @@ -79,7 +74,6 @@ export type SimpleSelector = | AttributeSelector | PseudoElementSelector | PseudoClassSelector - | NestedSelector | Percentage; export interface Combinator extends BaseNode { @@ -87,6 +81,11 @@ export interface Combinator extends BaseNode { name: string; } +export interface NestingSelector extends BaseNode { + type: 'NestedSelector'; + name: "&"; +} + export interface Block extends BaseNode { type: 'Block'; children: Array; From b59387b04637937d4698b0027d62af3621b52648 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 20 Nov 2023 15:56:35 +1030 Subject: [PATCH 07/69] use for of --- .../src/compiler/phases/2-analyze/css/Selector.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 8ca686976599..a93e547504a6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -828,14 +828,17 @@ class Block { } } -/** @param {import('#compiler').Css.Selector} selector */ +/** + * Groups selectors by combinator into blocks + * @param {import('#compiler').Css.Selector} selector + * */ function group_selectors(selector) { let block = new Block(null); const blocks = [block]; - selector.children.forEach((child) => { + for (const child of selector.children) { if (child.type === 'Combinator') { - if(block.nested && !block.combinator) { + if (block.nested && !block.combinator) { block.combinator = child; } else { block = new Block(child); @@ -846,7 +849,7 @@ function group_selectors(selector) { } else { block.add(child); } - }); + } return blocks; } From fcdc02e51bc29ee3280e6045fb3308556f71f4cd Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 27 Nov 2023 12:43:32 +1030 Subject: [PATCH 08/69] Got CSS combinator prefixes working Excluding CSS type element selectors closes #9320 closes #8587 --- CONTRIBUTING.md | 2 +- .../src/compiler/phases/1-parse/read/style.js | 19 +++++++++++++------ .../compiler/phases/2-analyze/css/Selector.js | 17 +++++++++++++++-- .../expected.css | 9 +++++++++ .../input.svelte | 17 +++++++++++++++++ .../expected.css | 0 .../input.svelte | 0 .../nested-css-child-combinator/expected.css | 9 +++++++++ .../nested-css-child-combinator/input.svelte | 17 +++++++++++++++++ .../expected.css | 9 +++++++++ .../input.svelte | 16 ++++++++++++++++ .../expected.css | 4 +++- .../input.svelte | 4 +++- .../css/samples/unknown-at-rule/expected.css | 4 +++- .../css/samples/unknown-at-rule/input.svelte | 4 +++- 15 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css-adjacent-combinator/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-adjacent-combinator/input.svelte rename packages/svelte/tests/css/samples/{nested-css => nested-css-ampersand-combinator}/expected.css (100%) rename packages/svelte/tests/css/samples/{nested-css => nested-css-ampersand-combinator}/input.svelte (100%) create mode 100644 packages/svelte/tests/css/samples/nested-css-child-combinator/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-child-combinator/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-descendant-combinator/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-descendant-combinator/input.svelte diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 924a0a752dcd..7a0338bbd9b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,7 +100,7 @@ Test samples are kept in `/test/xxx/samples` folder. > PREREQUISITE: Install chromium via playwright by running `pnpm playwright install chromium` 1. To run test, run `pnpm test`. -1. To run test for a specific feature, you can use the `-g` (aka `--grep`) option. For example, to only run test involving transitions, run `pnpm test -- -g transition`. +1. To run test for a specific feature, you can use the `-t` (aka `--testNamePattern `) option. For example, to only run test involving transitions, run `pnpm test -- -t transistion`. ##### Running solo test diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index cf49b5d8daa4..d8b7d3301f48 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -395,14 +395,21 @@ function read_declaration(parser) { * @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule} */ function read_declaration_or_rule(parser) { - // We will only allow css nesting using & selector for now + // We need to know if this is a rule or a declaration + // so we'll attempt to read an identifier first + // if we can't, we'll assume it's a rule + // This needs to change when we support type (element) selectors // due to complexities with https://bugs.chromium.org/p/chromium/issues/detail?id=1427259 - // as most browsers as of 17/11/2023 do not support nesting without & selector - if (parser.match('&')) { - return read_rule(parser, true) - } else { - return read_declaration(parser); + const start = parser.index; + const is_ident = !!parser.read(REGEX_VALID_IDENTIFIER_CHAR); + + parser.index = start; + + if (!is_ident) { + return read_rule(parser, true); } + + return read_declaration(parser); } /** 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 a93e547504a6..7ff629415dac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -321,6 +321,16 @@ function block_might_apply_to_node(block, node) { return NO_MATCH; } + if ( + block.nested && + block.selectors.length === 1 && + block.combinator !== null + ) { + // nested selector with combinator, eg: `.foo { + .bar { ... } }` + // TODO: How to handle this? + return UNKNOWN_SELECTOR; + } + if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') { continue; } @@ -831,14 +841,17 @@ class Block { /** * Groups selectors by combinator into blocks * @param {import('#compiler').Css.Selector} selector - * */ + */ function group_selectors(selector) { let block = new Block(null); const blocks = [block]; for (const child of selector.children) { if (child.type === 'Combinator') { - if (block.nested && !block.combinator) { + // If we start with a combinator, that means + // we're dealing with a nested selector + if(block.selectors.length === 0 && !block.combinator) { + block.nested = true; block.combinator = child; } else { block = new Block(child); diff --git a/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/expected.css new file mode 100644 index 000000000000..58a0fbec0c73 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/expected.css @@ -0,0 +1,9 @@ +button.svelte-xyz { + color: red; + + + .abc.svelte-xyz { + color: purple; + } + + color: black; +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/input.svelte new file mode 100644 index 000000000000..85a8e0131172 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/input.svelte @@ -0,0 +1,17 @@ + +
+ +
+ \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-combinator/expected.css similarity index 100% rename from packages/svelte/tests/css/samples/nested-css/expected.css rename to packages/svelte/tests/css/samples/nested-css-ampersand-combinator/expected.css diff --git a/packages/svelte/tests/css/samples/nested-css/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-combinator/input.svelte similarity index 100% rename from packages/svelte/tests/css/samples/nested-css/input.svelte rename to packages/svelte/tests/css/samples/nested-css-ampersand-combinator/input.svelte diff --git a/packages/svelte/tests/css/samples/nested-css-child-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-child-combinator/expected.css new file mode 100644 index 000000000000..7745ccd7f04a --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-child-combinator/expected.css @@ -0,0 +1,9 @@ +button.svelte-xyz { + color: red; + + > .abc.svelte-xyz { + color: purple; + } + + color: black; +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-child-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-child-combinator/input.svelte new file mode 100644 index 000000000000..2dd98dc84876 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-child-combinator/input.svelte @@ -0,0 +1,17 @@ + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-descendant-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-descendant-combinator/expected.css new file mode 100644 index 000000000000..2a26dba22224 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-descendant-combinator/expected.css @@ -0,0 +1,9 @@ +button.svelte-xyz { + color: red; + + .hello.svelte-xyz { + color: pink; + } + + color: black; +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-descendant-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-descendant-combinator/input.svelte new file mode 100644 index 000000000000..3641e8ff985c --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-descendant-combinator/input.svelte @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css index f75d9d89f2e0..a02fa365896d 100644 --- a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css +++ b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css @@ -1,5 +1,7 @@ div.svelte-xyz { - @apply --funky-div; + /* DISABLED THIS FOR CSS NESTING */ + /* @apply --funky-div; */ + color: red; } /* (empty) div.svelte-xyz { diff --git a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte index 6b75d37e3478..2de71392e19e 100644 --- a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte +++ b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte @@ -2,7 +2,9 @@ \ No newline at end of file From dab01b107aff97436ab1d4e88a6ee8f797780ab9 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 27 Nov 2023 12:45:28 +1030 Subject: [PATCH 09/69] closes #9320 closes #8587 From 05e959c77dda02a02c4c2b060406b3f5db7d09fd Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 27 Nov 2023 14:34:49 +1030 Subject: [PATCH 10/69] Implement full CSS nesting support - parsing of at-rules - parsing of element type selectors --- .../src/compiler/phases/1-parse/read/style.js | 95 ++++++++----------- .../compiler/phases/2-analyze/css/Selector.js | 2 +- .../nested-css-ampersand-suffix/expected.css | 9 ++ .../nested-css-ampersand-suffix/input.svelte | 16 ++++ .../samples/nested-css-at-rule/expected.css | 12 +++ .../samples/nested-css-at-rule/input.svelte | 20 ++++ .../nested-css-element-selector/expected.css | 7 ++ .../nested-css-element-selector/input.svelte | 14 +++ .../expected.css | 3 +- .../input.svelte | 3 +- .../css/samples/unknown-at-rule/expected.css | 3 +- .../css/samples/unknown-at-rule/input.svelte | 3 +- 12 files changed, 125 insertions(+), 62 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-at-rule/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-at-rule/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-element-selector/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-element-selector/input.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index d8b7d3301f48..bf60db96dfee 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -10,6 +10,7 @@ const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/; const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_BRACE_OR_SEMICOLON = /[{;]/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/; +const REGEX_SEMICOLON_OR_OPEN_BRACE_OR_CLOSE_BRACE = /[;{}]/; const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/; const REGEX_COMMENT_CLOSE = /\*\//; const REGEX_HTML_COMMENT_CLOSE = /-->/; @@ -84,36 +85,23 @@ function read_at_rule(parser) { let block = null; if (parser.match('{')) { - // if the parser could easily distinguish between rules and declarations, this wouldn't be necessary. - // but this approach is much simpler. in future, when we support CSS nesting, the parser _will_ need - // to be able to distinguish between them, but since we'll also need other changes to support that - // this remains a TODO - const contains_declarations = [ - 'color-profile', - 'counter-style', - 'font-face', - 'font-palette-values', - 'page', - 'property' - ].includes(name); - - if (contains_declarations) { - block = read_block(parser); - } else { - const start = parser.index; - - parser.eat('{', true); - const children = read_body(parser, '}'); - parser.eat('}', true); - - block = { - type: 'Block', - start, - end: parser.index, - children - }; - } + // // if the parser could easily distinguish between rules and declarations, this wouldn't be necessary. + // // but this approach is much simpler. in future, when we support CSS nesting, the parser _will_ need + // // to be able to distinguish between them, but since we'll also need other changes to support that + // // this remains a TODO + // const contains_declarations = [ + // 'color-profile', + // 'counter-style', + // 'font-face', + // 'font-palette-values', + // 'page', + // 'property' + // ].includes(name); + + // eg: `@media (max-width: 600px) { ... }` + block = read_block(parser); } else { + // eg: `@import 'foo';` parser.eat(';', true); } @@ -129,15 +117,14 @@ function read_at_rule(parser) { /** * @param {import('../index.js').Parser} parser - * @param {boolean} nested Whether this rule is nested inside another rule * @returns {import('#compiler').Css.Rule} */ -function read_rule(parser, nested = false) { +function read_rule(parser) { const start = parser.index; return { type: 'Rule', - prelude: read_selector_list(parser, nested), + prelude: read_selector_list(parser), block: read_block(parser), start, end: parser.index @@ -146,17 +133,16 @@ function read_rule(parser, nested = false) { /** * @param {import('../index.js').Parser} parser - * @param {boolean} nested Whether this selector list is nested inside another rule * @returns {import('#compiler').Css.SelectorList} */ -function read_selector_list(parser, nested) { +function read_selector_list(parser) { /** @type {import('#compiler').Css.Selector[]} */ const children = []; const start = parser.index; while (parser.index < parser.template.length) { - children.push(read_selector(parser, nested)); + children.push(read_selector(parser)); const end = parser.index; @@ -180,10 +166,9 @@ function read_selector_list(parser, nested) { /** * @param {import('../index.js').Parser} parser - * @param {boolean} nested Whether this selector is nested inside another rule * @returns {import('#compiler').Css.Selector} */ -function read_selector(parser, nested) { +function read_selector(parser) { const list_start = parser.index; /** @type {Array} */ @@ -192,7 +177,7 @@ function read_selector(parser, nested) { while (parser.index < parser.template.length) { const start = parser.index; - if (nested && parser.eat('&')){ + if (parser.eat('&')){ children.push({ type: 'NestedSelector', name: '&', @@ -338,7 +323,7 @@ function read_block(parser) { parser.eat('{', true); - /** @type {Array} */ + /** @type {Array} */ const children = []; while (parser.index < parser.template.length) { @@ -347,7 +332,7 @@ function read_block(parser) { if (parser.match('}')) { break; } else { - children.push(read_declaration_or_rule(parser)); + children.push(read_block_item(parser)); } } @@ -391,24 +376,28 @@ function read_declaration(parser) { } /** + * Reads a declaration, rule or at-rule + * * @param {import('../index.js').Parser} parser - * @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule} + * @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule} */ -function read_declaration_or_rule(parser) { - // We need to know if this is a rule or a declaration - // so we'll attempt to read an identifier first - // if we can't, we'll assume it's a rule - // This needs to change when we support type (element) selectors - // due to complexities with https://bugs.chromium.org/p/chromium/issues/detail?id=1427259 - const start = parser.index; - const is_ident = !!parser.read(REGEX_VALID_IDENTIFIER_CHAR); +function read_block_item(parser) { + if (parser.match('@')) { + return read_at_rule(parser); + } - parser.index = start; + const start = parser.index; + parser.read_until(REGEX_SEMICOLON_OR_OPEN_BRACE_OR_CLOSE_BRACE); - if (!is_ident) { - return read_rule(parser, true); + // if we've run into a '{', it's a rule, otherwise we ran into + // a ';' or '}' so it's a declaration + if (parser.match('{')) { + // Rewind to the start of the rule + parser.index = start; + return read_rule(parser); } - + // Rewind to the start of the declaration + parser.index = start; return read_declaration(parser); } 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 7ff629415dac..29b67ce27234 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -172,7 +172,7 @@ export default class Selector { 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) { + if (block.selectors.length === 0 && !block.nested) { error(this.node, 'invalid-css-selector'); } } diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css new file mode 100644 index 000000000000..ae1d3885ea98 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css @@ -0,0 +1,9 @@ +button.svelte-xyz { + color: red; + + .xyz.svelte-xyz & { + color: yellow; + } + + color: black; +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte new file mode 100644 index 000000000000..1aa3bca7ce01 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte @@ -0,0 +1,16 @@ +
+ +
+ \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-at-rule/expected.css b/packages/svelte/tests/css/samples/nested-css-at-rule/expected.css new file mode 100644 index 000000000000..a7f507bd495c --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-at-rule/expected.css @@ -0,0 +1,12 @@ +button.svelte-xyz { + color: red; + + @media (max-width: 500px) { + color: blue; + .xyz.svelte-xyz { + color: yellow; + } + } + + color: black; +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-at-rule/input.svelte b/packages/svelte/tests/css/samples/nested-css-at-rule/input.svelte new file mode 100644 index 000000000000..cd07582c6ea7 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-at-rule/input.svelte @@ -0,0 +1,20 @@ + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-element-selector/expected.css b/packages/svelte/tests/css/samples/nested-css-element-selector/expected.css new file mode 100644 index 000000000000..7efa5e5367a0 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-element-selector/expected.css @@ -0,0 +1,7 @@ +div.svelte-xyz { + color: red; + + button.svelte-xyz { + color: yellow; + } +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-element-selector/input.svelte b/packages/svelte/tests/css/samples/nested-css-element-selector/input.svelte new file mode 100644 index 000000000000..9f839fc0f011 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-element-selector/input.svelte @@ -0,0 +1,14 @@ +
+ +
+ \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css index a02fa365896d..a7bc48137bc0 100644 --- a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css +++ b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/expected.css @@ -1,6 +1,5 @@ div.svelte-xyz { - /* DISABLED THIS FOR CSS NESTING */ - /* @apply --funky-div; */ + @apply --funky-div; color: red; } diff --git a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte index 2de71392e19e..06a8726707fe 100644 --- a/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte +++ b/packages/svelte/tests/css/samples/unknown-at-rule-with-following-rules/input.svelte @@ -2,8 +2,7 @@ \ No newline at end of file From 1ba667dadc18b69137522af11a02627ef559947a Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 27 Nov 2023 14:37:46 +1030 Subject: [PATCH 11/69] Update changeset message --- .changeset/small-kids-switch.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/small-kids-switch.md b/.changeset/small-kids-switch.md index 3d3d81b72d99..dd92aad7cfe6 100644 --- a/.changeset/small-kids-switch.md +++ b/.changeset/small-kids-switch.md @@ -2,4 +2,10 @@ 'svelte': patch --- -Implement CSS nesting with & prefix requirement +Implement CSS nesting support +- [x] CSS rule nesting using `&` prefix (ie `.foo { & div { color: red } }`) +- [x] CSS At-Rules nesting +- [x] CSS rule nesting without `&` type (element) selector (ie. `.foo { div { color: red } }`) +- [x] CSS rule nesting without `&` for CSS Combinators (ie. `.foo { + div { color: red } }`) +- [x] CSS rule nesting without `&` for class & ID selectors (ie. `.foo { .bar { color: red } }`) +- [x] Appending the `&` nesting selector to reverse rule context (ie. `.foo { .bar & { color: red } }`, equiv to `.bar { .foo { color: red } }`) \ No newline at end of file From b0397d190e239559f441c8cb10be6c13d5656629 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 27 Nov 2023 14:48:30 +1030 Subject: [PATCH 12/69] Remove some unused code/comments --- .../src/compiler/phases/1-parse/read/style.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index bf60db96dfee..910a8a31c455 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -84,21 +84,8 @@ function read_at_rule(parser) { /** @type {import('#compiler').Css.Block | null} */ let block = null; + // eg: `@media (max-width: 600px) { ... }` if (parser.match('{')) { - // // if the parser could easily distinguish between rules and declarations, this wouldn't be necessary. - // // but this approach is much simpler. in future, when we support CSS nesting, the parser _will_ need - // // to be able to distinguish between them, but since we'll also need other changes to support that - // // this remains a TODO - // const contains_declarations = [ - // 'color-profile', - // 'counter-style', - // 'font-face', - // 'font-palette-values', - // 'page', - // 'property' - // ].includes(name); - - // eg: `@media (max-width: 600px) { ... }` block = read_block(parser); } else { // eg: `@import 'foo';` From a37b9755976e19791faa0002a4dc21a2e5259b04 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 27 Nov 2023 16:43:50 +1030 Subject: [PATCH 13/69] Update .changeset/small-kids-switch.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- .changeset/small-kids-switch.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.changeset/small-kids-switch.md b/.changeset/small-kids-switch.md index dd92aad7cfe6..85460c87a99d 100644 --- a/.changeset/small-kids-switch.md +++ b/.changeset/small-kids-switch.md @@ -2,10 +2,4 @@ 'svelte': patch --- -Implement CSS nesting support -- [x] CSS rule nesting using `&` prefix (ie `.foo { & div { color: red } }`) -- [x] CSS At-Rules nesting -- [x] CSS rule nesting without `&` type (element) selector (ie. `.foo { div { color: red } }`) -- [x] CSS rule nesting without `&` for CSS Combinators (ie. `.foo { + div { color: red } }`) -- [x] CSS rule nesting without `&` for class & ID selectors (ie. `.foo { .bar { color: red } }`) -- [x] Appending the `&` nesting selector to reverse rule context (ie. `.foo { .bar & { color: red } }`, equiv to `.bar { .foo { color: red } }`) \ No newline at end of file +feat: CSS nesting support \ No newline at end of file From 48c6cdf0d3dc13242002af150555004d4b9592b8 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Thu, 30 Nov 2023 09:21:50 +1030 Subject: [PATCH 14/69] Revert contributor change --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7a0338bbd9b8..924a0a752dcd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,7 +100,7 @@ Test samples are kept in `/test/xxx/samples` folder. > PREREQUISITE: Install chromium via playwright by running `pnpm playwright install chromium` 1. To run test, run `pnpm test`. -1. To run test for a specific feature, you can use the `-t` (aka `--testNamePattern `) option. For example, to only run test involving transitions, run `pnpm test -- -t transistion`. +1. To run test for a specific feature, you can use the `-g` (aka `--grep`) option. For example, to only run test involving transitions, run `pnpm test -- -g transition`. ##### Running solo test From 2b4e73026c47282f4c9cf818e6e577a36169b000 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Thu, 30 Nov 2023 09:26:00 +1030 Subject: [PATCH 15/69] Minor change Not sure how to handle unused selectors & etc with CSS nestign --- .../svelte/src/compiler/phases/2-analyze/css/Selector.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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 29b67ce27234..30bdb716d225 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -321,13 +321,8 @@ function block_might_apply_to_node(block, node) { return NO_MATCH; } - if ( - block.nested && - block.selectors.length === 1 && - block.combinator !== null - ) { - // nested selector with combinator, eg: `.foo { + .bar { ... } }` - // TODO: How to handle this? + if (block.nested) { + // TODO: How to handle knowing whether a nested selector was used? return UNKNOWN_SELECTOR; } From b6b872c0351fd74288a8d259d2c1c57142eaaee6 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Thu, 30 Nov 2023 09:28:12 +1030 Subject: [PATCH 16/69] Modify comment --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 1 + 1 file changed, 1 insertion(+) 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 30bdb716d225..b0afea4b5484 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -323,6 +323,7 @@ function block_might_apply_to_node(block, node) { if (block.nested) { // TODO: How to handle knowing whether a nested selector was used? + // For now, we'll just assume it was used to prevent encapsulation with (unused) return UNKNOWN_SELECTOR; } From a8fb36d0e0b71da538a15c9d8a6b6d8776a254b3 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Thu, 30 Nov 2023 10:36:27 +1030 Subject: [PATCH 17/69] Add additional test fixing bug with multiple nested layers --- .../compiler/phases/2-analyze/css/Selector.js | 10 ++++++-- .../phases/2-analyze/css/Stylesheet.js | 2 +- .../nested-css-multiple-layers/expected.css | 13 +++++++++++ .../nested-css-multiple-layers/input.svelte | 23 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte 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 b0afea4b5484..da8e01be70cb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -35,11 +35,15 @@ export default class Selector { /** @type {boolean} */ used; + /** @type {boolean} */ + nested; + /** * @param {import('#compiler').Css.Selector} node * @param {import('./Stylesheet.js').default} stylesheet + * @param {boolean} nested */ - constructor(node, stylesheet) { + constructor(node, stylesheet, nested) { this.node = node; this.stylesheet = stylesheet; this.blocks = group_selectors(node); @@ -53,6 +57,7 @@ export default class Selector { const host_only = this.blocks.length === 1 && this.blocks[0].host; const root_only = this.blocks.length === 1 && this.blocks[0].root; this.used = this.local_blocks.length === 0 || host_only || root_only; + this.nested = nested; } /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ @@ -99,6 +104,7 @@ export default class Selector { let i = block.selectors.length; while (i--) { const selector = block.selectors[i]; + if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { if (!block.root && !block.host && !block.nested) { if (i === 0) code.prependRight(selector.start, attr); @@ -120,7 +126,7 @@ export default class Selector { if (block.should_encapsulate) { encapsulate_block( block, - index === this.blocks.length - 1 + (block.nested ? 1 : 0) + index === this.blocks.length - 1 + (this.nested ? 1 : 0) ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); 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 5c6a98d8a780..203a506a6f6a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -72,7 +72,7 @@ class Rule { constructor(node, stylesheet, parent) { this.node = node; this.parent = parent; - this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet)); + this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet, parent?.node.type === "Rule")); this.nested_rules = node.block.children .filter((node) => node.type === 'Rule') diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css b/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css new file mode 100644 index 000000000000..55b845121bf1 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css @@ -0,0 +1,13 @@ +a.svelte-xyz { + color: red; + b.svelte-xyz { + color: yellow; + c.svelte-xyz { + color: green; + } + } + + b.svelte-xyz c.svelte-xyz { + color: blue; + } +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte b/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte new file mode 100644 index 000000000000..7c4b35d01566 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file From 92f1e3a92914aa5785a844eb122f9cff5779f706 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 1 Feb 2024 09:44:25 -0500 Subject: [PATCH 18/69] fix syntax error --- packages/svelte/src/compiler/phases/1-parse/read/style.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 8a09a252b4bf..7593c3a0cc18 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -168,7 +168,7 @@ function read_selector(parser, inside_pseudo_class = false) { while (parser.index < parser.template.length) { const start = parser.index; - if (parser.eat('&')){ + if (parser.eat('&')) { children.push({ type: 'NestedSelector', name: '&', @@ -176,7 +176,6 @@ function read_selector(parser, inside_pseudo_class = false) { end: parser.index }); } else if (parser.eat('*')) { - if (parser.eat('*')) { let name = '*'; if (parser.match('|')) { // * is the namespace (which we ignore) From 6373f1604fe7007c7f5242a1f5215d276dcf53d4 Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 4 Feb 2024 02:30:42 +1030 Subject: [PATCH 19/69] Handle CSS nesting by adding invisible selectors before nested rules --- .../compiler/phases/2-analyze/css/Selector.js | 96 ++++++++++++------- .../phases/2-analyze/css/Stylesheet.js | 26 ++++- .../nested-css-ampersand-suffix/expected.css | 4 +- .../nested-css-ampersand-suffix/input.svelte | 13 +-- .../nested-css-multiple-layers/expected.css | 6 +- .../nested-css-multiple-layers/input.svelte | 6 ++ 6 files changed, 108 insertions(+), 43 deletions(-) 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 9b6ed5dadce3..58ed4762f7cf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -34,18 +34,15 @@ export default class Selector { /** @type {boolean} */ used; - /** @type {boolean} */ - nested; - /** * @param {import('#compiler').Css.Selector} node * @param {import('./Stylesheet.js').default} stylesheet - * @param {boolean} nested + * @param {Selector | null} parent_selector */ - constructor(node, stylesheet, nested) { + constructor(node, stylesheet, parent_selector) { this.node = node; this.stylesheet = stylesheet; - this.blocks = group_selectors(node); + this.blocks = group_selectors(node, parent_selector); // take trailing :global(...) selectors out of consideration let i = this.blocks.length; while (i > 0) { @@ -56,7 +53,6 @@ export default class Selector { const host_only = this.blocks.length === 1 && this.blocks[0].host; const root_only = this.blocks.length === 1 && this.blocks[0].root; this.used = this.local_blocks.length === 0 || host_only || root_only; - this.nested = nested; } /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ @@ -101,11 +97,19 @@ export default 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 && !block.nested) { + if (!block.root && !block.host) { if (i === 0) code.prependRight(selector.start, attr); } continue; @@ -125,7 +129,7 @@ export default class Selector { if (block.should_encapsulate) { encapsulate_block( block, - index === this.blocks.length - 1 + (this.nested ? 1 : 0) + index === this.blocks.filter(block => !block.invisible).length - 1 ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); @@ -176,7 +180,7 @@ export default class Selector { 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 && !block.nested) { + if (block.selectors.length === 0) { error(this.node, 'invalid-css-selector'); } } @@ -331,12 +335,6 @@ function block_might_apply_to_node(block, node) { return NO_MATCH; } - if (block.nested) { - // TODO: How to handle knowing whether a nested selector was used? - // For now, we'll just assume it was used to prevent encapsulation with (unused) - return UNKNOWN_SELECTOR; - } - if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') { continue; } @@ -797,7 +795,7 @@ class Block { /** @type {import('#compiler').Css.Combinator | null} */ combinator; - /** @type {import('#compiler').Css.SimpleSelector[]} */ + /** @type {(import('#compiler').Css.SimpleSelector & { invisible?: boolean})[]} */ selectors; /** @type {number} */ @@ -809,16 +807,19 @@ class Block { /** @type {boolean} */ should_encapsulate; + /** @type {boolean} */ + invisible + /** @param {import('#compiler').Css.Combinator | null} combinator */ constructor(combinator) { this.combinator = combinator; this.host = false; this.root = false; - this.nested = false; this.selectors = []; this.start = -1; this.end = -1; this.should_encapsulate = false; + this.invisible = false } /** @param {import('#compiler').Css.SimpleSelector} selector */ @@ -844,31 +845,62 @@ class Block { } } +const InvisibleCombinator = { type: /** @type {"Combinator"} **/ ("Combinator"), name: ' ', start: -1, end: -1} + /** * Groups selectors by combinator into blocks + * + * If there is a parent_selector + * - We need to find the position of the `&` selector or front if there is no `&` selector + * - Then insert the parent_selector's blocks at that position + * * @param {import('#compiler').Css.Selector} selector + * @param {Selector | null} parent_selector */ -function group_selectors(selector) { +function group_selectors(selector, parent_selector) { let block = new Block(null); + const blocks = [block]; - for (const child of selector.children) { - if (child.type === 'Combinator') { - // If we start with a combinator, that means - // we're dealing with a nested selector - if(block.selectors.length === 0 && !block.combinator) { - block.nested = true; - block.combinator = child; - } else { - block = new Block(child); - blocks.push(block); + const real_selectors_start = parent_selector?.node.children.length || 0; + + if (parent_selector) { + const nested_rule_indices = selector.children + .map((child, index) => (child.type === 'NestedSelector' ? index : -1)) + .filter(index => index !== -1); + + const parent_children = parent_selector.node.children.map(child => ({ + ...child, + invisible: 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); } - } else if (child.type === "NestedSelector") { - block.nested = true; + selector.children.unshift(...parent_children); } else { - block.add(child); + // 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)); } } + selector.children.forEach((child, i) => { + if (child.type === 'Combinator') { + block = new Block(child); + blocks.push(block); + } else if (child.type === 'NestedSelector') { + // Don't think we need to add it here + } else { + block.add(child); + } + if (real_selectors_start > i) { + block.invisible = true; + } + }); + return blocks; } 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 203a506a6f6a..9bba7467ed16 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -48,7 +48,7 @@ function escape_comment_close(node, code) { } } -class Rule { +export class Rule { /** @type {import('./Selector.js').default[]} */ selectors; @@ -72,7 +72,29 @@ class Rule { constructor(node, stylesheet, parent) { this.node = node; this.parent = parent; - this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet, parent?.node.type === "Rule")); + + /** + * We need to add selectors for each parent rule's selectors + * because of CSS nesting. For example: + * ```css + * .a, .b { + * .c { + * color: red; + * } + * } + * ``` + * Results in the following selectors: + * - .a .c + * - .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() + } else { + this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet, null)); + } this.nested_rules = node.block.children .filter((node) => node.type === 'Rule') diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css index ae1d3885ea98..2fe0503db551 100644 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css @@ -1,7 +1,7 @@ -button.svelte-xyz { +a.svelte-xyz { color: red; - .xyz.svelte-xyz & { + b & { color: yellow; } diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte index 1aa3bca7ce01..91a0532d97f5 100644 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte @@ -1,16 +1,17 @@ -
- -
+ + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css b/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css index 55b845121bf1..8a6dd5548275 100644 --- a/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css +++ b/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css @@ -7,7 +7,11 @@ a.svelte-xyz { } } - b.svelte-xyz c.svelte-xyz { + b c.svelte-xyz { color: blue; } + + b c d.svelte-xyz { + color: orange; + } } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte b/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte index 7c4b35d01566..7cf031528544 100644 --- a/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte +++ b/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte @@ -1,7 +1,9 @@ + + @@ -18,6 +20,10 @@ a { b c { color: blue; } + + b c d { + color: orange; + } } \ No newline at end of file From b5444cff08f2471453558905322fb07d82000d67 Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 4 Feb 2024 02:34:37 +1030 Subject: [PATCH 20/69] Correct comment --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 58ed4762f7cf..2768e5053236 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -851,7 +851,7 @@ const InvisibleCombinator = { type: /** @type {"Combinator"} **/ ("Combinator"), * Groups selectors by combinator into blocks * * If there is a parent_selector - * - We need to find the position of the `&` selector or front if there is no `&` selector + * - We need to find the position(s) of the `&` selector or upshift them at the front if there is no `&` selector * - Then insert the parent_selector's blocks at that position * * @param {import('#compiler').Css.Selector} selector From 7e2875b40089bfee1fb33b6f41c724214cd42ac9 Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 4 Feb 2024 02:38:56 +1030 Subject: [PATCH 21/69] Added a failing test --- .../phases/2-analyze/css/Stylesheet.js | 6 +++--- .../expected.css | 9 +++++++++ .../input.svelte | 20 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css-with-selector-list/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte 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 9bba7467ed16..45c337119055 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -89,9 +89,9 @@ export class Rule { */ 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() + .selectors + .map(parent_selector => node.prelude.children.map((node) => new Selector(node, stylesheet, parent_selector))) + .flat() } else { this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet, null)); } diff --git a/packages/svelte/tests/css/samples/nested-css-with-selector-list/expected.css b/packages/svelte/tests/css/samples/nested-css-with-selector-list/expected.css new file mode 100644 index 000000000000..2c2d32faa3c0 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-with-selector-list/expected.css @@ -0,0 +1,9 @@ +a.svelte-xyz, b.svelte-xyz { + color: red; + + x.svelte-xyz { + color: purple; + } + + color: black; +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte b/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte new file mode 100644 index 000000000000..fe97359f4c95 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte @@ -0,0 +1,20 @@ + + + + + + +
+ +
+ \ No newline at end of file From 61497b2616e249cb8821a7587f071eb4cf412333 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Feb 2024 16:28:43 -0500 Subject: [PATCH 22/69] prettier --- .../compiler/phases/2-analyze/css/Selector.js | 27 ++++++++++++------- .../phases/2-analyze/css/Stylesheet.js | 23 +++++++++------- packages/svelte/src/compiler/types/css.d.ts | 2 +- 3 files changed, 32 insertions(+), 20 deletions(-) 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 2768e5053236..a4c4262a96d2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -104,8 +104,8 @@ export default class Selector { // 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.invisible) { + continue; } if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { @@ -129,7 +129,7 @@ export default class Selector { if (block.should_encapsulate) { encapsulate_block( block, - index === this.blocks.filter(block => !block.invisible).length - 1 + index === this.blocks.filter((block) => !block.invisible).length - 1 ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); @@ -808,7 +808,7 @@ class Block { should_encapsulate; /** @type {boolean} */ - invisible + invisible; /** @param {import('#compiler').Css.Combinator | null} combinator */ constructor(combinator) { @@ -819,7 +819,7 @@ class Block { this.start = -1; this.end = -1; this.should_encapsulate = false; - this.invisible = false + this.invisible = false; } /** @param {import('#compiler').Css.SimpleSelector} selector */ @@ -845,7 +845,12 @@ class Block { } } -const InvisibleCombinator = { type: /** @type {"Combinator"} **/ ("Combinator"), name: ' ', start: -1, end: -1} +const InvisibleCombinator = { + type: /** @type {"Combinator"} **/ ('Combinator'), + name: ' ', + start: -1, + end: -1 +}; /** * Groups selectors by combinator into blocks @@ -867,11 +872,11 @@ function group_selectors(selector, parent_selector) { if (parent_selector) { const nested_rule_indices = selector.children .map((child, index) => (child.type === 'NestedSelector' ? index : -1)) - .filter(index => index !== -1); + .filter((index) => index !== -1); - const parent_children = parent_selector.node.children.map(child => ({ + const parent_children = parent_selector.node.children.map((child) => ({ ...child, - invisible: true, + invisible: true })); if (nested_rule_indices.length === 0) { @@ -884,7 +889,9 @@ function group_selectors(selector, parent_selector) { } 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)); + nested_rule_indices.forEach((nested_rule_index) => + selector.children.splice(nested_rule_index, 1, ...parent_children) + ); } } 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 45c337119055..597351f433ec 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -88,17 +88,18 @@ export 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() + this.selectors = /** @type {Rule} **/ (parent).selectors + .map((parent_selector) => + node.prelude.children.map((node) => new Selector(node, stylesheet, parent_selector)) + ) + .flat(); } else { this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet, null)); } this.nested_rules = node.block.children .filter((node) => node.type === 'Rule') - .map(node => new Rule(/** @type {import('#compiler').Css.Rule}*/ (node), stylesheet, this)); + .map((node) => new Rule(/** @type {import('#compiler').Css.Rule}*/ (node), stylesheet, this)); this.declarations = node.block.children .filter((node) => node.type === 'Declaration') .map((node) => new Declaration(/** @type {import('#compiler').Css.Declaration} */ (node))); @@ -122,7 +123,9 @@ export class Rule { // see them in devtools if (this.declarations.length === 0) return dev; - return [this.selectors.some((s) => s.used), this.nested_rules.some(r => r.is_used(dev))].some(Boolean); + return [this.selectors.some((s) => s.used), this.nested_rules.some((r) => r.is_used(dev))].some( + Boolean + ); } /** @@ -141,7 +144,9 @@ export class Rule { selector.transform(code, attr, max_amount_class_specificity_increased) ); this.declarations.forEach((declaration) => declaration.transform(code, keyframes)); - this.nested_rules.forEach((rule) => rule.transform(code, id, keyframes, max_amount_class_specificity_increased)); + this.nested_rules.forEach((rule) => + rule.transform(code, id, keyframes, max_amount_class_specificity_increased) + ); } /** @param {import('../../types.js').ComponentAnalysis} analysis */ @@ -430,7 +435,7 @@ export default class Stylesheet { const state = { /** @type {Atrule | undefined} */ - atrule: undefined, + atrule: undefined }; walk(/** @type {import('#compiler').Css.Node} */ (ast), state, { @@ -478,7 +483,7 @@ export default class Stylesheet { if (rule.nested_rules.length > 0) { // Skip nested rules as they are instantiated in the Rule constructor - return node + return node; } context.next(); } diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 0aa905a44829..2ca708fc7c5b 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -89,7 +89,7 @@ export interface Combinator extends BaseNode { export interface NestingSelector extends BaseNode { type: 'NestedSelector'; - name: "&"; + name: '&'; } export interface Block extends BaseNode { From 751c73cd22d03355ec501dace24328c68c35bebe Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Feb 2024 16:45:56 -0500 Subject: [PATCH 23/69] partial fix --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 2 +- .../css/samples/nested-css-ampersand-suffix/expected.css | 4 ++-- .../css/samples/nested-css-with-selector-list/input.svelte | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 a4c4262a96d2..f58ca62b73bf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -77,7 +77,7 @@ export default class Selector { transform(code, attr, max_amount_class_specificity_increased) { const amount_class_specificity_to_increase = max_amount_class_specificity_increased - - this.blocks.filter((block) => block.should_encapsulate).length; + this.blocks.filter((block) => !block.invisible && block.should_encapsulate).length; /** @param {import('#compiler').Css.SimpleSelector} selector */ function remove_global_pseudo_class(selector) { diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css index 2fe0503db551..8f108793820b 100644 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css @@ -1,9 +1,9 @@ a.svelte-xyz { color: red; - b & { + b.svelte-xyz & { color: yellow; } color: black; -} \ No newline at end of file +} diff --git a/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte b/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte index fe97359f4c95..69ac2adcc796 100644 --- a/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte +++ b/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte @@ -17,4 +17,4 @@ a, b { color: black; } - \ No newline at end of file + From bdccdcb8bfdc7aacbc9c600d9c0c23998667e0fe Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Feb 2024 16:46:55 -0500 Subject: [PATCH 24/69] remove unused export keyword --- packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 597351f433ec..7d4247ef15e1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -48,7 +48,7 @@ function escape_comment_close(node, code) { } } -export class Rule { +class Rule { /** @type {import('./Selector.js').default[]} */ selectors; From 633e05a5a6554a0e62b7947b2026c50d0d912744 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Feb 2024 16:47:51 -0500 Subject: [PATCH 25/69] move type annotation --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 f58ca62b73bf..0f5e5742eefe 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -845,8 +845,9 @@ class Block { } } +/** @type {import('#compiler').Css.Combinator} */ const InvisibleCombinator = { - type: /** @type {"Combinator"} **/ ('Combinator'), + type: 'Combinator', name: ' ', start: -1, end: -1 From 4d4d9736225a63335f5a07d70c81e0bf397b1a5c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 6 Feb 2024 16:50:11 -0500 Subject: [PATCH 26/69] use NestingSelector everywhere, include in SimpleSelector --- .../src/compiler/phases/1-parse/read/style.js | 4 ++-- .../src/compiler/phases/2-analyze/css/Selector.js | 4 ++-- packages/svelte/src/compiler/types/css.d.ts | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 7593c3a0cc18..79707c59b114 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -162,7 +162,7 @@ function read_selector_list(parser, inside_pseudo_class = false) { function read_selector(parser, inside_pseudo_class = false) { const list_start = parser.index; - /** @type {Array} */ + /** @type {Array} */ const children = []; while (parser.index < parser.template.length) { @@ -170,7 +170,7 @@ function read_selector(parser, inside_pseudo_class = false) { if (parser.eat('&')) { children.push({ - type: 'NestedSelector', + type: 'NestingSelector', name: '&', start, end: parser.index 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 0f5e5742eefe..810b308ad65a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -872,7 +872,7 @@ function group_selectors(selector, parent_selector) { if (parent_selector) { const nested_rule_indices = selector.children - .map((child, index) => (child.type === 'NestedSelector' ? index : -1)) + .map((child, index) => (child.type === 'NestingSelector' ? index : -1)) .filter((index) => index !== -1); const parent_children = parent_selector.node.children.map((child) => ({ @@ -900,7 +900,7 @@ function group_selectors(selector, parent_selector) { if (child.type === 'Combinator') { block = new Block(child); blocks.push(block); - } else if (child.type === 'NestedSelector') { + } else if (child.type === 'NestingSelector') { // Don't think we need to add it here } else { block.add(child); diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 2ca708fc7c5b..8de0254ca932 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -25,7 +25,7 @@ export interface SelectorList extends BaseNode { export interface Selector extends BaseNode { type: 'Selector'; - children: Array; + children: Array; } export interface TypeSelector extends BaseNode { @@ -72,6 +72,11 @@ export interface Nth extends BaseNode { value: string; } +export interface NestingSelector extends BaseNode { + type: 'NestingSelector'; + name: '&'; +} + export type SimpleSelector = | TypeSelector | IdSelector @@ -80,18 +85,14 @@ export type SimpleSelector = | PseudoElementSelector | PseudoClassSelector | Percentage - | Nth; + | Nth + | NestingSelector; export interface Combinator extends BaseNode { type: 'Combinator'; name: string; } -export interface NestingSelector extends BaseNode { - type: 'NestedSelector'; - name: '&'; -} - export interface Block extends BaseNode { type: 'Block'; children: Array; From b884daec36841507cab8fabb590af1b5191a8320 Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 10:36:56 +1030 Subject: [PATCH 27/69] Push some broken code, added block nested groupings --- packages/svelte/src/compiler/errors.js | 3 +- .../compiler/phases/2-analyze/css/Selector.js | 119 ++++++++++++------ .../phases/2-analyze/css/Stylesheet.js | 11 +- 3 files changed, 84 insertions(+), 49 deletions(-) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index fa0964636f71..6b8d3333d71b 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 810b308ad65a..914ce4aa4012 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 7d4247ef15e1..4ca39900eda6 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)); } From 46efb43d7c0e81885eed109452f63d4424955b36 Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 11:38:08 +1030 Subject: [PATCH 28/69] Started working on encapsulation logic --- .../compiler/phases/2-analyze/css/Selector.js | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) 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 914ce4aa4012..aa5e5f0e02b6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -28,8 +28,8 @@ export default class Selector { /** @type {BlockUse[][]} */ block_groups; - /** @type {Block[]} */ - local_blocks; + /** @type {BlockUse[][]} */ + local_block_groups; /** @type {boolean} */ used; @@ -43,27 +43,57 @@ export default class Selector { this.node = node; this.stylesheet = stylesheet; this.block_groups = group_selectors(node, parent_blocks); - // take trailing :global(...) selectors out of consideration - let i = this.blocks.length; - while (i > 0) { - if (!this.blocks[i - 1].global) break; - i -= 1; + + this.used = false; + + // Initialize local_block_groups + this.local_block_groups = []; + + // Process each block group to take trailing :global(...) selectors out of consideration + this.block_groups.forEach(group => { + let i = group.length; + while (i > 0) { + if (!group[i - 1].block.global) break; + i -= 1; + } + // Add the processed group (with global selectors removed) to local_block_groups + this.local_block_groups.push(group.slice(0, i)); + }); + + // Determine `used` based on the processed local_block_groups + let host_only = false; + let root_only = false; + + // Check if there's exactly one group and one block within that group, and if it's host or root + if (this.local_block_groups.length === 1 && this.local_block_groups[0].length === 1) { + const single_block = this.local_block_groups[0][0].block; + host_only = single_block.host; + root_only = single_block.root; } - this.local_blocks = this.blocks.slice(0, i); - const host_only = this.blocks.length === 1 && this.blocks[0].host; - const root_only = this.blocks.length === 1 && this.blocks[0].root; - this.used = this.local_blocks.length === 0 || host_only || root_only; + + // Check if there are no local blocks across all groups, or if there's a host_only or root_only situation + const no_local_blocks = this.local_block_groups.every(group => group.length === 0); + this.used = no_local_blocks || host_only || root_only; + } /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ apply(node) { /** @type {Array<{ node: import('#compiler').RegularElement | import('#compiler').SvelteElement; block: Block }>} */ const to_encapsulate = []; - apply_selector(this.local_blocks.slice(), node, to_encapsulate); + this.local_block_groups.map(group => { + const blocks = group.map(block_use => block_use.block); + apply_selector(blocks, node, to_encapsulate); + }); + if (to_encapsulate.length > 0) { to_encapsulate.forEach(({ node, block }) => { - this.stylesheet.nodes_with_css_class.add(node); - block.should_encapsulate = true; + // 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); + } }); this.used = true; } @@ -807,6 +837,9 @@ class Block { /** @type {boolean} */ should_encapsulate; + /** @type {boolean} */ + has_been_encapsulated; + /** @type {boolean} */ invisible; @@ -820,6 +853,7 @@ class Block { this.end = -1; this.should_encapsulate = false; this.invisible = false; + this.has_been_encapsulated = false; } /** @param {import('#compiler').Css.SimpleSelector} selector */ @@ -870,15 +904,13 @@ class BlockUse { } } + /** - * Groups selectors by combinator into blocks + * Groups selectors and inserts parent blocks into nested rules. * - * If there is a parent_selector - * - We need to find the position(s) of the `&` selector or upshift them at the front if there is no `&` selector - * - Then insert the parent_selector's blocks at that position - * - * @param {import('#compiler').Css.Selector} selector - * @param {BlockUse[][] | null} parent_blocks_group + * @param {import('#compiler').Css.Selector} selector - The selector to group and analyze. + * @param {Array> | null} parent_blocks_group - The parent blocks group to insert into nested rules. + * @returns {Array>} - The grouped selectors with parent's blocks inserted if nested. */ function group_selectors(selector, parent_blocks_group) { // If it isn't a nested rule, then we add an empty block group From 0708f16f3c546a83fc8390401b52b9f34afda42d Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 12:39:23 +1030 Subject: [PATCH 29/69] Wohooo. Almost working --- .../compiler/phases/2-analyze/css/Selector.js | 316 +++++++++++------- 1 file changed, 194 insertions(+), 122 deletions(-) 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 aa5e5f0e02b6..bb2cea87189e 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; } From eef0b3bf6c7968dde02e7432fe5a4d1c3bf5a3ab Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 13:17:25 +1030 Subject: [PATCH 30/69] 2 failing tests --- .../compiler/phases/2-analyze/css/Selector.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) 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 bb2cea87189e..a49d29a286b5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -988,7 +988,7 @@ 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)) + selector_to_blocks(selector, false).map((block) => new BlockUse(block, true)) ]; } @@ -996,6 +996,10 @@ function group_selectors(selector, parent_blocks_group) { // or at the front if there is no `&` selector // TODO: handle multiple `&` selectors let nested_rule_index = selector.children.findIndex((child) => child.type === 'NestingSelector'); + let is_next_combinator = selector.children[nested_rule_index + 1]?.type === 'Combinator'; + + console.log("selector", JSON.stringify(selector, null, 2)) + console.log("=========") // if there is no `&` selector, we need to add a fake combinator at the start if (nested_rule_index === -1) { @@ -1009,8 +1013,7 @@ function group_selectors(selector, parent_blocks_group) { // Create the new blocks for the nested rule, visible by default const blocks = selector_to_blocks( selector, - nested_rule_index === 0 ? null : FakeCombinator, - false + true ); return parent_blocks_group.map(parent_blocks => { @@ -1023,6 +1026,8 @@ function group_selectors(selector, parent_blocks_group) { // insert the parent blocks at the position of the `&` selector block_uses.splice(nested_rule_index, 0, ...parent_block_uses); + console.log(JSON.stringify(block_uses.map(b => b.block), null, 2)) + return block_uses; }) } @@ -1030,13 +1035,14 @@ function group_selectors(selector, parent_blocks_group) { /** * @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); +function selector_to_blocks(selector, allow_nesting) { + const is_next_combinator = selector.children[0]?.type === 'Combinator'; + const combinator = is_next_combinator ? selector.children.shift() : null; + let block = new Block(/** @type {import('#compiler').Css.Combinator | null} */ (combinator)); const blocks = [block]; - selector.children.forEach((child) => { + selector.children.map(child => { if (child.type === 'Combinator') { block = new Block(child); blocks.push(block); From 8f347a9aefa553e89e628a570855993e20d422a6 Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 20:42:09 +1030 Subject: [PATCH 31/69] Refactor a bunch of code --- .vscode/launch.json | 22 +- .../compiler/phases/2-analyze/css/Selector.js | 398 ++++++++---------- .../phases/2-analyze/css/Stylesheet.js | 2 +- 3 files changed, 199 insertions(+), 223 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 41d8017ce29f..8335a3ceb0f9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,24 +10,38 @@ { "type": "node", "request": "launch", - "runtimeArgs": ["--watch"], + "runtimeArgs": [ + "--watch" + ], "name": "Playground: Server", "outputCapture": "std", "program": "start.js", "cwd": "${workspaceFolder}/playgrounds/demo", - "cascadeTerminateToConfigurations": ["Playground: Browser"] + "cascadeTerminateToConfigurations": [ + "Playground: Browser" + ] }, { "type": "node", "request": "launch", "name": "Run sandbox", "program": "${workspaceFolder}/playgrounds/sandbox/run.js" + }, + { + "type": "node-terminal", + "name": "Run Script: test", + "request": "launch", + "command": "pnpm test css -- -t omit-scoping-attribute-whitespace-multiple", + "cwd": "${workspaceFolder}" } ], "compounds": [ { "name": "Playground: Full", - "configurations": ["Playground: Server", "Playground: Browser"] + "configurations": [ + "Playground: Server", + "Playground: Browser" + ] } ] -} +} \ No newline at end of file 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 a49d29a286b5..87ada13aa85c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -11,7 +11,11 @@ const NodeExist = /** @type {const} */ ({ Definitely: 1 }); -/** @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue */ +/** + * @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue + * @typedef {Array} ComplexSelector + * @typedef {Array} SelectorList + * */ const whitelist_attribute_selector = new Map([ ['details', new Set(['open'])], @@ -25,11 +29,11 @@ export default class Selector { /** @type {import('./Stylesheet.js').default} */ stylesheet; - /** @type {BlockUse[][]} */ - block_groups; + /** @type {SelectorList} */ + selector_list; - /** @type {BlockUse[][]} */ - local_block_groups; + /** @type {SelectorList} */ + local_selector_list; /** @type {boolean} */ used; @@ -37,46 +41,44 @@ export default class Selector { /** * @param {import('#compiler').Css.Selector} node * @param {import('./Stylesheet.js').default} stylesheet - * @param {BlockUse[][] | null} parent_blocks + * @param {ComplexSelector[] | null} parent_selector_list */ - constructor(node, stylesheet, parent_blocks) { + constructor(node, stylesheet, parent_selector_list) { this.node = node; this.stylesheet = stylesheet; - this.block_groups = group_selectors(node, parent_blocks); + this.selector_list = group_selectors(node, parent_selector_list); this.used = false; - // Initialize local_block_groups - this.local_block_groups = []; + // Initialize local_selector_list + this.local_selector_list = []; // Process each block group to take trailing :global(...) selectors out of consideration - this.block_groups.forEach(group => { - let i = group.length; + this.selector_list.forEach(complex_selector => { + let i = complex_selector.length; while (i > 0) { - if (!group[i - 1].block.global) break; + if (!complex_selector[i - 1].global) break; i -= 1; } - // Add the processed group (with global selectors removed) to local_block_groups - this.local_block_groups.push(group.slice(0, i)); + // Add the processed group (with global selectors removed) to local_selector_list + this.local_selector_list.push(complex_selector.slice(0, i)); }); - // Determine `used` based on the processed local_block_groups + // Determine `used` based on the processed local_selector_list let host_only = false; let root_only = false; // Check if there's exactly one group and one block within that group, and if it's host or root - if (this.local_block_groups.length === 1 && this.local_block_groups[0].length === 1) { - const single_block = this.local_block_groups[0][0].block; - host_only = single_block.host; - root_only = single_block.root; + if (this.local_selector_list.length === 1 && this.local_selector_list[0].length === 1) { + const single_block = this.local_selector_list[0][0]; + host_only = single_block.compound.host; + root_only = single_block.compound.root; } // Check if there are no local blocks across all groups, or if there's a host_only or root_only situation - const no_local_blocks = this.local_block_groups.every(group => group.length === 0); + const no_local_blocks = this.local_selector_list.every(group => group.length === 0); this.used = no_local_blocks || host_only || root_only; - } - /** * Determines whether the given selector is used within the component's nodes * and marks the corresponding blocks for encapsulation if so. @@ -100,8 +102,7 @@ export default class Selector { * ``` * * 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. + * We should mark it for encapsulation as a result. * * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node - The node to apply the selector to. * @returns {void} @@ -109,18 +110,15 @@ export default class Selector { apply(node) { /** * Create a map of blocks to their nodes to know whether they should be encapsulated - * @type {Map>} + * @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, used_blocks); - }); + this.local_selector_list.map(complex_selector => apply_selector(complex_selector.slice(), node, used_blocks)); // Iterate over used_blocks - for (const [block, nodes] of used_blocks) { - block.should_encapsulate = true; + for (const [relative_selector, nodes] of used_blocks) { + relative_selector.should_encapsulate = true; for (const node of nodes) { this.stylesheet.nodes_with_css_class.add(node); } @@ -134,72 +132,58 @@ export default class Selector { * @param {number} max_amount_class_specificity_increased */ transform(code, attr, max_amount_class_specificity_increased) { + /** @param {import('#compiler').Css.SimpleSelector} selector */ + function remove_global_pseudo_class(selector) { + code + .remove(selector.start, selector.start + ':global('.length) + .remove(selector.end - 1, selector.end); + } + /** - * @param {BlockUse} block_use + * @param {RelativeSelector} relative_selector * @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); - } + function encapsulate_block(relative_selector, attr) { + for (const selector of relative_selector.compound.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]; + } + let i = relative_selector.compound.selectors.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; - } + while (i--) { + const selector = relative_selector.compound.selectors[i]; - if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { - if (!block.root && !block.host) { - if (i === 0) code.prependRight(selector.start, attr); - } - continue; + if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { + if (!relative_selector.root && !relative_selector.host) { + if (i === 0) code.prependRight(selector.start, attr); } - if (selector.type === 'TypeSelector' && selector.name === '*') { - code.update(selector.start, selector.end, attr); - } else { - code.appendLeft(selector.end, attr); - } - break; + 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) { - code - .remove(selector.start, selector.start + ':global('.length) - .remove(selector.end - 1, selector.end); - } - - 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; + for (const complex_selector of this.selector_list) { + const amount_class_specificity_to_increase = + max_amount_class_specificity_increased + - complex_selector.filter(selector => selector.should_encapsulate).length; - group.map((block_use, index) => { - const block = block_use.block; - if (block.global) { + complex_selector.map((relative_selector, index) => { + if (relative_selector.global) { // Remove the global pseudo class from the selector - remove_global_pseudo_class(block.selectors[0]); + remove_global_pseudo_class(relative_selector.compound.selectors[0]); } - if(block.should_encapsulate) { + if(relative_selector.should_encapsulate) { encapsulate_block( - block_use, - index === group.length - 1 + relative_selector, + index === complex_selector.length - 1 ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); @@ -218,18 +202,18 @@ export default class Selector { validate_invalid_css_global_placement() { - for (let group of this.block_groups) { + for (let complex_selector of this.selector_list) { let start = 0; - let end = group.length; + let end = complex_selector.length; for (; start < end; start += 1) { - if (!group[start].block.global) break; + if (!complex_selector[start].global) break; } for (; end > start; end -= 1) { - if (!group[end - 1].block.global) break; + if (!complex_selector[end - 1].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'); + if (complex_selector[i].global) { + error(complex_selector[i].compound.selectors[0], 'invalid-css-global-placement'); } } } @@ -237,14 +221,13 @@ export default class Selector { validate_global_with_multiple_selectors() { - for (const group of this.block_groups) { - if (group.length === 1 && group[0].block.selectors.length === 1) { + for (const complex_selector of this.selector_list) { + if (complex_selector.length === 1 && complex_selector[0].compound.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) { + for (const relative_selector of complex_selector) { + for (const selector of relative_selector.compound.selectors) { if ( selector.type === 'PseudoClassSelector' && selector.name === 'global' && @@ -260,24 +243,22 @@ export default class Selector { /** @param {import('../../types.js').ComponentAnalysis} analysis */ validate_invalid_combinator_without_selector(analysis) { - 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'); + for (const complex_selector of this.selector_list) { + for (const relative_selector of complex_selector) { + if (relative_selector.compound.selectors.length === 0) { + error(this.node, 'invalid-css-selector'); } } } } validate_global_compound_selector() { - 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 (const group of this.selector_list) { + for (const relative_selector of group) { + if (relative_selector.compound.selectors.length === 1) continue; - for (let i = 0; i < block.selectors.length; i++) { - const selector = block.selectors[i]; + for (let i = 0; i < relative_selector.compound.selectors.length; i++) { + const selector = relative_selector.compound.selectors[i]; if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { const child = selector.args?.children[0].children[0]; @@ -285,11 +266,9 @@ export default class Selector { child?.type === 'TypeSelector' && !/[.:#]/.test(child.name[0]) && (i !== 0 || - block.selectors + relative_selector.compound.selectors .slice(1) - .some( - (s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector' - )) + .some(s => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector')) ) { error(selector, 'invalid-css-global-selector-list'); } @@ -302,14 +281,14 @@ export default class Selector { get_amount_class_specificity_increased() { // 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; + return this.selector_list[0].filter(selector => selector.should_encapsulate).length; } } /** - * @param {Map>} map - * @param {Block} block + * @param {Map>} map + * @param {RelativeSelector} block * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ function add_node(map, block, node) { @@ -320,9 +299,9 @@ function add_node(map, block, node) { } /** - * @param {Block[]} blocks + * @param {RelativeSelector[]} blocks * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node - * @param {Map>} to_encapsulate + * @param {Map>} to_encapsulate * @returns {boolean} */ function apply_selector(blocks, node, to_encapsulate) { @@ -366,7 +345,7 @@ function apply_selector(blocks, node, to_encapsulate) { return true; } } - if (blocks.every((block) => block.global)) { + if (blocks.every(block => block.global)) { add_node(to_encapsulate, block, node); return true; } @@ -411,16 +390,16 @@ function apply_selector(blocks, node, to_encapsulate) { const regex_backslash_and_following_character = /\\(.)/g; /** - * @param {Block} block + * @param {RelativeSelector} block * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node * @returns {NO_MATCH | POSSIBLE_MATCH | UNKNOWN_SELECTOR} */ function block_might_apply_to_node(block, node) { if (block.host || block.root) return NO_MATCH; - let i = block.selectors.length; + let i = block.compound.selectors.length; while (i--) { - const selector = block.selectors[i]; + const selector = block.compound.selectors[i]; if (selector.type === 'Percentage' || selector.type === 'Nth') continue; @@ -430,7 +409,7 @@ function block_might_apply_to_node(block, node) { return NO_MATCH; } if ( - block.selectors.length === 1 && + block.compound.selectors.length === 1 && selector.type === 'PseudoClassSelector' && name === 'global' ) { @@ -887,17 +866,64 @@ function loop_child(children, adjacent_only) { return result; } -class Block { + +/** + * Not shared between different Selector instances + */ +class RelativeSelector { + /** @type {import('#compiler').Css.Combinator | null} */ + combinator; + + /** @type {CompoundSelector} */ + compound; + /** @type {boolean} */ - host; + should_encapsulate; /** @type {boolean} */ - root; + visible; - /** @type {import('#compiler').Css.Combinator | null} */ - combinator; + /** + * @param {import('#compiler').Css.Combinator | null} combinator + * @param {CompoundSelector} compound + * @param {boolean} visible - Whether this complex selector is visible, used in nested rules + * */ + constructor(combinator, compound, visible) { + this.combinator = combinator; + this.compound = compound; + this.should_encapsulate = false; + this.visible = visible; + } - /** @type {(import('#compiler').Css.SimpleSelector & { invisible?: boolean})[]} */ + /** @param {import('#compiler').Css.SimpleSelector} selector */ + add(selector) { + this.compound.add(selector); + } + + get global() { return this.compound.global } + get host() { return this.compound.host } + get root() { return this.compound.root } + get end() {return this.compound.end } + get start() { + if (this.combinator) return this.combinator.start; + return this.compound.start; + } +} + +/** @type {import('#compiler').Css.Combinator} */ +const FakeCombinator = { + type: 'Combinator', + name: ' ', + start: -1, + end: -1 +}; + +/** + * Shared between different Selector instances, so they are + * not encapsulated multiple times + **/ +class CompoundSelector { + /** @type {Array} */ selectors; /** @type {number} */ @@ -907,24 +933,20 @@ class Block { end; /** @type {boolean} */ - should_encapsulate; + host; /** @type {boolean} */ - has_been_encapsulated; + root; /** @type {boolean} */ - invisible; + has_been_encapsulated; - /** @param {import('#compiler').Css.Combinator | null} combinator */ - constructor(combinator) { - this.combinator = combinator; - this.host = false; - this.root = false; + constructor() { this.selectors = []; this.start = -1; this.end = -1; - this.should_encapsulate = false; - this.invisible = false; + this.host = false + this.root = false this.has_been_encapsulated = false; } @@ -938,124 +960,64 @@ class Block { this.selectors.push(selector); this.end = selector.end; } + get global() { return ( this.selectors.length >= 1 && this.selectors[0].type === 'PseudoClassSelector' && this.selectors[0].name === 'global' && - this.selectors.every( - (selector) => - selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' - ) + this.selectors.every(selector => selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') ); } } -/** @type {import('#compiler').Css.Combinator} */ -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 and inserts parent blocks into nested rules. * * @param {import('#compiler').Css.Selector} selector - The selector to group and analyze. - * @param {Array> | null} parent_blocks_group - The parent blocks group to insert into nested rules. - * @returns {Array>} - The grouped selectors with parent's blocks inserted if nested. + * @param {SelectorList | null} parent_selector_list - The parent blocks group to insert into nested rules. + * @returns {SelectorList} - The grouped selectors with parent's blocks inserted if nested. */ -function group_selectors(selector, parent_blocks_group) { +function group_selectors(selector, parent_selector_list) { // If it isn't a nested rule, then we add an empty block group - if (parent_blocks_group === null) { - return [ - selector_to_blocks(selector, false).map((block) => new BlockUse(block, true)) - ]; - } - - // 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'); - let is_next_combinator = selector.children[nested_rule_index + 1]?.type === 'Combinator'; - - console.log("selector", JSON.stringify(selector, null, 2)) - console.log("=========") - - // 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); + if (parent_selector_list === null) { + return [selector_to_blocks(selector.children, null)]; } - // Create the new blocks for the nested rule, visible by default - const blocks = selector_to_blocks( - selector, - true - ); - - 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)); - - // 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)); - - // insert the parent blocks at the position of the `&` selector - block_uses.splice(nested_rule_index, 0, ...parent_block_uses); + return parent_selector_list.map(parent_complex_selector => { + const block_group = selector_to_blocks( + selector.children, + [...parent_complex_selector] // Clone the parent's blocks to avoid modifying the original array + ); - console.log(JSON.stringify(block_uses.map(b => b.block), null, 2)) + // console.log(JSON.stringify(blocks, null, 2)) - return block_uses; + return block_group; }) } /** - * @param {import('#compiler').Css.Selector} selector - * @param {boolean} allow_nesting + * @param {import('#compiler').Css.Selector["children"]} children + * @param {ComplexSelector | null} parent_complex_selector - The parent blocks to insert into the nesting selector positions. */ -function selector_to_blocks(selector, allow_nesting) { - const is_next_combinator = selector.children[0]?.type === 'Combinator'; - const combinator = is_next_combinator ? selector.children.shift() : null; - let block = new Block(/** @type {import('#compiler').Css.Combinator | null} */ (combinator)); +function selector_to_blocks(children, parent_complex_selector) { + let block = new RelativeSelector(null, new CompoundSelector, true); const blocks = [block]; - selector.children.map(child => { + for (const child of children) { if (child.type === 'Combinator') { - block = new Block(child); + block = new RelativeSelector(child, new CompoundSelector, true); blocks.push(block); } else if (child.type === 'NestingSelector') { - if (!allow_nesting) { + if (!parent_complex_selector) { error(child, 'nesting-selector-not-allowed'); } else { - // We should've already removed the `&` selector - throw new Error("Unexpected nesting selector"); + throw new Error('TODO'); } } else { block.add(child); } - }); + } + 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 4ca39900eda6..c6d14bab86f9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -88,7 +88,7 @@ class Rule { * - .b .c */ if (parent && parent.node.type === 'Rule') { - let block_groups = /** @type {Rule} **/ (parent).selectors.map(selector => selector.block_groups).flat(); + let block_groups = /** @type {Rule} **/ (parent).selectors.map(selector => selector.selector_list).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)); From 20629caf9120f2adfc854d2d23326fdc04935134 Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 22:23:23 +1030 Subject: [PATCH 32/69] Getting closer --- .../compiler/phases/2-analyze/css/Selector.js | 95 ++++++++++++++----- 1 file changed, 70 insertions(+), 25 deletions(-) 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 87ada13aa85c..75dbdbc5bd5a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -149,11 +149,20 @@ export default class Selector { remove_global_pseudo_class(selector); } } + let i = relative_selector.compound.selectors.length; while (i--) { const selector = relative_selector.compound.selectors[i]; + if (!selector.visible) continue + if (selector.use_wrapper.used) continue; + + + console.log(selector, attr) + + selector.use_wrapper.used = true; + if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { if (!relative_selector.root && !relative_selector.host) { if (i === 0) code.prependRight(selector.start, attr); @@ -170,9 +179,12 @@ export default class Selector { }; for (const complex_selector of this.selector_list) { - const amount_class_specificity_to_increase = - max_amount_class_specificity_increased - - complex_selector.filter(selector => selector.should_encapsulate).length; + let amount_class_specificity_to_increase = + max_amount_class_specificity_increased - complex_selector + .filter(selector => selector.should_encapsulate) + // .filter(selector => !selector.contains_invisible_selectors) + // .filter(selector => !selector.compound.selectors[0].use_wrapper.used) + .length; complex_selector.map((relative_selector, index) => { if (relative_selector.global) { @@ -183,7 +195,10 @@ export default class Selector { if(relative_selector.should_encapsulate) { encapsulate_block( relative_selector, - index === complex_selector.length - 1 + index === complex_selector + .filter(selector => !selector.contains_invisible_selectors) + // .filter(selector => selector.compound.selectors.some(selector => selector.use_wrapper.used)) + .length - 1 ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); @@ -880,22 +895,17 @@ class RelativeSelector { /** @type {boolean} */ should_encapsulate; - /** @type {boolean} */ - visible; - /** * @param {import('#compiler').Css.Combinator | null} combinator * @param {CompoundSelector} compound - * @param {boolean} visible - Whether this complex selector is visible, used in nested rules * */ - constructor(combinator, compound, visible) { + constructor(combinator, compound) { this.combinator = combinator; this.compound = compound; this.should_encapsulate = false; - this.visible = visible; } - /** @param {import('#compiler').Css.SimpleSelector} selector */ + /** @param {import('#compiler').Css.SimpleSelector & { use_wrapper: { used: boolean }, visible: boolean }} selector */ add(selector) { this.compound.add(selector); } @@ -908,6 +918,10 @@ class RelativeSelector { if (this.combinator) return this.combinator.start; return this.compound.start; } + + get contains_invisible_selectors() { + return this.compound.selectors.some(selector => !selector.visible); + } } /** @type {import('#compiler').Css.Combinator} */ @@ -923,7 +937,7 @@ const FakeCombinator = { * not encapsulated multiple times **/ class CompoundSelector { - /** @type {Array} */ + /** @type {Array} */ selectors; /** @type {number} */ @@ -938,19 +952,15 @@ class CompoundSelector { /** @type {boolean} */ root; - /** @type {boolean} */ - has_been_encapsulated; - constructor() { this.selectors = []; this.start = -1; this.end = -1; this.host = false this.root = false - this.has_been_encapsulated = false; } - /** @param {import('#compiler').Css.SimpleSelector} selector */ + /** @param {import('#compiler').Css.SimpleSelector & { use_wrapper: { used: boolean }, visible: boolean }} selector */ add(selector) { if (this.selectors.length === 0) { this.start = selector.start; @@ -981,17 +991,15 @@ class CompoundSelector { function group_selectors(selector, parent_selector_list) { // If it isn't a nested rule, then we add an empty block group if (parent_selector_list === null) { - return [selector_to_blocks(selector.children, null)]; + return [selector_to_blocks([...selector.children], null)]; } return parent_selector_list.map(parent_complex_selector => { const block_group = selector_to_blocks( - selector.children, + [...selector.children], [...parent_complex_selector] // Clone the parent's blocks to avoid modifying the original array ); - // console.log(JSON.stringify(blocks, null, 2)) - return block_group; }) } @@ -1002,20 +1010,57 @@ function group_selectors(selector, parent_selector_list) { * @param {ComplexSelector | null} parent_complex_selector - The parent blocks to insert into the nesting selector positions. */ function selector_to_blocks(children, parent_complex_selector) { - let block = new RelativeSelector(null, new CompoundSelector, true); + let block = new RelativeSelector(null, new CompoundSelector); const blocks = [block]; + + // If this is a nested rule + if (parent_complex_selector) { + let nested_selector_index = children.findIndex(child => child.type === 'NestingSelector'); + + if (nested_selector_index === -1) { + nested_selector_index = 0; // insert the parent selectors at the beginning of the children array + } else { + children.splice(nested_selector_index, 1); // remove the nesting selector, so we can insert there + } + + let parent_selectors = [] + for (const relative_selector of parent_complex_selector) { + if (relative_selector.combinator) parent_selectors.push(relative_selector.combinator); + parent_selectors.push(...relative_selector.compound.selectors.map(selector => ({ ...selector, visible: false }))); + } + + /** + * Some cases + * b { c { color: red }} -> need to insert ' ' before c, so output needs to look like b c + * b { & c { color: red }} -> already has a child combinator before c, so output needs to look like b c + * b { & > c { color: red }} -> next combinator is '>' so output needs to look like b > c + * b { c & { color: red }} -> so we need to insert ' ' after c so output needs to look like c b + */ + + children.unshift(FakeCombinator); // insert a fake combinator at the beginning + + + // Finally, insert the parent selectors into the children array + children.splice(nested_selector_index, 0, ...parent_selectors); + } + + for (const child of children) { if (child.type === 'Combinator') { - block = new RelativeSelector(child, new CompoundSelector, true); + block = new RelativeSelector(child, new CompoundSelector); blocks.push(block); } else if (child.type === 'NestingSelector') { if (!parent_complex_selector) { error(child, 'nesting-selector-not-allowed'); } else { - throw new Error('TODO'); + // We've already handled these above + throw new Error('unexpected nesting selector'); } } else { - block.add(child); + let new_child = /** @type {typeof child & { use_wrapper: { used: boolean }, visible: boolean}} */ (child); + new_child.visible = new_child.visible ?? true; + new_child.use_wrapper = new_child.use_wrapper ?? { used: false }; + block.add(new_child); } } From 862be02d0f3716f650da71cfc8ed4a1e4efcca28 Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 22:24:59 +1030 Subject: [PATCH 33/69] Even closer --- .../svelte/src/compiler/phases/2-analyze/css/Selector.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 75dbdbc5bd5a..89fbebc745ac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1037,7 +1037,10 @@ function selector_to_blocks(children, parent_complex_selector) { * b { c & { color: red }} -> so we need to insert ' ' after c so output needs to look like c b */ - children.unshift(FakeCombinator); // insert a fake combinator at the beginning + // if the first child is not a combinator, insert a fake combinator at the beginning + if (children[0].type !== 'Combinator') { + children.unshift(FakeCombinator); // insert a fake combinator at the beginning + } // Finally, insert the parent selectors into the children array From ef4b7431aafcdcdd77c6af7265ea53f56c973920 Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 23:10:45 +1030 Subject: [PATCH 34/69] One test left! --- .../compiler/phases/2-analyze/css/Selector.js | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) 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 89fbebc745ac..65690d746d0f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -15,6 +15,7 @@ const NodeExist = /** @type {const} */ ({ * @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue * @typedef {Array} ComplexSelector * @typedef {Array} SelectorList + * @typedef {import("#compiler").Css.SimpleSelector & { use_wrapper: { used: boolean }, visible: boolean }} SimpleSelectorWithData * */ const whitelist_attribute_selector = new Map([ @@ -158,12 +159,10 @@ export default class Selector { if (!selector.visible) continue if (selector.use_wrapper.used) continue; - - console.log(selector, attr) - selector.use_wrapper.used = true; if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { + console.log("PseudoElementSelector or PseudoClassSelector", selector) if (!relative_selector.root && !relative_selector.host) { if (i === 0) code.prependRight(selector.start, attr); } @@ -905,7 +904,7 @@ class RelativeSelector { this.should_encapsulate = false; } - /** @param {import('#compiler').Css.SimpleSelector & { use_wrapper: { used: boolean }, visible: boolean }} selector */ + /** @param {SimpleSelectorWithData} selector */ add(selector) { this.compound.add(selector); } @@ -937,7 +936,7 @@ const FakeCombinator = { * not encapsulated multiple times **/ class CompoundSelector { - /** @type {Array} */ + /** @type {Array} */ selectors; /** @type {number} */ @@ -960,7 +959,7 @@ class CompoundSelector { this.root = false } - /** @param {import('#compiler').Css.SimpleSelector & { use_wrapper: { used: boolean }, visible: boolean }} selector */ + /** @param {SimpleSelectorWithData} selector */ add(selector) { if (this.selectors.length === 0) { this.start = selector.start; @@ -1023,10 +1022,14 @@ function selector_to_blocks(children, parent_complex_selector) { children.splice(nested_selector_index, 1); // remove the nesting selector, so we can insert there } + // Modify the first child after the nesting selector to have a flag disabling attr + let parent_selectors = [] for (const relative_selector of parent_complex_selector) { if (relative_selector.combinator) parent_selectors.push(relative_selector.combinator); - parent_selectors.push(...relative_selector.compound.selectors.map(selector => ({ ...selector, visible: false }))); + parent_selectors.push( + ...relative_selector.compound.selectors.map(selector => ({ ...selector, visible: false})) + ); } /** @@ -1037,16 +1040,24 @@ function selector_to_blocks(children, parent_complex_selector) { * b { c & { color: red }} -> so we need to insert ' ' after c so output needs to look like c b */ - // if the first child is not a combinator, insert a fake combinator at the beginning - if (children[0].type !== 'Combinator') { - children.unshift(FakeCombinator); // insert a fake combinator at the beginning + // if the first child is a PseudoClass, mark it as invisible + if (children[nested_selector_index]?.type === 'PseudoClassSelector') { + /** @type {SimpleSelectorWithData} */ (children[nested_selector_index]).visible = false; } + let first_child_combinator = children[nested_selector_index]?.type === 'Combinator' + ? /** @type {import('#compiler').Css.Combinator} */(children[nested_selector_index]) + : null; + + if (first_child_combinator?.type !== 'Combinator') { + children.unshift(FakeCombinator); // insert a fake combinator at the beginning + } // Finally, insert the parent selectors into the children array children.splice(nested_selector_index, 0, ...parent_selectors); } + console.log(children) for (const child of children) { if (child.type === 'Combinator') { @@ -1060,8 +1071,9 @@ function selector_to_blocks(children, parent_complex_selector) { throw new Error('unexpected nesting selector'); } } else { - let new_child = /** @type {typeof child & { use_wrapper: { used: boolean }, visible: boolean}} */ (child); - new_child.visible = new_child.visible ?? true; + // We want to maintain a reference to the original child, so we can modify it later + let new_child = /** @type {SimpleSelectorWithData}} */ (child); + new_child.visible = new_child.visible === undefined ? true : new_child.visible; new_child.use_wrapper = new_child.use_wrapper ?? { used: false }; block.add(new_child); } From 1000cdfb62184924a3afe3b4831a94245ad9c1cd Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 7 Feb 2024 23:15:01 +1030 Subject: [PATCH 35/69] Comment out some console.logs --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 65690d746d0f..3c6099e36880 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -162,7 +162,7 @@ export default class Selector { selector.use_wrapper.used = true; if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { - console.log("PseudoElementSelector or PseudoClassSelector", selector) + // console.log("PseudoElementSelector or PseudoClassSelector", selector) if (!relative_selector.root && !relative_selector.host) { if (i === 0) code.prependRight(selector.start, attr); } @@ -1057,7 +1057,7 @@ function selector_to_blocks(children, parent_complex_selector) { children.splice(nested_selector_index, 0, ...parent_selectors); } - console.log(children) + // console.log(children) for (const child of children) { if (child.type === 'Combinator') { From 89667b4ea7b16e44dd7bd59224f9406407f4fd8c Mon Sep 17 00:00:00 2001 From: Albert Date: Thu, 8 Feb 2024 10:20:31 +1030 Subject: [PATCH 36/69] All tests passing & clean up a little bit of code --- .../compiler/phases/2-analyze/css/Selector.js | 140 +++++++++++------- packages/svelte/src/compiler/types/css.d.ts | 10 +- .../nested-css-ampersand-suffix/expected.css | 2 +- .../nested-css-ampersand-suffix/input.svelte | 6 +- .../nested-css-self-referential/expected.css | 9 ++ .../nested-css-self-referential/input.svelte | 17 +++ 6 files changed, 125 insertions(+), 59 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css-self-referential/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-self-referential/input.svelte 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 3c6099e36880..1e2ae8b14f92 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1003,61 +1003,16 @@ function group_selectors(selector, parent_selector_list) { }) } - /** * @param {import('#compiler').Css.Selector["children"]} children - * @param {ComplexSelector | null} parent_complex_selector - The parent blocks to insert into the nesting selector positions. + * @param {ComplexSelector | null} parent_complex_selector - The parent rule's selectors to insert/swap into the nesting selector positions. */ function selector_to_blocks(children, parent_complex_selector) { let block = new RelativeSelector(null, new CompoundSelector); const blocks = [block]; // If this is a nested rule - if (parent_complex_selector) { - let nested_selector_index = children.findIndex(child => child.type === 'NestingSelector'); - - if (nested_selector_index === -1) { - nested_selector_index = 0; // insert the parent selectors at the beginning of the children array - } else { - children.splice(nested_selector_index, 1); // remove the nesting selector, so we can insert there - } - - // Modify the first child after the nesting selector to have a flag disabling attr - - let parent_selectors = [] - for (const relative_selector of parent_complex_selector) { - if (relative_selector.combinator) parent_selectors.push(relative_selector.combinator); - parent_selectors.push( - ...relative_selector.compound.selectors.map(selector => ({ ...selector, visible: false})) - ); - } - - /** - * Some cases - * b { c { color: red }} -> need to insert ' ' before c, so output needs to look like b c - * b { & c { color: red }} -> already has a child combinator before c, so output needs to look like b c - * b { & > c { color: red }} -> next combinator is '>' so output needs to look like b > c - * b { c & { color: red }} -> so we need to insert ' ' after c so output needs to look like c b - */ - - // if the first child is a PseudoClass, mark it as invisible - if (children[nested_selector_index]?.type === 'PseudoClassSelector') { - /** @type {SimpleSelectorWithData} */ (children[nested_selector_index]).visible = false; - } - - let first_child_combinator = children[nested_selector_index]?.type === 'Combinator' - ? /** @type {import('#compiler').Css.Combinator} */(children[nested_selector_index]) - : null; - - if (first_child_combinator?.type !== 'Combinator') { - children.unshift(FakeCombinator); // insert a fake combinator at the beginning - } - - // Finally, insert the parent selectors into the children array - children.splice(nested_selector_index, 0, ...parent_selectors); - } - - // console.log(children) + if (parent_complex_selector) nest_fake_parents(children, parent_complex_selector); for (const child of children) { if (child.type === 'Combinator') { @@ -1067,17 +1022,96 @@ function selector_to_blocks(children, parent_complex_selector) { if (!parent_complex_selector) { error(child, 'nesting-selector-not-allowed'); } else { - // We've already handled these above + // We shoudld've already handled these above throw new Error('unexpected nesting selector'); } } else { - // We want to maintain a reference to the original child, so we can modify it later - let new_child = /** @type {SimpleSelectorWithData}} */ (child); - new_child.visible = new_child.visible === undefined ? true : new_child.visible; - new_child.use_wrapper = new_child.use_wrapper ?? { used: false }; - block.add(new_child); + // shared reference bween all children + child.use_wrapper = child.use_wrapper ?? { used: false }; + + // Shallow copy the child to avoid modifying the original's visibility + block.add(/** @type SimpleSelectorWithData */ ({ + ...child, + visible: child.visible === undefined ? true : child.visible + })); } } return blocks; +} + +/** + * @param {ComplexSelector} parent_complex_selector - The parent blocks to insert into the nesting selector positions. + * @returns {import('#compiler').Css.Selector["children"]} - The parent selectors to insert into the nesting selector positions. + */ +function get_parent_selectors(parent_complex_selector) { + const parent_selectors = []; + for (const relative_selector of parent_complex_selector) { + if (relative_selector.combinator) { + parent_selectors.push(relative_selector.combinator); + } + parent_selectors.push( + ...relative_selector.compound.selectors.map(selector => ({ + ...selector, + visible: false, + })) + ); + } + return parent_selectors; +} + +/** + * Nest the parent selectors into the children array so we can easily + * check for usage and scoping. + * + * @param {import('#compiler').Css.Selector["children"]} children + * @param {ComplexSelector} parent_complex_selector - The parent blocks to insert into the nesting selector positions. + */ +function nest_fake_parents(children, parent_complex_selector) { + let nested_selector_index = children.findIndex(child => child.type === 'NestingSelector'); + + // TODO: Handle multiple nesting selectors? eg: &&& {...} + if (nested_selector_index === -1) { + nested_selector_index = 0; // insert the parent selectors at the beginning of the children array + } else { + children.splice(nested_selector_index, 1); // remove the nesting selector, so we can insert there + } + + // Modify the first child after the nesting selector to have a flag disabling attr + + /** @type typeof children */ + const parent_selectors = get_parent_selectors(parent_complex_selector); + + /** + * Some cases + * b { c { color: red }} -> need to insert ' ' before c, so output needs to look like [b, " ", c] + * b { & c { color: red }} -> already has a child combinator before c, so output needs to look like [b, " ", c] + * b { & > c { color: red }} -> next combinator is '>' so output needs to look like [b, >, c] + * b { c & { color: red }} -> so we need to insert ' ' after c so children needs to look like [c, " ",b] + * b { & { color: red }} -> no combinator, so children needs to look like [b] + */ + + if (children.length > 0) { + // if the first child is a PseudoClass, mark it as invisible because the & provides scoping + let child_after = children[nested_selector_index]; + if (child_after?.type === 'PseudoClassSelector') { + child_after.visible = false; + } + + let child_before = children[nested_selector_index - 1]; + let last_parent_child = parent_selectors[parent_selectors.length - 1]; + + if(child_before?.type !== "Combinator") { + if (child_after?.type !== 'Combinator') { + children.splice(nested_selector_index, 0, FakeCombinator); + } + } + + if(last_parent_child.type !== "Combinator" && !child_after) { + // Case: b { c & { color: red }} (we need to mark b as visible so we increase specifity) + last_parent_child.visible = true; + } + } + // Finally, insert the parent selectors into the children array + children.splice(nested_selector_index, 0, ...parent_selectors); } \ No newline at end of file diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 8de0254ca932..8d8eef2ccf35 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -77,7 +77,7 @@ export interface NestingSelector extends BaseNode { name: '&'; } -export type SimpleSelector = +export type SimpleSelector = ( | TypeSelector | IdSelector | ClassSelector @@ -86,7 +86,13 @@ export type SimpleSelector = | PseudoClassSelector | Percentage | Nth - | NestingSelector; + | NestingSelector +) & { + // Deeply nested because we want to track whether the selector is used + // between clones of the AST + use_wrapper?: { used: boolean }, + visible?: boolean +}; export interface Combinator extends BaseNode { type: 'Combinator'; diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css index 8f108793820b..bcdc01d1e60b 100644 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css @@ -1,4 +1,4 @@ -a.svelte-xyz { +c.svelte-xyz { color: red; b.svelte-xyz & { diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte index 91a0532d97f5..7b0a45bcd55f 100644 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte @@ -1,10 +1,10 @@ - + - + \ No newline at end of file From 8c81aa759a920696984eae373481935ce3e0017a Mon Sep 17 00:00:00 2001 From: Albert Date: Thu, 8 Feb 2024 10:42:27 +1030 Subject: [PATCH 37/69] Rever launch.json changes --- .vscode/launch.json | 22 ++++--------------- .../compiler/phases/2-analyze/css/Selector.js | 2 +- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 8335a3ceb0f9..41d8017ce29f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,38 +10,24 @@ { "type": "node", "request": "launch", - "runtimeArgs": [ - "--watch" - ], + "runtimeArgs": ["--watch"], "name": "Playground: Server", "outputCapture": "std", "program": "start.js", "cwd": "${workspaceFolder}/playgrounds/demo", - "cascadeTerminateToConfigurations": [ - "Playground: Browser" - ] + "cascadeTerminateToConfigurations": ["Playground: Browser"] }, { "type": "node", "request": "launch", "name": "Run sandbox", "program": "${workspaceFolder}/playgrounds/sandbox/run.js" - }, - { - "type": "node-terminal", - "name": "Run Script: test", - "request": "launch", - "command": "pnpm test css -- -t omit-scoping-attribute-whitespace-multiple", - "cwd": "${workspaceFolder}" } ], "compounds": [ { "name": "Playground: Full", - "configurations": [ - "Playground: Server", - "Playground: Browser" - ] + "configurations": ["Playground: Server", "Playground: Browser"] } ] -} \ No newline at end of file +} 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 1e2ae8b14f92..2801e12af70a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1022,7 +1022,7 @@ function selector_to_blocks(children, parent_complex_selector) { if (!parent_complex_selector) { error(child, 'nesting-selector-not-allowed'); } else { - // We shoudld've already handled these above + // We shoudld've already handled these above (except for multiple nesting selectors, which is supposed to work?) throw new Error('unexpected nesting selector'); } } else { From 51016b5c4da128a592dfc159ac5d0fc0ce35d122 Mon Sep 17 00:00:00 2001 From: Albert Date: Thu, 8 Feb 2024 11:18:35 +1030 Subject: [PATCH 38/69] Add more tests --- .../expected.css | 9 +++++++++ .../input.svelte | 15 +++++++++++++++ .../expected.css | 7 +++++++ .../input.svelte | 15 +++++++++++++++ .../expected.css | 9 +++++++++ .../input.svelte | 17 +++++++++++++++++ .../samples/nested-css-empty-child/expected.css | 6 ++++++ .../samples/nested-css-empty-child/input.svelte | 11 +++++++++++ .../nested-css-empty-parent/expected.css | 5 +++++ .../nested-css-empty-parent/input.svelte | 10 ++++++++++ .../nested-css-unused-child/expected.css | 9 +++++++++ .../nested-css-unused-child/input.svelte | 15 +++++++++++++++ .../nested-css-unused-parent/expected.css | 9 +++++++++ .../nested-css-unused-parent/input.svelte | 15 +++++++++++++++ 14 files changed, 152 insertions(+) create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-empty-child/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-empty-child/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-empty-parent/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-empty-parent/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-unused-child/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-unused-child/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-unused-parent/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-unused-parent/input.svelte diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css new file mode 100644 index 000000000000..50ad637d577f --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css @@ -0,0 +1,9 @@ +c.svelte-xyz { + color: red; + + /* (empty) b.svelte-xyz & { + + }*/ + + color: black; +} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/input.svelte new file mode 100644 index 000000000000..bb88e2012d2a --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/input.svelte @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/expected.css new file mode 100644 index 000000000000..50060c1aaba1 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/expected.css @@ -0,0 +1,7 @@ +c.svelte-xyz { + color: red; + + /* (unused) b + & { + color: red; + }*/ +} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/input.svelte new file mode 100644 index 000000000000..0504532aa849 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/input.svelte @@ -0,0 +1,15 @@ + +
+ + +
+ \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css new file mode 100644 index 000000000000..fb3721185a3f --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css @@ -0,0 +1,9 @@ +c.svelte-xyz { + color: red; + + b.svelte-xyz + & { + color: yellow; + } + + color: black; +} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/input.svelte new file mode 100644 index 000000000000..ae1be9dce273 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/input.svelte @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-empty-child/expected.css b/packages/svelte/tests/css/samples/nested-css-empty-child/expected.css new file mode 100644 index 000000000000..019bd61efef1 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-empty-child/expected.css @@ -0,0 +1,6 @@ +b.svelte-xyz { + color: red; + /* (empty) x.svelte-xyz { + + }*/ +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-empty-child/input.svelte b/packages/svelte/tests/css/samples/nested-css-empty-child/input.svelte new file mode 100644 index 000000000000..03a1339a7c50 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-empty-child/input.svelte @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-empty-parent/expected.css b/packages/svelte/tests/css/samples/nested-css-empty-parent/expected.css new file mode 100644 index 000000000000..02f77ae45170 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-empty-parent/expected.css @@ -0,0 +1,5 @@ +b.svelte-xyz { + x.svelte-xyz { + color: yellow; + } +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-empty-parent/input.svelte b/packages/svelte/tests/css/samples/nested-css-empty-parent/input.svelte new file mode 100644 index 000000000000..90d26c419a7b --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-empty-parent/input.svelte @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-unused-child/expected.css b/packages/svelte/tests/css/samples/nested-css-unused-child/expected.css new file mode 100644 index 000000000000..99741c1c5855 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-unused-child/expected.css @@ -0,0 +1,9 @@ +c.svelte-xyz { + color: red; + + /* (unused) d { + color: yellow; + }*/ + + color: black; +} diff --git a/packages/svelte/tests/css/samples/nested-css-unused-child/input.svelte b/packages/svelte/tests/css/samples/nested-css-unused-child/input.svelte new file mode 100644 index 000000000000..7dcfdf1dcd43 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-unused-child/input.svelte @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-unused-parent/expected.css b/packages/svelte/tests/css/samples/nested-css-unused-parent/expected.css new file mode 100644 index 000000000000..c9867eca17d0 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-unused-parent/expected.css @@ -0,0 +1,9 @@ +/* (unused) a*/ b.svelte-xyz { + color: red; + + x.svelte-xyz { + color: yellow; + } + + color: black; +} diff --git a/packages/svelte/tests/css/samples/nested-css-unused-parent/input.svelte b/packages/svelte/tests/css/samples/nested-css-unused-parent/input.svelte new file mode 100644 index 000000000000..7940ec195c90 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-unused-parent/input.svelte @@ -0,0 +1,15 @@ + + + + \ No newline at end of file From c1dfaafde76c534accd34abeb476674b4cd066a0 Mon Sep 17 00:00:00 2001 From: Albert Date: Thu, 8 Feb 2024 12:29:12 +1030 Subject: [PATCH 39/69] Enhance logic and add some more tests --- .../compiler/phases/2-analyze/css/Selector.js | 46 ++++++++++--------- .../expected.css | 15 ++++++ .../input.svelte | 20 ++++++++ .../expected.css | 9 ++++ .../input.svelte | 19 ++++++++ .../expected.css | 9 ++++ .../input.svelte | 17 +++++++ 7 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-examples/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-examples/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css-class-nested-element/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-class-nested-element/input.svelte 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 2801e12af70a..b68d303aeb2a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -153,16 +153,18 @@ export default class Selector { let i = relative_selector.compound.selectors.length; + let first_selector = relative_selector.compound.selectors[0]; + if (first_selector.type === "TypeSelector" && !first_selector.visible) return + while (i--) { const selector = relative_selector.compound.selectors[i]; - if (!selector.visible) continue - if (selector.use_wrapper.used) continue; + if (!selector.visible) break; + if (selector.use_wrapper.used) break; selector.use_wrapper.used = true; if (selector.type === 'PseudoElementSelector' || selector.type === 'PseudoClassSelector') { - // console.log("PseudoElementSelector or PseudoClassSelector", selector) if (!relative_selector.root && !relative_selector.host) { if (i === 0) code.prependRight(selector.start, attr); } @@ -181,8 +183,6 @@ export default class Selector { let amount_class_specificity_to_increase = max_amount_class_specificity_increased - complex_selector .filter(selector => selector.should_encapsulate) - // .filter(selector => !selector.contains_invisible_selectors) - // .filter(selector => !selector.compound.selectors[0].use_wrapper.used) .length; complex_selector.map((relative_selector, index) => { @@ -191,13 +191,15 @@ export default class Selector { remove_global_pseudo_class(relative_selector.compound.selectors[0]); } + let amount = complex_selector + // Ignore invisible selectors because they are not part of the specificity + .filter(selector => !selector.contains_invisible_selectors) + .length - 1; + if(relative_selector.should_encapsulate) { encapsulate_block( relative_selector, - index === complex_selector - .filter(selector => !selector.contains_invisible_selectors) - // .filter(selector => selector.compound.selectors.some(selector => selector.use_wrapper.used)) - .length - 1 + index === amount ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); @@ -1030,7 +1032,7 @@ function selector_to_blocks(children, parent_complex_selector) { child.use_wrapper = child.use_wrapper ?? { used: false }; // Shallow copy the child to avoid modifying the original's visibility - block.add(/** @type SimpleSelectorWithData */ ({ + block.add(/** @type {SimpleSelectorWithData} */ ({ ...child, visible: child.visible === undefined ? true : child.visible })); @@ -1069,11 +1071,14 @@ function get_parent_selectors(parent_complex_selector) { */ function nest_fake_parents(children, parent_complex_selector) { let nested_selector_index = children.findIndex(child => child.type === 'NestingSelector'); + let had_ampersand = false; - // TODO: Handle multiple nesting selectors? eg: &&& {...} + // TODO: Handle multiple nesting selectors? + // eg: &&& {...} or .bar & .foo {...} if (nested_selector_index === -1) { nested_selector_index = 0; // insert the parent selectors at the beginning of the children array } else { + had_ampersand = true; children.splice(nested_selector_index, 1); // remove the nesting selector, so we can insert there } @@ -1094,23 +1099,22 @@ function nest_fake_parents(children, parent_complex_selector) { if (children.length > 0) { // if the first child is a PseudoClass, mark it as invisible because the & provides scoping let child_after = children[nested_selector_index]; - if (child_after?.type === 'PseudoClassSelector') { - child_after.visible = false; + let last_parent_child = parent_selectors[parent_selectors.length - 1]; + let child_before = children[nested_selector_index - 1]; + + if(last_parent_child.type !== "Combinator" && !child_after) { + // Case: b { c & { color: red }} (we need to mark b as visible so we increase specifity) + last_parent_child.visible = true; } - let child_before = children[nested_selector_index - 1]; - let last_parent_child = parent_selectors[parent_selectors.length - 1]; if(child_before?.type !== "Combinator") { if (child_after?.type !== 'Combinator') { - children.splice(nested_selector_index, 0, FakeCombinator); + if(!had_ampersand) { + children.splice(nested_selector_index, 0, FakeCombinator); + } } } - - if(last_parent_child.type !== "Combinator" && !child_after) { - // Case: b { c & { color: red }} (we need to mark b as visible so we increase specifity) - last_parent_child.visible = true; - } } // Finally, insert the parent selectors into the children array children.splice(nested_selector_index, 0, ...parent_selectors); diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-examples/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-examples/expected.css new file mode 100644 index 000000000000..50a95e2f14ca --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-examples/expected.css @@ -0,0 +1,15 @@ +c.svelte-xyz { + color: red; + + &.bar { + color: yellow; + } + + &:last-child { + color: black; + } + + &::before { + color: green; + } +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-examples/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-examples/input.svelte new file mode 100644 index 000000000000..3192f99cd6e3 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-examples/input.svelte @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/expected.css new file mode 100644 index 000000000000..6dc03b2d5a44 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/expected.css @@ -0,0 +1,9 @@ +c.svelte-xyz { + color: red; + + b.svelte-xyz & x.svelte-xyz { + color: yellow; + } + + color: black; +} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/input.svelte new file mode 100644 index 000000000000..f75592abb096 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/input.svelte @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-class-nested-element/expected.css b/packages/svelte/tests/css/samples/nested-css-class-nested-element/expected.css new file mode 100644 index 000000000000..2e3bb32c6bee --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-class-nested-element/expected.css @@ -0,0 +1,9 @@ +.foo.svelte-xyz { + color: red; + + &c.svelte-xyz { + color: yellow; + } + + color: black; +} diff --git a/packages/svelte/tests/css/samples/nested-css-class-nested-element/input.svelte b/packages/svelte/tests/css/samples/nested-css-class-nested-element/input.svelte new file mode 100644 index 000000000000..1fcaf6936515 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-class-nested-element/input.svelte @@ -0,0 +1,17 @@ +
+
+ + + + \ No newline at end of file From 015e6958ce97e625ecfe0004f57eb8a18e5fe875 Mon Sep 17 00:00:00 2001 From: Albert Date: Thu, 8 Feb 2024 12:31:42 +1030 Subject: [PATCH 40/69] Clean some logic --- .../compiler/phases/2-analyze/css/Selector.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 b68d303aeb2a..f72133d4d8d5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1071,14 +1071,14 @@ function get_parent_selectors(parent_complex_selector) { */ function nest_fake_parents(children, parent_complex_selector) { let nested_selector_index = children.findIndex(child => child.type === 'NestingSelector'); - let had_ampersand = false; + let used_ampersand = false; // TODO: Handle multiple nesting selectors? // eg: &&& {...} or .bar & .foo {...} if (nested_selector_index === -1) { nested_selector_index = 0; // insert the parent selectors at the beginning of the children array } else { - had_ampersand = true; + used_ampersand = true; children.splice(nested_selector_index, 1); // remove the nesting selector, so we can insert there } @@ -1108,12 +1108,12 @@ function nest_fake_parents(children, parent_complex_selector) { } - if(child_before?.type !== "Combinator") { - if (child_after?.type !== 'Combinator') { - if(!had_ampersand) { - children.splice(nested_selector_index, 0, FakeCombinator); - } - } + if( + child_before?.type !== "Combinator" + && child_after?.type !== "Combinator" + && !used_ampersand + ) { + children.splice(nested_selector_index, 0, FakeCombinator); } } // Finally, insert the parent selectors into the children array From bd375825b67f4e627683d983b485b12d781e1f21 Mon Sep 17 00:00:00 2001 From: Albert Date: Thu, 8 Feb 2024 13:18:03 +1030 Subject: [PATCH 41/69] Update deprecated function --- packages/svelte/src/compiler/phases/1-parse/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index dc8c8d553779..45628df4aaba 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -47,7 +47,7 @@ export class Parser { throw new TypeError('Template must be a string'); } - this.template = template.trimRight(); + this.template = template.trimEnd(); let match_lang; From 4b4922e10ed93afaaea78437c01e6381c024d827 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Feb 2024 15:43:30 -0500 Subject: [PATCH 42/69] prettier --- packages/svelte/src/compiler/errors.js | 2 +- .../compiler/phases/2-analyze/css/Selector.js | 107 ++++++++++-------- .../phases/2-analyze/css/Stylesheet.js | 8 +- packages/svelte/src/compiler/types/css.d.ts | 4 +- 4 files changed, 67 insertions(+), 54 deletions(-) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 6b8d3333d71b..91bbb13cc999 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -108,7 +108,7 @@ const css = { `: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', - 'nesting-selector-not-allowed': () => 'Nesting selector is not allowed in top level rules', + '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 f72133d4d8d5..14241f483ba7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -55,7 +55,7 @@ export default class Selector { this.local_selector_list = []; // Process each block group to take trailing :global(...) selectors out of consideration - this.selector_list.forEach(complex_selector => { + this.selector_list.forEach((complex_selector) => { let i = complex_selector.length; while (i > 0) { if (!complex_selector[i - 1].global) break; @@ -77,7 +77,7 @@ export default class Selector { } // Check if there are no local blocks across all groups, or if there's a host_only or root_only situation - const no_local_blocks = this.local_selector_list.every(group => group.length === 0); + const no_local_blocks = this.local_selector_list.every((group) => group.length === 0); this.used = no_local_blocks || host_only || root_only; } /** @@ -115,7 +115,9 @@ export default class Selector { * */ const used_blocks = new Map(); - this.local_selector_list.map(complex_selector => apply_selector(complex_selector.slice(), node, used_blocks)); + this.local_selector_list.map((complex_selector) => + apply_selector(complex_selector.slice(), node, used_blocks) + ); // Iterate over used_blocks for (const [relative_selector, nodes] of used_blocks) { @@ -154,7 +156,7 @@ export default class Selector { let i = relative_selector.compound.selectors.length; let first_selector = relative_selector.compound.selectors[0]; - if (first_selector.type === "TypeSelector" && !first_selector.visible) return + if (first_selector.type === 'TypeSelector' && !first_selector.visible) return; while (i--) { const selector = relative_selector.compound.selectors[i]; @@ -177,13 +179,12 @@ export default class Selector { } break; } - }; + } for (const complex_selector of this.selector_list) { let amount_class_specificity_to_increase = - max_amount_class_specificity_increased - complex_selector - .filter(selector => selector.should_encapsulate) - .length; + max_amount_class_specificity_increased - + complex_selector.filter((selector) => selector.should_encapsulate).length; complex_selector.map((relative_selector, index) => { if (relative_selector.global) { @@ -191,17 +192,15 @@ export default class Selector { remove_global_pseudo_class(relative_selector.compound.selectors[0]); } - let amount = complex_selector - // Ignore invisible selectors because they are not part of the specificity - .filter(selector => !selector.contains_invisible_selectors) - .length - 1; + let amount = + complex_selector + // Ignore invisible selectors because they are not part of the specificity + .filter((selector) => !selector.contains_invisible_selectors).length - 1; - if(relative_selector.should_encapsulate) { + if (relative_selector.should_encapsulate) { encapsulate_block( relative_selector, - index === amount - ? attr.repeat(amount_class_specificity_to_increase + 1) - : attr + index === amount ? attr.repeat(amount_class_specificity_to_increase + 1) : attr ); } }); @@ -216,7 +215,6 @@ export default class Selector { this.validate_invalid_combinator_without_selector(analysis); } - validate_invalid_css_global_placement() { for (let complex_selector of this.selector_list) { let start = 0; @@ -235,7 +233,6 @@ export default class Selector { } } - validate_global_with_multiple_selectors() { for (const complex_selector of this.selector_list) { if (complex_selector.length === 1 && complex_selector[0].compound.selectors.length === 1) { @@ -284,7 +281,9 @@ export default class Selector { (i !== 0 || relative_selector.compound.selectors .slice(1) - .some(s => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector')) + .some( + (s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector' + )) ) { error(selector, 'invalid-css-global-selector-list'); } @@ -297,9 +296,8 @@ export default class Selector { get_amount_class_specificity_increased() { // 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.selector_list[0].filter(selector => selector.should_encapsulate).length; + return this.selector_list[0].filter((selector) => selector.should_encapsulate).length; } - } /** @@ -361,7 +359,7 @@ function apply_selector(blocks, node, to_encapsulate) { return true; } } - if (blocks.every(block => block.global)) { + if (blocks.every((block) => block.global)) { add_node(to_encapsulate, block, node); return true; } @@ -882,7 +880,6 @@ function loop_child(children, adjacent_only) { return result; } - /** * Not shared between different Selector instances */ @@ -911,17 +908,25 @@ class RelativeSelector { this.compound.add(selector); } - get global() { return this.compound.global } - get host() { return this.compound.host } - get root() { return this.compound.root } - get end() {return this.compound.end } + get global() { + return this.compound.global; + } + get host() { + return this.compound.host; + } + get root() { + return this.compound.root; + } + get end() { + return this.compound.end; + } get start() { if (this.combinator) return this.combinator.start; return this.compound.start; } get contains_invisible_selectors() { - return this.compound.selectors.some(selector => !selector.visible); + return this.compound.selectors.some((selector) => !selector.visible); } } @@ -957,8 +962,8 @@ class CompoundSelector { this.selectors = []; this.start = -1; this.end = -1; - this.host = false - this.root = false + this.host = false; + this.root = false; } /** @param {SimpleSelectorWithData} selector */ @@ -977,7 +982,10 @@ class CompoundSelector { this.selectors.length >= 1 && this.selectors[0].type === 'PseudoClassSelector' && this.selectors[0].name === 'global' && - this.selectors.every(selector => selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') + this.selectors.every( + (selector) => + selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' + ) ); } } @@ -995,14 +1003,14 @@ function group_selectors(selector, parent_selector_list) { return [selector_to_blocks([...selector.children], null)]; } - return parent_selector_list.map(parent_complex_selector => { + return parent_selector_list.map((parent_complex_selector) => { const block_group = selector_to_blocks( [...selector.children], [...parent_complex_selector] // Clone the parent's blocks to avoid modifying the original array ); return block_group; - }) + }); } /** @@ -1010,7 +1018,7 @@ function group_selectors(selector, parent_selector_list) { * @param {ComplexSelector | null} parent_complex_selector - The parent rule's selectors to insert/swap into the nesting selector positions. */ function selector_to_blocks(children, parent_complex_selector) { - let block = new RelativeSelector(null, new CompoundSelector); + let block = new RelativeSelector(null, new CompoundSelector()); const blocks = [block]; // If this is a nested rule @@ -1018,7 +1026,7 @@ function selector_to_blocks(children, parent_complex_selector) { for (const child of children) { if (child.type === 'Combinator') { - block = new RelativeSelector(child, new CompoundSelector); + block = new RelativeSelector(child, new CompoundSelector()); blocks.push(block); } else if (child.type === 'NestingSelector') { if (!parent_complex_selector) { @@ -1032,10 +1040,12 @@ function selector_to_blocks(children, parent_complex_selector) { child.use_wrapper = child.use_wrapper ?? { used: false }; // Shallow copy the child to avoid modifying the original's visibility - block.add(/** @type {SimpleSelectorWithData} */ ({ - ...child, - visible: child.visible === undefined ? true : child.visible - })); + block.add( + /** @type {SimpleSelectorWithData} */ ({ + ...child, + visible: child.visible === undefined ? true : child.visible + }) + ); } } @@ -1053,9 +1063,9 @@ function get_parent_selectors(parent_complex_selector) { parent_selectors.push(relative_selector.combinator); } parent_selectors.push( - ...relative_selector.compound.selectors.map(selector => ({ + ...relative_selector.compound.selectors.map((selector) => ({ ...selector, - visible: false, + visible: false })) ); } @@ -1070,7 +1080,7 @@ function get_parent_selectors(parent_complex_selector) { * @param {ComplexSelector} parent_complex_selector - The parent blocks to insert into the nesting selector positions. */ function nest_fake_parents(children, parent_complex_selector) { - let nested_selector_index = children.findIndex(child => child.type === 'NestingSelector'); + let nested_selector_index = children.findIndex((child) => child.type === 'NestingSelector'); let used_ampersand = false; // TODO: Handle multiple nesting selectors? @@ -1102,20 +1112,19 @@ function nest_fake_parents(children, parent_complex_selector) { let last_parent_child = parent_selectors[parent_selectors.length - 1]; let child_before = children[nested_selector_index - 1]; - if(last_parent_child.type !== "Combinator" && !child_after) { + if (last_parent_child.type !== 'Combinator' && !child_after) { // Case: b { c & { color: red }} (we need to mark b as visible so we increase specifity) last_parent_child.visible = true; } - - if( - child_before?.type !== "Combinator" - && child_after?.type !== "Combinator" - && !used_ampersand + if ( + child_before?.type !== 'Combinator' && + child_after?.type !== 'Combinator' && + !used_ampersand ) { children.splice(nested_selector_index, 0, FakeCombinator); } } // Finally, insert the parent selectors into the children array children.splice(nested_selector_index, 0, ...parent_selectors); -} \ 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 c6d14bab86f9..1e6902cc43c0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -88,8 +88,12 @@ class Rule { * - .b .c */ if (parent && parent.node.type === 'Rule') { - let block_groups = /** @type {Rule} **/ (parent).selectors.map(selector => selector.selector_list).flat(); - this.selectors = node.prelude.children.map(node => new Selector(node, stylesheet, block_groups)); + let block_groups = /** @type {Rule} **/ (parent).selectors + .map((selector) => selector.selector_list) + .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)); } diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 8d8eef2ccf35..f54024842e5f 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -90,8 +90,8 @@ export type SimpleSelector = ( ) & { // Deeply nested because we want to track whether the selector is used // between clones of the AST - use_wrapper?: { used: boolean }, - visible?: boolean + use_wrapper?: { used: boolean }; + visible?: boolean; }; export interface Combinator extends BaseNode { From 055ec389cf03622d081b95b775225d6ac8ff01a5 Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 12 Feb 2024 19:50:58 +1030 Subject: [PATCH 43/69] WIP Test --- .../compiler/phases/2-analyze/css/Selector.js | 1 + .../expected.css | 0 .../input.svelte | 27 +++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/input.svelte 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 f72133d4d8d5..8bc51f2be36a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -194,6 +194,7 @@ export default class Selector { let amount = complex_selector // Ignore invisible selectors because they are not part of the specificity .filter(selector => !selector.contains_invisible_selectors) + // .filter(selector => selector.should_encapsulate) .length - 1; if(relative_selector.should_encapsulate) { diff --git a/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css b/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/input.svelte b/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/input.svelte new file mode 100644 index 000000000000..dd7ac9d1d364 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/input.svelte @@ -0,0 +1,27 @@ + + + x + + y + + + + + + + + + + \ No newline at end of file From a42170579fb28d4262d82e4cb287e46b2fa9626f Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 12 Feb 2024 19:58:42 +1030 Subject: [PATCH 44/69] Add test expected result --- .../nested-css-deep-nested-selector-list/expected.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css b/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css index e69de29bb2d1..8bb94954bd4d 100644 --- a/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css +++ b/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css @@ -0,0 +1,11 @@ +foo.svelte-xyz, bar.svelte-xyz { + color: purple; + + x:where(.svelte-xyz) { + color: red; + + y:where(.svelte-xyz) { + color: blue; + } + } +} \ No newline at end of file From b9b4cabc9a314fed9297eda2aa3e873bd6d4f69a Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 12 Feb 2024 21:24:43 +1030 Subject: [PATCH 45/69] Pass all tests --- .../svelte/src/compiler/phases/2-analyze/css/Selector.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 017f7edba763..c945965a8ee2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1,6 +1,7 @@ import { get_possible_values } from './gather_possible_values.js'; import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../patterns.js'; import { error } from '../../../errors.js'; +import { relative } from 'node:path'; const NO_MATCH = 'NO_MATCH'; const POSSIBLE_MATCH = 'POSSIBLE_MATCH'; @@ -182,8 +183,12 @@ export default class Selector { } } - let first = true; + for (const complex_selector of this.selector_list) { + // We must wrap modifier with :where if the first selector is invisible (part of a nested rule) + let is_first_invisible_selector = complex_selector[0].contains_invisible_selectors + let contains_any_invisible_selectors = complex_selector.some(relative_selector => relative_selector.contains_invisible_selectors) + let first = contains_any_invisible_selectors ? is_first_invisible_selector : true for (const relative_selector of complex_selector) { if (relative_selector.global) { // Remove the global pseudo class from the selector @@ -1021,7 +1026,7 @@ function selector_to_blocks(children, parent_complex_selector) { error(child, 'nesting-selector-not-allowed'); } else { // We shoudld've already handled these above (except for multiple nesting selectors, which is supposed to work?) - throw new Error('unexpected nesting selector'); + throw new Error('Unexpected nesting selector, multiple nesting selectors (&) are not supported yet'); } } else { // shared reference bween all children From f6a77967abd4a7111d52ffa5f12258896cfcf59a Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 12 Feb 2024 22:19:24 +1030 Subject: [PATCH 46/69] Pass test for invalid characters and simplify block item parse code --- packages/svelte/src/compiler/errors.js | 3 ++- .../src/compiler/phases/1-parse/read/style.js | 24 ++++++++++--------- .../weird-syntactic-escaping/expected.css | 13 ++++++++++ .../weird-syntactic-escaping/input.svelte | 19 +++++++++++++++ 4 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 packages/svelte/tests/css/samples/weird-syntactic-escaping/expected.css create mode 100644 packages/svelte/tests/css/samples/weird-syntactic-escaping/input.svelte diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 5fcac7e39b13..4795c17182b8 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -93,7 +93,8 @@ const parse = { 'invalid-render-arguments': () => 'expected at most one argument', 'invalid-render-spread-argument': () => 'cannot use spread arguments in {@render ...} tags', 'invalid-snippet-rest-parameter': () => - 'snippets do not support rest parameters; use an array instead' + 'snippets do not support rest parameters; use an array instead', + 'expected-declaration-or-nested-rule': () => 'Expected a CSS declaration or nested rule', }; /** @satisfies {Errors} */ diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 79707c59b114..70a23add3189 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -10,7 +10,6 @@ const REGEX_NTH_OF = /^(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/; const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/; -const REGEX_SEMICOLON_OR_OPEN_BRACE_OR_CLOSE_BRACE = /[;{}]/; const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/; const REGEX_COMMENT_CLOSE = /\*\//; const REGEX_HTML_COMMENT_CLOSE = /-->/; @@ -407,18 +406,21 @@ function read_block_item(parser) { } const start = parser.index; - parser.read_until(REGEX_SEMICOLON_OR_OPEN_BRACE_OR_CLOSE_BRACE); - // if we've run into a '{', it's a rule, otherwise we ran into - // a ';' or '}' so it's a declaration - if (parser.match('{')) { - // Rewind to the start of the rule - parser.index = start; - return read_rule(parser); + // Here we need to distinguish between potential declarations and potentially nested CSS rules + // properly ignoring syntactic tokens such as comments or strings that may contain the real + // syntactically valid tokens such as ; { or } + // For example: content: '{;}'; is a valid declaration + try { + try { + return read_declaration(parser) + } catch(e) { + parser.index = start + return read_rule(parser) + } + } catch(e) { + error(parser.index, 'expected-declaration-or-nested-rule') } - // Rewind to the start of the declaration - parser.index = start; - return read_declaration(parser); } /** diff --git a/packages/svelte/tests/css/samples/weird-syntactic-escaping/expected.css b/packages/svelte/tests/css/samples/weird-syntactic-escaping/expected.css new file mode 100644 index 000000000000..6d3eaf6c3d46 --- /dev/null +++ b/packages/svelte/tests/css/samples/weird-syntactic-escaping/expected.css @@ -0,0 +1,13 @@ +[title='{;}'].svelte-xyz { + content: "{};[]"; + /* {} */ + color: red; + foo:where(.svelte-xyz) { + color: red; + /* [] ; { } */ + color: green; + /* [] { } ; */ + color: black; + } + color: red; +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/weird-syntactic-escaping/input.svelte b/packages/svelte/tests/css/samples/weird-syntactic-escaping/input.svelte new file mode 100644 index 000000000000..a94156eb068c --- /dev/null +++ b/packages/svelte/tests/css/samples/weird-syntactic-escaping/input.svelte @@ -0,0 +1,19 @@ +
+ +
+ + From fb0991769523ef42751c78fc39f72b3fe7e39d49 Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 12 Feb 2024 23:03:41 +1030 Subject: [PATCH 47/69] Add multiple nesting selector support --- .../compiler/phases/2-analyze/css/Selector.js | 84 ++++++++++--------- .../expected.css | 2 +- .../expected.css | 2 +- .../nested-css-ampersand-suffix/expected.css | 2 +- .../expected.css | 12 +++ .../input.svelte | 31 +++++++ 6 files changed, 89 insertions(+), 44 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css-multiple-ampersands/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css-multiple-ampersands/input.svelte 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 c945965a8ee2..0b8d7403e125 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1025,8 +1025,8 @@ function selector_to_blocks(children, parent_complex_selector) { if (!parent_complex_selector) { error(child, 'nesting-selector-not-allowed'); } else { - // We shoudld've already handled these above (except for multiple nesting selectors, which is supposed to work?) - throw new Error('Unexpected nesting selector, multiple nesting selectors (&) are not supported yet'); + // We shoudld've already handled these above + throw new Error('Unexpected nesting selector'); } } else { // shared reference bween all children @@ -1069,55 +1069,57 @@ function get_parent_selectors(parent_complex_selector) { * Nest the parent selectors into the children array so we can easily * check for usage and scoping. * + * Some cases: + * b { c { color: red }} -> need to insert ' ' before c, so output needs to look like [b, " ", c] + * b { & c { color: red }} -> already has a child combinator before c, so output needs to look like [b, " ", c] + * b { & > c { color: red }} -> next combinator is '>' so output needs to look like [b, >, c] + * b { c & { color: red }} -> so we need to insert ' ' after c so children needs to look like [c, " ",b] + * .x { & { color: red }} -> no combinator, so children needs to look like .x.x + * * @param {import('#compiler').Css.Selector["children"]} children * @param {ComplexSelector} parent_complex_selector - The parent blocks to insert into the nesting selector positions. */ function nest_fake_parents(children, parent_complex_selector) { - let nested_selector_index = children.findIndex((child) => child.type === 'NestingSelector'); - let used_ampersand = false; - - // TODO: Handle multiple nesting selectors? - // eg: &&& {...} or .bar & .foo {...} - if (nested_selector_index === -1) { - nested_selector_index = 0; // insert the parent selectors at the beginning of the children array - } else { - used_ampersand = true; - children.splice(nested_selector_index, 1); // remove the nesting selector, so we can insert there + const nested_selector_indexes = children.reduce((indexes, child, index) => { + if (child.type === 'NestingSelector') { + indexes.push(index); + } + return indexes; + }, /** @type {number[]} */ ([])); + + let used_ampersand = nested_selector_indexes.length !== 0; + + if (!used_ampersand) { + // insert the parent selectors at the beginning of the children array + nested_selector_indexes.push(0); + // If there are no nesting selectors and the next item is not a combinator + // we need to insert a fake combinator because: + // a { b { color: red }} is equivalent to a { & b { color: red }} + // however a { + b { color: red }} is not equivalent to a [ "&", " ", "+", "b" ] { color: red }} + if (children[0].type !== 'Combinator') { + children.unshift(FakeCombinator); + } + children.unshift({ type: 'NestingSelector', name: "&", start: -1, end: -1 }); } - // Modify the first child after the nesting selector to have a flag disabling attr - /** @type typeof children */ const parent_selectors = get_parent_selectors(parent_complex_selector); - /** - * Some cases - * b { c { color: red }} -> need to insert ' ' before c, so output needs to look like [b, " ", c] - * b { & c { color: red }} -> already has a child combinator before c, so output needs to look like [b, " ", c] - * b { & > c { color: red }} -> next combinator is '>' so output needs to look like [b, >, c] - * b { c & { color: red }} -> so we need to insert ' ' after c so children needs to look like [c, " ",b] - * b { & { color: red }} -> no combinator, so children needs to look like [b] - */ - - if (children.length > 0) { - // if the first child is a PseudoClass, mark it as invisible because the & provides scoping - let child_after = children[nested_selector_index]; - let last_parent_child = parent_selectors[parent_selectors.length - 1]; - let child_before = children[nested_selector_index - 1]; + // Insert the parent selectors into the children array in reverse order (so we don't mess up the indexes) + for (const nested_selector_index of nested_selector_indexes.reverse()) { + if (used_ampersand) { + let child_after = children[nested_selector_index + 1]; + let child_before = children[nested_selector_index - 1]; - if (last_parent_child.type !== 'Combinator' && !child_after) { - // Case: b { c & { color: red }} (we need to mark b as visible so we increase specifity) - last_parent_child.visible = true; - } - - if ( - child_before?.type !== 'Combinator' && - child_after?.type !== 'Combinator' && - !used_ampersand - ) { - children.splice(nested_selector_index, 0, FakeCombinator); + if ( + child_before?.type !== 'Combinator' && + child_after?.type !== 'Combinator' && + !used_ampersand + ) { + children.splice(nested_selector_index, 0, FakeCombinator); + } } + // Finally, insert the parent selectors into the children array + children.splice(nested_selector_index, 1, ...parent_selectors); } - // Finally, insert the parent selectors into the children array - children.splice(nested_selector_index, 0, ...parent_selectors); } diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css index 50ad637d577f..1a5fec41ee8b 100644 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css @@ -1,7 +1,7 @@ c.svelte-xyz { color: red; - /* (empty) b.svelte-xyz & { + /* (empty) b:where(.svelte-xyz) & { }*/ diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css index fb3721185a3f..dae6bf588bb9 100644 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css @@ -1,7 +1,7 @@ c.svelte-xyz { color: red; - b.svelte-xyz + & { + b:where(.svelte-xyz) + & { color: yellow; } diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css index bcdc01d1e60b..66603952a3ec 100644 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css +++ b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css @@ -1,7 +1,7 @@ c.svelte-xyz { color: red; - b.svelte-xyz & { + b:where(.svelte-xyz) & { color: yellow; } diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/expected.css b/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/expected.css new file mode 100644 index 000000000000..8848b5bfee1d --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/expected.css @@ -0,0 +1,12 @@ +button.svelte-xyz { + color: red; + &&& { + color: blue; + } +} + +x.svelte-xyz { + foo:where(.svelte-xyz) & foo & { + color: green; + } +} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/input.svelte b/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/input.svelte new file mode 100644 index 000000000000..075771be2a9e --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/input.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + + + + \ No newline at end of file From 9c5c05e55acb3058f0ce71cfa7862d360edd9e81 Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 12 Feb 2024 23:10:42 +1030 Subject: [PATCH 48/69] Lol wot? Apparently I didn't need that logic --- .../src/compiler/phases/2-analyze/css/Selector.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) 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 0b8d7403e125..fcfcec9c783e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1025,7 +1025,7 @@ function selector_to_blocks(children, parent_complex_selector) { if (!parent_complex_selector) { error(child, 'nesting-selector-not-allowed'); } else { - // We shoudld've already handled these above + // We shoudld've already handled these above (except for multiple nesting selectors, which is supposed to work?) throw new Error('Unexpected nesting selector'); } } else { @@ -1107,19 +1107,6 @@ function nest_fake_parents(children, parent_complex_selector) { // Insert the parent selectors into the children array in reverse order (so we don't mess up the indexes) for (const nested_selector_index of nested_selector_indexes.reverse()) { - if (used_ampersand) { - let child_after = children[nested_selector_index + 1]; - let child_before = children[nested_selector_index - 1]; - - if ( - child_before?.type !== 'Combinator' && - child_after?.type !== 'Combinator' && - !used_ampersand - ) { - children.splice(nested_selector_index, 0, FakeCombinator); - } - } - // Finally, insert the parent selectors into the children array children.splice(nested_selector_index, 1, ...parent_selectors); } } From 26ba13047f3c6515a2a4f0d911762eb485ab49f7 Mon Sep 17 00:00:00 2001 From: Albert Date: Mon, 12 Feb 2024 23:24:40 +1030 Subject: [PATCH 49/69] Remove some unused logic --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 1 - 1 file changed, 1 deletion(-) 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 fcfcec9c783e..e81dd876aa01 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -161,7 +161,6 @@ export default class Selector { while (i--) { const selector = relative_selector.compound.selectors[i]; - if (!selector.visible) break; if (selector.use_wrapper.used) break; selector.use_wrapper.used = true; From 2c3cdabfe031d5b92af565cfe16528ab6de4ab62 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 14:28:10 -0500 Subject: [PATCH 50/69] avoid try-catch --- .../src/compiler/phases/1-parse/read/style.js | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 70a23add3189..79d30f799070 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -407,20 +407,13 @@ function read_block_item(parser) { const start = parser.index; - // Here we need to distinguish between potential declarations and potentially nested CSS rules - // properly ignoring syntactic tokens such as comments or strings that may contain the real - // syntactically valid tokens such as ; { or } - // For example: content: '{;}'; is a valid declaration - try { - try { - return read_declaration(parser) - } catch(e) { - parser.index = start - return read_rule(parser) - } - } catch(e) { - error(parser.index, 'expected-declaration-or-nested-rule') - } + // read ahead to understand whether we're dealing with a declaration or a nested rule. + // this involves some duplicated work, but avoids a try-catch that would disguise errors + read_value(parser); + const char = parser.template[parser.index]; + parser.index = start; + + return char === '{' ? read_rule(parser) : read_declaration(parser); } /** From a528ee66197a194bd2d98cb9d8fb5b98519cf8cd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 14:28:47 -0500 Subject: [PATCH 51/69] prettier --- packages/svelte/src/compiler/errors.js | 2 +- .../src/compiler/phases/2-analyze/css/Selector.js | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 4795c17182b8..f5e2786c4429 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -94,7 +94,7 @@ const parse = { 'invalid-render-spread-argument': () => 'cannot use spread arguments in {@render ...} tags', 'invalid-snippet-rest-parameter': () => 'snippets do not support rest parameters; use an array instead', - 'expected-declaration-or-nested-rule': () => 'Expected a CSS declaration or nested rule', + 'expected-declaration-or-nested-rule': () => 'Expected a CSS declaration or nested rule' }; /** @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 e81dd876aa01..0d760b10d857 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -182,12 +182,13 @@ export default class Selector { } } - for (const complex_selector of this.selector_list) { // We must wrap modifier with :where if the first selector is invisible (part of a nested rule) - let is_first_invisible_selector = complex_selector[0].contains_invisible_selectors - let contains_any_invisible_selectors = complex_selector.some(relative_selector => relative_selector.contains_invisible_selectors) - let first = contains_any_invisible_selectors ? is_first_invisible_selector : true + let is_first_invisible_selector = complex_selector[0].contains_invisible_selectors; + let contains_any_invisible_selectors = complex_selector.some( + (relative_selector) => relative_selector.contains_invisible_selectors + ); + let first = contains_any_invisible_selectors ? is_first_invisible_selector : true; for (const relative_selector of complex_selector) { if (relative_selector.global) { // Remove the global pseudo class from the selector @@ -1098,7 +1099,7 @@ function nest_fake_parents(children, parent_complex_selector) { if (children[0].type !== 'Combinator') { children.unshift(FakeCombinator); } - children.unshift({ type: 'NestingSelector', name: "&", start: -1, end: -1 }); + children.unshift({ type: 'NestingSelector', name: '&', start: -1, end: -1 }); } /** @type typeof children */ From bb8993bb0e4b64bc5afeab9de7cba8dee180d295 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 15:18:18 -0500 Subject: [PATCH 52/69] we really don't need all these tests, it's overkill. one will do. still some bugs --- .../expected.css | 9 --- .../input.svelte | 17 ----- .../expected.css | 20 ------ .../input.svelte | 28 -------- .../expected.css | 15 ---- .../input.svelte | 20 ------ .../expected.css | 9 --- .../input.svelte | 15 ---- .../expected.css | 7 -- .../input.svelte | 15 ---- .../expected.css | 9 --- .../input.svelte | 19 ----- .../expected.css | 9 --- .../input.svelte | 17 ----- .../nested-css-ampersand-suffix/expected.css | 9 --- .../nested-css-ampersand-suffix/input.svelte | 17 ----- .../samples/nested-css-at-rule/expected.css | 12 ---- .../samples/nested-css-at-rule/input.svelte | 20 ------ .../nested-css-child-combinator/expected.css | 9 --- .../nested-css-child-combinator/input.svelte | 17 ----- .../expected.css | 9 --- .../input.svelte | 17 ----- .../expected.css | 11 --- .../input.svelte | 27 -------- .../expected.css | 9 --- .../input.svelte | 16 ----- .../nested-css-element-selector/expected.css | 7 -- .../nested-css-element-selector/input.svelte | 14 ---- .../nested-css-empty-child/expected.css | 6 -- .../nested-css-empty-child/input.svelte | 11 --- .../nested-css-empty-parent/expected.css | 5 -- .../nested-css-empty-parent/input.svelte | 10 --- .../expected.css | 12 ---- .../input.svelte | 31 --------- .../nested-css-multiple-layers/expected.css | 17 ----- .../nested-css-multiple-layers/input.svelte | 29 -------- .../nested-css-self-referential/expected.css | 9 --- .../nested-css-self-referential/input.svelte | 17 ----- .../nested-css-unused-child/expected.css | 9 --- .../nested-css-unused-child/input.svelte | 15 ---- .../nested-css-unused-parent/expected.css | 9 --- .../nested-css-unused-parent/input.svelte | 15 ---- .../expected.css | 9 --- .../input.svelte | 20 ------ .../tests/css/samples/nested-css/expected.css | 54 +++++++++++++++ .../tests/css/samples/nested-css/input.svelte | 69 +++++++++++++++++++ 46 files changed, 123 insertions(+), 627 deletions(-) delete mode 100644 packages/svelte/tests/css/samples/nested-css-adjacent-combinator/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-adjacent-combinator/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-combinator/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-combinator/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-examples/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-examples/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-at-rule/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-at-rule/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-child-combinator/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-child-combinator/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-class-nested-element/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-class-nested-element/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-descendant-combinator/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-descendant-combinator/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-element-selector/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-element-selector/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-empty-child/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-empty-child/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-empty-parent/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-empty-parent/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-multiple-ampersands/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-multiple-ampersands/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-self-referential/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-self-referential/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-unused-child/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-unused-child/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-unused-parent/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-unused-parent/input.svelte delete mode 100644 packages/svelte/tests/css/samples/nested-css-with-selector-list/expected.css delete mode 100644 packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte create mode 100644 packages/svelte/tests/css/samples/nested-css/expected.css create mode 100644 packages/svelte/tests/css/samples/nested-css/input.svelte diff --git a/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/expected.css deleted file mode 100644 index 5d2f1b8ba203..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -button.svelte-xyz { - color: red; - - + .abc:where(.svelte-xyz) { - color: purple; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/input.svelte deleted file mode 100644 index 85a8e0131172..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-adjacent-combinator/input.svelte +++ /dev/null @@ -1,17 +0,0 @@ - -
- -
- \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-combinator/expected.css deleted file mode 100644 index 8bbe45357064..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-combinator/expected.css +++ /dev/null @@ -1,20 +0,0 @@ -button.svelte-xyz { - color: red; - & > div:where(.svelte-xyz) { - color: blue; - } - & .foo:where(.svelte-xyz) { - color: yellow; - } - &:last-child { - color: green; - } - - & .bar:where(.svelte-xyz) { - & .hello:where(.svelte-xyz) { - color: orange; - } - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-combinator/input.svelte deleted file mode 100644 index 8e4d3bead9bd..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-combinator/input.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-examples/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-examples/expected.css deleted file mode 100644 index 50a95e2f14ca..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-examples/expected.css +++ /dev/null @@ -1,15 +0,0 @@ -c.svelte-xyz { - color: red; - - &.bar { - color: yellow; - } - - &:last-child { - color: black; - } - - &::before { - color: green; - } -} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-examples/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-examples/input.svelte deleted file mode 100644 index 3192f99cd6e3..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-examples/input.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css deleted file mode 100644 index 1a5fec41ee8b..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -c.svelte-xyz { - color: red; - - /* (empty) b:where(.svelte-xyz) & { - - }*/ - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/input.svelte deleted file mode 100644 index bb88e2012d2a..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-empty/input.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/expected.css deleted file mode 100644 index 50060c1aaba1..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/expected.css +++ /dev/null @@ -1,7 +0,0 @@ -c.svelte-xyz { - color: red; - - /* (unused) b + & { - color: red; - }*/ -} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/input.svelte deleted file mode 100644 index 0504532aa849..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-unused-combinator/input.svelte +++ /dev/null @@ -1,15 +0,0 @@ - -
- - -
- \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/expected.css deleted file mode 100644 index 0904eaa1b3c3..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -c.svelte-xyz { - color: red; - - b:where(.svelte-xyz) & x:where(.svelte-xyz) { - color: yellow; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/input.svelte deleted file mode 100644 index f75592abb096..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-children-after/input.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css deleted file mode 100644 index dae6bf588bb9..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -c.svelte-xyz { - color: red; - - b:where(.svelte-xyz) + & { - color: yellow; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/input.svelte deleted file mode 100644 index ae1be9dce273..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix-with-combinator-prefix/input.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css deleted file mode 100644 index 66603952a3ec..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -c.svelte-xyz { - color: red; - - b:where(.svelte-xyz) & { - color: yellow; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte b/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte deleted file mode 100644 index 7b0a45bcd55f..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-ampersand-suffix/input.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-at-rule/expected.css b/packages/svelte/tests/css/samples/nested-css-at-rule/expected.css deleted file mode 100644 index a7f507bd495c..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-at-rule/expected.css +++ /dev/null @@ -1,12 +0,0 @@ -button.svelte-xyz { - color: red; - - @media (max-width: 500px) { - color: blue; - .xyz.svelte-xyz { - color: yellow; - } - } - - color: black; -} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-at-rule/input.svelte b/packages/svelte/tests/css/samples/nested-css-at-rule/input.svelte deleted file mode 100644 index cd07582c6ea7..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-at-rule/input.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-child-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-child-combinator/expected.css deleted file mode 100644 index b5e0140e9c41..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-child-combinator/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -button.svelte-xyz { - color: red; - - > .abc:where(.svelte-xyz) { - color: purple; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-child-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-child-combinator/input.svelte deleted file mode 100644 index 2dd98dc84876..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-child-combinator/input.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-class-nested-element/expected.css b/packages/svelte/tests/css/samples/nested-css-class-nested-element/expected.css deleted file mode 100644 index 2e3bb32c6bee..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-class-nested-element/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -.foo.svelte-xyz { - color: red; - - &c.svelte-xyz { - color: yellow; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-class-nested-element/input.svelte b/packages/svelte/tests/css/samples/nested-css-class-nested-element/input.svelte deleted file mode 100644 index 1fcaf6936515..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-class-nested-element/input.svelte +++ /dev/null @@ -1,17 +0,0 @@ -
-
- - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css b/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css deleted file mode 100644 index 8bb94954bd4d..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/expected.css +++ /dev/null @@ -1,11 +0,0 @@ -foo.svelte-xyz, bar.svelte-xyz { - color: purple; - - x:where(.svelte-xyz) { - color: red; - - y:where(.svelte-xyz) { - color: blue; - } - } -} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/input.svelte b/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/input.svelte deleted file mode 100644 index dd7ac9d1d364..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-deep-nested-selector-list/input.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - x - - y - - - - - - - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-descendant-combinator/expected.css b/packages/svelte/tests/css/samples/nested-css-descendant-combinator/expected.css deleted file mode 100644 index a490e7eeb172..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-descendant-combinator/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -button.svelte-xyz { - color: red; - - .hello:where(.svelte-xyz) { - color: pink; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-descendant-combinator/input.svelte b/packages/svelte/tests/css/samples/nested-css-descendant-combinator/input.svelte deleted file mode 100644 index 3641e8ff985c..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-descendant-combinator/input.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-element-selector/expected.css b/packages/svelte/tests/css/samples/nested-css-element-selector/expected.css deleted file mode 100644 index bd2940ce8fb4..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-element-selector/expected.css +++ /dev/null @@ -1,7 +0,0 @@ -div.svelte-xyz { - color: red; - - button:where(.svelte-xyz) { - color: yellow; - } -} diff --git a/packages/svelte/tests/css/samples/nested-css-element-selector/input.svelte b/packages/svelte/tests/css/samples/nested-css-element-selector/input.svelte deleted file mode 100644 index 9f839fc0f011..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-element-selector/input.svelte +++ /dev/null @@ -1,14 +0,0 @@ -
- -
- \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-empty-child/expected.css b/packages/svelte/tests/css/samples/nested-css-empty-child/expected.css deleted file mode 100644 index c633e0b169ac..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-empty-child/expected.css +++ /dev/null @@ -1,6 +0,0 @@ -b.svelte-xyz { - color: red; - /* (empty) x:where(.svelte-xyz) { - - }*/ -} diff --git a/packages/svelte/tests/css/samples/nested-css-empty-child/input.svelte b/packages/svelte/tests/css/samples/nested-css-empty-child/input.svelte deleted file mode 100644 index 03a1339a7c50..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-empty-child/input.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-empty-parent/expected.css b/packages/svelte/tests/css/samples/nested-css-empty-parent/expected.css deleted file mode 100644 index c6409e4b740b..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-empty-parent/expected.css +++ /dev/null @@ -1,5 +0,0 @@ -b.svelte-xyz { - x:where(.svelte-xyz) { - color: yellow; - } -} diff --git a/packages/svelte/tests/css/samples/nested-css-empty-parent/input.svelte b/packages/svelte/tests/css/samples/nested-css-empty-parent/input.svelte deleted file mode 100644 index 90d26c419a7b..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-empty-parent/input.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/expected.css b/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/expected.css deleted file mode 100644 index 8848b5bfee1d..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/expected.css +++ /dev/null @@ -1,12 +0,0 @@ -button.svelte-xyz { - color: red; - &&& { - color: blue; - } -} - -x.svelte-xyz { - foo:where(.svelte-xyz) & foo & { - color: green; - } -} \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/input.svelte b/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/input.svelte deleted file mode 100644 index 075771be2a9e..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-multiple-ampersands/input.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css b/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css deleted file mode 100644 index de637471428a..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-multiple-layers/expected.css +++ /dev/null @@ -1,17 +0,0 @@ -a.svelte-xyz { - color: red; - b:where(.svelte-xyz) { - color: yellow; - c:where(.svelte-xyz) { - color: green; - } - } - - b c:where(.svelte-xyz) { - color: blue; - } - - b c d:where(.svelte-xyz) { - color: orange; - } -} diff --git a/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte b/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte deleted file mode 100644 index 7cf031528544..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-multiple-layers/input.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-self-referential/expected.css b/packages/svelte/tests/css/samples/nested-css-self-referential/expected.css deleted file mode 100644 index 900a83aab988..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-self-referential/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -c.svelte-xyz { - color: red; - - & { - color: yellow; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-self-referential/input.svelte b/packages/svelte/tests/css/samples/nested-css-self-referential/input.svelte deleted file mode 100644 index f72e49eee953..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-self-referential/input.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-unused-child/expected.css b/packages/svelte/tests/css/samples/nested-css-unused-child/expected.css deleted file mode 100644 index 99741c1c5855..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-unused-child/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -c.svelte-xyz { - color: red; - - /* (unused) d { - color: yellow; - }*/ - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-unused-child/input.svelte b/packages/svelte/tests/css/samples/nested-css-unused-child/input.svelte deleted file mode 100644 index 7dcfdf1dcd43..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-unused-child/input.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-unused-parent/expected.css b/packages/svelte/tests/css/samples/nested-css-unused-parent/expected.css deleted file mode 100644 index ab24279f969c..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-unused-parent/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -/* (unused) a*/ b.svelte-xyz { - color: red; - - x:where(.svelte-xyz) { - color: yellow; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-unused-parent/input.svelte b/packages/svelte/tests/css/samples/nested-css-unused-parent/input.svelte deleted file mode 100644 index 7940ec195c90..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-unused-parent/input.svelte +++ /dev/null @@ -1,15 +0,0 @@ - - - - \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/nested-css-with-selector-list/expected.css b/packages/svelte/tests/css/samples/nested-css-with-selector-list/expected.css deleted file mode 100644 index bb84d35a6e1d..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-with-selector-list/expected.css +++ /dev/null @@ -1,9 +0,0 @@ -a.svelte-xyz, b.svelte-xyz { - color: red; - - x:where(.svelte-xyz) { - color: purple; - } - - color: black; -} diff --git a/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte b/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte deleted file mode 100644 index 69ac2adcc796..000000000000 --- a/packages/svelte/tests/css/samples/nested-css-with-selector-list/input.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - -
- -
- diff --git a/packages/svelte/tests/css/samples/nested-css/expected.css b/packages/svelte/tests/css/samples/nested-css/expected.css new file mode 100644 index 000000000000..763cf0d0e357 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css/expected.css @@ -0,0 +1,54 @@ + + .a.svelte-xyz { + color: green; + + /* implicit & */ + .b:where(.svelte-xyz) /* (unused) .unused*/ { + color: green; + + .c:where(.svelte-xyz) { + color: green; + } + + /* (unused) .unused { + color: red; + + .c:where(.svelte-xyz) { + color: red; + } + }*/ + } + + /* (empty) .d:where(.svelte-xyz) { + /* (unused) .unused { + color: red; + }*\/ + }*/ + + /* explicit & */ + & .b:where(.svelte-xyz) { + color: green; + + /* (empty) .c:where(.svelte-xyz) { + /* (unused) & { + color: red; + }*\/ + }*/ + } + + & & { + color: green; + } + + .container:where(.svelte-xyz) & { + color: green; + } + + /* (unused) &.b { + color: red; + }*/ + + /* (unused) .unused { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/nested-css/input.svelte b/packages/svelte/tests/css/samples/nested-css/input.svelte new file mode 100644 index 000000000000..3682f0919c9b --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css/input.svelte @@ -0,0 +1,69 @@ +
+
+ +
+
+
+ +
+
+ +
+
+
+ + From 32da098933ddcc6c57b18355103f96a76bbfb0cc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 15:19:33 -0500 Subject: [PATCH 53/69] tweak --- packages/svelte/src/compiler/phases/1-parse/read/style.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 79d30f799070..371f9c75b51d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -405,10 +405,9 @@ function read_block_item(parser) { return read_at_rule(parser); } - const start = parser.index; - // read ahead to understand whether we're dealing with a declaration or a nested rule. // this involves some duplicated work, but avoids a try-catch that would disguise errors + const start = parser.index; read_value(parser); const char = parser.template[parser.index]; parser.index = start; From a46d9fdd8b4b1958172cd24f1dd90bf294432a05 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 15:49:42 -0500 Subject: [PATCH 54/69] unused import --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 1 - 1 file changed, 1 deletion(-) 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 0d760b10d857..65c2c1562891 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1,7 +1,6 @@ import { get_possible_values } from './gather_possible_values.js'; import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../patterns.js'; import { error } from '../../../errors.js'; -import { relative } from 'node:path'; const NO_MATCH = 'NO_MATCH'; const POSSIBLE_MATCH = 'POSSIBLE_MATCH'; From 1543993b465bfb0a08fc94d66e2adacd1046ff9e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 16:32:45 -0500 Subject: [PATCH 55/69] comment out rules with unused/empty children --- .../phases/2-analyze/css/Stylesheet.js | 48 ++++++++++++------- .../tests/css/samples/nested-css/_config.js | 7 +++ .../tests/css/samples/nested-css/expected.css | 4 +- 3 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 packages/svelte/tests/css/samples/nested-css/_config.js 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 dc63dab0fc38..4239f797c59b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -113,20 +113,35 @@ class Rule { } /** - * @param {boolean} dev * @returns {boolean} */ - is_used(dev) { - if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) + is_empty() { + if (this.declarations.length > 0) return false; + + for (const rule of this.nested_rules) { + if (rule.is_used() && !rule.is_empty()) return false; + } + + return true; + } + + /** + * @returns {boolean} + */ + is_used() { + if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) { return true; + } - // keep empty rules in dev, because it's convenient to - // see them in devtools - if (this.declarations.length === 0) return dev; + for (const selector of this.selectors) { + if (selector.used) return true; + } + + for (const rule of this.nested_rules) { + if (rule.is_used()) return true; + } - return [this.selectors.some((s) => s.used), this.nested_rules.some((r) => r.is_used(dev))].some( - Boolean - ); + return false; } /** @@ -176,13 +191,10 @@ class Rule { // keep empty rules in dev, because it's convenient to // see them in devtools - if (this.declarations.length === 0 && this.nested_rules.length === 0) { - if (!dev) { - code.prependRight(this.node.start, '/* (empty) '); - code.appendLeft(this.node.end, '*/'); - escape_comment_close(this.node, code); - } - + if (!dev && this.is_empty()) { + code.prependRight(this.node.start, '/* (empty) '); + code.appendLeft(this.node.end, '*/'); + escape_comment_close(this.node, code); return; } @@ -228,9 +240,9 @@ class Rule { } } - this.nested_rules.forEach((rule) => { + for (const rule of this.nested_rules) { rule.prune(code, dev); - }); + } } } diff --git a/packages/svelte/tests/css/samples/nested-css/_config.js b/packages/svelte/tests/css/samples/nested-css/_config.js new file mode 100644 index 000000000000..53ebc0cee861 --- /dev/null +++ b/packages/svelte/tests/css/samples/nested-css/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: false + } +}); diff --git a/packages/svelte/tests/css/samples/nested-css/expected.css b/packages/svelte/tests/css/samples/nested-css/expected.css index 763cf0d0e357..b82579d918d2 100644 --- a/packages/svelte/tests/css/samples/nested-css/expected.css +++ b/packages/svelte/tests/css/samples/nested-css/expected.css @@ -20,9 +20,9 @@ } /* (empty) .d:where(.svelte-xyz) { - /* (unused) .unused { + .unused { color: red; - }*\/ + } }*/ /* explicit & */ From 4a7ac18a9e0c43cfddd942b406317342935ed822 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 16:43:23 -0500 Subject: [PATCH 56/69] tweak --- .../svelte/src/compiler/phases/2-analyze/css/Selector.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 65c2c1562891..e8220fa1243b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -115,9 +115,9 @@ export default class Selector { * */ const used_blocks = new Map(); - this.local_selector_list.map((complex_selector) => - apply_selector(complex_selector.slice(), node, used_blocks) - ); + for (const complex_selector of this.local_selector_list) { + apply_selector(complex_selector.slice(), node, used_blocks); + } // Iterate over used_blocks for (const [relative_selector, nodes] of used_blocks) { From f03e316007719b630b0484b75e10d4c2d9205668 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 17:52:22 -0500 Subject: [PATCH 57/69] update test --- packages/svelte/tests/css/samples/nested-css/expected.css | 4 ++-- packages/svelte/tests/css/samples/nested-css/input.svelte | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/tests/css/samples/nested-css/expected.css b/packages/svelte/tests/css/samples/nested-css/expected.css index b82579d918d2..19b1717780a8 100644 --- a/packages/svelte/tests/css/samples/nested-css/expected.css +++ b/packages/svelte/tests/css/samples/nested-css/expected.css @@ -30,9 +30,9 @@ color: green; /* (empty) .c:where(.svelte-xyz) { - /* (unused) & { + & & { color: red; - }*\/ + } }*/ } diff --git a/packages/svelte/tests/css/samples/nested-css/input.svelte b/packages/svelte/tests/css/samples/nested-css/input.svelte index 3682f0919c9b..1beb11d7edef 100644 --- a/packages/svelte/tests/css/samples/nested-css/input.svelte +++ b/packages/svelte/tests/css/samples/nested-css/input.svelte @@ -44,7 +44,7 @@ color: green; .c { - & { + & & { color: red; } } From a53fa5aeaa0cc455e55ecaa6240fb9951e60dc7d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 18:57:03 -0500 Subject: [PATCH 58/69] generate wrappers during parse, allow rules to contain atrules --- .../phases/2-analyze/css/Stylesheet.js | 98 +++++++------------ packages/svelte/src/compiler/types/css.d.ts | 2 +- 2 files changed, 35 insertions(+), 65 deletions(-) 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 4239f797c59b..995f946453c0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -52,22 +52,22 @@ class Rule { /** @type {import('./Selector.js').default[]} */ selectors; - /** @type {Declaration[]} */ - declarations; - /** @type {import('#compiler').Css.Rule} */ node; - /** @type {Atrule | Rule | undefined} */ + /** @type {Stylesheet | Atrule | Rule} */ parent; - /** @type {Rule[]} */ - nested_rules; + /** @type {Declaration[]} */ + declarations = []; + + /** @type {Array} */ + children = []; /** * @param {import('#compiler').Css.Rule} node * @param {Stylesheet} stylesheet - * @param {Atrule | Rule | undefined} parent + * @param {Stylesheet | Atrule | Rule} parent */ constructor(node, stylesheet, parent) { this.node = node; @@ -87,7 +87,7 @@ class Rule { * - .a .c * - .b .c */ - if (parent && parent.node.type === 'Rule') { + if (parent instanceof Rule) { let block_groups = /** @type {Rule} **/ (parent).selectors .map((selector) => selector.selector_list) .flat(); @@ -97,19 +97,12 @@ class Rule { } else { this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet, null)); } - - this.nested_rules = node.block.children - .filter((node) => node.type === 'Rule') - .map((node) => new Rule(/** @type {import('#compiler').Css.Rule}*/ (node), stylesheet, this)); - this.declarations = node.block.children - .filter((node) => node.type === 'Declaration') - .map((node) => new Declaration(/** @type {import('#compiler').Css.Declaration} */ (node))); } /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ apply(node) { this.selectors.forEach((selector) => selector.apply(node)); // TODO move the logic in here? - this.nested_rules.forEach((rule) => rule.apply(node)); + this.children.forEach((rule) => rule.apply(node)); } /** @@ -118,7 +111,7 @@ class Rule { is_empty() { if (this.declarations.length > 0) return false; - for (const rule of this.nested_rules) { + for (const rule of this.children) { if (rule.is_used() && !rule.is_empty()) return false; } @@ -129,7 +122,7 @@ class Rule { * @returns {boolean} */ is_used() { - if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) { + if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) { return true; } @@ -137,7 +130,7 @@ class Rule { if (selector.used) return true; } - for (const rule of this.nested_rules) { + for (const rule of this.children) { if (rule.is_used()) return true; } @@ -150,14 +143,14 @@ class Rule { * @param {Map} keyframes */ transform(code, id, keyframes) { - if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) { + if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) { return; } const modifier = `.${id}`; this.selectors.forEach((selector) => selector.transform(code, modifier)); this.declarations.forEach((declaration) => declaration.transform(code, keyframes)); - this.nested_rules.forEach((rule) => rule.transform(code, id, keyframes)); + this.children.forEach((rule) => rule.transform(code, id, keyframes)); } /** @param {import('../../types.js').ComponentAnalysis} analysis */ @@ -165,7 +158,7 @@ class Rule { this.selectors.forEach((selector) => { selector.validate(analysis); }); - this.nested_rules.forEach((rule) => { + this.children.forEach((rule) => { rule.validate(analysis); }); } @@ -175,7 +168,7 @@ class Rule { this.selectors.forEach((selector) => { if (!selector.used) handler(selector); }); - this.nested_rules.forEach((rule) => { + this.children.forEach((rule) => { rule.warn_on_unused_selector(handler); }); } @@ -185,7 +178,7 @@ class Rule { * @param {boolean} dev */ prune(code, dev) { - if (this.parent && this.parent.node.type === 'Atrule' && is_keyframes_node(this.parent.node)) { + if (this.parent instanceof Atrule && is_keyframes_node(this.parent.node)) { return; } @@ -240,7 +233,7 @@ class Rule { } } - for (const rule of this.nested_rules) { + for (const rule of this.children) { rule.prune(code, dev); } } @@ -327,8 +320,11 @@ class Atrule { } } - /** @param {boolean} _dev */ - is_used(_dev) { + is_empty() { + return true; // TODO + } + + is_used() { return true; // TODO } @@ -435,58 +431,32 @@ export default class Stylesheet { this.has_styles = true; const state = { - /** @type {Atrule | undefined} */ - atrule: undefined + /** @type {Stylesheet | Atrule | Rule} */ + current: this }; walk(/** @type {import('#compiler').Css.Node} */ (ast), state, { Atrule: (node, context) => { const atrule = new Atrule(node); - if (context.state.atrule) { - context.state.atrule.children.push(atrule); - } else { - this.children.push(atrule); - } - if (is_keyframes_node(node)) { if (!node.prelude.startsWith('-global-')) { this.keyframes.set(node.prelude, `${this.id}-${node.prelude}`); } - } else if (node.block) { - /** @type {Declaration[]} */ - const declarations = []; - - for (const child of node.block.children) { - if (child.type === 'Declaration') { - declarations.push(new Declaration(child)); - } - } - - if (declarations.length > 0) { - push_array(atrule.declarations, declarations); - } } - context.next({ - ...context.state, - atrule - }); + context.state.current.children.push(atrule); + context.next({ current: atrule }); + }, + Declaration: (node, context) => { + const declaration = new Declaration(node); + /** @type {Atrule | Rule} */ (context.state.current).declarations.push(declaration); }, Rule: (node, context) => { - const rule = new Rule(node, this, context.state.atrule); - - if (context.state.atrule) { - context.state.atrule.children.push(rule); - } else { - this.children.push(rule); - } + const rule = new Rule(node, this, context.state.current); - if (rule.nested_rules.length > 0) { - // Skip nested rules as they are instantiated in the Rule constructor - return node; - } - context.next(); + context.state.current.children.push(rule); + context.next({ current: rule }); } }); } diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index f54024842e5f..a39540028049 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -111,4 +111,4 @@ export interface Declaration extends BaseNode { } // for zimmerframe -export type Node = Style | Rule | Atrule; +export type Node = Style | Rule | Atrule | Declaration; From 67bf6285dfc238b1645d7395e5be86dba6fb6b6e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 19:32:57 -0500 Subject: [PATCH 59/69] move grouping logic into Selector --- .../compiler/phases/2-analyze/css/Selector.js | 17 ++++++++--- .../phases/2-analyze/css/Stylesheet.js | 29 ++----------------- .../src/compiler/phases/2-analyze/index.js | 2 +- .../svelte/src/compiler/phases/types.d.ts | 2 +- 4 files changed, 18 insertions(+), 32 deletions(-) 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 e8220fa1243b..b155091920af 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -1,6 +1,7 @@ import { get_possible_values } from './gather_possible_values.js'; import { regex_starts_with_whitespace, regex_ends_with_whitespace } from '../../patterns.js'; import { error } from '../../../errors.js'; +import { Stylesheet, Rule } from './Stylesheet.js'; const NO_MATCH = 'NO_MATCH'; const POSSIBLE_MATCH = 'POSSIBLE_MATCH'; @@ -27,7 +28,7 @@ export default class Selector { /** @type {import('#compiler').Css.Selector} */ node; - /** @type {import('./Stylesheet.js').default} */ + /** @type {Stylesheet} */ stylesheet; /** @type {SelectorList} */ @@ -41,10 +42,18 @@ export default class Selector { /** * @param {import('#compiler').Css.Selector} node - * @param {import('./Stylesheet.js').default} stylesheet - * @param {ComplexSelector[] | null} parent_selector_list + * @param {Stylesheet} stylesheet + * @param {Rule} parent */ - constructor(node, stylesheet, parent_selector_list) { + constructor(node, stylesheet, parent) { + let parent_selector_list = null; + + if (parent.parent instanceof Rule) { + parent_selector_list = parent.parent.selectors + .map((selector) => selector.selector_list) + .flat(); + } + this.node = node; this.stylesheet = stylesheet; this.selector_list = group_selectors(node, parent_selector_list); 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 995f946453c0..74f64b3ba0f6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Stylesheet.js @@ -48,7 +48,7 @@ function escape_comment_close(node, code) { } } -class Rule { +export class Rule { /** @type {import('./Selector.js').default[]} */ selectors; @@ -73,30 +73,7 @@ class Rule { this.node = node; this.parent = parent; - /** - * 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 { - * color: red; - * } - * } - * ``` - * Results in the following selectors: - * - .a .c - * - .b .c - */ - if (parent instanceof Rule) { - let block_groups = /** @type {Rule} **/ (parent).selectors - .map((selector) => selector.selector_list) - .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)); - } + this.selectors = node.prelude.children.map((node) => new Selector(node, stylesheet, this)); } /** @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node */ @@ -381,7 +358,7 @@ class Atrule { } } -export default class Stylesheet { +export class Stylesheet { /** @type {import('#compiler').Style | null} */ ast; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index a7d4a9180ab0..cdb6de929f72 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -13,7 +13,7 @@ import * as b from '../../utils/builders.js'; import { ReservedKeywords, Runes, SVGElements } from '../constants.js'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import { merge } from '../visitors.js'; -import Stylesheet from './css/Stylesheet.js'; +import { Stylesheet } from './css/Stylesheet.js'; import { validation_legacy, validation_runes, validation_runes_js } from './validation.js'; import { warn } from '../../warnings.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 44d9ce0d6013..0a92486c85e5 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -7,7 +7,7 @@ import type { SvelteOptions } from '#compiler'; import type { Identifier, LabeledStatement, Program } from 'estree'; -import type Stylesheet from './2-analyze/css/Stylesheet.js'; +import { Stylesheet } from './2-analyze/css/Stylesheet.js'; import type { Scope, ScopeRoot } from './scope.js'; export interface Js { From 3d0a8856d30fa0b4da80bb81b20d1f38622cd47c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 20:21:55 -0500 Subject: [PATCH 60/69] shrink things down a touch --- .../compiler/phases/2-analyze/css/Selector.js | 92 +++++++++---------- 1 file changed, 42 insertions(+), 50 deletions(-) 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 b155091920af..033e5bf71d47 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -118,23 +118,10 @@ export default class Selector { * @returns {void} */ apply(node) { - /** - * Create a map of blocks to their nodes to know whether they should be encapsulated - * @type {Map>} - * */ - const used_blocks = new Map(); - for (const complex_selector of this.local_selector_list) { - apply_selector(complex_selector.slice(), node, used_blocks); - } - - // Iterate over used_blocks - for (const [relative_selector, nodes] of used_blocks) { - relative_selector.should_encapsulate = true; - for (const node of nodes) { - this.stylesheet.nodes_with_css_class.add(node); + if (apply_selector(complex_selector.slice(), node, this.stylesheet)) { + this.used = true; } - this.used = true; } } @@ -301,25 +288,13 @@ export default class Selector { } } -/** - * @param {Map>} map - * @param {RelativeSelector} 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 {RelativeSelector[]} blocks * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} node - * @param {Map>} to_encapsulate + * @param {Stylesheet} stylesheet * @returns {boolean} */ -function apply_selector(blocks, node, to_encapsulate) { +function apply_selector(blocks, node, stylesheet) { const block = blocks.pop(); if (!block) return false; if (!node) { @@ -333,48 +308,64 @@ function apply_selector(blocks, node, to_encapsulate) { return false; } - if (applies === UNKNOWN_SELECTOR) { - add_node(to_encapsulate, block, node); + /** + * Mark both the compound selector and the node it selects as encapsulated, + * for transformation in a later step + * @param {RelativeSelector} block + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node + */ + function mark(block, node) { + block.should_encapsulate = true; + stylesheet.nodes_with_css_class.add(node); return true; } + if (applies === UNKNOWN_SELECTOR) { + return mark(block, node); + } + if (block.combinator) { if (block.combinator.type === 'Combinator' && block.combinator.name === ' ') { for (const ancestor_block of blocks) { if (ancestor_block.global) { continue; } + if (ancestor_block.host) { - add_node(to_encapsulate, block, node); - return true; + return mark(block, node); } + /** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */ let parent = node; + let matched = false; while ((parent = get_element_parent(parent))) { if (block_might_apply_to_node(ancestor_block, parent) !== NO_MATCH) { - add_node(to_encapsulate, ancestor_block, parent); + mark(ancestor_block, parent); + matched = true; } } - if (to_encapsulate.size) { - add_node(to_encapsulate, block, node); - return true; + + if (matched) { + return mark(block, node); } } + if (blocks.every((block) => block.global)) { - add_node(to_encapsulate, block, node); - return true; + return mark(block, node); } + 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)) { - add_node(to_encapsulate, block, node); - return true; + if (has_global_parent || apply_selector(blocks, get_element_parent(node), stylesheet)) { + return mark(block, node); } + return false; } else if (block.combinator.name === '+' || block.combinator.name === '~') { const siblings = get_possible_element_siblings(node, block.combinator.name === '+'); 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 @@ -383,23 +374,24 @@ function apply_selector(blocks, node, to_encapsulate) { if (siblings.size === 0 && get_element_parent(node) !== null) { return false; } - add_node(to_encapsulate, block, node); - return true; + return mark(block, node); } + for (const possible_sibling of siblings.keys()) { - if (apply_selector(blocks.slice(), possible_sibling, to_encapsulate)) { - add_node(to_encapsulate, block, node); + if (apply_selector(blocks.slice(), possible_sibling, stylesheet)) { + mark(block, node); has_match = true; } } + return has_match; } + // TODO other combinators - add_node(to_encapsulate, block, node); - return true; + return mark(block, node); } - add_node(to_encapsulate, block, node); - return true; + + return mark(block, node); } const regex_backslash_and_following_character = /\\(.)/g; From 6e149f248909d9454881c3a55652831b2fa63131 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 20:58:49 -0500 Subject: [PATCH 61/69] simplify --- .../compiler/phases/2-analyze/css/Selector.js | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) 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 033e5bf71d47..75c3261fbecf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -37,8 +37,7 @@ export default class Selector { /** @type {SelectorList} */ local_selector_list; - /** @type {boolean} */ - used; + used = false; /** * @param {import('#compiler').Css.Selector} node @@ -46,6 +45,9 @@ export default class Selector { * @param {Rule} parent */ constructor(node, stylesheet, parent) { + this.node = node; + this.stylesheet = stylesheet; + let parent_selector_list = null; if (parent.parent instanceof Rule) { @@ -54,24 +56,11 @@ export default class Selector { .flat(); } - this.node = node; - this.stylesheet = stylesheet; this.selector_list = group_selectors(node, parent_selector_list); - this.used = false; - - // Initialize local_selector_list - this.local_selector_list = []; - - // Process each block group to take trailing :global(...) selectors out of consideration - this.selector_list.forEach((complex_selector) => { - let i = complex_selector.length; - while (i > 0) { - if (!complex_selector[i - 1].global) break; - i -= 1; - } - // Add the processed group (with global selectors removed) to local_selector_list - this.local_selector_list.push(complex_selector.slice(0, i)); + this.local_selector_list = this.selector_list.map((complex_selector) => { + const i = complex_selector.findLastIndex((block) => !block.global); + return complex_selector.slice(0, i + 1); }); // Determine `used` based on the processed local_selector_list From 353d697f8c441e2e97f010a4ee69800d3956c7fa Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 21:13:57 -0500 Subject: [PATCH 62/69] simplify --- .../compiler/phases/2-analyze/css/Selector.js | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) 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 75c3261fbecf..9844b7944499 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -59,25 +59,15 @@ export default class Selector { this.selector_list = group_selectors(node, parent_selector_list); this.local_selector_list = this.selector_list.map((complex_selector) => { - const i = complex_selector.findLastIndex((block) => !block.global); + const i = complex_selector.findLastIndex((block) => !block.can_ignore()); return complex_selector.slice(0, i + 1); }); - // Determine `used` based on the processed local_selector_list - let host_only = false; - let root_only = false; - - // Check if there's exactly one group and one block within that group, and if it's host or root - if (this.local_selector_list.length === 1 && this.local_selector_list[0].length === 1) { - const single_block = this.local_selector_list[0][0]; - host_only = single_block.compound.host; - root_only = single_block.compound.root; - } - - // Check if there are no local blocks across all groups, or if there's a host_only or root_only situation - const no_local_blocks = this.local_selector_list.every((group) => group.length === 0); - this.used = no_local_blocks || host_only || root_only; + // if we have a `:root {...}` or `:global(...) {...}` selector, we need to mark + // this selector as `used` even if the component doesn't contain any nodes + this.used = this.local_selector_list.some((blocks) => blocks.length === 0); } + /** * Determines whether the given selector is used within the component's nodes * and marks the corresponding blocks for encapsulation if so. @@ -108,7 +98,9 @@ export default class Selector { */ apply(node) { for (const complex_selector of this.local_selector_list) { - if (apply_selector(complex_selector.slice(), node, this.stylesheet)) { + if (complex_selector.length === 0) { + this.used = true; + } else if (apply_selector(complex_selector.slice(), node, this.stylesheet)) { this.used = true; } } @@ -890,22 +882,21 @@ class RelativeSelector { this.compound.add(selector); } + can_ignore() { + return this.compound.global || this.compound.host || this.compound.root; + } + get global() { return this.compound.global; } + get host() { return this.compound.host; } + get root() { return this.compound.root; } - get end() { - return this.compound.end; - } - get start() { - if (this.combinator) return this.combinator.start; - return this.compound.start; - } get contains_invisible_selectors() { return this.compound.selectors.some((selector) => !selector.visible); From 84b6f8b1b12c15ccda2b6a4f37f1f4c115d24113 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 21:17:28 -0500 Subject: [PATCH 63/69] actually we don't need that --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 9844b7944499..74a6f37a18a7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -98,9 +98,7 @@ export default class Selector { */ apply(node) { for (const complex_selector of this.local_selector_list) { - if (complex_selector.length === 0) { - this.used = true; - } else if (apply_selector(complex_selector.slice(), node, this.stylesheet)) { + if (apply_selector(complex_selector.slice(), node, this.stylesheet)) { this.used = true; } } From 36123fc26fea467517fb38dd12535def6c51dba5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 21:19:38 -0500 Subject: [PATCH 64/69] shrink explanation --- .../compiler/phases/2-analyze/css/Selector.js | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) 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 74a6f37a18a7..a4088f059ec6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -69,30 +69,8 @@ export default class Selector { } /** - * 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. - * We should mark it for encapsulation as a result. - * + * Determines whether the given selector potentially applies to `node` — + * if so, marks both the selector and the node as encapsulated * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node - The node to apply the selector to. * @returns {void} */ From ecff92581cc24021abf8ac99e68100225b59880d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 21:31:16 -0500 Subject: [PATCH 65/69] move --- .../compiler/phases/2-analyze/css/Selector.js | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) 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 a4088f059ec6..11413790dabf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -48,15 +48,7 @@ export default class Selector { this.node = node; this.stylesheet = stylesheet; - let parent_selector_list = null; - - if (parent.parent instanceof Rule) { - parent_selector_list = parent.parent.selectors - .map((selector) => selector.selector_list) - .flat(); - } - - this.selector_list = group_selectors(node, parent_selector_list); + this.selector_list = group_selectors(node, parent); this.local_selector_list = this.selector_list.map((complex_selector) => { const i = complex_selector.findLastIndex((block) => !block.can_ignore()); @@ -943,23 +935,27 @@ class CompoundSelector { * Groups selectors and inserts parent blocks into nested rules. * * @param {import('#compiler').Css.Selector} selector - The selector to group and analyze. - * @param {SelectorList | null} parent_selector_list - The parent blocks group to insert into nested rules. + * @param {Rule} rule * @returns {SelectorList} - The grouped selectors with parent's blocks inserted if nested. */ -function group_selectors(selector, parent_selector_list) { - // If it isn't a nested rule, then we add an empty block group - if (parent_selector_list === null) { - return [selector_to_blocks([...selector.children], null)]; - } +function group_selectors(selector, rule) { + // TODO this logic isn't quite right, as it doesn't properly account for atrules + if (rule.parent instanceof Rule) { + const parent_selector_list = rule.parent.selectors + .map((selector) => selector.selector_list) + .flat(); + + return parent_selector_list.map((parent_complex_selector) => { + const block_group = selector_to_blocks( + [...selector.children], + [...parent_complex_selector] // Clone the parent's blocks to avoid modifying the original array + ); - return parent_selector_list.map((parent_complex_selector) => { - const block_group = selector_to_blocks( - [...selector.children], - [...parent_complex_selector] // Clone the parent's blocks to avoid modifying the original array - ); + return block_group; + }); + } - return block_group; - }); + return [selector_to_blocks([...selector.children], null)]; } /** From 1f23ce5b425876f5163a6324c6bc29201387785a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 21:40:39 -0500 Subject: [PATCH 66/69] rename --- packages/svelte/src/compiler/phases/2-analyze/css/Selector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 11413790dabf..9437ae571ca3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -152,13 +152,13 @@ export default class Selector { /** @param {import('../../types.js').ComponentAnalysis} analysis */ validate(analysis) { - this.validate_invalid_css_global_placement(); + this.validate_global_placement(); this.validate_global_with_multiple_selectors(); this.validate_global_compound_selector(); this.validate_invalid_combinator_without_selector(analysis); } - validate_invalid_css_global_placement() { + validate_global_placement() { for (let complex_selector of this.selector_list) { let start = 0; let end = complex_selector.length; From ec49db44e8835cf11f03b9bd7b49ac8462035f72 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 13 Feb 2024 21:48:38 -0500 Subject: [PATCH 67/69] rename --- .../svelte/src/compiler/phases/2-analyze/css/Selector.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 9437ae571ca3..a0d805af3234 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -42,13 +42,13 @@ export default class Selector { /** * @param {import('#compiler').Css.Selector} node * @param {Stylesheet} stylesheet - * @param {Rule} parent + * @param {Rule} rule */ - constructor(node, stylesheet, parent) { + constructor(node, stylesheet, rule) { this.node = node; this.stylesheet = stylesheet; - this.selector_list = group_selectors(node, parent); + this.selector_list = group_selectors(node, rule); this.local_selector_list = this.selector_list.map((complex_selector) => { const i = complex_selector.findLastIndex((block) => !block.can_ignore()); From f6ba25cc04181f1a32c9abf21895afac3b6799ec Mon Sep 17 00:00:00 2001 From: Albert Date: Wed, 14 Feb 2024 13:57:28 +1030 Subject: [PATCH 68/69] That nested rule wasn't empty --- .../svelte/tests/css/samples/nested-css/expected.css | 9 +++++++-- .../svelte/tests/css/samples/nested-css/input.svelte | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/css/samples/nested-css/expected.css b/packages/svelte/tests/css/samples/nested-css/expected.css index 19b1717780a8..3cb42f862444 100644 --- a/packages/svelte/tests/css/samples/nested-css/expected.css +++ b/packages/svelte/tests/css/samples/nested-css/expected.css @@ -29,17 +29,22 @@ & .b:where(.svelte-xyz) { color: green; - /* (empty) .c:where(.svelte-xyz) { + .c:where(.svelte-xyz) { & & { color: red; } - }*/ + } } & & { color: green; } + /* silly but valid */ + && { + color: rebeccapurple; + } + .container:where(.svelte-xyz) & { color: green; } diff --git a/packages/svelte/tests/css/samples/nested-css/input.svelte b/packages/svelte/tests/css/samples/nested-css/input.svelte index 1beb11d7edef..b14b68463e23 100644 --- a/packages/svelte/tests/css/samples/nested-css/input.svelte +++ b/packages/svelte/tests/css/samples/nested-css/input.svelte @@ -54,6 +54,11 @@ color: green; } + /* silly but valid */ + && { + color: rebeccapurple; + } + .container & { color: green; } From 4201cf7cc45baaad7b764c4f75565cbf433a9474 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 14 Feb 2024 09:43:50 -0500 Subject: [PATCH 69/69] revert --- packages/svelte/tests/css/samples/nested-css/expected.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/tests/css/samples/nested-css/expected.css b/packages/svelte/tests/css/samples/nested-css/expected.css index 3cb42f862444..3a2c455fe99c 100644 --- a/packages/svelte/tests/css/samples/nested-css/expected.css +++ b/packages/svelte/tests/css/samples/nested-css/expected.css @@ -29,11 +29,11 @@ & .b:where(.svelte-xyz) { color: green; - .c:where(.svelte-xyz) { + /* (empty) .c:where(.svelte-xyz) { & & { color: red; } - } + }*/ } & & {