Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { type ParserOptions } from './parse'

// Types
export { CSSNode, type CSSNodeType } from './css-node'
export type { LexerPosition } from './lexer'

export {
ATTR_OPERATOR_NONE,
Expand Down
43 changes: 43 additions & 0 deletions src/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ const CHAR_UPPERCASE_E = 0x45 // E
const CHAR_CARRIAGE_RETURN = 0x0d // \r
const CHAR_LINE_FEED = 0x0a // \n

export interface LexerPosition {
pos: number
line: number
column: number
token_type: TokenType
token_start: number
token_end: number
token_line: number
token_column: number
}

export class Lexer {
source: string
pos: number
Expand Down Expand Up @@ -542,4 +553,36 @@ export class Lexer {
column: this.token_column,
}
}

/**
* Save complete lexer state for backtracking
* @returns Object containing all lexer state
*/
save_position(): LexerPosition {
return {
pos: this.pos,
line: this.line,
column: this.column,
token_type: this.token_type,
token_start: this.token_start,
token_end: this.token_end,
token_line: this.token_line,
token_column: this.token_column,
}
}

/**
* Restore lexer state from saved position
* @param saved The saved position to restore
*/
restore_position(saved: LexerPosition): void {
this.pos = saved.pos
this.line = saved.line
this.column = saved.column
this.token_type = saved.token_type
this.token_start = saved.token_start
this.token_end = saved.token_end
this.token_line = saved.token_line
this.token_column = saved.token_column
}
}
6 changes: 3 additions & 3 deletions src/parse-anplusb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class ANplusBParser {
// Handle +n pattern
if (this.lexer.token_type === TOKEN_DELIM && this.source.charCodeAt(this.lexer.token_start) === CHAR_PLUS) {
// Look ahead for 'n'
const saved_pos = this.lexer.pos
const saved = this.lexer.save_position()
this.lexer.next_token_fast(true)

if ((this.lexer.token_type as TokenType) === TOKEN_IDENT) {
Expand All @@ -146,7 +146,7 @@ export class ANplusBParser {
if (first_char === 0x6e /* n */) {
// Store +n as authored (including the +)
a = '+n'
a_start = saved_pos - 1 // Position of the + delim
a_start = saved.pos - 1 // Position of the + delim
a_end = this.lexer.token_start + 1

// Check for attached n-digit pattern
Expand All @@ -171,7 +171,7 @@ export class ANplusBParser {
}
}

this.lexer.pos = saved_pos
this.lexer.restore_position(saved)
}

// Handle dimension tokens: 2n, 3n+1, -5n-2
Expand Down
24 changes: 6 additions & 18 deletions src/parse-atrule-prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,7 @@ export class AtRulePreludeParser {
// Parse import layer: layer or layer(name)
private parse_import_layer(): number | null {
// Peek at next token
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

this.next_token()

Expand Down Expand Up @@ -569,18 +567,14 @@ export class AtRulePreludeParser {
}

// Not a layer, restore position
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
return null
}

// Parse import supports: supports(condition)
private parse_import_supports(): number | null {
// Peek at next token
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

this.next_token()

Expand Down Expand Up @@ -621,9 +615,7 @@ export class AtRulePreludeParser {
}

// Not supports(), restore position
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
return null
}

Expand All @@ -634,16 +626,12 @@ export class AtRulePreludeParser {

// Helper: Peek at next token type without consuming
private peek_token_type(): number {
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

this.next_token()
let type = this.lexer.token_type

this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)

return type
}
Expand Down
72 changes: 18 additions & 54 deletions src/parse-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,7 @@ export class SelectorParser {

// Check for leading combinator (relative selector) if allowed
if (allow_relative && this.lexer.pos < this.selector_end) {
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

this.lexer.next_token_fast(false)
let token_type = this.lexer.token_type
Expand All @@ -192,15 +190,11 @@ export class SelectorParser {
// Continue to parse the rest normally
} else {
// Not a combinator, restore position
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
}
} else {
// Not a delimiter, restore position
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
}
}

Expand All @@ -225,25 +219,19 @@ export class SelectorParser {
}

// Peek ahead for comma or end
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()
this.skip_whitespace()
if (this.lexer.pos >= this.selector_end) break

this.lexer.next_token_fast(false)
let token_type = this.lexer.token_type
if (token_type === TOKEN_COMMA || this.lexer.pos >= this.selector_end) {
// Reset position for comma handling
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
break
}
// Reset for next iteration
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
break
}

Expand Down Expand Up @@ -271,9 +259,7 @@ export class SelectorParser {

while (this.lexer.pos < this.selector_end) {
// Save lexer state before getting token
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()
this.lexer.next_token_fast(false)

if (this.lexer.token_start >= this.selector_end) break
Expand All @@ -286,9 +272,7 @@ export class SelectorParser {
parts.push(part)
} else {
// Not a simple selector part, restore lexer state and break
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
break
}
}
Expand Down Expand Up @@ -409,17 +393,13 @@ export class SelectorParser {
// Parse class selector (.classname)
private parse_class_selector(dot_pos: number): number | null {
// Save lexer state for potential restoration
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

// Next token should be identifier
this.lexer.next_token_fast(false)
if (this.lexer.token_type !== TOKEN_IDENT) {
// Restore lexer state and return null
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
return null
}

Expand Down Expand Up @@ -607,9 +587,7 @@ export class SelectorParser {
// Parse pseudo-class or pseudo-element (:hover, ::before)
private parse_pseudo(start: number): number | null {
// Save lexer state for potential restoration
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

// Check for double colon (::)
let is_pseudo_element = false
Expand Down Expand Up @@ -643,9 +621,7 @@ export class SelectorParser {
}

// Restore lexer state and return null
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
return null
}

Expand Down Expand Up @@ -716,9 +692,7 @@ export class SelectorParser {
// Parse as selector (for :is(), :where(), :has(), etc.)
// Save current lexer state and selector_end
let saved_selector_end = this.selector_end
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

// Recursively parse the content as a selector
// Only :has() accepts relative selectors (starting with combinator)
Expand All @@ -727,9 +701,7 @@ export class SelectorParser {

// Restore lexer state and selector_end
this.selector_end = saved_selector_end
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)

// Add as child if parsed successfully
if (child_selector !== null) {
Expand Down Expand Up @@ -759,9 +731,7 @@ export class SelectorParser {
private parse_lang_identifiers(start: number, end: number, parent_node: number): void {
// Save current lexer state
let saved_selector_end = this.selector_end
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

// Set lexer to parse this range
this.lexer.pos = start
Expand Down Expand Up @@ -822,9 +792,7 @@ export class SelectorParser {

// Restore lexer state
this.selector_end = saved_selector_end
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)
}

// Parse An+B expression for nth-* pseudo-classes
Expand All @@ -846,9 +814,7 @@ export class SelectorParser {

// Save current state
let saved_selector_end = this.selector_end
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
const saved = this.lexer.save_position()

// Parse selector list
this.selector_end = end
Expand All @@ -857,9 +823,7 @@ export class SelectorParser {

// Restore state
this.selector_end = saved_selector_end
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.restore_position(saved)

// Create NTH_OF wrapper
let of_node = this.arena.create_node()
Expand Down
27 changes: 3 additions & 24 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,28 +283,14 @@ export class Parser {
let decl_column = this.lexer.token_column

// Lookahead: save lexer state before consuming
let saved_pos = this.lexer.pos
let saved_line = this.lexer.line
let saved_column = this.lexer.column
let saved_token_type = this.lexer.token_type
let saved_token_start = this.lexer.token_start
let saved_token_end = this.lexer.token_end
let saved_token_line = this.lexer.token_line
let saved_token_column = this.lexer.token_column
const saved = this.lexer.save_position()

this.next_token() // consume property name

// Expect ':'
if (this.peek_type() !== TOKEN_COLON) {
// Restore lexer state and return null
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.token_type = saved_token_type
this.lexer.token_start = saved_token_start
this.lexer.token_end = saved_token_end
this.lexer.token_line = saved_token_line
this.lexer.token_column = saved_token_column
this.lexer.restore_position(saved)
return null
}
this.next_token() // consume ':'
Expand Down Expand Up @@ -340,14 +326,7 @@ export class Parser {
// If we encounter '{', this is actually a style rule, not a declaration
if (token_type === TOKEN_LEFT_BRACE) {
// Restore lexer state and return null
this.lexer.pos = saved_pos
this.lexer.line = saved_line
this.lexer.column = saved_column
this.lexer.token_type = saved_token_type
this.lexer.token_start = saved_token_start
this.lexer.token_end = saved_token_end
this.lexer.token_line = saved_token_line
this.lexer.token_column = saved_token_column
this.lexer.restore_position(saved)
return null
}

Expand Down
Loading