From 8f2dcae1a5fefddff6d0836a0371ff98fea8cb94 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Wed, 11 Mar 2026 10:16:44 +0100 Subject: [PATCH 1/4] fix: remove value from atrule nodes, fix MediaType/MediaFeature accessors atrule.value: 'screen' -> undefined (use atrule.prelude instead) mediaType.value: null -> 'screen' mediaFeature.name: 'min-width' -> undefined (use mediaFeature.property instead) - Stop setting value offsets on AT_RULE nodes in parse.ts; the prelude is now fully represented by child nodes (AT_RULE_PRELUDE or RAW) - atrule.value returns undefined; atrule.prelude is the correct API - mediaType.value returns the node text (e.g. "screen", "print") - mediaFeature.name returns undefined; use mediaFeature.property instead - has_prelude for AT_RULE now checks first_child type instead of value length Co-Authored-By: Claude Sonnet 4.6 --- src/css-node.ts | 16 ++++---- src/parse-atrule-prelude.test.ts | 62 ++++++++++++++++--------------- src/parse.test.ts | 63 ++++++++++++++++---------------- src/parse.ts | 3 -- 4 files changed, 71 insertions(+), 73 deletions(-) diff --git a/src/css-node.ts b/src/css-node.ts index 2876127..4da75ce 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -60,13 +60,7 @@ import { ATTR_FLAG_CASE_SENSITIVE, } from './arena' -import { - CHAR_MINUS_HYPHEN, - CHAR_PLUS, - is_whitespace, - is_vendor_prefixed, - str_starts_with, -} from './string-utils' +import { CHAR_MINUS_HYPHEN, CHAR_PLUS, is_whitespace, is_vendor_prefixed, str_starts_with } from './string-utils' import { parse_dimension } from './parse-dimension' // Type name lookup table - maps numeric type to CSSTree-compatible strings @@ -272,7 +266,7 @@ export class CSSNode { /** Get the "content" text (at-rule name for at-rules, layer name for import layers) */ get name(): string | undefined { let { type } = this - if (type === DECLARATION || type === OPERATOR || type === SELECTOR) return + if (type === DECLARATION || type === OPERATOR || type === SELECTOR || type === MEDIA_FEATURE) return return this.get_content() } @@ -296,6 +290,10 @@ export class CSSNode { get value(): CSSNode | string | number | null | undefined { let { type, text, first_child } = this + if (type === AT_RULE) return undefined + + if (type === MEDIA_TYPE) return text + // For DECLARATION nodes with parsed values, return the VALUE node // For DECLARATION nodes without parsed values, fall through to get raw text if (type === DECLARATION && first_child) { @@ -446,7 +444,7 @@ export class CSSNode { get has_prelude(): boolean { let { type } = this if (type === AT_RULE) { - return this.arena.get_value_length(this.index) > 0 + return this.first_child !== null && this.first_child.type !== BLOCK } if (type === STYLE_RULE) { return this.first_child !== null diff --git a/src/parse-atrule-prelude.test.ts b/src/parse-atrule-prelude.test.ts index c856b0a..545b152 100644 --- a/src/parse-atrule-prelude.test.ts +++ b/src/parse-atrule-prelude.test.ts @@ -409,26 +409,39 @@ describe('At-Rule Prelude Nodes', () => { // First child should be a media query expect(children[0].type).toBe(MEDIA_QUERY) - // Query should have a media type child - const queryChildren = children[0].children - expect(queryChildren.some((c) => c.type === MEDIA_TYPE)).toBe(true) + const mediaType = children[0].first_child! + expect(mediaType.type).toBe(MEDIA_TYPE) + expect(mediaType.value).toBe('screen') }) - it('should parse media feature', () => { + it('should parse media feature (min-width: 768px)', () => { const css = '@media (min-width: 768px) { }' const ast = parse(css) const atRule = ast.first_child! - const children = atRule.prelude?.children || [] + const prelude = atRule.prelude! + const query = prelude.first_child! - expect(children[0].type).toBe(MEDIA_QUERY) + expect(query.type).toBe(MEDIA_QUERY) - // Query should have a media feature child - const queryChildren = children[0].children - expect(queryChildren.some((c) => c.type === MEDIA_FEATURE)).toBe(true) + // Feature should have content + const feature = query.first_child + expect(feature?.property).toBe('min-width') + expect(feature?.name).toBeUndefined() + }) + + it('should parse media feature (hover)', () => { + const css = '@media (hover) { }' + const ast = parse(css) + const atRule = ast.first_child! + const prelude = atRule.prelude! + const query = prelude.first_child! + + expect(query.type).toBe(MEDIA_QUERY) // Feature should have content - const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) - expect(feature?.name).toBe('min-width') + const feature = query.first_child + expect(feature?.property).toBe('hover') + expect(feature?.name).toBeUndefined() }) it('should trim whitespace and comments from media features', () => { @@ -439,7 +452,7 @@ describe('At-Rule Prelude Nodes', () => { const queryChildren = children[0].children const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) - expect(feature?.name).toBe('min-width') + expect(feature?.property).toBe('min-width') }) it('should parse complex media query with and operator', () => { @@ -475,7 +488,7 @@ describe('At-Rule Prelude Nodes', () => { const queryChildren = atRule.prelude?.children[0].children || [] const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) - expect(feature?.name).toBe('orientation') + expect(feature?.property).toBe('orientation') expect(feature?.children.length).toBe(1) expect(feature?.children[0].type).toBe(IDENTIFIER) }) @@ -487,7 +500,7 @@ describe('At-Rule Prelude Nodes', () => { const queryChildren = atRule.prelude?.children[0].children || [] const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) - expect(feature?.name).toBe('hover') + expect(feature?.property).toBe('hover') }) it('should parse feature values as typed children', () => { @@ -497,7 +510,7 @@ describe('At-Rule Prelude Nodes', () => { const queryChildren = atRule.prelude?.children[0].children || [] const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) - expect(feature?.name).toBe('min-width') + expect(feature?.property).toBe('min-width') expect(feature?.children.length).toBe(1) expect(feature?.children[0].type).toBe(DIMENSION) }) @@ -1358,8 +1371,7 @@ describe('At-Rule Prelude Nodes', () => { }) it('should parse with all features combined', () => { - const css = - '@import url("styles.css") layer(base) supports(display: grid) screen and (min-width: 768px);' + const css = '@import url("styles.css") layer(base) supports(display: grid) screen and (min-width: 768px);' const ast = parse(css, { parse_atrule_preludes: true }) const atRule = ast.first_child! const children = atRule.prelude?.children || [] @@ -1522,9 +1534,7 @@ describe('At-Rule Prelude Nodes', () => { const ast = parse(css) const atRule = ast.first_child! - expect(atRule?.prelude?.text).toBe( - 'url("a.css") layer(utilities) supports(display: flex) screen', - ) + expect(atRule?.prelude?.text).toBe('url("a.css") layer(utilities) supports(display: flex) screen') expect(atRule?.prelude?.length).toBe(60) }) }) @@ -1824,10 +1834,7 @@ describe('parse_atrule_prelude()', () => { }) test('should parse function name with returns clause', () => { - const result = parse_atrule_prelude( - 'function', - '--clamp-it(--val, --min, --max) returns ', - ) + const result = parse_atrule_prelude('function', '--clamp-it(--val, --min, --max) returns ') expect(result.length).toBe(1) expect(result[0].type).toBe(IDENTIFIER) @@ -2064,9 +2071,7 @@ describe('Comment Handling in At-Rule Preludes', () => { }) it('should parse supports with comments between queries', () => { - const root = parse( - '@supports (display: grid) /* comment */ or /* comment */ (display: flex) { }', - ) + const root = parse('@supports (display: grid) /* comment */ or /* comment */ (display: flex) { }') const atrule = root.first_child expect(atrule?.name).toBe('supports') expect(atrule?.prelude?.children.length).toBeGreaterThan(0) @@ -2133,8 +2138,7 @@ and (min-width: 768px) { }`) describe('@function', () => { it('should parse function name as IDENTIFIER', () => { - const css = - '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' + const css = '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' const ast = parse(css) const atRule = ast.first_child! diff --git a/src/parse.test.ts b/src/parse.test.ts index 873ffea..209031e 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -902,8 +902,7 @@ describe('Core Nodes', () => { describe('Nested at-rules', () => { test('@media inside @supports', () => { - const source = - '@supports (display: grid) { @media (min-width: 768px) { body { color: red; } } }' + const source = '@supports (display: grid) { @media (min-width: 768px) { body { color: red; } } }' const root = parse(source, { parse_atrule_preludes: false }) const supports = root.first_child! @@ -926,8 +925,7 @@ describe('Core Nodes', () => { describe('Multiple at-rules', () => { test('multiple at-rules at top level', () => { - const source = - '@import url("a.css"); @layer base { body { margin: 0; } } @media print { body { color: black; } }' + const source = '@import url("a.css"); @layer base { body { margin: 0; } } @media print { body { color: black; } }' const root = parse(source) const [import1, layer, media] = root.children @@ -989,8 +987,7 @@ describe('Core Nodes', () => { }) test('@property', () => { - let source = - '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' + let source = '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' let root = parse(source) let property = root.first_child! @@ -1087,7 +1084,7 @@ describe('Core Nodes', () => { let atrule = root.first_child! expect(atrule.name).toBe('charset') - expect(atrule.value).toBe('"UTF-8"') + // expect(atrule.value).toBe('"UTF-8"') expect(atrule.prelude).not.toBeNull() expect(atrule.prelude?.text).toBe('"UTF-8"') }) @@ -1098,7 +1095,7 @@ describe('Core Nodes', () => { let atrule = root.first_child! expect(atrule.name).toBe('namespace') - expect(atrule.value).toBe('svg url(http://www.w3.org/2000/svg)') + // expect(atrule.value).toBe('svg url(http://www.w3.org/2000/svg)') // @namespace doesn't have structured prelude parsing, but prelude wrapper exists expect(atrule.prelude).not.toBeNull() expect(atrule.prelude?.text).toBe('svg url(http://www.w3.org/2000/svg)') @@ -1110,7 +1107,7 @@ describe('Core Nodes', () => { let atrule = root.first_child! expect(atrule.name).toBe('imaginary-atrule') - expect(atrule.value).toBe('prelude-stuff') + // expect(atrule.value).toBe('prelude-stuff') // Unknown at-rules get a RAW prelude (not AT_RULE_PRELUDE) expect(atrule.prelude).not.toBeNull() expect(atrule.prelude?.type).toBe(RAW) @@ -1151,6 +1148,21 @@ describe('Core Nodes', () => { 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) + + let atrule = root.first_child! + let block = atrule.block! + let rule = block.first_child! + expect(rule.type).toBe(STYLE_RULE) + + expect(atrule.value).toBeUndefined() + + let 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) @@ -1180,9 +1192,6 @@ describe('Core Nodes', () => { let root = parse(source) let atrule = root.first_child! - // value returns the raw string from arena, prelude returns the wrapper node - expect(typeof atrule.value).toBe('string') - expect(atrule.value).toBe('(min-width: 768px)') expect(atrule.prelude?.text).toBe('(min-width: 768px)') }) @@ -1561,8 +1570,7 @@ describe('Core Nodes', () => { }) test('multiple vendor-prefixed properties', () => { - let source = - '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' + let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) let rule = root.first_child! @@ -2042,8 +2050,7 @@ describe('Core Nodes', () => { }) test('multiple nested rules with different leading combinators', () => { - let source = - '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' + let source = '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' let root = parse(source) let parent = root.first_child! @@ -2162,8 +2169,7 @@ describe('Core Nodes', () => { }) test('@keyframes with mixed percentages and keywords', () => { - let source = - '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' + let source = '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' let root = parse(source, { parse_atrule_preludes: false }) let keyframes = root.first_child! @@ -2174,8 +2180,7 @@ describe('Core Nodes', () => { describe('@function at-rule', () => { test('@function basic', () => { - let source = - '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' + let source = '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' let root = parse(source, { parse_atrule_preludes: false }) let fn = root.first_child! @@ -2208,8 +2213,7 @@ describe('Core Nodes', () => { }) test('@function with nested @media', () => { - let source = - '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' + let source = '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' let root = parse(source, { parse_atrule_preludes: false }) let fn = root.first_child! @@ -2485,8 +2489,7 @@ describe('Core Nodes', () => { }) test('vendor prefixed properties', () => { - let source = - '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' + let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) let rule = root.first_child! @@ -2498,8 +2501,7 @@ describe('Core Nodes', () => { }) test('complex selector list', () => { - let source = - 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' + let source = 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' let root = parse(source) let rule = root.first_child! @@ -2531,8 +2533,7 @@ describe('Core Nodes', () => { }) test('CSS with calc() and other functions', () => { - let source = - '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' + let source = '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' let root = parse(source) let rule = root.first_child! @@ -2543,8 +2544,7 @@ describe('Core Nodes', () => { }) test('custom properties', () => { - let source = - ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' + let source = ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' let root = parse(source) expect(root.children.length).toBeGreaterThan(0) @@ -2682,8 +2682,7 @@ describe('Core Nodes', () => { describe('Large inline SVG', () => { test('should correctly parse declaration with huge inline SVG background-image', () => { // Generate a very long SVG string (> 65535 chars) - const svgPart = - '' + const svgPart = '' const longSvg = svgPart.repeat(1000) // 89,000 chars // Add a second declaration after the huge SVG to test startColumn overflow const css = `.test { background-image: url("data:image/svg+xml,${longSvg}"); color: red; }` diff --git a/src/parse.ts b/src/parse.ts index 9e5bfbb..9f5e938 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -409,9 +409,6 @@ export class Parser { let trimmed = trim_boundaries(this.source, prelude_start, prelude_end) let prelude_wrapper: number | null = null if (trimmed) { - this.arena.set_value_start_delta(at_rule, trimmed[0] - at_rule_start) - this.arena.set_value_length(at_rule, trimmed[1] - trimmed[0]) - // Create AT_RULE_PRELUDE wrapper if prelude parsing is enabled if (this.prelude_parser) { // Parse prelude and add structured nodes as children From 66dd7a61baf8dee61a5493bc3b869c9183750dbc Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Wed, 11 Mar 2026 10:18:50 +0100 Subject: [PATCH 2/4] format --- src/css-node.ts | 11 +++++++-- src/parse-atrule-prelude.test.ts | 19 ++++++++++++---- src/parse.test.ts | 39 +++++++++++++++++++++----------- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/css-node.ts b/src/css-node.ts index 4da75ce..b4194ec 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -60,7 +60,13 @@ import { ATTR_FLAG_CASE_SENSITIVE, } from './arena' -import { CHAR_MINUS_HYPHEN, CHAR_PLUS, is_whitespace, is_vendor_prefixed, str_starts_with } from './string-utils' +import { + CHAR_MINUS_HYPHEN, + CHAR_PLUS, + is_whitespace, + is_vendor_prefixed, + str_starts_with, +} from './string-utils' import { parse_dimension } from './parse-dimension' // Type name lookup table - maps numeric type to CSSTree-compatible strings @@ -266,7 +272,8 @@ export class CSSNode { /** Get the "content" text (at-rule name for at-rules, layer name for import layers) */ get name(): string | undefined { let { type } = this - if (type === DECLARATION || type === OPERATOR || type === SELECTOR || type === MEDIA_FEATURE) return + if (type === DECLARATION || type === OPERATOR || type === SELECTOR || type === MEDIA_FEATURE) + return return this.get_content() } diff --git a/src/parse-atrule-prelude.test.ts b/src/parse-atrule-prelude.test.ts index 545b152..3a46602 100644 --- a/src/parse-atrule-prelude.test.ts +++ b/src/parse-atrule-prelude.test.ts @@ -1371,7 +1371,8 @@ describe('At-Rule Prelude Nodes', () => { }) it('should parse with all features combined', () => { - const css = '@import url("styles.css") layer(base) supports(display: grid) screen and (min-width: 768px);' + const css = + '@import url("styles.css") layer(base) supports(display: grid) screen and (min-width: 768px);' const ast = parse(css, { parse_atrule_preludes: true }) const atRule = ast.first_child! const children = atRule.prelude?.children || [] @@ -1534,7 +1535,9 @@ describe('At-Rule Prelude Nodes', () => { const ast = parse(css) const atRule = ast.first_child! - expect(atRule?.prelude?.text).toBe('url("a.css") layer(utilities) supports(display: flex) screen') + expect(atRule?.prelude?.text).toBe( + 'url("a.css") layer(utilities) supports(display: flex) screen', + ) expect(atRule?.prelude?.length).toBe(60) }) }) @@ -1834,7 +1837,10 @@ describe('parse_atrule_prelude()', () => { }) test('should parse function name with returns clause', () => { - const result = parse_atrule_prelude('function', '--clamp-it(--val, --min, --max) returns ') + const result = parse_atrule_prelude( + 'function', + '--clamp-it(--val, --min, --max) returns ', + ) expect(result.length).toBe(1) expect(result[0].type).toBe(IDENTIFIER) @@ -2071,7 +2077,9 @@ describe('Comment Handling in At-Rule Preludes', () => { }) it('should parse supports with comments between queries', () => { - const root = parse('@supports (display: grid) /* comment */ or /* comment */ (display: flex) { }') + const root = parse( + '@supports (display: grid) /* comment */ or /* comment */ (display: flex) { }', + ) const atrule = root.first_child expect(atrule?.name).toBe('supports') expect(atrule?.prelude?.children.length).toBeGreaterThan(0) @@ -2138,7 +2146,8 @@ and (min-width: 768px) { }`) describe('@function', () => { it('should parse function name as IDENTIFIER', () => { - const css = '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' + const css = + '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' const ast = parse(css) const atRule = ast.first_child! diff --git a/src/parse.test.ts b/src/parse.test.ts index 209031e..f756391 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -902,7 +902,8 @@ describe('Core Nodes', () => { describe('Nested at-rules', () => { test('@media inside @supports', () => { - const source = '@supports (display: grid) { @media (min-width: 768px) { body { color: red; } } }' + const source = + '@supports (display: grid) { @media (min-width: 768px) { body { color: red; } } }' const root = parse(source, { parse_atrule_preludes: false }) const supports = root.first_child! @@ -925,7 +926,8 @@ describe('Core Nodes', () => { describe('Multiple at-rules', () => { test('multiple at-rules at top level', () => { - const source = '@import url("a.css"); @layer base { body { margin: 0; } } @media print { body { color: black; } }' + const source = + '@import url("a.css"); @layer base { body { margin: 0; } } @media print { body { color: black; } }' const root = parse(source) const [import1, layer, media] = root.children @@ -987,7 +989,8 @@ describe('Core Nodes', () => { }) test('@property', () => { - let source = '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' + let source = + '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' let root = parse(source) let property = root.first_child! @@ -1570,7 +1573,8 @@ describe('Core Nodes', () => { }) test('multiple vendor-prefixed properties', () => { - let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' + let source = + '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) let rule = root.first_child! @@ -2050,7 +2054,8 @@ describe('Core Nodes', () => { }) test('multiple nested rules with different leading combinators', () => { - let source = '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' + let source = + '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' let root = parse(source) let parent = root.first_child! @@ -2169,7 +2174,8 @@ describe('Core Nodes', () => { }) test('@keyframes with mixed percentages and keywords', () => { - let source = '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' + let source = + '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' let root = parse(source, { parse_atrule_preludes: false }) let keyframes = root.first_child! @@ -2180,7 +2186,8 @@ describe('Core Nodes', () => { describe('@function at-rule', () => { test('@function basic', () => { - let source = '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' + let source = + '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' let root = parse(source, { parse_atrule_preludes: false }) let fn = root.first_child! @@ -2213,7 +2220,8 @@ describe('Core Nodes', () => { }) test('@function with nested @media', () => { - let source = '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' + let source = + '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' let root = parse(source, { parse_atrule_preludes: false }) let fn = root.first_child! @@ -2489,7 +2497,8 @@ describe('Core Nodes', () => { }) test('vendor prefixed properties', () => { - let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' + let source = + '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) let rule = root.first_child! @@ -2501,7 +2510,8 @@ describe('Core Nodes', () => { }) test('complex selector list', () => { - let source = 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' + let source = + 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' let root = parse(source) let rule = root.first_child! @@ -2533,7 +2543,8 @@ describe('Core Nodes', () => { }) test('CSS with calc() and other functions', () => { - let source = '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' + let source = + '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' let root = parse(source) let rule = root.first_child! @@ -2544,7 +2555,8 @@ describe('Core Nodes', () => { }) test('custom properties', () => { - let source = ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' + let source = + ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' let root = parse(source) expect(root.children.length).toBeGreaterThan(0) @@ -2682,7 +2694,8 @@ describe('Core Nodes', () => { describe('Large inline SVG', () => { test('should correctly parse declaration with huge inline SVG background-image', () => { // Generate a very long SVG string (> 65535 chars) - const svgPart = '' + const svgPart = + '' const longSvg = svgPart.repeat(1000) // 89,000 chars // Add a second declaration after the huge SVG to test startColumn overflow const css = `.test { background-image: url("data:image/svg+xml,${longSvg}"); color: red; }` From 04ca3da8d3570170a6cc8a2459c940ede8ddc0f7 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Wed, 11 Mar 2026 10:21:11 +0100 Subject: [PATCH 3/4] review --- src/parse.test.ts | 42 +++++++++++++----------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/src/parse.test.ts b/src/parse.test.ts index f756391..8cff2b2 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -902,8 +902,7 @@ describe('Core Nodes', () => { describe('Nested at-rules', () => { test('@media inside @supports', () => { - const source = - '@supports (display: grid) { @media (min-width: 768px) { body { color: red; } } }' + const source = '@supports (display: grid) { @media (min-width: 768px) { body { color: red; } } }' const root = parse(source, { parse_atrule_preludes: false }) const supports = root.first_child! @@ -926,8 +925,7 @@ describe('Core Nodes', () => { describe('Multiple at-rules', () => { test('multiple at-rules at top level', () => { - const source = - '@import url("a.css"); @layer base { body { margin: 0; } } @media print { body { color: black; } }' + const source = '@import url("a.css"); @layer base { body { margin: 0; } } @media print { body { color: black; } }' const root = parse(source) const [import1, layer, media] = root.children @@ -989,8 +987,7 @@ describe('Core Nodes', () => { }) test('@property', () => { - let source = - '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' + let source = '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' let root = parse(source) let property = root.first_child! @@ -1087,7 +1084,6 @@ describe('Core Nodes', () => { let atrule = root.first_child! expect(atrule.name).toBe('charset') - // expect(atrule.value).toBe('"UTF-8"') expect(atrule.prelude).not.toBeNull() expect(atrule.prelude?.text).toBe('"UTF-8"') }) @@ -1098,7 +1094,6 @@ describe('Core Nodes', () => { let atrule = root.first_child! expect(atrule.name).toBe('namespace') - // expect(atrule.value).toBe('svg url(http://www.w3.org/2000/svg)') // @namespace doesn't have structured prelude parsing, but prelude wrapper exists expect(atrule.prelude).not.toBeNull() expect(atrule.prelude?.text).toBe('svg url(http://www.w3.org/2000/svg)') @@ -1110,7 +1105,6 @@ describe('Core Nodes', () => { let atrule = root.first_child! expect(atrule.name).toBe('imaginary-atrule') - // expect(atrule.value).toBe('prelude-stuff') // Unknown at-rules get a RAW prelude (not AT_RULE_PRELUDE) expect(atrule.prelude).not.toBeNull() expect(atrule.prelude?.type).toBe(RAW) @@ -1573,8 +1567,7 @@ describe('Core Nodes', () => { }) test('multiple vendor-prefixed properties', () => { - let source = - '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' + let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) let rule = root.first_child! @@ -2054,8 +2047,7 @@ describe('Core Nodes', () => { }) test('multiple nested rules with different leading combinators', () => { - let source = - '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' + let source = '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' let root = parse(source) let parent = root.first_child! @@ -2174,8 +2166,7 @@ describe('Core Nodes', () => { }) test('@keyframes with mixed percentages and keywords', () => { - let source = - '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' + let source = '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' let root = parse(source, { parse_atrule_preludes: false }) let keyframes = root.first_child! @@ -2186,8 +2177,7 @@ describe('Core Nodes', () => { describe('@function at-rule', () => { test('@function basic', () => { - let source = - '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' + let source = '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' let root = parse(source, { parse_atrule_preludes: false }) let fn = root.first_child! @@ -2220,8 +2210,7 @@ describe('Core Nodes', () => { }) test('@function with nested @media', () => { - let source = - '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' + let source = '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' let root = parse(source, { parse_atrule_preludes: false }) let fn = root.first_child! @@ -2497,8 +2486,7 @@ describe('Core Nodes', () => { }) test('vendor prefixed properties', () => { - let source = - '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' + let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) let rule = root.first_child! @@ -2510,8 +2498,7 @@ describe('Core Nodes', () => { }) test('complex selector list', () => { - let source = - 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' + let source = 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' let root = parse(source) let rule = root.first_child! @@ -2543,8 +2530,7 @@ describe('Core Nodes', () => { }) test('CSS with calc() and other functions', () => { - let source = - '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' + let source = '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' let root = parse(source) let rule = root.first_child! @@ -2555,8 +2541,7 @@ describe('Core Nodes', () => { }) test('custom properties', () => { - let source = - ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' + let source = ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' let root = parse(source) expect(root.children.length).toBeGreaterThan(0) @@ -2694,8 +2679,7 @@ describe('Core Nodes', () => { describe('Large inline SVG', () => { test('should correctly parse declaration with huge inline SVG background-image', () => { // Generate a very long SVG string (> 65535 chars) - const svgPart = - '' + const svgPart = '' const longSvg = svgPart.repeat(1000) // 89,000 chars // Add a second declaration after the huge SVG to test startColumn overflow const css = `.test { background-image: url("data:image/svg+xml,${longSvg}"); color: red; }` From 5842de6b2c55c1289c47d72d372af80ef7ff692b Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Wed, 11 Mar 2026 10:22:07 +0100 Subject: [PATCH 4/4] format --- src/parse.test.ts | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/parse.test.ts b/src/parse.test.ts index 8cff2b2..44565e1 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -902,7 +902,8 @@ describe('Core Nodes', () => { describe('Nested at-rules', () => { test('@media inside @supports', () => { - const source = '@supports (display: grid) { @media (min-width: 768px) { body { color: red; } } }' + const source = + '@supports (display: grid) { @media (min-width: 768px) { body { color: red; } } }' const root = parse(source, { parse_atrule_preludes: false }) const supports = root.first_child! @@ -925,7 +926,8 @@ describe('Core Nodes', () => { describe('Multiple at-rules', () => { test('multiple at-rules at top level', () => { - const source = '@import url("a.css"); @layer base { body { margin: 0; } } @media print { body { color: black; } }' + const source = + '@import url("a.css"); @layer base { body { margin: 0; } } @media print { body { color: black; } }' const root = parse(source) const [import1, layer, media] = root.children @@ -987,7 +989,8 @@ describe('Core Nodes', () => { }) test('@property', () => { - let source = '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' + let source = + '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' let root = parse(source) let property = root.first_child! @@ -1567,7 +1570,8 @@ describe('Core Nodes', () => { }) test('multiple vendor-prefixed properties', () => { - let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' + let source = + '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) let rule = root.first_child! @@ -2047,7 +2051,8 @@ describe('Core Nodes', () => { }) test('multiple nested rules with different leading combinators', () => { - let source = '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' + let source = + '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' let root = parse(source) let parent = root.first_child! @@ -2166,7 +2171,8 @@ describe('Core Nodes', () => { }) test('@keyframes with mixed percentages and keywords', () => { - let source = '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' + let source = + '@keyframes slide { from { left: 0; } 25%, 75% { left: 50%; } to { left: 100%; } }' let root = parse(source, { parse_atrule_preludes: false }) let keyframes = root.first_child! @@ -2177,7 +2183,8 @@ describe('Core Nodes', () => { describe('@function at-rule', () => { test('@function basic', () => { - let source = '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' + let source = + '@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }' let root = parse(source, { parse_atrule_preludes: false }) let fn = root.first_child! @@ -2210,7 +2217,8 @@ describe('Core Nodes', () => { }) test('@function with nested @media', () => { - let source = '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' + let source = + '@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }' let root = parse(source, { parse_atrule_preludes: false }) let fn = root.first_child! @@ -2486,7 +2494,8 @@ describe('Core Nodes', () => { }) test('vendor prefixed properties', () => { - let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' + let source = + '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) let rule = root.first_child! @@ -2498,7 +2507,8 @@ describe('Core Nodes', () => { }) test('complex selector list', () => { - let source = 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' + let source = + 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' let root = parse(source) let rule = root.first_child! @@ -2530,7 +2540,8 @@ describe('Core Nodes', () => { }) test('CSS with calc() and other functions', () => { - let source = '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' + let source = + '.box { width: calc(100% - 2rem); background: linear-gradient(to right, red, blue); }' let root = parse(source) let rule = root.first_child! @@ -2541,7 +2552,8 @@ describe('Core Nodes', () => { }) test('custom properties', () => { - let source = ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' + let source = + ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' let root = parse(source) expect(root.children.length).toBeGreaterThan(0) @@ -2679,7 +2691,8 @@ describe('Core Nodes', () => { describe('Large inline SVG', () => { test('should correctly parse declaration with huge inline SVG background-image', () => { // Generate a very long SVG string (> 65535 chars) - const svgPart = '' + const svgPart = + '' const longSvg = svgPart.repeat(1000) // 89,000 chars // Add a second declaration after the huge SVG to test startColumn overflow const css = `.test { background-image: url("data:image/svg+xml,${longSvg}"); color: red; }`