@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.
- package/README.md +39 -103
- package/package.json +1 -1
- package/src/cssState.js +53 -9
- package/src/index.js +282 -40
- package/src/stateInspector.js +140 -0
- package/src/stateSerializer.js +204 -0
- package/src/telemetryPlugin.js +638 -0
- package/src/templateManager.js +119 -0
|
@@ -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 };
|