@pre.style/core 3.1.0-alpha.1 → 3.1.0-alpha.3

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.
@@ -3,10 +3,13 @@ import { dirname, relative, resolve as resolvePath, sep } from 'node:path';
3
3
  import { transform } from 'lightningcss';
4
4
  import SweatMap from 'sweatmap';
5
5
  import { dropOverridden, parseBlock, PreStyleError, valueOf } from './block.js';
6
+ import { interpolate } from './interpolate.js';
6
7
  import { print } from './print.js';
7
- import { lvhfaRank, withClass } from './selectors.js';
8
+ import { lvhfaRank, specificity, withClass } from './selectors.js';
8
9
  import { KEYFRAMES_PREFIX } from './keyframes.js';
10
+ import { VARIABLE_PREFIX } from './variables.js';
9
11
  import { MARKER_PREFIX, markerKey, parseStylesheet } from './stylesheet.js';
12
+ import { SHORTHANDS } from '../shorthands.js';
10
13
  const RANGES = {
11
14
  Digits: { start: '0030', end: '0039' },
12
15
  Uppercase: { start: '0041', end: '005A' },
@@ -30,6 +33,16 @@ const QUIRKS_NAME_OPTIONS = {
30
33
  cssSafe: true,
31
34
  ranges: Object.fromEntries(Object.entries(RANGES).filter(([name]) => name !== 'Uppercase')),
32
35
  };
36
+ // A block with `${…}`, from the text around its expressions and their source.
37
+ export function interpolatedBlock(block) {
38
+ const { strings, expressions, ...rest } = block;
39
+ try {
40
+ return { ...rest, ...interpolate(strings, expressions, block.lang) };
41
+ }
42
+ catch (error) {
43
+ throw error instanceof PreStyleError ? locate(error, { ...rest, code: '' }) : error;
44
+ }
45
+ }
33
46
  export async function createCompiler(options = {}) {
34
47
  let sass = null;
35
48
  try {
@@ -54,19 +67,22 @@ export class Compiler {
54
67
  #root;
55
68
  // One name list for every class, one for prefixes (the at-rules and
56
69
  // selector variant a class is under), which only the `cn` table uses, and
57
- // one for localized keyframes.
70
+ // one each for localized keyframes and runtime variables.
58
71
  #names;
59
- // How often each class, prefix, and localized keyframes is used across
60
- // scanned blocks.
72
+ // How often each class, prefix, localized keyframes, and variable is used
73
+ // across scanned blocks.
61
74
  #counts = {
62
75
  atoms: new Map(),
63
76
  prefixes: new Map(),
64
77
  keyframes: new Map(),
78
+ variables: new Map(),
65
79
  };
66
80
  #atoms = new Map();
67
81
  #globals = new Map();
68
82
  // The current global for each overridable at-rule.
69
83
  #overrides = new Map();
84
+ // Each runtime variable's custom property, or null for `--<its name>`.
85
+ #variables = new Map();
70
86
  #order = 0;
71
87
  constructor(options, sass) {
72
88
  this.#options = options;
@@ -77,6 +93,8 @@ export class Compiler {
77
93
  // four characters only after about 227,000 names, so they're never
78
94
  // `none` or a CSS-wide keyword.
79
95
  keyframes: new SweatMap(NAME_OPTIONS),
96
+ // Part of the classes generated at runtime, so these follow class names.
97
+ variables: new SweatMap(options.quirksMode ? QUIRKS_NAME_OPTIONS : NAME_OPTIONS),
80
98
  };
81
99
  this.#layer = layerRule(options.layer);
82
100
  const root = options.root ?? process.cwd();
@@ -89,6 +107,7 @@ export class Compiler {
89
107
  dir: dir + sep,
90
108
  prepend: readPrepend(dir, library.prependFiles),
91
109
  keyframeLocalization: library.keyframeLocalization ?? false,
110
+ customPropertyLocalization: library.customPropertyLocalization ?? false,
92
111
  };
93
112
  })
94
113
  .toSorted((a, b) => b.dir.length - a.dir.length);
@@ -110,7 +129,9 @@ export class Compiler {
110
129
  this.#register(block, parsed);
111
130
  this.#count(this.#resolveBlock(block, parsed, []));
112
131
  for (const global of parsed.globals)
113
- this.#countKeyframes(global.key);
132
+ this.#countNames(global.key);
133
+ for (const variable of parsed.variables ?? [])
134
+ count(this.#counts.variables, variable.key);
114
135
  }
115
136
  // Pass 1 of naming for a stylesheet file: counts what each class uses.
116
137
  scanStylesheet(sheet) {
@@ -118,12 +139,12 @@ export class Compiler {
118
139
  for (const name of parsed.classes.keys())
119
140
  this.#count(this.#resolveClass(sheet.file, parsed, name, [], []));
120
141
  for (const global of parsed.globals)
121
- this.#countKeyframes(global.key);
142
+ this.#countNames(global.key);
122
143
  }
123
144
  // Pass 2 of naming: gives the most-used entries the shortest names. Ties
124
145
  // are broken by key, so names don't depend on the order files are read.
125
146
  assignNames() {
126
- for (const list of ['atoms', 'prefixes', 'keyframes']) {
147
+ for (const list of ['atoms', 'prefixes', 'keyframes', 'variables']) {
127
148
  const sorted = [...this.#counts[list]].toSorted(([a, countA], [b, countB]) => countB - countA || (a < b ? -1 : a > b ? 1 : 0));
128
149
  for (const [key] of sorted)
129
150
  this.#names[list].set(key);
@@ -136,6 +157,20 @@ export class Compiler {
136
157
  this.#register(block, parsed);
137
158
  return this.#classString(this.#resolveBlock(block, parsed, []));
138
159
  }
160
+ // The variables a block sets at runtime, in the order they're written.
161
+ variables(block) {
162
+ const parsed = this.#parse(block);
163
+ this.#register(block, parsed);
164
+ return (parsed.variables ?? []).map((variable) => {
165
+ const name = this.#names.variables.set(variable.key);
166
+ return {
167
+ name,
168
+ property: variable.property ? this.#propertyName(variable.property) : `--${name}`,
169
+ slot: block.slots[variable.slot],
170
+ selector: variable.selector,
171
+ };
172
+ });
173
+ }
139
174
  // The files a block's class string depends on, through `composes … from`.
140
175
  dependencies(block) {
141
176
  const dependencies = [];
@@ -155,6 +190,34 @@ export class Compiler {
155
190
  throw error instanceof PreStyleError ? locate(error, fileBlock(sheet.file)) : error;
156
191
  }
157
192
  }
193
+ // Where each of the given classes is used, in output order. Classes that
194
+ // aren't in the output are left out. Sources are only kept with `sourceMap`
195
+ // on, and only list what has been compiled so far.
196
+ usages(classNames) {
197
+ const wanted = new Set(classNames);
198
+ const usages = [];
199
+ let order = 0;
200
+ const walk = (group) => {
201
+ for (const record of sortAtoms(group.atoms)) {
202
+ order++;
203
+ if (record.className !== null && wanted.has(record.className))
204
+ usages.push(this.#usage(record, order));
205
+ }
206
+ for (const child of sortGroups([...group.children.values()]))
207
+ walk(child);
208
+ };
209
+ walk(this.#atomGroups());
210
+ return usages;
211
+ }
212
+ // Drops the places a file uses atoms, because it changed. They're added back
213
+ // as its blocks are compiled again. The atoms stay, like the rest of dev's
214
+ // output, which only grows.
215
+ forgetSources(file) {
216
+ for (const record of this.#atoms.values())
217
+ for (const [key, source] of record.sources)
218
+ if (source.file === file)
219
+ record.sources.delete(key);
220
+ }
158
221
  // Each class of a stylesheet file, with its class string.
159
222
  stylesheet(sheet) {
160
223
  const parsed = this.#parseSheet(sheet);
@@ -211,18 +274,29 @@ export class Compiler {
211
274
  const key = prefixKey(record.atom);
212
275
  const prefix = key === '' ? '' : this.#names.prefixes.set(key);
213
276
  const properties = (table[prefix] ??= {});
214
- const { property } = record.atom;
277
+ const property = this.#propertyName(record.atom.property);
215
278
  const name = this.#nameOf(record);
216
279
  properties[property] = properties[property] ? `${properties[property]} ${name}` : name;
217
280
  }
218
281
  return table;
219
282
  }
283
+ // The variables runtime values set, for `cn` to recognize generated classes.
284
+ cnVariables() {
285
+ const variables = {};
286
+ for (const [key, property] of this.#variables)
287
+ variables[this.#names.variables.set(key)] = property ? this.#propertyName(property) : '';
288
+ return variables;
289
+ }
220
290
  // A library's blocks and files get its prepended files and keyframes
221
291
  // setting instead of the app's.
222
292
  #library(file) {
223
293
  const library = file === undefined ? -1 : this.#libraries.findIndex(({ dir }) => file.startsWith(dir));
224
- const { prepend, keyframeLocalization } = library === -1
225
- ? { prepend: this.#prepend, keyframeLocalization: this.#options.keyframeLocalization }
294
+ const { prepend, keyframeLocalization, customPropertyLocalization } = library === -1
295
+ ? {
296
+ prepend: this.#prepend,
297
+ keyframeLocalization: this.#options.keyframeLocalization,
298
+ customPropertyLocalization: this.#options.customPropertyLocalization,
299
+ }
226
300
  : this.#libraries[library];
227
301
  return {
228
302
  library,
@@ -230,6 +304,7 @@ export class Compiler {
230
304
  ...this.#parseOptions,
231
305
  prepend,
232
306
  keyframeLocalization: keyframeLocalization ?? false,
307
+ customPropertyLocalization: customPropertyLocalization ?? false,
233
308
  },
234
309
  };
235
310
  }
@@ -348,7 +423,7 @@ export class Compiler {
348
423
  const prefix = prefixKey(atom);
349
424
  if (prefix !== '')
350
425
  count(this.#counts.prefixes, prefix);
351
- this.#countKeyframes(atom.declaration.key);
426
+ this.#countNames(atom.declaration.key);
352
427
  }
353
428
  for (const extra of resolved.extras)
354
429
  if (extra.startsWith(MARKER_PREFIX))
@@ -360,12 +435,18 @@ export class Compiler {
360
435
  names.push(extra.startsWith(MARKER_PREFIX) ? this.#names.atoms.set(extra) : extra);
361
436
  return [...new Set(names)].join(' ');
362
437
  }
363
- // Counts the localized keyframes a JSON key names.
364
- #countKeyframes(json) {
365
- if (!json.includes(KEYFRAMES_KEY))
366
- return;
367
- for (const [, hash] of json.matchAll(KEYFRAMES_KEY_RE))
368
- count(this.#counts.keyframes, KEYFRAMES_PREFIX + hash);
438
+ // Counts the localized keyframes and custom properties a JSON key names.
439
+ #countNames(json) {
440
+ if (json.includes(KEYFRAMES_KEY))
441
+ for (const [, hash] of json.matchAll(KEYFRAMES_KEY_RE))
442
+ count(this.#counts.keyframes, KEYFRAMES_PREFIX + hash);
443
+ if (json.includes(VARIABLE_KEY))
444
+ for (const [, hash] of json.matchAll(VARIABLE_KEY_RE))
445
+ count(this.#counts.variables, hash);
446
+ }
447
+ // A property's name in the output, for a custom property with a generated one.
448
+ #propertyName(property) {
449
+ return property.startsWith(VARIABLE_PREFIX) ? this.#named(property) : property;
369
450
  }
370
451
  // Replaces markers in selectors, and localized keyframes names, with their
371
452
  // names.
@@ -373,6 +454,8 @@ export class Compiler {
373
454
  const visit = (node) => {
374
455
  if (Array.isArray(node))
375
456
  return node.map(visit);
457
+ if (typeof node === 'string' && node.startsWith(VARIABLE_PREFIX))
458
+ return `--${this.#names.variables.set(node.slice(VARIABLE_PREFIX.length))}`;
376
459
  if (node === null || typeof node !== 'object')
377
460
  return node;
378
461
  const record = node;
@@ -389,16 +472,30 @@ export class Compiler {
389
472
  return visit(value);
390
473
  }
391
474
  #register(block, parsed) {
475
+ for (const variable of parsed.variables ?? []) {
476
+ if (this.#variables.has(variable.key))
477
+ continue;
478
+ this.#variables.set(variable.key, variable.property);
479
+ // A custom property set directly adds no atom or rule, but `cn` needs it.
480
+ this.#order++;
481
+ }
392
482
  for (const atom of parsed.atoms) {
393
483
  const key = atomKey(atom);
394
- if (!this.#atoms.has(key)) {
395
- this.#atoms.set(key, {
484
+ const source = sourceOf(block, atom.position);
485
+ let record = this.#atoms.get(key);
486
+ if (record === undefined) {
487
+ record = {
396
488
  atom,
397
489
  className: null,
398
490
  order: this.#order++,
399
- source: sourceOf(block, atom.position),
400
- });
491
+ source,
492
+ sources: new Map(),
493
+ printed: null,
494
+ };
495
+ this.#atoms.set(key, record);
401
496
  }
497
+ if (source !== null && this.#options.sourceMap)
498
+ record.sources.set(`${source.file}:${source.line}:${source.column}`, source);
402
499
  }
403
500
  for (const global of parsed.globals) {
404
501
  if (this.#globals.has(global.key))
@@ -420,8 +517,8 @@ export class Compiler {
420
517
  #nameOf(record) {
421
518
  return (record.className ??= this.#names.atoms.set(atomKey(record.atom)));
422
519
  }
423
- #atomRules(loc) {
424
- // Group atoms by their at-rules, nesting groups the same way.
520
+ // Atoms grouped by their at-rules, nesting groups the same way.
521
+ #atomGroups() {
425
522
  const root = { segment: null, order: 0, atoms: [], children: new Map() };
426
523
  for (const record of this.#atoms.values()) {
427
524
  let group = root;
@@ -435,45 +532,91 @@ export class Compiler {
435
532
  }
436
533
  group.atoms.push(record);
437
534
  }
535
+ return root;
536
+ }
537
+ #atomSelectors(record) {
538
+ const { atom } = record;
539
+ const className = this.#nameOf(record);
540
+ const selectors = atom.variant
541
+ ? withClass(atom.variant.selectors, className)
542
+ : [[{ type: 'class', name: className }]];
543
+ return atom.variant?.key.includes(MARKER_KEY) ? this.#named(selectors) : selectors;
544
+ }
545
+ #atomRule(record, loc) {
546
+ const { atom } = record;
547
+ const value = hasGenerated(atom.declaration.key)
548
+ ? this.#named(atom.declaration.value)
549
+ : atom.declaration.value;
550
+ const declarations = atom.declaration.important
551
+ ? { importantDeclarations: [value] }
552
+ : { declarations: [value] };
553
+ return {
554
+ type: 'style',
555
+ value: { selectors: this.#atomSelectors(record), declarations, rules: [], loc },
556
+ };
557
+ }
558
+ #atomRules(loc) {
438
559
  const emit = (group) => {
439
560
  const rules = sortAtoms(group.atoms).map((record) => {
440
- const { atom, source } = record;
441
- const className = this.#nameOf(record);
442
- let selectors = atom.variant
443
- ? withClass(atom.variant.selectors, className)
444
- : [[{ type: 'class', name: className }]];
445
- if (atom.variant?.key.includes(MARKER_KEY))
446
- selectors = this.#named(selectors);
447
- const value = atom.declaration.key.includes(KEYFRAMES_KEY)
448
- ? this.#named(atom.declaration.value)
449
- : atom.declaration.value;
450
- const declarations = atom.declaration.important
451
- ? { importantDeclarations: [value] }
452
- : { declarations: [value] };
453
- return {
454
- type: 'style',
455
- value: { selectors, declarations, rules: [], loc: loc(source) },
456
- };
561
+ // The first place it's still used, since `forgetSources` drops places.
562
+ const [source = record.source] = record.sources.values();
563
+ return this.#atomRule(record, loc(source));
457
564
  });
458
565
  for (const child of sortGroups([...group.children.values()])) {
566
+ const segment = child.segment;
567
+ const named = hasGenerated(segment.key) ? this.#named(segment.rule) : segment.rule;
459
568
  rules.push({
460
- type: child.segment.rule.type,
461
- value: { ...valueOf(child.segment.rule), rules: emit(child) },
569
+ type: segment.rule.type,
570
+ value: { ...valueOf(named), rules: emit(child) },
462
571
  });
463
572
  }
464
573
  return rules;
465
574
  };
466
- return emit(root);
575
+ return emit(this.#atomGroups());
576
+ }
577
+ #usage(record, order) {
578
+ const { atom } = record;
579
+ const loc = { source_index: 0, line: 0, column: 1 };
580
+ const rule = this.#atomRule(record, loc);
581
+ // Printed for reading, so without the user's `minify` and `targets`.
582
+ const { drafts, nonStandard, errorRecovery } = this.#parseOptions.lightningcss;
583
+ const text = (rules) => print([rules], [], { drafts, nonStandard, errorRecovery }, false).code;
584
+ const code = (record.printed ??= text(rule));
585
+ const open = code.indexOf(' {\n');
586
+ return {
587
+ className: record.className,
588
+ selector: code.slice(0, open).replaceAll('\n', ' '),
589
+ atRules: atom.atRules.map((segment) => {
590
+ const named = hasGenerated(segment.key) ? this.#named(segment.rule) : segment.rule;
591
+ const wrapped = { ...valueOf(named), rules: [rule] };
592
+ const printed = text({ type: segment.rule.type, value: wrapped });
593
+ return printed.slice(0, printed.indexOf(' {\n'));
594
+ }),
595
+ declaration: code
596
+ .slice(open + 3, code.lastIndexOf('\n}'))
597
+ .trim()
598
+ .replace(/;$/, ''),
599
+ property: this.#propertyName(atom.property),
600
+ covers: Object.hasOwn(SHORTHANDS, atom.property) ? SHORTHANDS[atom.property] : [],
601
+ important: atom.declaration.important,
602
+ specificity: specificity(this.#atomSelectors(record)),
603
+ order,
604
+ sources: [...record.sources.values()],
605
+ };
467
606
  }
468
607
  }
469
608
  // Markers and localized keyframes names as they appear in JSON keys, which
470
609
  // escape the `\0`.
471
610
  const MARKER_KEY = JSON.stringify(MARKER_PREFIX).slice(1, -1);
472
611
  const KEYFRAMES_KEY = JSON.stringify(KEYFRAMES_PREFIX).slice(1, -1);
612
+ const VARIABLE_KEY = JSON.stringify(VARIABLE_PREFIX).slice(1, -1);
613
+ const VARIABLE_KEY_RE = new RegExp(`${RegExp.escape(VARIABLE_KEY)}([\\w-]+)`, 'g');
473
614
  const KEYFRAMES_KEY_RE = new RegExp(`${RegExp.escape(KEYFRAMES_KEY)}([\\w-]+)`, 'g');
474
615
  // Rules that can't be inside a layer.
475
616
  const outside = (rule) => rule.type === 'import' || rule.type === 'namespace';
476
- const hasPlaceholder = (json) => json.includes(MARKER_KEY) || json.includes(KEYFRAMES_KEY);
617
+ // Whether a declaration has generated names.
618
+ const hasGenerated = (json) => json.includes(KEYFRAMES_KEY) || json.includes(VARIABLE_KEY);
619
+ const hasPlaceholder = (json) => json.includes(MARKER_KEY) || hasGenerated(json);
477
620
  // Parses the `layer` option into an `@layer` block rule.
478
621
  function layerRule(layer) {
479
622
  if (layer === undefined || layer === false)
@@ -7,6 +7,7 @@ export interface PackageConfig {
7
7
  libraries?: Record<string, LibraryConfig | boolean>;
8
8
  quirksMode?: boolean;
9
9
  keyframeLocalization?: boolean;
10
+ customPropertyLocalization?: boolean;
10
11
  }
11
12
  export declare function checkField(field: Record<string, unknown>, where: string, keys: Set<string>): string[];
12
13
  export declare function readConfig(root: string): {
@@ -7,6 +7,7 @@ const KEYS = new Set([
7
7
  'libraries',
8
8
  'quirksMode',
9
9
  'keyframeLocalization',
10
+ 'customPropertyLocalization',
10
11
  ]);
11
12
  const isStrings = (value) => typeof value === 'string' ||
12
13
  (Array.isArray(value) && value.every((item) => typeof item === 'string'));
@@ -31,6 +32,9 @@ export function checkField(field, where, keys) {
31
32
  fail('quirksMode', '`true` or `false`');
32
33
  if (field.keyframeLocalization !== undefined && typeof field.keyframeLocalization !== 'boolean')
33
34
  fail('keyframeLocalization', '`true` or `false`');
35
+ if (field.customPropertyLocalization !== undefined &&
36
+ typeof field.customPropertyLocalization !== 'boolean')
37
+ fail('customPropertyLocalization', '`true` or `false`');
34
38
  if (field.libraries !== undefined &&
35
39
  (field.libraries === null ||
36
40
  typeof field.libraries !== 'object' ||
@@ -0,0 +1,5 @@
1
+ import type { Atom, GlobalRule, Lang, ParseOptions } from './block.ts';
2
+ export declare function localizeCustomProperties(atoms: Atom[], globals: GlobalRule[], scope: string, lang: Lang, options: ParseOptions): {
3
+ globals: GlobalRule[];
4
+ atoms: (atoms: Atom[]) => Atom[];
5
+ };
@@ -0,0 +1,150 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { key, overrideKey, parseRules, valueOf, walk } from './block.js';
3
+ import { VARIABLE_PREFIX } from './variables.js';
4
+ // With `customPropertyLocalization`, a custom property that a block or
5
+ // stylesheet file declares gets a generated name, used only in that block or
6
+ // file. In parsed rules it's a variable name, which the compiler names when
7
+ // printing, in the same list as the variables runtime values set. Names it only
8
+ // reads, and names from prepended files, which are how values are shared, stay
9
+ // global.
10
+ // Properties whose values name custom properties. Other dashed names, such as
11
+ // `anchor-name: --x` or `anchor(--x top)`, aren't custom properties.
12
+ const NAMING = new Set(['will-change', 'transition', 'transition-property']);
13
+ // Calls `declared` with each custom property a value declares.
14
+ function eachDeclared(value, declared) {
15
+ if (Array.isArray(value)) {
16
+ for (const item of value)
17
+ eachDeclared(item, declared);
18
+ return;
19
+ }
20
+ if (value === null || typeof value !== 'object')
21
+ return;
22
+ const node = value;
23
+ if (node.property === 'custom' && node.value?.name?.startsWith('--'))
24
+ declared(node.value.name);
25
+ if (node.type === 'property' && typeof node.value?.name === 'string')
26
+ declared(node.value.name);
27
+ for (const child of Object.values(node))
28
+ eachDeclared(child, declared);
29
+ }
30
+ // Replaces custom property names in a value. Returns `value` itself when nothing
31
+ // changed.
32
+ function rename(value, local, naming = false) {
33
+ if (Array.isArray(value)) {
34
+ const mapped = value.map((item) => rename(item, local, naming));
35
+ return (mapped.some((item, i) => item !== value[i]) ? mapped : value);
36
+ }
37
+ if (value === null || typeof value !== 'object')
38
+ return value;
39
+ const node = value;
40
+ // `var(--x)`.
41
+ const ident = node.ident;
42
+ // `transition-property`'s `{ property: '--x' }`.
43
+ const property = node.property;
44
+ // `will-change: --x`.
45
+ const dashed = naming && node.type === 'dashed-ident' ? node.value : undefined;
46
+ const isNaming = naming ||
47
+ (node.property === 'custom' && NAMING.has(node.value?.name)) ||
48
+ (node.property === 'unparsed' && NAMING.has(node.value?.propertyId?.property));
49
+ let result = node;
50
+ const set = (field, next) => {
51
+ if (result === node)
52
+ result = { ...node };
53
+ result[field] = next;
54
+ };
55
+ for (const [field, child] of Object.entries(node)) {
56
+ const next = rename(child, local, isNaming);
57
+ if (next !== child)
58
+ set(field, next);
59
+ }
60
+ if (typeof ident === 'string' && local.has(ident))
61
+ set('ident', local.get(ident));
62
+ if (typeof property === 'string' && local.has(property))
63
+ set('property', local.get(property));
64
+ if (typeof dashed === 'string' && local.has(dashed))
65
+ set('value', local.get(dashed));
66
+ // `--x: 1` and `@property --x`.
67
+ if ((node.property === 'custom' || node.type === 'property') && local.has(result.value?.name))
68
+ set('value', { ...result.value, name: local.get(result.value.name) });
69
+ return result;
70
+ }
71
+ const prepended = new Map();
72
+ // The rules and custom properties prepended files output. They're parsed once
73
+ // more on their own, and only with this setting on, since within a block they
74
+ // can't be told apart from its own rules.
75
+ function prependedOf(lang, options) {
76
+ const code = options.prepend[lang];
77
+ const cacheKey = `${lang}\n${code}`;
78
+ let result = prepended.get(cacheKey);
79
+ if (result !== undefined)
80
+ return result;
81
+ result = { keys: new Set(), names: new Set() };
82
+ if (code.trim() !== '') {
83
+ const css = lang === 'scss' && options.sass !== null ? options.sass.compileString(code).css : code;
84
+ const parsed = { atoms: [], globals: [], composes: [] };
85
+ parseRules(css, options.lightningcss, () => null, (rules) => walk(rules, [], null, parsed, () => null));
86
+ for (const global of parsed.globals) {
87
+ result.keys.add(global.key);
88
+ eachDeclared(global.rule, (name) => result.names.add(name));
89
+ }
90
+ }
91
+ prepended.set(cacheKey, result);
92
+ return result;
93
+ }
94
+ // Localizes the custom properties a scope (a block, or a whole stylesheet file)
95
+ // declares. `scope` identifies it, and has to be stable between builds.
96
+ export function localizeCustomProperties(atoms, globals, scope, lang, options) {
97
+ const shared = prependedOf(lang, options);
98
+ const local = new Map();
99
+ const declared = (name) => {
100
+ if (shared.names.has(name) || local.has(name))
101
+ return;
102
+ const hash = createHash('sha256').update(`${scope}\n${name}`).digest('base64url').slice(0, 16);
103
+ local.set(name, VARIABLE_PREFIX + hash);
104
+ };
105
+ for (const atom of atoms)
106
+ eachDeclared(atom.declaration.value, declared);
107
+ const own = (global) => !shared.keys.has(global.key);
108
+ for (const global of globals)
109
+ if (own(global))
110
+ eachDeclared(global.rule, declared);
111
+ if (local.size === 0)
112
+ return { globals, atoms: (list) => list };
113
+ return {
114
+ globals: globals.map((global) => {
115
+ if (!own(global))
116
+ return global;
117
+ const rule = rename(global.rule, local);
118
+ if (rule === global.rule)
119
+ return global;
120
+ return {
121
+ ...global,
122
+ key: key(rule),
123
+ rule,
124
+ overrideKey: global.overrideKey === null ? null : overrideKey(rule),
125
+ };
126
+ }),
127
+ atoms: (list) => list.map((atom) => {
128
+ const value = rename(atom.declaration.value, local);
129
+ const atRules = atom.atRules.map((segment) => {
130
+ const rule = rename(segment.rule, local);
131
+ return rule === segment.rule ? segment : { key: key(valueKey(rule)), rule };
132
+ });
133
+ const unchanged = value === atom.declaration.value && atRules.every((s, i) => s === atom.atRules[i]);
134
+ if (unchanged)
135
+ return atom;
136
+ return {
137
+ ...atom,
138
+ atRules,
139
+ property: local.get(atom.property) ?? atom.property,
140
+ declaration: {
141
+ ...atom.declaration,
142
+ key: key(value) + (atom.declaration.important ? '!' : ''),
143
+ value,
144
+ },
145
+ };
146
+ }),
147
+ };
148
+ }
149
+ // The same key `walk` gives an at-rule.
150
+ const valueKey = (rule) => ({ type: rule.type, value: valueOf(rule) });
@@ -1,7 +1,8 @@
1
- export { Compiler, createCompiler } from './compiler.ts';
2
- export type { Block, CompiledStylesheet, CompilerOptions, Library, PrependFiles, Stylesheet, } from './compiler.ts';
1
+ export { Compiler, createCompiler, interpolatedBlock } from './compiler.ts';
2
+ export type { Block, BlockVariable, ClassUsage, CompiledStylesheet, CompilerOptions, Library, PrependFiles, Source, Stylesheet, } from './compiler.ts';
3
3
  export { PreStyleError } from './block.ts';
4
4
  export type { Lang, LightningOptions, Position } from './block.ts';
5
+ export type { Slot } from './interpolate.ts';
5
6
  export type { Printed } from './print.ts';
6
7
  export { checkField, readConfig } from './config.ts';
7
8
  export type { PackageConfig } from './config.ts';
@@ -1,4 +1,4 @@
1
- export { Compiler, createCompiler } from './compiler.js';
1
+ export { Compiler, createCompiler, interpolatedBlock } from './compiler.js';
2
2
  export { PreStyleError } from './block.js';
3
3
  export { checkField, readConfig } from './config.js';
4
4
  export { findLibraries } from './libraries.js';
@@ -0,0 +1,11 @@
1
+ import type { Lang } from './block.ts';
2
+ export interface Slot {
3
+ strings: string[];
4
+ expressions: number[];
5
+ }
6
+ export interface Interpolated {
7
+ code: string;
8
+ slots: Slot[];
9
+ }
10
+ export declare const SLOT_PREFIX = "--__pre-style-slot-";
11
+ export declare function interpolate(strings: string[], expressions: string[], lang: Lang): Interpolated;