@terrazzo/plugin-css 2.0.0-alpha.6 → 2.0.0-beta.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.
package/dist/index.js CHANGED
@@ -1,59 +1,124 @@
1
- import { kebabCase, validateCustomTransform } from "@terrazzo/token-tools";
2
1
  import { generateShorthand, makeCSSVar, transformCSSValue } from "@terrazzo/token-tools/css";
3
2
  import wcmatch from "wildcard-match";
3
+ import { kebabCase } from "@terrazzo/token-tools";
4
4
 
5
5
  //#region src/lib.ts
6
+ const PLUGIN_NAME = "@terrazzo/plugin-css";
6
7
  const FORMAT_ID = "css";
7
8
  const FILE_PREFIX = `/* -------------------------------------------
8
9
  * Autogenerated by ⛋ Terrazzo. DO NOT EDIT!
9
10
  * ------------------------------------------- */`;
10
- /** Convert CSSRules into a formatted, indented CSS string */
11
- function printRules(rules) {
12
- const output = [];
13
- for (const rule of rules) {
14
- if (!rule.selectors.length || !Object.keys(rule.declarations).length) continue;
15
- const mqSelectors = [];
16
- const joinableSelectors = [];
17
- for (const s of rule.selectors) (s.startsWith("@") ? mqSelectors : joinableSelectors).push(s);
18
- for (const s of mqSelectors) output.push(_printRule({
19
- ...rule,
20
- selectors: [s]
21
- }));
22
- if (joinableSelectors.length) output.push(_printRule({
23
- ...rule,
24
- selectors: joinableSelectors
25
- }));
11
+ /**
12
+ * Convert CSSRules into a formatted, indented CSS string.
13
+ * The reason we’re using this homemade version instead of something like css-tree is:
14
+ *
15
+ * 1. css-tree doesn’t support comments :(
16
+ * 2. we are only generating PARTIALS, not full CSS (the user controls the
17
+ * wrapper). So with a proper AST, we’d be hacking it a little anyway because
18
+ * we never really have a true, valid, finalized document.
19
+ * 3. we want @terrazzo/plugin-css to run in the browser AND be lean (i.e. don’t
20
+ * load Prettier or 25MB of wasm).
21
+ * 4. we only have to deal with a small subset of CSS—this doesn’t have to be robust
22
+ * by any means (even future additions won’t push the limits of the spec).
23
+ */
24
+ function printRules(nodes, { indentChar = " ", indentLv = 0 } = {}) {
25
+ let output = "";
26
+ for (const node of nodes) {
27
+ if (output && node.type === "Rule") output += "\n";
28
+ output += printNode(node, {
29
+ indentChar,
30
+ indentLv
31
+ });
26
32
  }
27
- return output.join("\n\n");
33
+ return output.trim();
28
34
  }
29
- function _printRule(rule) {
30
- const output = [];
31
- const isMediaQuery = rule.selectors.some((s) => s.startsWith("@"));
32
- let indent = "";
33
- if (rule.nestedQuery && isMediaQuery) {
34
- output.push(`${indent}${rule.selectors.join(`,\n${indent}`)} {`);
35
- indent += " ";
36
- output.push(`${indent}${rule.nestedQuery} {`);
37
- } else if (rule.nestedQuery && !isMediaQuery) {
38
- output.push(`${indent}${rule.nestedQuery} {`);
39
- indent += " ";
40
- output.push(`${indent}${rule.selectors.join(`,\n${indent}`)} {`);
41
- } else output.push(`${indent}${rule.selectors.join(`,\n${indent}`)} {`);
42
- indent += " ";
43
- if (isMediaQuery) {
44
- output.push(`${indent}:root {`);
45
- indent += " ";
35
+ /** Internal printer for individual nodes */
36
+ function printNode(node, { indentChar, indentLv }) {
37
+ let output = "";
38
+ const indent = indentChar.repeat(indentLv);
39
+ if (node.type === "Declaration") {
40
+ if (node.comment) output += `${indent}/* ${node.comment} */\n`;
41
+ output += `${indent}${node.property}: ${node.value};\n`;
42
+ return output;
43
+ }
44
+ if (!node.prelude.length || !node.children.length) return output;
45
+ const mediaQueryWithDecls = node.children.some((s) => s.type === "Declaration") ? node.prelude.find((s) => s.startsWith("@")) : void 0;
46
+ if (mediaQueryWithDecls) {
47
+ const nonMedia = node.prelude.filter((s) => s !== mediaQueryWithDecls);
48
+ output += `${indent}${mediaQueryWithDecls} {\n`;
49
+ output += printNode(rule([":root"], node.children), {
50
+ indentChar,
51
+ indentLv: indentLv + 1
52
+ });
53
+ output += `${indent}}\n\n`;
54
+ output += printNode(rule(nonMedia, node.children), {
55
+ indentChar,
56
+ indentLv
57
+ });
58
+ return output;
46
59
  }
47
- for (const [k, d] of Object.entries(rule.declarations)) output.push(`${indent}${k}: ${d.value};${d.description ? ` /* ${d.description} */` : ""}`);
48
- while (indent !== "") {
49
- indent = indent.substring(0, indent.length - 2);
50
- output.push(`${indent}}`);
60
+ let childOutput = "";
61
+ for (const child of node.children) childOutput += printNode(child, {
62
+ indentChar,
63
+ indentLv: indentLv + 1
64
+ });
65
+ childOutput = childOutput.trim();
66
+ if (!childOutput) return output;
67
+ output += `${indent}${node.prelude.join(", ")} {\n`;
68
+ output += `${indentChar.repeat(indentLv + 1)}${childOutput}\n`;
69
+ output += `${indent}}\n`;
70
+ return output;
71
+ }
72
+ /** Infer indentation preferences from a user-defined wrapping method. */
73
+ function getIndentFromPrepare(prepare) {
74
+ const str = "//css//";
75
+ const output = prepare(str).replace(/\/\*.*\*\//g, "");
76
+ let indentChar = " ";
77
+ let indentLv = 0;
78
+ let lineStartChar = 0;
79
+ for (let i = 0; i < output.length; i++) if (output[i] === "{") {
80
+ lineStartChar = i + 1;
81
+ indentLv++;
82
+ } else if (output[i] === "}") indentLv--;
83
+ else if (output[i] === "\n") lineStartChar = i + 1;
84
+ else if (output[i] === str[0] && output.slice(i).startsWith(str)) {
85
+ indentChar = output.slice(lineStartChar, i);
86
+ indentChar = indentChar.slice(0, Math.floor(indentChar.length / indentLv));
87
+ break;
51
88
  }
52
- return output.join("\n");
89
+ return {
90
+ indentChar: indentChar || " ",
91
+ indentLv
92
+ };
93
+ }
94
+ /** Syntactic sugar over Rule boilerplate */
95
+ function rule(prelude, children = []) {
96
+ return {
97
+ type: "Rule",
98
+ prelude,
99
+ children
100
+ };
101
+ }
102
+ /** Syntactic sugar over Declaration boilerplate */
103
+ function decl(property, value, comment) {
104
+ return {
105
+ type: "Declaration",
106
+ property,
107
+ value,
108
+ comment
109
+ };
110
+ }
111
+ /** Does a node list contain a root-level declaration with this property? */
112
+ function hasDecl(list, property) {
113
+ return list.some((d) => d.type === "Declaration" && d.property === property);
114
+ }
115
+ /** Add a declaration only if it’s unique (note: CSS, by design, allows duplication—it’s how fallbacks happen. Only use this if fallbacks aren’t needed. */
116
+ function addDeclUnique(list, declaration) {
117
+ if (!hasDecl(list, declaration.property)) list.push(declaration);
53
118
  }
54
119
 
55
120
  //#endregion
56
- //#region src/build/utility-css.ts
121
+ //#region src/utility-css.ts
57
122
  const GROUP_REGEX = {
58
123
  bg: /(^bg-|-bg-)/,
59
124
  border: /(^border-|-border-)/,
@@ -65,38 +130,39 @@ const GROUP_REGEX = {
65
130
  text: /(^text-|-text-)/
66
131
  };
67
132
  /** Make CSS class name from transformed token */
68
- function makeSelector(token, prefix, subgroup) {
69
- return `.${prefix}${subgroup || ""}-${kebabCase(token.token.id).replace(GROUP_REGEX[prefix], "")}`;
133
+ function makePrelude(token, prefix, subgroup) {
134
+ return [`.${prefix}${subgroup || ""}-${kebabCase(token.token.id).replace(GROUP_REGEX[prefix], "")}`];
70
135
  }
71
136
  function makeVarValue(token) {
72
- return { value: makeCSSVar(token.localID ?? token.token.id, { wrapVar: true }) };
137
+ return makeCSSVar(token.localID ?? token.token.id, { wrapVar: true });
73
138
  }
74
- function generateUtilityCSS(groups, tokens) {
75
- const output = [];
139
+ function generateUtilityCSS(groups, tokens, { logger }) {
140
+ const root = [];
76
141
  const groupEntries = Object.entries(groups);
77
142
  groupEntries.sort((a, b) => a[0].localeCompare(b[0]));
78
143
  for (const [group, selectors] of groupEntries) {
79
144
  const selectorMatcher = wcmatch(selectors);
80
145
  const matchingTokens = tokens.filter((token) => selectorMatcher(token.token.id));
81
146
  if (!matchingTokens.length) {
82
- console.warn(`[@terrazzo/plugin-css] utility group "${group}" matched 0 tokens: ${JSON.stringify(selectors)}`);
147
+ logger.warn({
148
+ group: "plugin",
149
+ label: PLUGIN_NAME,
150
+ message: `utility group "${group}" matched 0 tokens: ${JSON.stringify(selectors)}`
151
+ });
83
152
  break;
84
153
  }
85
154
  switch (group) {
86
155
  case "bg":
87
156
  for (const token of matchingTokens) {
88
- const selector = makeSelector(token, "bg");
157
+ const prelude = makePrelude(token, "bg");
89
158
  switch (token.token.$type) {
90
159
  case "color":
91
- output.push({
92
- selectors: [selector],
93
- declarations: { "background-color": makeVarValue(token) }
94
- });
160
+ root.push(rule(prelude, [decl("background-color", makeVarValue(token))]));
95
161
  break;
96
- case "gradient": output.push({
97
- selectors: [selector],
98
- declarations: { "background-image": { value: `linear-gradient(${makeCSSVar(token.localID ?? token.token.id, { wrapVar: true })})` } }
99
- });
162
+ case "gradient": {
163
+ const value = decl("background-image", `linear-gradient(${makeCSSVar(token.localID ?? token.token.id, { wrapVar: true })})`);
164
+ root.push(rule(prelude, [value]));
165
+ }
100
166
  }
101
167
  }
102
168
  break;
@@ -108,10 +174,7 @@ function generateUtilityCSS(groups, tokens) {
108
174
  dimension: "border-width",
109
175
  strokeStyle: "border-style"
110
176
  }[token.token.$type];
111
- if (property) output.push({
112
- selectors: [makeSelector(token, "border")],
113
- declarations: { [property]: makeVarValue(token) }
114
- });
177
+ if (property) root.push(rule(makePrelude(token, "border"), [decl(property, makeVarValue(token))]));
115
178
  }
116
179
  for (const token of matchingTokens) for (const side of [
117
180
  "top",
@@ -125,200 +188,202 @@ function generateUtilityCSS(groups, tokens) {
125
188
  dimension: `border-${side}-width`,
126
189
  strokeStyle: `border-${side}-style`
127
190
  }[token.token.$type];
128
- if (property) output.push({
129
- selectors: [makeSelector(token, "border", `-${side}`)],
130
- declarations: { [property]: makeVarValue(token) }
131
- });
191
+ if (property) root.push(rule(makePrelude(token, "border", `-${side}`), [decl(property, makeVarValue(token))]));
132
192
  }
133
193
  break;
134
194
  case "font":
135
195
  for (const token of matchingTokens) {
136
- const selector = makeSelector(token, "font");
196
+ const prelude = makePrelude(token, "font");
137
197
  if (token.token.$type === "typography" && token.type === "MULTI_VALUE") {
138
- const declarations = {};
139
- for (const k of Object.keys(token.value)) declarations[k] = { value: makeCSSVar(`${token.localID ?? token.token.id}-${k}`, { wrapVar: true }) };
140
- output.push({
141
- selectors: [selector],
142
- declarations
143
- });
198
+ const value = Object.keys(token.value).map((property) => decl(property, makeCSSVar(`${token.localID ?? token.token.id}-${property}`, { wrapVar: true })));
199
+ root.push(rule(prelude, value));
144
200
  } else {
145
201
  const property = {
146
202
  dimension: "font-size",
147
203
  fontFamily: "font-family",
148
204
  fontWeight: "font-weight"
149
205
  }[token.token.$type];
150
- if (property) output.push({
151
- selectors: [selector],
152
- declarations: { [property]: makeVarValue(token) }
153
- });
206
+ if (property) root.push(rule(prelude, [decl(property, makeVarValue(token))]));
154
207
  }
155
208
  }
156
209
  break;
157
210
  case "layout": {
158
211
  const filteredTokens = matchingTokens.filter((t) => t.token.$type === "dimension");
159
- for (const token of filteredTokens) output.push({
160
- selectors: [makeSelector(token, "gap")],
161
- declarations: { gap: makeVarValue(token) }
162
- });
163
- for (const token of filteredTokens) output.push({
164
- selectors: [makeSelector(token, "gap", "-col")],
165
- declarations: { "column-gap": makeVarValue(token) }
166
- });
167
- for (const token of filteredTokens) output.push({
168
- selectors: [makeSelector(token, "gap", "-row")],
169
- declarations: { "row-gap": makeVarValue(token) }
170
- });
212
+ for (const token of filteredTokens) root.push(rule(makePrelude(token, "gap"), [decl("gap", makeVarValue(token))]));
213
+ for (const token of filteredTokens) root.push(rule(makePrelude(token, "gap", "-col"), [decl("column-gap", makeVarValue(token))]));
214
+ for (const token of filteredTokens) root.push(rule(makePrelude(token, "gap", "-row"), [decl("row-gap", makeVarValue(token))]));
171
215
  for (const prefix of ["m", "p"]) {
172
216
  const property = prefix === "m" ? "margin" : "padding";
173
- for (const token of filteredTokens) output.push({
174
- selectors: [makeSelector(token, prefix, "a")],
175
- declarations: { [property]: makeVarValue(token) }
176
- });
217
+ for (const token of filteredTokens) root.push(rule(makePrelude(token, prefix, "a"), [decl(property, makeVarValue(token))]));
177
218
  for (const token of filteredTokens) {
178
219
  const value = makeVarValue(token);
179
- output.push({
180
- selectors: [makeSelector(token, prefix, "x")],
181
- declarations: {
182
- [`${property}-left`]: value,
183
- [`${property}-right`]: value
184
- }
185
- }, {
186
- selectors: [makeSelector(token, prefix, "y")],
187
- declarations: {
188
- [`${property}-bottom`]: value,
189
- [`${property}-top`]: value
190
- }
191
- });
220
+ root.push(rule(makePrelude(token, prefix, "x"), [
221
+ decl(`${property}-inline`, value),
222
+ decl(`${property}-left`, value),
223
+ decl(`${property}-right`, value)
224
+ ]), rule(makePrelude(token, prefix, "y"), [
225
+ decl(`${property}-block`, value),
226
+ decl(`${property}-bottom`, value),
227
+ decl(`${property}-top`, value)
228
+ ]));
192
229
  }
193
230
  for (const side of [
194
231
  "top",
195
232
  "right",
196
233
  "bottom",
197
234
  "left"
198
- ]) for (const token of filteredTokens) output.push({
199
- selectors: [makeSelector(token, prefix, side[0])],
200
- declarations: { [`${property}-${side}`]: makeVarValue(token) }
201
- });
235
+ ]) for (const token of filteredTokens) root.push(rule(makePrelude(token, prefix, side[0]), [decl(`${property}-${side}`, makeVarValue(token))]));
202
236
  for (const token of filteredTokens) {
203
237
  const value = makeVarValue(token);
204
- output.push({
205
- selectors: [makeSelector(token, prefix, "s")],
206
- declarations: { [`${property}-inline-start`]: value }
207
- }, {
208
- selectors: [makeSelector(token, prefix, "e")],
209
- declarations: { [`${property}-inline-end`]: value }
210
- });
238
+ root.push(rule(makePrelude(token, prefix, "bs"), [decl(`${property}-block-start`, value)]), rule(makePrelude(token, prefix, "be"), [decl(`${property}-block-end`, value)]), rule(makePrelude(token, prefix, "is"), [decl(`${property}-inline-start`, value)]), rule(makePrelude(token, prefix, "ie"), [decl(`${property}-inline-end`, value)]));
211
239
  }
212
240
  }
213
241
  break;
214
242
  }
215
243
  case "shadow":
216
- for (const token of matchingTokens) if (token.token.$type === "shadow") output.push({
217
- selectors: [makeSelector(token, "shadow")],
218
- declarations: { "box-shadow": makeVarValue(token) }
219
- });
244
+ for (const token of matchingTokens) if (token.token.$type === "shadow") root.push(rule(makePrelude(token, "shadow"), [decl("box-shadow", makeVarValue(token))]));
220
245
  break;
221
246
  case "text":
222
247
  for (const token of matchingTokens) {
223
- const selector = makeSelector(token, "text");
248
+ const prelude = makePrelude(token, "text");
224
249
  const value = makeVarValue(token);
225
250
  switch (token.token.$type) {
226
251
  case "color":
227
- output.push({
228
- selectors: [selector],
229
- declarations: { color: value }
230
- });
252
+ root.push(rule(prelude, [decl("color", value)]));
231
253
  break;
232
254
  case "gradient":
233
- output.push({
234
- selectors: [selector],
235
- declarations: {
236
- background: { value: `-webkit-linear-gradient(${value.value})` },
237
- "-webkit-background-clip": { value: "text" },
238
- "-webkit-text-fill-color": { value: "transparent" }
239
- }
240
- });
255
+ root.push(rule(prelude, [
256
+ decl("background", `-webkit-linear-gradient(${value})`),
257
+ decl("-webkit-background-clip", "text"),
258
+ decl("-webkit-text-fill-color", "transparent")
259
+ ]));
241
260
  break;
242
261
  }
243
262
  }
244
263
  break;
245
264
  default:
246
- console.warn(`[@terrazzo/plugin-css] unknown utility CSS group "${group}", ignoring`);
265
+ logger.warn({
266
+ group: "plugin",
267
+ label: PLUGIN_NAME,
268
+ message: `unknown utility CSS group "${group}", ignoring`
269
+ });
247
270
  break;
248
271
  }
249
272
  }
250
- return output;
273
+ return root;
251
274
  }
252
275
 
253
276
  //#endregion
254
- //#region src/build/index.ts
277
+ //#region src/build.ts
255
278
  const P3_MQ = "@media (color-gamut: p3)";
256
279
  const REC2020_MQ = "@media (color-gamut: rec2020)";
257
- function buildFormat({ getTransforms, exclude, utility, modeSelectors, baseSelector, baseScheme }) {
258
- const rules = [];
280
+ function buildCSS({ logger, getTransforms, exclude, utility, permutations, modeSelectors, baseSelector, baseScheme }) {
281
+ if (permutations?.length) {
282
+ let output$1 = "";
283
+ for (const p of permutations) {
284
+ if (typeof p.prepare !== "function") logger.error({
285
+ group: "plugin",
286
+ label: PLUGIN_NAME,
287
+ message: "prepare(css) must be a function!"
288
+ });
289
+ const tokens = getTransforms({
290
+ format: FORMAT_ID,
291
+ input: p.input
292
+ });
293
+ if (!tokens.length) continue;
294
+ const root = [];
295
+ const hdrColors = {
296
+ p3: [],
297
+ rec2020: []
298
+ };
299
+ for (const token of tokens) {
300
+ const localID = makeCSSVar(token.localID ?? token.token.id);
301
+ const aliasTokens = token.token.aliasedBy?.length ? getTransforms({
302
+ format: FORMAT_ID,
303
+ id: token.token.aliasedBy,
304
+ input: p.input
305
+ }) : [];
306
+ if (token.type === "SINGLE_VALUE") addDeclUnique(root, decl(localID, token.value, token.token.$description));
307
+ else if (token.value.srgb && token.value.p3 && token.value.rec2020) {
308
+ addDeclUnique(root, decl(localID, token.value.srgb, token.token.$description));
309
+ if (token.value.p3 !== token.value.srgb) {
310
+ addDeclUnique(hdrColors.p3, decl(localID, token.value.p3, token.token.$description));
311
+ addDeclUnique(hdrColors.rec2020, decl(localID, token.value.rec2020, token.token.$description));
312
+ for (const alias of aliasTokens) if (alias.localID && typeof alias.value === "string") {
313
+ const aliasID = makeCSSVar(alias.localID);
314
+ addDeclUnique(hdrColors.p3, decl(aliasID, alias.value, alias.token.$description));
315
+ addDeclUnique(hdrColors.rec2020, decl(aliasID, alias.value, alias.token.$description));
316
+ }
317
+ }
318
+ } else {
319
+ for (const [name, subValue] of Object.entries(token.value)) addDeclUnique(root, decl(`${localID}-${name}`, subValue, token.token.$description));
320
+ const shorthand = generateShorthand({
321
+ token: {
322
+ ...token.token,
323
+ $value: token.value
324
+ },
325
+ localID
326
+ });
327
+ if (shorthand) addDeclUnique(root, decl(localID, shorthand, token.token.$description));
328
+ }
329
+ for (const alias of aliasTokens) if (alias.localID && typeof alias.value === "string") addDeclUnique(root, decl(alias.localID, alias.value, token.token.$description));
330
+ }
331
+ const indentRules = getIndentFromPrepare(p.prepare);
332
+ if (output$1) output$1 += "\n";
333
+ output$1 += `${p.prepare(printRules(root, indentRules))}\n`;
334
+ for (const gamut of ["p3", "rec2020"]) if (hdrColors[gamut].length) {
335
+ output$1 += `\n@media (color-gamut: ${gamut}) {\n`;
336
+ output$1 += indentRules.indentChar;
337
+ output$1 += p.prepare(printRules(hdrColors[gamut], indentRules)).replace(/\n(?!\n)/g, `\n${indentRules.indentChar}`);
338
+ output$1 += "\n}\n";
339
+ }
340
+ }
341
+ if (utility && Object.keys(utility).length) {
342
+ if (output$1) output$1 += "\n\n";
343
+ output$1 += generateUtilityCSS(utility, getTransforms({ format: FORMAT_ID }), { logger });
344
+ }
345
+ return output$1;
346
+ }
347
+ let output = "";
259
348
  const rootTokens = getTransforms({
260
349
  format: FORMAT_ID,
261
350
  mode: "."
262
351
  });
263
352
  if (rootTokens.length) {
264
- const rootRule = {
265
- selectors: [baseSelector],
266
- declarations: {}
267
- };
268
- if (baseScheme) rootRule.declarations["color-scheme"] = { value: baseScheme };
269
- const p3Rule = {
270
- selectors: [baseSelector],
271
- nestedQuery: P3_MQ,
272
- declarations: {}
273
- };
274
- const rec2020Rule = {
275
- selectors: [baseSelector],
276
- nestedQuery: REC2020_MQ,
277
- declarations: {}
278
- };
279
- rules.push(rootRule, p3Rule, rec2020Rule);
353
+ const rules = [
354
+ rule([baseSelector], []),
355
+ rule([P3_MQ], [rule([baseSelector])]),
356
+ rule([REC2020_MQ], [rule([baseSelector])])
357
+ ];
358
+ const rootRule = rules[0];
359
+ const p3Rule = rules[1].children[0];
360
+ const rec2020Rule = rules[2].children[0];
361
+ if (baseScheme) rootRule.children.unshift(decl("color-scheme", baseScheme));
280
362
  const shouldExclude = wcmatch(exclude ?? []);
281
363
  for (const token of rootTokens) {
282
364
  if (shouldExclude(token.token.id)) continue;
283
365
  const localID = token.localID ?? token.token.id;
284
366
  const aliasTokens = token.token.aliasedBy?.length ? getTransforms({
285
367
  format: FORMAT_ID,
286
- id: token.token.aliasedBy
368
+ id: token.token.aliasedBy,
369
+ mode: "."
287
370
  }) : [];
288
- if (token.type === "SINGLE_VALUE") rootRule.declarations[localID] = {
289
- value: token.value,
290
- description: token.token.$description
291
- };
371
+ if (token.type === "SINGLE_VALUE") addDeclUnique(rootRule.children, decl(localID, token.value, token.token.$description));
292
372
  else if (token.value.srgb && token.value.p3 && token.value.rec2020) {
293
- rootRule.declarations[localID] = {
294
- value: token.value.srgb,
295
- description: token.token.$description
296
- };
373
+ addDeclUnique(rootRule.children, decl(localID, token.value.srgb, token.token.$description));
297
374
  if (token.value.p3 !== token.value.srgb) {
298
- p3Rule.declarations[localID] = {
299
- value: token.value.p3,
300
- description: token.token.$description
301
- };
302
- rec2020Rule.declarations[localID] = {
303
- value: token.value.rec2020,
304
- description: token.token.$description
305
- };
375
+ addDeclUnique(p3Rule.children, decl(localID, token.value.p3, token.token.$description));
376
+ addDeclUnique(rec2020Rule.children, decl(localID, token.value.rec2020, token.token.$description));
306
377
  for (const alias of aliasTokens) if (alias.localID && typeof alias.value === "string") {
307
- p3Rule.declarations[alias.localID] ??= {
308
- value: alias.value,
309
- description: token.token.$description
310
- };
311
- rec2020Rule.declarations[alias.localID] ??= {
312
- value: alias.value,
313
- description: token.token.$description
314
- };
378
+ addDeclUnique(p3Rule.children, decl(alias.localID, alias.value, token.token.$description));
379
+ addDeclUnique(rec2020Rule.children, decl(alias.localID, alias.value, token.token.$description));
315
380
  }
316
381
  }
317
382
  } else if (token.type === "MULTI_VALUE") {
318
- for (const [name, value] of Object.entries(token.value)) rootRule.declarations[name === "." ? localID : [localID, name].join("-")] = {
319
- value,
320
- description: token.token.$description
321
- };
383
+ for (const [name, value] of Object.entries(token.value)) {
384
+ const property = name === "." ? localID : [localID, name].join("-");
385
+ addDeclUnique(rootRule.children, decl(property, value, token.token.$description));
386
+ }
322
387
  const shorthand = generateShorthand({
323
388
  token: {
324
389
  ...token.token,
@@ -326,78 +391,40 @@ function buildFormat({ getTransforms, exclude, utility, modeSelectors, baseSelec
326
391
  },
327
392
  localID
328
393
  });
329
- if (shorthand) rootRule.declarations[token.localID ?? token.token.id] = {
330
- value: shorthand,
331
- description: token.token.$description
332
- };
394
+ if (shorthand) addDeclUnique(rootRule.children, decl(token.localID ?? token.token.id, shorthand, token.token.$description));
333
395
  }
334
396
  }
397
+ output += printRules(rules);
335
398
  }
336
- for (const { selectors, tokens, mode, scheme } of modeSelectors ?? []) {
337
- if (!selectors.length) continue;
399
+ for (const selector of modeSelectors ?? []) {
338
400
  const selectorTokens = getTransforms({
339
401
  format: FORMAT_ID,
340
- id: tokens,
341
- mode
402
+ id: selector.tokens,
403
+ mode: selector.mode
342
404
  });
343
405
  if (!selectorTokens.length) continue;
344
- const selectorRule = {
345
- selectors,
346
- declarations: {}
347
- };
348
- if (scheme) selectorRule.declarations["color-scheme"] = { value: scheme };
349
- const selectorP3Rule = {
350
- selectors,
351
- nestedQuery: P3_MQ,
352
- declarations: {}
353
- };
354
- const selectorRec2020Rule = {
355
- selectors,
356
- nestedQuery: REC2020_MQ,
357
- declarations: {}
406
+ const modeRule = rule(selector.selectors);
407
+ if (selector.scheme) modeRule.children.unshift(decl("color-scheme", selector.scheme));
408
+ const hdrColors = {
409
+ p3: [],
410
+ rec2020: []
358
411
  };
359
- const selectorAliasDeclarations = {};
360
- rules.push(selectorRule, selectorP3Rule, selectorRec2020Rule);
361
412
  for (const token of selectorTokens) {
362
413
  const localID = token.localID ?? token.token.id;
363
414
  const aliasTokens = token.token.aliasedBy?.length ? getTransforms({
364
415
  format: FORMAT_ID,
365
416
  id: token.token.aliasedBy
366
417
  }) : [];
367
- if (token.type === "SINGLE_VALUE") selectorRule.declarations[localID] = {
368
- value: token.value,
369
- description: token.token.$description
370
- };
418
+ if (token.type === "SINGLE_VALUE") addDeclUnique(modeRule.children, decl(localID, token.value, token.token.$description));
371
419
  else if (token.value.srgb && token.value.p3 && token.value.rec2020) {
372
- selectorRule.declarations[localID] = {
373
- value: token.value.srgb,
374
- description: token.token.$description
375
- };
420
+ addDeclUnique(modeRule.children, decl(localID, token.value.srgb, token.token.$description));
376
421
  if (token.value.p3 !== token.value.srgb) {
377
- selectorP3Rule.declarations[localID] = {
378
- value: token.value.p3,
379
- description: token.token.$description
380
- };
381
- selectorRec2020Rule.declarations[localID] = {
382
- value: token.value.rec2020,
383
- description: token.token.$description
384
- };
385
- for (const alias of aliasTokens) if (alias.localID && typeof alias.value === "string") {
386
- selectorP3Rule.declarations[alias.localID] ??= {
387
- value: alias.value,
388
- description: token.token.$description
389
- };
390
- selectorRec2020Rule.declarations[alias.localID] ??= {
391
- value: alias.value,
392
- description: token.token.$description
393
- };
394
- }
422
+ addDeclUnique(hdrColors.p3, decl(localID, token.value.p3, token.token.$description));
423
+ addDeclUnique(hdrColors.rec2020, decl(localID, token.value.rec2020, token.token.$description));
424
+ for (const alias of aliasTokens) if (alias.localID && typeof alias.value === "string") for (const gamut of ["p3", "rec2020"]) addDeclUnique(hdrColors[gamut], decl(alias.localID, alias.value, token.token.$description));
395
425
  }
396
426
  } else {
397
- for (const [name, subvalue] of Object.entries(token.value)) selectorRule.declarations[`${localID}-${name}`] = {
398
- value: subvalue,
399
- description: token.token.$description
400
- };
427
+ for (const [name, subValue] of Object.entries(token.value)) addDeclUnique(modeRule.children, decl(`${localID}-${name}`, subValue, token.token.$description));
401
428
  const shorthand = generateShorthand({
402
429
  token: {
403
430
  ...token.token,
@@ -405,107 +432,161 @@ function buildFormat({ getTransforms, exclude, utility, modeSelectors, baseSelec
405
432
  },
406
433
  localID
407
434
  });
408
- if (shorthand) selectorRule.declarations[localID] = {
409
- value: shorthand,
410
- description: token.token.$description
411
- };
435
+ if (shorthand) addDeclUnique(modeRule.children, decl(localID, shorthand, token.token.$description));
412
436
  }
413
- for (const alias of aliasTokens) if (alias.localID && typeof alias.value === "string") selectorAliasDeclarations[alias.localID] = {
414
- value: alias.value,
415
- description: token.token.$description
416
- };
437
+ for (const alias of aliasTokens) if (alias.localID && typeof alias.value === "string") addDeclUnique(modeRule.children, decl(alias.localID, alias.value, token.token.$description));
417
438
  }
418
- for (const [name, { value, description }] of Object.entries(selectorAliasDeclarations)) selectorRule.declarations[name] ??= {
419
- value,
420
- description
421
- };
439
+ if (output) output += "\n\n";
440
+ output += printRules([
441
+ modeRule,
442
+ rule([P3_MQ], [rule(selector.selectors, hdrColors.p3)]),
443
+ rule([REC2020_MQ], [rule(selector.selectors, hdrColors.rec2020)])
444
+ ]);
422
445
  }
423
- if (utility && Object.keys(utility).length) rules.push(...generateUtilityCSS(utility, getTransforms({
424
- format: FORMAT_ID,
425
- mode: "."
426
- })));
427
- return printRules(rules);
446
+ if (utility && Object.keys(utility).length) {
447
+ if (output) output += "\n\n";
448
+ output += printRules(generateUtilityCSS(utility, getTransforms({
449
+ format: FORMAT_ID,
450
+ mode: "."
451
+ }), { logger }));
452
+ }
453
+ return output;
428
454
  }
429
455
 
430
456
  //#endregion
431
- //#region src/index.ts
432
- function cssPlugin(options) {
433
- const { exclude, variableName, modeSelectors, transform: customTransform, utility, legacyHex, skipBuild, baseScheme } = options ?? {};
434
- const filename = options?.filename ?? options?.fileName ?? "index.css";
435
- const baseSelector = options?.baseSelector ?? ":root";
457
+ //#region src/transform.ts
458
+ function transformCSS({ transform: { context: { logger }, resolver, getTransforms, setTransform, tokens: baseTokens }, options: { permutations, exclude: userExclude, legacyHex, transform: customTransform, variableName } }) {
436
459
  function transformName(token) {
437
460
  const customName = variableName?.(token);
438
461
  if (customName !== void 0) {
439
- if (typeof customName !== "string") throw new Error(`variableName() must return a string; received ${customName}`);
462
+ if (typeof customName !== "string") logger.error({
463
+ group: "plugin",
464
+ label: PLUGIN_NAME,
465
+ message: `variableName() must return a string; received ${customName}`
466
+ });
440
467
  return customName;
441
468
  }
442
469
  return makeCSSVar(token.id);
443
470
  }
444
471
  const transformAlias = (token) => `var(${transformName(token)})`;
445
- return {
446
- name: "@terrazzo/plugin-css",
447
- async transform({ tokens, getTransforms, setTransform }) {
448
- if (getTransforms({
449
- format: FORMAT_ID,
450
- id: "*",
451
- mode: "*"
452
- }).length) return;
453
- for (const [id, token] of Object.entries(tokens)) {
454
- const localID = transformName(token);
455
- for (const mode of Object.keys(token.mode)) {
456
- if (customTransform) {
457
- const value = customTransform(token, mode);
458
- if (value !== void 0 && value !== null) {
459
- validateCustomTransform(value, { $type: token.$type });
460
- setTransform(id, {
461
- format: FORMAT_ID,
462
- localID,
463
- value,
464
- mode,
465
- meta: { "token-listing": { name: localID } }
466
- });
467
- continue;
468
- }
469
- }
470
- const transformedValue = transformCSSValue(token, {
471
- mode,
472
+ const exclude = userExclude ? wcmatch(userExclude) : void 0;
473
+ if (permutations?.length) {
474
+ for (const p of permutations) {
475
+ const input = p.input;
476
+ const ignore = p.ignore ? wcmatch(p.ignore) : void 0;
477
+ try {
478
+ const tokens = resolver.apply(input);
479
+ for (const token of Object.values(tokens)) {
480
+ if (ignore?.(token.id) || exclude?.(token.id)) continue;
481
+ const value = customTransform?.(token) ?? transformCSSValue(token, {
472
482
  tokensSet: tokens,
473
483
  transformAlias,
474
484
  color: { legacyHex }
475
485
  });
476
- if (transformedValue !== void 0) {
477
- let listingName = localID;
478
- if (typeof transformedValue === "object" && generateShorthand({
479
- token,
480
- localID
481
- }) === void 0) listingName = void 0;
482
- setTransform(id, {
486
+ if (value && isDifferentValue(value, getTransforms({
487
+ format: FORMAT_ID,
488
+ id: token.id
489
+ })[0]?.value)) {
490
+ const localID = transformName(token);
491
+ setTransform(token.id, {
483
492
  format: FORMAT_ID,
493
+ value,
484
494
  localID,
485
- value: transformedValue,
486
- mode,
487
- meta: { "token-listing": { name: listingName } }
495
+ input,
496
+ meta: { "token-listing": { name: localID } }
488
497
  });
489
498
  }
490
499
  }
500
+ } catch (err) {
501
+ logger.error({
502
+ group: "plugin",
503
+ label: PLUGIN_NAME,
504
+ message: `There was an error trying to apply input ${resolver.getPermutationID(input)}.`,
505
+ continueOnError: true
506
+ });
507
+ throw err;
508
+ }
509
+ }
510
+ return;
511
+ }
512
+ for (const token of Object.values(baseTokens)) {
513
+ if (exclude?.(token.id)) continue;
514
+ for (const mode of Object.keys(token.mode)) {
515
+ const value = customTransform?.(token, ".") ?? transformCSSValue({
516
+ ...token,
517
+ ...token.mode[mode]
518
+ }, {
519
+ tokensSet: baseTokens,
520
+ transformAlias,
521
+ color: { legacyHex }
522
+ });
523
+ if (value) {
524
+ const localID = transformName(token);
525
+ setTransform(token.id, {
526
+ format: FORMAT_ID,
527
+ localID,
528
+ value,
529
+ mode,
530
+ meta: { "token-listing": { name: localID } }
531
+ });
491
532
  }
533
+ }
534
+ }
535
+ }
536
+ /** Is the transformed value different from the base value? */
537
+ function isDifferentValue(value, baseValue) {
538
+ if (!value || !baseValue || typeof value !== typeof baseValue) return true;
539
+ if (typeof value === "string" && typeof baseValue === "string") return value !== baseValue;
540
+ const keysA = Object.keys(value);
541
+ const keysB = Object.keys(baseValue);
542
+ if (keysA.length !== keysB.length) return true;
543
+ if (!keysA.every((k) => keysB.includes(k) && value[k] === baseValue[k])) return true;
544
+ return false;
545
+ }
546
+
547
+ //#endregion
548
+ //#region src/index.ts
549
+ function cssPlugin(options) {
550
+ const { utility, skipBuild, baseScheme } = options ?? {};
551
+ const filename = options?.filename ?? options?.fileName ?? "index.css";
552
+ const baseSelector = options?.baseSelector ?? ":root";
553
+ return {
554
+ name: PLUGIN_NAME,
555
+ config(_config, context) {
556
+ if (options?.permutations && (options?.modeSelectors || options?.baseSelector || options?.baseScheme)) context.logger.error({
557
+ group: "plugin",
558
+ label: PLUGIN_NAME,
559
+ message: "Permutations option is incompatible with modeSelectors, baseSelector, and baseScheme."
560
+ });
561
+ },
562
+ async transform(transformOptions) {
563
+ if (transformOptions.getTransforms({
564
+ format: FORMAT_ID,
565
+ id: "*"
566
+ }).length) return;
567
+ transformCSS({
568
+ transform: transformOptions,
569
+ options: options ?? {}
570
+ });
492
571
  },
493
- async build({ getTransforms, outputFile }) {
572
+ async build({ getTransforms, outputFile, context }) {
494
573
  if (skipBuild === true) return;
495
- const output = [FILE_PREFIX, ""];
496
- output.push(buildFormat({
497
- exclude,
574
+ let contents = `${FILE_PREFIX}\n\n`;
575
+ contents += buildCSS({
576
+ exclude: options?.exclude,
498
577
  getTransforms,
499
- modeSelectors,
578
+ permutations: options?.permutations,
579
+ modeSelectors: options?.modeSelectors,
500
580
  utility,
501
581
  baseSelector,
502
- baseScheme
503
- }), "\n");
504
- outputFile(filename, output.join("\n"));
582
+ baseScheme,
583
+ logger: context.logger
584
+ });
585
+ outputFile(filename, contents.replace(/\n*$/, "\n"));
505
586
  }
506
587
  };
507
588
  }
508
589
 
509
590
  //#endregion
510
- export { FILE_PREFIX, FORMAT_ID, cssPlugin as default, printRules };
591
+ export { FILE_PREFIX, FORMAT_ID, PLUGIN_NAME, addDeclUnique, decl, cssPlugin as default, getIndentFromPrepare, hasDecl, printNode, printRules, rule };
511
592
  //# sourceMappingURL=index.js.map