diff --git a/src/parse-atrule-prelude.test.ts b/src/parse-atrule-prelude.test.ts index a727b1a..40aea05 100644 --- a/src/parse-atrule-prelude.test.ts +++ b/src/parse-atrule-prelude.test.ts @@ -976,6 +976,23 @@ describe('At-Rule Prelude Nodes', () => { expect(children[2].text).toBe('utilities') expect((children[2] as LayerName).value).toBe('utilities') }) + + test('should keep dotted layer names intact among comma-separated names', () => { + const css = '@layer a.b, c;' + const ast = parse(css) + const atRule = ast.first_child! as Atrule + + const children = (atRule.prelude as AtrulePrelude | null)?.children || [] + expect(children.length).toBe(2) + + expect(children[0].type).toBe(LAYER_NAME) + expect(children[0].text).toBe('a.b') + expect((children[0] as LayerName).value).toBe('a.b') + + expect(children[1].type).toBe(LAYER_NAME) + expect(children[1].text).toBe('c') + expect((children[1] as LayerName).value).toBe('c') + }) }) describe('@keyframes', () => { @@ -1969,7 +1986,32 @@ describe('parse_atrule_prelude()', () => { test('should parse dotted layer name', () => { const result = parse_atrule_prelude('layer', 'framework.base') - expect(result.length).toBeGreaterThan(0) + expect(result.length).toBe(1) + expect(result[0].type).toBe(LAYER_NAME) + expect(result[0].text).toBe('framework.base') + }) + + test('should parse a nested dotted layer name mixed with a simple one', () => { + const result = parse_atrule_prelude('layer', 'a, b.c') + + expect(result.length).toBe(2) + expect(result[0].type).toBe(LAYER_NAME) + expect(result[0].text).toBe('a') + expect(result[1].type).toBe(LAYER_NAME) + expect(result[1].text).toBe('b.c') + }) + + test('should parse deeply nested dotted layer names', () => { + const result = parse_atrule_prelude('layer', 'a, b.c, d.e.f') + + expect(result.length).toBe(3) + expect(result.map((n) => n.text)).toEqual(['a', 'b.c', 'd.e.f']) + }) + + test('should not glue a dot separated by whitespace onto the layer name', () => { + const result = parse_atrule_prelude('layer', 'a. b') + + expect(result.map((n) => n.text)).toEqual(['a', 'b']) }) }) diff --git a/src/parse-atrule-prelude.ts b/src/parse-atrule-prelude.ts index 0bc61da..1bd8967 100644 --- a/src/parse-atrule-prelude.ts +++ b/src/parse-atrule-prelude.ts @@ -34,6 +34,7 @@ import { TOKEN_NUMBER, TOKEN_PERCENTAGE, TOKEN_DIMENSION, + TOKEN_DELIM, type TokenType, } from './token-types' import { @@ -44,6 +45,7 @@ import { CHAR_LESS_THAN, CHAR_GREATER_THAN, CHAR_EQUALS, + CHAR_PERIOD, } from './string-utils' import { trim_boundaries, skip_whitespace_and_comments_forward } from './parse-utils' import { CSSNode } from './css-node' @@ -553,6 +555,8 @@ export class AtRulePreludeParser { } // Parse layer names: base, components, utilities + // A single name may be dotted for nested layers: base.normalize + // = ['.' ]* with no whitespace around the dots. private parse_layer_names(): number[] { let nodes: number[] = [] @@ -564,10 +568,37 @@ export class AtRulePreludeParser { let token_type = this.lexer.token_type if (token_type === TOKEN_IDENT) { - // Layer name - let layer = this.create_node(LAYER_NAME, this.lexer.token_start, this.lexer.token_end) + let name_start = this.lexer.token_start + let name_end = this.lexer.token_end + + // Glue on '.' ident segments immediately following, with no gaps. + while (this.lexer.pos < this.prelude_end) { + let saved = this.lexer.save_position() + + let dot_token_type = this.next_token() + if ( + dot_token_type !== TOKEN_DELIM || + this.source.charCodeAt(this.lexer.token_start) !== CHAR_PERIOD || + this.lexer.token_start !== name_end + ) { + this.lexer.restore_position(saved) + break + } + let dot_end = this.lexer.token_end + + let segment_token_type = this.next_token() + if (segment_token_type !== TOKEN_IDENT || this.lexer.token_start !== dot_end) { + this.lexer.restore_position(saved) + break + } + + name_end = this.lexer.token_end + } + + // Layer name (possibly dotted) + let layer = this.create_node(LAYER_NAME, name_start, name_end) this.arena.set_content_start_delta(layer, 0) - this.arena.set_content_length(layer, this.lexer.token_end - this.lexer.token_start) + this.arena.set_content_length(layer, name_end - name_start) nodes.push(layer) } else if (token_type === TOKEN_COMMA) { // Skip comma separator