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
8 changes: 1 addition & 7 deletions src/arena.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,13 +352,7 @@ export class CSSDataArena {
}
}

/**
* Shrink the buffer to exactly the number of live nodes, releasing wasted capacity.
* Call once after parsing is complete. Safe to call multiple times (no-op if already tight).
*
* @see https://doc.rust-lang.org/std/vec/struct.Vec.html#method.shrink_to_fit
* @see https://en.cppreference.com/w/cpp/container/vector/shrink_to_fit
*/
/** Shrink the buffer to the live node count, releasing wasted capacity. Call once after parsing; no-op if already tight. */
trim(): void {
if (this.count === this.capacity) return
let byte_count = this.count * BYTES_PER_NODE
Expand Down
54 changes: 10 additions & 44 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,13 +330,7 @@ export class CSSNode {
return content
}

/**
* Namespace prefix for type and universal selectors.
* - `null` — no namespace qualifier (plain `div` or `*`)
* - `''` — empty namespace (`|div` or `|*`)
* - `'ns'` — named namespace (`ns|div` or `ns|*`)
* - `'*'` — any namespace (`*|div` or `*|*`)
*/
/** Namespace prefix for type/universal selectors: null (none), '' (`|div`), 'ns' (`ns|div`), or '*' (`*|div`). */
get namespace(): string | null | undefined {
let { type } = this
if (type !== TYPE_SELECTOR && type !== UNIVERSAL_SELECTOR) return undefined
Expand All @@ -346,22 +340,16 @@ export class CSSNode {
return this.source.substring(start, start + length)
}

/**
* Alias for name (for declarations: "color" in "color: blue")
* More semantic than `name` for declaration nodes
*/
/** Alias for `name` on declarations ("color" in "color: blue"), more semantic than `name` there. */
get property(): string | undefined {
let { type } = this
if (type !== DECLARATION && type !== MEDIA_FEATURE) return
return this.get_content()
}

/**
* Get the value text (for declarations: "1px solid blue" in "border: 1px solid blue")
* For dimension/number nodes: returns the numeric value as a number
* For string nodes: returns the string content without quotes
* For URL nodes with quoted string: returns the string with quotes (consistent with STRING node)
* For URL nodes with unquoted URL: returns the URL content without quotes
* Declarations: the value text ("1px solid blue"). Dimension/number: the numeric value.
* String: content without quotes. URL: quoted string keeps quotes, unquoted URL doesn't.
*/
get value(): CSSNode | string | number | null | undefined {
let { type, text, first_child } = this
Expand Down Expand Up @@ -433,12 +421,7 @@ export class CSSNode {
return this.source.substring(start, start + length)
}

/**
* Get the prelude node:
* - For at-rules: AT_RULE_PRELUDE wrapper containing structured prelude children (media queries, layer names, etc.)
* - For style rules: SELECTOR_LIST or SELECTOR node
* Returns null if no prelude exists
*/
/** At-rules: AT_RULE_PRELUDE wrapper (media queries, layer names, …). Style rules: SELECTOR_LIST/SELECTOR. Null if none. */
get prelude(): CSSNode | null | undefined {
if (this.type === AT_RULE) {
let first = this.first_child
Expand All @@ -454,11 +437,7 @@ export class CSSNode {
return undefined
}

/**
* Get the attribute operator (for attribute selectors: =, ~=, |=, ^=, $=, *=)
* Returns the operator string, or null if no operator is present ([attr] form).
* Derived from source text between the attribute name and value.
*/
/** Attribute selector operator (=, ~=, |=, ^=, $=, *=), or null for the bare `[attr]` form. */
get attr_operator(): string | null | undefined {
if (this.type !== ATTRIBUTE_SELECTOR) return undefined

Expand All @@ -485,11 +464,7 @@ export class CSSNode {
return null
}

/**
* Get the attribute flags (for attribute selectors: i, s)
* Returns "i" (case-insensitive), "s" (case-sensitive), or null if no flag is present.
* Derived from source text after the attribute value.
*/
/** Attribute selector flag: "i" (case-insensitive), "s" (case-sensitive), or null if absent. */
get attr_flags(): string | null | undefined {
if (this.type !== ATTRIBUTE_SELECTOR) return undefined

Expand Down Expand Up @@ -684,11 +659,7 @@ export class CSSNode {
return sibling_index !== 0
}

/**
* Check if this node has children
* For pseudo-class/pseudo-element functions, returns true if FLAG_HAS_PARENS is set
* This allows formatters to distinguish :lang() from :hover
*/
/** Whether this node has children. For pseudo-class/element functions, tracks FLAG_HAS_PARENS so formatters can tell `:lang()` from `:hover`. */
get has_children(): boolean {
let { type } = this
// For pseudo-class/pseudo-element nodes, check if they have function syntax
Expand Down Expand Up @@ -795,13 +766,8 @@ export class CSSNode {
// --- Node Cloning ---

/**
* Clone this node as a mutable plain JavaScript object with children as arrays.
* See API.md for examples.
* Warning: this should be used sparingly because it potentially consumes a lot of memory.
*
* @param options - Cloning configuration
* @param options.deep - Recursively clone children (default: true)
* @param options.locations - Include line/column/start/length (default: false)
* Clone this node as a mutable plain object with children as arrays.
* Use sparingly — can consume a lot of memory.
*/
clone(options: CloneOptions = {}): PlainCSSNode {
const { deep = true, locations = false } = options
Expand Down
55 changes: 12 additions & 43 deletions src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,7 @@ type Leaf<Type extends CSSNodeType, Name extends TypeName, Extra = {}> = WithClo
CSSNode & Extra & { readonly type: Type; readonly type_name: Name }
>

/**
* Mixin for node types that have child nodes.
*
* Only a subset of node types expose children — structural and container nodes
* like StyleSheet, Block, SelectorList, Value, Function, etc. Leaf nodes
* (Identifier, Number, Dimension, …) do not extend WithChildren, reflecting
* that they never carry child nodes in a well-formed tree.
*/
/** Mixin for container node types (StyleSheet, Block, SelectorList, …). Leaf nodes never carry children, so they skip this. */
export interface WithChildren<T = AnyNode> {
readonly has_children: boolean
readonly child_count: number
Expand All @@ -127,21 +120,10 @@ export interface WithChildren<T = AnyNode> {
}

/**
* Maps a CssNodeCommon subtype interface to its plain-object equivalent,
* as returned by clone().
*
* The result is always a subtype of PlainCSSNode (the intersection starts
* with PlainCSSNode), with two additions:
* - `type` is narrowed to T's specific literal (enables discriminated unions)
* - subtype-specific properties (those not on CssNodeCommon) are added with
* CssNodeCommon references replaced by PlainCSSNode
*
* Traversal properties (first_child, next_sibling, etc.) are excluded since
* they live on CssNodeCommon and are never serialised by clone().
*
* const rule = root.first_child as Rule
* rule.clone().prelude // PlainCSSNode | null — not PlainCSSNode | undefined
* rule.clone().block // PlainCSSNode | null
* Maps a CssNodeCommon subtype to its clone() equivalent: `type` narrowed to
* T's literal, subtype-specific fields with CssNodeCommon refs replaced by
* PlainCSSNode, and traversal props (first_child, next_sibling, …) dropped
* since clone() never serialises them.
*/
export type ToPlain<T extends CSSNode> = PlainCSSNode & { type: T['type'] } & {
[K in Exclude<
Expand Down Expand Up @@ -232,12 +214,9 @@ export type SelectorList = WithClone<
>

/**
* A node that appears as a direct child of a Block.
*
* Identical to `Raw | Declaration | Atrule | Rule` except that `next_sibling`
* is narrowed to the same union instead of the generic `CSSNode`. This is
* safe because none of these four types use WithChildren themselves, so
* there is no recursive type graph to trigger TS2589.
* A direct child of a Block: `Raw | Declaration | Atrule | Rule` with
* `next_sibling` narrowed to the same union instead of generic `CSSNode`.
* Safe since none of the four use WithChildren, avoiding TS2589.
*/
export type BlockChild = (Raw | Declaration | Atrule | Rule) &
Toggle<'has_next', 'next_sibling', Raw | Declaration | Atrule | Rule>
Expand Down Expand Up @@ -539,20 +518,10 @@ export type LayerName = Leaf<
>

/**
* A parenthesised selector argument in an at-rule prelude.
*
* This node type exists because at-rule preludes that contain selectors (like
* @scope) cannot reuse SELECTOR_LIST: that type already appears inside the
* rule's block, and mixing the two would make traversal ambiguous. A distinct
* type lets walkers and tooling distinguish "this is a selector used as a
* scoping argument" from "this is a selector that matches elements".
*
* Currently produced only by @scope:
* @scope (.parent) to (.child) { }
* ^^^^^^^^^ ^^^^^^^^ — each parenthesised group is a PRELUDE_SELECTORLIST
*
* `value` is the raw selector text inside the parentheses, trimmed of
* whitespace: ".parent" from "(.parent)".
* A parenthesised selector argument in an at-rule prelude, e.g. `(.parent)`
* in `@scope (.parent) to (.child)`. Distinct from SELECTOR_LIST, which
* already means "matches elements", so tooling can tell scoping arguments
* apart from real selectors. `value` is the trimmed text inside the parens.
*/
export type PreludeSelectorList = Leaf<
typeof PRELUDE_SELECTORLIST,
Expand Down
13 changes: 1 addition & 12 deletions src/parse-dimension.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import { is_digit, CHAR_MINUS_HYPHEN, CHAR_PLUS, CHAR_PERIOD } from './string-utils'

/**
* Parse a dimension string into numeric value and unit
*
* @param text - Dimension text like "100px", "50%", "1.5em"
* @returns Object with value (number) and unit (string)
*
* Examples:
* - "100px" → { value: 100, unit: "px" }
* - "50%" → { value: 50, unit: "%" }
* - "1.5em" → { value: 1.5, unit: "em" }
* - "-10rem" → { value: -10, unit: "rem" }
*/
/** Parse a dimension string into value and unit, e.g. "100px" → { value: 100, unit: "px" } or "2%" → { value: 2, unit: "%" } */
export function parse_dimension(text: string): { value: number; unit: string } {
// Find where the numeric part ends
let num_end = 0
Expand Down
41 changes: 4 additions & 37 deletions src/parse-utils.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
import { CHAR_ASTERISK, CHAR_FORWARD_SLASH, is_whitespace } from './string-utils'

/**
* Skip whitespace forward from a position
*
* @param source - The source string
* @param pos - Starting position
* @param end - End boundary (exclusive)
* @returns New position after skipping whitespace
* @internal
*/
/** @internal */
export function skip_whitespace_forward(source: string, pos: number, end: number): number {
while (pos < end && is_whitespace(source.charCodeAt(pos))) {
pos++
}
return pos
}

/**
* Skip whitespace and comments forward from a position
*
* @param source - The source string
* @param pos - Starting position
* @param end - End boundary (exclusive)
* @returns New position after skipping whitespace/comments
* @internal
*/
/** @internal */
export function skip_whitespace_and_comments_forward(
source: string,
pos: number,
Expand Down Expand Up @@ -65,15 +49,7 @@ export function skip_whitespace_and_comments_forward(
return pos
}

/**
* Skip whitespace and comments backward from a position
*
* @param source - The source string
* @param pos - Starting position (exclusive, scanning backward from pos-1)
* @param start - Start boundary (inclusive, won't go before this)
* @returns New position after skipping whitespace/comments backward
* @internal
*/
/** @internal */
export function skip_whitespace_and_comments_backward(
source: string,
pos: number,
Expand Down Expand Up @@ -111,17 +87,8 @@ export function skip_whitespace_and_comments_backward(
}

/**
* Trim whitespace and comments from both ends of a string range
*
* @param source - The source string
* @param start - Start offset in source
* @param end - End offset in source
* @returns [trimmed_start, trimmed_end] or null if all whitespace/comments
* Trims whitespace and CSS comments from both ends; returns null if the range is only whitespace/comments.
* @internal
*
* Skips whitespace (space, tab, newline, CR, FF) and CSS comments from both ends
* of the specified range. Returns the trimmed boundaries or null if the range
* contains only whitespace and comments.
*/
export function trim_boundaries(
source: string,
Expand Down
Loading