@uistate/core
Version:
Revolutionary DOM-based state management using CSS custom properties - zero dependencies, potential O(1) updates
173 lines (141 loc) • 5.85 kB
JavaScript
function escapeCssValue(value) {
if (typeof value !== 'string') return value;
return value.replace(/[^\x20-\x7E]|[!;{}:()[\]/@,'"]/g, function(char) {
const hex = char.charCodeAt(0).toString(16);
return '\\' + hex + ' ';
});
}
function unescapeCssValue(value) {
if (typeof value !== 'string') return value;
if (!value.includes('\\')) return value;
return value.replace(/\\([0-9a-f]{1,6})\s?/gi, function(match, hex) {
return String.fromCharCode(parseInt(hex, 16));
});
}
function createSerializer(config = {}) {
const defaultConfig = {
mode: 'hybrid',
debug: false,
complexThreshold: 3,
preserveTypes: true,
};
const options = { ...defaultConfig, ...config };
const serializer = {
config: options,
configure(newConfig) {
Object.assign(this.config, newConfig);
if (this.config.debug) {
console.log('StateSerializer config updated:', this.config);
}
},
serialize(key, value) {
if (value === null || value === undefined) {
return '';
}
const valueType = typeof value;
const isComplex = valueType === 'object' &&
(Array.isArray(value) ||
(Object.keys(value).length >= this.config.complexThreshold));
if (this.config.mode === 'escape' ||
(this.config.mode === 'hybrid' && !isComplex)) {
if (valueType === 'string') {
return escapeCssValue(value);
} else if (valueType === 'object') {
const jsonStr = JSON.stringify(value);
return escapeCssValue(jsonStr);
} else {
return String(value);
}
} else {
return JSON.stringify(value);
}
},
deserialize(key, value) {
if (!value) return '';
if (this.config.mode !== 'escape' &&
((value.startsWith('{') && value.endsWith('}')) ||
(value.startsWith('[') && value.endsWith(']')))) {
try {
return JSON.parse(value);
} catch (e) {
if (this.config.debug) {
console.warn(`Failed to parse JSON for key "${key}":`, value);
}
}
}
const unescaped = unescapeCssValue(value);
if (this.config.mode !== 'escape' &&
((unescaped.startsWith('{') && unescaped.endsWith('}')) ||
(unescaped.startsWith('[') && unescaped.endsWith(']')))) {
try {
return JSON.parse(unescaped);
} catch (e) {
}
}
return unescaped;
},
serializeForAttribute(key, value) {
if (value === null || value === undefined) return null;
if (typeof value === 'object') {
return this.serialize(key, value);
}
return String(value);
},
applyToAttributes(key, value, element = document.documentElement) {
if (value === null || value === undefined) {
element.removeAttribute(`data-${key}`);
return;
}
if (typeof value === 'object') {
element.setAttribute(`data-${key}`, this.serialize(key, value));
if (!Array.isArray(value)) {
Object.entries(value).forEach(([propKey, propValue]) => {
const attributeKey = `data-${key}-${propKey.toLowerCase()}`;
if (propValue !== null && propValue !== undefined) {
if (typeof propValue === 'object') {
element.setAttribute(
attributeKey,
this.serialize(`${key}.${propKey}`, propValue)
);
} else {
element.setAttribute(attributeKey, propValue);
}
} else {
element.removeAttribute(attributeKey);
}
});
}
} else {
element.setAttribute(`data-${key}`, value);
}
},
needsComplexSerialization(value) {
return typeof value === 'object' && value !== null;
},
setStateWithCss(uistate, path, value) {
uistate.setState(path, value);
const cssPath = path.replace(/\./g, '-');
const serialized = this.serialize(path, value);
document.documentElement.style.setProperty(`--${cssPath}`, serialized);
const segments = path.split('.');
if (segments.length === 1) {
document.documentElement.dataset[path] = typeof value === 'object'
? JSON.stringify(value)
: value;
}
return value;
},
getStateFromCss(uistate, path) {
const value = uistate.getState(path);
if (value !== undefined) return value;
const cssPath = path.replace(/\./g, '-');
const cssValue = getComputedStyle(document.documentElement)
.getPropertyValue(`--${cssPath}`).trim();
return cssValue ? this.deserialize(path, cssValue) : undefined;
}
};
return serializer;
}
const StateSerializer = createSerializer();
export default StateSerializer;
export { createSerializer, escapeCssValue, unescapeCssValue };