@sc4rfurryx/proteusjs 1.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/API.md +438 -0
- package/FEATURES.md +286 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/proteus.cjs.js +16014 -0
- package/dist/proteus.cjs.js.map +1 -0
- package/dist/proteus.d.ts +3018 -0
- package/dist/proteus.esm.js +16005 -0
- package/dist/proteus.esm.js.map +1 -0
- package/dist/proteus.esm.min.js +8 -0
- package/dist/proteus.esm.min.js.map +1 -0
- package/dist/proteus.js +16020 -0
- package/dist/proteus.js.map +1 -0
- package/dist/proteus.min.js +8 -0
- package/dist/proteus.min.js.map +1 -0
- package/package.json +98 -0
- package/src/__tests__/mvp-integration.test.ts +518 -0
- package/src/accessibility/AccessibilityEngine.ts +2106 -0
- package/src/accessibility/ScreenReaderSupport.ts +444 -0
- package/src/accessibility/__tests__/ScreenReaderSupport.test.ts +435 -0
- package/src/animations/FLIPAnimationSystem.ts +491 -0
- package/src/compatibility/BrowserCompatibility.ts +1076 -0
- package/src/containers/BreakpointSystem.ts +347 -0
- package/src/containers/ContainerBreakpoints.ts +726 -0
- package/src/containers/ContainerManager.ts +370 -0
- package/src/containers/ContainerUnits.ts +336 -0
- package/src/containers/ContextIsolation.ts +394 -0
- package/src/containers/ElementQueries.ts +411 -0
- package/src/containers/SmartContainer.ts +536 -0
- package/src/containers/SmartContainers.ts +376 -0
- package/src/containers/__tests__/ContainerBreakpoints.test.ts +411 -0
- package/src/containers/__tests__/SmartContainers.test.ts +281 -0
- package/src/content/ResponsiveImages.ts +570 -0
- package/src/core/EventSystem.ts +147 -0
- package/src/core/MemoryManager.ts +321 -0
- package/src/core/PerformanceMonitor.ts +238 -0
- package/src/core/PluginSystem.ts +275 -0
- package/src/core/ProteusJS.test.ts +164 -0
- package/src/core/ProteusJS.ts +962 -0
- package/src/developer/PerformanceProfiler.ts +567 -0
- package/src/developer/VisualDebuggingTools.ts +656 -0
- package/src/developer/ZeroConfigSystem.ts +593 -0
- package/src/index.ts +35 -0
- package/src/integration.test.ts +227 -0
- package/src/layout/AdaptiveGrid.ts +429 -0
- package/src/layout/ContentReordering.ts +532 -0
- package/src/layout/FlexboxEnhancer.ts +406 -0
- package/src/layout/FlowLayout.ts +545 -0
- package/src/layout/SpacingSystem.ts +512 -0
- package/src/observers/IntersectionObserverPolyfill.ts +289 -0
- package/src/observers/ObserverManager.ts +299 -0
- package/src/observers/ResizeObserverPolyfill.ts +179 -0
- package/src/performance/BatchDOMOperations.ts +519 -0
- package/src/performance/CSSOptimizationEngine.ts +646 -0
- package/src/performance/CacheOptimizationSystem.ts +601 -0
- package/src/performance/EfficientEventHandler.ts +740 -0
- package/src/performance/LazyEvaluationSystem.ts +532 -0
- package/src/performance/MemoryManagementSystem.ts +497 -0
- package/src/performance/PerformanceMonitor.ts +931 -0
- package/src/performance/__tests__/BatchDOMOperations.test.ts +309 -0
- package/src/performance/__tests__/EfficientEventHandler.test.ts +268 -0
- package/src/performance/__tests__/PerformanceMonitor.test.ts +422 -0
- package/src/polyfills/BrowserPolyfills.ts +586 -0
- package/src/polyfills/__tests__/BrowserPolyfills.test.ts +328 -0
- package/src/test/setup.ts +115 -0
- package/src/theming/SmartThemeSystem.ts +591 -0
- package/src/types/index.ts +134 -0
- package/src/typography/ClampScaling.ts +356 -0
- package/src/typography/FluidTypography.ts +759 -0
- package/src/typography/LineHeightOptimization.ts +430 -0
- package/src/typography/LineHeightOptimizer.ts +326 -0
- package/src/typography/TextFitting.ts +355 -0
- package/src/typography/TypographicScale.ts +428 -0
- package/src/typography/VerticalRhythm.ts +369 -0
- package/src/typography/__tests__/FluidTypography.test.ts +432 -0
- package/src/typography/__tests__/LineHeightOptimization.test.ts +436 -0
- package/src/utils/Logger.ts +173 -0
- package/src/utils/debounce.ts +259 -0
- package/src/utils/performance.ts +371 -0
- package/src/utils/support.ts +106 -0
- package/src/utils/version.ts +24 -0
@@ -0,0 +1,646 @@
|
|
1
|
+
/**
|
2
|
+
* CSS Optimization Engine for ProteusJS
|
3
|
+
* Style deduplication, critical CSS extraction, and performance optimization
|
4
|
+
*/
|
5
|
+
|
6
|
+
import { logger } from '../utils/Logger';
|
7
|
+
|
8
|
+
export interface CSSOptimizationConfig {
|
9
|
+
deduplication: boolean;
|
10
|
+
criticalCSS: boolean;
|
11
|
+
unusedStyleRemoval: boolean;
|
12
|
+
customPropertyOptimization: boolean;
|
13
|
+
styleInvalidationTracking: boolean;
|
14
|
+
minification: boolean;
|
15
|
+
autoprefixer: boolean;
|
16
|
+
purgeUnused: boolean;
|
17
|
+
}
|
18
|
+
|
19
|
+
export interface StyleRule {
|
20
|
+
selector: string;
|
21
|
+
declarations: Map<string, string>;
|
22
|
+
specificity: number;
|
23
|
+
source: 'inline' | 'stylesheet' | 'generated';
|
24
|
+
used: boolean;
|
25
|
+
critical: boolean;
|
26
|
+
}
|
27
|
+
|
28
|
+
export interface OptimizationMetrics {
|
29
|
+
originalSize: number;
|
30
|
+
optimizedSize: number;
|
31
|
+
compressionRatio: number;
|
32
|
+
rulesRemoved: number;
|
33
|
+
duplicatesFound: number;
|
34
|
+
customPropertiesOptimized: number;
|
35
|
+
criticalRulesExtracted: number;
|
36
|
+
}
|
37
|
+
|
38
|
+
export class CSSOptimizationEngine {
|
39
|
+
private config: Required<CSSOptimizationConfig>;
|
40
|
+
private styleRules: Map<string, StyleRule> = new Map();
|
41
|
+
private customProperties: Map<string, string> = new Map();
|
42
|
+
private usageTracker: Map<string, number> = new Map();
|
43
|
+
private criticalSelectors: Set<string> = new Set();
|
44
|
+
private metrics!: OptimizationMetrics;
|
45
|
+
private mutationObserver: MutationObserver | null = null;
|
46
|
+
|
47
|
+
constructor(config: Partial<CSSOptimizationConfig> = {}) {
|
48
|
+
this.config = {
|
49
|
+
deduplication: true,
|
50
|
+
criticalCSS: true,
|
51
|
+
unusedStyleRemoval: true,
|
52
|
+
customPropertyOptimization: true,
|
53
|
+
styleInvalidationTracking: true,
|
54
|
+
minification: true,
|
55
|
+
autoprefixer: false,
|
56
|
+
purgeUnused: true,
|
57
|
+
...config
|
58
|
+
};
|
59
|
+
|
60
|
+
this.metrics = this.createInitialMetrics();
|
61
|
+
this.setupStyleTracking();
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Analyze and optimize all stylesheets
|
66
|
+
*/
|
67
|
+
public async optimizeAll(): Promise<OptimizationMetrics> {
|
68
|
+
const startTime = performance.now();
|
69
|
+
|
70
|
+
// Extract all styles
|
71
|
+
await this.extractAllStyles();
|
72
|
+
|
73
|
+
// Apply optimizations
|
74
|
+
if (this.config.deduplication) {
|
75
|
+
this.deduplicateStyles();
|
76
|
+
}
|
77
|
+
|
78
|
+
if (this.config.unusedStyleRemoval) {
|
79
|
+
this.removeUnusedStyles();
|
80
|
+
}
|
81
|
+
|
82
|
+
if (this.config.customPropertyOptimization) {
|
83
|
+
this.optimizeCustomProperties();
|
84
|
+
}
|
85
|
+
|
86
|
+
if (this.config.criticalCSS) {
|
87
|
+
this.extractCriticalCSS();
|
88
|
+
}
|
89
|
+
|
90
|
+
// Generate optimized CSS
|
91
|
+
const optimizedCSS = this.generateOptimizedCSS();
|
92
|
+
|
93
|
+
// Update metrics
|
94
|
+
this.updateMetrics(optimizedCSS);
|
95
|
+
|
96
|
+
const endTime = performance.now();
|
97
|
+
console.log(`CSS optimization completed in ${endTime - startTime}ms`);
|
98
|
+
|
99
|
+
return this.metrics;
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* Extract critical CSS for above-the-fold content
|
104
|
+
*/
|
105
|
+
public extractCriticalCSS(): string {
|
106
|
+
const criticalRules: StyleRule[] = [];
|
107
|
+
const viewportHeight = window.innerHeight;
|
108
|
+
|
109
|
+
// Find elements in viewport
|
110
|
+
const elementsInViewport = this.getElementsInViewport(viewportHeight);
|
111
|
+
|
112
|
+
// Extract selectors for viewport elements
|
113
|
+
elementsInViewport.forEach(element => {
|
114
|
+
const selectors = this.getSelectorsForElement(element);
|
115
|
+
selectors.forEach(selector => {
|
116
|
+
this.criticalSelectors.add(selector);
|
117
|
+
const rule = this.styleRules.get(selector);
|
118
|
+
if (rule) {
|
119
|
+
rule.critical = true;
|
120
|
+
criticalRules.push(rule);
|
121
|
+
}
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
this.metrics.criticalRulesExtracted = criticalRules.length;
|
126
|
+
|
127
|
+
return this.rulesToCSS(criticalRules);
|
128
|
+
}
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Remove unused CSS rules
|
132
|
+
*/
|
133
|
+
public removeUnusedStyles(): void {
|
134
|
+
if (!this.config.purgeUnused) return;
|
135
|
+
|
136
|
+
let removedCount = 0;
|
137
|
+
|
138
|
+
this.styleRules.forEach((rule, selector) => {
|
139
|
+
if (!rule.used && !rule.critical) {
|
140
|
+
// Check if selector matches any element in DOM
|
141
|
+
try {
|
142
|
+
const elements = document.querySelectorAll(selector);
|
143
|
+
if (elements.length === 0) {
|
144
|
+
this.styleRules.delete(selector);
|
145
|
+
removedCount++;
|
146
|
+
}
|
147
|
+
} catch (error) {
|
148
|
+
// Invalid selector, remove it
|
149
|
+
this.styleRules.delete(selector);
|
150
|
+
removedCount++;
|
151
|
+
}
|
152
|
+
}
|
153
|
+
});
|
154
|
+
|
155
|
+
this.metrics.rulesRemoved = removedCount;
|
156
|
+
}
|
157
|
+
|
158
|
+
/**
|
159
|
+
* Deduplicate identical style rules
|
160
|
+
*/
|
161
|
+
public deduplicateStyles(): void {
|
162
|
+
const declarationGroups = new Map<string, string[]>();
|
163
|
+
let duplicatesFound = 0;
|
164
|
+
|
165
|
+
// Group rules by declarations
|
166
|
+
this.styleRules.forEach((rule, selector) => {
|
167
|
+
const declarationsKey = this.serializeDeclarations(rule.declarations);
|
168
|
+
|
169
|
+
if (!declarationGroups.has(declarationsKey)) {
|
170
|
+
declarationGroups.set(declarationsKey, []);
|
171
|
+
}
|
172
|
+
|
173
|
+
declarationGroups.get(declarationsKey)!.push(selector);
|
174
|
+
});
|
175
|
+
|
176
|
+
// Merge duplicate rules
|
177
|
+
declarationGroups.forEach((selectors) => {
|
178
|
+
if (selectors.length > 1) {
|
179
|
+
duplicatesFound += selectors.length - 1;
|
180
|
+
|
181
|
+
// Keep the first rule and merge selectors
|
182
|
+
const primarySelector = selectors[0]!;
|
183
|
+
const primaryRule = this.styleRules.get(primarySelector)!;
|
184
|
+
|
185
|
+
// Create combined selector
|
186
|
+
const combinedSelector = selectors.join(', ');
|
187
|
+
|
188
|
+
// Remove individual rules
|
189
|
+
selectors.forEach(selector => {
|
190
|
+
this.styleRules.delete(selector);
|
191
|
+
});
|
192
|
+
|
193
|
+
// Add combined rule
|
194
|
+
this.styleRules.set(combinedSelector, {
|
195
|
+
...primaryRule,
|
196
|
+
selector: combinedSelector
|
197
|
+
});
|
198
|
+
}
|
199
|
+
});
|
200
|
+
|
201
|
+
this.metrics.duplicatesFound = duplicatesFound;
|
202
|
+
}
|
203
|
+
|
204
|
+
/**
|
205
|
+
* Optimize CSS custom properties
|
206
|
+
*/
|
207
|
+
public optimizeCustomProperties(): void {
|
208
|
+
let optimizedCount = 0;
|
209
|
+
|
210
|
+
// Find all custom property declarations
|
211
|
+
this.styleRules.forEach(rule => {
|
212
|
+
rule.declarations.forEach((value, property) => {
|
213
|
+
if (property.startsWith('--')) {
|
214
|
+
this.customProperties.set(property, value);
|
215
|
+
}
|
216
|
+
});
|
217
|
+
});
|
218
|
+
|
219
|
+
// Optimize custom property usage
|
220
|
+
this.customProperties.forEach((value, property) => {
|
221
|
+
const usage = this.findCustomPropertyUsage(property);
|
222
|
+
|
223
|
+
if (usage.length === 1) {
|
224
|
+
// Inline single-use custom properties
|
225
|
+
const targetRule = usage[0]!;
|
226
|
+
const targetProperty = this.findPropertyUsingCustomProperty(targetRule, property);
|
227
|
+
|
228
|
+
if (targetProperty && targetRule) {
|
229
|
+
targetRule.declarations.set(targetProperty, value);
|
230
|
+
targetRule.declarations.delete(property);
|
231
|
+
optimizedCount++;
|
232
|
+
}
|
233
|
+
}
|
234
|
+
});
|
235
|
+
|
236
|
+
this.metrics.customPropertiesOptimized = optimizedCount;
|
237
|
+
}
|
238
|
+
|
239
|
+
/**
|
240
|
+
* Track style invalidations
|
241
|
+
*/
|
242
|
+
public trackStyleInvalidations(): void {
|
243
|
+
if (!this.config.styleInvalidationTracking) return;
|
244
|
+
|
245
|
+
try {
|
246
|
+
// Monitor DOM mutations that might affect styles
|
247
|
+
if (typeof MutationObserver !== 'undefined' && document && document.body) {
|
248
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
249
|
+
mutations.forEach(mutation => {
|
250
|
+
if (mutation.type === 'attributes' &&
|
251
|
+
(mutation.attributeName === 'class' || mutation.attributeName === 'style')) {
|
252
|
+
this.handleStyleInvalidation(mutation.target as Element);
|
253
|
+
}
|
254
|
+
});
|
255
|
+
});
|
256
|
+
|
257
|
+
// Ensure document.body exists before observing
|
258
|
+
if (document.body && this.mutationObserver && typeof this.mutationObserver.observe === 'function') {
|
259
|
+
this.mutationObserver.observe(document.body, {
|
260
|
+
attributes: true,
|
261
|
+
attributeFilter: ['class', 'style'],
|
262
|
+
subtree: true
|
263
|
+
});
|
264
|
+
}
|
265
|
+
}
|
266
|
+
} catch (error) {
|
267
|
+
logger.warn('Failed to setup style invalidation tracking:', error);
|
268
|
+
}
|
269
|
+
}
|
270
|
+
|
271
|
+
/**
|
272
|
+
* Generate optimized CSS string
|
273
|
+
*/
|
274
|
+
public generateOptimizedCSS(): string {
|
275
|
+
const rules = Array.from(this.styleRules.values());
|
276
|
+
|
277
|
+
// Sort rules by specificity and source
|
278
|
+
rules.sort((a, b) => {
|
279
|
+
if (a.critical !== b.critical) {
|
280
|
+
return a.critical ? -1 : 1; // Critical rules first
|
281
|
+
}
|
282
|
+
return a.specificity - b.specificity;
|
283
|
+
});
|
284
|
+
|
285
|
+
return this.rulesToCSS(rules);
|
286
|
+
}
|
287
|
+
|
288
|
+
/**
|
289
|
+
* Get optimization metrics
|
290
|
+
*/
|
291
|
+
public getMetrics(): OptimizationMetrics {
|
292
|
+
return { ...this.metrics };
|
293
|
+
}
|
294
|
+
|
295
|
+
/**
|
296
|
+
* Clear optimization cache
|
297
|
+
*/
|
298
|
+
public clearCache(): void {
|
299
|
+
this.styleRules.clear();
|
300
|
+
this.customProperties.clear();
|
301
|
+
this.usageTracker.clear();
|
302
|
+
this.criticalSelectors.clear();
|
303
|
+
this.metrics = this.createInitialMetrics();
|
304
|
+
}
|
305
|
+
|
306
|
+
/**
|
307
|
+
* Destroy the optimization engine
|
308
|
+
*/
|
309
|
+
public destroy(): void {
|
310
|
+
this.mutationObserver?.disconnect();
|
311
|
+
this.clearCache();
|
312
|
+
}
|
313
|
+
|
314
|
+
/**
|
315
|
+
* Extract all styles from document
|
316
|
+
*/
|
317
|
+
private async extractAllStyles(): Promise<void> {
|
318
|
+
const originalSize = this.calculateCurrentCSSSize();
|
319
|
+
this.metrics.originalSize = originalSize;
|
320
|
+
|
321
|
+
// Extract from stylesheets
|
322
|
+
for (const stylesheet of document.styleSheets) {
|
323
|
+
try {
|
324
|
+
await this.extractFromStylesheet(stylesheet);
|
325
|
+
} catch (error) {
|
326
|
+
console.warn('Could not access stylesheet:', error);
|
327
|
+
}
|
328
|
+
}
|
329
|
+
|
330
|
+
// Extract inline styles
|
331
|
+
this.extractInlineStyles();
|
332
|
+
}
|
333
|
+
|
334
|
+
/**
|
335
|
+
* Extract styles from a stylesheet
|
336
|
+
*/
|
337
|
+
private async extractFromStylesheet(stylesheet: CSSStyleSheet): Promise<void> {
|
338
|
+
try {
|
339
|
+
const rules = stylesheet.cssRules;
|
340
|
+
|
341
|
+
for (let i = 0; i < rules.length; i++) {
|
342
|
+
const rule = rules[i];
|
343
|
+
|
344
|
+
if (rule instanceof CSSStyleRule) {
|
345
|
+
this.extractStyleRule(rule, 'stylesheet');
|
346
|
+
} else if (rule instanceof CSSMediaRule) {
|
347
|
+
// Handle media queries
|
348
|
+
for (let j = 0; j < rule.cssRules.length; j++) {
|
349
|
+
const mediaRule = rule.cssRules[j];
|
350
|
+
if (mediaRule instanceof CSSStyleRule) {
|
351
|
+
this.extractStyleRule(mediaRule, 'stylesheet');
|
352
|
+
}
|
353
|
+
}
|
354
|
+
}
|
355
|
+
}
|
356
|
+
} catch (error) {
|
357
|
+
console.warn('Error extracting from stylesheet:', error);
|
358
|
+
}
|
359
|
+
}
|
360
|
+
|
361
|
+
/**
|
362
|
+
* Extract a single style rule
|
363
|
+
*/
|
364
|
+
private extractStyleRule(rule: CSSStyleRule, source: 'inline' | 'stylesheet' | 'generated'): void {
|
365
|
+
const selector = rule.selectorText;
|
366
|
+
const declarations = new Map<string, string>();
|
367
|
+
|
368
|
+
// Extract declarations
|
369
|
+
for (let i = 0; i < rule.style.length; i++) {
|
370
|
+
const property = rule.style[i]!;
|
371
|
+
const value = rule.style.getPropertyValue(property);
|
372
|
+
declarations.set(property, value);
|
373
|
+
}
|
374
|
+
|
375
|
+
const styleRule: StyleRule = {
|
376
|
+
selector,
|
377
|
+
declarations,
|
378
|
+
specificity: this.calculateSpecificity(selector),
|
379
|
+
source,
|
380
|
+
used: false,
|
381
|
+
critical: false
|
382
|
+
};
|
383
|
+
|
384
|
+
this.styleRules.set(selector, styleRule);
|
385
|
+
}
|
386
|
+
|
387
|
+
/**
|
388
|
+
* Extract inline styles
|
389
|
+
*/
|
390
|
+
private extractInlineStyles(): void {
|
391
|
+
const elementsWithStyle = document.querySelectorAll('[style]');
|
392
|
+
|
393
|
+
elementsWithStyle.forEach(element => {
|
394
|
+
const style = (element as HTMLElement).style;
|
395
|
+
const declarations = new Map<string, string>();
|
396
|
+
|
397
|
+
for (let i = 0; i < style.length; i++) {
|
398
|
+
const property = style[i];
|
399
|
+
if (property) {
|
400
|
+
const value = style.getPropertyValue(property);
|
401
|
+
declarations.set(property, value);
|
402
|
+
}
|
403
|
+
}
|
404
|
+
|
405
|
+
if (declarations.size > 0) {
|
406
|
+
const selector = this.generateSelectorForElement(element);
|
407
|
+
|
408
|
+
const styleRule: StyleRule = {
|
409
|
+
selector,
|
410
|
+
declarations,
|
411
|
+
specificity: 1000, // Inline styles have high specificity
|
412
|
+
source: 'inline',
|
413
|
+
used: true,
|
414
|
+
critical: this.isElementInViewport(element)
|
415
|
+
};
|
416
|
+
|
417
|
+
this.styleRules.set(selector, styleRule);
|
418
|
+
}
|
419
|
+
});
|
420
|
+
}
|
421
|
+
|
422
|
+
/**
|
423
|
+
* Calculate CSS specificity
|
424
|
+
*/
|
425
|
+
private calculateSpecificity(selector: string): number {
|
426
|
+
let specificity = 0;
|
427
|
+
|
428
|
+
// Count IDs
|
429
|
+
specificity += (selector.match(/#/g) || []).length * 100;
|
430
|
+
|
431
|
+
// Count classes, attributes, and pseudo-classes
|
432
|
+
specificity += (selector.match(/\.|:|\[/g) || []).length * 10;
|
433
|
+
|
434
|
+
// Count elements and pseudo-elements
|
435
|
+
specificity += (selector.match(/[a-zA-Z]/g) || []).length;
|
436
|
+
|
437
|
+
return specificity;
|
438
|
+
}
|
439
|
+
|
440
|
+
/**
|
441
|
+
* Get elements in viewport
|
442
|
+
*/
|
443
|
+
private getElementsInViewport(viewportHeight: number): Element[] {
|
444
|
+
const elements: Element[] = [];
|
445
|
+
const allElements = document.querySelectorAll('*');
|
446
|
+
|
447
|
+
allElements.forEach(element => {
|
448
|
+
if (this.isElementInViewport(element, viewportHeight)) {
|
449
|
+
elements.push(element);
|
450
|
+
}
|
451
|
+
});
|
452
|
+
|
453
|
+
return elements;
|
454
|
+
}
|
455
|
+
|
456
|
+
/**
|
457
|
+
* Check if element is in viewport
|
458
|
+
*/
|
459
|
+
private isElementInViewport(element: Element, viewportHeight?: number): boolean {
|
460
|
+
const rect = element.getBoundingClientRect();
|
461
|
+
const height = viewportHeight || window.innerHeight;
|
462
|
+
|
463
|
+
return rect.top < height && rect.bottom > 0;
|
464
|
+
}
|
465
|
+
|
466
|
+
/**
|
467
|
+
* Get selectors that match an element
|
468
|
+
*/
|
469
|
+
private getSelectorsForElement(element: Element): string[] {
|
470
|
+
const selectors: string[] = [];
|
471
|
+
|
472
|
+
this.styleRules.forEach((rule, selector) => {
|
473
|
+
try {
|
474
|
+
if (element.matches(selector)) {
|
475
|
+
selectors.push(selector);
|
476
|
+
rule.used = true;
|
477
|
+
}
|
478
|
+
} catch (error) {
|
479
|
+
// Invalid selector
|
480
|
+
}
|
481
|
+
});
|
482
|
+
|
483
|
+
return selectors;
|
484
|
+
}
|
485
|
+
|
486
|
+
/**
|
487
|
+
* Generate selector for element
|
488
|
+
*/
|
489
|
+
private generateSelectorForElement(element: Element): string {
|
490
|
+
if (element.id) {
|
491
|
+
return `#${element.id}`;
|
492
|
+
}
|
493
|
+
|
494
|
+
if (element.className) {
|
495
|
+
const classes = element.className.split(' ').filter(c => c.trim());
|
496
|
+
if (classes.length > 0) {
|
497
|
+
return `.${classes.join('.')}`;
|
498
|
+
}
|
499
|
+
}
|
500
|
+
|
501
|
+
return element.tagName.toLowerCase();
|
502
|
+
}
|
503
|
+
|
504
|
+
/**
|
505
|
+
* Convert rules to CSS string
|
506
|
+
*/
|
507
|
+
private rulesToCSS(rules: StyleRule[]): string {
|
508
|
+
let css = '';
|
509
|
+
|
510
|
+
rules.forEach(rule => {
|
511
|
+
css += `${rule.selector} {\n`;
|
512
|
+
|
513
|
+
rule.declarations.forEach((value, property) => {
|
514
|
+
css += ` ${property}: ${value};\n`;
|
515
|
+
});
|
516
|
+
|
517
|
+
css += '}\n\n';
|
518
|
+
});
|
519
|
+
|
520
|
+
if (this.config.minification) {
|
521
|
+
css = this.minifyCSS(css);
|
522
|
+
}
|
523
|
+
|
524
|
+
return css;
|
525
|
+
}
|
526
|
+
|
527
|
+
/**
|
528
|
+
* Minify CSS
|
529
|
+
*/
|
530
|
+
private minifyCSS(css: string): string {
|
531
|
+
return css
|
532
|
+
.replace(/\s+/g, ' ')
|
533
|
+
.replace(/;\s*}/g, '}')
|
534
|
+
.replace(/{\s*/g, '{')
|
535
|
+
.replace(/}\s*/g, '}')
|
536
|
+
.replace(/:\s*/g, ':')
|
537
|
+
.replace(/;\s*/g, ';')
|
538
|
+
.trim();
|
539
|
+
}
|
540
|
+
|
541
|
+
/**
|
542
|
+
* Serialize declarations for comparison
|
543
|
+
*/
|
544
|
+
private serializeDeclarations(declarations: Map<string, string>): string {
|
545
|
+
const sorted = Array.from(declarations.entries()).sort();
|
546
|
+
return JSON.stringify(sorted);
|
547
|
+
}
|
548
|
+
|
549
|
+
/**
|
550
|
+
* Find custom property usage
|
551
|
+
*/
|
552
|
+
private findCustomPropertyUsage(property: string): StyleRule[] {
|
553
|
+
const usage: StyleRule[] = [];
|
554
|
+
|
555
|
+
this.styleRules.forEach(rule => {
|
556
|
+
rule.declarations.forEach(value => {
|
557
|
+
if (value.includes(`var(${property})`)) {
|
558
|
+
usage.push(rule);
|
559
|
+
}
|
560
|
+
});
|
561
|
+
});
|
562
|
+
|
563
|
+
return usage;
|
564
|
+
}
|
565
|
+
|
566
|
+
/**
|
567
|
+
* Find property using custom property
|
568
|
+
*/
|
569
|
+
private findPropertyUsingCustomProperty(rule: StyleRule, customProperty: string): string | null {
|
570
|
+
for (const [property, value] of rule.declarations) {
|
571
|
+
if (value.includes(`var(${customProperty})`)) {
|
572
|
+
return property;
|
573
|
+
}
|
574
|
+
}
|
575
|
+
return null;
|
576
|
+
}
|
577
|
+
|
578
|
+
/**
|
579
|
+
* Handle style invalidation
|
580
|
+
*/
|
581
|
+
private handleStyleInvalidation(element: Element): void {
|
582
|
+
const selectors = this.getSelectorsForElement(element);
|
583
|
+
|
584
|
+
selectors.forEach(selector => {
|
585
|
+
const count = this.usageTracker.get(selector) || 0;
|
586
|
+
this.usageTracker.set(selector, count + 1);
|
587
|
+
});
|
588
|
+
}
|
589
|
+
|
590
|
+
/**
|
591
|
+
* Calculate current CSS size
|
592
|
+
*/
|
593
|
+
private calculateCurrentCSSSize(): number {
|
594
|
+
let size = 0;
|
595
|
+
|
596
|
+
for (const stylesheet of document.styleSheets) {
|
597
|
+
try {
|
598
|
+
const rules = stylesheet.cssRules;
|
599
|
+
for (let i = 0; i < rules.length; i++) {
|
600
|
+
size += rules[i]!.cssText.length;
|
601
|
+
}
|
602
|
+
} catch (error) {
|
603
|
+
// Can't access external stylesheets
|
604
|
+
}
|
605
|
+
}
|
606
|
+
|
607
|
+
return size;
|
608
|
+
}
|
609
|
+
|
610
|
+
/**
|
611
|
+
* Update optimization metrics
|
612
|
+
*/
|
613
|
+
private updateMetrics(optimizedCSS: string): void {
|
614
|
+
this.metrics.optimizedSize = optimizedCSS.length;
|
615
|
+
this.metrics.compressionRatio = this.metrics.originalSize > 0
|
616
|
+
? (this.metrics.originalSize - this.metrics.optimizedSize) / this.metrics.originalSize
|
617
|
+
: 0;
|
618
|
+
}
|
619
|
+
|
620
|
+
/**
|
621
|
+
* Setup style tracking
|
622
|
+
*/
|
623
|
+
private setupStyleTracking(): void {
|
624
|
+
if (this.config.styleInvalidationTracking) {
|
625
|
+
// Delay tracking setup to allow DOM to settle
|
626
|
+
setTimeout(() => {
|
627
|
+
this.trackStyleInvalidations();
|
628
|
+
}, 100);
|
629
|
+
}
|
630
|
+
}
|
631
|
+
|
632
|
+
/**
|
633
|
+
* Create initial metrics
|
634
|
+
*/
|
635
|
+
private createInitialMetrics(): OptimizationMetrics {
|
636
|
+
return {
|
637
|
+
originalSize: 0,
|
638
|
+
optimizedSize: 0,
|
639
|
+
compressionRatio: 0,
|
640
|
+
rulesRemoved: 0,
|
641
|
+
duplicatesFound: 0,
|
642
|
+
customPropertiesOptimized: 0,
|
643
|
+
criticalRulesExtracted: 0
|
644
|
+
};
|
645
|
+
}
|
646
|
+
}
|