Skip to content

Commit

Permalink
Add BooleanExpression and NumberExpression to sass-parser (#2376)
Browse files Browse the repository at this point in the history
* Add boolean expressions to the Sass parser
* Add NumberExpression to sass-parser
 * Add "raw" and "value" to raws. The raws.value is compared with .value. If they are the same, then raws.raw is used when stringifying
* Add contributing instructions for sass-parser
  • Loading branch information
Goodwine authored Oct 18, 2024
1 parent 76cfd6b commit 84e281e
Show file tree
Hide file tree
Showing 18 changed files with 627 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.80.4

* No user-visible changes.

## 1.80.3

* Fix a bug where `@import url("...")` would crash in plain CSS files.
Expand Down
4 changes: 4 additions & 0 deletions pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.4.1

* Add `BooleanExpression` and `NumberExpression`.

## 0.4.0

* **Breaking change:** Warnings are no longer emitted during parsing, so the
Expand Down
14 changes: 14 additions & 0 deletions pkg/sass-parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,17 @@ There are a few cases where an operation that's valid in PostCSS won't work with

* Trying to add child nodes to a Sass statement that doesn't support children
like `@use` or `@error` is not supported.

## Contributing

Before sending out a pull request, please run the following commands from the
`pkg/sass-parser` directory:

* `npm run check` - Runs `eslint`, and then tries to compile the package with
`tsc`.

* `npm run test` - Runs all the tests in the package.

Note: You should run `dart run grinder before-test` from the `dart-sass`
directory beforehand to ensure you're running `sass-parser` against the latest
version of `dart-sass` JavaScript API.
11 changes: 10 additions & 1 deletion pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// https://opensource.org/licenses/MIT.

import * as postcss from 'postcss';
import * as sassApi from 'sass';

import {Root} from './src/statement/root';
import * as sassInternal from './src/sass-internal';
Expand All @@ -27,6 +26,16 @@ export {
StringExpressionProps,
StringExpressionRaws,
} from './src/expression/string';
export {
BooleanExpression,
BooleanExpressionProps,
BooleanExpressionRaws,
} from './src/expression/boolean';
export {
NumberExpression,
NumberExpressionProps,
NumberExpressionRaws,
} from './src/expression/number';
export {
Interpolation,
InterpolationProps,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a boolean expression toJSON 1`] = `
{
"inputs": [
{
"css": "@#{true}",
"hasBOM": false,
"id": "<input css _____>",
},
],
"raws": {},
"sassType": "boolean",
"source": <1:4-1:8 in 0>,
"value": true,
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a number expression toJSON 1`] = `
{
"inputs": [
{
"css": "@#{123%}",
"hasBOM": false,
"id": "<input css _____>",
},
],
"raws": {},
"sassType": "number",
"source": <1:4-1:8 in 0>,
"unit": "%",
"value": 123,
}
`;
122 changes: 122 additions & 0 deletions pkg/sass-parser/lib/src/expression/boolean.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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 {BooleanExpression} from '../..';
import * as utils from '../../../test/utils';

describe('a boolean expression', () => {
let node: BooleanExpression;

describe('true', () => {
function describeNode(
description: string,
create: () => BooleanExpression
): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has sassType boolean', () => expect(node.sassType).toBe('boolean'));

it('is true', () => expect(node.value).toBe(true));
});
}

describeNode('parsed', () => utils.parseExpression('true'));

describeNode(
'constructed manually',
() => new BooleanExpression({value: true})
);

describeNode('constructed from ExpressionProps', () =>
utils.fromExpressionProps({value: true})
);
});

describe('false', () => {
function describeNode(
description: string,
create: () => BooleanExpression
): void {
describe(description, () => {
beforeEach(() => void (node = create()));

it('has sassType boolean', () => expect(node.sassType).toBe('boolean'));

it('is false', () => expect(node.value).toBe(false));
});
}

describeNode('parsed', () => utils.parseExpression('false'));

describeNode(
'constructed manually',
() => new BooleanExpression({value: false})
);

describeNode('constructed from ExpressionProps', () =>
utils.fromExpressionProps({value: false})
);
});

it('assigned new value', () => {
node = utils.parseExpression('true');
node.value = false;
expect(node.value).toBe(false);
});

describe('stringifies', () => {
it('true', () => {
expect(utils.parseExpression('true').toString()).toBe('true');
});

it('false', () => {
expect(utils.parseExpression('false').toString()).toBe('false');
});
});

describe('clone', () => {
let original: BooleanExpression;

beforeEach(() => {
original = utils.parseExpression('true');
});

describe('with no overrides', () => {
let clone: BooleanExpression;

beforeEach(() => void (clone = original.clone()));

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

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

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

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

describe('overrides', () => {
describe('value', () => {
it('defined', () =>
expect(original.clone({value: false}).value).toBe(false));

it('undefined', () =>
expect(original.clone({value: undefined}).value).toBe(true));
});

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

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

it('toJSON', () => expect(utils.parseExpression('true')).toMatchSnapshot());
});
82 changes: 82 additions & 0 deletions pkg/sass-parser/lib/src/expression/boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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 {LazySource} from '../lazy-source';
import type * as sassInternal from '../sass-internal';
import * as utils from '../utils';
import {Expression} from '.';

/**
* The initializer properties for {@link BooleanExpression}.
*
* @category Expression
*/
export interface BooleanExpressionProps {
value: boolean;
raws?: BooleanExpressionRaws;
}

/**
* Raws indicating how to precisely serialize a {@link BooleanExpression}.
*
* @category Expression
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- No raws for a boolean expression yet.
export interface BooleanExpressionRaws {}

/**
* An expression representing a boolean literal in Sass.
*
* @category Expression
*/
export class BooleanExpression extends Expression {
readonly sassType = 'boolean' as const;
declare raws: BooleanExpressionRaws;

/** The boolean value of this expression. */
get value(): boolean {
return this._value;
}
set value(value: boolean) {
// TODO - postcss/postcss#1957: Mark this as dirty
this._value = value;
}
private _value!: boolean;

constructor(defaults: BooleanExpressionProps);
/** @hidden */
constructor(_: undefined, inner: sassInternal.BooleanExpression);
constructor(defaults?: object, inner?: sassInternal.BooleanExpression) {
super(defaults);
if (inner) {
this.source = new LazySource(inner);
this.value = inner.value;
} else {
this.value ??= false;
}
}

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

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

/** @hidden */
toString(): string {
return this.value ? 'true' : 'false';
}

/** @hidden */
get nonStatementChildren(): ReadonlyArray<Expression> {
return [];
}
}
4 changes: 4 additions & 0 deletions pkg/sass-parser/lib/src/expression/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import * as sassInternal from '../sass-internal';
import {BinaryOperationExpression} from './binary-operation';
import {StringExpression} from './string';
import {Expression} from '.';
import {BooleanExpression} from './boolean';
import {NumberExpression} from './number';

/** The visitor to use to convert internal Sass nodes to JS. */
const visitor = sassInternal.createExpressionVisitor<Expression>({
visitBinaryOperationExpression: inner =>
new BinaryOperationExpression(undefined, inner),
visitStringExpression: inner => new StringExpression(undefined, inner),
visitBooleanExpression: inner => new BooleanExpression(undefined, inner),
visitNumberExpression: inner => new NumberExpression(undefined, inner),
});

/** Converts an internal expression AST node into an external one. */
Expand Down
7 changes: 7 additions & 0 deletions pkg/sass-parser/lib/src/expression/from-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@
import {BinaryOperationExpression} from './binary-operation';
import {Expression, ExpressionProps} from '.';
import {StringExpression} from './string';
import {BooleanExpression} from './boolean';
import {NumberExpression} from './number';

/** Constructs an expression from {@link ExpressionProps}. */
export function fromProps(props: ExpressionProps): Expression {
if ('text' in props) return new StringExpression(props);
if ('left' in props) return new BinaryOperationExpression(props);
if ('value' in props) {
if (typeof props.value === 'boolean') return new BooleanExpression(props);
if (typeof props.value === 'number') return new NumberExpression(props);
}

throw new Error(`Unknown node type: ${props}`);
}
18 changes: 15 additions & 3 deletions pkg/sass-parser/lib/src/expression/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,31 @@ import type {
BinaryOperationExpression,
BinaryOperationExpressionProps,
} from './binary-operation';
import {BooleanExpression, BooleanExpressionProps} from './boolean';
import {NumberExpression, NumberExpressionProps} from './number';
import type {StringExpression, StringExpressionProps} from './string';

/**
* The union type of all Sass expressions.
*
* @category Expression
*/
export type AnyExpression = BinaryOperationExpression | StringExpression;
export type AnyExpression =
| BinaryOperationExpression
| StringExpression
| BooleanExpression
| NumberExpression;

/**
* Sass expression types.
*
* @category Expression
*/
export type ExpressionType = 'binary-operation' | 'string';
export type ExpressionType =
| 'binary-operation'
| 'string'
| 'boolean'
| 'number';

/**
* The union type of all properties that can be used to construct Sass
Expand All @@ -31,7 +41,9 @@ export type ExpressionType = 'binary-operation' | 'string';
*/
export type ExpressionProps =
| BinaryOperationExpressionProps
| StringExpressionProps;
| StringExpressionProps
| BooleanExpressionProps
| NumberExpressionProps;

/**
* The superclass of Sass expression nodes.
Expand Down
Loading

0 comments on commit 84e281e

Please sign in to comment.