From 6d722b03a564afee208f3d554c9c23d0c53c40e7 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Thu, 15 Oct 2026 23:47:26 +0200 Subject: [PATCH] chore: use `const` instead of `let` --- .oxlintrc.json | 1 + benchmark/comparison.ts | 16 +- src/arena.ts | 16 +- src/css-node.ts | 110 ++-- src/parse-anplusb.ts | 2 +- src/parse-atrule-prelude.ts | 128 ++--- src/parse-condition.ts | 108 ++-- src/parse-declaration.ts | 30 +- src/parse-dimension.ts | 14 +- src/parse-options.test.ts | 2 +- src/parse-selector.test.ts | 24 +- src/parse-selector.ts | 130 ++--- src/parse-utils.ts | 6 +- src/parse-value.ts | 10 +- src/parse.test.ts | 1023 ++++++++++++++++++----------------- src/parse.ts | 86 +-- src/string-utils.ts | 10 +- src/tokenize.test.ts | 252 ++++----- src/tokenize.ts | 110 ++-- src/value-node-parser.ts | 136 ++--- src/walk.test.ts | 2 +- 21 files changed, 1114 insertions(+), 1102 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index a669355..05844cb 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -7,6 +7,7 @@ }, "rules": { "no-null": "off", // AST's need null + "prefer-const": "error", "jest/no-conditional-in-test": "off", "vitest/no-conditional-in-test": "off", "jest/no-conditional-expect": "off", diff --git a/benchmark/comparison.ts b/benchmark/comparison.ts index 0f64437..d61e15b 100644 --- a/benchmark/comparison.ts +++ b/benchmark/comparison.ts @@ -41,7 +41,7 @@ for (const file of files) { }) bench.add(`Parse+Walk|${file}`, () => { - let ast = parse(css) + const ast = parse(css) walk(ast, (node, _depth) => { void node.type void node.line @@ -49,7 +49,7 @@ for (const file of files) { }) bench.add(`Fair-Wallace|${file}`, () => { - let ast = parse(css, { + const ast = parse(css, { parse_selectors: false, parse_values: false, parse_atrule_preludes: false, @@ -61,7 +61,7 @@ for (const file of files) { }) bench.add(`Fair-CSSTree|${file}`, () => { - let ast = csstree.parse(css, { + const ast = csstree.parse(css, { positions: true, parseValue: false, parseAtrulePrelude: false, @@ -83,7 +83,7 @@ for (const file of files) { }) bench.add(`CSSTree|${file}`, () => { - let ast = csstree.parse(css, { positions: true }) + const ast = csstree.parse(css, { positions: true }) // @ts-expect-error: no type definitions for css-tree csstree.walk(ast, (node) => { void node.type @@ -92,7 +92,7 @@ for (const file of files) { }) bench.add(`PostCSS|${file}`, () => { - let root = postcss.parse(css) + const root = postcss.parse(css) root.walk((node) => { void node.type void node.source?.start?.line @@ -140,20 +140,20 @@ function measureMemoryMB( const before = process.memoryUsage() if (parser === 'wallace') { - let ast = parse(css) + const ast = parse(css) walk(ast, (node, _depth) => { void node.type void node.line }) } else if (parser === 'csstree') { - let ast = csstree.parse(css, { positions: true }) + const ast = csstree.parse(css, { positions: true }) // @ts-expect-error: no type definitions for css-tree csstree.walk(ast, (node) => { void node.type void node.loc?.start.line }) } else { - let root = postcss.parse(css) + const root = postcss.parse(css) root.walk((node) => { void node.type void node.source?.start?.line diff --git a/src/arena.ts b/src/arena.ts index 4885c9a..3eff134 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -38,7 +38,7 @@ // lastChild field saved another 4 bytes. startColumn was changed from Uint16 to Uint32 to avoid // overflow on long lines (common in minified CSS). -let BYTES_PER_NODE = 32 +const BYTES_PER_NODE = 32 // Node type constants export const STYLESHEET = 1 @@ -141,10 +141,10 @@ export class CSSDataArena { // Calculate recommended initial capacity based on CSS source size static capacity_for_source(source_length: number): number { - let size_in_kb = source_length / 1024 - let estimated_nodes = Math.ceil(size_in_kb * CSSDataArena.NODES_PER_KB) + const size_in_kb = source_length / 1024 + const estimated_nodes = Math.ceil(size_in_kb * CSSDataArena.NODES_PER_KB) // Add 15% buffer to avoid growth during parsing - let capacity = Math.ceil(estimated_nodes * CSSDataArena.CAPACITY_BUFFER) + const capacity = Math.ceil(estimated_nodes * CSSDataArena.CAPACITY_BUFFER) // Ensure minimum capacity of 16 nodes (even for empty stylesheets) return Math.max(16, capacity) } @@ -313,8 +313,8 @@ export class CSSDataArena { // Grow the arena by 1.3x when capacity is exceeded private grow(): void { this.growth_count++ - let new_capacity = Math.ceil(this.capacity * CSSDataArena.GROWTH_FACTOR) - let new_buffer = new ArrayBuffer(new_capacity * BYTES_PER_NODE) + const new_capacity = Math.ceil(this.capacity * CSSDataArena.GROWTH_FACTOR) + const new_buffer = new ArrayBuffer(new_capacity * BYTES_PER_NODE) // Copy existing data to new buffer new Uint8Array(new_buffer).set(new Uint8Array(this.buffer)) @@ -375,8 +375,8 @@ export class CSSDataArena { /** 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 - let new_buffer = new ArrayBuffer(byte_count) + const byte_count = this.count * BYTES_PER_NODE + const new_buffer = new ArrayBuffer(byte_count) new Uint8Array(new_buffer).set(new Uint8Array(this.buffer, 0, byte_count)) this.buffer = new_buffer this.view = new DataView(new_buffer) diff --git a/src/css-node.ts b/src/css-node.ts index 0f9056a..6bb5a17 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -314,8 +314,8 @@ export class CSSNode { } private get_content(): string { - let start = this.arena.get_content_start(this.index) - let length = this.arena.get_content_length(this.index) + const start = this.arena.get_content_start(this.index) + const length = this.arena.get_content_length(this.index) if (length === 0) return '' return this.source.substring(start, start + length) } @@ -332,34 +332,34 @@ export class CSSNode { /** Get the full text of this node from source */ get text(): string { - let start = this.arena.get_start_offset(this.index) - let length = this.arena.get_length(this.index) + 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 name(): string | null | undefined { if (!nodes_with_name.has(this.type)) return - let { type } = this + const { type } = this if (type === UNIVERSAL_SELECTOR) return null - let content = this.get_content() + const content = this.get_content() if (type === LANG_SELECTOR && content === '') return null return content } /** Namespace prefix for type/universal selectors: null (none), '' (`|div`), 'ns' (`ns|div`), or '*' (`*|div`). */ get namespace(): string | null | undefined { - let { type } = this + const { type } = this if (type !== TYPE_SELECTOR && type !== UNIVERSAL_SELECTOR) return undefined if (!this.arena.has_flag(this.index, FLAG_HAS_NAMESPACE)) return null - let start = this.arena.get_value_start(this.index) - let length = this.arena.get_value_length(this.index) + const start = this.arena.get_value_start(this.index) + const length = this.arena.get_value_length(this.index) return this.source.substring(start, start + length) } /** Alias for `name` on declarations ("color" in "color: blue"), more semantic than `name` there. */ get property(): string | undefined { - let { type } = this + const { type } = this if (type !== DECLARATION && type !== MEDIA_FEATURE && type !== SUPPORTS_DECLARATION) return return this.get_content() } @@ -369,7 +369,7 @@ export class CSSNode { * 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 + const { type, text, first_child } = this if (type === AT_RULE) return undefined @@ -397,7 +397,7 @@ export class CSSNode { if (type === DIMENSION) { // Slice only the numeric part — avoids parse_dimension's extra unit substring + object - let num_str = text.substring(0, dimension_number_end(text)) + const num_str = text.substring(0, dimension_number_end(text)) // oxlint-disable-next-line prefer-number-coercion return num_str ? Number.parseFloat(num_str) : 0 } @@ -416,10 +416,10 @@ export class CSSNode { // Handle both url("...") and url('...') and just "..." or '...' if (str_starts_with(text, 'url(')) { // url("...") or url('...') or url(...) - extract content between parens - let open_paren = text.indexOf('(') - let close_paren = text.lastIndexOf(')') + const open_paren = text.indexOf('(') + const close_paren = text.lastIndexOf(')') if (open_paren !== -1 && close_paren !== -1 && close_paren > open_paren) { - let content = text.substring(open_paren + 1, close_paren).trim() + const content = text.substring(open_paren + 1, close_paren).trim() return content } } else if (text.startsWith('"') || text.startsWith("'")) { @@ -445,8 +445,8 @@ export class CSSNode { return undefined } - let start = this.arena.get_value_start(this.index) - let length = this.arena.get_value_length(this.index) + const start = this.arena.get_value_start(this.index) + const length = this.arena.get_value_length(this.index) if (length === 0) return null return this.source.substring(start, start + length) } @@ -454,7 +454,7 @@ export class CSSNode { /** 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 + const first = this.first_child if (first?.type === AT_RULE_PRELUDE || first?.type === RAW) { return first } @@ -471,10 +471,10 @@ export class CSSNode { get attr_operator(): string | null | undefined { if (this.type !== ATTRIBUTE_SELECTOR) return undefined - let content_start = this.arena.get_content_start(this.index) - let content_end = content_start + this.arena.get_content_length(this.index) - let node_start = this.arena.get_start_offset(this.index) - let node_end = node_start + this.arena.get_length(this.index) + const content_start = this.arena.get_content_start(this.index) + const content_end = content_start + this.arena.get_content_length(this.index) + const node_start = this.arena.get_start_offset(this.index) + const node_end = node_start + this.arena.get_length(this.index) // Skip whitespace after attribute name let pos = content_end @@ -482,8 +482,8 @@ export class CSSNode { if (pos >= node_end) return null - let ch1 = this.source.charCodeAt(pos) - let ch2 = pos + 1 < node_end ? this.source.charCodeAt(pos + 1) : 0 + const ch1 = this.source.charCodeAt(pos) + const ch2 = pos + 1 < node_end ? this.source.charCodeAt(pos + 1) : 0 if (ch1 === CHAR_EQUALS) return this.source.at(pos)! if (ch1 === CHAR_TILDE && ch2 === CHAR_EQUALS) return this.source.substring(pos, pos + 2) @@ -498,20 +498,20 @@ export class CSSNode { get attr_flags(): string | null | undefined { if (this.type !== ATTRIBUTE_SELECTOR) return undefined - let value_len = this.arena.get_value_length(this.index) + const value_len = this.arena.get_value_length(this.index) if (value_len === 0) return null - let value_start = this.arena.get_value_start(this.index) - let value_end = value_start + value_len - let node_start = this.arena.get_start_offset(this.index) - let node_end = node_start + this.arena.get_length(this.index) + const value_start = this.arena.get_value_start(this.index) + const value_end = value_start + value_len + const node_start = this.arena.get_start_offset(this.index) + const node_end = node_start + this.arena.get_length(this.index) // Skip whitespace after value let pos = value_end while (pos < node_end && is_whitespace(this.source.charCodeAt(pos))) pos++ if (pos >= node_end) return null - let flag_ch = this.source.charCodeAt(pos) + const flag_ch = this.source.charCodeAt(pos) if (flag_ch === 0x69 || flag_ch === 0x49 || flag_ch === 0x73 || flag_ch === 0x53) { return this.source.at(pos)! } @@ -521,7 +521,7 @@ export class CSSNode { /** Get the unit for dimension nodes (e.g., "px" from "100px", "%" from "50%") */ get unit(): string | undefined { if (this.type !== DIMENSION) return undefined - let text = this.text + const text = this.text return text.substring(dimension_number_end(text)) } @@ -597,7 +597,7 @@ export class CSSNode { /** Check if this node has a prelude (at-rules and style rules) */ get has_prelude(): boolean { - let { type } = this + const { type } = this if (type === AT_RULE) { return this.first_child !== null && this.first_child.type !== BLOCK } @@ -614,20 +614,20 @@ export class CSSNode { /** Check if this style rule has declarations */ get has_declarations(): boolean | undefined { - let { type } = this + 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 block(): CSSNode | null { - let { type } = this + const { type } = this // For StyleRule: block is sibling after selector list if (type === STYLE_RULE) { - let first = this.first_child + const first = this.first_child if (!first) return null // Block is the sibling after selector list - let block_node = first.next_sibling + const block_node = first.next_sibling if (block_node?.type === BLOCK) { return block_node } @@ -698,27 +698,27 @@ export class CSSNode { /** Get first child node */ get first_child(): CSSNode | null { - let child_index = this.arena.get_first_child(this.index) + 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(): CSSNode | null { - let sibling_index = this.arena.get_next_sibling(this.index) + 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 */ get has_next(): boolean { - let sibling_index = this.arena.get_next_sibling(this.index) + 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`. */ get has_children(): boolean { - let { type } = this + const { type } = this // For pseudo-class/pseudo-element nodes, check if they have function syntax if (type === PSEUDO_CLASS_SELECTOR || type === PSEUDO_ELEMENT_SELECTOR) { // If FLAG_HAS_PARENS is set, return true even if no actual children @@ -743,7 +743,7 @@ export class CSSNode { /** Get all children as an array */ get children(): CSSNode[] | undefined { - let result: CSSNode[] = [] + const result: CSSNode[] = [] let child = this.first_child while (child) { result.push(child) @@ -765,30 +765,30 @@ export class CSSNode { /** Get the 'a' coefficient from An+B expression (e.g., "2n" from "2n+1", "odd" from "odd") */ get nth_a(): string | null | undefined { - let { type, arena, index } = this + const { type, arena, index } = this if (type !== NTH_SELECTOR && type !== NTH_OF_SELECTOR) return undefined - let len = arena.get_content_length(index) + const len = arena.get_content_length(index) if (len === 0) return null - let start = arena.get_content_start(index) + const start = arena.get_content_start(index) return this.source.substring(start, start + len) } /** Get the 'b' coefficient from An+B expression (e.g., "+1" from "2n+1") */ get nth_b(): string | null | undefined { - let { type, arena, index, source } = this + const { type, arena, index, source } = this if (type !== NTH_SELECTOR && type !== NTH_OF_SELECTOR) return undefined - let len = arena.get_value_length(index) + const len = arena.get_value_length(index) if (len === 0) return null - let start = arena.get_value_start(index) + const start = arena.get_value_start(index) let value = source.substring(start, start + len) // Check if there's a - or + sign before this position (handling "2n - 1" or "2n + 1" with spaces) // Look backwards for a - or + sign, skipping whitespace let check_pos = start - 1 while (check_pos >= 0) { - let ch = source.charCodeAt(check_pos) + const ch = source.charCodeAt(check_pos) if (is_whitespace(ch)) { check_pos-- continue @@ -816,7 +816,7 @@ export class CSSNode { /** Get the selector list from :nth-child(2n+1 of .foo) */ get selector(): CSSNode | undefined { if (this.type !== NTH_OF_SELECTOR) return undefined - let first = this.first_child + const first = this.first_child return first?.next_sibling ?? undefined // Second child is NODE_SELECTOR_LIST } @@ -828,16 +828,16 @@ export class CSSNode { */ clone(options: CloneOptions = {}): PlainCSSNode { const { deep = true, locations = false } = options - let { type } = this + const { type } = this - let plain: any = { + const plain: any = { type, type_name: this.type_name, text: this.text, } - for (let key of enumerable_properties) { - let val = this[key] + for (const key of enumerable_properties) { + const val = this[key] if (val instanceof CSSNode) { plain[key] = val.clone({ deep, locations }) } else if (val !== undefined && val !== false) { @@ -858,9 +858,9 @@ export class CSSNode { plain.child_count = this.child_count plain.has_children = this.has_children - let children = this.children + const children = this.children if (children) { - for (let child of children) { + for (const child of children) { plain.children.push(child.clone({ deep: true, locations })) } } diff --git a/src/parse-anplusb.ts b/src/parse-anplusb.ts index f160a0b..9e6c55c 100644 --- a/src/parse-anplusb.ts +++ b/src/parse-anplusb.ts @@ -228,7 +228,7 @@ export class ANplusBParser { // Handle simple integer (b only, no 'a') if (this.lexer.token_type === TOKEN_NUMBER) { - let num_text = this.source.substring(this.lexer.token_start, this.lexer.token_end) + const num_text = this.source.substring(this.lexer.token_start, this.lexer.token_end) b = num_text b_start = this.lexer.token_start b_end = this.lexer.token_end diff --git a/src/parse-atrule-prelude.ts b/src/parse-atrule-prelude.ts index 194a6ad..8b85c42 100644 --- a/src/parse-atrule-prelude.ts +++ b/src/parse-atrule-prelude.ts @@ -80,7 +80,7 @@ export class AtRulePreludeParser { // Dispatch to appropriate parser based on at-rule type private parse_prelude_dispatch(at_rule_name: string): number[] { // Strip vendor prefix to treat @-webkit-keyframes same as @keyframes - let normalized_name = strip_vendor_prefix(at_rule_name).toLowerCase() + const normalized_name = strip_vendor_prefix(at_rule_name).toLowerCase() switch (normalized_name) { case 'media': @@ -119,13 +119,13 @@ export class AtRulePreludeParser { // Parse media query list: screen, (min-width: 768px), ... private parse_media_query_list(): number[] { - let nodes: number[] = [] + const nodes: number[] = [] while (this.lexer.pos < this.prelude_end) { this.skip_whitespace() if (this.lexer.pos >= this.prelude_end) break - let query = this.parse_single_media_query() + const query = this.parse_single_media_query() if (query !== null) { nodes.push(query) } @@ -169,7 +169,7 @@ export class AtRulePreludeParser { let close_end = this.lexer.token_end while (this.lexer.pos < this.prelude_end && depth > 0) { - let token_type = this.next_token() + const token_type = this.next_token() if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) { depth++ } else if (token_type === TOKEN_RIGHT_PAREN) { @@ -190,7 +190,7 @@ export class AtRulePreludeParser { // Parse a single media query: screen and (min-width: 768px) private parse_single_media_query(): number | null { - let query_start = this.lexer.pos + const query_start = this.lexer.pos // Skip whitespace this.skip_whitespace() @@ -207,14 +207,14 @@ export class AtRulePreludeParser { this.next_token() if (this.lexer.token_type === TOKEN_IDENT) { - let source = this.source - let token_start = this.lexer.token_start - let token_end = this.lexer.token_end + const source = this.source + const token_start = this.lexer.token_start + const 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) + const modifier = this.create_node(PRELUDE_OPERATOR, token_start, token_end) first_component = modifier last_component = modifier } else { @@ -234,7 +234,7 @@ export class AtRulePreludeParser { this.next_token() - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type let component: number | null = null // Media feature: (min-width: 768px) @@ -252,8 +252,8 @@ export class AtRulePreludeParser { } // Identifier: media type or operator (and, or, not) else if (token_type === TOKEN_IDENT) { - let token_start = this.lexer.token_start - let token_end = this.lexer.token_end + const token_start = this.lexer.token_start + const 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, token_start, token_end) @@ -279,7 +279,7 @@ export class AtRulePreludeParser { if (first_component === 0) return null // Create media query node - let query_node = this.create_node(MEDIA_QUERY, query_start, this.lexer.pos) + const query_node = this.create_node(MEDIA_QUERY, query_start, this.lexer.pos) // Link components as children this.arena.set_first_child(query_node, first_component) @@ -289,7 +289,7 @@ export class AtRulePreludeParser { // Parse container query: [name] and (min-width: 400px) private parse_container_query(): number[] { - let query_start = this.lexer.pos + const query_start = this.lexer.pos // Parse components (identifiers, operators, features), chained as siblings without an // intermediate array — most container queries have a single component. @@ -302,7 +302,7 @@ export class AtRulePreludeParser { this.next_token() - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type let component: number | null = null // Container feature: (min-width: 400px) @@ -329,8 +329,8 @@ export class AtRulePreludeParser { } // Identifier: operator (and, or, not) or container name else if (token_type === TOKEN_IDENT) { - let token_start = this.lexer.token_start - let token_end = this.lexer.token_end + const token_start = this.lexer.token_start + const 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, token_start, token_end) @@ -353,7 +353,7 @@ export class AtRulePreludeParser { if (first_component === 0) return [] // Create container query node - let query_node = this.create_node(CONTAINER_QUERY, query_start, this.lexer.pos) + const query_node = this.create_node(CONTAINER_QUERY, query_start, this.lexer.pos) // Link components as children this.arena.set_first_child(query_node, first_component) @@ -375,7 +375,7 @@ export class AtRulePreludeParser { // A single name may be dotted for nested layers: base.normalize // = ['.' ]* with no whitespace around the dots. private parse_layer_names(): number[] { - let nodes: number[] = [] + const nodes: number[] = [] while (this.lexer.pos < this.prelude_end) { this.skip_whitespace() @@ -383,16 +383,16 @@ export class AtRulePreludeParser { this.next_token() - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_IDENT) { - let name_start = this.lexer.token_start + const name_start = this.lexer.token_start let name_end = this.lexer.token_end // Glue on '.' ident segments immediately following, with no gaps. while (this.lexer.pos < this.prelude_end) { - let saved = this.lexer.save_position() + const saved = this.lexer.save_position() - let dot_token_type = this.next_token() + const dot_token_type = this.next_token() if ( dot_token_type !== TOKEN_DELIM || this.source.charCodeAt(this.lexer.token_start) !== CHAR_PERIOD || @@ -401,9 +401,9 @@ export class AtRulePreludeParser { this.lexer.restore_position(saved) break } - let dot_end = this.lexer.token_end + const dot_end = this.lexer.token_end - let segment_token_type = this.next_token() + const segment_token_type = this.next_token() if (segment_token_type !== TOKEN_IDENT || this.lexer.token_start !== dot_end) { this.lexer.restore_position(saved) break @@ -413,7 +413,7 @@ export class AtRulePreludeParser { } // Layer name (possibly dotted) - let layer = this.create_node(LAYER_NAME, name_start, name_end) + const layer = this.create_node(LAYER_NAME, name_start, name_end) this.arena.set_content_start_delta(layer, 0) this.arena.set_content_length(layer, name_end - name_start) nodes.push(layer) @@ -443,8 +443,8 @@ export class AtRulePreludeParser { if (this.lexer.token_type !== TOKEN_FUNCTION) return [] // Create an IDENTIFIER node for just the function name (without '(') - let name_start = this.lexer.token_start - let name_end = this.lexer.token_end - 1 // Exclude '(' + const name_start = this.lexer.token_start + const name_end = this.lexer.token_end - 1 // Exclude '(' return [this.create_node(IDENTIFIER, name_start, name_end)] } @@ -459,7 +459,7 @@ export class AtRulePreludeParser { if (this.lexer.token_type !== TOKEN_IDENT) return [] // Create identifier node - let ident = this.create_node(IDENTIFIER, this.lexer.token_start, this.lexer.token_end) + const ident = this.create_node(IDENTIFIER, this.lexer.token_start, this.lexer.token_end) return [ident] } @@ -474,7 +474,7 @@ export class AtRulePreludeParser { if (this.lexer.token_type !== TOKEN_STRING) return [] // Create string node - let str = this.create_node(STRING, this.lexer.token_start, this.lexer.token_end) + const str = this.create_node(STRING, this.lexer.token_start, this.lexer.token_end) return [str] } @@ -482,13 +482,13 @@ export class AtRulePreludeParser { // Parse @import prelude: url() [layer] [supports()] [media-query-list] // @import url("styles.css") layer(base) supports(display: grid) screen and (min-width: 768px); private parse_import_prelude(): number[] { - let nodes: number[] = [] + const nodes: number[] = [] // 1. Parse URL (required) - url("...") or "..." this.skip_whitespace() if (this.lexer.pos >= this.prelude_end) return [] - let url_node = this.parse_import_url() + const url_node = this.parse_import_url() if (url_node === null) { return [] // URL is required, fail if not found } @@ -499,7 +499,7 @@ export class AtRulePreludeParser { this.skip_whitespace() if (this.lexer.pos >= this.prelude_end) return nodes - let layer_node = this.parse_import_layer() + const layer_node = this.parse_import_layer() if (layer_node !== null) { nodes.push(layer_node) } @@ -508,7 +508,7 @@ export class AtRulePreludeParser { this.skip_whitespace() if (this.lexer.pos >= this.prelude_end) return nodes - let supports_node = this.parse_import_supports() + const supports_node = this.parse_import_supports() if (supports_node !== null) { nodes.push(supports_node) } @@ -518,7 +518,7 @@ export class AtRulePreludeParser { if (this.lexer.pos >= this.prelude_end) return nodes // Parse media queries (reuse existing parser) - let media_nodes = this.parse_media_query_list() + const media_nodes = this.parse_media_query_list() nodes.push(...media_nodes) return nodes @@ -538,18 +538,18 @@ export class AtRulePreludeParser { } // For url() function, we need to consume all tokens until the closing paren - let url_start = this.lexer.token_start + const url_start = this.lexer.token_start let url_end = this.lexer.token_end if (this.lexer.token_type === TOKEN_FUNCTION) { // It's url( ... we need to find the matching ) this.scan_matching_paren() - let close_end = this.paren_close_end + const close_end = this.paren_close_end url_end = close_end } // Create URL node - let url_node = this.create_node(URL, url_start, url_end) + const url_node = this.create_node(URL, url_start, url_end) return url_node } @@ -563,11 +563,11 @@ export class AtRulePreludeParser { // Check for 'layer' keyword or 'layer(' function if (this.lexer.token_type === TOKEN_IDENT || this.lexer.token_type === TOKEN_FUNCTION) { // For function tokens, exclude the trailing '(' - let name_end = + const name_end = this.lexer.token_type === TOKEN_FUNCTION ? this.lexer.token_end - 1 : this.lexer.token_end if (str_equals_range(this.source, this.lexer.token_start, name_end, 'layer')) { - let layer_start = this.lexer.token_start + const layer_start = this.lexer.token_start let layer_end = this.lexer.token_end let content_start = 0 let content_length = 0 @@ -576,9 +576,9 @@ export class AtRulePreludeParser { if (this.lexer.token_type === TOKEN_FUNCTION) { // Track the content inside the parentheses content_start = this.lexer.pos - let matched = this.scan_matching_paren() - let content_end = this.paren_content_end - let close_end = this.paren_close_end + const matched = this.scan_matching_paren() + const content_end = this.paren_content_end + const close_end = this.paren_close_end if (matched) { content_length = content_end - content_start layer_end = close_end @@ -586,11 +586,15 @@ export class AtRulePreludeParser { } // Create layer node - let layer_node = this.create_node(LAYER_NAME, layer_start, layer_end) + const layer_node = this.create_node(LAYER_NAME, layer_start, layer_end) // Store the layer name (content inside parentheses), trimmed if (content_length > 0) { - let trimmed = trim_boundaries(this.source, content_start, content_start + content_length) + const trimmed = trim_boundaries( + this.source, + content_start, + content_start + content_length, + ) if (trimmed) { this.arena.set_content_start_delta(layer_node, trimmed[0] - layer_start) this.arena.set_content_length(layer_node, trimmed[1] - trimmed[0]) @@ -619,25 +623,25 @@ export class AtRulePreludeParser { 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 '(' + const supports_start = this.lexer.token_start + const content_start = this.lexer.token_end // After the opening '(' // Find matching closing parenthesis this.scan_matching_paren() - let content_end = this.paren_content_end - let supports_end = this.paren_close_end + const content_end = this.paren_content_end + const supports_end = this.paren_close_end // Create supports node - let supports_node = this.create_node(SUPPORTS_QUERY, supports_start, supports_end) + const supports_node = this.create_node(SUPPORTS_QUERY, supports_start, supports_end) // Store query content in value fields, trimmed - let trimmed = trim_boundaries(this.source, content_start, content_end) + const trimmed = trim_boundaries(this.source, content_start, content_end) if (trimmed) { this.arena.set_value_start_delta(supports_node, trimmed[0] - supports_start) this.arena.set_value_length(supports_node, trimmed[1] - trimmed[0]) // Check for simple declaration: supports(property: value) - let decl_child = this.condition_parser.parse_supports_declaration_content( + const decl_child = this.condition_parser.parse_supports_declaration_content( trimmed[0], trimmed[1], ) @@ -665,7 +669,7 @@ export class AtRulePreludeParser { const saved = this.lexer.save_position() this.next_token() - let type = this.lexer.token_type + const type = this.lexer.token_type this.lexer.restore_position(saved) @@ -685,7 +689,7 @@ export class AtRulePreludeParser { // e.g. @namespace url("http://www.w3.org/1999/xhtml"); // e.g. @namespace svg url("http://www.w3.org/2000/svg"); private parse_namespace_prelude(): number[] { - let nodes: number[] = [] + const nodes: number[] = [] this.skip_whitespace() if (this.lexer.pos >= this.prelude_end) return [] @@ -709,7 +713,7 @@ export class AtRulePreludeParser { // Parse @scope prelude: [()] [to ()] // e.g. @scope (.parent) to (.child) { } private parse_scope_prelude(): number[] { - let nodes: number[] = [] + const nodes: number[] = [] while (this.lexer.pos < this.prelude_end) { this.skip_whitespace() @@ -719,15 +723,15 @@ export class AtRulePreludeParser { if (token_type === TOKEN_LEFT_PAREN) { this.next_token() // consume '(' - let paren_start = this.lexer.token_start - let content_start = this.lexer.pos + const paren_start = this.lexer.token_start + const content_start = this.lexer.pos this.scan_matching_paren() - let content_end = this.paren_content_end - let paren_end = this.paren_close_end + const content_end = this.paren_content_end + const 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) + const scope_node = this.create_node(PRELUDE_SELECTORLIST, paren_start, paren_end) + const trimmed = trim_boundaries(this.source, content_start, content_end) if (trimmed) { this.arena.set_value_start_delta(scope_node, trimmed[0] - paren_start) this.arena.set_value_length(scope_node, trimmed[1] - trimmed[0]) @@ -751,7 +755,7 @@ export class AtRulePreludeParser { // Parse @custom-media prelude: --name // e.g. @custom-media --small (max-width: 30em); private parse_custom_media_prelude(): number[] { - let nodes: number[] = [] + const nodes: number[] = [] this.skip_whitespace() if (this.lexer.pos >= this.prelude_end) return [] diff --git a/src/parse-condition.ts b/src/parse-condition.ts index 3f1eda6..58ab6f3 100644 --- a/src/parse-condition.ts +++ b/src/parse-condition.ts @@ -110,7 +110,7 @@ export class ConditionParser { let close_end = this.lexer.token_end while (this.lexer.pos < this.end && depth > 0) { - let token_type = this.next_token() + const token_type = this.next_token() if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) { depth++ } else if (token_type === TOKEN_RIGHT_PAREN) { @@ -141,20 +141,20 @@ export class ConditionParser { private wrap_ratio_value(first_node: number): number { if (this.arena.get_type(first_node) !== NUMBER) return first_node - let op_node = this.arena.get_next_sibling(first_node) + const op_node = this.arena.get_next_sibling(first_node) if (op_node === 0 || this.arena.get_type(op_node) !== OPERATOR) return first_node if (this.arena.get_length(op_node) !== 1) return first_node if (this.source.charCodeAt(this.arena.get_start_offset(op_node)) !== CHAR_FORWARD_SLASH) { return first_node } - let second_node = this.arena.get_next_sibling(op_node) + const second_node = this.arena.get_next_sibling(op_node) if (second_node === 0 || this.arena.get_type(second_node) !== NUMBER) return first_node if (this.arena.get_next_sibling(second_node) !== 0) return first_node - let start = this.arena.get_start_offset(first_node) - let end = this.arena.get_start_offset(second_node) + this.arena.get_length(second_node) - let ratio_node = this.arena.create_node( + const start = this.arena.get_start_offset(first_node) + const end = this.arena.get_start_offset(second_node) + this.arena.get_length(second_node) + const ratio_node = this.arena.create_node( RATIO, start, end - start, @@ -174,24 +174,24 @@ export class ConditionParser { content_end: number, colon_pos: number, ): number { - let prop_trimmed = trim_boundaries(this.source, content_start, colon_pos) - let val_trimmed = trim_boundaries(this.source, colon_pos + 1, content_end) + const prop_trimmed = trim_boundaries(this.source, content_start, colon_pos) + const val_trimmed = trim_boundaries(this.source, colon_pos + 1, content_end) if (!prop_trimmed) { // No property name — degenerate input, return a bare SUPPORTS_DECLARATION - let bare = this.create_node(SUPPORTS_DECLARATION, content_start, content_end) + const bare = this.create_node(SUPPORTS_DECLARATION, content_start, content_end) return bare } // DECLARATION spans from property start to value end (or colon if no value) - let decl_start = prop_trimmed[0] - let decl_end = val_trimmed ? val_trimmed[1] : colon_pos + 1 - let decl = this.create_node(DECLARATION, decl_start, decl_end) + const decl_start = prop_trimmed[0] + const decl_end = val_trimmed ? val_trimmed[1] : colon_pos + 1 + const decl = this.create_node(DECLARATION, decl_start, decl_end) this.arena.set_content_start_delta(decl, 0) // property starts at node start this.arena.set_content_length(decl, prop_trimmed[1] - prop_trimmed[0]) if (val_trimmed) { - let value_first = this.parse_feature_value(val_trimmed[0], val_trimmed[1]) + const value_first = this.parse_feature_value(val_trimmed[0], val_trimmed[1]) let value_node: number if (value_first === 0) { value_node = this.arena.create_node( @@ -214,7 +214,7 @@ export class ConditionParser { this.arena.set_first_child(decl, value_node) } - let supports_decl = this.create_node(SUPPORTS_DECLARATION, content_start, content_end) + const supports_decl = this.create_node(SUPPORTS_DECLARATION, content_start, content_end) // Mirror the property name onto the wrapper so .property doesn't need the inner Declaration this.arena.set_content_start_delta(supports_decl, prop_trimmed[0] - content_start) this.arena.set_content_length(supports_decl, prop_trimmed[1] - prop_trimmed[0]) @@ -229,7 +229,7 @@ export class ConditionParser { content_start: number, content_end: number, ): number { - let range_node = this.create_node(FEATURE_RANGE, feature_start, feature_end) + const range_node = this.create_node(FEATURE_RANGE, feature_start, feature_end) let first_child = 0 let last_child = 0 let feature_name_start = -1 @@ -241,16 +241,16 @@ export class ConditionParser { pos = skip_whitespace_and_comments_forward(this.source, pos, content_end) if (pos >= content_end) break - let ch = this.source.charCodeAt(pos) + const ch = this.source.charCodeAt(pos) // Comparison operator if (ch === CHAR_LESS_THAN || ch === CHAR_GREATER_THAN || ch === CHAR_EQUALS) { - let op_start = pos++ + const op_start = pos++ if (pos < content_end && this.source.charCodeAt(pos) === CHAR_EQUALS) { pos++ } - let op = this.create_node(PRELUDE_OPERATOR, op_start, pos) + const op = this.create_node(PRELUDE_OPERATOR, op_start, pos) if (first_child === 0) { first_child = op } else { @@ -259,7 +259,7 @@ export class ConditionParser { last_child = op } else { // Value or feature name - let saved = this.lexer.save_position() + const saved = this.lexer.save_position() this.lexer.pos = pos this.next_token() @@ -269,7 +269,7 @@ export class ConditionParser { feature_name_end = this.lexer.token_end } else { // Value (may itself be a short chain, e.g. a single dimension node) - let value_first = this.parse_feature_value(this.lexer.token_start, this.lexer.token_end) + const value_first = this.parse_feature_value(this.lexer.token_start, this.lexer.token_end) if (value_first !== 0) { if (first_child === 0) { first_child = value_first @@ -323,7 +323,7 @@ export class ConditionParser { i = skip_whitespace_and_comments_forward(this.source, i, content_end) if (i >= content_end) break - let ch = this.source.charCodeAt(i) + const ch = this.source.charCodeAt(i) if (ch === CHAR_LESS_THAN || ch === CHAR_GREATER_THAN || ch === CHAR_EQUALS) { has_comparison = true break @@ -336,7 +336,7 @@ export class ConditionParser { } // Standard feature or boolean feature - let feature = this.create_node(MEDIA_FEATURE, feature_start, feature_end) + const feature = this.create_node(MEDIA_FEATURE, feature_start, feature_end) // Find colon to separate name from value let colon_pos = -1 @@ -354,23 +354,23 @@ export class ConditionParser { if (colon_pos === -1) { // Boolean feature: (hover), (color) - let trimmed = trim_boundaries(this.source, content_start, content_end) + const trimmed = trim_boundaries(this.source, content_start, content_end) if (trimmed) { this.arena.set_content_start_delta(feature, trimmed[0] - feature_start) this.arena.set_content_length(feature, trimmed[1] - trimmed[0]) } } else { // Standard feature: (name: value) - let name_trimmed = trim_boundaries(this.source, content_start, colon_pos) + const name_trimmed = trim_boundaries(this.source, content_start, colon_pos) if (name_trimmed) { this.arena.set_content_start_delta(feature, name_trimmed[0] - feature_start) this.arena.set_content_length(feature, name_trimmed[1] - name_trimmed[0]) } // Parse value portion - let value_trimmed = trim_boundaries(this.source, colon_pos + 1, content_end) + const value_trimmed = trim_boundaries(this.source, colon_pos + 1, content_end) if (value_trimmed) { - let value_first = this.parse_feature_value(value_trimmed[0], value_trimmed[1]) + const value_first = this.parse_feature_value(value_trimmed[0], value_trimmed[1]) if (value_first !== 0) { this.arena.set_first_child(feature, this.wrap_ratio_value(value_first)) } @@ -395,12 +395,12 @@ export class ConditionParser { this.lexer.seek(open_paren_start, line, column) this.next_token() // consume '(' - let feature_start = this.lexer.token_start - let content_start = this.lexer.pos + const feature_start = this.lexer.token_start + const content_start = this.lexer.pos - let matched = this.scan_matching_paren() - let content_end = this.paren_content_end - let feature_end = this.paren_close_end + const matched = this.scan_matching_paren() + const content_end = this.paren_content_end + const feature_end = this.paren_close_end if (!matched) return null return this.parse_media_feature_content(feature_start, feature_end, content_start, content_end) @@ -414,17 +414,17 @@ export class ConditionParser { this.lexer.seek(func_start, line, column) this.next_token() // consume the function token, up to and including '(' - 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 '(' + const func_name_start = this.lexer.token_start + const func_name_end = this.lexer.token_end - 1 // -1 to exclude '(' + const content_start = this.lexer.token_end // After '(' // Find matching closing paren this.scan_matching_paren() - let content_end = this.paren_content_end - let func_end = this.paren_close_end + const content_end = this.paren_content_end + const func_end = this.paren_close_end // Create function node - let func_node = this.create_node(FUNCTION, func_start, func_end) + const 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_end - func_name_start) @@ -434,7 +434,7 @@ export class ConditionParser { // selector()'s argument is a — parse it into a real SelectorList if (str_equals_range(this.source, func_name_start, func_name_end, 'selector')) { - let selector_list = this.selector_parser.parse_selector( + const selector_list = this.selector_parser.parse_selector( content_start, content_end, this.lexer.line, @@ -446,7 +446,7 @@ export class ConditionParser { } // style()'s argument is a — parse it into the same tree as (property: value) 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) + const decl_child = this.parse_supports_declaration_content(content_start, content_end) if (decl_child !== null) { this.arena.set_first_child(func_node, decl_child) } @@ -459,7 +459,7 @@ export class ConditionParser { // ``/`` grammar shared by `style()`, `@supports (…)`, and // `@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 = find_colon_at_depth_zero(this.source, content_start, content_end) + const 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) } @@ -471,11 +471,11 @@ export class ConditionParser { // `start`/`end` bound the range to scan; the caller has already positioned nothing — this // seeks its own lexer, so it's safe to call with any sub-range of the source. parse_supports_condition(start: number, end: number, line: number, column: number): number[] { - let saved_end = this.end + const saved_end = this.end this.end = end this.lexer.seek(start, line, column) - let nodes: number[] = [] + const nodes: number[] = [] while (this.lexer.pos < this.end) { this.lexer.skip_whitespace_in_range(this.end) @@ -483,25 +483,25 @@ export class ConditionParser { this.next_token() - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type // Feature query: (property: value) if (token_type === TOKEN_LEFT_PAREN) { - let feature_start = this.lexer.token_start - let content_start = this.lexer.pos + const feature_start = this.lexer.token_start + const content_start = this.lexer.pos - let matched = this.scan_matching_paren() - let content_end = this.paren_content_end - let feature_end = this.paren_close_end + const matched = this.scan_matching_paren() + const content_end = this.paren_content_end + const feature_end = this.paren_close_end if (matched) { - let query = this.create_node(SUPPORTS_QUERY, feature_start, feature_end) + const query = this.create_node(SUPPORTS_QUERY, feature_start, feature_end) - let trimmed = trim_boundaries(this.source, content_start, content_end) + const trimmed = trim_boundaries(this.source, content_start, content_end) if (trimmed) { this.arena.set_value_start_delta(query, trimmed[0] - feature_start) this.arena.set_value_length(query, trimmed[1] - trimmed[0]) - let decl_child = this.parse_supports_declaration_content(trimmed[0], trimmed[1]) + const decl_child = this.parse_supports_declaration_content(trimmed[0], trimmed[1]) if (decl_child !== null) { this.arena.set_first_child(query, decl_child) } @@ -512,10 +512,10 @@ export class ConditionParser { } // Identifier: operator (and, or, not) else if (token_type === TOKEN_IDENT) { - let token_start = this.lexer.token_start - let token_end = this.lexer.token_end + const token_start = this.lexer.token_start + const 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) + const op = this.create_node(PRELUDE_OPERATOR, token_start, token_end) nodes.push(op) } } diff --git a/src/parse-declaration.ts b/src/parse-declaration.ts index a8eee30..0e3f726 100644 --- a/src/parse-declaration.ts +++ b/src/parse-declaration.ts @@ -136,11 +136,11 @@ export class DeclarationParser { return null } - let prop_start = has_browser_hack ? browser_hack_start : lexer.token_start - let prop_end = lexer.token_end + const prop_start = has_browser_hack ? browser_hack_start : lexer.token_start + const prop_end = lexer.token_end // CRITICAL: Capture line/column BEFORE consuming property token - let decl_line = has_browser_hack ? browser_hack_line : lexer.token_line - let decl_column = has_browser_hack ? browser_hack_column : lexer.token_column + const decl_line = has_browser_hack ? browser_hack_line : lexer.token_line + const decl_column = has_browser_hack ? browser_hack_column : lexer.token_column // Lookahead: save lexer state before consuming const saved = lexer.save_position_into(this.lookahead_position) @@ -162,7 +162,7 @@ export class DeclarationParser { lexer.skip_whitespace_in_range(this.source.length) // Create declaration node (length will be set later) - let declaration = this.arena.create_node( + const declaration = this.arena.create_node( DECLARATION, prop_start, 0, // length unknown yet @@ -175,9 +175,9 @@ export class DeclarationParser { this.arena.set_content_length(declaration, prop_end - prop_start) // Value start (after colon/whitespace) - lexer is positioned at the first, untokenized char. - let value_start = lexer.pos - let value_start_line = lexer.line - let value_start_column = lexer.column + const value_start = lexer.pos + const value_start_line = lexer.line + const value_start_column = lexer.column let value_end = value_start // Parse value (everything until ';' or EOF) @@ -191,7 +191,7 @@ export class DeclarationParser { // re-tokenizes the span afterward. Loop relies only on explicit break/return, since a // paren_depth adjustment can land exactly on `end` without the "ran out" case firing. while (true) { - let stop_ch = lexer.skip_to_declaration_stop(end) + const stop_ch = lexer.skip_to_declaration_stop(end) if (stop_ch === CHAR_LEFT_PAREN) { paren_depth++ @@ -239,7 +239,7 @@ export class DeclarationParser { value_end = lexer.pos lexer.pos++ // consume '!' // Check if next token is an identifier (doesn't verify it's literally "important") - let next_type = lexer.next_token_fast(true) // skip whitespace + const next_type = lexer.next_token_fast(true) // skip whitespace if (next_type === TOKEN_IDENT) { has_important = true last_end = lexer.token_end @@ -267,8 +267,8 @@ export class DeclarationParser { } // Store value position (trimmed) and parse value nodes - let trimmed_start = skip_whitespace_and_comments_forward(this.source, value_start, value_end) - let trimmed_end = skip_whitespace_and_comments_backward(this.source, value_end, trimmed_start) + const trimmed_start = skip_whitespace_and_comments_forward(this.source, value_start, value_end) + const trimmed_end = skip_whitespace_and_comments_backward(this.source, value_end, trimmed_start) if (trimmed_start < trimmed_end) { // Store raw value string offsets (for fast string access) this.arena.set_value_start_delta(declaration, trimmed_start - prop_start) @@ -277,7 +277,7 @@ export class DeclarationParser { // Parse value into structured nodes (only if enabled) if (this.value_parser) { // CRITICAL: Pass value_start_line and value_start_column to value parser - let valueNode = this.value_parser.parse_value( + const valueNode = this.value_parser.parse_value( value_start, trimmed_end, value_start_line, @@ -288,7 +288,7 @@ export class DeclarationParser { this.arena.set_first_child(declaration, valueNode) } else { // Create RAW node for unparsed value text - let rawNode = this.arena.create_node( + const rawNode = this.arena.create_node( RAW, trimmed_start, trimmed_end - trimmed_start, @@ -304,7 +304,7 @@ export class DeclarationParser { // Create empty VALUE node for consistency if (this.value_parser) { - let valueNode = this.value_parser.parse_value( + const valueNode = this.value_parser.parse_value( value_start, value_start, value_start_line, diff --git a/src/parse-dimension.ts b/src/parse-dimension.ts index 059dd41..343e79a 100644 --- a/src/parse-dimension.ts +++ b/src/parse-dimension.ts @@ -10,14 +10,14 @@ import { is_digit, CHAR_MINUS_HYPHEN, CHAR_PLUS, CHAR_PERIOD } from './string-ut export function dimension_number_end(text: string): number { let num_end = 0 for (let i = 0; i < text.length; i++) { - let ch = text.charCodeAt(i) + const ch = text.charCodeAt(i) // Check for e/E (scientific notation) if (ch === 0x65 || ch === 0x45) { // e or E // Only allow e/E if followed by digit or sign+digit if (i + 1 < text.length) { - let next_ch = text.charCodeAt(i + 1) + const next_ch = text.charCodeAt(i + 1) // Check if next is digit if (is_digit(next_ch)) { num_end = i + 1 @@ -25,7 +25,7 @@ export function dimension_number_end(text: string): number { } // Check if next is sign followed by digit if ((next_ch === 0x2b || next_ch === 0x2d) && i + 2 < text.length) { - let afterSign = text.charCodeAt(i + 2) + const afterSign = text.charCodeAt(i + 2) if (is_digit(afterSign)) { num_end = i + 1 continue @@ -49,11 +49,11 @@ export function dimension_number_end(text: string): number { /** 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 } { - let num_end = dimension_number_end(text) - let num_str = text.substring(0, num_end) - let unit = text.substring(num_end) + const num_end = dimension_number_end(text) + const num_str = text.substring(0, num_end) + const unit = text.substring(num_end) // oxlint-disable-next-line prefer-number-coercion - let value = num_str ? Number.parseFloat(num_str) : 0 + const value = num_str ? Number.parseFloat(num_str) : 0 return { value, unit } } diff --git a/src/parse-options.test.ts b/src/parse-options.test.ts index 96e621a..4ab26c1 100644 --- a/src/parse-options.test.ts +++ b/src/parse-options.test.ts @@ -292,7 +292,7 @@ describe('Parser Options', () => { ['@layer base, components;', 'base, components'], ['@supports (display: flex) { }', '(display: flex)'], ] - for (let [css, expectedText] of cases) { + for (const [css, expectedText] of cases) { const root = parse(css, { parse_atrule_preludes: false }) const atrule = root.first_child! as Atrule expect(atrule.prelude?.type).toBe(RAW) diff --git a/src/parse-selector.test.ts b/src/parse-selector.test.ts index 78bfb4e..a79288f 100644 --- a/src/parse-selector.test.ts +++ b/src/parse-selector.test.ts @@ -1075,9 +1075,9 @@ describe('Selector Nodes', () => { if (!root) return expect(root.type).toBe(SELECTOR_LIST) - let selector = root.first_child! as Selector + const selector = root.first_child! as Selector expect(selector.type).toBe(SELECTOR) - let attr = selector.first_child! as AttributeSelector + const attr = selector.first_child! as AttributeSelector expect(attr.type).toBe(ATTRIBUTE_SELECTOR) expect(attr.attr_flags).toBe('i') expect(attr.attr_operator).toBe('=') @@ -2226,39 +2226,39 @@ describe('Selector Nodes', () => { describe('keyframes selectors', () => { test('simple percentage', () => { - let root = parse_selector_list('50%') + const root = parse_selector_list('50%') expect(root.type_name).toBe('SelectorList') expect(root.first_child?.type_name).toBe('Selector') expect(root.first_child?.text).toBe('50%') expect(root.first_child?.first_child?.type_name).toBe('Dimension') }) test('fraction percentage', () => { - let root = parse_selector_list('50.1%') + const root = parse_selector_list('50.1%') expect(root.type_name).toBe('SelectorList') expect(root.first_child?.type_name).toBe('Selector') expect(root.first_child?.text).toBe('50.1%') expect(root.first_child?.first_child?.type_name).toBe('Dimension') }) test('scientific percentage', () => { - let root = parse_selector_list('50e1%') + const root = parse_selector_list('50e1%') expect(root.type_name).toBe('SelectorList') expect(root.first_child?.type_name).toBe('Selector') expect(root.first_child?.text).toBe('50e1%') expect(root.first_child?.first_child?.type_name).toBe('Dimension') }) test('from keyword', () => { - let root = parse_selector_list('from') + const root = parse_selector_list('from') expect(root.type_name).toBe('SelectorList') expect(root.first_child?.type_name).toBe('Selector') expect(root.first_child?.text).toBe('from') expect(root.first_child?.first_child?.type_name).toBe('TypeSelector') }) test('90%, to', () => { - let root = parse_selector_list('90%, to') + const root = parse_selector_list('90%, to') expect(root.type_name).toBe('SelectorList') expect(root.text).toBe('90%, to') - let [percentage, to] = root.children + const [percentage, to] = root.children expect(percentage.type_name).toBe('Selector') expect(percentage.text).toBe('90%') expect(percentage.first_child?.type_name).toBe('Dimension') @@ -2705,15 +2705,15 @@ describe('Selector Nodes', () => { }) test('should parse unknown pseudo-class without parens', () => { - let root = parse_selector_list(':hello') - let pseudo = root.first_child?.first_child + const root = parse_selector_list(':hello') + const pseudo = root.first_child?.first_child expect(pseudo?.type).toBe(PSEUDO_CLASS_SELECTOR) expect((pseudo as PseudoClassSelector | null | undefined)?.has_children).toBe(false) }) test('should parse unknown pseudo-class with empty parens', () => { - let root = parse_selector_list(':hello()') - let pseudo = root.first_child?.first_child + const root = parse_selector_list(':hello()') + const pseudo = root.first_child?.first_child expect(pseudo?.type).toBe(PSEUDO_CLASS_SELECTOR) expect((pseudo as PseudoClassSelector | null | undefined)?.has_children).toBe(true) expect((pseudo as PseudoClassSelector | null | undefined)?.children.length).toBe(0) diff --git a/src/parse-selector.ts b/src/parse-selector.ts index 6fcd62b..668a214 100644 --- a/src/parse-selector.ts +++ b/src/parse-selector.ts @@ -106,19 +106,19 @@ export class SelectorParser { // common case. let first_selector = 0 let last_selector = 0 - let list_start = this.lexer.pos - let list_line = this.lexer.line - let list_column = this.lexer.column + const list_start = this.lexer.pos + const list_line = this.lexer.line + const list_column = this.lexer.column while (this.lexer.pos < this.selector_end) { - let selector_start = this.lexer.pos - let selector_line = this.lexer.line - let selector_column = this.lexer.column + const selector_start = this.lexer.pos + const selector_line = this.lexer.line + const selector_column = this.lexer.column - let complex_selector = this.parse_complex_selector(allow_relative) + const complex_selector = this.parse_complex_selector(allow_relative) if (complex_selector !== null) { // Wrap the complex selector (chain of components) in a NODE_SELECTOR - let selector_wrapper = this.arena.create_node( + const selector_wrapper = this.arena.create_node( SELECTOR, selector_start, this.lexer.pos - selector_start, @@ -145,7 +145,7 @@ export class SelectorParser { // Check for comma (comments are auto-skipped by lexer) this.lexer.next_token_fast(false) - let token_type: TokenType = this.lexer.token_type + const token_type: TokenType = this.lexer.token_type if (token_type === TOKEN_COMMA) { // Skip whitespace after the comma @@ -159,7 +159,7 @@ export class SelectorParser { // Always wrap in selector list node, even for single selectors if (first_selector !== 0) { - let list_node = this.arena.create_node( + const list_node = this.arena.create_node( SELECTOR_LIST, list_start, this.lexer.pos - list_start, @@ -195,14 +195,14 @@ export class SelectorParser { const saved = this.lexer.save_position() this.lexer.next_token_fast(false) - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type // Check if token is a combinator if (token_type === TOKEN_DELIM) { - let ch = this.source.charCodeAt(this.lexer.token_start) + const ch = this.source.charCodeAt(this.lexer.token_start) if (ch === CHAR_GREATER_THAN || ch === CHAR_PLUS || ch === CHAR_TILDE) { // Found leading combinator (>, +, ~) - this is a relative selector - let combinator = this.create_node( + const combinator = this.create_node( COMBINATOR, this.lexer.token_start, this.lexer.token_end, @@ -223,7 +223,7 @@ export class SelectorParser { while (this.lexer.pos < this.selector_end) { // Parse compound selector first - let compound = this.parse_compound_selector() + const compound = this.parse_compound_selector() if (compound === null) { break } @@ -235,7 +235,7 @@ export class SelectorParser { chain_tail = this.arena.get_last_sibling(compound) // After a compound selector, check if there's a combinator - let combinator = this.try_parse_combinator() + const combinator = this.try_parse_combinator() if (combinator !== null) { this.arena.set_next_sibling(chain_tail, combinator) chain_tail = combinator @@ -267,10 +267,10 @@ export class SelectorParser { if (this.lexer.token_start >= this.selector_end) break - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_EOF) break - let part = this.parse_simple_selector() + const part = this.parse_simple_selector() if (part === null) { // Not a simple selector part, restore lexer state and break this.lexer.restore_position(saved) @@ -289,9 +289,9 @@ export class SelectorParser { // Parse a simple selector (single component) private parse_simple_selector(): number | null { - let token_type = this.lexer.token_type - let start = this.lexer.token_start - let end = this.lexer.token_end + const token_type = this.lexer.token_type + const start = this.lexer.token_start + const end = this.lexer.token_end switch (token_type) { case TOKEN_IDENT: @@ -305,7 +305,7 @@ export class SelectorParser { case TOKEN_DELIM: { // Could be: . (class), * (universal), & (nesting), | (namespace) - let ch = this.source.charCodeAt(start) + const ch = this.source.charCodeAt(start) if (ch === CHAR_PERIOD) { // . - class selector return this.parse_class_selector(start) @@ -376,9 +376,9 @@ export class SelectorParser { return null } - let local_start = this.lexer.token_start - let local_end = this.lexer.token_end - let node = this.create_node(node_type, selector_start, local_end) + const local_start = this.lexer.token_start + const local_end = this.lexer.token_end + const node = this.create_node(node_type, selector_start, local_end) // FLAG: has namespace qualifier (even if empty like |E) this.arena.set_flag(node, FLAG_HAS_NAMESPACE) // Content = local element name (after |) @@ -415,7 +415,7 @@ export class SelectorParser { this.source.charCodeAt(this.lexer.pos) === CHAR_PIPE ) { this.lexer.pos++ // skip | - let node = this.parse_namespace_local_part(start, start, end - start) + const node = this.parse_namespace_local_part(start, start, end - start) if (node !== null) return node } // Invalid or no pipe found - restore position @@ -440,7 +440,7 @@ export class SelectorParser { this.source.charCodeAt(this.lexer.pos) === CHAR_PIPE ) { this.lexer.pos++ // skip | - let node = this.parse_namespace_local_part(start, start, end - start) + const node = this.parse_namespace_local_part(start, start, end - start) if (node !== null) return node } // Invalid or no pipe found - restore position @@ -482,7 +482,7 @@ export class SelectorParser { // Check for explicit combinators if (this.lexer.token_type === TOKEN_DELIM) { - let ch = this.source.charCodeAt(this.lexer.token_start) + const ch = this.source.charCodeAt(this.lexer.token_start) if (is_combinator(ch)) { // > + ~ (combinator text excludes leading whitespace) // Use token's line and column for the combinator position @@ -530,13 +530,13 @@ export class SelectorParser { private parse_attribute_selector(start: number): number | null { let bracket_depth = 1 let end = this.lexer.token_end - let content_start = start + 1 // Position after '[' + const content_start = start + 1 // Position after '[' let content_end = content_start // Find matching ] while (this.lexer.pos < this.selector_end && bracket_depth > 0) { this.lexer.next_token_fast(false) - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_LEFT_BRACKET) { bracket_depth++ } else if (token_type === TOKEN_RIGHT_BRACKET) { @@ -549,7 +549,7 @@ export class SelectorParser { } } - let node = this.create_node(ATTRIBUTE_SELECTOR, start, end) + const node = this.create_node(ATTRIBUTE_SELECTOR, start, end) // Parse the content inside brackets to extract name, operator, and value this.parse_attribute_content(node, content_start, content_end) @@ -568,7 +568,7 @@ export class SelectorParser { if (start >= end) return // Find attribute name (up to operator or end) - let name_start = start + const name_start = start let name_end = start let operator_end = -1 let value_start = -1 @@ -576,7 +576,7 @@ export class SelectorParser { // Scan for attribute name while (name_end < end) { - let ch = this.source.charCodeAt(name_end) + const ch = this.source.charCodeAt(name_end) if ( is_whitespace(ch) || ch === CHAR_EQUALS /* = */ || @@ -606,8 +606,8 @@ export class SelectorParser { } // Parse operator - let ch1 = this.source.charCodeAt(pos) - let ch2 = pos + 1 < end ? this.source.charCodeAt(pos + 1) : 0 // Cache second character to avoid repeated calls + const ch1 = this.source.charCodeAt(pos) + const ch2 = pos + 1 < end ? this.source.charCodeAt(pos + 1) : 0 // Cache second character to avoid repeated calls if (ch1 === CHAR_EQUALS) { // = @@ -642,16 +642,16 @@ export class SelectorParser { // Parse value (can be quoted or unquoted) value_start = pos - let ch = this.source.charCodeAt(pos) + const ch = this.source.charCodeAt(pos) if (ch === CHAR_SINGLE_QUOTE || ch === CHAR_DOUBLE_QUOTE) { // " or ' // Quoted string - find matching quote - let quote = ch + const quote = ch value_start = pos // Include quotes in value pos++ while (pos < end) { - let c = this.source.charCodeAt(pos) + const c = this.source.charCodeAt(pos) if (c === quote) { pos++ break @@ -666,7 +666,7 @@ export class SelectorParser { } else { // Unquoted identifier while (pos < end) { - let c = this.source.charCodeAt(pos) + const c = this.source.charCodeAt(pos) if (is_whitespace(c)) { break } @@ -709,9 +709,9 @@ export class SelectorParser { // Next token should be identifier or function this.lexer.next_token_fast(false) - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_IDENT) { - let node = this.create_node( + const node = this.create_node( is_pseudo_element ? PSEUDO_ELEMENT_SELECTOR : PSEUDO_CLASS_SELECTOR, start, this.lexer.token_end, @@ -742,11 +742,11 @@ export class SelectorParser { start: number, is_pseudo_element: boolean, ): number | null { - let func_name_start = this.lexer.token_start - let func_name_end = this.lexer.token_end - 1 // Exclude the '(' + const func_name_start = this.lexer.token_start + const func_name_end = this.lexer.token_end - 1 // Exclude the '(' // Capture content start (right after the '(') - let content_start = this.lexer.pos + const content_start = this.lexer.pos let content_end = content_start // Find matching ) @@ -755,7 +755,7 @@ export class SelectorParser { while (this.lexer.pos < this.selector_end && paren_depth > 0) { this.lexer.next_token_fast(false) - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) { paren_depth++ } else if (token_type === TOKEN_RIGHT_PAREN) { @@ -768,7 +768,7 @@ export class SelectorParser { } } - let node = this.create_node( + const node = this.create_node( is_pseudo_element ? PSEUDO_ELEMENT_SELECTOR : PSEUDO_CLASS_SELECTOR, start, end, @@ -787,7 +787,7 @@ export class SelectorParser { // substring for every parenthesized pseudo-class, e.g. :not(), :is(), :nth-child()) if (this.is_nth_pseudo(func_name_start, func_name_end)) { // Parse as An+B expression - let child = this.parse_nth_expression(content_start, content_end) + const child = this.parse_nth_expression(content_start, content_end) if (child !== null) { this.arena.set_first_child(node, child) } @@ -797,13 +797,13 @@ export class SelectorParser { } else { // Parse as selector (for :is(), :where(), :has(), etc.) // Save current lexer state and selector_end - let saved_selector_end = this.selector_end + const saved_selector_end = this.selector_end const saved = this.lexer.save_position() // Recursively parse the content as a selector // Only :has() accepts relative selectors (starting with combinator) - let allow_relative = str_equals_range(this.source, func_name_start, func_name_end, 'has') - let child_selector = this.parse_selector( + const allow_relative = str_equals_range(this.source, func_name_start, func_name_end, 'has') + const child_selector = this.parse_selector( content_start, content_end, this.lexer.line, @@ -843,7 +843,7 @@ export class SelectorParser { const saved_position = this.lexer.save_position() this.lexer.seek(start, this.lexer.line, this.lexer.column) - let saved_selector_end = this.selector_end + const saved_selector_end = this.selector_end this.selector_end = end let first_child: number | null = null @@ -851,9 +851,9 @@ export class SelectorParser { while (this.lexer.pos < end) { this.lexer.next_token_fast(false) - let token_type = this.lexer.token_type - let token_start = this.lexer.token_start - let token_end = this.lexer.token_end + const token_type = this.lexer.token_type + const token_start = this.lexer.token_start + const token_end = this.lexer.token_end // Skip whitespace if (token_type === TOKEN_WHITESPACE) { @@ -868,7 +868,7 @@ export class SelectorParser { // Accept both strings and identifiers if (token_type === TOKEN_STRING || token_type === TOKEN_IDENT) { // Create language identifier node - let lang_node = this.create_node(LANG_SELECTOR, token_start, token_end) + const lang_node = this.create_node(LANG_SELECTOR, token_start, token_end) // Link as child of :lang() pseudo-class if (first_child === null) { @@ -900,9 +900,9 @@ export class SelectorParser { private parse_nth_expression(start: number, end: number): number | null { // Check for "of " syntax // e.g., "2n+1 of .active, .disabled" - let of_index = this.find_of_keyword(start, end) + const of_index = this.find_of_keyword(start, end) - let anplusb_parser = this.anplusb_parser + const anplusb_parser = this.anplusb_parser if (of_index === -1) { // Just An+B, no "of" clause @@ -910,7 +910,7 @@ export class SelectorParser { } // Parse An+B part before "of" - let anplusb_node = anplusb_parser.parse_anplusb(start, of_index, this.lexer.line) + const anplusb_node = anplusb_parser.parse_anplusb(start, of_index, this.lexer.line) // Parse selector list after "of" let selector_start = of_index + 2 // skip "of" @@ -918,20 +918,20 @@ export class SelectorParser { selector_start = skip_whitespace_forward(this.source, selector_start, end) // Save current state - let saved_selector_end = this.selector_end + const saved_selector_end = this.selector_end const saved = this.lexer.save_position() // Parse selector list this.selector_end = end this.lexer.pos = selector_start - let selector_list = this.parse_selector_list() + const selector_list = this.parse_selector_list() // Restore state this.selector_end = saved_selector_end this.lexer.restore_position(saved) // Create NTH_OF wrapper - let of_node = this.arena.create_node(NTH_OF_SELECTOR, start, end - start, this.lexer.line, 1) + const of_node = this.arena.create_node(NTH_OF_SELECTOR, start, end - start, this.lexer.line, 1) // Link An+B and selector list if (anplusb_node !== null && selector_list !== null) { @@ -951,13 +951,13 @@ export class SelectorParser { i = skip_whitespace_and_comments_forward(this.source, i, end) if (i >= end - 1) break - let ch1 = this.source.charCodeAt(i) - let ch2 = this.source.charCodeAt(i + 1) + const ch1 = this.source.charCodeAt(i) + const ch2 = this.source.charCodeAt(i + 1) // Check for 'o' or 'O' (0x6f or 0x4f) followed by 'f' or 'F' (0x66 or 0x46) if ((ch1 === 0x6f || ch1 === 0x4f) && (ch2 === 0x66 || ch2 === 0x46)) { // Check it's a word boundary - let before_ok = i === start || is_whitespace(this.source.charCodeAt(i - 1)) - let after_ok = i + 2 >= end || is_whitespace(this.source.charCodeAt(i + 2)) + const before_ok = i === start || is_whitespace(this.source.charCodeAt(i - 1)) + const after_ok = i + 2 >= end || is_whitespace(this.source.charCodeAt(i + 2)) if (before_ok && after_ok) { return i @@ -978,7 +978,7 @@ export class SelectorParser { line: number = this.lexer.token_line, column: number = this.lexer.token_column, ): number { - let node = this.arena.create_node(type, start, end - start, line, column) + const node = this.arena.create_node(type, start, end - start, line, column) this.arena.set_content_start_delta(node, 0) this.arena.set_content_length(node, end - start) return node diff --git a/src/parse-utils.ts b/src/parse-utils.ts index 38b86c3..5f048d3 100644 --- a/src/parse-utils.ts +++ b/src/parse-utils.ts @@ -20,7 +20,7 @@ export function skip_whitespace_and_comments_forward( end: number, ): number { while (pos < end) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) // Skip whitespace if (is_whitespace(ch)) { @@ -60,7 +60,7 @@ export function skip_whitespace_and_comments_backward( start: number, ): number { while (pos > start) { - let ch = source.charCodeAt(pos - 1) + const ch = source.charCodeAt(pos - 1) // Skip whitespace if (is_whitespace(ch)) { @@ -107,7 +107,7 @@ export function trim_boundaries( 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) + const 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 diff --git a/src/parse-value.ts b/src/parse-value.ts index 4640db4..f3fb3ca 100644 --- a/src/parse-value.ts +++ b/src/parse-value.ts @@ -17,20 +17,20 @@ export class ValueParser { // Parse a declaration value range into a VALUE wrapper node // Returns single VALUE node index parse_value(start: number, end: number, start_line: number, start_column: number): number { - let first_node = this.nodes.parse_chain(start, end, start_line, start_column) + const first_node = this.nodes.parse_chain(start, end, start_line, start_column) if (first_node === 0) { // Empty value - create VALUE node with no children return this.arena.create_node(VALUE, start, 0, start_line, start_column) } - let last_node = this.nodes.last_chain_node + const last_node = this.nodes.last_chain_node // Create VALUE wrapper node spanning all value tokens - let first_node_start = this.arena.get_start_offset(first_node) - let last_node_end = this.arena.get_start_offset(last_node) + this.arena.get_length(last_node) + const first_node_start = this.arena.get_start_offset(first_node) + const last_node_end = this.arena.get_start_offset(last_node) + this.arena.get_length(last_node) - let value_node = this.arena.create_node( + const value_node = this.arena.create_node( VALUE, first_node_start, last_node_end - first_node_start, diff --git a/src/parse.test.ts b/src/parse.test.ts index b4d15c4..c67247d 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -954,25 +954,25 @@ describe('Core Nodes', () => { describe('Special at-rules', () => { test('@charset', () => { - let source = '@charset "UTF-8"; body { color: red; }' - let root = parse(source) + const source = '@charset "UTF-8"; body { color: red; }' + const root = parse(source) - let [charset, _body] = root.children as Atrule[] + const [charset, _body] = root.children as Atrule[] expect(charset.type).toBe(AT_RULE) expect(charset.name).toBe('charset') }) test('@import with media query', () => { - let source = '@import url("print.css") print;' - let root = parse(source) + const source = '@import url("print.css") print;' + const root = parse(source) - let import_rule = root.first_child! as Atrule + const import_rule = root.first_child! as Atrule expect(import_rule.type).toBe(AT_RULE) expect(import_rule.name).toBe('import') }) test('@font-face with multiple descriptors', () => { - let source = ` + const source = ` @font-face { font-family: "Custom"; src: url("font.woff2") format("woff2"), @@ -982,132 +982,133 @@ describe('Core Nodes', () => { font-display: swap; } ` - let root = parse(source) + const root = parse(source) - let font_face = root.first_child! as Atrule + const font_face = root.first_child! as Atrule expect(font_face.name).toBe('font-face') - let block = font_face.block! + const block = font_face.block! expect(block.children.length).toBeGreaterThan(3) }) test('@counter-style', () => { - let source = '@counter-style custom { system: cyclic; symbols: "⚫" "⚪"; suffix: " "; }' - let root = parse(source) + const source = + '@counter-style custom { system: cyclic; symbols: "⚫" "⚪"; suffix: " "; }' + const root = parse(source) - let counter = root.first_child! as Atrule + const counter = root.first_child! as Atrule expect(counter.name).toBe('counter-style') - let block = counter.block! + const block = counter.block! expect(block.children.length).toBeGreaterThan(1) }) test('@property', () => { - let source = + const source = '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' - let root = parse(source) + const root = parse(source) - let property = root.first_child! as Atrule + const property = root.first_child! as Atrule expect(property.name).toBe('property') }) }) describe('At-rule preludes', () => { test('media query prelude', () => { - let source = '@media (min-width: 768px) { }' - let root = parse(source) + const source = '@media (min-width: 768px) { }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.type).toBe(AT_RULE) expect(atrule.name).toBe('media') expect(atrule.prelude?.text).toBe('(min-width: 768px)') }) test('complex media query prelude', () => { - let source = '@media screen and (min-width: 768px) and (max-width: 1024px) { }' - let root = parse(source) + const source = '@media screen and (min-width: 768px) and (max-width: 1024px) { }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('media') expect(atrule.prelude?.text).toBe('screen and (min-width: 768px) and (max-width: 1024px)') }) test('container query prelude', () => { - let source = '@container (width >= 200px) { }' - let root = parse(source) + const source = '@container (width >= 200px) { }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('container') expect(atrule.prelude?.text).toBe('(width >= 200px)') }) test('supports query prelude', () => { - let source = '@supports (display: grid) { }' - let root = parse(source) + const source = '@supports (display: grid) { }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('supports') expect(atrule.prelude?.text).toBe('(display: grid)') }) test('import prelude', () => { - let source = '@import url("styles.css");' - let root = parse(source) + const source = '@import url("styles.css");' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('import') expect(atrule.prelude?.text).toBe('url("styles.css")') }) test('at-rule without prelude', () => { - let source = '@font-face { font-family: MyFont; }' - let root = parse(source) + const source = '@font-face { font-family: MyFont; }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('font-face') expect(atrule.prelude).toBe(null) }) test('layer prelude', () => { - let source = '@layer utilities { }' - let root = parse(source) + const source = '@layer utilities { }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('layer') expect(atrule.prelude?.text).toBe('utilities') }) test('keyframes prelude', () => { - let source = '@keyframes slide-in { }' - let root = parse(source) + const source = '@keyframes slide-in { }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('keyframes') expect(atrule.prelude?.text).toBe('slide-in') }) test('prelude with extra whitespace', () => { - let source = '@media (min-width: 768px) { }' - let root = parse(source) + const source = '@media (min-width: 768px) { }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('media') expect(atrule.prelude?.text).toBe('(min-width: 768px)') }) test('charset prelude', () => { - let source = '@charset "UTF-8";' - let root = parse(source) + const source = '@charset "UTF-8";' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('charset') expect(atrule.prelude).not.toBeNull() expect(atrule.prelude?.text).toBe('"UTF-8"') }) test('namespace prelude', () => { - let source = '@namespace svg url(http://www.w3.org/2000/svg);' - let root = parse(source) + const source = '@namespace svg url(http://www.w3.org/2000/svg);' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('namespace') // @namespace doesn't have structured prelude parsing, but prelude wrapper exists expect(atrule.prelude).not.toBeNull() @@ -1115,10 +1116,10 @@ describe('Core Nodes', () => { }) test('unknown at-rule has prelude with accessible text', () => { - let source = '@imaginary-atrule prelude-stuff;' - let root = parse(source) + const source = '@imaginary-atrule prelude-stuff;' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('imaginary-atrule') // Unknown at-rules get a RAW prelude (not AT_RULE_PRELUDE) expect(atrule.prelude).not.toBeNull() @@ -1127,10 +1128,10 @@ describe('Core Nodes', () => { }) test('unknown at-rule without block has RAW prelude and no block', () => { - let source = '@custom prelude;' - let root = parse(source) + const source = '@custom prelude;' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('custom') expect(atrule.prelude?.type).toBe(RAW) expect(atrule.prelude?.text).toBe('prelude') @@ -1138,94 +1139,94 @@ describe('Core Nodes', () => { }) test('unknown at-rule block can contain declarations', () => { - let source = '@custom { color: red }' - let root = parse(source) + const source = '@custom { color: red }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('custom') - let block = atrule.block! + const block = atrule.block! expect(block).not.toBeNull() - let declaration = block.first_child! as Declaration + const declaration = block.first_child! as Declaration expect(declaration.type).toBe(DECLARATION) expect(declaration.property).toBe('color') }) test('unknown at-rule block can contain style rules', () => { - let source = '@custom { .a { color: red } }' - let root = parse(source) + const source = '@custom { .a { color: red } }' + const root = parse(source) - let atrule = root.first_child! as Atrule - let block = atrule.block! - let rule = block.first_child! + const atrule = root.first_child! as Atrule + const block = atrule.block! + const rule = block.first_child! expect(rule.type).toBe(STYLE_RULE) }) test('unknown at-rule with prelude block can contain style rules', () => { - let source = '@custom test { .a { color: red } }' - let root = parse(source) + const source = '@custom test { .a { color: red } }' + const root = parse(source) - let atrule = root.first_child! as Atrule - let block = atrule.block! - let rule = block.first_child! + const atrule = root.first_child! as Atrule + const block = atrule.block! + const rule = block.first_child! expect(rule.type).toBe(STYLE_RULE) expect((atrule as any).value).toBeUndefined() - let prelude = atrule.prelude! + const prelude = atrule.prelude! expect(prelude.type).toBe(RAW) }) test('unknown at-rule block can contain nested at-rules', () => { - let source = '@custom { @media (width) { } }' - let root = parse(source) + const source = '@custom { @media (width) { } }' + const root = parse(source) - let atrule = root.first_child! as Atrule - let block = atrule.block! - let nested = block.first_child! as Atrule + const atrule = root.first_child! as Atrule + const block = atrule.block! + const nested = block.first_child! as Atrule expect(nested.type).toBe(AT_RULE) expect(nested.name).toBe('media') }) test('known at-rule @keyframes still has AT_RULE_PRELUDE and correctly parsed frames', () => { - let source = '@keyframes foo { from { opacity: 0 } to { opacity: 1 } }' - let root = parse(source) + const source = '@keyframes foo { from { opacity: 0 } to { opacity: 1 } }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.name).toBe('keyframes') expect(atrule.prelude?.type).toBe(AT_RULE_PRELUDE) expect(atrule.prelude?.text).toBe('foo') - let block = atrule.block! - let from_rule = block.first_child! + const block = atrule.block! + const from_rule = block.first_child! expect(from_rule.type).toBe(STYLE_RULE) }) test('value string matches prelude text for at-rules', () => { - let source = '@media (min-width: 768px) { }' - let root = parse(source) + const source = '@media (min-width: 768px) { }' + const root = parse(source) - let atrule = root.first_child! as Atrule + const atrule = root.first_child! as Atrule expect(atrule.prelude?.text).toBe('(min-width: 768px)') }) test('at-rule prelude line tracking', () => { - let source = 'body { color: red; }\n\n@media screen { }' - let root = parse(source) + const source = 'body { color: red; }\n\n@media screen { }' + const root = parse(source) - let [_rule1, atRule] = root.children as Atrule[] + const [_rule1, atRule] = root.children as Atrule[] expect(atRule.line).toBe(3) // Check that prelude wrapper inherits the correct line - let preludeWrapper = atRule.prelude + const preludeWrapper = atRule.prelude expect(preludeWrapper).toBeTruthy() expect(preludeWrapper!.line).toBe(3) // Should be line 3, not line 1 }) }) describe('At-rule block children', () => { - let css = `@layer test { a {} }` - let sheet = parse(css) - let atrule = sheet?.first_child as Atrule | undefined - let rule = atrule?.block?.first_child as Rule | undefined + const css = `@layer test { a {} }` + const sheet = parse(css) + const atrule = sheet?.first_child as Atrule | undefined + const rule = atrule?.block?.first_child as Rule | undefined test('atrule should have block', () => { expect(sheet.type).toBe(STYLESHEET) @@ -1247,7 +1248,7 @@ describe('Core Nodes', () => { }) test('has correct nested selectors', () => { - let list = rule?.first_child as SelectorList | null + const list = rule?.first_child as SelectorList | null expect(list!.type).toBe(SELECTOR_LIST) expect(list!.children).toHaveLength(1) expect(list?.first_child?.type).toEqual(SELECTOR) @@ -1358,60 +1359,60 @@ describe('Core Nodes', () => { describe('Declaration values', () => { test('extract simple value', () => { - let source = 'a { color: blue; }' - let root = parse(source) + const source = 'a { color: blue; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('color') expect(decl.first_child!.text).toBe('blue') }) test('extract value with spaces', () => { - let source = 'a { padding: 1rem 2rem 3rem 4rem; }' - let root = parse(source) + const source = 'a { padding: 1rem 2rem 3rem 4rem; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('padding') expect(decl.first_child!.text).toBe('1rem 2rem 3rem 4rem') }) test('extract function value', () => { - let source = 'a { background: linear-gradient(to bottom, red, blue); }' - let root = parse(source) + const source = 'a { background: linear-gradient(to bottom, red, blue); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('background') expect(decl.first_child!.text).toBe('linear-gradient(to bottom, red, blue)') }) test('extract calc value', () => { - let source = 'a { width: calc(100% - 2rem); }' - let root = parse(source) + const source = 'a { width: calc(100% - 2rem); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('width') expect(decl.first_child!.text).toBe('calc(100% - 2rem)') }) test('exclude !important from value', () => { - let source = 'a { color: blue !important; }' - let root = parse(source) + const source = 'a { color: blue !important; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('color') expect(decl.first_child!.text).toBe('blue') @@ -1419,84 +1420,84 @@ describe('Core Nodes', () => { }) test('value with extra whitespace', () => { - let source = 'a { color: blue ; }' - let root = parse(source) + const source = 'a { color: blue ; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('color') expect(decl.first_child!.text).toBe('blue') }) test('CSS custom property value', () => { - let source = ':root { --brand-color: rgb(0% 10% 50% / 0.5); }' - let root = parse(source) + const source = ':root { --brand-color: rgb(0% 10% 50% / 0.5); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('--brand-color') expect(decl.first_child!.text).toBe('rgb(0% 10% 50% / 0.5)') }) test('var() reference value', () => { - let source = 'a { color: var(--primary-color); }' - let root = parse(source) + const source = 'a { color: var(--primary-color); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('color') expect(decl.first_child!.text).toBe('var(--primary-color)') }) test('nested function value', () => { - let source = 'a { transform: translate(calc(50% - 1rem), 0); }' - let root = parse(source) + const source = 'a { transform: translate(calc(50% - 1rem), 0); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('transform') expect(decl.first_child!.text).toBe('translate(calc(50% - 1rem), 0)') }) test('value without semicolon', () => { - let source = 'a { color: blue }' - let root = parse(source) + const source = 'a { color: blue }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('color') expect(decl.first_child!.text).toBe('blue') }) test('empty value', () => { - let source = 'a { color: ; }' - let root = parse(source) + const source = 'a { color: ; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('color') expect(decl.first_child!.text).toBe('') }) test('URL value', () => { - let source = 'a { background: url("image.png"); }' - let root = parse(source) + const source = 'a { background: url("image.png"); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('background') expect(decl.first_child!.text).toBe('url("image.png")') @@ -1505,90 +1506,90 @@ describe('Core Nodes', () => { describe('Vendor prefix detection', () => { test('-webkit- vendor prefix', () => { - let source = '.box { -webkit-transform: scale(1); }' - let root = parse(source) + const source = '.box { -webkit-transform: scale(1); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('-webkit-transform') expect(decl.is_vendor_prefixed).toBe(true) }) test('-moz- vendor prefix', () => { - let source = '.box { -moz-transform: scale(1); }' - let root = parse(source) + const source = '.box { -moz-transform: scale(1); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('-moz-transform') expect(decl.is_vendor_prefixed).toBe(true) }) test('-ms- vendor prefix', () => { - let source = '.box { -ms-transform: scale(1); }' - let root = parse(source) + const source = '.box { -ms-transform: scale(1); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('-ms-transform') expect(decl.is_vendor_prefixed).toBe(true) }) test('-o- vendor prefix', () => { - let source = '.box { -o-transform: scale(1); }' - let root = parse(source) + const source = '.box { -o-transform: scale(1); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('-o-transform') expect(decl.is_vendor_prefixed).toBe(true) }) test('no vendor prefix for standard properties', () => { - let source = '.box { transform: scale(1); }' - let root = parse(source) + const source = '.box { transform: scale(1); }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('transform') expect(decl.is_vendor_prefixed).toBe(false) }) test('no vendor prefix for properties with hyphens', () => { - let source = '.box { background-color: red; }' - let root = parse(source) + const source = '.box { background-color: red; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('background-color') expect(decl.is_vendor_prefixed).toBe(false) }) test('no vendor prefix for custom properties', () => { - let source = ':root { --primary-color: blue; }' - let root = parse(source) + const source = ':root { --primary-color: blue; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('--primary-color') expect(decl.is_vendor_prefixed).toBe(false) }) test('multiple vendor-prefixed properties', () => { - let source = + const source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' - let root = parse(source) + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let [webkit, moz, standard] = block.children as Declaration[] + const [webkit, moz, standard] = block.children as Declaration[] expect(webkit.property).toBe('-webkit-transform') expect(webkit.is_vendor_prefixed).toBe(true) @@ -1601,196 +1602,197 @@ describe('Core Nodes', () => { }) test('complex property names with vendor prefix', () => { - let source = '.box { -webkit-border-top-left-radius: 5px; }' - let root = parse(source) + const source = '.box { -webkit-border-top-left-radius: 5px; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('-webkit-border-top-left-radius') expect(decl.is_vendor_prefixed).toBe(true) }) test('no vendor prefix for similar but non-vendor properties', () => { - let source = '.box { border-radius: 5px; }' - let root = parse(source) + const source = '.box { border-radius: 5px; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! const block = (rule as Rule).block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.property).toBe('border-radius') expect(decl.is_vendor_prefixed).toBe(false) }) test('false for nodes without names', () => { - let source = 'body { }' - let root = parse(source) + const source = 'body { }' + const root = parse(source) - let rule = root.first_child! - let selector = rule.first_child! + const rule = root.first_child! + const selector = rule.first_child! expect(selector.is_vendor_prefixed).toBe(false) }) }) describe('Vendor prefix detection for selectors', () => { test('-webkit- vendor prefix in pseudo-class', () => { - let source = 'input:-webkit-autofill { color: black; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoClass = typeSelector.next_sibling! as PseudoClassSelector + const source = 'input:-webkit-autofill { color: black; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoClass = typeSelector.next_sibling! as PseudoClassSelector expect(pseudoClass.name).toBe('-webkit-autofill') expect(pseudoClass.is_vendor_prefixed).toBe(true) }) test('-moz- vendor prefix in pseudo-class', () => { - let source = 'button:-moz-focusring { outline: 2px solid blue; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoClass = typeSelector.next_sibling! as PseudoClassSelector + const source = 'button:-moz-focusring { outline: 2px solid blue; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoClass = typeSelector.next_sibling! as PseudoClassSelector expect(pseudoClass.name).toBe('-moz-focusring') expect(pseudoClass.is_vendor_prefixed).toBe(true) }) test('-ms- vendor prefix in pseudo-class', () => { - let source = 'input:-ms-input-placeholder { color: gray; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoClass = typeSelector.next_sibling! as PseudoClassSelector + const source = 'input:-ms-input-placeholder { color: gray; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoClass = typeSelector.next_sibling! as PseudoClassSelector expect(pseudoClass.name).toBe('-ms-input-placeholder') expect(pseudoClass.is_vendor_prefixed).toBe(true) }) test('-webkit- vendor prefix in pseudo-element', () => { - let source = 'div::-webkit-scrollbar { width: 10px; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoElement = typeSelector.next_sibling! as PseudoElementSelector + const source = 'div::-webkit-scrollbar { width: 10px; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoElement = typeSelector.next_sibling! as PseudoElementSelector expect(pseudoElement.name).toBe('-webkit-scrollbar') expect(pseudoElement.is_vendor_prefixed).toBe(true) }) test('-moz- vendor prefix in pseudo-element', () => { - let source = 'div::-moz-selection { background: yellow; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoElement = typeSelector.next_sibling! as PseudoElementSelector + const source = 'div::-moz-selection { background: yellow; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoElement = typeSelector.next_sibling! as PseudoElementSelector expect(pseudoElement.name).toBe('-moz-selection') expect(pseudoElement.is_vendor_prefixed).toBe(true) }) test('-webkit- vendor prefix in pseudo-element with multiple parts', () => { - let source = 'input::-webkit-input-placeholder { color: gray; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoElement = typeSelector.next_sibling! as PseudoElementSelector + const source = 'input::-webkit-input-placeholder { color: gray; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoElement = typeSelector.next_sibling! as PseudoElementSelector expect(pseudoElement.name).toBe('-webkit-input-placeholder') expect(pseudoElement.is_vendor_prefixed).toBe(true) }) test('-webkit- vendor prefix in pseudo-class function', () => { - let source = 'input:-webkit-any(input, button) { margin: 0; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoClass = typeSelector.next_sibling! as PseudoClassSelector + const source = 'input:-webkit-any(input, button) { margin: 0; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoClass = typeSelector.next_sibling! as PseudoClassSelector expect(pseudoClass.name).toBe('-webkit-any') expect(pseudoClass.is_vendor_prefixed).toBe(true) }) test('no vendor prefix for standard pseudo-classes', () => { - let source = 'a:hover { color: blue; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoClass = typeSelector.next_sibling! as PseudoClassSelector + const source = 'a:hover { color: blue; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoClass = typeSelector.next_sibling! as PseudoClassSelector expect(pseudoClass.name).toBe('hover') expect(pseudoClass.is_vendor_prefixed).toBe(false) }) test('no vendor prefix for standard pseudo-elements', () => { - let source = 'div::before { content: ""; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let pseudoElement = typeSelector.next_sibling! as PseudoElementSelector + const source = 'div::before { content: ""; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const pseudoElement = typeSelector.next_sibling! as PseudoElementSelector expect(pseudoElement.name).toBe('before') expect(pseudoElement.is_vendor_prefixed).toBe(false) }) test('multiple vendor-prefixed pseudo-elements', () => { - let source = 'div::-webkit-scrollbar { } div::-webkit-scrollbar-thumb { } div::after { }' - let root = parse(source) + const source = + 'div::-webkit-scrollbar { } div::-webkit-scrollbar-thumb { } div::after { }' + const root = parse(source) - let [rule1, rule2, rule3] = root.children + const [rule1, rule2, rule3] = root.children - let selectorList1 = rule1.first_child! - let selector1 = selectorList1.first_child! - let typeSelector1 = selector1.first_child! - let pseudo1 = typeSelector1.next_sibling! as PseudoElementSelector + const selectorList1 = rule1.first_child! + const selector1 = selectorList1.first_child! + const typeSelector1 = selector1.first_child! + const pseudo1 = typeSelector1.next_sibling! as PseudoElementSelector expect(pseudo1.name).toBe('-webkit-scrollbar') expect(pseudo1.is_vendor_prefixed).toBe(true) - let selectorList2 = rule2.first_child! - let selector2 = selectorList2.first_child! - let typeSelector2 = selector2.first_child! - let pseudo2 = typeSelector2.next_sibling! as PseudoElementSelector + const selectorList2 = rule2.first_child! + const selector2 = selectorList2.first_child! + const typeSelector2 = selector2.first_child! + const pseudo2 = typeSelector2.next_sibling! as PseudoElementSelector expect(pseudo2.name).toBe('-webkit-scrollbar-thumb') expect(pseudo2.is_vendor_prefixed).toBe(true) - let selectorList3 = rule3.first_child! - let selector3 = selectorList3.first_child! - let typeSelector3 = selector3.first_child! - let pseudo3 = typeSelector3.next_sibling! as PseudoElementSelector + const selectorList3 = rule3.first_child! + const selector3 = selectorList3.first_child! + const typeSelector3 = selector3.first_child! + const pseudo3 = typeSelector3.next_sibling! as PseudoElementSelector expect(pseudo3.name).toBe('after') expect(pseudo3.is_vendor_prefixed).toBe(false) }) test('vendor prefix in complex selector', () => { - let source = 'input:-webkit-autofill:focus { color: black; }' - let root = parse(source) - - let rule = root.first_child! - let selectorList = rule.first_child! - let selector = selectorList.first_child! - let typeSelector = selector.first_child! - let webkitPseudo = typeSelector.next_sibling! as PseudoClassSelector + const source = 'input:-webkit-autofill:focus { color: black; }' + const root = parse(source) + + const rule = root.first_child! + const selectorList = rule.first_child! + const selector = selectorList.first_child! + const typeSelector = selector.first_child! + const webkitPseudo = typeSelector.next_sibling! as PseudoClassSelector expect(webkitPseudo.name).toBe('-webkit-autofill') expect(webkitPseudo.is_vendor_prefixed).toBe(true) - let focusPseudo = webkitPseudo.next_sibling! as PseudoClassSelector + const focusPseudo = webkitPseudo.next_sibling! as PseudoClassSelector expect(focusPseudo.name).toBe('focus') expect(focusPseudo.is_vendor_prefixed).toBe(false) }) @@ -1885,126 +1887,126 @@ describe('Core Nodes', () => { describe('CSS Nesting', () => { test('nested rule with & selector', () => { - let source = '.parent { color: red; & .child { color: blue; } }' - let root = parse(source) + const source = '.parent { color: red; & .child { color: blue; } }' + const root = parse(source) - let parent = root.first_child! + const parent = root.first_child! expect(parent.type).toBe(STYLE_RULE) const block = (parent as Rule).block! - let [decl, nested_rule] = block.children as [Declaration, CSSNode] + const [decl, nested_rule] = block.children as [Declaration, CSSNode] expect(decl.type).toBe(DECLARATION) expect(decl.property).toBe('color') expect(nested_rule.type).toBe(STYLE_RULE) - let nested_selector = nested_rule.first_child! + const nested_selector = nested_rule.first_child! expect(nested_selector.text).toBe('& .child') }) test('nested rule without & selector', () => { - let source = '.parent { color: red; .child { color: blue; } }' - let root = parse(source) + const source = '.parent { color: red; .child { color: blue; } }' + const root = parse(source) - let parent = root.first_child! as Rule + const parent = root.first_child! as Rule const block = parent.block! - let [_decl, nested_rule] = block.children + const [_decl, nested_rule] = block.children expect(nested_rule.type).toBe(STYLE_RULE) - let nested_selector = nested_rule.first_child! + const nested_selector = nested_rule.first_child! expect(nested_selector.text).toBe('.child') }) test('multiple nested rules', () => { - let source = '.parent { .child1 { } .child2 { } }' - let root = parse(source) + const source = '.parent { .child1 { } .child2 { } }' + const root = parse(source) - let parent = root.first_child! as Rule + const parent = root.first_child! as Rule const block = parent.block! - let [nested1, nested2] = block.children + const [nested1, nested2] = block.children expect(nested1.type).toBe(STYLE_RULE) expect(nested2.type).toBe(STYLE_RULE) }) test('deeply nested rules', () => { - let source = '.a { .b { .c { color: red; } } }' - let root = parse(source) + const source = '.a { .b { .c { color: red; } } }' + const root = parse(source) - let a = root.first_child! + const a = root.first_child! expect(a.length).toBe(32) const block_a = (a as Rule).block! - let b = block_a.first_child! + const b = block_a.first_child! expect(b.type).toBe(STYLE_RULE) expect(b.length).toBe(25) const block_b = (b as Rule).block! - let c = block_b.first_child! + const c = block_b.first_child! expect(c.type).toBe(STYLE_RULE) expect(c.length).toBe(18) const block_c = (c as Rule).block! - let decl = block_c.first_child! as Declaration + const decl = block_c.first_child! as Declaration expect(decl.type).toBe(DECLARATION) expect(decl.property).toBe('color') }) test('nested @media inside rule', () => { - let source = '.card { color: red; @media (min-width: 768px) { padding: 2rem; } }' - let root = parse(source, { parse_atrule_preludes: false }) + const source = '.card { color: red; @media (min-width: 768px) { padding: 2rem; } }' + const root = parse(source, { parse_atrule_preludes: false }) - let card = root.first_child! + const card = root.first_child! const block = (card as Rule).block! - let [decl, media] = block.children as [Declaration, Atrule] + const [decl, media] = block.children as [Declaration, Atrule] expect(decl.type).toBe(DECLARATION) expect(media.type).toBe(AT_RULE) expect(media.name).toBe('media') - let media_block = media.block! - let nested_decl = media_block.first_child! as Declaration + const media_block = media.block! + const nested_decl = media_block.first_child! as Declaration expect(nested_decl.type).toBe(DECLARATION) expect(nested_decl.property).toBe('padding') }) test(':is() pseudo-class', () => { - let source = ':is(.a, .b) { color: red; }' - let root = parse(source) + const source = ':is(.a, .b) { color: red; }' + const root = parse(source) - let rule = root.first_child! - let selector = rule.first_child! + const rule = root.first_child! + const selector = rule.first_child! expect(selector.text).toBe(':is(.a, .b)') }) test(':where() pseudo-class', () => { - let source = ':where(h1, h2, h3) { margin: 0; }' - let root = parse(source) + const source = ':where(h1, h2, h3) { margin: 0; }' + const root = parse(source) - let rule = root.first_child! - let selector = rule.first_child! + const rule = root.first_child! + const selector = rule.first_child! expect(selector.text).toBe(':where(h1, h2, h3)') }) test(':has() pseudo-class', () => { - let source = 'div:has(> img) { display: flex; }' - let root = parse(source) + const source = 'div:has(> img) { display: flex; }' + const root = parse(source) - let rule = root.first_child! - let selector = rule.first_child! + const rule = root.first_child! + const selector = rule.first_child! expect(selector.text).toBe('div:has(> img)') }) test('complex nesting with mixed declarations and rules', () => { - let source = `.card { + const source = `.card { color: red; .title { font-size: 2rem; } padding: 1rem; .body { line-height: 1.5; } }` - let root = parse(source) + const root = parse(source) - let card = root.first_child! + const card = root.first_child! const block = (card as Rule).block! - let [decl1, title, decl2, body] = block.children as [ + const [decl1, title, decl2, body] = block.children as [ Declaration, CSSNode, Declaration, @@ -2024,57 +2026,57 @@ describe('Core Nodes', () => { describe('Relaxed nesting (CSS Nesting Module Level 1)', () => { test('nested rule with leading child combinator', () => { - let source = '.parent { > a { color: red; } }' - let root = parse(source) + const source = '.parent { > a { color: red; } }' + const root = parse(source) - let parent = root.first_child! + const parent = root.first_child! expect(parent.type).toBe(STYLE_RULE) const block = (parent as Rule).block! - let nested_rule = block.first_child! + const nested_rule = block.first_child! expect(nested_rule.type).toBe(STYLE_RULE) - let nested_selector = nested_rule.first_child! as SelectorList + const nested_selector = nested_rule.first_child! as SelectorList expect(nested_selector.text).toBe('> a') expect(nested_selector.has_children).toBe(true) }) test('nested rule with leading next-sibling combinator', () => { - let source = '.parent { + span { color: blue; } }' - let root = parse(source) + const source = '.parent { + span { color: blue; } }' + const root = parse(source) - let parent = root.first_child! + const parent = root.first_child! const block = (parent as Rule).block! - let nested_rule = block.first_child! + const nested_rule = block.first_child! expect(nested_rule.type).toBe(STYLE_RULE) - let nested_selector = nested_rule.first_child! as SelectorList + const nested_selector = nested_rule.first_child! as SelectorList expect(nested_selector.text).toBe('+ span') expect(nested_selector.has_children).toBe(true) }) test('nested rule with leading subsequent-sibling combinator', () => { - let source = '.parent { ~ div { color: green; } }' - let root = parse(source) + const source = '.parent { ~ div { color: green; } }' + const root = parse(source) - let parent = root.first_child! + const parent = root.first_child! const block = (parent as Rule).block! - let nested_rule = block.first_child! + const nested_rule = block.first_child! expect(nested_rule.type).toBe(STYLE_RULE) - let nested_selector = nested_rule.first_child! as SelectorList + const nested_selector = nested_rule.first_child! as SelectorList expect(nested_selector.text).toBe('~ div') expect(nested_selector.has_children).toBe(true) }) test('multiple nested rules with different leading combinators', () => { - let source = + const source = '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' - let root = parse(source) + const root = parse(source) - let parent = root.first_child! + const parent = root.first_child! const block = (parent as Rule).block! - let [rule1, rule2, rule3] = block.children + const [rule1, rule2, rule3] = block.children expect(rule1.type).toBe(STYLE_RULE) expect(rule1.first_child!.text).toBe('> a') @@ -2090,43 +2092,43 @@ describe('Core Nodes', () => { }) test('complex selector after leading combinator', () => { - let source = '.parent { > a.link#nav[href]:hover { color: red; } }' - let root = parse(source) + const source = '.parent { > a.link#nav[href]:hover { color: red; } }' + const root = parse(source) - let parent = root.first_child! + const parent = root.first_child! const block = (parent as Rule).block! - let nested_rule = block.first_child! + const nested_rule = block.first_child! - let nested_selector = nested_rule.first_child! as SelectorList + const nested_selector = nested_rule.first_child! as SelectorList expect(nested_selector.text).toBe('> a.link#nav[href]:hover') expect(nested_selector.has_children).toBe(true) }) test('deeply nested rules with leading combinators', () => { - let source = '.a { > .b { > .c { color: red; } } }' - let root = parse(source) + const source = '.a { > .b { > .c { color: red; } } }' + const root = parse(source) - let a = root.first_child! + const a = root.first_child! const block_a = (a as Rule).block! - let b = block_a.first_child! + const b = block_a.first_child! expect(b.type).toBe(STYLE_RULE) expect(b.first_child!.text).toBe('> .b') expect((b.first_child! as SelectorList).has_children).toBe(true) const block_b = (b as Rule).block! - let c = block_b.first_child! + const c = block_b.first_child! expect(c.type).toBe(STYLE_RULE) expect(c.first_child!.text).toBe('> .c') expect((c.first_child! as SelectorList).has_children).toBe(true) }) test('mixed nested rules with and without leading combinators', () => { - let source = '.parent { .normal { } > .combinator { } }' - let root = parse(source) + const source = '.parent { .normal { } > .combinator { } }' + const root = parse(source) - let parent = root.first_child! + const parent = root.first_child! const block = (parent as Rule).block! - let [normal, combinator] = block.children + const [normal, combinator] = block.children expect(normal.type).toBe(STYLE_RULE) expect(normal.first_child!.text).toBe('.normal') @@ -2140,93 +2142,94 @@ describe('Core Nodes', () => { describe('@keyframes parsing', () => { test('@keyframes with from/to', () => { - let source = '@keyframes fade { from { opacity: 0; } to { opacity: 1; } }' - let root = parse(source, { parse_atrule_preludes: false }) + const source = '@keyframes fade { from { opacity: 0; } to { opacity: 1; } }' + const root = parse(source, { parse_atrule_preludes: false }) - let keyframes = root.first_child! as Atrule + const keyframes = root.first_child! as Atrule expect(keyframes.type).toBe(AT_RULE) expect(keyframes.name).toBe('keyframes') - let block = keyframes.block! - let [from_rule, to_rule] = block.children + const block = keyframes.block! + const [from_rule, to_rule] = block.children expect(from_rule.type).toBe(STYLE_RULE) expect(to_rule.type).toBe(STYLE_RULE) - let from_selector = from_rule.first_child! + const from_selector = from_rule.first_child! expect(from_selector.text).toBe('from') - let to_selector = to_rule.first_child! + const to_selector = to_rule.first_child! expect(to_selector.text).toBe('to') }) test('@keyframes with percentages', () => { - let source = '@keyframes slide { 0% { left: 0; } 50% { left: 50%; } 100% { left: 100%; } }' - let root = parse(source, { parse_atrule_preludes: false }) + const source = + '@keyframes slide { 0% { left: 0; } 50% { left: 50%; } 100% { left: 100%; } }' + const root = parse(source, { parse_atrule_preludes: false }) - let keyframes = root.first_child! as Atrule - let block = keyframes.block! - let [rule0, rule50, rule100] = block.children + const keyframes = root.first_child! as Atrule + const block = keyframes.block! + const [rule0, rule50, rule100] = block.children expect(rule0.type).toBe(STYLE_RULE) expect(rule50.type).toBe(STYLE_RULE) expect(rule100.type).toBe(STYLE_RULE) - let selector0 = rule0.first_child! + const selector0 = rule0.first_child! expect(selector0.text).toBe('0%') }) test('@keyframes with multiple selectors', () => { - let source = '@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }' - let root = parse(source, { parse_atrule_preludes: false }) + const source = '@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }' + const root = parse(source, { parse_atrule_preludes: false }) - let keyframes = root.first_child! as Atrule - let block = keyframes.block! - let [rule1, _rule2] = block.children + const keyframes = root.first_child! as Atrule + const block = keyframes.block! + const [rule1, _rule2] = block.children - let selector1 = rule1.first_child! + const selector1 = rule1.first_child! expect(selector1.text).toBe('0%, 100%') }) test('@keyframes with mixed percentages and keywords', () => { - let source = + const source = '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' - let root = parse(source, { parse_atrule_preludes: false }) + const root = parse(source, { parse_atrule_preludes: false }) - let keyframes = root.first_child! as Atrule - let block = keyframes.block! + const keyframes = root.first_child! as Atrule + const block = keyframes.block! expect(block.children.length).toBe(3) }) }) describe('@function at-rule', () => { test('@function basic', () => { - let source = + const source = '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' - let root = parse(source, { parse_atrule_preludes: false }) + const root = parse(source, { parse_atrule_preludes: false }) - let fn = root.first_child! as Atrule + const fn = root.first_child! as Atrule expect(fn.type).toBe(AT_RULE) expect(fn.name).toBe('function') expect(fn.has_block).toBe(true) - let block = fn.block! + const block = fn.block! expect(block.children.length).toBe(1) - let result_decl = block.first_child! as Declaration + const result_decl = block.first_child! as Declaration expect(result_decl.type).toBe(DECLARATION) expect(result_decl.property).toBe('result') }) test('@function with local custom properties', () => { - let source = + const source = '@function --anim(--animation, --count) { --duration: 1s; --easing: linear; result: var(--animation) var(--duration) var(--count) var(--easing); }' - let root = parse(source, { parse_atrule_preludes: false }) + const root = parse(source, { parse_atrule_preludes: false }) - let fn = root.first_child! as Atrule - let block = fn.block! + const fn = root.first_child! as Atrule + const block = fn.block! expect(block.children.length).toBe(3) - let [dur, ease, result_decl] = block.children as Declaration[] + const [dur, ease, result_decl] = block.children as Declaration[] expect(dur.type).toBe(DECLARATION) expect(dur.property).toBe('--duration') expect(ease.property).toBe('--easing') @@ -2234,15 +2237,15 @@ describe('Core Nodes', () => { }) test('@function with nested @media', () => { - let source = + const source = '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' - let root = parse(source, { parse_atrule_preludes: false }) + const root = parse(source, { parse_atrule_preludes: false }) - let fn = root.first_child! as Atrule - let block = fn.block! + const fn = root.first_child! as Atrule + const block = fn.block! expect(block.child_count).toBeGreaterThan(0) - let [result_decl, media] = block.children as [Declaration, Atrule] + const [result_decl, media] = block.children as [Declaration, Atrule] expect(result_decl.type).toBe(DECLARATION) expect(result_decl.property).toBe('result') expect(media.type).toBe(AT_RULE) @@ -2250,10 +2253,10 @@ describe('Core Nodes', () => { }) test('@function with no parameters', () => { - let source = '@function --my-func() { result: 42px; }' - let root = parse(source, { parse_atrule_preludes: false }) + const source = '@function --my-func() { result: 42px; }' + const root = parse(source, { parse_atrule_preludes: false }) - let fn = root.first_child! as Atrule + const fn = root.first_child! as Atrule expect(fn.type).toBe(AT_RULE) expect(fn.name).toBe('function') expect(fn.has_block).toBe(true) @@ -2262,13 +2265,13 @@ describe('Core Nodes', () => { test('multiple @function rules with result: descriptor are all parsed (#187)', () => { // Regression test: @function blocks with result: descriptor caused subsequent // @function rules to be skipped (every other rule was silently dropped) - let source = + const source = '@function --a() { result: red; }\n@function --b() { result: blue; }\n@function --c() { result: green; }' - let root = parse(source) + const root = parse(source) expect(root.children.length).toBe(3) - let [a, b, c] = root.children as Atrule[] + const [a, b, c] = root.children as Atrule[] expect(a.type).toBe(AT_RULE) expect(a.name).toBe('function') expect(b.type).toBe(AT_RULE) @@ -2288,30 +2291,30 @@ describe('Core Nodes', () => { describe('@nest at-rule', () => { test('@nest with & selector', () => { - let source = '.parent { @nest & .child { color: blue; } }' - let root = parse(source) + const source = '.parent { @nest & .child { color: blue; } }' + const root = parse(source) - let parent = root.first_child! as Rule + const parent = root.first_child! as Rule const block = parent.block! - let nest = block.first_child! as Atrule + const nest = block.first_child! as Atrule expect(nest.type).toBe(AT_RULE) expect(nest.name).toBe('nest') expect(nest.has_prelude || nest.has_block).toBe(true) - let nest_block = nest.block! - let decl = nest_block.first_child! as Declaration + const nest_block = nest.block! + const decl = nest_block.first_child! as Declaration expect(decl.type).toBe(DECLARATION) expect(decl.property).toBe('color') }) test('@nest with complex selector', () => { - let source = '.a { @nest :not(&) { color: red; } }' - let root = parse(source) + const source = '.a { @nest :not(&) { color: red; } }' + const root = parse(source) - let a = root.first_child! + const a = root.first_child! const block = (a as Rule).block! - let nest = block.first_child! as Atrule + const nest = block.first_child! as Atrule expect(nest.type).toBe(AT_RULE) expect(nest.name).toBe('nest') @@ -2320,129 +2323,129 @@ describe('Core Nodes', () => { describe('Error recovery and edge cases', () => { test('malformed rule without opening brace', () => { - let source = 'body color: red; } div { margin: 0; }' - let root = parse(source) + const source = 'body color: red; } div { margin: 0; }' + const root = parse(source) expect(root.children.length).toBeGreaterThan(0) }) test('rule without closing brace', () => { - let source = 'body { color: red; div { margin: 0; }' - let root = parse(source) + const source = 'body { color: red; div { margin: 0; }' + const root = parse(source) expect(root.has_children).toBe(true) }) test('empty rule block', () => { - let source = '.empty { }' - let root = parse(source) + const source = '.empty { }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) expect((rule as Rule).block).not.toBeNull() }) test('declaration without value', () => { - let source = 'body { color: }' - let root = parse(source) + const source = 'body { color: }' + const root = parse(source) - let rule = root.first_child! as Rule + const rule = root.first_child! as Rule const block = rule.block! - let decl = block.first_child! as Declaration + const decl = block.first_child! as Declaration expect(decl.type).toBe(DECLARATION) }) test('multiple semicolons', () => { - let source = 'body { color: red;;; margin: 0;; }' - let root = parse(source) + const source = 'body { color: red;;; margin: 0;; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect((rule as Rule).block).not.toBeNull() }) test('invalid tokens in declaration block', () => { - let source = 'body { color: red; @@@; margin: 0; }' - let root = parse(source) + const source = 'body { color: red; @@@; margin: 0; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect((rule as Rule).block).not.toBeNull() }) test('declaration without colon', () => { - let source = 'body { color red; margin: 0; }' - let root = parse(source) + const source = 'body { color red; margin: 0; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect((rule as Rule).block).not.toBeNull() }) test('at-rule without name', () => { - let source = '@ { color: red; } body { margin: 0; }' - let root = parse(source) + const source = '@ { color: red; } body { margin: 0; }' + const root = parse(source) expect(root.children.length).toBeGreaterThan(0) }) test('nested empty blocks', () => { - let source = '.a { .b { .c { } } }' - let root = parse(source) + const source = '.a { .b { .c { } } }' + const root = parse(source) - let a = root.first_child! + const a = root.first_child! expect(a.type).toBe(STYLE_RULE) }) test('trailing comma in selector', () => { - let source = '.a, .b, { color: red; }' - let root = parse(source) + const source = '.a, .b, { color: red; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) }) }) describe('Comment handling', () => { test('skip comments at top level', () => { - let source = '/* comment */ body { color: red; } /* another comment */' - let root = parse(source) + const source = '/* comment */ body { color: red; } /* another comment */' + const root = parse(source) expect(root.children.length).toBe(1) - let rule = root.first_child! + const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) }) test('skip comments in declaration block', () => { - let source = 'body { color: red; /* comment */ margin: 0; }' - let root = parse(source) + const source = 'body { color: red; /* comment */ margin: 0; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) expect((rule as Rule).block).not.toBeNull() }) test('skip comments in selector', () => { - let source = 'body /* comment */ , /* comment */ div { color: red; }' - let root = parse(source) + const source = 'body /* comment */ , /* comment */ div { color: red; }' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) }) test('comment between property and colon', () => { - let source = 'body { color /* comment */ : red; }' - let root = parse(source) + const source = 'body { color /* comment */ : red; }' + const root = parse(source) expect(root.has_children).toBe(true) }) test('multi-line comments', () => { - let source = ` + const source = ` /* * Multi-line * comment */ body { color: red; } ` - let root = parse(source) + const root = parse(source) expect(root.children.length).toBe(1) }) @@ -2450,28 +2453,28 @@ describe('Core Nodes', () => { describe('Whitespace handling', () => { test('excessive whitespace', () => { - let source = ' body { color : red ; } ' - let root = parse(source) + const source = ' body { color : red ; } ' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) }) test('tabs and newlines', () => { - let source = 'body\t{\n\tcolor:\tred;\n}\n' - let root = parse(source) + const source = 'body\t{\n\tcolor:\tred;\n}\n' + const root = parse(source) - let rule = root.first_child! + const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) }) test('no whitespace', () => { - let source = 'body{color:red;margin:0}' - let root = parse(source) + const source = 'body{color:red;margin:0}' + const root = parse(source) - let rule = root.first_child! as Rule + const rule = root.first_child! as Rule const block = rule.block! - let [decl1, decl2] = block.children as Declaration[] + const [decl1, decl2] = block.children as Declaration[] expect(decl1.property).toBe('color') expect(decl2.property).toBe('margin') }) @@ -2479,7 +2482,7 @@ describe('Core Nodes', () => { describe('Complex real-world scenarios', () => { test('complex nested structure', () => { - let source = ` + const source = ` .card { display: flex; padding: 1rem; @@ -2506,15 +2509,15 @@ describe('Core Nodes', () => { } } ` - let root = parse(source) + const root = parse(source) - let card = root.first_child! + const card = root.first_child! expect(card.type).toBe(STYLE_RULE) expect((card as Rule).block).not.toBeNull() }) test('multiple at-rules with nesting', () => { - let source = ` + const source = ` @layer base { body { margin: 0; } } @@ -2529,39 +2532,39 @@ describe('Core Nodes', () => { } } ` - let root = parse(source) + const root = parse(source) - let [layer1, layer2] = root.children + const [layer1, layer2] = root.children expect(layer1.type).toBe(AT_RULE) expect(layer2.type).toBe(AT_RULE) }) test('vendor prefixed properties', () => { - let source = + const source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' - let root = parse(source) + const root = parse(source) - let rule = root.first_child! as Rule + const rule = root.first_child! as Rule const block = rule.block! - let [decl1, decl2, decl3] = block.children as Declaration[] + const [decl1, decl2, decl3] = block.children as Declaration[] expect(decl1.property).toBe('-webkit-transform') expect(decl2.property).toBe('-moz-transform') expect(decl3.property).toBe('transform') }) test('complex selector list', () => { - let source = + const source = 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' - let root = parse(source) + const root = parse(source) - let rule = root.first_child! - let selector = rule.first_child! + const rule = root.first_child! + const selector = rule.first_child! expect(selector.text).toContain('h1') expect(selector.text).toContain('[role="heading"]') }) test('deeply nested at-rules', () => { - let source = ` + const source = ` @supports (display: grid) { @media (min-width: 768px) { @layer utilities { @@ -2570,68 +2573,68 @@ describe('Core Nodes', () => { } } ` - let root = parse(source, { parse_atrule_preludes: false }) + const root = parse(source, { parse_atrule_preludes: false }) - let supports = root.first_child! as Atrule - let supports_block = supports.block! - let media = supports_block.first_child! as Atrule - let media_block = media.block! - let layer = media_block.first_child! as Atrule + const supports = root.first_child! as Atrule + const supports_block = supports.block! + const media = supports_block.first_child! as Atrule + const media_block = media.block! + const layer = media_block.first_child! as Atrule expect(supports.name).toBe('supports') expect(media.name).toBe('media') expect(layer.name).toBe('layer') }) test('CSS with calc() and other functions', () => { - let source = + const source = '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' - let root = parse(source) + const root = parse(source) - let rule = root.first_child! as Rule + const rule = root.first_child! as Rule const block = rule.block! - let [width_decl, bg_decl] = block.children as Declaration[] + const [width_decl, bg_decl] = block.children as Declaration[] expect(width_decl.property).toBe('width') expect(bg_decl.property).toBe('background') }) test('custom properties', () => { - let source = + const source = ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' - let root = parse(source) + const root = parse(source) expect(root.children.length).toBeGreaterThan(0) - let first_rule = root.first_child! + const first_rule = root.first_child! expect(first_rule.type).toBe(STYLE_RULE) }) test('attribute selectors with operators', () => { - let source = '[href^="https"][href$=".pdf"][class*="doc"] { color: red; }' - let root = parse(source) + const source = '[href^="https"][href$=".pdf"][class*="doc"] { color: red; }' + const root = parse(source) - let rule = root.first_child! - let selector = rule.first_child! + const rule = root.first_child! + const selector = rule.first_child! expect(selector.text).toContain('^=') expect(selector.text).toContain('$=') expect(selector.text).toContain('*=') }) test('pseudo-elements', () => { - let source = '.text::before { content: "→"; } .text::after { content: "←"; }' - let root = parse(source) + const source = '.text::before { content: "→"; } .text::after { content: "←"; }' + const root = parse(source) - let [rule1, rule2] = root.children + const [rule1, rule2] = root.children expect(rule1.type).toBe(STYLE_RULE) expect(rule2.type).toBe(STYLE_RULE) }) test('multiple !important declarations', () => { - let source = '.override { color: red !important; margin: 0 !important; padding: 0 !ie; }' - let root = parse(source) + const source = '.override { color: red !important; margin: 0 !important; padding: 0 !ie; }' + const root = parse(source) - let rule = root.first_child! as Rule - let block = rule.block! + const rule = root.first_child! as Rule + const block = rule.block! expect(block.children.length).toBeGreaterThan(1) - let declarations = (block.children as Declaration[]).filter((c) => c.type === DECLARATION) + const declarations = (block.children as Declaration[]).filter((c) => c.type === DECLARATION) expect(declarations.length).toBeGreaterThan(0) expect(declarations[0].is_important).toBe(true) }) @@ -2639,8 +2642,8 @@ describe('Core Nodes', () => { describe('Deeply nested modern CSS', () => { test('@container should parse nested style rules', () => { - let css = `@container (width > 0) { div { color: red; } }` - let ast = parse(css) + const css = `@container (width > 0) { div { color: red; } }` + const ast = parse(css) const container = ast.first_child! as Atrule expect(container.type).toBe(AT_RULE) @@ -2652,8 +2655,8 @@ describe('Core Nodes', () => { }) test('@container should parse rules with :has() selector', () => { - let css = `@container (width > 0) { ul:has(li) { color: red; } }` - let ast = parse(css) + const css = `@container (width > 0) { ul:has(li) { color: red; } }` + const ast = parse(css) const container = ast.first_child! as Atrule const containerBlock = container.block! @@ -2662,7 +2665,7 @@ describe('Core Nodes', () => { }) test('modern CSS example by Vadim Makeev', () => { - let css = ` + const css = ` @layer what { @container (width > 0) { ul:has(:nth-child(1 of li)) { @@ -2674,7 +2677,7 @@ describe('Core Nodes', () => { } } }` - let ast = parse(css) + const ast = parse(css) expect(ast.type).toBe(STYLESHEET) expect(ast.has_children).toBe(true) @@ -2733,8 +2736,8 @@ describe('Core Nodes', () => { describe('Regression tests', () => { test('issue #170: pseudo-class selector at start of rule inside at-rule should not be parsed as TypeSelector', () => { - let source = `@media all {\n\t:focus-visible {}\n}` - let ast = parse(source) + const source = `@media all {\n\t:focus-visible {}\n}` + const ast = parse(source) const mediaRule = ast.first_child! as Atrule expect(mediaRule.type).toBe(AT_RULE) diff --git a/src/parse.ts b/src/parse.ts index 27c57de..d7a0a16 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -55,7 +55,7 @@ export interface ParserOptions { } // Static at-rule lookup sets for fast classification -let DECLARATION_AT_RULES = new Set([ +const DECLARATION_AT_RULES = new Set([ 'font-face', 'font-feature-values', 'page', @@ -82,7 +82,7 @@ export class Parser { constructor(source: string, options?: ParserOptions) { this.source = source - let opts: ParserOptions = options || {} + const opts: ParserOptions = options || {} this.parse_values_enabled = opts.parse_values ?? true this.parse_selectors_enabled = opts.parse_selectors ?? true @@ -90,7 +90,7 @@ export class Parser { this.lexer = new Lexer(source, opts.on_comment) // Calculate optimal capacity based on source size - let capacity = CSSDataArena.capacity_for_source(source.length) + const capacity = CSSDataArena.capacity_for_source(source.length) this.arena = new CSSDataArena(capacity) // Only create parsers if needed @@ -134,13 +134,13 @@ export class Parser { this.next_token() // Create the root stylesheet node - let stylesheet = this.arena.create_node(STYLESHEET, 0, this.source.length, 1, 1) + const stylesheet = this.arena.create_node(STYLESHEET, 0, this.source.length, 1, 1) // Parse all rules at the top level, chained as siblings without an intermediate array let first_rule = 0 let last_rule = 0 while (!this.is_eof()) { - let rule = this.parse_rule() + const rule = this.parse_rule() if (rule === null) { // Skip unknown tokens this.next_token() @@ -191,7 +191,7 @@ export class Parser { let last_child = 0 while (!this.is_eof()) { - let token_type = this.peek_type() + const token_type = this.peek_type() if (token_type === TOKEN_RIGHT_BRACE) break let child: number | null = null @@ -233,12 +233,12 @@ export class Parser { private parse_style_rule(): number | null { if (this.is_eof()) return null - let rule_start = this.lexer.token_start - let rule_line = this.lexer.token_line - let rule_column = this.lexer.token_column + const rule_start = this.lexer.token_start + const rule_line = this.lexer.token_line + const rule_column = this.lexer.token_column // Create the style rule node (length will be set later) - let style_rule = this.arena.create_node( + const style_rule = this.arena.create_node( STYLE_RULE, rule_start, 0, // length unknown yet @@ -247,7 +247,7 @@ export class Parser { ) // Parse selector (everything until '{') - let selector = this.parse_selector() + const selector = this.parse_selector() // Expect '{' if (this.peek_type() !== TOKEN_LEFT_BRACE) { @@ -255,14 +255,14 @@ export class Parser { return null } // Capture block start position (right after '{') before consuming the token - let block_start = this.lexer.token_end + const block_start = this.lexer.token_end this.next_token() // consume '{' this.arena.set_flag(style_rule, FLAG_HAS_BLOCK) // Style rules always have blocks // Create block node (length will be set later) - let block_line = this.lexer.token_line - let block_column = this.lexer.token_column - let block_node = this.arena.create_node( + const block_line = this.lexer.token_line + const block_column = this.lexer.token_column + const block_node = this.arena.create_node( BLOCK, block_start, 0, // length unknown yet @@ -271,7 +271,7 @@ export class Parser { ) // Parse declarations block (and nested rules for CSS Nesting) - let first_child = this.parse_block_children(style_rule, false) + const first_child = this.parse_block_children(style_rule, false) // Expect '}' and calculate lengths (block excludes brace, rule includes it) let block_end = this.lexer.token_start @@ -304,9 +304,9 @@ export class Parser { private parse_selector(): number | null { if (this.is_eof()) return null - let selector_start = this.lexer.token_start - let selector_line = this.lexer.token_line - let selector_column = this.lexer.token_column + const selector_start = this.lexer.token_start + const selector_line = this.lexer.token_line + const selector_column = this.lexer.token_column let last_end = this.lexer.token_end if (this.peek_type() !== TOKEN_LEFT_BRACE) { @@ -320,7 +320,7 @@ export class Parser { // If detailed selector parsing is enabled, use SelectorParser if (this.parse_selectors_enabled && this.selector_parser) { - let selector = this.selector_parser.parse_selector( + const selector = this.selector_parser.parse_selector( selector_start, last_end, selector_line, @@ -332,8 +332,8 @@ export class Parser { } // Create node: RAW when parsing disabled, SELECTOR_LIST as error fallback - let node_type = this.parse_selectors_enabled ? SELECTOR_LIST : RAW - let selector_node = this.arena.create_node( + const node_type = this.parse_selectors_enabled ? SELECTOR_LIST : RAW + const selector_node = this.arena.create_node( node_type, selector_start, last_end - selector_start, @@ -389,19 +389,19 @@ export class Parser { return null } - let at_rule_start = this.lexer.token_start - let at_rule_line = this.lexer.token_line - let at_rule_column = this.lexer.token_column + const at_rule_start = this.lexer.token_start + const at_rule_line = this.lexer.token_line + const at_rule_column = this.lexer.token_column // Extract at-rule name (skip the '@') - let name_start = this.lexer.token_start + 1 - let name_length = this.lexer.token_end - name_start - let at_rule_name = this.source.substring(name_start, this.lexer.token_end) + const name_start = this.lexer.token_start + 1 + const name_length = this.lexer.token_end - name_start + const at_rule_name = this.source.substring(name_start, this.lexer.token_end) this.next_token() // consume @keyword // Create at-rule node (length will be set later) - let at_rule = this.arena.create_node( + const at_rule = this.arena.create_node( AT_RULE, at_rule_start, 0, // length unknown yet @@ -414,7 +414,7 @@ export class Parser { this.arena.set_content_length(at_rule, name_length) // Track prelude start and end - let prelude_start = this.lexer.token_start + const prelude_start = this.lexer.token_start let prelude_end = prelude_start // Track parenthesis depth to handle semicolons inside functions (e.g., url(data:image/png;base64,...)) // NOTE: Same pattern exists in parse-declaration.ts for value parsing - keep in sync @@ -422,7 +422,7 @@ export class Parser { // Parse prelude (everything before '{' or ';') while (!this.is_eof()) { - let token_type = this.peek_type() + const token_type = this.peek_type() // Track parenthesis depth if (token_type === TOKEN_LEFT_PAREN || token_type === TOKEN_FUNCTION) { @@ -439,18 +439,22 @@ export class Parser { } // Store prelude position (trimmed) - let trimmed_start = skip_whitespace_and_comments_forward( + const trimmed_start = skip_whitespace_and_comments_forward( this.source, prelude_start, prelude_end, ) - let trimmed_end = skip_whitespace_and_comments_backward(this.source, prelude_end, trimmed_start) + const trimmed_end = skip_whitespace_and_comments_backward( + this.source, + prelude_end, + trimmed_start, + ) let prelude_wrapper: number | null = null if (trimmed_start < trimmed_end) { // Create AT_RULE_PRELUDE wrapper if prelude parsing is enabled if (this.prelude_parser) { // Parse prelude and add structured nodes as children - let prelude_nodes = this.prelude_parser.parse_prelude( + const prelude_nodes = this.prelude_parser.parse_prelude( at_rule_name, trimmed_start, trimmed_end, @@ -491,14 +495,14 @@ export class Parser { // Check if this at-rule has a block or is a statement if (this.peek_type() === TOKEN_LEFT_BRACE) { // Capture block start position (right after '{') before consuming the token - let block_start = this.lexer.token_end + const block_start = this.lexer.token_end this.next_token() // consume '{' this.arena.set_flag(at_rule, FLAG_HAS_BLOCK) // At-rule has a block // Create block node (length will be set later) - let block_line = this.lexer.token_line - let block_column = this.lexer.token_column - let block_node = this.arena.create_node( + const block_line = this.lexer.token_line + const block_column = this.lexer.token_column + const block_node = this.arena.create_node( BLOCK, block_start, 0, // length unknown yet @@ -509,12 +513,12 @@ export class Parser { // Determine what to parse inside the block based on the at-rule name: // declarations only (like @font-face, @page), or declarations + rules + at-rules // (like @media, @keyframes, unknown at-rules, and nested conditional groups) - let declarations_only = this.atrule_has_declarations(at_rule_name) - let first_child = this.parse_block_children(at_rule, declarations_only) + const declarations_only = this.atrule_has_declarations(at_rule_name) + const first_child = this.parse_block_children(at_rule, declarations_only) // Consume '}' (block excludes closing brace, but at-rule includes it) if (this.peek_type() === TOKEN_RIGHT_BRACE) { - let block_end = this.lexer.token_start // Position of '}' (not included in block) + const block_end = this.lexer.token_start // Position of '}' (not included in block) last_end = this.lexer.token_end // Position after '}' (included in at-rule) this.next_token() diff --git a/src/string-utils.ts b/src/string-utils.ts index dcc61ef..a74aee9 100644 --- a/src/string-utils.ts +++ b/src/string-utils.ts @@ -59,7 +59,7 @@ export function str_equals(a: string, b: string): boolean { } for (let i = 0; i < a.length; i++) { - let ca = a.charCodeAt(i) + const ca = a.charCodeAt(i) let cb = b.charCodeAt(i) // normalize ASCII uppercase A-Z → a-z @@ -101,7 +101,7 @@ export function str_equals_range( // True if `source[start, end)` is and/or/not (case-insensitive) export function is_and_or_not_range(source: string, start: number, end: number): boolean { - let length = end - start + const length = end - start if (length === 3) { return ( str_equals_range(source, start, end, 'and') || str_equals_range(source, start, end, 'not') @@ -118,7 +118,7 @@ export function str_starts_with(str: string, prefix: string): boolean { for (let i = 0; i < prefix.length; i++) { let ca = str.charCodeAt(i) - let cb = prefix.charCodeAt(i) + const cb = prefix.charCodeAt(i) // normalize only the string char (prefix is already lowercase) if (ca >= 65 && ca <= 90) ca |= 32 // A-Z → a-z @@ -156,7 +156,7 @@ export function str_index_of(str: string, searchChar: string): number { let match = true for (let j = 0; j < searchChar.length; j++) { let ca = str.charCodeAt(i + j) - let cb = searchChar.charCodeAt(j) + const cb = searchChar.charCodeAt(j) if (ca >= 65 && ca <= 90) ca |= 32 // A-Z → a-z if (ca !== cb) { match = false @@ -193,7 +193,7 @@ export function is_vendor_prefixed(source: string, start?: number, end?: number) } // Must be at least 3 chars (-o- is shortest vendor prefix) - let length = end - start + const length = end - start if (length < 3) { return false } diff --git a/src/tokenize.test.ts b/src/tokenize.test.ts index 3e256ee..aa436ba 100644 --- a/src/tokenize.test.ts +++ b/src/tokenize.test.ts @@ -52,8 +52,8 @@ describe('Lexer', () => { }) test('should tokenize comma', () => { - let lexer = new Lexer(',') - let token = lexer.next_token() + const lexer = new Lexer(',') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_COMMA) }) @@ -80,27 +80,27 @@ describe('Lexer', () => { describe('whitespace', () => { test('should tokenize spaces', () => { - let lexer = new Lexer(' ') - let token = lexer.next_token() + const lexer = new Lexer(' ') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_WHITESPACE) expect(token!.end - token!.start).toBe(3) }) test('should tokenize tabs', () => { - let lexer = new Lexer('\t\t') - let token = lexer.next_token() + const lexer = new Lexer('\t\t') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_WHITESPACE) }) test('should tokenize newlines', () => { - let lexer = new Lexer('\n\r\n') - let token = lexer.next_token() + const lexer = new Lexer('\n\r\n') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_WHITESPACE) expect(lexer.line).toBe(3) }) test('should track line', () => { - let lexer = new Lexer('a\nb') + const lexer = new Lexer('a\nb') let token = lexer.next_token() // a expect(token?.line).toBe(1) @@ -146,67 +146,67 @@ describe('Lexer', () => { describe('strings', () => { test('should tokenize double-quoted strings', () => { - let lexer = new Lexer('"hello"') - let token = lexer.next_token() + const lexer = new Lexer('"hello"') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_STRING) expect(lexer.source.slice(token!.start, token!.end)).toBe('"hello"') }) test('should tokenize single-quoted strings', () => { - let lexer = new Lexer("'hello'") - let token = lexer.next_token() + const lexer = new Lexer("'hello'") + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_STRING) }) test('should handle escaped quotes', () => { - let lexer = new Lexer('"hello \\"world\\""') - let token = lexer.next_token() + const lexer = new Lexer('"hello \\"world\\""') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_STRING) }) test('should handle hex escapes', () => { - let lexer = new Lexer('"\\20\\000020"') - let token = lexer.next_token() + const lexer = new Lexer('"\\20\\000020"') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_STRING) }) test('should handle escaped newlines', () => { - let lexer = new Lexer('"line\\\ncontinued"') - let token = lexer.next_token() + const lexer = new Lexer('"line\\\ncontinued"') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_STRING) }) test('should mark unclosed strings as bad', () => { - let lexer = new Lexer('"unclosed\n') - let token = lexer.next_token() + const lexer = new Lexer('"unclosed\n') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_BAD_STRING) }) test('should mark EOF unclosed strings as bad', () => { - let lexer = new Lexer('"unclosed') - let token = lexer.next_token() + const lexer = new Lexer('"unclosed') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_BAD_STRING) }) }) describe('numbers', () => { test('should tokenize integers', () => { - let lexer = new Lexer('123') - let token = lexer.next_token() + const lexer = new Lexer('123') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_NUMBER) expect(lexer.source.slice(token!.start, token!.end)).toBe('123') }) test('should tokenize decimals', () => { - let lexer = new Lexer('123.456') - let token = lexer.next_token() + const lexer = new Lexer('123.456') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_NUMBER) expect(lexer.source.slice(token!.start, token!.end)).toBe('123.456') }) test('should tokenize decimals starting with dot', () => { - let lexer = new Lexer('.5') - let token = lexer.next_token() + const lexer = new Lexer('.5') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_NUMBER) }) @@ -234,36 +234,36 @@ describe('Lexer', () => { describe('percentages', () => { test('should tokenize percentages', () => { - let lexer = new Lexer('50%') - let token = lexer.next_token() + const lexer = new Lexer('50%') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_PERCENTAGE) expect(lexer.source.slice(token!.start, token!.end)).toBe('50%') }) test('should tokenize negative percentages', () => { - let lexer = new Lexer('-50%') - let token = lexer.next_token() + const lexer = new Lexer('-50%') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_PERCENTAGE) expect(lexer.source.slice(token!.start, token!.end)).toBe('-50%') }) test('should tokenize positive percentages', () => { - let lexer = new Lexer('+50%') - let token = lexer.next_token() + const lexer = new Lexer('+50%') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_PERCENTAGE) expect(lexer.source.slice(token!.start, token!.end)).toBe('+50%') }) test('should tokenize fraction percentages', () => { - let lexer = new Lexer('+5.5%') - let token = lexer.next_token() + const lexer = new Lexer('+5.5%') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_PERCENTAGE) expect(lexer.source.slice(token!.start, token!.end)).toBe('+5.5%') }) test('should tokenize scientific fraction percentages', () => { - let lexer = new Lexer('+5.2e-10%') - let token = lexer.next_token() + const lexer = new Lexer('+5.2e-10%') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_PERCENTAGE) expect(lexer.source.slice(token!.start, token!.end)).toBe('+5.2e-10%') }) @@ -271,17 +271,17 @@ describe('Lexer', () => { describe('dimensions', () => { test('should tokenize pixel dimensions', () => { - let lexer = new Lexer('10px') - let token = lexer.next_token() + const lexer = new Lexer('10px') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_DIMENSION) expect(lexer.source.slice(token!.start, token!.end)).toBe('10px') }) test('should tokenize various units', () => { const units = ['em', 'rem', 'vh', 'vw', 'ch', 'ex', 'cm', 'mm', 'in', 'pt', 'pc'] - for (let unit of units) { - let lexer = new Lexer(`10${unit}`) - let token = lexer.next_token() + for (const unit of units) { + const lexer = new Lexer(`10${unit}`) + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_DIMENSION) } }) @@ -289,30 +289,30 @@ describe('Lexer', () => { describe('identifiers', () => { test('should tokenize identifiers', () => { - let lexer = new Lexer('color') - let token = lexer.next_token() + const lexer = new Lexer('color') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('color') }) test('should tokenize vendor prefixes', () => { - let lexer = new Lexer('-webkit-transform') - let token = lexer.next_token() + const lexer = new Lexer('-webkit-transform') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('-webkit-transform') }) test('should tokenize CSS custom properties (variables)', () => { let lexer = new Lexer('--test') - let token = lexer.next_token() + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('--test') // Test in context: a{--custom-prop:1} lexer = new Lexer('a{--custom-prop:1}') - let tokens = [] + const tokens = [] while (true) { - let t = lexer.next_token() + const t = lexer.next_token() if (!t || t.type === TOKEN_EOF) break tokens.push(t) } @@ -331,30 +331,30 @@ describe('Lexer', () => { }) test('should tokenize identifiers with underscores', () => { - let lexer = new Lexer('_private') - let token = lexer.next_token() + const lexer = new Lexer('_private') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) }) test('should tokenize identifiers with digits', () => { - let lexer = new Lexer('grid2') - let token = lexer.next_token() + const lexer = new Lexer('grid2') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) }) describe('escape sequences', () => { test('should handle hex escape sequences', () => { // \32 = character code 0x32 (the digit '2') - let lexer = new Lexer('\\32xl') - let token = lexer.next_token() + const lexer = new Lexer('\\32xl') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('\\32xl') }) test('should handle character escape sequences', () => { // \: = escaped colon - let lexer = new Lexer('\\:ease-in') - let token = lexer.next_token() + const lexer = new Lexer('\\:ease-in') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('\\:ease-in') }) @@ -362,10 +362,10 @@ describe('Lexer', () => { test('should handle combined escape sequences (Tailwind CSS)', () => { // This is the exact case from the bug report: .\\32xl\\:ease-in{} // Should tokenize as: DELIM(.) + IDENT(\32xl\:ease-in) + LEFT_BRACE + RIGHT_BRACE - let lexer = new Lexer('.\\32xl\\:ease-in{}') - let tokens = [] + const lexer = new Lexer('.\\32xl\\:ease-in{}') + const tokens = [] while (true) { - let t = lexer.next_token() + const t = lexer.next_token() if (!t || t.type === TOKEN_EOF) break tokens.push(t) } @@ -381,52 +381,52 @@ describe('Lexer', () => { test('should handle whitespace after hex escapes', () => { // Per CSS spec, hex escapes can be followed by optional whitespace // \32 xl = character 0x32 followed by 'xl' - let lexer = new Lexer('\\32 xl') - let token = lexer.next_token() + const lexer = new Lexer('\\32 xl') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('\\32 xl') }) test('should handle 6-digit hex escapes', () => { // Maximum 6 hex digits per escape - let lexer = new Lexer('\\000032xl') - let token = lexer.next_token() + const lexer = new Lexer('\\000032xl') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('\\000032xl') }) test('should handle multiple escapes in sequence', () => { - let lexer = new Lexer('\\32\\33\\34') - let token = lexer.next_token() + const lexer = new Lexer('\\32\\33\\34') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('\\32\\33\\34') }) test('should handle escape at start of identifier', () => { - let lexer = new Lexer('\\:hover') - let token = lexer.next_token() + const lexer = new Lexer('\\:hover') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('\\:hover') }) test('should handle escape at end of identifier', () => { - let lexer = new Lexer('ease-in\\:') - let token = lexer.next_token() + const lexer = new Lexer('ease-in\\:') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('ease-in\\:') }) test('should stop at newline after backslash', () => { // Backslash followed by newline is invalid in identifier - let lexer = new Lexer('test\\\nmore') - let token = lexer.next_token() + const lexer = new Lexer('test\\\nmore') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_IDENT) expect(lexer.source.slice(token!.start, token!.end)).toBe('test') }) test('should handle escapes in functions', () => { - let lexer = new Lexer('\\32xl\\:fn(') - let token = lexer.next_token() + const lexer = new Lexer('\\32xl\\:fn(') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_FUNCTION) expect(lexer.source.slice(token!.start, token!.end)).toBe('\\32xl\\:fn(') }) @@ -435,96 +435,96 @@ describe('Lexer', () => { describe('functions', () => { test('should tokenize functions', () => { - let lexer = new Lexer('rgb(') - let token = lexer.next_token() + const lexer = new Lexer('rgb(') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_FUNCTION) expect(lexer.source.slice(token!.start, token!.end)).toBe('rgb(') }) test('should tokenize vendor-prefixed functions', () => { - let lexer = new Lexer('-webkit-calc(') - let token = lexer.next_token() + const lexer = new Lexer('-webkit-calc(') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_FUNCTION) }) }) describe('at-keywords', () => { test('should tokenize @media', () => { - let lexer = new Lexer('@media') - let token = lexer.next_token() + const lexer = new Lexer('@media') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_AT_KEYWORD) expect(lexer.source.slice(token!.start, token!.end)).toBe('@media') }) test('should tokenize @keyframes', () => { - let lexer = new Lexer('@keyframes') - let token = lexer.next_token() + const lexer = new Lexer('@keyframes') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_AT_KEYWORD) }) test('should tokenize @-webkitkeyframes', () => { - let lexer = new Lexer('@-webkitkeyframes') - let token = lexer.next_token() + const lexer = new Lexer('@-webkitkeyframes') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_AT_KEYWORD) }) }) describe('hash', () => { test('should tokenize hash colors', () => { - let lexer = new Lexer('#fff') - let token = lexer.next_token() + const lexer = new Lexer('#fff') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_HASH) expect(lexer.source.slice(token!.start, token!.end)).toBe('#fff') }) test('should tokenize hash IDs', () => { - let lexer = new Lexer('#main-content') - let token = lexer.next_token() + const lexer = new Lexer('#main-content') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_HASH) }) }) describe('CDO/CDC', () => { test('should tokenize CDO', () => { - let lexer = new Lexer('') - let token = lexer.next_token() + const lexer = new Lexer('-->') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_CDC) }) }) describe('delimiters', () => { test('should tokenize unknown characters as delimiters', () => { - let lexer = new Lexer('~') - let token = lexer.next_token() + const lexer = new Lexer('~') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_DELIM) }) test('should tokenize + as delimiter when not part of number', () => { - let lexer = new Lexer('+ ') - let token = lexer.next_token() + const lexer = new Lexer('+ ') + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_DELIM) }) }) describe('EOF', () => { test('should return EOF token at end', () => { - let lexer = new Lexer('a') + const lexer = new Lexer('a') lexer.next_token() // a - let token = lexer.next_token() + const token = lexer.next_token() expect(token?.type).toBe(TOKEN_EOF) }) }) describe('real CSS', () => { test('should tokenize a CSS rule', () => { - let lexer = new Lexer('.class { color: red; }') - let tokens = [] + const lexer = new Lexer('.class { color: red; }') + const tokens = [] let token while ((token = lexer.next_token()) && token.type !== TOKEN_EOF) { tokens.push(token.type) @@ -546,8 +546,8 @@ describe('Lexer', () => { }) test('should tokenize declaration with number and unit', () => { - let lexer = new Lexer('width: 100px') - let tokens = [] + const lexer = new Lexer('width: 100px') + const tokens = [] let token while ((token = lexer.next_token()) && token.type !== TOKEN_EOF) { if (token.type !== TOKEN_WHITESPACE) { @@ -560,7 +560,7 @@ describe('Lexer', () => { describe('column tracking', () => { test('should track column for single line tokens', () => { - let lexer = new Lexer('body { color: red; }') + const lexer = new Lexer('body { color: red; }') let token token = lexer.next_token() // 'body' @@ -585,7 +585,7 @@ describe('Lexer', () => { }) test('should track column across multiple lines', () => { - let lexer = new Lexer('body {\n color: red;\n}') + const lexer = new Lexer('body {\n color: red;\n}') let token token = lexer.next_token() // 'body' @@ -614,7 +614,7 @@ describe('Lexer', () => { }) test('should reset column to 1 after newline', () => { - let lexer = new Lexer('a\nb') + const lexer = new Lexer('a\nb') let token token = lexer.next_token() // 'a' @@ -631,7 +631,7 @@ describe('Lexer', () => { }) test('should track column for multi-character tokens', () => { - let lexer = new Lexer('font-size: 16px;') + const lexer = new Lexer('font-size: 16px;') let token token = lexer.next_token() // 'font-size' @@ -653,7 +653,7 @@ describe('Lexer', () => { }) test('should track column for strings', () => { - let lexer = new Lexer('content: "hello world";') + const lexer = new Lexer('content: "hello world";') let token token = lexer.next_token() // 'content' @@ -670,7 +670,7 @@ describe('Lexer', () => { }) test('should track column for comments', () => { - let lexer = new Lexer('/* comment */ body') + const lexer = new Lexer('/* comment */ body') let token token = lexer.next_token() // ' ' @@ -685,7 +685,7 @@ describe('Lexer', () => { }) test('should track column for multi-line comments', () => { - let lexer = new Lexer('/* line1\nline2 */ a') + const lexer = new Lexer('/* line1\nline2 */ a') let token token = lexer.next_token() // ' ' @@ -699,7 +699,7 @@ describe('Lexer', () => { }) test('should track column for numbers with decimals', () => { - let lexer = new Lexer('opacity: 0.5;') + const lexer = new Lexer('opacity: 0.5;') let token token = lexer.next_token() // 'opacity' @@ -715,7 +715,7 @@ describe('Lexer', () => { }) test('should track column for at-keywords', () => { - let lexer = new Lexer('@media screen') + const lexer = new Lexer('@media screen') let token token = lexer.next_token() // '@media' @@ -730,7 +730,7 @@ describe('Lexer', () => { }) test('should handle \\r\\n as single newline for column counting', () => { - let lexer = new Lexer('a\r\nb') + const lexer = new Lexer('a\r\nb') let token token = lexer.next_token() // 'a' @@ -756,7 +756,7 @@ describe('Lexer', () => { const oracle_digit = (c: number) => c >= 0x30 && c <= 0x39 test('is_ident_start matches letters, underscore and non-ASCII for every code unit', () => { - let mismatches: string[] = [] + const mismatches: string[] = [] for (let c = 0; c <= 0xffff; c++) { if (is_ident_start(c) !== oracle_ident_start(c)) mismatches.push(`0x${c.toString(16)}`) } @@ -775,12 +775,12 @@ describe('Lexer', () => { 0xfeff, 0xffff, ] - let mismatches: string[] = [] - for (let c of codes) { - let lexer = new Lexer(String.fromCharCode(c)) + const mismatches: string[] = [] + for (const c of codes) { + const lexer = new Lexer(String.fromCharCode(c)) lexer.next_token_fast(false) - let type = lexer.token_type - let ok = + const type = lexer.token_type + const ok = (type === TOKEN_IDENT) === oracle_ident_start(c) && (type === TOKEN_WHITESPACE) === oracle_whitespace(c) && (type === TOKEN_NUMBER) === oracle_digit(c) && @@ -793,8 +793,8 @@ describe('Lexer', () => { }) test('skip_whitespace=true lands on the same token as an explicit whitespace token', () => { - let source = ' \t\n\r\f_x' - let lexer = new Lexer(source) + const source = ' \t\n\r\f_x' + const lexer = new Lexer(source) lexer.next_token_fast(true) expect(lexer.token_type).toBe(TOKEN_IDENT) expect(source.slice(lexer.token_start, lexer.token_end)).toBe('_x') @@ -802,8 +802,8 @@ describe('Lexer', () => { }) test('identifiers may start with underscore, a letter or a non-ASCII character', () => { - for (let source of ['_a', 'a', 'Z', 'é', '日本', '\u{1F600}x']) { - let lexer = new Lexer(source) + for (const source of ['_a', 'a', 'Z', 'é', '日本', '\u{1F600}x']) { + const lexer = new Lexer(source) lexer.next_token_fast(false) expect(lexer.token_type).toBe(TOKEN_IDENT) expect(lexer.token_end).toBe(source.length) @@ -811,8 +811,8 @@ describe('Lexer', () => { }) test('digits start numbers and are not identifiers', () => { - for (let source of ['0', '9', '5px', '.5']) { - let lexer = new Lexer(source) + for (const source of ['0', '9', '5px', '.5']) { + const lexer = new Lexer(source) lexer.next_token_fast(false) expect(lexer.token_type === TOKEN_IDENT).toBe(false) } diff --git a/src/tokenize.ts b/src/tokenize.ts index 030d32a..4b65005 100644 --- a/src/tokenize.ts +++ b/src/tokenize.ts @@ -173,7 +173,7 @@ export class Lexer { let line = this._line let line_offset = this._line_offset while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch >= 128 || (char_types[ch] & CHAR_WHITESPACE_OR_NEWLINE) === 0) break pos++ if ((char_types[ch] & CHAR_NEWLINE) !== 0) { @@ -197,10 +197,10 @@ export class Lexer { return this.make_token(TOKEN_EOF, this.pos, this.pos) } - let ch = source.charCodeAt(this.pos) - let start = this.pos - let start_line = this._line - let start_column = this.pos - this._line_offset + 1 + const ch = source.charCodeAt(this.pos) + const start = this.pos + const start_line = this._line + const start_column = this.pos - this._line_offset + 1 // Fast path for single-character tokens — pos++ safe (none are newlines) switch (ch) { @@ -236,7 +236,7 @@ export class Lexer { // One table read classifies ch for the dispatch below (non-ASCII → 0, handled via // ch >= 128). Deferred until after the switch so the 9 single-char-token cases // above — among the most common bytes in CSS ({ } : ; , [ ] ( )) — skip this lookup. - let ch_type = ch < 128 ? char_types[ch] : 0 + const ch_type = ch < 128 ? char_types[ch] : 0 // Whitespace if ((ch_type & CHAR_WHITESPACE_OR_NEWLINE) !== 0) { @@ -266,7 +266,7 @@ export class Lexer { // legacy --> CDC token — consolidated into one block sharing a single lookahead read. // CDC must be checked first: "-->" would otherwise read as identifier "--" plus ">". if (ch === CHAR_HYPHEN) { - let next = this.pos + 1 < source_length ? source.charCodeAt(this.pos + 1) : 0 + const next = this.pos + 1 < source_length ? source.charCodeAt(this.pos + 1) : 0 if ( next === CHAR_HYPHEN && @@ -286,7 +286,7 @@ export class Lexer { return this.consume_number(start_line, start_column) } if (next === CHAR_DOT) { - let next2 = this.pos + 2 < source_length ? source.charCodeAt(this.pos + 2) : 0 + const next2 = this.pos + 2 < source_length ? source.charCodeAt(this.pos + 2) : 0 if (next2 < 128 && (char_types[next2] & CHAR_DIGIT) !== 0) { return this.consume_number(start_line, start_column) } @@ -295,7 +295,7 @@ export class Lexer { // Backslash: escape sequence starting an identifier if (ch === CHAR_BACKSLASH) { - let next = this.pos + 1 < source_length ? source.charCodeAt(this.pos + 1) : 0 + const next = this.pos + 1 < source_length ? source.charCodeAt(this.pos + 1) : 0 if (next !== 0 && !is_newline(next)) { return this.consume_ident_or_function(start_line, start_column) } @@ -307,7 +307,7 @@ export class Lexer { } if (ch === CHAR_DOT) { - let next = this.pos + 1 < source_length ? source.charCodeAt(this.pos + 1) : 0 + const next = this.pos + 1 < source_length ? source.charCodeAt(this.pos + 1) : 0 if (next < 128 && (char_types[next] & CHAR_DIGIT) !== 0) { return this.consume_number(start_line, start_column) } @@ -315,12 +315,12 @@ export class Lexer { // Plus: could be a signed number like +5 (the hyphen-led case is handled above) if (ch === CHAR_PLUS) { - let next = this.pos + 1 < source_length ? source.charCodeAt(this.pos + 1) : 0 + const next = this.pos + 1 < source_length ? source.charCodeAt(this.pos + 1) : 0 if (next < 128 && (char_types[next] & CHAR_DIGIT) !== 0) { return this.consume_number(start_line, start_column) } if (next === CHAR_DOT) { - let next2 = this.pos + 2 < source_length ? source.charCodeAt(this.pos + 2) : 0 + const next2 = this.pos + 2 < source_length ? source.charCodeAt(this.pos + 2) : 0 if (next2 < 128 && (char_types[next2] & CHAR_DIGIT) !== 0) { return this.consume_number(start_line, start_column) } @@ -366,7 +366,7 @@ export class Lexer { private _scan_newlines(from: number, to: number): void { const source = this.source for (let i = from; i < to; i++) { - let c = source.charCodeAt(i) + const c = source.charCodeAt(i) if (c === CHAR_LINE_FEED) { this._line++ this._line_offset = i + 1 @@ -388,16 +388,16 @@ export class Lexer { private _skip_comment(): void { const source = this.source const source_length = source.length - let comment_start = this.pos - let comment_line = this._line - let comment_column = this.pos - this._line_offset + 1 + const comment_start = this.pos + const comment_line = this._line + const comment_column = this.pos - this._line_offset + 1 // Neither / nor * are newlines — safe to skip without newline tracking this.pos += 2 // Use native string search to find */ — dramatically faster than a JS loop // for typical comment bodies (SIMD-accelerated in V8). - let end_idx = source.indexOf('*/', this.pos) + const end_idx = source.indexOf('*/', this.pos) if (end_idx < 0) { // Unterminated comment: scan tail for newlines, advance to EOF this._scan_newlines(this.pos, source_length) @@ -436,7 +436,7 @@ export class Lexer { let pos = this.pos while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch === target) { this.pos = pos @@ -496,7 +496,7 @@ export class Lexer { let pos = this.pos while (pos < end) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch < 128 && DECL_VALUE_STOP[ch] !== 0) { this.pos = pos @@ -542,14 +542,14 @@ export class Lexer { consume_whitespace(start_line: number, start_column: number): TokenType { const source = this.source const source_length = source.length - let start = this.pos + const start = this.pos // Hoisted to locals (synced back once), same as next_token_fast's inline skip. let pos = start let line = this._line let line_offset = this._line_offset // Inline advance: read ch once, pos++, then track newlines with already-read value while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch >= 128 || (char_types[ch] & CHAR_WHITESPACE_OR_NEWLINE) === 0) break pos++ if ((char_types[ch] & CHAR_NEWLINE) !== 0) { @@ -573,11 +573,11 @@ export class Lexer { consume_string(quote: number, start_line: number, start_column: number): TokenType { const source = this.source const source_length = source.length - let start = this.pos + const start = this.pos this.pos++ // Skip opening quote — quote chars are never newlines while (this.pos < source_length) { - let ch = source.charCodeAt(this.pos) + const ch = source.charCodeAt(this.pos) // Closing quote if (ch === quote) { @@ -594,7 +594,7 @@ export class Lexer { if (ch === CHAR_BACKSLASH) { this.pos++ // \ is never a newline if (this.pos < source_length) { - let next = source.charCodeAt(this.pos) + const next = source.charCodeAt(this.pos) if (is_hex_digit(next)) { this.consume_hex_escape() } else if (next < 128 && (char_types[next] & CHAR_NEWLINE) !== 0) { @@ -631,14 +631,14 @@ export class Lexer { // Consume up to 6 hex digits — hex digits are never newlines, use pos++ let count = 0 while (count < 6 && this.pos < source_length) { - let ch = source.charCodeAt(this.pos) + const ch = source.charCodeAt(this.pos) if (!is_hex_digit(ch)) break this.pos++ // hex digits: 0-9, a-f, A-F — never newlines count++ } // Optional whitespace after hex escape (may be a newline — inline advance) if (this.pos < source_length) { - let ch = source.charCodeAt(this.pos) + const ch = source.charCodeAt(this.pos) if (ch < 128 && (char_types[ch] & CHAR_WHITESPACE_OR_NEWLINE) !== 0) { this.pos++ if ((char_types[ch] & CHAR_NEWLINE) !== 0) { @@ -659,31 +659,31 @@ export class Lexer { consume_number(start_line: number, start_column: number): TokenType { const source = this.source const source_length = source.length - let start = this.pos + const start = this.pos // Numbers never contain newlines, so the whole scan can run on a local var — // no line/column bookkeeping to keep in sync with this.pos along the way. let pos = start // Optional sign — + and - are never newlines - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch === CHAR_PLUS || ch === CHAR_HYPHEN) { pos++ } // Integer part — digits are never newlines, use pos++ while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch >= 128 || (char_types[ch] & CHAR_DIGIT) === 0) break pos++ } // Decimal part if (pos < source_length && source.charCodeAt(pos) === CHAR_DOT && pos + 1 < source_length) { - let next = source.charCodeAt(pos + 1) + const next = source.charCodeAt(pos + 1) if (next < 128 && (char_types[next] & CHAR_DIGIT) !== 0) { pos++ // . is never a newline while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch >= 128 || (char_types[ch] & CHAR_DIGIT) === 0) break pos++ // digits: never newlines } @@ -692,22 +692,22 @@ export class Lexer { // Exponent: e or E if (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch === CHAR_LOWERCASE_E || ch === CHAR_UPPERCASE_E) { - let next = pos + 1 < source_length ? source.charCodeAt(pos + 1) : 0 - let is_next_digit = next < 128 && (char_types[next] & CHAR_DIGIT) !== 0 - let next2 = pos + 2 < source_length ? source.charCodeAt(pos + 2) : 0 - let is_next2_digit = next2 < 128 && (char_types[next2] & CHAR_DIGIT) !== 0 + const next = pos + 1 < source_length ? source.charCodeAt(pos + 1) : 0 + const is_next_digit = next < 128 && (char_types[next] & CHAR_DIGIT) !== 0 + const next2 = pos + 2 < source_length ? source.charCodeAt(pos + 2) : 0 + const is_next2_digit = next2 < 128 && (char_types[next2] & CHAR_DIGIT) !== 0 if (is_next_digit || ((next === CHAR_PLUS || next === CHAR_HYPHEN) && is_next2_digit)) { pos++ // e/E — never a newline if (pos < source_length) { - let sign = source.charCodeAt(pos) + const sign = source.charCodeAt(pos) if (sign === CHAR_PLUS || sign === CHAR_HYPHEN) { pos++ // +/- — never a newline } } while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch >= 128 || (char_types[ch] & CHAR_DIGIT) === 0) break pos++ // digits: never newlines } @@ -717,7 +717,7 @@ export class Lexer { // Check for unit (dimension) or percentage if (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch === CHAR_PERCENT) { pos++ // % is never a newline this.pos = pos @@ -730,7 +730,7 @@ export class Lexer { ) { // Unit: px, em, rem, etc — ident chars and non-ASCII are never newlines while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch < 0x80 && (char_types[ch] & CHAR_IDENT) === 0) break pos++ } @@ -746,7 +746,7 @@ export class Lexer { consume_ident_or_function(start_line: number, start_column: number): TokenType { const source = this.source const source_length = source.length - let start = this.pos + const start = this.pos // Hoisted to a local: the common case below (plain ident chars, no escapes) never // touches line/column, so it can advance a local var instead of this.pos on every // char, syncing back only when the rare backslash-escape path needs this.pos (which @@ -755,14 +755,14 @@ export class Lexer { // Consume identifier (with escape sequence support) while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) // Handle escape sequences: \ followed by hex digits or any character if (ch === CHAR_BACKSLASH) { this.pos = pos if (this.pos + 1 >= source_length) break - let next = source.charCodeAt(this.pos + 1) + const next = source.charCodeAt(this.pos + 1) if (is_newline(next)) break @@ -777,7 +777,7 @@ export class Lexer { } // Consume optional whitespace after hex escape (may be newline) if (this.pos < source_length) { - let ws = source.charCodeAt(this.pos) + const ws = source.charCodeAt(this.pos) if (ws < 128 && (char_types[ws] & CHAR_WHITESPACE_OR_NEWLINE) !== 0) { this.advance() } @@ -800,7 +800,7 @@ export class Lexer { // Check for unicode-range: u+ or U+ if (this.pos - start === 1) { - let first_ch = source.charCodeAt(start) + const first_ch = source.charCodeAt(start) if ( (first_ch === CHAR_LOWERCASE_U || first_ch === CHAR_UPPERCASE_U) && this.pos < source_length && @@ -831,7 +831,7 @@ export class Lexer { // Consume hex digits and/or question marks (up to 6 total) // Hex digits and ? are never newlines — use pos++ while (this.pos < source_length && hex_digits < 6) { - let ch = source.charCodeAt(this.pos) + const ch = source.charCodeAt(this.pos) if (is_hex_digit(ch)) { if (has_question) break this.pos++ @@ -856,7 +856,7 @@ export class Lexer { let end_hex_digits = 0 while (this.pos < source_length && end_hex_digits < 6) { - let ch = source.charCodeAt(this.pos) + const ch = source.charCodeAt(this.pos) if (is_hex_digit(ch)) { this.pos++ // hex digits: never newlines end_hex_digits++ @@ -873,12 +873,12 @@ export class Lexer { consume_at_keyword(start_line: number, start_column: number): TokenType { const source = this.source const source_length = source.length - let start = this.pos + const start = this.pos let pos = start + 1 // Skip @ — never a newline // Ident chars (a-z,A-Z,0-9,-,_) and non-ASCII are never newlines — use pos++ while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch < 0x80 && (char_types[ch] & CHAR_IDENT) === 0) break pos++ } @@ -890,12 +890,12 @@ export class Lexer { consume_hash(start_line: number, start_column: number): TokenType { const source = this.source const source_length = source.length - let start = this.pos + const start = this.pos let pos = start + 1 // Skip # — never a newline // Ident chars and non-ASCII are never newlines — use pos++ while (pos < source_length) { - let ch = source.charCodeAt(pos) + const ch = source.charCodeAt(pos) if (ch < 0x80 && (char_types[ch] & CHAR_IDENT) === 0) break pos++ } @@ -909,7 +909,7 @@ export class Lexer { if (count === 1) { if (this.pos >= this.source.length) return - let ch = this.source.charCodeAt(this.pos) + const ch = this.source.charCodeAt(this.pos) this.pos++ // Inline newline check - only update on newline @@ -932,7 +932,7 @@ export class Lexer { for (let i = 0; i < count; i++) { if (this.pos >= this.source.length) break - let ch = this.source.charCodeAt(this.pos) + const ch = this.source.charCodeAt(this.pos) this.pos++ // Inline newline check - only update on newline @@ -953,7 +953,7 @@ export class Lexer { } peek(offset: number = 1): number { - let index = this.pos + offset + const index = this.pos + offset if (index >= this.source.length) return 0 return this.source.charCodeAt(index) } @@ -1038,7 +1038,7 @@ export class Lexer { */ skip_whitespace_in_range(end: number): void { while (this.pos < end) { - let ch = this.source.charCodeAt(this.pos) + const ch = this.source.charCodeAt(this.pos) // Skip whitespace (space, tab, and newlines — is_whitespace() from string-utils // covers newlines too, but here we go straight to char_types to avoid the import) diff --git a/src/value-node-parser.ts b/src/value-node-parser.ts index 4e72eb7..5a9c487 100644 --- a/src/value-node-parser.ts +++ b/src/value-node-parser.ts @@ -101,11 +101,11 @@ export class ValueNodeParser { // Stop if we've reached the end of the range if (this.lexer.token_start >= this.end) break - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_EOF) break // Parse this token into a value node (token_type already cached in lexer.token_type) - let node = this.parse_value_node() + const node = this.parse_value_node() if (node !== null) { if (first_node === 0) { first_node = node @@ -121,9 +121,9 @@ export class ValueNodeParser { } private parse_value_node(): number | null { - let token_type = this.lexer.token_type - let start = this.lexer.token_start - let end = this.lexer.token_end + const token_type = this.lexer.token_type + const start = this.lexer.token_start + const end = this.lexer.token_end switch (token_type) { case TOKEN_IDENT: @@ -166,7 +166,7 @@ export class ValueNodeParser { } private create_node(node_type: number, start: number, end: number): number { - let node = this.arena.create_node( + const node = this.arena.create_node( node_type, start, end - start, @@ -180,7 +180,7 @@ export class ValueNodeParser { private parse_operator_node(start: number, end: number): number | null { // Only create operator nodes for specific delimiters: + - * / - let ch = this.source.charCodeAt(start) + const ch = this.source.charCodeAt(start) if ( ch === CHAR_PLUS || ch === CHAR_MINUS_HYPHEN || @@ -212,7 +212,7 @@ export class ValueNodeParser { while ((!bounded || this.lexer.pos < this.end) && depth > 0) { this.lexer.next_token_fast(false) - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_EOF) break if (bounded && this.lexer.token_start >= this.end) break @@ -235,7 +235,7 @@ export class ValueNodeParser { private parse_function_node(start: number, end: number): number { // Function name is everything before the '(' // The lexer's TOKEN_FUNCTION includes the '(' at the end - let name_end = end - 1 // Exclude the '(' + const name_end = end - 1 // Exclude the '(' // Check function name for special handling without allocating a substring — most // function calls (calc(), var(), rgb(), translate(), ...) aren't any of these, and @@ -246,10 +246,10 @@ export class ValueNodeParser { return this.parse_if_function_node(start, end) } - let is_url = str_equals_range(this.source, start, name_end, 'url') + const is_url = str_equals_range(this.source, start, name_end, 'url') // Create URL or function node based on function name (length will be set later) - let node = this.arena.create_node( + const node = this.arena.create_node( is_url ? URL : FUNCTION, start, 0, // length unknown yet @@ -266,7 +266,7 @@ export class ValueNodeParser { if (is_url || str_equals_range(this.source, start, name_end, 'src')) { // Peek at the next token to see if it's a string // If it's a string, parse normally. Otherwise, skip parsing children. - let save_pos = this.lexer.save_position() + const save_pos = this.lexer.save_position() this.lexer.next_token_fast(false) // Skip whitespace @@ -274,7 +274,7 @@ export class ValueNodeParser { this.lexer.next_token_fast(false) } - let first_token_type = this.lexer.token_type + const first_token_type = this.lexer.token_type // Restore lexer position this.lexer.restore_position(save_pos) @@ -288,12 +288,12 @@ export class ValueNodeParser { // that confuse the declaration parser (e.g., data:image/png;base64,...) // So we consume tokens until we find the matching ')' regardless of `end` let func_end = end - let content_start = end // Position after 'url(' + const content_start = end // Position after 'url(' let content_end = end - let matched = this.scan_matching_paren(false) - let scanned_content_end = this.paren_content_end - let scanned_func_end = this.paren_close_end + const matched = this.scan_matching_paren(false) + const scanned_content_end = this.paren_content_end + const scanned_func_end = this.paren_close_end if (matched) { content_end = scanned_content_end func_end = scanned_func_end @@ -316,13 +316,13 @@ export class ValueNodeParser { let last_arg = 0 let paren_depth = 1 let func_end = end - let content_start = end // Position after function name and '(' + const content_start = end // Position after function name and '(' let content_end = end while (this.lexer.pos < this.end && paren_depth > 0) { this.lexer.next_token_fast(true) - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_EOF) break if (this.lexer.token_start >= this.end) break @@ -339,7 +339,7 @@ export class ValueNodeParser { } // Parse argument node - let arg_node = this.parse_value_node() + const arg_node = this.parse_value_node() if (arg_node !== null) { if (first_arg === 0) { first_arg = arg_node @@ -375,23 +375,23 @@ export class ValueNodeParser { // Each branch becomes an IF_BRANCH child (see the `IfBranch` type in node-types.ts // for its shape). Colons/semicolons here are structural separators, not OPERATOR nodes. private parse_if_function_node(start: number, end: number): number { - let name_end = end - 1 // exclude '(' - let save_line = this.lexer.token_line - let save_col = this.lexer.token_column + const name_end = end - 1 // exclude '(' + const save_line = this.lexer.token_line + const save_col = this.lexer.token_column - let node = this.arena.create_node(FUNCTION, start, 0, save_line, save_col) + const node = this.arena.create_node(FUNCTION, start, 0, save_line, save_col) this.arena.set_content_start_delta(node, 0) this.arena.set_content_length(node, name_end - start) // length of "if" - let branches: number[] = [] + const branches: number[] = [] let func_end = end - let content_start = end // right after 'if(' + const content_start = end // right after 'if(' let content_end = end let if_closed = false while (this.lexer.pos < this.end && !if_closed) { this.lexer.next_token_fast(false) - let tt = this.lexer.token_type + const tt = this.lexer.token_type if (tt === TOKEN_EOF) break if (this.lexer.token_start >= this.end) break @@ -406,21 +406,21 @@ export class ValueNodeParser { if (tt === TOKEN_WHITESPACE || tt === TOKEN_SEMICOLON || tt === TOKEN_COLON) continue // ── Condition ────────────────────────────────────────────────────── - let branch_start = this.lexer.token_start - let branch_line = this.lexer.token_line - let branch_col = this.lexer.token_column + const branch_start = this.lexer.token_start + const branch_line = this.lexer.token_line + const branch_col = this.lexer.token_column - let condition_node = this.parse_if_condition() + const condition_node = this.parse_if_condition() if (condition_node === null) continue - let condition_end_pos = + const condition_end_pos = this.arena.get_start_offset(condition_node) + this.arena.get_length(condition_node) // ── Find the ':' separator ───────────────────────────────────────── let colon_found = false while (this.lexer.pos < this.end) { this.lexer.next_token_fast(false) - let t = this.lexer.token_type + const t = this.lexer.token_type if (t === TOKEN_EOF) break if (this.lexer.token_start >= this.end) break if (t === TOKEN_WHITESPACE) continue @@ -439,7 +439,7 @@ export class ValueNodeParser { } // ── Value tokens until ';' or end of if() ──────────────────────── - let value_tokens: number[] = [] + const value_tokens: number[] = [] let value_start = -1 let value_last_end = condition_end_pos let value_line = 0 @@ -448,7 +448,7 @@ export class ValueNodeParser { if (colon_found && !if_closed) { while (this.lexer.pos < this.end) { this.lexer.next_token_fast(false) - let t = this.lexer.token_type + const t = this.lexer.token_type if (t === TOKEN_EOF) break if (this.lexer.token_start >= this.end) break if (t === TOKEN_WHITESPACE) continue @@ -462,9 +462,9 @@ export class ValueNodeParser { break } - let vnode = this.parse_value_node() + const vnode = this.parse_value_node() if (vnode !== null) { - let ns = this.arena.get_start_offset(vnode) + const ns = this.arena.get_start_offset(vnode) if (value_start === -1) { value_start = ns value_line = this.arena.get_start_line(vnode) @@ -490,8 +490,8 @@ export class ValueNodeParser { } // ── Create IF_BRANCH node ────────────────────────────────────────── - let branch_end = value_node === null ? condition_end_pos : value_last_end - let branch_node = this.arena.create_node( + const branch_end = value_node === null ? condition_end_pos : value_last_end + const branch_node = this.arena.create_node( IF_BRANCH, branch_start, branch_end - branch_start, @@ -506,7 +506,7 @@ export class ValueNodeParser { this.arena.set_value_length(branch_node, value_last_end - value_start) } - let branch_children: number[] = [condition_node] + const branch_children: number[] = [condition_node] if (value_node !== null) branch_children.push(value_node) this.arena.append_children(branch_node, branch_children) branches.push(branch_node) @@ -557,15 +557,15 @@ export class ValueNodeParser { let compound = false for (;;) { - let tt = this.lexer.token_type + const tt = this.lexer.token_type let component: number | null = null let is_operator = false if (tt === TOKEN_FUNCTION) { component = this.parse_if_condition_function(this.lexer.token_start, this.lexer.token_end) } else if (tt === TOKEN_IDENT) { - let token_start = this.lexer.token_start - let token_end = this.lexer.token_end + const token_start = this.lexer.token_start + const token_end = this.lexer.token_end if (is_and_or_not_range(this.source, token_start, token_end)) { is_operator = true component = this.arena.create_node( @@ -593,8 +593,8 @@ export class ValueNodeParser { // condition; anything else ends the condition here. if (tt !== TOKEN_FUNCTION && !is_operator) break - let saved = this.lexer.save_position() - let next_tt = this.next_significant_token() + const saved = this.lexer.save_position() + const next_tt = this.next_significant_token() if (next_tt === TOKEN_FUNCTION) { compound = true continue @@ -613,11 +613,11 @@ export class ValueNodeParser { if (component_count === 1 && !compound) return first // Compound condition — wrap the flat not/and/or/function chain in an IF_CONDITION node - let wrapper_start = this.arena.get_start_offset(first) - let last_sibling = this.arena.get_last_sibling(first) - let wrapper_end = + const wrapper_start = this.arena.get_start_offset(first) + const last_sibling = this.arena.get_last_sibling(first) + const wrapper_end = this.arena.get_start_offset(last_sibling) + this.arena.get_length(last_sibling) - let wrapper = this.arena.create_node( + const wrapper = this.arena.create_node( IF_CONDITION, wrapper_start, wrapper_end - wrapper_start, @@ -640,25 +640,25 @@ export class ValueNodeParser { // @param func_start Offset of the function name's first char. // @param token_end Offset right after '(' (== lexer.token_end here). private parse_if_condition_function(func_start: number, token_end: number): number { - let func_name_end = token_end - 1 // before '(' - let func_line = this.lexer.token_line - let func_col = this.lexer.token_column + const func_name_end = token_end - 1 // before '(' + const func_line = this.lexer.token_line + const func_col = this.lexer.token_column - let content_start = token_end // right after '(' + const content_start = token_end // right after '(' let content_end = content_start let func_end = content_start // Scan for matching ')' to find the full function extent - let matched = this.scan_matching_paren(true) - let scanned_content_end = this.paren_content_end - let scanned_func_end = this.paren_close_end + const matched = this.scan_matching_paren(true) + const scanned_content_end = this.paren_content_end + const scanned_func_end = this.paren_close_end if (matched) { content_end = scanned_content_end func_end = scanned_func_end } // Create FUNCTION node spanning the full function text - let func_node = this.arena.create_node( + const func_node = this.arena.create_node( FUNCTION, func_start, func_end - func_start, @@ -677,7 +677,7 @@ export class ValueNodeParser { // style(): a bare single declaration when the content has a top-level // ':' (style(--x: 1)); otherwise the full compound and/or/not grammar over // parenthesized declarations, same as `@supports` — style((--a: 1) or (--a: 2)). - let decl = this.condition_parser.parse_supports_declaration_content( + const decl = this.condition_parser.parse_supports_declaration_content( content_start, content_end, ) @@ -696,7 +696,7 @@ export class ValueNodeParser { // top-level ':' (matching style()'s shorthand — supports(display: grid), no extra // parens needed); otherwise the full compound grammar, same as `@supports`'s own // prelude — supports((a) and (b)), supports(not (c)), nested style()/selector()/… - let decl = this.condition_parser.parse_supports_declaration_content( + const decl = this.condition_parser.parse_supports_declaration_content( content_start, content_end, ) @@ -713,7 +713,7 @@ export class ValueNodeParser { } else if (str_equals_range(this.source, func_start, func_name_end, 'media')) { // media()'s own parens delimit the feature; there's no separate inner paren pair, // so the feature span equals the content span (see parse_media_feature_content's docs) - let feature = this.condition_parser.parse_media_feature_content( + const feature = this.condition_parser.parse_media_feature_content( content_start, content_end, content_start, @@ -731,19 +731,19 @@ export class ValueNodeParser { // Parse value tokens in a source sub-range using save/restore to protect lexer state. private parse_value_nodes_in_range(start: number, end: number): number[] { - let saved_end = this.end - let saved_pos = this.lexer.save_position() + const saved_end = this.end + const saved_pos = this.lexer.save_position() this.end = end this.lexer.seek(start, this.lexer.line, this.lexer.column) - let nodes: number[] = [] + const nodes: number[] = [] while (this.lexer.pos < this.end) { this.lexer.next_token_fast(true) if (this.lexer.token_start >= this.end) break - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_EOF) break - let node = this.parse_value_node() + const node = this.parse_value_node() if (node !== null) nodes.push(node) } @@ -755,7 +755,7 @@ export class ValueNodeParser { private parse_parenthesis_node(start: number, end: number): number { // Create parenthesis node (length will be set later) - let node = this.arena.create_node( + const node = this.arena.create_node( PARENTHESIS, start, 0, // length unknown yet @@ -773,7 +773,7 @@ export class ValueNodeParser { while (this.lexer.pos < this.end && paren_depth > 0) { this.lexer.next_token_fast(true) - let token_type = this.lexer.token_type + const token_type = this.lexer.token_type if (token_type === TOKEN_EOF) break if (this.lexer.token_start >= this.end) break @@ -791,7 +791,7 @@ export class ValueNodeParser { // Parse child node // Note: We don't track paren_depth for LEFT_PAREN or TOKEN_FUNCTION here // because parse_value_node() will recursively handle them - let child_node = this.parse_value_node() + const child_node = this.parse_value_node() if (child_node !== null) { if (first_child === 0) { first_child = child_node diff --git a/src/walk.test.ts b/src/walk.test.ts index 7a605f0..bbc70f8 100644 --- a/src/walk.test.ts +++ b/src/walk.test.ts @@ -294,7 +294,7 @@ describe('walk', () => { }) test('export types', () => { - let ast = parse('a{}') + const ast = parse('a{}') walk(ast, (node, _depth) => { expectTypeOf(node.type).toBeNumber() if (node.type === SELECTOR_LIST) {