Skip to content
Open
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
105 changes: 54 additions & 51 deletions src/parse-atrule-prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -148,20 +153,17 @@ 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
// before/after the closing ')'; matched is false if EOF was hit first, in which case
// 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
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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),
)
Expand Down
50 changes: 29 additions & 21 deletions src/parse-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -92,20 +94,17 @@ 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
// before/after the closing ')'; matched is false if EOF was hit first, in which case
// 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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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 <complex-selector> — 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,
Expand All @@ -439,7 +445,7 @@ export class ConditionParser {
}
}
// style()'s argument is a <declaration> — 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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
Expand Down
Loading
Loading