UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

166 lines 5.9 kB
/** * @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