Skip to content

Commit

Permalink
Add sass-parser support for the @while rule (#2410)
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 authored Oct 31, 2024
1 parent df77b66 commit a9254df
Show file tree
Hide file tree
Showing 8 changed files with 428 additions and 11 deletions.
2 changes: 1 addition & 1 deletion pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## 0.4.3-dev

* No user-visible changes.
* Add support for parsing the `@while` rule.

## 0.4.2

Expand Down
5 changes: 5 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ export {
VariableDeclarationRaws,
} from './src/statement/variable-declaration';
export {WarnRule, WarnRuleProps, WarnRuleRaws} from './src/statement/warn-rule';
export {
WhileRule,
WhileRuleProps,
WhileRuleRaws,
} from './src/statement/while-rule';

/** Options that can be passed to the Sass parsers to control their behavior. */
export type SassParserOptions = Pick<postcss.ProcessOptions, 'from' | 'map'>;
Expand Down
6 changes: 6 additions & 0 deletions pkg/sass-parser/lib/src/sass-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ declare namespace SassInternal {
readonly expression: Expression;
}

class WhileRule extends ParentStatement<Statement[]> {
readonly condition: Expression;
}

class ConfiguredVariable extends SassNode {
readonly name: string;
readonly expression: Expression;
Expand Down Expand Up @@ -252,6 +256,7 @@ export type SupportsRule = SassInternal.SupportsRule;
export type UseRule = SassInternal.UseRule;
export type VariableDeclaration = SassInternal.VariableDeclaration;
export type WarnRule = SassInternal.WarnRule;
export type WhileRule = SassInternal.WhileRule;
export type ConfiguredVariable = SassInternal.ConfiguredVariable;
export type Interpolation = SassInternal.Interpolation;
export type Expression = SassInternal.Expression;
Expand All @@ -276,6 +281,7 @@ export interface StatementVisitorObject<T> {
visitUseRule(node: UseRule): T;
visitVariableDeclaration(node: VariableDeclaration): T;
visitWarnRule(node: WarnRule): T;
visitWhileRule(node: WhileRule): T;
}

export interface ExpressionVisitorObject<T> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a @while rule toJSON 1`] = `
{
"inputs": [
{
"css": "@while foo {}",
"hasBOM": false,
"id": "<input css _____>",
},
],
"name": "while",
"nodes": [],
"params": "foo",
"raws": {},
"sassType": "while-rule",
"source": <1:1-1:14 in 0>,
"type": "atrule",
"whileCondition": <foo>,
}
`;
13 changes: 10 additions & 3 deletions pkg/sass-parser/lib/src/statement/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
VariableDeclarationProps,
} from './variable-declaration';
import {WarnRule, WarnRuleProps} from './warn-rule';
import {WhileRule, WhileRuleProps} from './while-rule';

// TODO: Replace this with the corresponding Sass types once they're
// implemented.
Expand Down Expand Up @@ -56,7 +57,8 @@ export type StatementType =
| 'use-rule'
| 'sass-comment'
| 'variable-declaration'
| 'warn-rule';
| 'warn-rule'
| 'while-rule';

/**
* All Sass statements that are also at-rules.
Expand All @@ -70,7 +72,8 @@ export type AtRule =
| ForRule
| GenericAtRule
| UseRule
| WarnRule;
| WarnRule
| WhileRule;

/**
* All Sass statements that are comments.
Expand Down Expand Up @@ -107,7 +110,8 @@ export type ChildProps =
| SassCommentChildProps
| UseRuleProps
| VariableDeclarationProps
| WarnRuleProps;
| WarnRuleProps
| WhileRuleProps;

/**
* The Sass eqivalent of PostCSS's `ContainerProps`.
Expand Down Expand Up @@ -197,6 +201,7 @@ const visitor = sassInternal.createStatementVisitor<Statement>({
visitUseRule: inner => new UseRule(undefined, inner),
visitVariableDeclaration: inner => new VariableDeclaration(undefined, inner),
visitWarnRule: inner => new WarnRule(undefined, inner),
visitWhileRule: inner => new WhileRule(undefined, inner),
});

/** Appends parsed versions of `internal`'s children to `container`. */
Expand Down Expand Up @@ -317,6 +322,8 @@ export function normalize(
result.push(new VariableDeclaration(node));
} else if ('warnExpression' in node) {
result.push(new WarnRule(node));
} else if ('whileCondition' in node) {
result.push(new WhileRule(node));
} else {
result.push(...postcssNormalizeAndConvertToSass(self, node, sample));
}
Expand Down
239 changes: 239 additions & 0 deletions pkg/sass-parser/lib/src/statement/while-rule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// 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, StringExpression, WhileRule, sass, scss} from '../..';
import * as utils from '../../../test/utils';

describe('a @while rule', () => {
let node: WhileRule;
describe('with empty children', () => {
function describeNode(description: string, create: () => WhileRule): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has a name', () => expect(node.name.toString()).toBe('while'));

it('has an expression', () =>
expect(node).toHaveStringExpression('whileCondition', 'foo'));

it('has matching params', () => expect(node.params).toBe('foo'));

it('has empty nodes', () => expect(node.nodes).toEqual([]));
});
}

describeNode(
'parsed as SCSS',
() => scss.parse('@while foo {}').nodes[0] as WhileRule,
);

describeNode(
'parsed as Sass',
() => sass.parse('@while foo').nodes[0] as WhileRule,
);

describeNode(
'constructed manually',
() =>
new WhileRule({
whileCondition: {text: 'foo'},
}),
);

describeNode('constructed from ChildProps', () =>
utils.fromChildProps({
whileCondition: {text: 'foo'},
}),
);
});

describe('with a child', () => {
function describeNode(description: string, create: () => WhileRule): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has a name', () => expect(node.name.toString()).toBe('while'));

it('has an expression', () =>
expect(node).toHaveStringExpression('whileCondition', 'foo'));

it('has matching params', () => expect(node.params).toBe('foo'));

it('has a child node', () => {
expect(node.nodes).toHaveLength(1);
expect(node.nodes[0]).toBeInstanceOf(GenericAtRule);
expect(node.nodes[0]).toHaveProperty('name', 'child');
});
});
}

describeNode(
'parsed as SCSS',
() => scss.parse('@while foo {@child}').nodes[0] as WhileRule,
);

describeNode(
'parsed as Sass',
() => sass.parse('@while foo\n @child').nodes[0] as WhileRule,
);

describeNode(
'constructed manually',
() =>
new WhileRule({
whileCondition: {text: 'foo'},
nodes: [{name: 'child'}],
}),
);

describeNode('constructed from ChildProps', () =>
utils.fromChildProps({
whileCondition: {text: 'foo'},
nodes: [{name: 'child'}],
}),
);
});

describe('throws an error when assigned a new', () => {
beforeEach(
() => void (node = new WhileRule({whileCondition: {text: 'foo'}})),
);

it('name', () => expect(() => (node.name = 'bar')).toThrow());

it('params', () => expect(() => (node.params = 'true')).toThrow());
});

describe('assigned a new expression', () => {
beforeEach(() => {
node = scss.parse('@while foo {}').nodes[0] as WhileRule;
});

it("removes the old expression's parent", () => {
const oldExpression = node.whileCondition;
node.whileCondition = {text: 'bar'};
expect(oldExpression.parent).toBeUndefined();
});

it("assigns the new expression's parent", () => {
const expression = new StringExpression({text: 'bar'});
node.whileCondition = expression;
expect(expression.parent).toBe(node);
});

it('assigns the expression explicitly', () => {
const expression = new StringExpression({text: 'bar'});
node.whileCondition = expression;
expect(node.whileCondition).toBe(expression);
});

it('assigns the expression as ExpressionProps', () => {
node.whileCondition = {text: 'bar'};
expect(node).toHaveStringExpression('whileCondition', 'bar');
});
});

describe('stringifies', () => {
describe('to SCSS', () => {
it('with default raws', () =>
expect(
new WhileRule({
whileCondition: {text: 'foo'},
}).toString(),
).toBe('@while foo {}'));

it('with afterName', () =>
expect(
new WhileRule({
whileCondition: {text: 'foo'},
raws: {afterName: '/**/'},
}).toString(),
).toBe('@while/**/foo {}'));

it('with between', () =>
expect(
new WhileRule({
whileCondition: {text: 'foo'},
raws: {between: '/**/'},
}).toString(),
).toBe('@while foo/**/{}'));
});
});

describe('clone', () => {
let original: WhileRule;
beforeEach(() => {
original = scss.parse('@while foo {}').nodes[0] as WhileRule;
// TODO: remove this once raws are properly parsed
original.raws.between = ' ';
});

describe('with no overrides', () => {
let clone: WhileRule;
beforeEach(() => void (clone = original.clone()));

describe('has the same properties:', () => {
it('params', () => expect(clone.params).toBe('foo'));

it('whileCondition', () =>
expect(clone).toHaveStringExpression('whileCondition', 'foo'));

it('raws', () => expect(clone.raws).toEqual({between: ' '}));

it('source', () => expect(clone.source).toBe(original.source));
});

describe('creates a new', () => {
it('self', () => expect(clone).not.toBe(original));

for (const attr of ['whileCondition', 'raws'] as const) {
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
}
});
});

describe('overrides', () => {
describe('raws', () => {
it('defined', () =>
expect(original.clone({raws: {afterName: ' '}}).raws).toEqual({
afterName: ' ',
}));

it('undefined', () =>
expect(original.clone({raws: undefined}).raws).toEqual({
between: ' ',
}));
});

describe('whileCondition', () => {
describe('defined', () => {
let clone: WhileRule;
beforeEach(() => {
clone = original.clone({whileCondition: {text: 'bar'}});
});

it('changes params', () => expect(clone.params).toBe('bar'));

it('changes whileCondition', () =>
expect(clone).toHaveStringExpression('whileCondition', 'bar'));
});

describe('undefined', () => {
let clone: WhileRule;
beforeEach(() => {
clone = original.clone({whileCondition: undefined});
});

it('preserves params', () => expect(clone.params).toBe('foo'));

it('preserves whileCondition', () =>
expect(clone).toHaveStringExpression('whileCondition', 'foo'));
});
});
});
});

it('toJSON', () =>
expect(scss.parse('@while foo {}').nodes[0]).toMatchSnapshot());
});
Loading

0 comments on commit a9254df

Please sign in to comment.