Skip to content

Commit 768a60a

Browse files
committed
Support improvements to the new JS API
1 parent ea074cd commit 768a60a

File tree

6 files changed

+198
-226
lines changed

6 files changed

+198
-226
lines changed

lib/src/value/color.ts

Lines changed: 108 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ interface HslColor {
2020
alpha: number;
2121
}
2222

23+
interface HwbColor {
24+
hue: number;
25+
whiteness: number;
26+
blackness: number;
27+
alpha: number;
28+
}
29+
2330
/** A SassScript color. */
2431
export class SassColor extends Value {
2532
private redInternal?: number;
@@ -30,102 +37,62 @@ export class SassColor extends Value {
3037
private lightnessInternal?: number;
3138
private readonly alphaInternal: number;
3239

33-
private constructor(color: RgbColor | HslColor) {
40+
constructor(color: RgbColor | HslColor | HwbColor) {
3441
super();
3542

3643
if ('red' in color) {
37-
this.redInternal = color.red;
38-
this.greenInternal = color.green;
39-
this.blueInternal = color.blue;
44+
this.redInternal = fuzzyAssertInRange(color.red, 0, 255, 'red');
45+
this.greenInternal = fuzzyAssertInRange(color.green, 0, 255, 'green');
46+
this.blueInternal = fuzzyAssertInRange(color.blue, 0, 255, 'blue');
47+
} else if ('saturation' in color) {
48+
this.hueInternal = color.hue % 360;
49+
this.saturationInternal = fuzzyAssertInRange(
50+
color.saturation,
51+
0,
52+
100,
53+
'saturation'
54+
);
55+
this.lightnessInternal = fuzzyAssertInRange(
56+
color.lightness,
57+
0,
58+
100,
59+
'lightness'
60+
);
4061
} else {
41-
this.hueInternal = color.hue;
42-
this.saturationInternal = color.saturation;
43-
this.lightnessInternal = color.lightness;
62+
// From https://www.w3.org/TR/css-color-4/#hwb-to-rgb
63+
const scaledHue = (color.hue % 360) / 360;
64+
let scaledWhiteness =
65+
fuzzyAssertInRange(color.whiteness, 0, 100, 'whiteness') / 100;
66+
let scaledBlackness =
67+
fuzzyAssertInRange(color.blackness, 0, 100, 'blackness') / 100;
68+
69+
const sum = scaledWhiteness + scaledBlackness;
70+
if (sum > 1) {
71+
scaledWhiteness /= sum;
72+
scaledBlackness /= sum;
73+
}
74+
75+
// Because HWB is (currently) used much less frequently than HSL or RGB, we
76+
// don't cache its values because we expect the memory overhead of doing so
77+
// to outweigh the cost of recalculating it on access. Instead, we eagerly
78+
// convert it to RGB and then convert back if necessary.
79+
this.redInternal = toRgb(
80+
scaledHue + 1 / 3,
81+
scaledWhiteness,
82+
scaledBlackness
83+
);
84+
this.greenInternal = toRgb(scaledHue, scaledWhiteness, scaledBlackness);
85+
this.blueInternal = toRgb(
86+
scaledHue - 1 / 3,
87+
scaledWhiteness,
88+
scaledBlackness
89+
);
4490
}
45-
this.alphaInternal = color.alpha;
46-
}
4791

48-
/**
49-
* Creates an RGB color.
50-
*
51-
* Throws an error if `red`, `green`, and `blue` aren't between `0` and `255`,
52-
* or if `alpha` isn't between `0` and `1`.
53-
*/
54-
static rgb(
55-
red: number,
56-
green: number,
57-
blue: number,
58-
alpha?: number
59-
): SassColor {
60-
return new SassColor({
61-
red: fuzzyAssertInRange(red, 0, 255, 'red'),
62-
green: fuzzyAssertInRange(green, 0, 255, 'green'),
63-
blue: fuzzyAssertInRange(blue, 0, 255, 'blue'),
64-
alpha: alpha === undefined ? 1 : fuzzyAssertInRange(alpha, 0, 1, 'alpha'),
65-
});
66-
}
67-
68-
/**
69-
* Creates an HSL color.
70-
*
71-
* Throws an error if `saturation` or `lightness` aren't between `0` and
72-
* `100`, or if `alpha` isn't between `0` and `1`.
73-
*/
74-
static hsl(
75-
hue: number,
76-
saturation: number,
77-
lightness: number,
78-
alpha?: number
79-
) {
80-
return new SassColor({
81-
hue: hue % 360,
82-
saturation: fuzzyAssertInRange(saturation, 0, 100, 'saturation'),
83-
lightness: fuzzyAssertInRange(lightness, 0, 100, 'lightness'),
84-
alpha: alpha === undefined ? 1 : fuzzyAssertInRange(alpha, 0, 1, 'alpha'),
85-
});
86-
}
87-
88-
/**
89-
* Creates an HWB color.
90-
*
91-
* Throws an error if `whiteness` or `blackness` aren't between `0` and `100`,
92-
* or if `alpha` isn't between `0` and `1`.
93-
*/
94-
static hwb(
95-
hue: number,
96-
whiteness: number,
97-
blackness: number,
98-
alpha?: number
99-
) {
100-
// From https://www.w3.org/TR/css-color-4/#hwb-to-rgb
101-
const scaledHue = (hue % 360) / 360;
102-
let scaledWhiteness =
103-
fuzzyAssertInRange(whiteness, 0, 100, 'whiteness') / 100;
104-
let scaledBlackness =
105-
fuzzyAssertInRange(blackness, 0, 100, 'blackness') / 100;
106-
107-
const sum = scaledWhiteness + scaledBlackness;
108-
if (sum > 1) {
109-
scaledWhiteness /= sum;
110-
scaledBlackness /= sum;
111-
}
112-
113-
const factor = 1 - scaledWhiteness - scaledBlackness;
114-
function toRgb(hue: number) {
115-
const channel = hueToRgb(0, 1, hue) * factor + scaledWhiteness;
116-
return fuzzyRound(channel * 255);
117-
}
118-
119-
// Because HWB is (currently) used much less frequently than HSL or RGB, we
120-
// don't cache its values because we expect the memory overhead of doing so
121-
// to outweigh the cost of recalculating it on access. Instead, we eagerly
122-
// convert it to RGB and then convert back if necessary.
123-
return SassColor.rgb(
124-
toRgb(scaledHue + 1 / 3),
125-
toRgb(scaledHue),
126-
toRgb(scaledHue - 1 / 3),
127-
alpha
128-
);
92+
this.alphaInternal =
93+
color.alpha === undefined
94+
? 1
95+
: fuzzyAssertInRange(color.alpha, 0, 1, 'alpha');
12996
}
13097

13198
/** `this`'s red channel. */
@@ -202,67 +169,51 @@ export class SassColor extends Value {
202169
}
203170

204171
/**
205-
* Returns a copy of `this` with its RGB channels changed to `red`, `green`,
206-
* `blue`, and/or `alpha`.
207-
*/
208-
changeRgb(options: {
209-
red?: number;
210-
green?: number;
211-
blue?: number;
212-
alpha?: number;
213-
}): SassColor {
214-
return SassColor.rgb(
215-
options.red ?? this.red,
216-
options.green ?? this.green,
217-
options.blue ?? this.blue,
218-
options.alpha ?? this.alpha
219-
);
220-
}
221-
222-
/**
223-
* Returns a copy of `this` with its HSL values changed to `hue`,
224-
* `saturation`, `lightness`, and/or `alpha`.
225-
*/
226-
changeHsl(options: {
227-
hue?: number;
228-
saturation?: number;
229-
lightness?: number;
230-
alpha?: number;
231-
}): SassColor {
232-
return SassColor.hsl(
233-
options.hue ?? this.hue,
234-
options.saturation ?? this.saturation,
235-
options.lightness ?? this.lightness,
236-
options.alpha ?? this.alpha
237-
);
238-
}
239-
240-
/**
241-
* Returns a copy of `this` with its HWB values changed to `hue`, `whiteness`,
242-
* `blackness`, and/or `alpha`.
172+
* Returns a copy of `this` with its channels changed to match `color`.
243173
*/
244-
changeHwb(options: {
245-
hue?: number;
246-
whiteness?: number;
247-
blackness?: number;
248-
alpha?: number;
249-
}): SassColor {
250-
return SassColor.hwb(
251-
options.hue ?? this.hue,
252-
options.whiteness ?? this.whiteness,
253-
options.blackness ?? this.blackness,
254-
options.alpha ?? this.alpha
255-
);
256-
}
257-
258-
/** Returns a copy of `this` with its alpha channel changed to `alpha`. */
259-
changeAlpha(alpha: number): SassColor {
260-
return new SassColor({
261-
red: this.red,
262-
green: this.green,
263-
blue: this.blue,
264-
alpha: fuzzyAssertInRange(alpha, 0, 1, 'alpha'),
265-
});
174+
change(
175+
color: Partial<RgbColor> | Partial<HslColor> | Partial<HwbColor>
176+
): SassColor {
177+
if ('whiteness' in color || 'blackness' in color) {
178+
return new SassColor({
179+
hue: color.hue ?? this.hue,
180+
whiteness: color.whiteness ?? this.whiteness,
181+
blackness: color.blackness ?? this.blackness,
182+
alpha: color.alpha ?? this.alpha,
183+
});
184+
} else if (
185+
'hue' in color ||
186+
'saturation' in color ||
187+
'lightness' in color
188+
) {
189+
// Tell TypeScript this isn't a Partial<HwbColor>.
190+
const hsl = color as Partial<HslColor>;
191+
return new SassColor({
192+
hue: hsl.hue ?? this.hue,
193+
saturation: hsl.saturation ?? this.saturation,
194+
lightness: hsl.lightness ?? this.lightness,
195+
alpha: hsl.alpha ?? this.alpha,
196+
});
197+
} else if (
198+
'red' in color ||
199+
'green' in color ||
200+
'blue' in color ||
201+
this.redInternal
202+
) {
203+
return new SassColor({
204+
red: color.red ?? this.red,
205+
green: color.green ?? this.green,
206+
blue: color.blue ?? this.blue,
207+
alpha: color.alpha ?? this.alpha,
208+
});
209+
} else {
210+
return new SassColor({
211+
hue: this.hue,
212+
saturation: this.saturation,
213+
lightness: this.lightness,
214+
alpha: color.alpha ?? this.alpha,
215+
});
216+
}
266217
}
267218

268219
equals(other: Value): boolean {
@@ -344,6 +295,13 @@ export class SassColor extends Value {
344295
}
345296
}
346297

298+
// A helper for converting HWB colors to RGB.
299+
function toRgb(hue: number, scaledWhiteness: number, scaledBlackness: number) {
300+
const factor = 1 - scaledWhiteness - scaledBlackness;
301+
const channel = hueToRgb(0, 1, hue) * factor + scaledWhiteness;
302+
return fuzzyRound(channel * 255);
303+
}
304+
347305
// An algorithm from the CSS3 spec: http://www.w3.org/TR/css3-color/#hsl-color.
348306
function hueToRgb(m1: number, m2: number, hue: number): number {
349307
if (hue < 0) hue += 1;

lib/src/value/list.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5-
import {hash, List, OrderedMap} from 'immutable';
5+
import {hash, isList, List} from 'immutable';
66

77
import {Value} from './value';
88
import {SassMap} from './map';
@@ -15,6 +15,12 @@ export type ListSeparator = ',' | '/' | ' ' | null;
1515
// the value.
1616
const emptyListHashCode = hash([]);
1717

18+
/** The options that are passed to the constructor. */
19+
interface ConstructorOptions {
20+
separator?: ListSeparator;
21+
brackets?: boolean;
22+
}
23+
1824
/** A SassScript list. */
1925
export class SassList extends Value {
2026
private readonly contentsInternal: List<Value>;
@@ -26,14 +32,18 @@ export class SassList extends Value {
2632
* `brackets`.
2733
*/
2834
constructor(
29-
contents: Value[] | List<Value>,
30-
options?: {
31-
/** @default ',' */ separator?: ListSeparator;
32-
/** @default false */ brackets?: boolean;
33-
}
35+
contentsOrOptions?: Value[] | List<Value> | ConstructorOptions,
36+
options?: ConstructorOptions
3437
) {
3538
super();
36-
this.contentsInternal = asImmutableList(contents);
39+
40+
if (isList(contentsOrOptions) || Array.isArray(contentsOrOptions)) {
41+
this.contentsInternal = asImmutableList(contentsOrOptions);
42+
} else {
43+
this.contentsInternal = List();
44+
options = contentsOrOptions;
45+
}
46+
3747
if (this.contentsInternal.size > 1 && options?.separator === null) {
3848
throw Error(
3949
'Non-null separator required for SassList with more than one element.'
@@ -44,17 +54,6 @@ export class SassList extends Value {
4454
this.hasBracketsInternal = options?.brackets ?? false;
4555
}
4656

47-
/** Returns an empty list with the given `separator` and `brackets`. */
48-
static empty(options?: {
49-
/** @default null */ separator?: ListSeparator;
50-
/** @default false */ brackets?: boolean;
51-
}) {
52-
return new SassList([], {
53-
separator: options?.separator ?? null,
54-
brackets: options?.brackets,
55-
});
56-
}
57-
5857
get asList(): List<Value> {
5958
return this.contentsInternal;
6059
}
@@ -73,12 +72,16 @@ export class SassList extends Value {
7372
return this.contentsInternal.size;
7473
}
7574

75+
get(index: number): Value | undefined {
76+
return this.contentsInternal.get(index);
77+
}
78+
7679
assertList(): SassList {
7780
return this;
7881
}
7982

8083
assertMap(name?: string): SassMap {
81-
if (this.contentsInternal.isEmpty()) return SassMap.empty();
84+
if (this.contentsInternal.isEmpty()) return new SassMap();
8285
throw valueError(`${this} is not a map`, name);
8386
}
8487

@@ -87,8 +90,8 @@ export class SassList extends Value {
8790
*
8891
* Otherwise, returns null.
8992
*/
90-
tryMap(): OrderedMap<Value, Value> | null {
91-
return this.contentsInternal.isEmpty() ? OrderedMap<Value, Value>() : null;
93+
tryMap(): SassMap | null {
94+
return this.contentsInternal.isEmpty() ? new SassMap() : null;
9295
}
9396

9497
equals(other: Value): boolean {

0 commit comments

Comments
 (0)