Skip to content

Commit 306c298

Browse files
authored
Fix/parse url with semicolons (#145)
closes #125
1 parent 9324c3b commit 306c298

File tree

3 files changed

+31
-1
lines changed

3 files changed

+31
-1
lines changed

src/parse-atrule-prelude.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,7 @@ describe('At-Rule Prelude Nodes', () => {
985985
// URL node in @import returns the string with quotes
986986
expect(url?.value).toBe('"example.com"')
987987
})
988+
988989
it('should parse with anonymous layer', () => {
989990
const css = '@import url("styles.css") layer;'
990991
const ast = parse(css, { parse_atrule_preludes: true })
@@ -1177,6 +1178,20 @@ describe('At-Rule Prelude Nodes', () => {
11771178

11781179
expect(atRule?.prelude?.text).toBe('url("styles.css") layer(base) screen')
11791180
})
1181+
1182+
it('should parse unquoted URL that contains ;', () => {
1183+
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`
1184+
const css = `@import url(${url});`
1185+
const ast = parse(css)
1186+
const atRule = ast.first_child!
1187+
1188+
// Prelude text should not include trailing semicolon
1189+
expect.soft(atRule.prelude?.text).toBe(`url(${url})`)
1190+
const url_node = atRule.prelude?.first_child
1191+
expect(url_node).not.toBeNull()
1192+
expect.soft(url_node?.type_name).toBe('Url')
1193+
expect.soft(url_node?.value).toBe(url)
1194+
})
11801195
})
11811196

11821197
describe('Length property correctness (regression tests for commit 5c6e2cd)', () => {

src/parse-declaration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ export class DeclarationParser {
173173
let has_important = false
174174
let last_end = lexer.token_end
175175
// Track parenthesis depth to handle semicolons inside functions (e.g., url(data:image/png;base64,...))
176+
// NOTE: Same pattern exists in parse.ts for at-rule prelude parsing - keep in sync
176177
let paren_depth = 0
177178

178179
// Process tokens until we hit semicolon, EOF, or end of input

src/parse.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
TOKEN_RIGHT_BRACKET,
3131
TOKEN_COMMA,
3232
TOKEN_COLON,
33+
TOKEN_FUNCTION,
3334
} from './token-types'
3435
import { trim_boundaries } from './parse-utils'
3536
import { CHAR_PERIOD, CHAR_GREATER_THAN, CHAR_PLUS, CHAR_TILDE, CHAR_AMPERSAND } from './string-utils'
@@ -346,11 +347,24 @@ export class Parser {
346347
// Track prelude start and end
347348
let prelude_start = this.lexer.token_start
348349
let prelude_end = prelude_start
350+
// Track parenthesis depth to handle semicolons inside functions (e.g., url(data:image/png;base64,...))
351+
// NOTE: Same pattern exists in parse-declaration.ts for value parsing - keep in sync
352+
let paren_depth = 0
349353

350354
// Parse prelude (everything before '{' or ';')
351355
while (!this.is_eof()) {
352356
let token_type = this.peek_type()
353-
if (token_type === TOKEN_LEFT_BRACE || token_type === TOKEN_SEMICOLON) break
357+
358+
// Track parenthesis depth
359+
if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) {
360+
paren_depth++
361+
} else if (token_type === TOKEN_RIGHT_PAREN) {
362+
paren_depth--
363+
}
364+
365+
// Only break on '{' or ';' when outside all parentheses
366+
if (token_type === TOKEN_LEFT_BRACE && paren_depth === 0) break
367+
if (token_type === TOKEN_SEMICOLON && paren_depth === 0) break
354368
prelude_end = this.lexer.token_end
355369
this.next_token()
356370
}

0 commit comments

Comments
 (0)