cataract 0.1.2 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci-manual-rubies.yml +27 -0
- data/.overcommit.yml +1 -1
- data/.rubocop.yml +62 -0
- data/.rubocop_todo.yml +186 -0
- data/BENCHMARKS.md +60 -139
- data/CHANGELOG.md +14 -0
- data/README.md +30 -2
- data/Rakefile +49 -22
- data/cataract.gemspec +4 -1
- data/ext/cataract/cataract.c +47 -47
- data/ext/cataract/css_parser.c +17 -33
- data/ext/cataract/merge.c +58 -2
- data/lib/cataract/at_rule.rb +8 -9
- data/lib/cataract/declaration.rb +18 -0
- data/lib/cataract/import_resolver.rb +3 -4
- data/lib/cataract/pure/byte_constants.rb +69 -0
- data/lib/cataract/pure/helpers.rb +35 -0
- data/lib/cataract/pure/imports.rb +255 -0
- data/lib/cataract/pure/merge.rb +1146 -0
- data/lib/cataract/pure/parser.rb +1236 -0
- data/lib/cataract/pure/serializer.rb +590 -0
- data/lib/cataract/pure/specificity.rb +206 -0
- data/lib/cataract/pure.rb +130 -0
- data/lib/cataract/rule.rb +22 -13
- data/lib/cataract/stylesheet.rb +14 -9
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +18 -5
- metadata +12 -25
- data/benchmarks/benchmark_harness.rb +0 -193
- data/benchmarks/benchmark_merging.rb +0 -121
- data/benchmarks/benchmark_optimization_comparison.rb +0 -168
- data/benchmarks/benchmark_parsing.rb +0 -153
- data/benchmarks/benchmark_ragel_removal.rb +0 -56
- data/benchmarks/benchmark_runner.rb +0 -70
- data/benchmarks/benchmark_serialization.rb +0 -180
- data/benchmarks/benchmark_shorthand.rb +0 -109
- data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
- data/benchmarks/benchmark_specificity.rb +0 -124
- data/benchmarks/benchmark_string_allocation.rb +0 -151
- data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
- data/benchmarks/benchmark_to_s_cached.rb +0 -55
- data/benchmarks/benchmark_value_splitter.rb +0 -54
- data/benchmarks/benchmark_yjit.rb +0 -158
- data/benchmarks/benchmark_yjit_workers.rb +0 -61
- data/benchmarks/profile_to_s.rb +0 -23
- data/benchmarks/speedup_calculator.rb +0 -83
- data/benchmarks/system_metadata.rb +0 -81
- data/benchmarks/templates/benchmarks.md.erb +0 -221
- data/benchmarks/yjit_tests.rb +0 -141
- data/scripts/fuzzer/run.rb +0 -828
- data/scripts/fuzzer/worker.rb +0 -99
- 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
|