diff --git a/src/index.ts b/src/index.ts index a409324..7d50968 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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, diff --git a/src/lexer.ts b/src/lexer.ts index 1cd4b6c..b966e44 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -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 @@ -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 + } } diff --git a/src/parse-anplusb.ts b/src/parse-anplusb.ts index e3540fa..eb26b0d 100644 --- a/src/parse-anplusb.ts +++ b/src/parse-anplusb.ts @@ -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) { @@ -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 @@ -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 diff --git a/src/parse-atrule-prelude.ts b/src/parse-atrule-prelude.ts index 9bec67d..7866e18 100644 --- a/src/parse-atrule-prelude.ts +++ b/src/parse-atrule-prelude.ts @@ -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() @@ -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() @@ -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 } @@ -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 } diff --git a/src/parse-selector.ts b/src/parse-selector.ts index 9c224ef..60e94dc 100644 --- a/src/parse-selector.ts +++ b/src/parse-selector.ts @@ -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 @@ -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) } } @@ -225,9 +219,7 @@ 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 @@ -235,15 +227,11 @@ export class SelectorParser { 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 } @@ -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 @@ -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 } } @@ -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 } @@ -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 @@ -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 } @@ -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) @@ -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) { @@ -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 @@ -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 @@ -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 @@ -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() diff --git a/src/parse.ts b/src/parse.ts index 87c20b4..ef33d0f 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -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 ':' @@ -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 }