@uistate/core 2.0.1 → 3.0.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.
@@ -0,0 +1,204 @@
1
+ /**
2
+ * StateSerializer - Configurable serialization module for UIstate
3
+ * Handles transformation between JavaScript values and CSS-compatible string values
4
+ *
5
+ * Supports multiple serialization strategies:
6
+ * - 'escape': Uses custom escaping for all values (original UIstate approach)
7
+ * - 'json': Uses JSON.stringify for complex objects, direct values for primitives
8
+ * - 'hybrid': Automatically selects the best strategy based on value type
9
+ */
10
+
11
+ // Utility functions for CSS value escaping/unescaping
12
+ function escapeCssValue(value) {
13
+ if (typeof value !== 'string') return value;
14
+ return value.replace(/[^\x20-\x7E]|[!;{}:()[\]/@,'"]/g, function(char) {
15
+ const hex = char.charCodeAt(0).toString(16);
16
+ return '\\' + hex + ' ';
17
+ });
18
+ }
19
+
20
+ function unescapeCssValue(value) {
21
+ if (typeof value !== 'string') return value;
22
+ // Only perform unescaping if there are escape sequences
23
+ if (!value.includes('\\')) return value;
24
+
25
+ return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
26
+ return String.fromCharCode(parseInt(hex, 16));
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Create a configured serializer instance
32
+ * @param {Object} config - Configuration options
33
+ * @returns {Object} - Serializer instance
34
+ */
35
+ function createSerializer(config = {}) {
36
+ // Default configuration
37
+ const defaultConfig = {
38
+ mode: 'hybrid', // 'escape', 'json', or 'hybrid'
39
+ debug: false, // Enable debug logging
40
+ complexThreshold: 3, // Object properties threshold for hybrid mode
41
+ preserveTypes: true // Preserve type information in serialization
42
+ };
43
+
44
+ // Merge provided config with defaults
45
+ const options = { ...defaultConfig, ...config };
46
+
47
+ // Serializer instance
48
+ const serializer = {
49
+ // Current configuration
50
+ config: options,
51
+
52
+ /**
53
+ * Update configuration
54
+ * @param {Object} newConfig - New configuration options
55
+ */
56
+ configure(newConfig) {
57
+ Object.assign(this.config, newConfig);
58
+ if (this.config.debug) {
59
+ console.log('StateSerializer config updated:', this.config);
60
+ }
61
+ },
62
+
63
+ /**
64
+ * Serialize a value for storage in CSS variables
65
+ * @param {string} key - The state key (for context-aware serialization)
66
+ * @param {any} value - The value to serialize
67
+ * @returns {string} - Serialized value
68
+ */
69
+ serialize(key, value) {
70
+ // Handle null/undefined
71
+ if (value === null || value === undefined) {
72
+ return '';
73
+ }
74
+
75
+ const valueType = typeof value;
76
+ const isComplex = valueType === 'object' &&
77
+ (Array.isArray(value) ||
78
+ (Object.keys(value).length >= this.config.complexThreshold));
79
+
80
+ // Select serialization strategy based on configuration and value type
81
+ if (this.config.mode === 'escape' ||
82
+ (this.config.mode === 'hybrid' && !isComplex)) {
83
+ // Use escape strategy for primitives or when escape mode is forced
84
+ if (valueType === 'string') {
85
+ return escapeCssValue(value);
86
+ } else if (valueType === 'object') {
87
+ // For simple objects in escape mode, still use JSON but with escaping
88
+ const jsonStr = JSON.stringify(value);
89
+ return escapeCssValue(jsonStr);
90
+ } else {
91
+ // For other primitives, convert to string
92
+ return String(value);
93
+ }
94
+ } else {
95
+ // Use JSON strategy for complex objects or when JSON mode is forced
96
+ return JSON.stringify(value);
97
+ }
98
+ },
99
+
100
+ /**
101
+ * Deserialize a value from CSS variable storage
102
+ * @param {string} key - The state key (for context-aware deserialization)
103
+ * @param {string} value - The serialized value
104
+ * @returns {any} - Deserialized value
105
+ */
106
+ deserialize(key, value) {
107
+ // Handle empty values
108
+ if (!value) return '';
109
+
110
+ // Try JSON parse first for values that look like JSON
111
+ if (this.config.mode !== 'escape' &&
112
+ ((value.startsWith('{') && value.endsWith('}')) ||
113
+ (value.startsWith('[') && value.endsWith(']')))) {
114
+ try {
115
+ return JSON.parse(value);
116
+ } catch (e) {
117
+ if (this.config.debug) {
118
+ console.warn(`Failed to parse JSON for key "${key}":`, value);
119
+ }
120
+ // Fall through to unescaping if JSON parse fails
121
+ }
122
+ }
123
+
124
+ // For non-JSON or escape mode, try unescaping
125
+ const unescaped = unescapeCssValue(value);
126
+
127
+ // If unescaped looks like JSON (might have been double-escaped), try parsing it
128
+ if (this.config.mode !== 'escape' &&
129
+ ((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
130
+ (unescaped.startsWith('[') && unescaped.endsWith(']')))) {
131
+ try {
132
+ return JSON.parse(unescaped);
133
+ } catch (e) {
134
+ // Not valid JSON, return unescaped string
135
+ }
136
+ }
137
+
138
+ return unescaped;
139
+ },
140
+
141
+ /**
142
+ * Utility method to determine if a value needs complex serialization
143
+ * @param {any} value - Value to check
144
+ * @returns {boolean} - True if complex serialization is needed
145
+ */
146
+ needsComplexSerialization(value) {
147
+ return typeof value === 'object' && value !== null;
148
+ },
149
+
150
+ /**
151
+ * Set state with proper serialization for CSS variables
152
+ * @param {Object} uistate - UIstate instance
153
+ * @param {string} path - State path
154
+ * @param {any} value - Value to set
155
+ * @returns {any} - The set value
156
+ */
157
+ setStateWithCss(uistate, path, value) {
158
+ // Update UIstate
159
+ uistate.setState(path, value);
160
+
161
+ // Update CSS variable with properly serialized value
162
+ const cssPath = path.replace(/\./g, '-');
163
+ const serialized = this.serialize(path, value);
164
+ document.documentElement.style.setProperty(`--${cssPath}`, serialized);
165
+
166
+ // Update data attribute for root level state
167
+ const segments = path.split('.');
168
+ if (segments.length === 1) {
169
+ document.documentElement.dataset[path] = typeof value === 'object'
170
+ ? JSON.stringify(value)
171
+ : value;
172
+ }
173
+
174
+ return value;
175
+ },
176
+
177
+ /**
178
+ * Get state with fallback to CSS variables
179
+ * @param {Object} uistate - UIstate instance
180
+ * @param {string} path - State path
181
+ * @returns {any} - Retrieved value
182
+ */
183
+ getStateFromCss(uistate, path) {
184
+ // First try UIstate
185
+ const value = uistate.getState(path);
186
+ if (value !== undefined) return value;
187
+
188
+ // If not found, try CSS variable
189
+ const cssPath = path.replace(/\./g, '-');
190
+ const cssValue = getComputedStyle(document.documentElement)
191
+ .getPropertyValue(`--${cssPath}`).trim();
192
+
193
+ return cssValue ? this.deserialize(path, cssValue) : undefined;
194
+ }
195
+ };
196
+
197
+ return serializer;
198
+ }
199
+
200
+ // Create a default instance with hybrid mode
201
+ const StateSerializer = createSerializer();
202
+
203
+ export default StateSerializer;
204
+ export { createSerializer, escapeCssValue, unescapeCssValue };