From 69b2713af303fef4c96ea0f0789c355cff065ad0 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 19 Jul 2026 13:33:13 +0200 Subject: [PATCH 1/2] perf: make comments terser --- src/arena.ts | 8 +---- src/css-node.ts | 54 ++++++------------------------- src/node-types.ts | 55 +++++++------------------------- src/parse-dimension.ts | 13 +------- src/parse-utils.ts | 44 +++----------------------- src/string-utils.ts | 72 +++--------------------------------------- src/walk.ts | 18 ++++------- 7 files changed, 39 insertions(+), 225 deletions(-) diff --git a/src/arena.ts b/src/arena.ts index d136729..81092b4 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -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 diff --git a/src/css-node.ts b/src/css-node.ts index 376b019..7b67c78 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -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 @@ -346,10 +340,7 @@ 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 @@ -357,11 +348,8 @@ export class CSSNode { } /** - * 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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. See API.md. + * Use sparingly — can consume a lot of memory. */ clone(options: CloneOptions = {}): PlainCSSNode { const { deep = true, locations = false } = options diff --git a/src/node-types.ts b/src/node-types.ts index 320363d..e4c2e8a 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -110,14 +110,7 @@ type Leaf = 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 { readonly has_children: boolean readonly child_count: number @@ -127,21 +120,10 @@ export interface WithChildren { } /** - * 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 = PlainCSSNode & { type: T['type'] } & { [K in Exclude< @@ -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> @@ -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, diff --git a/src/parse-dimension.ts b/src/parse-dimension.ts index 8e1765b..b61bfbc 100644 --- a/src/parse-dimension.ts +++ b/src/parse-dimension.ts @@ -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" } */ export function parse_dimension(text: string): { value: number; unit: string } { // Find where the numeric part ends let num_end = 0 diff --git a/src/parse-utils.ts b/src/parse-utils.ts index aa3b14c..21ed8ef 100644 --- a/src/parse-utils.ts +++ b/src/parse-utils.ts @@ -1,14 +1,6 @@ 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++ @@ -16,15 +8,7 @@ export function skip_whitespace_forward(source: string, pos: number, end: number 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, @@ -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, @@ -110,19 +86,7 @@ export function skip_whitespace_and_comments_backward( return pos } -/** - * 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 - * @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. - */ +/** Trims whitespace and CSS comments from both ends; returns null if the range is only whitespace/comments. @internal */ export function trim_boundaries( source: string, start: number, diff --git a/src/string-utils.ts b/src/string-utils.ts index e2d7675..506c03d 100644 --- a/src/string-utils.ts +++ b/src/string-utils.ts @@ -71,15 +71,7 @@ export function str_equals(a: string, b: string): boolean { return true } -/** - * Case-insensitive ASCII prefix check without allocations - * Returns true if string `str` starts with prefix (case-insensitive) - * - * IMPORTANT: prefix MUST be lowercase for correct comparison - * - * @param str - The string to check - * @param prefix - The lowercase prefix to match against - */ +/** 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) { return false @@ -100,16 +92,7 @@ export function str_starts_with(str: string, prefix: string): boolean { return true } -/** - * Case-insensitive character/substring search without allocations - * Returns the index of the first occurrence of searchChar (case-insensitive) - * - * IMPORTANT: searchChar MUST be lowercase for correct comparison - * - * @param str - The string to search in - * @param searchChar - The lowercase character/substring to find - * @returns The index of the first match, or -1 if not found - */ +/** Case-insensitive substring search without allocations, or -1 if not found. `searchChar` MUST be lowercase. */ export function str_index_of(str: string, searchChar: string): number { if (searchChar.length === 0) { return -1 @@ -148,27 +131,7 @@ export function str_index_of(str: string, searchChar: string): number { return -1 } -/** - * Check if a string range has a vendor prefix - * - * @param source - The source string - * @param start - Start offset in source - * @param end - End offset in source - * @returns true if the range starts with a vendor prefix (-webkit-, -moz-, -ms-, -o-) - * - * Detects vendor prefixes by checking: - * 1. Starts with a single hyphen (not --) - * 2. Contains at least 3 characters (shortest is -o-) - * 3. Has a second hyphen after the vendor name - * - * Examples: - * - `-webkit-transform` → true - * - `-moz-appearance` → true - * - `-ms-filter` → true - * - `-o-border-image` → true - * - `--custom-property` → false (CSS custom property) - * - `border-radius` → false (doesn't start with hyphen) - */ +/** True if the string/range is vendor-prefixed (-webkit-, -moz-, -ms-, -o-). `--custom-property` is not. */ // Overload signatures export function is_vendor_prefixed(text: string): boolean export function is_vendor_prefixed(source: string, start: number, end: number): boolean @@ -207,39 +170,14 @@ export function is_vendor_prefixed(source: string, start?: number, end?: number) return false } -/** - * Check if a string is a CSS custom property (starts with --) - * - * @param str - The string to check - * @returns true if the string starts with -- (custom property) - * - * Examples: - * - `--primary-color` → true - * - `--my-var` → true - * - `-webkit-transform` → false (vendor prefix, not custom) - * - `border-radius` → false (standard property) - * - `color` → false - */ +/** True if the string is a CSS custom property (starts with `--`). */ export function is_custom(str: string): boolean { // Must start with two hyphens and have at least one character after if (str.length < 3) return false return str.charCodeAt(0) === CHAR_MINUS_HYPHEN && str.charCodeAt(1) === CHAR_MINUS_HYPHEN } -/** - * Strip vendor prefix from a string - * - * @param str - The string to strip vendor prefix from - * @returns The string without vendor prefix, or original string if no prefix found - * - * Examples: - * - `-webkit-keyframes` → `keyframes` - * - `-moz-appearance` → `appearance` - * - `-ms-filter` → `filter` - * - `-o-border-image` → `border-image` - * - `keyframes` → `keyframes` (no change) - * - `--custom-property` → `--custom-property` (custom property, not vendor prefix) - */ +/** Strip a vendor prefix, e.g. `-webkit-keyframes` → `keyframes`. Returns the input unchanged if it has no prefix. */ export function strip_vendor_prefix(str: string): string { if (!is_vendor_prefixed(str)) { return str diff --git a/src/walk.ts b/src/walk.ts index 001c452..37fd373 100644 --- a/src/walk.ts +++ b/src/walk.ts @@ -11,13 +11,9 @@ export const BREAK = Symbol('BREAK') type WalkCallback = (node: AnyNode, depth: number) => void | typeof SKIP | typeof BREAK /** - * Walk the AST in depth-first order, calling the callback for each node. - * Return SKIP to skip children, BREAK to stop traversal. See API.md for examples. - * - * @param node - The root node to start walking from - * @param callback - Function called for each node. Receives the node and its nesting depth. - * Depth increments only for STYLE_RULE and AT_RULE nodes (tracks rule nesting, not tree depth). - * @param depth - Starting depth (default: 0) + * Walk the AST depth-first, calling `callback` for each node. Return SKIP to skip + * children, BREAK to stop. `depth` tracks rule nesting (STYLE_RULE/AT_RULE only), + * not tree depth. See API.md for examples. */ export function walk(node: CSSNode, callback: WalkCallback, depth = 0): boolean { const result = callback(node as AnyNode, depth) @@ -77,11 +73,9 @@ interface WalkEnterLeaveOptions { } /** - * Walk the AST in depth-first order, calling enter before visiting children and leave after. - * Return SKIP in enter to skip children (leave still called), BREAK to stop (leave NOT called). See API.md for examples. - * - * @param node - The root node to start walking from - * @param options - Object with optional enter and leave callback functions + * Walk the AST depth-first, calling `enter` before children and `leave` after. + * SKIP in enter skips children (leave still runs); BREAK stops (leave doesn't run). + * See API.md for examples. */ export function traverse( node: CSSNode, From f585bc09c056c381a39578e420783113896a47fa Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 19 Jul 2026 13:39:47 +0200 Subject: [PATCH 2/2] fine tune --- src/css-node.ts | 2 +- src/parse-dimension.ts | 2 +- src/parse-utils.ts | 5 ++++- src/walk.ts | 3 +-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/css-node.ts b/src/css-node.ts index 7b67c78..e121d3f 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -766,7 +766,7 @@ export class CSSNode { // --- Node Cloning --- /** - * Clone this node as a mutable plain object with children as arrays. See API.md. + * Clone this node as a mutable plain object with children as arrays. * Use sparingly — can consume a lot of memory. */ clone(options: CloneOptions = {}): PlainCSSNode { diff --git a/src/parse-dimension.ts b/src/parse-dimension.ts index b61bfbc..cf8f47f 100644 --- a/src/parse-dimension.ts +++ b/src/parse-dimension.ts @@ -1,6 +1,6 @@ import { is_digit, CHAR_MINUS_HYPHEN, CHAR_PLUS, CHAR_PERIOD } from './string-utils' -/** Parse a dimension string into value and unit, e.g. "100px" → { value: 100, unit: "px" } */ +/** 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 diff --git a/src/parse-utils.ts b/src/parse-utils.ts index 21ed8ef..946ed7f 100644 --- a/src/parse-utils.ts +++ b/src/parse-utils.ts @@ -86,7 +86,10 @@ export function skip_whitespace_and_comments_backward( return pos } -/** Trims whitespace and CSS comments from both ends; returns null if the range is only whitespace/comments. @internal */ +/** + * Trims whitespace and CSS comments from both ends; returns null if the range is only whitespace/comments. + * @internal + */ export function trim_boundaries( source: string, start: number, diff --git a/src/walk.ts b/src/walk.ts index 37fd373..a8ac905 100644 --- a/src/walk.ts +++ b/src/walk.ts @@ -13,7 +13,7 @@ type WalkCallback = (node: AnyNode, depth: number) => void | typeof SKIP | typeo /** * Walk the AST depth-first, calling `callback` for each node. Return SKIP to skip * children, BREAK to stop. `depth` tracks rule nesting (STYLE_RULE/AT_RULE only), - * not tree depth. See API.md for examples. + * not tree depth. */ export function walk(node: CSSNode, callback: WalkCallback, depth = 0): boolean { const result = callback(node as AnyNode, depth) @@ -75,7 +75,6 @@ interface WalkEnterLeaveOptions { /** * Walk the AST depth-first, calling `enter` before children and `leave` after. * SKIP in enter skips children (leave still runs); BREAK stops (leave doesn't run). - * See API.md for examples. */ export function traverse( node: CSSNode,