juris-test
Version:
JavaScript Unified Reactive Interface Solution - Browser-optimized version for script tags and CDN usage
596 lines (505 loc) • 23.6 kB
JavaScript
if (typeof CSSExtractor === 'undefined') {
class CSSExtractor {
constructor() {
this.cache = new Map();
this.reactiveCache = new Map();
this.styleSheet = null;
this.componentCounter = 0;
this.reactiveCounter = 0;
this.pseudoVarCounter = 0;
this.patterns = {
media: /^/,
container: /^/,
keyframes: /^/,
layer: /^/,
pseudo: /^&[:]/,
interactive: /^\$/,
cssVar: /^--/
};
}
// Main entry point for processing props
processProps(props, elementName, domRenderer) {
if (!props.style) return props;
// Skip reactive style functions - they'll be post-processed later
if (typeof props.style === 'function') {
return props;
}
try {
const extraction = this.extractCSS(elementName, props.style);
if (!extraction) return props;
const modifiedProps = { ...props };
if (extraction.className) {
modifiedProps.className = this.combineClassNames(
props.className,
extraction.className
);
}
if (extraction.reactiveStyle) {
modifiedProps.style = extraction.reactiveStyle;
} else {
delete modifiedProps.style;
}
if (extraction.handlers) {
Object.assign(modifiedProps, extraction.handlers);
}
return modifiedProps;
} catch (error) {
console.error('CSS extractor error:', error);
return props;
}
}
// NEW: Post-process reactive style function results
postProcessReactiveResult(result, componentName, element) {
if (!result || typeof result !== 'object') {
return result;
}
// Check if result has complex CSS features
const hasComplexFeatures = this.hasComplexFeatures(result);
if (!hasComplexFeatures) {
// Simple styles, can be applied inline
return result;
}
// Generate cache key
const cacheKey = this.generateReactiveCacheKey(result, componentName);
let cached = this.reactiveCache.get(cacheKey);
if (!cached) {
// Process complex styles
const separation = this.separateStyles(result);
const className = this.generateReactiveClassName(componentName);
// Build and inject CSS for complex features
const css = this.buildReactiveCSS(className, separation);
if (css) {
this.injectCSS(css);
}
cached = {
className: css ? className : null,
inlineStyles: separation.staticStyles || {}
};
this.reactiveCache.set(cacheKey, cached);
}
// Apply class to element if needed
if (cached.className && element && element.classList) {
element.classList.add(cached.className);
}
return cached.inlineStyles;
}
// Check if object has complex CSS features
hasComplexFeatures(styleObj) {
return Object.keys(styleObj).some(key =>
key.startsWith('@') ||
key.startsWith('&') ||
key.startsWith('$')
);
}
// Extract CSS from style objects
extractCSS(componentName, styleObj) {
const separation = this.separateStyles(styleObj);
if (separation.isSimple) {
return this.handleSimpleExtraction(componentName, separation);
}
return this.handleComplexExtraction(componentName, separation);
}
// Separate styles into categories
separateStyles(styleObj) {
if (typeof styleObj === 'function') {
return {
staticStyles: {},
reactiveStyles: styleObj,
isFunction: true,
isSimple: true
};
}
const result = {
staticStyles: {},
reactiveStyles: {},
pseudoStyles: {},
mediaQueries: {},
containerQueries: {},
animations: {},
cssVariables: {},
interactiveStates: {},
others: {},
isSimple: true
};
for (const [key, value] of Object.entries(styleObj)) {
const category = this.categorizeStyle(key);
if (category !== 'staticStyles' && category !== 'reactiveStyles') {
result.isSimple = false;
}
if (typeof value === 'function') {
if (key.startsWith('--')) {
result.cssVariables[key] = value;
} else {
result.reactiveStyles[key] = value;
}
} else if (typeof value === 'object' && value !== null) {
result[category][key] = value;
if (category !== 'staticStyles') {
result.isSimple = false;
}
} else if (value !== null && value !== undefined) {
result[category][key] = value;
}
}
return result;
}
// Categorize CSS property
categorizeStyle(key) {
if (this.patterns.media.test(key)) return 'mediaQueries';
if (this.patterns.container.test(key)) return 'containerQueries';
if (this.patterns.keyframes.test(key)) return 'animations';
if (this.patterns.pseudo.test(key) || key.includes('&')) return 'pseudoStyles';
if (this.patterns.interactive.test(key)) return 'interactiveStates';
if (this.patterns.cssVar.test(key)) return 'cssVariables';
if (key.startsWith('@')) return 'others';
return 'staticStyles';
}
// Handle simple style extraction
handleSimpleExtraction(componentName, { staticStyles, reactiveStyles, isFunction }) {
if (isFunction) {
return { reactiveStyle: reactiveStyles };
}
const hasStatic = Object.keys(staticStyles).length > 0;
const hasReactive = Object.keys(reactiveStyles).length > 0;
if (!hasStatic) {
return hasReactive ? { reactiveStyle: reactiveStyles } : null;
}
const className = this.generateClassName(componentName, staticStyles);
if (!this.cache.has(className)) {
const css = this.generateSimpleCSS(className, staticStyles);
this.injectCSS(css);
this.cache.set(className, true);
}
const result = { className };
if (hasReactive) {
result.reactiveStyle = reactiveStyles;
}
return result;
}
// Handle complex style extraction
handleComplexExtraction(componentName, separation) {
const className = this.generateClassName(componentName);
const css = this.buildCompleteCSS(className, separation);
if (css && !this.cache.has(className)) {
this.injectCSS(css);
this.cache.set(className, true);
}
const result = {};
if (css) {
result.className = className;
}
// Combine all reactive styles
const allReactive = {
...separation.reactiveStyles,
...separation.cssVariables
};
if (Object.keys(allReactive).length > 0) {
result.reactiveStyle = allReactive;
}
if (Object.keys(separation.interactiveStates).length > 0) {
result.handlers = this.createInteractiveHandlers(
separation.interactiveStates,
className
);
}
return result;
}
// Build CSS for reactive styles (complex features)
buildReactiveCSS(className, separation) {
let css = '';
// Static styles
if (Object.keys(separation.staticStyles).length > 0) {
css += `.${className} {\n${this.styleObjectToCSS(separation.staticStyles)}}\n`;
}
// Pseudo selectors
for (const [selector, styles] of Object.entries(separation.pseudoStyles)) {
css += this.buildPseudoCSS(className, selector, styles);
}
// Media queries
for (const [query, styles] of Object.entries(separation.mediaQueries)) {
css += this.buildMediaQueryCSS(className, query, styles);
}
// Container queries
for (const [query, styles] of Object.entries(separation.containerQueries)) {
css += this.buildContainerQueryCSS(className, query, styles);
}
// Animations
for (const [name, keyframes] of Object.entries(separation.animations)) {
css += this.buildAnimationCSS(name, keyframes);
}
// Other at-rules
for (const [rule, styles] of Object.entries(separation.others)) {
css += this.buildAtRuleCSS(className, rule, styles);
}
return css;
}
// Build complete CSS for complex styles
buildCompleteCSS(className, separation) {
return this.buildReactiveCSS(className, separation);
}
// Build pseudo-selector CSS
buildPseudoCSS(className, selector, styles) {
const processedSelector = selector.replace('&', `.${className}`);
return `${processedSelector} {\n${this.styleObjectToCSS(styles)}}\n`;
}
// Build media query CSS
buildMediaQueryCSS(className, query, styles) {
return `${query} {\n .${className} {\n${this.styleObjectToCSS(styles, ' ')} }\n}\n`;
}
// Build container query CSS
buildContainerQueryCSS(className, query, styles) {
return `${query} {\n .${className} {\n${this.styleObjectToCSS(styles, ' ')} }\n}\n`;
}
// Build animation CSS
buildAnimationCSS(name, keyframes) {
let css = `${name} {\n`;
for (const [stop, styles] of Object.entries(keyframes)) {
css += ` ${stop} {\n${this.styleObjectToCSS(styles, ' ')} }\n`;
}
css += '}\n';
return css;
}
// Build at-rule CSS
buildAtRuleCSS(className, rule, styles) {
if (typeof styles === 'object' && !Array.isArray(styles)) {
return `${rule} {\n .${className} {\n${this.styleObjectToCSS(styles, ' ')} }\n}\n`;
}
return `${rule} { ${styles} }\n`;
}
// Create interactive event handlers
createInteractiveHandlers(interactiveStates, className) {
const handlers = {};
for (const [state, styles] of Object.entries(interactiveStates)) {
switch (state) {
case '$hover':
handlers.onMouseEnter = (e) => this.applyStyles(e.target, styles);
handlers.onMouseLeave = (e) => this.removeStyles(e.target, styles);
break;
case '$focus':
handlers.onFocus = (e) => this.applyStyles(e.target, styles);
handlers.onBlur = (e) => this.removeStyles(e.target, styles);
break;
case '$active':
handlers.onMouseDown = (e) => this.applyStyles(e.target, styles);
handlers.onMouseUp = (e) => this.removeStyles(e.target, styles);
break;
}
}
return handlers;
}
// Apply temporary styles
applyStyles(element, styles) {
if (!element._originalStyles) element._originalStyles = {};
for (const [prop, value] of Object.entries(styles)) {
if (typeof value !== 'function') {
element._originalStyles[prop] = element.style[prop];
element.style[this.camelToKebab(prop)] = value;
}
}
}
// Remove temporary styles
removeStyles(element, styles) {
if (!element._originalStyles) return;
for (const prop of Object.keys(styles)) {
if (typeof styles[prop] !== 'function') {
element.style[this.camelToKebab(prop)] = element._originalStyles[prop] || '';
}
}
}
// Generate simple CSS
generateSimpleCSS(className, styles) {
return `.${className} {\n${this.styleObjectToCSS(styles)}}\n`;
}
// Convert style object to CSS string
styleObjectToCSS(styles, indent = ' ') {
return Object.entries(styles)
.filter(([, value]) => typeof value !== 'function' && typeof value !== 'object')
.map(([prop, value]) => {
if (value == null) return '';
const stringValue = String(value);
return `${indent}${this.camelToKebab(prop)}: ${stringValue};`;
})
.filter(Boolean)
.join('\n') + (Object.keys(styles).length ? '\n' : '');
}
// Generate class name
generateClassName(componentName, styles = null) {
const cleanName = (componentName || 'element').replace(/[^a-zA-Z0-9]/g, '');
const hash = styles ? this.hashStyles(styles) : ++this.componentCounter;
return `j-${cleanName}-${hash}`;
}
// Generate reactive class name
generateReactiveClassName(componentName) {
const cleanName = (componentName || 'reactive').replace(/[^a-zA-Z0-9]/g, '');
return `j-${cleanName}-r${++this.reactiveCounter}`;
}
// Generate cache key for reactive styles
generateReactiveCacheKey(result, componentName) {
const str = JSON.stringify(result, Object.keys(result).sort()) + (componentName || '');
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash + str.charCodeAt(i)) & 0x7fffffff;
}
return `reactive-${hash.toString(36)}`;
}
// Hash styles for class generation
hashStyles(styles) {
const str = JSON.stringify(styles, Object.keys(styles).sort());
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash + str.charCodeAt(i)) & 0x7fffffff;
}
return hash.toString(36);
}
// Convert camelCase to kebab-case
camelToKebab(str) {
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}
// Combine class names
combineClassNames(...classNames) {
return classNames.filter(Boolean).join(' ');
}
// Inject CSS into DOM
injectCSS(css) {
if (typeof document === 'undefined') return;
try {
if (!this.styleSheet || !this.styleSheet.ownerNode) {
const style = document.createElement('style');
style.setAttribute('data-juris-css-extractor', '');
if (!document.head) {
if (document.documentElement) {
const head = document.createElement('head');
document.documentElement.insertBefore(head, document.body);
} else {
console.warn('Cannot inject CSS: document.head not available');
return;
}
}
document.head.appendChild(style);
this.styleSheet = style.sheet;
}
const rules = this.parseIndividualRules(css);
let hasInsertRuleFailure = false;
for (const rule of rules) {
if (rule.trim()) {
try {
if (this.styleSheet && this.styleSheet.insertRule && !hasInsertRuleFailure) {
this.styleSheet.insertRule(rule, this.styleSheet.cssRules.length);
} else {
hasInsertRuleFailure = true;
break;
}
} catch (ruleError) {
console.warn('Failed to insert individual rule:', rule, ruleError);
hasInsertRuleFailure = true;
break;
}
}
}
if (hasInsertRuleFailure) {
const styleElement = this.styleSheet?.ownerNode;
if (styleElement && typeof styleElement.textContent === 'string') {
styleElement.textContent += css + '\n';
} else {
const newStyle = document.createElement('style');
newStyle.textContent = css;
newStyle.setAttribute('data-juris-css-extractor-fallback', '');
document.head.appendChild(newStyle);
}
}
} catch (error) {
console.warn('CSS injection failed:', error);
try {
const emergencyStyle = document.createElement('style');
emergencyStyle.textContent = css;
emergencyStyle.setAttribute('data-juris-css-extractor-emergency', '');
document.head.appendChild(emergencyStyle);
} catch (emergencyError) {
console.error('Emergency CSS injection failed:', emergencyError);
}
}
}
// Parse CSS into individual rules
parseIndividualRules(css) {
const rules = [];
let currentRule = '';
let braceCount = 0;
let inAtRule = false;
let inString = false;
let stringChar = '';
for (let i = 0; i < css.length; i++) {
const char = css[i];
const prevChar = i > 0 ? css[i - 1] : '';
if ((char === '"' || char === "'") && prevChar !== '\\') {
if (!inString) {
inString = true;
stringChar = char;
} else if (char === stringChar) {
inString = false;
stringChar = '';
}
}
if (!inString) {
if (char === '@' && braceCount === 0) {
if (currentRule.trim()) {
rules.push(currentRule.trim());
currentRule = '';
}
inAtRule = true;
}
if (char === '{') {
braceCount++;
} else if (char === '}') {
braceCount--;
if (braceCount === 0) {
currentRule += char;
if (currentRule.trim()) {
rules.push(currentRule.trim());
}
currentRule = '';
inAtRule = false;
continue;
}
}
}
currentRule += char;
}
if (currentRule.trim()) {
rules.push(currentRule.trim());
}
return rules;
}
// Clear all caches and styles
clear() {
if (this.styleSheet?.ownerNode) {
this.styleSheet.ownerNode.remove();
this.styleSheet = null;
}
this.cache.clear();
this.reactiveCache.clear();
this.componentCounter = 0;
this.reactiveCounter = 0;
this.pseudoVarCounter = 0;
}
// Get statistics
getStats() {
return {
cachedClasses: this.cache.size,
reactiveCachedClasses: this.reactiveCache.size,
totalRules: this.styleSheet?.cssRules?.length || 0,
componentCounter: this.componentCounter,
reactiveCounter: this.reactiveCounter
};
}
}
if (typeof window !== 'undefined') {
window.CSSExtractor = CSSExtractor;
Object.freeze(window.CSSExtractor);
Object.freeze(window.CSSExtractor.prototype);
}
// Basic CommonJS for compatibility
if (typeof module !== 'undefined' && module.exports) {
module.exports.CSSExtractor = CSSExtractor;
module.exports.default = CSSExtractor;
}
}