cataract 0.3.0 → 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.
- checksums.yaml +4 -4
- data/.gitignore +0 -3
- data/.rubocop_todo.yml +11 -19
- data/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +9 -0
- data/ext/cataract/cataract.c +72 -103
- data/ext/cataract/cataract.h +0 -1
- data/ext/cataract/css_parser.c +0 -18
- data/lib/cataract/declarations.rb +14 -4
- data/lib/cataract/native.rb +30 -0
- data/lib/cataract/pure/byte_constants.rb +70 -66
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1197 -1182
- data/lib/cataract/pure/parser.rb +1725 -1729
- data/lib/cataract/pure/serializer.rb +575 -715
- data/lib/cataract/pure/specificity.rb +165 -144
- data/lib/cataract/pure.rb +73 -101
- data/lib/cataract/rule.rb +6 -3
- data/lib/cataract/stylesheet.rb +19 -7
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +3 -2
- data/lib/cataract/pure/helpers.rb +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67046f0d371d7b0b077971e8634ff4054e64fe23a55c80b1a9f80e3dee6d804b
|
|
4
|
+
data.tar.gz: 776865b7490e22c326c0bab93a3da32f088177817b3e237c68b66e233df39021
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 21f5ccd9b4ed3e0831fa8723d02493b7b4a3582c359a3e3bae927e554d207422a0139592b23889d273ca1ed3a1888679c729d9ad0f4aa2bf4c90d18b2e16fe0c
|
|
7
|
+
data.tar.gz: f149256218337a47a1b27cd945857dd358199ceb47f70274c0c7db5cb3fa196d84a55281528c0f22d12da4bbbaff3b92c6e315210906a75aee2d304a9b91c275
|
data/.gitignore
CHANGED
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
15
|
-
- **Generated**:
|
|
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) |
|
|
24
|
-
| Medium CSS with @media (139 lines, 1.6KB) |
|
|
25
|
-
| Selector lists (3500 lines, 62.5KB, 500 lists) |
|
|
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) |
|
|
32
|
-
| Native vs Pure (YJIT) | 3.
|
|
33
|
-
| YJIT impact on Pure Ruby | 4.
|
|
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 |
|
|
42
|
-
| Medium CSS (139 lines) - with error checking |
|
|
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 |
|
|
50
|
-
| Pure (no YJIT) |
|
|
51
|
-
| Pure (YJIT) | 10.
|
|
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.
|
|
62
|
-
| to_s (Compact utilities - 2.9KB) | 1.
|
|
63
|
-
| to_formatted_s (Nested CSS - 1.3KB) |
|
|
64
|
-
| to_s with selector_lists (3.4KB) |
|
|
65
|
-
| Media filtering (Bootstrap print only) |
|
|
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) |
|
|
72
|
-
| Native vs Pure (YJIT) | 1.
|
|
73
|
-
| YJIT impact on Pure Ruby | 1.
|
|
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 |
|
|
84
|
-
| Compound Selectors |
|
|
85
|
-
| Combinators |
|
|
86
|
-
| Pseudo-classes & Pseudo-elements |
|
|
87
|
-
| :not() Pseudo-class (CSS3) |
|
|
88
|
-
| Complex Real-world Selectors |
|
|
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) |
|
|
95
|
-
| Native vs Pure (YJIT) |
|
|
96
|
-
| YJIT impact on Pure Ruby | 2.
|
|
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) |
|
|
107
|
-
| Simple properties |
|
|
108
|
-
| Cascade with specificity |
|
|
109
|
-
| Important declarations |
|
|
110
|
-
| Shorthand expansion |
|
|
111
|
-
| Complex flattening |
|
|
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.
|
|
118
|
-
| Native vs Pure (YJIT) | 1.
|
|
119
|
-
| YJIT impact on Pure Ruby | 1.
|
|
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,3 +1,12 @@
|
|
|
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
|
+
|
|
1
10
|
## [0.3.0] - 2026-07-06
|
|
2
11
|
|
|
3
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.
|
data/ext/cataract/cataract.c
CHANGED
|
@@ -377,6 +377,37 @@ static int build_mq_reverse_map_callback(VALUE list_id, VALUE mq_ids, VALUE arg)
|
|
|
377
377
|
return ST_CONTINUE;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// Build the media_query_id => list_id reverse map used by both stylesheet
|
|
381
|
+
// serializers (grouping and nesting) to detect when a rule's media query is
|
|
382
|
+
// part of a comma-separated list (e.g. "@media screen, print").
|
|
383
|
+
static VALUE build_mq_id_to_list_id(VALUE media_query_lists) {
|
|
384
|
+
VALUE mq_id_to_list_id = rb_hash_new();
|
|
385
|
+
if (!NIL_P(media_query_lists) && TYPE(media_query_lists) == T_HASH) {
|
|
386
|
+
struct build_mq_reverse_map_ctx ctx = { mq_id_to_list_id };
|
|
387
|
+
rb_hash_foreach(media_query_lists, build_mq_reverse_map_callback, (VALUE)&ctx);
|
|
388
|
+
}
|
|
389
|
+
return mq_id_to_list_id;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Build a rule_id => rule lookup. rules_array is NOT guaranteed to satisfy
|
|
393
|
+
// rules[i].id == i here - that invariant only holds for the full,
|
|
394
|
+
// freshly-parsed rules array; Stylesheet#to_s(media: ...) passes a filtered
|
|
395
|
+
// subset whenever the media filter isn't :all, so a plain array index would
|
|
396
|
+
// silently fetch the wrong rule (or even an AtRule, which doesn't share
|
|
397
|
+
// Rule's struct layout) once any rule has been filtered out. Both Rule and
|
|
398
|
+
// AtRule keep id at field index 0, so a single RULE_ID read is safe for
|
|
399
|
+
// either struct type.
|
|
400
|
+
static VALUE build_rules_by_id(VALUE rules_array) {
|
|
401
|
+
VALUE rules_by_id = rb_hash_new();
|
|
402
|
+
long len = RARRAY_LEN(rules_array);
|
|
403
|
+
for (long i = 0; i < len; i++) {
|
|
404
|
+
VALUE rule = rb_ary_entry(rules_array, i);
|
|
405
|
+
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
406
|
+
rb_hash_aset(rules_by_id, rule_id, rule);
|
|
407
|
+
}
|
|
408
|
+
return rules_by_id;
|
|
409
|
+
}
|
|
410
|
+
|
|
380
411
|
// Rule and AtRule don't share a member layout past their first few fields
|
|
381
412
|
// (see the AT_RULE_* comment in cataract.h), so reading parent_rule_id or
|
|
382
413
|
// media_query_id off a rule of unknown type needs to dispatch on which
|
|
@@ -421,7 +452,7 @@ static inline void emit_rule(VALUE result, VALUE rule, const char *decl_indent,
|
|
|
421
452
|
// each match as processed. Returns an array of their selectors (does not
|
|
422
453
|
// include the current rule's own selector or mark it processed - the
|
|
423
454
|
// caller's rule_id is handled by serialize_rule_in_group).
|
|
424
|
-
static VALUE collect_matching_selectors(VALUE rule_ids_in_list, VALUE rule_media_query_id, VALUE rule_declarations, VALUE
|
|
455
|
+
static VALUE collect_matching_selectors(VALUE rule_ids_in_list, VALUE rule_media_query_id, VALUE rule_declarations, VALUE rules_by_id, VALUE processed_rule_ids) {
|
|
425
456
|
VALUE matching_selectors = rb_ary_new();
|
|
426
457
|
long list_len = RARRAY_LEN(rule_ids_in_list);
|
|
427
458
|
|
|
@@ -434,7 +465,7 @@ static VALUE collect_matching_selectors(VALUE rule_ids_in_list, VALUE rule_media
|
|
|
434
465
|
}
|
|
435
466
|
|
|
436
467
|
// Find the rule by ID
|
|
437
|
-
VALUE other_rule =
|
|
468
|
+
VALUE other_rule = rb_hash_aref(rules_by_id, other_rule_id);
|
|
438
469
|
if (NIL_P(other_rule)) continue;
|
|
439
470
|
|
|
440
471
|
// Check same media context (compare media_query_id directly)
|
|
@@ -481,7 +512,7 @@ static void emit_grouped_rule(VALUE result, VALUE rule, VALUE matching_selectors
|
|
|
481
512
|
struct rule_group_ctx {
|
|
482
513
|
VALUE selector_lists;
|
|
483
514
|
int grouping_enabled;
|
|
484
|
-
VALUE
|
|
515
|
+
VALUE rules_by_id;
|
|
485
516
|
VALUE processed_rule_ids;
|
|
486
517
|
const struct format_opts *opts;
|
|
487
518
|
};
|
|
@@ -512,7 +543,7 @@ static void serialize_rule_in_group(
|
|
|
512
543
|
|
|
513
544
|
if (!NIL_P(rule_ids_in_list) && RARRAY_LEN(rule_ids_in_list) > 1) {
|
|
514
545
|
VALUE rule_declarations = rb_struct_aref(rule, INT2FIX(RULE_DECLARATIONS));
|
|
515
|
-
VALUE matching_selectors = collect_matching_selectors(rule_ids_in_list, rule_media_query_id, rule_declarations, ctx->
|
|
546
|
+
VALUE matching_selectors = collect_matching_selectors(rule_ids_in_list, rule_media_query_id, rule_declarations, ctx->rules_by_id, ctx->processed_rule_ids);
|
|
516
547
|
emit_grouped_rule(result, rule, matching_selectors, rule_declarations, ctx->opts, decl_indent, rule_prefix);
|
|
517
548
|
return;
|
|
518
549
|
}
|
|
@@ -542,16 +573,18 @@ static VALUE serialize_stylesheet_with_grouping(
|
|
|
542
573
|
|
|
543
574
|
// Build reverse map: media_query_id => list_id
|
|
544
575
|
// This allows us to detect when multiple rules share a comma-separated media query list
|
|
545
|
-
VALUE mq_id_to_list_id =
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
576
|
+
VALUE mq_id_to_list_id = build_mq_id_to_list_id(media_query_lists);
|
|
577
|
+
|
|
578
|
+
// Only needed by the grouping path - rules_array here may already be
|
|
579
|
+
// filtered (Stylesheet#to_s(media: ...) for anything but :all), so
|
|
580
|
+
// looking up "other rules in this selector list" needs a real id => rule
|
|
581
|
+
// map rather than indexing directly into rules_array.
|
|
582
|
+
VALUE rules_by_id = grouping_enabled ? build_rules_by_id(rules_array) : Qnil;
|
|
550
583
|
|
|
551
584
|
// Track processed rules to avoid duplicates when grouping
|
|
552
585
|
VALUE processed_rule_ids = rb_hash_new();
|
|
553
586
|
|
|
554
|
-
struct rule_group_ctx group_ctx = { selector_lists, grouping_enabled,
|
|
587
|
+
struct rule_group_ctx group_ctx = { selector_lists, grouping_enabled, rules_by_id, processed_rule_ids, opts };
|
|
555
588
|
|
|
556
589
|
// Iterate through rules in insertion order, grouping consecutive media queries
|
|
557
590
|
VALUE current_media = Qnil;
|
|
@@ -626,6 +659,7 @@ static VALUE serialize_stylesheet_with_grouping(
|
|
|
626
659
|
|
|
627
660
|
// Guard hash objects we created and used throughout
|
|
628
661
|
RB_GC_GUARD(mq_id_to_list_id);
|
|
662
|
+
RB_GC_GUARD(rules_by_id);
|
|
629
663
|
RB_GC_GUARD(processed_rule_ids);
|
|
630
664
|
return result;
|
|
631
665
|
}
|
|
@@ -846,12 +880,18 @@ static void serialize_rule_with_children(VALUE result, VALUE rules_array, long r
|
|
|
846
880
|
static VALUE serialize_stylesheet_with_nesting(
|
|
847
881
|
VALUE rules_array,
|
|
848
882
|
VALUE media_queries,
|
|
883
|
+
VALUE media_query_lists,
|
|
849
884
|
VALUE result,
|
|
850
885
|
const struct format_opts *opts
|
|
851
886
|
) {
|
|
852
887
|
int formatted = (opts->decl_indent_base != NULL);
|
|
853
888
|
long total_rules = RARRAY_LEN(rules_array);
|
|
854
889
|
|
|
890
|
+
// Reverse map: media_query_id => list_id, so a rule whose media query is
|
|
891
|
+
// part of a comma-separated list (e.g. "@media screen, print") renders
|
|
892
|
+
// the full list, not just its own single media query.
|
|
893
|
+
VALUE mq_id_to_list_id = build_mq_id_to_list_id(media_query_lists);
|
|
894
|
+
|
|
855
895
|
// Build parent_to_children map (parent_rule_id -> array of child indices)
|
|
856
896
|
// This allows O(1) lookup of children when serializing each parent
|
|
857
897
|
VALUE parent_to_children = rb_hash_new();
|
|
@@ -930,7 +970,7 @@ static VALUE serialize_stylesheet_with_nesting(
|
|
|
930
970
|
// Open new media block - store the MediaQuery object for comparison
|
|
931
971
|
current_media = rule_media;
|
|
932
972
|
rb_str_cat2(result, "@media ");
|
|
933
|
-
|
|
973
|
+
append_media_query_string(result, rule_media_query_id, mq_id_to_list_id, media_query_lists, media_queries);
|
|
934
974
|
rb_str_cat2(result, " {\n");
|
|
935
975
|
in_media_block = 1;
|
|
936
976
|
}
|
|
@@ -1013,7 +1053,7 @@ static VALUE stylesheet_serialize_impl(VALUE rules_array, VALUE charset, VALUE h
|
|
|
1013
1053
|
|
|
1014
1054
|
// Has nesting - use the parent/child lookahead serializer
|
|
1015
1055
|
// TODO: Phase 2 - use selector_lists for grouping on the nesting path too
|
|
1016
|
-
return serialize_stylesheet_with_nesting(rules_array, media_queries, result, &opts);
|
|
1056
|
+
return serialize_stylesheet_with_nesting(rules_array, media_queries, media_query_lists, result, &opts);
|
|
1017
1057
|
}
|
|
1018
1058
|
|
|
1019
1059
|
// New stylesheet serialization entry point - checks for nesting and delegates
|
|
@@ -1077,78 +1117,6 @@ static VALUE new_parse_declarations_string(const char *start, const char *end) {
|
|
|
1077
1117
|
return declarations;
|
|
1078
1118
|
}
|
|
1079
1119
|
|
|
1080
|
-
/*
|
|
1081
|
-
* Convert array of Declaration structs to CSS string
|
|
1082
|
-
* Format: "prop: value; prop2: value2 !important; "
|
|
1083
|
-
*
|
|
1084
|
-
* This is a copy of declarations_array_to_s from cataract.c,
|
|
1085
|
-
* but works with Declaration structs instead of Declaration structs
|
|
1086
|
-
*/
|
|
1087
|
-
static VALUE new_declarations_array_to_s(VALUE declarations_array) {
|
|
1088
|
-
Check_Type(declarations_array, T_ARRAY);
|
|
1089
|
-
|
|
1090
|
-
long len = RARRAY_LEN(declarations_array);
|
|
1091
|
-
if (len == 0) {
|
|
1092
|
-
return rb_str_new_cstr("");
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// Use rb_str_buf_new for efficient string building
|
|
1096
|
-
VALUE result = rb_str_buf_new(len * 32); // Estimate 32 chars per declaration
|
|
1097
|
-
|
|
1098
|
-
for (long i = 0; i < len; i++) {
|
|
1099
|
-
VALUE decl = rb_ary_entry(declarations_array, i);
|
|
1100
|
-
|
|
1101
|
-
// Validate this is a Declaration struct
|
|
1102
|
-
if (!RB_TYPE_P(decl, T_STRUCT) || rb_obj_class(decl) != cDeclaration) {
|
|
1103
|
-
rb_raise(rb_eTypeError,
|
|
1104
|
-
"Expected array of Declaration structs, got %s at index %ld",
|
|
1105
|
-
rb_obj_classname(decl), i);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
// Extract struct fields
|
|
1109
|
-
VALUE property = rb_struct_aref(decl, INT2FIX(DECL_PROPERTY));
|
|
1110
|
-
VALUE value = rb_struct_aref(decl, INT2FIX(DECL_VALUE));
|
|
1111
|
-
VALUE important = rb_struct_aref(decl, INT2FIX(DECL_IMPORTANT));
|
|
1112
|
-
|
|
1113
|
-
// Append: "property: value"
|
|
1114
|
-
rb_str_buf_append(result, property);
|
|
1115
|
-
rb_str_buf_cat2(result, ": ");
|
|
1116
|
-
rb_str_buf_append(result, value);
|
|
1117
|
-
|
|
1118
|
-
// Append " !important" if needed
|
|
1119
|
-
if (RTEST(important)) {
|
|
1120
|
-
rb_str_buf_cat2(result, " !important");
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
rb_str_buf_cat2(result, "; ");
|
|
1124
|
-
|
|
1125
|
-
RB_GC_GUARD(decl);
|
|
1126
|
-
RB_GC_GUARD(property);
|
|
1127
|
-
RB_GC_GUARD(value);
|
|
1128
|
-
RB_GC_GUARD(important);
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
// Strip trailing space
|
|
1132
|
-
rb_str_set_len(result, RSTRING_LEN(result) - 1);
|
|
1133
|
-
|
|
1134
|
-
RB_GC_GUARD(result);
|
|
1135
|
-
return result;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
/*
|
|
1139
|
-
* Instance method: Declarations#to_s
|
|
1140
|
-
* Converts declarations to CSS string
|
|
1141
|
-
*
|
|
1142
|
-
* @return [String] CSS declarations like "color: red; margin: 10px !important;"
|
|
1143
|
-
*/
|
|
1144
|
-
static VALUE new_declarations_to_s_method(VALUE self) {
|
|
1145
|
-
// Get @values instance variable (array of Declaration structs)
|
|
1146
|
-
VALUE values = rb_ivar_get(self, rb_intern("@values"));
|
|
1147
|
-
|
|
1148
|
-
// Call core serialization function
|
|
1149
|
-
return new_declarations_array_to_s(values);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
1120
|
/*
|
|
1153
1121
|
* Ruby-facing wrapper for new_parse_declarations
|
|
1154
1122
|
*
|
|
@@ -1239,23 +1207,24 @@ void Init_native_extension(void) {
|
|
|
1239
1207
|
rb_raise(rb_eLoadError, "Cataract::MediaQuery not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
|
|
1240
1208
|
}
|
|
1241
1209
|
|
|
1242
|
-
// Define
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
// Define Stylesheet class (Ruby will add instance methods like each_selector)
|
|
1210
|
+
// Define Stylesheet class (Ruby will add instance methods like each_selector).
|
|
1211
|
+
// Declarations is left alone here - it's a plain Ruby class (declarations.rb)
|
|
1212
|
+
// with its own #to_s, not something this backend attaches methods to.
|
|
1247
1213
|
cStylesheet = rb_define_class_under(mCataract, "Stylesheet", rb_cObject);
|
|
1248
1214
|
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
rb_define_module_function(
|
|
1256
|
-
rb_define_module_function(
|
|
1257
|
-
rb_define_module_function(
|
|
1258
|
-
rb_define_module_function(
|
|
1215
|
+
// Every native-specific entry point lives under Cataract::Backends::Native,
|
|
1216
|
+
// never directly on Cataract itself, so the pure Ruby backend can be loaded
|
|
1217
|
+
// in the same process without either backend clobbering the other's methods.
|
|
1218
|
+
VALUE mBackends = rb_define_module_under(mCataract, "Backends");
|
|
1219
|
+
VALUE mNative = rb_define_module_under(mBackends, "Native");
|
|
1220
|
+
|
|
1221
|
+
rb_define_module_function(mNative, "parse", parse_css_new, -1);
|
|
1222
|
+
rb_define_module_function(mNative, "stylesheet_to_s", stylesheet_to_s, 6);
|
|
1223
|
+
rb_define_module_function(mNative, "stylesheet_to_formatted_s", stylesheet_to_formatted_s, 6);
|
|
1224
|
+
rb_define_module_function(mNative, "parse_declarations", new_parse_declarations, 1);
|
|
1225
|
+
rb_define_module_function(mNative, "flatten", cataract_flatten, 1);
|
|
1226
|
+
rb_define_module_function(mNative, "calculate_specificity", calculate_specificity, 1);
|
|
1227
|
+
rb_define_module_function(mNative, "expand_shorthand", cataract_expand_shorthand, 1);
|
|
1259
1228
|
|
|
1260
1229
|
// Initialize flatten constants (cached property strings)
|
|
1261
1230
|
init_flatten_constants();
|
|
@@ -1275,11 +1244,11 @@ void Init_native_extension(void) {
|
|
|
1275
1244
|
rb_hash_aset(compile_flags, ID2SYM(rb_intern("str_buf_optimization")), Qtrue);
|
|
1276
1245
|
#endif
|
|
1277
1246
|
|
|
1278
|
-
rb_define_const(
|
|
1247
|
+
rb_define_const(mNative, "COMPILE_FLAGS", compile_flags);
|
|
1279
1248
|
|
|
1280
|
-
// Flag to indicate native
|
|
1281
|
-
rb_define_const(
|
|
1249
|
+
// Flag to indicate the native backend is loaded (for pure Ruby fallback detection)
|
|
1250
|
+
rb_define_const(mNative, "NATIVE_EXTENSION_LOADED", Qtrue);
|
|
1282
1251
|
|
|
1283
1252
|
// Implementation type constant
|
|
1284
|
-
rb_define_const(
|
|
1253
|
+
rb_define_const(mNative, "IMPLEMENTATION", ID2SYM(rb_intern("native")));
|
|
1285
1254
|
}
|
data/ext/cataract/cataract.h
CHANGED
|
@@ -262,7 +262,6 @@ static inline VALUE strip_string(const char *str, long len) {
|
|
|
262
262
|
// CSS parser (css_parser_new.c)
|
|
263
263
|
VALUE parse_css_new(int argc, VALUE *argv, VALUE self);
|
|
264
264
|
VALUE parse_css_new_impl(VALUE css_string, VALUE parser_options, int rule_id_offset);
|
|
265
|
-
VALUE parse_media_types(VALUE self, VALUE media_query_sym);
|
|
266
265
|
|
|
267
266
|
// Flatten (flatten.c)
|
|
268
267
|
VALUE cataract_flatten(VALUE self, VALUE rules_array);
|
data/ext/cataract/css_parser.c
CHANGED
|
@@ -2312,24 +2312,6 @@ static void parse_css_recursive(ParserContext *ctx, const char *css, const char
|
|
|
2312
2312
|
}
|
|
2313
2313
|
}
|
|
2314
2314
|
|
|
2315
|
-
/*
|
|
2316
|
-
* Parse media query string and extract media types (Ruby-facing function)
|
|
2317
|
-
* Example: "screen, print" => [:screen, :print]
|
|
2318
|
-
* Example: "screen and (min-width: 768px)" => [:screen]
|
|
2319
|
-
*
|
|
2320
|
-
* @param media_query_sym [Symbol] Media query as symbol
|
|
2321
|
-
* @return [Array<Symbol>] Array of media type symbols
|
|
2322
|
-
*/
|
|
2323
|
-
VALUE parse_media_types(VALUE self, VALUE media_query_sym) {
|
|
2324
|
-
Check_Type(media_query_sym, T_SYMBOL);
|
|
2325
|
-
|
|
2326
|
-
VALUE query_string = rb_sym2str(media_query_sym);
|
|
2327
|
-
const char *query_str = RSTRING_PTR(query_string);
|
|
2328
|
-
long query_len = RSTRING_LEN(query_string);
|
|
2329
|
-
|
|
2330
|
-
return extract_media_types(query_str, query_len);
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
2315
|
/*
|
|
2334
2316
|
* Main parse entry point
|
|
2335
2317
|
* Returns: { rules: [...], media_index: {...}, charset: "..." | nil, last_rule_id: N }
|
|
@@ -198,12 +198,22 @@ module Cataract
|
|
|
198
198
|
|
|
199
199
|
# Convert declarations to CSS string.
|
|
200
200
|
#
|
|
201
|
-
# Implemented in C for performance.
|
|
202
|
-
#
|
|
203
201
|
# @return [String] CSS declaration block string
|
|
204
202
|
#
|
|
205
203
|
# @example
|
|
206
204
|
# decls.to_s #=> "color: red; margin: 10px !important;"
|
|
205
|
+
def to_s
|
|
206
|
+
result = +''
|
|
207
|
+
@values.each_with_index do |decl, i|
|
|
208
|
+
result << decl.property
|
|
209
|
+
result << ': '
|
|
210
|
+
result << decl.value
|
|
211
|
+
result << ' !important' if decl.important
|
|
212
|
+
result << ';'
|
|
213
|
+
result << ' ' if i < @values.length - 1 # Add space after semicolon except for last
|
|
214
|
+
end
|
|
215
|
+
result
|
|
216
|
+
end
|
|
207
217
|
|
|
208
218
|
# Enable implicit string conversion for comparisons
|
|
209
219
|
alias to_str to_s
|
|
@@ -290,7 +300,7 @@ module Cataract
|
|
|
290
300
|
# @param source [Declarations] Source Declarations being copied
|
|
291
301
|
def initialize_copy(source)
|
|
292
302
|
super
|
|
293
|
-
@values = source.
|
|
303
|
+
@values = source.to_a.dup
|
|
294
304
|
end
|
|
295
305
|
|
|
296
306
|
# Compare this Declarations with another object.
|
|
@@ -331,7 +341,7 @@ module Cataract
|
|
|
331
341
|
|
|
332
342
|
# Parse "color: red; background: blue" string into array of Declaration structs
|
|
333
343
|
def parse_declaration_string(str)
|
|
334
|
-
Cataract.parse_declarations(str)
|
|
344
|
+
Cataract::Backends.active.parse_declarations(str)
|
|
335
345
|
end
|
|
336
346
|
end
|
|
337
347
|
end
|