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,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module CSSAnalyzer
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Analyzes color usage in CSS
|
|
8
|
+
class Colors < Base
|
|
9
|
+
# Color properties to search for
|
|
10
|
+
COLOR_PROPERTIES = %w[
|
|
11
|
+
color
|
|
12
|
+
background-color
|
|
13
|
+
border-color
|
|
14
|
+
border-top-color
|
|
15
|
+
border-right-color
|
|
16
|
+
border-bottom-color
|
|
17
|
+
border-left-color
|
|
18
|
+
outline-color
|
|
19
|
+
text-decoration-color
|
|
20
|
+
column-rule-color
|
|
21
|
+
fill
|
|
22
|
+
stroke
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def analyze
|
|
26
|
+
color_counts = Hash.new(0)
|
|
27
|
+
color_examples = Hash.new { |h, k| h[k] = [] }
|
|
28
|
+
|
|
29
|
+
# Iterate through all rules
|
|
30
|
+
stylesheet.rules.each do |rule|
|
|
31
|
+
# Skip AtRules (like @keyframes) - they don't have declarations
|
|
32
|
+
next unless rule.is_a?(Cataract::Rule)
|
|
33
|
+
|
|
34
|
+
selector = rule.selector
|
|
35
|
+
media_types = media_queries_for_rule(rule)
|
|
36
|
+
|
|
37
|
+
# Check each declaration for color values
|
|
38
|
+
rule.declarations.each do |decl|
|
|
39
|
+
property = decl.property
|
|
40
|
+
value = decl.value
|
|
41
|
+
|
|
42
|
+
next unless COLOR_PROPERTIES.include?(property)
|
|
43
|
+
|
|
44
|
+
# Extract color values from the declaration
|
|
45
|
+
colors = extract_colors(value)
|
|
46
|
+
|
|
47
|
+
colors.each do |color|
|
|
48
|
+
normalized = normalize_color(color)
|
|
49
|
+
color_counts[normalized] += 1
|
|
50
|
+
|
|
51
|
+
# Store example (limit to 3 per color)
|
|
52
|
+
next unless color_examples[normalized].length < 3
|
|
53
|
+
|
|
54
|
+
color_examples[normalized] << {
|
|
55
|
+
property: property,
|
|
56
|
+
original_value: color,
|
|
57
|
+
selector: selector,
|
|
58
|
+
media: media_types
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Sort by frequency
|
|
65
|
+
sorted_colors = color_counts.sort_by { |_color, count| -count }
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
total_colors: color_counts.values.sum,
|
|
69
|
+
unique_colors: color_counts.size,
|
|
70
|
+
colors: sorted_colors.map do |color, count|
|
|
71
|
+
{
|
|
72
|
+
color: color,
|
|
73
|
+
count: count,
|
|
74
|
+
percentage: (count.to_f / color_counts.values.sum * 100).round(1),
|
|
75
|
+
examples: color_examples[color],
|
|
76
|
+
hex: color_to_hex(color)
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Extract color values from a CSS value string
|
|
85
|
+
# Handles: hex, rgb(), rgba(), hsl(), hsla(), named colors
|
|
86
|
+
def extract_colors(value)
|
|
87
|
+
colors = []
|
|
88
|
+
|
|
89
|
+
# Hex colors: #fff, #ffffff, #ffffffff
|
|
90
|
+
colors += value.scan(/#[0-9a-fA-F]{3,8}\b/)
|
|
91
|
+
|
|
92
|
+
# rgb/rgba: rgb(255, 255, 255), rgba(255, 255, 255, 0.5)
|
|
93
|
+
colors += value.scan(/rgba?\([^)]+\)/)
|
|
94
|
+
|
|
95
|
+
# hsl/hsla: hsl(120, 100%, 50%), hsla(120, 100%, 50%, 0.5)
|
|
96
|
+
colors += value.scan(/hsla?\([^)]+\)/)
|
|
97
|
+
|
|
98
|
+
# Named colors (basic set - extend as needed)
|
|
99
|
+
named_colors = %w[
|
|
100
|
+
transparent currentcolor inherit
|
|
101
|
+
black white red green blue yellow orange purple pink
|
|
102
|
+
gray grey silver maroon olive lime aqua teal navy fuchsia
|
|
103
|
+
]
|
|
104
|
+
named_colors.each do |named|
|
|
105
|
+
colors << named if value.downcase.include?(named)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
colors.uniq
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Normalize color to lowercase for grouping
|
|
112
|
+
def normalize_color(color)
|
|
113
|
+
color.downcase.strip
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Convert color to hex for display (best effort)
|
|
117
|
+
def color_to_hex(color)
|
|
118
|
+
# If already hex, return as-is
|
|
119
|
+
return color if color.start_with?('#')
|
|
120
|
+
|
|
121
|
+
# For rgb/rgba, try to extract and convert
|
|
122
|
+
if color.start_with?('rgb')
|
|
123
|
+
# Simple extraction - just return the color for now
|
|
124
|
+
# In future, could parse and convert to hex
|
|
125
|
+
return color
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# For named colors, return as-is (browser will handle)
|
|
129
|
+
color
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module CSSAnalyzer
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Analyzes !important usage in CSS
|
|
8
|
+
class Important < Base
|
|
9
|
+
def analyze
|
|
10
|
+
total_declarations = 0
|
|
11
|
+
important_count = 0
|
|
12
|
+
important_by_property = Hash.new(0)
|
|
13
|
+
important_by_selector = Hash.new(0)
|
|
14
|
+
important_examples = []
|
|
15
|
+
|
|
16
|
+
# Iterate through all rules
|
|
17
|
+
stylesheet.rules.each do |rule|
|
|
18
|
+
# Skip AtRules (like @keyframes) - they don't have declarations
|
|
19
|
+
next unless rule.is_a?(Cataract::Rule)
|
|
20
|
+
|
|
21
|
+
selector = rule.selector
|
|
22
|
+
media_types = media_queries_for_rule(rule)
|
|
23
|
+
selector_important_count = 0
|
|
24
|
+
|
|
25
|
+
rule.declarations.each do |decl|
|
|
26
|
+
property = decl.property
|
|
27
|
+
value = decl.value
|
|
28
|
+
important = decl.important
|
|
29
|
+
|
|
30
|
+
total_declarations += 1
|
|
31
|
+
|
|
32
|
+
next unless important
|
|
33
|
+
|
|
34
|
+
important_count += 1
|
|
35
|
+
important_by_property[property] += 1
|
|
36
|
+
selector_important_count += 1
|
|
37
|
+
|
|
38
|
+
important_examples << {
|
|
39
|
+
selector: selector,
|
|
40
|
+
property: property,
|
|
41
|
+
value: value,
|
|
42
|
+
media: media_types
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if selector_important_count.positive?
|
|
47
|
+
important_by_selector[selector] = selector_important_count
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sort properties by !important usage
|
|
52
|
+
top_properties = important_by_property.sort_by { |_prop, count| -count }.first(20)
|
|
53
|
+
|
|
54
|
+
# Sort selectors by !important count
|
|
55
|
+
top_selectors = important_by_selector.sort_by { |_sel, count| -count }.first(20)
|
|
56
|
+
|
|
57
|
+
# Calculate percentage
|
|
58
|
+
important_percentage = if total_declarations.positive?
|
|
59
|
+
(important_count.to_f / total_declarations * 100).round(1)
|
|
60
|
+
else
|
|
61
|
+
0.0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
total_declarations: total_declarations,
|
|
66
|
+
important_count: important_count,
|
|
67
|
+
important_percentage: important_percentage,
|
|
68
|
+
properties_using_important: important_by_property.size,
|
|
69
|
+
selectors_using_important: important_by_selector.size,
|
|
70
|
+
top_properties: top_properties.map do |prop, count|
|
|
71
|
+
{
|
|
72
|
+
property: prop,
|
|
73
|
+
count: count,
|
|
74
|
+
percentage: (count.to_f / important_count * 100).round(1)
|
|
75
|
+
}
|
|
76
|
+
end,
|
|
77
|
+
top_selectors: top_selectors.map do |selector, count|
|
|
78
|
+
{
|
|
79
|
+
selector: selector,
|
|
80
|
+
count: count
|
|
81
|
+
}
|
|
82
|
+
end,
|
|
83
|
+
all_important: important_examples
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module CSSAnalyzer
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Analyzes CSS property usage frequency
|
|
8
|
+
class Properties < Base
|
|
9
|
+
def analyze
|
|
10
|
+
property_counts = Hash.new(0)
|
|
11
|
+
property_examples = Hash.new { |h, k| h[k] = [] }
|
|
12
|
+
|
|
13
|
+
# Iterate through all rules
|
|
14
|
+
stylesheet.rules.each do |rule|
|
|
15
|
+
# Skip AtRules (like @keyframes) - they don't have declarations
|
|
16
|
+
next unless rule.is_a?(Cataract::Rule)
|
|
17
|
+
|
|
18
|
+
selector = rule.selector
|
|
19
|
+
media_types = media_queries_for_rule(rule)
|
|
20
|
+
|
|
21
|
+
# Iterate through declarations
|
|
22
|
+
# Each declaration is a struct with property, value, important
|
|
23
|
+
rule.declarations.each do |decl|
|
|
24
|
+
property = decl.property
|
|
25
|
+
value = decl.value
|
|
26
|
+
important = decl.important
|
|
27
|
+
|
|
28
|
+
property_counts[property] += 1
|
|
29
|
+
|
|
30
|
+
# Store example (limit to 3 examples per property)
|
|
31
|
+
next unless property_examples[property].length < 3
|
|
32
|
+
|
|
33
|
+
property_examples[property] << {
|
|
34
|
+
value: value,
|
|
35
|
+
important: important,
|
|
36
|
+
selector: selector,
|
|
37
|
+
media: media_types
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Sort by frequency and take top N
|
|
43
|
+
top_n = options[:top] || 20
|
|
44
|
+
top_properties = property_counts.sort_by { |_prop, count| -count }.first(top_n)
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
total_properties: property_counts.values.sum,
|
|
48
|
+
unique_properties: property_counts.size,
|
|
49
|
+
top_properties: top_properties.map do |property, count|
|
|
50
|
+
{
|
|
51
|
+
name: property,
|
|
52
|
+
count: count,
|
|
53
|
+
percentage: (count.to_f / property_counts.values.sum * 100).round(1),
|
|
54
|
+
examples: property_examples[property]
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module CSSAnalyzer
|
|
6
|
+
module Analyzers
|
|
7
|
+
# Analyzes CSS selector specificity
|
|
8
|
+
class Specificity < Base
|
|
9
|
+
def analyze
|
|
10
|
+
specificity_data = []
|
|
11
|
+
specificity_histogram = Hash.new(0)
|
|
12
|
+
|
|
13
|
+
# Iterate through all rules
|
|
14
|
+
stylesheet.rules.each do |rule|
|
|
15
|
+
# Skip AtRules (like @keyframes) - they don't have specificity like regular rules
|
|
16
|
+
next unless rule.is_a?(Cataract::Rule)
|
|
17
|
+
|
|
18
|
+
spec = rule.specificity
|
|
19
|
+
media_types = media_queries_for_rule(rule)
|
|
20
|
+
specificity_histogram[spec] += 1
|
|
21
|
+
|
|
22
|
+
specificity_data << {
|
|
23
|
+
selector: rule.selector,
|
|
24
|
+
specificity: spec,
|
|
25
|
+
media: media_types,
|
|
26
|
+
declaration_count: rule.declarations.length
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Sort by specificity (highest first)
|
|
31
|
+
sorted_by_spec = specificity_data.sort_by { |r| -r[:specificity] }
|
|
32
|
+
|
|
33
|
+
# Calculate statistics
|
|
34
|
+
specificities = specificity_data.map { |r| r[:specificity] }
|
|
35
|
+
avg_specificity = specificities.sum.to_f / specificities.length
|
|
36
|
+
max_specificity = specificities.max
|
|
37
|
+
min_specificity = specificities.min
|
|
38
|
+
|
|
39
|
+
# Categorize selectors by specificity ranges
|
|
40
|
+
# Specificity guide:
|
|
41
|
+
# 0-10: Element selectors (div, p, etc)
|
|
42
|
+
# 11-100: Class selectors (.class)
|
|
43
|
+
# 101-1000: ID selectors (#id)
|
|
44
|
+
# 1000+: Inline styles or many IDs
|
|
45
|
+
categories = {
|
|
46
|
+
low: specificity_data.count { |r| r[:specificity] <= 10 },
|
|
47
|
+
medium: specificity_data.count { |r| r[:specificity] > 10 && r[:specificity] <= 100 },
|
|
48
|
+
high: specificity_data.count { |r| r[:specificity] > 100 && r[:specificity] <= 1000 },
|
|
49
|
+
very_high: specificity_data.count { |r| r[:specificity] > 1000 }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Find problematic selectors (high specificity)
|
|
53
|
+
high_specificity = sorted_by_spec.select { |r| r[:specificity] > 100 }
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
total_selectors: specificity_data.length,
|
|
57
|
+
average_specificity: avg_specificity.round(1),
|
|
58
|
+
max_specificity: max_specificity,
|
|
59
|
+
min_specificity: min_specificity,
|
|
60
|
+
categories: categories,
|
|
61
|
+
top_20_highest: sorted_by_spec.first(20),
|
|
62
|
+
high_specificity_count: high_specificity.length,
|
|
63
|
+
histogram: specificity_histogram.sort_by { |spec, _count| -spec }.first(20)
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|