rainbowindex 0.4.0 → 0.4.1

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.
@@ -42,7 +42,91 @@ import {
42
42
  registerCustomTextSizes,
43
43
  registerCustomUtility,
44
44
  snapshotCompilationContext
45
- } from "./chunk-5Y7EXXLS.mjs";
45
+ } from "./chunk-KRZL4IDK.mjs";
46
+
47
+ // src/shared.ts
48
+ function isRIDebug() {
49
+ return typeof process !== "undefined" && !!process.env.RI_DEBUG;
50
+ }
51
+ function withTimeout(promise, ms, message) {
52
+ return new Promise((resolve, reject) => {
53
+ const timer = setTimeout(() => reject(new Error(message)), ms);
54
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
55
+ promise.then(
56
+ (value) => {
57
+ clearTimeout(timer);
58
+ resolve(value);
59
+ },
60
+ (err) => {
61
+ clearTimeout(timer);
62
+ reject(err);
63
+ }
64
+ );
65
+ });
66
+ }
67
+ function stripCSSComments(input) {
68
+ if (!input.includes("/")) return input;
69
+ const len = input.length;
70
+ let result = "";
71
+ let copyStart = 0;
72
+ let i = 0;
73
+ let quote = 0;
74
+ while (i < len) {
75
+ const c = input.charCodeAt(i);
76
+ if (quote !== 0) {
77
+ if (c === 92) {
78
+ i += 2;
79
+ continue;
80
+ }
81
+ if (c === quote) quote = 0;
82
+ i++;
83
+ continue;
84
+ }
85
+ if (c === 34 || c === 39) {
86
+ quote = c;
87
+ i++;
88
+ continue;
89
+ }
90
+ if (c === 47) {
91
+ const next = input.charCodeAt(i + 1);
92
+ if (next === 42) {
93
+ result += input.slice(copyStart, i);
94
+ result += " ";
95
+ const end = input.indexOf("*/", i + 2);
96
+ copyStart = end === -1 ? len : end + 2;
97
+ if (end === -1) break;
98
+ i = copyStart;
99
+ continue;
100
+ }
101
+ if (next === 47) {
102
+ result += input.slice(copyStart, i);
103
+ const end = input.indexOf("\n", i + 2);
104
+ copyStart = end === -1 ? len : end + 1;
105
+ if (end === -1) break;
106
+ result += "\n";
107
+ i = copyStart;
108
+ continue;
109
+ }
110
+ }
111
+ i++;
112
+ }
113
+ if (copyStart === 0) return input;
114
+ if (copyStart < len) result += input.slice(copyStart);
115
+ return result;
116
+ }
117
+ var BOUNDARY_WHITESPACE_RE = /\s/;
118
+ function isAtRuleBoundary(css, idx) {
119
+ if (idx === 0) return true;
120
+ const prev = css.charCodeAt(idx - 1);
121
+ if (prev === 32 || prev >= 9 && prev <= 13 || prev === 59 || prev === 123 || prev === 125)
122
+ return true;
123
+ if (prev === 47 && idx >= 2 && css.charCodeAt(idx - 2) === 42) return true;
124
+ return prev > 127 && BOUNDARY_WHITESPACE_RE.test(css[idx - 1]);
125
+ }
126
+ var CSS_CUSTOM_IDENT_RE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
127
+ function codepointCompare(a, b) {
128
+ return a < b ? -1 : a > b ? 1 : 0;
129
+ }
46
130
 
47
131
  // src/directives/foundation.ts
48
132
  var DIRECTIVE_TYPE_NAMES = [
@@ -71,7 +155,29 @@ var DIRECTIVE_TYPE_NAMES = [
71
155
  "register"
72
156
  ];
73
157
  var REMOVAL_KEY = "--ri-rm";
158
+ var IDENT_KEY_RE = /^[\w-]+$/;
74
159
  var WS_CHAR_RE = /\s/;
160
+ function topLevelIndexOf(str, char) {
161
+ let depth = 0;
162
+ for (let i = 0; i < str.length; i++) {
163
+ const c = str[i];
164
+ if (c === '"' || c === "'") {
165
+ i++;
166
+ while (i < str.length && str[i] !== c) {
167
+ if (str[i] === "\\") i++;
168
+ i++;
169
+ }
170
+ continue;
171
+ }
172
+ if (c === "(" || c === "[") depth++;
173
+ else if (c === ")" || c === "]") {
174
+ if (depth > 0) depth--;
175
+ } else if (c === char && depth === 0) {
176
+ return i;
177
+ }
178
+ }
179
+ return -1;
180
+ }
75
181
  function* scanEntries(src, opts) {
76
182
  let i = 0;
77
183
  while (i < src.length) {
@@ -133,7 +239,7 @@ function* scanEntries(src, opts) {
133
239
  const block = src.slice(i + 1, close).trim();
134
240
  i = close + 1;
135
241
  if (colonIdx === -1) {
136
- if (value) yield { key: "", value, fragment: true };
242
+ if (value) yield { key: "", value, fragment: true, block };
137
243
  continue;
138
244
  }
139
245
  if (key === REMOVAL_KEY) {
@@ -155,6 +261,70 @@ function* scanEntries(src, opts) {
155
261
  yield { key, value };
156
262
  }
157
263
  }
264
+ function parseKeyValueBody(body, warnings, directiveName) {
265
+ const entries = [];
266
+ const removals = [];
267
+ const cleanedBody = stripCSSComments(body);
268
+ const flush = (raw) => {
269
+ const line = raw.trim();
270
+ if (!line) return;
271
+ if (line.startsWith("!")) {
272
+ removals.push(line.slice(1).trim());
273
+ return;
274
+ }
275
+ const colonIdx = line.indexOf(":");
276
+ if (colonIdx === -1) return;
277
+ const key = line.slice(0, colonIdx).trim();
278
+ const value = line.slice(colonIdx + 1).trim();
279
+ if (key === REMOVAL_KEY && value) {
280
+ removals.push(value);
281
+ return;
282
+ }
283
+ if (key && !IDENT_KEY_RE.test(key)) {
284
+ warnings?.push(
285
+ `[RI-1035] Invalid @${directiveName ?? "directive"} entry key "${key}" \u2014 keys may only contain letters, numbers, hyphens, and underscores. The entry was skipped.`
286
+ );
287
+ return;
288
+ }
289
+ if (key && value) {
290
+ entries.push([key, value]);
291
+ }
292
+ };
293
+ let start = 0;
294
+ let depth = 0;
295
+ let lastNonWS = "";
296
+ for (let i = 0; i < cleanedBody.length; i++) {
297
+ const ch = cleanedBody[i];
298
+ if (ch === '"' || ch === "'") {
299
+ i++;
300
+ while (i < cleanedBody.length && cleanedBody[i] !== ch) {
301
+ if (cleanedBody[i] === "\\") i++;
302
+ i++;
303
+ }
304
+ lastNonWS = ch;
305
+ continue;
306
+ }
307
+ if (ch === "(" || ch === "[") {
308
+ depth++;
309
+ lastNonWS = ch;
310
+ continue;
311
+ }
312
+ if (ch === ")" || ch === "]") {
313
+ if (depth > 0) depth--;
314
+ lastNonWS = ch;
315
+ continue;
316
+ }
317
+ if (depth === 0 && (ch === ";" || ch === "\n" && lastNonWS !== ",")) {
318
+ flush(cleanedBody.slice(start, i));
319
+ start = i + 1;
320
+ lastNonWS = "";
321
+ continue;
322
+ }
323
+ if (!WS_CHAR_RE.test(ch)) lastNonWS = ch;
324
+ }
325
+ flush(cleanedBody.slice(start));
326
+ return { entries, removals };
327
+ }
158
328
  function findClosingBrace(src, start) {
159
329
  let depth = 1;
160
330
  for (let i = start + 1; i < src.length; i++) {
@@ -191,90 +361,6 @@ function hasApplyLikeDirective(src) {
191
361
  }
192
362
  var APPLY_LIKE_MATCH_RE = new RegExp(`@(?:${APPLY_LIKE_NAMES})\\s+([^;{}]+)`, "g");
193
363
 
194
- // src/shared.ts
195
- function isRIDebug() {
196
- return typeof process !== "undefined" && !!process.env.RI_DEBUG;
197
- }
198
- function withTimeout(promise, ms, message) {
199
- return new Promise((resolve, reject) => {
200
- const timer = setTimeout(() => reject(new Error(message)), ms);
201
- if (typeof timer === "object" && "unref" in timer) timer.unref();
202
- promise.then(
203
- (value) => {
204
- clearTimeout(timer);
205
- resolve(value);
206
- },
207
- (err) => {
208
- clearTimeout(timer);
209
- reject(err);
210
- }
211
- );
212
- });
213
- }
214
- function stripCSSComments(input) {
215
- if (!input.includes("/")) return input;
216
- const len = input.length;
217
- let result = "";
218
- let copyStart = 0;
219
- let i = 0;
220
- let quote = 0;
221
- while (i < len) {
222
- const c = input.charCodeAt(i);
223
- if (quote !== 0) {
224
- if (c === 92) {
225
- i += 2;
226
- continue;
227
- }
228
- if (c === quote) quote = 0;
229
- i++;
230
- continue;
231
- }
232
- if (c === 34 || c === 39) {
233
- quote = c;
234
- i++;
235
- continue;
236
- }
237
- if (c === 47) {
238
- const next = input.charCodeAt(i + 1);
239
- if (next === 42) {
240
- result += input.slice(copyStart, i);
241
- result += " ";
242
- const end = input.indexOf("*/", i + 2);
243
- copyStart = end === -1 ? len : end + 2;
244
- if (end === -1) break;
245
- i = copyStart;
246
- continue;
247
- }
248
- if (next === 47) {
249
- result += input.slice(copyStart, i);
250
- const end = input.indexOf("\n", i + 2);
251
- copyStart = end === -1 ? len : end + 1;
252
- if (end === -1) break;
253
- result += "\n";
254
- i = copyStart;
255
- continue;
256
- }
257
- }
258
- i++;
259
- }
260
- if (copyStart === 0) return input;
261
- if (copyStart < len) result += input.slice(copyStart);
262
- return result;
263
- }
264
- var BOUNDARY_WHITESPACE_RE = /\s/;
265
- function isAtRuleBoundary(css, idx) {
266
- if (idx === 0) return true;
267
- const prev = css.charCodeAt(idx - 1);
268
- if (prev === 32 || prev >= 9 && prev <= 13 || prev === 59 || prev === 123 || prev === 125)
269
- return true;
270
- if (prev === 47 && idx >= 2 && css.charCodeAt(idx - 2) === 42) return true;
271
- return prev > 127 && BOUNDARY_WHITESPACE_RE.test(css[idx - 1]);
272
- }
273
- var CSS_CUSTOM_IDENT_RE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
274
- function codepointCompare(a, b) {
275
- return a < b ? -1 : a > b ? 1 : 0;
276
- }
277
-
278
364
  // src/directives/activation.ts
279
365
  var DIRECTIVE_NAMES_SET = /* @__PURE__ */ new Set([
280
366
  ...DIRECTIVE_TYPE_NAMES,
@@ -445,77 +531,8 @@ function mixColorAlpha(color, alpha) {
445
531
  return `color-mix(in oklab, ${color} ${alpha}, transparent)`;
446
532
  }
447
533
 
448
- // src/integrations/font-providers/model.ts
449
- var SAFE_FONT_FAMILY_CHARS = "a-zA-Z0-9 ._-";
450
- var SAFE_FONT_FAMILY_RE = new RegExp(`^[${SAFE_FONT_FAMILY_CHARS}]+$`);
451
- var GOOGLE_DEFAULT_WEIGHT = "100 900";
452
- var GOOGLE_DEFAULT_STYLE = "normal italic";
453
- function kindFromProvider(provider) {
454
- if (provider === "google") return "google";
455
- if (provider === "system") return "system";
456
- if (provider === "") return "manual";
457
- return "local";
458
- }
459
- function createFontFace(partial) {
460
- const isGoogle = partial.provider === "google";
461
- return {
462
- weight: isGoogle ? GOOGLE_DEFAULT_WEIGHT : "400",
463
- style: isGoogle ? GOOGLE_DEFAULT_STYLE : "normal",
464
- display: "swap",
465
- subset: "latin",
466
- ...partial
467
- };
468
- }
469
- function createFontSlot(partial) {
470
- const defaultProvider = partial.kind === "google" ? "google" : partial.kind === "system" ? "system" : "";
471
- const faces = partial.faces && partial.faces.length > 0 ? partial.faces.map((f) => ({ ...f })) : [createFontFace({ provider: defaultProvider })];
472
- return {
473
- slot: partial.slot,
474
- family: partial.family,
475
- kind: partial.kind ?? kindFromProvider(faces[0].provider),
476
- fallback: [...partial.fallback ?? []],
477
- features: partial.features ?? null,
478
- variation: partial.variation ?? null,
479
- preload: partial.preload ?? false,
480
- faces,
481
- metricsFallback: partial.metricsFallback,
482
- sizeAdjust: partial.sizeAdjust,
483
- ascent: partial.ascent,
484
- descent: partial.descent,
485
- lineGap: partial.lineGap
486
- };
487
- }
488
-
489
- // src/directives/parsers.ts
490
- var UNSAFE_FONT_FAMILY_CHARS_RE = new RegExp(`[^${SAFE_FONT_FAMILY_CHARS}]`, "g");
491
- var MAX_UTILITY_BODY_LENGTH = 1e4;
492
- var MAX_CUSTOM_SELECTOR_LENGTH = 2e3;
534
+ // src/directives/color.ts
493
535
  var MAX_COLOR_ENTRIES = 500;
494
- var MAX_FONT_CONFIGS = 20;
495
- var CUSTOM_VARIANT_NAME_RE = /^[a-z][\w-]*$/;
496
- var IDENT_KEY_RE = /^[\w-]+$/;
497
- var WS_CHAR_RE2 = /\s/;
498
- function topLevelIndexOf(str, char) {
499
- let depth = 0;
500
- for (let i = 0; i < str.length; i++) {
501
- const c = str[i];
502
- if (c === '"' || c === "'") {
503
- i++;
504
- while (i < str.length && str[i] !== c) {
505
- if (str[i] === "\\") i++;
506
- i++;
507
- }
508
- continue;
509
- }
510
- if (c === "(" || c === "[") depth++;
511
- else if (c === ")" || c === "]") {
512
- if (depth > 0) depth--;
513
- } else if (c === char && depth === 0) {
514
- return i;
515
- }
516
- }
517
- return -1;
518
- }
519
536
  function expandColorStopRef(v) {
520
537
  if (v.includes("(") || v.startsWith("#")) return v;
521
538
  const m = v.match(/^([\w][\w-]*)-(\d+)$/);
@@ -548,147 +565,6 @@ function expandColorSide(side) {
548
565
  if (Number.isNaN(num)) return expanded;
549
566
  return mixColorAlpha(expanded, clampAlphaPercent(num, isPercent));
550
567
  }
551
- var TRUTHY_FONT_VALUES = /* @__PURE__ */ new Set(["true", "yes", "on"]);
552
- var FACE_DEFAULT_KEYS = /* @__PURE__ */ new Set([
553
- "weight",
554
- "style",
555
- "display",
556
- "subset",
557
- "unicodeRange",
558
- "unicode-range"
559
- ]);
560
- function applyFaceOptions(face, entries) {
561
- for (const [key, value] of entries) {
562
- switch (key) {
563
- case "weight":
564
- face.weight = value;
565
- face._weightExplicit = true;
566
- break;
567
- case "style":
568
- face.style = value;
569
- face._styleExplicit = true;
570
- break;
571
- case "display":
572
- face.display = value;
573
- break;
574
- case "subset":
575
- face.subset = value;
576
- break;
577
- case "unicodeRange":
578
- case "unicode-range":
579
- face.unicodeRange = value;
580
- break;
581
- case "preload":
582
- face.preload = TRUTHY_FONT_VALUES.has(value);
583
- break;
584
- }
585
- }
586
- }
587
- function applySlotOptions(slot, entries) {
588
- for (const [key, value] of entries) {
589
- switch (key) {
590
- case "fallback":
591
- slot.fallback = value.split(",").map((s) => s.trim());
592
- break;
593
- case "features":
594
- slot.features = value;
595
- break;
596
- case "variation":
597
- slot.variation = value;
598
- break;
599
- case "preload":
600
- slot.preload = TRUTHY_FONT_VALUES.has(value);
601
- break;
602
- case "metricsFallback":
603
- slot.metricsFallback = value.replace(/["']/g, "");
604
- break;
605
- case "sizeAdjust": {
606
- const n = Number.parseFloat(value);
607
- if (!Number.isNaN(n)) slot.sizeAdjust = n;
608
- break;
609
- }
610
- case "ascent": {
611
- const n = Number.parseFloat(value);
612
- if (!Number.isNaN(n)) slot.ascent = n;
613
- break;
614
- }
615
- case "descent": {
616
- const n = Number.parseFloat(value);
617
- if (!Number.isNaN(n)) slot.descent = n;
618
- break;
619
- }
620
- case "lineGap": {
621
- const n = Number.parseFloat(value);
622
- if (!Number.isNaN(n)) slot.lineGap = n;
623
- break;
624
- }
625
- }
626
- }
627
- }
628
- function parseKeyValueBody(body, warnings, directiveName) {
629
- const entries = [];
630
- const removals = [];
631
- const cleanedBody = stripCSSComments(body);
632
- const flush = (raw) => {
633
- const line = raw.trim();
634
- if (!line) return;
635
- if (line.startsWith("!")) {
636
- removals.push(line.slice(1).trim());
637
- return;
638
- }
639
- const colonIdx = line.indexOf(":");
640
- if (colonIdx === -1) return;
641
- const key = line.slice(0, colonIdx).trim();
642
- const value = line.slice(colonIdx + 1).trim();
643
- if (key === REMOVAL_KEY && value) {
644
- removals.push(value);
645
- return;
646
- }
647
- if (key && !IDENT_KEY_RE.test(key)) {
648
- warnings?.push(
649
- `[RI-1035] Invalid @${directiveName ?? "directive"} entry key "${key}" \u2014 keys may only contain letters, numbers, hyphens, and underscores. The entry was skipped.`
650
- );
651
- return;
652
- }
653
- if (key && value) {
654
- entries.push([key, value]);
655
- }
656
- };
657
- let start = 0;
658
- let depth = 0;
659
- let lastNonWS = "";
660
- for (let i = 0; i < cleanedBody.length; i++) {
661
- const ch = cleanedBody[i];
662
- if (ch === '"' || ch === "'") {
663
- i++;
664
- while (i < cleanedBody.length && cleanedBody[i] !== ch) {
665
- if (cleanedBody[i] === "\\") i++;
666
- i++;
667
- }
668
- lastNonWS = ch;
669
- continue;
670
- }
671
- if (ch === "(" || ch === "[") {
672
- depth++;
673
- lastNonWS = ch;
674
- continue;
675
- }
676
- if (ch === ")" || ch === "]") {
677
- if (depth > 0) depth--;
678
- lastNonWS = ch;
679
- continue;
680
- }
681
- if (depth === 0 && (ch === ";" || ch === "\n" && lastNonWS !== ",")) {
682
- flush(cleanedBody.slice(start, i));
683
- start = i + 1;
684
- lastNonWS = "";
685
- continue;
686
- }
687
- if (!WS_CHAR_RE2.test(ch)) lastNonWS = ch;
688
- }
689
- flush(cleanedBody.slice(start));
690
- return { entries, removals };
691
- }
692
568
  function parseColorBody(body, warnings) {
693
569
  const cleanedBody = stripCSSComments(body);
694
570
  const colors = {};
@@ -759,104 +635,602 @@ function parseColorBody(body, warnings) {
759
635
  `[RI-1108] @color "${key}" has an options block but its value is not generative \u2014 dark/inline/parabolic options only apply to "chroma hue" colors and were ignored.`
760
636
  );
761
637
  }
762
- if (!Object.hasOwn(colors, key)) colorCount++;
763
- colors[key] = def;
638
+ if (!Object.hasOwn(colors, key)) colorCount++;
639
+ colors[key] = def;
640
+ }
641
+ return { colors, removals };
642
+ }
643
+ function parseColorValue(key, value, warnings) {
644
+ const slashIdx = findPairSeparator(value);
645
+ if (slashIdx !== -1) {
646
+ const light = expandColorSide(value.slice(0, slashIdx).trim());
647
+ const dark = expandColorSide(value.slice(slashIdx + 1).trim());
648
+ return { type: "pair", light, dark };
649
+ }
650
+ if (/^light-dark\s*\(/.test(value)) {
651
+ const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")")).trim();
652
+ const commaIdx = topLevelIndexOf(inner, ",");
653
+ if (commaIdx !== -1) {
654
+ const light = expandColorSide(inner.slice(0, commaIdx).trim());
655
+ const dark = expandColorSide(inner.slice(commaIdx + 1).trim());
656
+ return { type: "pair", light, dark };
657
+ }
658
+ }
659
+ if (value === "transparent" || value === "currentColor" || value === "inherit") {
660
+ return { type: "keyword", value };
661
+ }
662
+ if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value)) {
663
+ return { type: "explicit", value };
664
+ }
665
+ if (/^(oklch|oklab|rgb|hsl|hwb|lch|lab|color|color-mix|var)\s*\(/.test(value)) {
666
+ return { type: "explicit", value };
667
+ }
668
+ const parts = value.split(/\s+/);
669
+ if (parts.length === 2) {
670
+ const chroma = Number.parseFloat(parts[0]);
671
+ const hue = Number.parseFloat(parts[1]);
672
+ if (!Number.isNaN(chroma) && !Number.isNaN(hue)) {
673
+ if (chroma < 0 || chroma > 0.4) {
674
+ warnings?.push(
675
+ `[RI-1102] @color "${key}" chroma ${chroma} is outside the typical range [0, 0.4] \u2014 clamping to valid range.`
676
+ );
677
+ }
678
+ const clampedChroma = Math.max(0, Math.min(0.4, chroma));
679
+ const normalizedHue = (hue % 360 + 360) % 360;
680
+ return { type: "generative", chroma: clampedChroma, hue: normalizedHue };
681
+ }
682
+ warnings?.push(
683
+ `[RI-1101] Invalid @color value "${key}: ${value}" \u2014 expected "chroma hue" (e.g., "0.15 30") or a CSS color function.`
684
+ );
685
+ return null;
686
+ }
687
+ if (parts.length === 1) {
688
+ const expanded = expandColorSide(value);
689
+ if (expanded !== value) return { type: "explicit", value: expanded };
690
+ if (/^[\w-]+$/.test(value)) return { type: "alias", source: value };
691
+ }
692
+ warnings?.push(
693
+ `[RI-1101] Invalid @color value "${key}: ${value}" \u2014 expected "chroma hue" (e.g., "0.15 30"), a color name alias, or a CSS color function.`
694
+ );
695
+ return null;
696
+ }
697
+ function parseDarkOverrideValue(value) {
698
+ const val = value.trim();
699
+ if (val === "mirror") return { strategy: "mirror" };
700
+ if (val === "fixed") return { strategy: "fixed" };
701
+ if (val.startsWith("shift")) {
702
+ const chromaMatch = val.match(/chroma\s+([+-]?\d+(?:\.\d+)?)/);
703
+ const hueMatch = val.match(/hue\s+([+-]?\d+(?:\.\d+)?)/);
704
+ const chromaDelta = chromaMatch ? Number.parseFloat(chromaMatch[1]) : 0;
705
+ const hueDelta = hueMatch ? Number.parseFloat(hueMatch[1]) : 0;
706
+ return {
707
+ strategy: "shift",
708
+ chromaDelta: Number.isNaN(chromaDelta) ? 0 : chromaDelta,
709
+ hueDelta: Number.isNaN(hueDelta) ? 0 : hueDelta
710
+ };
711
+ }
712
+ return void 0;
713
+ }
714
+ function findPairSeparator(value) {
715
+ let depth = 0;
716
+ for (let i = 0; i < value.length; i++) {
717
+ if (value[i] === "(" || value[i] === "[") depth++;
718
+ else if (value[i] === ")" || value[i] === "]") {
719
+ depth--;
720
+ if (depth < 0) return -1;
721
+ } else if (value[i] === "/" && depth === 0) {
722
+ const prev = value[i - 1];
723
+ const next = value[i + 1];
724
+ if (prev !== void 0 && !/\s/.test(prev) && next !== void 0 && /[\d.]/.test(next)) {
725
+ continue;
726
+ }
727
+ const before = value.slice(0, i).trim();
728
+ const after = value.slice(i + 1).trim();
729
+ if (before && after) {
730
+ return i;
731
+ }
732
+ }
733
+ }
734
+ return -1;
735
+ }
736
+
737
+ // src/integrations/font-providers/model.ts
738
+ var SAFE_FONT_FAMILY_CHARS = "a-zA-Z0-9 ._-";
739
+ var SAFE_FONT_FAMILY_RE = new RegExp(`^[${SAFE_FONT_FAMILY_CHARS}]+$`);
740
+ var GOOGLE_DEFAULT_WEIGHT = "100 900";
741
+ var GOOGLE_DEFAULT_STYLE = "normal italic";
742
+ function kindFromProvider(provider) {
743
+ if (provider === "google") return "google";
744
+ if (provider === "system") return "system";
745
+ if (provider === "") return "manual";
746
+ return "local";
747
+ }
748
+ function createFontFace(partial) {
749
+ const isGoogle = partial.provider === "google";
750
+ return {
751
+ weight: isGoogle ? GOOGLE_DEFAULT_WEIGHT : "400",
752
+ style: isGoogle ? GOOGLE_DEFAULT_STYLE : "normal",
753
+ display: "swap",
754
+ ...partial
755
+ };
756
+ }
757
+ function createFontSlot(partial) {
758
+ const defaultProvider = partial.kind === "google" ? "google" : partial.kind === "system" ? "system" : "";
759
+ const faces = partial.faces && partial.faces.length > 0 ? partial.faces.map((f) => ({ ...f })) : [createFontFace({ provider: defaultProvider })];
760
+ const slot = {
761
+ slot: partial.slot,
762
+ family: partial.family,
763
+ kind: partial.kind ?? kindFromProvider(faces[0].provider),
764
+ fallback: [...partial.fallback ?? []],
765
+ features: partial.features ?? null,
766
+ variation: partial.variation ?? null,
767
+ faces
768
+ };
769
+ if (partial.metrics !== void 0) slot.metrics = partial.metrics;
770
+ return slot;
771
+ }
772
+
773
+ // src/directives/font.ts
774
+ var UNSAFE_FONT_FAMILY_CHARS_RE = new RegExp(`[^${SAFE_FONT_FAMILY_CHARS}]`, "g");
775
+ var MAX_FONT_CONFIGS = 20;
776
+ var TRUTHY_FONT_VALUES = /* @__PURE__ */ new Set(["true", "yes", "on"]);
777
+ function stripQuotes(s) {
778
+ return s.replace(/["']/g, "");
779
+ }
780
+ function sanitizeFamily(family) {
781
+ return family.replace(UNSAFE_FONT_FAMILY_CHARS_RE, "");
782
+ }
783
+ function sanitizeFallbacks(parts) {
784
+ return parts.map((s) => SAFE_FONT_FAMILY_RE.test(s) ? s : sanitizeFamily(s).trim()).filter((s) => s.length > 0);
785
+ }
786
+ function isUnsafeCSSValue(value) {
787
+ if (value.includes("\0")) return true;
788
+ for (let i = 0; i < value.length; i++) {
789
+ const c = value[i];
790
+ if (c === '"' || c === "'") {
791
+ i++;
792
+ while (i < value.length && value[i] !== c) {
793
+ if (value[i] === "\\") i++;
794
+ i++;
795
+ }
796
+ if (i >= value.length) return true;
797
+ continue;
798
+ }
799
+ if (c === "}") return true;
800
+ }
801
+ return false;
802
+ }
803
+ function dropUnsafeValue(key, value, slot, warnings) {
804
+ if (!isUnsafeCSSValue(value)) return false;
805
+ warnings?.push(
806
+ `[RI-1217] @font option "${key}" in slot "${slot}" has a value that can't be emitted safely into CSS \u2014 the entry was ignored.`
807
+ );
808
+ return true;
809
+ }
810
+ function warnDeprecated(warnings, slot, oldForm, replacement) {
811
+ warnings?.push(
812
+ `[RI-1218] @font slot "${slot}": ${oldForm} is deprecated \u2014 ${replacement}. The old form still works but will be removed.`
813
+ );
814
+ }
815
+ function topLevelFromIndex(s) {
816
+ let last = -1;
817
+ for (let i = 0; i < s.length; i++) {
818
+ const c = s[i];
819
+ if (c === '"' || c === "'") {
820
+ i++;
821
+ while (i < s.length && s[i] !== c) {
822
+ if (s[i] === "\\") i++;
823
+ i++;
824
+ }
825
+ continue;
826
+ }
827
+ if (c === "f" && i > 0 && s[i - 1] === " " && s.startsWith("from ", i)) last = i;
828
+ }
829
+ return last;
830
+ }
831
+ function splitTopLevelCommas(s) {
832
+ const parts = [];
833
+ let rest = s;
834
+ for (; ; ) {
835
+ const i = topLevelIndexOf(rest, ",");
836
+ if (i === -1) {
837
+ parts.push(rest);
838
+ return parts;
839
+ }
840
+ parts.push(rest.slice(0, i));
841
+ rest = rest.slice(i + 1);
842
+ }
843
+ }
844
+ function parsePreamble(text, slot, warnings) {
845
+ const pre = text.replace(/\s+/g, " ").trim();
846
+ if (pre === "system") return { system: true, family: "", fallback: [] };
847
+ let stackText = pre;
848
+ let provider;
849
+ const fromIdx = topLevelFromIndex(pre);
850
+ if (fromIdx !== -1) {
851
+ const tail = stripQuotes(pre.slice(fromIdx + 4).trim());
852
+ if (tail) {
853
+ provider = tail;
854
+ stackText = pre.slice(0, fromIdx).trim();
855
+ }
856
+ }
857
+ const parts = splitTopLevelCommas(stackText).map((s) => stripQuotes(s.trim()).trim());
858
+ const family = parts[0] ?? "";
859
+ const fallback = sanitizeFallbacks(parts.slice(1));
860
+ if (provider === void 0 || provider === "google") {
861
+ return provider === "google" ? { family, fallback, google: true } : { family, fallback };
862
+ }
863
+ if (provider === "system") {
864
+ warnDeprecated(warnings, slot, "`from system`", "use the bare `system` keyword");
865
+ return { system: true, family: "", fallback: [] };
866
+ }
867
+ warnDeprecated(
868
+ warnings,
869
+ slot,
870
+ `\`from "${provider}"\``,
871
+ `declare the file as a face entry (\`face: ${provider};\`)`
872
+ );
873
+ return { family, fallback, legacyFaceSrc: provider };
874
+ }
875
+ function parseMetricsValue(value, slot, warnings) {
876
+ if (value === "none") return null;
877
+ const tokens = value.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
878
+ const familyParts = [];
879
+ const nums = [];
880
+ let malformed = false;
881
+ for (const t of tokens) {
882
+ const n = Number.parseFloat(t);
883
+ if (Number.isNaN(n)) {
884
+ if (nums.length > 0) {
885
+ malformed = true;
886
+ break;
887
+ }
888
+ familyParts.push(stripQuotes(t));
889
+ } else {
890
+ nums.push(n);
891
+ }
892
+ }
893
+ const fallbackName = familyParts.join(" ").trim();
894
+ if (malformed || nums.length !== 0 && nums.length !== 4 || familyParts.length > 0 && fallbackName === "") {
895
+ warnings?.push(
896
+ `[RI-1220] @font slot "${slot}" has an invalid metrics value "${value}" \u2014 use \`metrics: none\`, \`metrics: "<local font>"\`, or \`metrics: "<local font>" <size-adjust> <ascent> <descent> <line-gap>\` (four percentages). The entry was ignored.`
897
+ );
898
+ return void 0;
899
+ }
900
+ const cfg = {};
901
+ if (fallbackName) cfg.fallback = fallbackName;
902
+ if (nums.length === 4) {
903
+ cfg.sizeAdjust = nums[0];
904
+ cfg.ascent = nums[1];
905
+ cfg.descent = nums[2];
906
+ cfg.lineGap = nums[3];
907
+ }
908
+ return cfg;
909
+ }
910
+ function normalizeFaceEntries(entries, slot, warnings) {
911
+ const out = [];
912
+ for (const [key, value] of entries) {
913
+ if (key === "unicodeRange") {
914
+ warnDeprecated(warnings, slot, "`unicodeRange:`", "use the CSS spelling `unicode-range:`");
915
+ out.push(["unicode-range", value]);
916
+ } else if (key === "subset") {
917
+ warnDeprecated(
918
+ warnings,
919
+ slot,
920
+ "`subset:`",
921
+ "remove the entry (the Google css2 API takes no subset hint)"
922
+ );
923
+ } else {
924
+ out.push([key, value]);
925
+ }
926
+ }
927
+ return out;
928
+ }
929
+ function applyFaceOptions(face, entries, slot, warnings) {
930
+ for (const [key, value] of entries) {
931
+ if (dropUnsafeValue(key, value, slot, warnings)) continue;
932
+ switch (key) {
933
+ case "weight":
934
+ face.weight = value;
935
+ face._weightExplicit = true;
936
+ break;
937
+ case "style":
938
+ face.style = value;
939
+ face._styleExplicit = true;
940
+ break;
941
+ case "display":
942
+ face.display = value;
943
+ break;
944
+ case "unicode-range":
945
+ face.unicodeRange = value;
946
+ break;
947
+ case "preload":
948
+ face.preload = TRUTHY_FONT_VALUES.has(value);
949
+ break;
950
+ case "src":
951
+ warnings?.push(
952
+ `[RI-1217] Unknown @font option "src" in slot "${slot}" \u2014 the face source is the face: value itself (\`face: /path.woff2 { \u2026 }\`). The entry was ignored.`
953
+ );
954
+ break;
955
+ default:
956
+ warnings?.push(
957
+ `[RI-1217] Unknown @font option "${key}" in slot "${slot}" \u2014 the entry was ignored.`
958
+ );
959
+ break;
960
+ }
961
+ }
962
+ }
963
+ var LEGACY_METRICS_KEYS = /* @__PURE__ */ new Set([
964
+ "metricsFallback",
965
+ "sizeAdjust",
966
+ "ascent",
967
+ "descent",
968
+ "lineGap"
969
+ ]);
970
+ function parseSlotBody(body, slot, warnings) {
971
+ const out = { faceDefaults: [], faces: [], legacyMetrics: [] };
972
+ for (const entry of scanEntries(body, { newlineTerminates: true })) {
973
+ if (entry.unclosedBlock) {
974
+ warnings?.push(
975
+ `[RI-1217] @font slot "${slot}" has an unterminated { block after "${entry.key || entry.value}" \u2014 the entry was ignored.`
976
+ );
977
+ continue;
978
+ }
979
+ if (entry.fragment) {
980
+ if (entry.value === "@face" && entry.block !== void 0) {
981
+ warnDeprecated(warnings, slot, "`@face { src: \u2026; }`", "use `face: <src> { \u2026 }`");
982
+ let src = "";
983
+ const own = [];
984
+ for (const [k, v] of parseKeyValueBody(entry.block, warnings, "font").entries) {
985
+ if (k === "src") src = stripQuotes(v);
986
+ else own.push([k, v]);
987
+ }
988
+ out.faces.push({ src, entries: own });
989
+ } else if (entry.value) {
990
+ warnings?.push(
991
+ `[RI-1217] @font slot "${slot}" has a stray value "${entry.value.slice(0, 60)}" with no key \u2014 if it continues the previous entry's value, keep the entry on one line. The text was ignored.`
992
+ );
993
+ }
994
+ continue;
995
+ }
996
+ if (entry.removal || !entry.key) continue;
997
+ if (!IDENT_KEY_RE.test(entry.key)) {
998
+ warnings?.push(
999
+ `[RI-1035] Invalid @font entry key "${entry.key}" \u2014 keys may only contain letters, numbers, hyphens, and underscores. The entry was skipped.`
1000
+ );
1001
+ continue;
1002
+ }
1003
+ if (entry.block !== void 0 && entry.key !== "face") {
1004
+ warnings?.push(
1005
+ `[RI-1217] @font option "${entry.key}" in slot "${slot}" takes no { \u2026 } block \u2014 the block was ignored.`
1006
+ );
1007
+ }
1008
+ if (entry.key === "face" && !entry.value) {
1009
+ warnings?.push(
1010
+ `[RI-1217] @font slot "${slot}" has a face: entry with no source \u2014 use \`face: <src> [{ \u2026 }]\`. The entry was ignored.`
1011
+ );
1012
+ continue;
1013
+ }
1014
+ if (!entry.value) continue;
1015
+ if (LEGACY_METRICS_KEYS.has(entry.key)) {
1016
+ out.legacyMetrics.push([entry.key, entry.value]);
1017
+ continue;
1018
+ }
1019
+ switch (entry.key) {
1020
+ case "face":
1021
+ out.faces.push({
1022
+ src: stripQuotes(entry.value),
1023
+ entries: entry.block !== void 0 ? parseKeyValueBody(entry.block, warnings, "font").entries : []
1024
+ });
1025
+ break;
1026
+ case "features":
1027
+ if (!dropUnsafeValue(entry.key, entry.value, slot, warnings)) out.features = entry.value;
1028
+ break;
1029
+ case "variation":
1030
+ if (!dropUnsafeValue(entry.key, entry.value, slot, warnings)) out.variation = entry.value;
1031
+ break;
1032
+ case "metrics": {
1033
+ const cfg = parseMetricsValue(entry.value, slot, warnings);
1034
+ if (cfg !== void 0) out.metrics = cfg;
1035
+ break;
1036
+ }
1037
+ case "weight":
1038
+ case "style":
1039
+ case "display":
1040
+ case "unicode-range":
1041
+ case "preload":
1042
+ out.faceDefaults.push([entry.key, entry.value]);
1043
+ break;
1044
+ case "italic":
1045
+ warnDeprecated(warnings, slot, "`italic: <src>`", "use `face: <src> { style: italic; }`");
1046
+ out.faces.push({ src: stripQuotes(entry.value), entries: [["style", "italic"]] });
1047
+ break;
1048
+ case "fallback":
1049
+ warnDeprecated(
1050
+ warnings,
1051
+ slot,
1052
+ "`fallback:`",
1053
+ 'list fallbacks after the family in the slot preamble (`sans: "Inter", ui-sans-serif from google;`)'
1054
+ );
1055
+ out.fallbackOverride = sanitizeFallbacks(
1056
+ splitTopLevelCommas(entry.value).map((s) => stripQuotes(s.trim()).trim())
1057
+ );
1058
+ break;
1059
+ case "unicodeRange":
1060
+ warnDeprecated(warnings, slot, "`unicodeRange:`", "use the CSS spelling `unicode-range:`");
1061
+ out.faceDefaults.push(["unicode-range", entry.value]);
1062
+ break;
1063
+ case "subset":
1064
+ warnDeprecated(
1065
+ warnings,
1066
+ slot,
1067
+ "`subset:`",
1068
+ "remove the entry (the Google css2 API takes no subset hint)"
1069
+ );
1070
+ break;
1071
+ default:
1072
+ warnings?.push(
1073
+ `[RI-1217] Unknown @font option "${entry.key}" in slot "${slot}" \u2014 the entry was ignored.`
1074
+ );
1075
+ break;
1076
+ }
764
1077
  }
765
- return { colors, removals };
1078
+ foldLegacyMetrics(out, slot, warnings);
1079
+ return out;
766
1080
  }
767
- function parseColorValue(key, value, warnings) {
768
- const slashIdx = findPairSeparator(value);
769
- if (slashIdx !== -1) {
770
- const light = expandColorSide(value.slice(0, slashIdx).trim());
771
- const dark = expandColorSide(value.slice(slashIdx + 1).trim());
772
- return { type: "pair", light, dark };
1081
+ function foldLegacyMetrics(body, slot, warnings) {
1082
+ if (body.legacyMetrics.length === 0) return;
1083
+ warnDeprecated(
1084
+ warnings,
1085
+ slot,
1086
+ "the metricsFallback/sizeAdjust/ascent/descent/lineGap keys",
1087
+ 'use the single `metrics:` key (e.g. `metrics: "Arial" 107.64 90.49 22.48 0;`) or omit it for automatic metrics'
1088
+ );
1089
+ if (body.metrics !== void 0) return;
1090
+ const cfg = {};
1091
+ for (const [key, value] of body.legacyMetrics) {
1092
+ if (key === "metricsFallback") {
1093
+ cfg.fallback = stripQuotes(value);
1094
+ } else {
1095
+ const n = Number.parseFloat(value);
1096
+ if (!Number.isNaN(n)) cfg[key] = n;
1097
+ }
773
1098
  }
774
- if (/^light-dark\s*\(/.test(value)) {
775
- const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")")).trim();
776
- const commaIdx = topLevelIndexOf(inner, ",");
777
- if (commaIdx !== -1) {
778
- const light = expandColorSide(inner.slice(0, commaIdx).trim());
779
- const dark = expandColorSide(inner.slice(commaIdx + 1).trim());
780
- return { type: "pair", light, dark };
1099
+ const numeric = [cfg.sizeAdjust, cfg.ascent, cfg.descent, cfg.lineGap].filter(
1100
+ (n) => n !== void 0
1101
+ ).length;
1102
+ if (numeric === 4 || numeric === 0 && cfg.fallback !== void 0) {
1103
+ body.metrics = cfg;
1104
+ } else if (numeric > 0) {
1105
+ warnings?.push(
1106
+ `[RI-1220] @font slot "${slot}" sets only ${numeric} of the four metric overrides \u2014 size-adjust, ascent, descent, and line-gap are all required. The values were ignored (automatic metrics still apply).`
1107
+ );
1108
+ }
1109
+ }
1110
+ function buildFace(provider, defaults, slot, warnings, own) {
1111
+ const face = createFontFace({ provider });
1112
+ applyFaceOptions(face, defaults, slot, warnings);
1113
+ if (own) applyFaceOptions(face, own, slot, warnings);
1114
+ return face;
1115
+ }
1116
+ function warnDuplicateFaces(slot, warnings) {
1117
+ if (!warnings || slot.faces.length < 2) return;
1118
+ const seen = /* @__PURE__ */ new Set();
1119
+ for (const face of slot.faces) {
1120
+ const key = `${face.weight}|${face.style}`;
1121
+ if (seen.has(key)) {
1122
+ warnings.push(
1123
+ `[RI-1214] @font slot "${slot.slot}" has duplicate faces with weight "${face.weight}" and style "${face.style}" \u2014 the later @font-face wins. Remove the duplicate or give it a distinct weight/style.`
1124
+ );
781
1125
  }
1126
+ seen.add(key);
782
1127
  }
783
- if (value === "transparent" || value === "currentColor" || value === "inherit") {
784
- return { type: "keyword", value };
1128
+ }
1129
+ function buildSlot(slot, preambleText, blockBody, warnings) {
1130
+ const pre = parsePreamble(preambleText, slot, warnings);
1131
+ const body = parseSlotBody(blockBody ?? "", slot, warnings);
1132
+ const preloadDefault = body.faceDefaults.some(([k]) => k === "preload");
1133
+ if (pre.system) {
1134
+ if (preloadDefault) {
1135
+ warnings?.push(
1136
+ `[RI-1219] @font slot "${slot}": preload has no effect on system fonts \u2014 only local font files can be preloaded.`
1137
+ );
1138
+ }
1139
+ return createFontSlot({ slot, family: "", kind: "system" });
785
1140
  }
786
- if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value)) {
787
- return { type: "explicit", value };
1141
+ if (!SAFE_FONT_FAMILY_RE.test(pre.family)) {
1142
+ if (pre.family === "") {
1143
+ warnings?.push(
1144
+ `[RI-1217] @font slot "${slot}" has no font family \u2014 the slot emits an empty font variable.`
1145
+ );
1146
+ }
1147
+ return createFontSlot({ slot, family: sanitizeFamily(pre.family), kind: "manual" });
788
1148
  }
789
- if (/^(oklch|oklab|rgb|hsl|hwb|lch|lab|color|color-mix|var)\s*\(/.test(value)) {
790
- return { type: "explicit", value };
1149
+ const srcFaces = pre.legacyFaceSrc ? [{ src: pre.legacyFaceSrc, entries: [] }, ...body.faces] : body.faces;
1150
+ let config;
1151
+ if (pre.google) {
1152
+ if (srcFaces.length > 0) {
1153
+ warnings?.push(
1154
+ `[RI-1204] @font slot "${slot}" loads "${pre.family}" from google but also declares local face entries \u2014 provider fonts can't be combined with local faces. The extra faces were ignored.`
1155
+ );
1156
+ }
1157
+ config = createFontSlot({
1158
+ slot,
1159
+ family: pre.family,
1160
+ kind: "google",
1161
+ fallback: pre.fallback,
1162
+ faces: [buildFace("google", body.faceDefaults, slot, warnings)]
1163
+ });
1164
+ } else if (srcFaces.length > 0) {
1165
+ config = createFontSlot({
1166
+ slot,
1167
+ family: pre.family,
1168
+ kind: "local",
1169
+ fallback: pre.fallback,
1170
+ faces: srcFaces.map(
1171
+ (f) => buildFace(
1172
+ f.src,
1173
+ body.faceDefaults,
1174
+ slot,
1175
+ warnings,
1176
+ normalizeFaceEntries(f.entries, slot, warnings)
1177
+ )
1178
+ )
1179
+ });
1180
+ warnDuplicateFaces(config, warnings);
1181
+ } else {
1182
+ config = createFontSlot({
1183
+ slot,
1184
+ family: pre.family,
1185
+ kind: "manual",
1186
+ fallback: pre.fallback,
1187
+ faces: [buildFace("", body.faceDefaults, slot, warnings)]
1188
+ });
791
1189
  }
792
- const parts = value.split(/\s+/);
793
- if (parts.length === 2) {
794
- const chroma = Number.parseFloat(parts[0]);
795
- const hue = Number.parseFloat(parts[1]);
796
- if (!Number.isNaN(chroma) && !Number.isNaN(hue)) {
797
- if (chroma < 0 || chroma > 0.4) {
798
- warnings?.push(
799
- `[RI-1102] @color "${key}" chroma ${chroma} is outside the typical range [0, 0.4] \u2014 clamping to valid range.`
800
- );
801
- }
802
- const clampedChroma = Math.max(0, Math.min(0.4, chroma));
803
- const normalizedHue = (hue % 360 + 360) % 360;
804
- return { type: "generative", chroma: clampedChroma, hue: normalizedHue };
1190
+ if (body.fallbackOverride) config.fallback = body.fallbackOverride;
1191
+ if (body.features !== void 0) config.features = body.features;
1192
+ if (body.variation !== void 0) config.variation = body.variation;
1193
+ if (body.metrics !== void 0) {
1194
+ if (config.kind === "manual") {
1195
+ warnings?.push(
1196
+ `[RI-1220] @font slot "${slot}" sets metrics on a manual font stack \u2014 metrics apply to loaded (google or local file) fonts only. The entry has no effect.`
1197
+ );
805
1198
  }
1199
+ config.metrics = body.metrics;
1200
+ }
1201
+ if (config.kind !== "local" && preloadDefault) {
806
1202
  warnings?.push(
807
- `[RI-1101] Invalid @color value "${key}: ${value}" \u2014 expected "chroma hue" (e.g., "0.15 30") or a CSS color function.`
1203
+ `[RI-1219] @font slot "${slot}": preload has no effect on ${config.kind} fonts \u2014 only local font files can be preloaded.`
808
1204
  );
809
- return null;
810
- }
811
- if (parts.length === 1) {
812
- const expanded = expandColorSide(value);
813
- if (expanded !== value) return { type: "explicit", value: expanded };
814
- if (/^[\w-]+$/.test(value)) return { type: "alias", source: value };
815
- }
816
- warnings?.push(
817
- `[RI-1101] Invalid @color value "${key}: ${value}" \u2014 expected "chroma hue" (e.g., "0.15 30"), a color name alias, or a CSS color function.`
818
- );
819
- return null;
820
- }
821
- function parseDarkOverrideValue(value) {
822
- const val = value.trim();
823
- if (val === "mirror") return { strategy: "mirror" };
824
- if (val === "fixed") return { strategy: "fixed" };
825
- if (val.startsWith("shift")) {
826
- const chromaMatch = val.match(/chroma\s+([+-]?\d+(?:\.\d+)?)/);
827
- const hueMatch = val.match(/hue\s+([+-]?\d+(?:\.\d+)?)/);
828
- const chromaDelta = chromaMatch ? Number.parseFloat(chromaMatch[1]) : 0;
829
- const hueDelta = hueMatch ? Number.parseFloat(hueMatch[1]) : 0;
830
- return {
831
- strategy: "shift",
832
- chromaDelta: Number.isNaN(chromaDelta) ? 0 : chromaDelta,
833
- hueDelta: Number.isNaN(hueDelta) ? 0 : hueDelta
834
- };
835
1205
  }
836
- return void 0;
1206
+ return config;
837
1207
  }
838
- function findPairSeparator(value) {
839
- let depth = 0;
840
- for (let i = 0; i < value.length; i++) {
841
- if (value[i] === "(" || value[i] === "[") depth++;
842
- else if (value[i] === ")" || value[i] === "]") {
843
- depth--;
844
- if (depth < 0) return -1;
845
- } else if (value[i] === "/" && depth === 0) {
846
- const prev = value[i - 1];
847
- const next = value[i + 1];
848
- if (prev !== void 0 && !/\s/.test(prev) && next !== void 0 && /[\d.]/.test(next)) {
849
- continue;
850
- }
851
- const before = value.slice(0, i).trim();
852
- const after = value.slice(i + 1).trim();
853
- if (before && after) {
854
- return i;
855
- }
1208
+ function parseNestedFontBlock(body, warnings) {
1209
+ const cleanedBody = stripCSSComments(body);
1210
+ const configs = [];
1211
+ for (const entry of scanEntries(cleanedBody, { newlineTerminates: false })) {
1212
+ if (entry.removal || entry.fragment) continue;
1213
+ if (entry.unclosedBlock) break;
1214
+ if (!entry.key) continue;
1215
+ if (configs.length >= MAX_FONT_CONFIGS) {
1216
+ warnings?.push(
1217
+ `[RI-1216] @font block exceeds ${MAX_FONT_CONFIGS} slot definitions \u2014 the remaining slots were skipped. Split rarely-used slots into a separate stylesheet or remove unused ones.`
1218
+ );
1219
+ break;
1220
+ }
1221
+ if (entry.block !== void 0) {
1222
+ configs.push(buildSlot(entry.key, entry.value, entry.block, warnings));
1223
+ } else if (entry.value) {
1224
+ configs.push(buildSlot(entry.key, entry.value, void 0, warnings));
856
1225
  }
857
1226
  }
858
- return -1;
1227
+ return configs;
859
1228
  }
1229
+
1230
+ // src/directives/parsers.ts
1231
+ var MAX_UTILITY_BODY_LENGTH = 1e4;
1232
+ var MAX_CUSTOM_SELECTOR_LENGTH = 2e3;
1233
+ var CUSTOM_VARIANT_NAME_RE = /^[a-z][\w-]*$/;
860
1234
  function parseTextBody(body, warnings) {
861
1235
  const { entries, removals } = parseKeyValueBody(body, warnings, "text");
862
1236
  const text = {};
@@ -941,186 +1315,6 @@ function parsePreflightDirective(body, modifier, base) {
941
1315
  }
942
1316
  return config;
943
1317
  }
944
- function sanitizeFamily(family) {
945
- return family.replace(UNSAFE_FONT_FAMILY_CHARS_RE, "");
946
- }
947
- function buildFace(provider, faceDefaults, forceStyle) {
948
- const face = createFontFace({ provider });
949
- applyFaceOptions(face, faceDefaults);
950
- if (forceStyle !== void 0) {
951
- face.style = forceStyle;
952
- face._styleExplicit = true;
953
- }
954
- return face;
955
- }
956
- function faceFromBlock(entries, faceDefaults) {
957
- let src = "";
958
- const own = [];
959
- for (const [k, v] of entries) {
960
- if (k === "src") src = v.replace(/["']/g, "");
961
- else own.push([k, v]);
962
- }
963
- const face = createFontFace({ provider: src });
964
- applyFaceOptions(face, faceDefaults);
965
- applyFaceOptions(face, own);
966
- return face;
967
- }
968
- function extractFaceBlocks(body, warnings) {
969
- const faceBlocks = [];
970
- let rest = "";
971
- let i = 0;
972
- while (i < body.length) {
973
- const at = body.indexOf("@face", i);
974
- if (at === -1) {
975
- rest += body.slice(i);
976
- break;
977
- }
978
- rest += body.slice(i, at);
979
- let j = at + 5;
980
- while (j < body.length && /\s/.test(body[j])) j++;
981
- if (body[j] !== "{") {
982
- rest += body.slice(at, j);
983
- i = j;
984
- continue;
985
- }
986
- const close = findClosingBrace(body, j);
987
- if (close === -1) {
988
- rest += body.slice(at);
989
- break;
990
- }
991
- faceBlocks.push(parseKeyValueBody(body.slice(j + 1, close), warnings, "font").entries);
992
- i = close + 1;
993
- }
994
- return { faceBlocks, rest };
995
- }
996
- function processSlotBlock(blockBody, warnings) {
997
- const { faceBlocks, rest } = extractFaceBlocks(blockBody, warnings);
998
- const { entries } = parseKeyValueBody(rest, warnings, "font");
999
- const faceDefaults = [];
1000
- const slotOpts = [];
1001
- const italicSrcs = [];
1002
- for (const [k, v] of entries) {
1003
- if (k === "italic") italicSrcs.push(v.replace(/["']/g, ""));
1004
- else if (FACE_DEFAULT_KEYS.has(k)) faceDefaults.push([k, v]);
1005
- else slotOpts.push([k, v]);
1006
- }
1007
- return { faceDefaults, slotOpts, faceBlocks, italicSrcs };
1008
- }
1009
- function buildFaces(primaryProvider, block) {
1010
- const faces = [];
1011
- if (primaryProvider !== null) faces.push(buildFace(primaryProvider, block.faceDefaults));
1012
- for (const fb of block.faceBlocks) faces.push(faceFromBlock(fb, block.faceDefaults));
1013
- for (const src of block.italicSrcs) faces.push(buildFace(src, block.faceDefaults, "italic"));
1014
- if (faces.length === 0) faces.push(buildFace("", block.faceDefaults));
1015
- return faces;
1016
- }
1017
- function warnDuplicateFaces(slot, warnings) {
1018
- if (!warnings || slot.faces.length < 2) return;
1019
- const seen = /* @__PURE__ */ new Set();
1020
- for (const face of slot.faces) {
1021
- const key = `${face.weight}|${face.style}`;
1022
- if (seen.has(key)) {
1023
- warnings.push(
1024
- `[RI-1214] @font slot "${slot.slot}" has duplicate faces with weight "${face.weight}" and style "${face.style}" \u2014 the later @font-face wins. Remove the duplicate or give it a distinct weight/style.`
1025
- );
1026
- }
1027
- seen.add(key);
1028
- }
1029
- }
1030
- function parseFontBody(body, slot, warnings) {
1031
- const normalizedBody = stripCSSComments(body).trim();
1032
- if (normalizedBody === "system") {
1033
- return createFontSlot({ slot, family: "", kind: "system" });
1034
- }
1035
- const braceIdx = topLevelIndexOf(normalizedBody, "{");
1036
- const matchable = braceIdx === -1 ? normalizedBody.replace(/\s+/g, " ") : normalizedBody.slice(0, braceIdx).replace(/\s+/g, " ") + normalizedBody.slice(braceIdx);
1037
- const fromMatch = matchable.match(/^(["'])(.+?)\1\s+from\s+(.+?)(?:\s*\{([\s\S]*)\})?$/);
1038
- if (fromMatch) {
1039
- const family = fromMatch[2];
1040
- if (!SAFE_FONT_FAMILY_RE.test(family)) {
1041
- return createFontSlot({ slot, family: sanitizeFamily(family), kind: "manual" });
1042
- }
1043
- const provider = fromMatch[3].trim().replace(/["']/g, "");
1044
- const kind = kindFromProvider(provider);
1045
- const block = processSlotBlock(fromMatch[4] || "", warnings);
1046
- if (kind === "google" || kind === "system") {
1047
- if (block.faceBlocks.length > 0 || block.italicSrcs.length > 0) {
1048
- warnings?.push(
1049
- `[RI-1204] @font slot "${slot}" loads "${family}" from "${provider}" but also declares @face/italic faces \u2014 provider fonts can't be combined with local faces. The extra faces were ignored.`
1050
- );
1051
- }
1052
- const config2 = createFontSlot({
1053
- slot,
1054
- family,
1055
- kind,
1056
- faces: [buildFace(provider, block.faceDefaults)]
1057
- });
1058
- applySlotOptions(config2, block.slotOpts);
1059
- return config2;
1060
- }
1061
- const config = createFontSlot({ slot, family, kind, faces: buildFaces(provider, block) });
1062
- applySlotOptions(config, block.slotOpts);
1063
- warnDuplicateFaces(config, warnings);
1064
- return config;
1065
- }
1066
- const manualMatch = matchable.match(/^(.+?)(?:\s*\{([\s\S]*)\})?$/);
1067
- if (manualMatch) {
1068
- const stackParts = manualMatch[1].trim().split(",").map((s) => s.trim());
1069
- const family = stackParts[0].replace(/["']/g, "");
1070
- if (!SAFE_FONT_FAMILY_RE.test(family)) {
1071
- return createFontSlot({ slot, family: sanitizeFamily(family), kind: "manual" });
1072
- }
1073
- const fallback = stackParts.slice(1).map((s) => s.replace(/["']/g, "").trim());
1074
- const block = processSlotBlock(manualMatch[2] || "", warnings);
1075
- if (block.faceBlocks.length > 0 || block.italicSrcs.length > 0) {
1076
- const config2 = createFontSlot({
1077
- slot,
1078
- family,
1079
- kind: "local",
1080
- fallback,
1081
- faces: buildFaces(null, block)
1082
- });
1083
- applySlotOptions(config2, block.slotOpts);
1084
- warnDuplicateFaces(config2, warnings);
1085
- return config2;
1086
- }
1087
- const config = createFontSlot({
1088
- slot,
1089
- family,
1090
- kind: "manual",
1091
- fallback,
1092
- faces: [buildFace("", block.faceDefaults)]
1093
- });
1094
- applySlotOptions(config, block.slotOpts);
1095
- return config;
1096
- }
1097
- return createFontSlot({
1098
- slot,
1099
- family: sanitizeFamily(normalizedBody.replace(/["']/g, "")),
1100
- kind: "manual"
1101
- });
1102
- }
1103
- function parseNestedFontBlock(body, warnings) {
1104
- const cleanedBody = stripCSSComments(body);
1105
- const configs = [];
1106
- for (const entry of scanEntries(cleanedBody, { newlineTerminates: false })) {
1107
- if (entry.removal || entry.fragment) continue;
1108
- if (entry.unclosedBlock) break;
1109
- if (!entry.key) continue;
1110
- if (configs.length >= MAX_FONT_CONFIGS) {
1111
- warnings?.push(
1112
- `[RI-1216] @font block exceeds ${MAX_FONT_CONFIGS} slot definitions \u2014 the remaining slots were skipped. Split rarely-used slots into a separate stylesheet or remove unused ones.`
1113
- );
1114
- break;
1115
- }
1116
- if (entry.block !== void 0) {
1117
- configs.push(parseFontBody(`${entry.value} { ${entry.block} }`, entry.key, warnings));
1118
- } else if (entry.value) {
1119
- configs.push(parseFontBody(entry.value, entry.key, warnings));
1120
- }
1121
- }
1122
- return configs;
1123
- }
1124
1318
  function stripLeadingClassDot(name) {
1125
1319
  return name.startsWith(".") ? name.slice(1) : name;
1126
1320
  }
@@ -2996,6 +3190,17 @@ function parseCustomUtilityBody(body) {
2996
3190
  flushStatement(body.length);
2997
3191
  return root;
2998
3192
  }
3193
+ var parsedBodyCache = /* @__PURE__ */ new Map();
3194
+ var PARSED_BODY_CACHE_MAX = 500;
3195
+ function getParsedBody(body) {
3196
+ let tree = parsedBodyCache.get(body);
3197
+ if (!tree) {
3198
+ if (parsedBodyCache.size >= PARSED_BODY_CACHE_MAX) parsedBodyCache.clear();
3199
+ tree = parseCustomUtilityBody(body);
3200
+ parsedBodyCache.set(body, tree);
3201
+ }
3202
+ return tree;
3203
+ }
2999
3204
  var MAX_CUSTOM_APPLY_DEPTH = 5;
3000
3205
  var APPLY_CLASS_SPLIT_RE = /\s+/;
3001
3206
  function forEachApplyClass(body, visit) {
@@ -3039,7 +3244,7 @@ function resolveCustomUtility(utility, value, theme, resolve, visiting) {
3039
3244
  const map = getCustomUtilityMap(theme);
3040
3245
  const cu = value === null ? map.get(utility) : map.get(`${utility}-${value}`);
3041
3246
  if (!cu || cu.functional) return null;
3042
- const tree = parseCustomUtilityBody(cu.body);
3247
+ const tree = getParsedBody(cu.body);
3043
3248
  let expansion = null;
3044
3249
  if (hasApplyLikeDirective(cu.body)) {
3045
3250
  const visited = visiting ?? /* @__PURE__ */ new Set();
@@ -3063,7 +3268,7 @@ function resolveCustomUtility(utility, value, theme, resolve, visiting) {
3063
3268
  return { declarations, nested };
3064
3269
  }
3065
3270
  function extractCustomUtilityRootInfo(body) {
3066
- const tree = parseCustomUtilityBody(body);
3271
+ const tree = getParsedBody(body);
3067
3272
  const seen = /* @__PURE__ */ new Set();
3068
3273
  const properties = [];
3069
3274
  for (const d of tree.declarations) {
@@ -3072,7 +3277,7 @@ function extractCustomUtilityRootInfo(body) {
3072
3277
  properties.push(d.property);
3073
3278
  }
3074
3279
  }
3075
- return { properties, applyClasses: tree.applyClasses };
3280
+ return { properties, applyClasses: [...tree.applyClasses] };
3076
3281
  }
3077
3282
 
3078
3283
  // src/utilities/helpers.ts
@@ -8424,6 +8629,48 @@ function createEmptyCompilationResult() {
8424
8629
  warnings: []
8425
8630
  };
8426
8631
  }
8632
+ var classCompileMemo = /* @__PURE__ */ new WeakMap();
8633
+ function compileClassEntry(raw, theme, customVariantMap, breakpointWeights, variantMemo) {
8634
+ const scratch = createEmptyCompilationResult();
8635
+ const parsed = parseUtility(raw);
8636
+ const rule = compileUtility(
8637
+ parsed,
8638
+ theme,
8639
+ scratch,
8640
+ customVariantMap,
8641
+ /* @__PURE__ */ new Set(),
8642
+ breakpointWeights,
8643
+ variantMemo
8644
+ );
8645
+ const support = new Array(SUPPORT_BLOCKS.length).fill(false);
8646
+ if (rule) {
8647
+ for (let i = 0; i < SUPPORT_BLOCKS.length; i++) {
8648
+ const block = SUPPORT_BLOCKS[i];
8649
+ if (block.test(rule.css) || block.utilities?.includes(parsed.utility)) {
8650
+ support[i] = true;
8651
+ }
8652
+ }
8653
+ } else if (parsed.arbitrary && parsed.value) {
8654
+ const bracketContent = parsed.value.replace(/^\[|\]$/g, "");
8655
+ if (shouldWarnUnresolvedArbitrary(bracketContent)) {
8656
+ const truncated = parsed.raw.length > 100 ? `${parsed.raw.slice(0, 100)}...` : parsed.raw;
8657
+ scratch.warnings.push(
8658
+ `[RI-1002] Could not resolve arbitrary utility "${truncated}". Arbitrary utilities use \`[property:value]\` syntax \u2014 e.g. \`[padding:1rem]\` or \`[mask-type:luminance]\`. Check that the property name is a known CSS property and the value is well-formed (no stray spaces, quoted strings escaped). If you meant to set a CSS variable, use \`[--my-var:value]\`.`
8659
+ );
8660
+ }
8661
+ }
8662
+ return {
8663
+ rule,
8664
+ warnings: scratch.warnings,
8665
+ usedColorStops: scratch.usedColorStops,
8666
+ usedTextSizes: scratch.usedTextSizes,
8667
+ usedFonts: scratch.usedFonts,
8668
+ usedRounded: scratch.usedRounded,
8669
+ usedShadows: scratch.usedShadows,
8670
+ usedAnimations: scratch.usedAnimations,
8671
+ support
8672
+ };
8673
+ }
8427
8674
  function shouldWarnUnresolvedArbitrary(bracketContent) {
8428
8675
  if (/[=*]/.test(bracketContent)) return false;
8429
8676
  if (/\s/.test(bracketContent)) return false;
@@ -8476,42 +8723,39 @@ function compileInternal(classNames, theme, variantMapCache) {
8476
8723
  const variantMemo = /* @__PURE__ */ new Map();
8477
8724
  const seen = /* @__PURE__ */ new Set();
8478
8725
  const supportNeeded = new Array(SUPPORT_BLOCKS.length).fill(false);
8726
+ let classMemo = classCompileMemo.get(theme);
8727
+ if (!classMemo) {
8728
+ classMemo = /* @__PURE__ */ new Map();
8729
+ classCompileMemo.set(theme, classMemo);
8730
+ }
8479
8731
  for (const raw of classNames) {
8480
8732
  if (seen.has(raw)) continue;
8481
8733
  seen.add(raw);
8482
- const parsed = parseUtility(raw);
8483
- const compiled = compileUtility(
8484
- parsed,
8485
- theme,
8486
- result,
8487
- customVariantMap,
8488
- warnSeen,
8489
- breakpointWeights,
8490
- variantMemo
8491
- );
8492
- if (!compiled) {
8493
- if (parsed.arbitrary && parsed.value) {
8494
- const bracketContent = parsed.value.replace(/^\[|\]$/g, "");
8495
- if (shouldWarnUnresolvedArbitrary(bracketContent)) {
8496
- const truncated = parsed.raw.length > 100 ? `${parsed.raw.slice(0, 100)}...` : parsed.raw;
8497
- pushWarningsDeduped(
8498
- result.warnings,
8499
- [
8500
- `[RI-1002] Could not resolve arbitrary utility "${truncated}". Arbitrary utilities use \`[property:value]\` syntax \u2014 e.g. \`[padding:1rem]\` or \`[mask-type:luminance]\`. Check that the property name is a known CSS property and the value is well-formed (no stray spaces, quoted strings escaped). If you meant to set a CSS variable, use \`[--my-var:value]\`.`
8501
- ],
8502
- warnSeen
8503
- );
8504
- }
8734
+ let entry = classMemo.get(raw);
8735
+ if (!entry) {
8736
+ entry = compileClassEntry(raw, theme, customVariantMap, breakpointWeights, variantMemo);
8737
+ classMemo.set(raw, entry);
8738
+ }
8739
+ if (entry.warnings.length > 0) {
8740
+ pushWarningsDeduped(result.warnings, entry.warnings, warnSeen);
8741
+ }
8742
+ for (const [hue, stops] of entry.usedColorStops) {
8743
+ let set = result.usedColorStops.get(hue);
8744
+ if (!set) {
8745
+ set = /* @__PURE__ */ new Set();
8746
+ result.usedColorStops.set(hue, set);
8505
8747
  }
8506
- continue;
8507
- }
8508
- result.rules.push(compiled);
8748
+ for (const stop of stops) set.add(stop);
8749
+ }
8750
+ for (const v of entry.usedTextSizes) result.usedTextSizes.add(v);
8751
+ for (const v of entry.usedFonts) result.usedFonts.add(v);
8752
+ for (const v of entry.usedRounded) result.usedRounded.add(v);
8753
+ for (const v of entry.usedShadows) result.usedShadows.add(v);
8754
+ for (const v of entry.usedAnimations) result.usedAnimations.add(v);
8755
+ if (!entry.rule) continue;
8756
+ result.rules.push(entry.rule);
8509
8757
  for (let i = 0; i < SUPPORT_BLOCKS.length; i++) {
8510
- if (supportNeeded[i]) continue;
8511
- const block = SUPPORT_BLOCKS[i];
8512
- if (block.test(compiled.css) || block.utilities?.includes(parsed.utility)) {
8513
- supportNeeded[i] = true;
8514
- }
8758
+ if (entry.support[i]) supportNeeded[i] = true;
8515
8759
  }
8516
8760
  }
8517
8761
  result.rules.sort(
@@ -9087,6 +9331,8 @@ var CandidateCollector = class {
9087
9331
  origin = "plain";
9088
9332
  helperName = null;
9089
9333
  byCandidate = /* @__PURE__ */ new Map();
9334
+ /** value -> byCandidate keys, so delete() is O(occurrences of the value). */
9335
+ keysByValue = /* @__PURE__ */ new Map();
9090
9336
  contexts = [];
9091
9337
  setOrigin(origin) {
9092
9338
  this.origin = origin;
@@ -9111,22 +9357,45 @@ var CandidateCollector = class {
9111
9357
  const candidate = { value, start, end, origin: "plain" };
9112
9358
  if (prefixStart >= 0) candidate.groupPrefix = { start: prefixStart, end: prefixEnd };
9113
9359
  this.byCandidate.set(key, candidate);
9360
+ let keys = this.keysByValue.get(value);
9361
+ if (!keys) {
9362
+ keys = /* @__PURE__ */ new Set();
9363
+ this.keysByValue.set(value, keys);
9364
+ }
9365
+ keys.add(key);
9114
9366
  }
9115
9367
  delete(value) {
9116
- for (const [key, candidate] of this.byCandidate) {
9117
- if (candidate.value === value) this.byCandidate.delete(key);
9118
- }
9368
+ const keys = this.keysByValue.get(value);
9369
+ if (!keys) return;
9370
+ for (const key of keys) this.byCandidate.delete(key);
9371
+ this.keysByValue.delete(value);
9119
9372
  }
9120
9373
  finish() {
9121
9374
  const candidates = [...this.byCandidate.values()].sort(
9122
9375
  (a, b) => a.start - b.start || a.end - b.end
9123
9376
  );
9124
9377
  if (this.contexts.length > 0) {
9378
+ const byStart = [...this.contexts].sort((a, b) => a.start - b.start);
9379
+ const live = [];
9380
+ let next = 0;
9125
9381
  for (const candidate of candidates) {
9382
+ while (next < byStart.length && byStart[next].start <= candidate.start) {
9383
+ live.push(byStart[next]);
9384
+ next++;
9385
+ }
9126
9386
  let best = null;
9127
- for (const context of this.contexts) {
9128
- if (context.start <= candidate.start && candidate.end <= context.end) {
9129
- if (!best || context.end - context.start < best.end - best.start) {
9387
+ for (let i = 0; i < live.length; i++) {
9388
+ const context = live[i];
9389
+ if (context.end < candidate.start) {
9390
+ live[i] = live[live.length - 1];
9391
+ live.pop();
9392
+ i--;
9393
+ continue;
9394
+ }
9395
+ if (candidate.end <= context.end) {
9396
+ const width = context.end - context.start;
9397
+ const bestWidth = best ? best.end - best.start : -1;
9398
+ if (!best || width < bestWidth || width === bestWidth && context.id < best.id) {
9130
9399
  best = context;
9131
9400
  }
9132
9401
  }
@@ -9716,15 +9985,15 @@ function analyzeProjectCSS(css) {
9716
9985
  }
9717
9986
 
9718
9987
  export {
9988
+ isRIDebug,
9989
+ withTimeout,
9990
+ isAtRuleBoundary,
9991
+ codepointCompare,
9719
9992
  DIRECTIVE_TYPE_NAMES,
9720
9993
  findClosingBrace,
9721
9994
  APPLY_ALIASES,
9722
9995
  hasApplyLikeDirective,
9723
9996
  APPLY_LIKE_MATCH_RE,
9724
- isRIDebug,
9725
- withTimeout,
9726
- isAtRuleBoundary,
9727
- codepointCompare,
9728
9997
  DIRECTIVE_NAMES_SET,
9729
9998
  directiveAtRulePattern,
9730
9999
  isAtRuleNameChar,