diff --git a/README.md b/README.md index 3afada5..01a3ead 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Heavily influenced by [CSSTree](https://github.com/csstree/csstree). Some parsin - **Full modern CSS** — CSS Nesting, `:is()`, `:where()`, `:has()`, `@layer`, `@container` - **Error-tolerant** — keeps parsing through malformed input; one bad rule won't kill the file - **Precise source locations** — line, column, offset, and length on every node -- **Tiny footprint** — tree-shakeable subparsers; as small as 278 bytes gzipped, < 16 kB kB worst case +- **Tiny footprint** — tree-shakeable subparsers; as small as 278 bytes gzipped, < 16 kB worst case ## Installation @@ -65,34 +65,30 @@ for (const rule of ast) { ## API -Everything is exported from the package root. Each part is also available from its own entry point, so you can import only what you need and skip evaluating the rest of the package. Node properties and return types ship as TypeScript types, so this section sticks to short examples. +Everything is exported from the package root. Each part is also available from its own entry point, so you can import only what you need. Node properties and return types ship as TypeScript types. ### Entry points -| Import | Exports | -| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | -| `@projectwallace/css-parser` | Everything below, except the `Lexer` and `Parser` classes | -| `@projectwallace/css-parser/parse` | `parse`, `Parser`, `ParserOptions` | -| `@projectwallace/css-parser/parse-selector` | `parse_selector`, `parse_selector_list` | -| `@projectwallace/css-parser/parse-atrule-prelude` | `parse_atrule_prelude` | -| `@projectwallace/css-parser/parse-declaration` | `parse_declaration` | -| `@projectwallace/css-parser/parse-value` | `parse_value` | -| `@projectwallace/css-parser/parse-anplusb` | `parse_anplusb` | -| `@projectwallace/css-parser/parse-dimension` | `parse_dimension` | -| `@projectwallace/css-parser/tokenizer` | `tokenize`, `Lexer` | -| `@projectwallace/css-parser/walker` | `walk`, `traverse`, `SKIP`, `BREAK` | -| `@projectwallace/css-parser/nodes` | Node type constants (`STYLE_RULE`, `DECLARATION`, …), `NODE_TYPES` | -| `@projectwallace/css-parser/node-types` | Type guards (`is_declaration`, `is_rule`, …) and node types (`CSSNode`, `Rule`, `Declaration`, …) | -| `@projectwallace/css-parser/string-utils` | `is_custom`, `is_vendor_prefixed`, `strip_vendor_prefix`, `str_equals`, `str_starts_with`, `str_index_of` | -| `@projectwallace/css-parser/token-types` | Token type constants (`TOKEN_*`) | +- `@projectwallace/css-parser` — everything below, except the `Lexer` and `Parser` classes +- `/parse` — `parse`, `Parser`, `ParserOptions` +- `/parse-selector` — `parse_selector`, `parse_selector_list` +- `/parse-atrule-prelude` — `parse_atrule_prelude` +- `/parse-declaration` — `parse_declaration` +- `/parse-value` — `parse_value` +- `/parse-anplusb` — `parse_anplusb` +- `/parse-dimension` — `parse_dimension` +- `/tokenizer` — `tokenize`, `Lexer` +- `/walker` — `walk`, `traverse`, `SKIP`, `BREAK` +- `/nodes` — node type constants (`STYLE_RULE`, `DECLARATION`, …), `NODE_TYPES` +- `/node-types` — type guards (`is_declaration`, `is_rule`, …) and node types (`CSSNode`, `Rule`, …) +- `/string-utils` — `is_custom`, `is_vendor_prefixed`, `strip_vendor_prefix`, `str_equals`, `str_starts_with`, `str_index_of` +- `/token-types` — token type constants (`TOKEN_*`) ### `parse(source, options?)` -Parses a full stylesheet and returns the root `StyleSheet` node. Nodes are iterable and expose `first_child`, `next_sibling`, `children`, `text`, `line`, `column`, `start`, `end` and `length`. +Returns the root `StyleSheet` node. Nodes are iterable and expose `first_child`, `next_sibling`, `children`, `text`, `line`, `column`, `start`, `end` and `length`. ```typescript -import { parse } from '@projectwallace/css-parser' - const ast = parse('body { color: red; }') const rule = ast.first_child rule.block.first_child.property // "color" @@ -104,72 +100,26 @@ Options: - `parse_values` (default `true`) — parse declaration values into nodes - `parse_selectors` (default `true`) — parse selectors into nodes - `parse_atrule_preludes` (default `true`) — parse at-rule preludes into nodes -- `on_comment` — called for every comment; comments are never part of the tree - -```typescript -const comments = [] -parse('/* hi */ a { color: red }', { - parse_values: false, - on_comment: (info) => comments.push(info), // { start, end, length, line, column } -}) -``` - -### `parse_selector(source)` and `parse_selector_list(source)` +- `on_comment` — called for every comment (`{ start, end, length, line, column }`); comments are never part of the tree -`parse_selector` returns the first selector, `parse_selector_list` the whole `SelectorList`. +### Sub-parsers ```typescript -import { parse_selector, parse_selector_list } from '@projectwallace/css-parser' - parse_selector('a.b > c').children.map((part) => part.text) // ["a", ".b", ">", "c"] parse_selector_list('h1, h2').children.map((s) => s.text) // ["h1", "h2"] -``` - -### `parse_declaration(source)` - -```typescript -import { parse_declaration } from '@projectwallace/css-parser' const decl = parse_declaration('color: red !important') decl.property // "color" decl.is_important // true -decl.value.text // "red" -``` - -### `parse_value(source)` - -Parses a declaration value into a `Value` node with one child per component. - -```typescript -import { parse_value } from '@projectwallace/css-parser' for (const node of parse_value('10px 20px')) { - console.log(node.type_name, node.value, node.unit) // "Dimension" 10 "px", then "Dimension" 20 "px" + node.type_name // "Dimension" } -``` - -### `parse_atrule_prelude(name, prelude)` - -Returns an array of nodes for the prelude of the at-rule called `name` (without the `@`). - -```typescript -import { parse_atrule_prelude } from '@projectwallace/css-parser' -const [query] = parse_atrule_prelude('media', '(min-width: 768px)') +const [query] = parse_atrule_prelude('media', '(min-width: 768px)') // name without the `@` query.type_name // "MediaQuery" -``` - -### `parse_dimension(text)` - -Splits a number from its unit. It works for anything shaped like a number followed by a unit: lengths (`100px`), percentages (`2%`), durations (`200ms`) and so on. It does not validate; text without a leading number gives `value: 0`. -```typescript -import { parse_dimension } from '@projectwallace/css-parser' - -parse_dimension('100px') // { value: 100, unit: "px" } -parse_dimension('2%') // { value: 2, unit: "%" } -parse_dimension('1.5e3s') // { value: 1500, unit: "s" } -parse_dimension('5') // { value: 5, unit: "" } +parse_dimension('1.5e3s') // { value: 1500, unit: "s" } (no validation; no leading number gives `value: 0`) ``` ### `walk(node, callback)` and `traverse(node, { enter, leave })` @@ -177,28 +127,13 @@ parse_dimension('5') // { value: 5, unit: "" } Both walk the tree depth-first. Return `SKIP` to skip the children of a node and `BREAK` to stop entirely. `walk` also passes a `depth` that counts rule and at-rule nesting. `traverse` calls `enter` before and `leave` after the children (`leave` still runs after `SKIP`, but not after `BREAK`). ```typescript -import { - parse, - walk, - traverse, - SKIP, - BREAK, - DECLARATION, - AT_RULE, -} from '@projectwallace/css-parser' - -const ast = parse('@media screen { a { color: red; margin: 0 } }') - walk(ast, (node) => { - if (node.type === DECLARATION) { - console.log(node.property) // "color" - return BREAK - } + if (node.type === DECLARATION) return BREAK }) traverse(ast, { enter(node) { - if (node.type === AT_RULE) return SKIP // leave() still runs for this node + if (node.type === AT_RULE) return SKIP }, leave(node) {}, }) @@ -209,8 +144,6 @@ traverse(ast, { A generator of tokens; comments are skipped and reported through `on_comment`. ```typescript -import { tokenize } from '@projectwallace/css-parser' - for (const token of tokenize('a { color: red }')) { console.log(token.type, token.start, token.end) } @@ -218,31 +151,19 @@ for (const token of tokenize('a { color: red }')) { ### Node types, type guards and `clone()` -Node type constants (`STYLE_RULE`, `DECLARATION`, `DIMENSION`, …) and `NODE_TYPES` are exported from the root and from `@projectwallace/css-parser/nodes`. Type guards such as `is_declaration()` and `is_dimension()` that narrow a node to its specific type are exported from the root and from `@projectwallace/css-parser/node-types`. `node.clone({ deep?, locations? })` returns a mutable plain-object copy of a node. +Type guards such as `is_declaration()` narrow a node to its specific type. `node.clone({ deep?, locations? })` returns a mutable plain-object copy of a node. ```typescript -import { parse, is_declaration } from '@projectwallace/css-parser' - const decl = parse('a { color: red }').first_child.block.first_child is_declaration(decl) // true decl.type_name // "Declaration" -decl.clone({ deep: false }).property // "color" ``` ### String helpers -Allocation-free helpers, handy when working with node names. Also available from `@projectwallace/css-parser/string-utils`; token type constants (`TOKEN_*`) are available from `@projectwallace/css-parser/token-types`. The lowercase argument is always the first one. +Allocation-free helpers, handy when working with node names. The lowercase argument is always the first one. ```typescript -import { - is_custom, - is_vendor_prefixed, - strip_vendor_prefix, - str_equals, - str_starts_with, - str_index_of, -} from '@projectwallace/css-parser' - is_custom('--primary') // true is_vendor_prefixed('-webkit-transform') // true strip_vendor_prefix('-webkit-transform') // "transform" diff --git a/src/arena.ts b/src/arena.ts index 3eff134..b2f016a 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -372,7 +372,7 @@ export class CSSDataArena { } } - /** Shrink the buffer to the live node count, releasing wasted capacity. Call once after parsing; no-op if already tight. */ + // 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 const byte_count = this.count * BYTES_PER_NODE diff --git a/src/css-node.ts b/src/css-node.ts index 6d2bcff..03ffa3c 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -317,12 +317,12 @@ export class CSSNode { return this.source.substring(start, start + length) } - /** Get node type as number (for performance) */ + // Get node type as number (for performance) get type(): CSSNodeType { return this.arena.get_type(this.index) as CSSNodeType } - /** Get node type as human-readable string */ + // Get node type as human-readable string get type_name(): TypeName { // A switch compiles to a jump table, which is faster than an object lookup switch (this.type) { @@ -423,14 +423,14 @@ export class CSSNode { } } - /** Get the full text of this node from source */ + // Get the full text of this node from source get text(): string { const start = this.arena.get_start_offset(this.index) const length = this.arena.get_length(this.index) return this.source.substring(start, start + length) } - /** Get the "content" text (at-rule name for at-rules, layer name for import layers) */ + // Get the "content" text (at-rule name for at-rules, layer name for import layers) get name(): string | null | undefined { if (!nodes_with_name.has(this.type)) return const { type } = this @@ -440,7 +440,7 @@ export class CSSNode { return content } - /** Namespace prefix for type/universal selectors: null (none), '' (`|div`), 'ns' (`ns|div`), or '*' (`*|div`). */ + // Namespace prefix for type/universal selectors: null (none), '' (`|div`), 'ns' (`ns|div`), or '*' (`*|div`). get namespace(): string | null | undefined { const { type } = this if (type !== TYPE_SELECTOR && type !== UNIVERSAL_SELECTOR) return undefined @@ -450,17 +450,15 @@ export class CSSNode { return this.source.substring(start, start + length) } - /** Alias for `name` on declarations ("color" in "color: blue"), more semantic than `name` there. */ + // Alias for `name` on declarations ("color" in "color: blue"), more semantic than `name` there. get property(): string | undefined { const { type } = this if (type !== DECLARATION && type !== MEDIA_FEATURE && type !== SUPPORTS_DECLARATION) return return this.get_content() } - /** - * 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. - */ + // 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 { const { type, text, first_child } = this @@ -544,7 +542,7 @@ export class CSSNode { return this.source.substring(start, start + length) } - /** At-rules: AT_RULE_PRELUDE wrapper (media queries, layer names, …). Style rules: SELECTOR_LIST/SELECTOR. Null if none. */ + // 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) { const first = this.first_child @@ -560,7 +558,7 @@ export class CSSNode { return undefined } - /** Attribute selector operator (=, ~=, |=, ^=, $=, *=), or null for the bare `[attr]` form. */ + // Attribute selector operator (=, ~=, |=, ^=, $=, *=), or null for the bare `[attr]` form. get attr_operator(): string | null | undefined { if (this.type !== ATTRIBUTE_SELECTOR) return undefined @@ -587,7 +585,7 @@ export class CSSNode { return null } - /** Attribute selector flag: "i" (case-insensitive), "s" (case-sensitive), or null if absent. */ + // 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 @@ -611,26 +609,26 @@ export class CSSNode { return null } - /** Get the unit for dimension nodes (e.g., "px" from "100px", "%" from "50%") */ + // Get the unit for dimension nodes (e.g., "px" from "100px", "%" from "50%") get unit(): string | undefined { if (this.type !== DIMENSION) return undefined const text = this.text return text.substring(dimension_number_end(text)) } - /** Numerator for ratio values, e.g. the Number "16" in `aspect-ratio: 16/9` */ + // Numerator for ratio values, e.g. the Number "16" in `aspect-ratio: 16/9` get left(): CSSNode | undefined { if (this.type !== RATIO) return undefined return this.first_child ?? undefined } - /** Denominator for ratio values, e.g. the Number "9" in `aspect-ratio: 16/9` */ + // Denominator for ratio values, e.g. the Number "9" in `aspect-ratio: 16/9` get right(): CSSNode | undefined { if (this.type !== RATIO) return undefined 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 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 @@ -638,25 +636,25 @@ export class CSSNode { return this.first_child ?? undefined } - /** True when this is the else branch of an if() function */ + // 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 */ + // Check if this declaration has !important get is_important(): boolean | undefined { if (this.type !== DECLARATION) return undefined return this.arena.has_flag(this.index, FLAG_IMPORTANT) } - /** Check if this declaration has a browser hack prefix */ + // Check if this declaration has a browser hack prefix get is_browserhack(): boolean | undefined { if (this.type !== DECLARATION) return undefined return this.arena.has_flag(this.index, FLAG_BROWSERHACK) } - /** Check if this has a vendor prefix (computed on-demand) */ + // Check if this has a vendor prefix (computed on-demand) get is_vendor_prefixed(): boolean { switch (this.type) { case DECLARATION: @@ -683,12 +681,12 @@ export class CSSNode { } } - /** Check if this node has an error */ + // Check if this node has an error get has_error(): boolean { return this.arena.has_flag(this.index, FLAG_HAS_ERROR) } - /** Check if this node has a prelude (at-rules and style rules) */ + // Check if this node has a prelude (at-rules and style rules) get has_prelude(): boolean { const { type } = this if (type === AT_RULE) { @@ -700,19 +698,19 @@ export class CSSNode { return false } - /** Check if this rule has a block { } */ + // Check if this rule has a block { } get has_block(): boolean { return this.arena.has_flag(this.index, FLAG_HAS_BLOCK) } - /** Check if this style rule has declarations */ + // Check if this style rule has declarations get has_declarations(): boolean | undefined { const { type } = this if (type !== AT_RULE && type !== STYLE_RULE) return undefined return this.arena.has_flag(this.index, FLAG_HAS_DECLARATIONS) } - /** Get the block node (for style rules and at-rules with blocks) */ + // Get the block node (for style rules and at-rules with blocks) get block(): CSSNode | null { const { type } = this // For StyleRule: block is sibling after selector list @@ -743,7 +741,7 @@ export class CSSNode { return null } - /** Check if this block is empty (no declarations or rules, only comments allowed) */ + // Check if this block is empty (no declarations or rules, only comments allowed) get is_empty(): boolean | undefined { // Only valid on block nodes if (this.type !== BLOCK) return undefined @@ -759,57 +757,55 @@ export class CSSNode { return true } - /** Get start line number */ + // Get start line number get line(): number { return this.arena.get_start_line(this.index) } - /** Get start column number */ + // Get start column number get column(): number { return this.arena.get_start_column(this.index) } - /** Get start offset in source */ + // Get start offset in source get start(): number { return this.arena.get_start_offset(this.index) } - /** Get length in source */ + // Get length in source get length(): number { return this.arena.get_length(this.index) } - /** - * Get end offset in source - * End is not stored, must be calculated - */ + // Get end offset in source + // End is not stored, must be calculated get end(): number { return this.start + this.length } // --- Tree Traversal --- - /** Get first child node */ + // Get first child node get first_child(): CSSNode | null { const child_index = this.arena.get_first_child(this.index) if (child_index === 0) return null return new CSSNode(this.arena, this.source, child_index) } - /** Get next sibling node */ + // Get next sibling node get next_sibling(): CSSNode | null { const sibling_index = this.arena.get_next_sibling(this.index) if (sibling_index === 0) return null return new CSSNode(this.arena, this.source, sibling_index) } - /** Check if this node has a next sibling */ + // Check if this node has a next sibling get has_next(): boolean { const sibling_index = this.arena.get_next_sibling(this.index) return sibling_index !== 0 } - /** Whether this node has children. For pseudo-class/element functions, tracks FLAG_HAS_PARENS so formatters can tell `: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 { const { type } = this // For pseudo-class/pseudo-element nodes, check if they have function syntax @@ -823,7 +819,7 @@ export class CSSNode { return this.arena.has_children(this.index) } - /** Count children without allocating an intermediate array */ + // Count children without allocating an intermediate array get child_count(): number { let count = 0 let child_index = this.arena.get_first_child(this.index) @@ -834,7 +830,7 @@ export class CSSNode { return count } - /** Get all children as an array */ + // Get all children as an array get children(): CSSNode[] | undefined { const result: CSSNode[] = [] let child = this.first_child @@ -845,7 +841,7 @@ export class CSSNode { return result } - /** Make CSSNode iterable over its children */ + // Make CSSNode iterable over its children *[Symbol.iterator](): Iterator { let child = this.first_child while (child) { @@ -856,7 +852,7 @@ export class CSSNode { // --- An+B Expression Helpers (for NODE_SELECTOR_NTH) --- - /** Get the 'a' coefficient from An+B expression (e.g., "2n" from "2n+1", "odd" from "odd") */ + // Get the 'a' coefficient from An+B expression (e.g., "2n" from "2n+1", "odd" from "odd") get nth_a(): string | null | undefined { const { type, arena, index } = this if (type !== NTH_SELECTOR && type !== NTH_OF_SELECTOR) return undefined @@ -867,7 +863,7 @@ export class CSSNode { return this.source.substring(start, start + len) } - /** Get the 'b' coefficient from An+B expression (e.g., "+1" from "2n+1") */ + // Get the 'b' coefficient from An+B expression (e.g., "+1" from "2n+1") get nth_b(): string | null | undefined { const { type, arena, index, source } = this if (type !== NTH_SELECTOR && type !== NTH_OF_SELECTOR) return undefined @@ -900,13 +896,13 @@ export class CSSNode { // --- Pseudo-Class Nth-Of Helpers (for NODE_SELECTOR_NTH_OF) --- - /** Get the An+B formula node from :nth-child(2n+1 of .foo) */ + // Get the An+B formula node from :nth-child(2n+1 of .foo) get nth(): CSSNode | undefined { if (this.type !== NTH_OF_SELECTOR) return undefined return this.first_child ?? undefined // First child is always NODE_SELECTOR_NTH } - /** Get the selector list from :nth-child(2n+1 of .foo) */ + // Get the selector list from :nth-child(2n+1 of .foo) get selector(): CSSNode | undefined { if (this.type !== NTH_OF_SELECTOR) return undefined const first = this.first_child @@ -915,10 +911,8 @@ export class CSSNode { // --- Node Cloning --- - /** - * Clone this node as a mutable plain object with children as arrays. - * Use sparingly — can consume a lot of memory. - */ + // 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 const { type } = this diff --git a/src/node-types.ts b/src/node-types.ts index 40c7e4b..5c8aa5b 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -349,14 +349,7 @@ export type Value = WithClone< CSSNode & WithChildren & { readonly type: typeof VALUE; readonly type_name: 'Value' } > -/** - * A compound `if()`-branch condition combining multiple tests with `not`/`and`/`or`, - * e.g. `not style(--scheme: light)` or `style(--a: 1) and media(width > 600px)`. - * - * Children are the flat sequence of PreludeOperator ("not"/"and"/"or") and - * Function (`style()`/`media()`/`supports()`) nodes, in source order — the same - * shape `@supports`'s own compound condition uses. - */ +/** Compound `if()` condition: a flat sequence of `not`/`and`/`or` operators and `style()`/`media()`/`supports()` functions, e.g. `not style(--a: 1)`. */ export type IfCondition = WithClone< CSSNode & WithChildren & { @@ -365,29 +358,15 @@ export type IfCondition = WithClone< } > -/** - * One branch inside a CSS `if()` inline conditional function. - * - * Each branch corresponds to a `: ` pair in: - * `if( : ; … else: )` - * - * - `condition` — the parsed condition node: a Function (e.g. `style(--x: 1)`), an - * Identifier (`else`), or an IfCondition for compound `not`/`and`/`or` conditions - * (e.g. `not style(--x: 1)`) - * - `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 - */ +/** A `: ` branch of an `if()` function. `children` is the condition followed by the parsed value nodes. */ export type IfBranch = CSSNode & WithChildren & { readonly type: typeof IF_BRANCH readonly type_name: 'IfBranch' - /** The parsed condition node, e.g. the Function "style(--active: 1)", the Identifier "else", or an IfCondition for compound conditions */ + /** Function (`style(--x: 1)`), Identifier (`else`) or IfCondition */ readonly condition: Function | Identifier | IfCondition - /** The parsed value as a VALUE node, or null when the branch value is empty */ + /** Branch value, or null when empty */ readonly value: Value | null - /** True when this is the else branch */ readonly is_else: boolean clone(options?: CloneOptions): ToPlain } diff --git a/src/parse-anplusb.ts b/src/parse-anplusb.ts index 03fadca..e6b1679 100644 --- a/src/parse-anplusb.ts +++ b/src/parse-anplusb.ts @@ -31,10 +31,8 @@ export class ANplusBParser { this.expr_end = 0 } - /** - * Parse An+B expression - * Examples: odd, even, 3, n, -n, 2n, 2n+1, -3n-5 - */ + // Parse An+B expression + // Examples: odd, even, 3, n, -n, 2n, 2n+1, -3n-5 parse_anplusb(start: number, end: number, line: number = 1): number | null { this.expr_end = end this.lexer.seek(start, line) @@ -238,10 +236,8 @@ export class ANplusBParser { return null } - /** - * Parse the b part after 'n' - * Handles: +5, -3, whitespace variations - */ + // Parse the b part after 'n' + // Handles: +5, -3, whitespace variations private parse_b_part(): string | null { this.skip_whitespace() diff --git a/src/tokenize.ts b/src/tokenize.ts index 4185f4d..d7ad26d 100644 --- a/src/tokenize.ts +++ b/src/tokenize.ts @@ -418,18 +418,16 @@ export class Lexer { } } - /** - * Fast-forward to the next `target` char that isn't inside a string or comment, without - * fully tokenizing what's in between — used by callers that only need to know *where a - * construct ends* (e.g. a selector's `{`), since the sub-parser for that construct - * re-tokenizes the span properly afterward. - * - * Strings go through consume_string and comments through _skip_comment, so escapes and - * on_comment callbacks behave identically to real tokenization. Ordinary characters cost - * one SKIP_SCAN_INTERESTING lookup each, same as consume_ident_or_function's inner loop. - * - * Returns true with this.pos at target, or false with this.pos at source_length. - */ + // Fast-forward to the next `target` char that isn't inside a string or comment, without + // fully tokenizing what's in between — used by callers that only need to know *where a + // construct ends* (e.g. a selector's `{`), since the sub-parser for that construct + // re-tokenizes the span properly afterward. + // + // Strings go through consume_string and comments through _skip_comment, so escapes and + // on_comment callbacks behave identically to real tokenization. Ordinary characters cost + // one SKIP_SCAN_INTERESTING lookup each, same as consume_ident_or_function's inner loop. + // + // Returns true with this.pos at target, or false with this.pos at source_length. skip_to_unquoted(target: number): boolean { const source = this.source const source_length = source.length @@ -484,13 +482,11 @@ export class Lexer { return false } - /** - * Same approach as skip_to_unquoted, but stops at any of `; { } ( ) !` — the stop - * characters parse-declaration.ts's value scan needs (paren depth, statement end, - * !important). The caller decides what each one means; this only locates the next one. - * - * Returns the stop character's code (this.pos left at it), or 0 at `end` if none found. - */ + // Same approach as skip_to_unquoted, but stops at any of `; { } ( ) !` — the stop + // characters parse-declaration.ts's value scan needs (paren depth, statement end, + // !important). The caller decides what each one means; this only locates the next one. + // + // Returns the stop character's code (this.pos left at it), or 0 at `end` if none found. skip_to_declaration_stop(end: number): number { const source = this.source let pos = this.pos @@ -973,10 +969,8 @@ export class Lexer { return type } - /** - * Save complete lexer state for backtracking - * @returns Object containing all lexer state - */ + // Save complete lexer state for backtracking + // @returns Object containing all lexer state save_position(): LexerPosition { return { pos: this.pos, @@ -1005,10 +999,8 @@ export class Lexer { return target } - /** - * Restore lexer state from saved position - * @param saved The saved position to restore - */ + // Restore lexer state from saved position + // @param saved The saved position to restore restore_position(saved: LexerPosition): void { this.pos = saved.pos this._line = saved.line @@ -1020,10 +1012,8 @@ export class Lexer { this.token_column = saved.token_column } - /** - * Skip whitespace and comments within a range, maintaining line/column tracking - * @param end The end boundary (exclusive) - */ + // Skip whitespace and comments within a range, maintaining line/column tracking + // @param end The end boundary (exclusive) skip_whitespace_in_range(end: number): void { while (this.pos < end) { const ch = this.source.charCodeAt(this.pos)