From b60577eace65df9151269281f1f3e061a0853d08 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 16 Aug 2024 20:08:42 -0700 Subject: [PATCH] Add support for `@extend` rules --- pkg/sass-parser/lib/src/interpolation.ts | 5 +- pkg/sass-parser/lib/src/sass-internal.ts | 7 +++ .../lib/src/statement/at-root-rule.test.ts | 8 +-- .../lib/src/statement/extend-rule.test.ts | 61 +++++++++++++++++++ pkg/sass-parser/lib/src/statement/index.ts | 9 +++ 5 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 pkg/sass-parser/lib/src/statement/extend-rule.test.ts diff --git a/pkg/sass-parser/lib/src/interpolation.ts b/pkg/sass-parser/lib/src/interpolation.ts index 387127e85..8ffbb94a2 100644 --- a/pkg/sass-parser/lib/src/interpolation.ts +++ b/pkg/sass-parser/lib/src/interpolation.ts @@ -112,9 +112,8 @@ export class Interpolation extends Node { */ get asPlain(): string | null { if (this.nodes.length === 0) return ''; - if (this.nodes.length !== 1) return null; - if (typeof this.nodes[0] !== 'string') return null; - return this.nodes[0] as string; + if (this.nodes.some(node => typeof node !== 'string')) return null; + return this.nodes.join(''); } /** diff --git a/pkg/sass-parser/lib/src/sass-internal.ts b/pkg/sass-parser/lib/src/sass-internal.ts index 110e777aa..f54f83aaf 100644 --- a/pkg/sass-parser/lib/src/sass-internal.ts +++ b/pkg/sass-parser/lib/src/sass-internal.ts @@ -89,6 +89,11 @@ declare namespace SassInternal { readonly expression: Expression; } + class ExtendRule extends Statement { + readonly selector: Interpolation; + readonly isOptional: boolean; + } + class Stylesheet extends ParentStatement {} class StyleRule extends ParentStatement { @@ -129,6 +134,7 @@ export type AtRule = SassInternal.AtRule; export type DebugRule = SassInternal.DebugRule; export type EachRule = SassInternal.EachRule; export type ErrorRule = SassInternal.ErrorRule; +export type ExtendRule = SassInternal.ExtendRule; export type Stylesheet = SassInternal.Stylesheet; export type StyleRule = SassInternal.StyleRule; export type Interpolation = SassInternal.Interpolation; @@ -142,6 +148,7 @@ export interface StatementVisitorObject { visitDebugRule(node: DebugRule): T; visitEachRule(node: EachRule): T; visitErrorRule(node: ErrorRule): T; + visitExtendRule(node: ExtendRule): T; visitStyleRule(node: StyleRule): T; } diff --git a/pkg/sass-parser/lib/src/statement/at-root-rule.test.ts b/pkg/sass-parser/lib/src/statement/at-root-rule.test.ts index 40e9167ce..b5c7a17f7 100644 --- a/pkg/sass-parser/lib/src/statement/at-root-rule.test.ts +++ b/pkg/sass-parser/lib/src/statement/at-root-rule.test.ts @@ -12,7 +12,7 @@ describe('an @at-root rule', () => { () => void (node = scss.parse('@at-root {}').nodes[0] as GenericAtRule) ); - it('has a name', () => expect(node.name.toString()).toBe('at-root')); + it('has a name', () => expect(node.name).toBe('at-root')); it('has no paramsInterpolation', () => expect(node.paramsInterpolation).toBeUndefined()); @@ -27,7 +27,7 @@ describe('an @at-root rule', () => { .nodes[0] as GenericAtRule) ); - it('has a name', () => expect(node.name.toString()).toBe('at-root')); + it('has a name', () => expect(node.name).toBe('at-root')); it('has a paramsInterpolation', () => expect(node).toHaveInterpolation('paramsInterpolation', '(with: rule)')); @@ -44,7 +44,7 @@ describe('an @at-root rule', () => { .nodes[0] as GenericAtRule) ); - it('has a name', () => expect(node.name.toString()).toBe('at-root')); + it('has a name', () => expect(node.name).toBe('at-root')); it('has a paramsInterpolation', () => { const params = node.paramsInterpolation!; @@ -63,7 +63,7 @@ describe('an @at-root rule', () => { void (node = scss.parse('@at-root .foo {}').nodes[0] as GenericAtRule) ); - it('has a name', () => expect(node.name.toString()).toBe('at-root')); + it('has a name', () => expect(node.name).toBe('at-root')); it('has no paramsInterpolation', () => expect(node.paramsInterpolation).toBeUndefined()); diff --git a/pkg/sass-parser/lib/src/statement/extend-rule.test.ts b/pkg/sass-parser/lib/src/statement/extend-rule.test.ts new file mode 100644 index 000000000..15485b604 --- /dev/null +++ b/pkg/sass-parser/lib/src/statement/extend-rule.test.ts @@ -0,0 +1,61 @@ +// Copyright 2024 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import {GenericAtRule, Rule, scss} from '../..'; + +describe('an @extend rule', () => { + let node: GenericAtRule; + + describe('with no interpolation', () => { + beforeEach( + () => + void (node = (scss.parse('.foo {@extend .bar}').nodes[0] as Rule) + .nodes[0] as GenericAtRule) + ); + + it('has a name', () => expect(node.name).toBe('extend')); + + it('has a paramsInterpolation', () => + expect(node).toHaveInterpolation('paramsInterpolation', '.bar')); + + it('has matching params', () => expect(node.params).toBe('.bar')); + }); + + describe('with interpolation', () => { + beforeEach( + () => + void (node = (scss.parse('.foo {@extend .#{bar}}').nodes[0] as Rule) + .nodes[0] as GenericAtRule) + ); + + it('has a name', () => expect(node.name).toBe('extend')); + + it('has a paramsInterpolation', () => { + const params = node.paramsInterpolation!; + expect(params.nodes[0]).toBe('.'); + expect(params).toHaveStringExpression(1, 'bar'); + }); + + it('has matching params', () => expect(node.params).toBe('.#{bar}')); + }); + + describe('with !optional', () => { + beforeEach( + () => + void (node = ( + scss.parse('.foo {@extend .bar !optional}').nodes[0] as Rule + ).nodes[0] as GenericAtRule) + ); + + it('has a name', () => expect(node.name).toBe('extend')); + + it('has a paramsInterpolation', () => + expect(node).toHaveInterpolation( + 'paramsInterpolation', + '.bar !optional' + )); + + it('has matching params', () => expect(node.params).toBe('.bar !optional')); + }); +}); diff --git a/pkg/sass-parser/lib/src/statement/index.ts b/pkg/sass-parser/lib/src/statement/index.ts index 99e58289f..94df27a33 100644 --- a/pkg/sass-parser/lib/src/statement/index.ts +++ b/pkg/sass-parser/lib/src/statement/index.ts @@ -125,6 +125,15 @@ const visitor = sassInternal.createStatementVisitor({ visitDebugRule: inner => new DebugRule(undefined, inner), visitErrorRule: inner => new ErrorRule(undefined, inner), visitEachRule: inner => new EachRule(undefined, inner), + visitExtendRule: inner => { + const paramsInterpolation = new Interpolation(undefined, inner.selector); + if (inner.isOptional) paramsInterpolation.append('!optional'); + return new GenericAtRule({ + name: 'extend', + paramsInterpolation, + source: new LazySource(inner), + }); + }, visitStyleRule: inner => new Rule(undefined, inner), });