Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

117 changes: 51 additions & 66 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,23 @@ export type PlainCSSNode = {
end?: number
}

const nodes_with_name = new Set<number>([
AT_RULE,
IDENTIFIER,
FUNCTION,
TYPE_SELECTOR,
CLASS_SELECTOR,
ID_SELECTOR,
ATTRIBUTE_SELECTOR,
PSEUDO_CLASS_SELECTOR,
PSEUDO_ELEMENT_SELECTOR,
COMBINATOR,
UNIVERSAL_SELECTOR,
LANG_SELECTOR,
LAYER_NAME,
FEATURE_RANGE,
])

const nodes_with_children = new Set<number>([
STYLESHEET,
SELECTOR,
Expand All @@ -227,6 +244,22 @@ const nodes_with_children = new Set<number>([
FEATURE_RANGE,
])

const enumerable_properties = [
'name',
'property',
'value',
'unit',
'attr_operator',
'attr_flags',
'nth_a',
'nth_b',
'selector',
'is_browserhack',
'is_vendor_prefixed',
'has_error',
'is_important',
] as const

export class CSSNode {
private arena: CSSDataArena
private source: string
Expand Down Expand Up @@ -271,11 +304,12 @@ export class CSSNode {
}

/** Get the "content" text (at-rule name for at-rules, layer name for import layers) */
get name(): string | undefined {
get name(): string | null | undefined {
if (!nodes_with_name.has(this.type)) return
let content = this.get_content()
let { type } = this
if (type === DECLARATION || type === OPERATOR || type === SELECTOR || type === MEDIA_FEATURE)
return
return this.get_content()
if ((type === UNIVERSAL_SELECTOR || type === LANG_SELECTOR) && content === '') return null
return content
}

/**
Expand Down Expand Up @@ -495,7 +529,9 @@ export class CSSNode {
}

/** Check if this style rule has declarations */
get has_declarations(): boolean {
get has_declarations(): boolean | undefined {
let { type } = this
if (type !== AT_RULE && type !== STYLE_RULE) return undefined
return this.arena.has_flag(this.index, FLAG_HAS_DECLARATIONS)
}

Expand Down Expand Up @@ -648,23 +684,23 @@ export class CSSNode {
// --- An+B Expression Helpers (for NODE_SELECTOR_NTH) ---

/** Get the 'a' coefficient from An+B expression (e.g., "2n" from "2n+1", "odd" from "odd") */
get nth_a(): string | undefined {
get nth_a(): string | null | undefined {
let { type, arena, index } = this
if (type !== NTH_SELECTOR && type !== NTH_OF_SELECTOR) return undefined

let len = arena.get_content_length(index)
if (len === 0) return undefined
if (len === 0) return null
let start = arena.get_content_start(index)
return this.source.substring(start, start + len)
}

/** Get the 'b' coefficient from An+B expression (e.g., "+1" from "2n+1") */
get nth_b(): string | undefined {
get nth_b(): string | null | undefined {
let { type, arena, index, source } = this
if (type !== NTH_SELECTOR && type !== NTH_OF_SELECTOR) return undefined

let len = arena.get_value_length(index)
if (len === 0) return undefined
if (len === 0) return null
let start = arena.get_value_start(index)
let value = source.substring(start, start + len)

Expand Down Expand Up @@ -717,71 +753,21 @@ export class CSSNode {
*/
clone(options: CloneOptions = {}): PlainCSSNode {
const { deep = true, locations = false } = options
let { type, name, property, value, unit } = this
let { type } = this

// 1. Create plain object with base properties
let plain: any = {
type: type,
type,
type_name: this.type_name,
text: this.text,
}

// 2. Extract type-specific properties (only if meaningful)
if (name) {
plain.name = name
}
if (property) {
plain.property = property
}

// 3. Handle value types
if (value) {
plain.value = value

if (unit) {
plain.unit = unit
}
}

// 4. At-rule preludes are now child nodes (AT_RULE_PRELUDE wrapper)
// They will be cloned as part of children in deep clones
// No special extraction needed - breaking change from string to CSSNode

// 5. Extract flags
if (type === DECLARATION) {
let { is_important, is_browserhack } = this
if (is_important) {
plain.is_important = true
}
if (is_browserhack) {
plain.is_browserhack = true
for (let key of enumerable_properties) {
let val = this[key]
if (val !== undefined && val !== false) {
plain[key] = val
}
}

let { is_vendor_prefixed, has_error } = this

if (is_vendor_prefixed) {
plain.is_vendor_prefixed = true
}

if (has_error) {
plain.has_error = true
}

// 6. Extract selector-specific properties
if (type === ATTRIBUTE_SELECTOR) {
plain.attr_operator = this.attr_operator
plain.attr_flags = this.attr_flags
}
if (type === NTH_SELECTOR || type === NTH_OF_SELECTOR) {
plain.nth_a = this.nth_a
plain.nth_b = this.nth_b
}
if (type === NTH_OF_SELECTOR) {
plain.selector = this.selector
}

// 7. Include location if requested
if (locations) {
plain.line = this.line
plain.column = this.column
Expand All @@ -790,7 +776,6 @@ export class CSSNode {
plain.end = this.end
}

// 8. Deep clone children - just push to array!
if (deep && nodes_with_children.has(type)) {
plain.children = []
plain.child_count = this.child_count
Expand Down
1 change: 0 additions & 1 deletion src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ export type StyleSheet = CSSNode &

export type Rule = CSSNode & {
readonly type: typeof STYLE_RULE
readonly name: string
readonly has_declarations: boolean
clone(options?: CloneOptions): ToPlain<Rule>
} &
Expand Down
31 changes: 17 additions & 14 deletions src/parse-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
AttributeSelector,
ClassSelector,
IdSelector,
LangSelector,
NthSelector,
PseudoClassSelector,
PseudoElementSelector,
Expand Down Expand Up @@ -297,9 +298,10 @@ describe('Selector Nodes', () => {
const node = parse_selector(':lang(en)')
const selector = node.first_child! as Selector
const pseudoClass = selector.first_child!
const langNode = pseudoClass.first_child!
const langNode = pseudoClass.first_child! as LangSelector
expect(langNode.type).toBe(LANG_SELECTOR)
expect(langNode.length).toBeGreaterThan(0)
expect(langNode.name).toBe('en')
})
})
})
Expand Down Expand Up @@ -468,8 +470,9 @@ describe('Selector Nodes', () => {
test('UNIVERSAL_SELECTOR type_name', () => {
const node = parse_selector('*')
const selector = node.first_child! as Selector
const universalSelector = selector.first_child!
const universalSelector = selector.first_child! as UniversalSelector
expect(universalSelector.type_name).toBe('UniversalSelector')
expect(universalSelector.name).toBe('*')
})

test('NESTING_SELECTOR type_name', () => {
Expand Down Expand Up @@ -1742,23 +1745,23 @@ describe('Selector Nodes', () => {
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.type).toBe(NTH_SELECTOR)
expect(nthNode.nth_a).toBeUndefined()
expect(nthNode.nth_a).toBeNull()
expect(nthNode.nth_b).toBe('3')
})

test(':nth-child(-5)', () => {
const root = parse_selector(':nth-child(-5)')
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBeUndefined()
expect(nthNode.nth_a).toBeNull()
expect(nthNode.nth_b).toBe('-5')
})

test(':nth-child(0)', () => {
const root = parse_selector(':nth-child(0)')
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBeUndefined()
expect(nthNode.nth_a).toBeNull()
expect(nthNode.nth_b).toBe('0')
})
})
Expand All @@ -1769,15 +1772,15 @@ describe('Selector Nodes', () => {
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBe('odd')
expect(nthNode.nth_b).toBeUndefined()
expect(nthNode.nth_b).toBeNull()
})

test('even keyword', () => {
const root = parse_selector(':nth-child(even)')
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBe('even')
expect(nthNode.nth_b).toBeUndefined()
expect(nthNode.nth_b).toBeNull()
})
})

Expand All @@ -1787,23 +1790,23 @@ describe('Selector Nodes', () => {
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBe('n')
expect(nthNode.nth_b).toBeUndefined()
expect(nthNode.nth_b).toBeNull()
})

test('+n', () => {
const root = parse_selector(':nth-child(+n)')
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBe('+n')
expect(nthNode.nth_b).toBeUndefined()
expect(nthNode.nth_b).toBeNull()
})

test('-n', () => {
const root = parse_selector(':nth-child(-n)')
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBe('-n')
expect(nthNode.nth_b).toBeUndefined()
expect(nthNode.nth_b).toBeNull()
})
})

Expand All @@ -1813,23 +1816,23 @@ describe('Selector Nodes', () => {
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBe('2n')
expect(nthNode.nth_b).toBeUndefined()
expect(nthNode.nth_b).toBeNull()
})

test('-3n', () => {
const root = parse_selector(':nth-child(-3n)')
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBe('-3n')
expect(nthNode.nth_b).toBeUndefined()
expect(nthNode.nth_b).toBeNull()
})

test('+5n', () => {
const root = parse_selector(':nth-child(+5n)')
const pseudoClass = root.first_child!.first_child!
const nthNode = pseudoClass.first_child! as NthSelector
expect(nthNode.nth_a).toBe('+5n')
expect(nthNode.nth_b).toBeUndefined()
expect(nthNode.nth_b).toBeNull()
})
})

Expand Down Expand Up @@ -1916,7 +1919,7 @@ describe('Selector Nodes', () => {
const anplusb = nthOfNode.first_child! as NthSelector
expect(anplusb.type).toBe(NTH_SELECTOR)
expect(anplusb.nth_a).toBe('2n')
expect(anplusb.nth_b).toBeUndefined()
expect(anplusb.nth_b).toBeNull()

// Second child is the selector list
const selectorList = (nthOfNode as unknown as WithChildren).children[1]
Expand Down
Loading