Skip to content

Commit c428a0b

Browse files
bartvenemanclaude
andauthored
Support dotted layer names in @layer at-rule prelude parsing (#268)
## Summary This PR adds support for parsing dotted layer names (e.g., `a.b.c`) in CSS `@layer` at-rule prelude declarations, allowing nested layer references to be properly recognized and preserved. ## Key Changes - **Enhanced layer name parsing**: Modified `parse_layer_names()` to recognize and preserve dotted notation in layer names by consuming consecutive `.` and `<ident>` tokens that immediately follow the initial identifier with no whitespace gaps - **Whitespace sensitivity**: Implemented strict position checking to ensure dots separated by whitespace are not glued to layer names (e.g., `a. b` parses as two separate names) - **Token imports**: Added `TOKEN_DELIM` import to support dot token detection - **String utilities**: Added `CHAR_PERIOD` constant for dot character code comparison - **Comprehensive test coverage**: Added four new test cases covering: - Dotted layer names mixed with simple names in comma-separated lists - Deeply nested dotted layer names (e.g., `d.e.f`) - Whitespace-separated dots not being attached to names - Improved assertion specificity for existing dotted layer name test ## Implementation Details The parser now uses a lookahead mechanism with position save/restore to safely attempt consuming dot-identifier segments. It validates that: 1. The next token is a `TOKEN_DELIM` with character code matching `CHAR_PERIOD` 2. The dot immediately follows the previous token (no whitespace gap) 3. An identifier immediately follows the dot (no whitespace gap) If any condition fails, the parser restores the previous position and stops consuming segments, allowing the comma separator or end of prelude to be processed normally. https://claude.ai/code/session_01R9GWUSxhM3VVAWVFs9F5bG Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4fe1f0a commit c428a0b

2 files changed

Lines changed: 77 additions & 4 deletions

File tree

src/parse-atrule-prelude.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,23 @@ describe('At-Rule Prelude Nodes', () => {
976976
expect(children[2].text).toBe('utilities')
977977
expect((children[2] as LayerName).value).toBe('utilities')
978978
})
979+
980+
test('should keep dotted layer names intact among comma-separated names', () => {
981+
const css = '@layer a.b, c;'
982+
const ast = parse(css)
983+
const atRule = ast.first_child! as Atrule
984+
985+
const children = (atRule.prelude as AtrulePrelude | null)?.children || []
986+
expect(children.length).toBe(2)
987+
988+
expect(children[0].type).toBe(LAYER_NAME)
989+
expect(children[0].text).toBe('a.b')
990+
expect((children[0] as LayerName).value).toBe('a.b')
991+
992+
expect(children[1].type).toBe(LAYER_NAME)
993+
expect(children[1].text).toBe('c')
994+
expect((children[1] as LayerName).value).toBe('c')
995+
})
979996
})
980997

981998
describe('@keyframes', () => {
@@ -1969,7 +1986,32 @@ describe('parse_atrule_prelude()', () => {
19691986
test('should parse dotted layer name', () => {
19701987
const result = parse_atrule_prelude('layer', 'framework.base')
19711988

1972-
expect(result.length).toBeGreaterThan(0)
1989+
expect(result.length).toBe(1)
1990+
expect(result[0].type).toBe(LAYER_NAME)
1991+
expect(result[0].text).toBe('framework.base')
1992+
})
1993+
1994+
test('should parse a nested dotted layer name mixed with a simple one', () => {
1995+
const result = parse_atrule_prelude('layer', 'a, b.c')
1996+
1997+
expect(result.length).toBe(2)
1998+
expect(result[0].type).toBe(LAYER_NAME)
1999+
expect(result[0].text).toBe('a')
2000+
expect(result[1].type).toBe(LAYER_NAME)
2001+
expect(result[1].text).toBe('b.c')
2002+
})
2003+
2004+
test('should parse deeply nested dotted layer names', () => {
2005+
const result = parse_atrule_prelude('layer', 'a, b.c, d.e.f')
2006+
2007+
expect(result.length).toBe(3)
2008+
expect(result.map((n) => n.text)).toEqual(['a', 'b.c', 'd.e.f'])
2009+
})
2010+
2011+
test('should not glue a dot separated by whitespace onto the layer name', () => {
2012+
const result = parse_atrule_prelude('layer', 'a. b')
2013+
2014+
expect(result.map((n) => n.text)).toEqual(['a', 'b'])
19732015
})
19742016
})
19752017

src/parse-atrule-prelude.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
TOKEN_NUMBER,
3535
TOKEN_PERCENTAGE,
3636
TOKEN_DIMENSION,
37+
TOKEN_DELIM,
3738
type TokenType,
3839
} from './token-types'
3940
import {
@@ -44,6 +45,7 @@ import {
4445
CHAR_LESS_THAN,
4546
CHAR_GREATER_THAN,
4647
CHAR_EQUALS,
48+
CHAR_PERIOD,
4749
} from './string-utils'
4850
import { trim_boundaries, skip_whitespace_and_comments_forward } from './parse-utils'
4951
import { CSSNode } from './css-node'
@@ -553,6 +555,8 @@ export class AtRulePreludeParser {
553555
}
554556

555557
// Parse layer names: base, components, utilities
558+
// A single name may be dotted for nested layers: base.normalize
559+
// <layer-name> = <ident> ['.' <ident>]* with no whitespace around the dots.
556560
private parse_layer_names(): number[] {
557561
let nodes: number[] = []
558562

@@ -564,10 +568,37 @@ export class AtRulePreludeParser {
564568

565569
let token_type = this.lexer.token_type
566570
if (token_type === TOKEN_IDENT) {
567-
// Layer name
568-
let layer = this.create_node(LAYER_NAME, this.lexer.token_start, this.lexer.token_end)
571+
let name_start = this.lexer.token_start
572+
let name_end = this.lexer.token_end
573+
574+
// Glue on '.' ident segments immediately following, with no gaps.
575+
while (this.lexer.pos < this.prelude_end) {
576+
let saved = this.lexer.save_position()
577+
578+
let dot_token_type = this.next_token()
579+
if (
580+
dot_token_type !== TOKEN_DELIM ||
581+
this.source.charCodeAt(this.lexer.token_start) !== CHAR_PERIOD ||
582+
this.lexer.token_start !== name_end
583+
) {
584+
this.lexer.restore_position(saved)
585+
break
586+
}
587+
let dot_end = this.lexer.token_end
588+
589+
let segment_token_type = this.next_token()
590+
if (segment_token_type !== TOKEN_IDENT || this.lexer.token_start !== dot_end) {
591+
this.lexer.restore_position(saved)
592+
break
593+
}
594+
595+
name_end = this.lexer.token_end
596+
}
597+
598+
// Layer name (possibly dotted)
599+
let layer = this.create_node(LAYER_NAME, name_start, name_end)
569600
this.arena.set_content_start_delta(layer, 0)
570-
this.arena.set_content_length(layer, this.lexer.token_end - this.lexer.token_start)
601+
this.arena.set_content_length(layer, name_end - name_start)
571602
nodes.push(layer)
572603
} else if (token_type === TOKEN_COMMA) {
573604
// Skip comma separator

0 commit comments

Comments
 (0)