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 d8b7d3301f..bf60db96df 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 7ff629415d..29b67ce272 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 0000000000..ae1d3885ea --- /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 0000000000..1aa3bca7ce --- /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 0000000000..a7f507bd49 --- /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 0000000000..cd07582c6e --- /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 0000000000..7efa5e5367 --- /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 0000000000..9f839fc0f0 --- /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 a02fa36589..a7bc48137b 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 2de71392e1..06a8726707 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