Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sass-parser support for the @while rule #2410

Merged
merged 6 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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