@uistate/core 2.0.1 → 3.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.
@@ -0,0 +1,267 @@
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
+ * Also handles serialization of values for data attributes and CSS variables
11
+ * with consistent rules and unified serialization behavior
12
+ */
13
+
14
+ // Utility functions for CSS value escaping/unescaping
15
+ function escapeCssValue(value) {
16
+ if (typeof value !== 'string') return value;
17
+ return value.replace(/[^\x20-\x7E]|[!;{}:()[\]/@,'"]/g, function(char) {
18
+ const hex = char.charCodeAt(0).toString(16);
19
+ return '\\' + hex + ' ';
20
+ });
21
+ }
22
+
23
+ function unescapeCssValue(value) {
24
+ if (typeof value !== 'string') return value;
25
+ // Only perform unescaping if there are escape sequences
26
+ if (!value.includes('\\')) return value;
27
+
28
+ return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
29
+ return String.fromCharCode(parseInt(hex, 16));
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Create a configured serializer instance
35
+ * @param {Object} config - Configuration options
36
+ * @returns {Object} - Serializer instance
37
+ */
38
+ function createSerializer(config = {}) {
39
+ // Default configuration
40
+ const defaultConfig = {
41
+ mode: 'hybrid', // 'escape', 'json', or 'hybrid'
42
+ debug: false, // Enable debug logging
43
+ complexThreshold: 3, // Object properties threshold for hybrid mode
44
+ preserveTypes: true // Preserve type information in serialization
45
+ };
46
+
47
+ // Merge provided config with defaults
48
+ const options = { ...defaultConfig, ...config };
49
+
50
+ // Serializer instance
51
+ const serializer = {
52
+ // Current configuration
53
+ config: options,
54
+
55
+ /**
56
+ * Update configuration
57
+ * @param {Object} newConfig - New configuration options
58
+ */
59
+ configure(newConfig) {
60
+ Object.assign(this.config, newConfig);
61
+ if (this.config.debug) {
62
+ console.log('StateSerializer config updated:', this.config);
63
+ }
64
+ },
65
+
66
+ /**
67
+ * Serialize a value for storage in CSS variables
68
+ * @param {string} key - The state key (for context-aware serialization)
69
+ * @param {any} value - The value to serialize
70
+ * @returns {string} - Serialized value
71
+ */
72
+ serialize(key, value) {
73
+ // Handle null/undefined
74
+ if (value === null || value === undefined) {
75
+ return '';
76
+ }
77
+
78
+ const valueType = typeof value;
79
+ const isComplex = valueType === 'object' &&
80
+ (Array.isArray(value) ||
81
+ (Object.keys(value).length >= this.config.complexThreshold));
82
+
83
+ // Select serialization strategy based on configuration and value type
84
+ if (this.config.mode === 'escape' ||
85
+ (this.config.mode === 'hybrid' && !isComplex)) {
86
+ // Use escape strategy for primitives or when escape mode is forced
87
+ if (valueType === 'string') {
88
+ return escapeCssValue(value);
89
+ } else if (valueType === 'object') {
90
+ // For simple objects in escape mode, still use JSON but with escaping
91
+ const jsonStr = JSON.stringify(value);
92
+ return escapeCssValue(jsonStr);
93
+ } else {
94
+ // For other primitives, convert to string
95
+ return String(value);
96
+ }
97
+ } else {
98
+ // Use JSON strategy for complex objects or when JSON mode is forced
99
+ return JSON.stringify(value);
100
+ }
101
+ },
102
+
103
+ /**
104
+ * Deserialize a value from CSS variable storage
105
+ * @param {string} key - The state key (for context-aware deserialization)
106
+ * @param {string} value - The serialized value
107
+ * @returns {any} - Deserialized value
108
+ */
109
+ deserialize(key, value) {
110
+ // Handle empty values
111
+ if (!value) return '';
112
+
113
+ // Try JSON parse first for values that look like JSON
114
+ if (this.config.mode !== 'escape' &&
115
+ ((value.startsWith('{') && value.endsWith('}')) ||
116
+ (value.startsWith('[') && value.endsWith(']')))) {
117
+ try {
118
+ return JSON.parse(value);
119
+ } catch (e) {
120
+ if (this.config.debug) {
121
+ console.warn(`Failed to parse JSON for key "${key}":`, value);
122
+ }
123
+ // Fall through to unescaping if JSON parse fails
124
+ }
125
+ }
126
+
127
+ // For non-JSON or escape mode, try unescaping
128
+ const unescaped = unescapeCssValue(value);
129
+
130
+ // If unescaped looks like JSON (might have been double-escaped), try parsing it
131
+ if (this.config.mode !== 'escape' &&
132
+ ((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
133
+ (unescaped.startsWith('[') && unescaped.endsWith(']')))) {
134
+ try {
135
+ return JSON.parse(unescaped);
136
+ } catch (e) {
137
+ // Not valid JSON, return unescaped string
138
+ }
139
+ }
140
+
141
+ return unescaped;
142
+ },
143
+
144
+ /**
145
+ * Serialize a value for data-* attribute
146
+ * @param {string} key - The state key
147
+ * @param {any} value - The value to serialize for attribute
148
+ * @returns {string} - Serialized attribute value
149
+ */
150
+ serializeForAttribute(key, value) {
151
+ if (value === null || value === undefined) return null;
152
+
153
+ // For objects, use the standard serializer
154
+ if (typeof value === 'object') {
155
+ return this.serialize(key, value);
156
+ }
157
+
158
+ // For primitive values, use direct string conversion
159
+ return String(value);
160
+ },
161
+
162
+ /**
163
+ * Apply serialized state to HTML element attributes and properties
164
+ * @param {string} key - State key
165
+ * @param {any} value - Value to apply
166
+ * @param {HTMLElement} element - Target element (defaults to documentElement)
167
+ */
168
+ applyToAttributes(key, value, element = document.documentElement) {
169
+ // Skip null/undefined values
170
+ if (value === null || value === undefined) {
171
+ element.removeAttribute(`data-${key}`);
172
+ return;
173
+ }
174
+
175
+ // Handle objects specially
176
+ if (typeof value === 'object') {
177
+ // Set the main attribute with serialized value
178
+ element.setAttribute(`data-${key}`, this.serialize(key, value));
179
+
180
+ // For non-array objects, set individual property attributes
181
+ if (!Array.isArray(value)) {
182
+ Object.entries(value).forEach(([propKey, propValue]) => {
183
+ const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
184
+ if (propValue !== null && propValue !== undefined) {
185
+ if (typeof propValue === 'object') {
186
+ element.setAttribute(
187
+ attributeKey,
188
+ this.serialize(`${key}.${propKey}`, propValue)
189
+ );
190
+ } else {
191
+ element.setAttribute(attributeKey, propValue);
192
+ }
193
+ } else {
194
+ element.removeAttribute(attributeKey);
195
+ }
196
+ });
197
+ }
198
+ } else {
199
+ // For primitives, set directly
200
+ element.setAttribute(`data-${key}`, value);
201
+ }
202
+ },
203
+
204
+ /**
205
+ * Utility method to determine if a value needs complex serialization
206
+ * @param {any} value - Value to check
207
+ * @returns {boolean} - True if complex serialization is needed
208
+ */
209
+ needsComplexSerialization(value) {
210
+ return typeof value === 'object' && value !== null;
211
+ },
212
+
213
+ /**
214
+ * Set state with proper serialization for CSS variables
215
+ * @param {Object} uistate - UIstate instance
216
+ * @param {string} path - State path
217
+ * @param {any} value - Value to set
218
+ * @returns {any} - The set value
219
+ */
220
+ setStateWithCss(uistate, path, value) {
221
+ // Update UIstate
222
+ uistate.setState(path, value);
223
+
224
+ // Update CSS variable with properly serialized value
225
+ const cssPath = path.replace(/\./g, '-');
226
+ const serialized = this.serialize(path, value);
227
+ document.documentElement.style.setProperty(`--${cssPath}`, serialized);
228
+
229
+ // Update data attribute for root level state
230
+ const segments = path.split('.');
231
+ if (segments.length === 1) {
232
+ document.documentElement.dataset[path] = typeof value === 'object'
233
+ ? JSON.stringify(value)
234
+ : value;
235
+ }
236
+
237
+ return value;
238
+ },
239
+
240
+ /**
241
+ * Get state with fallback to CSS variables
242
+ * @param {Object} uistate - UIstate instance
243
+ * @param {string} path - State path
244
+ * @returns {any} - Retrieved value
245
+ */
246
+ getStateFromCss(uistate, path) {
247
+ // First try UIstate
248
+ const value = uistate.getState(path);
249
+ if (value !== undefined) return value;
250
+
251
+ // If not found, try CSS variable
252
+ const cssPath = path.replace(/\./g, '-');
253
+ const cssValue = getComputedStyle(document.documentElement)
254
+ .getPropertyValue(`--${cssPath}`).trim();
255
+
256
+ return cssValue ? this.deserialize(path, cssValue) : undefined;
257
+ }
258
+ };
259
+
260
+ return serializer;
261
+ }
262
+
263
+ // Create a default instance with hybrid mode
264
+ const StateSerializer = createSerializer();
265
+
266
+ export default StateSerializer;
267
+ export { createSerializer, escapeCssValue, unescapeCssValue };