cataract 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +7 -0
  2. data/.clang-tidy +30 -0
  3. data/.github/workflows/ci-macos.yml +12 -0
  4. data/.github/workflows/ci.yml +77 -0
  5. data/.github/workflows/test.yml +76 -0
  6. data/.gitignore +45 -0
  7. data/.overcommit.yml +38 -0
  8. data/.rubocop.yml +83 -0
  9. data/BENCHMARKS.md +201 -0
  10. data/CHANGELOG.md +1 -0
  11. data/Gemfile +27 -0
  12. data/LICENSE +21 -0
  13. data/RAGEL_MIGRATION.md +60 -0
  14. data/README.md +292 -0
  15. data/Rakefile +209 -0
  16. data/benchmarks/benchmark_harness.rb +193 -0
  17. data/benchmarks/benchmark_merging.rb +121 -0
  18. data/benchmarks/benchmark_optimization_comparison.rb +168 -0
  19. data/benchmarks/benchmark_parsing.rb +153 -0
  20. data/benchmarks/benchmark_ragel_removal.rb +56 -0
  21. data/benchmarks/benchmark_runner.rb +70 -0
  22. data/benchmarks/benchmark_serialization.rb +180 -0
  23. data/benchmarks/benchmark_shorthand.rb +109 -0
  24. data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
  25. data/benchmarks/benchmark_specificity.rb +124 -0
  26. data/benchmarks/benchmark_string_allocation.rb +151 -0
  27. data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
  28. data/benchmarks/benchmark_to_s_cached.rb +55 -0
  29. data/benchmarks/benchmark_value_splitter.rb +54 -0
  30. data/benchmarks/benchmark_yjit.rb +158 -0
  31. data/benchmarks/benchmark_yjit_workers.rb +61 -0
  32. data/benchmarks/profile_to_s.rb +23 -0
  33. data/benchmarks/speedup_calculator.rb +83 -0
  34. data/benchmarks/system_metadata.rb +81 -0
  35. data/benchmarks/templates/benchmarks.md.erb +221 -0
  36. data/benchmarks/yjit_tests.rb +141 -0
  37. data/cataract.gemspec +34 -0
  38. data/cliff.toml +92 -0
  39. data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
  40. data/examples/color_conversion_visual_test/generate.rb +202 -0
  41. data/examples/color_conversion_visual_test/template.html.erb +259 -0
  42. data/examples/css_analyzer/analyzer.rb +164 -0
  43. data/examples/css_analyzer/analyzers/base.rb +33 -0
  44. data/examples/css_analyzer/analyzers/colors.rb +133 -0
  45. data/examples/css_analyzer/analyzers/important.rb +88 -0
  46. data/examples/css_analyzer/analyzers/properties.rb +61 -0
  47. data/examples/css_analyzer/analyzers/specificity.rb +68 -0
  48. data/examples/css_analyzer/templates/report.html.erb +575 -0
  49. data/examples/css_analyzer.rb +69 -0
  50. data/examples/github_analysis.html +5343 -0
  51. data/ext/cataract/cataract.c +1086 -0
  52. data/ext/cataract/cataract.h +174 -0
  53. data/ext/cataract/css_parser.c +1435 -0
  54. data/ext/cataract/extconf.rb +48 -0
  55. data/ext/cataract/import_scanner.c +174 -0
  56. data/ext/cataract/merge.c +973 -0
  57. data/ext/cataract/shorthand_expander.c +902 -0
  58. data/ext/cataract/specificity.c +213 -0
  59. data/ext/cataract/value_splitter.c +116 -0
  60. data/ext/cataract_color/cataract_color.c +16 -0
  61. data/ext/cataract_color/color_conversion.c +1687 -0
  62. data/ext/cataract_color/color_conversion.h +136 -0
  63. data/ext/cataract_color/color_conversion_lab.c +571 -0
  64. data/ext/cataract_color/color_conversion_named.c +259 -0
  65. data/ext/cataract_color/color_conversion_oklab.c +547 -0
  66. data/ext/cataract_color/extconf.rb +23 -0
  67. data/ext/cataract_old/cataract.c +393 -0
  68. data/ext/cataract_old/cataract.h +250 -0
  69. data/ext/cataract_old/css_parser.c +933 -0
  70. data/ext/cataract_old/extconf.rb +67 -0
  71. data/ext/cataract_old/import_scanner.c +174 -0
  72. data/ext/cataract_old/merge.c +776 -0
  73. data/ext/cataract_old/shorthand_expander.c +902 -0
  74. data/ext/cataract_old/specificity.c +213 -0
  75. data/ext/cataract_old/stylesheet.c +290 -0
  76. data/ext/cataract_old/value_splitter.c +116 -0
  77. data/lib/cataract/at_rule.rb +97 -0
  78. data/lib/cataract/color_conversion.rb +18 -0
  79. data/lib/cataract/declarations.rb +332 -0
  80. data/lib/cataract/import_resolver.rb +210 -0
  81. data/lib/cataract/rule.rb +131 -0
  82. data/lib/cataract/stylesheet.rb +716 -0
  83. data/lib/cataract/stylesheet_scope.rb +257 -0
  84. data/lib/cataract/version.rb +5 -0
  85. data/lib/cataract.rb +107 -0
  86. data/lib/tasks/gem.rake +158 -0
  87. data/scripts/fuzzer/run.rb +828 -0
  88. data/scripts/fuzzer/worker.rb +99 -0
  89. data/scripts/generate_benchmarks_md.rb +155 -0
  90. metadata +135 -0
@@ -0,0 +1,575 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CSS Analysis Report - <%= analysis[:summary][:file_name] %></title>
7
+
8
+ <!-- Bootstrap CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <style>
12
+ body {
13
+ padding: 2rem 0;
14
+ }
15
+ .stat-card {
16
+ text-align: center;
17
+ padding: 1.5rem;
18
+ }
19
+ .stat-number {
20
+ font-size: 2.5rem;
21
+ font-weight: bold;
22
+ color: #0d6efd;
23
+ }
24
+ .stat-label {
25
+ color: #6c757d;
26
+ font-size: 0.875rem;
27
+ text-transform: uppercase;
28
+ letter-spacing: 0.5px;
29
+ }
30
+ .property-row:hover {
31
+ background-color: #f8f9fa;
32
+ }
33
+ .example-box {
34
+ background-color: #f8f9fa;
35
+ padding: 0.5rem;
36
+ border-radius: 0.25rem;
37
+ font-family: 'Courier New', monospace;
38
+ font-size: 0.875rem;
39
+ margin-bottom: 0.5rem;
40
+ }
41
+ .badge-important {
42
+ font-size: 0.75rem;
43
+ margin-left: 0.25rem;
44
+ }
45
+ .progress {
46
+ height: 1.5rem;
47
+ }
48
+ .color-swatch {
49
+ display: inline-block;
50
+ width: 50px;
51
+ height: 50px;
52
+ border: 1px solid #dee2e6;
53
+ border-radius: 0.25rem;
54
+ margin-right: 0.5rem;
55
+ vertical-align: middle;
56
+ }
57
+ .color-row:hover {
58
+ background-color: #f8f9fa;
59
+ }
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <div class="container">
64
+ <!-- Header -->
65
+ <div class="row mb-4">
66
+ <div class="col">
67
+ <h1 class="display-4">CSS Analysis Report</h1>
68
+ <p class="lead text-muted">
69
+ Analysis of <code><%= analysis[:summary][:file_name] %></code>
70
+ </p>
71
+ <p class="text-muted">
72
+ Generated on <%= analysis[:summary][:generated_at].strftime('%B %d, %Y at %I:%M %p') %> using
73
+ <a href="https://github.com/anthropics/cataract" target="_blank">Cataract</a>
74
+ </p>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Summary Statistics -->
79
+ <div class="row mb-5">
80
+ <div class="col-md-3">
81
+ <div class="card stat-card">
82
+ <div class="card-body">
83
+ <div class="stat-number"><%= analysis[:summary][:total_rules] %></div>
84
+ <div class="stat-label">Total Rules</div>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ <div class="col-md-3">
89
+ <div class="card stat-card">
90
+ <div class="card-body">
91
+ <div class="stat-number"><%= analysis[:properties][:total_properties] %></div>
92
+ <div class="stat-label">Total Declarations</div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ <div class="col-md-3">
97
+ <div class="card stat-card">
98
+ <div class="card-body">
99
+ <div class="stat-number"><%= analysis[:properties][:unique_properties] %></div>
100
+ <div class="stat-label">Unique Properties</div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ <div class="col-md-3">
105
+ <div class="card stat-card">
106
+ <div class="card-body">
107
+ <div class="stat-number"><%= analysis[:colors][:unique_colors] %></div>
108
+ <div class="stat-label">Unique Colors</div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Analysis Tabs -->
115
+ <div class="row">
116
+ <div class="col">
117
+ <ul class="nav nav-tabs mb-4" id="analysisTabs" role="tablist">
118
+ <li class="nav-item" role="presentation">
119
+ <button class="nav-link active" id="properties-tab" data-bs-toggle="tab" data-bs-target="#properties" type="button" role="tab" aria-controls="properties" aria-selected="true">
120
+ Top Properties
121
+ </button>
122
+ </li>
123
+ <li class="nav-item" role="presentation">
124
+ <button class="nav-link" id="colors-tab" data-bs-toggle="tab" data-bs-target="#colors" type="button" role="tab" aria-controls="colors" aria-selected="false">
125
+ Colors
126
+ </button>
127
+ </li>
128
+ <li class="nav-item" role="presentation">
129
+ <button class="nav-link" id="specificity-tab" data-bs-toggle="tab" data-bs-target="#specificity" type="button" role="tab" aria-controls="specificity" aria-selected="false">
130
+ Specificity
131
+ </button>
132
+ </li>
133
+ <li class="nav-item" role="presentation">
134
+ <button class="nav-link" id="important-tab" data-bs-toggle="tab" data-bs-target="#important" type="button" role="tab" aria-controls="important" aria-selected="false">
135
+ !important
136
+ </button>
137
+ </li>
138
+ </ul>
139
+
140
+ <div class="tab-content" id="analysisTabsContent">
141
+ <!-- Top Properties Tab -->
142
+ <div class="tab-pane fade show active" id="properties" role="tabpanel" aria-labelledby="properties-tab">
143
+ <h3 class="mb-4">Top <%= options[:top] %> Most Used Properties</h3>
144
+
145
+ <div class="table-responsive">
146
+ <table class="table table-hover">
147
+ <thead class="table-light">
148
+ <tr>
149
+ <th style="width: 5%">#</th>
150
+ <th style="width: 25%">Property</th>
151
+ <th style="width: 15%">Count</th>
152
+ <th style="width: 15%">Percentage</th>
153
+ <th style="width: 40%">Usage</th>
154
+ </tr>
155
+ </thead>
156
+ <tbody>
157
+ <% analysis[:properties][:top_properties].each_with_index do |prop, index| %>
158
+ <tr class="property-row">
159
+ <td class="text-muted"><%= index + 1 %></td>
160
+ <td>
161
+ <code class="text-primary"><%= prop[:name] %></code>
162
+ </td>
163
+ <td>
164
+ <strong><%= prop[:count] %></strong>
165
+ </td>
166
+ <td>
167
+ <%= prop[:percentage] %>%
168
+ </td>
169
+ <td>
170
+ <div class="progress">
171
+ <div class="progress-bar" role="progressbar"
172
+ style="width: <%= prop[:percentage] %>%"
173
+ aria-valuenow="<%= prop[:percentage] %>"
174
+ aria-valuemin="0"
175
+ aria-valuemax="100">
176
+ </div>
177
+ </div>
178
+ </td>
179
+ </tr>
180
+ <tr>
181
+ <td></td>
182
+ <td colspan="4">
183
+ <details>
184
+ <summary class="text-muted" style="cursor: pointer;">
185
+ Show examples (<%= prop[:examples].length %>)
186
+ </summary>
187
+ <div class="mt-2">
188
+ <% prop[:examples].each do |example| %>
189
+ <div class="example-box">
190
+ <span class="text-muted"><%= example[:selector] %></span>
191
+ { <strong><%= prop[:name] %></strong>: <%= example[:value] %><% if example[:important] %><span class="badge bg-danger badge-important">!important</span><% end %>; }
192
+ <% unless example[:media].empty? %>
193
+ <span class="badge bg-secondary ms-2"><%= example[:media].join(', ') %></span>
194
+ <% end %>
195
+ </div>
196
+ <% end %>
197
+ </div>
198
+ </details>
199
+ </td>
200
+ </tr>
201
+ <% end %>
202
+ </tbody>
203
+ </table>
204
+ </div>
205
+ </div>
206
+
207
+ <!-- Colors Tab -->
208
+ <div class="tab-pane fade" id="colors" role="tabpanel" aria-labelledby="colors-tab">
209
+ <h3 class="mb-4">Color Palette (<%= analysis[:colors][:unique_colors] %> unique colors)</h3>
210
+
211
+ <div class="table-responsive">
212
+ <table class="table table-hover">
213
+ <thead class="table-light">
214
+ <tr>
215
+ <th style="width: 10%">Swatch</th>
216
+ <th style="width: 25%">Color</th>
217
+ <th style="width: 15%">Count</th>
218
+ <th style="width: 15%">Percentage</th>
219
+ <th style="width: 35%">Usage</th>
220
+ </tr>
221
+ </thead>
222
+ <tbody>
223
+ <% analysis[:colors][:colors].first(50).each do |color_data| %>
224
+ <tr class="color-row">
225
+ <td>
226
+ <div class="color-swatch" style="background-color: <%= color_data[:hex] %>" title="<%= color_data[:color] %>"></div>
227
+ </td>
228
+ <td>
229
+ <code class="text-dark"><%= color_data[:color] %></code>
230
+ </td>
231
+ <td>
232
+ <strong><%= color_data[:count] %></strong>
233
+ </td>
234
+ <td>
235
+ <%= color_data[:percentage] %>%
236
+ </td>
237
+ <td>
238
+ <div class="progress">
239
+ <div class="progress-bar bg-secondary" role="progressbar"
240
+ style="width: <%= color_data[:percentage] %>%"
241
+ aria-valuenow="<%= color_data[:percentage] %>"
242
+ aria-valuemin="0"
243
+ aria-valuemax="100">
244
+ </div>
245
+ </div>
246
+ </td>
247
+ </tr>
248
+ <tr>
249
+ <td></td>
250
+ <td colspan="4">
251
+ <details>
252
+ <summary class="text-muted" style="cursor: pointer;">
253
+ Show examples (<%= color_data[:examples].length %>)
254
+ </summary>
255
+ <div class="mt-2">
256
+ <% color_data[:examples].each do |example| %>
257
+ <div class="example-box">
258
+ <span class="text-muted"><%= example[:selector] %></span>
259
+ { <strong><%= example[:property] %></strong>: <%= example[:original_value] %>; }
260
+ <% unless example[:media].empty? %>
261
+ <span class="badge bg-secondary ms-2"><%= example[:media].join(', ') %></span>
262
+ <% end %>
263
+ </div>
264
+ <% end %>
265
+ </div>
266
+ </details>
267
+ </td>
268
+ </tr>
269
+ <% end %>
270
+ </tbody>
271
+ </table>
272
+ </div>
273
+
274
+ <% if analysis[:colors][:unique_colors] > 50 %>
275
+ <div class="alert alert-info mt-3">
276
+ Showing top 50 of <%= analysis[:colors][:unique_colors] %> colors
277
+ </div>
278
+ <% end %>
279
+ </div>
280
+
281
+ <!-- Specificity Tab -->
282
+ <div class="tab-pane fade" id="specificity" role="tabpanel" aria-labelledby="specificity-tab">
283
+ <h3 class="mb-4">Specificity Analysis</h3>
284
+
285
+ <!-- Statistics Cards -->
286
+ <div class="row mb-4">
287
+ <div class="col-md-3">
288
+ <div class="card text-center">
289
+ <div class="card-body">
290
+ <h5 class="card-title text-primary"><%= analysis[:specificity][:average_specificity] %></h5>
291
+ <p class="card-text text-muted small">Average Specificity</p>
292
+ </div>
293
+ </div>
294
+ </div>
295
+ <div class="col-md-3">
296
+ <div class="card text-center">
297
+ <div class="card-body">
298
+ <h5 class="card-title text-danger"><%= analysis[:specificity][:max_specificity] %></h5>
299
+ <p class="card-text text-muted small">Highest Specificity</p>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ <div class="col-md-3">
304
+ <div class="card text-center">
305
+ <div class="card-body">
306
+ <h5 class="card-title text-success"><%= analysis[:specificity][:min_specificity] %></h5>
307
+ <p class="card-text text-muted small">Lowest Specificity</p>
308
+ </div>
309
+ </div>
310
+ </div>
311
+ <div class="col-md-3">
312
+ <div class="card text-center">
313
+ <div class="card-body">
314
+ <h5 class="card-title text-warning"><%= analysis[:specificity][:high_specificity_count] %></h5>
315
+ <p class="card-text text-muted small">High Specificity (>100)</p>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ </div>
320
+
321
+ <!-- Specificity Distribution -->
322
+ <div class="card mb-4">
323
+ <div class="card-header">
324
+ <h5 class="mb-0">Specificity Distribution</h5>
325
+ </div>
326
+ <div class="card-body">
327
+ <div class="row text-center">
328
+ <div class="col-md-3">
329
+ <div class="mb-2">
330
+ <span class="badge bg-success" style="font-size: 1rem; padding: 0.5rem 1rem;">
331
+ <%= analysis[:specificity][:categories][:low] %>
332
+ </span>
333
+ </div>
334
+ <small class="text-muted">Low (0-10)<br>Element selectors</small>
335
+ </div>
336
+ <div class="col-md-3">
337
+ <div class="mb-2">
338
+ <span class="badge bg-info" style="font-size: 1rem; padding: 0.5rem 1rem;">
339
+ <%= analysis[:specificity][:categories][:medium] %>
340
+ </span>
341
+ </div>
342
+ <small class="text-muted">Medium (11-100)<br>Class selectors</small>
343
+ </div>
344
+ <div class="col-md-3">
345
+ <div class="mb-2">
346
+ <span class="badge bg-warning" style="font-size: 1rem; padding: 0.5rem 1rem;">
347
+ <%= analysis[:specificity][:categories][:high] %>
348
+ </span>
349
+ </div>
350
+ <small class="text-muted">High (101-1000)<br>ID selectors</small>
351
+ </div>
352
+ <div class="col-md-3">
353
+ <div class="mb-2">
354
+ <span class="badge bg-danger" style="font-size: 1rem; padding: 0.5rem 1rem;">
355
+ <%= analysis[:specificity][:categories][:very_high] %>
356
+ </span>
357
+ </div>
358
+ <small class="text-muted">Very High (>1000)<br>Multiple IDs</small>
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ <!-- Top 20 Highest Specificity Selectors -->
365
+ <h4 class="mb-3">Top 20 Highest Specificity Selectors</h4>
366
+ <div class="table-responsive">
367
+ <table class="table table-hover">
368
+ <thead class="table-light">
369
+ <tr>
370
+ <th style="width: 10%">Rank</th>
371
+ <th style="width: 15%">Specificity</th>
372
+ <th style="width: 50%">Selector</th>
373
+ <th style="width: 15%">Declarations</th>
374
+ <th style="width: 10%">Media</th>
375
+ </tr>
376
+ </thead>
377
+ <tbody>
378
+ <% analysis[:specificity][:top_20_highest].each_with_index do |item, index| %>
379
+ <%
380
+ badge_class = if item[:specificity] > 1000
381
+ 'bg-danger'
382
+ elsif item[:specificity] > 100
383
+ 'bg-warning'
384
+ elsif item[:specificity] > 10
385
+ 'bg-info'
386
+ else
387
+ 'bg-success'
388
+ end
389
+ %>
390
+ <tr>
391
+ <td class="text-muted"><%= index + 1 %></td>
392
+ <td>
393
+ <span class="badge <%= badge_class %>"><%= item[:specificity] %></span>
394
+ </td>
395
+ <td>
396
+ <code style="font-size: 0.875rem;"><%= item[:selector] %></code>
397
+ </td>
398
+ <td class="text-center">
399
+ <%= item[:declaration_count] %>
400
+ </td>
401
+ <td>
402
+ <% unless item[:media].empty? %>
403
+ <span class="badge bg-secondary" style="font-size: 0.75rem;"><%= item[:media].first %></span>
404
+ <% end %>
405
+ </td>
406
+ </tr>
407
+ <% end %>
408
+ </tbody>
409
+ </table>
410
+ </div>
411
+ </div>
412
+
413
+ <!-- !important Tab -->
414
+ <div class="tab-pane fade" id="important" role="tabpanel" aria-labelledby="important-tab">
415
+ <h3 class="mb-4">!important Usage Analysis</h3>
416
+
417
+ <!-- Statistics Cards -->
418
+ <div class="row mb-4">
419
+ <div class="col-md-3">
420
+ <div class="card text-center">
421
+ <div class="card-body">
422
+ <h5 class="card-title text-danger"><%= analysis[:important][:important_count] %></h5>
423
+ <p class="card-text text-muted small">!important Declarations</p>
424
+ </div>
425
+ </div>
426
+ </div>
427
+ <div class="col-md-3">
428
+ <div class="card text-center">
429
+ <div class="card-body">
430
+ <h5 class="card-title text-warning"><%= analysis[:important][:important_percentage] %>%</h5>
431
+ <p class="card-text text-muted small">Of All Declarations</p>
432
+ </div>
433
+ </div>
434
+ </div>
435
+ <div class="col-md-3">
436
+ <div class="card text-center">
437
+ <div class="card-body">
438
+ <h5 class="card-title text-info"><%= analysis[:important][:properties_using_important] %></h5>
439
+ <p class="card-text text-muted small">Properties Affected</p>
440
+ </div>
441
+ </div>
442
+ </div>
443
+ <div class="col-md-3">
444
+ <div class="card text-center">
445
+ <div class="card-body">
446
+ <h5 class="card-title text-primary"><%= analysis[:important][:selectors_using_important] %></h5>
447
+ <p class="card-text text-muted small">Selectors Using It</p>
448
+ </div>
449
+ </div>
450
+ </div>
451
+ </div>
452
+
453
+ <!-- Top Properties Using !important -->
454
+ <h4 class="mb-3 mt-4">Top Properties Using !important</h4>
455
+ <div class="table-responsive">
456
+ <table class="table table-hover">
457
+ <thead class="table-light">
458
+ <tr>
459
+ <th style="width: 10%">Rank</th>
460
+ <th style="width: 40%">Property</th>
461
+ <th style="width: 20%">Count</th>
462
+ <th style="width: 30%">% of !important</th>
463
+ </tr>
464
+ </thead>
465
+ <tbody>
466
+ <% analysis[:important][:top_properties].first(10).each_with_index do |prop, index| %>
467
+ <tr>
468
+ <td class="text-muted"><%= index + 1 %></td>
469
+ <td>
470
+ <code class="text-primary"><%= prop[:property] %></code>
471
+ </td>
472
+ <td>
473
+ <strong><%= prop[:count] %></strong>
474
+ </td>
475
+ <td>
476
+ <div class="progress">
477
+ <div class="progress-bar bg-danger" role="progressbar"
478
+ style="width: <%= prop[:percentage] %>%"
479
+ aria-valuenow="<%= prop[:percentage] %>"
480
+ aria-valuemin="0"
481
+ aria-valuemax="100">
482
+ <%= prop[:percentage] %>%
483
+ </div>
484
+ </div>
485
+ </td>
486
+ </tr>
487
+ <% end %>
488
+ </tbody>
489
+ </table>
490
+ </div>
491
+
492
+ <!-- Top Selectors Using !important -->
493
+ <h4 class="mb-3 mt-4">Top Selectors Using !important</h4>
494
+ <div class="table-responsive">
495
+ <table class="table table-hover">
496
+ <thead class="table-light">
497
+ <tr>
498
+ <th style="width: 10%">Rank</th>
499
+ <th style="width: 70%">Selector</th>
500
+ <th style="width: 20%">!important Count</th>
501
+ </tr>
502
+ </thead>
503
+ <tbody>
504
+ <% analysis[:important][:top_selectors].first(20).each_with_index do |sel, index| %>
505
+ <tr>
506
+ <td class="text-muted"><%= index + 1 %></td>
507
+ <td>
508
+ <code style="font-size: 0.875rem;"><%= sel[:selector] %></code>
509
+ </td>
510
+ <td>
511
+ <span class="badge bg-danger"><%= sel[:count] %></span>
512
+ </td>
513
+ </tr>
514
+ <% end %>
515
+ </tbody>
516
+ </table>
517
+ </div>
518
+
519
+ <!-- All !important Declarations -->
520
+ <% if analysis[:important][:important_count] <= 50 %>
521
+ <h4 class="mb-3 mt-4">All !important Declarations (<%= analysis[:important][:important_count] %>)</h4>
522
+ <div class="table-responsive">
523
+ <table class="table table-hover table-sm">
524
+ <thead class="table-light">
525
+ <tr>
526
+ <th style="width: 40%">Selector</th>
527
+ <th style="width: 25%">Property</th>
528
+ <th style="width: 25%">Value</th>
529
+ <th style="width: 10%">Media</th>
530
+ </tr>
531
+ </thead>
532
+ <tbody>
533
+ <% analysis[:important][:all_important].each do |item| %>
534
+ <tr>
535
+ <td>
536
+ <code style="font-size: 0.75rem;"><%= item[:selector] %></code>
537
+ </td>
538
+ <td>
539
+ <code class="text-primary"><%= item[:property] %></code>
540
+ </td>
541
+ <td>
542
+ <code class="text-muted" style="font-size: 0.75rem;"><%= item[:value] %></code>
543
+ </td>
544
+ <td>
545
+ <% unless item[:media].empty? %>
546
+ <span class="badge bg-secondary" style="font-size: 0.65rem;"><%= item[:media].first %></span>
547
+ <% end %>
548
+ </td>
549
+ </tr>
550
+ <% end %>
551
+ </tbody>
552
+ </table>
553
+ </div>
554
+ <% else %>
555
+ <div class="alert alert-info mt-4">
556
+ <%= analysis[:important][:important_count] %> !important declarations found. Showing top properties and selectors above.
557
+ </div>
558
+ <% end %>
559
+ </div>
560
+ </div>
561
+ </div>
562
+ </div>
563
+
564
+ <!-- Footer -->
565
+ <footer class="mt-5 pt-4 border-top text-center text-muted">
566
+ <p>
567
+ Generated by <a href="https://github.com/anthropics/cataract" target="_blank">Cataract CSS Parser</a>
568
+ </p>
569
+ </footer>
570
+ </div>
571
+
572
+ <!-- Bootstrap JS (optional, for interactive components) -->
573
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
574
+ </body>
575
+ </html>
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require_relative 'css_analyzer/analyzer'
6
+
7
+ # CLI Interface
8
+ if __FILE__ == $PROGRAM_NAME
9
+ options = {}
10
+
11
+ OptionParser.new do |opts|
12
+ opts.banner = 'Usage: css_analyzer.rb [options] URL_OR_FILE'
13
+ opts.separator ''
14
+ opts.separator 'Analyze CSS from a file, URL, or website:'
15
+ opts.separator ' - Local file: css_analyzer.rb styles.css'
16
+ opts.separator ' - CSS URL: css_analyzer.rb https://example.com/styles.css'
17
+ opts.separator ' - Website: css_analyzer.rb https://example.com (analyzes all CSS)'
18
+ opts.separator ''
19
+
20
+ opts.on('-t', '--top N', Integer, 'Show top N properties (default: 20)') do |n|
21
+ options[:top] = n
22
+ end
23
+
24
+ opts.on('-o', '--output FILE', 'Write report to FILE instead of stdout') do |file|
25
+ options[:output] = file
26
+ end
27
+
28
+ opts.on('--use-shim', 'Use Cataract shim for css_parser (for Premailer)') do
29
+ options[:use_shim] = true
30
+ end
31
+
32
+ opts.on('-h', '--help', 'Show this help message') do
33
+ puts opts
34
+ exit
35
+ end
36
+ end.parse!
37
+
38
+ # Check for ENV var to enable shim
39
+ options[:use_shim] = true if ENV['CATARACT_SHIM']
40
+
41
+ # Check for required argument
42
+ if ARGV.empty?
43
+ warn 'Error: No URL or file specified'
44
+ warn 'Usage: css_analyzer.rb [options] URL_OR_FILE'
45
+ exit 1
46
+ end
47
+
48
+ source = ARGV[0]
49
+
50
+ # Run analyzer
51
+ total_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
52
+ analyzer = CSSAnalyzer::Analyzer.new(source, options)
53
+
54
+ analysis_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
55
+ analyzer.save_report
56
+
57
+ # Output timing information to stderr
58
+ total_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - total_start
59
+ analysis_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - analysis_start
60
+
61
+ warn "\n=== Timing ==="
62
+ if analyzer.timings[:fetch]
63
+ warn "Fetch webpage: #{format('%.3f', analyzer.timings[:fetch])}s"
64
+ warn "Premailer parse: #{format('%.3f', analyzer.timings[:premailer_parse])}s"
65
+ warn "Cataract parse: #{format('%.3f', analyzer.timings[:cataract_parse])}s"
66
+ end
67
+ warn "Analysis & report: #{format('%.3f', analysis_time)}s"
68
+ warn "Total: #{format('%.3f', total_time)}s"
69
+ end