rainbowindex 0.4.0 → 0.5.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.
@@ -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
+ }
1077
+ }
1078
+ foldLegacyMetrics(out, slot, warnings);
1079
+ return out;
1080
+ }
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
+ }
1098
+ }
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
+ );
1125
+ }
1126
+ seen.add(key);
764
1127
  }
765
- return { colors, removals };
766
1128
  }
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 };
773
- }
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 };
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
+ );
781
1138
  }
1139
+ return createFontSlot({ slot, family: "", kind: "system" });
782
1140
  }
783
- if (value === "transparent" || value === "currentColor" || value === "inherit") {
784
- return { type: "keyword", value };
785
- }
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,191 +1315,18 @@ 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
- });
1318
+ function stripLeadingClassDot(name) {
1319
+ return name.startsWith(".") ? name.slice(1) : name;
1102
1320
  }
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) {
1321
+ function isValidUtilityName(name, warnings) {
1322
+ if (IDENT_KEY_RE.test(name)) {
1323
+ if (/[A-Z]/.test(name)) {
1111
1324
  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.`
1325
+ `[RI-1038] @utility name "${name}" contains uppercase letters \u2014 the markup scanner only matches lowercase tokens, so class="${name}" will never generate it. It still works via @a/@apply and inline @source. Prefer a lowercase-hyphen name.`
1113
1326
  );
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
1327
  }
1328
+ return true;
1121
1329
  }
1122
- return configs;
1123
- }
1124
- function stripLeadingClassDot(name) {
1125
- return name.startsWith(".") ? name.slice(1) : name;
1126
- }
1127
- function isValidUtilityName(name, warnings) {
1128
- if (IDENT_KEY_RE.test(name)) return true;
1129
1330
  warnings?.push(
1130
1331
  `[RI-1035] Invalid @utility name "${name}" \u2014 names may only contain letters, numbers, hyphens, and underscores (plus an optional trailing "-*" for functional utilities). The utility was skipped.`
1131
1332
  );
@@ -2996,6 +3197,17 @@ function parseCustomUtilityBody(body) {
2996
3197
  flushStatement(body.length);
2997
3198
  return root;
2998
3199
  }
3200
+ var parsedBodyCache = /* @__PURE__ */ new Map();
3201
+ var PARSED_BODY_CACHE_MAX = 500;
3202
+ function getParsedBody(body) {
3203
+ let tree = parsedBodyCache.get(body);
3204
+ if (!tree) {
3205
+ if (parsedBodyCache.size >= PARSED_BODY_CACHE_MAX) parsedBodyCache.clear();
3206
+ tree = parseCustomUtilityBody(body);
3207
+ parsedBodyCache.set(body, tree);
3208
+ }
3209
+ return tree;
3210
+ }
2999
3211
  var MAX_CUSTOM_APPLY_DEPTH = 5;
3000
3212
  var APPLY_CLASS_SPLIT_RE = /\s+/;
3001
3213
  function forEachApplyClass(body, visit) {
@@ -3039,7 +3251,7 @@ function resolveCustomUtility(utility, value, theme, resolve, visiting) {
3039
3251
  const map = getCustomUtilityMap(theme);
3040
3252
  const cu = value === null ? map.get(utility) : map.get(`${utility}-${value}`);
3041
3253
  if (!cu || cu.functional) return null;
3042
- const tree = parseCustomUtilityBody(cu.body);
3254
+ const tree = getParsedBody(cu.body);
3043
3255
  let expansion = null;
3044
3256
  if (hasApplyLikeDirective(cu.body)) {
3045
3257
  const visited = visiting ?? /* @__PURE__ */ new Set();
@@ -3063,7 +3275,7 @@ function resolveCustomUtility(utility, value, theme, resolve, visiting) {
3063
3275
  return { declarations, nested };
3064
3276
  }
3065
3277
  function extractCustomUtilityRootInfo(body) {
3066
- const tree = parseCustomUtilityBody(body);
3278
+ const tree = getParsedBody(body);
3067
3279
  const seen = /* @__PURE__ */ new Set();
3068
3280
  const properties = [];
3069
3281
  for (const d of tree.declarations) {
@@ -3072,7 +3284,7 @@ function extractCustomUtilityRootInfo(body) {
3072
3284
  properties.push(d.property);
3073
3285
  }
3074
3286
  }
3075
- return { properties, applyClasses: tree.applyClasses };
3287
+ return { properties, applyClasses: [...tree.applyClasses] };
3076
3288
  }
3077
3289
 
3078
3290
  // src/utilities/helpers.ts
@@ -8424,6 +8636,48 @@ function createEmptyCompilationResult() {
8424
8636
  warnings: []
8425
8637
  };
8426
8638
  }
8639
+ var classCompileMemo = /* @__PURE__ */ new WeakMap();
8640
+ function compileClassEntry(raw, theme, customVariantMap, breakpointWeights, variantMemo) {
8641
+ const scratch = createEmptyCompilationResult();
8642
+ const parsed = parseUtility(raw);
8643
+ const rule = compileUtility(
8644
+ parsed,
8645
+ theme,
8646
+ scratch,
8647
+ customVariantMap,
8648
+ /* @__PURE__ */ new Set(),
8649
+ breakpointWeights,
8650
+ variantMemo
8651
+ );
8652
+ const support = new Array(SUPPORT_BLOCKS.length).fill(false);
8653
+ if (rule) {
8654
+ for (let i = 0; i < SUPPORT_BLOCKS.length; i++) {
8655
+ const block = SUPPORT_BLOCKS[i];
8656
+ if (block.test(rule.css) || block.utilities?.includes(parsed.utility)) {
8657
+ support[i] = true;
8658
+ }
8659
+ }
8660
+ } else if (parsed.arbitrary && parsed.value) {
8661
+ const bracketContent = parsed.value.replace(/^\[|\]$/g, "");
8662
+ if (shouldWarnUnresolvedArbitrary(bracketContent)) {
8663
+ const truncated = parsed.raw.length > 100 ? `${parsed.raw.slice(0, 100)}...` : parsed.raw;
8664
+ scratch.warnings.push(
8665
+ `[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]\`.`
8666
+ );
8667
+ }
8668
+ }
8669
+ return {
8670
+ rule,
8671
+ warnings: scratch.warnings,
8672
+ usedColorStops: scratch.usedColorStops,
8673
+ usedTextSizes: scratch.usedTextSizes,
8674
+ usedFonts: scratch.usedFonts,
8675
+ usedRounded: scratch.usedRounded,
8676
+ usedShadows: scratch.usedShadows,
8677
+ usedAnimations: scratch.usedAnimations,
8678
+ support
8679
+ };
8680
+ }
8427
8681
  function shouldWarnUnresolvedArbitrary(bracketContent) {
8428
8682
  if (/[=*]/.test(bracketContent)) return false;
8429
8683
  if (/\s/.test(bracketContent)) return false;
@@ -8476,42 +8730,39 @@ function compileInternal(classNames, theme, variantMapCache) {
8476
8730
  const variantMemo = /* @__PURE__ */ new Map();
8477
8731
  const seen = /* @__PURE__ */ new Set();
8478
8732
  const supportNeeded = new Array(SUPPORT_BLOCKS.length).fill(false);
8733
+ let classMemo = classCompileMemo.get(theme);
8734
+ if (!classMemo) {
8735
+ classMemo = /* @__PURE__ */ new Map();
8736
+ classCompileMemo.set(theme, classMemo);
8737
+ }
8479
8738
  for (const raw of classNames) {
8480
8739
  if (seen.has(raw)) continue;
8481
8740
  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
- }
8741
+ let entry = classMemo.get(raw);
8742
+ if (!entry) {
8743
+ entry = compileClassEntry(raw, theme, customVariantMap, breakpointWeights, variantMemo);
8744
+ classMemo.set(raw, entry);
8745
+ }
8746
+ if (entry.warnings.length > 0) {
8747
+ pushWarningsDeduped(result.warnings, entry.warnings, warnSeen);
8748
+ }
8749
+ for (const [hue, stops] of entry.usedColorStops) {
8750
+ let set = result.usedColorStops.get(hue);
8751
+ if (!set) {
8752
+ set = /* @__PURE__ */ new Set();
8753
+ result.usedColorStops.set(hue, set);
8505
8754
  }
8506
- continue;
8507
- }
8508
- result.rules.push(compiled);
8755
+ for (const stop of stops) set.add(stop);
8756
+ }
8757
+ for (const v of entry.usedTextSizes) result.usedTextSizes.add(v);
8758
+ for (const v of entry.usedFonts) result.usedFonts.add(v);
8759
+ for (const v of entry.usedRounded) result.usedRounded.add(v);
8760
+ for (const v of entry.usedShadows) result.usedShadows.add(v);
8761
+ for (const v of entry.usedAnimations) result.usedAnimations.add(v);
8762
+ if (!entry.rule) continue;
8763
+ result.rules.push(entry.rule);
8509
8764
  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
- }
8765
+ if (entry.support[i]) supportNeeded[i] = true;
8515
8766
  }
8516
8767
  }
8517
8768
  result.rules.sort(
@@ -8961,7 +9212,8 @@ function readAssignedValue(source, start) {
8961
9212
  return {
8962
9213
  value: source.slice(i + 1, end2),
8963
9214
  end: end2,
8964
- valueStart: i + 1
9215
+ valueStart: i + 1,
9216
+ quoted: true
8965
9217
  };
8966
9218
  }
8967
9219
  if (ch === "`") {
@@ -8969,7 +9221,8 @@ function readAssignedValue(source, start) {
8969
9221
  return {
8970
9222
  value: source.slice(i + 1, end2),
8971
9223
  end: end2,
8972
- valueStart: i + 1
9224
+ valueStart: i + 1,
9225
+ quoted: false
8973
9226
  };
8974
9227
  }
8975
9228
  if (ch in BRACKET_PAIRS) {
@@ -8978,7 +9231,8 @@ function readAssignedValue(source, start) {
8978
9231
  return {
8979
9232
  value: source.slice(i + 1, end2),
8980
9233
  end: end2,
8981
- valueStart: i + 1
9234
+ valueStart: i + 1,
9235
+ quoted: false
8982
9236
  };
8983
9237
  }
8984
9238
  let end = i;
@@ -8986,7 +9240,8 @@ function readAssignedValue(source, start) {
8986
9240
  return {
8987
9241
  value: source.slice(i, end),
8988
9242
  end: end - 1,
8989
- valueStart: i
9243
+ valueStart: i,
9244
+ quoted: false
8990
9245
  };
8991
9246
  }
8992
9247
  function splitTopLevelArgs(source) {
@@ -9087,6 +9342,8 @@ var CandidateCollector = class {
9087
9342
  origin = "plain";
9088
9343
  helperName = null;
9089
9344
  byCandidate = /* @__PURE__ */ new Map();
9345
+ /** value -> byCandidate keys, so delete() is O(occurrences of the value). */
9346
+ keysByValue = /* @__PURE__ */ new Map();
9090
9347
  contexts = [];
9091
9348
  setOrigin(origin) {
9092
9349
  this.origin = origin;
@@ -9111,22 +9368,45 @@ var CandidateCollector = class {
9111
9368
  const candidate = { value, start, end, origin: "plain" };
9112
9369
  if (prefixStart >= 0) candidate.groupPrefix = { start: prefixStart, end: prefixEnd };
9113
9370
  this.byCandidate.set(key, candidate);
9371
+ let keys = this.keysByValue.get(value);
9372
+ if (!keys) {
9373
+ keys = /* @__PURE__ */ new Set();
9374
+ this.keysByValue.set(value, keys);
9375
+ }
9376
+ keys.add(key);
9114
9377
  }
9115
9378
  delete(value) {
9116
- for (const [key, candidate] of this.byCandidate) {
9117
- if (candidate.value === value) this.byCandidate.delete(key);
9118
- }
9379
+ const keys = this.keysByValue.get(value);
9380
+ if (!keys) return;
9381
+ for (const key of keys) this.byCandidate.delete(key);
9382
+ this.keysByValue.delete(value);
9119
9383
  }
9120
9384
  finish() {
9121
9385
  const candidates = [...this.byCandidate.values()].sort(
9122
9386
  (a, b) => a.start - b.start || a.end - b.end
9123
9387
  );
9124
9388
  if (this.contexts.length > 0) {
9389
+ const byStart = [...this.contexts].sort((a, b) => a.start - b.start);
9390
+ const live = [];
9391
+ let next = 0;
9125
9392
  for (const candidate of candidates) {
9393
+ while (next < byStart.length && byStart[next].start <= candidate.start) {
9394
+ live.push(byStart[next]);
9395
+ next++;
9396
+ }
9126
9397
  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) {
9398
+ for (let i = 0; i < live.length; i++) {
9399
+ const context = live[i];
9400
+ if (context.end < candidate.start) {
9401
+ live[i] = live[live.length - 1];
9402
+ live.pop();
9403
+ i--;
9404
+ continue;
9405
+ }
9406
+ if (candidate.end <= context.end) {
9407
+ const width = context.end - context.start;
9408
+ const bestWidth = best ? best.end - best.start : -1;
9409
+ if (!best || width < bestWidth || width === bestWidth && context.id < best.id) {
9130
9410
  best = context;
9131
9411
  }
9132
9412
  }
@@ -9156,7 +9436,7 @@ var ARBITRARY_PROPERTY = /\[[a-z-][^\]]*:[^\]]+\](?:\/(?:[\w.]+|\[[^\]]*\]|\([^)
9156
9436
  var CLASS_RE_SOURCE = `${BOUNDARY}(${NEGATIVE}${VARIANT_PREFIX}(?:${UTILITY_VALUE}|${VARIANT_GROUP}|${ARBITRARY_PROPERTY})${IMPORTANT_SUFFIX})`;
9157
9437
  var CLASS_RE = new RegExp(CLASS_RE_SOURCE, "g");
9158
9438
  var VARIANT_STRIP_RE = new RegExp(`^(?:${VARIANT_SEGMENT})+`);
9159
- var MAX_LINE_LENGTH = 2e3;
9439
+ var MAX_LINE_LENGTH = 1e4;
9160
9440
  var CLASS_HELPERS = [
9161
9441
  "clsx",
9162
9442
  "cn",
@@ -9359,12 +9639,11 @@ function collectAssignedValues(sink, source, regex, visitor = scanClassTokens, b
9359
9639
  const value = raw.trim();
9360
9640
  if (!value) continue;
9361
9641
  sink.markContext?.(base + parsed.valueStart, base + parsed.valueStart + raw.length);
9362
- visitor(
9363
- sink,
9364
- value,
9365
- base + parsed.valueStart + (raw.length - raw.trimStart().length),
9366
- warnings
9367
- );
9642
+ const valueOffset = base + parsed.valueStart + (raw.length - raw.trimStart().length);
9643
+ if (parsed.quoted && visitor !== scanClassTokens) {
9644
+ scanClassTokens(sink, value, valueOffset, warnings);
9645
+ }
9646
+ visitor(sink, value, valueOffset, warnings);
9368
9647
  regex.lastIndex = Math.max(regex.lastIndex, parsed.end + 1);
9369
9648
  }
9370
9649
  }
@@ -9538,10 +9817,8 @@ var NOOP_VISITOR = () => void 0;
9538
9817
  function extractHTML(sink, context, warnings) {
9539
9818
  sink.setOrigin?.("plain");
9540
9819
  scanClassTokens(sink, context.content, 0, warnings);
9541
- if (sink.wantsPositions) {
9542
- sink.setOrigin?.("attribute");
9543
- collectAssignedValues(sink, context.content, /\bclass\s*=/g, NOOP_VISITOR, 0, warnings);
9544
- }
9820
+ sink.setOrigin?.("attribute");
9821
+ collectAssignedValues(sink, context.content, /\bclass\s*=/g, NOOP_VISITOR, 0, warnings);
9545
9822
  }
9546
9823
  function extractJSXTSX(sink, context, warnings) {
9547
9824
  const content = context.content;
@@ -9585,16 +9862,7 @@ function extractVue(sink, context, warnings) {
9585
9862
  0,
9586
9863
  warnings
9587
9864
  );
9588
- if (sink.wantsPositions) {
9589
- collectAssignedValues(
9590
- sink,
9591
- context.content,
9592
- /(?<![:\w-])class\s*=/g,
9593
- NOOP_VISITOR,
9594
- 0,
9595
- warnings
9596
- );
9597
- }
9865
+ collectAssignedValues(sink, context.content, /(?<![:\w-])class\s*=/g, NOOP_VISITOR, 0, warnings);
9598
9866
  }
9599
9867
  function extractSvelte(sink, context, warnings) {
9600
9868
  sink.setOrigin?.("plain");
@@ -9628,7 +9896,26 @@ var EXTRACTORS = [
9628
9896
  extract: extractJSXTSX
9629
9897
  }
9630
9898
  ];
9899
+ function warnOverLongLines(input, warnings) {
9900
+ const content = input.content;
9901
+ if (!warnings || content.length <= MAX_LINE_LENGTH) return;
9902
+ if (input.path?.includes("node_modules")) return;
9903
+ let count = 0;
9904
+ let start = 0;
9905
+ for (; ; ) {
9906
+ const idx = content.indexOf("\n", start);
9907
+ const end = idx === -1 ? content.length : idx;
9908
+ if (end - start > MAX_LINE_LENGTH) count++;
9909
+ if (idx === -1) break;
9910
+ start = idx + 1;
9911
+ }
9912
+ if (count === 0) return;
9913
+ warnings.push(
9914
+ `[RI-1411] ${input.path ?? "<source>"}: ${count} line(s) longer than ${MAX_LINE_LENGTH} characters were skipped by the class scanner (minified-input guard). Quoted class attributes on those lines are still read; other class references there are not \u2014 split the long lines.`
9915
+ );
9916
+ }
9631
9917
  function extractInto(sink, input, warnings) {
9918
+ warnOverLongLines(input, warnings);
9632
9919
  let handled = false;
9633
9920
  for (const extractor of EXTRACTORS) {
9634
9921
  if (extractor.test(input)) {
@@ -9716,15 +10003,15 @@ function analyzeProjectCSS(css) {
9716
10003
  }
9717
10004
 
9718
10005
  export {
10006
+ isRIDebug,
10007
+ withTimeout,
10008
+ isAtRuleBoundary,
10009
+ codepointCompare,
9719
10010
  DIRECTIVE_TYPE_NAMES,
9720
10011
  findClosingBrace,
9721
10012
  APPLY_ALIASES,
9722
10013
  hasApplyLikeDirective,
9723
10014
  APPLY_LIKE_MATCH_RE,
9724
- isRIDebug,
9725
- withTimeout,
9726
- isAtRuleBoundary,
9727
- codepointCompare,
9728
10015
  DIRECTIVE_NAMES_SET,
9729
10016
  directiveAtRulePattern,
9730
10017
  isAtRuleNameChar,