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,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ # Collects system metadata for benchmark runs
7
+ class SystemMetadata
8
+ RESULTS_DIR = File.expand_path('.results', __dir__)
9
+
10
+ def self.collect
11
+ metadata = {
12
+ 'ruby_version' => RUBY_VERSION,
13
+ 'ruby_description' => RUBY_DESCRIPTION,
14
+ 'platform' => RUBY_PLATFORM,
15
+ 'cpu' => detect_cpu,
16
+ 'memory' => detect_memory,
17
+ 'os' => detect_os,
18
+ 'timestamp' => Time.now.iso8601
19
+ }
20
+
21
+ FileUtils.mkdir_p(RESULTS_DIR)
22
+ File.write(File.join(RESULTS_DIR, 'metadata.json'), JSON.pretty_generate(metadata))
23
+
24
+ metadata
25
+ end
26
+
27
+ def self.detect_cpu
28
+ if RUBY_PLATFORM.include?('darwin')
29
+ `sysctl -n machdep.cpu.brand_string`.strip
30
+ elsif File.exist?('/proc/cpuinfo')
31
+ cpuinfo = File.read('/proc/cpuinfo')
32
+ if (match = cpuinfo.match(/model name\s*:\s*(.+)/))
33
+ match[1].strip
34
+ else
35
+ 'Unknown'
36
+ end
37
+ else
38
+ 'Unknown'
39
+ end
40
+ rescue StandardError
41
+ 'Unknown'
42
+ end
43
+
44
+ def self.detect_memory
45
+ if RUBY_PLATFORM.include?('darwin')
46
+ # Output in GB
47
+ bytes = `sysctl -n hw.memsize`.strip.to_i
48
+ "#{bytes / (1024 * 1024 * 1024)}GB"
49
+ elsif File.exist?('/proc/meminfo')
50
+ meminfo = File.read('/proc/meminfo')
51
+ if (match = meminfo.match(/MemTotal:\s+(\d+)\s+kB/))
52
+ kb = match[1].to_i
53
+ "#{kb / (1024 * 1024)}GB"
54
+ else
55
+ 'Unknown'
56
+ end
57
+ else
58
+ 'Unknown'
59
+ end
60
+ rescue StandardError
61
+ 'Unknown'
62
+ end
63
+
64
+ def self.detect_os
65
+ if RUBY_PLATFORM.include?('darwin')
66
+ version = `sw_vers -productVersion`.strip
67
+ "macOS #{version}"
68
+ elsif File.exist?('/etc/os-release')
69
+ os_release = File.read('/etc/os-release')
70
+ if (match = os_release.match(/PRETTY_NAME="(.+)"/))
71
+ match[1]
72
+ else
73
+ RUBY_PLATFORM
74
+ end
75
+ else
76
+ RUBY_PLATFORM
77
+ end
78
+ rescue StandardError
79
+ RUBY_PLATFORM
80
+ end
81
+ end
@@ -0,0 +1,221 @@
1
+ <!-- AUTO-GENERATED FILE - DO NOT EDIT -->
2
+ <!-- This file is automatically generated from benchmark results. -->
3
+ <!-- To regenerate: rake benchmark:generate_docs -->
4
+
5
+ # Performance Benchmarks
6
+
7
+ Comprehensive performance comparison between Cataract and css_parser gem.
8
+
9
+ ## Test Environment
10
+
11
+ - **Ruby**: <%= metadata['ruby_description'] %>
12
+ - **CPU**: <%= metadata['cpu'] %>
13
+ - **Memory**: <%= metadata['memory'] %>
14
+ - **OS**: <%= metadata['os'] %>
15
+ - **Generated**: <%= metadata['timestamp'] %>
16
+
17
+ <%- if parsing_data -%>
18
+ <details>
19
+ <summary><h2>CSS Parsing</h2></summary>
20
+
21
+ Performance of parsing CSS into internal data structures.
22
+
23
+ <%= parsing_data['description'] %>
24
+
25
+ <%- parsing_data['metadata']['test_cases'].each do |test_case| -%>
26
+ ### <%= test_case['name'] %>
27
+
28
+ <%- results = parsing_data['results'].select { |r| r['name'].include?(test_case['fixture']) } -%>
29
+ <%- css_parser_result = results.find { |r| r['name'].include?('css_parser') } -%>
30
+ <%- cataract_result = results.find { |r| r['name'].include?('cataract') } -%>
31
+ <%- if css_parser_result && cataract_result -%>
32
+ <%- speedup = cataract_result['central_tendency'] / css_parser_result['central_tendency'] -%>
33
+
34
+ | Parser | Speed | Time per operation |
35
+ |--------|-------|-------------------|
36
+ | css_parser | <%= format_ips(css_parser_result, short: true) %> | <%= format_time_per_op(css_parser_result) %> |
37
+ | **Cataract** | **<%= format_ips(cataract_result, short: true) %>** | **<%= format_time_per_op(cataract_result) %>** |
38
+ | **Speedup** | **<%= format_speedup(speedup) %>** | |
39
+
40
+ <%- end -%>
41
+ <%- end -%>
42
+
43
+ </details>
44
+
45
+ ---
46
+ <%- end -%>
47
+
48
+ <%- if serialization_data -%>
49
+ <details>
50
+ <summary><h2>CSS Serialization (to_s)</h2></summary>
51
+
52
+ Performance of converting parsed CSS back to string format.
53
+
54
+ <%= serialization_data['description'] %>
55
+
56
+ <%- serialization_data['metadata']['test_cases'].each do |test_case| -%>
57
+ ### <%= test_case['name'] %>
58
+
59
+ <%- results = serialization_data['results'].select { |r| r['name'].include?(test_case['key']) } -%>
60
+ <%- css_parser_result = results.find { |r| r['name'].include?('css_parser') } -%>
61
+ <%- cataract_result = results.find { |r| r['name'].include?('cataract') } -%>
62
+ <%- if css_parser_result && cataract_result -%>
63
+ <%- speedup = cataract_result['central_tendency'] / css_parser_result['central_tendency'] -%>
64
+
65
+ | Parser | Speed | Time per operation |
66
+ |--------|-------|-------------------|
67
+ | css_parser | <%= format_ips(css_parser_result, short: true) %> | <%= format_time_per_op(css_parser_result) %> |
68
+ | **Cataract** | **<%= format_ips(cataract_result, short: true) %>** | **<%= format_time_per_op(cataract_result) %>** |
69
+ | **Speedup** | **<%= format_speedup(speedup) %>** | |
70
+
71
+ <%- end -%>
72
+ <%- end -%>
73
+
74
+ </details>
75
+
76
+ ---
77
+ <%- end -%>
78
+
79
+ <%- if specificity_data -%>
80
+ <details>
81
+ <summary><h2>Specificity Calculation</h2></summary>
82
+
83
+ Performance of calculating CSS selector specificity values.
84
+
85
+ <%= specificity_data['description'] %>
86
+
87
+ | Test Case | Speedup |
88
+ |-----------|---------|
89
+ <%- specificity_data['metadata']['test_cases'].each do |test_case| -%>
90
+ | <%= test_case['name'] %> | **<%= format_speedup(test_case['speedup']) %>** |
91
+ <%- end -%>
92
+
93
+ **Summary:** <%= format_speedup(specificity_data['metadata']['speedups']['min']) %> to <%= format_speedup(specificity_data['metadata']['speedups']['max']) %> (avg <%= format_speedup(specificity_data['metadata']['speedups']['avg']) %>)
94
+
95
+ </details>
96
+
97
+ ---
98
+ <%- end -%>
99
+
100
+ <%- if merging_data -%>
101
+ <details>
102
+ <summary><h2>CSS Merging</h2></summary>
103
+
104
+ Performance of merging multiple CSS rule sets with the same selector.
105
+
106
+ <%= merging_data['description'] %>
107
+
108
+ | Test Case | Speedup |
109
+ |-----------|---------|
110
+ <%- merging_data['metadata']['test_cases'].each do |test_case| -%>
111
+ | <%= test_case['name'] %> | **<%= format_speedup(test_case['speedup']) %>** |
112
+ <%- end -%>
113
+
114
+ **Summary:** <%= format_speedup(merging_data['metadata']['speedups']['min']) %> to <%= format_speedup(merging_data['metadata']['speedups']['max']) %> (avg <%= format_speedup(merging_data['metadata']['speedups']['avg']) %>)
115
+
116
+ ### What's Being Tested
117
+ - Specificity-based CSS cascade (ID > class > element)
118
+ - `!important` declaration handling
119
+ - Shorthand property expansion (e.g., `margin` โ†’ `margin-top`, `margin-right`, etc.)
120
+ - Shorthand property creation from longhand properties
121
+
122
+ </details>
123
+
124
+ ---
125
+ <%- end -%>
126
+
127
+ <%- if yjit_data -%>
128
+ <details>
129
+ <summary><h2>YJIT Impact</h2></summary>
130
+
131
+ Impact of Ruby's YJIT JIT compiler on Ruby-side operations. The C extension performance is the same regardless of YJIT.
132
+
133
+ <%= yjit_data['description'] %>
134
+
135
+ ### Operations Per Second
136
+
137
+ | Operation | Without YJIT | With YJIT | YJIT Improvement |
138
+ |-----------|--------------|-----------|------------------|
139
+ <%- yjit_data['metadata']['operations'].each do |operation| -%>
140
+ <%- results = yjit_data['results'].select { |r| r['name'].include?(operation) } -%>
141
+ <%- no_yjit = results.find { |r| r['name'].include?('no YJIT') } -%>
142
+ <%- with_yjit = results.find { |r| r['name'].include?('YJIT') && !r['name'].include?('no YJIT') } -%>
143
+ <%- if no_yjit && with_yjit -%>
144
+ <%- improvement = with_yjit['central_tendency'] / no_yjit['central_tendency'] -%>
145
+ <%- pct = ((improvement - 1) * 100).round -%>
146
+ | <%= operation %> | <%= format_ips(no_yjit, short: true) %> | <%= format_ips(with_yjit, short: true) %> | **<%= format_speedup(improvement) %>** (<%= pct %>% faster) |
147
+ <%- end -%>
148
+ <%- end -%>
149
+
150
+ ### Key Takeaways
151
+ - YJIT provides significant performance boost for Ruby-side operations
152
+ - Greatest impact on declaration merging
153
+ - Parse + iterate benefits least since most work is in C
154
+ - Recommended: Enable YJIT in production (`--yjit` flag or `RUBY_YJIT_ENABLE=1`)
155
+
156
+ </details>
157
+
158
+ ---
159
+ <%- end -%>
160
+
161
+ ## Summary
162
+
163
+ ### Performance Highlights
164
+
165
+ | Category | Min Speedup | Max Speedup | Avg Speedup |
166
+ |----------|-------------|-------------|-------------|
167
+ <%- if parsing_data && parsing_data['metadata']['speedups'] -%>
168
+ <%- sp = parsing_data['metadata']['speedups'] -%>
169
+ | **Parsing** | <%= format_speedup(sp['min']) %> | <%= format_speedup(sp['max']) %> | <%= format_speedup(sp['avg']) %> |
170
+ <%- end -%>
171
+ <%- if serialization_data && serialization_data['metadata']['speedups'] -%>
172
+ <%- sp = serialization_data['metadata']['speedups'] -%>
173
+ | **Serialization** | <%= format_speedup(sp['min']) %> | <%= format_speedup(sp['max']) %> | <%= format_speedup(sp['avg']) %> |
174
+ <%- end -%>
175
+ <%- if specificity_data && specificity_data['metadata']['speedups'] -%>
176
+ <%- sp = specificity_data['metadata']['speedups'] -%>
177
+ | **Specificity** | <%= format_speedup(sp['min']) %> | <%= format_speedup(sp['max']) %> | <%= format_speedup(sp['avg']) %> |
178
+ <%- end -%>
179
+ <%- if merging_data && merging_data['metadata']['speedups'] -%>
180
+ <%- sp = merging_data['metadata']['speedups'] -%>
181
+ | **Merging** | <%= format_speedup(sp['min']) %> | <%= format_speedup(sp['max']) %> | <%= format_speedup(sp['avg']) %> |
182
+ <%- end -%>
183
+
184
+ ### Implementation Notes
185
+
186
+ 1. **C Extension**: Critical paths (parsing, specificity, merging, serialization) implemented in C
187
+ 2. **Efficient Data Structures**: Rules grouped by media query for O(1) lookups
188
+ 3. **Memory Efficient**: Pre-allocated string buffers, minimal Ruby object allocations
189
+ 4. **Optimized Algorithms**: Purpose-built CSS specificity calculator
190
+
191
+ ### Use Cases
192
+
193
+ - **Large CSS files**: Handles complex stylesheets efficiently
194
+ - **Specificity calculations**: Optimized for selector analysis
195
+ - **High-volume processing**: Reduced allocations minimize GC pressure
196
+ - **Production applications**: Tested with Bootstrap CSS and real-world stylesheets
197
+
198
+ ---
199
+
200
+ ## Running Benchmarks
201
+
202
+ ```bash
203
+ # All benchmarks
204
+ rake benchmark 2>&1 | tee benchmark_output.txt
205
+
206
+ # Individual benchmarks
207
+ rake benchmark:parsing
208
+ rake benchmark:serialization
209
+ rake benchmark:specificity
210
+ rake benchmark:merging
211
+ rake benchmark:yjit
212
+
213
+ # Generate documentation
214
+ rake benchmark:generate_docs
215
+ ```
216
+
217
+ ## Notes
218
+
219
+ - All benchmarks use benchmark-ips with 3s warmup and 5-10s measurement periods
220
+ - Measurements are median i/s (iterations per second) with standard deviation
221
+ - css_parser gem must be installed for comparison benchmarks
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared test logic for YJIT benchmarks
4
+ # Extended by both YjitWithoutBenchmark and YjitWithBenchmark
5
+ module YjitTests
6
+ SAMPLE_CSS = <<~CSS
7
+ body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
8
+ .header { color: #333; padding: 20px; background: #f8f9fa; }
9
+ .container { max-width: 1200px; margin: 0 auto; }
10
+ div p { line-height: 1.6; }
11
+ .container > .item { margin-bottom: 20px; }
12
+ h1 + p { margin-top: 0; font-size: 1.2em; }
13
+ CSS
14
+
15
+ def self.metadata
16
+ {
17
+ 'operations' => [
18
+ 'property access',
19
+ 'declaration merging',
20
+ 'to_s generation',
21
+ 'parse + iterate'
22
+ ],
23
+ 'note' => 'C extension performance is the same regardless of YJIT. This measures Ruby code.'
24
+ }
25
+ end
26
+
27
+ def self.speedup_config
28
+ # Compare without YJIT (baseline) vs with YJIT (comparison)
29
+ {
30
+ baseline_matcher: SpeedupCalculator::Matchers.without_yjit,
31
+ comparison_matcher: SpeedupCalculator::Matchers.with_yjit,
32
+ test_case_key: nil # No test_cases array, just operations
33
+ }
34
+ end
35
+
36
+ def sanity_checks
37
+ # Verify basic operations work
38
+ decls = Cataract::Declarations.new
39
+ decls['color'] = 'red'
40
+ raise 'Property access failed' unless decls['color']
41
+
42
+ parser = Cataract::Stylesheet.new
43
+ parser.add_block(SAMPLE_CSS)
44
+ raise 'Parse failed' if parser.rules_count.zero?
45
+ end
46
+
47
+ def call
48
+ run_property_access_benchmark
49
+ run_declaration_merging_benchmark
50
+ run_to_s_benchmark
51
+ run_parse_iterate_benchmark
52
+ end
53
+
54
+ private
55
+
56
+ def yjit_label
57
+ @yjit_label ||= defined?(RubyVM::YJIT.enabled?) && RubyVM::YJIT.enabled? ? 'YJIT' : 'no YJIT'
58
+ end
59
+
60
+ def run_property_access_benchmark
61
+ puts '=' * 80
62
+ puts "TEST: Property access (get/set) - #{yjit_label}"
63
+ puts '=' * 80
64
+
65
+ benchmark('property_access') do |x|
66
+ x.config(time: 3, warmup: 1)
67
+
68
+ x.report("#{yjit_label}: property access") do
69
+ decls = Cataract::Declarations.new
70
+ decls['color'] = 'red'
71
+ decls['background'] = 'blue'
72
+ decls['font-size'] = '16px'
73
+ decls['margin'] = '10px'
74
+ decls['padding'] = '5px'
75
+ _ = decls['color']
76
+ _ = decls['background']
77
+ _ = decls['font-size']
78
+ end
79
+ end
80
+ end
81
+
82
+ def run_declaration_merging_benchmark
83
+ puts "\n#{'=' * 80}"
84
+ puts "TEST: Declaration merging - #{yjit_label}"
85
+ puts '=' * 80
86
+
87
+ benchmark('declaration_merging') do |x|
88
+ x.config(time: 3, warmup: 1)
89
+
90
+ x.report("#{yjit_label}: declaration merging") do
91
+ decls1 = Cataract::Declarations.new
92
+ decls1['color'] = 'red'
93
+ decls1['font-size'] = '16px'
94
+
95
+ decls2 = Cataract::Declarations.new
96
+ decls2['background'] = 'blue'
97
+ decls2['margin'] = '10px'
98
+
99
+ decls1.merge(decls2)
100
+ end
101
+ end
102
+ end
103
+
104
+ def run_to_s_benchmark
105
+ puts "\n#{'=' * 80}"
106
+ puts "TEST: to_s generation - #{yjit_label}"
107
+ puts '=' * 80
108
+
109
+ benchmark('to_s') do |x|
110
+ x.config(time: 3, warmup: 1)
111
+
112
+ x.report("#{yjit_label}: to_s generation") do
113
+ decls = Cataract::Declarations.new
114
+ decls['color'] = 'red'
115
+ decls['background'] = 'blue'
116
+ decls['font-size'] = '16px'
117
+ decls['margin'] = '10px'
118
+ decls['padding'] = '5px'
119
+ decls.to_s
120
+ end
121
+ end
122
+ end
123
+
124
+ def run_parse_iterate_benchmark
125
+ puts "\n#{'=' * 80}"
126
+ puts "TEST: Parse + iterate - #{yjit_label}"
127
+ puts '=' * 80
128
+
129
+ benchmark('parse_iterate') do |x|
130
+ x.config(time: 3, warmup: 1)
131
+
132
+ x.report("#{yjit_label}: parse + iterate") do
133
+ parser = Cataract::Stylesheet.new
134
+ parser.add_block(SAMPLE_CSS)
135
+ parser.select(&:selector?).each do |rule|
136
+ _ = rule.declarations
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
data/cataract.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/cataract/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'cataract'
7
+ spec.version = Cataract::VERSION
8
+ spec.authors = ['Your Name']
9
+ spec.email = ['your.email@example.com']
10
+
11
+ spec.summary = 'CSS parser built with Ragel state machines'
12
+ spec.description = 'A high-performance CSS parser using Ragel finite state machines for accurate parsing of complex CSS structures'
13
+ spec.homepage = 'https://github.com/jamescook/cataract'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 3.1.0')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+ spec.metadata['rubygems_mfa_required'] = 'true'
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) || f.match(/^test_.*\.rb$/) }
25
+ end
26
+
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ # C extension configuration
32
+ # Two separate extensions: core parser and optional color conversion
33
+ spec.extensions = ['ext/cataract/extconf.rb', 'ext/cataract_color/extconf.rb']
34
+ end
data/cliff.toml ADDED
@@ -0,0 +1,92 @@
1
+ # git-cliff ~ configuration file
2
+ # https://git-cliff.org/docs/configuration
3
+
4
+
5
+ [changelog]
6
+ # A Tera template to be rendered for each release in the changelog.
7
+ # See https://keats.github.io/tera/docs/#introduction
8
+ body = """
9
+ {% if version %}\
10
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
11
+ {% else %}\
12
+ ## [unreleased]
13
+ {% endif %}\
14
+ {% for group, commits in commits | group_by(attribute="group") %}
15
+ ### {{ group | striptags | trim | upper_first }}
16
+ {% for commit in commits %}
17
+ - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
18
+ {% if commit.breaking %}[**breaking**] {% endif %}\
19
+ {{ commit.message | upper_first }}\
20
+ {% endfor %}
21
+ {% endfor %}
22
+ """
23
+ # Remove leading and trailing whitespaces from the changelog's body.
24
+ trim = true
25
+ # Render body even when there are no releases to process.
26
+ render_always = true
27
+ # An array of regex based postprocessors to modify the changelog.
28
+ postprocessors = [
29
+ # Replace the placeholder <REPO> with a URL.
30
+ #{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
31
+ ]
32
+ # render body even when there are no releases to process
33
+ # render_always = true
34
+ # output file path
35
+ # output = "test.md"
36
+
37
+ [git]
38
+ # Parse commits according to the conventional commits specification.
39
+ # See https://www.conventionalcommits.org
40
+ conventional_commits = true
41
+ # Exclude commits that do not match the conventional commits specification.
42
+ filter_unconventional = true
43
+ # Require all commits to be conventional.
44
+ # Takes precedence over filter_unconventional.
45
+ require_conventional = false
46
+ # Split commits on newlines, treating each line as an individual commit.
47
+ split_commits = false
48
+ # An array of regex based parsers to modify commit messages prior to further processing.
49
+ commit_preprocessors = [
50
+ # Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
51
+ #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
52
+ # Check spelling of the commit message using https://github.com/crate-ci/typos.
53
+ # If the spelling is incorrect, it will be fixed automatically.
54
+ #{ pattern = '.*', replace_command = 'typos --write-changes -' },
55
+ ]
56
+ # Prevent commits that are breaking from being excluded by commit parsers.
57
+ protect_breaking_commits = false
58
+ # An array of regex based parsers for extracting data from the commit message.
59
+ # Assigns commits to groups.
60
+ # Optionally sets the commit's scope and can decide to exclude commits from further processing.
61
+ commit_parsers = [
62
+ { message = "^feat", group = "<!-- 0 -->๐Ÿš€ Features" },
63
+ { message = "^fix", group = "<!-- 1 -->๐Ÿ› Bug Fixes" },
64
+ { message = "^doc", group = "<!-- 3 -->๐Ÿ“š Documentation" },
65
+ { message = "^perf", group = "<!-- 4 -->โšก Performance" },
66
+ { message = "^refactor", group = "<!-- 2 -->๐Ÿšœ Refactor" },
67
+ { message = "^style", group = "<!-- 5 -->๐ŸŽจ Styling" },
68
+ { message = "^test", group = "<!-- 6 -->๐Ÿงช Testing" },
69
+ { message = "^chore\\(release\\): prepare for", skip = true },
70
+ { message = "^chore\\(deps.*\\)", skip = true },
71
+ { message = "^chore\\(pr\\)", skip = true },
72
+ { message = "^chore\\(pull\\)", skip = true },
73
+ { message = "^chore|^ci", group = "<!-- 7 -->โš™๏ธ Miscellaneous Tasks" },
74
+ { body = ".*security", group = "<!-- 8 -->๐Ÿ›ก๏ธ Security" },
75
+ { message = "^revert", group = "<!-- 9 -->โ—€๏ธ Revert" },
76
+ { message = ".*", group = "<!-- 10 -->๐Ÿ’ผ Other" },
77
+ ]
78
+ # Exclude commits that are not matched by any commit parser.
79
+ filter_commits = false
80
+ # An array of link parsers for extracting external references, and turning them into URLs, using regex.
81
+ link_parsers = []
82
+ # Include only the tags that belong to the current branch.
83
+ use_branch_tags = false
84
+ # Order releases topologically instead of chronologically.
85
+ topo_order = false
86
+ # Order releases topologically instead of chronologically.
87
+ topo_order_commits = true
88
+ # Order of commits in each group/release within the changelog.
89
+ # Allowed values: newest, oldest
90
+ sort_commits = "oldest"
91
+ # Process submodules commits
92
+ recurse_submodules = false