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.
- package/CHANGELOG.md +92 -0
- package/README.md +5 -4
- package/dist/browser.mjs +1 -1
- package/dist/{chunk-4SVFFDS2.mjs → chunk-53WD6U2X.mjs} +226 -44
- package/dist/{chunk-6DAHUFNU.mjs → chunk-6GTG5SZJ.mjs} +872 -603
- package/dist/{chunk-TLR6RP5L.mjs → chunk-IYIUS7AE.mjs} +27 -10
- package/dist/{chunk-5Y7EXXLS.mjs → chunk-KRZL4IDK.mjs} +18 -5
- package/dist/{chunk-YLB6FGIG.mjs → chunk-WYMCN5OC.mjs} +2 -2
- package/dist/cli.mjs +5 -5
- package/dist/editor.d.ts +2 -2
- package/dist/editor.mjs +4 -4
- package/dist/{index-BB2HoeMj.d.ts → index-Dp6i5TSv.d.ts} +15 -11
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +4 -4
- package/dist/vite.mjs +36 -60
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,98 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.1] - 2026-08-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Automatic zero-CLS font fallbacks.** Any `@font` slot whose family is in
|
|
13
|
+
the built-in metrics table (~100 common Google + system families, generated
|
|
14
|
+
from `@capsizecss/metrics` at development time — no new runtime dependency)
|
|
15
|
+
now gets a metrics-adjusted local fallback `@font-face` automatically. The
|
|
16
|
+
fallback font is picked from the slot's stack, or by the web font's
|
|
17
|
+
category (Arial / Times New Roman / Courier New). Opt out with
|
|
18
|
+
`metrics: none;`, pick the matched local font with `metrics: "Segoe UI";`,
|
|
19
|
+
or keep full manual control with
|
|
20
|
+
`metrics: "Arial" <size-adjust> <ascent> <descent> <line-gap>;`.
|
|
21
|
+
- **`face:` entries** — local font files are now declared as repeatable
|
|
22
|
+
`face: <src> [{ overrides }]` entries, one grammar for every face:
|
|
23
|
+
`display: "Satoshi" { face: /Satoshi.woff2; face: /Satoshi-Italic.woff2 { style: italic; } }`.
|
|
24
|
+
- **Fallback stacks work with `from google`** —
|
|
25
|
+
`sans: "Inter", ui-sans-serif, sans-serif from google;` now parses as a
|
|
26
|
+
google slot with fallbacks (previously it silently became a manual stack).
|
|
27
|
+
- **@font now warns instead of ignoring silently**: unknown option keys
|
|
28
|
+
(RI-1217), `preload` on non-local slots where it can have no effect
|
|
29
|
+
(RI-1219), and partial or invalid `metrics` values (RI-1220).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- The `@font` parser was rebuilt on the shared entry scanner: one pass, no
|
|
34
|
+
regex preambles, no re-serialization. Public API (`parseFontBody`,
|
|
35
|
+
`parseNestedFontBlock`) is unchanged.
|
|
36
|
+
- **Incremental rebuilds only re-do changed work** — the scanner keeps a
|
|
37
|
+
per-file cache keyed on mtime + size, so a CLI watch or Vite HMR rebuild
|
|
38
|
+
re-reads and re-extracts only the files that actually changed instead of
|
|
39
|
+
the whole project. The CSS entry analysis is memoized on the entry text
|
|
40
|
+
(stable theme identity across rebuilds), Google-font resolution keeps its
|
|
41
|
+
identity when metadata is unchanged, and per-class compilation results
|
|
42
|
+
(rule, warnings, token usage) are replayed across rebuilds from a
|
|
43
|
+
theme-keyed cache. A one-file edit in a large project now costs
|
|
44
|
+
O(changed files) instead of O(project).
|
|
45
|
+
- **`ri()` conditional args hit the cache fast path** — falsy arguments
|
|
46
|
+
(`ri("flex", isActive && "bg-blue-500")`) no longer force re-tokenization
|
|
47
|
+
on every call: the raw-key cache skips falsy primitives, making cached
|
|
48
|
+
conditional-pattern calls ~4x faster.
|
|
49
|
+
- **Cheaper compile passes** — duplicate class names across `@apply` rules
|
|
50
|
+
resolve once per pass, the `@slot`/`@apply` walks no longer traverse the
|
|
51
|
+
generated utility CSS, custom `@utility` bodies parse once per body text
|
|
52
|
+
instead of once per variant form, and the editor candidate collector's
|
|
53
|
+
context assignment went from quadratic to a sorted sweep. As part of the
|
|
54
|
+
compile-result replay, engine-level compilation warnings are now
|
|
55
|
+
deduplicated (final project output already was).
|
|
56
|
+
- **Internal restructuring (behavior-preserving)** — the directive body
|
|
57
|
+
parsers were split by grammar family: `@color` and `@font` now own their
|
|
58
|
+
own modules, and the generic key-value/entry grammars sit together in the
|
|
59
|
+
directives foundation. The `parsers` import surface is unchanged.
|
|
60
|
+
|
|
61
|
+
### Deprecated
|
|
62
|
+
|
|
63
|
+
- The older `@font` forms still parse and desugar into the new model, but
|
|
64
|
+
warn (RI-1218) and will be removed in a future release: `@face { src: …; }`
|
|
65
|
+
blocks and the `italic:` shorthand (use `face:`), `from "<path>"` (use
|
|
66
|
+
`face:`), `from system` (use bare `system`), the `fallback:` key (list
|
|
67
|
+
fallbacks in the slot preamble), the five-key metrics cluster
|
|
68
|
+
`metricsFallback`/`sizeAdjust`/`ascent`/`descent`/`lineGap` (use
|
|
69
|
+
`metrics:`), and the `unicodeRange` spelling (use `unicode-range`).
|
|
70
|
+
|
|
71
|
+
### Removed
|
|
72
|
+
|
|
73
|
+
- **`@font` `subset:`** — the Google css2 API takes no subset hint, so the
|
|
74
|
+
key never affected the emitted URL. It now warns (RI-1218) and is ignored.
|
|
75
|
+
- **`FontSlot` / `FontFace` fields** — the five slot-level metrics fields
|
|
76
|
+
(`metricsFallback`, `sizeAdjust`, `ascent`, `descent`, `lineGap`) are
|
|
77
|
+
replaced by a single `metrics?: FontMetricsConfig | null`; slot-level
|
|
78
|
+
`preload` moved onto the faces; `FontFace.subset` is gone. Stylesheets are
|
|
79
|
+
unaffected — this only touches code importing those types directly.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **`ri()` stroke-width vs stroke-color conflict — for real this time.** The
|
|
84
|
+
v0.4.0 changelog described this fix, but the implementation did not ship in
|
|
85
|
+
that release: `ri("stroke-2 stroke-red-500")` still dropped `stroke-2`. The
|
|
86
|
+
`stroke` prefix is now actually width-vs-color dual-mode (mirroring
|
|
87
|
+
`border`/`outline`): decimal and non-color arbitrary values claim
|
|
88
|
+
`stroke-width`, color values claim `stroke`. Same-property conflicts
|
|
89
|
+
(`stroke-2` vs `stroke-4`, `stroke-red-500` vs `stroke-blue-500`) still
|
|
90
|
+
merge as before. The emission↔claim parity suite no longer carries a
|
|
91
|
+
stroke exception, and regression tests pin the merged output.
|
|
92
|
+
- **Directive rewriter no longer duplicates unclosed blocks** — an
|
|
93
|
+
unterminated `{` inside a `@color` body made the Vite-plugin pre-pass
|
|
94
|
+
re-emit the text before it (`@color { accent: 0.18 330 { inline` came out
|
|
95
|
+
with the entry doubled). The rewriter's two hand-rolled brace walks were
|
|
96
|
+
replaced by one shared walker built on the same brace matcher the directive
|
|
97
|
+
scan already uses, which also makes quotes and comments inside blocks
|
|
98
|
+
behave consistently across the rewrite passes.
|
|
99
|
+
|
|
8
100
|
## [0.4.0] - 2026-08-20
|
|
9
101
|
|
|
10
102
|
### Added
|
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ Customization happens in your CSS input, not a JS config. The engine recognizes:
|
|
|
211
211
|
| `@color` | Define color tokens. Supports generative (`chroma hue`), explicit (`oklch(...)`, `#rrggbb`), light/dark pairs, and aliases. |
|
|
212
212
|
| `@spacing` | Set the spacing base unit. |
|
|
213
213
|
| `@text` | Define text size tokens (`size, line-height`). |
|
|
214
|
-
| `@font`, `@font-face` | Register font families inside a single `@font { … }` block.
|
|
214
|
+
| `@font`, `@font-face` | Register font families inside a single `@font { … }` block. Local files are repeatable `face:` entries (e.g. upright + italic). Known families get an automatic zero-CLS metrics fallback (`metrics: none` opts out). |
|
|
215
215
|
| `@rounded` | Border-radius tokens; modifier sets corner shape (`round`, `squircle`, `superellipse(N)`, etc). |
|
|
216
216
|
| `@fluid` | Configure fluid type/spacing range. |
|
|
217
217
|
| `@animate` | Register named animations with inline `@keyframes`. |
|
|
@@ -242,10 +242,11 @@ Example:
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
@font {
|
|
245
|
-
sans: "Inter" from google { weight: 400 700; }
|
|
246
|
-
display: "Satoshi"
|
|
245
|
+
sans: "Inter", ui-sans-serif, sans-serif from google { weight: 400 700; }
|
|
246
|
+
display: "Satoshi" {
|
|
247
247
|
weight: 300 900;
|
|
248
|
-
|
|
248
|
+
face: /fonts/Satoshi.woff2;
|
|
249
|
+
face: /fonts/Satoshi-Italic.woff2 { style: italic; }
|
|
249
250
|
}
|
|
250
251
|
}
|
|
251
252
|
|
package/dist/browser.mjs
CHANGED
|
@@ -19,12 +19,12 @@ import {
|
|
|
19
19
|
renderCSS,
|
|
20
20
|
scanCSSForTokenUsage,
|
|
21
21
|
withTimeout
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-6GTG5SZJ.mjs";
|
|
23
23
|
import {
|
|
24
24
|
checkPaletteContrast,
|
|
25
25
|
generateAllColorVariables,
|
|
26
26
|
generateThemeOverrides
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-KRZL4IDK.mjs";
|
|
28
28
|
|
|
29
29
|
// src/integrations/font-providers/google/state.ts
|
|
30
30
|
var googleFontInternals = {
|
|
@@ -318,8 +318,13 @@ async function fetchGoogleFontList() {
|
|
|
318
318
|
googleFontInternals.googleFontListPromise = localPromise;
|
|
319
319
|
return localPromise;
|
|
320
320
|
}
|
|
321
|
+
var refreshMemo = /* @__PURE__ */ new WeakMap();
|
|
321
322
|
function refreshFontWeightDefaults(fonts) {
|
|
322
|
-
|
|
323
|
+
const state = googleFontInternals.googleFontState;
|
|
324
|
+
const memo = refreshMemo.get(fonts);
|
|
325
|
+
if (memo && memo.state === state) return memo.result;
|
|
326
|
+
let anyChanged = false;
|
|
327
|
+
const refreshed = fonts.map((slot) => {
|
|
323
328
|
if (slot.kind !== "google") return slot;
|
|
324
329
|
const meta = googleFontInternals.googleFontState.cache.get(slot.family);
|
|
325
330
|
if (!meta) return slot;
|
|
@@ -339,8 +344,12 @@ function refreshFontWeightDefaults(fonts) {
|
|
|
339
344
|
if (next !== f) changed = true;
|
|
340
345
|
return next;
|
|
341
346
|
});
|
|
347
|
+
if (changed) anyChanged = true;
|
|
342
348
|
return changed ? { ...slot, faces } : slot;
|
|
343
349
|
});
|
|
350
|
+
const result = anyChanged ? refreshed : fonts;
|
|
351
|
+
refreshMemo.set(fonts, { state, result });
|
|
352
|
+
return result;
|
|
344
353
|
}
|
|
345
354
|
var FONT_FETCH_TIMEOUT_MS = 1e4;
|
|
346
355
|
async function resolveGoogleFonts(fonts) {
|
|
@@ -359,6 +368,145 @@ async function resolveGoogleFonts(fonts) {
|
|
|
359
368
|
return refreshFontWeightDefaults(fonts);
|
|
360
369
|
}
|
|
361
370
|
|
|
371
|
+
// src/integrations/font-providers/metrics-data.ts
|
|
372
|
+
var FONT_METRICS_TABLE = {
|
|
373
|
+
abel: [2006, -604, 0, 2048, 783, "sans-serif"],
|
|
374
|
+
alegreya: [1016, -345, 0, 1e3, 410, "serif"],
|
|
375
|
+
"anonymous pro": [1675, -373, 0, 2048, 1118, "monospace"],
|
|
376
|
+
archivo: [878, -210, 0, 1e3, 440, "sans-serif"],
|
|
377
|
+
arial: [1854, -434, 67, 2048, 913, "sans-serif"],
|
|
378
|
+
asap: [934, -212, 0, 1e3, 442, "sans-serif"],
|
|
379
|
+
barlow: [1e3, -200, 0, 1e3, 431, "sans-serif"],
|
|
380
|
+
bitter: [935, -265, 0, 1e3, 465, "serif"],
|
|
381
|
+
"bricolage grotesque": [930, -270, 0, 1e3, 470, "sans-serif"],
|
|
382
|
+
cabin: [1930, -500, 0, 2e3, 844, "sans-serif"],
|
|
383
|
+
catamaran: [1100, -540, 0, 1e3, 411, "sans-serif"],
|
|
384
|
+
chivo: [940, -250, 0, 1e3, 478, "sans-serif"],
|
|
385
|
+
"cormorant garamond": [924, -287, 0, 1e3, 394, "serif"],
|
|
386
|
+
"courier new": [1705, -615, 0, 2048, 1229, "monospace"],
|
|
387
|
+
"crimson pro": [918, -220, 0, 1024, 410, "serif"],
|
|
388
|
+
"crimson text": [972, -359, 0, 1024, 405, "serif"],
|
|
389
|
+
"dm sans": [992, -310, 0, 1e3, 466, "sans-serif"],
|
|
390
|
+
"dm serif display": [1036, -335, 0, 1e3, 446, "serif"],
|
|
391
|
+
dosis: [1027, -237, 0, 1e3, 377, "sans-serif"],
|
|
392
|
+
"eb garamond": [1007, -298, 0, 1e3, 385, "serif"],
|
|
393
|
+
epilogue: [1580, -470, 0, 2e3, 990, "sans-serif"],
|
|
394
|
+
"exo 2": [999, -201, 0, 1e3, 455, "sans-serif"],
|
|
395
|
+
figtree: [950, -250, 0, 1e3, 449, "sans-serif"],
|
|
396
|
+
"fira code": [1980, -644, 0, 2e3, 1200, "monospace"],
|
|
397
|
+
"fira sans": [935, -265, 0, 1e3, 458, "sans-serif"],
|
|
398
|
+
fraunces: [1956, -510, 0, 2e3, 938, "serif"],
|
|
399
|
+
gabarito: [940, -260, 0, 1e3, 442, "display"],
|
|
400
|
+
"geist mono": [1005, -295, 0, 1e3, 600, "monospace"],
|
|
401
|
+
geist: [1005, -295, 0, 1e3, 467, "sans-serif"],
|
|
402
|
+
georgia: [1878, -449, 0, 2048, 913, "serif"],
|
|
403
|
+
heebo: [2146, -862, 0, 2048, 912, "sans-serif"],
|
|
404
|
+
"helvetica neue": [952, -213, 28, 1e3, 450, "sans-serif"],
|
|
405
|
+
helvetica: [1577, -471, 0, 2048, 913, "sans-serif"],
|
|
406
|
+
hind: [1055, -546, 0, 1e3, 429, "sans-serif"],
|
|
407
|
+
"ibm plex mono": [1025, -275, 0, 1e3, 600, "monospace"],
|
|
408
|
+
"ibm plex sans": [1025, -275, 0, 1e3, 451, "sans-serif"],
|
|
409
|
+
"ibm plex serif": [1025, -275, 0, 1e3, 473, "serif"],
|
|
410
|
+
inconsolata: [859, -190, 0, 1e3, 500, "monospace"],
|
|
411
|
+
"instrument sans": [970, -250, 0, 1e3, 458, "sans-serif"],
|
|
412
|
+
"instrument serif": [990, -310, 0, 1e3, 341, "serif"],
|
|
413
|
+
inter: [1984, -494, 0, 2048, 978, "sans-serif"],
|
|
414
|
+
"jetbrains mono": [1020, -300, 0, 1e3, 600, "monospace"],
|
|
415
|
+
"josefin sans": [750, -250, 0, 1e3, 456, "sans-serif"],
|
|
416
|
+
kanit: [1100, -395, 0, 1e3, 452, "sans-serif"],
|
|
417
|
+
karla: [1834, -504, 0, 2e3, 913, "sans-serif"],
|
|
418
|
+
lato: [1974, -426, 0, 2e3, 871, "sans-serif"],
|
|
419
|
+
lexend: [1e3, -250, 0, 1e3, 490, "sans-serif"],
|
|
420
|
+
"libre baskerville": [970, -270, 0, 1e3, 517, "serif"],
|
|
421
|
+
"libre franklin": [966, -246, 0, 1e3, 465, "sans-serif"],
|
|
422
|
+
lora: [1006, -274, 0, 1e3, 468, "serif"],
|
|
423
|
+
manrope: [2132, -600, 0, 2e3, 920, "sans-serif"],
|
|
424
|
+
"maven pro": [965, -210, 0, 1e3, 462, "sans-serif"],
|
|
425
|
+
merriweather: [1968, -546, 0, 2e3, 970, "serif"],
|
|
426
|
+
montserrat: [968, -251, 0, 1e3, 503, "sans-serif"],
|
|
427
|
+
mulish: [1005, -250, 0, 1e3, 464, "sans-serif"],
|
|
428
|
+
newsreader: [1470, -530, 0, 2e3, 857, "serif"],
|
|
429
|
+
"noto sans": [1069, -293, 0, 1e3, 474, "sans-serif"],
|
|
430
|
+
"noto serif": [1069, -293, 0, 1e3, 481, "serif"],
|
|
431
|
+
"nunito sans": [1011, -353, 0, 1e3, 452, "sans-serif"],
|
|
432
|
+
nunito: [1011, -353, 0, 1e3, 452, "sans-serif"],
|
|
433
|
+
onest: [970, -305, 0, 1e3, 469, "sans-serif"],
|
|
434
|
+
"open sans": [2189, -600, 0, 2048, 960, "sans-serif"],
|
|
435
|
+
oswald: [1193, -289, 0, 1e3, 363, "sans-serif"],
|
|
436
|
+
outfit: [1e3, -260, 0, 1e3, 445, "sans-serif"],
|
|
437
|
+
overpass: [1766, -766, 0, 2e3, 898, "sans-serif"],
|
|
438
|
+
oxygen: [2103, -483, 0, 2048, 923, "sans-serif"],
|
|
439
|
+
"playfair display": [1082, -251, 0, 1e3, 452, "serif"],
|
|
440
|
+
"plus jakarta sans": [1038, -222, 0, 1e3, 468, "sans-serif"],
|
|
441
|
+
poppins: [1050, -350, 100, 1e3, 500, "sans-serif"],
|
|
442
|
+
"pt sans": [1018, -276, 0, 1e3, 431, "sans-serif"],
|
|
443
|
+
"pt serif": [1039, -286, 0, 1e3, 448, "serif"],
|
|
444
|
+
"public sans": [1900, -450, 0, 2e3, 935, "sans-serif"],
|
|
445
|
+
quicksand: [1e3, -250, 0, 1e3, 465, "sans-serif"],
|
|
446
|
+
raleway: [940, -234, 0, 1e3, 463, "sans-serif"],
|
|
447
|
+
"red hat display": [1018, -305, 0, 1e3, 442, "sans-serif"],
|
|
448
|
+
"red hat text": [1018, -305, 0, 1e3, 447, "sans-serif"],
|
|
449
|
+
"roboto condensed": [1900, -500, 0, 2048, 811, "sans-serif"],
|
|
450
|
+
"roboto flex": [1900, -500, 0, 2048, 908, "sans-serif"],
|
|
451
|
+
"roboto mono": [2146, -555, 0, 2048, 1229, "monospace"],
|
|
452
|
+
"roboto slab": [2146, -555, 0, 2048, 972, "serif"],
|
|
453
|
+
roboto: [1900, -500, 0, 2048, 911, "sans-serif"],
|
|
454
|
+
rubik: [935, -250, 0, 1e3, 468, "sans-serif"],
|
|
455
|
+
"schibsted grotesk": [2e3, -528, 0, 2048, 954, "sans-serif"],
|
|
456
|
+
"segoe ui": [2210, -514, 0, 2048, 908, "sans-serif"],
|
|
457
|
+
sora: [970, -290, 0, 1e3, 507, "sans-serif"],
|
|
458
|
+
"source code pro": [984, -273, 0, 1e3, 600, "monospace"],
|
|
459
|
+
"source sans 3": [1024, -400, 0, 1e3, 418, "sans-serif"],
|
|
460
|
+
"source serif 4": [1036, -335, 0, 1e3, 479, "serif"],
|
|
461
|
+
"space grotesk": [984, -292, 0, 1e3, 489, "sans-serif"],
|
|
462
|
+
"space mono": [1120, -361, 0, 1e3, 612, "monospace"],
|
|
463
|
+
spectral: [1059, -463, 0, 1e3, 446, "serif"],
|
|
464
|
+
tahoma: [2049, -423, 0, 2048, 917, "sans-serif"],
|
|
465
|
+
"times new roman": [1825, -443, 87, 2048, 832, "serif"],
|
|
466
|
+
"titillium web": [1133, -388, 0, 1e3, 421, "sans-serif"],
|
|
467
|
+
"trebuchet ms": [1923, -455, 0, 2048, 934, "sans-serif"],
|
|
468
|
+
"ubuntu mono": [830, -170, 0, 1e3, 500, "monospace"],
|
|
469
|
+
ubuntu: [932, -189, 28, 1e3, 455, "sans-serif"],
|
|
470
|
+
urbanist: [1900, -500, 0, 2e3, 883, "sans-serif"],
|
|
471
|
+
"varela round": [918, -286, 0, 1e3, 478, "sans-serif"],
|
|
472
|
+
verdana: [2059, -430, 0, 2048, 1049, "sans-serif"],
|
|
473
|
+
"victor mono": [1100, -250, 0, 1e3, 600, "monospace"],
|
|
474
|
+
vollkorn: [952, -441, 0, 1e3, 438, "serif"],
|
|
475
|
+
"work sans": [930, -243, 0, 1e3, 499, "sans-serif"],
|
|
476
|
+
"zilla slab": [944, -256, 0, 1e3, 434, "serif"]
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// src/integrations/font-providers/metrics.ts
|
|
480
|
+
var CATEGORY_FALLBACK = {
|
|
481
|
+
"sans-serif": "Arial",
|
|
482
|
+
serif: "Times New Roman",
|
|
483
|
+
monospace: "Courier New"
|
|
484
|
+
};
|
|
485
|
+
function lookupFontMetrics(family) {
|
|
486
|
+
return FONT_METRICS_TABLE[family.trim().toLowerCase()];
|
|
487
|
+
}
|
|
488
|
+
var round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
489
|
+
function computeFallbackMetrics(fallbackName, font, fallbackFont) {
|
|
490
|
+
const [ascent, descent, lineGap, unitsPerEm, xWidthAvg] = font;
|
|
491
|
+
const [, , , fbUnitsPerEm, fbXWidthAvg] = fallbackFont;
|
|
492
|
+
const sizeAdjust = xWidthAvg / unitsPerEm / (fbXWidthAvg / fbUnitsPerEm);
|
|
493
|
+
return {
|
|
494
|
+
fallback: fallbackName,
|
|
495
|
+
sizeAdjust: round4(sizeAdjust * 100),
|
|
496
|
+
ascent: round4(ascent / unitsPerEm / sizeAdjust * 100),
|
|
497
|
+
descent: round4(Math.abs(descent) / unitsPerEm / sizeAdjust * 100),
|
|
498
|
+
lineGap: round4(lineGap / unitsPerEm / sizeAdjust * 100)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function resolveAutoMetrics(family, fallbackStack, explicitFallback) {
|
|
502
|
+
const font = lookupFontMetrics(family);
|
|
503
|
+
if (!font) return null;
|
|
504
|
+
const fallbackName = explicitFallback ?? fallbackStack.find((f) => lookupFontMetrics(f)) ?? CATEGORY_FALLBACK[font[5]] ?? "Arial";
|
|
505
|
+
const fallbackFont = lookupFontMetrics(fallbackName);
|
|
506
|
+
if (!fallbackFont) return null;
|
|
507
|
+
return computeFallbackMetrics(fallbackName, font, fallbackFont);
|
|
508
|
+
}
|
|
509
|
+
|
|
362
510
|
// src/integrations/font-providers/index.ts
|
|
363
511
|
var SYSTEM_STACKS = Object.freeze({
|
|
364
512
|
sans: 'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
|
|
@@ -390,8 +538,7 @@ function googleFontsUrl(family, face) {
|
|
|
390
538
|
axisParam = face.style.includes("italic") ? `ital,wght@0,${w};1,${w}` : `wght@${w}`;
|
|
391
539
|
}
|
|
392
540
|
const display = face.display || "swap";
|
|
393
|
-
|
|
394
|
-
return `https://fonts.googleapis.com/css2?family=${encodedFamily}:${axisParam}&display=${display}${subset}`;
|
|
541
|
+
return `https://fonts.googleapis.com/css2?family=${encodedFamily}:${axisParam}&display=${display}`;
|
|
395
542
|
}
|
|
396
543
|
function escapeFontFamily(name) {
|
|
397
544
|
return name.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\0/g, "\\0").replace(/\n/g, "\\a ").replace(/\r/g, "\\d ");
|
|
@@ -400,7 +547,7 @@ function generateFallbackFontFace(family, metrics) {
|
|
|
400
547
|
const safeFamily = escapeFontFamily(family);
|
|
401
548
|
return `@font-face {
|
|
402
549
|
font-family: "${safeFamily} Fallback";
|
|
403
|
-
src: local("${metrics.fallback}");
|
|
550
|
+
src: local("${escapeFontFamily(metrics.fallback)}");
|
|
404
551
|
size-adjust: ${metrics.sizeAdjust}%;
|
|
405
552
|
ascent-override: ${metrics.ascent}%;
|
|
406
553
|
descent-override: ${metrics.descent}%;
|
|
@@ -425,7 +572,7 @@ function generateWebFontFace(family, face) {
|
|
|
425
572
|
if (face.provider === "google") {
|
|
426
573
|
return { type: "import", css: `@import url("${googleFontsUrl(family, face)}");` };
|
|
427
574
|
}
|
|
428
|
-
const safeProvider = face.provider
|
|
575
|
+
const safeProvider = escapeFontFamily(face.provider);
|
|
429
576
|
const format = inferFontFormat(face.provider);
|
|
430
577
|
const src = `url("${safeProvider}") format("${format}")`;
|
|
431
578
|
const declarations = [` font-family: "${escapeFontFamily(family)}";`, ` src: ${src};`];
|
|
@@ -447,6 +594,27 @@ function normalizeLocalStyle(style, family, warnings) {
|
|
|
447
594
|
}
|
|
448
595
|
return style;
|
|
449
596
|
}
|
|
597
|
+
function resolveSlotMetrics(slot, warnings) {
|
|
598
|
+
const cfg = slot.metrics;
|
|
599
|
+
if (cfg === null) return null;
|
|
600
|
+
if (cfg?.sizeAdjust !== void 0) {
|
|
601
|
+
return {
|
|
602
|
+
fallback: cfg.fallback || slot.fallback[0] || "Arial",
|
|
603
|
+
sizeAdjust: cfg.sizeAdjust,
|
|
604
|
+
ascent: cfg.ascent,
|
|
605
|
+
descent: cfg.descent,
|
|
606
|
+
lineGap: cfg.lineGap
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const resolved = resolveAutoMetrics(slot.family, slot.fallback, cfg?.fallback);
|
|
610
|
+
if (!resolved && cfg?.fallback) {
|
|
611
|
+
const missing = lookupFontMetrics(slot.family) ? cfg.fallback : slot.family;
|
|
612
|
+
warnings.push(
|
|
613
|
+
`[RI-1220] @font slot "${slot.slot}" requests metrics matching against "${cfg.fallback}", but "${missing}" is not in the built-in metrics table \u2014 no fallback @font-face was generated. Provide the four percentages explicitly (metrics: "${cfg.fallback}" <size-adjust> <ascent> <descent> <line-gap>) or use \`metrics: none\`.`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
return resolved;
|
|
617
|
+
}
|
|
450
618
|
function generateFontCSS(slot) {
|
|
451
619
|
const imports = [];
|
|
452
620
|
const fontFaces = [];
|
|
@@ -466,20 +634,8 @@ function generateFontCSS(slot) {
|
|
|
466
634
|
pushFeatureVars();
|
|
467
635
|
return { imports, fontFaces, variables, warnings };
|
|
468
636
|
}
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
if (hasMetrics) {
|
|
472
|
-
const metricsFallbackFont = slot.metricsFallback || slot.fallback[0] || "Arial";
|
|
473
|
-
fontFaces.push(
|
|
474
|
-
generateFallbackFontFace(slot.family, {
|
|
475
|
-
fallback: metricsFallbackFont,
|
|
476
|
-
sizeAdjust,
|
|
477
|
-
ascent,
|
|
478
|
-
descent,
|
|
479
|
-
lineGap
|
|
480
|
-
})
|
|
481
|
-
);
|
|
482
|
-
}
|
|
637
|
+
const metrics = resolveSlotMetrics(slot, warnings);
|
|
638
|
+
if (metrics) fontFaces.push(generateFallbackFontFace(slot.family, metrics));
|
|
483
639
|
for (const face of slot.faces) {
|
|
484
640
|
if (face.provider !== "google" && !face.provider.startsWith("/") && !face.provider.startsWith("http") && !face.provider.startsWith(".")) {
|
|
485
641
|
warnings.push(
|
|
@@ -496,7 +652,7 @@ function generateFontCSS(slot) {
|
|
|
496
652
|
const fallbackStack = slot.fallback.length > 0 ? slot.fallback.join(", ") : getFallbackStack(slot.slot);
|
|
497
653
|
const safeFamily = escapeFontFamily(slot.family);
|
|
498
654
|
const stackParts = [`"${safeFamily}"`];
|
|
499
|
-
if (
|
|
655
|
+
if (metrics) stackParts.push(`"${safeFamily} Fallback"`);
|
|
500
656
|
stackParts.push(fallbackStack);
|
|
501
657
|
variables.push(`--font-${slot.slot}: ${stackParts.join(", ")};`);
|
|
502
658
|
pushFeatureVars();
|
|
@@ -507,8 +663,7 @@ function getFontPreloadLinks(slots) {
|
|
|
507
663
|
const seen = /* @__PURE__ */ new Set();
|
|
508
664
|
for (const slot of slots) {
|
|
509
665
|
for (const face of slot.faces) {
|
|
510
|
-
|
|
511
|
-
if (!preload) continue;
|
|
666
|
+
if (!face.preload) continue;
|
|
512
667
|
if (face.provider === "system" || face.provider === "google" || !face.provider) continue;
|
|
513
668
|
if (seen.has(face.provider)) continue;
|
|
514
669
|
seen.add(face.provider);
|
|
@@ -775,31 +930,38 @@ function collectInlineClasses(sources) {
|
|
|
775
930
|
return { classes, warnings };
|
|
776
931
|
}
|
|
777
932
|
var FILE_IO_TIMEOUT_MS = 1e4;
|
|
933
|
+
var SCAN_CACHE_MAX_ENTRIES = 2e4;
|
|
934
|
+
var scanCache = /* @__PURE__ */ new Map();
|
|
778
935
|
async function scanOneFile(file) {
|
|
779
936
|
const warnings = [];
|
|
780
937
|
try {
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
938
|
+
const stats = await withTimeout(stat(file), FILE_IO_TIMEOUT_MS, "stat() timed out");
|
|
939
|
+
const cached = scanCache.get(file);
|
|
940
|
+
if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
|
|
941
|
+
return cached.result;
|
|
942
|
+
}
|
|
943
|
+
let result;
|
|
944
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
945
|
+
result = {
|
|
788
946
|
classes: null,
|
|
789
947
|
warnings,
|
|
790
|
-
failure: `[RI-1405] Skipping source file "${file}" (${
|
|
948
|
+
failure: `[RI-1405] Skipping source file "${file}" (${stats.size} bytes) \u2014 exceeds ${MAX_FILE_SIZE} byte limit.`
|
|
949
|
+
};
|
|
950
|
+
} else {
|
|
951
|
+
const content = await withTimeout(
|
|
952
|
+
readFile(file, "utf-8"),
|
|
953
|
+
FILE_IO_TIMEOUT_MS,
|
|
954
|
+
"readFile() timed out"
|
|
955
|
+
);
|
|
956
|
+
result = {
|
|
957
|
+
classes: extractClassesFromSource({ path: file, content }, warnings),
|
|
958
|
+
warnings,
|
|
959
|
+
failure: null
|
|
791
960
|
};
|
|
792
961
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
"readFile() timed out"
|
|
797
|
-
);
|
|
798
|
-
return {
|
|
799
|
-
classes: extractClassesFromSource({ path: file, content }, warnings),
|
|
800
|
-
warnings,
|
|
801
|
-
failure: null
|
|
802
|
-
};
|
|
962
|
+
if (scanCache.size >= SCAN_CACHE_MAX_ENTRIES) scanCache.clear();
|
|
963
|
+
scanCache.set(file, { mtimeMs: stats.mtimeMs, size: stats.size, result });
|
|
964
|
+
return result;
|
|
803
965
|
} catch (err) {
|
|
804
966
|
return {
|
|
805
967
|
classes: null,
|
|
@@ -1575,13 +1737,20 @@ function collectApplyClassNames(css, warnings) {
|
|
|
1575
1737
|
}
|
|
1576
1738
|
return classes;
|
|
1577
1739
|
}
|
|
1740
|
+
var effectiveThemeMemo = /* @__PURE__ */ new WeakMap();
|
|
1578
1741
|
async function finalizeProjectCompilation(options) {
|
|
1579
1742
|
const { analysis } = options;
|
|
1580
1743
|
let effectiveTheme = analysis.theme;
|
|
1581
1744
|
if (options.resolveFonts) {
|
|
1582
1745
|
const resolvedFonts = await options.resolveFonts(analysis.theme.fonts);
|
|
1583
1746
|
if (resolvedFonts !== analysis.theme.fonts) {
|
|
1584
|
-
|
|
1747
|
+
const memo = effectiveThemeMemo.get(analysis.theme);
|
|
1748
|
+
if (memo && memo.fonts === resolvedFonts) {
|
|
1749
|
+
effectiveTheme = memo.theme;
|
|
1750
|
+
} else {
|
|
1751
|
+
effectiveTheme = { ...analysis.theme, fonts: [...resolvedFonts] };
|
|
1752
|
+
effectiveThemeMemo.set(analysis.theme, { fonts: resolvedFonts, theme: effectiveTheme });
|
|
1753
|
+
}
|
|
1585
1754
|
}
|
|
1586
1755
|
}
|
|
1587
1756
|
const expansionWarnings = [];
|
|
@@ -1625,8 +1794,21 @@ async function finalizeProjectCompilation(options) {
|
|
|
1625
1794
|
}
|
|
1626
1795
|
|
|
1627
1796
|
// src/project/scan.ts
|
|
1797
|
+
var lastAnalysis = null;
|
|
1798
|
+
function analyzeProjectCSSMemo(css) {
|
|
1799
|
+
if (lastAnalysis === null || lastAnalysis.css !== css) {
|
|
1800
|
+
lastAnalysis = { css, analysis: analyzeProjectCSS(css) };
|
|
1801
|
+
}
|
|
1802
|
+
const cached = lastAnalysis.analysis;
|
|
1803
|
+
return {
|
|
1804
|
+
...cached,
|
|
1805
|
+
warnings: [...cached.warnings],
|
|
1806
|
+
warningSeen: new Set(cached.warningSeen),
|
|
1807
|
+
diagnostics: [...cached.diagnostics]
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1628
1810
|
async function compileScannedProject(options) {
|
|
1629
|
-
const analysis =
|
|
1811
|
+
const analysis = analyzeProjectCSSMemo(options.css);
|
|
1630
1812
|
const resolveFonts = options.resolveFonts ?? resolveGoogleFonts;
|
|
1631
1813
|
const fontsReady = Promise.resolve(resolveFonts(analysis.theme.fonts));
|
|
1632
1814
|
fontsReady.catch(() => {
|