Contenuti




Python: CSS cleaner

Rimuovi regole CSS inutilizzate e ottimizza le performance


CSS cleaner: ottimizzazione automatica dei file CSS

I file CSS inutilizzati possono rallentare significativamente le performance del tuo sito web. Questo strumento Python avanzato analizza il codice HTML e rimuove automaticamente le regole CSS non utilizzate, riducendo la dimensione dei file e migliorando i tempi di caricamento.

In questo articolo
  • CSS Cleaner Base: Script Python per rimuovere CSS inutilizzato
  • Analisi Avanzata: Parsing HTML completo e analisi dinamica
  • Versione pronta per la produzione: Strumento robusto con logging e gestione errori
  • Interfaccia Grafica: GUI per uso desktop
  • Integrazione CI/CD: Automazione per pipeline di build
  • Metriche di performance: Misurazione impatto ottimizzazioni
  • Supporto plugin: Estensioni per framework popolari

Indice della Guida

Parte I - Fondamenti

  1. Problema e Soluzione
  2. Configurazione ambiente di sviluppo
  3. CSS Cleaner Base
  4. Testing e Validazione

Parte II - Versione avanzata

  1. Parser CSS Completo
  2. Analisi HTML Dinamica
  3. Gestione Media Queries
  4. Backup e ripristino

Parte III - Pronta per la produzione

  1. Logging e monitoraggio
  2. Benchmark delle prestazioni
  3. Integrazione CI/CD
  4. Plugin per Framework

Problema e Soluzione

Impatto delle regole CSS inutilizzate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
PROBLEMI COMUNI:
├── File CSS gonfiati (fino a 80% di codice inutile)
├── Tempi di caricamento lenti
├── Bandwidth sprecato
├── Core Web Vitals peggiori
└── SEO ranking inferiore

BENEFICI CSS CLEANING:
├── Riduzione dimensione file (30-80%)
├── First Contentful Paint piu rapido
├── Largest Contentful Paint migliorato
├── Time to Interactive ridotto
└── Migliori performance SEO

Casi d’uso comuni

Siti e-commerce:

  • Template con centinaia di componenti CSS
  • Solo 20-30% del CSS utilizzato per pagina
  • Riduzione tipica: 60-70% della dimensione file

Siti aziendali:

  • Framework CSS completi (Bootstrap, Tailwind)
  • Utilizzo parziale delle utility classes
  • Riduzione tipica: 40-60%

SPA (applicazioni a pagina singola):

  • CSS accumulato da più componenti
  • Dead code da refactoring
  • Riduzione tipica: 30-50%

Configurazione ambiente di sviluppo

Installazione Dipendenze

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Crea virtual environment
python -m venv css_cleaner_env
source css_cleaner_env/bin/activate  # Linux/Mac
# css_cleaner_env\Scripts\activate  # Windows

# Installa dipendenze base
pip install beautifulsoup4 lxml cssutils tinycss2

# Dipendenze avanzate
pip install click rich colorama watchdog
pip install selenium webdriver-manager  # Per JS-generated content
pip install flask flask-cors  # Per web interface
pip install tkinter-modern  # Per GUI moderna

# Development tools
pip install black pytest coverage mypy

Per dettagli su venv e Poetry, vedi guida ai virtual environment.

Struttura Progetto

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
css-cleaner/
├── src/
│   ├── __init__.py
│   ├── core/
│   │   ├── __init__.py
│   │   ├── html_parser.py
│   │   ├── css_parser.py
│   │   └── optimizer.py
│   ├── interfaces/
│   │   ├── cli.py
│   │   ├── gui.py
│   │   └── web_app.py
│   └── utils/
│       ├── __init__.py
│       ├── logger.py
│       └── performance.py
├── tests/
├── examples/
├── docs/
└── requirements.txt

CSS Cleaner Base

Versione Migliorata dello Script Base

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
#!/usr/bin/env python3
"""
CSS Cleaner - Advanced Version
Rimuove automaticamente regole CSS non utilizzate
"""

import os
import re
import logging
from typing import Set, List, Dict, Tuple
from pathlib import Path
from dataclasses import dataclass
from bs4 import BeautifulSoup, Comment
import cssutils
from urllib.parse import urljoin, urlparse

@dataclass
class CleaningStats:
    """Statistiche del processo di pulizia"""
    original_size: int
    cleaned_size: int
    rules_removed: int
    rules_kept: int
    selectors_found: int
    files_processed: int

class CSSCleaner:
    """CSS Cleaner con funzionalità avanzate"""

    def __init__(self, html_dir: str, css_dir: str, output_dir: str = None):
        self.html_dir = Path(html_dir)
        self.css_dir = Path(css_dir)
        self.output_dir = Path(output_dir) if output_dir else self.css_dir
        self.used_selectors: Set[str] = set()
        self.stats = CleaningStats(0, 0, 0, 0, 0, 0)

        # Imposta logging
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s'
        )
        self.logger = logging.getLogger(__name__)

        # Configurazione CSS parser
        cssutils.log.setLevel(logging.CRITICAL)  # Silenzia warning CSS

    def extract_selectors_from_html(self) -> Set[str]:
        """Estrae tutti i selettori utilizzati dai file HTML"""
        selectors = set()

        # Pattern per attributi class e id
        class_pattern = re.compile(r'class\s*=\s*["\']([^"\']+)["\']', re.IGNORECASE)
        id_pattern = re.compile(r'id\s*=\s*["\']([^"\']+)["\']', re.IGNORECASE)

        # Pattern per selettori in JavaScript
        js_selector_patterns = [
            re.compile(r'document\.querySelector\(["\']([^"\']+)["\']\)'),
            re.compile(r'document\.querySelectorAll\(["\']([^"\']+)["\']\)'),
            re.compile(r'jQuery\(["\']([^"\']+)["\']\)'),
            re.compile(r'\$\(["\']([^"\']+)["\']\)'),
            re.compile(r'getElementsByClassName\(["\']([^"\']+)["\']\)'),
            re.compile(r'getElementById\(["\']([^"\']+)["\']\)'),
        ]

        for html_file in self.html_dir.rglob("*.html"):
            try:
                with open(html_file, 'r', encoding='utf-8') as f:
                    content = f.read()

                # Parse HTML con BeautifulSoup
                soup = BeautifulSoup(content, 'html.parser')

                # Rimuovi commenti HTML
                for comment in soup.find_all(string=lambda text: isinstance(text, Comment)):
                    comment.extract()

                # Estrai classi e ID da attributi HTML
                for element in soup.find_all():
                    # Classi CSS
                    if element.get('class'):
                        classes = element.get('class')
                        if isinstance(classes, list):
                            classes = ' '.join(classes)
                        for css_class in classes.split():
                            selectors.add(f".{css_class}")

                    # ID CSS
                    if element.get('id'):
                        selectors.add(f"#{element.get('id')}")

                    # Data attributes che potrebbero essere usati come selettori
                    for attr in element.attrs:
                        if attr.startswith('data-'):
                            selectors.add(f"[{attr}]")

                # Cerca selettori in JavaScript inline
                script_tags = soup.find_all('script')
                for script in script_tags:
                    if script.string:
                        for pattern in js_selector_patterns:
                            matches = pattern.findall(script.string)
                            for match in matches:
                                selectors.add(match)

                # Cerca selettori nel contenuto raw (per pattern complessi)
                class_matches = class_pattern.findall(content)
                for match in class_matches:
                    for css_class in match.split():
                        selectors.add(f".{css_class}")

                id_matches = id_pattern.findall(content)
                for match in id_matches:
                    selectors.add(f"#{match}")

                self.stats.files_processed += 1
                self.logger.info(f"Processed HTML: {html_file}")

            except Exception as e:
                self.logger.error(f"Errore durante l'elaborazione di {html_file}: {e}")

        self.stats.selectors_found = len(selectors)
        self.logger.info(f"Trovati {len(selectors)} selettori unici")
        return selectors

    def is_selector_used(self, selector: str) -> bool:
        """Verifica se un selettore CSS è utilizzato"""
        selector = selector.strip()

        # Selettori universali sempre mantenuti
        universal_selectors = ['*', 'html', 'body', ':root']
        if selector in universal_selectors:
            return True

        # Pseudo-elementi e pseudo-classi comuni
        if any(pseudo in selector for pseudo in [':hover', ':focus', ':active', ':visited',
                                                ':before', ':after', ':first-child', ':last-child']):
            base_selector = re.sub(r':[a-zA-Z-]+(\([^)]*\))?', '', selector)
            return base_selector in self.used_selectors or selector in self.used_selectors

        # Media query prefixes
        if selector.startswith('@'):
            return True

        # Verifica diretta
        if selector in self.used_selectors:
            return True

        # Verifica selettori composti (es: .class1.class2)
        if '.' in selector and selector.count('.') > 1:
            classes = [f".{cls}" for cls in selector.replace('.', ' ').split() if cls]
            return any(cls in self.used_selectors for cls in classes)

        # Verifica selettori discendenti (es: .parent .child)
        if ' ' in selector:
            parts = [part.strip() for part in selector.split()]
            return any(part in self.used_selectors for part in parts)

        return False

    def clean_css_file(self, css_file: Path) -> Tuple[str, int, int]:
        """Pulisce un singolo file CSS"""
        try:
            with open(css_file, 'r', encoding='utf-8') as f:
                css_content = f.read()

            original_size = len(css_content)
            self.stats.original_size += original_size

            # Parse CSS con cssutils per handling più robusto
            sheet = cssutils.parseString(css_content)
            cleaned_rules = []
            rules_kept = 0
            rules_removed = 0

            for rule in sheet:
                if rule.type == rule.STYLE_RULE:
                    # Analizza ogni selettore nella regola
                    selectors = [s.selectorText.strip() for s in rule.selectorList]
                    used_selectors = [s for s in selectors if self.is_selector_used(s)]

                    if used_selectors:
                        # Ricostruisci la regola solo con selettori utilizzati
                        rule.selectorText = ', '.join(used_selectors)
                        cleaned_rules.append(rule.cssText)
                        rules_kept += 1
                    else:
                        rules_removed += 1

                elif rule.type in [rule.MEDIA_RULE, rule.KEYFRAMES_RULE, rule.FONT_FACE_RULE]:
                    # Mantieni media queries, keyframes e font-face
                    cleaned_rules.append(rule.cssText)
                    rules_kept += 1

                elif rule.type == rule.IMPORT_RULE:
                    # Mantieni import rules
                    cleaned_rules.append(rule.cssText)
                    rules_kept += 1

            cleaned_css = '\n'.join(cleaned_rules)
            cleaned_size = len(cleaned_css)

            self.stats.cleaned_size += cleaned_size
            self.stats.rules_kept += rules_kept
            self.stats.rules_removed += rules_removed

            return cleaned_css, rules_kept, rules_removed

        except Exception as e:
            self.logger.error(f"Errore durante la pulizia del file CSS {css_file}: {e}")
            return "", 0, 0

    def process_all_css(self, backup: bool = True) -> CleaningStats:
        """Processa tutti i file CSS"""
        self.used_selectors = self.extract_selectors_from_html()

        if backup:
            self.create_backup()

        css_files = list(self.css_dir.rglob("*.css"))
        self.logger.info(f"Elaborazione di {len(css_files)} file CSS")

        for css_file in css_files:
            cleaned_css, kept, removed = self.clean_css_file(css_file)

            if cleaned_css:
                output_file = self.output_dir / css_file.relative_to(self.css_dir)
                output_file.parent.mkdir(parents=True, exist_ok=True)

                with open(output_file, 'w', encoding='utf-8') as f:
                    f.write(cleaned_css)

                self.logger.info(f"Cleaned {css_file}: {kept} kept, {removed} removed")

        return self.stats

    def create_backup(self):
        """Crea backup dei file CSS originali"""
        import shutil
        from datetime import datetime

        backup_dir = self.css_dir.parent / f"css_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
        backup_dir.mkdir(exist_ok=True)

        for css_file in self.css_dir.rglob("*.css"):
            relative_path = css_file.relative_to(self.css_dir)
            backup_file = backup_dir / relative_path
            backup_file.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(css_file, backup_file)

        self.logger.info(f"Backup created: {backup_dir}")

    def generate_report(self) -> str:
        """Genera report dettagliato della pulizia"""
        if self.stats.original_size == 0:
            return "Nessun file elaborato"

        reduction_percent = ((self.stats.original_size - self.stats.cleaned_size) /
                           self.stats.original_size) * 100

        report = f"""
Report pulizia CSS
==================
Files processed: {self.stats.files_processed} HTML, {len(list(self.css_dir.rglob('*.css')))} CSS
Selectors found: {self.stats.selectors_found}
Original size: {self.stats.original_size:,} bytes ({self.stats.original_size/1024:.1f} KB)
Cleaned size: {self.stats.cleaned_size:,} bytes ({self.stats.cleaned_size/1024:.1f} KB)
Size reduction: {self.stats.original_size - self.stats.cleaned_size:,} bytes ({reduction_percent:.1f}%)
Rules kept: {self.stats.rules_kept}
Rules removed: {self.stats.rules_removed}
Removal rate: {(self.stats.rules_removed/(self.stats.rules_kept + self.stats.rules_removed)*100):.1f}%
"""
        return report

# Esempio di utilizzo
if __name__ == "__main__":
    import sys

    if len(sys.argv) < 3:
        print("Uso: python css_cleaner.py <html_dir> <css_dir> [output_dir]")
        sys.exit(1)

    html_dir = sys.argv[1]
    css_dir = sys.argv[2]
    output_dir = sys.argv[3] if len(sys.argv) > 3 else None

    cleaner = CSSCleaner(html_dir, css_dir, output_dir)
    stats = cleaner.process_all_css()

    print(cleaner.generate_report())

Test e Validazione

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env python3
"""
Test suite per CSS Cleaner
"""

import pytest
import tempfile
import shutil
from pathlib import Path
from css_cleaner import CSSCleaner

class TestCSSCleaner:

    @pytest.fixture
    def temp_dirs(self):
        """Crea directory temporanee per i test"""
        temp_dir = Path(tempfile.mkdtemp())
        html_dir = temp_dir / "html"
        css_dir = temp_dir / "css"
        html_dir.mkdir()
        css_dir.mkdir()

        yield html_dir, css_dir

        shutil.rmtree(temp_dir)

    def test_basic_cleaning(self, temp_dirs):
        """Test pulizia base CSS"""
        html_dir, css_dir = temp_dirs

        # Crea file HTML di test
        html_content = '''
        <html>
        <body>
            <div class="used-class">Content</div>
            <p id="used-id">Paragraph</p>
        </body>
        </html>
        '''

        # Crea file CSS di test
        css_content = '''
        .used-class { color: red; }
        .unused-class { color: blue; }
        #used-id { font-size: 16px; }
        #unused-id { font-size: 14px; }
        '''

        (html_dir / "test.html").write_text(html_content)
        (css_dir / "test.css").write_text(css_content)

        # Esegui pulizia
        cleaner = CSSCleaner(str(html_dir), str(css_dir))
        stats = cleaner.process_all_css(backup=False)

        # Verifica risultati
        cleaned_css = (css_dir / "test.css").read_text()

        assert ".used-class" in cleaned_css
        assert "#used-id" in cleaned_css
        assert ".unused-class" not in cleaned_css
        assert "#unused-id" not in cleaned_css
        assert stats.rules_removed == 2
        assert stats.rules_kept == 2

    def test_javascript_selectors(self, temp_dirs):
        """Test riconoscimento selettori in JavaScript"""
        html_dir, css_dir = temp_dirs

        html_content = '''
        <html>
        <head>
            <script>
                document.querySelector('.js-class');
                $('#jquery-id').hide();
            </script>
        </head>
        <body></body>
        </html>
        '''

        css_content = '''
        .js-class { color: red; }
        #jquery-id { display: block; }
        .unused { color: blue; }
        '''

        (html_dir / "test.html").write_text(html_content)
        (css_dir / "test.css").write_text(css_content)

        cleaner = CSSCleaner(str(html_dir), str(css_dir))
        stats = cleaner.process_all_css(backup=False)

        cleaned_css = (css_dir / "test.css").read_text()

        assert ".js-class" in cleaned_css
        assert "#jquery-id" in cleaned_css
        assert ".unused" not in cleaned_css

    def test_pseudo_classes(self, temp_dirs):
        """Test gestione pseudo-classes"""
        html_dir, css_dir = temp_dirs

        html_content = '''
        <html>
        <body>
            <a class="link">Link</a>
        </body>
        </html>
        '''

        css_content = '''
        .link { color: blue; }
        .link:hover { color: red; }
        .unused:hover { color: green; }
        '''

        (html_dir / "test.html").write_text(html_content)
        (css_dir / "test.css").write_text(css_content)

        cleaner = CSSCleaner(str(html_dir), str(css_dir))
        stats = cleaner.process_all_css(backup=False)

        cleaned_css = (css_dir / "test.css").read_text()

        assert ".link" in cleaned_css
        assert ".link:hover" in cleaned_css
        assert ".unused:hover" not in cleaned_css

# Esegui test
if __name__ == "__main__":
    pytest.main([__file__])

Parser CSS Completo

Gestione Avanzata delle Regole CSS

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
#!/usr/bin/env python3
"""
Advanced CSS Parser - Gestione completa di tutte le regole CSS
"""

import re
import cssutils
from tinycss2 import parse_stylesheet, parse_declaration_list
from tinycss2.ast import QualifiedRule, AtRule, Declaration
from typing import Dict, List, Set, Tuple, Optional
import logging

class AdvancedCSSParser:
    """Parser CSS avanzato per analisi completa delle regole"""

    def __init__(self):
        self.media_queries: Dict[str, List] = {}
        self.keyframes: Dict[str, Dict] = {}
        self.font_faces: List[Dict] = []
        self.imports: List[str] = []
        self.variables: Dict[str, str] = {}
        self.supports_rules: Dict[str, List] = {}

        # Configurazione logging
        cssutils.log.setLevel(logging.CRITICAL)

    def parse_css_file(self, css_file_path: str) -> Dict:
        """Parsing completo di un file CSS"""

        try:
            with open(css_file_path, 'r', encoding='utf-8') as f:
                css_content = f.read()

            # Parse con tinycss2 per parsing moderno
            stylesheet = parse_stylesheet(css_content)

            # Parse con cssutils per compatibilità
            css_sheet = cssutils.parseString(css_content)

            result = {
                'selectors': self._extract_selectors(stylesheet),
                'media_queries': self._extract_media_queries(css_sheet),
                'keyframes': self._extract_keyframes(css_sheet),
                'font_faces': self._extract_font_faces(css_sheet),
                'imports': self._extract_imports(css_sheet),
                'variables': self._extract_css_variables(css_content),
                'supports': self._extract_supports_rules(css_sheet),
                'performance_metrics': self._calculate_performance_metrics(stylesheet),
                'file_stats': self._calculate_file_stats(css_content)
            }

            return result

        except Exception as e:
            logging.error(f"Errore durante il parsing del file CSS {css_file_path}: {e}")
            return {}

    def _extract_selectors(self, stylesheet) -> List[Dict]:
        """Estrae tutti i selettori con dettagli"""
        selectors = []

        for rule in stylesheet:
            if hasattr(rule, 'prelude') and hasattr(rule, 'content'):
                # Regola normale con selettore
                selector_text = ''.join([token.serialize() for token in rule.prelude])
                declarations = self._parse_declarations(rule.content)

                selectors.append({
                    'selector': selector_text.strip(),
                    'specificity': self._calculate_specificity(selector_text),
                    'declarations': declarations,
                    'complexity_score': self._calculate_complexity(selector_text),
                    'performance_impact': self._assess_performance_impact(selector_text)
                })

        return selectors

    def _calculate_specificity(self, selector: str) -> Tuple[int, int, int, int]:
        """Calcola specificità CSS (inline, ids, classes, elements)"""

        # Rimuovi pseudo-elements e pseudo-classes per il calcolo
        clean_selector = re.sub(r':+[\w-]+(\([^)]*\))?', '', selector)

        # Conta IDs
        ids = len(re.findall(r'#[\w-]+', clean_selector))

        # Conta classi, attributi, pseudo-classi
        classes = len(re.findall(r'\.[\w-]+', clean_selector))
        attributes = len(re.findall(r'\[[^\]]*\]', clean_selector))
        pseudo_classes = len(re.findall(r':[\w-]+(?!\()', selector))

        # Conta elementi e pseudo-elementi
        elements = len(re.findall(r'(?:^|[\s>+~])(?![.#:])[a-zA-Z][\w-]*', clean_selector))
        pseudo_elements = len(re.findall(r'::[\w-]+', selector))

        return (0, ids, classes + attributes + pseudo_classes, elements + pseudo_elements)

    def _calculate_complexity(self, selector: str) -> int:
        """Calcola score di complessità del selettore"""
        complexity = 0

        # Punti per lunghezza
        complexity += len(selector.split()) * 2

        # Punti per combinatori
        complexity += selector.count('>') * 3
        complexity += selector.count('+') * 3
        complexity += selector.count('~') * 3

        # Punti per pseudo-selettori complessi
        complexity += selector.count(':nth-child') * 5
        complexity += selector.count(':nth-of-type') * 5
        complexity += selector.count(':not(') * 4

        # Punti per attributi complessi
        complexity += len(re.findall(r'\[[^\]]*[*^$|~][^]]*\]', selector)) * 4

        return complexity

    def _assess_performance_impact(self, selector: str) -> str:
        """Valuta l'impatto performance del selettore"""

        # Selettori ad alto impatto
        if selector.startswith('*') or '*' in selector:
            return 'HIGH'

        if re.search(r'[^>+~\s][>+~]\s*\*', selector):
            return 'HIGH'

        # Selettori che finiscono con tag
        if re.search(r'\s+[a-zA-Z]+$', selector):
            return 'MEDIUM'

        # Selettori molto specifici
        specificity = self._calculate_specificity(selector)
        total_spec = sum(specificity)
        if total_spec > 100:
            return 'MEDIUM'

        return 'LOW'

    def _extract_media_queries(self, css_sheet) -> Dict:
        """Estrae e organizza media queries"""
        media_queries = {}

        for rule in css_sheet:
            if rule.type == rule.MEDIA_RULE:
                media_text = rule.media.mediaText
                rules_in_media = []

                for nested_rule in rule:
                    if hasattr(nested_rule, 'selectorText'):
                        rules_in_media.append({
                            'selector': nested_rule.selectorText,
                            'declarations': nested_rule.style.cssText
                        })

                media_queries[media_text] = rules_in_media

        return media_queries

    def _extract_keyframes(self, css_sheet) -> Dict:
        """Estrae definizioni keyframes"""
        keyframes = {}

        for rule in css_sheet:
            if rule.type == rule.KEYFRAMES_RULE:
                animation_name = rule.name
                keyframe_rules = {}

                for keyframe in rule:
                    keyframe_rules[keyframe.keyText] = keyframe.style.cssText

                keyframes[animation_name] = keyframe_rules

        return keyframes

    def _extract_css_variables(self, css_content: str) -> Dict:
        """Estrae variabili CSS custom properties"""
        variables = {}

        # Pattern per custom properties
        var_pattern = r'--([^:]+):\s*([^;]+);'
        matches = re.findall(var_pattern, css_content)

        for var_name, var_value in matches:
            variables[f"--{var_name.strip()}"] = var_value.strip()

        return variables

    def _calculate_performance_metrics(self, stylesheet) -> Dict:
        """Calcola metriche di performance"""

        total_rules = 0
        complex_selectors = 0
        universal_selectors = 0
        id_selectors = 0
        class_selectors = 0

        for rule in stylesheet:
            if hasattr(rule, 'prelude'):
                total_rules += 1
                selector_text = ''.join([token.serialize() for token in rule.prelude])

                complexity = self._calculate_complexity(selector_text)
                if complexity > 20:
                    complex_selectors += 1

                if '*' in selector_text:
                    universal_selectors += 1

                if '#' in selector_text:
                    id_selectors += 1

                if '.' in selector_text:
                    class_selectors += 1

        return {
            'total_rules': total_rules,
            'complex_selectors': complex_selectors,
            'universal_selectors': universal_selectors,
            'id_selectors': id_selectors,
            'class_selectors': class_selectors,
            'complexity_ratio': (complex_selectors / total_rules * 100) if total_rules > 0 else 0
        }

# Esempio di utilizzo avanzato
if __name__ == "__main__":
    parser = AdvancedCSSParser()

    # Analizza file CSS complesso
    css_analysis = parser.parse_css_file("complex_stylesheet.css")

    print("=== CSS ANALYSIS REPORT ===")
    print(f"Total rules: {css_analysis['performance_metrics']['total_rules']}")
    print(f"Complex selectors: {css_analysis['performance_metrics']['complex_selectors']}")
    print(f"Media queries: {len(css_analysis['media_queries'])}")
    print(f"CSS Variables: {len(css_analysis['variables'])}")
    print(f"Keyframes: {len(css_analysis['keyframes'])}")

Analisi HTML Dinamica

Scanner HTML Avanzato per Selettori Dinamici

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
#!/usr/bin/env python3
"""
Dynamic HTML Analyzer - Analisi avanzata per contenuto dinamico
"""

import re
import json
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from pathlib import Path
from typing import Set, List, Dict, Optional
import time

class DynamicHTMLAnalyzer:
    """Analizzatore HTML per contenuto generato dinamicamente"""

    def __init__(self, use_selenium: bool = False, headless: bool = True):
        self.use_selenium = use_selenium
        self.headless = headless
        self.driver = None
        self.dynamic_selectors: Set[str] = set()
        self.js_selectors: Set[str] = set()
        self.framework_selectors: Set[str] = set()

    def setup_selenium(self):
        """Configura Selenium WebDriver"""
        if not self.use_selenium:
            return

        chrome_options = Options()
        if self.headless:
            chrome_options.add_argument('--headless')
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        chrome_options.add_argument('--disable-gpu')

        try:
            self.driver = webdriver.Chrome(options=chrome_options)
            self.driver.set_page_load_timeout(30)
        except Exception as e:
            print(f"Avviso: impossibile inizializzare Selenium WebDriver: {e}")
            self.use_selenium = False

    def analyze_static_html(self, html_content: str) -> Set[str]:
        """Analizza HTML statico con BeautifulSoup"""
        selectors = set()

        soup = BeautifulSoup(html_content, 'html.parser')

        # Selettori da attributi HTML standard
        for element in soup.find_all():
            # Classi CSS
            if element.get('class'):
                classes = element.get('class')
                if isinstance(classes, list):
                    classes = ' '.join(classes)
                for css_class in classes.split():
                    selectors.add(f".{css_class}")

            # IDs
            if element.get('id'):
                selectors.add(f"#{element.get('id')}")

            # Data attributes (spesso usati da framework)
            for attr in element.attrs:
                if attr.startswith('data-'):
                    selectors.add(f"[{attr}]")
                    # Valore specifico per data attributes importanti
                    if attr in ['data-component', 'data-widget', 'data-module']:
                        selectors.add(f"[{attr}='{element.get(attr)}']")

            # Attributi framework-specific
            framework_attrs = ['ng-class', 'v-bind:class', ':class', 'class-name', 'className']
            for attr in framework_attrs:
                if element.get(attr):
                    # Parsing per framework attributes
                    self._parse_framework_classes(element.get(attr), selectors)

        return selectors

    def analyze_javascript_selectors(self, html_content: str) -> Set[str]:
        """Estrae selettori da codice JavaScript"""
        selectors = set()

        # Pattern per selettori in JavaScript
        js_patterns = [
            r'document\.querySelector\(["\']([^"\']+)["\']\)',
            r'document\.querySelectorAll\(["\']([^"\']+)["\']\)',
            r'document\.getElementById\(["\']([^"\']+)["\']\)',
            r'document\.getElementsByClassName\(["\']([^"\']+)["\']\)',
            r'jQuery\(["\']([^"\']+)["\']\)',
            r'\$\(["\']([^"\']+)["\']\)',
            r'\.addClass\(["\']([^"\']+)["\']\)',
            r'\.removeClass\(["\']([^"\']+)["\']\)',
            r'\.toggleClass\(["\']([^"\']+)["\']\)',
            r'\.hasClass\(["\']([^"\']+)["\']\)',
        ]

        for pattern in js_patterns:
            matches = re.findall(pattern, html_content, re.IGNORECASE)
            for match in matches:
                # Pulisci e standardizza il selettore
                cleaned_selector = self._clean_selector(match)
                if cleaned_selector:
                    selectors.add(cleaned_selector)

        # Pattern per classi in stringhe JavaScript
        class_patterns = [
            r'["\']([a-zA-Z][\w-]*(?:\s+[a-zA-Z][\w-]*)*)["\']',
        ]

        # Cerca in tag script
        soup = BeautifulSoup(html_content, 'html.parser')
        scripts = soup.find_all('script')

        for script in scripts:
            if script.string:
                # Analizza contenuto script per possibili classi CSS
                potential_classes = re.findall(r'["\']([a-z][\w-]+(?:-[a-z][\w-]*)*)["\']', script.string)
                for class_name in potential_classes:
                    # Filtro per classi che sembrano CSS (pattern comuni)
                    if self._looks_like_css_class(class_name):
                        selectors.add(f".{class_name}")

        return selectors

    def analyze_dynamic_content(self, url: str, wait_time: int = 5) -> Set[str]:
        """Analizza contenuto generato dinamicamente con Selenium"""
        if not self.use_selenium or not self.driver:
            return set()

        selectors = set()

        try:
            # Carica la pagina
            self.driver.get(url)

            # Attendi caricamento iniziale
            time.sleep(wait_time)

            # Attendi elementi dinamici comuni
            common_dynamic_selectors = [
                '[data-loaded="true"]',
                '.loaded',
                '.ready',
                '[aria-hidden="false"]'
            ]

            for selector in common_dynamic_selectors:
                try:
                    WebDriverWait(self.driver, 10).until(
                        EC.presence_of_element_located((By.CSS_SELECTOR, selector))
                    )
                except:
                    pass

            # Simula interazioni per trigger contenuto dinamico
            self._simulate_interactions()

            # Estrai selettori dal DOM finale
            final_html = self.driver.page_source
            selectors.update(self.analyze_static_html(final_html))
            selectors.update(self.analyze_javascript_selectors(final_html))

            # Analizza stili inline aggiunti dinamicamente
            elements_with_style = self.driver.find_elements(By.XPATH, "//*[@style]")
            for element in elements_with_style:
                class_names = element.get_attribute('class')
                if class_names:
                    for class_name in class_names.split():
                        selectors.add(f".{class_name}")

        except Exception as e:
            print(f"Errore durante l'analisi del contenuto dinamico: {e}")

        return selectors

    def _simulate_interactions(self):
        """Simula interazioni utente per trigger contenuto dinamico"""
        try:
            # Click su elementi interattivi comuni
            interactive_selectors = [
                'button', '[role="button"]', '.btn', '.button',
                '.menu-toggle', '.dropdown-toggle', '.tab',
                '[data-toggle]', '[data-trigger]'
            ]

            for selector in interactive_selectors:
                try:
                    elements = self.driver.find_elements(By.CSS_SELECTOR, selector)
                    for element in elements[:3]:  # Limita a primi 3 elementi
                        if element.is_displayed() and element.is_enabled():
                            self.driver.execute_script("arguments[0].click();", element)
                            time.sleep(1)  # Attendi animazioni
                except:
                    continue

            # Scroll per trigger lazy loading
            self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            time.sleep(2)
            self.driver.execute_script("window.scrollTo(0, 0);")

        except Exception as e:
            print(f"Errore durante la simulazione delle interazioni: {e}")

    def _parse_framework_classes(self, attr_value: str, selectors: Set[str]):
        """Parsing per attributi classe di framework JavaScript"""

        # Vue.js :class binding
        if attr_value.startswith('{') or attr_value.startswith('['):
            try:
                # Estrai nomi di classi da oggetti/array JavaScript
                class_names = re.findall(r'["\']([a-zA-Z][\w-]+)["\']', attr_value)
                for class_name in class_names:
                    selectors.add(f".{class_name}")
            except:
                pass

        # Angular [ngClass]
        elif 'ngClass' in attr_value or any(op in attr_value for op in ['?', ':']):
            class_names = re.findall(r'["\']([a-zA-Z][\w-]+)["\']', attr_value)
            for class_name in class_names:
                selectors.add(f".{class_name}")

        # React className con espressioni
        else:
            # Estrai classi da stringhe template o concatenazioni
            class_names = re.findall(r'["\']([a-zA-Z][\w-]+(?:\s+[a-zA-Z][\w-]+)*)["\']', attr_value)
            for class_string in class_names:
                for class_name in class_string.split():
                    selectors.add(f".{class_name}")

    def _clean_selector(self, selector: str) -> Optional[str]:
        """Pulisce e valida selettori estratti da JavaScript"""
        selector = selector.strip()

        # Rimuovi spazi extra e caratteri non validi
        selector = re.sub(r'\s+', ' ', selector)

        # Valida che sia un selettore CSS valido
        if not selector:
            return None

        # Se è solo una classe, aggiungi il punto
        if re.match(r'^[a-zA-Z][\w-]*$', selector):
            return f".{selector}"

        # Se è solo un ID, aggiungi il cancelletto
        if re.match(r'^[a-zA-Z][\w-]*$', selector) and selector not in ['body', 'html', 'div', 'span']:
            return f"#{selector}"

        return selector

    def _looks_like_css_class(self, class_name: str) -> bool:
        """Determina se una stringa sembra essere una classe CSS"""

        # Pattern comuni per classi CSS
        css_patterns = [
            r'^[a-z]+(-[a-z]+)*$',  # kebab-case
            r'^[a-zA-Z]+[A-Z][a-zA-Z]*$',  # camelCase
            r'^[a-z]+(_[a-z]+)*$',  # snake_case
            r'^(btn|nav|menu|header|footer|sidebar|content|main|wrapper|container)',  # Prefissi comuni
            r'(active|disabled|hidden|visible|primary|secondary|success|danger|warning|info)$'  # Stati comuni
        ]

        return any(re.match(pattern, class_name) for pattern in css_patterns)

    def close(self):
        """Chiude il WebDriver"""
        if self.driver:
            self.driver.quit()

# Esempio di utilizzo completo
if __name__ == "__main__":
    # Analisi statica
    static_analyzer = DynamicHTMLAnalyzer(use_selenium=False)

    html_files = Path("./html").glob("*.html")
    all_selectors = set()

    for html_file in html_files:
        with open(html_file, 'r', encoding='utf-8') as f:
            content = f.read()

        static_selectors = static_analyzer.analyze_static_html(content)
        js_selectors = static_analyzer.analyze_javascript_selectors(content)

        all_selectors.update(static_selectors)
        all_selectors.update(js_selectors)

        print(f"File: {html_file}")
        print(f"  Static selectors: {len(static_selectors)}")
        print(f"  JS selectors: {len(js_selectors)}")

    # Analisi dinamica (opzionale)
    dynamic_analyzer = DynamicHTMLAnalyzer(use_selenium=True)
    dynamic_analyzer.setup_selenium()

    if dynamic_analyzer.use_selenium:
        dynamic_selectors = dynamic_analyzer.analyze_dynamic_content("http://localhost:3000")
        all_selectors.update(dynamic_selectors)
        print(f"Dynamic selectors: {len(dynamic_selectors)}")

    dynamic_analyzer.close()

    print(f"\nTotal unique selectors found: {len(all_selectors)}")
    print("Selectors:", sorted(all_selectors))

Gestione Media Queries

Processore Avanzato Media Queries

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
#!/usr/bin/env python3
"""
Media Query Processor - Gestione intelligente delle media queries
"""

import re
from typing import Dict, List, Set, Tuple
from dataclasses import dataclass

@dataclass
class MediaQueryRule:
    """Rappresenta una regola dentro una media query"""
    selector: str
    declarations: str
    usage_context: str = ""

@dataclass
class MediaQuery:
    """Rappresenta una media query completa"""
    query_text: str
    rules: List[MediaQueryRule]
    breakpoint: int = 0
    device_type: str = ""
    is_responsive: bool = True

class MediaQueryProcessor:
    """Processore avanzato per media queries"""

    def __init__(self):
        self.common_breakpoints = {
            320: "mobile-small",
            375: "mobile",
            768: "tablet",
            1024: "desktop",
            1200: "desktop-large",
            1440: "desktop-xl"
        }

        self.processed_queries: Dict[str, MediaQuery] = {}

    def process_media_queries(self, css_content: str, used_selectors: Set[str]) -> Dict[str, MediaQuery]:
        """Processa tutte le media queries nel CSS"""

        # Pattern per estrarre media queries
        media_pattern = r'@media\s+([^{]+)\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}'

        matches = re.finditer(media_pattern, css_content, re.DOTALL | re.IGNORECASE)

        for match in matches:
            query_text = match.group(1).strip()
            query_content = match.group(2).strip()

            # Analizza la query
            media_query = self._parse_media_query(query_text, query_content)

            # Filtra regole basate sui selettori utilizzati
            filtered_rules = self._filter_media_rules(media_query.rules, used_selectors)
            media_query.rules = filtered_rules

            # Salva solo se ha regole utilizzate
            if filtered_rules:
                self.processed_queries[query_text] = media_query

        return self.processed_queries

    def _parse_media_query(self, query_text: str, content: str) -> MediaQuery:
        """Parsing di una singola media query"""

        # Determina breakpoint e tipo device
        breakpoint = self._extract_breakpoint(query_text)
        device_type = self._determine_device_type(query_text)
        is_responsive = self._is_responsive_query(query_text)

        # Estrai regole CSS dentro la media query
        rules = self._extract_rules_from_content(content)

        return MediaQuery(
            query_text=query_text,
            rules=rules,
            breakpoint=breakpoint,
            device_type=device_type,
            is_responsive=is_responsive
        )

    def _extract_breakpoint(self, query_text: str) -> int:
        """Estrae il breakpoint principale dalla media query"""

        # Pattern per width/height values
        width_pattern = r'(?:max-width|min-width):\s*(\d+)px'
        matches = re.findall(width_pattern, query_text)

        if matches:
            return int(matches[0])

        # Pattern per em/rem values
        em_pattern = r'(?:max-width|min-width):\s*(\d+(?:\.\d+)?)em'
        em_matches = re.findall(em_pattern, query_text)

        if em_matches:
            # Convert em to px (assuming 16px = 1em)
            return int(float(em_matches[0]) * 16)

        return 0

    def _determine_device_type(self, query_text: str) -> str:
        """Determina il tipo di device dalla media query"""

        query_lower = query_text.lower()

        if 'print' in query_lower:
            return 'print'
        elif 'screen' in query_lower:
            if 'max-width' in query_lower:
                breakpoint = self._extract_breakpoint(query_text)
                if breakpoint <= 767:
                    return 'mobile'
                elif breakpoint <= 1023:
                    return 'tablet'
                else:
                    return 'desktop'
            return 'screen'
        elif 'handheld' in query_lower:
            return 'mobile'
        else:
            return 'all'

    def _is_responsive_query(self, query_text: str) -> bool:
        """Determina se è una media query responsive"""

        responsive_keywords = ['width', 'height', 'orientation', 'resolution', 'aspect-ratio']
        return any(keyword in query_text.lower() for keyword in responsive_keywords)

    def _extract_rules_from_content(self, content: str) -> List[MediaQueryRule]:
        """Estrae regole CSS dal contenuto della media query"""

        rules = []

        # Pattern per regole CSS
        rule_pattern = r'([^{}]+)\s*\{([^{}]+)\}'
        matches = re.finditer(rule_pattern, content)

        for match in matches:
            selector = match.group(1).strip()
            declarations = match.group(2).strip()

            # Pulisci il selettore
            selector = re.sub(r'\s+', ' ', selector)

            rules.append(MediaQueryRule(
                selector=selector,
                declarations=declarations,
                usage_context=self._determine_usage_context(declarations)
            ))

        return rules

    def _determine_usage_context(self, declarations: str) -> str:
        """Determina il contesto d'uso delle dichiarazioni CSS"""

        declarations_lower = declarations.lower()

        if any(prop in declarations_lower for prop in ['display: none', 'visibility: hidden']):
            return 'hiding'
        elif any(prop in declarations_lower for prop in ['display: block', 'display: flex', 'display: grid']):
            return 'showing'
        elif any(prop in declarations_lower for prop in ['font-size', 'line-height', 'letter-spacing']):
            return 'typography'
        elif any(prop in declarations_lower for prop in ['width', 'height', 'padding', 'margin']):
            return 'layout'
        elif any(prop in declarations_lower for prop in ['background', 'color', 'border']):
            return 'styling'
        else:
            return 'general'

    def _filter_media_rules(self, rules: List[MediaQueryRule], used_selectors: Set[str]) -> List[MediaQueryRule]:
        """Filtra regole basate sui selettori utilizzati"""

        filtered_rules = []

        for rule in rules:
            # Check se almeno uno dei selettori nella regola è utilizzato
            rule_selectors = [s.strip() for s in rule.selector.split(',')]

            used_rule_selectors = []
            for selector in rule_selectors:
                if self._is_selector_used(selector, used_selectors):
                    used_rule_selectors.append(selector)

            # Se almeno un selettore è utilizzato, mantieni la regola
            if used_rule_selectors:
                # Aggiorna la regola con solo i selettori utilizzati
                rule.selector = ', '.join(used_rule_selectors)
                filtered_rules.append(rule)

        return filtered_rules

    def _is_selector_used(self, selector: str, used_selectors: Set[str]) -> bool:
        """Verifica se un selettore è utilizzato"""

        # Check diretto
        if selector in used_selectors:
            return True

        # Check per selettori composti
        if ' ' in selector:
            parts = selector.split()
            return any(part in used_selectors for part in parts)

        # Check per pseudo-selettori
        if ':' in selector:
            base_selector = selector.split(':')[0]
            return base_selector in used_selectors

        return False

    def generate_optimized_media_queries(self) -> str:
        """Genera CSS ottimizzato per le media queries"""

        if not self.processed_queries:
            return ""

        css_output = []

        # Ordina media queries per breakpoint
        sorted_queries = sorted(
            self.processed_queries.items(),
            key=lambda x: x[1].breakpoint
        )

        for query_text, media_query in sorted_queries:
            css_output.append(f"@media {query_text} {{")

            for rule in media_query.rules:
                css_output.append(f"  {rule.selector} {{")
                css_output.append(f"    {rule.declarations}")
                css_output.append(f"  }}")

            css_output.append("}")
            css_output.append("")  # Riga vuota tra media queries

        return '\n'.join(css_output)

    def get_media_query_report(self) -> Dict:
        """Genera report dettagliato delle media queries"""

        total_queries = len(self.processed_queries)
        responsive_queries = sum(1 for mq in self.processed_queries.values() if mq.is_responsive)

        device_breakdown = {}
        for mq in self.processed_queries.values():
            device_type = mq.device_type
            if device_type not in device_breakdown:
                device_breakdown[device_type] = 0
            device_breakdown[device_type] += 1

        breakpoint_usage = {}
        for mq in self.processed_queries.values():
            if mq.breakpoint > 0:
                # Trova il breakpoint più vicino
                closest_bp = min(self.common_breakpoints.keys(),
                               key=lambda x: abs(x - mq.breakpoint))
                bp_name = self.common_breakpoints[closest_bp]
                if bp_name not in breakpoint_usage:
                    breakpoint_usage[bp_name] = 0
                breakpoint_usage[bp_name] += 1

        return {
            'total_media_queries': total_queries,
            'responsive_queries': responsive_queries,
            'device_breakdown': device_breakdown,
            'breakpoint_usage': breakpoint_usage,
            'coverage_percentage': (responsive_queries / total_queries * 100) if total_queries > 0 else 0
        }

Backup e ripristino

Sistema completo di backup e ripristino

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#!/usr/bin/env python3
"""
Sistema di backup e ripristino CSS - gestione completa
"""

import os
import shutil
import json
import hashlib
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional
import zipfile
import tempfile

class CSSBackupManager:
    """Gestione completa backup e recovery per file CSS"""

    def __init__(self, backup_dir: str = "./css_backups"):
        self.backup_dir = Path(backup_dir)
        self.backup_dir.mkdir(exist_ok=True)
        self.metadata_file = self.backup_dir / "backup_metadata.json"
        self.metadata = self._load_metadata()

    def create_backup(self, css_files: List[str], backup_name: Optional[str] = None) -> str:
        """Crea backup completo dei file CSS"""

        if not backup_name:
            backup_name = f"css_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"

        backup_path = self.backup_dir / backup_name
        backup_path.mkdir(exist_ok=True)

        backup_info = {
            'name': backup_name,
            'created_at': datetime.now().isoformat(),
            'files': {},
            'total_size': 0,
            'file_count': 0
        }

        for css_file in css_files:
            css_path = Path(css_file)
            if not css_path.exists():
                continue

            # Calcola hash per verifica integrità
            file_hash = self._calculate_file_hash(css_path)
            file_size = css_path.stat().st_size

            # Copia file nel backup
            backup_file_path = backup_path / css_path.name
            shutil.copy2(css_path, backup_file_path)

            # Salva metadati file
            backup_info['files'][str(css_path)] = {
                'original_path': str(css_path),
                'backup_path': str(backup_file_path),
                'size': file_size,
                'hash': file_hash,
                'modified_at': datetime.fromtimestamp(css_path.stat().st_mtime).isoformat()
            }

            backup_info['total_size'] += file_size
            backup_info['file_count'] += 1

        # Salva metadati backup
        metadata_file = backup_path / "backup_info.json"
        with open(metadata_file, 'w', encoding='utf-8') as f:
            json.dump(backup_info, f, indent=2, ensure_ascii=False)

        # Aggiorna metadata globali
        self.metadata[backup_name] = backup_info
        self._save_metadata()

        # Crea archivio ZIP
        zip_path = self.backup_dir / f"{backup_name}.zip"
        self._create_zip_archive(backup_path, zip_path)

        print(f"Backup creato: {backup_name}")
        print(f"File processati: {backup_info['file_count']}")
        print(f"Dimensione totale: {self._format_size(backup_info['total_size'])}")
        print(f"Archivio: {zip_path}")

        return backup_name

    def restore_backup(self, backup_name: str, restore_path: Optional[str] = None, verify_integrity: bool = True) -> bool:
        """Ripristina backup in una directory specifica"""

        if backup_name not in self.metadata:
            print(f"Backup non trovato: {backup_name}")
            return False

        backup_info = self.metadata[backup_name]

        # Determina path di ripristino
        if restore_path:
            restore_dir = Path(restore_path)
        else:
            restore_dir = Path(f"./restored_{backup_name}")

        restore_dir.mkdir(exist_ok=True)

        print(f"Ripristino backup: {backup_name}")
        print(f"Destinazione: {restore_dir}")

        # Estrai da archivio se necessario
        zip_path = self.backup_dir / f"{backup_name}.zip"
        backup_dir = self.backup_dir / backup_name

        if zip_path.exists() and not backup_dir.exists():
            self._extract_zip_archive(zip_path, backup_dir.parent)

        # Verifica esistenza backup
        if not backup_dir.exists():
            print(f"Directory backup non trovata: {backup_dir}")
            return False

        restored_files = 0
        failed_files = []

        for original_path, file_info in backup_info['files'].items():
            backup_file_path = Path(file_info['backup_path'])

            if not backup_file_path.exists():
                failed_files.append(f"File backup non trovato: {backup_file_path}")
                continue

            # Verifica integrità se richiesta
            if verify_integrity:
                current_hash = self._calculate_file_hash(backup_file_path)
                if current_hash != file_info['hash']:
                    failed_files.append(f"Hash mismatch per: {backup_file_path}")
                    continue

            # Ripristina file
            try:
                restore_file_path = restore_dir / backup_file_path.name
                shutil.copy2(backup_file_path, restore_file_path)
                restored_files += 1
                print(f"Ripristinato: {restore_file_path}")
            except Exception as e:
                failed_files.append(f"Errore ripristino {backup_file_path}: {e}")

        # Report risultati
        print(f"\\nRipristino completato:")
        print(f"File ripristinati: {restored_files}")
        print(f"Errori: {len(failed_files)}")

        if failed_files:
            print("\\nErrori riscontrati:")
            for error in failed_files:
                print(f"  - {error}")

        return len(failed_files) == 0

    def list_backups(self) -> None:
        """Elenca tutti i backup disponibili"""

        if not self.metadata:
            print("Nessun backup trovato")
            return

        print("Backup disponibili:")
        print("-" * 80)

        for backup_name, backup_info in self.metadata.items():
            created_at = datetime.fromisoformat(backup_info['created_at'])

            print(f"Nome: {backup_name}")
            print(f"Creato: {created_at.strftime('%Y-%m-%d %H:%M:%S')}")
            print(f"File: {backup_info['file_count']}")
            print(f"Dimensione: {self._format_size(backup_info['total_size'])}")
            print("-" * 40)

    def delete_backup(self, backup_name: str) -> bool:
        """Elimina un backup specifico"""

        if backup_name not in self.metadata:
            print(f"Backup non trovato: {backup_name}")
            return False

        # Rimuovi directory backup
        backup_dir = self.backup_dir / backup_name
        if backup_dir.exists():
            shutil.rmtree(backup_dir)

        # Rimuovi archivio ZIP
        zip_path = self.backup_dir / f"{backup_name}.zip"
        if zip_path.exists():
            zip_path.unlink()

        # Rimuovi da metadata
        del self.metadata[backup_name]
        self._save_metadata()

        print(f"Backup eliminato: {backup_name}")
        return True

    def cleanup_old_backups(self, keep_days: int = 30, keep_count: int = 10) -> None:
        """Pulizia automatica backup vecchi"""

        cutoff_date = datetime.now().timestamp() - (keep_days * 24 * 60 * 60)

        # Ordina backup per data di creazione
        backup_list = []
        for name, info in self.metadata.items():
            created_at = datetime.fromisoformat(info['created_at'])
            backup_list.append((name, created_at.timestamp()))

        backup_list.sort(key=lambda x: x[1], reverse=True)  # Più recenti primi

        deleted_count = 0

        # Mantieni almeno keep_count backup più recenti
        backups_to_check = backup_list[keep_count:]

        for backup_name, timestamp in backups_to_check:
            if timestamp < cutoff_date:
                self.delete_backup(backup_name)
                deleted_count += 1

        print(f"Pulizia completata: {deleted_count} backup eliminati")

    def _calculate_file_hash(self, file_path: Path) -> str:
        """Calcola hash SHA256 di un file"""

        hash_sha256 = hashlib.sha256()
        with open(file_path, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hash_sha256.update(chunk)
        return hash_sha256.hexdigest()

    def _create_zip_archive(self, source_dir: Path, zip_path: Path) -> None:
        """Crea archivio ZIP del backup"""

        with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
            for file_path in source_dir.rglob('*'):
                if file_path.is_file():
                    arcname = file_path.relative_to(source_dir)
                    zipf.write(file_path, arcname)

    def _extract_zip_archive(self, zip_path: Path, extract_dir: Path) -> None:
        """Estrae archivio ZIP"""

        with zipfile.ZipFile(zip_path, 'r') as zipf:
            zipf.extractall(extract_dir)

    def _load_metadata(self) -> Dict:
        """Carica metadata backup"""

        if self.metadata_file.exists():
            try:
                with open(self.metadata_file, 'r', encoding='utf-8') as f:
                    return json.load(f)
            except Exception as e:
                print(f"Errore caricamento metadata: {e}")

        return {}

    def _save_metadata(self) -> None:
        """Salva metadata backup"""

        try:
            with open(self.metadata_file, 'w', encoding='utf-8') as f:
                json.dump(self.metadata, f, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"Errore salvataggio metadata: {e}")

    def _format_size(self, size_bytes: int) -> str:
        """Formatta dimensione file in formato leggibile"""

        for unit in ['B', 'KB', 'MB', 'GB']:
            if size_bytes < 1024.0:
                return f"{size_bytes:.1f} {unit}"
            size_bytes /= 1024.0
        return f"{size_bytes:.1f} TB"

Logging e monitoraggio

Sistema Avanzato di Logging e Monitoraggio

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
#!/usr/bin/env python3
"""
Sistema di logging e monitoraggio per CSS Cleaner
"""

import logging
from logging.handlers import RotatingFileHandler, SMTPHandler
import json
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional
import threading
import time
from dataclasses import dataclass, asdict
from enum import Enum

class LogLevel(Enum):
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"

@dataclass
class CleaningMetrics:
    """Metriche per operazione di pulizia"""
    operation_id: str
    start_time: datetime
    end_time: Optional[datetime] = None
    files_processed: int = 0
    selectors_found: int = 0
    rules_removed: int = 0
    rules_kept: int = 0
    size_before: int = 0
    size_after: int = 0
    errors_count: int = 0
    warnings_count: int = 0

class CSSCleanerLogger:
    """Sistema di logging avanzato per CSS Cleaner"""

    def __init__(self, log_dir: str = "./logs", app_name: str = "css_cleaner"):
        self.log_dir = Path(log_dir)
        self.log_dir.mkdir(exist_ok=True)
        self.app_name = app_name

        # File di log
        self.main_log_file = self.log_dir / f"{app_name}.log"
        self.error_log_file = self.log_dir / f"{app_name}_errors.log"
        self.metrics_file = self.log_dir / f"{app_name}_metrics.json"

        # Configurazione logger
        self.logger = logging.getLogger(app_name)
        self.logger.setLevel(logging.DEBUG)

        # Rimuovi handler esistenti
        for handler in self.logger.handlers[:]:
            self.logger.removeHandler(handler)

        self._setup_handlers()

        # Storage metriche
        self.metrics_data: List[CleaningMetrics] = []
        self._load_metrics()

        # Thread di monitoraggio
        self._monitoring = True
        self._monitor_thread = None

    def _setup_handlers(self):
        """Configura handler per il logging"""

        # Formatter
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
        )

        # Console Handler
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.INFO)
        console_handler.setFormatter(formatter)
        self.logger.addHandler(console_handler)

        # File Handler (tutte le informazioni)
        file_handler = RotatingFileHandler(
            self.main_log_file,
            maxBytes=10*1024*1024,  # 10MB
            backupCount=5
        )
        file_handler.setLevel(logging.DEBUG)
        file_handler.setFormatter(formatter)
        self.logger.addHandler(file_handler)

        # Error File Handler (solo errori)
        error_handler = RotatingFileHandler(
            self.error_log_file,
            maxBytes=5*1024*1024,  # 5MB
            backupCount=3
        )
        error_handler.setLevel(logging.ERROR)
        error_handler.setFormatter(formatter)
        self.logger.addHandler(error_handler)

    def setup_email_alerts(self, smtp_server: str, smtp_port: int,
                          username: str, password: str,
                          from_email: str, to_emails: List[str]):
        """Configura alerting via email per errori critici"""

        smtp_handler = SMTPHandler(
            mailhost=(smtp_server, smtp_port),
            fromaddr=from_email,
            toaddrs=to_emails,
            subject=f'Errore critico CSS Cleaner - {self.app_name}',
            credentials=(username, password),
            secure=()
        )
        smtp_handler.setLevel(logging.CRITICAL)

        smtp_formatter = logging.Formatter(
            '''
            Report errore critico CSS Cleaner

            Time: %(asctime)s
            Logger: %(name)s
            Level: %(levelname)s
            File: %(filename)s:%(lineno)d

            Message:
            %(message)s

            Please check the logs for more details.
            '''
        )
        smtp_handler.setFormatter(smtp_formatter)
        self.logger.addHandler(smtp_handler)

    def start_operation(self, operation_name: str = "css_cleaning") -> str:
        """Inizia tracking di una nuova operazione"""

        operation_id = f"{operation_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"

        metrics = CleaningMetrics(
            operation_id=operation_id,
            start_time=datetime.now()
        )

        self.metrics_data.append(metrics)

        self.logger.info(f"Started operation: {operation_id}")
        return operation_id

    def end_operation(self, operation_id: str):
        """Termina tracking operazione"""

        for metrics in self.metrics_data:
            if metrics.operation_id == operation_id:
                metrics.end_time = datetime.now()

                duration = metrics.end_time - metrics.start_time
                size_reduction = metrics.size_before - metrics.size_after
                reduction_percent = (size_reduction / metrics.size_before * 100) if metrics.size_before > 0 else 0

                self.logger.info(f"Operazione completata: {operation_id}")
                self.logger.info(f"Duration: {duration}")
                self.logger.info(f"Files processed: {metrics.files_processed}")
                self.logger.info(f"Selectors found: {metrics.selectors_found}")
                self.logger.info(f"Rules removed: {metrics.rules_removed}")
                self.logger.info(f"Rules kept: {metrics.rules_kept}")
                self.logger.info(f"Size reduction: {size_reduction} bytes ({reduction_percent:.1f}%)")

                if metrics.errors_count > 0:
                    self.logger.warning(f"Errori rilevati: {metrics.errors_count}")
                if metrics.warnings_count > 0:
                    self.logger.warning(f"Avvisi: {metrics.warnings_count}")

                self._save_metrics()
                break

    def update_metrics(self, operation_id: str, **kwargs):
        """Aggiorna metriche operazione corrente"""

        for metrics in self.metrics_data:
            if metrics.operation_id == operation_id:
                for key, value in kwargs.items():
                    if hasattr(metrics, key):
                        setattr(metrics, key, value)
                break

    def increment_counter(self, operation_id: str, counter_name: str, increment: int = 1):
        """Incrementa contatore specifico"""

        for metrics in self.metrics_data:
            if metrics.operation_id == operation_id:
                if hasattr(metrics, counter_name):
                    current_value = getattr(metrics, counter_name)
                    setattr(metrics, counter_name, current_value + increment)
                break

    def log_file_processed(self, operation_id: str, file_path: str,
                          rules_removed: int, rules_kept: int,
                          size_before: int, size_after: int):
        """Log elaborazione singolo file"""

        reduction = size_before - size_after
        reduction_percent = (reduction / size_before * 100) if size_before > 0 else 0

        self.logger.info(f"Processed file: {file_path}")
        self.logger.debug(f"  Rules removed: {rules_removed}")
        self.logger.debug(f"  Rules kept: {rules_kept}")
        self.logger.debug(f"  Size: {size_before} -> {size_after} bytes ({reduction_percent:.1f}% reduction)")

        # Aggiorna metriche
        self.increment_counter(operation_id, 'files_processed')
        self.increment_counter(operation_id, 'rules_removed', rules_removed)
        self.increment_counter(operation_id, 'rules_kept', rules_kept)

        for metrics in self.metrics_data:
            if metrics.operation_id == operation_id:
                metrics.size_before += size_before
                metrics.size_after += size_after
                break

    def log_error(self, operation_id: str, error_msg: str, file_path: str = None):
        """Log errore con context"""

        if file_path:
            self.logger.error(f"Errore durante l'elaborazione di {file_path}: {error_msg}")
        else:
            self.logger.error(f"Errore: {error_msg}")

        self.increment_counter(operation_id, 'errors_count')

    def log_warning(self, operation_id: str, warning_msg: str, file_path: str = None):
        """Log warning con context"""

        if file_path:
            self.logger.warning(f"Avviso per {file_path}: {warning_msg}")
        else:
            self.logger.warning(f"Avviso: {warning_msg}")

        self.increment_counter(operation_id, 'warnings_count')

    def get_operation_metrics(self, operation_id: str) -> Optional[CleaningMetrics]:
        """Ottieni metriche per operazione specifica"""

        for metrics in self.metrics_data:
            if metrics.operation_id == operation_id:
                return metrics
        return None

    def get_performance_report(self, days: int = 7) -> Dict:
        """Genera report performance per periodo specificato"""

        cutoff_date = datetime.now() - timedelta(days=days)
        recent_operations = [
            m for m in self.metrics_data
            if m.start_time >= cutoff_date and m.end_time is not None
        ]

        if not recent_operations:
            return {"message": "Nessuna operazione trovata nel periodo indicato"}

        # Calcola statistiche aggregate
        total_operations = len(recent_operations)
        total_files = sum(m.files_processed for m in recent_operations)
        total_size_before = sum(m.size_before for m in recent_operations)
        total_size_after = sum(m.size_after for m in recent_operations)
        total_reduction = total_size_before - total_size_after
        avg_reduction = (total_reduction / total_size_before * 100) if total_size_before > 0 else 0

        # Durata operazioni
        durations = [(m.end_time - m.start_time).total_seconds() for m in recent_operations]
        avg_duration = sum(durations) / len(durations) if durations else 0

        # Tasso di successo
        operations_with_errors = sum(1 for m in recent_operations if m.errors_count > 0)
        success_rate = ((total_operations - operations_with_errors) / total_operations * 100) if total_operations > 0 else 0

        return {
            'period_days': days,
            'total_operations': total_operations,
            'total_files_processed': total_files,
            'total_size_reduction_bytes': total_reduction,
            'average_size_reduction_percent': avg_reduction,
            'average_operation_duration_seconds': avg_duration,
            'success_rate_percent': success_rate,
            'operations_with_errors': operations_with_errors
        }

    def start_monitoring(self, check_interval: int = 300):
        """Avvia monitoring continuo (5 minuti di default)"""

        def monitor_loop():
            while self._monitoring:
                try:
                    # Log statistiche sistema
                    self._log_system_stats()

                    # Cleanup log vecchi
                    self._cleanup_old_logs()

                    time.sleep(check_interval)
                except Exception as e:
                    self.logger.error(f"Errore di monitoraggio: {e}")
                    time.sleep(60)  # Wait 1 minute before retry

        if not self._monitor_thread or not self._monitor_thread.is_alive():
            self._monitor_thread = threading.Thread(target=monitor_loop, daemon=True)
            self._monitor_thread.start()
            self.logger.info("Monitoraggio avviato")

    def stop_monitoring(self):
        """Ferma monitoring"""

        self._monitoring = False
        if self._monitor_thread:
            self._monitor_thread.join(timeout=10)
        self.logger.info("Monitoraggio arrestato")

    def _log_system_stats(self):
        """Log statistiche sistema"""

        log_dir_size = sum(f.stat().st_size for f in self.log_dir.rglob('*') if f.is_file())
        recent_operations = len([
            m for m in self.metrics_data
            if m.start_time >= datetime.now() - timedelta(hours=24)
        ])

        self.logger.info(f"Statistiche sistema - Dimensione log: {log_dir_size} byte, Operazioni recenti (24h): {recent_operations}")

    def _cleanup_old_logs(self, keep_days: int = 30):
        """Pulisci log vecchi"""

        cutoff_date = datetime.now() - timedelta(days=keep_days)

        # Rimuovi metriche vecchie
        old_count = len(self.metrics_data)
        self.metrics_data = [
            m for m in self.metrics_data
            if m.start_time >= cutoff_date
        ]

        if len(self.metrics_data) < old_count:
            removed = old_count - len(self.metrics_data)
            self.logger.info(f"Cleaned up {removed} old metrics entries")
            self._save_metrics()

    def _save_metrics(self):
        """Salva metriche su file"""

        try:
            metrics_data = [asdict(m) for m in self.metrics_data]

            # Converti datetime in string per JSON
            for metrics in metrics_data:
                metrics['start_time'] = metrics['start_time'].isoformat()
                if metrics['end_time']:
                    metrics['end_time'] = metrics['end_time'].isoformat()

            with open(self.metrics_file, 'w', encoding='utf-8') as f:
                json.dump(metrics_data, f, indent=2, ensure_ascii=False)

        except Exception as e:
            self.logger.error(f"Errore durante il salvataggio delle metriche: {e}")

    def _load_metrics(self):
        """Carica metriche da file"""

        if not self.metrics_file.exists():
            return

        try:
            with open(self.metrics_file, 'r', encoding='utf-8') as f:
                metrics_data = json.load(f)

            for metrics_dict in metrics_data:
                # Converti string in datetime
                metrics_dict['start_time'] = datetime.fromisoformat(metrics_dict['start_time'])
                if metrics_dict['end_time']:
                    metrics_dict['end_time'] = datetime.fromisoformat(metrics_dict['end_time'])
                else:
                    metrics_dict['end_time'] = None

                metrics = CleaningMetrics(**metrics_dict)
                self.metrics_data.append(metrics)

        except Exception as e:
            self.logger.error(f"Errore durante il caricamento delle metriche: {e}")

# Esempio di integrazione con CSS Cleaner
if __name__ == "__main__":
    # Imposta logger
    logger = CSSCleanerLogger()

    # Configura email alerts (opzionale)
    # logger.setup_email_alerts(
    #     smtp_server="smtp.gmail.com",
    #     smtp_port=587,
    #     username="your-email@gmail.com",
    #     password="your-app-password",
    #     from_email="your-email@gmail.com",
    #     to_emails=["admin@yoursite.com"]
    # )

    # Avvia monitoring
    logger.start_monitoring()

    # Simula operazione
    operation_id = logger.start_operation("test_cleaning")

    logger.log_file_processed(operation_id, "style.css", 50, 100, 10000, 7500)
    logger.log_file_processed(operation_id, "main.css", 25, 75, 5000, 4000)

    logger.end_operation(operation_id)

    # Genera report
    report = logger.get_performance_report(days=1)
    print("Report prestazioni:", report)

    # Ferma monitoring
    logger.stop_monitoring()

Benchmark delle prestazioni

Sistema completo di test prestazionali

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
#!/usr/bin/env python3
"""
Sistema di benchmark delle prestazioni CSS - analisi e report
"""

import time
import psutil
import gc
from typing import Dict, List, Callable, Any
from dataclasses import dataclass
from pathlib import Path
import json
from datetime import datetime
import threading
import subprocess
import os

@dataclass
class BenchmarkResult:
    """Risultato di un singolo benchmark"""
    test_name: str
    execution_time: float
    memory_before: float
    memory_after: float
    memory_peak: float
    cpu_percent: float
    files_processed: int = 0
    size_reduction: int = 0
    success: bool = True
    error_message: str = ""

class PerformanceBenchmark:
    """Sistema di benchmark per CSS Cleaner"""

    def __init__(self):
        self.results: List[BenchmarkResult] = []
        self.baseline_results: Dict[str, BenchmarkResult] = {}

    def run_benchmark(self, test_name: str, test_function: Callable, *args, **kwargs) -> BenchmarkResult:
        """Esegue un benchmark specifico"""

        print(f"Eseguo benchmark: {test_name}")

        # Pulizia memoria prima del test
        gc.collect()

        # Memoria iniziale
        process = psutil.Process()
        memory_before = process.memory_info().rss / 1024 / 1024  # MB

        # Avvia monitoring CPU
        cpu_percent = process.cpu_percent()

        # Esegui test
        start_time = time.time()
        memory_peak = memory_before

        # Monitor thread per memoria peak
        monitoring = True
        def memory_monitor():
            nonlocal memory_peak, monitoring
            while monitoring:
                current_memory = process.memory_info().rss / 1024 / 1024
                memory_peak = max(memory_peak, current_memory)
                time.sleep(0.1)

        monitor_thread = threading.Thread(target=memory_monitor, daemon=True)
        monitor_thread.start()

        try:
            result_data = test_function(*args, **kwargs)
            success = True
            error_message = ""
        except Exception as e:
            result_data = {}
            success = False
            error_message = str(e)
        finally:
            monitoring = False
            monitor_thread.join(timeout=1)

        # Misure finali
        end_time = time.time()
        execution_time = end_time - start_time
        memory_after = process.memory_info().rss / 1024 / 1024  # MB
        cpu_percent = process.cpu_percent() - cpu_percent

        # Estrai metadati dal risultato
        files_processed = result_data.get('files_processed', 0) if isinstance(result_data, dict) else 0
        size_reduction = result_data.get('size_reduction', 0) if isinstance(result_data, dict) else 0

        # Crea risultato benchmark
        benchmark_result = BenchmarkResult(
            test_name=test_name,
            execution_time=execution_time,
            memory_before=memory_before,
            memory_after=memory_after,
            memory_peak=memory_peak,
            cpu_percent=cpu_percent,
            files_processed=files_processed,
            size_reduction=size_reduction,
            success=success,
            error_message=error_message
        )

        self.results.append(benchmark_result)

        print(f"Completato: {test_name}")
        print(f"  Time: {execution_time:.2f}s")
        print(f"  Memory: {memory_before:.1f}MB -> {memory_after:.1f}MB (Peak: {memory_peak:.1f}MB)")
        print(f"  CPU: {cpu_percent:.1f}%")

        return benchmark_result

    def benchmark_css_cleaner_suite(self, css_cleaner_instance, test_data_dir: str) -> Dict[str, BenchmarkResult]:
        """Suite completa di benchmark per CSS Cleaner"""

        test_data_path = Path(test_data_dir)

        # Test 1: Small files (< 10KB)
        small_files = [f for f in test_data_path.glob("*.css") if f.stat().st_size < 10000]
        if small_files:
            def test_small_files():
                stats = css_cleaner_instance.process_all_css()
                return {
                    'files_processed': len(small_files),
                    'size_reduction': stats.original_size - stats.cleaned_size
                }

            self.run_benchmark("Elaborazione file piccoli", test_small_files)

        # Test 2: Large files (> 100KB)
        large_files = [f for f in test_data_path.glob("*.css") if f.stat().st_size > 100000]
        if large_files:
            def test_large_files():
                stats = css_cleaner_instance.process_all_css()
                return {
                    'files_processed': len(large_files),
                    'size_reduction': stats.original_size - stats.cleaned_size
                }

            self.run_benchmark("Elaborazione file grandi", test_large_files)

        # Test 3: Complex selectors
        def test_complex_selectors():
            complex_css = """
            .nav > ul li:nth-child(odd) a:hover { color: red; }
            .sidebar .widget[data-type="recent"] .post-list > li:first-child { margin: 0; }
            .content .article:not(.featured) .meta .author::before { content: "by "; }
            """

            with open(test_data_path / "complex_test.css", 'w') as f:
                f.write(complex_css)

            stats = css_cleaner_instance.process_all_css()
            return {'files_processed': 1, 'size_reduction': len(complex_css) - stats.cleaned_size}

        self.run_benchmark("Selettori complessi", test_complex_selectors)

        # Test 4: Memory stress test
        def test_memory_stress():
            # Genera CSS molto grande per testare memoria
            huge_css = ""
            for i in range(10000):
                huge_css += f".class-{i} {{ color: #{i:06x}; }}\n"
                huge_css += f"#id-{i} {{ background: #{i:06x}; }}\n"

            with open(test_data_path / "huge_test.css", 'w') as f:
                f.write(huge_css)

            stats = css_cleaner_instance.process_all_css()
            return {'files_processed': 1, 'size_reduction': len(huge_css) - stats.cleaned_size}

        self.run_benchmark("Stress test memoria", test_memory_stress)

        # Test 5: Concurrent processing
        def test_concurrent():
            import concurrent.futures
            import threading

            def process_single_file(file_path):
                return css_cleaner_instance.clean_css_file(file_path)

            css_files = list(test_data_path.glob("*.css"))[:5]  # Limita a 5 file

            with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
                futures = [executor.submit(process_single_file, f) for f in css_files]
                results = [future.result() for future in futures]

            return {'files_processed': len(css_files), 'size_reduction': sum(r[1] for r in results if r)}

        self.run_benchmark("Elaborazione concorrente", test_concurrent)

        return {result.test_name: result for result in self.results}

    def set_baseline(self, test_name: str):
        """Imposta riferimento per confronti futuri"""

        for result in self.results:
            if result.test_name == test_name:
                self.baseline_results[test_name] = result
                print(f"Riferimento impostato per: {test_name}")
                break

    def compare_with_baseline(self, test_name: str) -> Dict:
        """Confronta risultato corrente con riferimento"""

        if test_name not in self.baseline_results:
            return {"error": f"Nessun riferimento trovato per {test_name}"}

        baseline = self.baseline_results[test_name]
        current = None

        for result in reversed(self.results):
            if result.test_name == test_name:
                current = result
                break

        if not current:
            return {"error": f"Nessun risultato corrente trovato per {test_name}"}

        # Calcola differenze percentuali
        time_diff = ((current.execution_time - baseline.execution_time) / baseline.execution_time) * 100
        memory_diff = ((current.memory_peak - baseline.memory_peak) / baseline.memory_peak) * 100

        return {
            "test_name": test_name,
            "time_change_percent": time_diff,
            "memory_change_percent": memory_diff,
            "baseline_time": baseline.execution_time,
            "current_time": current.execution_time,
            "baseline_memory": baseline.memory_peak,
            "current_memory": current.memory_peak,
            "performance_regression": time_diff > 10 or memory_diff > 20  # Soglie di allarme
        }

    def generate_performance_report(self, output_file: str = None) -> str:
        """Genera report completo delle performance"""

        if not self.results:
            return "Nessun risultato di benchmark disponibile"

        report_lines = []
        report_lines.append("CSS CLEANER PERFORMANCE REPORT")
        report_lines.append("=" * 50)
        report_lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        report_lines.append("")

        # Summary statistiche
        total_time = sum(r.execution_time for r in self.results)
        avg_memory = sum(r.memory_peak for r in self.results) / len(self.results)
        success_rate = sum(1 for r in self.results if r.success) / len(self.results) * 100

        report_lines.append("SUMMARY:")
        report_lines.append(f"  Total Tests: {len(self.results)}")
        report_lines.append(f"  Total Time: {total_time:.2f}s")
        report_lines.append(f"  Average Memory Peak: {avg_memory:.1f}MB")
        report_lines.append(f"  Tasso di successo: {success_rate:.1f}%")
        report_lines.append("")

        # Dettagli per test
        report_lines.append("DETAILED RESULTS:")
        report_lines.append("-" * 80)

        for result in self.results:
            status = "✓ PASS" if result.success else "✗ FAIL"

            report_lines.append(f"Test: {result.test_name}")
            report_lines.append(f"  Status: {status}")
            report_lines.append(f"  Execution Time: {result.execution_time:.3f}s")
            report_lines.append(f"  Uso memoria: {result.memory_before:.1f}MB → {result.memory_after:.1f}MB (Picco: {result.memory_peak:.1f}MB)")
            report_lines.append(f"  Uso CPU: {result.cpu_percent:.1f}%")

            if result.files_processed > 0:
                report_lines.append(f"  Files Processed: {result.files_processed}")
                files_per_sec = result.files_processed / result.execution_time if result.execution_time > 0 else 0
                report_lines.append(f"  Velocita di elaborazione: {files_per_sec:.1f} file/s")

            if result.size_reduction > 0:
                report_lines.append(f"  Size Reduction: {result.size_reduction:,} bytes")

            if not result.success:
                report_lines.append(f"  Errore: {result.error_message}")

            report_lines.append("")

        # Confronti con riferimento se disponibili
        if self.baseline_results:
            report_lines.append("BASELINE COMPARISONS:")
            report_lines.append("-" * 80)

            for test_name in self.baseline_results.keys():
                comparison = self.compare_with_baseline(test_name)
                if "error" not in comparison:
                    time_symbol = "↑" if comparison["time_change_percent"] > 0 else "↓"
                    memory_symbol = "↑" if comparison["memory_change_percent"] > 0 else "↓"

                    report_lines.append(f"{test_name}:")
                    report_lines.append(f"  Time: {time_symbol} {abs(comparison['time_change_percent']):.1f}%")
                    report_lines.append(f"  Memory: {memory_symbol} {abs(comparison['memory_change_percent']):.1f}%")

                    if comparison["performance_regression"]:
                        report_lines.append("  PERFORMANCE REGRESSION DETECTED")

                    report_lines.append("")

        report_content = "\n".join(report_lines)

        # Salva su file se specificato
        if output_file:
            with open(output_file, 'w', encoding='utf-8') as f:
                f.write(report_content)
            print(f"Report prestazioni salvato in: {output_file}")

        return report_content

    def export_results_json(self, output_file: str):
        """Esporta risultati in formato JSON"""

        export_data = {
            "timestamp": datetime.now().isoformat(),
            "results": [],
            "baselines": {}
        }

        for result in self.results:
            export_data["results"].append({
                "test_name": result.test_name,
                "execution_time": result.execution_time,
                "memory_before": result.memory_before,
                "memory_after": result.memory_after,
                "memory_peak": result.memory_peak,
                "cpu_percent": result.cpu_percent,
                "files_processed": result.files_processed,
                "size_reduction": result.size_reduction,
                "success": result.success,
                "error_message": result.error_message
            })

        for test_name, baseline in self.baseline_results.items():
            export_data["baselines"][test_name] = {
                "execution_time": baseline.execution_time,
                "memory_peak": baseline.memory_peak
            }

        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(export_data, f, indent=2, ensure_ascii=False)

        print(f"Risultati esportati in: {output_file}")

    def run_continuous_benchmarks(self, interval_hours: int = 24):
        """Esegue benchmark continui per monitoraggio nel tempo"""

        import schedule

        def run_benchmark_suite():
            print("Eseguo la suite di benchmark pianificata...")
            # Implementa qui la logica per eseguire suite completa
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            report_file = f"benchmark_report_{timestamp}.txt"
            json_file = f"benchmark_results_{timestamp}.json"

            # Genera report
            self.generate_performance_report(report_file)
            self.export_results_json(json_file)

        schedule.every(interval_hours).hours.do(run_benchmark_suite)

        print(f"Benchmark continui pianificati ogni {interval_hours} ore")

        while True:
            schedule.run_pending()
            time.sleep(3600)  # Check ogni ora

# Esempio di utilizzo completo
if __name__ == "__main__":
    from css_cleaner import CSSCleaner  # Importa la classe principale

    # Imposta benchmark
    benchmark = PerformanceBenchmark()

    # Crea dati di test
    test_dir = Path("./test_data")
    test_dir.mkdir(exist_ok=True)

    # Genera file CSS di test di varie dimensioni
    small_css = ".small { color: red; } .tiny { font-size: 12px; }"
    medium_css = "\n".join([f".class-{i} {{ color: #{i:03x}; }}" for i in range(100)])
    large_css = "\n".join([f".large-class-{i} {{ background: #{i:06x}; margin: {i}px; }}" for i in range(1000)])

    (test_dir / "small.css").write_text(small_css)
    (test_dir / "medium.css").write_text(medium_css)
    (test_dir / "large.css").write_text(large_css)

    # HTML di test
    html_content = """
    <html><body>
    <div class="small">Small content</div>
    <div class="class-50">Medium content</div>
    <div class="large-class-500">Large content</div>
    </body></html>
    """

    html_dir = Path("./test_html")
    html_dir.mkdir(exist_ok=True)
    (html_dir / "test.html").write_text(html_content)

    # Inizializza CSS Cleaner
    cleaner = CSSCleaner(str(html_dir), str(test_dir))

    # Esegui suite di benchmark
    results = benchmark.benchmark_css_cleaner_suite(cleaner, str(test_dir))

    # Imposta riferimento per il primo test
    if results:
        first_test = list(results.keys())[0]
        benchmark.set_baseline(first_test)

    # Genera report
    report = benchmark.generate_performance_report("performance_report.txt")
    print(report)

    # Esporta JSON
    benchmark.export_results_json("benchmark_results.json")

Integrazione CI/CD

Pipeline Completa per Automazione

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# .github/workflows/css-cleaner.yml
# GitHub Actions workflow per CSS Cleaner

name: CSS Cleaner CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    # Esegui ogni notte alle 2:00 UTC
    - cron: '0 2 * * *'

env:
  PYTHON_VERSION: '3.11'
  NODE_VERSION: '18'

jobs:
  # Job 1: Linting e Code Quality
  code-quality:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.PYTHON_VERSION }}

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install black flake8 mypy pylint bandit safety
        pip install -r requirements.txt        

    - name: Code formatting check (Black)
      run: black --check --diff .

    - name: Linting (Flake8)
      run: flake8 src/ tests/ --max-line-length=100 --exclude=__pycache__

    - name: Type checking (MyPy)
      run: mypy src/ --ignore-missing-imports

    - name: Advanced linting (Pylint)
      run: pylint src/ --disable=C0114,C0116,R0903

    - name: Security check (Bandit)
      run: bandit -r src/ -f json -o bandit-report.json

    - name: Dependency security check
      run: safety check --json --output safety-report.json

    - name: Carica report di sicurezza
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: security-reports
        path: |
          bandit-report.json
          safety-report.json          

  # Job 2: Testing
  testing:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.9', '3.10', '3.11', '3.12']

    steps:
    - uses: actions/checkout@v4

    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install pytest pytest-cov pytest-xvfb selenium
        pip install -r requirements.txt        

    - name: Install Chrome for Selenium tests
      run: |
        sudo apt-get update
        sudo apt-get install -y google-chrome-stable        

    - name: Esegui test unitari
      run: |
                pytest tests/unit/ -v --cov=src/ --cov-report=xml --cov-report=html

    - name: Esegui test di integrazione
      run: |
                pytest tests/integration/ -v --tb=short

    - name: Esegui test prestazioni
      run: |
                pytest tests/performance/ -v --benchmark-only

    - name: Carica report di copertura
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.xml
        flags: unittests
        name: codecov-umbrella
        fail_ci_if_error: true

  # Job 3: Benchmark delle prestazioni
  performance:
    runs-on: ubuntu-latest
    needs: [code-quality, testing]

    steps:
    - uses: actions/checkout@v4

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.PYTHON_VERSION }}

    - name: Install dependencies
      run: |
        pip install -r requirements.txt
        pip install memory-profiler psutil        

    - name: Generate test data
      run: |
                python scripts/generate_test_data.py --size large

    - name: Esegui benchmark prestazioni
      run: |
                python -m pytest tests/performance/ --benchmark-json=benchmark.json

    - name: Verifica regressioni di performance
      run: |
                python scripts/check_performance_regression.py benchmark.json

    - name: Carica risultati benchmark
      uses: actions/upload-artifact@v3
      with:
        name: benchmark-results
        path: benchmark.json

  # Job 4: CSS Optimization Testing
  css-optimization:
    runs-on: ubuntu-latest
    needs: testing

    steps:
    - uses: actions/checkout@v4

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.PYTHON_VERSION }}

    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}

    - name: Install CSS tools
      run: |
        npm install -g cssnano postcss-cli autoprefixer
        pip install -r requirements.txt        

    - name: Prepare test websites
      run: |
        # Clone test websites for CSS optimization
        git clone https://github.com/h5bp/html5-boilerplate.git test-sites/h5bp
        git clone https://github.com/twbs/bootstrap.git test-sites/bootstrap        

    - name: Esegui test ottimizzazione CSS
      run: |
                python scripts/test_real_world_css.py test-sites/

    - name: Compare with other tools
      run: |
                python scripts/compare_with_competitors.py test-sites/

    - name: Carica report ottimizzazione
      uses: actions/upload-artifact@v3
      with:
        name: optimization-reports
        path: reports/

  # Job 5: Docker Build and Test
  docker:
    runs-on: ubuntu-latest
    needs: [code-quality, testing]

    steps:
    - uses: actions/checkout@v4

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Build Docker image
      run: |
                docker build -t css-cleaner:latest .

    - name: Test Docker container
      run: |
        docker run --rm -v $(pwd)/test-data:/data css-cleaner:latest \
          --html-dir /data/html --css-dir /data/css --output-dir /data/output        

    - name: Security scan (Trivy)
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: 'css-cleaner:latest'
        format: 'sarif'
        output: 'trivy-results.sarif'

    - name: Carica risultati scansione Trivy
      uses: github/codeql-action/upload-sarif@v2
      with:
        sarif_file: 'trivy-results.sarif'

  # Job 6: Release and Deploy
  release:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    needs: [code-quality, testing, performance, css-optimization, docker]

    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.PYTHON_VERSION }}

    - name: Install build dependencies
      run: |
                pip install build twine wheel

    - name: Build package
      run: |
                python -m build

    - name: Create GitHub Release
      id: create_release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: v${{ github.run_number }}
        release_name: CSS Cleaner v${{ github.run_number }}
        draft: false
        prerelease: false
        body: |
          ## Changes in this Release
          - Automated release from CI/CD pipeline
          - All tests passed
          - Benchmark prestazioni validati

          ## Installation
          ```bash
          pip install css-cleaner==${{ github.run_number }}
          ```          

    - name: Publish to PyPI
      env:
        TWINE_USERNAME: __token__
        TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
      run: |
                twine upload dist/*

    - name: Build and push Docker image
      env:
        DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
      run: |
        echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
        docker build -t csstools/css-cleaner:latest -t csstools/css-cleaner:v${{ github.run_number }} .
        docker push csstools/css-cleaner:latest
        docker push csstools/css-cleaner:v${{ github.run_number }}        

    - name: Deploy documentation
      run: |
        pip install mkdocs mkdocs-material
        mkdocs build
        mkdocs gh-deploy --force        

  # Job 7: Notification
  notify:
    runs-on: ubuntu-latest
    needs: [release]
    if: always()

    steps:
    - name: Notify Slack on success
      if: success()
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_CHANNEL: 'css-cleaner'
        SLACK_COLOR: good
        SLACK_MESSAGE: |
          CSS Cleaner CI/CD Pipeline completata con successo!
          - Version: v${{ github.run_number }}
          - Published to PyPI and Docker Hub
          - Documentation updated          

    - name: Notify Slack on failure
      if: failure()
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_CHANNEL: 'css-cleaner'
        SLACK_COLOR: danger
        SLACK_MESSAGE: |
          CSS Cleaner CI/CD Pipeline fallita!
          - Branch: ${{ github.ref }}
          - Commit: ${{ github.sha }}
          - Please check the logs for details.          
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# scripts/check_performance_regression.py
"""
Script di rilevamento regressioni delle performance per CI/CD
"""

import json
import sys
from pathlib import Path

def check_performance_regression(benchmark_file: str, threshold_percent: float = 15.0) -> bool:
    """
    Verifica regressioni di performance confrontando con il riferimento

    Args:
        benchmark_file: Path del file JSON con risultati benchmark
        threshold_percent: Soglia di regressione in percentuale

    Returns:
        True se performance accettabili, False se regressione rilevata
    """

    if not Path(benchmark_file).exists():
        print(f"File benchmark non trovato: {benchmark_file}")
        return False

    # Carica risultati correnti
    with open(benchmark_file, 'r') as f:
        current_results = json.load(f)

    # Carica il riferimento (se esiste)
    baseline_file = "benchmark_baseline.json"
    if not Path(baseline_file).exists():
        print(f"Nessun riferimento trovato, creo il nuovo riferimento: {baseline_file}")
        # Crea il riferimento con risultati correnti
        with open(baseline_file, 'w') as f:
            json.dump(current_results, f, indent=2)
        return True

    with open(baseline_file, 'r') as f:
        baseline_results = json.load(f)

    # Confronta performance
    regressions = []
    improvements = []

    for benchmark in current_results.get('benchmarks', []):
        test_name = benchmark['name']
        current_time = benchmark['stats']['mean']

        # Trova riferimento corrispondente
        baseline_benchmark = None
        for b in baseline_results.get('benchmarks', []):
            if b['name'] == test_name:
                baseline_benchmark = b
                break

        if not baseline_benchmark:
            print(f"Nessun riferimento trovato per il test: {test_name}")
            continue

        baseline_time = baseline_benchmark['stats']['mean']

        # Calcola differenza percentuale
        time_diff_percent = ((current_time - baseline_time) / baseline_time) * 100

        if time_diff_percent > threshold_percent:
            regressions.append({
                'test': test_name,
                'regression_percent': time_diff_percent,
                'current_time': current_time,
                'baseline_time': baseline_time
            })
        elif time_diff_percent < -5:  # Miglioramento significativo
            improvements.append({
                'test': test_name,
                'improvement_percent': abs(time_diff_percent),
                'current_time': current_time,
                'baseline_time': baseline_time
            })

    # Report risultati
    print("\nREPORT REGRESSIONI PRESTAZIONALI")
    print("=" * 50)

    if improvements:
        print(f"\nMiglioramenti prestazioni ({len(improvements)}):")
        for imp in improvements:
            print(f"  • {imp['test']}: {imp['improvement_percent']:.1f}% faster")

    if regressions:
        print(f"\nRegressioni prestazioni ({len(regressions)}):")
        for reg in regressions:
            print(f"  • {reg['test']}: {reg['regression_percent']:.1f}% slower")
            print(f"    Corrente: {reg['current_time']:.3f}s, Riferimento: {reg['baseline_time']:.3f}s")

        print("\nREGRESSIONE PRESTAZIONALE RILEVATA!")
        print(f"Threshold: {threshold_percent}%")
        print("Please investigate and optimize before merging.")
        return False

    else:
        print("\nNessuna regressione prestazionale significativa rilevata.")
        print(f"Tutti i test entro {threshold_percent}% del riferimento prestazionale.")

        # Aggiorna il riferimento se le performance migliorano significativamente
        if improvements and len(improvements) > len(current_results.get('benchmarks', [])) / 2:
            print("Aggiorno il riferimento per miglioramenti significativi...")
            with open(baseline_file, 'w') as f:
                json.dump(current_results, f, indent=2)

        return True

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Uso: python check_performance_regression.py <benchmark_file.json>")
        sys.exit(1)

    benchmark_file = sys.argv[1]

    success = check_performance_regression(benchmark_file)

    if not success:
        sys.exit(1)  # Exit with error code for CI/CD

    print("\nVerifica prestazioni superata!")
    sys.exit(0)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Dockerfile per CSS Cleaner
FROM python:3.11-slim

# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
    gcc \
    g++ \
    chromium \
    chromium-driver \
    && rm -rf /var/lib/apt/lists/*

# Copy requirements first for better caching
COPY requirements.txt .

# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY src/ ./src/
COPY scripts/ ./scripts/
COPY tests/ ./tests/

# Create directories for data
RUN mkdir -p /data/html /data/css /data/output /data/logs

# Set environment variables
ENV PYTHONPATH=/app/src
ENV CSS_CLEANER_LOG_LEVEL=INFO
ENV CHROME_BIN=/usr/bin/chromium
ENV CHROME_PATH=/usr/bin/chromium

# Create non-root user
RUN useradd -m -u 1000 cssuser && \
    chown -R cssuser:cssuser /app /data

USER cssuser

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD python -c "import src.css_cleaner; print('OK')" || exit 1

# Default command
ENTRYPOINT ["python", "-m", "src.css_cleaner"]

# Uso:
# docker run -v /path/to/data:/data css-cleaner:latest --html-dir /data/html --css-dir /data/css --output-dir /data/output

Plugin per Framework

Sistema di Plugin per Framework Popolari

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
#!/usr/bin/env python3
"""
Plugin framework per CSS Cleaner - supporto per framework popolari
"""

from abc import ABC, abstractmethod
from typing import Dict, List, Set, Any, Optional
import re
import json
from pathlib import Path

class FrameworkPlugin(ABC):
    """Base class per plugin framework"""

    @property
    @abstractmethod
    def framework_name(self) -> str:
        """Nome del framework"""
        pass

    @property
    @abstractmethod
    def supported_extensions(self) -> List[str]:
        """Estensioni file supportate"""
        pass

    @abstractmethod
    def extract_selectors(self, file_path: str, content: str) -> Set[str]:
        """Estrae selettori specifici del framework"""
        pass

    @abstractmethod
    def should_keep_selector(self, selector: str, context: Dict) -> bool:
        """Determina se mantenere un selettore"""
        pass

class ReactPlugin(FrameworkPlugin):
    """Plugin per React e JSX"""

    @property
    def framework_name(self) -> str:
        return "React"

    @property
    def supported_extensions(self) -> List[str]:
        return ['.jsx', '.tsx', '.js', '.ts']

    def extract_selectors(self, file_path: str, content: str) -> Set[str]:
        selectors = set()

        # Pattern per className in JSX
        className_patterns = [
            r'className\s*=\s*["\']([^"\']+)["\']',
            r'className\s*=\s*{["\']([^"\']+)["\']',
            r'className\s*=\s*{\s*`([^`]+)`\s*}',
            r'className\s*=\s*{([^}]+)}',  # Espressioni dinamiche
        ]

        for pattern in className_patterns:
            matches = re.findall(pattern, content, re.MULTILINE)
            for match in matches:
                # Gestisci classi multiple
                class_names = match.split()
                for class_name in class_names:
                    # Pulisci caratteri speciali da espressioni JS
                    cleaned = re.sub(r'[{}$`]', '', class_name).strip()
                    if cleaned and cleaned.isidentifier():
                        selectors.add(f".{cleaned}")

        # CSS Modules pattern
        css_modules_pattern = r'styles\.(\w+)'
        css_modules_matches = re.findall(css_modules_pattern, content)
        for match in css_modules_matches:
            selectors.add(f".{match}")

        # Styled Components pattern
        styled_pattern = r'styled\.(\w+)`([^`]+)`'
        styled_matches = re.findall(styled_pattern, content, re.DOTALL)
        for tag, css_content in styled_matches:
            # Estrai selettori dal CSS di styled-components
            css_selectors = self._extract_css_selectors_from_template(css_content)
            selectors.update(css_selectors)

        return selectors

    def should_keep_selector(self, selector: str, context: Dict) -> bool:
        # Mantieni selettori comunemente usati in React
        react_common = [
            '.react-', '.rc-', '.ant-', '.mui-', '.chakra-',  # UI libraries
            '.App', '.app', '.container', '.wrapper', '.root',  # Common patterns
        ]

        return any(selector.startswith(prefix) for prefix in react_common)

    def _extract_css_selectors_from_template(self, css_content: str) -> Set[str]:
        """Estrae selettori da template CSS di styled-components"""
        selectors = set()

        # Pattern per selettori CSS dentro template literals
        selector_patterns = [
            r'&\.(\w+)',  # &.className
            r'\.(\w+)',   # .className
            r'#(\w+)',    # #id
        ]

        for pattern in selector_patterns:
            matches = re.findall(pattern, css_content)
            for match in matches:
                if pattern.startswith('&\\.'):
                    selectors.add(f".{match}")
                elif pattern.startswith('\\.'):
                    selectors.add(f".{match}")
                else:
                    selectors.add(f"#{match}")

        return selectors

class VuePlugin(FrameworkPlugin):
    """Plugin per Vue.js"""

    @property
    def framework_name(self) -> str:
        return "Vue"

    @property
    def supported_extensions(self) -> List[str]:
        return ['.vue', '.js', '.ts']

    def extract_selectors(self, file_path: str, content: str) -> Set[str]:
        selectors = set()

        # Pattern per Vue SFC (Single File Components)
        if file_path.endswith('.vue'):
            selectors.update(self._extract_from_vue_sfc(content))
        else:
            selectors.update(self._extract_from_vue_js(content))

        return selectors

    def _extract_from_vue_sfc(self, content: str) -> Set[str]:
        """Estrae selettori da Vue Single File Component"""
        selectors = set()

        # Estrai sezione template
        template_match = re.search(r'<template[^>]*>(.*?)</template>', content, re.DOTALL)
        if template_match:
            template_content = template_match.group(1)

            # Pattern per :class binding
            class_binding_patterns = [
                r':class\s*=\s*["\']([^"\']+)["\']',
                r':class\s*=\s*{([^}]+)}',
                r'v-bind:class\s*=\s*["\']([^"\']+)["\']',
                r'class\s*=\s*["\']([^"\']+)["\']',
            ]

            for pattern in class_binding_patterns:
                matches = re.findall(pattern, template_content)
                for match in matches:
                    if '{' not in match:  # Stringa semplice
                        class_names = match.split()
                        for class_name in class_names:
                            selectors.add(f".{class_name}")
                    else:  # Oggetto o espressione
                        # Estrai nomi classi da oggetti JavaScript
                        class_names = re.findall(r'["\'](\w+)["\']', match)
                        for class_name in class_names:
                            selectors.add(f".{class_name}")

        # Estrai sezione style con scoped
        style_matches = re.finditer(r'<style[^>]*scoped[^>]*>(.*?)</style>', content, re.DOTALL)
        for style_match in style_matches:
            style_content = style_match.group(1)
            # In Vue scoped styles, tutti i selettori sono potenzialmente utilizzati
            css_selectors = re.findall(r'\.(\w+)', style_content)
            for selector in css_selectors:
                selectors.add(f".{selector}")

        return selectors

    def _extract_from_vue_js(self, content: str) -> Set[str]:
        """Estrae selettori da file JavaScript Vue"""
        selectors = set()

        # Pattern per template strings in Vue components
        template_pattern = r'template\s*:\s*`([^`]+)`'
        template_matches = re.findall(template_pattern, content, re.DOTALL)

        for template in template_matches:
            # Stessi pattern del SFC template
            class_matches = re.findall(r'class\s*=\s*["\']([^"\']+)["\']', template)
            for match in class_matches:
                class_names = match.split()
                for class_name in class_names:
                    selectors.add(f".{class_name}")

        return selectors

    def should_keep_selector(self, selector: str, context: Dict) -> bool:
        # Mantieni selettori Vue comuni
        vue_common = [
            '.v-', '.vue-', '.el-',  # Vue/Element UI prefixes
            '.fade-', '.slide-', '.bounce-',  # Transition classes
        ]

        return any(selector.startswith(prefix) for prefix in vue_common)

class AngularPlugin(FrameworkPlugin):
    """Plugin per Angular"""

    @property
    def framework_name(self) -> str:
        return "Angular"

    @property
    def supported_extensions(self) -> List[str]:
        return ['.ts', '.html', '.scss', '.css']

    def extract_selectors(self, file_path: str, content: str) -> Set[str]:
        selectors = set()

        if file_path.endswith('.html'):
            selectors.update(self._extract_from_angular_template(content))
        elif file_path.endswith('.ts'):
            selectors.update(self._extract_from_angular_component(content))

        return selectors

    def _extract_from_angular_template(self, content: str) -> Set[str]:
        """Estrae selettori da template Angular"""
        selectors = set()

        # Pattern per [ngClass]
        ng_class_patterns = [
            r'\[ngClass\]\s*=\s*["\']([^"\']+)["\']',
            r'\[ngClass\]\s*=\s*{([^}]+)}',
            r'ngClass\s*=\s*["\']([^"\']+)["\']',
        ]

        for pattern in ng_class_patterns:
            matches = re.findall(pattern, content)
            for match in matches:
                # Estrai nomi classi
                class_names = re.findall(r'["\'](\w+)["\']', match)
                for class_name in class_names:
                    selectors.add(f".{class_name}")

        # Pattern per class normale
        class_matches = re.findall(r'class\s*=\s*["\']([^"\']+)["\']', content)
        for match in class_matches:
            class_names = match.split()
            for class_name in class_names:
                selectors.add(f".{class_name}")

        return selectors

    def _extract_from_angular_component(self, content: str) -> Set[str]:
        """Estrae selettori da componente Angular TypeScript"""
        selectors = set()

        # Pattern per template inline
        template_pattern = r'template\s*:\s*`([^`]+)`'
        template_matches = re.findall(template_pattern, content, re.DOTALL)

        for template in template_matches:
            selectors.update(self._extract_from_angular_template(template))

        # Pattern per styleUrls e styles
        styles_pattern = r'styles\s*:\s*\[\s*`([^`]+)`\s*\]'
        styles_matches = re.findall(styles_pattern, content, re.DOTALL)

        for styles in styles_matches:
            css_selectors = re.findall(r'\.(\w+)', styles)
            for selector in css_selectors:
                selectors.add(f".{selector}")

        return selectors

    def should_keep_selector(self, selector: str, context: Dict) -> bool:
        # Mantieni selettori Angular comuni
        angular_common = [
            '.mat-', '.cdk-', '.ng-',  # Angular Material/CDK
            '.p-',  # PrimeNG
            '.ngx-',  # NGX libraries
        ]

        return any(selector.startswith(prefix) for prefix in angular_common)

class TailwindPlugin(FrameworkPlugin):
    """Plugin per Tailwind CSS"""

    def __init__(self):
        # Carica configurazione Tailwind se disponibile
        self.config = self._load_tailwind_config()
        self.utility_patterns = self._build_utility_patterns()

    @property
    def framework_name(self) -> str:
        return "Tailwind"

    @property
    def supported_extensions(self) -> List[str]:
        return ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.php', '.twig']

    def extract_selectors(self, file_path: str, content: str) -> Set[str]:
        selectors = set()

        # Pattern per classi Tailwind
        class_patterns = [
            r'class\s*=\s*["\']([^"\']+)["\']',
            r'className\s*=\s*["\']([^"\']+)["\']',
            r':class\s*=\s*["\']([^"\']+)["\']',
        ]

        for pattern in class_patterns:
            matches = re.findall(pattern, content)
            for match in matches:
                class_names = match.split()
                for class_name in class_names:
                    if self._is_tailwind_class(class_name):
                        selectors.add(f".{class_name}")

        return selectors

    def _is_tailwind_class(self, class_name: str) -> bool:
        """Verifica se una classe è una utility Tailwind"""

        # Pattern comuni Tailwind
        tailwind_patterns = [
            r'^(bg|text|border|p|m|w|h)-',  # Background, text, border, padding, margin, width, height
            r'^(flex|grid|block|inline|hidden)',  # Display
            r'^(justify|items|self)-',  # Flexbox/Grid
            r'^(rounded|shadow|opacity)-',  # Effects
            r'^(hover|focus|active|disabled):',  # States
            r'^(sm|md|lg|xl|2xl):',  # Breakpoints
            r'^(space|divide)-',  # Space/Divide
        ]

        return any(re.match(pattern, class_name) for pattern in tailwind_patterns)

    def should_keep_selector(self, selector: str, context: Dict) -> bool:
        # In Tailwind, mantieni tutte le utilities che matchano i pattern
        class_name = selector[1:]  # Rimuovi il punto
        return self._is_tailwind_class(class_name)

    def _load_tailwind_config(self) -> Dict:
        """Carica configurazione Tailwind se disponibile"""
        config_files = ['tailwind.config.js', 'tailwind.config.ts']

        for config_file in config_files:
            if Path(config_file).exists():
                try:
                    # Parsing semplificato della config Tailwind
                    with open(config_file, 'r') as f:
                        content = f.read()

                    # Estrai content paths
                    content_match = re.search(r'content\s*:\s*\[(.*?)\]', content, re.DOTALL)
                    if content_match:
                        content_paths = re.findall(r'["\']([^"\']+)["\']', content_match.group(1))
                        return {'content': content_paths}

                except Exception:
                    pass

        return {}

    def _build_utility_patterns(self) -> List[str]:
        """Costruisce pattern per utilities Tailwind"""
        # Pattern base per utilities Tailwind più comuni
        return [
            r'^bg-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+$',
            r'^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$',
            r'^(p|m|w|h)-\d+$',
            r'^(flex|grid|block|inline-block|inline|hidden)$',
        ]

class FrameworkDetector:
    """Rileva automaticamente il framework utilizzato"""

    def __init__(self, project_root: str):
        self.project_root = Path(project_root)
        self.detected_frameworks = self._detect_frameworks()

    def _detect_frameworks(self) -> Dict[str, bool]:
        """Rileva framework presenti nel progetto"""
        frameworks = {
            'react': False,
            'vue': False,
            'angular': False,
            'tailwind': False,
            'bootstrap': False,
        }

        # Controlla package.json
        package_json = self.project_root / 'package.json'
        if package_json.exists():
            try:
                with open(package_json) as f:
                    package_data = json.load(f)

                dependencies = {**package_data.get('dependencies', {}),
                              **package_data.get('devDependencies', {})}

                if any(dep.startswith('react') for dep in dependencies):
                    frameworks['react'] = True

                if any(dep.startswith('vue') for dep in dependencies):
                    frameworks['vue'] = True

                if any(dep.startswith('@angular') for dep in dependencies):
                    frameworks['angular'] = True

                if 'tailwindcss' in dependencies:
                    frameworks['tailwind'] = True

                if 'bootstrap' in dependencies:
                    frameworks['bootstrap'] = True

            except Exception:
                pass

        # Controlla file di configurazione specifici
        config_files = {
            'angular.json': 'angular',
            'vue.config.js': 'vue',
            'tailwind.config.js': 'tailwind',
            'tailwind.config.ts': 'tailwind',
        }

        for config_file, framework in config_files.items():
            if (self.project_root / config_file).exists():
                frameworks[framework] = True

        return frameworks

    def get_recommended_plugins(self) -> List[FrameworkPlugin]:
        """Restituisce plugin raccomandati basati sui framework rilevati"""
        plugins = []

        if self.detected_frameworks['react']:
            plugins.append(ReactPlugin())

        if self.detected_frameworks['vue']:
            plugins.append(VuePlugin())

        if self.detected_frameworks['angular']:
            plugins.append(AngularPlugin())

        if self.detected_frameworks['tailwind']:
            plugins.append(TailwindPlugin())

        return plugins

class FrameworkAwareCSSCleaner:
    """CSS Cleaner con supporto framework avanzato"""

    def __init__(self, html_dir: str, css_dir: str, output_dir: str = None, project_root: str = "."):
        self.html_dir = Path(html_dir)
        self.css_dir = Path(css_dir)
        self.output_dir = Path(output_dir) if output_dir else self.css_dir

        # Rileva framework e carica plugin
        self.detector = FrameworkDetector(project_root)
        self.plugins = self.detector.get_recommended_plugins()

        print(f"Framework rilevati: {list(k for k, v in self.detector.detected_frameworks.items() if v)}")
        print(f"Plugin caricati: {[p.framework_name for p in self.plugins]}")

    def extract_framework_selectors(self) -> Set[str]:
        """Estrae selettori usando tutti i plugin framework"""
        all_selectors = set()

        # Scansiona tutti i file supportati
        for plugin in self.plugins:
            for ext in plugin.supported_extensions:
                pattern = f"**/*{ext}"

                # Cerca in directory HTML e altre directory del progetto
                search_paths = [self.html_dir, self.html_dir.parent]

                for search_path in search_paths:
                    if search_path.exists():
                        for file_path in search_path.rglob(pattern):
                            try:
                                with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                                    content = f.read()

                                selectors = plugin.extract_selectors(str(file_path), content)
                                all_selectors.update(selectors)

                                if selectors:
                                    print(f"Trovati {len(selectors)} selettori in {file_path} usando il plugin {plugin.framework_name}")

                            except Exception as e:
                                print(f"Errore durante l'elaborazione di {file_path}: {e}")

        return all_selectors

    def should_keep_selector_with_plugins(self, selector: str) -> bool:
        """Verifica se mantenere un selettore consultando i plugin"""

        context = {
            'detected_frameworks': self.detector.detected_frameworks
        }

        # Se almeno un plugin dice di mantenerlo, mantienilo
        for plugin in self.plugins:
            if plugin.should_keep_selector(selector, context):
                return True

        return False

# Esempio di utilizzo completo
if __name__ == "__main__":
    # Inizializza CSS Cleaner con supporto framework
    cleaner = FrameworkAwareCSSCleaner(
        html_dir="./src",
        css_dir="./public/css",
        output_dir="./dist/css",
        project_root="."
    )

    # Estrai selettori con plugin framework
    framework_selectors = cleaner.extract_framework_selectors()
    print(f"Total framework selectors found: {len(framework_selectors)}")

    # Esempio di utilizzo dei plugin
    react_plugin = ReactPlugin()

    # Test con contenuto React
    react_content = """
    function MyComponent() {
        return (
            <div className="container mx-auto">
                <h1 className={`title ${isActive ? 'active' : 'inactive'}`}>
                    Hello World
                </h1>
                <Button className="btn-primary">Click me</Button>
            </div>
        );
    }
    """

    react_selectors = react_plugin.extract_selectors("Component.jsx", react_content)
    print(f"React selectors: {react_selectors}")

Conclusioni

Congratulazioni! Hai completato la Guida CSS Cleaner!

Ora hai a disposizione un ecosistema completo per l’ottimizzazione CSS:

  • CSS Cleaner avanzato con parsing intelligente e gestione errori
  • Analisi HTML dinamica per applicazioni moderne (React, Vue, Angular)
  • Sistema di backup completo con versionamento e controllo integrita
  • Monitoraggio e logging di livello enterprise con metriche dettagliate
  • Benchmark delle prestazioni per ottimizzazione continua
  • Pipeline CI/CD completa per automazione e garanzia qualita
  • Plugin per framework per integrazione con tecnologie moderne
  • Interfacce multiple (CLI, GUI, Web API) per ogni scenario d’uso

Prossimi Passi Consigliati

Roadmap di implementazione
  1. Inizia con la versione base e testa su progetti reali
  2. Implementa gradualmente le funzionalità avanzate necessarie
  3. Configura monitoring per trackare performance nel tempo
  4. Configura la pipeline CI/CD per automazione completa
  5. Estendi con plugin per framework specifici del tuo stack
  6. Contribuisci al progetto con miglioramenti e nuove funzionalità

Metriche di Successo Tipiche

Progetti che hanno implementato CSS optimization sistematica riportano:

  • 30-80% riduzione dimensione file CSS
  • 15-40% miglioramento First Contentful Paint
  • Miglioramento 10-25% punteggio prestazioni di Lighthouse
  • Riduzione significativa bandwidth e costi hosting
  • Miglioramento SEO attraverso Core Web Vitals ottimali

Risorse per Continuare

Contributi e Feedback

Questo CSS Cleaner è progettato per essere estensibile e migliorabile. Considera di:

  1. Condividere le tue ottimizzazioni con la community
  2. Creare plugin personalizzati per framework o CMS specifici
  3. Contribuire al testing con dataset diversificati
  4. Migliorare la documentazione basata sulla tua esperienza

La performance web è un viaggio continuo, e con questi strumenti hai tutto il necessario per costruire siti web velocissimi e ottimizzati!


Buon lavoro e buona ottimizzazione! Il web ti ringraziera per ogni byte risparmiato.