@terrazzo/token-tools 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,487 @@
1
+ import { type Color, formatCss, clampChroma } from 'culori';
2
+ import { kebabCase } from 'scule';
3
+ import { CSS_TO_CULORI, parseColor } from '../color.js';
4
+
5
+ /** Function that generates a var(…) statement */
6
+ export type IDGenerator = (id: string) => string;
7
+
8
+ export type ColorValue = string | { colorSpace: string; channels: [number, number, number]; alpha: number };
9
+
10
+ export const defaultAliasTransform = (id: string) => `var(${makeCSSVar(id)})`;
11
+
12
+ /** Convert boolean value to CSS string */
13
+ export function transformBooleanValue(
14
+ value: boolean,
15
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf?: string; transformAlias?: IDGenerator } = {},
16
+ ): string {
17
+ if (aliasOf) {
18
+ return transformAlias(aliasOf);
19
+ }
20
+ if (typeof value !== 'boolean') {
21
+ throw new Error(`Expected boolean, received ${typeof value} "${value}"`);
22
+ }
23
+ return value ? '1' : '0';
24
+ }
25
+
26
+ export interface BorderValue {
27
+ width: string;
28
+ color: string | ColorValue;
29
+ style: string | StrokeStyleValue;
30
+ }
31
+
32
+ /** Convert border value to multiple CSS values */
33
+ export function transformBorderValue(
34
+ value: BorderValue,
35
+
36
+ {
37
+ aliasOf,
38
+ partialAliasOf,
39
+ transformAlias = defaultAliasTransform,
40
+ }: {
41
+ aliasOf?: string;
42
+ partialAliasOf?: Partial<Record<keyof typeof value, string>>;
43
+ transformAlias?: IDGenerator;
44
+ } = {},
45
+ ): {
46
+ width: ReturnType<typeof transformDimensionValue>;
47
+ color: ReturnType<typeof transformColorValue>;
48
+ style: ReturnType<typeof transformStrokeStyleValue>;
49
+ } {
50
+ if (aliasOf) {
51
+ return transformCompositeAlias(value, { aliasOf, transformAlias });
52
+ }
53
+ return {
54
+ width: partialAliasOf?.width
55
+ ? transformAlias(partialAliasOf.width)
56
+ : transformDimensionValue(value.width, { transformAlias }),
57
+ color: partialAliasOf?.color
58
+ ? transformAlias(partialAliasOf.color)
59
+ : transformColorValue(value.color, { transformAlias }),
60
+ style: partialAliasOf?.style
61
+ ? transformAlias(partialAliasOf.style)
62
+ : transformStrokeStyleValue(value.style, { transformAlias }),
63
+ };
64
+ }
65
+
66
+ /** Convert color value to CSS string */
67
+ export function transformColorValue(
68
+ value: string | ColorValue,
69
+ /** (optional) Clamp gamut to `srgb` or `p3` gamut (default: don’t clamp) */
70
+ {
71
+ aliasOf,
72
+ gamut,
73
+ transformAlias = defaultAliasTransform,
74
+ }: {
75
+ aliasOf?: string;
76
+ gamut?: 'srgb' | 'p3';
77
+ transformAlias?: IDGenerator;
78
+ } = {},
79
+ ): string {
80
+ if (aliasOf) {
81
+ return transformAlias(aliasOf);
82
+ }
83
+
84
+ const { colorSpace, channels, alpha } = typeof value === 'string' ? parseColor(value) : value;
85
+ let color = { mode: CSS_TO_CULORI[colorSpace], alpha } as Color;
86
+ switch (color.mode) {
87
+ case 'a98':
88
+ case 'rec2020':
89
+ case 'p3':
90
+ case 'prophoto':
91
+ case 'lrgb':
92
+ case 'rgb': {
93
+ color.r = channels[0];
94
+ color.g = channels[1];
95
+ color.b = channels[2];
96
+ break;
97
+ }
98
+ case 'hsl': {
99
+ color.h = channels[0];
100
+ color.s = channels[1];
101
+ color.l = channels[2];
102
+ break;
103
+ }
104
+ case 'hsv': {
105
+ color.h = channels[0];
106
+ color.s = channels[1];
107
+ color.v = channels[2];
108
+ break;
109
+ }
110
+ case 'hwb': {
111
+ color.h = channels[0];
112
+ color.w = channels[1];
113
+ color.b = channels[2];
114
+ break;
115
+ }
116
+ case 'lab':
117
+ case 'oklab': {
118
+ color.l = channels[0];
119
+ color.a = channels[1];
120
+ color.b = channels[2];
121
+ break;
122
+ }
123
+ case 'lch':
124
+ case 'oklch': {
125
+ color.l = channels[0];
126
+ color.c = channels[1];
127
+ color.h = channels[2];
128
+ break;
129
+ }
130
+ case 'xyz50':
131
+ case 'xyz65': {
132
+ color.x = channels[0];
133
+ color.y = channels[1];
134
+ color.z = channels[2];
135
+ break;
136
+ }
137
+ }
138
+ if (gamut === 'srgb') {
139
+ color = clampChroma(color, color.mode, 'rgb');
140
+ } else if (gamut === 'p3') {
141
+ color = clampChroma(color, color.mode, 'p3');
142
+ }
143
+ return formatCss(color);
144
+ }
145
+
146
+ export type CubicBézierValue = [string | number, string | number, string | number, string | number];
147
+
148
+ /** Convert cubicBezier value to CSS */
149
+ export function transformCubicBezierValue(
150
+ value: CubicBézierValue,
151
+ {
152
+ aliasOf,
153
+ partialAliasOf,
154
+ transformAlias = defaultAliasTransform,
155
+ }: {
156
+ aliasOf?: string;
157
+ partialAliasOf?: [string | undefined, string | undefined, string | undefined, string | undefined];
158
+ transformAlias?: IDGenerator;
159
+ } = {},
160
+ ): string {
161
+ if (aliasOf) {
162
+ return transformAlias(aliasOf);
163
+ }
164
+ return `cubic-bezier(${value
165
+ .map((v, i) => (partialAliasOf?.[i] ? transformAlias(partialAliasOf[i]!) : v))
166
+ .join(', ')})`;
167
+ }
168
+
169
+ /** Build object of alias values */
170
+ export function transformCompositeAlias<T extends {}>(
171
+ value: T,
172
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf: string; transformAlias?: IDGenerator },
173
+ ): Record<keyof T, string> {
174
+ const output: Record<string, string> = {};
175
+ for (const key in value) {
176
+ output[kebabCase(key)] = transformAlias(`${aliasOf}-${key}`);
177
+ }
178
+ return output as Record<keyof T, string>;
179
+ }
180
+
181
+ export type DimensionValue = string;
182
+
183
+ /** Convert dimension value to CSS */
184
+ export function transformDimensionValue(
185
+ value: number | string,
186
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf?: string; transformAlias?: IDGenerator } = {},
187
+ ): string {
188
+ if (aliasOf) {
189
+ return transformAlias(aliasOf);
190
+ }
191
+ if (typeof value === 'number') {
192
+ return value === 0 ? '0' : `${value}px`;
193
+ }
194
+ return value;
195
+ }
196
+
197
+ export type DurationValue = string;
198
+
199
+ /** Convert duration value to CSS */
200
+ export function transformDurationValue(
201
+ value: number | string,
202
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf?: string; transformAlias?: IDGenerator } = {},
203
+ ): string {
204
+ if (aliasOf) {
205
+ return transformAlias(aliasOf);
206
+ }
207
+ if (typeof value === 'number' || String(Number.parseFloat(value)) === value) {
208
+ return `${value}ms`;
209
+ }
210
+ return value;
211
+ }
212
+
213
+ export const FONT_FAMILY_KEYWORDS = new Set([
214
+ 'sans-serif',
215
+ 'serif',
216
+ 'monospace',
217
+ 'system-ui',
218
+ 'ui-monospace',
219
+ '-apple-system',
220
+ ]);
221
+
222
+ export function transformFontFamilyValue(
223
+ value: string | string[],
224
+ {
225
+ aliasOf,
226
+ partialAliasOf,
227
+ transformAlias = defaultAliasTransform,
228
+ }: { aliasOf?: string; partialAliasOf?: string[]; transformAlias?: IDGenerator } = {},
229
+ ): string {
230
+ if (aliasOf) {
231
+ return transformAlias(aliasOf);
232
+ }
233
+ return (typeof value === 'string' ? [value] : value)
234
+ .map((fontName, i) =>
235
+ partialAliasOf?.[i]
236
+ ? transformAlias(partialAliasOf[i]!)
237
+ : FONT_FAMILY_KEYWORDS.has(fontName)
238
+ ? fontName
239
+ : `"${fontName}"`,
240
+ )
241
+ .join(', ');
242
+ }
243
+
244
+ /** Convert fontWeight value to CSS */
245
+ export function transformFontWeightValue(
246
+ value: number | string,
247
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf?: string; transformAlias?: IDGenerator } = {},
248
+ ): string {
249
+ if (aliasOf) {
250
+ return transformAlias(aliasOf);
251
+ }
252
+ return String(value);
253
+ }
254
+
255
+ export interface GradientStop {
256
+ color: ColorValue;
257
+ position: number;
258
+ }
259
+
260
+ /** Convert gradient value to CSS */
261
+ export function transformGradientValue(
262
+ value: GradientStop[],
263
+ {
264
+ aliasOf,
265
+ partialAliasOf,
266
+ transformAlias = defaultAliasTransform,
267
+ }: {
268
+ aliasOf?: string;
269
+ partialAliasOf?: Partial<Record<keyof GradientStop, string>>[];
270
+ transformAlias?: IDGenerator;
271
+ } = {},
272
+ ): string {
273
+ if (aliasOf) {
274
+ return transformAlias(aliasOf);
275
+ }
276
+ return value
277
+ .map(({ color, position }, i) =>
278
+ [
279
+ partialAliasOf?.[i]?.color ? transformAlias(partialAliasOf[i]!.color as string) : transformColorValue(color),
280
+ partialAliasOf?.[i]?.position ? transformAlias(String(partialAliasOf[i]!.position)) : `${100 * position}%`,
281
+ ].join(' '),
282
+ )
283
+ .join(', ');
284
+ }
285
+
286
+ export interface ShadowLayer {
287
+ color: ColorValue;
288
+ offsetX: string;
289
+ offsetY: string;
290
+ blur: string;
291
+ spread: string;
292
+ }
293
+
294
+ /** Convert link value to CSS */
295
+ export function transformLinkValue(
296
+ value: string,
297
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf?: string; transformAlias?: IDGenerator } = {},
298
+ ): string {
299
+ if (aliasOf) {
300
+ return transformAlias(aliasOf);
301
+ }
302
+ return `url("${value}")`;
303
+ }
304
+
305
+ /** Convert number value to CSS */
306
+ export function transformNumberValue(
307
+ value: number,
308
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf?: string; transformAlias?: IDGenerator } = {},
309
+ ): string {
310
+ return aliasOf ? transformAlias(aliasOf) : String(value);
311
+ }
312
+
313
+ /** Convert shadow subvalue to CSS */
314
+ export function transformShadowLayer(
315
+ value: ShadowLayer,
316
+ {
317
+ partialAliasOf,
318
+ transformAlias = defaultAliasTransform,
319
+ }: { partialAliasOf?: Partial<Record<keyof ShadowLayer, string>>; transformAlias?: IDGenerator } = {},
320
+ ): string {
321
+ return [
322
+ partialAliasOf?.offsetX
323
+ ? transformAlias(partialAliasOf.offsetX)
324
+ : transformDimensionValue(value.offsetX, { transformAlias }),
325
+ partialAliasOf?.offsetY
326
+ ? transformAlias(partialAliasOf.offsetY)
327
+ : transformDimensionValue(value.offsetY, { transformAlias }),
328
+ partialAliasOf?.blur
329
+ ? transformAlias(partialAliasOf.blur)
330
+ : transformDimensionValue(value.blur, { transformAlias }),
331
+ partialAliasOf?.spread
332
+ ? transformAlias(partialAliasOf.spread)
333
+ : transformDimensionValue(value.spread, { transformAlias }),
334
+ partialAliasOf?.color ? transformAlias(partialAliasOf.color) : transformColorValue(value.color, { transformAlias }),
335
+ ].join(' ');
336
+ }
337
+
338
+ /** Convert shadow value to CSS */
339
+ export function transformShadowValue(
340
+ value: ShadowLayer[],
341
+ {
342
+ aliasOf,
343
+ partialAliasOf,
344
+ transformAlias = defaultAliasTransform,
345
+ }: {
346
+ aliasOf?: string;
347
+ partialAliasOf?: Partial<Record<keyof ShadowLayer, string>>[];
348
+ transformAlias?: IDGenerator;
349
+ } = {},
350
+ ): string {
351
+ if (aliasOf) {
352
+ return transformAlias(aliasOf);
353
+ }
354
+ return value
355
+ .map((v, i) => transformShadowLayer(v, { partialAliasOf: partialAliasOf?.[i], transformAlias }))
356
+ .join(', ');
357
+ }
358
+
359
+ /** Convert string value to CSS */
360
+ export function transformStringValue(
361
+ value: string | number | boolean,
362
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf?: string; transformAlias?: IDGenerator } = {},
363
+ ): string {
364
+ // this seems like a useless function—because it is—but this is a placeholder
365
+ // that can handle unexpected values in the future should any arise
366
+ return aliasOf ? transformAlias(aliasOf) : String(value);
367
+ }
368
+
369
+ export type StrokeStyleValue =
370
+ | 'dotted'
371
+ | 'dashed'
372
+ | 'solid'
373
+ | 'double'
374
+ | 'groove'
375
+ | 'ridge'
376
+ | 'inset'
377
+ | 'outset'
378
+ | { dashArray: DimensionValue[]; lineCap: string };
379
+
380
+ /** Convert strokeStyle value to CSS */
381
+ export function transformStrokeStyleValue(
382
+ value: string | StrokeStyleValue,
383
+ { aliasOf, transformAlias = defaultAliasTransform }: { aliasOf?: string; transformAlias?: IDGenerator } = {},
384
+ ): string {
385
+ if (aliasOf) {
386
+ return transformAlias(aliasOf);
387
+ }
388
+ return typeof value === 'string' ? value : 'dashed'; // CSS doesn’t have `dash-array`; it’s just "dashed"
389
+ }
390
+
391
+ export interface TransitionValue {
392
+ duration: string;
393
+ delay: string;
394
+ timingFunction: CubicBézierValue;
395
+ }
396
+
397
+ /** Convert transition value to multiple CSS values */
398
+ export function transformTransitionValue(
399
+ value: TransitionValue,
400
+ {
401
+ aliasOf,
402
+ partialAliasOf,
403
+ transformAlias = defaultAliasTransform,
404
+ }: {
405
+ aliasOf?: string;
406
+ partialAliasOf?: Partial<Record<keyof typeof value, string>>;
407
+ transformAlias?: IDGenerator;
408
+ } = {},
409
+ ): {
410
+ duration: ReturnType<typeof transformDurationValue>;
411
+ delay: ReturnType<typeof transformDurationValue>;
412
+ timingFunction: ReturnType<typeof transformCubicBezierValue>;
413
+ } {
414
+ if (aliasOf) {
415
+ return transformCompositeAlias(value, { aliasOf, transformAlias });
416
+ }
417
+ return {
418
+ duration: partialAliasOf?.duration
419
+ ? transformAlias(partialAliasOf.duration)
420
+ : transformDurationValue(value.duration, { transformAlias }),
421
+ delay: partialAliasOf?.delay
422
+ ? transformAlias(partialAliasOf.delay)
423
+ : transformDurationValue(value.delay, { transformAlias }),
424
+ timingFunction: partialAliasOf?.timingFunction
425
+ ? transformAlias(partialAliasOf.timingFunction)
426
+ : transformCubicBezierValue(value.timingFunction, { transformAlias }),
427
+ };
428
+ }
429
+
430
+ /** Convert typography value to multiple CSS values */
431
+ export function transformTypographyValue(
432
+ value: Record<string, string | string[]>,
433
+ {
434
+ aliasOf,
435
+ partialAliasOf,
436
+ transformAlias = defaultAliasTransform,
437
+ }: { aliasOf?: string; partialAliasOf?: Record<keyof typeof value, string>; transformAlias?: IDGenerator } = {},
438
+ ): Record<string, string> {
439
+ const output: Record<string, string> = {};
440
+ if (aliasOf) {
441
+ return transformCompositeAlias(value, { aliasOf, transformAlias });
442
+ }
443
+ for (const [property, subvalue] of Object.entries(value)) {
444
+ let transformedValue: string;
445
+ if (partialAliasOf?.[property]) {
446
+ transformedValue = transformAlias(partialAliasOf[property]!);
447
+ } else {
448
+ switch (property) {
449
+ case 'fontFamily': {
450
+ transformedValue = transformFontFamilyValue(subvalue as string[], { transformAlias });
451
+ break;
452
+ }
453
+ case 'fontSize':
454
+ case 'fontWeight': {
455
+ transformedValue = transformFontWeightValue(subvalue as string, { transformAlias });
456
+ break;
457
+ }
458
+ default: {
459
+ transformedValue = transformStringValue(subvalue as string, { transformAlias });
460
+ break;
461
+ }
462
+ }
463
+ }
464
+ output[kebabCase(property)] = transformedValue;
465
+ }
466
+ return output;
467
+ }
468
+
469
+ const CSS_VAR_RE =
470
+ /(?:(\p{Uppercase_Letter}?\p{Lowercase_Letter}+|\p{Uppercase_Letter}+|\p{Number}+|[\u{80}-\u{10FFFF}]+|_)|.)/u;
471
+
472
+ export interface MakeCSSVarOptions {
473
+ /** Prefix with string */
474
+ prefix?: string;
475
+ /** Wrap with `var(…)` (default: false) */
476
+ wrapVar?: boolean;
477
+ }
478
+
479
+ /**
480
+ * Generate a valid CSS variable from any string
481
+ * Code by @dfrankland
482
+ */
483
+ export function makeCSSVar(name: string, { prefix, wrapVar = false }: MakeCSSVarOptions = {}): string {
484
+ const property = [...(prefix ? [prefix] : []), ...name.split(CSS_VAR_RE).filter(Boolean)].join('-');
485
+ const finalProperty = `--${property}`.toLocaleLowerCase();
486
+ return wrapVar ? `var(${finalProperty})` : finalProperty;
487
+ }
package/src/id.ts ADDED
@@ -0,0 +1,41 @@
1
+ import wcmatch from 'wildcard-match';
2
+
3
+ const ALIAS_RE = /^\{([^}]+)\}$/;
4
+
5
+ /** Is this token an alias of another? */
6
+ export function isAlias(value: unknown): boolean {
7
+ if (typeof value !== 'string') {
8
+ return false;
9
+ }
10
+ return ALIAS_RE.test(value);
11
+ }
12
+
13
+ /** Match token against globs */
14
+ export function isTokenMatch(tokenID: string, globPatterns: string[]): boolean {
15
+ return wcmatch(globPatterns)(tokenID);
16
+ }
17
+
18
+ /** Make an alias */
19
+ export function makeAlias(input: string): string {
20
+ return input.replace(/^\{?([^}]+)\}?$/, '{$1}');
21
+ }
22
+
23
+ /** Parse an alias */
24
+ export function parseAlias(input: string): { id: string; mode?: string } {
25
+ const match = input.match(ALIAS_RE);
26
+ if (!match) {
27
+ return { id: input };
28
+ }
29
+ const rawID = match[1] ?? match[0];
30
+ const hashI = rawID.indexOf('#');
31
+ return hashI === -1 ? { id: rawID } : { id: rawID.substring(0, hashI), mode: rawID.substring(hashI + 1) };
32
+ }
33
+
34
+ /** split a token ID into a local ID and group ID */
35
+ export function splitID(id: string): { local: string; group?: string } {
36
+ const lastSeparatorI = id.lastIndexOf('.');
37
+ if (lastSeparatorI === -1) {
38
+ return { local: id };
39
+ }
40
+ return { local: id.substring(lastSeparatorI + 1), group: id.substring(0, lastSeparatorI) };
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './color.js';
2
+ export * from './id.js';
3
+ export * from './string.js';
package/src/string.ts ADDED
@@ -0,0 +1,27 @@
1
+ /** Pad string lengths */
2
+ export function padStr(input: string, length: number, alignment: 'left' | 'center' | 'right' = 'left'): string {
3
+ const d =
4
+ Math.min(length || 0, 1000) - // guard against NaNs and Infinity
5
+ input.length;
6
+ if (d > 0) {
7
+ switch (alignment) {
8
+ case 'left': {
9
+ return `${input}${' '.repeat(d)}`;
10
+ }
11
+ case 'right': {
12
+ return `${' '.repeat(d)}${input}`;
13
+ }
14
+ case 'center': {
15
+ const left = Math.floor(d / 2);
16
+ const right = d - left;
17
+ return `${' '.repeat(left)}${input}${' '.repeat(right)}`;
18
+ }
19
+ }
20
+ }
21
+ return input;
22
+ }
23
+
24
+ /** Pluralize strings */
25
+ export function pluralize<T = string>(count: number, singular: T, plural: T): T {
26
+ return count === 1 ? singular : plural;
27
+ }