cataract 0.2.5 → 0.4.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +6 -6
  3. data/.gitignore +17 -6
  4. data/.rubocop_todo.yml +11 -19
  5. data/BENCHMARKS.md +40 -40
  6. data/CHANGELOG.md +28 -1
  7. data/Gemfile +2 -2
  8. data/examples/css_analyzer/analyzer.rb +9 -3
  9. data/examples/css_analyzer/analyzers/base.rb +1 -1
  10. data/ext/cataract/cataract.c +312 -550
  11. data/ext/cataract/cataract.h +126 -16
  12. data/ext/cataract/css_parser.c +782 -806
  13. data/ext/cataract/extconf.rb +1 -2
  14. data/ext/cataract/flatten.c +279 -991
  15. data/ext/cataract/shorthand_expander.c +80 -102
  16. data/ext/cataract_color/color_conversion.c +1 -1
  17. data/lib/cataract/declarations.rb +26 -10
  18. data/lib/cataract/import_resolver.rb +36 -16
  19. data/lib/cataract/native.rb +30 -0
  20. data/lib/cataract/pure/byte_constants.rb +70 -67
  21. data/lib/cataract/pure/declarations.rb +125 -0
  22. data/lib/cataract/pure/flatten.rb +1195 -1182
  23. data/lib/cataract/pure/parser.rb +1721 -1652
  24. data/lib/cataract/pure/serializer.rb +575 -710
  25. data/lib/cataract/pure/specificity.rb +152 -175
  26. data/lib/cataract/pure.rb +73 -108
  27. data/lib/cataract/rule.rb +6 -3
  28. data/lib/cataract/stylesheet.rb +475 -415
  29. data/lib/cataract/version.rb +1 -1
  30. data/lib/cataract.rb +41 -30
  31. data/lib/tasks/profile.rake +6 -3
  32. metadata +4 -14
  33. data/ext/cataract/import_scanner.c +0 -174
  34. data/ext/cataract_old/cataract.c +0 -393
  35. data/ext/cataract_old/cataract.h +0 -250
  36. data/ext/cataract_old/css_parser.c +0 -933
  37. data/ext/cataract_old/extconf.rb +0 -67
  38. data/ext/cataract_old/import_scanner.c +0 -174
  39. data/ext/cataract_old/merge.c +0 -776
  40. data/ext/cataract_old/shorthand_expander.c +0 -902
  41. data/ext/cataract_old/specificity.c +0 -213
  42. data/ext/cataract_old/stylesheet.c +0 -290
  43. data/ext/cataract_old/value_splitter.c +0 -116
  44. data/lib/cataract/pure/helpers.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 898883ee4a30ccd34c034b4799e7a43eccae7f79693a4309d7ac7ef130f21840
4
- data.tar.gz: 511eaf6d4a9fd1f1a601a999f496bc3493900daa2696d20bc1a6b97eaff60eae
3
+ metadata.gz: 67046f0d371d7b0b077971e8634ff4054e64fe23a55c80b1a9f80e3dee6d804b
4
+ data.tar.gz: 776865b7490e22c326c0bab93a3da32f088177817b3e237c68b66e233df39021
5
5
  SHA512:
6
- metadata.gz: 59f87ff83a95fecb4e508a71e3055d8ebc53c23ad2d1316358e4a9e53ff8532c66b5686611e1706d6d95f1309428eda2e2df3b1884cbcb8d318dc866a256920b
7
- data.tar.gz: 85eb22a3be35e25d7cfd01fcbb2bd9148411e3cb4ee49ccfd808f57ec17a6a8c95c1dc56b29cb563cc9cc7523fbacfc4083a5953dcf3010c6cb14c872a48872c
6
+ metadata.gz: 21f5ccd9b4ed3e0831fa8723d02493b7b4a3582c359a3e3bae927e554d207422a0139592b23889d273ca1ed3a1888679c729d9ad0f4aa2bf4c90d18b2e16fe0c
7
+ data.tar.gz: f149256218337a47a1b27cd945857dd358199ceb47f70274c0c7db5cb3fa196d84a55281528c0f22d12da4bbbaff3b92c6e315210906a75aee2d304a9b91c275
@@ -18,7 +18,7 @@ jobs:
18
18
  strategy:
19
19
  fail-fast: false
20
20
  matrix:
21
- ruby: ['3.1', '3.2', '3.3', '3.4']
21
+ ruby: ['3.1', '3.2', '3.3', '3.4', '4.0']
22
22
 
23
23
  steps:
24
24
  - uses: actions/checkout@v4
@@ -37,8 +37,8 @@ jobs:
37
37
  env:
38
38
  COVERAGE: 1
39
39
 
40
- - name: Run debug test (Ruby 3.4 only)
41
- if: matrix.ruby == '3.4'
40
+ - name: Run debug test (Ruby 4.0 only)
41
+ if: matrix.ruby == '4.0'
42
42
  run: |
43
43
  bundle exec rake clean compile
44
44
  bundle exec ruby -Ilib:test test/test_stylesheet.rb
@@ -46,7 +46,7 @@ jobs:
46
46
  CONFIGURE_ARGS: --enable-debug
47
47
 
48
48
  - name: Upload coverage to Codecov
49
- if: runner.os == 'Linux' && matrix.ruby == '3.4'
49
+ if: runner.os == 'Linux' && matrix.ruby == '4.0'
50
50
  uses: codecov/codecov-action@v5
51
51
  with:
52
52
  fail_ci_if_error: false
@@ -54,7 +54,7 @@ jobs:
54
54
  verbose: true
55
55
 
56
56
  - name: Upload coverage report artifact
57
- if: runner.os == 'Linux' && matrix.ruby == '3.4'
57
+ if: runner.os == 'Linux' && matrix.ruby == '4.0'
58
58
  uses: actions/upload-artifact@v4
59
59
  with:
60
60
  name: coverage-report
@@ -69,7 +69,7 @@ jobs:
69
69
  - name: Set up Ruby
70
70
  uses: ruby/setup-ruby@v1
71
71
  with:
72
- ruby-version: '3.4'
72
+ ruby-version: '4.0'
73
73
  bundler-cache: true
74
74
 
75
75
  - name: Run rubocop
data/.gitignore CHANGED
@@ -4,12 +4,11 @@ tmp/
4
4
  *.so
5
5
  *.o
6
6
 
7
- # Generated C code from Ragel
8
- ext/**/cataract.c
9
-
10
7
  # Compiled extensions
11
8
  lib/**/*.so
12
9
  lib/**/*.bundle
10
+ ext/**/*.bundle
11
+ ext/**/*.dSYM/
13
12
 
14
13
  # Makefile generated by extconf.rb
15
14
  ext/**/Makefile
@@ -29,19 +28,31 @@ Thumbs.db
29
28
  vendor/
30
29
  .bundle/
31
30
 
31
+ # Node (this is a Ruby gem; no JS toolchain is tracked, but guard against stray npm installs)
32
+ node_modules/
33
+
32
34
  # Test artifacts
33
35
  coverage/
34
36
  test/.benchmark_results/
35
- test/fuzz_last_input.css
36
- test/fuzz_crash_*.css
37
- test/fuzz_crash_*.log
37
+
38
+ # Fuzzer output (crash inputs, logs, recent-input dumps)
39
+ scripts/fuzzer/output/
38
40
 
39
41
  # Benchmark results
40
42
  benchmarks/.results/
41
43
  benchmarks/.benchmark_results/
42
44
 
45
+ # CSS analyzer example debug output
46
+ examples/css_analyzer/output/
47
+
43
48
  # Documentation (generated by YARD)
44
49
  docs/
45
50
  !docs/files/
46
51
  !docs/files/EXAMPLE.md
47
52
  .yardoc/
53
+
54
+ # Claude Code local project instructions (personal, not shared)
55
+ .claude/CLAUDE.md
56
+
57
+ .cache/
58
+ .cocci/
data/.rubocop_todo.yml CHANGED
@@ -22,21 +22,13 @@ Layout/EmptyLinesAfterModuleInclusion:
22
22
  Exclude:
23
23
  - 'benchmarks/parsing_tests.rb'
24
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
25
+ # Offense count: 2
34
26
  # This cop supports safe autocorrection (--autocorrect).
35
27
  Lint/AmbiguousOperatorPrecedence:
36
28
  Exclude:
37
29
  - 'lib/cataract/pure/flatten.rb'
38
30
 
39
- # Offense count: 2
31
+ # Offense count: 4
40
32
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
41
33
  Lint/DuplicateBranch:
42
34
  Exclude:
@@ -69,7 +61,7 @@ Minitest/EmptyLineBeforeAssertionMethods:
69
61
  Exclude:
70
62
  - 'test/test_stylesheet.rb'
71
63
 
72
- # Offense count: 7
64
+ # Offense count: 14
73
65
  # This cop supports unsafe autocorrection (--autocorrect-all).
74
66
  Performance/UnfreezeString:
75
67
  Exclude:
@@ -77,7 +69,7 @@ Performance/UnfreezeString:
77
69
  - 'lib/cataract/pure/flatten.rb'
78
70
  - 'lib/cataract/pure/parser.rb'
79
71
 
80
- # Offense count: 4
72
+ # Offense count: 8
81
73
  # This cop supports safe autocorrection (--autocorrect).
82
74
  # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
83
75
  # SupportedStyles: assign_to_condition, assign_inside_condition
@@ -86,7 +78,7 @@ Style/ConditionalAssignment:
86
78
  - 'lib/cataract/pure/imports.rb'
87
79
  - 'lib/cataract/pure/flatten.rb'
88
80
 
89
- # Offense count: 4
81
+ # Offense count: 8
90
82
  # Configuration parameters: AllowedConstants.
91
83
  Style/Documentation:
92
84
  Exclude:
@@ -107,7 +99,7 @@ Style/HashLikeCase:
107
99
  - 'benchmarks/serialization_tests.rb'
108
100
  - 'benchmarks/specificity_tests.rb'
109
101
 
110
- # Offense count: 14
102
+ # Offense count: 28
111
103
  # This cop supports unsafe autocorrection (--autocorrect-all).
112
104
  Style/IdenticalConditionalBranches:
113
105
  Exclude:
@@ -118,7 +110,7 @@ Style/IdenticalConditionalBranches:
118
110
  - 'lib/cataract/pure/parser.rb'
119
111
  - 'lib/cataract/pure/serializer.rb'
120
112
 
121
- # Offense count: 5
113
+ # Offense count: 10
122
114
  # This cop supports safe autocorrection (--autocorrect).
123
115
  # Configuration parameters: AllowIfModifier.
124
116
  Style/IfInsideElse:
@@ -127,19 +119,19 @@ Style/IfInsideElse:
127
119
  - 'lib/cataract/pure/serializer.rb'
128
120
  - 'lib/cataract/pure/specificity.rb'
129
121
 
130
- # Offense count: 1
122
+ # Offense count: 2
131
123
  # This cop supports safe autocorrection (--autocorrect).
132
124
  Style/NegatedIfElseCondition:
133
125
  Exclude:
134
126
  - 'lib/cataract/pure/flatten.rb'
135
127
 
136
- # Offense count: 1
128
+ # Offense count: 2
137
129
  # This cop supports safe autocorrection (--autocorrect).
138
130
  Style/RedundantAssignment:
139
131
  Exclude:
140
132
  - 'lib/cataract/pure/specificity.rb'
141
133
 
142
- # Offense count: 2
134
+ # Offense count: 4
143
135
  # This cop supports safe autocorrection (--autocorrect).
144
136
  Style/RedundantParentheses:
145
137
  Exclude:
@@ -152,7 +144,7 @@ Style/RedundantRegexpArgument:
152
144
  Exclude:
153
145
  - 'test/test_stylesheet.rb'
154
146
 
155
- # Offense count: 3
147
+ # Offense count: 6
156
148
  # This cop supports safe autocorrection (--autocorrect).
157
149
  # Configuration parameters: AllowModifier.
158
150
  Style/SoleNestedConditional:
data/BENCHMARKS.md CHANGED
@@ -8,11 +8,11 @@ Performance comparison between Cataract's C extension and pure Ruby implementati
8
8
 
9
9
  ## Test Environment
10
10
 
11
- - **Ruby**: ruby 3.4.7 (2025-10-08 revision 7a5688e2a2) +YJIT +PRISM [arm64-darwin23]
11
+ - **Ruby**: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin25]
12
12
  - **CPU**: Apple M1 Pro
13
13
  - **Memory**: 32GB
14
- - **OS**: macOS 14.5
15
- - **Generated**: 2025-11-11T16:02:08-06:00
14
+ - **OS**: macOS 26.3.1
15
+ - **Generated**: 2026-07-08T10:35:16-05:00
16
16
 
17
17
  ## CSS Parsing
18
18
 
@@ -20,17 +20,17 @@ Time to parse CSS into internal data structures
20
20
 
21
21
  | Test Case | Native | Pure (no YJIT) | Pure (YJIT) |
22
22
  |-----------|--------|----------------|-------------|
23
- | Small CSS (64 lines, 1.0KB) | 37.27K i/s | 3.32K i/s | 13.48K i/s |
24
- | Medium CSS with @media (139 lines, 1.6KB) | 34.66K i/s | 2.06K i/s | 8.94K i/s |
25
- | Selector lists (3500 lines, 62.5KB, 500 lists) | 485.7 i/s | 55.7 i/s | 213.6 i/s |
23
+ | Small CSS (64 lines, 1.0KB) | 36.76K i/s | 3.33K i/s | 13.66K i/s |
24
+ | Medium CSS with @media (139 lines, 1.6KB) | 29.98K i/s | 2.11K i/s | 8.98K i/s |
25
+ | Selector lists (3500 lines, 62.5KB, 500 lists) | 447.4 i/s | 58.0 i/s | 219.8 i/s |
26
26
 
27
27
  ### Speedups
28
28
 
29
29
  | Comparison | Speedup |
30
30
  |------------|---------|
31
- | Native vs Pure (no YJIT) | 14.4x faster (avg) |
32
- | Native vs Pure (YJIT) | 3.38x faster (avg) |
33
- | YJIT impact on Pure Ruby | 4.23x faster (avg) |
31
+ | Native vs Pure (no YJIT) | 13.03x faster (avg) |
32
+ | Native vs Pure (YJIT) | 3.12x faster (avg) |
33
+ | YJIT impact on Pure Ruby | 4.15x faster (avg) |
34
34
 
35
35
  ### Parse Error Checking Overhead
36
36
 
@@ -38,17 +38,17 @@ Parse error detection can be enabled with `raise_parse_errors: true`. This compa
38
38
 
39
39
  | Configuration | Native | Pure (no YJIT) | Pure (YJIT) |
40
40
  |---------------|--------|----------------|-------------|
41
- | Medium CSS (139 lines) - no error checking | 35.18K i/s | 2.07K i/s | 9.05K i/s |
42
- | Medium CSS (139 lines) - with error checking | 34.84K i/s | 1.9K i/s | 8.19K i/s |
41
+ | Medium CSS (139 lines) - no error checking | 33.57K i/s | 2.12K i/s | 9.08K i/s |
42
+ | Medium CSS (139 lines) - with error checking | 33.09K i/s | 1.94K i/s | 8.19K i/s |
43
43
 
44
44
  **Overhead Analysis:**
45
45
 
46
46
 
47
47
  | Implementation | Overhead |
48
48
  |----------------|----------|
49
- | Native | ~0% (within noise) |
50
- | Pure (no YJIT) | 8.9% slower |
51
- | Pure (YJIT) | 10.4% slower |
49
+ | Native | 1.5% slower |
50
+ | Pure (no YJIT) | 9.2% slower |
51
+ | Pure (YJIT) | 10.9% slower |
52
52
 
53
53
  ---
54
54
 
@@ -58,19 +58,19 @@ Time to convert parsed CSS back to string format
58
58
 
59
59
  | Test Case | Native | Pure (no YJIT) | Pure (YJIT) |
60
60
  |-----------|--------|----------------|-------------|
61
- | to_s (Bootstrap - 191KB) | 1.32K i/s | 360.2 i/s | 656.6 i/s |
62
- | to_s (Compact utilities - 2.9KB) | 1.32K i/s | 360.2 i/s | 656.6 i/s |
63
- | to_formatted_s (Nested CSS - 1.3KB) | 176.13K i/s | 60.97K i/s | 121.5K i/s |
64
- | to_s with selector_lists (3.4KB) | 19.03K i/s | 8.01K i/s | 14.2K i/s |
65
- | Media filtering (Bootstrap print only) | 283.24K i/s | 97.28K i/s | 161.56K i/s |
61
+ | to_s (Bootstrap - 191KB) | 1.05K i/s | 368.6 i/s | 555.6 i/s |
62
+ | to_s (Compact utilities - 2.9KB) | 1.05K i/s | 368.6 i/s | 555.6 i/s |
63
+ | to_formatted_s (Nested CSS - 1.3KB) | 156.05K i/s | 53.98K i/s | 90.18K i/s |
64
+ | to_s with selector_lists (3.4KB) | 15.37K i/s | 7.71K i/s | 12.09K i/s |
65
+ | Media filtering (Bootstrap print only) | 1.18K i/s | 547.1 i/s | 769.2 i/s |
66
66
 
67
67
  ### Speedups
68
68
 
69
69
  | Comparison | Speedup |
70
70
  |------------|---------|
71
- | Native vs Pure (no YJIT) | 3.24x faster (avg) |
72
- | Native vs Pure (YJIT) | 1.73x faster (avg) |
73
- | YJIT impact on Pure Ruby | 1.86x faster (avg) |
71
+ | Native vs Pure (no YJIT) | 2.71x faster (avg) |
72
+ | Native vs Pure (YJIT) | 1.75x faster (avg) |
73
+ | YJIT impact on Pure Ruby | 1.55x faster (avg) |
74
74
 
75
75
  ---
76
76
 
@@ -80,20 +80,20 @@ Time to calculate CSS selector specificity values
80
80
 
81
81
  | Test Case | Native | Pure (no YJIT) | Pure (YJIT) |
82
82
  |-----------|--------|----------------|-------------|
83
- | Simple Selectors | 7.28M i/s | 506.61K i/s | 2.45M i/s |
84
- | Compound Selectors | 6.4M i/s | 186.62K i/s | 405.15K i/s |
85
- | Combinators | 5.04M i/s | 147.07K i/s | 255.92K i/s |
86
- | Pseudo-classes & Pseudo-elements | 5.17M i/s | 117.16K i/s | 195.12K i/s |
87
- | :not() Pseudo-class (CSS3) | 3.48M i/s | 103.47K i/s | 161.34K i/s |
88
- | Complex Real-world Selectors | 4.01M i/s | 52.41K i/s | 77.48K i/s |
83
+ | Simple Selectors | 1.97M i/s | 419.98K i/s | 1.44M i/s |
84
+ | Compound Selectors | 1.89M i/s | 186.59K i/s | 523.08K i/s |
85
+ | Combinators | 1.43M i/s | 142.61K i/s | 285.41K i/s |
86
+ | Pseudo-classes & Pseudo-elements | 1.41M i/s | 114.7K i/s | 235.07K i/s |
87
+ | :not() Pseudo-class (CSS3) | 1.52M i/s | 110.64K i/s | 247.84K i/s |
88
+ | Complex Real-world Selectors | 1.62M i/s | 56.2K i/s | 126.65K i/s |
89
89
 
90
90
  ### Speedups
91
91
 
92
92
  | Comparison | Speedup |
93
93
  |------------|---------|
94
- | Native vs Pure (no YJIT) | 39.54x faster (avg) |
95
- | Native vs Pure (YJIT) | 23.05x faster (avg) |
96
- | YJIT impact on Pure Ruby | 2.24x faster (avg) |
94
+ | Native vs Pure (no YJIT) | 13.28x faster (avg) |
95
+ | Native vs Pure (YJIT) | 5.82x faster (avg) |
96
+ | YJIT impact on Pure Ruby | 2.46x faster (avg) |
97
97
 
98
98
  ---
99
99
 
@@ -103,20 +103,20 @@ Time to flatten multiple CSS rule sets with same selector
103
103
 
104
104
  | Test Case | Native | Pure (no YJIT) | Pure (YJIT) |
105
105
  |-----------|--------|----------------|-------------|
106
- | No shorthand properties (large) | 13.91K i/s | 3.23K i/s | 6.39K i/s |
107
- | Simple properties | 144.19K i/s | 71.11K i/s | 101.38K i/s |
108
- | Cascade with specificity | 172.96K i/s | 69.16K i/s | 99.23K i/s |
109
- | Important declarations | 173.35K i/s | 69.12K i/s | 100.16K i/s |
110
- | Shorthand expansion | 13.91K i/s | 3.23K i/s | 6.39K i/s |
111
- | Complex flattening | 29.86K i/s | 15.77K i/s | 22.09K i/s |
106
+ | No shorthand properties (large) | 12.75K i/s | 3.69K i/s | 6.41K i/s |
107
+ | Simple properties | 142.64K i/s | 69.09K i/s | 100.71K i/s |
108
+ | Cascade with specificity | 178.29K i/s | 67.87K i/s | 101.77K i/s |
109
+ | Important declarations | 177.88K i/s | 68.12K i/s | 101.75K i/s |
110
+ | Shorthand expansion | 12.75K i/s | 3.69K i/s | 6.41K i/s |
111
+ | Complex flattening | 31.73K i/s | 16.04K i/s | 23.28K i/s |
112
112
 
113
113
  ### Speedups
114
114
 
115
115
  | Comparison | Speedup |
116
116
  |------------|---------|
117
- | Native vs Pure (no YJIT) | 2.54x faster (avg) |
118
- | Native vs Pure (YJIT) | 1.63x faster (avg) |
119
- | YJIT impact on Pure Ruby | 1.53x faster (avg) |
117
+ | Native vs Pure (no YJIT) | 2.45x faster (avg) |
118
+ | Native vs Pure (YJIT) | 1.6x faster (avg) |
119
+ | YJIT impact on Pure Ruby | 1.52x faster (avg) |
120
120
 
121
121
  ---
122
122
 
data/CHANGELOG.md CHANGED
@@ -1,4 +1,31 @@
1
- ## [Unreleased]
1
+ ## [0.4.0] - 2026-07-08
2
+
3
+ - Fix: `Declarations.new(some_string)` (standalone CSS declaration-block parsing) now works under the pure Ruby backend (`CATARACT_PURE=1`) - previously raised `NoMethodError`.
4
+ - Fix: a compound `@media` list (e.g. `@media screen, print { ... }`) no longer silently collapses to just its first member when the document also contains CSS nesting elsewhere.
5
+ - Fix: `to_s`/`to_formatted_s` with a specific `media:` filter (anything but `:all`) could crash while grouping comma-separated selector lists (e.g. `h1, h2 { ... }`), if an unrelated rule elsewhere in the document was excluded by the filter. Present in both backends since at least 0.3.0.
6
+ - Fix: `Stylesheet#flatten` (non-destructive) could silently expand shorthand properties (e.g. `margin: 0`) on the *original* stylesheet as a side effect, under the pure Ruby backend. Present since at least 0.3.0; the C extension was unaffected.
7
+ - Breaking (minor): removed `Cataract.merge` (a long-deprecated alias for `.flatten`) and `Cataract.parse_media_types` (undocumented, no real callers) from the C extension's public surface. Use `Cataract.flatten`/`Stylesheet#flatten` instead of `.merge`.
8
+ - Internal: `Cataract`'s own directly-callable methods are now just `.flatten` and `.parse_css`. Previously `Cataract` also exposed `calculate_specificity`, `expand_shorthand`, `parse_declarations`, `stylesheet_to_s`, `stylesheet_to_formatted_s`, and `_parse_css` directly - undocumented implementation leakage, not intended for direct use. The documented ways to reach this functionality (`Rule#specificity`, `Rule#expanded_declarations`, `Declarations.new(some_string)`, `Stylesheet#to_s`/`#to_formatted_s`) are unchanged. The native C extension and pure Ruby backend also now live under `Cataract::Backends::Native` / `Cataract::Backends::Pure`, resolved once per process and held per-`Stylesheet` - this lets both backends coexist in the same process (e.g. for direct comparison). Existing callers relying only on the documented public API should notice nothing.
9
+
10
+ ## [0.3.0] - 2026-07-06
11
+
12
+ - Security/Breaking: `Stylesheet#load_uri` and `#load_file` now reject `file://` paths under `/etc/`, `/proc/`, `/sys/`, and `/dev/` by default (override with `dangerous_path_prefixes: []`). Previously these methods used a separate, unvalidated fetch implementation; they now reuse `ImportResolver` (the same validation and fetching `@import` resolution already uses), which also fixes `load_uri` not following HTTP redirects.
13
+ - Fix: rules nested 3+ levels deep, or nested inside a top-level `@media`/`@supports`/`@container`/`@scope` block, could serialize incorrectly (silently flattened, or printed as an unrelated top-level rule) in the pure Ruby backend.
14
+ - Fix: a bare type selector nested without `&` (invalid CSS nesting) no longer corrupts serialized output with unbalanced braces, in both the C and pure Ruby parsers.
15
+ - Fix: `@keyframes` parsing no longer pollutes the outer rule list with its own inner selectors.
16
+ - Fix: at-rules (`@keyframes`, `@font-face`, etc.) nested inside `@media` blocks now correctly keep their media context across parsing, merging (`add_block`, `concat`/`+`), `@import` resolution, and serialization - previously this could be lost, misattributed, or (combined with CSS nesting) raise `NoMethodError` in the pure Ruby serializer.
17
+ - Fix: `remove_rules!` and the `-` operator no longer desync media query ids from the rules that reference them.
18
+ - Fix: resolving `@import`s no longer leaves duplicate/colliding rule ids.
19
+ - Fix: a plain `@import "file.css";` (no media qualifier) no longer loses that file's own `@media` rules.
20
+ - Fix: `Stylesheet#concat`/`+` now merges the other stylesheet's media queries and selector-list groupings too - previously this silently dropped `h1, h2`-style grouping and corrupted media attribution.
21
+ - Fix: `@import` media queries with a type keyword (e.g. `"screen and (min-width: 500px)"`) no longer keep a stray `"and"` in the parsed conditions.
22
+ - Fix: `to_s`/`to_formatted_s` media filtering is now consistent between the two - both include base (non-media) rules when filtering to a specific media type; previously `to_s` excluded them while `to_formatted_s` included them.
23
+ - Fix: values containing `url(...;...)` no longer lose everything after the first semicolon when the rule also uses CSS nesting.
24
+ - Fix: `!important` now parses correctly with whitespace between `!` and `important`, per the CSS grammar.
25
+ - Fix: expanding `border`, `border-{top,right,bottom,left}`, `font`, `background`, and `list-style` shorthands (used when comparing rules for equality) no longer silently drops `!important`.
26
+ - Fix: `Declarations`/`Stylesheet` `#dup`/`#clone` now produce fully independent copies; previously mutating a copy could mutate the original.
27
+ - Performance: CSS selector specificity calculation is ~30% faster (no longer re-allocates an internal lookup array on every call).
28
+ - Performance: native (C extension) CSS flattening is ~1-5% faster (fewer string allocations when recreating shorthand properties).
2
29
 
3
30
  ## [0.2.5 - 2025-11-25]
4
31
 
data/Gemfile CHANGED
@@ -16,10 +16,10 @@ gem 'css_parser', '~> 1.0' # for benchmarking against
16
16
  gem 'minitest'
17
17
  gem 'minitest-spec'
18
18
  gem 'nokogiri' # for docs
19
- gem 'ruby-prof', require: false # for profiling
19
+ gem 'ruby-prof', require: false, platforms: :ruby # for profiling (MRI only)
20
20
  gem 'simplecov', require: false
21
21
  gem 'simplecov-cobertura', require: false
22
- gem 'stackprof', require: false # for profiling
22
+ gem 'stackprof', require: false, platforms: :ruby # for profiling (MRI only)
23
23
  gem 'webmock', '~> 3.0' # for testing URL loading
24
24
 
25
25
  gem 'overcommit', '~> 0.64', group: :development
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'erb'
4
+ require 'fileutils'
4
5
  require 'uri'
5
6
  require_relative '../../lib/cataract'
6
7
  require_relative 'analyzers/properties'
@@ -71,13 +72,18 @@ module CSSAnalyzer
71
72
  def save_parsed_css
72
73
  # Generate a unique filename based on source
73
74
  source_slug = @source.gsub(%r{[:/]}, '_').gsub(/[^a-zA-Z0-9_.-]/, '')
74
- filename = "parsed-css-#{source_slug}.css"
75
+
76
+ # Write next to this script (not the caller's cwd) so running the
77
+ # analyzer from repo root doesn't scatter debug output there.
78
+ output_dir = File.join(__dir__, 'output')
79
+ FileUtils.mkdir_p(output_dir)
80
+ path = File.join(output_dir, "parsed-css-#{source_slug}.css")
75
81
 
76
82
  # Serialize stylesheet to CSS
77
83
  css_output = @stylesheet.to_s
78
84
 
79
- File.write(filename, css_output)
80
- warn "Parsed CSS saved to #{filename} (#{@stylesheet.size} rules, #{css_output.length} bytes)"
85
+ File.write(path, css_output)
86
+ warn "Parsed CSS saved to #{path} (#{@stylesheet.size} rules, #{css_output.length} bytes)"
81
87
  end
82
88
 
83
89
  private
@@ -26,7 +26,7 @@ module CSSAnalyzer
26
26
  # @param rule [Cataract::Rule] The rule to check
27
27
  # @return [Array<Symbol>] Array of media query symbols this rule appears in
28
28
  def media_queries_for_rule(rule)
29
- stylesheet.instance_variable_get(:@_media_index).select { |_media, ids| ids.include?(rule.id) }.keys
29
+ stylesheet.media_index.select { |_media, ids| ids.include?(rule.id) }.keys
30
30
  end
31
31
  end
32
32
  end