@lovrozagar/flare 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/package.json +146 -0
- package/scripts/download-fonts.ts +136 -0
- package/scripts/populate-fonts.ts +721 -0
- package/src/await.ts +2 -0
- package/src/boundaries/index.ts +99 -0
- package/src/broadcast/channel.ts +65 -0
- package/src/broadcast/context.ts +12 -0
- package/src/broadcast/index.ts +75 -0
- package/src/broadcast/provider.tsx +7 -0
- package/src/build-location.ts +2 -0
- package/src/caches/index.ts +384 -0
- package/src/client/index.ts +75 -0
- package/src/client.tsx +2 -0
- package/src/codegen/index.ts +41 -0
- package/src/components/await.tsx +140 -0
- package/src/components/dev-error-overlay.tsx +154 -0
- package/src/components/direction-script.tsx +22 -0
- package/src/components/index.ts +150 -0
- package/src/components/locale-script.tsx +26 -0
- package/src/components/reset-css.tsx +14 -0
- package/src/components/ssr-context.tsx +37 -0
- package/src/components/theme-script.tsx +22 -0
- package/src/components/view-transition-css.tsx +21 -0
- package/src/config/index.ts +84 -0
- package/src/create-client-only-fn.ts +1 -0
- package/src/create-isomorphic-fn.ts +2 -0
- package/src/create-layout.ts +1 -0
- package/src/create-page.ts +1 -0
- package/src/create-path-segment.ts +1 -0
- package/src/create-query-client-getter.ts +2 -0
- package/src/create-root-layout.ts +1 -0
- package/src/create-router.ts +8 -0
- package/src/create-server-only-fn.ts +1 -0
- package/src/dedupe/index.ts +151 -0
- package/src/defer/index.ts +82 -0
- package/src/direction/index.tsx +163 -0
- package/src/direction.ts +3 -0
- package/src/duration/index.ts +18 -0
- package/src/env-fn/index.ts +101 -0
- package/src/env-fn.ts +2 -0
- package/src/errors/index.ts +166 -0
- package/src/fonts/abeezee.ts +50 -0
- package/src/fonts/abel.ts +26 -0
- package/src/fonts/abril-fatface.ts +34 -0
- package/src/fonts/acme.ts +26 -0
- package/src/fonts/advent-pro.ts +92 -0
- package/src/fonts/alegreya-sans.ts +228 -0
- package/src/fonts/alegreya.ts +228 -0
- package/src/fonts/alex-brush.ts +42 -0
- package/src/fonts/alfa-slab-one.ts +42 -0
- package/src/fonts/alice.ts +48 -0
- package/src/fonts/amatic-sc.ts +94 -0
- package/src/fonts/amiri.ts +114 -0
- package/src/fonts/antic-slab.ts +26 -0
- package/src/fonts/anton.ts +42 -0
- package/src/fonts/archivo-black.ts +34 -0
- package/src/fonts/archivo-narrow.ts +114 -0
- package/src/fonts/archivo.ts +66 -0
- package/src/fonts/arimo.ts +256 -0
- package/src/fonts/arvo.ts +50 -0
- package/src/fonts/asap.ts +66 -0
- package/src/fonts/assistant.ts +64 -0
- package/src/fonts/baloo-2.ts +82 -0
- package/src/fonts/barlow-condensed.ts +114 -0
- package/src/fonts/barlow-semi-condensed.ts +114 -0
- package/src/fonts/barlow.ts +114 -0
- package/src/fonts/be-vietnam-pro.ts +114 -0
- package/src/fonts/bebas-neue.ts +34 -0
- package/src/fonts/besley.ts +82 -0
- package/src/fonts/biorhyme.ts +50 -0
- package/src/fonts/bitter.ts +94 -0
- package/src/fonts/black-ops-one.ts +49 -0
- package/src/fonts/bree-serif.ts +34 -0
- package/src/fonts/bricolage-grotesque.ts +66 -0
- package/src/fonts/cabin.ts +114 -0
- package/src/fonts/cairo.ts +66 -0
- package/src/fonts/cardo.ts +193 -0
- package/src/fonts/catamaran.ts +41 -0
- package/src/fonts/caveat.ts +78 -0
- package/src/fonts/chakra-petch.ts +142 -0
- package/src/fonts/chivo.ts +66 -0
- package/src/fonts/cinzel.ts +50 -0
- package/src/fonts/comfortaa.ts +109 -0
- package/src/fonts/commissioner.ts +64 -0
- package/src/fonts/component.tsx +27 -0
- package/src/fonts/cormorant-garamond.ts +171 -0
- package/src/fonts/cormorant.ts +170 -0
- package/src/fonts/courgette.ts +34 -0
- package/src/fonts/courier-prime.ts +82 -0
- package/src/fonts/create-font.ts +47 -0
- package/src/fonts/create-registry-font.ts +40 -0
- package/src/fonts/crete-round.ts +50 -0
- package/src/fonts/crimson-text.ts +114 -0
- package/src/fonts/css.ts +86 -0
- package/src/fonts/dancing-script.ts +66 -0
- package/src/fonts/dm-mono.ts +50 -0
- package/src/fonts/dm-sans.ts +50 -0
- package/src/fonts/dm-serif-display.ts +50 -0
- package/src/fonts/domine.ts +50 -0
- package/src/fonts/dosis.ts +66 -0
- package/src/fonts/eb-garamond.ts +228 -0
- package/src/fonts/eczar.ts +94 -0
- package/src/fonts/encode-sans.ts +42 -0
- package/src/fonts/epilogue.ts +66 -0
- package/src/fonts/exo-2.ts +94 -0
- package/src/fonts/exo.ts +66 -0
- package/src/fonts/figtree.ts +82 -0
- package/src/fonts/fira-code.ts +121 -0
- package/src/fonts/fira-sans-condensed.ts +228 -0
- package/src/fonts/fira-sans.ts +228 -0
- package/src/fonts/fjalla-one.ts +49 -0
- package/src/fonts/font-urls.gen.json +1962 -0
- package/src/fonts/fraunces.ts +66 -0
- package/src/fonts/fredoka.ts +64 -0
- package/src/fonts/gelasio.ts +114 -0
- package/src/fonts/gloria-hallelujah.ts +34 -0
- package/src/fonts/gothic-a1.ts +124 -0
- package/src/fonts/great-vibes.ts +64 -0
- package/src/fonts/gudea.ts +66 -0
- package/src/fonts/heebo.ts +57 -0
- package/src/fonts/hind-siliguri.ts +66 -0
- package/src/fonts/hind.ts +66 -0
- package/src/fonts/ibm-plex-mono.ts +172 -0
- package/src/fonts/ibm-plex-sans-arabic.ts +80 -0
- package/src/fonts/ibm-plex-sans.ts +199 -0
- package/src/fonts/ibm-plex-serif.ts +171 -0
- package/src/fonts/inconsolata.ts +66 -0
- package/src/fonts/index.ts +4 -0
- package/src/fonts/indie-flower.ts +34 -0
- package/src/fonts/instrument-sans.ts +82 -0
- package/src/fonts/instrument-serif.ts +50 -0
- package/src/fonts/inter-display.ts +52 -0
- package/src/fonts/inter-tight.ts +124 -0
- package/src/fonts/inter.ts +123 -0
- package/src/fonts/jetbrains-mono.ts +199 -0
- package/src/fonts/josefin-sans.ts +114 -0
- package/src/fonts/josefin-slab.ts +50 -0
- package/src/fonts/jost.ts +64 -0
- package/src/fonts/kalam.ts +66 -0
- package/src/fonts/kanit.ts +142 -0
- package/src/fonts/karla.ts +82 -0
- package/src/fonts/khand.ts +66 -0
- package/src/fonts/lato.ts +82 -0
- package/src/fonts/league-spartan.ts +42 -0
- package/src/fonts/lexend-deca.ts +42 -0
- package/src/fonts/lexend.ts +42 -0
- package/src/fonts/libre-baskerville.ts +82 -0
- package/src/fonts/libre-franklin.ts +95 -0
- package/src/fonts/lilita-one.ts +34 -0
- package/src/fonts/lobster-two.ts +50 -0
- package/src/fonts/lobster.ts +56 -0
- package/src/fonts/lora.ts +235 -0
- package/src/fonts/m-plus-rounded-1c.ts +138 -0
- package/src/fonts/manrope.ts +109 -0
- package/src/fonts/maven-pro.ts +66 -0
- package/src/fonts/merriweather-sans.ts +142 -0
- package/src/fonts/merriweather.ts +171 -0
- package/src/fonts/montserrat-alternates.ts +171 -0
- package/src/fonts/montserrat.ts +94 -0
- package/src/fonts/mukta.ts +66 -0
- package/src/fonts/mulish.ts +170 -0
- package/src/fonts/nanum-gothic.ts +34 -0
- package/src/fonts/nanum-myeongjo.ts +34 -0
- package/src/fonts/neuton.ts +66 -0
- package/src/fonts/newsreader.ts +114 -0
- package/src/fonts/noticia-text.ts +114 -0
- package/src/fonts/noto-sans-arabic.ts +58 -0
- package/src/fonts/noto-sans-hk.ts +49 -0
- package/src/fonts/noto-sans-jp.ts +49 -0
- package/src/fonts/noto-sans-kr.ts +49 -0
- package/src/fonts/noto-sans-sc.ts +49 -0
- package/src/fonts/noto-sans-tc.ts +49 -0
- package/src/fonts/noto-sans.ts +140 -0
- package/src/fonts/noto-serif-jp.ts +80 -0
- package/src/fonts/noto-serif.ts +140 -0
- package/src/fonts/nunito-sans.ts +170 -0
- package/src/fonts/nunito.ts +170 -0
- package/src/fonts/old-standard-tt.ts +133 -0
- package/src/fonts/onest.ts +48 -0
- package/src/fonts/open-sans.ts +340 -0
- package/src/fonts/orbitron.ts +34 -0
- package/src/fonts/oswald.ts +94 -0
- package/src/fonts/outfit.ts +34 -0
- package/src/fonts/overpass.ts +94 -0
- package/src/fonts/oxygen.ts +50 -0
- package/src/fonts/pacifico.ts +56 -0
- package/src/fonts/passion-one.ts +50 -0
- package/src/fonts/pathway-gothic-one.ts +34 -0
- package/src/fonts/permanent-marker.ts +26 -0
- package/src/fonts/philosopher.ts +172 -0
- package/src/fonts/play.ts +109 -0
- package/src/fonts/playfair-display.ts +142 -0
- package/src/fonts/plus-jakarta-sans.ts +142 -0
- package/src/fonts/poppins.ts +114 -0
- package/src/fonts/prompt.ts +142 -0
- package/src/fonts/pt-sans-narrow.ts +78 -0
- package/src/fonts/pt-sans.ts +138 -0
- package/src/fonts/pt-serif.ts +138 -0
- package/src/fonts/public-sans.ts +66 -0
- package/src/fonts/questrial.ts +42 -0
- package/src/fonts/quicksand.ts +66 -0
- package/src/fonts/rajdhani.ts +66 -0
- package/src/fonts/raleway.ts +94 -0
- package/src/fonts/recursive.ts +80 -0
- package/src/fonts/red-hat-display.ts +82 -0
- package/src/fonts/red-rose.ts +66 -0
- package/src/fonts/registry.gen.ts +173 -0
- package/src/fonts/righteous.ts +34 -0
- package/src/fonts/roboto-condensed.ts +124 -0
- package/src/fonts/roboto-flex.ts +64 -0
- package/src/fonts/roboto-mono.ts +199 -0
- package/src/fonts/roboto-slab.ts +72 -0
- package/src/fonts/roboto.ts +156 -0
- package/src/fonts/rokkitt.ts +66 -0
- package/src/fonts/rubik.ts +199 -0
- package/src/fonts/sacramento.ts +34 -0
- package/src/fonts/saira.ts +66 -0
- package/src/fonts/sarabun.ts +142 -0
- package/src/fonts/satisfy.ts +26 -0
- package/src/fonts/sawarabi-mincho.ts +41 -0
- package/src/fonts/shadows-into-light.ts +34 -0
- package/src/fonts/signika-negative.ts +66 -0
- package/src/fonts/signika.ts +66 -0
- package/src/fonts/slabo-27px.ts +34 -0
- package/src/fonts/sora.ts +50 -0
- package/src/fonts/source-code-pro.ts +228 -0
- package/src/fonts/source-sans-3.ts +228 -0
- package/src/fonts/source-serif-4.ts +199 -0
- package/src/fonts/space-grotesk.ts +66 -0
- package/src/fonts/space-mono.ts +114 -0
- package/src/fonts/spectral.ts +170 -0
- package/src/fonts/tajawal.ts +50 -0
- package/src/fonts/teko.ts +66 -0
- package/src/fonts/tenor-sans.ts +41 -0
- package/src/fonts/tinos.ts +256 -0
- package/src/fonts/titillium-web.ts +82 -0
- package/src/fonts/types.ts +43 -0
- package/src/fonts/ubuntu-condensed.ts +63 -0
- package/src/fonts/ubuntu-mono.ts +195 -0
- package/src/fonts/ubuntu.ts +195 -0
- package/src/fonts/unbounded.ts +94 -0
- package/src/fonts/unicode-ranges.gen.ts +28 -0
- package/src/fonts/urbanist.ts +50 -0
- package/src/fonts/varela-round.ts +49 -0
- package/src/fonts/vollkorn.ts +199 -0
- package/src/fonts/work-sans.ts +66 -0
- package/src/fonts/yanone-kaffeesatz.ts +128 -0
- package/src/fonts/yantramanav.ts +66 -0
- package/src/fonts/yellowtail.ts +34 -0
- package/src/fonts/young-serif.ts +34 -0
- package/src/fonts/zen-kaku-gothic-new.ts +64 -0
- package/src/fonts/zilla-slab.ts +82 -0
- package/src/form/index.tsx +205 -0
- package/src/generators/index.ts +1589 -0
- package/src/head-client/index.ts +649 -0
- package/src/history/index.ts +210 -0
- package/src/hooks.ts +61 -0
- package/src/hydrate/index.tsx +381 -0
- package/src/hydration/index.ts +187 -0
- package/src/i18n/index.ts +250 -0
- package/src/i18n/jsx.tsx +42 -0
- package/src/i18n/message-format.ts +174 -0
- package/src/image/index.tsx +473 -0
- package/src/image.ts +12 -0
- package/src/intercept-outlet/index.tsx +18 -0
- package/src/intercept-outlet.ts +2 -0
- package/src/internal/index.ts +143 -0
- package/src/internal/once-idle.ts +24 -0
- package/src/internal.ts +12 -0
- package/src/lazy/index.tsx +205 -0
- package/src/link/index.tsx +367 -0
- package/src/loader-pipeline/index.ts +639 -0
- package/src/locale/index.tsx +137 -0
- package/src/locale.ts +3 -0
- package/src/logger.ts +22 -0
- package/src/middleware/builtins/api-proxy.ts +64 -0
- package/src/middleware/builtins/cdn-proxy.ts +58 -0
- package/src/middleware/builtins/i18n.ts +256 -0
- package/src/middleware/builtins/keepalive.ts +24 -0
- package/src/middleware/builtins/markdown-negotiation.ts +141 -0
- package/src/middleware/builtins/static-assets.ts +32 -0
- package/src/middleware/index.ts +223 -0
- package/src/module-graph/index.ts +181 -0
- package/src/mount/index.ts +146 -0
- package/src/navigate.ts +1 -0
- package/src/navigation/index.ts +1278 -0
- package/src/navigation/types.ts +59 -0
- package/src/navigation-progress/index.tsx +247 -0
- package/src/navigation-progress.ts +1 -0
- package/src/ndjson-client/index.ts +376 -0
- package/src/ndjson-server/index.ts +284 -0
- package/src/outlet/index.tsx +633 -0
- package/src/outlet/types.ts +243 -0
- package/src/plugins/css-transform.ts +70 -0
- package/src/plugins/dev-cdn-cache.ts +454 -0
- package/src/plugins/dev-dashboard/client.tsx +2603 -0
- package/src/plugins/dev-dashboard/match-route-tree.ts +159 -0
- package/src/plugins/dev-dashboard/plugin.ts +404 -0
- package/src/plugins/dev-prerender.ts +168 -0
- package/src/plugins/dev-server.ts +327 -0
- package/src/plugins/generate-watch.ts +32 -0
- package/src/plugins/image-plugin.ts +171 -0
- package/src/plugins/index.ts +445 -0
- package/src/plugins/options.ts +37 -0
- package/src/plugins/parse-utils.ts +59 -0
- package/src/plugins/prerender-plugin.ts +144 -0
- package/src/plugins/purge.ts +242 -0
- package/src/plugins/server-fn.ts +641 -0
- package/src/plugins/service-worker.ts +225 -0
- package/src/plugins/solid-jsx.d.ts +12 -0
- package/src/plugins/sx-ast/atomic.ts +166 -0
- package/src/plugins/sx-ast/evaluator.ts +235 -0
- package/src/plugins/sx-ast/index.ts +372 -0
- package/src/plugins/sx-ast/parser.ts +22 -0
- package/src/plugins/sx-ast/rewrite.ts +763 -0
- package/src/plugins/sx.d.ts +14 -0
- package/src/plugins/tw-compile.ts +240 -0
- package/src/plugins/types.ts +41 -0
- package/src/plugins/virtual.d.ts +110 -0
- package/src/plugins/virtual.ts +184 -0
- package/src/preload/index.ts +77 -0
- package/src/prerender/index.ts +431 -0
- package/src/query-client/index.tsx +168 -0
- package/src/registry/index.ts +88 -0
- package/src/reset-css.ts +1 -0
- package/src/revalidation/index.ts +84 -0
- package/src/rewrite/index.ts +83 -0
- package/src/route-builder/create-layout.ts +655 -0
- package/src/route-builder/create-page.ts +806 -0
- package/src/route-builder/create-path-segment.ts +34 -0
- package/src/route-builder/create-root-layout.ts +608 -0
- package/src/route-builder/index.ts +49 -0
- package/src/route-builder/register.ts +242 -0
- package/src/route-builder/types.ts +294 -0
- package/src/router-config/index.ts +91 -0
- package/src/router-primitives/index.ts +25 -0
- package/src/router-primitives/location.ts +29 -0
- package/src/router-primitives/match-id.ts +111 -0
- package/src/router-primitives/paths.ts +109 -0
- package/src/router-primitives/tree.ts +243 -0
- package/src/router-primitives/types.ts +74 -0
- package/src/router.ts +31 -0
- package/src/search-engine/bing.ts +32 -0
- package/src/search-engine/google-jwt.ts +95 -0
- package/src/search-engine/google.ts +139 -0
- package/src/search-engine/index-now.ts +64 -0
- package/src/search-engine/index.ts +12 -0
- package/src/security/index.ts +230 -0
- package/src/server/index.ts +286 -0
- package/src/server-context/browser.ts +113 -0
- package/src/server-context/index.ts +217 -0
- package/src/server-fn/index.ts +710 -0
- package/src/server-fn-query.ts +169 -0
- package/src/server-handler/etag.ts +51 -0
- package/src/server-handler/headers.ts +5 -0
- package/src/server-handler/index.ts +1664 -0
- package/src/server-handler/types.ts +24 -0
- package/src/server-handler/validate-static-params.ts +130 -0
- package/src/server.ts +2 -0
- package/src/service-worker/template.ts +153 -0
- package/src/service-worker-hook/index.ts +70 -0
- package/src/sitemap/index.ts +231 -0
- package/src/ssr/critical-css.ts +150 -0
- package/src/ssr/head-prefix.ts +65 -0
- package/src/ssr/head.ts +259 -0
- package/src/ssr/index.tsx +879 -0
- package/src/state-parser/index.ts +266 -0
- package/src/store/filesystem.ts +131 -0
- package/src/store/index.ts +29 -0
- package/src/styles/index.ts +1200 -0
- package/src/styles/sx-types.ts +11 -0
- package/src/testing/index.ts +251 -0
- package/src/testing.ts +2 -0
- package/src/theme/index.tsx +232 -0
- package/src/theme.ts +3 -0
- package/src/tracing/index.ts +4 -0
- package/src/tracing/noop.ts +13 -0
- package/src/tracing/otel.ts +37 -0
- package/src/tracing/timing.ts +34 -0
- package/src/tracing/types.ts +9 -0
- package/src/url/index.ts +140 -0
- package/src/use-router.ts +11 -0
- package/src/use-suspense-query.ts +2 -0
- package/src/validation/index.ts +57 -0
- package/src/view-transition-css.ts +1 -0
|
@@ -0,0 +1,1200 @@
|
|
|
1
|
+
import { warn } from "../logger.ts";
|
|
2
|
+
|
|
3
|
+
declare const __FLARE_IS_DEV__: boolean | undefined;
|
|
4
|
+
const isDev = typeof __FLARE_IS_DEV__ === "boolean" ? __FLARE_IS_DEV__ : process.env.NODE_ENV !== "production";
|
|
5
|
+
import type { Sx } from "./sx-types.ts";
|
|
6
|
+
|
|
7
|
+
export type { ClassValue, Sx } from "./sx-types.ts";
|
|
8
|
+
|
|
9
|
+
type CSSProperties = Record<string, string>;
|
|
10
|
+
|
|
11
|
+
interface StylesResult {
|
|
12
|
+
"data-c": string;
|
|
13
|
+
style?: CSSProperties;
|
|
14
|
+
[key: `data-${string}`]: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type StateSelectors<S> = {
|
|
18
|
+
[K in keyof S]: (value: S[K]) => string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type VarAccessors<V> = {
|
|
22
|
+
[K in keyof V]: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
interface StylesConfigFull<S extends Record<string, unknown>, V extends Record<string, unknown>> {
|
|
26
|
+
css?: ((s: StateSelectors<S>, v: VarAccessors<V>) => string) | string;
|
|
27
|
+
outerCss?: string;
|
|
28
|
+
state?: S;
|
|
29
|
+
style?: CSSProperties;
|
|
30
|
+
tw?: string;
|
|
31
|
+
vars?: V;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type StylesConfig<S extends Record<string, unknown>, V extends Record<string, unknown>> =
|
|
35
|
+
| StylesConfigFull<S, V>
|
|
36
|
+
| string;
|
|
37
|
+
|
|
38
|
+
export const RUNTIME_SHEET_ID = "flare-runtime";
|
|
39
|
+
const RT_CLASS_PREFIX = "flare-rt-";
|
|
40
|
+
|
|
41
|
+
let maxRegistrySize = 5000;
|
|
42
|
+
|
|
43
|
+
/** @internal visible for testing only */
|
|
44
|
+
export function __setMaxRegistrySize(size: number): void {
|
|
45
|
+
maxRegistrySize = size;
|
|
46
|
+
}
|
|
47
|
+
const registry = new Map<string, string>();
|
|
48
|
+
let domInjectionEnabled = false;
|
|
49
|
+
let ssrSheetPresent = false;
|
|
50
|
+
|
|
51
|
+
export function enableDomInjection(): void {
|
|
52
|
+
domInjectionEnabled = true;
|
|
53
|
+
if (typeof document !== "undefined") {
|
|
54
|
+
const el = document.getElementById(RUNTIME_SHEET_ID) as HTMLStyleElement | null;
|
|
55
|
+
ssrSheetPresent = el !== null && (el.sheet?.cssRules?.length ?? 0) > 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function finishHydration(): void {
|
|
60
|
+
ssrSheetPresent = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hashString(str: string): string {
|
|
64
|
+
let h = 0x811c9dc5;
|
|
65
|
+
for (let i = 0; i < str.length; i++) {
|
|
66
|
+
h = Math.imul(h ^ str.charCodeAt(i), 0x01000193);
|
|
67
|
+
}
|
|
68
|
+
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
|
|
69
|
+
return (h >>> 0).toString(36);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function escapeCSSAttrValue(v: string): string {
|
|
73
|
+
return v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const T_IDENT = 0;
|
|
77
|
+
const T_COLON = 1;
|
|
78
|
+
const T_SEMI = 2;
|
|
79
|
+
const T_LBRACE = 3;
|
|
80
|
+
const T_RBRACE = 4;
|
|
81
|
+
const T_STRING = 5;
|
|
82
|
+
const T_WS = 6;
|
|
83
|
+
const T_AT = 7;
|
|
84
|
+
const T_DELIM = 8;
|
|
85
|
+
const T_FUNC = 9;
|
|
86
|
+
const T_RPAREN = 10;
|
|
87
|
+
const T_EOF = 11;
|
|
88
|
+
const T_BAD_STRING = 12;
|
|
89
|
+
const T_BAD_COMMENT = 13;
|
|
90
|
+
|
|
91
|
+
type Token = { t: number; v: string };
|
|
92
|
+
|
|
93
|
+
function isIdentChar(c: string): boolean {
|
|
94
|
+
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c === "_" || c === "-";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function tokenizeCSS(css: string): Token[] {
|
|
98
|
+
const tokens: Token[] = [];
|
|
99
|
+
let i = 0;
|
|
100
|
+
|
|
101
|
+
while (i < css.length) {
|
|
102
|
+
const ch = css[i];
|
|
103
|
+
|
|
104
|
+
/* whitespace */
|
|
105
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
106
|
+
i++;
|
|
107
|
+
while (i < css.length && (css[i] === " " || css[i] === "\t" || css[i] === "\n" || css[i] === "\r")) i++;
|
|
108
|
+
tokens.push({ t: T_WS, v: " " });
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* block comments */
|
|
113
|
+
if (ch === "/" && i + 1 < css.length && css[i + 1] === "*") {
|
|
114
|
+
i += 2;
|
|
115
|
+
let closed = false;
|
|
116
|
+
while (i < css.length - 1) {
|
|
117
|
+
if (css[i] === "*" && css[i + 1] === "/") {
|
|
118
|
+
closed = true;
|
|
119
|
+
i += 2;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
i++;
|
|
123
|
+
}
|
|
124
|
+
if (!closed) {
|
|
125
|
+
tokens.push({ t: T_BAD_COMMENT, v: "" });
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* strings */
|
|
132
|
+
if (ch === '"' || ch === "'") {
|
|
133
|
+
const q = ch;
|
|
134
|
+
let val = q;
|
|
135
|
+
i++;
|
|
136
|
+
let closed = false;
|
|
137
|
+
while (i < css.length) {
|
|
138
|
+
if (css[i] === "\\") {
|
|
139
|
+
val += css[i];
|
|
140
|
+
i++;
|
|
141
|
+
if (i < css.length) {
|
|
142
|
+
val += css[i];
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (css[i] === q) {
|
|
148
|
+
val += q;
|
|
149
|
+
i++;
|
|
150
|
+
closed = true;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
val += css[i];
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
tokens.push({ t: closed ? T_STRING : T_BAD_STRING, v: val });
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* punctuation */
|
|
161
|
+
if (ch === ":") {
|
|
162
|
+
tokens.push({ t: T_COLON, v: ":" });
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === ";") {
|
|
167
|
+
tokens.push({ t: T_SEMI, v: ";" });
|
|
168
|
+
i++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (ch === "{") {
|
|
172
|
+
tokens.push({ t: T_LBRACE, v: "{" });
|
|
173
|
+
i++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (ch === "}") {
|
|
177
|
+
tokens.push({ t: T_RBRACE, v: "}" });
|
|
178
|
+
i++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (ch === ")") {
|
|
182
|
+
tokens.push({ t: T_RPAREN, v: ")" });
|
|
183
|
+
i++;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (ch === "@") {
|
|
187
|
+
let val = "@";
|
|
188
|
+
i++;
|
|
189
|
+
while (i < css.length && isIdentChar(css[i])) {
|
|
190
|
+
val += css[i];
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
tokens.push({ t: T_AT, v: val });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* ident or function (ident followed by '(') */
|
|
198
|
+
if (isIdentChar(ch)) {
|
|
199
|
+
let val = "";
|
|
200
|
+
while (i < css.length && isIdentChar(css[i])) {
|
|
201
|
+
val += css[i];
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
if (i < css.length && css[i] === "(") {
|
|
205
|
+
val += "(";
|
|
206
|
+
i++;
|
|
207
|
+
tokens.push({ t: T_FUNC, v: val });
|
|
208
|
+
} else {
|
|
209
|
+
tokens.push({ t: T_IDENT, v: val });
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* anything else: & . # ( , + ~ > [ ] ! % = etc */
|
|
215
|
+
tokens.push({ t: T_DELIM, v: ch });
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
tokens.push({ t: T_EOF, v: "" });
|
|
220
|
+
return tokens;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function validateCSS(name: string, css: string): void {
|
|
224
|
+
const trimmed = css.trim();
|
|
225
|
+
if (trimmed.length === 0) return;
|
|
226
|
+
|
|
227
|
+
const tokens = tokenizeCSS(trimmed);
|
|
228
|
+
|
|
229
|
+
/* check for tokenizer-level errors */
|
|
230
|
+
for (const tok of tokens) {
|
|
231
|
+
if (tok.t === T_BAD_STRING) {
|
|
232
|
+
warn("styles", `"${name}": unclosed string literal`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (tok.t === T_BAD_COMMENT) {
|
|
236
|
+
warn("styles", `"${name}": unclosed comment`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* check brace balance */
|
|
242
|
+
let braceDepth = 0;
|
|
243
|
+
for (const tok of tokens) {
|
|
244
|
+
if (tok.t === T_LBRACE) braceDepth++;
|
|
245
|
+
else if (tok.t === T_RBRACE) braceDepth--;
|
|
246
|
+
}
|
|
247
|
+
if (braceDepth !== 0) {
|
|
248
|
+
warn("styles", `"${name}": unbalanced braces (depth ${braceDepth})`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* validate declarations at each brace depth */
|
|
252
|
+
validateTokenDeclarations(name, tokens);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function validateTokenDeclarations(name: string, tokens: Token[]): void {
|
|
256
|
+
let i = 0;
|
|
257
|
+
|
|
258
|
+
while (i < tokens.length && tokens[i].t !== T_EOF) {
|
|
259
|
+
const tok = tokens[i];
|
|
260
|
+
|
|
261
|
+
if (tok.t === T_WS) {
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (tok.t === T_LBRACE) {
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (tok.t === T_RBRACE) {
|
|
270
|
+
i++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (tok.t === T_SEMI) {
|
|
274
|
+
i++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* skip @ rules at current depth — advance past their block or to next ; */
|
|
279
|
+
if (tok.t === T_AT) {
|
|
280
|
+
i++;
|
|
281
|
+
while (i < tokens.length && tokens[i].t !== T_LBRACE && tokens[i].t !== T_SEMI && tokens[i].t !== T_EOF) i++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* skip selectors (& . # or ident followed by { without colon) */
|
|
286
|
+
if (tok.t === T_DELIM && (tok.v === "&" || tok.v === "." || tok.v === "#" || tok.v === "[")) {
|
|
287
|
+
while (i < tokens.length && tokens[i].t !== T_LBRACE && tokens[i].t !== T_SEMI && tokens[i].t !== T_EOF) i++;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* expect a declaration: IDENT WS? COLON value... SEMI */
|
|
292
|
+
if (tok.t === T_IDENT || (tok.t === T_DELIM && tok.v === "*")) {
|
|
293
|
+
const propName = tok.v;
|
|
294
|
+
i++;
|
|
295
|
+
/* skip whitespace */
|
|
296
|
+
while (i < tokens.length && tokens[i].t === T_WS) i++;
|
|
297
|
+
|
|
298
|
+
/* expect colon */
|
|
299
|
+
if (i >= tokens.length || tokens[i].t === T_EOF) break;
|
|
300
|
+
if (tokens[i].t !== T_COLON) {
|
|
301
|
+
/* no colon — might be a selector like `p { ... }` or `from { ... }` */
|
|
302
|
+
/* only treat as selector if a { exists before ; or EOF */
|
|
303
|
+
let hasBrace = false;
|
|
304
|
+
for (let s = i; s < tokens.length && tokens[s].t !== T_SEMI && tokens[s].t !== T_EOF; s++) {
|
|
305
|
+
if (tokens[s].t === T_LBRACE) {
|
|
306
|
+
hasBrace = true;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (hasBrace) {
|
|
311
|
+
while (i < tokens.length && tokens[i].t !== T_LBRACE && tokens[i].t !== T_SEMI && tokens[i].t !== T_EOF) i++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
warn("styles", `"${name}": declaration missing colon after "${propName}"`);
|
|
315
|
+
while (i < tokens.length && tokens[i].t !== T_SEMI && tokens[i].t !== T_RBRACE && tokens[i].t !== T_EOF) i++;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
i++; /* skip colon */
|
|
319
|
+
|
|
320
|
+
/* skip whitespace after colon */
|
|
321
|
+
while (i < tokens.length && tokens[i].t === T_WS) i++;
|
|
322
|
+
|
|
323
|
+
/* check for empty value */
|
|
324
|
+
if (i < tokens.length && (tokens[i].t === T_SEMI || tokens[i].t === T_RBRACE || tokens[i].t === T_EOF)) {
|
|
325
|
+
warn("styles", `"${name}": empty value in declaration "${propName}:"`);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* consume value tokens, watching for missing semicolon pattern */
|
|
330
|
+
let parenDepth = 0;
|
|
331
|
+
while (i < tokens.length && tokens[i].t !== T_SEMI && tokens[i].t !== T_RBRACE && tokens[i].t !== T_EOF) {
|
|
332
|
+
if (tokens[i].t === T_FUNC || (tokens[i].t === T_DELIM && tokens[i].v === "(")) {
|
|
333
|
+
parenDepth++;
|
|
334
|
+
} else if (tokens[i].t === T_RPAREN) {
|
|
335
|
+
if (parenDepth > 0) parenDepth--;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* missing semicolon heuristic: IDENT followed by COLON outside parens */
|
|
339
|
+
if (parenDepth === 0 && tokens[i].t === T_IDENT) {
|
|
340
|
+
/* look ahead past optional whitespace for colon */
|
|
341
|
+
let peek = i + 1;
|
|
342
|
+
while (peek < tokens.length && tokens[peek].t === T_WS) peek++;
|
|
343
|
+
if (peek < tokens.length && tokens[peek].t === T_COLON) {
|
|
344
|
+
warn("styles", `"${name}": missing semicolon before "${tokens[i].v}"`);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
i++;
|
|
350
|
+
}
|
|
351
|
+
if (i < tokens.length && tokens[i].t === T_SEMI) i++;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* skip anything else */
|
|
356
|
+
i++;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const MAX_CSS_SIZE = 512 * 1024;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Match a CSS functional pseudo-selector that wraps a nested rule.
|
|
364
|
+
* Handles `:where(`, `:is(`, `:has(`, `:not(` — CSS nesting constructs
|
|
365
|
+
* emitted by Tailwind v4 for utilities like `space-y-*`, `divide-*`.
|
|
366
|
+
*/
|
|
367
|
+
const NESTED_PSEUDO_RE = /^:(where|is|has|not)\(/;
|
|
368
|
+
|
|
369
|
+
function scopeCSS(name: string, css: string): string {
|
|
370
|
+
if (css.length > MAX_CSS_SIZE) {
|
|
371
|
+
warn("styles", `CSS for ${name} exceeds ${MAX_CSS_SIZE} bytes, skipping`);
|
|
372
|
+
return "";
|
|
373
|
+
}
|
|
374
|
+
const selector = `[data-c="${escapeCSSAttrValue(name)}"]`;
|
|
375
|
+
const trimmed = css.trim();
|
|
376
|
+
if (trimmed.length === 0) return "";
|
|
377
|
+
|
|
378
|
+
const rules: string[] = [];
|
|
379
|
+
let remaining = trimmed;
|
|
380
|
+
|
|
381
|
+
while (remaining.length > 0) {
|
|
382
|
+
remaining = remaining.trim();
|
|
383
|
+
if (remaining.length === 0) break;
|
|
384
|
+
|
|
385
|
+
if (remaining.startsWith("@keyframes")) {
|
|
386
|
+
/* pass through unscoped */
|
|
387
|
+
const braceStart = remaining.indexOf("{");
|
|
388
|
+
if (braceStart === -1) break;
|
|
389
|
+
const end = findMatchingBrace(remaining, braceStart);
|
|
390
|
+
rules.push(remaining.slice(0, end + 1));
|
|
391
|
+
remaining = remaining.slice(end + 1);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (remaining.startsWith("@")) {
|
|
396
|
+
/* at-rules: wrap content with scope */
|
|
397
|
+
const braceStart = remaining.indexOf("{");
|
|
398
|
+
if (braceStart === -1) break;
|
|
399
|
+
const atRule = remaining.slice(0, braceStart).trim();
|
|
400
|
+
const end = findMatchingBrace(remaining, braceStart);
|
|
401
|
+
const content = remaining.slice(braceStart + 1, end).trim();
|
|
402
|
+
const scopedContent = scopeBlock(name, content);
|
|
403
|
+
rules.push(`${atRule} { ${scopedContent} }`);
|
|
404
|
+
remaining = remaining.slice(end + 1);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (remaining.startsWith("&")) {
|
|
409
|
+
/* & rule */
|
|
410
|
+
const braceStart = remaining.indexOf("{");
|
|
411
|
+
if (braceStart === -1) break;
|
|
412
|
+
const selectorPart = remaining.slice(0, braceStart).trim();
|
|
413
|
+
const end = findMatchingBrace(remaining, braceStart);
|
|
414
|
+
const body = remaining.slice(braceStart + 1, end).trim();
|
|
415
|
+
const replaced = selectorPart.replace(/&/g, selector);
|
|
416
|
+
rules.push(`${replaced} { ${body} }`);
|
|
417
|
+
remaining = remaining.slice(end + 1);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/*
|
|
422
|
+
* CSS nesting: `:where(& > ...)`, `:is(&...)`, `:has(&...)`, `:not(&...)`
|
|
423
|
+
* These are functional pseudo-selectors wrapping `&`-relative selectors.
|
|
424
|
+
* Tailwind v4 emits these for space-y-*, divide-*, etc.
|
|
425
|
+
* Find the matching `)`, then the `{`, treat as a scoped selector block.
|
|
426
|
+
*/
|
|
427
|
+
if (NESTED_PSEUDO_RE.test(remaining)) {
|
|
428
|
+
const braceStart = findBraceAfterMatchingParen(remaining);
|
|
429
|
+
if (braceStart !== -1) {
|
|
430
|
+
const selectorPart = remaining.slice(0, braceStart).trim();
|
|
431
|
+
const end = findMatchingBrace(remaining, braceStart);
|
|
432
|
+
const body = remaining.slice(braceStart + 1, end).trim();
|
|
433
|
+
const replaced = selectorPart.replace(/&/g, selector);
|
|
434
|
+
rules.push(`${replaced} { ${body} }`);
|
|
435
|
+
remaining = remaining.slice(end + 1);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* check if this is a rule with braces (non-& selector) */
|
|
441
|
+
const braceIdx = remaining.indexOf("{");
|
|
442
|
+
const semiIdx = remaining.indexOf(";");
|
|
443
|
+
if (braceIdx !== -1 && (semiIdx === -1 || braceIdx < semiIdx)) {
|
|
444
|
+
/* has braces before semicolon — it's a rule block */
|
|
445
|
+
const end = findMatchingBrace(remaining, braceIdx);
|
|
446
|
+
const body = remaining.slice(braceIdx + 1, end).trim();
|
|
447
|
+
const selectorPrefix = remaining.slice(0, braceIdx).trim();
|
|
448
|
+
if (selectorPrefix.includes("&")) {
|
|
449
|
+
const replaced = selectorPrefix.replace(/&/g, selector);
|
|
450
|
+
rules.push(`${replaced} { ${body} }`);
|
|
451
|
+
} else {
|
|
452
|
+
rules.push(`${selector} ${selectorPrefix} { ${body} }`);
|
|
453
|
+
}
|
|
454
|
+
remaining = remaining.slice(end + 1);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* plain declarations — collect up to next rule-like construct */
|
|
459
|
+
const nextAt = findUnquotedChar(remaining, "@");
|
|
460
|
+
const nextAmp = findUnquotedChar(remaining, "&");
|
|
461
|
+
/*
|
|
462
|
+
* Find nested pseudo-selector starts (:where, :is, :has, :not) in remaining.
|
|
463
|
+
* These need to be treated as rule boundaries — the `&` inside them is NOT
|
|
464
|
+
* a standalone `&` rule start.
|
|
465
|
+
*/
|
|
466
|
+
const nextNestedPseudo = findNestedPseudoStart(remaining);
|
|
467
|
+
/* Find next selector block: look for `{` preceded by a non-declaration context */
|
|
468
|
+
let nextRuleStart = -1;
|
|
469
|
+
for (let scan = 0; scan < remaining.length; scan++) {
|
|
470
|
+
const c = remaining[scan];
|
|
471
|
+
if (c === "{") {
|
|
472
|
+
/* Walk back to find the start of the selector */
|
|
473
|
+
let selStart = scan - 1;
|
|
474
|
+
while (selStart >= 0 && remaining[selStart] === " ") selStart--;
|
|
475
|
+
/* If preceded by a semicolon or start-of-string, there's a selector before the brace */
|
|
476
|
+
if (selStart >= 0) {
|
|
477
|
+
/* Find the start of this selector (after last ; or start) */
|
|
478
|
+
let lineStart = remaining.lastIndexOf(";", selStart);
|
|
479
|
+
lineStart = lineStart === -1 ? 0 : lineStart + 1;
|
|
480
|
+
const candidateSel = remaining.slice(lineStart, scan).trim();
|
|
481
|
+
/* If it looks like a selector (contains . or & or alphabetic start), split here */
|
|
482
|
+
if (candidateSel.length > 0 && /^[.&#@a-zA-Z:[]/.test(candidateSel)) {
|
|
483
|
+
nextRuleStart = lineStart;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/*
|
|
491
|
+
* Prefer nested pseudo position over raw `&` — a `&` inside `:where(&...)`
|
|
492
|
+
* is not a standalone `&` rule start, the split should be at the `:where(`.
|
|
493
|
+
*/
|
|
494
|
+
let effectiveAmp = nextAmp;
|
|
495
|
+
if (nextNestedPseudo > 0 && (effectiveAmp === -1 || nextNestedPseudo <= effectiveAmp)) {
|
|
496
|
+
effectiveAmp = -1;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* Also consider @ and & positions */
|
|
500
|
+
const candidates = [nextAt, effectiveAmp, nextRuleStart, nextNestedPseudo].filter((n) => n > 0);
|
|
501
|
+
if (candidates.length > 0) {
|
|
502
|
+
const splitAt = Math.min(...candidates);
|
|
503
|
+
const declPart = remaining.slice(0, splitAt).trim();
|
|
504
|
+
if (declPart.length > 0) {
|
|
505
|
+
rules.push(`${selector} { ${declPart} }`);
|
|
506
|
+
}
|
|
507
|
+
remaining = remaining.slice(splitAt);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
rules.push(`${selector} { ${remaining} }`);
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return rules.join("\n");
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Find the `{` that follows the matching `)` of a functional pseudo-selector.
|
|
520
|
+
* E.g. for `:where(& > :not(:last-child)) { ... }`, returns the index of `{`.
|
|
521
|
+
*/
|
|
522
|
+
function findBraceAfterMatchingParen(str: string): number {
|
|
523
|
+
const openParen = str.indexOf("(");
|
|
524
|
+
if (openParen === -1) return -1;
|
|
525
|
+
let depth = 1;
|
|
526
|
+
let i = openParen + 1;
|
|
527
|
+
for (; i < str.length; i++) {
|
|
528
|
+
if (str[i] === "(") depth++;
|
|
529
|
+
else if (str[i] === ")") {
|
|
530
|
+
depth--;
|
|
531
|
+
if (depth === 0) break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (depth !== 0) return -1;
|
|
535
|
+
/* Skip whitespace after `)`, expect `{` */
|
|
536
|
+
i++;
|
|
537
|
+
while (i < str.length && (str[i] === " " || str[i] === "\t" || str[i] === "\n")) i++;
|
|
538
|
+
return i < str.length && str[i] === "{" ? i : -1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Find the first `:where(`, `:is(`, `:has(`, or `:not(` that contains `&`,
|
|
543
|
+
* signaling a CSS nesting construct. Returns the position of the `:` or -1.
|
|
544
|
+
*/
|
|
545
|
+
function findNestedPseudoStart(str: string): number {
|
|
546
|
+
let i = 0;
|
|
547
|
+
while (i < str.length) {
|
|
548
|
+
if (str[i] === ":" && i + 1 < str.length) {
|
|
549
|
+
const after = str.slice(i + 1);
|
|
550
|
+
const m = after.match(/^(where|is|has|not)\(/);
|
|
551
|
+
if (m) {
|
|
552
|
+
/* Verify this pseudo contains `&` (it's a nesting construct, not a plain pseudo) */
|
|
553
|
+
const parenStart = i + 1 + m[1].length;
|
|
554
|
+
let depth = 1;
|
|
555
|
+
let j = parenStart + 1;
|
|
556
|
+
let hasAmp = false;
|
|
557
|
+
for (; j < str.length; j++) {
|
|
558
|
+
if (str[j] === "(") depth++;
|
|
559
|
+
else if (str[j] === ")") {
|
|
560
|
+
depth--;
|
|
561
|
+
if (depth === 0) break;
|
|
562
|
+
} else if (str[j] === "&") {
|
|
563
|
+
hasAmp = true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (hasAmp) return i;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
i++;
|
|
570
|
+
}
|
|
571
|
+
return -1;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* Delegate to scopeCSS so at-rule bodies get full rule-level parsing (no blind & replace in values) */
|
|
575
|
+
function scopeBlock(name: string, content: string): string {
|
|
576
|
+
return scopeCSS(name, content);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function findMatchingBrace(str: string, start: number): number {
|
|
580
|
+
let depth = 0;
|
|
581
|
+
for (let i = start; i < str.length; i++) {
|
|
582
|
+
const ch = str[i];
|
|
583
|
+
/* Skip string literals (handles escaped quotes) */
|
|
584
|
+
if (ch === '"' || ch === "'") {
|
|
585
|
+
const quote = ch;
|
|
586
|
+
i++;
|
|
587
|
+
while (i < str.length && str[i] !== quote) {
|
|
588
|
+
if (str[i] === "\\") i++;
|
|
589
|
+
i++;
|
|
590
|
+
}
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
/* Skip block comments */
|
|
594
|
+
if (ch === "/" && i + 1 < str.length && str[i + 1] === "*") {
|
|
595
|
+
i += 2;
|
|
596
|
+
while (i < str.length - 1 && !(str[i] === "*" && str[i + 1] === "/")) {
|
|
597
|
+
i++;
|
|
598
|
+
}
|
|
599
|
+
i++;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
/* Skip unquoted url() content — braces inside url() are not CSS block braces */
|
|
603
|
+
if (ch === "u" && str.slice(i, i + 4) === "url(") {
|
|
604
|
+
i += 4;
|
|
605
|
+
/* If url starts with quote, string-skipping above handles the content */
|
|
606
|
+
if (i < str.length && (str[i] === '"' || str[i] === "'")) {
|
|
607
|
+
const quote = str[i];
|
|
608
|
+
i++;
|
|
609
|
+
while (i < str.length && str[i] !== quote) {
|
|
610
|
+
if (str[i] === "\\") i++;
|
|
611
|
+
i++;
|
|
612
|
+
}
|
|
613
|
+
/* skip closing quote */
|
|
614
|
+
if (i < str.length) i++;
|
|
615
|
+
} else {
|
|
616
|
+
/* Unquoted url — skip until closing paren */
|
|
617
|
+
while (i < str.length && str[i] !== ")") {
|
|
618
|
+
if (str[i] === "\\") i++;
|
|
619
|
+
i++;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/* skip closing paren */
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (ch === "{") depth++;
|
|
626
|
+
else if (ch === "}") {
|
|
627
|
+
depth--;
|
|
628
|
+
if (depth === 0) return i;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return str.length - 1;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function minifyCSS(css: string): string {
|
|
635
|
+
if (css.length === 0) return css;
|
|
636
|
+
|
|
637
|
+
let result = "";
|
|
638
|
+
let i = 0;
|
|
639
|
+
/*
|
|
640
|
+
* Paren depth tracks whether we are inside a functional context like
|
|
641
|
+
* `@media (min-width: 768px)` or `calc(...)`. Colons inside parens must
|
|
642
|
+
* NOT be treated as declaration separators — their surrounding spaces are
|
|
643
|
+
* significant for at-rule conditions.
|
|
644
|
+
*/
|
|
645
|
+
let parenDepth = 0;
|
|
646
|
+
|
|
647
|
+
while (i < css.length) {
|
|
648
|
+
/* skip block comments */
|
|
649
|
+
if (css[i] === "/" && i + 1 < css.length && css[i + 1] === "*") {
|
|
650
|
+
i += 2;
|
|
651
|
+
while (i < css.length - 1 && !(css[i] === "*" && css[i + 1] === "/")) i++;
|
|
652
|
+
i += 2;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* preserve quoted strings */
|
|
657
|
+
if (css[i] === '"' || css[i] === "'") {
|
|
658
|
+
const q = css[i];
|
|
659
|
+
result += q;
|
|
660
|
+
i++;
|
|
661
|
+
while (i < css.length && css[i] !== q) {
|
|
662
|
+
if (css[i] === "\\") {
|
|
663
|
+
result += css[i];
|
|
664
|
+
i++;
|
|
665
|
+
}
|
|
666
|
+
if (i < css.length) {
|
|
667
|
+
result += css[i];
|
|
668
|
+
i++;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (i < css.length) {
|
|
672
|
+
result += css[i];
|
|
673
|
+
i++;
|
|
674
|
+
}
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* preserve url() content */
|
|
679
|
+
if (css[i] === "u" && css.slice(i, i + 4) === "url(") {
|
|
680
|
+
result += "url(";
|
|
681
|
+
i += 4;
|
|
682
|
+
parenDepth++;
|
|
683
|
+
/* skip whitespace after url( */
|
|
684
|
+
while (i < css.length && (css[i] === " " || css[i] === "\t" || css[i] === "\n")) i++;
|
|
685
|
+
if (i < css.length && (css[i] === '"' || css[i] === "'")) {
|
|
686
|
+
/* quoted url — handled by string preservation above on next iteration */
|
|
687
|
+
} else {
|
|
688
|
+
/* unquoted url */
|
|
689
|
+
while (i < css.length && css[i] !== ")") {
|
|
690
|
+
result += css[i];
|
|
691
|
+
i++;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/* track paren depth */
|
|
698
|
+
if (css[i] === "(") {
|
|
699
|
+
parenDepth++;
|
|
700
|
+
result += "(";
|
|
701
|
+
i++;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (css[i] === ")") {
|
|
705
|
+
if (parenDepth > 0) parenDepth--;
|
|
706
|
+
result += ")";
|
|
707
|
+
i++;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* collapse whitespace */
|
|
712
|
+
if (css[i] === " " || css[i] === "\t" || css[i] === "\n" || css[i] === "\r") {
|
|
713
|
+
i++;
|
|
714
|
+
while (i < css.length && (css[i] === " " || css[i] === "\t" || css[i] === "\n" || css[i] === "\r")) i++;
|
|
715
|
+
/* inside parens: keep one space (at-rule conditions, calc, etc.) */
|
|
716
|
+
if (parenDepth > 0) {
|
|
717
|
+
result += " ";
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
/* outside parens: skip space around : ; { } , */
|
|
721
|
+
const prev = result.length > 0 ? result[result.length - 1] : "";
|
|
722
|
+
const next = i < css.length ? css[i] : "";
|
|
723
|
+
if (
|
|
724
|
+
prev === ":" ||
|
|
725
|
+
prev === ";" ||
|
|
726
|
+
prev === "{" ||
|
|
727
|
+
prev === "}" ||
|
|
728
|
+
prev === "," ||
|
|
729
|
+
next === ":" ||
|
|
730
|
+
next === ";" ||
|
|
731
|
+
next === "{" ||
|
|
732
|
+
next === "}" ||
|
|
733
|
+
next === ","
|
|
734
|
+
) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
result += " ";
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
result += css[i];
|
|
742
|
+
i++;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/* remove trailing semicolons before } */
|
|
746
|
+
result = result.replace(/;}/g, "}");
|
|
747
|
+
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function findUnquotedChar(str: string, char: string, from = 0): number {
|
|
752
|
+
for (let i = from; i < str.length; i++) {
|
|
753
|
+
const c = str[i];
|
|
754
|
+
if (c === '"' || c === "'") {
|
|
755
|
+
const q = c;
|
|
756
|
+
i++;
|
|
757
|
+
while (i < str.length && str[i] !== q) {
|
|
758
|
+
if (str[i] === "\\") i++;
|
|
759
|
+
i++;
|
|
760
|
+
}
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (c === "/" && i + 1 < str.length && str[i + 1] === "*") {
|
|
764
|
+
i += 2;
|
|
765
|
+
while (i < str.length - 1 && !(str[i] === "*" && str[i + 1] === "/")) i++;
|
|
766
|
+
i++;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (c === "u" && str.slice(i, i + 4) === "url(") {
|
|
770
|
+
i += 4;
|
|
771
|
+
if (i < str.length && (str[i] === '"' || str[i] === "'")) {
|
|
772
|
+
const q = str[i];
|
|
773
|
+
i++;
|
|
774
|
+
while (i < str.length && str[i] !== q) {
|
|
775
|
+
if (str[i] === "\\") i++;
|
|
776
|
+
i++;
|
|
777
|
+
}
|
|
778
|
+
if (i < str.length) i++;
|
|
779
|
+
} else {
|
|
780
|
+
while (i < str.length && str[i] !== ")") {
|
|
781
|
+
if (str[i] === "\\") i++;
|
|
782
|
+
i++;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if (c === char) return i;
|
|
788
|
+
}
|
|
789
|
+
return -1;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function createStateSelectors<S extends Record<string, unknown>>(_state: S): StateSelectors<S> {
|
|
793
|
+
return new Proxy({} as StateSelectors<S>, {
|
|
794
|
+
get(_target, prop: string) {
|
|
795
|
+
return (value: unknown) => `&[data-${prop}="${escapeCSSAttrValue(String(value))}"]`;
|
|
796
|
+
},
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
function createVarAccessors<V extends Record<string, unknown>>(
|
|
801
|
+
vars: V,
|
|
802
|
+
): { accessors: VarAccessors<V>; styleMap: CSSProperties } {
|
|
803
|
+
const keys = Object.keys(vars).sort();
|
|
804
|
+
const styleMap: CSSProperties = {};
|
|
805
|
+
const indexMap = new Map<string, number>();
|
|
806
|
+
|
|
807
|
+
for (let i = 0; i < keys.length; i++) {
|
|
808
|
+
indexMap.set(keys[i], i);
|
|
809
|
+
styleMap[`--_${i}`] = String(vars[keys[i]]);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const accessors = new Proxy({} as VarAccessors<V>, {
|
|
813
|
+
get(_target, prop: string) {
|
|
814
|
+
const idx = indexMap.get(prop);
|
|
815
|
+
if (idx !== undefined) return `var(--_${idx})`;
|
|
816
|
+
throw new Error(`styles(): unknown var key "${prop}". Available keys: ${keys.join(", ")}`);
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
return { accessors, styleMap };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function styles<
|
|
824
|
+
S extends Record<string, unknown> = Record<string, never>,
|
|
825
|
+
V extends Record<string, unknown> = Record<string, never>,
|
|
826
|
+
>(name: string, config: StylesConfig<S, V>): StylesResult {
|
|
827
|
+
if (typeof config === "string") {
|
|
828
|
+
registerCSSByName(name, config);
|
|
829
|
+
return { "data-c": name };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
let effectiveName = name;
|
|
833
|
+
if (config.outerCss) {
|
|
834
|
+
effectiveName = `${name}-${hashString(config.outerCss)}`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const result: StylesResult = { "data-c": effectiveName };
|
|
838
|
+
|
|
839
|
+
/* state → data-* attributes */
|
|
840
|
+
if (config.state) {
|
|
841
|
+
for (const [key, value] of Object.entries(config.state)) {
|
|
842
|
+
result[`data-${key}` as `data-${string}`] = String(value);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/* vars → inline style + accessors */
|
|
847
|
+
let varAccessors: VarAccessors<V> = {} as VarAccessors<V>;
|
|
848
|
+
let varStyles: CSSProperties = {};
|
|
849
|
+
if (config.vars) {
|
|
850
|
+
const va = createVarAccessors(config.vars);
|
|
851
|
+
varAccessors = va.accessors;
|
|
852
|
+
varStyles = va.styleMap;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/* merge style */
|
|
856
|
+
if (config.style || Object.keys(varStyles).length > 0) {
|
|
857
|
+
result.style = { ...varStyles, ...config.style };
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/* build CSS */
|
|
861
|
+
const stateSelectors = config.state ? createStateSelectors(config.state) : ({} as StateSelectors<S>);
|
|
862
|
+
let cssText = "";
|
|
863
|
+
|
|
864
|
+
if (config.tw) {
|
|
865
|
+
cssText += config.tw;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (config.css) {
|
|
869
|
+
const cssPart = typeof config.css === "function" ? config.css(stateSelectors, varAccessors) : config.css;
|
|
870
|
+
if (cssText.length > 0) {
|
|
871
|
+
cssText += `\n${cssPart}`;
|
|
872
|
+
} else {
|
|
873
|
+
cssText = cssPart;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (config.outerCss) {
|
|
878
|
+
cssText += `\n${config.outerCss}`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (cssText.length > 0) {
|
|
882
|
+
registerCSSByName(effectiveName, cssText);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export function registerCSS(css: string): string {
|
|
889
|
+
const hash = hashString(css.trim());
|
|
890
|
+
registerCSSByName(hash, css);
|
|
891
|
+
return hash;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export function registerCSSByName(name: string, css: string): string {
|
|
895
|
+
if (registry.has(name)) return name;
|
|
896
|
+
if (isDev) validateCSS(name, css);
|
|
897
|
+
const scoped = minifyCSS(scopeCSS(name, css));
|
|
898
|
+
registry.set(name, scoped);
|
|
899
|
+
injectStyleToDOM(scoped);
|
|
900
|
+
/* FIFO eviction: remove oldest entries when over limit */
|
|
901
|
+
if (registry.size > maxRegistrySize) {
|
|
902
|
+
const iter = registry.keys();
|
|
903
|
+
while (registry.size > maxRegistrySize) {
|
|
904
|
+
const oldest = iter.next();
|
|
905
|
+
if (oldest.done) break;
|
|
906
|
+
registry.delete(oldest.value);
|
|
907
|
+
}
|
|
908
|
+
rebuildSheet();
|
|
909
|
+
}
|
|
910
|
+
return name;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* HMR-only: forcibly update an existing registry entry.
|
|
915
|
+
* Deletes the old scoped CSS, re-scopes, re-injects.
|
|
916
|
+
* Never call in production — guarded at call site by import.meta.hot.
|
|
917
|
+
*/
|
|
918
|
+
export function forceRegisterCSSByName(name: string, css: string): void {
|
|
919
|
+
registry.delete(name);
|
|
920
|
+
registerCSSByName(name, css);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Split minified CSS into individual rules by tracking brace depth.
|
|
925
|
+
* Handles nested @media / @supports blocks correctly.
|
|
926
|
+
*/
|
|
927
|
+
function splitCSSRules(css: string): string[] {
|
|
928
|
+
const rules: string[] = [];
|
|
929
|
+
let depth = 0;
|
|
930
|
+
let start = 0;
|
|
931
|
+
for (let i = 0; i < css.length; i++) {
|
|
932
|
+
if (css[i] === "{") depth++;
|
|
933
|
+
else if (css[i] === "}") {
|
|
934
|
+
depth--;
|
|
935
|
+
if (depth === 0) {
|
|
936
|
+
const rule = css.slice(start, i + 1).trim();
|
|
937
|
+
if (rule.length > 0) rules.push(rule);
|
|
938
|
+
start = i + 1;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return rules;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function getStyleEl(): HTMLStyleElement {
|
|
946
|
+
let styleEl = document.getElementById(RUNTIME_SHEET_ID) as HTMLStyleElement | null;
|
|
947
|
+
if (!styleEl) {
|
|
948
|
+
styleEl = document.createElement("style");
|
|
949
|
+
styleEl.id = RUNTIME_SHEET_ID;
|
|
950
|
+
document.head.appendChild(styleEl);
|
|
951
|
+
}
|
|
952
|
+
return styleEl;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Inject scoped CSS into the DOM via CSSOM insertRule (CSP-safe).
|
|
957
|
+
* Falls back to textContent append for environments without CSSOM (jsdom).
|
|
958
|
+
*/
|
|
959
|
+
function injectStyleToDOM(scoped: string): void {
|
|
960
|
+
if (!domInjectionEnabled || ssrSheetPresent || typeof document === "undefined") return;
|
|
961
|
+
const styleEl = getStyleEl();
|
|
962
|
+
const sheet = styleEl.sheet;
|
|
963
|
+
if (sheet) {
|
|
964
|
+
for (const rule of splitCSSRules(scoped)) {
|
|
965
|
+
try {
|
|
966
|
+
sheet.insertRule(rule, sheet.cssRules.length);
|
|
967
|
+
} catch {
|
|
968
|
+
/* Invalid rule — dev validator catches these separately */
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} else {
|
|
972
|
+
styleEl.appendChild(document.createTextNode(scoped));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Rebuild the sheet from current registry after eviction.
|
|
978
|
+
*/
|
|
979
|
+
function rebuildSheet(): void {
|
|
980
|
+
if (!domInjectionEnabled || typeof document === "undefined") return;
|
|
981
|
+
const styleEl = getStyleEl();
|
|
982
|
+
const sheet = styleEl.sheet;
|
|
983
|
+
if (sheet) {
|
|
984
|
+
while (sheet.cssRules.length > 0) sheet.deleteRule(0);
|
|
985
|
+
for (const scoped of registry.values()) {
|
|
986
|
+
for (const rule of splitCSSRules(scoped)) {
|
|
987
|
+
try {
|
|
988
|
+
sheet.insertRule(rule, sheet.cssRules.length);
|
|
989
|
+
} catch {}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
} else {
|
|
993
|
+
styleEl.textContent = Array.from(registry.values()).join("");
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
export function getScopedStyles(): string {
|
|
998
|
+
return Array.from(registry.values()).join("") + Array.from(classRegistry.values()).join("");
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const classRegistry = new Map<string, string>();
|
|
1002
|
+
|
|
1003
|
+
function wrapInLayer(css: string, layer: "user.lib" | "user.app"): string {
|
|
1004
|
+
return `@layer ${layer}{${css}}`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** Scope raw CSS text under a class selector (`.cls`) instead of an attribute selector. */
|
|
1008
|
+
function scopeCssToClass(className: string, css: string): string {
|
|
1009
|
+
/* scopeCSS uses `[data-c="<name>"]` as the scope selector.
|
|
1010
|
+
* We pass a sentinel name, then replace every occurrence with `.cls`. */
|
|
1011
|
+
const sentinel = `__flare_scope_sentinel_${className}__`;
|
|
1012
|
+
const scoped = scopeCSS(sentinel, css);
|
|
1013
|
+
/* Replace `[data-c="<sentinel>"]` with `.cls` */
|
|
1014
|
+
const attrSel = `[data-c="${escapeCSSAttrValue(sentinel)}"]`;
|
|
1015
|
+
return scoped.split(attrSel).join(`.${className}`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Register class-scoped CSS (from compileSx / compileCss).
|
|
1020
|
+
* Mirrors registerCSSByName but keys by class name and uses class selectors.
|
|
1021
|
+
*/
|
|
1022
|
+
export function registerCSSAsClass(className: string, scopedCssInLayer: string): string {
|
|
1023
|
+
if (classRegistry.has(className)) return className;
|
|
1024
|
+
classRegistry.set(className, scopedCssInLayer);
|
|
1025
|
+
injectStyleToDOM(scopedCssInLayer);
|
|
1026
|
+
/* FIFO eviction mirrors main registry */
|
|
1027
|
+
if (classRegistry.size > maxRegistrySize) {
|
|
1028
|
+
const iter = classRegistry.keys();
|
|
1029
|
+
while (classRegistry.size > maxRegistrySize) {
|
|
1030
|
+
const oldest = iter.next();
|
|
1031
|
+
if (oldest.done) break;
|
|
1032
|
+
classRegistry.delete(oldest.value);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return className;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** Serialize an Sx object to stable CSS text (keys sorted at every level). */
|
|
1039
|
+
function serializeSx(sx: Sx): string {
|
|
1040
|
+
const parts: string[] = [];
|
|
1041
|
+
const sorted = Object.keys(sx).sort();
|
|
1042
|
+
for (const key of sorted) {
|
|
1043
|
+
if (key === "variants") continue;
|
|
1044
|
+
const val = (sx as Record<string, unknown>)[key];
|
|
1045
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
1046
|
+
parts.push(`${key}{${serializeSx(val as Sx)}}`);
|
|
1047
|
+
} else {
|
|
1048
|
+
parts.push(`${key}:${String(val)};`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/* variants block */
|
|
1052
|
+
const variants = sx.variants;
|
|
1053
|
+
if (variants) {
|
|
1054
|
+
for (const varName of Object.keys(variants).sort()) {
|
|
1055
|
+
const values = variants[varName];
|
|
1056
|
+
for (const varVal of Object.keys(values).sort()) {
|
|
1057
|
+
parts.push(`variants.${varName}.${varVal}{${serializeSx(values[varVal])}}`);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
return parts.join("");
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Runtime fallback for unresolvable-static sx objects.
|
|
1066
|
+
* Returns a stable class name and optional inline style for dynamic vars.
|
|
1067
|
+
*/
|
|
1068
|
+
export function compileSx(
|
|
1069
|
+
obj: Sx,
|
|
1070
|
+
layer: "user.lib" | "user.app" = "user.app",
|
|
1071
|
+
): { class: string; style?: Record<string, string> } {
|
|
1072
|
+
const serialized = serializeSx(obj);
|
|
1073
|
+
const hash = hashString(serialized);
|
|
1074
|
+
const cls = `${RT_CLASS_PREFIX}${hash}`;
|
|
1075
|
+
|
|
1076
|
+
if (classRegistry.has(cls)) return { class: cls };
|
|
1077
|
+
|
|
1078
|
+
const cssLines: string[] = [];
|
|
1079
|
+
buildSxCSS(obj, `.${cls}`, cssLines);
|
|
1080
|
+
const minified = minifyCSS(cssLines.join(" "));
|
|
1081
|
+
const layered = wrapInLayer(minified, layer);
|
|
1082
|
+
|
|
1083
|
+
registerCSSAsClass(cls, layered);
|
|
1084
|
+
return { class: cls };
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function camelToKebab(prop: string): string {
|
|
1088
|
+
return prop.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function buildSxCSS(sx: Sx, scopeSelector: string, out: string[]): void {
|
|
1092
|
+
const declarations: string[] = [];
|
|
1093
|
+
const raw = sx as Record<string, unknown>;
|
|
1094
|
+
|
|
1095
|
+
for (const key of Object.keys(raw).sort()) {
|
|
1096
|
+
if (key === "variants") continue;
|
|
1097
|
+
const val = raw[key];
|
|
1098
|
+
|
|
1099
|
+
if (key.startsWith("&")) {
|
|
1100
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
1101
|
+
buildSxCSS(val as Sx, key.replace(/&/g, scopeSelector), out);
|
|
1102
|
+
}
|
|
1103
|
+
} else if (key.startsWith("@")) {
|
|
1104
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
1105
|
+
const inner: string[] = [];
|
|
1106
|
+
buildSxCSS(val as Sx, scopeSelector, inner);
|
|
1107
|
+
out.push(`${key}{${inner.join(" ")}}`);
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
declarations.push(`${camelToKebab(key)}:${String(val)};`);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (declarations.length > 0) {
|
|
1115
|
+
out.push(`${scopeSelector}{${declarations.join("")}}`);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const variants = sx.variants;
|
|
1119
|
+
if (variants) {
|
|
1120
|
+
for (const varName of Object.keys(variants).sort()) {
|
|
1121
|
+
const values = variants[varName];
|
|
1122
|
+
for (const varVal of Object.keys(values).sort()) {
|
|
1123
|
+
buildSxCSS(values[varVal], `${scopeSelector}[data-${varName}="${varVal}"]`, out);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Register raw CSS text under a stable content-hashed class name.
|
|
1131
|
+
* Serves the `css="..."` prop and manual CMS calls.
|
|
1132
|
+
*/
|
|
1133
|
+
export function compileCss(text: string, layer: "user.lib" | "user.app" = "user.app"): string {
|
|
1134
|
+
const trimmed = text.trim();
|
|
1135
|
+
const hash = hashString(trimmed);
|
|
1136
|
+
const cls = `${RT_CLASS_PREFIX}${hash}`;
|
|
1137
|
+
|
|
1138
|
+
if (classRegistry.has(cls)) return cls;
|
|
1139
|
+
|
|
1140
|
+
const scoped = scopeCssToClass(cls, trimmed);
|
|
1141
|
+
const minified = minifyCSS(scoped);
|
|
1142
|
+
const layered = wrapInLayer(minified, layer);
|
|
1143
|
+
|
|
1144
|
+
registerCSSAsClass(cls, layered);
|
|
1145
|
+
return cls;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Accepted input shapes for `cn()`. Superset of the JSX `ClassValue` — adds object maps. */
|
|
1149
|
+
export type CnValue = string | false | null | undefined | Record<string, boolean> | CnValue[];
|
|
1150
|
+
|
|
1151
|
+
function collectClasses(input: CnValue, seen: Set<string>, out: string[]): void {
|
|
1152
|
+
if (!input) return;
|
|
1153
|
+
if (typeof input === "string") {
|
|
1154
|
+
/* Split on whitespace — each token deduped individually */
|
|
1155
|
+
for (const token of input.trim().split(/\s+/)) {
|
|
1156
|
+
if (token && !seen.has(token)) {
|
|
1157
|
+
seen.add(token);
|
|
1158
|
+
out.push(token);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (Array.isArray(input)) {
|
|
1164
|
+
for (const item of input) collectClasses(item as CnValue, seen, out);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (typeof input === "object") {
|
|
1168
|
+
for (const [key, active] of Object.entries(input)) {
|
|
1169
|
+
if (active && !seen.has(key)) {
|
|
1170
|
+
seen.add(key);
|
|
1171
|
+
out.push(key);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
/** Merge class names, filter falsy, deduplicate. Matches clsx API. */
|
|
1178
|
+
export function cn(...inputs: CnValue[]): string {
|
|
1179
|
+
const seen = new Set<string>();
|
|
1180
|
+
const out: string[] = [];
|
|
1181
|
+
for (const input of inputs) collectClasses(input, seen, out);
|
|
1182
|
+
return out.join(" ");
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
export function clearScopedStyles(): void {
|
|
1186
|
+
registry.clear();
|
|
1187
|
+
classRegistry.clear();
|
|
1188
|
+
domInjectionEnabled = false;
|
|
1189
|
+
ssrSheetPresent = false;
|
|
1190
|
+
if (typeof document !== "undefined") {
|
|
1191
|
+
const styleEl = document.getElementById(RUNTIME_SHEET_ID) as HTMLStyleElement | null;
|
|
1192
|
+
if (styleEl) {
|
|
1193
|
+
styleEl.textContent = "";
|
|
1194
|
+
const sheet = styleEl.sheet;
|
|
1195
|
+
if (sheet) {
|
|
1196
|
+
while (sheet.cssRules.length > 0) sheet.deleteRule(0);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|