diff --git a/src/api.test.ts b/src/api.test.ts index c017e68..8d342e5 100644 --- a/src/api.test.ts +++ b/src/api.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest' -import { Parser, parse } from './parse' +import { parse } from './parse' import { parse_selector } from './parse-selector' import { DECLARATION, @@ -18,8 +18,7 @@ describe('CSSNode', () => { describe('iteration', () => { test('should be iterable with for-of', () => { const source = 'body { color: red; margin: 0; padding: 10px; }' - const parser = new Parser(source, { parse_selectors: false, parse_values: false }) - const root = parser.parse() + const root = parse(source, { parse_selectors: false, parse_values: false }) const rule = root.first_child! const block = rule.block! @@ -34,8 +33,7 @@ describe('CSSNode', () => { test('should work with spread operator', () => { const source = 'body { color: red; } div { margin: 0; }' - const parser = new Parser(source, { parse_selectors: false, parse_values: false }) - const root = parser.parse() + const root = parse(source, { parse_selectors: false, parse_values: false }) const rules = [...root] expect(rules).toHaveLength(2) @@ -45,8 +43,7 @@ describe('CSSNode', () => { test('should work with Array.from', () => { const source = '@media print { body { color: black; } }' - const parser = new Parser(source, { parse_selectors: false, parse_values: false, parse_atrule_preludes: false }) - const root = parser.parse() + const root = parse(source, { parse_selectors: false, parse_values: false, parse_atrule_preludes: false }) const media = root.first_child! const block = media.block! @@ -58,12 +55,11 @@ describe('CSSNode', () => { test('should iterate over empty children', () => { const source = '@import url("style.css");' - const parser = new Parser(source, { + const root = parse(source, { parse_selectors: false, parse_values: false, parse_atrule_preludes: false, }) - const root = parser.parse() const importRule = root.first_child! const children = [...importRule] @@ -75,8 +71,7 @@ describe('CSSNode', () => { describe('has_prelude', () => { test('should return true for @media with prelude', () => { const source = '@media (min-width: 768px) { body { color: red; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const media = root.first_child! expect(media.type).toBe(AT_RULE) @@ -86,8 +81,7 @@ describe('CSSNode', () => { test('should return true for @supports with prelude', () => { const source = '@supports (display: grid) { .grid { display: grid; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const supports = root.first_child! expect(supports.type).toBe(AT_RULE) @@ -97,8 +91,7 @@ describe('CSSNode', () => { test('should return true for @layer with name', () => { const source = '@layer utilities { .btn { padding: 1rem; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const layer = root.first_child! expect(layer.type).toBe(AT_RULE) @@ -108,8 +101,7 @@ describe('CSSNode', () => { test('should return false for @layer without name', () => { const source = '@layer { .btn { padding: 1rem; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const layer = root.first_child! expect(layer.type).toBe(AT_RULE) @@ -119,8 +111,7 @@ describe('CSSNode', () => { test('should return true for @keyframes with name', () => { const source = '@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const keyframes = root.first_child! expect(keyframes.type).toBe(AT_RULE) @@ -130,8 +121,7 @@ describe('CSSNode', () => { test('should return false for @font-face without prelude', () => { const source = '@font-face { font-family: "Custom"; src: url("font.woff2"); }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const fontFace = root.first_child! expect(fontFace.type).toBe(AT_RULE) @@ -141,8 +131,7 @@ describe('CSSNode', () => { test('should return false for @page without prelude', () => { const source = '@page { margin: 1in; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const page = root.first_child! expect(page.type).toBe(AT_RULE) @@ -152,8 +141,7 @@ describe('CSSNode', () => { test('should return true for @import with options', () => { const source = '@import url("styles.css") layer(base) supports(display: flex);' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const importRule = root.first_child! expect(importRule.type).toBe(AT_RULE) @@ -163,8 +151,7 @@ describe('CSSNode', () => { test('should work efficiently without creating strings', () => { const source = '@media (min-width: 768px) { body { color: red; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const media = root.first_child! // has_prelude should be faster than prelude !== null @@ -175,8 +162,7 @@ describe('CSSNode', () => { test('should work for other node types that use value field', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selector = rule.first_child! const block = selector.next_sibling! @@ -196,8 +182,7 @@ describe('CSSNode', () => { describe('has_block', () => { test('should return true for style rules with blocks', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -206,8 +191,7 @@ describe('CSSNode', () => { test('should return true for empty style rule blocks', () => { const source = 'body { }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -216,8 +200,7 @@ describe('CSSNode', () => { test('should return true for @media with block', () => { const source = '@media (min-width: 768px) { body { color: red; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const media = root.first_child! expect(media.type).toBe(AT_RULE) @@ -226,8 +209,7 @@ describe('CSSNode', () => { test('should return true for @supports with block', () => { const source = '@supports (display: grid) { .grid { display: grid; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const supports = root.first_child! expect(supports.type).toBe(AT_RULE) @@ -236,8 +218,7 @@ describe('CSSNode', () => { test('should return true for @layer with block', () => { const source = '@layer utilities { .btn { padding: 1rem; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const layer = root.first_child! expect(layer.type).toBe(AT_RULE) @@ -246,8 +227,7 @@ describe('CSSNode', () => { test('should return true for anonymous @layer with block', () => { const source = '@layer { .btn { padding: 1rem; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const layer = root.first_child! expect(layer.type).toBe(AT_RULE) @@ -256,8 +236,7 @@ describe('CSSNode', () => { test('should return true for @font-face with block', () => { const source = '@font-face { font-family: "Custom"; src: url("font.woff2"); }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const fontFace = root.first_child! expect(fontFace.type).toBe(AT_RULE) @@ -266,8 +245,7 @@ describe('CSSNode', () => { test('should return true for @keyframes with block', () => { const source = '@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const keyframes = root.first_child! expect(keyframes.type).toBe(AT_RULE) @@ -276,8 +254,7 @@ describe('CSSNode', () => { test('should return false for @import without block', () => { const source = '@import url("styles.css");' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const importRule = root.first_child! expect(importRule.type).toBe(AT_RULE) @@ -286,8 +263,7 @@ describe('CSSNode', () => { test('should return false for @import with preludes but no block', () => { const source = '@import url("styles.css") layer(base) supports(display: flex);' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const importRule = root.first_child! expect(importRule.type).toBe(AT_RULE) @@ -301,8 +277,7 @@ describe('CSSNode', () => { @import url("file.css") layer(base); @layer utilities { .btn { padding: 1rem; } } ` - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const importRule = root.first_child! const layerRule = importRule.next_sibling! @@ -317,8 +292,7 @@ describe('CSSNode', () => { test('should return false for non-rule nodes', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selector = rule.first_child! const declaration = selector.next_sibling! @@ -337,8 +311,7 @@ describe('CSSNode', () => { @font-face { font-family: "Custom"; } @keyframes fadeIn { from { opacity: 0; } } ` - const parser = new Parser(css) - const root = parser.parse() + const root = parse(css) const nodes = [...root] const [media, importRule, supports, layer, fontFace, keyframes] = nodes @@ -355,8 +328,7 @@ describe('CSSNode', () => { describe('has_declarations', () => { test('should return true for style rules with declarations', () => { const source = 'body { color: red; margin: 0; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -365,8 +337,7 @@ describe('CSSNode', () => { test('should return false for empty style rules', () => { const source = 'body { }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -375,8 +346,7 @@ describe('CSSNode', () => { test('should return false for style rules with only nested rules', () => { const source = 'body { .nested { color: red; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -385,8 +355,7 @@ describe('CSSNode', () => { test('should return true for style rules with both declarations and nested rules', () => { const source = 'body { color: blue; .nested { margin: 0; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -395,8 +364,7 @@ describe('CSSNode', () => { test('should return false for at-rules', () => { const source = '@media screen { body { color: red; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const media = root.first_child! expect(media.type).toBe(AT_RULE) @@ -407,16 +375,14 @@ describe('CSSNode', () => { describe('type_name property', () => { test('should return stylesheet for root node', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) expect(root.type_name).toBe('StyleSheet') }) test('should return style_rule for style rules', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! expect(rule.type_name).toBe('Rule') @@ -424,8 +390,7 @@ describe('CSSNode', () => { test('should return declaration for declarations', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const block = rule.block! const decl = block.first_child! @@ -435,8 +400,7 @@ describe('CSSNode', () => { test('should return at_rule for at-rules', () => { const source = '@media screen { body { color: red; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const media = root.first_child! expect(media.type_name).toBe('Atrule') @@ -444,8 +408,7 @@ describe('CSSNode', () => { test('should return selector_list for selector lists', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! @@ -454,8 +417,7 @@ describe('CSSNode', () => { test('should return selector_type for type selectors', () => { const source = 'div { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! const selector = selectorList.first_child! @@ -466,8 +428,7 @@ describe('CSSNode', () => { test('should return selector_class for class selectors', () => { const source = '.foo { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! const selector = selectorList.first_child! @@ -478,8 +439,7 @@ describe('CSSNode', () => { test('should return selector_id for ID selectors', () => { const source = '#bar { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! const selector = selectorList.first_child! @@ -490,8 +450,7 @@ describe('CSSNode', () => { test('should return selector_universal for universal selectors', () => { const source = '* { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! const selector = selectorList.first_child! @@ -502,8 +461,7 @@ describe('CSSNode', () => { test('should return selector_attribute for attribute selectors', () => { const source = '[href] { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! const selector = selectorList.first_child! @@ -514,8 +472,7 @@ describe('CSSNode', () => { test('should return selector_pseudo_class for pseudo-class selectors', () => { const source = ':hover { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! const selector = selectorList.first_child! @@ -526,8 +483,7 @@ describe('CSSNode', () => { test('should return selector_pseudo_element for pseudo-element selectors', () => { const source = '::before { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! const selector = selectorList.first_child! @@ -538,8 +494,7 @@ describe('CSSNode', () => { test('should return selector_combinator for combinators', () => { const source = 'div > span { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const selectorList = rule.first_child! const selector = selectorList.first_child! @@ -550,8 +505,7 @@ describe('CSSNode', () => { test('should return value_keyword for keyword values', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const block = rule.block! const decl = block.first_child! @@ -562,8 +516,7 @@ describe('CSSNode', () => { test('should return value_number for numeric values', () => { const source = 'body { opacity: 0.5; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const block = rule.block! const decl = block.first_child! @@ -574,8 +527,7 @@ describe('CSSNode', () => { test('should return value_dimension for dimension values', () => { const source = 'body { width: 100px; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const block = rule.block! const decl = block.first_child! @@ -586,8 +538,7 @@ describe('CSSNode', () => { test('should return value_string for string values', () => { const source = 'body { content: "hello"; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const block = rule.block! const decl = block.first_child! @@ -598,8 +549,7 @@ describe('CSSNode', () => { test('should return value_color for color values', () => { const source = 'body { color: #ff0000; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const block = rule.block! const decl = block.first_child! @@ -610,8 +560,7 @@ describe('CSSNode', () => { test('should return value_function for function values', () => { const source = 'body { width: calc(100% - 20px); }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const rule = root.first_child! const block = rule.block! const decl = block.first_child! @@ -622,8 +571,7 @@ describe('CSSNode', () => { test('should return prelude_media_query for media query preludes', () => { const source = '@media screen and (min-width: 768px) { body { color: red; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) const media = root.first_child! const prelude = media.first_child! diff --git a/src/arena.ts b/src/arena.ts index b1695cc..4232dfd 100644 --- a/src/arena.ts +++ b/src/arena.ts @@ -89,6 +89,7 @@ export const ATTR_FLAG_NONE = 0 // No flag export const ATTR_FLAG_CASE_INSENSITIVE = 1 // [attr=value i] export const ATTR_FLAG_CASE_SENSITIVE = 2 // [attr=value s] +/** @internal */ export class CSSDataArena { private buffer: ArrayBuffer private view: DataView diff --git a/src/lexer.ts b/src/lexer.ts index b966e44..8080d97 100644 --- a/src/lexer.ts +++ b/src/lexer.ts @@ -68,6 +68,7 @@ export interface LexerPosition { token_column: number } +/** @internal */ export class Lexer { source: string pos: number diff --git a/src/parse-anplusb.ts b/src/parse-anplusb.ts index ac3db97..2879f30 100644 --- a/src/parse-anplusb.ts +++ b/src/parse-anplusb.ts @@ -11,6 +11,7 @@ import { CHAR_MINUS_HYPHEN, CHAR_PLUS } from './string-utils' import { skip_whitespace_forward } from './parse-utils' import { CSSNode } from './css-node' +/** @internal */ export class ANplusBParser { private lexer: Lexer private arena: CSSDataArena diff --git a/src/parse-atrule-prelude.ts b/src/parse-atrule-prelude.ts index 26441a0..bcdb691 100644 --- a/src/parse-atrule-prelude.ts +++ b/src/parse-atrule-prelude.ts @@ -28,6 +28,7 @@ import { str_equals } from './string-utils' import { trim_boundaries, skip_whitespace_forward } from './parse-utils' import { CSSNode } from './css-node' +/** @internal */ export class AtRulePreludeParser { private lexer: Lexer private arena: CSSDataArena diff --git a/src/parse-options.test.ts b/src/parse-options.test.ts index 85a1bd7..c3b9ac9 100644 --- a/src/parse-options.test.ts +++ b/src/parse-options.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { Parser } from './parse' +import { parse } from './parse' import { SELECTOR_LIST, DECLARATION, IDENTIFIER } from './arena' describe('Parser Options', () => { @@ -7,8 +7,7 @@ describe('Parser Options', () => { describe('Default behavior (all parsing enabled)', () => { it('should parse values and selectors by default', () => { - const parser = new Parser(css) - const root = parser.parse() + const root = parse(css) const rule = root.first_child // Check selector is parsed with detailed structure @@ -27,8 +26,7 @@ describe('Parser Options', () => { }) it('should parse values and selectors with explicit options', () => { - const parser = new Parser(css, { parse_values: true, parse_selectors: true }) - const root = parser.parse() + const root = parse(css, { parse_values: true, parse_selectors: true }) const rule = root.first_child // Check selector is parsed @@ -46,8 +44,7 @@ describe('Parser Options', () => { describe('parse_values disabled', () => { it('should not parse value details when parse_values is false', () => { - const parser = new Parser(css, { parse_values: false }) - const root = parser.parse() + const root = parse(css, { parse_values: false }) const rule = root.first_child // Selector should still be parsed @@ -67,8 +64,7 @@ describe('Parser Options', () => { }) it('should handle complex values without parsing', () => { - const parser = new Parser('div { margin: 10px 20px; }', { parse_values: false }) - const root = parser.parse() + const root = parse('div { margin: 10px 20px; }', { parse_values: false }) const rule = root.first_child const selector = rule?.first_child const block = selector?.next_sibling @@ -80,8 +76,7 @@ describe('Parser Options', () => { }) it('should handle function values without parsing', () => { - const parser = new Parser('div { color: rgb(255, 0, 0); }', { parse_values: false }) - const root = parser.parse() + const root = parse('div { color: rgb(255, 0, 0); }', { parse_values: false }) const rule = root.first_child const selector = rule?.first_child const block = selector?.next_sibling @@ -95,8 +90,7 @@ describe('Parser Options', () => { describe('parseSelectors disabled', () => { it('should not parse selector details when parseSelectors is false', () => { - const parser = new Parser(css, { parse_selectors: false }) - const root = parser.parse() + const root = parse(css, { parse_selectors: false }) const rule = root.first_child // Selector should exist but be simple (just NODE_SELECTOR_LIST, no detailed structure) @@ -114,8 +108,7 @@ describe('Parser Options', () => { }) it('should handle complex selectors without parsing', () => { - const parser = new Parser('div.container#app { color: red; }', { parse_selectors: false }) - const root = parser.parse() + const root = parse('div.container#app { color: red; }', { parse_selectors: false }) const rule = root.first_child const selector = rule?.first_child @@ -125,8 +118,7 @@ describe('Parser Options', () => { }) it('should handle selector lists without parsing', () => { - const parser = new Parser('div, p, span { color: red; }', { parse_selectors: false }) - const root = parser.parse() + const root = parse('div, p, span { color: red; }', { parse_selectors: false }) const rule = root.first_child const selector = rule?.first_child @@ -138,8 +130,7 @@ describe('Parser Options', () => { describe('Both parse_values and parseSelectors disabled', () => { it('should not parse details for values or selectors', () => { - const parser = new Parser(css, { parse_values: false, parse_selectors: false }) - const root = parser.parse() + const root = parse(css, { parse_values: false, parse_selectors: false }) const rule = root.first_child // Selector should be simple @@ -164,8 +155,7 @@ describe('Parser Options', () => { color: rgb(255, 0, 0); } ` - const parser = new Parser(css, { parse_values: false, parse_selectors: false }) - const root = parser.parse() + const root = parse(css, { parse_values: false, parse_selectors: false }) const rule = root.first_child const selector = rule?.first_child @@ -194,8 +184,7 @@ describe('Parser Options', () => { margin: 10px 20px 30px 40px; } ` - const parser = new Parser(css, { parse_values: false }) - const root = parser.parse() + const root = parse(css, { parse_values: false }) const rule = root.first_child const selector = rule?.first_child @@ -219,8 +208,7 @@ describe('Parser Options', () => { .another-complex[data-attr~="value"] { margin: 0; } #very-specific-id:not(.excluded) { padding: 10px; } ` - const parser = new Parser(css, { parse_selectors: false }) - const root = parser.parse() + const root = parse(css, { parse_selectors: false }) // Can quickly count rules without parsing complex selectors let count = 0 @@ -236,8 +224,7 @@ describe('Parser Options', () => { describe('Options validation', () => { it('should accept empty options object', () => { - const parser = new Parser(css, {}) - const root = parser.parse() + const root = parse(css, {}) const rule = root.first_child const selector = rule?.first_child const block = selector?.next_sibling @@ -249,8 +236,7 @@ describe('Parser Options', () => { }) it('should accept partial options', () => { - const parser = new Parser(css, { parse_values: false }) - const root = parser.parse() + const root = parse(css, { parse_values: false }) const rule = root.first_child const selector = rule?.first_child const block = selector?.next_sibling @@ -263,11 +249,10 @@ describe('Parser Options', () => { }) it('should accept skip_comments with parsing options', () => { - const parser = new Parser('/* test */ body { color: red; }', { + const root = parse('/* test */ body { color: red; }', { skip_comments: true, parse_values: false, }) - const root = parser.parse() const rule = root.first_child // Comment should be skipped diff --git a/src/parse-selector.ts b/src/parse-selector.ts index 0276a43..557bc6d 100644 --- a/src/parse-selector.ts +++ b/src/parse-selector.ts @@ -69,6 +69,7 @@ import { import { ANplusBParser } from './parse-anplusb' import { CSSNode } from './css-node' +/** @internal */ export class SelectorParser { private lexer: Lexer private arena: CSSDataArena diff --git a/src/parse-utils.ts b/src/parse-utils.ts index b8a6b2a..305219a 100644 --- a/src/parse-utils.ts +++ b/src/parse-utils.ts @@ -64,6 +64,7 @@ export function parse_dimension(text: string): { value: number; unit: string } { * @param pos - Starting position * @param end - End boundary (exclusive) * @returns New position after skipping whitespace + * @internal */ export function skip_whitespace_forward(source: string, pos: number, end: number): number { while (pos < end && is_whitespace(source.charCodeAt(pos))) { @@ -79,6 +80,7 @@ export function skip_whitespace_forward(source: string, pos: number, end: number * @param pos - Starting position * @param end - End boundary (exclusive) * @returns New position after skipping whitespace/comments + * @internal */ export function skip_whitespace_and_comments_forward(source: string, pos: number, end: number): number { while (pos < end) { @@ -115,6 +117,7 @@ export function skip_whitespace_and_comments_forward(source: string, pos: number * @param pos - Starting position (exclusive, scanning backward from pos-1) * @param start - Start boundary (inclusive, won't go before this) * @returns New position after skipping whitespace/comments backward + * @internal */ export function skip_whitespace_and_comments_backward(source: string, pos: number, start: number): number { while (pos > start) { @@ -151,6 +154,7 @@ export function skip_whitespace_and_comments_backward(source: string, pos: numbe * @param start - Start offset in source * @param end - End offset in source * @returns [trimmed_start, trimmed_end] or null if all whitespace/comments + * @internal * * Skips whitespace (space, tab, newline, CR, FF) and CSS comments from both ends * of the specified range. Returns the trimmed boundaries or null if the range diff --git a/src/parse-value.test.ts b/src/parse-value.test.ts index 867edf7..02d2f6f 100644 --- a/src/parse-value.test.ts +++ b/src/parse-value.test.ts @@ -1,12 +1,11 @@ import { describe, it, expect } from 'vitest' -import { Parser } from './parse' +import { parse } from './parse' import { IDENTIFIER, NUMBER, DIMENSION, STRING, HASH, FUNCTION, OPERATOR, PARENTHESIS, URL } from './arena' describe('Value Node Types', () => { // Helper to get first value node from a declaration const getValue = (css: string) => { - const parser = new Parser(css) - const root = parser.parse() + const root = parse(css) const rule = root.first_child const decl = rule?.first_child?.next_sibling?.first_child // selector → block → declaration return decl?.values[0] @@ -68,8 +67,7 @@ describe('Value Node Types', () => { describe('OPERATOR', () => { it('should have correct offset and length', () => { - const parser = new Parser('div { font-family: Arial, sans-serif; }') - const root = parser.parse() + const root = parse('div { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child const comma = decl?.values[1] expect(comma?.offset).toBe(24) @@ -80,8 +78,7 @@ describe('Value Node Types', () => { describe('PARENTHESIS', () => { it('should have correct offset and length', () => { - const parser = new Parser('div { width: calc((100% - 50px) / 2); }') - const root = parser.parse() + const root = parse('div { width: calc((100% - 50px) / 2); }') const func = root.first_child?.first_child?.next_sibling?.first_child?.values[0] const paren = func?.children[0] expect(paren?.offset).toBe(18) @@ -131,15 +128,13 @@ describe('Value Node Types', () => { }) it('OPERATOR type constant', () => { - const parser = new Parser('div { font-family: Arial, sans-serif; }') - const root = parser.parse() + const root = parse('div { font-family: Arial, sans-serif; }') const comma = root.first_child?.first_child?.next_sibling?.first_child?.values[1] expect(comma?.type).toBe(OPERATOR) }) it('PARENTHESIS type constant', () => { - const parser = new Parser('div { width: calc((100% - 50px) / 2); }') - const root = parser.parse() + const root = parse('div { width: calc((100% - 50px) / 2); }') const func = root.first_child?.first_child?.next_sibling?.first_child?.values[0] const paren = func?.children[0] expect(paren?.type).toBe(PARENTHESIS) @@ -183,15 +178,13 @@ describe('Value Node Types', () => { }) it('OPERATOR type_name', () => { - const parser = new Parser('div { font-family: Arial, sans-serif; }') - const root = parser.parse() + const root = parse('div { font-family: Arial, sans-serif; }') const comma = root.first_child?.first_child?.next_sibling?.first_child?.values[1] expect(comma?.type_name).toBe('Operator') }) it('PARENTHESIS type_name', () => { - const parser = new Parser('div { width: calc((100% - 50px) / 2); }') - const root = parser.parse() + const root = parse('div { width: calc((100% - 50px) / 2); }') const func = root.first_child?.first_child?.next_sibling?.first_child?.values[0] const paren = func?.children[0] expect(paren?.type_name).toBe('Parentheses') @@ -206,8 +199,7 @@ describe('Value Node Types', () => { describe('Value Properties', () => { describe('IDENTIFIER', () => { it('should parse keyword values', () => { - const parser = new Parser('body { color: red; }') - const root = parser.parse() + const root = parse('body { color: red; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBe('red') @@ -216,8 +208,7 @@ describe('Value Node Types', () => { }) it('should parse multiple keywords', () => { - const parser = new Parser('body { font-family: Arial, sans-serif; }') - const root = parser.parse() + const root = parse('body { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(3) @@ -230,8 +221,7 @@ describe('Value Node Types', () => { describe('NUMBER', () => { it('should parse number values', () => { - const parser = new Parser('body { opacity: 0.5; }') - const root = parser.parse() + const root = parse('body { opacity: 0.5; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBe('0.5') @@ -240,8 +230,7 @@ describe('Value Node Types', () => { }) it('should handle negative numbers', () => { - const parser = new Parser('body { margin: -10px; }') - const root = parser.parse() + const root = parse('body { margin: -10px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(1) @@ -250,8 +239,7 @@ describe('Value Node Types', () => { }) it('should handle zero without unit', () => { - const parser = new Parser('body { margin: 0; }') - const root = parser.parse() + const root = parse('body { margin: 0; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(1) @@ -262,8 +250,7 @@ describe('Value Node Types', () => { describe('DIMENSION', () => { it('should parse px dimension values', () => { - const parser = new Parser('body { width: 100px; }') - const root = parser.parse() + const root = parse('body { width: 100px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBe('100px') @@ -274,8 +261,7 @@ describe('Value Node Types', () => { }) it('should parse em dimension values', () => { - const parser = new Parser('body { font-size: 3em; }') - const root = parser.parse() + const root = parse('body { font-size: 3em; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBe('3em') @@ -286,8 +272,7 @@ describe('Value Node Types', () => { }) it('should parse percentage values', () => { - const parser = new Parser('body { width: 50%; }') - const root = parser.parse() + const root = parse('body { width: 50%; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBe('50%') @@ -296,8 +281,7 @@ describe('Value Node Types', () => { }) it('should handle zero with unit', () => { - const parser = new Parser('body { margin: 0px; }') - const root = parser.parse() + const root = parse('body { margin: 0px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(1) @@ -306,8 +290,7 @@ describe('Value Node Types', () => { }) it('should parse margin shorthand', () => { - const parser = new Parser('body { margin: 10px 20px 30px 40px; }') - const root = parser.parse() + const root = parse('body { margin: 10px 20px 30px 40px; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(4) @@ -324,8 +307,7 @@ describe('Value Node Types', () => { describe('STRING', () => { it('should parse string values', () => { - const parser = new Parser('body { content: "hello"; }') - const root = parser.parse() + const root = parse('body { content: "hello"; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBe('"hello"') @@ -336,8 +318,7 @@ describe('Value Node Types', () => { describe('HASH', () => { it('should parse color values', () => { - const parser = new Parser('body { color: #ff0000; }') - const root = parser.parse() + const root = parse('body { color: #ff0000; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBe('#ff0000') @@ -348,8 +329,7 @@ describe('Value Node Types', () => { describe('FUNCTION', () => { it('should parse simple function', () => { - const parser = new Parser('body { color: rgb(255, 0, 0); }') - const root = parser.parse() + const root = parse('body { color: rgb(255, 0, 0); }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(1) @@ -359,8 +339,7 @@ describe('Value Node Types', () => { }) it('should parse function arguments', () => { - const parser = new Parser('body { color: rgb(255, 0, 0); }') - const root = parser.parse() + const root = parse('body { color: rgb(255, 0, 0); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -378,8 +357,7 @@ describe('Value Node Types', () => { }) it('should parse nested functions', () => { - const parser = new Parser('body { width: calc(100% - 20px); }') - const root = parser.parse() + const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(1) @@ -395,8 +373,7 @@ describe('Value Node Types', () => { }) it('should parse var() function', () => { - const parser = new Parser('body { color: var(--primary-color); }') - const root = parser.parse() + const root = parse('body { color: var(--primary-color); }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(1) @@ -408,8 +385,7 @@ describe('Value Node Types', () => { }) it('should provide node.value for calc()', () => { - const parser = new Parser('body { width: calc(100% - 20px); }') - const root = parser.parse() + const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -421,8 +397,7 @@ describe('Value Node Types', () => { }) it('should provide node.value for var() function', () => { - const parser = new Parser('body { color: var(--primary-color); }') - const root = parser.parse() + const root = parse('body { color: var(--primary-color); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -434,8 +409,7 @@ describe('Value Node Types', () => { }) it('should parse transform value', () => { - const parser = new Parser('body { transform: translateX(10px) rotate(45deg); }') - const root = parser.parse() + const root = parse('body { transform: translateX(10px) rotate(45deg); }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(2) @@ -446,8 +420,7 @@ describe('Value Node Types', () => { }) it('should parse filter value', () => { - const parser = new Parser('body { filter: blur(5px) brightness(1.2); }') - const root = parser.parse() + const root = parse('body { filter: blur(5px) brightness(1.2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(2) @@ -462,8 +435,7 @@ describe('Value Node Types', () => { describe('OPERATOR', () => { it('should parse comma operator', () => { - const parser = new Parser('body { font-family: Arial, sans-serif; }') - const root = parser.parse() + const root = parse('body { font-family: Arial, sans-serif; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values[1].type).toBe(OPERATOR) @@ -471,8 +443,7 @@ describe('Value Node Types', () => { }) it('should parse calc operators', () => { - const parser = new Parser('body { width: calc(100% - 20px); }') - const root = parser.parse() + const root = parse('body { width: calc(100% - 20px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -481,8 +452,7 @@ describe('Value Node Types', () => { }) it('should parse all calc operators', () => { - const parser = new Parser('body { width: calc(1px + 2px * 3px / 4px - 5px); }') - const root = parser.parse() + const root = parse('body { width: calc(1px + 2px * 3px / 4px - 5px); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -497,8 +467,7 @@ describe('Value Node Types', () => { describe('PARENTHESIS', () => { it('should parse parenthesized expressions in calc()', () => { - const parser = new Parser('body { width: calc((100% - 50px) / 2); }') - const root = parser.parse() + const root = parse('body { width: calc((100% - 50px) / 2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -530,8 +499,7 @@ describe('Value Node Types', () => { }) it('should parse complex nested parentheses', () => { - const parser = new Parser('body { width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); }') - const root = parser.parse() + const root = parse('body { width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -570,8 +538,7 @@ describe('Value Node Types', () => { describe('URL', () => { it('should parse url() function with quoted string', () => { - const parser = new Parser('body { background: url("image.png"); }') - const root = parser.parse() + const root = parse('body { background: url("image.png"); }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(1) @@ -583,8 +550,7 @@ describe('Value Node Types', () => { }) it('should parse url() function with unquoted URL containing dots', () => { - const parser = new Parser('body { cursor: url(mycursor.cur); }') - const root = parser.parse() + const root = parse('body { cursor: url(mycursor.cur); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -598,8 +564,7 @@ describe('Value Node Types', () => { }) it('should parse src() function with unquoted URL', () => { - const parser = new Parser('body { content: src(myfont.woff2); }') - const root = parser.parse() + const root = parse('body { content: src(myfont.woff2); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -611,8 +576,7 @@ describe('Value Node Types', () => { }) it('should parse url() with base64 data URL', () => { - const parser = new Parser('body { background: url(data:image/png;base64,iVBORw0KGg); }') - const root = parser.parse() + const root = parse('body { background: url(data:image/png;base64,iVBORw0KGg); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -623,8 +587,7 @@ describe('Value Node Types', () => { }) it('should parse url() with inline SVG', () => { - const parser = new Parser('body { background: url(data:image/svg+xml,); }') - const root = parser.parse() + const root = parse('body { background: url(data:image/svg+xml,); }') const decl = root.first_child?.first_child?.next_sibling?.first_child const func = decl?.values[0] @@ -635,8 +598,7 @@ describe('Value Node Types', () => { }) it('should parse complex background value with url()', () => { - const parser = new Parser('body { background: url("bg.png") no-repeat center center / cover; }') - const root = parser.parse() + const root = parse('body { background: url("bg.png") no-repeat center center / cover; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values.length).toBeGreaterThan(1) @@ -649,8 +611,7 @@ describe('Value Node Types', () => { describe('Mixed values', () => { it('should parse mixed value types', () => { - const parser = new Parser('body { border: 1px solid red; }') - const root = parser.parse() + const root = parse('body { border: 1px solid red; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.values).toHaveLength(3) @@ -663,8 +624,7 @@ describe('Value Node Types', () => { }) it('should handle empty value', () => { - const parser = new Parser('body { color: ; }') - const root = parser.parse() + const root = parse('body { color: ; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBeNull() @@ -672,8 +632,7 @@ describe('Value Node Types', () => { }) it('should handle value with !important', () => { - const parser = new Parser('body { color: red !important; }') - const root = parser.parse() + const root = parse('body { color: red !important; }') const decl = root.first_child?.first_child?.next_sibling?.first_child expect(decl?.value).toBe('red') diff --git a/src/parse-value.ts b/src/parse-value.ts index 998a714..5b9ed40 100644 --- a/src/parse-value.ts +++ b/src/parse-value.ts @@ -18,6 +18,7 @@ import { import { is_whitespace, CHAR_MINUS_HYPHEN, CHAR_PLUS, CHAR_ASTERISK, CHAR_FORWARD_SLASH } from './string-utils' import { CSSNode } from './css-node' +/** @internal */ export class ValueParser { private lexer: Lexer private arena: CSSDataArena diff --git a/src/parse.test.ts b/src/parse.test.ts index afa5028..4795cbd 100644 --- a/src/parse.test.ts +++ b/src/parse.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from 'vitest' -import { Parser, parse } from './parse' +import { parse } from './parse' import { STYLESHEET, STYLE_RULE, @@ -277,26 +277,19 @@ describe('Core Nodes', () => { describe('Node Properties', () => { describe('STYLESHEET', () => { test('empty stylesheet has no children', () => { - const parser = new Parser('') - const root = parser.parse() + const root = parse('') + expect(root.type).toBe(STYLESHEET) expect(root.has_children).toBe(false) }) test('stylesheet with only whitespace has no children', () => { - const parser = new Parser(' \n\n ') - const root = parser.parse() + const root = parse(' \n\n ') + expect(root.type).toBe(STYLESHEET) expect(root.has_children).toBe(false) }) - test('parser creates arena sized for source', () => { - const source = 'body { color: red; }' - const parser = new Parser(source) - const arena = parser.get_arena() - expect(arena.get_capacity()).toBeGreaterThan(0) - expect(arena.get_count()).toBe(1) // Count starts at 1 (0 is reserved for "no node") - }) }) describe('STYLE_RULE', () => { @@ -642,8 +635,8 @@ describe('Core Nodes', () => { describe('Selector parsing', () => { test('should parse simple selector', () => { const source = 'body { }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! expect(rule.has_children).toBe(true) @@ -657,8 +650,8 @@ describe('Core Nodes', () => { test('should parse complex selector', () => { const source = 'div.class > p#id { }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const selectorlist = rule.first_child! @@ -708,8 +701,8 @@ describe('Core Nodes', () => { describe('Multiple rules', () => { test('should parse multiple style rules', () => { - const parser = new Parser('body { } div { }') - const root = parser.parse() + const root = parse('body { } div { }') + const [rule1, rule2] = root.children expect(rule1.type).toBe(STYLE_RULE) @@ -723,8 +716,8 @@ describe('Core Nodes', () => { describe('Statement at-rules (no block)', () => { test('@import', () => { const source = '@import url("style.css");' - const parser = new Parser(source, { parse_atrule_preludes: false }) - const root = parser.parse() + const root = parse(source, { parse_atrule_preludes: false }) + const atRule = root.first_child! expect(atRule.type).toBe(AT_RULE) @@ -734,8 +727,8 @@ describe('Core Nodes', () => { test('@namespace', () => { const source = '@namespace url(http://www.w3.org/1999/xhtml);' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const atRule = root.first_child! expect(atRule.type).toBe(AT_RULE) @@ -747,8 +740,8 @@ describe('Core Nodes', () => { describe('Case-insensitive at-rule names', () => { test('should parse @MEDIA (uppercase)', () => { const source = '@MEDIA (min-width: 768px) { body { color: red; } }' - const parser = new Parser(source, { parse_atrule_preludes: false }) - const root = parser.parse() + const root = parse(source, { parse_atrule_preludes: false }) + const media = root.first_child! expect(media.type).toBe(AT_RULE) @@ -761,8 +754,8 @@ describe('Core Nodes', () => { test('should parse @Font-Face (mixed case)', () => { const source = '@Font-Face { font-family: "MyFont"; src: url("font.woff"); }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const fontFace = root.first_child! expect(fontFace.type).toBe(AT_RULE) @@ -776,8 +769,8 @@ describe('Core Nodes', () => { test('should parse @SUPPORTS (uppercase)', () => { const source = '@SUPPORTS (display: grid) { .grid { display: grid; } }' - const parser = new Parser(source, { parse_atrule_preludes: false }) - const root = parser.parse() + const root = parse(source, { parse_atrule_preludes: false }) + const supports = root.first_child! expect(supports.type).toBe(AT_RULE) @@ -789,8 +782,8 @@ describe('Core Nodes', () => { describe('Block at-rules with nested rules', () => { test('@media with nested rule', () => { const source = '@media (min-width: 768px) { body { color: red; } }' - const parser = new Parser(source, { parse_atrule_preludes: false }) - const root = parser.parse() + const root = parse(source, { parse_atrule_preludes: false }) + const media = root.first_child! expect(media.type).toBe(AT_RULE) @@ -806,8 +799,8 @@ describe('Core Nodes', () => { test('@layer with name', () => { const source = '@layer utilities { .text-center { text-align: center; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const layer = root.first_child! expect(layer.type).toBe(AT_RULE) @@ -817,8 +810,8 @@ describe('Core Nodes', () => { test('anonymous @layer', () => { const source = '@layer { body { margin: 0; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const layer = root.first_child! expect(layer.type).toBe(AT_RULE) @@ -828,8 +821,8 @@ describe('Core Nodes', () => { test('@supports', () => { const source = '@supports (display: grid) { .grid { display: grid; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const supports = root.first_child! expect(supports.type).toBe(AT_RULE) @@ -839,8 +832,8 @@ describe('Core Nodes', () => { test('@container', () => { const source = '@container (min-width: 400px) { .card { padding: 2rem; } }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const container = root.first_child! expect(container.type).toBe(AT_RULE) @@ -852,8 +845,8 @@ describe('Core Nodes', () => { describe('Descriptor at-rules (with declarations)', () => { test('@font-face', () => { const source = '@font-face { font-family: "Open Sans"; src: url(font.woff2); }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const fontFace = root.first_child! expect(fontFace.type).toBe(AT_RULE) @@ -868,8 +861,8 @@ describe('Core Nodes', () => { test('@page', () => { const source = '@page { margin: 1in; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const page = root.first_child! expect(page.type).toBe(AT_RULE) @@ -882,8 +875,8 @@ describe('Core Nodes', () => { test('@counter-style', () => { const source = '@counter-style thumbs { system: cyclic; symbols: "👍"; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const counterStyle = root.first_child! expect(counterStyle.type).toBe(AT_RULE) @@ -898,8 +891,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 parser = new Parser(source, { parse_atrule_preludes: false }) - const root = parser.parse() + const root = parse(source, { parse_atrule_preludes: false }) + const supports = root.first_child! expect(supports.name).toBe('supports') @@ -922,8 +915,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 parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const [import1, layer, media] = root.children expect(import1.name).toBe('import') @@ -938,8 +931,8 @@ describe('Core Nodes', () => { describe('Special at-rules', () => { test('@charset', () => { let source = '@charset "UTF-8"; body { color: red; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let [charset, _body] = root.children expect(charset.type).toBe(AT_RULE) @@ -948,8 +941,8 @@ describe('Core Nodes', () => { test('@import with media query', () => { let source = '@import url("print.css") print;' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let import_rule = root.first_child! expect(import_rule.type).toBe(AT_RULE) @@ -967,8 +960,8 @@ describe('Core Nodes', () => { font-display: swap; } ` - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let font_face = root.first_child! expect(font_face.name).toBe('font-face') @@ -978,8 +971,8 @@ describe('Core Nodes', () => { test('@counter-style', () => { let source = '@counter-style custom { system: cyclic; symbols: "⚫" "⚪"; suffix: " "; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let counter = root.first_child! expect(counter.name).toBe('counter-style') @@ -989,8 +982,8 @@ describe('Core Nodes', () => { test('@property', () => { let source = '@property --my-color { syntax: ""; inherits: false; initial-value: #c0ffee; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let property = root.first_child! expect(property.name).toBe('property') @@ -1000,8 +993,8 @@ describe('Core Nodes', () => { describe('At-rule preludes', () => { test('media query prelude', () => { let source = '@media (min-width: 768px) { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.type).toBe(AT_RULE) @@ -1011,8 +1004,8 @@ describe('Core Nodes', () => { test('complex media query prelude', () => { let source = '@media screen and (min-width: 768px) and (max-width: 1024px) { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('media') @@ -1021,8 +1014,8 @@ describe('Core Nodes', () => { test('container query prelude', () => { let source = '@container (width >= 200px) { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('container') @@ -1031,8 +1024,8 @@ describe('Core Nodes', () => { test('supports query prelude', () => { let source = '@supports (display: grid) { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('supports') @@ -1041,8 +1034,8 @@ describe('Core Nodes', () => { test('import prelude', () => { let source = '@import url("styles.css");' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('import') @@ -1051,8 +1044,8 @@ describe('Core Nodes', () => { test('at-rule without prelude', () => { let source = '@font-face { font-family: MyFont; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('font-face') @@ -1061,8 +1054,8 @@ describe('Core Nodes', () => { test('layer prelude', () => { let source = '@layer utilities { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('layer') @@ -1071,8 +1064,8 @@ describe('Core Nodes', () => { test('keyframes prelude', () => { let source = '@keyframes slide-in { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('keyframes') @@ -1081,8 +1074,8 @@ describe('Core Nodes', () => { test('prelude with extra whitespace', () => { let source = '@media (min-width: 768px) { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('media') @@ -1091,8 +1084,8 @@ describe('Core Nodes', () => { test('charset prelude', () => { let source = '@charset "UTF-8";' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('charset') @@ -1101,8 +1094,8 @@ describe('Core Nodes', () => { test('namespace prelude', () => { let source = '@namespace svg url(http://www.w3.org/2000/svg);' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.name).toBe('namespace') @@ -1111,8 +1104,8 @@ describe('Core Nodes', () => { test('value and prelude should be aliases for at-rules', () => { let source = '@media (min-width: 768px) { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let atrule = root.first_child! expect(atrule.value).toBe(atrule.prelude) @@ -1121,8 +1114,8 @@ describe('Core Nodes', () => { test('at-rule prelude line tracking', () => { let source = 'body { color: red; }\n\n@media screen { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let [_rule1, atRule] = root.children expect(atRule.line).toBe(3) @@ -1173,8 +1166,8 @@ describe('Core Nodes', () => { describe('Basic declaration properties', () => { test('should parse property name', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const [_selector, block] = rule.children @@ -1185,8 +1178,8 @@ describe('Core Nodes', () => { test('simple declaration without !important', () => { const source = 'body { color: red; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const [_selector, block] = rule.children @@ -1198,8 +1191,8 @@ describe('Core Nodes', () => { test('declaration with !important', () => { const source = 'body { color: red !important; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const [_selector, block] = rule.children @@ -1211,8 +1204,8 @@ describe('Core Nodes', () => { test('declaration with !ie (historic !important)', () => { const source = 'body { color: red !ie; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const [_selector, block] = rule.children @@ -1224,8 +1217,8 @@ describe('Core Nodes', () => { test('declaration with ! followed by any identifier', () => { const source = 'body { color: red !foo; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const [_selector, block] = rule.children @@ -1237,8 +1230,8 @@ describe('Core Nodes', () => { test('declaration without semicolon at end of block', () => { const source = 'body { color: red }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const [_selector, block] = rule.children @@ -1249,8 +1242,8 @@ describe('Core Nodes', () => { test('complex declaration value', () => { const source = 'body { background: url(image.png) no-repeat center; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const [_selector, block] = rule.children @@ -1264,8 +1257,8 @@ describe('Core Nodes', () => { describe('Multiple declarations', () => { test('should parse multiple declarations', () => { const source = 'body { color: red; margin: 0; }' - const parser = new Parser(source) - const root = parser.parse() + const root = parse(source) + const rule = root.first_child! const [_selector, block] = rule.children @@ -1280,8 +1273,8 @@ describe('Core Nodes', () => { describe('Declaration values', () => { test('extract simple value', () => { let source = 'a { color: blue; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1293,8 +1286,8 @@ describe('Core Nodes', () => { test('extract value with spaces', () => { let source = 'a { padding: 1rem 2rem 3rem 4rem; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1306,8 +1299,8 @@ describe('Core Nodes', () => { test('extract function value', () => { let source = 'a { background: linear-gradient(to bottom, red, blue); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1319,8 +1312,8 @@ describe('Core Nodes', () => { test('extract calc value', () => { let source = 'a { width: calc(100% - 2rem); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1332,8 +1325,8 @@ describe('Core Nodes', () => { test('exclude !important from value', () => { let source = 'a { color: blue !important; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1346,8 +1339,8 @@ describe('Core Nodes', () => { test('value with extra whitespace', () => { let source = 'a { color: blue ; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1359,8 +1352,8 @@ describe('Core Nodes', () => { test('CSS custom property value', () => { let source = ':root { --brand-color: rgb(0% 10% 50% / 0.5); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1372,8 +1365,8 @@ describe('Core Nodes', () => { test('var() reference value', () => { let source = 'a { color: var(--primary-color); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1385,8 +1378,8 @@ describe('Core Nodes', () => { test('nested function value', () => { let source = 'a { transform: translate(calc(50% - 1rem), 0); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1398,8 +1391,8 @@ describe('Core Nodes', () => { test('value without semicolon', () => { let source = 'a { color: blue }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1411,8 +1404,8 @@ describe('Core Nodes', () => { test('empty value', () => { let source = 'a { color: ; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1424,8 +1417,8 @@ describe('Core Nodes', () => { test('URL value', () => { let source = 'a { background: url("image.png"); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1439,8 +1432,8 @@ describe('Core Nodes', () => { describe('Vendor prefix detection', () => { test('-webkit- vendor prefix', () => { let source = '.box { -webkit-transform: scale(1); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1451,8 +1444,8 @@ describe('Core Nodes', () => { test('-moz- vendor prefix', () => { let source = '.box { -moz-transform: scale(1); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1463,8 +1456,8 @@ describe('Core Nodes', () => { test('-ms- vendor prefix', () => { let source = '.box { -ms-transform: scale(1); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1475,8 +1468,8 @@ describe('Core Nodes', () => { test('-o- vendor prefix', () => { let source = '.box { -o-transform: scale(1); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1487,8 +1480,8 @@ describe('Core Nodes', () => { test('no vendor prefix for standard properties', () => { let source = '.box { transform: scale(1); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1499,8 +1492,8 @@ describe('Core Nodes', () => { test('no vendor prefix for properties with hyphens', () => { let source = '.box { background-color: red; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1511,8 +1504,8 @@ describe('Core Nodes', () => { test('no vendor prefix for custom properties', () => { let source = ':root { --primary-color: blue; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1523,8 +1516,8 @@ describe('Core Nodes', () => { test('multiple vendor-prefixed properties', () => { let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1542,8 +1535,8 @@ describe('Core Nodes', () => { test('complex property names with vendor prefix', () => { let source = '.box { -webkit-border-top-left-radius: 5px; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1554,8 +1547,8 @@ describe('Core Nodes', () => { test('no vendor prefix for similar but non-vendor properties', () => { let source = '.box { border-radius: 5px; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -1566,8 +1559,8 @@ describe('Core Nodes', () => { test('false for nodes without names', () => { let source = 'body { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selector = rule.first_child! @@ -1578,8 +1571,8 @@ describe('Core Nodes', () => { describe('Vendor prefix detection for selectors', () => { test('-webkit- vendor prefix in pseudo-class', () => { let source = 'input:-webkit-autofill { color: black; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1592,8 +1585,8 @@ describe('Core Nodes', () => { test('-moz- vendor prefix in pseudo-class', () => { let source = 'button:-moz-focusring { outline: 2px solid blue; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1606,8 +1599,8 @@ describe('Core Nodes', () => { test('-ms- vendor prefix in pseudo-class', () => { let source = 'input:-ms-input-placeholder { color: gray; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1620,8 +1613,8 @@ describe('Core Nodes', () => { test('-webkit- vendor prefix in pseudo-element', () => { let source = 'div::-webkit-scrollbar { width: 10px; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1634,8 +1627,8 @@ describe('Core Nodes', () => { test('-moz- vendor prefix in pseudo-element', () => { let source = 'div::-moz-selection { background: yellow; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1648,8 +1641,8 @@ describe('Core Nodes', () => { test('-webkit- vendor prefix in pseudo-element with multiple parts', () => { let source = 'input::-webkit-input-placeholder { color: gray; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1662,8 +1655,8 @@ describe('Core Nodes', () => { test('-webkit- vendor prefix in pseudo-class function', () => { let source = 'input:-webkit-any(input, button) { margin: 0; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1676,8 +1669,8 @@ describe('Core Nodes', () => { test('no vendor prefix for standard pseudo-classes', () => { let source = 'a:hover { color: blue; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1690,8 +1683,8 @@ describe('Core Nodes', () => { test('no vendor prefix for standard pseudo-elements', () => { let source = 'div::before { content: ""; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1704,8 +1697,8 @@ describe('Core Nodes', () => { test('multiple vendor-prefixed pseudo-elements', () => { let source = 'div::-webkit-scrollbar { } div::-webkit-scrollbar-thumb { } div::after { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let [rule1, rule2, rule3] = root.children @@ -1733,8 +1726,8 @@ describe('Core Nodes', () => { test('vendor prefix in complex selector', () => { let source = 'input:-webkit-autofill:focus { color: black; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selectorList = rule.first_child! @@ -1753,8 +1746,8 @@ describe('Core Nodes', () => { describe('BLOCK', () => { test('block text excludes braces for empty at-rule block', () => { - const parser = new Parser('@layer test {}') - const root = parser.parse() + const root = parse('@layer test {}') + const atRule = root.first_child! expect(atRule.has_block).toBe(true) @@ -1763,8 +1756,8 @@ describe('Core Nodes', () => { }) test('at-rule block with content excludes braces', () => { - const parser = new Parser('@layer test { .foo { color: red; } }') - const root = parser.parse() + const root = parse('@layer test { .foo { color: red; } }') + const atRule = root.first_child! expect(atRule.has_block).toBe(true) @@ -1773,8 +1766,8 @@ describe('Core Nodes', () => { }) test('empty style rule block has empty text', () => { - const parser = new Parser('body {}') - const root = parser.parse() + const root = parse('body {}') + const styleRule = root.first_child! expect(styleRule.has_block).toBe(true) @@ -1783,8 +1776,8 @@ describe('Core Nodes', () => { }) test('style rule block with declaration excludes braces', () => { - const parser = new Parser('body { color: red; }') - const root = parser.parse() + const root = parse('body { color: red; }') + const styleRule = root.first_child! expect(styleRule.has_block).toBe(true) @@ -1793,8 +1786,8 @@ describe('Core Nodes', () => { }) test('nested style rule blocks exclude braces', () => { - const parser = new Parser('.parent { .child { margin: 0; } }') - const root = parser.parse() + const root = parse('.parent { .child { margin: 0; } }') + const parent = root.first_child! const parentBlock = parent.block! const child = parentBlock.first_child! @@ -1805,32 +1798,32 @@ describe('Core Nodes', () => { }) test('at-rule with multiple declarations excludes braces', () => { - const parser = new Parser('@font-face { font-family: "Test"; src: url(test.woff); }') - const root = parser.parse() + 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); ') }) test('media query with nested rules excludes braces', () => { - const parser = new Parser('@media screen { body { color: blue; } }') - const root = parser.parse() + const root = parse('@media screen { body { color: blue; } }') + const mediaRule = root.first_child! expect(mediaRule.block!.text).toBe(' body { color: blue; } ') }) test('block with no whitespace is empty', () => { - const parser = new Parser('div{}') - const root = parser.parse() + const root = parse('div{}') + const styleRule = root.first_child! expect(styleRule.block!.text).toBe('') }) test('block with only whitespace preserves whitespace', () => { - const parser = new Parser('div{ \n\t }') - const root = parser.parse() + const root = parse('div{ \n\t }') + const styleRule = root.first_child! expect(styleRule.block!.text).toBe(' \n\t ') @@ -1840,8 +1833,8 @@ describe('Core Nodes', () => { describe('CSS Nesting', () => { test('nested rule with & selector', () => { let source = '.parent { color: red; & .child { color: blue; } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! expect(parent.type).toBe(STYLE_RULE) @@ -1858,8 +1851,8 @@ describe('Core Nodes', () => { test('nested rule without & selector', () => { let source = '.parent { color: red; .child { color: blue; } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! let [_selector, block] = parent.children @@ -1872,8 +1865,8 @@ describe('Core Nodes', () => { test('multiple nested rules', () => { let source = '.parent { .child1 { } .child2 { } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! let [_selector, block] = parent.children @@ -1885,8 +1878,8 @@ describe('Core Nodes', () => { test('deeply nested rules', () => { let source = '.a { .b { .c { color: red; } } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let a = root.first_child! expect(a.length).toBe(32) @@ -1908,8 +1901,8 @@ describe('Core Nodes', () => { test('nested @media inside rule', () => { let source = '.card { color: red; @media (min-width: 768px) { padding: 2rem; } }' - let parser = new Parser(source, { parse_atrule_preludes: false }) - let root = parser.parse() + let root = parse(source, { parse_atrule_preludes: false }) + let card = root.first_child! let [_selector, block] = card.children @@ -1927,8 +1920,8 @@ describe('Core Nodes', () => { test(':is() pseudo-class', () => { let source = ':is(.a, .b) { color: red; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selector = rule.first_child! @@ -1937,8 +1930,8 @@ describe('Core Nodes', () => { test(':where() pseudo-class', () => { let source = ':where(h1, h2, h3) { margin: 0; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selector = rule.first_child! @@ -1947,8 +1940,8 @@ describe('Core Nodes', () => { test(':has() pseudo-class', () => { let source = 'div:has(> img) { display: flex; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selector = rule.first_child! @@ -1962,8 +1955,8 @@ describe('Core Nodes', () => { padding: 1rem; .body { line-height: 1.5; } }` - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let card = root.first_child! let [_selector, block] = card.children @@ -1983,8 +1976,8 @@ 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 parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! expect(parent.type).toBe(STYLE_RULE) @@ -2000,8 +1993,8 @@ describe('Core Nodes', () => { test('nested rule with leading next-sibling combinator', () => { let source = '.parent { + span { color: blue; } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! let [_selector, block] = parent.children @@ -2015,8 +2008,8 @@ describe('Core Nodes', () => { test('nested rule with leading subsequent-sibling combinator', () => { let source = '.parent { ~ div { color: green; } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! let [_selector, block] = parent.children @@ -2030,8 +2023,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 parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! let [_selector, block] = parent.children @@ -2052,8 +2045,8 @@ describe('Core Nodes', () => { test('complex selector after leading combinator', () => { let source = '.parent { > a.link#nav[href]:hover { color: red; } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! let [_selector, block] = parent.children @@ -2066,8 +2059,8 @@ describe('Core Nodes', () => { test('deeply nested rules with leading combinators', () => { let source = '.a { > .b { > .c { color: red; } } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let a = root.first_child! let [_selector_a, block_a] = a.children @@ -2085,8 +2078,8 @@ describe('Core Nodes', () => { test('mixed nested rules with and without leading combinators', () => { let source = '.parent { .normal { } > .combinator { } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! let [_selector, block] = parent.children @@ -2105,8 +2098,8 @@ describe('Core Nodes', () => { describe('@keyframes parsing', () => { test('@keyframes with from/to', () => { let source = '@keyframes fade { from { opacity: 0; } to { opacity: 1; } }' - let parser = new Parser(source, { parse_atrule_preludes: false }) - let root = parser.parse() + let root = parse(source, { parse_atrule_preludes: false }) + let keyframes = root.first_child! expect(keyframes.type).toBe(AT_RULE) @@ -2126,8 +2119,8 @@ describe('Core Nodes', () => { test('@keyframes with percentages', () => { let source = '@keyframes slide { 0% { left: 0; } 50% { left: 50%; } 100% { left: 100%; } }' - let parser = new Parser(source, { parse_atrule_preludes: false }) - let root = parser.parse() + let root = parse(source, { parse_atrule_preludes: false }) + let keyframes = root.first_child! let block = keyframes.block! @@ -2143,8 +2136,8 @@ describe('Core Nodes', () => { test('@keyframes with multiple selectors', () => { let source = '@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }' - let parser = new Parser(source, { parse_atrule_preludes: false }) - let root = parser.parse() + let root = parse(source, { parse_atrule_preludes: false }) + let keyframes = root.first_child! let block = keyframes.block! @@ -2156,8 +2149,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 parser = new Parser(source, { parse_atrule_preludes: false }) - let root = parser.parse() + let root = parse(source, { parse_atrule_preludes: false }) + let keyframes = root.first_child! let block = keyframes.block! @@ -2168,8 +2161,8 @@ describe('Core Nodes', () => { describe('@nest at-rule', () => { test('@nest with & selector', () => { let source = '.parent { @nest & .child { color: blue; } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let parent = root.first_child! let [_selector, block] = parent.children @@ -2187,8 +2180,8 @@ describe('Core Nodes', () => { test('@nest with complex selector', () => { let source = '.a { @nest :not(&) { color: red; } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let a = root.first_child! let [_selector, block] = a.children @@ -2202,24 +2195,24 @@ describe('Core Nodes', () => { describe('Error recovery and edge cases', () => { test('malformed rule without opening brace', () => { let source = 'body color: red; } div { margin: 0; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + expect(root.children.length).toBeGreaterThan(0) }) test('rule without closing brace', () => { let source = 'body { color: red; div { margin: 0; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + expect(root.has_children).toBe(true) }) test('empty rule block', () => { let source = '.empty { }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2228,8 +2221,8 @@ describe('Core Nodes', () => { test('declaration without value', () => { let source = 'body { color: }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -2239,8 +2232,8 @@ describe('Core Nodes', () => { test('multiple semicolons', () => { let source = 'body { color: red;;; margin: 0;; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.children.length).toBe(2) @@ -2248,8 +2241,8 @@ describe('Core Nodes', () => { test('invalid tokens in declaration block', () => { let source = 'body { color: red; @@@; margin: 0; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.children.length).toBe(2) @@ -2257,8 +2250,8 @@ describe('Core Nodes', () => { test('declaration without colon', () => { let source = 'body { color red; margin: 0; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.children.length).toBe(2) @@ -2266,16 +2259,16 @@ describe('Core Nodes', () => { test('at-rule without name', () => { let source = '@ { color: red; } body { margin: 0; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + expect(root.children.length).toBeGreaterThan(0) }) test('nested empty blocks', () => { let source = '.a { .b { .c { } } }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let a = root.first_child! expect(a.type).toBe(STYLE_RULE) @@ -2283,8 +2276,8 @@ describe('Core Nodes', () => { test('trailing comma in selector', () => { let source = '.a, .b, { color: red; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2294,8 +2287,8 @@ describe('Core Nodes', () => { describe('Comment handling', () => { test('skip comments at top level', () => { let source = '/* comment */ body { color: red; } /* another comment */' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + expect(root.children.length).toBe(1) let rule = root.first_child! @@ -2304,8 +2297,8 @@ describe('Core Nodes', () => { test('skip comments in declaration block', () => { let source = 'body { color: red; /* comment */ margin: 0; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2314,8 +2307,8 @@ describe('Core Nodes', () => { test('skip comments in selector', () => { let source = 'body /* comment */ , /* comment */ div { color: red; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2323,8 +2316,8 @@ describe('Core Nodes', () => { test('comment between property and colon', () => { let source = 'body { color /* comment */ : red; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + expect(root.has_children).toBe(true) }) @@ -2337,8 +2330,8 @@ describe('Core Nodes', () => { */ body { color: red; } ` - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + expect(root.children.length).toBe(1) }) @@ -2347,8 +2340,8 @@ describe('Core Nodes', () => { describe('Whitespace handling', () => { test('excessive whitespace', () => { let source = ' body { color : red ; } ' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2356,8 +2349,8 @@ describe('Core Nodes', () => { test('tabs and newlines', () => { let source = 'body\t{\n\tcolor:\tred;\n}\n' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! expect(rule.type).toBe(STYLE_RULE) @@ -2365,8 +2358,8 @@ describe('Core Nodes', () => { test('no whitespace', () => { let source = 'body{color:red;margin:0}' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -2405,8 +2398,8 @@ describe('Core Nodes', () => { } } ` - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let card = root.first_child! expect(card.type).toBe(STYLE_RULE) @@ -2429,8 +2422,8 @@ describe('Core Nodes', () => { } } ` - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let [layer1, layer2] = root.children expect(layer1.type).toBe(AT_RULE) @@ -2439,8 +2432,8 @@ describe('Core Nodes', () => { test('vendor prefixed properties', () => { let source = '.box { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -2452,8 +2445,8 @@ describe('Core Nodes', () => { test('complex selector list', () => { let source = 'h1, h2, h3, h4, h5, h6, .heading, [role="heading"] { font-family: sans-serif; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selector = rule.first_child! @@ -2471,8 +2464,8 @@ describe('Core Nodes', () => { } } ` - let parser = new Parser(source, { parse_atrule_preludes: false }) - let root = parser.parse() + let root = parse(source, { parse_atrule_preludes: false }) + let supports = root.first_child! let supports_block = supports.block! @@ -2486,8 +2479,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 parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let [_selector, block] = rule.children @@ -2498,8 +2491,8 @@ describe('Core Nodes', () => { test('custom properties', () => { let source = ':root { --primary-color: #007bff; --spacing: 1rem; } body { color: var(--primary-color); }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + expect(root.children.length).toBeGreaterThan(0) let first_rule = root.first_child! @@ -2508,8 +2501,8 @@ describe('Core Nodes', () => { test('attribute selectors with operators', () => { let source = '[href^="https"][href$=".pdf"][class*="doc"] { color: red; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let selector = rule.first_child! @@ -2520,8 +2513,8 @@ describe('Core Nodes', () => { test('pseudo-elements', () => { let source = '.text::before { content: "→"; } .text::after { content: "←"; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let [rule1, rule2] = root.children expect(rule1.type).toBe(STYLE_RULE) @@ -2530,8 +2523,8 @@ describe('Core Nodes', () => { test('multiple !important declarations', () => { let source = '.override { color: red !important; margin: 0 !important; padding: 0 !ie; }' - let parser = new Parser(source) - let root = parser.parse() + let root = parse(source) + let rule = root.first_child! let block = rule.block! diff --git a/src/parse.ts b/src/parse.ts index 35c38b1..f68966d 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -41,6 +41,7 @@ export interface ParserOptions { let DECLARATION_AT_RULES = new Set(['font-face', 'font-feature-values', 'page', 'property', 'counter-style']) let CONDITIONAL_AT_RULES = new Set(['media', 'supports', 'container', 'layer', 'nest']) +/** @internal */ export class Parser { private source: string private lexer: Lexer diff --git a/src/string-utils.ts b/src/string-utils.ts index e156cc4..4d85ac1 100644 --- a/src/string-utils.ts +++ b/src/string-utils.ts @@ -24,15 +24,18 @@ export const CHAR_COLON = 0x3a // : /** * Check if a character code is whitespace (space, tab, newline, CR, or FF) + * @internal */ export function is_whitespace(ch: number): boolean { return ch === CHAR_SPACE || ch === CHAR_TAB || ch === CHAR_NEWLINE || ch === CHAR_CARRIAGE_RETURN || ch === CHAR_FORM_FEED } +/** @internal */ export function is_combinator(ch: number): boolean { return ch === CHAR_GREATER_THAN || ch === CHAR_PLUS || ch === CHAR_TILDE } +/** @internal */ export function is_digit(ch: number): boolean { return ch >= 0x30 && ch <= 0x39 // 0-9 } diff --git a/src/walk.test.ts b/src/walk.test.ts index 6a4ef87..f069294 100644 --- a/src/walk.test.ts +++ b/src/walk.test.ts @@ -1,12 +1,11 @@ import { describe, it, expect } from 'vitest' -import { Parser } from './parse' +import { parse } from './parse' import { STYLESHEET, STYLE_RULE, SELECTOR_LIST, DECLARATION, AT_RULE, BLOCK, IDENTIFIER, NUMBER, DIMENSION } from './constants' import { walk, traverse } from './walk' describe('walk', () => { it('should visit single node', () => { - const parser = new Parser('', { parse_selectors: false, parse_values: false }) - const root = parser.parse() + const root = parse('', { parse_selectors: false, parse_values: false }) const visited: number[] = [] walk(root, (node) => { @@ -17,8 +16,7 @@ describe('walk', () => { }) it('should visit all nodes in simple rule', () => { - const parser = new Parser('body { color: red; }', { parse_selectors: false, parse_values: true }) - const root = parser.parse() + const root = parse('body { color: red; }', { parse_selectors: false, parse_values: true }) const visited: number[] = [] walk(root, (node) => { @@ -36,8 +34,7 @@ describe('walk', () => { }) it('should visit nodes in depth-first order', () => { - const parser = new Parser('body { color: red; margin: 0; } div { padding: 1rem; }', { parse_selectors: false, parse_values: true }) - const root = parser.parse() + const root = parse('body { color: red; margin: 0; } div { padding: 1rem; }', { parse_selectors: false, parse_values: true }) const visited: number[] = [] walk(root, (node) => { @@ -62,8 +59,7 @@ describe('walk', () => { }) it('should visit nested rules', () => { - const parser = new Parser('.parent { color: red; .child { color: blue; } }', { parse_selectors: false, parse_values: false }) - const root = parser.parse() + const root = parse('.parent { color: red; .child { color: blue; } }', { parse_selectors: false, parse_values: false }) const visited: number[] = [] walk(root, (node) => { @@ -84,12 +80,11 @@ describe('walk', () => { }) it('should visit at-rule nodes', () => { - const parser = new Parser('@media (min-width: 768px) { body { color: red; } }', { + const root = parse('@media (min-width: 768px) { body { color: red; } }', { parse_selectors: false, parse_values: false, parse_atrule_preludes: false, }) - const root = parser.parse() const visited: number[] = [] walk(root, (node) => { @@ -108,8 +103,7 @@ describe('walk', () => { }) it('should allow collecting node data', () => { - const parser = new Parser('body { color: red; } .btn { margin: 0; }', { parse_selectors: false, parse_values: false }) - const root = parser.parse() + const root = parse('body { color: red; } .btn { margin: 0; }', { parse_selectors: false, parse_values: false }) const selectors: string[] = [] walk(root, (node) => { @@ -122,8 +116,7 @@ describe('walk', () => { }) it('should allow collecting property names', () => { - const parser = new Parser('body { color: red; margin: 0; padding: 1rem; }', { parse_selectors: false, parse_values: false }) - const root = parser.parse() + const root = parse('body { color: red; margin: 0; padding: 1rem; }', { parse_selectors: false, parse_values: false }) const properties: string[] = [] walk(root, (node) => { @@ -137,14 +130,13 @@ describe('walk', () => { }) it('should allow counting nodes by type', () => { - const parser = new Parser( + const root = parse( ` body { color: red; } .card { padding: 1rem; } @media screen { div { margin: 0; } } `, ) - const root = parser.parse() const counts: Record = {} walk(root, (node) => { @@ -159,8 +151,7 @@ describe('walk', () => { }) it('should work with deeply nested structures', () => { - const parser = new Parser('.a { .b { .c { color: red; } } }') - const root = parser.parse() + const root = parse('.a { .b { .c { color: red; } } }') const rules: number[] = [] walk(root, (node) => { @@ -173,8 +164,7 @@ describe('walk', () => { }) it('should track depth correctly', () => { - const parser = new Parser('body { color: red; }', { parse_selectors: false, parse_values: true }) - const root = parser.parse() + const root = parse('body { color: red; }', { parse_selectors: false, parse_values: true }) const depths: number[] = [] walk(root, (_node, depth) => { @@ -186,8 +176,7 @@ describe('walk', () => { }) it('should track depth in nested structures', () => { - const parser = new Parser('.a { .b { .c { color: red; } } }', { parse_selectors: false, parse_values: true }) - const root = parser.parse() + const root = parse('.a { .b { .c { color: red; } } }', { parse_selectors: false, parse_values: true }) const ruleDepths: number[] = [] walk(root, (node, depth) => { @@ -200,12 +189,11 @@ describe('walk', () => { }) it('should track depth with at-rules', () => { - const parser = new Parser('@media screen { body { color: red; } }', { + const root = parse('@media screen { body { color: red; } }', { parse_selectors: false, parse_values: false, parse_atrule_preludes: false, }) - const root = parser.parse() const typeAndDepth: Array<{ type: number; depth: number }> = [] walk(root, (node, depth) => { @@ -224,12 +212,11 @@ describe('walk', () => { }) it('should track depth with consecutive at-rules', () => { - const parser = new Parser('@media screen { body { color: red; } } @layer { a { color: red; } }', { + const root = parse('@media screen { body { color: red; } } @layer { a { color: red; } }', { parse_selectors: false, parse_values: false, parse_atrule_preludes: false, }) - const root = parser.parse() const typeAndDepth: Array<{ type: number; depth: number }> = [] walk(root, (node, depth) => { @@ -254,7 +241,7 @@ describe('walk', () => { }) test('export types', () => { - let ast = new Parser('a{}').parse() + let ast = parse('a{}') walk(ast, (node, _depth) => { expectTypeOf(node.type).toBeNumber() if (node.type === SELECTOR_LIST) { @@ -265,12 +252,11 @@ describe('walk', () => { }) describe('walk enter/leave', () => { - const parser = new Parser('@media screen { body { color: red; } }', { + const root = parse('@media screen { body { color: red; } }', { parse_selectors: false, parse_values: false, parse_atrule_preludes: false, }) - const root = parser.parse() test('both enter + leave', () => { const enter: number[] = [] diff --git a/tsconfig.build.json b/tsconfig.build.json index b9aa892..9d25229 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,8 +5,15 @@ "emitDeclarationOnly": true, "noEmit": false, "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "stripInternal": true }, "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist", "**/*.test.ts", "benchmark"] + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "benchmark", + "src/char-types.ts" + ] }