cataract 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci-manual-rubies.yml +18 -1
- data/.rubocop.yml +36 -6
- data/.rubocop_todo.yml +7 -7
- data/BENCHMARKS.md +30 -30
- data/CHANGELOG.md +14 -0
- data/RAGEL_MIGRATION.md +2 -2
- data/README.md +7 -2
- data/Rakefile +24 -11
- data/cataract.gemspec +1 -1
- data/ext/cataract/cataract.c +120 -5
- data/ext/cataract/cataract.h +9 -3
- data/ext/cataract/css_parser.c +156 -32
- data/ext/cataract/extconf.rb +2 -2
- data/ext/cataract/{merge.c → flatten.c} +520 -468
- data/ext/cataract/shorthand_expander.c +164 -115
- data/lib/cataract/import_resolver.rb +60 -39
- data/lib/cataract/import_statement.rb +49 -0
- data/lib/cataract/pure/{merge.rb → flatten.rb} +39 -40
- data/lib/cataract/pure/imports.rb +13 -0
- data/lib/cataract/pure/parser.rb +108 -4
- data/lib/cataract/pure/serializer.rb +3 -0
- data/lib/cataract/pure.rb +32 -9
- data/lib/cataract/rule.rb +51 -6
- data/lib/cataract/stylesheet.rb +343 -41
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +28 -24
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1c4727f2f1b3c3aafa117ca105ee0d55a6d6f33b0fed789533af10c24173918
|
|
4
|
+
data.tar.gz: 86c0e96b1a953fb817fa235517e00215b4b363baea7d1225c5fcb2b9643b29c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7282bde75fb5cd81ad2b7f2d7134a6fde2941a8f5320a4de2d463114d25bfc709e80f1639e9479cb69b7f22d685d203a7097fa0cf8c2e952041b733420991080
|
|
7
|
+
data.tar.gz: f8af1d50b389d64c36c5138f431b5b09f95ec3ca8f68b6c342ec280e7e1c84a8fca95ec6c95a81d97f9bced8d0f295acfbf2849ca47939e16cbf1b408544cdbe
|
|
@@ -4,12 +4,29 @@ on:
|
|
|
4
4
|
workflow_dispatch:
|
|
5
5
|
|
|
6
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
|
+
|
|
7
24
|
test-alt-rubies:
|
|
8
25
|
runs-on: ubuntu-latest
|
|
9
26
|
strategy:
|
|
10
27
|
fail-fast: false
|
|
11
28
|
matrix:
|
|
12
|
-
ruby: [
|
|
29
|
+
ruby: [truffleruby, ruby-head]
|
|
13
30
|
|
|
14
31
|
steps:
|
|
15
32
|
- uses: actions/checkout@v4
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
inherit_from: .rubocop_todo.yml
|
|
2
2
|
|
|
3
|
+
require:
|
|
4
|
+
- ./.rubocop/cataract/ban_assert_includes.rb
|
|
5
|
+
|
|
3
6
|
plugins:
|
|
4
7
|
- rubocop-performance
|
|
5
8
|
- rubocop-minitest
|
|
@@ -66,13 +69,17 @@ Layout/LineLength:
|
|
|
66
69
|
Minitest/MultipleAssertions:
|
|
67
70
|
Max: 10
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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:
|
|
72
79
|
- 'test/**/*'
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
80
|
+
|
|
81
|
+
Style/Documentation:
|
|
82
|
+
Enabled: false
|
|
76
83
|
|
|
77
84
|
# Disable modifier if/unless enforcement - use it when it's clearer, not because a cop says so
|
|
78
85
|
Style/IfUnlessModifier:
|
|
@@ -143,3 +150,26 @@ Style/MapIntoArray:
|
|
|
143
150
|
Style/OptionalBooleanParameter:
|
|
144
151
|
Exclude:
|
|
145
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
CHANGED
|
@@ -34,13 +34,13 @@ Layout/MultilineOperationIndentation:
|
|
|
34
34
|
# This cop supports safe autocorrection (--autocorrect).
|
|
35
35
|
Lint/AmbiguousOperatorPrecedence:
|
|
36
36
|
Exclude:
|
|
37
|
-
- 'lib/cataract/pure/
|
|
37
|
+
- 'lib/cataract/pure/flatten.rb'
|
|
38
38
|
|
|
39
39
|
# Offense count: 2
|
|
40
40
|
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
|
|
41
41
|
Lint/DuplicateBranch:
|
|
42
42
|
Exclude:
|
|
43
|
-
- 'lib/cataract/pure/
|
|
43
|
+
- 'lib/cataract/pure/flatten.rb'
|
|
44
44
|
|
|
45
45
|
# Offense count: 1
|
|
46
46
|
Lint/IneffectiveAccessModifier:
|
|
@@ -74,7 +74,7 @@ Minitest/EmptyLineBeforeAssertionMethods:
|
|
|
74
74
|
Performance/UnfreezeString:
|
|
75
75
|
Exclude:
|
|
76
76
|
- 'lib/cataract/pure.rb'
|
|
77
|
-
- 'lib/cataract/pure/
|
|
77
|
+
- 'lib/cataract/pure/flatten.rb'
|
|
78
78
|
- 'lib/cataract/pure/parser.rb'
|
|
79
79
|
|
|
80
80
|
# Offense count: 4
|
|
@@ -84,7 +84,7 @@ Performance/UnfreezeString:
|
|
|
84
84
|
Style/ConditionalAssignment:
|
|
85
85
|
Exclude:
|
|
86
86
|
- 'lib/cataract/pure/imports.rb'
|
|
87
|
-
- 'lib/cataract/pure/
|
|
87
|
+
- 'lib/cataract/pure/flatten.rb'
|
|
88
88
|
|
|
89
89
|
# Offense count: 4
|
|
90
90
|
# Configuration parameters: AllowedConstants.
|
|
@@ -123,7 +123,7 @@ Style/IdenticalConditionalBranches:
|
|
|
123
123
|
# Configuration parameters: AllowIfModifier.
|
|
124
124
|
Style/IfInsideElse:
|
|
125
125
|
Exclude:
|
|
126
|
-
- 'lib/cataract/pure/
|
|
126
|
+
- 'lib/cataract/pure/flatten.rb'
|
|
127
127
|
- 'lib/cataract/pure/serializer.rb'
|
|
128
128
|
- 'lib/cataract/pure/specificity.rb'
|
|
129
129
|
|
|
@@ -131,7 +131,7 @@ Style/IfInsideElse:
|
|
|
131
131
|
# This cop supports safe autocorrection (--autocorrect).
|
|
132
132
|
Style/NegatedIfElseCondition:
|
|
133
133
|
Exclude:
|
|
134
|
-
- 'lib/cataract/pure/
|
|
134
|
+
- 'lib/cataract/pure/flatten.rb'
|
|
135
135
|
|
|
136
136
|
# Offense count: 1
|
|
137
137
|
# This cop supports safe autocorrection (--autocorrect).
|
|
@@ -157,7 +157,7 @@ Style/RedundantRegexpArgument:
|
|
|
157
157
|
# Configuration parameters: AllowModifier.
|
|
158
158
|
Style/SoleNestedConditional:
|
|
159
159
|
Exclude:
|
|
160
|
-
- 'lib/cataract/pure/
|
|
160
|
+
- 'lib/cataract/pure/flatten.rb'
|
|
161
161
|
- 'lib/cataract/pure/parser.rb'
|
|
162
162
|
|
|
163
163
|
# Offense count: 3
|
data/BENCHMARKS.md
CHANGED
|
@@ -20,15 +20,15 @@ Time to parse CSS into internal data structures
|
|
|
20
20
|
|
|
21
21
|
| Test Case | Native | Pure (no YJIT) | Pure (YJIT) | css_parser (no YJIT) | css_parser (YJIT) |
|
|
22
22
|
|-----------|--------|----------------|-------------|----------------------|-------------------|
|
|
23
|
-
| Small CSS (64 lines, 1.0KB) |
|
|
24
|
-
| Medium CSS with @media (139 lines, 1.6KB) |
|
|
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 |
|
|
25
25
|
|
|
26
26
|
### Speedups
|
|
27
27
|
|
|
28
28
|
| Comparison | Speedup |
|
|
29
29
|
|------------|---------|
|
|
30
|
-
| Native vs Pure (no YJIT) | 18.
|
|
31
|
-
| Native vs Pure (YJIT) | 4.
|
|
30
|
+
| Native vs Pure (no YJIT) | 18.4x faster (avg) |
|
|
31
|
+
| Native vs Pure (YJIT) | 4.2x faster (avg) |
|
|
32
32
|
| YJIT impact on Pure Ruby | 4.31x faster (avg) |
|
|
33
33
|
|
|
34
34
|
---
|
|
@@ -39,16 +39,16 @@ Time to convert parsed CSS back to string format
|
|
|
39
39
|
|
|
40
40
|
| Test Case | Native | Pure (no YJIT) | Pure (YJIT) | css_parser (no YJIT) | css_parser (YJIT) |
|
|
41
41
|
|-----------|--------|----------------|-------------|----------------------|-------------------|
|
|
42
|
-
| Full Serialization (Bootstrap CSS - 191KB) | 1.
|
|
43
|
-
| Media Type Filtering (print only) | 282.
|
|
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 |
|
|
44
44
|
|
|
45
45
|
### Speedups
|
|
46
46
|
|
|
47
47
|
| Comparison | Speedup |
|
|
48
48
|
|------------|---------|
|
|
49
|
-
| Native vs Pure (no YJIT) | 3.
|
|
50
|
-
| Native vs Pure (YJIT) | 1.
|
|
51
|
-
| YJIT impact on Pure Ruby | 1.
|
|
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) |
|
|
52
52
|
|
|
53
53
|
---
|
|
54
54
|
|
|
@@ -58,43 +58,43 @@ Time to calculate CSS selector specificity values
|
|
|
58
58
|
|
|
59
59
|
| Test Case | Native | Pure (no YJIT) | Pure (YJIT) | css_parser (no YJIT) | css_parser (YJIT) |
|
|
60
60
|
|-----------|--------|----------------|-------------|----------------------|-------------------|
|
|
61
|
-
| Simple Selectors |
|
|
62
|
-
| Compound Selectors | 6.
|
|
63
|
-
| Combinators | 5.
|
|
64
|
-
| Pseudo-classes & Pseudo-elements | 5.
|
|
65
|
-
| :not() Pseudo-class (CSS3) | 3.
|
|
66
|
-
| Complex Real-world Selectors | 4.
|
|
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 |
|
|
67
67
|
|
|
68
68
|
### Speedups
|
|
69
69
|
|
|
70
70
|
| Comparison | Speedup |
|
|
71
71
|
|------------|---------|
|
|
72
|
-
| Native vs Pure (no YJIT) |
|
|
73
|
-
| Native vs Pure (YJIT) | 8.
|
|
74
|
-
| YJIT impact on Pure Ruby | 3.
|
|
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) |
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
-
## CSS
|
|
78
|
+
## CSS Flattening (Cascade)
|
|
79
79
|
|
|
80
|
-
Time to
|
|
80
|
+
Time to flatten multiple CSS rule sets with same selector
|
|
81
81
|
|
|
82
82
|
| Test Case | Native | Pure (no YJIT) | Pure (YJIT) | css_parser (no YJIT) | css_parser (YJIT) |
|
|
83
83
|
|-----------|--------|----------------|-------------|----------------------|-------------------|
|
|
84
|
-
| No shorthand properties (large) |
|
|
85
|
-
| Simple properties |
|
|
86
|
-
| Cascade with specificity |
|
|
87
|
-
| Important declarations |
|
|
88
|
-
| Shorthand expansion |
|
|
89
|
-
| Complex merging |
|
|
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 |
|
|
90
90
|
|
|
91
91
|
### Speedups
|
|
92
92
|
|
|
93
93
|
| Comparison | Speedup |
|
|
94
94
|
|------------|---------|
|
|
95
|
-
| Native vs Pure (no YJIT) |
|
|
96
|
-
| Native vs Pure (YJIT) | 1.
|
|
97
|
-
| YJIT impact on Pure Ruby | 1.
|
|
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) |
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
@@ -108,7 +108,7 @@ rake benchmark
|
|
|
108
108
|
rake benchmark:parsing
|
|
109
109
|
rake benchmark:serialization
|
|
110
110
|
rake benchmark:specificity
|
|
111
|
-
rake benchmark:
|
|
111
|
+
rake benchmark:flattening
|
|
112
112
|
|
|
113
113
|
# Generate documentation
|
|
114
114
|
rake benchmark:generate_docs
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [0.2.1] - 2025-11-14
|
|
2
|
+
|
|
3
|
+
- Fix serializer bug related to media queries
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2025-11-14
|
|
6
|
+
|
|
7
|
+
- Major: CSS `@import` resolution refactored from string-concatenation to parsed-object architecture with proper charset handling, media query combining,
|
|
8
|
+
and circular import detection
|
|
9
|
+
- Major: Terminology change: all `merge` methods renamed to `flatten` to better represent CSS cascade behavior (old names deprecated with warnings)
|
|
10
|
+
- Major: Rule equality now considers shorthand/longhand property equivalence (e.g., `margin: 10px` equals `margin-top: 10px; margin-right: 10px; ...`)
|
|
11
|
+
- Performance: Flatten operation optimized with array-based property storage, pre-allocated frozen strings, and lazy specificity calculation
|
|
12
|
+
- Feature: New Stylesheet collection methods (`+`, `-`, `|`, `concat`, `take`, `take_while`) with cascade rules applied
|
|
13
|
+
- Feature: Added source order tracking for proper CSS cascade resolution
|
|
14
|
+
|
|
1
15
|
## [0.1.4] - 2025-11-12
|
|
2
16
|
- Major: Pure Ruby implementation added (#12)
|
|
3
17
|
- Fix: Media query serialization bugs - parentheses now preserved per CSS spec (min-width: 768px), fixed media query ordering
|
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
|
|
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
|
|
58
|
+
- Swapped in October 2025
|
|
59
59
|
- Files: `css_parser.c`, `specificity.c`, `value_splitter.c`
|
|
60
60
|
- API unchanged, all tests pass
|
data/README.md
CHANGED
|
@@ -181,7 +181,10 @@ Cataract parses and preserves all standard CSS including:
|
|
|
181
181
|
|
|
182
182
|
Cataract supports converting colors between multiple CSS color formats with high precision.
|
|
183
183
|
|
|
184
|
-
**Note:** Color conversion is an optional extension
|
|
184
|
+
**Note:** Color conversion is an optional extension and _not_ loaded by default.
|
|
185
|
+
|
|
186
|
+
<details>
|
|
187
|
+
<summary>Color conversion examples and supported formats</summary>
|
|
185
188
|
|
|
186
189
|
```ruby
|
|
187
190
|
require 'cataract'
|
|
@@ -241,6 +244,8 @@ sheet.convert_colors!(to: :hex) # Converts all formats to hex
|
|
|
241
244
|
- CSS Color Level 5 features (`none`, `infinity`, relative color syntax with `from`) are preserved but not converted
|
|
242
245
|
- Unknown or future color functions are passed through unchanged
|
|
243
246
|
|
|
247
|
+
</details>
|
|
248
|
+
|
|
244
249
|
### `@import` Support
|
|
245
250
|
|
|
246
251
|
`@import` statements can be resolved with security controls:
|
|
@@ -301,7 +306,7 @@ Each `Rule` is a struct containing:
|
|
|
301
306
|
- `specificity`: Calculated CSS specificity (cached)
|
|
302
307
|
|
|
303
308
|
Implementation details:
|
|
304
|
-
- **C implementation**: Critical paths implemented in C (parsing,
|
|
309
|
+
- **C implementation**: Critical paths implemented in C (parsing, cascade/flatten, serialization)
|
|
305
310
|
- **Flat rule array**: All rules stored in a single array, preserving source order
|
|
306
311
|
- **Efficient media query handling**: O(1) lookup via internal media index
|
|
307
312
|
- **Memory efficient**: Minimal allocations, reuses string buffers where possible
|
data/Rakefile
CHANGED
|
@@ -79,7 +79,7 @@ task :benchmark do
|
|
|
79
79
|
Rake::Task['benchmark:parsing'].invoke
|
|
80
80
|
Rake::Task['benchmark:serialization'].invoke
|
|
81
81
|
Rake::Task['benchmark:specificity'].invoke
|
|
82
|
-
Rake::Task['benchmark:
|
|
82
|
+
Rake::Task['benchmark:flattening'].invoke
|
|
83
83
|
puts "\n#{'-' * 80}"
|
|
84
84
|
puts 'All benchmarks complete!'
|
|
85
85
|
puts 'Generate documentation with: rake benchmark:generate_docs'
|
|
@@ -105,10 +105,10 @@ namespace :benchmark do
|
|
|
105
105
|
ruby 'benchmarks/benchmark_specificity.rb'
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
desc 'Benchmark CSS
|
|
109
|
-
task
|
|
110
|
-
puts 'Running
|
|
111
|
-
ruby 'benchmarks/
|
|
108
|
+
desc 'Benchmark CSS flattening performance'
|
|
109
|
+
task flattening: :compile do
|
|
110
|
+
puts 'Running flattening benchmark...'
|
|
111
|
+
ruby 'benchmarks/benchmark_flattening.rb'
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
desc 'Benchmark string allocation optimization (buffer vs dynamic)'
|
|
@@ -195,14 +195,27 @@ task :lint do
|
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
# Fuzz testing
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
namespace :fuzz do
|
|
199
|
+
desc 'Run fuzzer with C extension'
|
|
200
|
+
task c: :compile do
|
|
201
|
+
iterations = ENV['ITERATIONS'] || '10000'
|
|
202
|
+
puts "Running CSS parser fuzzer with C extension (#{iterations} iterations)..."
|
|
203
|
+
system(ENV.to_h, RbConfig.ruby, '-Ilib', 'scripts/fuzzer/run.rb', iterations)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
desc 'Run fuzzer with pure Ruby implementation'
|
|
207
|
+
task :pure do
|
|
208
|
+
iterations = ENV['ITERATIONS'] || '10000'
|
|
209
|
+
debug_msg = ENV['FUZZ_DEBUG'] == '1' ? ' (debug mode)' : ''
|
|
210
|
+
puts "Running CSS parser fuzzer with pure Ruby (#{iterations} iterations#{debug_msg})..."
|
|
211
|
+
env = ENV.to_h.merge('CATARACT_PURE' => '1')
|
|
212
|
+
system(env, RbConfig.ruby, '-Ilib', 'scripts/fuzzer/run.rb', iterations)
|
|
213
|
+
end
|
|
204
214
|
end
|
|
205
215
|
|
|
216
|
+
desc 'Run fuzzer with both C extension and pure Ruby'
|
|
217
|
+
task fuzz: ['fuzz:c', 'fuzz:pure']
|
|
218
|
+
|
|
206
219
|
# Documentation generation with YARD
|
|
207
220
|
begin
|
|
208
221
|
require 'yard'
|
data/cataract.gemspec
CHANGED
|
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
|
22
22
|
# Specify which files should be added to the gem when it is released.
|
|
23
23
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
24
24
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
-
f.match(%r{^(test|spec|features|benchmarks|scripts)/}) ||
|
|
25
|
+
f.match(%r{^(test|spec|features|benchmarks|scripts|\.rubocop)/}) ||
|
|
26
26
|
f.match(/^(test|benchmark)_.*\.rb$/)
|
|
27
27
|
end
|
|
28
28
|
end
|
data/ext/cataract/cataract.c
CHANGED
|
@@ -7,6 +7,7 @@ VALUE cRule;
|
|
|
7
7
|
VALUE cDeclaration;
|
|
8
8
|
VALUE cAtRule;
|
|
9
9
|
VALUE cStylesheet;
|
|
10
|
+
VALUE cImportStatement;
|
|
10
11
|
|
|
11
12
|
// Error class definitions (shared with main extension)
|
|
12
13
|
VALUE eCataractError;
|
|
@@ -548,18 +549,34 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
|
|
|
548
549
|
|
|
549
550
|
if (formatted) {
|
|
550
551
|
// Formatted output with indentation
|
|
552
|
+
DEBUG_PRINTF("[SERIALIZE_RULE] Formatted mode, indent_level=%d, selector=%s\n", indent_level, RSTRING_PTR(selector));
|
|
551
553
|
rb_str_append(result, selector);
|
|
552
554
|
rb_str_cat2(result, " {\n");
|
|
553
555
|
|
|
556
|
+
// Build indent strings based on indent_level
|
|
557
|
+
// Declarations are inside the rule, so add 1 level (2 spaces per level)
|
|
558
|
+
// Closing brace matches the opening selector level
|
|
559
|
+
char decl_indent[MAX_INDENT_BUFFER];
|
|
560
|
+
char closing_indent[MAX_INDENT_BUFFER];
|
|
561
|
+
int decl_spaces = (indent_level + 1) * 2;
|
|
562
|
+
int closing_spaces = indent_level * 2;
|
|
563
|
+
memset(decl_indent, ' ', decl_spaces);
|
|
564
|
+
decl_indent[decl_spaces] = '\0';
|
|
565
|
+
memset(closing_indent, ' ', closing_spaces);
|
|
566
|
+
closing_indent[closing_spaces] = '\0';
|
|
567
|
+
|
|
554
568
|
// Serialize own declarations with indentation (each on its own line)
|
|
555
569
|
if (!NIL_P(declarations) && RARRAY_LEN(declarations) > 0) {
|
|
556
|
-
|
|
570
|
+
DEBUG_PRINTF("[SERIALIZE_RULE] Serializing %ld declarations with indent='%s' (%d spaces)\n",
|
|
571
|
+
RARRAY_LEN(declarations), decl_indent, decl_spaces);
|
|
572
|
+
serialize_declarations_formatted(result, declarations, decl_indent);
|
|
557
573
|
}
|
|
558
574
|
|
|
559
575
|
// Serialize nested children
|
|
560
576
|
serialize_children_only(result, rules_array, rule_idx, rule_to_media, parent_to_children,
|
|
561
577
|
selector, declarations, formatted, indent_level + 1);
|
|
562
578
|
|
|
579
|
+
rb_str_cat2(result, closing_indent);
|
|
563
580
|
rb_str_cat2(result, "}\n");
|
|
564
581
|
} else {
|
|
565
582
|
// Compact output
|
|
@@ -575,6 +592,9 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
|
|
|
575
592
|
|
|
576
593
|
rb_str_cat2(result, " }\n");
|
|
577
594
|
}
|
|
595
|
+
|
|
596
|
+
// Prevent compiler from optimizing away 'rule' before we're done with selector/declarations
|
|
597
|
+
RB_GC_GUARD(rule);
|
|
578
598
|
}
|
|
579
599
|
|
|
580
600
|
// New stylesheet serialization entry point - checks for nesting and delegates
|
|
@@ -625,6 +645,10 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
|
|
|
625
645
|
|
|
626
646
|
DEBUG_PRINTF("[MAP] parent_to_children map: %s\n", RSTRING_PTR(rb_inspect(parent_to_children)));
|
|
627
647
|
|
|
648
|
+
// Track media block state for proper opening/closing
|
|
649
|
+
VALUE current_media = Qnil;
|
|
650
|
+
int in_media_block = 0;
|
|
651
|
+
|
|
628
652
|
// Serialize only top-level rules (parent_rule_id == nil)
|
|
629
653
|
// Children are serialized recursively
|
|
630
654
|
DEBUG_PRINTF("[SERIALIZE] Starting serialization, total_rules=%ld\n", total_rules);
|
|
@@ -642,6 +666,34 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
|
|
|
642
666
|
continue;
|
|
643
667
|
}
|
|
644
668
|
|
|
669
|
+
// Get media for this rule
|
|
670
|
+
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
671
|
+
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
|
|
672
|
+
|
|
673
|
+
// Handle media block transitions
|
|
674
|
+
if (NIL_P(rule_media)) {
|
|
675
|
+
// Not in media - close any open media block
|
|
676
|
+
if (in_media_block) {
|
|
677
|
+
rb_str_cat2(result, "}\n");
|
|
678
|
+
in_media_block = 0;
|
|
679
|
+
current_media = Qnil;
|
|
680
|
+
}
|
|
681
|
+
} else {
|
|
682
|
+
// In media - check if we need to open/change block
|
|
683
|
+
if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
|
|
684
|
+
// Close previous media block if open
|
|
685
|
+
if (in_media_block) {
|
|
686
|
+
rb_str_cat2(result, "}\n");
|
|
687
|
+
}
|
|
688
|
+
// Open new media block
|
|
689
|
+
current_media = rule_media;
|
|
690
|
+
rb_str_cat2(result, "@media ");
|
|
691
|
+
rb_str_append(result, rb_sym2str(rule_media));
|
|
692
|
+
rb_str_cat2(result, " {\n");
|
|
693
|
+
in_media_block = 1;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
645
697
|
// Check if this is an AtRule
|
|
646
698
|
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
647
699
|
serialize_at_rule(result, rule);
|
|
@@ -656,6 +708,11 @@ static VALUE stylesheet_to_s_new(VALUE self, VALUE rules_array, VALUE media_inde
|
|
|
656
708
|
);
|
|
657
709
|
}
|
|
658
710
|
|
|
711
|
+
// Close final media block if still open
|
|
712
|
+
if (in_media_block) {
|
|
713
|
+
rb_str_cat2(result, "}\n");
|
|
714
|
+
}
|
|
715
|
+
|
|
659
716
|
return result;
|
|
660
717
|
}
|
|
661
718
|
|
|
@@ -778,6 +835,10 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
|
|
|
778
835
|
}
|
|
779
836
|
}
|
|
780
837
|
|
|
838
|
+
// Track media block state for proper opening/closing
|
|
839
|
+
VALUE current_media = Qnil;
|
|
840
|
+
int in_media_block = 0;
|
|
841
|
+
|
|
781
842
|
// Serialize only top-level rules (parent_rule_id == nil)
|
|
782
843
|
for (long i = 0; i < total_rules; i++) {
|
|
783
844
|
VALUE rule = rb_ary_entry(rules_array, i);
|
|
@@ -788,20 +849,66 @@ static VALUE stylesheet_to_formatted_s_new(VALUE self, VALUE rules_array, VALUE
|
|
|
788
849
|
continue;
|
|
789
850
|
}
|
|
790
851
|
|
|
852
|
+
// Get media for this rule
|
|
853
|
+
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
854
|
+
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
|
|
855
|
+
|
|
856
|
+
// Handle media block transitions
|
|
857
|
+
if (NIL_P(rule_media)) {
|
|
858
|
+
// Not in media - close any open media block
|
|
859
|
+
if (in_media_block) {
|
|
860
|
+
rb_str_cat2(result, "}\n");
|
|
861
|
+
in_media_block = 0;
|
|
862
|
+
current_media = Qnil;
|
|
863
|
+
|
|
864
|
+
// Add blank line after closing media block
|
|
865
|
+
rb_str_cat2(result, "\n");
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
// In media - check if we need to open/change block
|
|
869
|
+
if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
|
|
870
|
+
// Close previous media block if open
|
|
871
|
+
if (in_media_block) {
|
|
872
|
+
rb_str_cat2(result, "}\n");
|
|
873
|
+
} else if (RSTRING_LEN(result) > 0) {
|
|
874
|
+
// Add blank line before new media block (except at start)
|
|
875
|
+
rb_str_cat2(result, "\n");
|
|
876
|
+
}
|
|
877
|
+
// Open new media block
|
|
878
|
+
current_media = rule_media;
|
|
879
|
+
rb_str_cat2(result, "@media ");
|
|
880
|
+
rb_str_append(result, rb_sym2str(rule_media));
|
|
881
|
+
rb_str_cat2(result, " {\n");
|
|
882
|
+
in_media_block = 1;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
791
886
|
// Check if this is an AtRule
|
|
792
887
|
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
793
888
|
serialize_at_rule(result, rule);
|
|
794
889
|
continue;
|
|
795
890
|
}
|
|
796
891
|
|
|
892
|
+
// Add indent if inside media block
|
|
893
|
+
if (in_media_block) {
|
|
894
|
+
DEBUG_PRINTF("[FORMATTED] Adding base indent for media block\n");
|
|
895
|
+
rb_str_cat2(result, " ");
|
|
896
|
+
}
|
|
897
|
+
|
|
797
898
|
// Serialize rule with nested children
|
|
899
|
+
DEBUG_PRINTF("[FORMATTED] Calling serialize_rule_with_children, in_media_block=%d\n", in_media_block);
|
|
798
900
|
serialize_rule_with_children(
|
|
799
901
|
result, rules_array, i, rule_to_media, parent_to_children,
|
|
800
902
|
1, // formatted (with indentation)
|
|
801
|
-
0 // indent_level (
|
|
903
|
+
in_media_block ? 1 : 0 // indent_level (1 if inside media block, 0 otherwise)
|
|
802
904
|
);
|
|
803
905
|
}
|
|
804
906
|
|
|
907
|
+
// Close final media block if still open
|
|
908
|
+
if (in_media_block) {
|
|
909
|
+
rb_str_cat2(result, "}\n");
|
|
910
|
+
}
|
|
911
|
+
|
|
805
912
|
return result;
|
|
806
913
|
}
|
|
807
914
|
|
|
@@ -1044,6 +1151,12 @@ void Init_native_extension(void) {
|
|
|
1044
1151
|
rb_raise(rb_eLoadError, "Cataract::AtRule not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
|
|
1045
1152
|
}
|
|
1046
1153
|
|
|
1154
|
+
if (rb_const_defined(mCataract, rb_intern("ImportStatement"))) {
|
|
1155
|
+
cImportStatement = rb_const_get(mCataract, rb_intern("ImportStatement"));
|
|
1156
|
+
} else {
|
|
1157
|
+
rb_raise(rb_eLoadError, "Cataract::ImportStatement not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1047
1160
|
// Define Declarations class and add to_s method
|
|
1048
1161
|
VALUE cDeclarations = rb_define_class_under(mCataract, "Declarations", rb_cObject);
|
|
1049
1162
|
rb_define_method(cDeclarations, "to_s", new_declarations_to_s_method, 0);
|
|
@@ -1057,12 +1170,14 @@ void Init_native_extension(void) {
|
|
|
1057
1170
|
rb_define_module_function(mCataract, "_stylesheet_to_formatted_s", stylesheet_to_formatted_s_new, 4);
|
|
1058
1171
|
rb_define_module_function(mCataract, "parse_media_types", parse_media_types, 1);
|
|
1059
1172
|
rb_define_module_function(mCataract, "parse_declarations", new_parse_declarations, 1);
|
|
1060
|
-
rb_define_module_function(mCataract, "
|
|
1173
|
+
rb_define_module_function(mCataract, "flatten", cataract_flatten, 1);
|
|
1174
|
+
rb_define_module_function(mCataract, "merge", cataract_flatten, 1); // Deprecated alias for backwards compatibility
|
|
1061
1175
|
rb_define_module_function(mCataract, "extract_imports", extract_imports, 1);
|
|
1062
1176
|
rb_define_module_function(mCataract, "calculate_specificity", calculate_specificity, 1);
|
|
1177
|
+
rb_define_module_function(mCataract, "_expand_shorthand", cataract_expand_shorthand, 1);
|
|
1063
1178
|
|
|
1064
|
-
// Initialize
|
|
1065
|
-
|
|
1179
|
+
// Initialize flatten constants (cached property strings)
|
|
1180
|
+
init_flatten_constants();
|
|
1066
1181
|
|
|
1067
1182
|
// Export compile-time flags as a hash for runtime introspection
|
|
1068
1183
|
VALUE compile_flags = rb_hash_new();
|