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,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