Skip to content

Commit 6b5fc81

Browse files
authored
fix: continue parsing declarations after encountering ; in data-url (#144)
refs projectwallace/format-css#144
1 parent 0c5e906 commit 6b5fc81

File tree

2 files changed

+97
-51
lines changed

2 files changed

+97
-51
lines changed

src/parse-declaration.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
TOKEN_COMMA,
1919
TOKEN_HASH,
2020
TOKEN_AT_KEYWORD,
21+
TOKEN_FUNCTION,
2122
type TokenType,
2223
} from './token-types'
2324
import { trim_boundaries } from './parse-utils'
@@ -171,12 +172,23 @@ export class DeclarationParser {
171172
// Parse value (everything until ';' or EOF)
172173
let has_important = false
173174
let last_end = lexer.token_end
175+
// Track parenthesis depth to handle semicolons inside functions (e.g., url(data:image/png;base64,...))
176+
let paren_depth = 0
174177

175178
// Process tokens until we hit semicolon, EOF, or end of input
176179
while ((lexer.token_type as TokenType) !== TOKEN_EOF && lexer.token_start < end) {
177180
let token_type = lexer.token_type as TokenType
178-
if (token_type === TOKEN_SEMICOLON) break
179-
if (token_type === TOKEN_RIGHT_BRACE) break
181+
182+
// Track parenthesis depth
183+
if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) {
184+
paren_depth++
185+
} else if (token_type === TOKEN_RIGHT_PAREN) {
186+
paren_depth--
187+
}
188+
189+
// Only break on semicolon/brace when outside all parentheses
190+
if (token_type === TOKEN_SEMICOLON && paren_depth === 0) break
191+
if (token_type === TOKEN_RIGHT_BRACE && paren_depth === 0) break
180192

181193
// If we encounter '{', this is actually a style rule, not a declaration
182194
if (token_type === TOKEN_LEFT_BRACE) {

src/parse-value.test.ts

Lines changed: 83 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import { describe, it, expect } from 'vitest'
22
import { parse } from './parse'
3-
import { IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL, UNICODE_RANGE, VALUE } from './arena'
3+
import {
4+
IDENTIFIER,
5+
NUMBER,
6+
DIMENSION,
7+
STRING,
8+
HASH,
9+
FUNCTION,
10+
OPERATOR,
11+
PARENTHESIS,
12+
URL,
13+
UNICODE_RANGE,
14+
VALUE,
15+
DECLARATION,
16+
} from './arena'
417

518
describe('Value Node Types', () => {
619
// Helper to get first value node from a declaration
@@ -607,48 +620,48 @@ describe('Value Node Types', () => {
607620
})
608621

609622
describe('OPERATOR', () => {
610-
it('should parse comma operator', () => {
611-
const root = parse('body { font-family: Arial, sans-serif; }')
612-
const decl = root.first_child?.first_child?.next_sibling?.first_child
623+
it('should parse comma operator', () => {
624+
const root = parse('body { font-family: Arial, sans-serif; }')
625+
const decl = root.first_child?.first_child?.next_sibling?.first_child
613626

614-
expect(decl?.first_child!.children[1].type).toBe(OPERATOR)
615-
expect(decl?.first_child!.children[1].text).toBe(',')
616-
expect(decl?.first_child!.children[1].name).toBe(undefined)
617-
expect(decl?.first_child!.children[1].value).toBe(',')
618-
})
627+
expect(decl?.first_child!.children[1].type).toBe(OPERATOR)
628+
expect(decl?.first_child!.children[1].text).toBe(',')
629+
expect(decl?.first_child!.children[1].name).toBe(undefined)
630+
expect(decl?.first_child!.children[1].value).toBe(',')
631+
})
619632

620-
it('should parse calc operators', () => {
621-
const root = parse('body { width: calc(100% - 20px); }')
622-
const decl = root.first_child?.first_child?.next_sibling?.first_child
623-
const func = decl?.first_child!.children[0]
633+
it('should parse calc operators', () => {
634+
const root = parse('body { width: calc(100% - 20px); }')
635+
const decl = root.first_child?.first_child?.next_sibling?.first_child
636+
const func = decl?.first_child!.children[0]
624637

625-
expect(func?.children[1].type).toBe(OPERATOR)
626-
expect(func?.children[1].text).toBe('-')
627-
expect(func?.children[1].name).toBe(undefined)
628-
expect(func?.children[1].value).toBe('-')
629-
})
638+
expect(func?.children[1].type).toBe(OPERATOR)
639+
expect(func?.children[1].text).toBe('-')
640+
expect(func?.children[1].name).toBe(undefined)
641+
expect(func?.children[1].value).toBe('-')
642+
})
630643

631-
it('should parse all calc operators', () => {
632-
const root = parse('body { width: calc(1px + 2px * 3px / 4px - 5px); }')
633-
const decl = root.first_child?.first_child?.next_sibling?.first_child
634-
const func = decl?.first_child!.children[0]
635-
636-
const operators = func?.children.filter((n) => n.type === OPERATOR)
637-
expect(operators).toHaveLength(4)
638-
expect(operators?.[0].text).toBe('+')
639-
expect(operators?.[0].name).toBe(undefined)
640-
expect(operators?.[0].value).toBe('+')
641-
expect(operators?.[1].text).toBe('*')
642-
expect(operators?.[1].name).toBe(undefined)
643-
expect(operators?.[1].value).toBe('*')
644-
expect(operators?.[2].text).toBe('/')
645-
expect(operators?.[2].name).toBe(undefined)
646-
expect(operators?.[2].value).toBe('/')
647-
expect(operators?.[3].text).toBe('-')
648-
expect(operators?.[3].name).toBe(undefined)
649-
expect(operators?.[3].value).toBe('-')
644+
it('should parse all calc operators', () => {
645+
const root = parse('body { width: calc(1px + 2px * 3px / 4px - 5px); }')
646+
const decl = root.first_child?.first_child?.next_sibling?.first_child
647+
const func = decl?.first_child!.children[0]
648+
649+
const operators = func?.children.filter((n) => n.type === OPERATOR)
650+
expect(operators).toHaveLength(4)
651+
expect(operators?.[0].text).toBe('+')
652+
expect(operators?.[0].name).toBe(undefined)
653+
expect(operators?.[0].value).toBe('+')
654+
expect(operators?.[1].text).toBe('*')
655+
expect(operators?.[1].name).toBe(undefined)
656+
expect(operators?.[1].value).toBe('*')
657+
expect(operators?.[2].text).toBe('/')
658+
expect(operators?.[2].name).toBe(undefined)
659+
expect(operators?.[2].value).toBe('/')
660+
expect(operators?.[3].text).toBe('-')
661+
expect(operators?.[3].name).toBe(undefined)
662+
expect(operators?.[3].value).toBe('-')
663+
})
650664
})
651-
})
652665

653666
describe('PARENTHESIS', () => {
654667
it('should parse parenthesized expressions in calc()', () => {
@@ -776,15 +789,36 @@ describe('Value Node Types', () => {
776789
expect(decl?.first_child!.children[0].value).toBe("'image.png'")
777790
})
778791

779-
it('should parse url() with base64 data URL', () => {
780-
const root = parse('body { background: url(data:image/png;base64,iVBORw0KGg); }')
781-
const decl = root.first_child?.first_child?.next_sibling?.first_child
782-
const func = decl?.first_child!.children[0]
783-
784-
expect(func?.type).toBe(URL)
785-
expect(func?.name).toBe('url')
786-
expect(func?.has_children).toBe(false)
787-
expect(func?.value).toBe('data:image/png;base64,iVBORw0KGg')
792+
describe.each([
793+
`data:image/png;base64,iVBORw0KGg`,
794+
`data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgaGVpZ2h0PSIyNHB4IiB3aWR0aD0iMjRweCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5lYXItZ3JhZGllbnQiIHgxPSIyMi4zMSIgeTE9IjIzLjYyIiB4Mj0iMy43MyIgeTI9IjMuMDUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNlOTM3MjIiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmODZmMjUiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48dGl0bGU+TWFnbmlmaWVyPC90aXRsZT48cGF0aCBmaWxsPSJ1cmwoI2xpbmVhci1ncmFkaWVudCkiIGQ9Ik0yMy4zMyAyMC4xbC00LjczLTQuNzRhMTAuMDYgMTAuMDYgMCAxIDAtMy4yMyAzLjIzbDQuNzQgNC43NGEyLjI5IDIuMjkgMCAxIDAgMy4yMi0zLjIzem0tMTcuNDgtNS44NGE1Ljk0IDUuOTQgMCAxIDEgOC40MiAwIDYgNiAwIDAgMS04LjQyIDB6Ii8+PC9zdmc+`,
795+
`'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16px" height="16px" viewBox="0 0 24 24"><path fill="rgba(0,0,0,0.5)" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"></path></svg>'`,
796+
])('should parse url() with base64 data URL', (input) => {
797+
test(`parses value: ${input.slice(0, 40)}`, () => {
798+
const root = parse(`body { background: url(${input}); }`)
799+
const decl = root.first_child?.first_child?.next_sibling?.first_child
800+
const func = decl?.first_child!.children[0]
801+
802+
expect(func?.type).toBe(URL)
803+
expect(func?.name).toBe('url')
804+
expect(func?.value).toBe(input)
805+
})
806+
807+
test('does not break parsing declarations coming after', () => {
808+
const root = parse(`
809+
body {
810+
background: url(${input});
811+
font-size: 1em;
812+
}
813+
`)
814+
const block = root.first_child?.first_child?.next_sibling
815+
816+
expect(block?.children.length).toBe(2)
817+
const [with_data_url, declaration] = block!.children
818+
819+
expect(with_data_url.type).toBe(DECLARATION)
820+
expect(declaration.type).toBe(DECLARATION)
821+
})
788822
})
789823

790824
it('should parse url() with inline SVG', () => {
@@ -801,7 +835,7 @@ describe('Value Node Types', () => {
801835
it('should parse complex background value with url()', () => {
802836
const root = parse('body { background: url("bg.png") no-repeat center center / cover; }')
803837
const decl = root.first_child?.first_child?.next_sibling?.first_child
804-
expect(decl?.first_child!.children.length).toBeGreaterThan(1)
838+
expect(decl?.first_child!.children.length).toBeGreaterThan(1)
805839
expect(decl?.first_child!.children[0].type).toBe(URL)
806840
expect(decl?.first_child!.children[0].name).toBe('url')
807841
expect(decl?.first_child!.children[1].type).toBe(IDENTIFIER)
@@ -911,7 +945,7 @@ describe('Value Node Types', () => {
911945
const root = parse('body { color: ; }')
912946
const decl = root.first_child?.first_child?.next_sibling?.first_child
913947

914-
expect(decl?.first_child!.type).toBe(VALUE)
948+
expect(decl?.first_child!.type).toBe(VALUE)
915949
expect(decl?.first_child!.children).toHaveLength(0)
916950
})
917951

0 commit comments

Comments
 (0)