cataract 0.1.3 → 0.2.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci-manual-rubies.yml +44 -0
  3. data/.overcommit.yml +1 -1
  4. data/.rubocop.yml +96 -4
  5. data/.rubocop_todo.yml +186 -0
  6. data/BENCHMARKS.md +62 -141
  7. data/CHANGELOG.md +20 -0
  8. data/RAGEL_MIGRATION.md +2 -2
  9. data/README.md +37 -4
  10. data/Rakefile +72 -32
  11. data/cataract.gemspec +4 -1
  12. data/ext/cataract/cataract.c +59 -50
  13. data/ext/cataract/cataract.h +5 -3
  14. data/ext/cataract/css_parser.c +173 -65
  15. data/ext/cataract/extconf.rb +2 -2
  16. data/ext/cataract/{merge.c → flatten.c} +526 -468
  17. data/ext/cataract/shorthand_expander.c +164 -115
  18. data/lib/cataract/at_rule.rb +8 -9
  19. data/lib/cataract/declaration.rb +18 -0
  20. data/lib/cataract/import_resolver.rb +63 -43
  21. data/lib/cataract/import_statement.rb +49 -0
  22. data/lib/cataract/pure/byte_constants.rb +69 -0
  23. data/lib/cataract/pure/flatten.rb +1145 -0
  24. data/lib/cataract/pure/helpers.rb +35 -0
  25. data/lib/cataract/pure/imports.rb +268 -0
  26. data/lib/cataract/pure/parser.rb +1340 -0
  27. data/lib/cataract/pure/serializer.rb +590 -0
  28. data/lib/cataract/pure/specificity.rb +206 -0
  29. data/lib/cataract/pure.rb +153 -0
  30. data/lib/cataract/rule.rb +69 -15
  31. data/lib/cataract/stylesheet.rb +356 -49
  32. data/lib/cataract/version.rb +1 -1
  33. data/lib/cataract.rb +43 -26
  34. metadata +14 -26
  35. data/benchmarks/benchmark_harness.rb +0 -193
  36. data/benchmarks/benchmark_merging.rb +0 -121
  37. data/benchmarks/benchmark_optimization_comparison.rb +0 -168
  38. data/benchmarks/benchmark_parsing.rb +0 -153
  39. data/benchmarks/benchmark_ragel_removal.rb +0 -56
  40. data/benchmarks/benchmark_runner.rb +0 -70
  41. data/benchmarks/benchmark_serialization.rb +0 -180
  42. data/benchmarks/benchmark_shorthand.rb +0 -109
  43. data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
  44. data/benchmarks/benchmark_specificity.rb +0 -124
  45. data/benchmarks/benchmark_string_allocation.rb +0 -151
  46. data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
  47. data/benchmarks/benchmark_to_s_cached.rb +0 -55
  48. data/benchmarks/benchmark_value_splitter.rb +0 -54
  49. data/benchmarks/benchmark_yjit.rb +0 -158
  50. data/benchmarks/benchmark_yjit_workers.rb +0 -61
  51. data/benchmarks/profile_to_s.rb +0 -23
  52. data/benchmarks/speedup_calculator.rb +0 -83
  53. data/benchmarks/system_metadata.rb +0 -81
  54. data/benchmarks/templates/benchmarks.md.erb +0 -221
  55. data/benchmarks/yjit_tests.rb +0 -141
  56. data/scripts/fuzzer/run.rb +0 -828
  57. data/scripts/fuzzer/worker.rb +0 -99
  58. 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: bcb5043777c52ed2a3f4bcd96966dcbe00e0f7bcf5ac410398afc855d59407f5
4
+ data.tar.gz: 65fabbfff1bbc559a0998f5a412800987ed2921252cdd2d4a757c6b59041c66a
5
5
  SHA512:
6
- metadata.gz: 0d395bcb60f2a6646daae924eabe1c83c2aa79e174de9186b6ab3bf9f855c34a2f464fa1d221e550afa47d4e271b0aaf9d4184986ac516d0feb412fecd18772f
7
- data.tar.gz: feb093da902ee218121c112471c2ea899540373c9ce74eff0db588f94ae5ab0992f7b39014bf6a3edb74a54e9b11e7f80d7c584f15dd08a56e4e4b37a63ee2c1
6
+ metadata.gz: cee4076685ff5a5e6ae8dbe441a9e7cefc8f1ad109fa7179373cb0bb39e4118b570ddfef68aeef573b002a683b3ca66148293f460b52a562472596d4d1e11842
7
+ data.tar.gz: de9ae8a5f1d684c797706b48e10e7372cda9ae280698e67ea3dac25be81331a901c9a181dd8ca04d2cb9ce46b2447fc0709501b0fd53c848bcc6981a4d286597
@@ -0,0 +1,44 @@
1
+ name: CI - Manual Rubies
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ jobs:
7
+ test-jruby:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+
12
+ - name: Set up Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: jruby
16
+ bundler-cache: true
17
+
18
+ - name: Display Ruby version
19
+ run: ruby --version
20
+
21
+ - name: Run tests
22
+ run: bundle exec rake test:pure
23
+
24
+ test-alt-rubies:
25
+ runs-on: ubuntu-latest
26
+ strategy:
27
+ fail-fast: false
28
+ matrix:
29
+ ruby: [truffleruby, ruby-head]
30
+
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+
34
+ - name: Set up Ruby
35
+ uses: ruby/setup-ruby@v1
36
+ with:
37
+ ruby-version: ${{ matrix.ruby }}
38
+ bundler-cache: true
39
+
40
+ - name: Display Ruby version
41
+ run: ruby --version
42
+
43
+ - name: Run tests
44
+ 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,8 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ require:
4
+ - ./.rubocop/cataract/ban_assert_includes.rb
5
+
1
6
  plugins:
2
7
  - rubocop-performance
3
8
  - rubocop-minitest
@@ -44,6 +49,12 @@ Metrics/BlockLength:
44
49
  Metrics/ParameterLists:
45
50
  Enabled: false
46
51
 
52
+ # Block nesting in byte-level parsers reflects CSS structure, not complexity
53
+ # Extracting helper methods would add method dispatch overhead in hot path
54
+ Metrics/BlockNesting:
55
+ Exclude:
56
+ - 'lib/cataract/pure/**/*.rb'
57
+
47
58
  # Keep line length for tests disabled - long test strings are fine
48
59
  Layout/LineLength:
49
60
  Exclude:
@@ -58,11 +69,17 @@ Layout/LineLength:
58
69
  Minitest/MultipleAssertions:
59
70
  Max: 10
60
71
 
61
- Style/Documentation:
62
- Enabled: true
63
- Exclude:
72
+ # Disable ambiguous block association warnings in tests
73
+ # Tests frequently use assert/refute with blocks that call methods with their own blocks,
74
+ # e.g., `assert sheet.rules.any? { |r| r == '.box { color: red; }' }`
75
+ # This compares Rule objects against CSS strings using our custom Rule#== implementation.
76
+ # While this triggers the ambiguous block warning, it's intentional and clear in context.
77
+ Lint/AmbiguousBlockAssociation:
78
+ Exclude:
64
79
  - 'test/**/*'
65
- - 'benchmarks/**/*'
80
+
81
+ Style/Documentation:
82
+ Enabled: false
66
83
 
67
84
  # Disable modifier if/unless enforcement - use it when it's clearer, not because a cop says so
68
85
  Style/IfUnlessModifier:
@@ -81,3 +98,78 @@ Naming/VariableNumber:
81
98
  Naming/PredicatePrefix:
82
99
  Exclude:
83
100
  - 'lib/**/*.rb'
101
+
102
+ # Performance-critical hot path exclusions for lib/cataract/pure
103
+ # These cosmetic style rules add overhead via method dispatch or allocation
104
+
105
+ # Disable numeric predicates in hot path - method dispatch overhead
106
+ # Benchmark shows `> 0` is faster than `.positive?` due to avoiding method call
107
+ Style/NumericPredicate:
108
+ Exclude:
109
+ - 'lib/cataract/pure/**/*.rb'
110
+
111
+ # Disable multiple comparison style - avoid array allocation
112
+ # `||` chain is faster than `Array#include?` which allocates an array
113
+ Style/MultipleComparison:
114
+ Exclude:
115
+ - 'lib/cataract/pure/**/*.rb'
116
+
117
+ # Disable zero-length predicate in hot path - method dispatch overhead
118
+ # Direct comparison `.length > 0` is faster than `.empty?` method call
119
+ Style/ZeroLengthPredicate:
120
+ Exclude:
121
+ - 'lib/cataract/pure/**/*.rb'
122
+
123
+ # Benchmarked range vs offsets. Range has to allocated
124
+ # so not ideal in the hot path
125
+ Style/SlicingWithRange:
126
+ Exclude:
127
+ - 'lib/cataract/pure/**/*.rb'
128
+
129
+ # Disable modifier while/until style in parsing code - readability
130
+ # Byte-level parsing loops are clearer with traditional while...end form
131
+ # Example: `while i < len && ident_char?(selector.getbyte(i)); i += 1; end`
132
+ # is more readable than `i += 1 while i < len && ident_char?(selector.getbyte(i))`
133
+ Style/WhileUntilModifier:
134
+ Exclude:
135
+ - 'lib/cataract/pure/**/*.rb'
136
+
137
+ # Disable between? suggestion in hot path - method dispatch overhead
138
+ # Benchmark shows `byte >= X && byte <= Y` is 2.60-4.82x faster than `.between?(X, Y)`
139
+ Style/ComparableBetween:
140
+ Exclude:
141
+ - 'lib/cataract/pure/**/*.rb'
142
+
143
+ # Disable map suggestion for array building - performance
144
+ # Benchmark shows `each { arr << x }` is 1.05-1.11x faster than `arr = map { x }`
145
+ # The << pattern is faster than map's implicit array return (even without YJIT)
146
+ Style/MapIntoArray:
147
+ Exclude:
148
+ - 'lib/cataract/pure/**/*.rb'
149
+
150
+ Style/OptionalBooleanParameter:
151
+ Exclude:
152
+ - 'lib/cataract/pure/**/*.rb'
153
+
154
+ Style/StderrPuts:
155
+ Exclude:
156
+ - 'scripts/**/*.rb'
157
+
158
+ # Disable all performance cops in tests - test clarity is more important than micro-optimizations
159
+ # Performance suggestions often hurt readability without meaningful benefit in test code
160
+ Performance:
161
+ Exclude:
162
+ - 'test/**/*'
163
+
164
+ Style/HashLikeCase:
165
+ Exclude:
166
+ - 'benchmarks/**/*'
167
+
168
+ # Custom Cataract cops
169
+ Cataract/BanAssertIncludes:
170
+ Enabled: true
171
+ Include:
172
+ - 'test/**/*'
173
+ Exclude:
174
+ - 'test/test_benchmark_doc_generator.rb'
175
+ - 'test/support/**/*' # Support files define assert_contains which uses assert_includes internally
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/flatten.rb'
38
+
39
+ # Offense count: 2
40
+ # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
41
+ Lint/DuplicateBranch:
42
+ Exclude:
43
+ - 'lib/cataract/pure/flatten.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/flatten.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/flatten.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/flatten.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/flatten.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/flatten.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) | 62.47K i/s | 3.59K i/s | 15.67K i/s | 4.73K i/s | 6.17K i/s |
24
+ | Medium CSS with @media (139 lines, 1.6KB) | 39.7K i/s | 2.05K i/s | 8.64K i/s | 2.76K i/s | 3.52K 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.4x faster (avg) |
31
+ | Native vs Pure (YJIT) | 4.2x 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.77K i/s | 444.3 i/s | 676.3 i/s | 37.1 i/s | 35.9 i/s |
43
+ | Media Type Filtering (print only) | 282.58K i/s | 114.27K i/s | 155.82K i/s | 2.67K i/s | 4.03K 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.23x faster (avg) |
50
+ | Native vs Pure (YJIT) | 1.82x faster (avg) |
51
+ | YJIT impact on Pure Ruby | 1.36x 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 | 7.35M i/s | 506.04K i/s | 2.59M i/s | 357.72K i/s | 353.42K i/s |
62
+ | Compound Selectors | 6.36M i/s | 184.96K i/s | 406.99K i/s | 216.59K i/s | 214.86K i/s |
63
+ | Combinators | 5.04M i/s | 146.91K i/s | 252.94K i/s | 185.44K i/s | 183.71K i/s |
64
+ | Pseudo-classes & Pseudo-elements | 5.1M i/s | 116.46K i/s | 197.06K i/s | 113.05K i/s | 113.99K i/s |
65
+ | :not() Pseudo-class (CSS3) | 3.23M i/s | 102.63K i/s | 161.55K i/s | 144.04K i/s | 144.53K i/s |
66
+ | Complex Real-world Selectors | 4.01M i/s | 52.03K i/s | 77.65K i/s | 83.91K i/s | 82.03K 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) | 39.27x faster (avg) |
73
+ | Native vs Pure (YJIT) | 8.43x faster (avg) |
74
+ | YJIT impact on Pure Ruby | 3.32x 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.
103
-
104
- Time to merge multiple CSS rule sets with same selector
105
-
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 |
78
+ ## CSS Flattening (Cascade)
163
79
 
164
- ### Implementation Notes
80
+ Time to flatten multiple CSS rule sets with same selector
165
81
 
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) | 21.34K i/s | 3.11K i/s | 5.23K i/s | 1.58K i/s | 2.3K i/s |
85
+ | Simple properties | 158.81K i/s | 75.72K i/s | 102.44K i/s | 28.29K i/s | 40.72K i/s |
86
+ | Cascade with specificity | 204.11K i/s | 77.93K i/s | 108.77K i/s | 31.92K i/s | 46.39K i/s |
87
+ | Important declarations | 203.43K i/s | 77.88K i/s | 109.38K i/s | 31.25K i/s | 45.25K i/s |
88
+ | Shorthand expansion | 21.34K i/s | 3.11K i/s | 5.23K i/s | 1.58K i/s | 2.3K i/s |
89
+ | Complex merging | 30.94K i/s | 16.09K i/s | 21.57K i/s | 11.6K i/s | 16.63K 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) | 3.03x faster (avg) |
96
+ | Native vs Pure (YJIT) | 1.72x faster (avg) |
97
+ | YJIT impact on Pure Ruby | 1.38x 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
- rake benchmark:merging
191
- rake benchmark:yjit
111
+ rake benchmark:flattening
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,23 @@
1
+ ## [0.2.0] - 2025-11-14
2
+
3
+ - Major: CSS `@import` resolution refactored from string-concatenation to parsed-object architecture with proper charset handling, media query combining,
4
+ and circular import detection
5
+ - Major: Terminology change: all `merge` methods renamed to `flatten` to better represent CSS cascade behavior (old names deprecated with warnings)
6
+ - Major: Rule equality now considers shorthand/longhand property equivalence (e.g., `margin: 10px` equals `margin-top: 10px; margin-right: 10px; ...`)
7
+ - Performance: Flatten operation optimized with array-based property storage, pre-allocated frozen strings, and lazy specificity calculation
8
+ - Feature: New Stylesheet collection methods (`+`, `-`, `|`, `concat`, `take`, `take_while`) with cascade rules applied
9
+ - Feature: Added source order tracking for proper CSS cascade resolution
10
+
11
+ ## [0.1.4] - 2025-11-12
12
+ - Major: Pure Ruby implementation added (#12)
13
+ - Fix: Media query serialization bugs - parentheses now preserved per CSS spec (min-width: 768px), fixed media query ordering
14
+ - Fix: CSS merge declaration ordering made consistent between C and pure Ruby implementations
15
+ - Fix: Shorthand property recreation (margin, padding, border, font, background, list-style) ordering
16
+ - Fix: Rule equality comparisons (Rule#==, AtRule#==)
17
+
18
+ ## [0.1.3] - 2025-11-11
19
+ - Fix: Proper handling of at-rules (@keyframes, @font-face, etc.) during CSS merge operations
20
+
1
21
  ## [0.1.2] - 2025-11-11
2
22
 
3
23
  - Fix segfault in merge
data/RAGEL_MIGRATION.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Why We Switched
4
4
 
5
- Started with Ragel as an experiment for the CSS parser, but quickly moved to hand-written pure C in October 2024.
5
+ Started with Ragel as an experiment for the CSS parser, but quickly moved to hand-written pure C in October 2025.
6
6
 
7
7
  ### Performance
8
8
 
@@ -55,6 +55,6 @@ Pure C is more verbose than Ragel's DSL, but the performance gains and simpler b
55
55
 
56
56
  ## Details
57
57
 
58
- - Swapped in October 2024
58
+ - Swapped in October 2025
59
59
  - Files: `css_parser.c`, `specificity.c`, `value_splitter.c`
60
60
  - API unchanged, all tests pass