diff --git a/src/css-node.ts b/src/css-node.ts index 9142f18..db80a7d 100644 --- a/src/css-node.ts +++ b/src/css-node.ts @@ -229,20 +229,29 @@ export class CSSNode { // For URL nodes with quoted string: returns the string with quotes (consistent with STRING node) // For URL nodes with unquoted URL: returns the URL content without quotes get value(): string | number | null { + let { type, text } = this + + if (type === DIMENSION) { + return parse_dimension(text).value + } + + if (type === NUMBER) { + return Number.parseFloat(this.text) + } + // Special handling for URL nodes - if (this.type === URL) { - const firstChild = this.first_child + if (type === URL) { + let firstChild = this.first_child if (firstChild && firstChild.type === STRING) { // Return the string as-is (with quotes) - consistent with STRING node return firstChild.text } // For URL nodes without children (e.g., @import url(...)), extract from text // Handle both url("...") and url('...') and just "..." or '...' - const text = this.text if (str_starts_with(text, 'url(')) { // url("...") or url('...') or url(...) - extract content between parens - const openParen = text.indexOf('(') - const closeParen = text.lastIndexOf(')') + let openParen = text.indexOf('(') + let closeParen = text.lastIndexOf(')') if (openParen !== -1 && closeParen !== -1 && closeParen > openParen) { let content = text.substring(openParen + 1, closeParen).trim() return content @@ -254,11 +263,6 @@ export class CSSNode { // For unquoted URLs, fall through to value delta logic below } - // For dimension and number nodes, parse and return as number - if (this.type === DIMENSION || this.type === NUMBER) { - return parse_dimension(this.text).value - } - // For other nodes, return as string let start = this.arena.get_value_start(this.index) let length = this.arena.get_value_length(this.index) @@ -266,6 +270,17 @@ export class CSSNode { return this.source.substring(start, start + length) } + get value_as_number(): number | null { + let text = this.text + if (this.type === NUMBER) { + return Number.parseFloat(text) + } + if (this.type === DIMENSION) { + return parse_dimension(text).value + } + return null + } + // Get the prelude text (for at-rules: "(min-width: 768px)" in "@media (min-width: 768px)") // This is an alias for `value` to make at-rule usage more semantic get prelude(): string | null { diff --git a/src/parse-value.test.ts b/src/parse-value.test.ts index 72d4a94..b0fe825 100644 --- a/src/parse-value.test.ts +++ b/src/parse-value.test.ts @@ -660,6 +660,90 @@ describe('Value Node Types', () => { }) }) + describe('value_as_number getter', () => { + it('should return number for NUMBER nodes', () => { + const root = parse('div { opacity: 0.5; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const numberNode = decl?.values[0] + + expect(numberNode?.type).toBe(NUMBER) + expect(numberNode?.value_as_number).toBe(0.5) + }) + + it('should return number for DIMENSION nodes', () => { + const root = parse('div { width: 100px; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const dimNode = decl?.values[0] + + expect(dimNode?.type).toBe(DIMENSION) + expect(dimNode?.value_as_number).toBe(100) + }) + + it('should handle negative numbers', () => { + const root = parse('div { margin: -10px; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const dimNode = decl?.values[0] + + expect(dimNode?.type).toBe(DIMENSION) + expect(dimNode?.value_as_number).toBe(-10) + }) + + it('should handle zero', () => { + const root = parse('div { margin: 0; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const numberNode = decl?.values[0] + + expect(numberNode?.type).toBe(NUMBER) + expect(numberNode?.value_as_number).toBe(0) + }) + + it('should handle decimal numbers', () => { + const root = parse('div { line-height: 1.5; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const numberNode = decl?.values[0] + + expect(numberNode?.type).toBe(NUMBER) + expect(numberNode?.value_as_number).toBe(1.5) + }) + + it('should handle percentage dimensions', () => { + const root = parse('div { width: 50%; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const dimNode = decl?.values[0] + + expect(dimNode?.type).toBe(DIMENSION) + expect(dimNode?.value_as_number).toBe(50) + expect(dimNode?.unit).toBe('%') + }) + + it('should return null for IDENTIFIER nodes', () => { + const root = parse('div { color: red; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const identNode = decl?.values[0] + + expect(identNode?.type).toBe(IDENTIFIER) + expect(identNode?.value_as_number).toBeNull() + }) + + it('should return null for STRING nodes', () => { + const root = parse('div { content: "hello"; }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const stringNode = decl?.values[0] + + expect(stringNode?.type).toBe(STRING) + expect(stringNode?.value_as_number).toBeNull() + }) + + it('should return null for FUNCTION nodes', () => { + const root = parse('div { width: calc(100% - 20px); }') + const decl = root.first_child?.first_child?.next_sibling?.first_child + const funcNode = decl?.values[0] + + expect(funcNode?.type).toBe(FUNCTION) + expect(funcNode?.value_as_number).toBeNull() + }) + }) + describe('Case-insensitive function names', () => { const getValue = (css: string) => { const root = parse(css) diff --git a/src/parse.test.ts b/src/parse.test.ts index 4795cbd..57287ec 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -6,7 +6,6 @@ import { AT_RULE, DECLARATION, BLOCK, - COMMENT, SELECTOR_LIST, SELECTOR, PSEUDO_CLASS_SELECTOR, @@ -278,18 +277,17 @@ describe('Core Nodes', () => { describe('STYLESHEET', () => { test('empty stylesheet has no children', () => { const root = parse('') - + expect(root.type).toBe(STYLESHEET) expect(root.has_children).toBe(false) }) test('stylesheet with only whitespace has no children', () => { const root = parse(' \n\n ') - + expect(root.type).toBe(STYLESHEET) expect(root.has_children).toBe(false) }) - }) describe('STYLE_RULE', () => { @@ -636,7 +634,6 @@ describe('Core Nodes', () => { test('should parse simple selector', () => { const source = 'body { }' const root = parse(source) - const rule = root.first_child! expect(rule.has_children).toBe(true) @@ -651,7 +648,6 @@ describe('Core Nodes', () => { test('should parse complex selector', () => { const source = 'div.class > p#id { }' const root = parse(source) - const rule = root.first_child! const selectorlist = rule.first_child! @@ -702,7 +698,6 @@ describe('Core Nodes', () => { describe('Multiple rules', () => { test('should parse multiple style rules', () => { const root = parse('body { } div { }') - const [rule1, rule2] = root.children expect(rule1.type).toBe(STYLE_RULE) @@ -717,7 +712,6 @@ describe('Core Nodes', () => { test('@import', () => { const source = '@import url("style.css");' const root = parse(source, { parse_atrule_preludes: false }) - const atRule = root.first_child! expect(atRule.type).toBe(AT_RULE) @@ -728,7 +722,6 @@ describe('Core Nodes', () => { test('@namespace', () => { const source = '@namespace url(http://www.w3.org/1999/xhtml);' const root = parse(source) - const atRule = root.first_child! expect(atRule.type).toBe(AT_RULE) @@ -741,7 +734,6 @@ describe('Core Nodes', () => { test('should parse @MEDIA (uppercase)', () => { const source = '@MEDIA (min-width: 768px) { body { color: red; } }' const root = parse(source, { parse_atrule_preludes: false }) - const media = root.first_child! expect(media.type).toBe(AT_RULE) @@ -755,7 +747,6 @@ describe('Core Nodes', () => { test('should parse @Font-Face (mixed case)', () => { const source = '@Font-Face { font-family: "MyFont"; src: url("font.woff"); }' const root = parse(source) - const fontFace = root.first_child! expect(fontFace.type).toBe(AT_RULE) @@ -770,7 +761,6 @@ describe('Core Nodes', () => { test('should parse @SUPPORTS (uppercase)', () => { const source = '@SUPPORTS (display: grid) { .grid { display: grid; } }' const root = parse(source, { parse_atrule_preludes: false }) - const supports = root.first_child! expect(supports.type).toBe(AT_RULE) @@ -783,7 +773,6 @@ describe('Core Nodes', () => { test('@media with nested rule', () => { const source = '@media (min-width: 768px) { body { color: red; } }' const root = parse(source, { parse_atrule_preludes: false }) - const media = root.first_child! expect(media.type).toBe(AT_RULE) @@ -800,7 +789,6 @@ describe('Core Nodes', () => { test('@layer with name', () => { const source = '@layer utilities { .text-center { text-align: center; } }' const root = parse(source) - const layer = root.first_child! expect(layer.type).toBe(AT_RULE) @@ -811,7 +799,6 @@ describe('Core Nodes', () => { test('anonymous @layer', () => { const source = '@layer { body { margin: 0; } }' const root = parse(source) - const layer = root.first_child! expect(layer.type).toBe(AT_RULE) @@ -822,7 +809,6 @@ describe('Core Nodes', () => { test('@supports', () => { const source = '@supports (display: grid) { .grid { display: grid; } }' const root = parse(source) - const supports = root.first_child! expect(supports.type).toBe(AT_RULE) @@ -833,7 +819,6 @@ describe('Core Nodes', () => { test('@container', () => { const source = '@container (min-width: 400px) { .card { padding: 2rem; } }' const root = parse(source) - const container = root.first_child! expect(container.type).toBe(AT_RULE) @@ -846,7 +831,6 @@ describe('Core Nodes', () => { test('@font-face', () => { const source = '@font-face { font-family: "Open Sans"; src: url(font.woff2); }' const root = parse(source) - const fontFace = root.first_child! expect(fontFace.type).toBe(AT_RULE) @@ -862,7 +846,6 @@ describe('Core Nodes', () => { test('@page', () => { const source = '@page { margin: 1in; }' const root = parse(source) - const page = root.first_child! expect(page.type).toBe(AT_RULE) @@ -876,7 +859,6 @@ describe('Core Nodes', () => { test('@counter-style', () => { const source = '@counter-style thumbs { system: cyclic; symbols: "👍"; }' const root = parse(source) - const counterStyle = root.first_child! expect(counterStyle.type).toBe(AT_RULE) @@ -892,7 +874,6 @@ describe('Core Nodes', () => { test('@media inside @supports', () => { 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! expect(supports.name).toBe('supports') @@ -916,7 +897,6 @@ describe('Core Nodes', () => { test('multiple at-rules at top level', () => { 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 expect(import1.name).toBe('import') @@ -932,7 +912,6 @@ describe('Core Nodes', () => { test('@charset', () => { let source = '@charset "UTF-8"; body { color: red; }' let root = parse(source) - let [charset, _body] = root.children expect(charset.type).toBe(AT_RULE) @@ -942,7 +921,6 @@ describe('Core Nodes', () => { test('@import with media query', () => { let source = '@import url("print.css") print;' let root = parse(source) - let import_rule = root.first_child! expect(import_rule.type).toBe(AT_RULE) @@ -961,7 +939,6 @@ describe('Core Nodes', () => { } ` let root = parse(source) - let font_face = root.first_child! expect(font_face.name).toBe('font-face') @@ -972,7 +949,6 @@ describe('Core Nodes', () => { test('@counter-style', () => { let source = '@counter-style custom { system: cyclic; symbols: "⚫" "⚪"; suffix: " "; }' let root = parse(source) - let counter = root.first_child! expect(counter.name).toBe('counter-style') @@ -983,7 +959,6 @@ describe('Core Nodes', () => { test('@property', () => { let source = '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' let root = parse(source) - let property = root.first_child! expect(property.name).toBe('property') @@ -994,7 +969,6 @@ describe('Core Nodes', () => { test('media query prelude', () => { let source = '@media (min-width: 768px) { }' let root = parse(source) - let atrule = root.first_child! expect(atrule.type).toBe(AT_RULE) @@ -1005,7 +979,6 @@ describe('Core Nodes', () => { test('complex media query prelude', () => { let source = '@media screen and (min-width: 768px) and (max-width: 1024px) { }' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('media') @@ -1015,7 +988,6 @@ describe('Core Nodes', () => { test('container query prelude', () => { let source = '@container (width >= 200px) { }' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('container') @@ -1025,7 +997,6 @@ describe('Core Nodes', () => { test('supports query prelude', () => { let source = '@supports (display: grid) { }' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('supports') @@ -1035,7 +1006,6 @@ describe('Core Nodes', () => { test('import prelude', () => { let source = '@import url("styles.css");' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('import') @@ -1045,7 +1015,6 @@ describe('Core Nodes', () => { test('at-rule without prelude', () => { let source = '@font-face { font-family: MyFont; }' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('font-face') @@ -1055,7 +1024,6 @@ describe('Core Nodes', () => { test('layer prelude', () => { let source = '@layer utilities { }' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('layer') @@ -1065,7 +1033,6 @@ describe('Core Nodes', () => { test('keyframes prelude', () => { let source = '@keyframes slide-in { }' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('keyframes') @@ -1075,7 +1042,6 @@ describe('Core Nodes', () => { test('prelude with extra whitespace', () => { let source = '@media (min-width: 768px) { }' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('media') @@ -1085,7 +1051,6 @@ describe('Core Nodes', () => { test('charset prelude', () => { let source = '@charset "UTF-8";' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('charset') @@ -1095,7 +1060,6 @@ describe('Core Nodes', () => { test('namespace prelude', () => { let source = '@namespace svg url(http://www.w3.org/2000/svg);' let root = parse(source) - let atrule = root.first_child! expect(atrule.name).toBe('namespace') @@ -1105,7 +1069,6 @@ describe('Core Nodes', () => { test('value and prelude should be aliases for at-rules', () => { let source = '@media (min-width: 768px) { }' let root = parse(source) - let atrule = root.first_child! expect(atrule.value).toBe(atrule.prelude) @@ -1115,7 +1078,6 @@ describe('Core Nodes', () => { test('at-rule prelude line tracking', () => { let source = 'body { color: red; }\n\n@media screen { }' let root = parse(source) - let [_rule1, atRule] = root.children expect(atRule.line).toBe(3) @@ -1167,7 +1129,6 @@ describe('Core Nodes', () => { test('should parse property name', () => { const source = 'body { color: red; }' const root = parse(source) - const rule = root.first_child! const [_selector, block] = rule.children @@ -1179,7 +1140,6 @@ describe('Core Nodes', () => { test('simple declaration without !important', () => { const source = 'body { color: red; }' const root = parse(source) - const rule = root.first_child! const [_selector, block] = rule.children @@ -1192,7 +1152,6 @@ describe('Core Nodes', () => { test('declaration with !important', () => { const source = 'body { color: red !important; }' const root = parse(source) - const rule = root.first_child! const [_selector, block] = rule.children @@ -1205,7 +1164,6 @@ describe('Core Nodes', () => { test('declaration with !ie (historic !important)', () => { const source = 'body { color: red !ie; }' const root = parse(source) - const rule = root.first_child! const [_selector, block] = rule.children @@ -1218,7 +1176,6 @@ describe('Core Nodes', () => { test('declaration with ! followed by any identifier', () => { const source = 'body { color: red !foo; }' const root = parse(source) - const rule = root.first_child! const [_selector, block] = rule.children @@ -1231,7 +1188,6 @@ describe('Core Nodes', () => { test('declaration without semicolon at end of block', () => { const source = 'body { color: red }' const root = parse(source) - const rule = root.first_child! const [_selector, block] = rule.children @@ -1243,7 +1199,6 @@ describe('Core Nodes', () => { test('complex declaration value', () => { const source = 'body { background: url(image.png) no-repeat center; }' const root = parse(source) - const rule = root.first_child! const [_selector, block] = rule.children @@ -1258,7 +1213,6 @@ describe('Core Nodes', () => { test('should parse multiple declarations', () => { const source = 'body { color: red; margin: 0; }' const root = parse(source) - const rule = root.first_child! const [_selector, block] = rule.children @@ -1274,7 +1228,6 @@ describe('Core Nodes', () => { test('extract simple value', () => { let source = 'a { color: blue; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1287,7 +1240,6 @@ describe('Core Nodes', () => { test('extract value with spaces', () => { let source = 'a { padding: 1rem 2rem 3rem 4rem; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1300,7 +1252,6 @@ describe('Core Nodes', () => { test('extract function value', () => { let source = 'a { background: linear-gradient(to bottom, red, blue); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1313,7 +1264,6 @@ describe('Core Nodes', () => { test('extract calc value', () => { let source = 'a { width: calc(100% - 2rem); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1326,7 +1276,6 @@ describe('Core Nodes', () => { test('exclude !important from value', () => { let source = 'a { color: blue !important; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1340,7 +1289,6 @@ describe('Core Nodes', () => { test('value with extra whitespace', () => { let source = 'a { color: blue ; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1353,7 +1301,6 @@ describe('Core Nodes', () => { test('CSS custom property value', () => { let source = ':root { --brand-color: rgb(0% 10% 50% / 0.5); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1366,7 +1313,6 @@ describe('Core Nodes', () => { test('var() reference value', () => { let source = 'a { color: var(--primary-color); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1379,7 +1325,6 @@ describe('Core Nodes', () => { test('nested function value', () => { let source = 'a { transform: translate(calc(50% - 1rem), 0); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1392,7 +1337,6 @@ describe('Core Nodes', () => { test('value without semicolon', () => { let source = 'a { color: blue }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1405,7 +1349,6 @@ describe('Core Nodes', () => { test('empty value', () => { let source = 'a { color: ; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1418,7 +1361,6 @@ describe('Core Nodes', () => { test('URL value', () => { let source = 'a { background: url("image.png"); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1433,7 +1375,6 @@ describe('Core Nodes', () => { test('-webkit- vendor prefix', () => { let source = '.box { -webkit-transform: scale(1); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1445,7 +1386,6 @@ describe('Core Nodes', () => { test('-moz- vendor prefix', () => { let source = '.box { -moz-transform: scale(1); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1457,7 +1397,6 @@ describe('Core Nodes', () => { test('-ms- vendor prefix', () => { let source = '.box { -ms-transform: scale(1); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1469,7 +1408,6 @@ describe('Core Nodes', () => { test('-o- vendor prefix', () => { let source = '.box { -o-transform: scale(1); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1481,7 +1419,6 @@ describe('Core Nodes', () => { test('no vendor prefix for standard properties', () => { let source = '.box { transform: scale(1); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1493,7 +1430,6 @@ describe('Core Nodes', () => { test('no vendor prefix for properties with hyphens', () => { let source = '.box { background-color: red; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1505,7 +1441,6 @@ describe('Core Nodes', () => { test('no vendor prefix for custom properties', () => { let source = ':root { --primary-color: blue; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1517,7 +1452,6 @@ describe('Core Nodes', () => { test('multiple vendor-prefixed properties', () => { let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1536,7 +1470,6 @@ describe('Core Nodes', () => { test('complex property names with vendor prefix', () => { let source = '.box { -webkit-border-top-left-radius: 5px; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1548,7 +1481,6 @@ describe('Core Nodes', () => { test('no vendor prefix for similar but non-vendor properties', () => { let source = '.box { border-radius: 5px; }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -1560,7 +1492,6 @@ describe('Core Nodes', () => { test('false for nodes without names', () => { let source = 'body { }' let root = parse(source) - let rule = root.first_child! let selector = rule.first_child! @@ -1572,7 +1503,6 @@ describe('Core Nodes', () => { 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! @@ -1586,7 +1516,6 @@ describe('Core Nodes', () => { 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! @@ -1600,7 +1529,6 @@ describe('Core Nodes', () => { 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! @@ -1614,7 +1542,6 @@ describe('Core Nodes', () => { 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! @@ -1628,7 +1555,6 @@ describe('Core Nodes', () => { 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! @@ -1642,7 +1568,6 @@ describe('Core Nodes', () => { 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! @@ -1656,7 +1581,6 @@ describe('Core Nodes', () => { 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! @@ -1670,7 +1594,6 @@ describe('Core Nodes', () => { 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! @@ -1684,7 +1607,6 @@ describe('Core Nodes', () => { 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! @@ -1698,7 +1620,6 @@ describe('Core Nodes', () => { test('multiple vendor-prefixed pseudo-elements', () => { let source = 'div::-webkit-scrollbar { } div::-webkit-scrollbar-thumb { } div::after { }' let root = parse(source) - let [rule1, rule2, rule3] = root.children @@ -1727,7 +1648,6 @@ describe('Core Nodes', () => { 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! @@ -1747,7 +1667,7 @@ describe('Core Nodes', () => { describe('BLOCK', () => { test('block text excludes braces for empty at-rule block', () => { const root = parse('@layer test {}') - + const atRule = root.first_child! expect(atRule.has_block).toBe(true) @@ -1757,7 +1677,7 @@ describe('Core Nodes', () => { test('at-rule block with content excludes braces', () => { const root = parse('@layer test { .foo { color: red; } }') - + const atRule = root.first_child! expect(atRule.has_block).toBe(true) @@ -1767,7 +1687,7 @@ describe('Core Nodes', () => { test('empty style rule block has empty text', () => { const root = parse('body {}') - + const styleRule = root.first_child! expect(styleRule.has_block).toBe(true) @@ -1777,7 +1697,7 @@ describe('Core Nodes', () => { test('style rule block with declaration excludes braces', () => { const root = parse('body { color: red; }') - + const styleRule = root.first_child! expect(styleRule.has_block).toBe(true) @@ -1787,7 +1707,7 @@ describe('Core Nodes', () => { test('nested style rule blocks exclude braces', () => { const root = parse('.parent { .child { margin: 0; } }') - + const parent = root.first_child! const parentBlock = parent.block! const child = parentBlock.first_child! @@ -1799,7 +1719,7 @@ describe('Core Nodes', () => { test('at-rule with multiple declarations excludes braces', () => { const root = parse('@font-face { font-family: "Test"; src: url(test.woff); }') - + const atRule = root.first_child! expect(atRule.block!.text).toBe(' font-family: "Test"; src: url(test.woff); ') @@ -1807,7 +1727,7 @@ describe('Core Nodes', () => { test('media query with nested rules excludes braces', () => { const root = parse('@media screen { body { color: blue; } }') - + const mediaRule = root.first_child! expect(mediaRule.block!.text).toBe(' body { color: blue; } ') @@ -1815,7 +1735,7 @@ describe('Core Nodes', () => { test('block with no whitespace is empty', () => { const root = parse('div{}') - + const styleRule = root.first_child! expect(styleRule.block!.text).toBe('') @@ -1823,7 +1743,7 @@ describe('Core Nodes', () => { test('block with only whitespace preserves whitespace', () => { const root = parse('div{ \n\t }') - + const styleRule = root.first_child! expect(styleRule.block!.text).toBe(' \n\t ') @@ -1834,7 +1754,6 @@ describe('Core Nodes', () => { test('nested rule with & selector', () => { let source = '.parent { color: red; & .child { color: blue; } }' let root = parse(source) - let parent = root.first_child! expect(parent.type).toBe(STYLE_RULE) @@ -1852,7 +1771,6 @@ describe('Core Nodes', () => { test('nested rule without & selector', () => { let source = '.parent { color: red; .child { color: blue; } }' let root = parse(source) - let parent = root.first_child! let [_selector, block] = parent.children @@ -1866,7 +1784,6 @@ describe('Core Nodes', () => { test('multiple nested rules', () => { let source = '.parent { .child1 { } .child2 { } }' let root = parse(source) - let parent = root.first_child! let [_selector, block] = parent.children @@ -1879,7 +1796,6 @@ describe('Core Nodes', () => { test('deeply nested rules', () => { let source = '.a { .b { .c { color: red; } } }' let root = parse(source) - let a = root.first_child! expect(a.length).toBe(32) @@ -1902,7 +1818,6 @@ describe('Core Nodes', () => { test('nested @media inside rule', () => { let source = '.card { color: red; @media (min-width: 768px) { padding: 2rem; } }' let root = parse(source, { parse_atrule_preludes: false }) - let card = root.first_child! let [_selector, block] = card.children @@ -1921,7 +1836,6 @@ describe('Core Nodes', () => { test(':is() pseudo-class', () => { let source = ':is(.a, .b) { color: red; }' let root = parse(source) - let rule = root.first_child! let selector = rule.first_child! @@ -1931,7 +1845,6 @@ describe('Core Nodes', () => { test(':where() pseudo-class', () => { let source = ':where(h1, h2, h3) { margin: 0; }' let root = parse(source) - let rule = root.first_child! let selector = rule.first_child! @@ -1941,7 +1854,6 @@ describe('Core Nodes', () => { test(':has() pseudo-class', () => { let source = 'div:has(> img) { display: flex; }' let root = parse(source) - let rule = root.first_child! let selector = rule.first_child! @@ -1956,7 +1868,6 @@ describe('Core Nodes', () => { .body { line-height: 1.5; } }` let root = parse(source) - let card = root.first_child! let [_selector, block] = card.children @@ -1977,7 +1888,6 @@ describe('Core Nodes', () => { test('nested rule with leading child combinator', () => { let source = '.parent { > a { color: red; } }' let root = parse(source) - let parent = root.first_child! expect(parent.type).toBe(STYLE_RULE) @@ -1994,7 +1904,6 @@ describe('Core Nodes', () => { test('nested rule with leading next-sibling combinator', () => { let source = '.parent { + span { color: blue; } }' let root = parse(source) - let parent = root.first_child! let [_selector, block] = parent.children @@ -2009,7 +1918,6 @@ describe('Core Nodes', () => { test('nested rule with leading subsequent-sibling combinator', () => { let source = '.parent { ~ div { color: green; } }' let root = parse(source) - let parent = root.first_child! let [_selector, block] = parent.children @@ -2024,7 +1932,6 @@ describe('Core Nodes', () => { test('multiple nested rules with different leading combinators', () => { let source = '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }' let root = parse(source) - let parent = root.first_child! let [_selector, block] = parent.children @@ -2046,7 +1953,6 @@ describe('Core Nodes', () => { test('complex selector after leading combinator', () => { let source = '.parent { > a.link#nav[href]:hover { color: red; } }' let root = parse(source) - let parent = root.first_child! let [_selector, block] = parent.children @@ -2060,7 +1966,6 @@ describe('Core Nodes', () => { test('deeply nested rules with leading combinators', () => { let source = '.a { > .b { > .c { color: red; } } }' let root = parse(source) - let a = root.first_child! let [_selector_a, block_a] = a.children @@ -2079,7 +1984,6 @@ describe('Core Nodes', () => { test('mixed nested rules with and without leading combinators', () => { let source = '.parent { .normal { } > .combinator { } }' let root = parse(source) - let parent = root.first_child! let [_selector, block] = parent.children @@ -2099,7 +2003,6 @@ describe('Core Nodes', () => { test('@keyframes with from/to', () => { let source = '@keyframes fade { from { opacity: 0; } to { opacity: 1; } }' let root = parse(source, { parse_atrule_preludes: false }) - let keyframes = root.first_child! expect(keyframes.type).toBe(AT_RULE) @@ -2120,7 +2023,6 @@ describe('Core Nodes', () => { 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 }) - let keyframes = root.first_child! let block = keyframes.block! @@ -2137,7 +2039,6 @@ describe('Core Nodes', () => { 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 }) - let keyframes = root.first_child! let block = keyframes.block! @@ -2150,7 +2051,6 @@ 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 root = parse(source, { parse_atrule_preludes: false }) - let keyframes = root.first_child! let block = keyframes.block! @@ -2162,7 +2062,6 @@ describe('Core Nodes', () => { test('@nest with & selector', () => { let source = '.parent { @nest & .child { color: blue; } }' let root = parse(source) - let parent = root.first_child! let [_selector, block] = parent.children @@ -2181,7 +2080,6 @@ describe('Core Nodes', () => { test('@nest with complex selector', () => { let source = '.a { @nest :not(&) { color: red; } }' let root = parse(source) - let a = root.first_child! let [_selector, block] = a.children @@ -2196,7 +2094,6 @@ describe('Core Nodes', () => { test('malformed rule without opening brace', () => { let source = 'body color: red; } div { margin: 0; }' let root = parse(source) - expect(root.children.length).toBeGreaterThan(0) }) @@ -2204,7 +2101,6 @@ describe('Core Nodes', () => { test('rule without closing brace', () => { let source = 'body { color: red; div { margin: 0; }' let root = parse(source) - expect(root.has_children).toBe(true) }) @@ -2212,7 +2108,6 @@ describe('Core Nodes', () => { test('empty rule block', () => { let source = '.empty { }' let root = parse(source) - let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2222,7 +2117,6 @@ describe('Core Nodes', () => { test('declaration without value', () => { let source = 'body { color: }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -2233,7 +2127,6 @@ describe('Core Nodes', () => { test('multiple semicolons', () => { let source = 'body { color: red;;; margin: 0;; }' let root = parse(source) - let rule = root.first_child! expect(rule.children.length).toBe(2) @@ -2242,7 +2135,6 @@ describe('Core Nodes', () => { test('invalid tokens in declaration block', () => { let source = 'body { color: red; @@@; margin: 0; }' let root = parse(source) - let rule = root.first_child! expect(rule.children.length).toBe(2) @@ -2251,7 +2143,6 @@ describe('Core Nodes', () => { test('declaration without colon', () => { let source = 'body { color red; margin: 0; }' let root = parse(source) - let rule = root.first_child! expect(rule.children.length).toBe(2) @@ -2260,7 +2151,6 @@ describe('Core Nodes', () => { test('at-rule without name', () => { let source = '@ { color: red; } body { margin: 0; }' let root = parse(source) - expect(root.children.length).toBeGreaterThan(0) }) @@ -2268,7 +2158,6 @@ describe('Core Nodes', () => { test('nested empty blocks', () => { let source = '.a { .b { .c { } } }' let root = parse(source) - let a = root.first_child! expect(a.type).toBe(STYLE_RULE) @@ -2277,7 +2166,6 @@ describe('Core Nodes', () => { test('trailing comma in selector', () => { let source = '.a, .b, { color: red; }' let root = parse(source) - let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2288,7 +2176,6 @@ describe('Core Nodes', () => { test('skip comments at top level', () => { let source = '/* comment */ body { color: red; } /* another comment */' let root = parse(source) - expect(root.children.length).toBe(1) let rule = root.first_child! @@ -2298,7 +2185,6 @@ describe('Core Nodes', () => { test('skip comments in declaration block', () => { let source = 'body { color: red; /* comment */ margin: 0; }' let root = parse(source) - let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2308,7 +2194,6 @@ describe('Core Nodes', () => { test('skip comments in selector', () => { let source = 'body /* comment */ , /* comment */ div { color: red; }' let root = parse(source) - let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2317,7 +2202,6 @@ describe('Core Nodes', () => { test('comment between property and colon', () => { let source = 'body { color /* comment */ : red; }' let root = parse(source) - expect(root.has_children).toBe(true) }) @@ -2331,7 +2215,6 @@ describe('Core Nodes', () => { body { color: red; } ` let root = parse(source) - expect(root.children.length).toBe(1) }) @@ -2341,7 +2224,6 @@ describe('Core Nodes', () => { test('excessive whitespace', () => { let source = ' body { color : red ; } ' let root = parse(source) - let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2350,7 +2232,6 @@ describe('Core Nodes', () => { test('tabs and newlines', () => { let source = 'body\t{\n\tcolor:\tred;\n}\n' let root = parse(source) - let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2359,7 +2240,6 @@ describe('Core Nodes', () => { test('no whitespace', () => { let source = 'body{color:red;margin:0}' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -2399,7 +2279,6 @@ describe('Core Nodes', () => { } ` let root = parse(source) - let card = root.first_child! expect(card.type).toBe(STYLE_RULE) @@ -2423,7 +2302,6 @@ describe('Core Nodes', () => { } ` let root = parse(source) - let [layer1, layer2] = root.children expect(layer1.type).toBe(AT_RULE) @@ -2433,7 +2311,6 @@ describe('Core Nodes', () => { test('vendor prefixed properties', () => { let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' let root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -2446,7 +2323,6 @@ describe('Core Nodes', () => { test('complex selector list', () => { let source = 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' let root = parse(source) - let rule = root.first_child! let selector = rule.first_child! @@ -2465,7 +2341,6 @@ describe('Core Nodes', () => { } ` let root = parse(source, { parse_atrule_preludes: false }) - let supports = root.first_child! let supports_block = supports.block! @@ -2480,7 +2355,6 @@ 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 root = parse(source) - let rule = root.first_child! let [_selector, block] = rule.children @@ -2492,7 +2366,6 @@ describe('Core Nodes', () => { test('custom properties', () => { let source = ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' let root = parse(source) - expect(root.children.length).toBeGreaterThan(0) let first_rule = root.first_child! @@ -2502,7 +2375,6 @@ describe('Core Nodes', () => { test('attribute selectors with operators', () => { let source = '[href^="https"][href$=".pdf"][class*="doc"] { color: red; }' let root = parse(source) - let rule = root.first_child! let selector = rule.first_child! @@ -2514,7 +2386,6 @@ describe('Core Nodes', () => { test('pseudo-elements', () => { let source = '.text::before { content: "→"; } .text::after { content: "←"; }' let root = parse(source) - let [rule1, rule2] = root.children expect(rule1.type).toBe(STYLE_RULE) @@ -2524,7 +2395,6 @@ describe('Core Nodes', () => { test('multiple !important declarations', () => { let source = '.override { color: red !important; margin: 0 !important; padding: 0 !ie; }' let root = parse(source) - let rule = root.first_child! let block = rule.block!