diff --git a/src/parse-atrule-prelude.test.ts b/src/parse-atrule-prelude.test.ts index c6fb2d7..7f5734f 100644 --- a/src/parse-atrule-prelude.test.ts +++ b/src/parse-atrule-prelude.test.ts @@ -985,6 +985,7 @@ describe('At-Rule Prelude Nodes', () => { // URL node in @import returns the string with quotes expect(url?.value).toBe('"example.com"') }) + it('should parse with anonymous layer', () => { const css = '@import url("styles.css") layer;' const ast = parse(css, { parse_atrule_preludes: true }) @@ -1177,6 +1178,20 @@ describe('At-Rule Prelude Nodes', () => { expect(atRule?.prelude?.text).toBe('url("styles.css") layer(base) screen') }) + + it('should parse unquoted URL that contains ;', () => { + const url = `https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,800;0,900;1,800&family=Roboto+Condensed:ital,wght@0,400;0,500;0,700;1,700&family=Roboto:ital,wght@0,300;0,400;0,500;0,700;0,900;1,300;1,400;1,500;1,700;1,900&display=swap` + const css = `@import url(${url});` + const ast = parse(css) + const atRule = ast.first_child! + + // Prelude text should not include trailing semicolon + expect.soft(atRule.prelude?.text).toBe(`url(${url})`) + const url_node = atRule.prelude?.first_child + expect(url_node).not.toBeNull() + expect.soft(url_node?.type_name).toBe('Url') + expect.soft(url_node?.value).toBe(url) + }) }) describe('Length property correctness (regression tests for commit 5c6e2cd)', () => { diff --git a/src/parse-declaration.ts b/src/parse-declaration.ts index e4d8940..8f28f08 100644 --- a/src/parse-declaration.ts +++ b/src/parse-declaration.ts @@ -173,6 +173,7 @@ export class DeclarationParser { let has_important = false let last_end = lexer.token_end // Track parenthesis depth to handle semicolons inside functions (e.g., url(data:image/png;base64,...)) + // NOTE: Same pattern exists in parse.ts for at-rule prelude parsing - keep in sync let paren_depth = 0 // Process tokens until we hit semicolon, EOF, or end of input diff --git a/src/parse.ts b/src/parse.ts index ce8b333..64808ec 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -30,6 +30,7 @@ import { TOKEN_RIGHT_BRACKET, TOKEN_COMMA, TOKEN_COLON, + TOKEN_FUNCTION, } from './token-types' import { trim_boundaries } from './parse-utils' import { CHAR_PERIOD, CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_AMPERSAND } from './string-utils' @@ -346,11 +347,24 @@ export class Parser { // Track prelude start and end let prelude_start = this.lexer.token_start let prelude_end = prelude_start + // Track parenthesis depth to handle semicolons inside functions (e.g., url(data:image/png;base64,...)) + // NOTE: Same pattern exists in parse-declaration.ts for value parsing - keep in sync + let paren_depth = 0 // Parse prelude (everything before '{' or ';') while (!this.is_eof()) { let token_type = this.peek_type() - if (token_type === TOKEN_LEFT_BRACE || token_type === TOKEN_SEMICOLON) break + + // Track parenthesis depth + if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) { + paren_depth++ + } else if (token_type === TOKEN_RIGHT_PAREN) { + paren_depth-- + } + + // Only break on '{' or ';' when outside all parentheses + if (token_type === TOKEN_LEFT_BRACE && paren_depth === 0) break + if (token_type === TOKEN_SEMICOLON && paren_depth === 0) break prelude_end = this.lexer.token_end this.next_token() }