@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
166 lines • 5.9 kB
JavaScript
/**
* @fileoverview CSS Scoping System for OrdoJS Framework
* Handles scoping CSS selectors to prevent style conflicts between components
*/
import {} from '../types/index.js';
/**
* Default CSS Scoping options
*/
const DEFAULT_CSS_SCOPING_OPTIONS = {
scopePrefix: 'ordojs',
useDataAttributes: true,
preserveOriginalSelectors: false
};
/**
* CSS Scoper for OrdoJS components
*/
export class OrdoJSCSSScoper {
options;
constructor(options = {}) {
this.options = { ...DEFAULT_CSS_SCOPING_OPTIONS, ...options };
}
/**
* Scope CSS rules to a specific component
*/
scopeStyles(styleBlock, componentName) {
// If the style block is not scoped, return it as is
if (!styleBlock.scoped) {
return styleBlock;
}
// Generate a unique scope identifier for the component
const scopeId = this.generateScopeId(componentName);
// Scope each rule
const scopedRules = styleBlock.rules.map(rule => this.scopeRule(rule, scopeId));
return {
...styleBlock,
rules: scopedRules
};
}
/**
* Scope a single CSS rule
*/
scopeRule(rule, scopeId) {
const scopedSelector = this.scopeSelector(rule.selector, scopeId);
return {
...rule,
selector: scopedSelector
};
}
/**
* Scope a CSS selector
*/
scopeSelector(selector, scopeId) {
// Split complex selectors (comma-separated)
const selectors = selector.split(',').map(s => s.trim());
// Scope each individual selector
const scopedSelectors = selectors.map(s => this.scopeSingleSelector(s, scopeId));
// If preserving original selectors, include both versions
if (this.options.preserveOriginalSelectors) {
return [...selectors, ...scopedSelectors].join(', ');
}
return scopedSelectors.join(', ');
}
/**
* Scope a single CSS selector (without commas)
*/
scopeSingleSelector(selector, scopeId) {
// Handle special selectors
if (selector === ':root' || selector === 'html' || selector === 'body') {
return selector;
}
// Handle complex selectors with combinators
const combinatorRegex = /(\s*[>+~]\s*)/;
const parts = selector.split(combinatorRegex);
// If there are combinators, scope each part separately
if (parts.length > 1) {
const scopedParts = parts.map((part, index) => {
// Skip combinator parts (odd indices)
if (index % 2 === 1) {
return part;
}
// Scope the selector part
return this.scopeSelectorPart(part.trim(), scopeId);
});
return scopedParts.join('');
}
// Simple selector without combinators
return this.scopeSelectorPart(selector, scopeId);
}
/**
* Scope a single selector part (no combinators)
*/
scopeSelectorPart(selector, scopeId) {
// Handle pseudo-elements and pseudo-classes
const hasPseudoElement = /::([a-z-]+)$/.test(selector);
const hasPseudoClass = /:([a-z-]+)$/.test(selector);
let baseSelector = selector;
let pseudoSuffix = '';
// Extract pseudo-elements and pseudo-classes to append them after scoping
if (hasPseudoElement) {
const match = selector.match(/::([a-z-]+)$/);
if (match) {
baseSelector = selector.substring(0, selector.length - match[0].length);
pseudoSuffix = match[0];
}
}
else if (hasPseudoClass) {
const match = selector.match(/:([a-z-]+)$/);
if (match) {
baseSelector = selector.substring(0, selector.length - match[0].length);
pseudoSuffix = match[0];
}
}
// Apply scoping attribute or class
let scopedSelector;
if (this.options.useDataAttributes) {
scopedSelector = `${baseSelector}[data-${this.options.scopePrefix}-${scopeId}]${pseudoSuffix}`;
}
else {
scopedSelector = `${baseSelector}.${this.options.scopePrefix}-${scopeId}${pseudoSuffix}`;
}
return scopedSelector;
}
/**
* Generate a unique scope ID for a component
*/
generateScopeId(componentName) {
// Create a hash from the component name for uniqueness
const hash = this.hashString(componentName);
return `${componentName.toLowerCase()}-${hash}`;
}
/**
* Generate a simple hash from a string
*/
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
// Convert to a positive hex string and take the first 8 characters
return Math.abs(hash).toString(16).substring(0, 8);
}
/**
* Generate the data attribute or class name for component markup
*/
generateScopeAttribute(componentName) {
const scopeId = this.generateScopeId(componentName);
if (this.options.useDataAttributes) {
return `data-${this.options.scopePrefix}-${scopeId}`;
}
else {
return `class="${this.options.scopePrefix}-${scopeId}"`;
}
}
/**
* Convert a scoped StyleBlockNode back to CSS text
*/
generateScopedCSS(styleBlock) {
return styleBlock.rules.map(rule => {
const declarations = rule.declarations.map(decl => ` ${decl.property}: ${decl.value};`).join('\n');
return `${rule.selector} {\n${declarations}\n}`;
}).join('\n\n');
}
}
//# sourceMappingURL=css-scoper.js.map