Skip to content

Commit

Permalink
Add NumberExpression to sass-parser
Browse files Browse the repository at this point in the history
  • Loading branch information
Goodwine committed Oct 10, 2024
1 parent 2825244 commit 95ed807
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 7 deletions.
4 changes: 2 additions & 2 deletions pkg/sass-parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,9 +261,9 @@ There are a few cases where an operation that's valid in PostCSS won't work with
## Contributing
Before sending out a pull request, please run the following commands from the
`sass-parser` directory:
`pkg/sass-parser` directory:
* `npm run check` - Runs `eslint`, and then tries to compile the package with
`tsc`.
* `npm run test` - Runs all tests in the package.
* `npm run test` - Runs all the tests in the package.
5 changes: 5 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export {
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,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,
}
`;
2 changes: 2 additions & 0 deletions pkg/sass-parser/lib/src/expression/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ 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: 6 additions & 1 deletion pkg/sass-parser/lib/src/expression/from-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ 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) return new BooleanExpression(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}`);
}
15 changes: 11 additions & 4 deletions pkg/sass-parser/lib/src/expression/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type {
BinaryOperationExpression,
BinaryOperationExpressionProps,
} from './binary-operation';
import {BooleanExpressionProps} from './boolean';
import {BooleanExpression, BooleanExpressionProps} from './boolean';
import {NumberExpression, NumberExpressionProps} from './number';
import type {StringExpression, StringExpressionProps} from './string';

/**
Expand All @@ -18,14 +19,19 @@ import type {StringExpression, StringExpressionProps} from './string';
export type AnyExpression =
| BinaryOperationExpression
| StringExpression
| BooleanExpressionProps;
| BooleanExpression
| NumberExpression;

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

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

/**
* The superclass of Sass expression nodes.
Expand Down
176 changes: 176 additions & 0 deletions pkg/sass-parser/lib/src/expression/number.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// 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 {NumberExpression} from '../..';
import * as utils from '../../../test/utils';

describe('a number expression', () => {
let node: NumberExpression;

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

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

it('is a number', () => expect(node.value).toBe(123));

it('has no unit', () => expect(node.unit).toBeNull());
});
}

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

describeNode(
'constructed manually',
() =>
new NumberExpression({
value: 123,
})
);

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

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

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

it('is a number', () => expect(node.value).toBe(123));

it('has a unit', () => expect(node.unit).toBe('px'));
});
}

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

describeNode(
'constructed manually',
() =>
new NumberExpression({
value: 123,
unit: 'px',
})
);

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

describe('floating-point number', () => {
describe('unitless', () => {
beforeEach(() => void (node = utils.parseExpression('3.14')));

it('value', () => expect(node.value).toBe(3.14));

it('unit', () => expect(node.unit).toBeNull());
});

describe('with a unit', () => {
beforeEach(() => void (node = utils.parseExpression('1.618px')));

it('value', () => expect(node.value).toBe(1.618));

it('unit', () => expect(node.unit).toBe('px'));
});
});

describe('assigned new', () => {
beforeEach(() => void (node = utils.parseExpression('123')));

it('value', () => {
node.value = 456;
expect(node.value).toBe(456);
});

it('unit', () => {
node.unit = 'px';
expect(node.unit).toBe('px');
});
});

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

it('with a unit', () => {
expect(utils.parseExpression('123px').toString()).toBe('123px');
});
});

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

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

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

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

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

it('unit', () => expect(clone.unit).toBeNull());

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

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

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

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

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

describe('unit', () => {
it('defined', () =>
expect(original.clone({unit: 'px'}).unit).toBe('px'));

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

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

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

it('toJSON', () => expect(utils.parseExpression('123%')).toMatchSnapshot());
});
100 changes: 100 additions & 0 deletions pkg/sass-parser/lib/src/expression/number.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 NumberExpression}.
*
* @category Expression
*/
export interface NumberExpressionProps {
value: number;
unit?: string;
raws?: NumberExpressionRaws;
}

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

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

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

/** The denominator units of this number. */
get unit(): string | null {
return this._unit;
}
set unit(unit: string | null) {
// TODO - postcss/postcss#1957: Mark this as dirty
this._unit = unit;
}
private _unit!: string | null;

/** Whether the number is unitless. */
isUnitless(): boolean {
return this.unit === null;
}

constructor(defaults: NumberExpressionProps);
/** @hidden */
constructor(_: undefined, inner: sassInternal.NumberExpression);
constructor(defaults?: object, inner?: sassInternal.NumberExpression) {
super(defaults);
if (inner) {
this.source = new LazySource(inner);
this.value = inner.value;
this.unit = inner.unit;
} else {
this.value ??= 0;
this.unit ??= null;
}
}

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

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

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

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

0 comments on commit 95ed807

Please sign in to comment.