Skip to content

Commit

Permalink
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

5 changes: 5 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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'>;
6 changes: 6 additions & 0 deletions pkg/sass-parser/lib/src/sass-internal.ts
Original file line number Diff line number Diff line change
@@ -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;
@@ -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;
@@ -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> {
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
@@ -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.
@@ -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.
@@ -70,7 +72,8 @@ export type AtRule =
| ForRule
| GenericAtRule
| UseRule
| WarnRule;
| WarnRule
| WhileRule;

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

/**
* The Sass eqivalent of PostCSS's `ContainerProps`.
@@ -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`. */
@@ -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));
}
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());
});
136 changes: 136 additions & 0 deletions pkg/sass-parser/lib/src/statement/while-rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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 * as postcss from 'postcss';
import type {AtRuleRaws} from 'postcss/lib/at-rule';

import {convertExpression} from '../expression/convert';
import {Expression, ExpressionProps} from '../expression';
import {fromProps} from '../expression/from-props';
import {LazySource} from '../lazy-source';
import type * as sassInternal from '../sass-internal';
import * as utils from '../utils';
import {
ChildNode,
ContainerProps,
NewNode,
Statement,
StatementWithChildren,
appendInternalChildren,
normalize,
} from '.';
import {_AtRule} from './at-rule-internal';
import {interceptIsClean} from './intercept-is-clean';
import * as sassParser from '../..';

/**
* The set of raws supported by {@link WhileRule}.
*
* @category Statement
*/
export type WhileRuleRaws = Omit<AtRuleRaws, 'params'>;

/**
* The initializer properties for {@link WhileRule}.
*
* @category Statement
*/
export type WhileRuleProps = ContainerProps & {
raws?: WhileRuleRaws;
whileCondition: Expression | ExpressionProps;
};

/**
* A `@while` rule. Extends [`postcss.AtRule`].
*
* [`postcss.AtRule`]: https://postcss.org/api/#atrule
*
* @category Statement
*/
export class WhileRule
extends _AtRule<Partial<WhileRuleProps>>
implements Statement
{
readonly sassType = 'while-rule' as const;
declare parent: StatementWithChildren | undefined;
declare raws: WhileRuleRaws;
declare nodes: ChildNode[];

get name(): string {
return 'while';
}
set name(value: string) {
throw new Error("WhileRule.name can't be overwritten.");
}

get params(): string {
return this.whileCondition.toString();
}
set params(value: string | number | undefined) {
throw new Error("WhileRule.params can't be overwritten.");
}

/** The expresison whose value is emitted when the while rule is executed. */
get whileCondition(): Expression {
return this._whileCondition!;
}
set whileCondition(whileCondition: Expression | ExpressionProps) {
if (this._whileCondition) this._whileCondition.parent = undefined;
if (!('sassType' in whileCondition)) {
whileCondition = fromProps(whileCondition);
}
if (whileCondition) whileCondition.parent = this;
this._whileCondition = whileCondition;
}
private _whileCondition?: Expression;

constructor(defaults: WhileRuleProps);
/** @hidden */
constructor(_: undefined, inner: sassInternal.WhileRule);
constructor(defaults?: WhileRuleProps, inner?: sassInternal.WhileRule) {
super(defaults as unknown as postcss.AtRuleProps);
this.nodes ??= [];

if (inner) {
this.source = new LazySource(inner);
this.whileCondition = convertExpression(inner.condition);
appendInternalChildren(this, inner.children);
}
}

clone(overrides?: Partial<WhileRuleProps>): this {
return utils.cloneNode(this, overrides, ['raws', 'whileCondition']);
}

toJSON(): object;
/** @hidden */
toJSON(_: string, inputs: Map<postcss.Input, number>): object;
toJSON(_?: string, inputs?: Map<postcss.Input, number>): object {
return utils.toJSON(
this,
['name', 'whileCondition', 'params', 'nodes'],
inputs,
);
}

/** @hidden */
toString(
stringifier: postcss.Stringifier | postcss.Syntax = sassParser.scss
.stringify,
): string {
return super.toString(stringifier);
}

/** @hidden */
get nonStatementChildren(): ReadonlyArray<Expression> {
return [this.whileCondition];
}

/** @hidden */
normalize(node: NewNode, sample?: postcss.Node): ChildNode[] {
return normalize(this, node, sample);
}
}

interceptIsClean(WhileRule);
17 changes: 10 additions & 7 deletions pkg/sass-parser/lib/src/stringifier.ts
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ import {Rule} from './statement/rule';
import {SassComment} from './statement/sass-comment';
import {UseRule} from './statement/use-rule';
import {WarnRule} from './statement/warn-rule';
import {WhileRule} from './statement/while-rule';

const PostCssStringifier = require('postcss/lib/stringifier');

@@ -165,18 +166,20 @@ export class Stringifier extends PostCssStringifier {
this.sassAtRule(node, semicolon);
}

private ['while-rule'](node: WhileRule): void {
this.sassAtRule(node);
}

/** Helper method for non-generic Sass at-rules. */
private sassAtRule(node: postcss.AtRule, semicolon?: boolean): void {
const start =
'@' +
node.name +
(node.raws.afterName ?? ' ') +
node.params +
(node.raws.between ?? '');
const start = '@' + node.name + (node.raws.afterName ?? ' ') + node.params;
if (node.nodes) {
this.block(node, start);
} else {
this.builder(start + (semicolon ? ';' : ''), node);
this.builder(
start + (node.raws.between ?? '') + (semicolon ? ';' : ''),
node,
);
}
}
}

0 comments on commit a9254df

Please sign in to comment.