From a3d4eafbecc096b924f359c3bc3da35afc77ab43 Mon Sep 17 00:00:00 2001 From: Albert Marashi Date: Mon, 20 Nov 2023 13:04:21 +1030 Subject: [PATCH] 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 21c3018903..ba000981d5 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 8e453f6859..6866850598 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 eecab0c16e..e2074ddbfb 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 077d0196c2..5bd8968670 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 1e5399ecd7..8e4d3bead9 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