cataract 0.1.3 → 0.1.4

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci-manual-rubies.yml +27 -0
  3. data/.overcommit.yml +1 -1
  4. data/.rubocop.yml +62 -0
  5. data/.rubocop_todo.yml +186 -0
  6. data/BENCHMARKS.md +60 -139
  7. data/CHANGELOG.md +10 -0
  8. data/README.md +30 -2
  9. data/Rakefile +49 -22
  10. data/cataract.gemspec +4 -1
  11. data/ext/cataract/cataract.c +47 -47
  12. data/ext/cataract/css_parser.c +17 -33
  13. data/ext/cataract/merge.c +6 -0
  14. data/lib/cataract/at_rule.rb +8 -9
  15. data/lib/cataract/declaration.rb +18 -0
  16. data/lib/cataract/import_resolver.rb +3 -4
  17. data/lib/cataract/pure/byte_constants.rb +69 -0
  18. data/lib/cataract/pure/helpers.rb +35 -0
  19. data/lib/cataract/pure/imports.rb +255 -0
  20. data/lib/cataract/pure/merge.rb +1146 -0
  21. data/lib/cataract/pure/parser.rb +1236 -0
  22. data/lib/cataract/pure/serializer.rb +590 -0
  23. data/lib/cataract/pure/specificity.rb +206 -0
  24. data/lib/cataract/pure.rb +130 -0
  25. data/lib/cataract/rule.rb +22 -13
  26. data/lib/cataract/stylesheet.rb +14 -9
  27. data/lib/cataract/version.rb +1 -1
  28. data/lib/cataract.rb +18 -5
  29. metadata +12 -25
  30. data/benchmarks/benchmark_harness.rb +0 -193
  31. data/benchmarks/benchmark_merging.rb +0 -121
  32. data/benchmarks/benchmark_optimization_comparison.rb +0 -168
  33. data/benchmarks/benchmark_parsing.rb +0 -153
  34. data/benchmarks/benchmark_ragel_removal.rb +0 -56
  35. data/benchmarks/benchmark_runner.rb +0 -70
  36. data/benchmarks/benchmark_serialization.rb +0 -180
  37. data/benchmarks/benchmark_shorthand.rb +0 -109
  38. data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
  39. data/benchmarks/benchmark_specificity.rb +0 -124
  40. data/benchmarks/benchmark_string_allocation.rb +0 -151
  41. data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
  42. data/benchmarks/benchmark_to_s_cached.rb +0 -55
  43. data/benchmarks/benchmark_value_splitter.rb +0 -54
  44. data/benchmarks/benchmark_yjit.rb +0 -158
  45. data/benchmarks/benchmark_yjit_workers.rb +0 -61
  46. data/benchmarks/profile_to_s.rb +0 -23
  47. data/benchmarks/speedup_calculator.rb +0 -83
  48. data/benchmarks/system_metadata.rb +0 -81
  49. data/benchmarks/templates/benchmarks.md.erb +0 -221
  50. data/benchmarks/yjit_tests.rb +0 -141
  51. data/scripts/fuzzer/run.rb +0 -828
  52. data/scripts/fuzzer/worker.rb +0 -99
  53. data/scripts/generate_benchmarks_md.rb +0 -155
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'benchmark/ips'
4
- require 'json'
5
- require 'fileutils'
6
-
7
- # Unified benchmark runner that outputs both human-readable console output
8
- # and structured JSON for documentation generation.
9
- class BenchmarkRunner
10
- RESULTS_DIR = File.expand_path('.results', __dir__)
11
-
12
- attr_reader :name, :description, :metadata
13
-
14
- # @param name [String] Short name for this benchmark (e.g., "parsing", "specificity")
15
- # @param description [String] One-line description of what's being measured
16
- # @param metadata [Hash] Additional metadata (fixture info, test cases, etc.)
17
- def initialize(name:, description:, metadata: {})
18
- @name = name
19
- @description = description
20
- @metadata = metadata
21
- @results = []
22
-
23
- FileUtils.mkdir_p(RESULTS_DIR)
24
- end
25
-
26
- # Run a benchmark-ips block and capture results
27
- # @yield [Benchmark::IPS::Job] The benchmark-ips job
28
- def run
29
- json_path = File.join(RESULTS_DIR, "#{@name}.json")
30
-
31
- Benchmark.ips do |x|
32
- # Allow benchmark to configure itself
33
- yield x
34
-
35
- # Enable JSON output
36
- x.json!(json_path)
37
- end
38
-
39
- # Read the generated JSON and enhance with metadata
40
- raw_data = JSON.parse(File.read(json_path))
41
- enhanced_data = {
42
- 'name' => @name,
43
- 'description' => @description,
44
- 'metadata' => @metadata,
45
- 'timestamp' => Time.now.iso8601,
46
- 'results' => raw_data
47
- }
48
-
49
- File.write(json_path, JSON.pretty_generate(enhanced_data))
50
- end
51
-
52
- # Helper to format results as a comparison hash
53
- # @param label [String] Label for this result
54
- # @param baseline [String] Baseline label to compare against
55
- # @return [Hash] Structured comparison data
56
- def self.format_comparison(label:, baseline:, results:)
57
- baseline_result = results.find { |r| r['name'] == baseline }
58
- comparison_result = results.find { |r| r['name'] == label }
59
-
60
- return nil unless baseline_result && comparison_result
61
-
62
- speedup = comparison_result['central_tendency'].to_f / baseline_result['central_tendency']
63
-
64
- {
65
- 'label' => label,
66
- 'baseline' => baseline,
67
- 'speedup' => speedup.round(2)
68
- }
69
- end
70
- end
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'benchmark_harness'
4
-
5
- # Load the local development version, not installed gem
6
- $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
7
- require 'cataract'
8
-
9
- # CSS Serialization Performance Benchmark
10
- class SerializationBenchmark < BenchmarkHarness
11
- def self.benchmark_name
12
- 'serialization'
13
- end
14
-
15
- def self.description
16
- 'Time to convert parsed CSS back to string format'
17
- end
18
-
19
- def self.metadata
20
- instance = new
21
- {
22
- 'test_cases' => [
23
- {
24
- 'name' => "Full Serialization (Bootstrap CSS - #{(instance.bootstrap_css.length / 1024.0).round}KB)",
25
- 'key' => 'all',
26
- 'bytes' => instance.bootstrap_css.length
27
- },
28
- {
29
- 'name' => 'Media Type Filtering (print only)',
30
- 'key' => 'print',
31
- 'bytes' => instance.bootstrap_css.length
32
- }
33
- ]
34
- }
35
- end
36
-
37
- # Uses default speedup_config (test_case_key differs from parsing)
38
- def self.speedup_config
39
- {
40
- baseline_matcher: SpeedupCalculator::Matchers.css_parser,
41
- comparison_matcher: SpeedupCalculator::Matchers.cataract,
42
- test_case_key: :key # serialization uses 'key' not 'fixture'
43
- }
44
- end
45
-
46
- def sanity_checks
47
- # Check css_parser gem is available
48
- require 'css_parser'
49
-
50
- # Verify Bootstrap fixture exists
51
- raise "Bootstrap CSS fixture not found at #{bootstrap_path}" unless File.exist?(bootstrap_path)
52
-
53
- # Verify parsing and serialization work
54
- cataract_sheet = Cataract.parse_css(bootstrap_css)
55
- raise 'Failed to parse Bootstrap CSS' if cataract_sheet.empty?
56
-
57
- cataract_output = cataract_sheet.to_s
58
- raise 'Serialization produced empty output' if cataract_output.empty?
59
-
60
- # Verify output can be re-parsed
61
- reparsed = Cataract.parse_css(cataract_output)
62
- raise 'Failed to re-parse serialized output' if reparsed.empty?
63
- end
64
-
65
- def call
66
- validate_correctness
67
- run_full_serialization_benchmark
68
- run_media_filtering_benchmark
69
- end
70
-
71
- def bootstrap_css
72
- @bootstrap_css ||= File.read(bootstrap_path)
73
- end
74
-
75
- private
76
-
77
- def bootstrap_path
78
- @bootstrap_path ||= File.expand_path('../test/fixtures/bootstrap.css', __dir__)
79
- end
80
-
81
- def validate_correctness
82
- puts '=' * 80
83
- puts 'CORRECTNESS VALIDATION'
84
- puts '=' * 80
85
- puts "Input: Bootstrap CSS (#{bootstrap_css.length} bytes)"
86
-
87
- # Parse with both libraries
88
- cataract_sheet = Cataract.parse_css(bootstrap_css)
89
- css_parser = CssParser::Parser.new
90
- css_parser.add_block!(bootstrap_css)
91
-
92
- # Serialize
93
- cataract_output = cataract_sheet.to_s
94
- css_parser_output = css_parser.to_s
95
-
96
- puts "Cataract output: #{cataract_output.length} bytes (#{cataract_sheet.size} rules)"
97
- puts "css_parser output: #{css_parser_output.length} bytes"
98
-
99
- # Basic sanity check - outputs should be similar in size
100
- size_ratio = cataract_output.length.to_f / css_parser_output.length
101
- unless size_ratio > 0.8 && size_ratio < 1.2
102
- puts "⚠️ Output sizes differ significantly (ratio: #{size_ratio.round(2)})"
103
- end
104
-
105
- # Check that output can be re-parsed
106
- begin
107
- reparsed = Cataract.parse_css(cataract_output)
108
- puts "Re-parsed output: #{reparsed.size} rules"
109
- rescue StandardError => e
110
- puts "❌ Failed to re-parse: #{e.message}"
111
- raise
112
- end
113
- end
114
-
115
- def run_full_serialization_benchmark
116
- puts "\n#{'=' * 80}"
117
- puts 'TEST: Full serialization (to_s)'
118
- puts '=' * 80
119
- puts '(Parsing done once before benchmark, not included in measurements)'
120
-
121
- # Pre-parse CSS once (outside benchmark loop)
122
- cataract_parsed = Cataract.parse_css(bootstrap_css)
123
- css_parser_parsed = CssParser::Parser.new
124
- css_parser_parsed.add_block!(bootstrap_css)
125
-
126
- benchmark('all') do |x|
127
- x.config(time: 5, warmup: 2)
128
-
129
- x.report('css_parser: all') do
130
- # Clear memoization if any
131
- if css_parser_parsed.instance_variable_defined?(:@css_string)
132
- css_parser_parsed.instance_variable_set(:@css_string, nil)
133
- end
134
- css_parser_parsed.to_s
135
- end
136
-
137
- x.report('cataract: all') do
138
- # Clear memoization
139
- cataract_parsed.instance_variable_set(:@serialized, nil)
140
- cataract_parsed.to_s
141
- end
142
-
143
- x.compare!
144
- end
145
- end
146
-
147
- def run_media_filtering_benchmark
148
- puts "\n#{'=' * 80}"
149
- puts 'TEST: Media type filtering - to_s(:print)'
150
- puts '=' * 80
151
- puts 'Note: Using Parser API (css_parser compatible) not Stylesheet'
152
-
153
- # Pre-parse using Parser API for media filtering
154
- cataract_parser = Cataract::Stylesheet.new
155
- cataract_parser.add_block(bootstrap_css)
156
-
157
- css_parser_for_filter = CssParser::Parser.new
158
- css_parser_for_filter.add_block!(bootstrap_css)
159
-
160
- benchmark('print') do |x|
161
- x.config(time: 5, warmup: 2)
162
-
163
- x.report('css_parser: print') do
164
- if css_parser_for_filter.instance_variable_defined?(:@css_string)
165
- css_parser_for_filter.instance_variable_set(:@css_string, nil)
166
- end
167
- css_parser_for_filter.to_s(:print)
168
- end
169
-
170
- x.report('cataract: print') do
171
- cataract_parser.to_s(media: :print)
172
- end
173
-
174
- x.compare!
175
- end
176
- end
177
- end
178
-
179
- # Run if executed directly
180
- SerializationBenchmark.run if __FILE__ == $PROGRAM_NAME
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'benchmark/ips'
4
-
5
- $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
6
- require 'cataract'
7
-
8
- # Get current git branch
9
- branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
10
- branch = 'unknown' if branch.empty?
11
-
12
- puts '=' * 60
13
- puts "Shorthand Expansion/Creation Benchmark: #{branch}"
14
- puts '=' * 60
15
- puts ''
16
-
17
- # Test cases for expansion
18
- expansion_tests = {
19
- 'margin' => '10px 20px',
20
- 'padding' => '5px 10px 15px 20px',
21
- 'border' => '1px solid red',
22
- 'border-color' => 'red blue',
23
- 'font' => 'bold 14px/1.5 Arial, sans-serif',
24
- 'background' => 'url(image.png) no-repeat center/cover'
25
- }
26
-
27
- # Test cases for shorthand creation
28
- creation_tests = {
29
- 'margin' => {
30
- 'margin-top' => '10px',
31
- 'margin-right' => '20px',
32
- 'margin-bottom' => '10px',
33
- 'margin-left' => '20px'
34
- },
35
- 'border' => {
36
- 'border-width' => '1px',
37
- 'border-style' => 'solid',
38
- 'border-color' => 'red'
39
- },
40
- 'font' => {
41
- 'font-style' => 'italic',
42
- 'font-weight' => 'bold',
43
- 'font-size' => '14px',
44
- 'line-height' => '1.5',
45
- 'font-family' => 'Arial, sans-serif'
46
- }
47
- }
48
-
49
- puts 'Expansion test cases:'
50
- expansion_tests.each do |prop, value|
51
- puts " #{prop}: '#{value}'"
52
- end
53
- puts ''
54
-
55
- puts 'Shorthand creation test cases:'
56
- creation_tests.each do |name, props|
57
- puts " #{name}: #{props.length} properties"
58
- end
59
- puts ''
60
-
61
- Benchmark.ips do |x|
62
- x.config(time: 10, warmup: 3)
63
-
64
- # Benchmark expansions
65
- x.report("#{branch}:expand_margin ") do
66
- Cataract.expand_margin(expansion_tests['margin'])
67
- end
68
-
69
- x.report("#{branch}:expand_padding ") do
70
- Cataract.expand_padding(expansion_tests['padding'])
71
- end
72
-
73
- x.report("#{branch}:expand_border ") do
74
- Cataract.expand_border(expansion_tests['border'])
75
- end
76
-
77
- x.report("#{branch}:expand_font ") do
78
- Cataract.expand_font(expansion_tests['font'])
79
- end
80
-
81
- x.report("#{branch}:expand_background ") do
82
- Cataract.expand_background(expansion_tests['background'])
83
- end
84
-
85
- # Benchmark shorthand creation
86
- x.report("#{branch}:create_margin ") do
87
- Cataract.create_margin_shorthand(creation_tests['margin'])
88
- end
89
-
90
- x.report("#{branch}:create_border ") do
91
- Cataract.create_border_shorthand(creation_tests['border'])
92
- end
93
-
94
- x.report("#{branch}:create_font ") do
95
- Cataract.create_font_shorthand(creation_tests['font'])
96
- end
97
-
98
- x.compare!
99
-
100
- # Save results to file for cross-branch comparison
101
- x.save! 'test/.benchmark_results/shorthand.json'
102
- x.hold! 'test/.benchmark_results/shorthand.json'
103
- end
104
-
105
- puts ''
106
- puts '=' * 60
107
- puts 'Results saved to test/.benchmark_results/shorthand.json'
108
- puts 'Switch git branches and run again to compare!'
109
- puts '=' * 60
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'benchmark/ips'
4
- require 'cataract'
5
-
6
- # Load css_parser for comparison
7
- begin
8
- require 'css_parser'
9
- CSS_PARSER_AVAILABLE = true
10
- rescue LoadError
11
- CSS_PARSER_AVAILABLE = false
12
- puts 'Warning: css_parser gem not found. Install with: gem install css_parser'
13
- puts 'Running Cataract-only benchmarks...'
14
- end
15
-
16
- # Test values
17
- MARGIN_VALUES = [
18
- '10px',
19
- '10px 20px',
20
- '10px 20px 30px',
21
- '10px 20px 30px 40px',
22
- '10px calc(100% - 20px)',
23
- '10px !important'
24
- ].freeze
25
-
26
- BORDER_VALUES = [
27
- '1px solid red',
28
- '2px dashed blue',
29
- 'thin dotted #000'
30
- ].freeze
31
-
32
- FONT_VALUES = [
33
- '12px Arial',
34
- "bold 14px/1.5 'Helvetica Neue', sans-serif"
35
- ].freeze
36
-
37
- puts "\n=== Shorthand Expansion Benchmark ==="
38
- puts "Comparing Cataract (C) vs css_parser (Ruby)\n\n"
39
-
40
- # Sanity check: verify both implementations produce same results
41
- if CSS_PARSER_AVAILABLE
42
- puts '--- Sanity Check: Comparing Outputs ---'
43
-
44
- # Test margin expansion
45
- cataract_result = Cataract._expand_margin('10px 20px 30px 40px')
46
- css_parser_rs = CssParser::RuleSet.new(block: 'margin: 10px 20px 30px 40px')
47
- css_parser_rs.expand_shorthand!
48
- css_parser_result = {}
49
- css_parser_rs.each_declaration { |prop, val, _| css_parser_result[prop] = val }
50
-
51
- if cataract_result == css_parser_result
52
- puts '✓ Margin expansion: MATCH'
53
- else
54
- puts '✗ Margin expansion: MISMATCH'
55
- puts " Cataract: #{cataract_result.inspect}"
56
- puts " css_parser: #{css_parser_result.inspect}"
57
- exit 1
58
- end
59
-
60
- # Test border expansion
61
- cataract_result = Cataract._expand_border('1px solid red')
62
- css_parser_rs = CssParser::RuleSet.new(block: 'border: 1px solid red')
63
- css_parser_rs.expand_shorthand!
64
- css_parser_result = {}
65
- css_parser_rs.each_declaration { |prop, val, _| css_parser_result[prop] = val }
66
-
67
- if cataract_result == css_parser_result
68
- puts '✓ Border expansion: MATCH'
69
- else
70
- puts '✗ Border expansion: MISMATCH'
71
- puts " Cataract: #{cataract_result.inspect}"
72
- puts " css_parser: #{css_parser_result.inspect}"
73
- exit 1
74
- end
75
-
76
- puts "All sanity checks passed!\n\n"
77
- end
78
-
79
- # Margin expansion
80
- puts '--- Margin Expansion (4 values) ---'
81
- Benchmark.ips do |x|
82
- x.config(time: 5, warmup: 2)
83
-
84
- x.report('Cataract (C)') do
85
- Cataract._expand_margin('10px 20px 30px 40px')
86
- end
87
-
88
- if CSS_PARSER_AVAILABLE
89
- x.report('css_parser (Ruby)') do
90
- rs = CssParser::RuleSet.new(block: 'margin: 10px 20px 30px 40px')
91
- rs.expand_shorthand!
92
- end
93
- end
94
-
95
- x.compare!
96
- end
97
-
98
- # Margin with calc()
99
- puts "\n--- Margin with calc() ---"
100
- Benchmark.ips do |x|
101
- x.config(time: 5, warmup: 2)
102
-
103
- x.report('Cataract (C)') do
104
- Cataract._expand_margin('10px calc(100% - 20px)')
105
- end
106
-
107
- if CSS_PARSER_AVAILABLE
108
- x.report('css_parser (Ruby)') do
109
- rs = CssParser::RuleSet.new(block: 'margin: 10px calc(100% - 20px)')
110
- rs.expand_shorthand!
111
- end
112
- end
113
-
114
- x.compare!
115
- end
116
-
117
- # Margin with !important
118
- puts "\n--- Margin with !important ---"
119
- Benchmark.ips do |x|
120
- x.config(time: 5, warmup: 2)
121
-
122
- x.report('Cataract (C)') do
123
- Cataract._expand_margin('10px !important')
124
- end
125
-
126
- if CSS_PARSER_AVAILABLE
127
- x.report('css_parser (Ruby)') do
128
- rs = CssParser::RuleSet.new(block: 'margin: 10px !important')
129
- rs.expand_shorthand!
130
- end
131
- end
132
-
133
- x.compare!
134
- end
135
-
136
- # Border expansion
137
- puts "\n--- Border Expansion ---"
138
- Benchmark.ips do |x|
139
- x.config(time: 5, warmup: 2)
140
-
141
- x.report('Cataract (C)') do
142
- Cataract._expand_border('1px solid red')
143
- end
144
-
145
- if CSS_PARSER_AVAILABLE
146
- x.report('css_parser (Ruby)') do
147
- rs = CssParser::RuleSet.new(block: 'border: 1px solid red')
148
- rs.expand_shorthand!
149
- end
150
- end
151
-
152
- x.compare!
153
- end
154
-
155
- # Font expansion
156
- puts "\n--- Font Expansion ---"
157
- Benchmark.ips do |x|
158
- x.config(time: 5, warmup: 2)
159
-
160
- x.report('Cataract (C)') do
161
- Cataract._expand_font("bold 14px/1.5 'Helvetica Neue', sans-serif")
162
- end
163
-
164
- if CSS_PARSER_AVAILABLE
165
- x.report('css_parser (Ruby)') do
166
- rs = CssParser::RuleSet.new(block: "font: bold 14px/1.5 'Helvetica Neue', sans-serif")
167
- rs.expand_shorthand!
168
- end
169
- end
170
-
171
- x.compare!
172
- end
173
-
174
- puts "\n=== Summary ==="
175
- puts 'Cataract uses a C implementation with Ragel state machine for value splitting'
176
- puts 'css_parser uses pure Ruby with regex-based parsing'
@@ -1,124 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'benchmark_harness'
4
-
5
- # Load the local development version, not installed gem
6
- $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
7
- require 'cataract'
8
-
9
- # CSS Specificity Calculation Benchmark
10
- class SpecificityBenchmark < BenchmarkHarness
11
- def self.benchmark_name
12
- 'specificity'
13
- end
14
-
15
- def self.description
16
- 'Time to calculate CSS selector specificity values'
17
- end
18
-
19
- def self.metadata
20
- {
21
- 'test_cases' => [
22
- {
23
- 'name' => 'Simple Selectors',
24
- 'key' => 'simple',
25
- 'selectors' => { 'div' => 1, '.class' => 10, '#id' => 100 }
26
- },
27
- {
28
- 'name' => 'Compound Selectors',
29
- 'key' => 'compound',
30
- 'selectors' => { 'div.container' => 11, 'div#main' => 101, 'div.container#main' => 111 }
31
- },
32
- {
33
- 'name' => 'Combinators',
34
- 'key' => 'combinators',
35
- 'selectors' => { 'div p' => 2, 'div > p' => 2, 'h1 + p' => 2, 'div.container > p.intro' => 22 }
36
- },
37
- {
38
- 'name' => 'Pseudo-classes & Pseudo-elements',
39
- 'key' => 'pseudo',
40
- 'selectors' => { 'a:hover' => 11, 'p::before' => 2, 'li:first-child' => 11, 'p:first-child::before' => 12 }
41
- },
42
- {
43
- 'name' => ':not() Pseudo-class (CSS3)',
44
- 'key' => 'not',
45
- 'selectors' => { '#s12:not(foo)' => 101, 'div:not(.active)' => 11, '.button:not([disabled])' => 20 },
46
- 'note' => "css_parser has a bug - doesn't parse :not() content"
47
- },
48
- {
49
- 'name' => 'Complex Real-world Selectors',
50
- 'key' => 'complex',
51
- 'selectors' => {
52
- 'ul#nav li.active a:hover' => 122,
53
- 'div.wrapper > article#main > section.content > p:first-child' => 123,
54
- "[data-theme='dark'] body.admin #dashboard .widget a[href^='http']::before" => 143
55
- }
56
- }
57
- ]
58
- }
59
- end
60
-
61
- def self.speedup_config
62
- {
63
- baseline_matcher: SpeedupCalculator::Matchers.css_parser,
64
- comparison_matcher: SpeedupCalculator::Matchers.cataract,
65
- test_case_key: :key
66
- }
67
- end
68
-
69
- def sanity_checks
70
- require 'css_parser'
71
-
72
- # Verify Cataract calculations
73
- raise 'Cataract simple selector failed' unless Cataract.calculate_specificity('div') == 1
74
- raise 'Cataract class selector failed' unless Cataract.calculate_specificity('.class') == 10
75
- raise 'Cataract id selector failed' unless Cataract.calculate_specificity('#id') == 100
76
- end
77
-
78
- def call
79
- self.class.metadata['test_cases'].each do |test_case|
80
- benchmark_category(test_case)
81
- end
82
- end
83
-
84
- private
85
-
86
- def benchmark_category(test_case)
87
- puts '=' * 80
88
- puts "TEST: #{test_case['name']}"
89
- puts test_case['note'] if test_case['note']
90
- puts '=' * 80
91
-
92
- key = test_case['key']
93
- selectors = test_case['selectors']
94
-
95
- # Show individual selector examples in terminal output
96
- puts 'Selectors tested:'
97
- selectors.each do |selector, expected_specificity|
98
- puts " #{selector} => #{expected_specificity}"
99
- end
100
- puts
101
-
102
- benchmark(key) do |x|
103
- x.config(time: 2, warmup: 1)
104
-
105
- # Report aggregated results per test case for speedup calculation
106
- x.report("css_parser: #{key}") do
107
- selectors.each_key do |selector|
108
- CssParser.calculate_specificity(selector)
109
- end
110
- end
111
-
112
- x.report("cataract: #{key}") do
113
- selectors.each_key do |selector|
114
- Cataract.calculate_specificity(selector)
115
- end
116
- end
117
-
118
- x.compare!
119
- end
120
- end
121
- end
122
-
123
- # Run if executed directly
124
- SpecificityBenchmark.run if __FILE__ == $PROGRAM_NAME