diff --git a/src/parse-atrule-prelude.ts b/src/parse-atrule-prelude.ts index 4229975..194a6ad 100644 --- a/src/parse-atrule-prelude.ts +++ b/src/parse-atrule-prelude.ts @@ -27,7 +27,12 @@ import { TOKEN_DELIM, type TokenType, } from './token-types' -import { str_equals, strip_vendor_prefix, CHAR_PERIOD } from './string-utils' +import { + str_equals_range, + is_and_or_not_range, + strip_vendor_prefix, + CHAR_PERIOD, +} from './string-utils' import { trim_boundaries, skip_whitespace_and_comments_forward } from './parse-utils' import { CSSNode } from './css-node' import type { AnyNode } from './node-types' @@ -148,12 +153,6 @@ export class AtRulePreludeParser { ) } - private is_and_or_not(str: string): boolean { - // All logical operators are 2-3 chars: "and" (3), "or" (2), "not" (3) - // The str_equals calls will quickly reject strings of other lengths - return str_equals('and', str) || str_equals('or', str) || str_equals('not', str) - } - // Scan tokens from just after an already-open '(' or function-call (depth 1) to its // matching ')'. Must be called right after consuming the opening token. Returns // [content_end, close_end, matched]: content_end/close_end are the positions right @@ -161,7 +160,10 @@ export class AtRulePreludeParser { // content_end/close_end are left at the position scanning started from (mirroring the // caller's own pre-loop defaults, so callers that don't check `matched` still get sane // fallback spans). - private scan_matching_paren(): [content_end: number, close_end: number, matched: boolean] { + private paren_content_end = 0 + private paren_close_end = 0 + + private scan_matching_paren(): boolean { let depth = 1 let content_end = this.lexer.pos let close_end = this.lexer.token_end @@ -181,7 +183,9 @@ export class AtRulePreludeParser { } } - return [content_end, close_end, depth === 0] + this.paren_content_end = content_end + this.paren_close_end = close_end + return depth === 0 } // Parse a single media query: screen and (min-width: 768px) @@ -203,13 +207,14 @@ export class AtRulePreludeParser { this.next_token() if (this.lexer.token_type === TOKEN_IDENT) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - if (str_equals('only', text) || str_equals('not', text)) { - let modifier = this.create_node( - PRELUDE_OPERATOR, - this.lexer.token_start, - this.lexer.token_end, - ) + let source = this.source + let token_start = this.lexer.token_start + let token_end = this.lexer.token_end + if ( + str_equals_range(source, token_start, token_end, 'only') || + str_equals_range(source, token_start, token_end, 'not') + ) { + let modifier = this.create_node(PRELUDE_OPERATOR, token_start, token_end) first_component = modifier last_component = modifier } else { @@ -242,23 +247,19 @@ export class AtRulePreludeParser { ) if (component !== null) { // condition_parser scanned with its own lexer — resync ours past what it consumed - this.lexer.seek(...this.condition_parser.end_position) + this.condition_parser.seek_to_end(this.lexer) } } // Identifier: media type or operator (and, or, not) else if (token_type === TOKEN_IDENT) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - - if (this.is_and_or_not(text)) { + let token_start = this.lexer.token_start + let token_end = this.lexer.token_end + if (is_and_or_not_range(this.source, token_start, token_end)) { // Logical operator - component = this.create_node( - PRELUDE_OPERATOR, - this.lexer.token_start, - this.lexer.token_end, - ) + component = this.create_node(PRELUDE_OPERATOR, token_start, token_end) } else { // Media type: screen, print, all - component = this.create_node(MEDIA_TYPE, this.lexer.token_start, this.lexer.token_end) + component = this.create_node(MEDIA_TYPE, token_start, token_end) } } else { // Unknown token, skip @@ -313,7 +314,7 @@ export class AtRulePreludeParser { this.lexer.token_column, ) if (component !== null) { - this.lexer.seek(...this.condition_parser.end_position) + this.condition_parser.seek_to_end(this.lexer) } } // Function: style(--custom: 1) @@ -324,22 +325,18 @@ export class AtRulePreludeParser { this.lexer.token_line, this.lexer.token_column, ) - this.lexer.seek(...this.condition_parser.end_position) + this.condition_parser.seek_to_end(this.lexer) } // Identifier: operator (and, or, not) or container name else if (token_type === TOKEN_IDENT) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - - if (this.is_and_or_not(text)) { + let token_start = this.lexer.token_start + let token_end = this.lexer.token_end + if (is_and_or_not_range(this.source, token_start, token_end)) { // Logical operator - component = this.create_node( - PRELUDE_OPERATOR, - this.lexer.token_start, - this.lexer.token_end, - ) + component = this.create_node(PRELUDE_OPERATOR, token_start, token_end) } else { // Container name or other identifier - component = this.create_node(IDENTIFIER, this.lexer.token_start, this.lexer.token_end) + component = this.create_node(IDENTIFIER, token_start, token_end) } } @@ -546,7 +543,8 @@ export class AtRulePreludeParser { if (this.lexer.token_type === TOKEN_FUNCTION) { // It's url( ... we need to find the matching ) - let [, close_end] = this.scan_matching_paren() + this.scan_matching_paren() + let close_end = this.paren_close_end url_end = close_end } @@ -564,13 +562,11 @@ export class AtRulePreludeParser { // Check for 'layer' keyword or 'layer(' function if (this.lexer.token_type === TOKEN_IDENT || this.lexer.token_type === TOKEN_FUNCTION) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - // For function tokens, remove the trailing '(' - if (this.lexer.token_type === TOKEN_FUNCTION && text.endsWith('(')) { - text = text.slice(0, -1) - } + // For function tokens, exclude the trailing '(' + let name_end = + this.lexer.token_type === TOKEN_FUNCTION ? this.lexer.token_end - 1 : this.lexer.token_end - if (str_equals('layer', text)) { + if (str_equals_range(this.source, this.lexer.token_start, name_end, 'layer')) { let layer_start = this.lexer.token_start let layer_end = this.lexer.token_end let content_start = 0 @@ -580,7 +576,9 @@ export class AtRulePreludeParser { if (this.lexer.token_type === TOKEN_FUNCTION) { // Track the content inside the parentheses content_start = this.lexer.pos - let [content_end, close_end, matched] = this.scan_matching_paren() + let matched = this.scan_matching_paren() + let content_end = this.paren_content_end + let close_end = this.paren_close_end if (matched) { content_length = content_end - content_start layer_end = close_end @@ -617,13 +615,17 @@ export class AtRulePreludeParser { // Check for 'supports(' function if (this.lexer.token_type === TOKEN_FUNCTION) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end - 1) // -1 to exclude '(' - if (str_equals('supports', text)) { + // -1 to exclude '(' + if ( + str_equals_range(this.source, this.lexer.token_start, this.lexer.token_end - 1, 'supports') + ) { let supports_start = this.lexer.token_start let content_start = this.lexer.token_end // After the opening '(' // Find matching closing parenthesis - let [content_end, supports_end] = this.scan_matching_paren() + this.scan_matching_paren() + let content_end = this.paren_content_end + let supports_end = this.paren_close_end // Create supports node let supports_node = this.create_node(SUPPORTS_QUERY, supports_start, supports_end) @@ -720,7 +722,9 @@ export class AtRulePreludeParser { let paren_start = this.lexer.token_start let content_start = this.lexer.pos - let [content_end, paren_end] = this.scan_matching_paren() + this.scan_matching_paren() + let content_end = this.paren_content_end + let paren_end = this.paren_close_end let scope_node = this.create_node(PRELUDE_SELECTORLIST, paren_start, paren_end) let trimmed = trim_boundaries(this.source, content_start, content_end) @@ -731,8 +735,7 @@ export class AtRulePreludeParser { nodes.push(scope_node) } else if (token_type === TOKEN_IDENT) { this.next_token() - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - if (str_equals('to', text)) { + if (str_equals_range(this.source, this.lexer.token_start, this.lexer.token_end, 'to')) { nodes.push( this.create_node(PRELUDE_OPERATOR, this.lexer.token_start, this.lexer.token_end), ) diff --git a/src/parse-condition.ts b/src/parse-condition.ts index 5fed5ed..3f1eda6 100644 --- a/src/parse-condition.ts +++ b/src/parse-condition.ts @@ -38,6 +38,8 @@ import { CHAR_GREATER_THAN, CHAR_EQUALS, CHAR_FORWARD_SLASH, + str_equals_range, + is_and_or_not_range, } from './string-utils' import { trim_boundaries, @@ -70,8 +72,8 @@ export class ConditionParser { // Where this instance's own lexer landed after the last parse_media_feature()/ // parse_function_condition() call — callers scanning the same range on their own lexer must reseek here after delegating. - get end_position(): [pos: number, line: number, column: number] { - return [this.lexer.pos, this.lexer.line, this.lexer.column] + seek_to_end(target: Lexer): void { + target.seek(this.lexer.pos, this.lexer.line, this.lexer.column) } private create_node(type: number, start: number, end: number): number { @@ -92,12 +94,6 @@ export class ConditionParser { return this.lexer.next_token_fast(false) } - private is_and_or_not(str: string): boolean { - // All logical operators are 2-3 chars: "and" (3), "or" (2), "not" (3) - // The str_equals calls will quickly reject strings of other lengths - return str_equals('and', str) || str_equals('or', str) || str_equals('not', str) - } - // Scan tokens from just after an already-open '(' or function-call (depth 1) to its // matching ')'. Must be called right after consuming the opening token. Returns // [content_end, close_end, matched]: content_end/close_end are the positions right @@ -105,7 +101,10 @@ export class ConditionParser { // content_end/close_end are left at the position scanning started from (mirroring the // caller's own pre-loop defaults, so callers that don't check `matched` still get sane // fallback spans). - private scan_matching_paren(): [content_end: number, close_end: number, matched: boolean] { + private paren_content_end = 0 + private paren_close_end = 0 + + private scan_matching_paren(): boolean { let depth = 1 let content_end = this.lexer.pos let close_end = this.lexer.token_end @@ -125,7 +124,9 @@ export class ConditionParser { } } - return [content_end, close_end, depth === 0] + this.paren_content_end = content_end + this.paren_close_end = close_end + return depth === 0 } // Parse feature value via the shared ValueNodeParser, so calc()/env()/var() get full children. @@ -397,7 +398,9 @@ export class ConditionParser { let feature_start = this.lexer.token_start let content_start = this.lexer.pos - let [content_end, feature_end, matched] = this.scan_matching_paren() + let matched = this.scan_matching_paren() + let content_end = this.paren_content_end + let feature_end = this.paren_close_end if (!matched) return null return this.parse_media_feature_content(feature_start, feature_end, content_start, content_end) @@ -411,23 +414,26 @@ export class ConditionParser { this.lexer.seek(func_start, line, column) this.next_token() // consume the function token, up to and including '(' - let func_name = this.source.substring(this.lexer.token_start, this.lexer.token_end - 1) // -1 to exclude '(' + let func_name_start = this.lexer.token_start + let func_name_end = this.lexer.token_end - 1 // -1 to exclude '(' let content_start = this.lexer.token_end // After '(' // Find matching closing paren - let [content_end, func_end] = this.scan_matching_paren() + this.scan_matching_paren() + let content_end = this.paren_content_end + let func_end = this.paren_close_end // Create function node let func_node = this.create_node(FUNCTION, func_start, func_end) // Set content fields to function name this.arena.set_content_start_delta(func_node, 0) - this.arena.set_content_length(func_node, func_name.length) + this.arena.set_content_length(func_node, func_name_end - func_name_start) // Set value fields to content inside parentheses this.arena.set_value_start_delta(func_node, content_start - func_start) this.arena.set_value_length(func_node, content_end - content_start) // selector()'s argument is a — parse it into a real SelectorList - if (str_equals('selector', func_name)) { + if (str_equals_range(this.source, func_name_start, func_name_end, 'selector')) { let selector_list = this.selector_parser.parse_selector( content_start, content_end, @@ -439,7 +445,7 @@ export class ConditionParser { } } // style()'s argument is a — parse it into the same tree as (property: value) - else if (str_equals('style', func_name)) { + else if (str_equals_range(this.source, func_name_start, func_name_end, 'style')) { let decl_child = this.parse_supports_declaration_content(content_start, content_end) if (decl_child !== null) { this.arena.set_first_child(func_node, decl_child) @@ -483,7 +489,9 @@ export class ConditionParser { let feature_start = this.lexer.token_start let content_start = this.lexer.pos - let [content_end, feature_end, matched] = this.scan_matching_paren() + let matched = this.scan_matching_paren() + let content_end = this.paren_content_end + let feature_end = this.paren_close_end if (matched) { let query = this.create_node(SUPPORTS_QUERY, feature_start, feature_end) @@ -504,10 +512,10 @@ export class ConditionParser { } // Identifier: operator (and, or, not) else if (token_type === TOKEN_IDENT) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - - if (this.is_and_or_not(text)) { - let op = this.create_node(PRELUDE_OPERATOR, this.lexer.token_start, this.lexer.token_end) + let token_start = this.lexer.token_start + let token_end = this.lexer.token_end + if (is_and_or_not_range(this.source, token_start, token_end)) { + let op = this.create_node(PRELUDE_OPERATOR, token_start, token_end) nodes.push(op) } } diff --git a/src/parse-declaration.ts b/src/parse-declaration.ts index 245a9f5..a8eee30 100644 --- a/src/parse-declaration.ts +++ b/src/parse-declaration.ts @@ -1,5 +1,5 @@ // Declaration Parser - Parses CSS declarations into structured AST nodes -import { Lexer } from './tokenize' +import { Lexer, type LexerPosition } from './tokenize' import { CSSDataArena, DECLARATION, RAW, FLAG_IMPORTANT, FLAG_BROWSERHACK } from './arena' import { ValueParser } from './parse-value' import { @@ -25,7 +25,10 @@ import { TOKEN_AT_KEYWORD, type TokenType, } from './token-types' -import { trim_boundaries, skip_whitespace_and_comments_backward } from './parse-utils' +import { + skip_whitespace_and_comments_forward, + skip_whitespace_and_comments_backward, +} from './parse-utils' import { CSSNode } from './css-node' import type { Declaration } from './node-types' @@ -34,6 +37,8 @@ export class DeclarationParser { private arena: CSSDataArena private source: string private value_parser: ValueParser | null + // Reused lookahead snapshot (never nested) to avoid an allocation per declaration + private lookahead_position: LexerPosition = new Lexer('').save_position() constructor(arena: CSSDataArena, source: string, parse_values: boolean = true) { this.arena = arena @@ -58,8 +63,8 @@ export class DeclarationParser { // Parse a declaration using a provided lexer (used by Parser to avoid re-tokenization) parse_declaration_with_lexer(lexer: Lexer, end: number): number | null { - // Save initial position so we can fully restore if parsing fails after consuming a delimiter prefix - const initial_saved = lexer.save_position() + // Only set when a delimiter prefix is consumed, to fully restore on failure + let initial_saved: LexerPosition | null = null // Check for browser hack prefix (single delimiter/special character before identifier) let has_browser_hack = false @@ -116,6 +121,7 @@ export class DeclarationParser { if ((lexer.token_type as TokenType) === TOKEN_IDENT) { has_browser_hack = true has_delimiter_prefix = true + initial_saved = delim_saved } else { lexer.restore_position(delim_saved) } @@ -137,7 +143,7 @@ export class DeclarationParser { let decl_column = has_browser_hack ? browser_hack_column : lexer.token_column // Lookahead: save lexer state before consuming - const saved = lexer.save_position() + const saved = lexer.save_position_into(this.lookahead_position) lexer.next_token_fast(true) // consume property name, skip whitespace @@ -146,7 +152,7 @@ export class DeclarationParser { // Restore lexer state and return null. // If we consumed a delimiter prefix (e.g. ':' before the property name), we must restore // all the way back to before that prefix so the caller's lexer position is correct. - lexer.restore_position(has_delimiter_prefix ? initial_saved : saved) + lexer.restore_position(initial_saved ?? saved) return null } // Skip whitespace after ':' without tokenizing the value's first token - the raw scan @@ -261,18 +267,19 @@ export class DeclarationParser { } // Store value position (trimmed) and parse value nodes - let trimmed = trim_boundaries(this.source, value_start, value_end) - if (trimmed) { + let trimmed_start = skip_whitespace_and_comments_forward(this.source, value_start, value_end) + let trimmed_end = skip_whitespace_and_comments_backward(this.source, value_end, trimmed_start) + if (trimmed_start < trimmed_end) { // Store raw value string offsets (for fast string access) - this.arena.set_value_start_delta(declaration, trimmed[0] - prop_start) - this.arena.set_value_length(declaration, trimmed[1] - trimmed[0]) + this.arena.set_value_start_delta(declaration, trimmed_start - prop_start) + this.arena.set_value_length(declaration, trimmed_end - trimmed_start) // Parse value into structured nodes (only if enabled) if (this.value_parser) { // CRITICAL: Pass value_start_line and value_start_column to value parser let valueNode = this.value_parser.parse_value( value_start, - trimmed[1], + trimmed_end, value_start_line, value_start_column, ) @@ -283,8 +290,8 @@ export class DeclarationParser { // Create RAW node for unparsed value text let rawNode = this.arena.create_node( RAW, - trimmed[0], - trimmed[1] - trimmed[0], + trimmed_start, + trimmed_end - trimmed_start, value_start_line, value_start_column, ) diff --git a/src/parse.ts b/src/parse.ts index d29627c..27c57de 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -34,7 +34,10 @@ import { TOKEN_COLON, TOKEN_FUNCTION, } from './token-types' -import { trim_boundaries, skip_whitespace_and_comments_backward } from './parse-utils' +import { + skip_whitespace_and_comments_forward, + skip_whitespace_and_comments_backward, +} from './parse-utils' import { CHAR_PERIOD, CHAR_GREATER_THAN, @@ -436,24 +439,29 @@ export class Parser { } // Store prelude position (trimmed) - let trimmed = trim_boundaries(this.source, prelude_start, prelude_end) + let trimmed_start = skip_whitespace_and_comments_forward( + this.source, + prelude_start, + prelude_end, + ) + let trimmed_end = skip_whitespace_and_comments_backward(this.source, prelude_end, trimmed_start) let prelude_wrapper: number | null = null - if (trimmed) { + if (trimmed_start < trimmed_end) { // Create AT_RULE_PRELUDE wrapper if prelude parsing is enabled if (this.prelude_parser) { // Parse prelude and add structured nodes as children let prelude_nodes = this.prelude_parser.parse_prelude( at_rule_name, - trimmed[0], - trimmed[1], + trimmed_start, + trimmed_end, at_rule_line, at_rule_column, ) if (prelude_nodes.length > 0) { prelude_wrapper = this.arena.create_node( AT_RULE_PRELUDE, - trimmed[0], - trimmed[1] - trimmed[0], + trimmed_start, + trimmed_end - trimmed_start, at_rule_line, at_rule_column, ) @@ -461,8 +469,8 @@ export class Parser { } else { prelude_wrapper = this.arena.create_node( RAW, - trimmed[0], - trimmed[1] - trimmed[0], + trimmed_start, + trimmed_end - trimmed_start, at_rule_line, at_rule_column, ) @@ -470,8 +478,8 @@ export class Parser { } else { prelude_wrapper = this.arena.create_node( RAW, - trimmed[0], - trimmed[1] - trimmed[0], + trimmed_start, + trimmed_end - trimmed_start, at_rule_line, at_rule_column, ) diff --git a/src/string-utils.ts b/src/string-utils.ts index 0e272e6..dcc61ef 100644 --- a/src/string-utils.ts +++ b/src/string-utils.ts @@ -99,6 +99,17 @@ export function str_equals_range( return true } +// True if `source[start, end)` is and/or/not (case-insensitive) +export function is_and_or_not_range(source: string, start: number, end: number): boolean { + let length = end - start + if (length === 3) { + return ( + str_equals_range(source, start, end, 'and') || str_equals_range(source, start, end, 'not') + ) + } + return length === 2 && str_equals_range(source, start, end, 'or') +} + /** Case-insensitive ASCII prefix check without allocations. `prefix` MUST be lowercase. */ export function str_starts_with(str: string, prefix: string): boolean { if (str.length < prefix.length) { diff --git a/src/tokenize.ts b/src/tokenize.ts index 559c0b2..8694816 100644 --- a/src/tokenize.ts +++ b/src/tokenize.ts @@ -997,6 +997,20 @@ export class Lexer { } } + // Like `save_position()`, but fills `target` instead of allocating + save_position_into(target: LexerPosition): LexerPosition { + target.pos = this.pos + target.line = this._line + target.column = this.pos - this._line_offset + 1 + target._line_offset = this._line_offset + target.token_type = this.token_type + target.token_start = this.token_start + target.token_end = this.token_end + target.token_line = this.token_line + target.token_column = this.token_column + return target + } + /** * Restore lexer state from saved position * @param saved The saved position to restore diff --git a/src/value-node-parser.ts b/src/value-node-parser.ts index 19eeb36..93f61e0 100644 --- a/src/value-node-parser.ts +++ b/src/value-node-parser.ts @@ -45,6 +45,7 @@ import { CHAR_FORWARD_SLASH, str_equals, str_equals_range, + is_and_or_not_range, } from './string-utils' import { ConditionParser } from './parse-condition' @@ -205,9 +206,10 @@ export class ValueNodeParser { // see the caller for why). // Returns [content_end, close_end, matched]; matched is false if EOF was hit first, in // which case content_end/close_end are left at their initial (scan-start) values. - private scan_matching_paren( - bounded: boolean, - ): [content_end: number, close_end: number, matched: boolean] { + private paren_content_end = 0 + private paren_close_end = 0 + + private scan_matching_paren(bounded: boolean): boolean { let depth = 1 let content_end = this.lexer.pos let close_end = this.lexer.token_end @@ -229,7 +231,9 @@ export class ValueNodeParser { } } - return [content_end, close_end, depth === 0] + this.paren_content_end = content_end + this.paren_close_end = close_end + return depth === 0 } private parse_function_node(start: number, end: number): number { @@ -291,7 +295,9 @@ export class ValueNodeParser { let content_start = end // Position after 'url(' let content_end = end - let [scanned_content_end, scanned_func_end, matched] = this.scan_matching_paren(false) + let matched = this.scan_matching_paren(false) + let scanned_content_end = this.paren_content_end + let scanned_func_end = this.paren_close_end if (matched) { content_end = scanned_content_end func_end = scanned_func_end @@ -521,11 +527,6 @@ export class ValueNodeParser { return node } - private is_and_or_not(str: string): boolean { - // All logical operators are 2-3 chars: "and" (3), "or" (2), "not" (3) - return str_equals('and', str) || str_equals('or', str) || str_equals('not', str) - } - // Advance past whitespace to the next real token, returning its type (TOKEN_EOF once // `this.end` is reached). Used to look ahead for a not/and/or continuation without // committing to consuming it — callers restore to a saved position when it doesn't. @@ -552,8 +553,7 @@ export class ValueNodeParser { private parse_if_condition(): number | null { // "else" never combines with not/and/or — always a bare identifier. if (this.lexer.token_type === TOKEN_IDENT) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - if (str_equals('else', text)) { + if (str_equals_range(this.source, this.lexer.token_start, this.lexer.token_end, 'else')) { return this.parse_value_node() } } @@ -571,13 +571,14 @@ export class ValueNodeParser { if (tt === TOKEN_FUNCTION) { component = this.parse_if_condition_function(this.lexer.token_start, this.lexer.token_end) } else if (tt === TOKEN_IDENT) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - if (this.is_and_or_not(text)) { + let token_start = this.lexer.token_start + let token_end = this.lexer.token_end + if (is_and_or_not_range(this.source, token_start, token_end)) { is_operator = true component = this.arena.create_node( PRELUDE_OPERATOR, - this.lexer.token_start, - this.lexer.token_end - this.lexer.token_start, + token_start, + token_end - token_start, this.lexer.token_line, this.lexer.token_column, ) @@ -606,8 +607,7 @@ export class ValueNodeParser { continue } if (next_tt === TOKEN_IDENT) { - let text = this.source.substring(this.lexer.token_start, this.lexer.token_end) - if (this.is_and_or_not(text)) { + if (is_and_or_not_range(this.source, this.lexer.token_start, this.lexer.token_end)) { compound = true continue } @@ -648,7 +648,6 @@ export class ValueNodeParser { // @param token_end Offset right after '(' (== lexer.token_end here). private parse_if_condition_function(func_start: number, token_end: number): number { let func_name_end = token_end - 1 // before '(' - let func_name = this.source.substring(func_start, func_name_end) let func_line = this.lexer.token_line let func_col = this.lexer.token_column @@ -657,7 +656,9 @@ export class ValueNodeParser { let func_end = content_start // Scan for matching ')' to find the full function extent - let [scanned_content_end, scanned_func_end, matched] = this.scan_matching_paren(true) + let matched = this.scan_matching_paren(true) + let scanned_content_end = this.paren_content_end + let scanned_func_end = this.paren_close_end if (matched) { content_end = scanned_content_end func_end = scanned_func_end @@ -679,7 +680,7 @@ export class ValueNodeParser { // Parse content based on function name let child_nodes: number[] = [] - if (str_equals('style', func_name)) { + if (str_equals_range(this.source, func_start, func_name_end, 'style')) { // style(): a bare single declaration when the content has a top-level // ':' (style(--x: 1)); otherwise the full compound and/or/not grammar over // parenthesized declarations, same as `@supports` — style((--a: 1) or (--a: 2)). @@ -697,7 +698,7 @@ export class ValueNodeParser { } else { child_nodes = [decl] } - } else if (str_equals('supports', func_name)) { + } else if (str_equals_range(this.source, func_start, func_name_end, 'supports')) { // supports(): a bare single declaration when the content has a // top-level ':' (matching style()'s shorthand — supports(display: grid), no extra // parens needed); otherwise the full compound grammar, same as `@supports`'s own @@ -716,7 +717,7 @@ export class ValueNodeParser { } else { child_nodes = [decl] } - } else if (str_equals('media', func_name)) { + } else if (str_equals_range(this.source, func_start, func_name_end, 'media')) { // media()'s own parens delimit the feature; there's no separate inner paren pair, // so the feature span equals the content span (see parse_media_feature_content's docs) let feature = this.condition_parser.parse_media_feature_content(