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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89396bf500fc32cfa8d632d90f8b1ff5e80b93a24793cc8c6fd183ca47619f7e
4
- data.tar.gz: bef49a87c354f9947616bb4d07047bb876e7a2861a7de2e2846f2dd0cd9cd8e8
3
+ metadata.gz: b8c9aa122ff45945bef0a05d6fa6a5dcfd46252ffe5970bf288074db5d3514c9
4
+ data.tar.gz: cdafcb1ca599a58449dc03c239e5b8891f4399629dbdf7ca4e3ef23fb7f6ab94
5
5
  SHA512:
6
- metadata.gz: 0d395bcb60f2a6646daae924eabe1c83c2aa79e174de9186b6ab3bf9f855c34a2f464fa1d221e550afa47d4e271b0aaf9d4184986ac516d0feb412fecd18772f
7
- data.tar.gz: feb093da902ee218121c112471c2ea899540373c9ce74eff0db588f94ae5ab0992f7b39014bf6a3edb74a54e9b11e7f80d7c584f15dd08a56e4e4b37a63ee2c1
6
+ metadata.gz: 35bbffc26c5eb757a90935fd0caf362818d80cc33af0860c738061ca3d0047f6b6d5c8124087d8283c0bb93b26699990279f98af96215456099a493d1e4720ee
7
+ data.tar.gz: 567ffe7ce56412c5913384ce8b6470b9d8d0e7e880463e2024a6d4cd6fca4bcb9b792ce0ca890aea1edf07ed218497a8369058f38b8b30ab94bee63694ef03e9
@@ -0,0 +1,27 @@
1
+ name: CI - Manual Rubies
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ test-alt-rubies:
8
+ runs-on: ubuntu-latest
9
+ strategy:
10
+ fail-fast: false
11
+ matrix:
12
+ ruby: [jruby, truffleruby]
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Set up Ruby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ matrix.ruby }}
21
+ bundler-cache: true
22
+
23
+ - name: Display Ruby version
24
+ run: ruby --version
25
+
26
+ - name: Run tests
27
+ run: bundle exec rake compile test
data/.overcommit.yml CHANGED
@@ -8,7 +8,7 @@ PreCommit:
8
8
  RuboCop:
9
9
  enabled: true
10
10
  on_warn: fail # Treat warnings as failures
11
- command: ['bundle', 'exec', 'rubocop']
11
+ # Remove explicit command to allow Overcommit to pass staged files only
12
12
  flags: ['--force-exclusion']
13
13
  include:
14
14
  - '**/*.rb'
data/.rubocop.yml CHANGED
@@ -1,3 +1,5 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
1
3
  plugins:
2
4
  - rubocop-performance
3
5
  - rubocop-minitest
@@ -44,6 +46,12 @@ Metrics/BlockLength:
44
46
  Metrics/ParameterLists:
45
47
  Enabled: false
46
48
 
49
+ # Block nesting in byte-level parsers reflects CSS structure, not complexity
50
+ # Extracting helper methods would add method dispatch overhead in hot path
51
+ Metrics/BlockNesting:
52
+ Exclude:
53
+ - 'lib/cataract/pure/**/*.rb'
54
+
47
55
  # Keep line length for tests disabled - long test strings are fine
48
56
  Layout/LineLength:
49
57
  Exclude:
@@ -63,6 +71,8 @@ Style/Documentation:
63
71
  Exclude:
64
72
  - 'test/**/*'
65
73
  - 'benchmarks/**/*'
74
+ - 'lib/cataract/pure/**/*.rb'
75
+ - 'lib/cataract/pure.rb'
66
76
 
67
77
  # Disable modifier if/unless enforcement - use it when it's clearer, not because a cop says so
68
78
  Style/IfUnlessModifier:
@@ -81,3 +91,55 @@ Naming/VariableNumber:
81
91
  Naming/PredicatePrefix:
82
92
  Exclude:
83
93
  - 'lib/**/*.rb'
94
+
95
+ # Performance-critical hot path exclusions for lib/cataract/pure
96
+ # These cosmetic style rules add overhead via method dispatch or allocation
97
+
98
+ # Disable numeric predicates in hot path - method dispatch overhead
99
+ # Benchmark shows `> 0` is faster than `.positive?` due to avoiding method call
100
+ Style/NumericPredicate:
101
+ Exclude:
102
+ - 'lib/cataract/pure/**/*.rb'
103
+
104
+ # Disable multiple comparison style - avoid array allocation
105
+ # `||` chain is faster than `Array#include?` which allocates an array
106
+ Style/MultipleComparison:
107
+ Exclude:
108
+ - 'lib/cataract/pure/**/*.rb'
109
+
110
+ # Disable zero-length predicate in hot path - method dispatch overhead
111
+ # Direct comparison `.length > 0` is faster than `.empty?` method call
112
+ Style/ZeroLengthPredicate:
113
+ Exclude:
114
+ - 'lib/cataract/pure/**/*.rb'
115
+
116
+ # Benchmarked range vs offsets. Range has to allocated
117
+ # so not ideal in the hot path
118
+ Style/SlicingWithRange:
119
+ Exclude:
120
+ - 'lib/cataract/pure/**/*.rb'
121
+
122
+ # Disable modifier while/until style in parsing code - readability
123
+ # Byte-level parsing loops are clearer with traditional while...end form
124
+ # Example: `while i < len && ident_char?(selector.getbyte(i)); i += 1; end`
125
+ # is more readable than `i += 1 while i < len && ident_char?(selector.getbyte(i))`
126
+ Style/WhileUntilModifier:
127
+ Exclude:
128
+ - 'lib/cataract/pure/**/*.rb'
129
+
130
+ # Disable between? suggestion in hot path - method dispatch overhead
131
+ # Benchmark shows `byte >= X && byte <= Y` is 2.60-4.82x faster than `.between?(X, Y)`
132
+ Style/ComparableBetween:
133
+ Exclude:
134
+ - 'lib/cataract/pure/**/*.rb'
135
+
136
+ # Disable map suggestion for array building - performance
137
+ # Benchmark shows `each { arr << x }` is 1.05-1.11x faster than `arr = map { x }`
138
+ # The << pattern is faster than map's implicit array return (even without YJIT)
139
+ Style/MapIntoArray:
140
+ Exclude:
141
+ - 'lib/cataract/pure/**/*.rb'
142
+
143
+ Style/OptionalBooleanParameter:
144
+ Exclude:
145
+ - 'lib/cataract/pure/**/*.rb'
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,186 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2025-11-12 02:24:55 UTC using RuboCop version 1.81.7.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 13
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ Layout/EmptyLineAfterGuardClause:
12
+ Exclude:
13
+ - 'benchmarks/benchmark_merging.rb'
14
+ - 'benchmarks/benchmark_parsing.rb'
15
+ - 'benchmarks/benchmark_serialization.rb'
16
+ - 'benchmarks/benchmark_specificity.rb'
17
+ - 'scripts/generate_benchmarks_md.rb'
18
+
19
+ # Offense count: 1
20
+ # This cop supports safe autocorrection (--autocorrect).
21
+ Layout/EmptyLinesAfterModuleInclusion:
22
+ Exclude:
23
+ - 'benchmarks/parsing_tests.rb'
24
+
25
+ # Offense count: 1
26
+ # This cop supports safe autocorrection (--autocorrect).
27
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
28
+ # SupportedStyles: aligned, indented
29
+ Layout/MultilineOperationIndentation:
30
+ Exclude:
31
+ - 'lib/cataract/pure/helpers.rb'
32
+
33
+ # Offense count: 1
34
+ # This cop supports safe autocorrection (--autocorrect).
35
+ Lint/AmbiguousOperatorPrecedence:
36
+ Exclude:
37
+ - 'lib/cataract/pure/merge.rb'
38
+
39
+ # Offense count: 2
40
+ # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
41
+ Lint/DuplicateBranch:
42
+ Exclude:
43
+ - 'lib/cataract/pure/merge.rb'
44
+
45
+ # Offense count: 1
46
+ Lint/IneffectiveAccessModifier:
47
+ Exclude:
48
+ - 'benchmarks/speedup_calculator.rb'
49
+
50
+ # Offense count: 1
51
+ # This cop supports safe autocorrection (--autocorrect).
52
+ # Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
53
+ Lint/UselessAccessModifier:
54
+ Exclude:
55
+ - 'benchmarks/speedup_calculator.rb'
56
+
57
+ # Offense count: 12
58
+ # This cop supports safe autocorrection (--autocorrect).
59
+ Lint/UselessAssignment:
60
+ Exclude:
61
+ - 'benchmarks/benchmark_merging.rb'
62
+ - 'benchmarks/benchmark_parsing.rb'
63
+ - 'benchmarks/benchmark_serialization.rb'
64
+ - 'benchmarks/benchmark_specificity.rb'
65
+
66
+ # Offense count: 1
67
+ # This cop supports safe autocorrection (--autocorrect).
68
+ Minitest/EmptyLineBeforeAssertionMethods:
69
+ Exclude:
70
+ - 'test/test_stylesheet.rb'
71
+
72
+ # Offense count: 7
73
+ # This cop supports unsafe autocorrection (--autocorrect-all).
74
+ Performance/UnfreezeString:
75
+ Exclude:
76
+ - 'lib/cataract/pure.rb'
77
+ - 'lib/cataract/pure/merge.rb'
78
+ - 'lib/cataract/pure/parser.rb'
79
+
80
+ # Offense count: 4
81
+ # This cop supports safe autocorrection (--autocorrect).
82
+ # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
83
+ # SupportedStyles: assign_to_condition, assign_inside_condition
84
+ Style/ConditionalAssignment:
85
+ Exclude:
86
+ - 'lib/cataract/pure/imports.rb'
87
+ - 'lib/cataract/pure/merge.rb'
88
+
89
+ # Offense count: 4
90
+ # Configuration parameters: AllowedConstants.
91
+ Style/Documentation:
92
+ Exclude:
93
+ - 'test/**/*'
94
+ - 'benchmarks/**/*'
95
+ - 'lib/cataract/pure/**/*.rb'
96
+ - 'lib/cataract/pure.rb'
97
+ - 'lib/cataract/at_rule.rb'
98
+ - 'lib/cataract/declaration.rb'
99
+ - 'lib/cataract/rule.rb'
100
+
101
+ # Offense count: 4
102
+ # Configuration parameters: MinBranchesCount.
103
+ Style/HashLikeCase:
104
+ Exclude:
105
+ - 'benchmarks/merging_tests.rb'
106
+ - 'benchmarks/parsing_tests.rb'
107
+ - 'benchmarks/serialization_tests.rb'
108
+ - 'benchmarks/specificity_tests.rb'
109
+
110
+ # Offense count: 14
111
+ # This cop supports unsafe autocorrection (--autocorrect-all).
112
+ Style/IdenticalConditionalBranches:
113
+ Exclude:
114
+ - 'benchmarks/benchmark_merging.rb'
115
+ - 'benchmarks/benchmark_parsing.rb'
116
+ - 'benchmarks/benchmark_serialization.rb'
117
+ - 'benchmarks/benchmark_specificity.rb'
118
+ - 'lib/cataract/pure/parser.rb'
119
+ - 'lib/cataract/pure/serializer.rb'
120
+
121
+ # Offense count: 5
122
+ # This cop supports safe autocorrection (--autocorrect).
123
+ # Configuration parameters: AllowIfModifier.
124
+ Style/IfInsideElse:
125
+ Exclude:
126
+ - 'lib/cataract/pure/merge.rb'
127
+ - 'lib/cataract/pure/serializer.rb'
128
+ - 'lib/cataract/pure/specificity.rb'
129
+
130
+ # Offense count: 1
131
+ # This cop supports safe autocorrection (--autocorrect).
132
+ Style/NegatedIfElseCondition:
133
+ Exclude:
134
+ - 'lib/cataract/pure/merge.rb'
135
+
136
+ # Offense count: 1
137
+ # This cop supports safe autocorrection (--autocorrect).
138
+ Style/RedundantAssignment:
139
+ Exclude:
140
+ - 'lib/cataract/pure/specificity.rb'
141
+
142
+ # Offense count: 2
143
+ # This cop supports safe autocorrection (--autocorrect).
144
+ Style/RedundantParentheses:
145
+ Exclude:
146
+ - 'lib/cataract/pure/imports.rb'
147
+ - 'lib/cataract/pure/parser.rb'
148
+
149
+ # Offense count: 1
150
+ # This cop supports safe autocorrection (--autocorrect).
151
+ Style/RedundantRegexpArgument:
152
+ Exclude:
153
+ - 'test/test_stylesheet.rb'
154
+
155
+ # Offense count: 3
156
+ # This cop supports safe autocorrection (--autocorrect).
157
+ # Configuration parameters: AllowModifier.
158
+ Style/SoleNestedConditional:
159
+ Exclude:
160
+ - 'lib/cataract/pure/merge.rb'
161
+ - 'lib/cataract/pure/parser.rb'
162
+
163
+ # Offense count: 3
164
+ # This cop supports safe autocorrection (--autocorrect).
165
+ # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
166
+ # SupportedStyles: single_quotes, double_quotes
167
+ Style/StringLiterals:
168
+ Exclude:
169
+ - 'benchmarks/merging_tests.rb'
170
+ - 'scripts/generate_benchmarks_md.rb'
171
+ - 'test/test_stylesheet.rb'
172
+
173
+ # Offense count: 1
174
+ # This cop supports safe autocorrection (--autocorrect).
175
+ # Configuration parameters: .
176
+ # SupportedStyles: percent, brackets
177
+ Style/SymbolArray:
178
+ EnforcedStyle: percent
179
+ MinSize: 3
180
+
181
+ # Offense count: 4
182
+ # This cop supports safe autocorrection (--autocorrect).
183
+ # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
184
+ # URISchemes: http, https
185
+ Layout/LineLength:
186
+ Max: 184
data/BENCHMARKS.md CHANGED
@@ -4,176 +4,97 @@
4
4
 
5
5
  # Performance Benchmarks
6
6
 
7
- Comprehensive performance comparison between Cataract and css_parser gem.
7
+ Performance comparison between Cataract's C extension and pure Ruby implementations, with css_parser as a reference.
8
8
 
9
9
  ## Test Environment
10
10
 
11
- - **Ruby**: ruby 3.4.5 (2025-07-16 revision 20cda200d3) +YJIT +PRISM [arm64-darwin23]
11
+ - **Ruby**: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin23]
12
12
  - **CPU**: Apple M1 Pro
13
13
  - **Memory**: 32GB
14
14
  - **OS**: macOS 14.5
15
- - **Generated**: 2025-10-30T16:01:15-05:00
15
+ - **Generated**: 2025-11-11T16:02:08-06:00
16
16
 
17
- <details>
18
- <summary><h2>CSS Parsing</h2></summary>
19
-
20
- Performance of parsing CSS into internal data structures.
17
+ ## CSS Parsing
21
18
 
22
19
  Time to parse CSS into internal data structures
23
20
 
24
- ### Small CSS (64 lines, 1.0KB)
25
-
26
-
27
- | Parser | Speed | Time per operation |
28
- |--------|-------|-------------------|
29
- | css_parser | 6.16K i/s | 162.34 μs |
30
- | **Cataract** | **63.79K i/s** | **15.68 μs** |
31
- | **Speedup** | **10.36x faster** | |
32
-
33
- ### Medium CSS with @media (139 lines, 1.6KB)
34
-
21
+ | Test Case | Native | Pure (no YJIT) | Pure (YJIT) | css_parser (no YJIT) | css_parser (YJIT) |
22
+ |-----------|--------|----------------|-------------|----------------------|-------------------|
23
+ | Small CSS (64 lines, 1.0KB) | 63.2K i/s | 3.56K i/s | 15.49K i/s | 4.7K i/s | 6.39K i/s |
24
+ | Medium CSS with @media (139 lines, 1.6KB) | 40.14K i/s | 2.07K i/s | 8.76K i/s | 2.81K i/s | 3.54K i/s |
35
25
 
36
- | Parser | Speed | Time per operation |
37
- |--------|-------|-------------------|
38
- | css_parser | 3.44K i/s | 290.64 μs |
39
- | **Cataract** | **41.45K i/s** | **24.13 μs** |
40
- | **Speedup** | **12.05x faster** | |
26
+ ### Speedups
41
27
 
42
-
43
- </details>
28
+ | Comparison | Speedup |
29
+ |------------|---------|
30
+ | Native vs Pure (no YJIT) | 18.58x faster (avg) |
31
+ | Native vs Pure (YJIT) | 4.26x faster (avg) |
32
+ | YJIT impact on Pure Ruby | 4.31x faster (avg) |
44
33
 
45
34
  ---
46
35
 
47
- <details>
48
- <summary><h2>CSS Serialization (to_s)</h2></summary>
49
-
50
- Performance of converting parsed CSS back to string format.
36
+ ## CSS Serialization
51
37
 
52
38
  Time to convert parsed CSS back to string format
53
39
 
54
- ### Full Serialization (Bootstrap CSS - 191KB)
55
-
56
-
57
- | Parser | Speed | Time per operation |
58
- |--------|-------|-------------------|
59
- | css_parser | 34.0 i/s | 29.41 ms |
60
- | **Cataract** | **714.8 i/s** | **1.4 ms** |
61
- | **Speedup** | **21.02x faster** | |
40
+ | Test Case | Native | Pure (no YJIT) | Pure (YJIT) | css_parser (no YJIT) | css_parser (YJIT) |
41
+ |-----------|--------|----------------|-------------|----------------------|-------------------|
42
+ | Full Serialization (Bootstrap CSS - 191KB) | 1.79K i/s | 442.2 i/s | 716.9 i/s | 38.5 i/s | 39.7 i/s |
43
+ | Media Type Filtering (print only) | 282.98K i/s | 114.55K i/s | 167.51K i/s | 2.74K i/s | 4.32K i/s |
62
44
 
63
- ### Media Type Filtering (print only)
45
+ ### Speedups
64
46
 
65
-
66
- | Parser | Speed | Time per operation |
67
- |--------|-------|-------------------|
68
- | css_parser | 4.06K i/s | 246.56 μs |
69
- | **Cataract** | **232.22K i/s** | **4.31 μs** |
70
- | **Speedup** | **57.26x faster** | |
71
-
72
-
73
- </details>
47
+ | Comparison | Speedup |
48
+ |------------|---------|
49
+ | Native vs Pure (no YJIT) | 3.26x faster (avg) |
50
+ | Native vs Pure (YJIT) | 1.69x faster (avg) |
51
+ | YJIT impact on Pure Ruby | 1.46x faster (avg) |
74
52
 
75
53
  ---
76
54
 
77
- <details>
78
- <summary><h2>Specificity Calculation</h2></summary>
79
-
80
- Performance of calculating CSS selector specificity values.
55
+ ## Specificity Calculation
81
56
 
82
57
  Time to calculate CSS selector specificity values
83
58
 
84
- | Test Case | Speedup |
85
- |-----------|---------|
86
- | Simple Selectors | **22.03x faster** |
87
- | Compound Selectors | **30.55x faster** |
88
- | Combinators | **28.34x faster** |
89
- | Pseudo-classes & Pseudo-elements | **46.06x faster** |
90
- | :not() Pseudo-class (CSS3) | **23.64x faster** |
91
- | Complex Real-world Selectors | **49.17x faster** |
59
+ | Test Case | Native | Pure (no YJIT) | Pure (YJIT) | css_parser (no YJIT) | css_parser (YJIT) |
60
+ |-----------|--------|----------------|-------------|----------------------|-------------------|
61
+ | Simple Selectors | 8.31M i/s | 499.15K i/s | 2.59M i/s | 370.14K i/s | 344.42K i/s |
62
+ | Compound Selectors | 6.57M i/s | 184.61K i/s | 403.41K i/s | 223.76K i/s | 204.19K i/s |
63
+ | Combinators | 5.24M i/s | 144.7K i/s | 253.9K i/s | 188.05K i/s | 177.88K i/s |
64
+ | Pseudo-classes & Pseudo-elements | 5.39M i/s | 115.46K i/s | 194.13K i/s | 116.96K i/s | 109.64K i/s |
65
+ | :not() Pseudo-class (CSS3) | 3.31M i/s | 101.48K i/s | 160.62K i/s | 144.35K i/s | 141.24K i/s |
66
+ | Complex Real-world Selectors | 4.09M i/s | 51.63K i/s | 77.37K i/s | 82.16K i/s | 80.6K i/s |
92
67
 
93
- **Summary:** 22.03x faster to 49.17x faster (avg 33.3x faster)
68
+ ### Speedups
94
69
 
95
- </details>
70
+ | Comparison | Speedup |
71
+ |------------|---------|
72
+ | Native vs Pure (no YJIT) | 41.16x faster (avg) |
73
+ | Native vs Pure (YJIT) | 8.94x faster (avg) |
74
+ | YJIT impact on Pure Ruby | 3.36x faster (avg) |
96
75
 
97
76
  ---
98
77
 
99
- <details>
100
- <summary><h2>CSS Merging</h2></summary>
101
-
102
- Performance of merging multiple CSS rule sets with the same selector.
78
+ ## CSS Merging
103
79
 
104
80
  Time to merge multiple CSS rule sets with same selector
105
81
 
106
- | Test Case | Speedup |
107
- |-----------|---------|
108
- | No shorthand properties (large) | **4.14x faster** |
109
- | Simple properties | **3.86x faster** |
110
- | Cascade with specificity | **5.75x faster** |
111
- | Important declarations | **6.1x faster** |
112
- | Shorthand expansion | **4.16x faster** |
113
- | Complex merging | **3.07x faster** |
114
-
115
- **Summary:** 3.07x faster to 6.1x faster (avg 4.51x faster)
116
-
117
- ### What's Being Tested
118
- - Specificity-based CSS cascade (ID > class > element)
119
- - `!important` declaration handling
120
- - Shorthand property expansion (e.g., `margin` → `margin-top`, `margin-right`, etc.)
121
- - Shorthand property creation from longhand properties
122
-
123
- </details>
124
-
125
- ---
126
-
127
- <details>
128
- <summary><h2>YJIT Impact</h2></summary>
129
-
130
- Impact of Ruby's YJIT JIT compiler on Ruby-side operations. The C extension performance is the same regardless of YJIT.
131
-
132
- Ruby-side operations with and without YJIT
133
-
134
- ### Operations Per Second
135
-
136
- | Operation | Without YJIT | With YJIT | YJIT Improvement |
137
- |-----------|--------------|-----------|------------------|
138
- | property access | 227.18K i/s | 322.32K i/s | **1.42x faster** (42% faster) |
139
- | declaration merging | 204.26K i/s | 337.81K i/s | **1.65x faster** (65% faster) |
140
- | to_s generation | 242.66K i/s | 391.16K i/s | **1.61x faster** (61% faster) |
141
- | parse + iterate | 121.52K i/s | 142.77K i/s | **1.17x faster** (17% faster) |
142
-
143
- ### Key Takeaways
144
- - YJIT provides significant performance boost for Ruby-side operations
145
- - Greatest impact on declaration merging
146
- - Parse + iterate benefits least since most work is in C
147
- - Recommended: Enable YJIT in production (`--yjit` flag or `RUBY_YJIT_ENABLE=1`)
148
-
149
- </details>
150
-
151
- ---
152
-
153
- ## Summary
154
-
155
- ### Performance Highlights
156
-
157
- | Category | Min Speedup | Max Speedup | Avg Speedup |
158
- |----------|-------------|-------------|-------------|
159
- | **Parsing** | 10.36x faster | 12.05x faster | 11.2x faster |
160
- | **Serialization** | 21.02x faster | 57.26x faster | 39.14x faster |
161
- | **Specificity** | 22.03x faster | 49.17x faster | 33.3x faster |
162
- | **Merging** | 3.07x faster | 6.1x faster | 4.51x faster |
163
-
164
- ### Implementation Notes
165
-
166
- 1. **C Extension**: Critical paths (parsing, specificity, merging, serialization) implemented in C
167
- 2. **Efficient Data Structures**: Rules grouped by media query for O(1) lookups
168
- 3. **Memory Efficient**: Pre-allocated string buffers, minimal Ruby object allocations
169
- 4. **Optimized Algorithms**: Purpose-built CSS specificity calculator
82
+ | Test Case | Native | Pure (no YJIT) | Pure (YJIT) | css_parser (no YJIT) | css_parser (YJIT) |
83
+ |-----------|--------|----------------|-------------|----------------------|-------------------|
84
+ | No shorthand properties (large) | 10.5K i/s | 3.46K i/s | 6.73K i/s | 1.51K i/s | 2.27K i/s |
85
+ | Simple properties | 125.51K i/s | 77.36K i/s | 110.86K i/s | 27.92K i/s | 40.42K i/s |
86
+ | Cascade with specificity | 140.46K i/s | 80.08K i/s | 116.35K i/s | 31.48K i/s | 45.99K i/s |
87
+ | Important declarations | 139.59K i/s | 80.29K i/s | 116.39K i/s | 30.84K i/s | 44.87K i/s |
88
+ | Shorthand expansion | 10.5K i/s | 3.46K i/s | 6.73K i/s | 1.51K i/s | 2.27K i/s |
89
+ | Complex merging | 24.74K i/s | 16.62K i/s | 23.57K i/s | 11.59K i/s | 16.48K i/s |
170
90
 
171
- ### Use Cases
91
+ ### Speedups
172
92
 
173
- - **Large CSS files**: Handles complex stylesheets efficiently
174
- - **Specificity calculations**: Optimized for selector analysis
175
- - **High-volume processing**: Reduced allocations minimize GC pressure
176
- - **Production applications**: Tested with Bootstrap CSS and real-world stylesheets
93
+ | Comparison | Speedup |
94
+ |------------|---------|
95
+ | Native vs Pure (no YJIT) | 1.87x faster (avg) |
96
+ | Native vs Pure (YJIT) | 1.16x faster (avg) |
97
+ | YJIT impact on Pure Ruby | 1.45x faster (avg) |
177
98
 
178
99
  ---
179
100
 
@@ -181,14 +102,13 @@ Ruby-side operations with and without YJIT
181
102
 
182
103
  ```bash
183
104
  # All benchmarks
184
- rake benchmark 2>&1 | tee benchmark_output.txt
105
+ rake benchmark
185
106
 
186
107
  # Individual benchmarks
187
108
  rake benchmark:parsing
188
109
  rake benchmark:serialization
189
110
  rake benchmark:specificity
190
111
  rake benchmark:merging
191
- rake benchmark:yjit
192
112
 
193
113
  # Generate documentation
194
114
  rake benchmark:generate_docs
@@ -196,6 +116,7 @@ rake benchmark:generate_docs
196
116
 
197
117
  ## Notes
198
118
 
199
- - All benchmarks use benchmark-ips with 3s warmup and 5-10s measurement periods
200
- - Measurements are median i/s (iterations per second) with standard deviation
201
- - css_parser gem must be installed for comparison benchmarks
119
+ - Benchmarks use benchmark-ips with 1-2s warmup and 2-5s measurement periods
120
+ - Measurements show median iterations per second (i/s)
121
+ - css_parser gem is included for reference comparison
122
+ - YJIT is enabled/disabled per subprocess for accurate comparison
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.1.4] - 2025-11-12
2
+ - Major: Pure Ruby implementation added (#12)
3
+ - Fix: Media query serialization bugs - parentheses now preserved per CSS spec (min-width: 768px), fixed media query ordering
4
+ - Fix: CSS merge declaration ordering made consistent between C and pure Ruby implementations
5
+ - Fix: Shorthand property recreation (margin, padding, border, font, background, list-style) ordering
6
+ - Fix: Rule equality comparisons (Rule#==, AtRule#==)
7
+
8
+ ## [0.1.3] - 2025-11-11
9
+ - Fix: Proper handling of at-rules (@keyframes, @font-face, etc.) during CSS merge operations
10
+
1
11
  ## [0.1.2] - 2025-11-11
2
12
 
3
13
  - Fix segfault in merge
data/README.md CHANGED
@@ -34,6 +34,32 @@ gem install cataract
34
34
 
35
35
  - Ruby >= 3.1.0
36
36
 
37
+ ### Pure Ruby Implementation
38
+
39
+ Cataract includes a pure Ruby implementation alongside the C extension. This is useful for:
40
+ - Platforms where C extensions cannot be compiled
41
+ - Development/debugging without needing to recompile C code
42
+ - Environments with restricted native code execution
43
+
44
+ **In your Gemfile:**
45
+ ```ruby
46
+ gem 'cataract', require: 'cataract/pure'
47
+ ```
48
+
49
+ **Or set environment variable:**
50
+ ```ruby
51
+ # For CI, testing, or one-off usage
52
+ ENV['CATARACT_PURE'] = '1'
53
+ require 'cataract'
54
+ ```
55
+
56
+ **Check which implementation is loaded:**
57
+ ```ruby
58
+ Cataract::IMPLEMENTATION # => :native or :ruby
59
+ ```
60
+
61
+ **Note:** The pure Ruby implementation does not include color conversion functionality (`convert_colors!`), which is only available in the C extension.
62
+
37
63
  ## Usage
38
64
 
39
65
  ### Basic Parsing
@@ -142,9 +168,11 @@ See [BENCHMARKS.md](BENCHMARKS.md) for detailed performance comparisons.
142
168
 
143
169
  ## CSS Support
144
170
 
145
- Cataract aims to support all CSS specifications including:
171
+ Cataract parses and preserves all standard CSS including:
146
172
  - **Selectors**: All CSS2/CSS3 selectors (type, class, ID, attribute, pseudo-classes, pseudo-elements, combinators)
147
- - **At-rules**: `@media`, `@font-face`, `@keyframes`, `@supports`, `@page`, `@layer`, `@container`, `@property`, `@scope`, `@counter-style`
173
+ - **At-rules**:
174
+ - **`@media`**: Special handling with indexing and filtering API (`with_media(:print)`, `with_media(:all)`)
175
+ - **Others** (`@font-face`, `@keyframes`, `@supports`, `@page`, `@layer`, `@container`, `@property`, `@scope`, `@counter-style`): Parsed and preserved as-is (pass-through)
148
176
  - **Media Queries**: Full support including nested queries and media features
149
177
  - **Special syntax**: Data URIs, `calc()`, `url()`, CSS functions with parentheses
150
178
  - **!important**: Full support with correct cascade behavior