Skip to content
Closed
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
6 changes: 4 additions & 2 deletions src/parse-anplusb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// Much inspiration taken from CSSTree (Roman Dvornov) - MIT License
// https://github.com/csstree/csstree/blob/56afb6dd761149099cd3cdfb0a38e15e8cc0a71a/lib/syntax/node/AnPlusB.js#L106-L271

import { Lexer } from './tokenize'
import { Lexer, type LexerPosition } from './tokenize'
import { NTH_SELECTOR, CSSDataArena } from './arena'
import {
TOKEN_IDENT,
Expand All @@ -23,6 +23,8 @@ export class ANplusBParser {
private arena: CSSDataArena
private source: string
private expr_end: number
// Reused "+n" lookahead snapshot (single use per parse_anplusb call, never nested)
private plus_n_lookahead_position: LexerPosition = new Lexer('').save_position()

constructor(arena: CSSDataArena, source: string) {
this.arena = arena
Expand Down Expand Up @@ -152,7 +154,7 @@ export class ANplusBParser {
this.source.charCodeAt(this.lexer.token_start) === CHAR_PLUS
) {
// Look ahead for 'n'
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.plus_n_lookahead_position)
this.lexer.next_token_fast(true)

if ((this.lexer.token_type as TokenType) === TOKEN_IDENT) {
Expand Down
21 changes: 13 additions & 8 deletions src/parse-atrule-prelude.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// At-Rule Prelude Parser - Parses at-rule preludes into structured AST nodes
import { Lexer } from './tokenize'
import { Lexer, type LexerPosition } from './tokenize'
import {
CSSDataArena,
MEDIA_QUERY,
Expand Down Expand Up @@ -50,6 +50,11 @@ export class AtRulePreludeParser {
// Shared media-feature/supports-condition/style()-selector() parsing, also used by ValueNodeParser
// for if()'s condition functions — see parse-condition.ts for why value_node_parser is injected
private condition_parser: ConditionParser
// Reused lookahead snapshot shared by all save/restore sites in this class — none of
// them stay live across a call that could itself need this slot (verified: none here
// recurse back into this class, and the two @import helpers resolve their save before
// delegating further parsing elsewhere).
private lookahead_position: LexerPosition = new Lexer('').save_position()

constructor(arena: CSSDataArena, source: string) {
this.arena = arena
Expand Down Expand Up @@ -132,7 +137,7 @@ export class AtRulePreludeParser {

// Skip comma separator
this.skip_whitespace()
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.lookahead_position)
this.next_token()
if (this.lexer.token_type !== TOKEN_COMMA) {
// Not a comma, restore position
Expand Down Expand Up @@ -203,7 +208,7 @@ export class AtRulePreludeParser {
let last_component = 0

// Leading modifier (only/not) — emit as PRELUDE_OPERATOR like the and/or/not combinators below
const saved_token_start = this.lexer.save_position()
const saved_token_start = this.lexer.save_position_into(this.lookahead_position)
this.next_token()

if (this.lexer.token_type === TOKEN_IDENT) {
Expand Down Expand Up @@ -390,7 +395,7 @@ export class AtRulePreludeParser {

// Glue on '.' ident segments immediately following, with no gaps.
while (this.lexer.pos < this.prelude_end) {
let saved = this.lexer.save_position()
let saved = this.lexer.save_position_into(this.lookahead_position)

let dot_token_type = this.next_token()
if (
Expand Down Expand Up @@ -556,7 +561,7 @@ export class AtRulePreludeParser {
// Parse import layer: layer or layer(name)
private parse_import_layer(): number | null {
// Peek at next token
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.lookahead_position)

this.next_token()

Expand Down Expand Up @@ -609,7 +614,7 @@ export class AtRulePreludeParser {
// Parse import supports: supports(condition)
private parse_import_supports(): number | null {
// Peek at next token
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.lookahead_position)

this.next_token()

Expand Down Expand Up @@ -662,7 +667,7 @@ export class AtRulePreludeParser {

// Helper: Peek at next token type without consuming
private peek_token_type(): number {
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.lookahead_position)

this.next_token()
let type = this.lexer.token_type
Expand Down Expand Up @@ -690,7 +695,7 @@ export class AtRulePreludeParser {
if (this.lexer.pos >= this.prelude_end) return []

// Peek: if it's an IDENT it's an optional prefix, otherwise let parse_import_url() take it
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.lookahead_position)
this.next_token()

if (this.lexer.token_type === TOKEN_IDENT) {
Expand Down
7 changes: 5 additions & 2 deletions src/parse-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// own (a plain `import { ValueNodeParser } from './value-node-parser'` here would be
// circular, since value-node-parser.ts imports this file). The type-only import below is
// erased at build time and creates no runtime dependency in either direction.
import { Lexer } from './tokenize'
import { Lexer, type LexerPosition } from './tokenize'
import {
CSSDataArena,
MEDIA_FEATURE,
Expand Down Expand Up @@ -61,6 +61,9 @@ export class ConditionParser {
// Deep-parses selector()'s argument into a real SelectorList. No cycle risk (SelectorParser
// doesn't depend on ValueNodeParser/AtRulePreludeParser), so this is safely owned here.
private selector_parser: SelectorParser
// Reused peek snapshot for parse_feature_range's token-type probe (never nested — resolved
// within the same loop iteration, before the next save).
private feature_range_peek_position: LexerPosition = new Lexer('').save_position()

constructor(arena: CSSDataArena, source: string, value_node_parser: ValueNodeParser) {
this.arena = arena
Expand Down Expand Up @@ -259,7 +262,7 @@ export class ConditionParser {
last_child = op
} else {
// Value or feature name
let saved = this.lexer.save_position()
let saved = this.lexer.save_position_into(this.feature_range_peek_position)
this.lexer.pos = pos
this.next_token()

Expand Down
5 changes: 4 additions & 1 deletion src/parse-declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ export class DeclarationParser {
private value_parser: ValueParser | null
// Reused lookahead snapshot (never nested) to avoid an allocation per declaration
private lookahead_position: LexerPosition = new Lexer('').save_position()
// Separate scratch slot: the browser-hack delimiter-prefix snapshot (below) stays live
// at the same time as lookahead_position, so it can't reuse that field.
private delim_prefix_position: LexerPosition = new Lexer('').save_position()

constructor(arena: CSSDataArena, source: string, parse_values: boolean = true) {
this.arena = arena
Expand Down Expand Up @@ -111,7 +114,7 @@ export class DeclarationParser {
lexer.token_type === TOKEN_COLON
) {
// Browser hacks can use various token types as prefixes (e.g., * hack)
const delim_saved = lexer.save_position()
const delim_saved = lexer.save_position_into(this.delim_prefix_position)
browser_hack_start = lexer.token_start
browser_hack_line = lexer.token_line
browser_hack_column = lexer.token_column
Expand Down
44 changes: 34 additions & 10 deletions src/parse-selector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Selector Parser - Parses CSS selectors into structured AST nodes
import { Lexer } from './tokenize'
import { Lexer, type LexerPosition } from './tokenize'
import { CSSDataArena } from './arena'
import {
SELECTOR,
Expand Down Expand Up @@ -71,6 +71,30 @@ export class SelectorParser {
private source: string
private selector_end: number
private anplusb_parser: ANplusBParser
// Reused lookahead snapshots for the save/restore sites that are never live across a
// recursive re-entry into the selector grammar (unlike parse_compound_selector's,
// parse_pseudo_function_after_colon's, and parse_nth_expression's own saves, which stay
// allocating for now — see the recursive :is()/:has()/:nth-child(of) call chain).
// Resolved entirely before parse_complex_selector's compound-selector loop runs.
private complex_selector_lookahead_position: LexerPosition = new Lexer('').save_position()
// Live only inside parse_namespace_local_part itself, never during its own call into
// anything else — but concurrently live with whichever of the two fields below called it,
// so it must stay distinct from both.
private namespace_local_part_lookahead_position: LexerPosition = new Lexer('').save_position()
// Shared by parse_type_or_namespace_selector and parse_universal_or_namespace_selector:
// alternative call paths (type selector vs universal selector) that are never both active,
// each held live across its own call into parse_namespace_local_part.
private namespace_selector_lookahead_position: LexerPosition = new Lexer('').save_position()
// try_parse_combinator: fully resolved within one call, no nested saves.
private combinator_lookahead_position: LexerPosition = new Lexer('').save_position()
// parse_class_selector: fully resolved within one call, no nested saves.
private class_selector_lookahead_position: LexerPosition = new Lexer('').save_position()
// parse_pseudo's two concurrent saves (`saved` persists longer, discarded rather than
// re-read on the path that recurses into parse_pseudo_function_after_colon).
private pseudo_lookahead_position: LexerPosition = new Lexer('').save_position()
private pseudo_double_colon_lookahead_position: LexerPosition = new Lexer('').save_position()
// parse_lang_identifiers: fully resolved within one call, no nested saves.
private lang_identifiers_lookahead_position: LexerPosition = new Lexer('').save_position()

constructor(arena: CSSDataArena, source: string) {
this.arena = arena
Expand Down Expand Up @@ -192,7 +216,7 @@ export class SelectorParser {

// Check for leading combinator (relative selector) if allowed
if (allow_relative && this.lexer.pos < this.selector_end) {
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.complex_selector_lookahead_position)

this.lexer.next_token_fast(false)
let token_type = this.lexer.token_type
Expand Down Expand Up @@ -357,7 +381,7 @@ export class SelectorParser {
namespace_start: number,
namespace_prefix_length: number,
): number | null {
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.namespace_local_part_lookahead_position)
this.lexer.next_token_fast(false)

let node_type: number
Expand Down Expand Up @@ -404,7 +428,7 @@ export class SelectorParser {
}

// Save position before skipping whitespace/comments
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.namespace_selector_lookahead_position)

// Skip whitespace and comments before checking for |
this.skip_whitespace()
Expand All @@ -429,7 +453,7 @@ export class SelectorParser {
// Called when we've seen a * DELIM token
private parse_universal_or_namespace_selector(start: number, end: number): number | null {
// Save position before skipping whitespace/comments
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.namespace_selector_lookahead_position)

// Skip whitespace and comments before checking for |
this.skip_whitespace()
Expand Down Expand Up @@ -459,7 +483,7 @@ export class SelectorParser {

// Parse combinator (>, +, ~, or descendant space)
private try_parse_combinator(): number | null {
const saved_whitespace_start = this.lexer.save_position()
const saved_whitespace_start = this.lexer.save_position_into(this.combinator_lookahead_position)
let has_whitespace = this.lexer.pos < this.selector_end

// Skip whitespace and comments
Expand Down Expand Up @@ -513,7 +537,7 @@ export class SelectorParser {
// Parse class selector (.classname)
private parse_class_selector(dot_pos: number): number | null {
// Save lexer state for potential restoration
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.class_selector_lookahead_position)

// Next token should be identifier
this.lexer.next_token_fast(false)
Expand Down Expand Up @@ -685,10 +709,10 @@ export class SelectorParser {
// Parse pseudo-class or pseudo-element (:hover, ::before)
private parse_pseudo(start: number): number | null {
// Save lexer state for potential restoration
const saved = this.lexer.save_position()
const saved = this.lexer.save_position_into(this.pseudo_lookahead_position)

// Save position before skipping whitespace/comments
const saved_ws = this.lexer.save_position()
const saved_ws = this.lexer.save_position_into(this.pseudo_double_colon_lookahead_position)

// Skip whitespace and comments before checking for second colon
this.skip_whitespace()
Expand Down Expand Up @@ -840,7 +864,7 @@ export class SelectorParser {
// Parse :lang() content - comma-separated language identifiers
// Accepts both quoted strings: :lang("en", "fr") and unquoted: :lang(en, fr)
private parse_lang_identifiers(start: number, end: number, parent_node: number): void {
const saved_position = this.lexer.save_position()
const saved_position = this.lexer.save_position_into(this.lang_identifiers_lookahead_position)
this.lexer.seek(start, this.lexer.line, this.lexer.column)

let saved_selector_end = this.selector_end
Expand Down
13 changes: 10 additions & 3 deletions src/value-node-parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Value Node Parser - shared recursive value-token parsing, used by both ValueParser
// (declaration values) and AtRulePreludeParser (feature values), so calc()/env()/var()
// get the same structured tree everywhere instead of an opaque text span.
import { Lexer } from './tokenize'
import { Lexer, type LexerPosition } from './tokenize'
import {
CSSDataArena,
IDENTIFIER,
Expand Down Expand Up @@ -66,6 +66,13 @@ export class ValueNodeParser {
// try to build another ValueNodeParser to inject, forever. The helper's own condition_parser is
// simply never accessed (it's only ever used via parse_chain), so the recursion stops there.
private _condition_parser: ConditionParser | null = null
// Reused peek snapshots. Two distinct fields: parse_function_node's url()/src() peek and
// parse_if_condition's and/or/not continuation peek are each resolved before anything else
// in this class could save again, so neither is ever live at the same time as the other —
// but parse_value_nodes_in_range's own save (elsewhere) CAN be live across a recursive
// descent into this class, so it deliberately does not share either of these.
private url_peek_position: LexerPosition = new Lexer('').save_position()
private if_condition_peek_position: LexerPosition = new Lexer('').save_position()

private get condition_parser(): ConditionParser {
if (this._condition_parser === null) {
Expand Down Expand Up @@ -266,7 +273,7 @@ export class ValueNodeParser {
if (is_url || str_equals_range(this.source, start, name_end, 'src')) {
// Peek at the next token to see if it's a string
// If it's a string, parse normally. Otherwise, skip parsing children.
let save_pos = this.lexer.save_position()
let save_pos = this.lexer.save_position_into(this.url_peek_position)
this.lexer.next_token_fast(false)

// Skip whitespace
Expand Down Expand Up @@ -593,7 +600,7 @@ export class ValueNodeParser {
// condition; anything else ends the condition here.
if (tt !== TOKEN_FUNCTION && !is_operator) break

let saved = this.lexer.save_position()
let saved = this.lexer.save_position_into(this.if_condition_peek_position)
let next_tt = this.next_significant_token()
if (next_tt === TOKEN_FUNCTION) {
compound = true
Expand Down
Loading