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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"test-build": "pnpm run build && vitest run --config vitest.config.build.ts",
"build": "tsdown",
"benchmark": "pnpm run build && node --expose-gc benchmark/index.ts",
"lint": "oxlint --config .oxlintrc.json; oxfmt --check",
"lint": "oxlint --config .oxlintrc.json && oxfmt --check",
"check": "tsc --noEmit",
"knip": "knip",
"precommit": "pnpm run test --run; pnpm run lint; pnpm run check"
Expand Down
1 change: 1 addition & 0 deletions src/arena.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const OPERATOR = 16 // operator: +, -, *, /, comma
export const PARENTHESIS = 17 // parenthesized expression: (100% - 50px)
export const URL = 18 // URL: url("file.css"), url(image.png), used in values and @import
export const UNICODE_RANGE = 19 // unicode range: u+0025-00ff, u+4??
export const IF_BRANCH = 59 // Branch inside an if() function: <condition>: <value>

// Selector node type constants (for detailed selector parsing)
export const SELECTOR_LIST = 20 // comma-separated selectors
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
PARENTHESIS,
URL,
UNICODE_RANGE,
IF_BRANCH,
VALUE,
SELECTOR_LIST,
TYPE_SELECTOR,
Expand Down Expand Up @@ -67,6 +68,7 @@ export {
PARENTHESIS,
URL,
UNICODE_RANGE,
IF_BRANCH,
VALUE,
SELECTOR_LIST,
TYPE_SELECTOR,
Expand Down Expand Up @@ -117,6 +119,7 @@ export const NODE_TYPES = {
PARENTHESIS,
URL,
UNICODE_RANGE,
IF_BRANCH,
VALUE,
// Selector nodes
SELECTOR_LIST,
Expand Down
30 changes: 30 additions & 0 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
PRELUDE_SELECTORLIST,
SUPPORTS_DECLARATION,
RATIO,
IF_BRANCH,
FLAG_IMPORTANT,
FLAG_HAS_ERROR,
FLAG_HAS_BLOCK,
Expand All @@ -66,6 +67,7 @@ import {
is_whitespace,
is_vendor_prefixed,
str_starts_with,
str_equals,
} from './string-utils'
import { parse_dimension } from './parse-dimension'

Expand Down Expand Up @@ -115,6 +117,7 @@ export const TYPE_NAMES = {
[AT_RULE_PRELUDE]: 'AtrulePrelude',
[PRELUDE_SELECTORLIST]: 'PreludeSelectorList',
[RATIO]: 'Ratio',
[IF_BRANCH]: 'IfBranch',
} as const

export type TypeName = (typeof TYPE_NAMES)[keyof typeof TYPE_NAMES] | 'unknown'
Expand Down Expand Up @@ -165,6 +168,7 @@ export type CSSNodeType =
| typeof PRELUDE_SELECTORLIST
| typeof SUPPORTS_DECLARATION
| typeof RATIO
| typeof IF_BRANCH

// Options for cloning nodes
export interface CloneOptions {
Expand Down Expand Up @@ -199,6 +203,10 @@ export type PlainCSSNode = {
left?: PlainCSSNode
right?: PlainCSSNode

// IfBranch-specific
condition?: PlainCSSNode
is_else?: boolean

// Flags (only when true)
is_important?: boolean
is_vendor_prefixed?: boolean
Expand Down Expand Up @@ -253,6 +261,7 @@ const nodes_with_children = new Set<number>([
FEATURE_RANGE,
SUPPORTS_QUERY,
SUPPORTS_DECLARATION,
IF_BRANCH,
])

const enumerable_properties = [
Expand All @@ -266,6 +275,8 @@ const enumerable_properties = [
'nth_a',
'nth_b',
'selector',
'condition',
'is_else',
'is_browserhack',
'is_vendor_prefixed',
'has_error',
Expand Down Expand Up @@ -380,6 +391,11 @@ export class CSSNode {
return first_child?.first_child ?? null
}

if (type === IF_BRANCH) {
// First child is the condition node; second child (if any) is the VALUE wrapper
return first_child?.next_sibling ?? null
}

if (type === DIMENSION) {
return parse_dimension(text).value
}
Expand Down Expand Up @@ -518,6 +534,20 @@ export class CSSNode {
return this.first_child?.next_sibling ?? undefined
}

/** Get the parsed condition node of an if() branch, e.g. the Function "style(--active: 1)" or the Identifier "else" */
get condition(): CSSNode | undefined {
if (this.type !== IF_BRANCH) {
return undefined
}
return this.first_child ?? undefined
}

/** True when this is the else branch of an if() function */
get is_else(): boolean | undefined {
if (this.type !== IF_BRANCH) return undefined
return str_equals('else', this.get_content())
}

/** Check if this declaration has !important */
get is_important(): boolean | undefined {
if (this.type !== DECLARATION) return undefined
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export {
type Parenthesis,
type Url,
type UnicodeRange,
type IfBranch,
type Value,
type SelectorNode,
type TypeSelector,
Expand Down Expand Up @@ -103,6 +104,7 @@ export {
is_parenthesis,
is_url,
is_unicode_range,
is_if_branch,
is_value,
is_type_selector,
is_class_selector,
Expand Down
45 changes: 44 additions & 1 deletion src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
PARENTHESIS,
URL,
UNICODE_RANGE,
IF_BRANCH,
VALUE,
SELECTOR_LIST,
TYPE_SELECTOR,
Expand Down Expand Up @@ -245,6 +246,7 @@ export type Raw = Leaf<typeof RAW, 'Raw'>

type ValueLike =
| Function
| IfBranch
| Identifier
| Operator
| Parenthesis
Expand Down Expand Up @@ -290,7 +292,19 @@ export type Hash = Leaf<typeof HASH, 'Hash'>

export type Function = WithClone<
CSSNode &
WithChildren<ValueLike> & {
// if()-conditions reuse the shared condition parser (parse-condition.ts), so `style()`/
// `supports()` hold SupportsDeclaration/SupportsQuery/PreludeOperator children (matching
// `@supports`'s own shape, including the full compound and/or/not grammar for
// `supports()`) and `media()` holds a MediaFeature or FeatureRange child (matching
// `@media`'s own shape, including range comparison syntax) — see parse_if_condition_function
WithChildren<
| ValueLike
| MediaFeature
| SupportsDeclaration
| SupportsQuery
| FeatureRange
| PreludeOperator
> & {
readonly type: typeof FUNCTION
readonly type_name: 'Function'
/** Function name, e.g. "rgb", "calc" */
Expand Down Expand Up @@ -328,6 +342,31 @@ export type Value = WithClone<
CSSNode & WithChildren<ValueLike> & { readonly type: typeof VALUE; readonly type_name: 'Value' }
>

/**
* One branch inside a CSS `if()` inline conditional function.
*
* Each branch corresponds to a `<condition>: <value>` pair in:
* `if( <condition>: <value>; … else: <fallback> )`
*
* - `condition` — the parsed condition node (Function, e.g. `style(--x: 1)`, or Identifier `else`)
* - `value` — the value text, e.g. `"green"`; `null` when omitted
* - `is_else` — `true` for the `else` branch
* - `first_child` — same node as `condition`
* - `children` — condition node followed by parsed value nodes
*/
export type IfBranch = CSSNode &
WithChildren<Function | Identifier | Value> & {
readonly type: typeof IF_BRANCH
readonly type_name: 'IfBranch'
/** The parsed condition node, e.g. the Function "style(--active: 1)" or the Identifier "else" */
readonly condition: Function | Identifier
/** The parsed value as a VALUE node, or null when the branch value is empty */
readonly value: Value | null
/** True when this is the else branch */
readonly is_else: boolean
clone(options?: CloneOptions): ToPlain<IfBranch>
}

// ---------------------------------------------------------------------------
// Selector nodes
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -598,6 +637,7 @@ export type AnyNode =
| Parenthesis
| Url
| UnicodeRange
| IfBranch
| Value
| TypeSelector
| ClassSelector
Expand Down Expand Up @@ -688,6 +728,9 @@ export function is_url(node: CSSNode): node is Url {
export function is_unicode_range(node: CSSNode): node is UnicodeRange {
return node.type === UNICODE_RANGE
}
export function is_if_branch(node: CSSNode): node is IfBranch {
return node.type === IF_BRANCH
}
export function is_value(node: CSSNode): node is Value {
return node.type === VALUE
}
Expand Down
25 changes: 12 additions & 13 deletions src/parse-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ import {
CHAR_EQUALS,
CHAR_FORWARD_SLASH,
} from './string-utils'
import { trim_boundaries, skip_whitespace_and_comments_forward } from './parse-utils'
import {
trim_boundaries,
skip_whitespace_and_comments_forward,
find_colon_at_depth_zero,
} from './parse-utils'
import { SelectorParser } from './parse-selector'
import type { ValueNodeParser } from './value-node-parser'

Expand Down Expand Up @@ -308,6 +312,12 @@ export class ConditionParser {
content_start: number,
content_end: number,
): number {
// parse_feature_range() below tokenizes via this.next_token(), which is bounded by
// this.end — set it here so a direct call (bypassing parse_media_feature(), which would
// otherwise set it) doesn't leave it stale. Never widens: content_end is always within
// whatever bound the caller already established.
this.end = content_end

// Check for range syntax (has comparison operators)
let has_comparison = false
let i = content_start
Expand Down Expand Up @@ -448,22 +458,11 @@ export class ConditionParser {
* `@import … supports(…)`. Returns null if no top-level ':' is found.
*/
parse_supports_declaration_content(content_start: number, content_end: number): number | null {
let colon_pos = this.find_colon_at_depth_zero(content_start, content_end)
let colon_pos = find_colon_at_depth_zero(this.source, content_start, content_end)
if (colon_pos === -1) return null
return this.create_supports_declaration(content_start, content_end, colon_pos)
}

private find_colon_at_depth_zero(start: number, end: number): number {
let depth = 0
for (let i = start; i < end; i++) {
let ch = this.source.charCodeAt(i)
if (ch === 0x28 /* ( */) depth++
else if (ch === 0x29 /* ) */) depth--
else if (ch === CHAR_COLON && depth === 0) return i
}
return -1
}

/**
* Parse a `<supports-condition>` — the compound and/or/not grammar shared by `@supports`
* preludes and if()'s `supports(...)` condition function: parenthesized `(property: value)`
Expand Down
21 changes: 20 additions & 1 deletion src/parse-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { CHAR_ASTERISK, CHAR_FORWARD_SLASH, is_whitespace } from './string-utils'
import {
CHAR_ASTERISK,
CHAR_COLON,
CHAR_FORWARD_SLASH,
CHAR_LEFT_PAREN,
CHAR_RIGHT_PAREN,
is_whitespace,
} from './string-utils'

/** @internal */
export function skip_whitespace_forward(source: string, pos: number, end: number): number {
Expand Down Expand Up @@ -101,3 +108,15 @@ export function trim_boundaries(
if (start >= end) return null
return [start, end]
}

/** Find the position of the first ':' at parenthesis depth 0 in [start, end). Returns -1 if not found. @internal */
export function find_colon_at_depth_zero(source: string, start: number, end: number): number {
let depth = 0
for (let i = start; i < end; i++) {
let ch = source.charCodeAt(i)
if (ch === CHAR_LEFT_PAREN) depth++
else if (ch === CHAR_RIGHT_PAREN) depth--
else if (ch === CHAR_COLON && depth === 0) return i
}
return -1
}
Loading
Loading