diff --git a/.changeset/seven-hornets-smile.md b/.changeset/seven-hornets-smile.md new file mode 100644 index 000000000000..7ae0106acb91 --- /dev/null +++ b/.changeset/seven-hornets-smile.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: correctly parse at-rules containing special characters in strings 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 545542d80f4b..ff9f9a8fecca 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -9,7 +9,6 @@ const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/; 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_BRACE_OR_SEMICOLON = /[{;]/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/; const REGEX_VALID_IDENTIFIER_CHAR = /[a-zA-Z0-9_-]/; const REGEX_COMMENT_CLOSE = /\*\//; @@ -79,7 +78,7 @@ function read_at_rule(parser) { const name = read_identifier(parser); - const prelude = parser.read_until(REGEX_BRACE_OR_SEMICOLON).trim(); + const prelude = read_value(parser); /** @type {import('#compiler').Css.Block | null} */ let block = null; @@ -398,7 +397,7 @@ function read_declaration(parser) { parser.eat(':'); parser.allow_whitespace(); - const value = read_declaration_value(parser); + const value = read_value(parser); const end = parser.index; @@ -419,7 +418,7 @@ function read_declaration(parser) { * @param {import('../index.js').Parser} parser * @returns {string} */ -function read_declaration_value(parser) { +function read_value(parser) { let value = ''; let escaped = false; let in_url = false; @@ -443,7 +442,7 @@ function read_declaration_value(parser) { quote_mark = char; } else if (char === '(' && value.slice(-3) === 'url') { in_url = true; - } else if ((char === ';' || char === '}') && !in_url && !quote_mark) { + } else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) { return value.trim(); } diff --git a/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/input.svelte b/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/input.svelte new file mode 100644 index 000000000000..8473391f94da --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/input.svelte @@ -0,0 +1,10 @@ +

+ Semicolon inside quotes +

+ diff --git a/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json b/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json new file mode 100644 index 000000000000..c6fbb599c7f0 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/semicolon-inside-quotes/output.json @@ -0,0 +1,107 @@ +{ + "css": { + "type": "Style", + "start": 36, + "end": 205, + "attributes": [], + "children": [ + { + "type": "Atrule", + "start": 45, + "end": 135, + "name": "import", + "prelude": "url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap\")", + "block": null + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "start": 137, + "end": 139, + "children": [ + { + "type": "Selector", + "start": 137, + "end": 139, + "children": [ + { + "type": "TypeSelector", + "name": "h1", + "start": 137, + "end": 139 + } + ] + } + ] + }, + "block": { + "type": "Block", + "start": 140, + "end": 196, + "children": [ + { + "type": "Declaration", + "start": 144, + "end": 161, + "property": "font-weight", + "value": "bold" + }, + { + "type": "Declaration", + "start": 165, + "end": 192, + "property": "background", + "value": "url(\"whatever\")" + } + ] + }, + "start": 137, + "end": 196 + } + ], + "content": { + "start": 43, + "end": 197, + "styles": "\n\t@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap\");\n\th1 {\n\t\tfont-weight: bold;\n\t\tbackground: url(\"whatever\");\n\t}\n" + } + }, + "js": [], + "start": 0, + "end": 35, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "RegularElement", + "start": 0, + "end": 35, + "name": "h1", + "attributes": [], + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 4, + "end": 30, + "raw": "\n\tSemicolon inside quotes\n", + "data": "\n\tSemicolon inside quotes\n" + } + ], + "transparent": true + } + }, + { + "type": "Text", + "start": 35, + "end": 36, + "raw": "\n", + "data": "\n" + } + ], + "transparent": false + }, + "options": null +}