stoop 0.2.0 → 0.3.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/dist/api/create-theme.js +43 -0
- package/dist/api/css.js +20 -0
- package/dist/api/global-css.d.ts +0 -11
- package/dist/api/global-css.js +89 -0
- package/dist/api/keyframes.js +95 -0
- package/dist/api/provider.d.ts +3 -3
- package/dist/api/provider.js +109 -0
- package/dist/api/styled.js +170 -0
- package/dist/api/use-theme.js +21 -0
- package/dist/constants.d.ts +2 -3
- package/dist/constants.js +144 -0
- package/dist/core/cache.d.ts +5 -9
- package/dist/core/cache.js +68 -0
- package/dist/core/compiler.js +198 -0
- package/dist/core/theme-manager.d.ts +0 -8
- package/dist/core/theme-manager.js +97 -0
- package/dist/core/variants.js +32 -0
- package/dist/create-stoop-server.d.ts +33 -0
- package/dist/create-stoop-server.js +130 -0
- package/dist/create-stoop.d.ts +2 -5
- package/dist/create-stoop.js +147 -0
- package/dist/index.js +5 -13
- package/dist/inject/browser.d.ts +2 -3
- package/dist/inject/browser.js +149 -0
- package/dist/inject/dedup.js +38 -0
- package/dist/inject/index.d.ts +0 -1
- package/dist/inject/index.js +75 -0
- package/dist/inject/ssr.d.ts +0 -1
- package/dist/inject/ssr.js +46 -0
- package/dist/ssr.d.ts +21 -0
- package/dist/ssr.js +19 -0
- package/dist/types/index.d.ts +10 -5
- package/dist/types/index.js +5 -0
- package/dist/types/react-polymorphic-types.d.ts +4 -8
- package/dist/utils/environment.d.ts +6 -0
- package/dist/utils/environment.js +12 -0
- package/dist/utils/string.d.ts +16 -9
- package/dist/utils/string.js +253 -0
- package/dist/utils/theme-map.d.ts +0 -3
- package/dist/utils/theme-map.js +97 -0
- package/dist/utils/theme-validation.js +36 -0
- package/dist/utils/theme.d.ts +3 -3
- package/dist/utils/theme.js +279 -0
- package/dist/utils/type-guards.js +38 -0
- package/dist/utils/utilities.js +43 -0
- package/package.json +26 -27
- package/LICENSE.md +0 -21
- package/README.md +0 -180
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR cache management for CSS injection.
|
|
3
|
+
* Maintains a cache of CSS text for server-side rendering.
|
|
4
|
+
*/
|
|
5
|
+
import { MAX_CSS_CACHE_SIZE } from "../constants";
|
|
6
|
+
const cssTextCache = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Adds CSS to the SSR cache with FIFO eviction.
|
|
9
|
+
*
|
|
10
|
+
* @param css - CSS string to cache
|
|
11
|
+
*/
|
|
12
|
+
export function addToSSRCache(css) {
|
|
13
|
+
if (cssTextCache.has(css)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (cssTextCache.size >= MAX_CSS_CACHE_SIZE) {
|
|
17
|
+
const firstKey = cssTextCache.keys().next().value;
|
|
18
|
+
if (firstKey !== undefined) {
|
|
19
|
+
cssTextCache.delete(firstKey);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
cssTextCache.set(css, true);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Gets all cached CSS text for SSR.
|
|
26
|
+
*
|
|
27
|
+
* @returns Joined CSS text string
|
|
28
|
+
*/
|
|
29
|
+
export function getSSRCacheText() {
|
|
30
|
+
return Array.from(cssTextCache.keys()).join("\n");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Clears the SSR cache.
|
|
34
|
+
*/
|
|
35
|
+
export function clearSSRCache() {
|
|
36
|
+
cssTextCache.clear();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Checks if CSS is already in the SSR cache.
|
|
40
|
+
*
|
|
41
|
+
* @param css - CSS string to check
|
|
42
|
+
* @returns True if CSS is cached
|
|
43
|
+
*/
|
|
44
|
+
export function isInSSRCache(css) {
|
|
45
|
+
return cssTextCache.has(css);
|
|
46
|
+
}
|
package/dist/ssr.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-safe entry point for Stoop.
|
|
3
|
+
* Exports only functions that work in Server Components.
|
|
4
|
+
* NO React dependencies, NO "use client" directive.
|
|
5
|
+
*
|
|
6
|
+
* Use this entry point when importing Stoop in Next.js Server Components.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // In a Server Component (layout.tsx)
|
|
11
|
+
* import { createStoop } from 'stoop/ssr';
|
|
12
|
+
*
|
|
13
|
+
* const { getCssText } = createStoop(config);
|
|
14
|
+
* const cssText = getCssText();
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { createStoop } from "./create-stoop-server";
|
|
18
|
+
export type { StoopServerInstance } from "./create-stoop-server";
|
|
19
|
+
export { getCssText, clearStylesheet } from "./inject";
|
|
20
|
+
export { generateCSSVariables } from "./utils/theme";
|
|
21
|
+
export type { Theme, CSS, StoopConfig, UtilityFunction, Variants } from "./types";
|
package/dist/ssr.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-safe entry point for Stoop.
|
|
3
|
+
* Exports only functions that work in Server Components.
|
|
4
|
+
* NO React dependencies, NO "use client" directive.
|
|
5
|
+
*
|
|
6
|
+
* Use this entry point when importing Stoop in Next.js Server Components.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // In a Server Component (layout.tsx)
|
|
11
|
+
* import { createStoop } from 'stoop/ssr';
|
|
12
|
+
*
|
|
13
|
+
* const { getCssText } = createStoop(config);
|
|
14
|
+
* const cssText = getCssText();
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { createStoop } from "./create-stoop-server";
|
|
18
|
+
export { getCssText, clearStylesheet } from "./inject";
|
|
19
|
+
export { generateCSSVariables } from "./utils/theme";
|
package/dist/types/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type UtilityFunction = (value: CSSPropertyValue | CSS | undefined) => CSS
|
|
|
38
38
|
*/
|
|
39
39
|
export type ThemeScale = keyof DefaultTheme;
|
|
40
40
|
/**
|
|
41
|
-
* Theme interface - strictly enforces only these
|
|
41
|
+
* Theme interface - strictly enforces only these 13 approved scales.
|
|
42
42
|
* Custom theme scales are NOT allowed.
|
|
43
43
|
* Media queries are also supported as part of the theme.
|
|
44
44
|
*/
|
|
@@ -51,6 +51,7 @@ export interface DefaultTheme {
|
|
|
51
51
|
fonts?: Record<string, string>;
|
|
52
52
|
fontWeights?: Record<string, string | number>;
|
|
53
53
|
fontSizes?: Record<string, string>;
|
|
54
|
+
lineHeights?: Record<string, string | number>;
|
|
54
55
|
letterSpacings?: Record<string, string>;
|
|
55
56
|
shadows?: Record<string, string>;
|
|
56
57
|
zIndices?: Record<string, string | number>;
|
|
@@ -97,6 +98,10 @@ export interface ProviderProps {
|
|
|
97
98
|
storageKey?: string;
|
|
98
99
|
attribute?: string;
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Stoop instance.
|
|
103
|
+
* Includes all APIs: styled, Provider, useTheme, css, globalCss, keyframes, etc.
|
|
104
|
+
*/
|
|
100
105
|
export interface StoopInstance {
|
|
101
106
|
styled: ReturnType<typeof createStyledFunction>;
|
|
102
107
|
css: ReturnType<typeof createCSSFunction>;
|
|
@@ -129,13 +134,13 @@ export interface StoopInstance {
|
|
|
129
134
|
preloadTheme: (theme: string | Theme) => void;
|
|
130
135
|
/**
|
|
131
136
|
* Provider component for managing theme state and updates.
|
|
132
|
-
*
|
|
137
|
+
* Available when themes are provided in createStoop config.
|
|
133
138
|
*/
|
|
134
|
-
Provider
|
|
139
|
+
Provider: ComponentType<ProviderProps>;
|
|
135
140
|
/**
|
|
136
141
|
* Hook to access theme management context.
|
|
137
|
-
*
|
|
142
|
+
* Available when themes are provided in createStoop config.
|
|
138
143
|
*/
|
|
139
|
-
useTheme
|
|
144
|
+
useTheme: () => ThemeManagementContextValue;
|
|
140
145
|
}
|
|
141
146
|
export {};
|
|
@@ -5,13 +5,9 @@
|
|
|
5
5
|
declare module "react-polymorphic-types" {
|
|
6
6
|
import type { ElementType, ComponentPropsWithRef, ComponentPropsWithoutRef } from "react";
|
|
7
7
|
|
|
8
|
-
export type PolymorphicPropsWithRef<
|
|
9
|
-
|
|
10
|
-
DefaultElement extends ElementType,
|
|
11
|
-
> = OwnProps & ComponentPropsWithRef<DefaultElement>;
|
|
8
|
+
export type PolymorphicPropsWithRef<OwnProps, DefaultElement extends ElementType> = OwnProps &
|
|
9
|
+
ComponentPropsWithRef<DefaultElement>;
|
|
12
10
|
|
|
13
|
-
export type PolymorphicPropsWithoutRef<
|
|
14
|
-
|
|
15
|
-
DefaultElement extends ElementType,
|
|
16
|
-
> = OwnProps & ComponentPropsWithoutRef<DefaultElement>;
|
|
11
|
+
export type PolymorphicPropsWithoutRef<OwnProps, DefaultElement extends ElementType> = OwnProps &
|
|
12
|
+
ComponentPropsWithoutRef<DefaultElement>;
|
|
17
13
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if code is running in a browser environment.
|
|
3
|
+
*
|
|
4
|
+
* @returns True if running in browser, false if in SSR/Node environment
|
|
5
|
+
*/
|
|
6
|
+
export function isBrowser() {
|
|
7
|
+
return (typeof window !== "undefined" &&
|
|
8
|
+
typeof document !== "undefined" &&
|
|
9
|
+
typeof window.document === "object" &&
|
|
10
|
+
// Ensure we're not in React Server Component or SSR context
|
|
11
|
+
typeof window.requestAnimationFrame === "function");
|
|
12
|
+
}
|
package/dist/utils/string.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* and CSS sanitization utilities for security.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
* Generates a hash string from an input string.
|
|
7
|
+
* Generates a hash string from an input string using FNV-1a algorithm.
|
|
8
|
+
* Includes string length to reduce collision probability.
|
|
8
9
|
*
|
|
9
10
|
* @param str - String to hash
|
|
10
11
|
* @returns Hashed string
|
|
@@ -35,6 +36,7 @@ export declare function escapeCSSValue(value: string | number): string;
|
|
|
35
36
|
/**
|
|
36
37
|
* Validates and sanitizes CSS selectors to prevent injection attacks.
|
|
37
38
|
* Only allows safe selector characters. Returns empty string for invalid selectors.
|
|
39
|
+
* Uses memoization for performance.
|
|
38
40
|
*
|
|
39
41
|
* @param selector - Selector to sanitize
|
|
40
42
|
* @returns Sanitized selector string or empty string if invalid
|
|
@@ -43,6 +45,7 @@ export declare function sanitizeCSSSelector(selector: string): string;
|
|
|
43
45
|
/**
|
|
44
46
|
* Validates and sanitizes CSS variable names to prevent injection attacks.
|
|
45
47
|
* CSS custom properties must start with -- and contain only valid characters.
|
|
48
|
+
* Uses memoization for performance.
|
|
46
49
|
*
|
|
47
50
|
* @param name - Variable name to sanitize
|
|
48
51
|
* @returns Sanitized variable name
|
|
@@ -58,18 +61,12 @@ export declare function escapeCSSVariableValue(value: string | number): string;
|
|
|
58
61
|
/**
|
|
59
62
|
* Sanitizes prefix for use in CSS selectors and class names.
|
|
60
63
|
* Only allows alphanumeric characters, hyphens, and underscores.
|
|
64
|
+
* Defaults to "stoop" if prefix is empty or becomes empty after sanitization.
|
|
61
65
|
*
|
|
62
66
|
* @param prefix - Prefix to sanitize
|
|
63
|
-
* @returns Sanitized prefix string
|
|
67
|
+
* @returns Sanitized prefix string (never empty, defaults to "stoop")
|
|
64
68
|
*/
|
|
65
69
|
export declare function sanitizePrefix(prefix: string): string;
|
|
66
|
-
/**
|
|
67
|
-
* Escapes prefix for use in CSS attribute selectors.
|
|
68
|
-
*
|
|
69
|
-
* @param prefix - Prefix to escape
|
|
70
|
-
* @returns Escaped prefix string
|
|
71
|
-
*/
|
|
72
|
-
export declare function escapePrefixForSelector(prefix: string): string;
|
|
73
70
|
/**
|
|
74
71
|
* Sanitizes media query strings to prevent injection attacks.
|
|
75
72
|
* Only allows safe characters for media queries.
|
|
@@ -81,6 +78,7 @@ export declare function sanitizeMediaQuery(mediaQuery: string): string;
|
|
|
81
78
|
/**
|
|
82
79
|
* Sanitizes CSS class names to prevent injection attacks.
|
|
83
80
|
* Only allows valid CSS class name characters.
|
|
81
|
+
* Uses memoization for performance.
|
|
84
82
|
*
|
|
85
83
|
* @param className - Class name(s) to sanitize
|
|
86
84
|
* @returns Sanitized class name string
|
|
@@ -88,6 +86,7 @@ export declare function sanitizeMediaQuery(mediaQuery: string): string;
|
|
|
88
86
|
export declare function sanitizeClassName(className: string): string;
|
|
89
87
|
/**
|
|
90
88
|
* Sanitizes CSS property names to prevent injection attacks.
|
|
89
|
+
* Uses memoization for performance.
|
|
91
90
|
*
|
|
92
91
|
* @param propertyName - Property name to sanitize
|
|
93
92
|
* @returns Sanitized property name
|
|
@@ -100,3 +99,11 @@ export declare function sanitizeCSSPropertyName(propertyName: string): string;
|
|
|
100
99
|
* @returns True if key is valid
|
|
101
100
|
*/
|
|
102
101
|
export declare function validateKeyframeKey(key: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Gets a pre-compiled regex for matching :root CSS selector blocks.
|
|
104
|
+
* Uses caching for performance.
|
|
105
|
+
*
|
|
106
|
+
* @param prefix - Optional prefix (unused, kept for API compatibility)
|
|
107
|
+
* @returns RegExp for matching :root selector blocks
|
|
108
|
+
*/
|
|
109
|
+
export declare function getRootRegex(prefix?: string): RegExp;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* String utility functions.
|
|
3
|
+
* Provides hashing for class name generation, camelCase to kebab-case conversion,
|
|
4
|
+
* and CSS sanitization utilities for security.
|
|
5
|
+
*/
|
|
6
|
+
import { LRUCache } from "../core/cache";
|
|
7
|
+
const sanitizeCacheSizeLimit = 1000;
|
|
8
|
+
let cachedRootRegex = null;
|
|
9
|
+
const selectorCache = new LRUCache(sanitizeCacheSizeLimit);
|
|
10
|
+
const propertyNameCache = new LRUCache(sanitizeCacheSizeLimit);
|
|
11
|
+
const sanitizeClassNameCache = new LRUCache(sanitizeCacheSizeLimit);
|
|
12
|
+
const variableNameCache = new LRUCache(sanitizeCacheSizeLimit);
|
|
13
|
+
/**
|
|
14
|
+
* Generates a hash string from an input string using FNV-1a algorithm.
|
|
15
|
+
* Includes string length to reduce collision probability.
|
|
16
|
+
*
|
|
17
|
+
* @param str - String to hash
|
|
18
|
+
* @returns Hashed string
|
|
19
|
+
*/
|
|
20
|
+
export function hash(str) {
|
|
21
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
22
|
+
const FNV_PRIME = 16777619;
|
|
23
|
+
let hash = FNV_OFFSET_BASIS;
|
|
24
|
+
for (let i = 0; i < str.length; i++) {
|
|
25
|
+
hash ^= str.charCodeAt(i);
|
|
26
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
27
|
+
}
|
|
28
|
+
hash ^= str.length;
|
|
29
|
+
return (hash >>> 0).toString(36);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generates a hash string from an object by stringifying it.
|
|
33
|
+
*
|
|
34
|
+
* @param obj - Object to hash
|
|
35
|
+
* @returns Hashed string
|
|
36
|
+
*/
|
|
37
|
+
export function hashObject(obj) {
|
|
38
|
+
try {
|
|
39
|
+
return hash(JSON.stringify(obj));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return hash(String(obj));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Converts a camelCase string to kebab-case.
|
|
47
|
+
*
|
|
48
|
+
* @param str - String to convert
|
|
49
|
+
* @returns Kebab-case string
|
|
50
|
+
*/
|
|
51
|
+
export function toKebabCase(str) {
|
|
52
|
+
return str.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Internal function to escape CSS values with optional brace escaping.
|
|
56
|
+
*
|
|
57
|
+
* @param value - Value to escape
|
|
58
|
+
* @param escapeBraces - Whether to escape curly braces
|
|
59
|
+
* @returns Escaped value string
|
|
60
|
+
*/
|
|
61
|
+
function escapeCSSValueInternal(value, escapeBraces = false) {
|
|
62
|
+
const str = String(value);
|
|
63
|
+
let result = str
|
|
64
|
+
.replace(/\\/g, "\\\\")
|
|
65
|
+
.replace(/"/g, '\\"')
|
|
66
|
+
.replace(/'/g, "\\'")
|
|
67
|
+
.replace(/;/g, "\\;")
|
|
68
|
+
.replace(/\n/g, "\\A ")
|
|
69
|
+
.replace(/\r/g, "")
|
|
70
|
+
.replace(/\f/g, "\\C ");
|
|
71
|
+
if (escapeBraces) {
|
|
72
|
+
result = result.replace(/\{/g, "\\7B ").replace(/\}/g, "\\7D ");
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Escapes CSS property values to prevent injection attacks.
|
|
78
|
+
* Escapes quotes, semicolons, and other special characters.
|
|
79
|
+
*
|
|
80
|
+
* @param value - Value to escape
|
|
81
|
+
* @returns Escaped value string
|
|
82
|
+
*/
|
|
83
|
+
export function escapeCSSValue(value) {
|
|
84
|
+
return escapeCSSValueInternal(value, false);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Validates and sanitizes CSS selectors to prevent injection attacks.
|
|
88
|
+
* Only allows safe selector characters. Returns empty string for invalid selectors.
|
|
89
|
+
* Uses memoization for performance.
|
|
90
|
+
*
|
|
91
|
+
* @param selector - Selector to sanitize
|
|
92
|
+
* @returns Sanitized selector string or empty string if invalid
|
|
93
|
+
*/
|
|
94
|
+
export function sanitizeCSSSelector(selector) {
|
|
95
|
+
const cached = selectorCache.get(selector);
|
|
96
|
+
if (cached !== undefined) {
|
|
97
|
+
return cached;
|
|
98
|
+
}
|
|
99
|
+
const sanitized = selector.replace(/[^a-zA-Z0-9\s\-_>+~:.#[\]&@()]/g, "");
|
|
100
|
+
const result = !sanitized.trim() || /^[>+~:.#[\]&@()\s]+$/.test(sanitized) ? "" : sanitized;
|
|
101
|
+
selectorCache.set(selector, result);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Validates and sanitizes CSS variable names to prevent injection attacks.
|
|
106
|
+
* CSS custom properties must start with -- and contain only valid characters.
|
|
107
|
+
* Uses memoization for performance.
|
|
108
|
+
*
|
|
109
|
+
* @param name - Variable name to sanitize
|
|
110
|
+
* @returns Sanitized variable name
|
|
111
|
+
*/
|
|
112
|
+
export function sanitizeCSSVariableName(name) {
|
|
113
|
+
const cached = variableNameCache.get(name);
|
|
114
|
+
if (cached !== undefined) {
|
|
115
|
+
return cached;
|
|
116
|
+
}
|
|
117
|
+
const sanitized = name.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
118
|
+
const cleaned = sanitized.replace(/^[\d-]+/, "").replace(/^-+/, "");
|
|
119
|
+
const result = cleaned || "invalid";
|
|
120
|
+
variableNameCache.set(name, result);
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Escapes CSS variable values to prevent injection attacks.
|
|
125
|
+
*
|
|
126
|
+
* @param value - Value to escape
|
|
127
|
+
* @returns Escaped value string
|
|
128
|
+
*/
|
|
129
|
+
export function escapeCSSVariableValue(value) {
|
|
130
|
+
return escapeCSSValueInternal(value, true);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Sanitizes prefix for use in CSS selectors and class names.
|
|
134
|
+
* Only allows alphanumeric characters, hyphens, and underscores.
|
|
135
|
+
* Defaults to "stoop" if prefix is empty or becomes empty after sanitization.
|
|
136
|
+
*
|
|
137
|
+
* @param prefix - Prefix to sanitize
|
|
138
|
+
* @returns Sanitized prefix string (never empty, defaults to "stoop")
|
|
139
|
+
*/
|
|
140
|
+
export function sanitizePrefix(prefix) {
|
|
141
|
+
if (!prefix) {
|
|
142
|
+
return "stoop";
|
|
143
|
+
}
|
|
144
|
+
const sanitized = prefix.replace(/[^a-zA-Z0-9-_]/g, "");
|
|
145
|
+
const cleaned = sanitized.replace(/^[\d-]+/, "").replace(/^-+/, "");
|
|
146
|
+
// Return "stoop" as default if sanitization results in empty string
|
|
147
|
+
return cleaned || "stoop";
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Sanitizes media query strings to prevent injection attacks.
|
|
151
|
+
* Only allows safe characters for media queries.
|
|
152
|
+
*
|
|
153
|
+
* @param mediaQuery - Media query string to sanitize
|
|
154
|
+
* @returns Sanitized media query string or empty string if invalid
|
|
155
|
+
*/
|
|
156
|
+
export function sanitizeMediaQuery(mediaQuery) {
|
|
157
|
+
if (!mediaQuery || typeof mediaQuery !== "string") {
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
const sanitized = mediaQuery.replace(/[^a-zA-Z0-9\s():,<>=\-@]/g, "");
|
|
161
|
+
if (!sanitized.trim() || !/[a-zA-Z]/.test(sanitized)) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
return sanitized;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Sanitizes CSS class names to prevent injection attacks.
|
|
168
|
+
* Only allows valid CSS class name characters.
|
|
169
|
+
* Uses memoization for performance.
|
|
170
|
+
*
|
|
171
|
+
* @param className - Class name(s) to sanitize
|
|
172
|
+
* @returns Sanitized class name string
|
|
173
|
+
*/
|
|
174
|
+
export function sanitizeClassName(className) {
|
|
175
|
+
if (!className || typeof className !== "string") {
|
|
176
|
+
return "";
|
|
177
|
+
}
|
|
178
|
+
const cached = sanitizeClassNameCache.get(className);
|
|
179
|
+
if (cached !== undefined) {
|
|
180
|
+
return cached;
|
|
181
|
+
}
|
|
182
|
+
const classes = className.trim().split(/\s+/);
|
|
183
|
+
const sanitizedClasses = [];
|
|
184
|
+
for (const cls of classes) {
|
|
185
|
+
if (!cls) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const sanitized = cls.replace(/[^a-zA-Z0-9-_]/g, "");
|
|
189
|
+
const cleaned = sanitized.replace(/^\d+/, "");
|
|
190
|
+
if (cleaned && /^[a-zA-Z-_]/.test(cleaned)) {
|
|
191
|
+
sanitizedClasses.push(cleaned);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const result = sanitizedClasses.join(" ");
|
|
195
|
+
sanitizeClassNameCache.set(className, result);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Sanitizes CSS property names to prevent injection attacks.
|
|
200
|
+
* Uses memoization for performance.
|
|
201
|
+
*
|
|
202
|
+
* @param propertyName - Property name to sanitize
|
|
203
|
+
* @returns Sanitized property name
|
|
204
|
+
*/
|
|
205
|
+
export function sanitizeCSSPropertyName(propertyName) {
|
|
206
|
+
if (!propertyName || typeof propertyName !== "string") {
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
const cached = propertyNameCache.get(propertyName);
|
|
210
|
+
if (cached !== undefined) {
|
|
211
|
+
return cached;
|
|
212
|
+
}
|
|
213
|
+
const kebab = toKebabCase(propertyName);
|
|
214
|
+
const sanitized = kebab.replace(/[^a-zA-Z0-9-]/g, "").replace(/^-+|-+$/g, "");
|
|
215
|
+
const result = sanitized.replace(/^\d+/, "") || "";
|
|
216
|
+
propertyNameCache.set(propertyName, result);
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Validates keyframe percentage keys (e.g., "0%", "50%", "from", "to").
|
|
221
|
+
*
|
|
222
|
+
* @param key - Keyframe key to validate
|
|
223
|
+
* @returns True if key is valid
|
|
224
|
+
*/
|
|
225
|
+
export function validateKeyframeKey(key) {
|
|
226
|
+
if (!key || typeof key !== "string") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (key === "from" || key === "to") {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
const percentageMatch = /^\d+(\.\d+)?%$/.test(key);
|
|
233
|
+
if (percentageMatch) {
|
|
234
|
+
const num = parseFloat(key);
|
|
235
|
+
return num >= 0 && num <= 100;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Gets a pre-compiled regex for matching :root CSS selector blocks.
|
|
241
|
+
* Uses caching for performance.
|
|
242
|
+
*
|
|
243
|
+
* @param prefix - Optional prefix (unused, kept for API compatibility)
|
|
244
|
+
* @returns RegExp for matching :root selector blocks
|
|
245
|
+
*/
|
|
246
|
+
export function getRootRegex(prefix = "") {
|
|
247
|
+
if (!cachedRootRegex) {
|
|
248
|
+
const rootSelector = ":root";
|
|
249
|
+
const escapedSelector = rootSelector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
250
|
+
cachedRootRegex = new RegExp(`${escapedSelector}\\s*\\{[\\s\\S]*?\\}`, "g");
|
|
251
|
+
}
|
|
252
|
+
return cachedRootRegex;
|
|
253
|
+
}
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
* Maps CSS properties to theme scales for deterministic token resolution.
|
|
4
4
|
*/
|
|
5
5
|
import type { ThemeScale } from "../types";
|
|
6
|
-
import { APPROVED_THEME_SCALES, DEFAULT_THEME_MAP } from "../constants";
|
|
7
|
-
export { APPROVED_THEME_SCALES, DEFAULT_THEME_MAP };
|
|
8
|
-
export type { ThemeScale };
|
|
9
6
|
/**
|
|
10
7
|
* Auto-detects theme scale from CSS property name using pattern matching.
|
|
11
8
|
* Used as fallback when property is not in DEFAULT_THEME_MAP.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeMap utilities for property-aware token resolution.
|
|
3
|
+
* Maps CSS properties to theme scales for deterministic token resolution.
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_THEME_MAP } from "../constants";
|
|
6
|
+
/**
|
|
7
|
+
* Auto-detects theme scale from CSS property name using pattern matching.
|
|
8
|
+
* Used as fallback when property is not in DEFAULT_THEME_MAP.
|
|
9
|
+
*
|
|
10
|
+
* @param property - CSS property name
|
|
11
|
+
* @returns Theme scale name or undefined if no pattern matches
|
|
12
|
+
*/
|
|
13
|
+
export function autoDetectScale(property) {
|
|
14
|
+
// Color properties
|
|
15
|
+
if (property.includes("Color") ||
|
|
16
|
+
property === "fill" ||
|
|
17
|
+
property === "stroke" ||
|
|
18
|
+
property === "accentColor" ||
|
|
19
|
+
property === "caretColor" ||
|
|
20
|
+
property === "border" ||
|
|
21
|
+
property === "outline" ||
|
|
22
|
+
(property.includes("background") && !property.includes("Size") && !property.includes("Image"))) {
|
|
23
|
+
return "colors";
|
|
24
|
+
}
|
|
25
|
+
// Spacing properties
|
|
26
|
+
if (/^(margin|padding|gap|inset|top|right|bottom|left|rowGap|columnGap|gridGap|gridRowGap|gridColumnGap)/.test(property) ||
|
|
27
|
+
property.includes("Block") ||
|
|
28
|
+
property.includes("Inline")) {
|
|
29
|
+
return "space";
|
|
30
|
+
}
|
|
31
|
+
// Size properties
|
|
32
|
+
if (/(width|height|size|basis)$/i.test(property) ||
|
|
33
|
+
property.includes("BlockSize") ||
|
|
34
|
+
property.includes("InlineSize")) {
|
|
35
|
+
return "sizes";
|
|
36
|
+
}
|
|
37
|
+
// Typography: Font Size
|
|
38
|
+
if (property === "fontSize" || (property === "font" && !property.includes("Family"))) {
|
|
39
|
+
return "fontSizes";
|
|
40
|
+
}
|
|
41
|
+
// Typography: Font Family
|
|
42
|
+
if (property === "fontFamily" || property.includes("FontFamily")) {
|
|
43
|
+
return "fonts";
|
|
44
|
+
}
|
|
45
|
+
// Typography: Font Weight
|
|
46
|
+
if (property === "fontWeight" || property.includes("FontWeight")) {
|
|
47
|
+
return "fontWeights";
|
|
48
|
+
}
|
|
49
|
+
// Typography: Letter Spacing
|
|
50
|
+
if (property === "letterSpacing" || property.includes("LetterSpacing")) {
|
|
51
|
+
return "letterSpacings";
|
|
52
|
+
}
|
|
53
|
+
// Border Radius
|
|
54
|
+
if (property.includes("Radius") || property.includes("radius")) {
|
|
55
|
+
return "radii";
|
|
56
|
+
}
|
|
57
|
+
// Shadows
|
|
58
|
+
if (property.includes("Shadow") ||
|
|
59
|
+
property.includes("shadow") ||
|
|
60
|
+
property === "filter" ||
|
|
61
|
+
property === "backdropFilter") {
|
|
62
|
+
return "shadows";
|
|
63
|
+
}
|
|
64
|
+
// Z-Index
|
|
65
|
+
if (property === "zIndex" || property.includes("ZIndex") || property.includes("z-index")) {
|
|
66
|
+
return "zIndices";
|
|
67
|
+
}
|
|
68
|
+
// Opacity
|
|
69
|
+
if (property === "opacity" || property.includes("Opacity")) {
|
|
70
|
+
return "opacities";
|
|
71
|
+
}
|
|
72
|
+
// Transitions and animations
|
|
73
|
+
if (property.startsWith("transition") ||
|
|
74
|
+
property.startsWith("animation") ||
|
|
75
|
+
property.includes("Transition") ||
|
|
76
|
+
property.includes("Animation")) {
|
|
77
|
+
return "transitions";
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Gets the theme scale for a CSS property.
|
|
83
|
+
* Checks user themeMap first, then default themeMap, then pattern matching.
|
|
84
|
+
*
|
|
85
|
+
* @param property - CSS property name
|
|
86
|
+
* @param userThemeMap - Optional user-provided themeMap override
|
|
87
|
+
* @returns Theme scale name or undefined if no mapping found
|
|
88
|
+
*/
|
|
89
|
+
export function getScaleForProperty(property, userThemeMap) {
|
|
90
|
+
if (userThemeMap && property in userThemeMap) {
|
|
91
|
+
return userThemeMap[property];
|
|
92
|
+
}
|
|
93
|
+
if (property in DEFAULT_THEME_MAP) {
|
|
94
|
+
return DEFAULT_THEME_MAP[property];
|
|
95
|
+
}
|
|
96
|
+
return autoDetectScale(property);
|
|
97
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme validation utilities.
|
|
3
|
+
* Ensures theme objects only contain approved scales.
|
|
4
|
+
*/
|
|
5
|
+
import { APPROVED_THEME_SCALES } from "../constants";
|
|
6
|
+
/**
|
|
7
|
+
* Validates that a theme object only contains approved scales.
|
|
8
|
+
*
|
|
9
|
+
* @param theme - Theme object to validate
|
|
10
|
+
* @returns Validated theme as DefaultTheme
|
|
11
|
+
* @throws Error if theme contains invalid scales (in development)
|
|
12
|
+
*/
|
|
13
|
+
export function validateTheme(theme) {
|
|
14
|
+
if (!theme || typeof theme !== "object" || Array.isArray(theme)) {
|
|
15
|
+
throw new Error("[Stoop] Theme must be a non-null object");
|
|
16
|
+
}
|
|
17
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
|
|
18
|
+
return theme;
|
|
19
|
+
}
|
|
20
|
+
const themeObj = theme;
|
|
21
|
+
const invalidScales = [];
|
|
22
|
+
for (const key in themeObj) {
|
|
23
|
+
if (key === "media") {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!APPROVED_THEME_SCALES.includes(key)) {
|
|
27
|
+
invalidScales.push(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (invalidScales.length > 0) {
|
|
31
|
+
const errorMessage = `[Stoop] Theme contains invalid scales: ${invalidScales.join(", ")}. ` +
|
|
32
|
+
`Only these scales are allowed: ${APPROVED_THEME_SCALES.join(", ")}`;
|
|
33
|
+
throw new Error(errorMessage);
|
|
34
|
+
}
|
|
35
|
+
return theme;
|
|
36
|
+
}
|
package/dist/utils/theme.d.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { CSS, Theme, ThemeScale } from "../types";
|
|
7
7
|
/**
|
|
8
|
-
* Compares two themes for
|
|
9
|
-
* Excludes 'media' property from comparison
|
|
8
|
+
* Compares two themes for structural and value equality.
|
|
9
|
+
* Excludes 'media' property from comparison.
|
|
10
10
|
*
|
|
11
11
|
* @param theme1 - First theme to compare
|
|
12
12
|
* @param theme2 - Second theme to compare
|
|
13
|
-
* @returns True if themes are equal
|
|
13
|
+
* @returns True if themes are equal
|
|
14
14
|
*/
|
|
15
15
|
export declare function themesAreEqual(theme1: Theme | null, theme2: Theme | null): boolean;
|
|
16
16
|
/**
|