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
4 changes: 3 additions & 1 deletion src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,14 @@ export type Function = WithClone<
// `supports()` hold SupportsDeclaration/SupportsQuery/PreludeOperator children (matching
// `@supports`'s own shape, including the full compound and/or/not grammar for
// `supports()`) and `media()` holds a MediaFeature or FeatureRange child (matching
// `@media`'s own shape, including range comparison syntax) — see parse_if_condition_function
// `@media`'s own shape, including range comparison syntax) — see parse_if_condition_function.
// `@import`'s `layer()` holds a LayerName child — see parse_import_layer
WithChildren<
| ValueLike
| MediaFeature
| SupportsDeclaration
| SupportsQuery
| LayerName
| FeatureRange
| PreludeOperator
> & {
Expand Down
102 changes: 77 additions & 25 deletions src/parse-atrule-prelude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1847,6 +1847,7 @@ describe('At-Rule Prelude Nodes', () => {
expect(children[1].type).toBe(LAYER_NAME)
expect(children[1].text).toBe('layer')
expect((children[1] as LayerName).name).toBe('')
expect((children[1] as LayerName).value).toBe('')
})

test('should parse with anonymous LAYER', () => {
Expand All @@ -1860,6 +1861,7 @@ describe('At-Rule Prelude Nodes', () => {
expect(children[1].type).toBe(LAYER_NAME)
expect(children[1].text).toBe('LAYER')
expect((children[1] as LayerName).name).toBe('')
expect((children[1] as LayerName).value).toBe('')
})

test('should parse with named layer', () => {
Expand All @@ -1870,9 +1872,14 @@ describe('At-Rule Prelude Nodes', () => {

expect(children.length).toBe(2)
expect(children[0].type).toBe(URL)
expect(children[1].type).toBe(LAYER_NAME)
expect(children[1].type).toBe(FUNCTION)
expect(children[1].text).toBe('layer(utilities)')
expect((children[1] as LayerName).name).toBe('utilities')
expect((children[1] as Function).name).toBe('layer')
expect((children[1] as Function).value).toBe('utilities')
const layer = (children[1] as Function).first_child as LayerName
expect(layer.type).toBe(LAYER_NAME)
expect(layer.text).toBe('utilities')
expect(layer.name).toBe('utilities')
})

test('should trim whitespace from layer names', () => {
Expand All @@ -1881,8 +1888,9 @@ describe('At-Rule Prelude Nodes', () => {
const atRule = ast.first_child! as Atrule
const children = (atRule.prelude as AtrulePrelude | null)?.children || []

expect(children[1].type).toBe(LAYER_NAME)
expect((children[1] as LayerName).name).toBe('utilities')
expect(children[1].type).toBe(FUNCTION)
expect(((children[1] as Function).first_child as LayerName).type).toBe(LAYER_NAME)
expect(((children[1] as Function).first_child as LayerName).name).toBe('utilities')
})

test('should trim comments from layer names', () => {
Expand All @@ -1891,8 +1899,9 @@ describe('At-Rule Prelude Nodes', () => {
const atRule = ast.first_child! as Atrule
const children = (atRule.prelude as AtrulePrelude | null)?.children || []

expect(children[1].type).toBe(LAYER_NAME)
expect((children[1] as LayerName).name).toBe('utilities')
expect(children[1].type).toBe(FUNCTION)
expect(((children[1] as Function).first_child as LayerName).type).toBe(LAYER_NAME)
expect(((children[1] as Function).first_child as LayerName).name).toBe('utilities')
})

test('should trim whitespace and comments from dotted layer names', () => {
Expand All @@ -1901,8 +1910,9 @@ describe('At-Rule Prelude Nodes', () => {
const atRule = ast.first_child! as Atrule
const children = (atRule.prelude as AtrulePrelude | null)?.children || []

expect(children[1].type).toBe(LAYER_NAME)
expect((children[1] as LayerName).name).toBe('named.nested')
expect(children[1].type).toBe(FUNCTION)
expect(((children[1] as Function).first_child as LayerName).type).toBe(LAYER_NAME)
expect(((children[1] as Function).first_child as LayerName).name).toBe('named.nested')
})

test('should parse with supports query', () => {
Expand All @@ -1913,11 +1923,18 @@ describe('At-Rule Prelude Nodes', () => {

expect(children.length).toBe(2)
expect(children[0].type).toBe(URL)
expect(children[1].type).toBe(SUPPORTS_QUERY)
expect(children[1].type).toBe(FUNCTION)
expect(children[1].text).toBe('supports(display: grid)')
expect((children[1] as Function).name).toBe('supports')

// The Function owns the parentheses, the SupportsQuery inside has none
const query = (children[1] as Function).first_child as SupportsQuery
expect(query.type).toBe(SUPPORTS_QUERY)
expect(query.text).toBe('display: grid')
expect(query.value).toBe('display: grid')

// supports()'s argument should be deep-parsed into a SupportsDeclaration
const decl = (children[1] as SupportsQuery).children[0]
const decl = query.children[0]
expect(decl.type).toBe(SUPPORTS_DECLARATION)
expect(decl.text).toBe('display: grid')
})
Expand Down Expand Up @@ -1964,10 +1981,10 @@ describe('At-Rule Prelude Nodes', () => {

expect(children.length).toBe(3)
expect(children[0].type).toBe(URL)
expect(children[1].type).toBe(LAYER_NAME)
expect(children[1].type).toBe(FUNCTION)
expect(children[1].text).toBe('layer(base)')
expect((children[1] as LayerName).value).toBe('base')
expect((children[1] as LayerName).name).toBe('base')
expect(((children[1] as Function).first_child as LayerName).value).toBe('base')
expect(((children[1] as Function).first_child as LayerName).name).toBe('base')
expect(children[2].type).toBe(MEDIA_QUERY)
})

Expand All @@ -1982,15 +1999,20 @@ describe('At-Rule Prelude Nodes', () => {
expect(prelude.first_child?.type).toBe(URL)
expect(url.value).toBe('"styles.css"')

const layer = url?.next_sibling as LayerName
const layer_fn = url?.next_sibling as Function
expect(layer_fn.type).toBe(FUNCTION)
expect(layer_fn.name).toBe('layer')
const layer = layer_fn.first_child as LayerName
expect(layer.type).toBe(LAYER_NAME)
expect(layer.value).toBe('base')
expect(layer.name).toBe('base')

const supports = layer?.next_sibling as SupportsQuery
expect(supports.type).toBe(SUPPORTS_QUERY)
const supports = layer_fn.next_sibling as Function
expect(supports.type).toBe(FUNCTION)
expect(supports.name).toBe('supports')
expect(supports.value).toBe('display: grid')
expect(supports.text).toBe('supports(display: grid)')
expect((supports.first_child as SupportsQuery).type).toBe(SUPPORTS_QUERY)
})

test('should parse with supports and media query', () => {
Expand All @@ -2001,7 +2023,7 @@ describe('At-Rule Prelude Nodes', () => {

expect(children.length).toBe(3)
expect(children[0].type).toBe(URL)
expect(children[1].type).toBe(SUPPORTS_QUERY)
expect(children[1].type).toBe(FUNCTION)
expect(children[2].type).toBe(MEDIA_QUERY)
})

Expand All @@ -2014,8 +2036,8 @@ describe('At-Rule Prelude Nodes', () => {

expect(children.length).toBe(4)
expect(children[0].type).toBe(URL)
expect(children[1].type).toBe(LAYER_NAME)
expect(children[2].type).toBe(SUPPORTS_QUERY)
expect(children[1].type).toBe(FUNCTION)
expect(children[2].type).toBe(FUNCTION)
expect(children[3].type).toBe(MEDIA_QUERY)
})

Expand All @@ -2027,9 +2049,37 @@ describe('At-Rule Prelude Nodes', () => {

expect(children.length).toBe(2)
expect(children[0].type).toBe(URL)
expect(children[1].type).toBe(SUPPORTS_QUERY)
expect(children[1].type).toBe(FUNCTION)
expect(children[1].text).toBe('supports((display: grid) and (gap: 1rem))')
expect((children[1] as SupportsQuery).value).toBe('(display: grid) and (gap: 1rem)')
expect((children[1] as Function).value).toBe('(display: grid) and (gap: 1rem)')

// Same flat children as an @supports prelude: each SupportsQuery owns its own parentheses
const inner = (children[1] as Function).children
expect(inner.map((c) => c.type)).toEqual([SUPPORTS_QUERY, PRELUDE_OPERATOR, SUPPORTS_QUERY])
expect(inner.map((c) => c.text)).toEqual(['(display: grid)', 'and', '(gap: 1rem)'])
})

test('should parse bare layer() as Function without children', () => {
const css = '@import url("styles.css") layer();'
const ast = parse(css, { parse_atrule_preludes: true })
const atRule = ast.first_child! as Atrule
const fn = (atRule.prelude as AtrulePrelude).children[1] as Function

expect(fn.type).toBe(FUNCTION)
expect(fn.name).toBe('layer')
expect(fn.text).toBe('layer()')
expect(fn.has_children).toBe(false)
})

test('should parse empty supports() as Function without children', () => {
const css = '@import url("styles.css") supports();'
const ast = parse(css, { parse_atrule_preludes: true })
const atRule = ast.first_child! as Atrule
const fn = (atRule.prelude as AtrulePrelude).children[1] as Function

expect(fn.type).toBe(FUNCTION)
expect(fn.name).toBe('supports')
expect(fn.has_children).toBe(false)
})

test('should preserve prelude text', () => {
Expand Down Expand Up @@ -2267,7 +2317,7 @@ describe('At-Rule Prelude Nodes', () => {
const atRule = ast.first_child! as Atrule
const children = (atRule.prelude as AtrulePrelude | null)?.children || []

const importLayer = children.find((c) => c.type === LAYER_NAME)
const importLayer = children.find((c) => c.type === FUNCTION)
expect(importLayer?.text).toBe('layer(utilities)')
expect(importLayer?.text.length).toBe(16)
})
Expand All @@ -2278,7 +2328,7 @@ describe('At-Rule Prelude Nodes', () => {
const atRule = ast.first_child! as Atrule
const children = (atRule.prelude as AtrulePrelude | null)?.children || []

const importSupports = children.find((c) => c.type === SUPPORTS_QUERY)
const importSupports = children.find((c) => c.type === FUNCTION)
expect(importSupports?.text).toBe('supports(display: flex)')
expect(importSupports?.text.length).toBe(23)
})
Expand Down Expand Up @@ -2563,15 +2613,17 @@ describe('parse_atrule_prelude()', () => {

expect(result.length).toBeGreaterThanOrEqual(2)
expect(result[0].type).toBe(URL)
expect(result[1].type).toBe(LAYER_NAME)
expect(result[1].type).toBe(FUNCTION)
expect((result[1].first_child as LayerName).type).toBe(LAYER_NAME)
})

test('should parse import with supports', () => {
const result = parse_atrule_prelude('import', 'url("modern.css") supports(display: grid)')

expect(result.length).toBeGreaterThanOrEqual(2)
expect(result[0].type).toBe(URL)
expect(result[1].type).toBe(SUPPORTS_QUERY)
expect(result[1].type).toBe(FUNCTION)
expect(result[1].first_child?.type).toBe(SUPPORTS_QUERY)
})
})

Expand Down
107 changes: 66 additions & 41 deletions src/parse-atrule-prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,10 @@ export class AtRulePreludeParser {
return url_node
}

// Parse import layer: layer or layer(name)
// Parse import layer: `layer` or `layer(name)`
// - `layer` → LayerName with name=''
// - `layer()` → Function{name='layer'} without children
// - `layer(a.b)` → Function{name='layer'} > LayerName{name='a.b'}
private parse_import_layer(): number | null {
// Peek at next token
const saved = this.lexer.save_position()
Expand All @@ -568,40 +571,39 @@ export class AtRulePreludeParser {

if (str_equals_range(this.source, this.lexer.token_start, name_end, 'layer')) {
const layer_start = this.lexer.token_start
let layer_end = this.lexer.token_end
let content_start = 0
let content_length = 0

// If it's a function token, parse the contents until closing paren
if (this.lexer.token_type === TOKEN_FUNCTION) {
// Track the content inside the parentheses
content_start = this.lexer.pos
const matched = this.scan_matching_paren()
const content_end = this.paren_content_end
const close_end = this.paren_close_end
if (matched) {
content_length = content_end - content_start
layer_end = close_end
}

// Bare `layer` keyword: an unnamed layer
if (this.lexer.token_type === TOKEN_IDENT) {
const layer_node = this.create_node(LAYER_NAME, layer_start, this.lexer.token_end)
this.arena.set_content_start_delta(layer_node, 0)
this.arena.set_content_length(layer_node, 0)
return layer_node
}

// Create layer node
const layer_node = this.create_node(LAYER_NAME, layer_start, layer_end)
// layer(...): the Function owns the parentheses, the LayerName only the name
const content_start = this.lexer.token_end // After the opening '('
const matched = this.scan_matching_paren()
const content_end = this.paren_content_end
const layer_end = matched ? this.paren_close_end : this.lexer.pos

const func_node = this.create_node(FUNCTION, layer_start, layer_end)
this.arena.set_content_start_delta(func_node, 0)
this.arena.set_content_length(func_node, name_end - layer_start)

// Store the layer name (content inside parentheses), trimmed
if (content_length > 0) {
const trimmed = trim_boundaries(
this.source,
content_start,
content_start + content_length,
)
if (trimmed) {
this.arena.set_content_start_delta(layer_node, trimmed[0] - layer_start)
this.arena.set_content_length(layer_node, trimmed[1] - trimmed[0])
}
const trimmed = matched ? trim_boundaries(this.source, content_start, content_end) : null
if (trimmed) {
const name_start = trimmed[0]
const name_length = trimmed[1] - name_start
this.arena.set_value_start_delta(func_node, name_start - layer_start)
this.arena.set_value_length(func_node, name_length)

const name_node = this.create_node(LAYER_NAME, name_start, trimmed[1])
this.arena.set_content_start_delta(name_node, 0)
this.arena.set_content_length(name_node, name_length)
this.arena.set_first_child(func_node, name_node)
}

return layer_node
return func_node
}
}

Expand All @@ -611,6 +613,12 @@ export class AtRulePreludeParser {
}

// Parse import supports: supports(condition)
// Function{name='supports'} owns the outer parentheses. Its children are either
// - SupportsQuery > SupportsDeclaration for `supports(display: grid)`, where the
// SupportsQuery has no parentheses of its own because the source has none, or
// - the same flat condition nodes as an `@supports` prelude for
// `supports((display: grid) and (color: red))`, where each SupportsQuery
// owns its own `(…)`.
private parse_import_supports(): number | null {
// Peek at next token
const saved = this.lexer.save_position()
Expand All @@ -624,33 +632,50 @@ export class AtRulePreludeParser {
str_equals_range(this.source, this.lexer.token_start, this.lexer.token_end - 1, 'supports')
) {
const supports_start = this.lexer.token_start
const name_end = this.lexer.token_end - 1
const content_start = this.lexer.token_end // After the opening '('
const line = this.lexer.line
const column = this.lexer.column

// Find matching closing parenthesis
this.scan_matching_paren()
const content_end = this.paren_content_end
const supports_end = this.paren_close_end

// Create supports node
const supports_node = this.create_node(SUPPORTS_QUERY, supports_start, supports_end)
const func_node = this.create_node(FUNCTION, supports_start, supports_end)
this.arena.set_content_start_delta(func_node, 0)
this.arena.set_content_length(func_node, name_end - supports_start)

// Store query content in value fields, trimmed
const trimmed = trim_boundaries(this.source, content_start, content_end)
if (trimmed) {
this.arena.set_value_start_delta(supports_node, trimmed[0] - supports_start)
this.arena.set_value_length(supports_node, trimmed[1] - trimmed[0])
const condition_start = trimmed[0]
const condition_end = trimmed[1]
const condition_length = condition_end - condition_start
this.arena.set_value_start_delta(func_node, condition_start - supports_start)
this.arena.set_value_length(func_node, condition_length)

// Check for simple declaration: supports(property: value)
const decl_child = this.condition_parser.parse_supports_declaration_content(
trimmed[0],
trimmed[1],
condition_start,
condition_end,
)
if (decl_child !== null) {
this.arena.set_first_child(supports_node, decl_child)
if (decl_child === null) {
const children = this.condition_parser.parse_supports_condition(
condition_start,
condition_end,
line,
column,
)
this.arena.append_children(func_node, children)
} else {
const query = this.create_node(SUPPORTS_QUERY, condition_start, condition_end)
this.arena.set_value_start_delta(query, 0)
this.arena.set_value_length(query, condition_length)
this.arena.set_first_child(query, decl_child)
this.arena.set_first_child(func_node, query)
}
}

return supports_node
return func_node
}
}

Expand Down
Loading