Skip to content

Commit 95ed807

Browse files
committed
Add NumberExpression to sass-parser
1 parent 2825244 commit 95ed807

File tree

9 files changed

+327
-7
lines changed

9 files changed

+327
-7
lines changed

pkg/sass-parser/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,9 +261,9 @@ There are a few cases where an operation that's valid in PostCSS won't work with
261261
## Contributing
262262

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

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

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

pkg/sass-parser/lib/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ export {
3232
BooleanExpressionProps,
3333
BooleanExpressionRaws,
3434
} from './src/expression/boolean';
35+
export {
36+
NumberExpression,
37+
NumberExpressionProps,
38+
NumberExpressionRaws,
39+
} from './src/expression/number';
3540
export {
3641
Interpolation,
3742
InterpolationProps,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a number expression toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@#{123%}",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"raws": {},
13+
"sassType": "number",
14+
"source": <1:4-1:8 in 0>,
15+
"unit": "%",
16+
"value": 123,
17+
}
18+
`;

pkg/sass-parser/lib/src/expression/convert.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import {BinaryOperationExpression} from './binary-operation';
88
import {StringExpression} from './string';
99
import {Expression} from '.';
1010
import {BooleanExpression} from './boolean';
11+
import {NumberExpression} from './number';
1112

1213
/** The visitor to use to convert internal Sass nodes to JS. */
1314
const visitor = sassInternal.createExpressionVisitor<Expression>({
1415
visitBinaryOperationExpression: inner =>
1516
new BinaryOperationExpression(undefined, inner),
1617
visitStringExpression: inner => new StringExpression(undefined, inner),
1718
visitBooleanExpression: inner => new BooleanExpression(undefined, inner),
19+
visitNumberExpression: inner => new NumberExpression(undefined, inner),
1820
});
1921

2022
/** Converts an internal expression AST node into an external one. */

pkg/sass-parser/lib/src/expression/from-props.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import {BinaryOperationExpression} from './binary-operation';
66
import {Expression, ExpressionProps} from '.';
77
import {StringExpression} from './string';
88
import {BooleanExpression} from './boolean';
9+
import {NumberExpression} from './number';
910

1011
/** Constructs an expression from {@link ExpressionProps}. */
1112
export function fromProps(props: ExpressionProps): Expression {
1213
if ('text' in props) return new StringExpression(props);
1314
if ('left' in props) return new BinaryOperationExpression(props);
14-
if ('value' in props) return new BooleanExpression(props);
15+
if ('value' in props) {
16+
if (typeof props.value === 'boolean') return new BooleanExpression(props);
17+
if (typeof props.value === 'number') return new NumberExpression(props);
18+
}
19+
1520
throw new Error(`Unknown node type: ${props}`);
1621
}

pkg/sass-parser/lib/src/expression/index.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import type {
77
BinaryOperationExpression,
88
BinaryOperationExpressionProps,
99
} from './binary-operation';
10-
import {BooleanExpressionProps} from './boolean';
10+
import {BooleanExpression, BooleanExpressionProps} from './boolean';
11+
import {NumberExpression, NumberExpressionProps} from './number';
1112
import type {StringExpression, StringExpressionProps} from './string';
1213

1314
/**
@@ -18,14 +19,19 @@ import type {StringExpression, StringExpressionProps} from './string';
1819
export type AnyExpression =
1920
| BinaryOperationExpression
2021
| StringExpression
21-
| BooleanExpressionProps;
22+
| BooleanExpression
23+
| NumberExpression;
2224

2325
/**
2426
* Sass expression types.
2527
*
2628
* @category Expression
2729
*/
28-
export type ExpressionType = 'binary-operation' | 'string' | 'boolean';
30+
export type ExpressionType =
31+
| 'binary-operation'
32+
| 'string'
33+
| 'boolean'
34+
| 'number';
2935

3036
/**
3137
* The union type of all properties that can be used to construct Sass
@@ -36,7 +42,8 @@ export type ExpressionType = 'binary-operation' | 'string' | 'boolean';
3642
export type ExpressionProps =
3743
| BinaryOperationExpressionProps
3844
| StringExpressionProps
39-
| BooleanExpressionProps;
45+
| BooleanExpressionProps
46+
| NumberExpressionProps;
4047

4148
/**
4249
* The superclass of Sass expression nodes.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright 2024 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import {NumberExpression} from '../..';
6+
import * as utils from '../../../test/utils';
7+
8+
describe('a number expression', () => {
9+
let node: NumberExpression;
10+
11+
describe('unitless', () => {
12+
function describeNode(
13+
description: string,
14+
create: () => NumberExpression
15+
): void {
16+
describe(description, () => {
17+
beforeEach(() => void (node = create()));
18+
19+
it('has sassType number', () => expect(node.sassType).toBe('number'));
20+
21+
it('is a number', () => expect(node.value).toBe(123));
22+
23+
it('has no unit', () => expect(node.unit).toBeNull());
24+
});
25+
}
26+
27+
describeNode('parsed', () => utils.parseExpression('123'));
28+
29+
describeNode(
30+
'constructed manually',
31+
() =>
32+
new NumberExpression({
33+
value: 123,
34+
})
35+
);
36+
37+
describeNode('constructed from ExpressionProps', () =>
38+
utils.fromExpressionProps({
39+
value: 123,
40+
})
41+
);
42+
});
43+
44+
describe('with a unit', () => {
45+
function describeNode(
46+
description: string,
47+
create: () => NumberExpression
48+
): void {
49+
describe(description, () => {
50+
beforeEach(() => void (node = create()));
51+
52+
it('has sassType number', () => expect(node.sassType).toBe('number'));
53+
54+
it('is a number', () => expect(node.value).toBe(123));
55+
56+
it('has a unit', () => expect(node.unit).toBe('px'));
57+
});
58+
}
59+
60+
describeNode('parsed', () => utils.parseExpression('123px'));
61+
62+
describeNode(
63+
'constructed manually',
64+
() =>
65+
new NumberExpression({
66+
value: 123,
67+
unit: 'px',
68+
})
69+
);
70+
71+
describeNode('constructed from ExpressionProps', () =>
72+
utils.fromExpressionProps({
73+
value: 123,
74+
unit: 'px',
75+
})
76+
);
77+
});
78+
79+
describe('floating-point number', () => {
80+
describe('unitless', () => {
81+
beforeEach(() => void (node = utils.parseExpression('3.14')));
82+
83+
it('value', () => expect(node.value).toBe(3.14));
84+
85+
it('unit', () => expect(node.unit).toBeNull());
86+
});
87+
88+
describe('with a unit', () => {
89+
beforeEach(() => void (node = utils.parseExpression('1.618px')));
90+
91+
it('value', () => expect(node.value).toBe(1.618));
92+
93+
it('unit', () => expect(node.unit).toBe('px'));
94+
});
95+
});
96+
97+
describe('assigned new', () => {
98+
beforeEach(() => void (node = utils.parseExpression('123')));
99+
100+
it('value', () => {
101+
node.value = 456;
102+
expect(node.value).toBe(456);
103+
});
104+
105+
it('unit', () => {
106+
node.unit = 'px';
107+
expect(node.unit).toBe('px');
108+
});
109+
});
110+
111+
describe('stringifies', () => {
112+
it('unitless', () => {
113+
expect(utils.parseExpression('123').toString()).toBe('123');
114+
});
115+
116+
it('with a unit', () => {
117+
expect(utils.parseExpression('123px').toString()).toBe('123px');
118+
});
119+
});
120+
121+
describe('clone', () => {
122+
let original: NumberExpression;
123+
124+
beforeEach(() => {
125+
original = utils.parseExpression('123');
126+
});
127+
128+
describe('with no overrides', () => {
129+
let clone: NumberExpression;
130+
131+
beforeEach(() => void (clone = original.clone()));
132+
133+
describe('has the same properties:', () => {
134+
it('value', () => expect(clone.value).toBe(123));
135+
136+
it('unit', () => expect(clone.unit).toBeNull());
137+
138+
it('raws', () => expect(clone.raws).toEqual({}));
139+
140+
it('source', () => expect(clone.source).toBe(original.source));
141+
});
142+
143+
describe('creates a new', () => {
144+
it('self', () => expect(clone).not.toBe(original));
145+
});
146+
});
147+
148+
describe('overrides', () => {
149+
describe('value', () => {
150+
it('defined', () =>
151+
expect(original.clone({value: 123}).value).toBe(123));
152+
153+
it('undefined', () =>
154+
expect(original.clone({value: undefined}).value).toBe(123));
155+
});
156+
157+
describe('unit', () => {
158+
it('defined', () =>
159+
expect(original.clone({unit: 'px'}).unit).toBe('px'));
160+
161+
it('undefined', () =>
162+
expect(original.clone({unit: undefined}).unit).toBeNull());
163+
});
164+
165+
describe('raws', () => {
166+
it('defined', () =>
167+
expect(original.clone({raws: {}}).raws).toEqual({}));
168+
169+
it('undefined', () =>
170+
expect(original.clone({raws: undefined}).raws).toEqual({}));
171+
});
172+
});
173+
});
174+
175+
it('toJSON', () => expect(utils.parseExpression('123%')).toMatchSnapshot());
176+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright 2024 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import * as postcss from 'postcss';
6+
7+
import {LazySource} from '../lazy-source';
8+
import type * as sassInternal from '../sass-internal';
9+
import * as utils from '../utils';
10+
import {Expression} from '.';
11+
12+
/**
13+
* The initializer properties for {@link NumberExpression}.
14+
*
15+
* @category Expression
16+
*/
17+
export interface NumberExpressionProps {
18+
value: number;
19+
unit?: string;
20+
raws?: NumberExpressionRaws;
21+
}
22+
23+
/**
24+
* Raws indicating how to precisely serialize a {@link NumberExpression}.
25+
*
26+
* @category Expression
27+
*/
28+
// eslint-disable-next-line @typescript-eslint/no-empty-interface -- No raws for a number expression yet.
29+
export interface NumberExpressionRaws {}
30+
31+
/**
32+
* An expression representing a number literal in Sass.
33+
*
34+
* @category Expression
35+
*/
36+
export class NumberExpression extends Expression {
37+
readonly sassType = 'number' as const;
38+
declare raws: NumberExpressionRaws;
39+
40+
/** The numeric value of this expression. */
41+
get value(): number {
42+
return this._value;
43+
}
44+
set value(value: number) {
45+
// TODO - postcss/postcss#1957: Mark this as dirty
46+
this._value = value;
47+
}
48+
private _value!: number;
49+
50+
/** The denominator units of this number. */
51+
get unit(): string | null {
52+
return this._unit;
53+
}
54+
set unit(unit: string | null) {
55+
// TODO - postcss/postcss#1957: Mark this as dirty
56+
this._unit = unit;
57+
}
58+
private _unit!: string | null;
59+
60+
/** Whether the number is unitless. */
61+
isUnitless(): boolean {
62+
return this.unit === null;
63+
}
64+
65+
constructor(defaults: NumberExpressionProps);
66+
/** @hidden */
67+
constructor(_: undefined, inner: sassInternal.NumberExpression);
68+
constructor(defaults?: object, inner?: sassInternal.NumberExpression) {
69+
super(defaults);
70+
if (inner) {
71+
this.source = new LazySource(inner);
72+
this.value = inner.value;
73+
this.unit = inner.unit;
74+
} else {
75+
this.value ??= 0;
76+
this.unit ??= null;
77+
}
78+
}
79+
80+
clone(overrides?: Partial<NumberExpressionProps>): this {
81+
return utils.cloneNode(this, overrides, ['raws', 'value', 'unit']);
82+
}
83+
84+
toJSON(): object;
85+
/** @hidden */
86+
toJSON(_: string, inputs: Map<postcss.Input, number>): object;
87+
toJSON(_?: string, inputs?: Map<postcss.Input, number>): object {
88+
return utils.toJSON(this, ['value', 'unit'], inputs);
89+
}
90+
91+
/** @hidden */
92+
toString(): string {
93+
return this.value + (this.unit ?? '');
94+
}
95+
96+
/** @hidden */
97+
get nonStatementChildren(): ReadonlyArray<Expression> {
98+
return [];
99+
}
100+
}

0 commit comments

Comments
 (0)