11// Declaration Parser - Parses CSS declarations into structured AST nodes
22import { Lexer } from './tokenize'
3- import { CSSDataArena , DECLARATION , FLAG_IMPORTANT } from './arena'
3+ import { CSSDataArena , DECLARATION , FLAG_IMPORTANT , FLAG_BROWSERHACK } from './arena'
44import { ValueParser } from './parse-value'
5+ import { is_vendor_prefixed } from './string-utils'
56import {
67 TOKEN_IDENT ,
78 TOKEN_COLON ,
@@ -10,6 +11,13 @@ import {
1011 TOKEN_EOF ,
1112 TOKEN_LEFT_BRACE ,
1213 TOKEN_RIGHT_BRACE ,
14+ TOKEN_LEFT_PAREN ,
15+ TOKEN_RIGHT_PAREN ,
16+ TOKEN_LEFT_BRACKET ,
17+ TOKEN_RIGHT_BRACKET ,
18+ TOKEN_COMMA ,
19+ TOKEN_HASH ,
20+ TOKEN_AT_KEYWORD ,
1321 type TokenType ,
1422} from './token-types'
1523import { trim_boundaries } from './parse-utils'
@@ -41,16 +49,86 @@ export class DeclarationParser {
4149
4250 // Parse a declaration using a provided lexer (used by Parser to avoid re-tokenization)
4351 parse_declaration_with_lexer ( lexer : Lexer , end : number ) : number | null {
44- // Expect identifier (property name) - whitespace already skipped by caller
45- if ( lexer . token_type !== TOKEN_IDENT ) {
52+ // Check for browser hack prefix (single delimiter/special character before identifier)
53+ let has_browser_hack = false
54+ let browser_hack_start = 0
55+ let browser_hack_line = 1
56+ let browser_hack_column = 1
57+
58+ // Handle @property and #property (tokenized as single tokens)
59+ if ( lexer . token_type === TOKEN_AT_KEYWORD || lexer . token_type === TOKEN_HASH ) {
60+ // These tokens already include the @ or # prefix in their text
61+ // Mark as browser hack since @ and # prefixes are not standard CSS
62+ has_browser_hack = true
63+ browser_hack_start = lexer . token_start
64+ browser_hack_line = lexer . token_line
65+ browser_hack_column = lexer . token_column
66+ } else if ( lexer . token_type === TOKEN_IDENT ) {
67+ // Check if identifier starts with browser hack character
68+ // Some hacks like -property, _property are tokenized as single identifiers
69+ const first_char = this . source . charCodeAt ( lexer . token_start )
70+ if ( first_char === 95 ) {
71+ // '_' - underscore prefix is always a browser hack
72+ has_browser_hack = true
73+ browser_hack_start = lexer . token_start
74+ browser_hack_line = lexer . token_line
75+ browser_hack_column = lexer . token_column
76+ } else if ( first_char === 45 ) {
77+ // '-' - hyphen prefix could be vendor prefix or browser hack
78+ // Use fast vendor prefix check (no allocations)
79+ if ( ! is_vendor_prefixed ( this . source , lexer . token_start , lexer . token_end ) ) {
80+ // This is a browser hack like -property
81+ has_browser_hack = true
82+ browser_hack_start = lexer . token_start
83+ browser_hack_line = lexer . token_line
84+ browser_hack_column = lexer . token_column
85+ }
86+ }
87+ } else {
88+ // Browser hacks can use various token types as prefixes
89+ const is_browser_hack_token =
90+ lexer . token_type === TOKEN_DELIM ||
91+ lexer . token_type === TOKEN_LEFT_PAREN ||
92+ lexer . token_type === TOKEN_RIGHT_PAREN ||
93+ lexer . token_type === TOKEN_LEFT_BRACKET ||
94+ lexer . token_type === TOKEN_RIGHT_BRACKET ||
95+ lexer . token_type === TOKEN_COMMA ||
96+ lexer . token_type === TOKEN_COLON
97+
98+ if ( is_browser_hack_token ) {
99+ // Save position in case this isn't a browser hack
100+ const delim_saved = lexer . save_position ( )
101+ browser_hack_start = lexer . token_start
102+ browser_hack_line = lexer . token_line
103+ browser_hack_column = lexer . token_column
104+
105+ // Consume delimiter and check if next token is identifier
106+ lexer . next_token_fast ( true ) // skip whitespace
107+
108+ if ( ( lexer . token_type as TokenType ) === TOKEN_IDENT ) {
109+ // This is a browser hack!
110+ has_browser_hack = true
111+ } else {
112+ // Not a browser hack, restore position
113+ lexer . restore_position ( delim_saved )
114+ }
115+ }
116+ }
117+
118+ // Expect identifier, at-keyword, or hash token (property name) - whitespace already skipped by caller
119+ if (
120+ lexer . token_type !== TOKEN_IDENT &&
121+ lexer . token_type !== TOKEN_AT_KEYWORD &&
122+ lexer . token_type !== TOKEN_HASH
123+ ) {
46124 return null
47125 }
48126
49- let prop_start = lexer . token_start
127+ let prop_start = has_browser_hack ? browser_hack_start : lexer . token_start
50128 let prop_end = lexer . token_end
51129 // CRITICAL: Capture line/column BEFORE consuming property token
52- let decl_line = lexer . token_line
53- let decl_column = lexer . token_column
130+ let decl_line = has_browser_hack ? browser_hack_line : lexer . token_line
131+ let decl_column = has_browser_hack ? browser_hack_column : lexer . token_column
54132
55133 // Lookahead: save lexer state before consuming
56134 const saved = lexer . save_position ( )
@@ -147,6 +225,11 @@ export class DeclarationParser {
147225 this . arena . set_flag ( declaration , FLAG_IMPORTANT )
148226 }
149227
228+ // Set browser hack flag if found
229+ if ( has_browser_hack ) {
230+ this . arena . set_flag ( declaration , FLAG_BROWSERHACK )
231+ }
232+
150233 // Consume ';' if present
151234 if ( ( lexer . token_type as TokenType ) === TOKEN_SEMICOLON ) {
152235 last_end = lexer . token_end
0 commit comments