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.
- checksums.yaml +7 -0
- data/.clang-tidy +30 -0
- data/.github/workflows/ci-macos.yml +12 -0
- data/.github/workflows/ci.yml +77 -0
- data/.github/workflows/test.yml +76 -0
- data/.gitignore +45 -0
- data/.overcommit.yml +38 -0
- data/.rubocop.yml +83 -0
- data/BENCHMARKS.md +201 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +27 -0
- data/LICENSE +21 -0
- data/RAGEL_MIGRATION.md +60 -0
- data/README.md +292 -0
- data/Rakefile +209 -0
- data/benchmarks/benchmark_harness.rb +193 -0
- data/benchmarks/benchmark_merging.rb +121 -0
- data/benchmarks/benchmark_optimization_comparison.rb +168 -0
- data/benchmarks/benchmark_parsing.rb +153 -0
- data/benchmarks/benchmark_ragel_removal.rb +56 -0
- data/benchmarks/benchmark_runner.rb +70 -0
- data/benchmarks/benchmark_serialization.rb +180 -0
- data/benchmarks/benchmark_shorthand.rb +109 -0
- data/benchmarks/benchmark_shorthand_expansion.rb +176 -0
- data/benchmarks/benchmark_specificity.rb +124 -0
- data/benchmarks/benchmark_string_allocation.rb +151 -0
- data/benchmarks/benchmark_stylesheet_to_s.rb +62 -0
- data/benchmarks/benchmark_to_s_cached.rb +55 -0
- data/benchmarks/benchmark_value_splitter.rb +54 -0
- data/benchmarks/benchmark_yjit.rb +158 -0
- data/benchmarks/benchmark_yjit_workers.rb +61 -0
- data/benchmarks/profile_to_s.rb +23 -0
- data/benchmarks/speedup_calculator.rb +83 -0
- data/benchmarks/system_metadata.rb +81 -0
- data/benchmarks/templates/benchmarks.md.erb +221 -0
- data/benchmarks/yjit_tests.rb +141 -0
- data/cataract.gemspec +34 -0
- data/cliff.toml +92 -0
- data/examples/color_conversion_visual_test/color_conversion_test.html +3603 -0
- data/examples/color_conversion_visual_test/generate.rb +202 -0
- data/examples/color_conversion_visual_test/template.html.erb +259 -0
- data/examples/css_analyzer/analyzer.rb +164 -0
- data/examples/css_analyzer/analyzers/base.rb +33 -0
- data/examples/css_analyzer/analyzers/colors.rb +133 -0
- data/examples/css_analyzer/analyzers/important.rb +88 -0
- data/examples/css_analyzer/analyzers/properties.rb +61 -0
- data/examples/css_analyzer/analyzers/specificity.rb +68 -0
- data/examples/css_analyzer/templates/report.html.erb +575 -0
- data/examples/css_analyzer.rb +69 -0
- data/examples/github_analysis.html +5343 -0
- data/ext/cataract/cataract.c +1086 -0
- data/ext/cataract/cataract.h +174 -0
- data/ext/cataract/css_parser.c +1435 -0
- data/ext/cataract/extconf.rb +48 -0
- data/ext/cataract/import_scanner.c +174 -0
- data/ext/cataract/merge.c +973 -0
- data/ext/cataract/shorthand_expander.c +902 -0
- data/ext/cataract/specificity.c +213 -0
- data/ext/cataract/value_splitter.c +116 -0
- data/ext/cataract_color/cataract_color.c +16 -0
- data/ext/cataract_color/color_conversion.c +1687 -0
- data/ext/cataract_color/color_conversion.h +136 -0
- data/ext/cataract_color/color_conversion_lab.c +571 -0
- data/ext/cataract_color/color_conversion_named.c +259 -0
- data/ext/cataract_color/color_conversion_oklab.c +547 -0
- data/ext/cataract_color/extconf.rb +23 -0
- data/ext/cataract_old/cataract.c +393 -0
- data/ext/cataract_old/cataract.h +250 -0
- data/ext/cataract_old/css_parser.c +933 -0
- data/ext/cataract_old/extconf.rb +67 -0
- data/ext/cataract_old/import_scanner.c +174 -0
- data/ext/cataract_old/merge.c +776 -0
- data/ext/cataract_old/shorthand_expander.c +902 -0
- data/ext/cataract_old/specificity.c +213 -0
- data/ext/cataract_old/stylesheet.c +290 -0
- data/ext/cataract_old/value_splitter.c +116 -0
- data/lib/cataract/at_rule.rb +97 -0
- data/lib/cataract/color_conversion.rb +18 -0
- data/lib/cataract/declarations.rb +332 -0
- data/lib/cataract/import_resolver.rb +210 -0
- data/lib/cataract/rule.rb +131 -0
- data/lib/cataract/stylesheet.rb +716 -0
- data/lib/cataract/stylesheet_scope.rb +257 -0
- data/lib/cataract/version.rb +5 -0
- data/lib/cataract.rb +107 -0
- data/lib/tasks/gem.rake +158 -0
- data/scripts/fuzzer/run.rb +828 -0
- data/scripts/fuzzer/worker.rb +99 -0
- data/scripts/generate_benchmarks_md.rb +155 -0
- metadata +135 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Visual test generator for Cataract color conversions
|
|
5
|
+
# Generates an HTML file showing color conversions across all supported formats
|
|
6
|
+
|
|
7
|
+
require 'erb'
|
|
8
|
+
require 'fileutils'
|
|
9
|
+
|
|
10
|
+
# Add lib to load path
|
|
11
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
12
|
+
require 'cataract'
|
|
13
|
+
|
|
14
|
+
# Sample of named colors to test (diverse hues, saturations, lightness)
|
|
15
|
+
SAMPLE_COLORS = %w[
|
|
16
|
+
red
|
|
17
|
+
green
|
|
18
|
+
blue
|
|
19
|
+
yellow
|
|
20
|
+
cyan
|
|
21
|
+
magenta
|
|
22
|
+
white
|
|
23
|
+
black
|
|
24
|
+
gray
|
|
25
|
+
silver
|
|
26
|
+
maroon
|
|
27
|
+
olive
|
|
28
|
+
lime
|
|
29
|
+
aqua
|
|
30
|
+
teal
|
|
31
|
+
navy
|
|
32
|
+
fuchsia
|
|
33
|
+
purple
|
|
34
|
+
orange
|
|
35
|
+
pink
|
|
36
|
+
coral
|
|
37
|
+
tomato
|
|
38
|
+
gold
|
|
39
|
+
indigo
|
|
40
|
+
violet
|
|
41
|
+
brown
|
|
42
|
+
tan
|
|
43
|
+
khaki
|
|
44
|
+
salmon
|
|
45
|
+
crimson
|
|
46
|
+
chocolate
|
|
47
|
+
peru
|
|
48
|
+
sienna
|
|
49
|
+
steelblue
|
|
50
|
+
skyblue
|
|
51
|
+
turquoise
|
|
52
|
+
orchid
|
|
53
|
+
plum
|
|
54
|
+
lavender
|
|
55
|
+
].freeze
|
|
56
|
+
|
|
57
|
+
def convert_color(color_str, from_format, to_format)
|
|
58
|
+
css = ".test { color: #{color_str}; }"
|
|
59
|
+
sheet = Cataract.parse_css(css)
|
|
60
|
+
sheet.convert_colors!(from: from_format, to: to_format)
|
|
61
|
+
decls = Cataract::Declarations.new(sheet.declarations)
|
|
62
|
+
decls['color']
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def auto_detect_convert(color_str, to_format)
|
|
66
|
+
css = ".test { color: #{color_str}; }"
|
|
67
|
+
sheet = Cataract.parse_css(css)
|
|
68
|
+
sheet.convert_colors!(to: to_format)
|
|
69
|
+
decls = Cataract::Declarations.new(sheet.declarations)
|
|
70
|
+
decls['color']
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def hex_difference(hex1, hex2)
|
|
74
|
+
# Parse hex colors and calculate RGB distance
|
|
75
|
+
r1, g1, b1 = hex1.scan(/[0-9a-f]{2}/i).map { |h| h.to_i(16) }
|
|
76
|
+
r2, g2, b2 = hex2.scan(/[0-9a-f]{2}/i).map { |h| h.to_i(16) }
|
|
77
|
+
|
|
78
|
+
Math.sqrt(((r1 - r2)**2) + ((g1 - g2)**2) + ((b1 - b2)**2))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Generate forward conversion tests (named → rgb → hwb → hsl → oklab → lab → lch)
|
|
82
|
+
def generate_forward_tests(colors)
|
|
83
|
+
colors.map do |color_name|
|
|
84
|
+
# Start with named color
|
|
85
|
+
named = color_name
|
|
86
|
+
|
|
87
|
+
# Convert through each format in sequence
|
|
88
|
+
rgb = convert_color(named, :named, :rgb)
|
|
89
|
+
hwb = convert_color(rgb, :rgb, :hwb)
|
|
90
|
+
hsl = convert_color(hwb, :hwb, :hsl)
|
|
91
|
+
oklab = convert_color(hsl, :hsl, :oklab)
|
|
92
|
+
lab = convert_color(oklab, :oklab, :lab)
|
|
93
|
+
lch = convert_color(lab, :lab, :lch)
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
name: color_name,
|
|
97
|
+
named: named,
|
|
98
|
+
rgb: rgb,
|
|
99
|
+
hwb: hwb,
|
|
100
|
+
hsl: hsl,
|
|
101
|
+
oklab: oklab,
|
|
102
|
+
lab: lab,
|
|
103
|
+
lch: lch
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Generate reverse conversion tests (lch → lab → oklab → hsl → hwb → rgb → hex)
|
|
109
|
+
def generate_reverse_tests(colors)
|
|
110
|
+
colors.map do |color_name|
|
|
111
|
+
# Get original hex value
|
|
112
|
+
original_hex = convert_color(color_name, :named, :hex)
|
|
113
|
+
|
|
114
|
+
# Convert to LCH first
|
|
115
|
+
lch = convert_color(color_name, :named, :lch)
|
|
116
|
+
|
|
117
|
+
# Convert back through the chain
|
|
118
|
+
lab = convert_color(lch, :lch, :lab)
|
|
119
|
+
oklab = convert_color(lab, :lab, :oklab)
|
|
120
|
+
hsl = convert_color(oklab, :oklab, :hsl)
|
|
121
|
+
hwb = convert_color(hsl, :hsl, :hwb)
|
|
122
|
+
rgb = convert_color(hwb, :hwb, :rgb)
|
|
123
|
+
hex = convert_color(rgb, :rgb, :hex)
|
|
124
|
+
|
|
125
|
+
# Calculate match quality
|
|
126
|
+
diff = hex_difference(original_hex, hex)
|
|
127
|
+
match_class = if diff.zero?
|
|
128
|
+
'perfect'
|
|
129
|
+
elsif diff < 2
|
|
130
|
+
'close'
|
|
131
|
+
else
|
|
132
|
+
'off'
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
match_symbol = case match_class
|
|
136
|
+
when 'perfect' then '✓'
|
|
137
|
+
when 'close' then '≈'
|
|
138
|
+
else '✗'
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
name: color_name,
|
|
143
|
+
original_hex: original_hex,
|
|
144
|
+
lch: lch,
|
|
145
|
+
lab: lab,
|
|
146
|
+
oklab: oklab,
|
|
147
|
+
hsl: hsl,
|
|
148
|
+
hwb: hwb,
|
|
149
|
+
rgb: rgb,
|
|
150
|
+
hex: hex,
|
|
151
|
+
match_class: match_class,
|
|
152
|
+
match_symbol: match_symbol,
|
|
153
|
+
diff: diff
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Calculate statistics
|
|
159
|
+
def calculate_stats(forward_tests, reverse_tests)
|
|
160
|
+
perfect = reverse_tests.count { |t| t[:match_class] == 'perfect' }
|
|
161
|
+
close = reverse_tests.count { |t| t[:match_class] == 'close' }
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
total_tests: forward_tests.length,
|
|
165
|
+
perfect_matches: perfect,
|
|
166
|
+
close_matches: close,
|
|
167
|
+
formats_tested: 7 # hex, rgb, hwb, hsl, oklab, lab, lch
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Main execution
|
|
172
|
+
puts 'Generating color conversion visual test...'
|
|
173
|
+
puts "Testing #{SAMPLE_COLORS.length} colors..."
|
|
174
|
+
|
|
175
|
+
# Generate test data
|
|
176
|
+
forward_tests = generate_forward_tests(SAMPLE_COLORS)
|
|
177
|
+
puts '✓ Generated forward conversion tests'
|
|
178
|
+
|
|
179
|
+
reverse_tests = generate_reverse_tests(SAMPLE_COLORS)
|
|
180
|
+
puts '✓ Generated reverse conversion tests'
|
|
181
|
+
|
|
182
|
+
stats = calculate_stats(forward_tests, reverse_tests)
|
|
183
|
+
puts '✓ Calculated statistics'
|
|
184
|
+
|
|
185
|
+
# Load ERB template
|
|
186
|
+
template_path = File.join(__dir__, 'template.html.erb')
|
|
187
|
+
template = ERB.new(File.read(template_path), trim_mode: '-')
|
|
188
|
+
|
|
189
|
+
# Render HTML
|
|
190
|
+
html = template.result(binding)
|
|
191
|
+
|
|
192
|
+
# Write output
|
|
193
|
+
output_path = File.join(__dir__, 'color_conversion_test.html')
|
|
194
|
+
File.write(output_path, html)
|
|
195
|
+
|
|
196
|
+
puts "\n✓ Generated: #{output_path}"
|
|
197
|
+
puts "\nStatistics:"
|
|
198
|
+
puts " Total colors tested: #{stats[:total_tests]}"
|
|
199
|
+
puts " Perfect round-trips: #{stats[:perfect_matches]}"
|
|
200
|
+
puts " Close matches: #{stats[:close_matches]}"
|
|
201
|
+
puts " Formats: #{stats[:formats_tested]}"
|
|
202
|
+
puts "\nOpen #{output_path} in a browser to view the results."
|
|
@@ -0,0 +1,259 @@
|
|
|
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>Cataract Color Conversion Visual Test</title>
|
|
7
|
+
|
|
8
|
+
<!-- Bootstrap CSS -->
|
|
9
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
10
|
+
|
|
11
|
+
<style>
|
|
12
|
+
body {
|
|
13
|
+
background: #f8f9fa;
|
|
14
|
+
}
|
|
15
|
+
.color-cell {
|
|
16
|
+
width: 80px;
|
|
17
|
+
height: 40px;
|
|
18
|
+
border: 1px solid #dee2e6;
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
display: inline-block;
|
|
21
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
22
|
+
cursor: help;
|
|
23
|
+
}
|
|
24
|
+
.match-indicator {
|
|
25
|
+
font-size: 1.5em;
|
|
26
|
+
}
|
|
27
|
+
.match-indicator.perfect { color: #198754; }
|
|
28
|
+
.match-indicator.close { color: #ffc107; }
|
|
29
|
+
.match-indicator.off { color: #dc3545; }
|
|
30
|
+
.stat-value {
|
|
31
|
+
font-size: 2.5em;
|
|
32
|
+
font-weight: bold;
|
|
33
|
+
color: #0d6efd;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="container-fluid py-5">
|
|
39
|
+
<h1 class="mb-4">🎨 Cataract Color Conversion Visual Test</h1>
|
|
40
|
+
|
|
41
|
+
<div class="card mb-4">
|
|
42
|
+
<div class="card-body">
|
|
43
|
+
<p><strong>Purpose:</strong> This page visually validates color conversions across different CSS color formats.</p>
|
|
44
|
+
<p>If conversions are correct, all color swatches in each row should appear identical.</p>
|
|
45
|
+
<p><strong>Tip:</strong> Hover over any color swatch to see the CSS color value.</p>
|
|
46
|
+
<p class="mb-0"><strong>Generated:</strong> <%= Time.now.strftime('%Y-%m-%d %H:%M:%S') %></p>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Tabs for Forward/Reverse Conversion -->
|
|
51
|
+
<ul class="nav nav-tabs mb-3" id="conversionTabs" role="tablist">
|
|
52
|
+
<li class="nav-item" role="presentation">
|
|
53
|
+
<button class="nav-link active" id="forward-tab" data-bs-toggle="tab" data-bs-target="#forward" type="button" role="tab" aria-controls="forward" aria-selected="true">
|
|
54
|
+
Forward: Named → RGB → HWB → HSL → Oklab → Lab → LCH
|
|
55
|
+
</button>
|
|
56
|
+
</li>
|
|
57
|
+
<li class="nav-item" role="presentation">
|
|
58
|
+
<button class="nav-link" id="reverse-tab" data-bs-toggle="tab" data-bs-target="#reverse" type="button" role="tab" aria-controls="reverse" aria-selected="false">
|
|
59
|
+
Reverse: LCH → Lab → Oklab → HSL → HWB → RGB → Hex
|
|
60
|
+
</button>
|
|
61
|
+
</li>
|
|
62
|
+
</ul>
|
|
63
|
+
|
|
64
|
+
<div class="tab-content mb-5" id="conversionTabContent">
|
|
65
|
+
<!-- Forward conversion tab -->
|
|
66
|
+
<div class="tab-pane fade show active" id="forward" role="tabpanel" aria-labelledby="forward-tab">
|
|
67
|
+
<p class="text-muted mb-3">Starting from named colors, converting through each format. All swatches should match.</p>
|
|
68
|
+
|
|
69
|
+
<div class="table-responsive">
|
|
70
|
+
<table class="table table-striped table-hover">
|
|
71
|
+
<thead class="table-light">
|
|
72
|
+
<tr>
|
|
73
|
+
<th>Color Name</th>
|
|
74
|
+
<th>Named</th>
|
|
75
|
+
<th>RGB</th>
|
|
76
|
+
<th>HWB</th>
|
|
77
|
+
<th>HSL</th>
|
|
78
|
+
<th>Oklab</th>
|
|
79
|
+
<th>Lab</th>
|
|
80
|
+
<th>LCH</th>
|
|
81
|
+
</tr>
|
|
82
|
+
</thead>
|
|
83
|
+
<tbody>
|
|
84
|
+
<% forward_tests.each do |test| %>
|
|
85
|
+
<tr>
|
|
86
|
+
<td class="fw-bold"><%= test[:name] %></td>
|
|
87
|
+
<td>
|
|
88
|
+
<div class="color-cell" style="background-color: <%= test[:named] %>;"
|
|
89
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
90
|
+
title="<%= test[:named] %>"></div>
|
|
91
|
+
</td>
|
|
92
|
+
<td>
|
|
93
|
+
<div class="color-cell" style="background-color: <%= test[:rgb] %>;"
|
|
94
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
95
|
+
title="<%= test[:rgb] %>"></div>
|
|
96
|
+
</td>
|
|
97
|
+
<td>
|
|
98
|
+
<div class="color-cell" style="background-color: <%= test[:hwb] %>;"
|
|
99
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
100
|
+
title="<%= test[:hwb] %>"></div>
|
|
101
|
+
</td>
|
|
102
|
+
<td>
|
|
103
|
+
<div class="color-cell" style="background-color: <%= test[:hsl] %>;"
|
|
104
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
105
|
+
title="<%= test[:hsl] %>"></div>
|
|
106
|
+
</td>
|
|
107
|
+
<td>
|
|
108
|
+
<div class="color-cell" style="background-color: <%= test[:oklab] %>;"
|
|
109
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
110
|
+
title="<%= test[:oklab] %>"></div>
|
|
111
|
+
</td>
|
|
112
|
+
<td>
|
|
113
|
+
<div class="color-cell" style="background-color: <%= test[:lab] %>;"
|
|
114
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
115
|
+
title="<%= test[:lab] %>"></div>
|
|
116
|
+
</td>
|
|
117
|
+
<td>
|
|
118
|
+
<div class="color-cell" style="background-color: <%= test[:lch] %>;"
|
|
119
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
120
|
+
title="<%= test[:lch] %>"></div>
|
|
121
|
+
</td>
|
|
122
|
+
</tr>
|
|
123
|
+
<% end %>
|
|
124
|
+
</tbody>
|
|
125
|
+
</table>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Reverse conversion tab -->
|
|
130
|
+
<div class="tab-pane fade" id="reverse" role="tabpanel" aria-labelledby="reverse-tab">
|
|
131
|
+
<p class="text-muted mb-3">Starting from LCH, converting back through each format. Some precision loss expected but should be visually close.</p>
|
|
132
|
+
|
|
133
|
+
<div class="table-responsive">
|
|
134
|
+
<table class="table table-striped table-hover">
|
|
135
|
+
<thead class="table-light">
|
|
136
|
+
<tr>
|
|
137
|
+
<th>Original</th>
|
|
138
|
+
<th>LCH</th>
|
|
139
|
+
<th>Lab</th>
|
|
140
|
+
<th>Oklab</th>
|
|
141
|
+
<th>HSL</th>
|
|
142
|
+
<th>HWB</th>
|
|
143
|
+
<th>RGB</th>
|
|
144
|
+
<th>Hex</th>
|
|
145
|
+
<th>Match</th>
|
|
146
|
+
</tr>
|
|
147
|
+
</thead>
|
|
148
|
+
<tbody>
|
|
149
|
+
<% reverse_tests.each do |test| %>
|
|
150
|
+
<tr>
|
|
151
|
+
<td>
|
|
152
|
+
<div class="color-cell" style="background-color: <%= test[:original_hex] %>;"
|
|
153
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
154
|
+
title="<%= test[:original_hex] %>"></div>
|
|
155
|
+
<div class="small fw-bold mt-1"><%= test[:name] %></div>
|
|
156
|
+
</td>
|
|
157
|
+
<td>
|
|
158
|
+
<div class="color-cell" style="background-color: <%= test[:lch] %>;"
|
|
159
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
160
|
+
title="<%= test[:lch] %>"></div>
|
|
161
|
+
</td>
|
|
162
|
+
<td>
|
|
163
|
+
<div class="color-cell" style="background-color: <%= test[:lab] %>;"
|
|
164
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
165
|
+
title="<%= test[:lab] %>"></div>
|
|
166
|
+
</td>
|
|
167
|
+
<td>
|
|
168
|
+
<div class="color-cell" style="background-color: <%= test[:oklab] %>;"
|
|
169
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
170
|
+
title="<%= test[:oklab] %>"></div>
|
|
171
|
+
</td>
|
|
172
|
+
<td>
|
|
173
|
+
<div class="color-cell" style="background-color: <%= test[:hsl] %>;"
|
|
174
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
175
|
+
title="<%= test[:hsl] %>"></div>
|
|
176
|
+
</td>
|
|
177
|
+
<td>
|
|
178
|
+
<div class="color-cell" style="background-color: <%= test[:hwb] %>;"
|
|
179
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
180
|
+
title="<%= test[:hwb] %>"></div>
|
|
181
|
+
</td>
|
|
182
|
+
<td>
|
|
183
|
+
<div class="color-cell" style="background-color: <%= test[:rgb] %>;"
|
|
184
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
185
|
+
title="<%= test[:rgb] %>"></div>
|
|
186
|
+
</td>
|
|
187
|
+
<td>
|
|
188
|
+
<div class="color-cell" style="background-color: <%= test[:hex] %>;"
|
|
189
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
190
|
+
title="<%= test[:hex] %>"></div>
|
|
191
|
+
</td>
|
|
192
|
+
<td class="match-indicator <%= test[:match_class] %>"
|
|
193
|
+
data-bs-toggle="tooltip" data-bs-placement="top"
|
|
194
|
+
title="RGB distance: <%= '%.2f' % test[:diff] %>">
|
|
195
|
+
<%= test[:match_symbol] %>
|
|
196
|
+
</td>
|
|
197
|
+
</tr>
|
|
198
|
+
<% end %>
|
|
199
|
+
</tbody>
|
|
200
|
+
</table>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<!-- Statistics -->
|
|
206
|
+
<div class="mb-5">
|
|
207
|
+
<h2 class="mb-3">Conversion Statistics</h2>
|
|
208
|
+
<div class="row g-4">
|
|
209
|
+
<div class="col-md-3">
|
|
210
|
+
<div class="card text-center">
|
|
211
|
+
<div class="card-body">
|
|
212
|
+
<div class="stat-value"><%= stats[:total_tests] %></div>
|
|
213
|
+
<div class="text-muted">Total Tests</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="col-md-3">
|
|
218
|
+
<div class="card text-center">
|
|
219
|
+
<div class="card-body">
|
|
220
|
+
<div class="stat-value"><%= stats[:perfect_matches] %></div>
|
|
221
|
+
<div class="text-muted">Perfect Round-trips</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="col-md-3">
|
|
226
|
+
<div class="card text-center">
|
|
227
|
+
<div class="card-body">
|
|
228
|
+
<div class="stat-value"><%= stats[:close_matches] %></div>
|
|
229
|
+
<div class="text-muted">Close Matches</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="col-md-3">
|
|
234
|
+
<div class="card text-center">
|
|
235
|
+
<div class="card-body">
|
|
236
|
+
<div class="stat-value"><%= stats[:formats_tested] %></div>
|
|
237
|
+
<div class="text-muted">Formats Tested</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<footer class="text-center text-muted mt-5">
|
|
245
|
+
Generated by Cataract Color Conversion Test Suite
|
|
246
|
+
</footer>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<!-- Bootstrap Bundle with Popper -->
|
|
250
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
251
|
+
|
|
252
|
+
<!-- Initialize tooltips -->
|
|
253
|
+
<script>
|
|
254
|
+
// Enable all tooltips
|
|
255
|
+
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
|
256
|
+
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
|
257
|
+
</script>
|
|
258
|
+
</body>
|
|
259
|
+
</html>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require_relative '../../lib/cataract'
|
|
6
|
+
require_relative 'analyzers/properties'
|
|
7
|
+
require_relative 'analyzers/colors'
|
|
8
|
+
require_relative 'analyzers/specificity'
|
|
9
|
+
require_relative 'analyzers/important'
|
|
10
|
+
|
|
11
|
+
module CSSAnalyzer
|
|
12
|
+
# Main analyzer orchestrator that coordinates all analysis modules
|
|
13
|
+
class Analyzer
|
|
14
|
+
attr_reader :stylesheet, :source, :options, :timings
|
|
15
|
+
|
|
16
|
+
def initialize(source, options = {})
|
|
17
|
+
@source = source
|
|
18
|
+
@options = {
|
|
19
|
+
top: 20,
|
|
20
|
+
use_shim: false
|
|
21
|
+
}.merge(options)
|
|
22
|
+
@timings = {}
|
|
23
|
+
|
|
24
|
+
# Load shim if requested
|
|
25
|
+
if @options[:use_shim]
|
|
26
|
+
require_relative '../../lib/cataract/css_parser_compat'
|
|
27
|
+
Cataract.mimic_CssParser!
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Load CSS based on source type
|
|
31
|
+
@stylesheet = load_css(source)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Run all analyses and collect results
|
|
35
|
+
def analyze_all
|
|
36
|
+
{
|
|
37
|
+
summary: analyze_summary,
|
|
38
|
+
properties: Analyzers::Properties.new(stylesheet, options).analyze,
|
|
39
|
+
colors: Analyzers::Colors.new(stylesheet, options).analyze,
|
|
40
|
+
specificity: Analyzers::Specificity.new(stylesheet, options).analyze,
|
|
41
|
+
important: Analyzers::Important.new(stylesheet, options).analyze
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generate summary statistics
|
|
46
|
+
def analyze_summary
|
|
47
|
+
{
|
|
48
|
+
total_rules: stylesheet.rules_count,
|
|
49
|
+
file_name: source_name,
|
|
50
|
+
file_path: source,
|
|
51
|
+
generated_at: Time.now
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generate HTML report
|
|
56
|
+
def generate_report
|
|
57
|
+
analysis = analyze_all
|
|
58
|
+
template = ERB.new(template_content, trim_mode: '-')
|
|
59
|
+
template.result(binding)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Save report to file or stdout
|
|
63
|
+
def save_report
|
|
64
|
+
report = generate_report
|
|
65
|
+
|
|
66
|
+
if options[:output]
|
|
67
|
+
File.write(options[:output], report)
|
|
68
|
+
puts "Report saved to #{options[:output]}"
|
|
69
|
+
else
|
|
70
|
+
puts report
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Also save the parsed CSS for debugging
|
|
74
|
+
save_parsed_css
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Save parsed CSS to a file for debugging/comparison
|
|
78
|
+
def save_parsed_css
|
|
79
|
+
# Generate a unique filename based on source and shim usage
|
|
80
|
+
source_slug = @source.gsub(%r{[:/]}, '_').gsub(/[^a-zA-Z0-9_.-]/, '')
|
|
81
|
+
shim_suffix = @options[:use_shim] ? '-shim' : '-direct'
|
|
82
|
+
filename = "parsed-css-#{source_slug}#{shim_suffix}.css"
|
|
83
|
+
|
|
84
|
+
# Serialize stylesheet to CSS
|
|
85
|
+
css_output = @stylesheet.to_s
|
|
86
|
+
|
|
87
|
+
File.write(filename, css_output)
|
|
88
|
+
warn "Parsed CSS saved to #{filename} (#{@stylesheet.size} rules, #{css_output.length} bytes)"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def load_css(source)
|
|
94
|
+
# Check if it's a URL
|
|
95
|
+
if /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/.match?(source)
|
|
96
|
+
load_from_url(source)
|
|
97
|
+
elsif File.exist?(source)
|
|
98
|
+
# Local file
|
|
99
|
+
Cataract::Stylesheet.load_file(source)
|
|
100
|
+
else
|
|
101
|
+
raise ArgumentError, "Invalid source: #{source} (not a valid URL or file path)"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def load_from_url(url)
|
|
106
|
+
# Check if it's a direct CSS file
|
|
107
|
+
if url.end_with?('.css')
|
|
108
|
+
Cataract::Stylesheet.load_uri(url)
|
|
109
|
+
else
|
|
110
|
+
# It's a webpage - use Premailer to fetch and combine all CSS
|
|
111
|
+
load_from_webpage(url)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def load_from_webpage(url)
|
|
116
|
+
require 'premailer'
|
|
117
|
+
|
|
118
|
+
# Fetch webpage and extract CSS
|
|
119
|
+
fetch_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
120
|
+
premailer = Premailer.new(url, with_html_string: false)
|
|
121
|
+
@timings[:fetch] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - fetch_start
|
|
122
|
+
|
|
123
|
+
# Get CSS parser from Premailer
|
|
124
|
+
parser = premailer.instance_variable_get(:@css_parser)
|
|
125
|
+
|
|
126
|
+
# If using Cataract shim, parser is already a Cataract::Stylesheet - use it directly
|
|
127
|
+
if defined?(CssParser::CATARACT_SHIM) && CssParser::CATARACT_SHIM
|
|
128
|
+
@timings[:premailer_parse] = 0 # Already parsed by Premailer/Cataract
|
|
129
|
+
@timings[:cataract_parse] = 0 # No reparsing needed
|
|
130
|
+
parser # Return the Cataract::Stylesheet directly
|
|
131
|
+
else
|
|
132
|
+
# Not using shim - parser is real css_parser, get CSS string and reparse
|
|
133
|
+
parse_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
134
|
+
css_string = parser.to_s
|
|
135
|
+
@timings[:premailer_parse] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - parse_start
|
|
136
|
+
|
|
137
|
+
# Parse it with Cataract
|
|
138
|
+
cataract_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
139
|
+
stylesheet = Cataract.parse_css(css_string)
|
|
140
|
+
@timings[:cataract_parse] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - cataract_start
|
|
141
|
+
|
|
142
|
+
stylesheet
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def source_name
|
|
147
|
+
if /\A#{URI::DEFAULT_PARSER.make_regexp(%w[http https])}\z/.match?(source)
|
|
148
|
+
uri = URI.parse(source)
|
|
149
|
+
if source.end_with?('.css')
|
|
150
|
+
File.basename(uri.path)
|
|
151
|
+
else
|
|
152
|
+
uri.host
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
File.basename(source)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def template_content
|
|
160
|
+
template_path = File.join(__dir__, 'templates', 'report.html.erb')
|
|
161
|
+
File.read(template_path)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CSSAnalyzer
|
|
4
|
+
module Analyzers
|
|
5
|
+
# Base class for all analyzers
|
|
6
|
+
# Each analyzer should implement #analyze method that returns a hash of results
|
|
7
|
+
class Base
|
|
8
|
+
attr_reader :stylesheet, :options
|
|
9
|
+
|
|
10
|
+
def initialize(stylesheet, options = {})
|
|
11
|
+
@stylesheet = stylesheet
|
|
12
|
+
@options = options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Override in subclasses
|
|
16
|
+
def analyze
|
|
17
|
+
raise NotImplementedError, "#{self.class} must implement #analyze"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Helper to get the tab name for this analyzer
|
|
21
|
+
def tab_name
|
|
22
|
+
self.class.name.split('::').last.downcase
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Helper to get media queries for a specific rule
|
|
26
|
+
# @param rule [Cataract::Rule] The rule to check
|
|
27
|
+
# @return [Array<Symbol>] Array of media query symbols this rule appears in
|
|
28
|
+
def media_queries_for_rule(rule)
|
|
29
|
+
stylesheet.instance_variable_get(:@_media_index).select { |_media, ids| ids.include?(rule.id) }.keys
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|