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.
- checksums.yaml +4 -4
- data/.github/workflows/ci-manual-rubies.yml +44 -0
- data/.overcommit.yml +1 -1
- data/.rubocop.yml +96 -4
- data/.rubocop_todo.yml +186 -0
- data/BENCHMARKS.md +62 -141
- data/CHANGELOG.md +20 -0
- data/RAGEL_MIGRATION.md +2 -2
- data/README.md +37 -4
- data/Rakefile +72 -32
- data/cataract.gemspec +4 -1
- data/ext/cataract/cataract.c +59 -50
- data/ext/cataract/cataract.h +5 -3
- data/ext/cataract/css_parser.c +173 -65
- data/ext/cataract/extconf.rb +2 -2
- data/ext/cataract/{merge.c → flatten.c} +526 -468
- data/ext/cataract/shorthand_expander.c +164 -115
- data/lib/cataract/at_rule.rb +8 -9
- data/lib/cataract/declaration.rb +18 -0
- data/lib/cataract/import_resolver.rb +63 -43
- data/lib/cataract/import_statement.rb +49 -0
- data/lib/cataract/pure/byte_constants.rb +69 -0
- data/lib/cataract/pure/flatten.rb +1145 -0
- data/lib/cataract/pure/helpers.rb +35 -0
- data/lib/cataract/pure/imports.rb +268 -0
- data/lib/cataract/pure/parser.rb +1340 -0
- data/lib/cataract/pure/serializer.rb +590 -0
- data/lib/cataract/pure/specificity.rb +206 -0
- data/lib/cataract/pure.rb +153 -0
- data/lib/cataract/rule.rb +69 -15
- data/lib/cataract/stylesheet.rb +356 -49
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +43 -26
- metadata +14 -26
- data/benchmarks/benchmark_harness.rb +0 -193
- data/benchmarks/benchmark_merging.rb +0 -121
- data/benchmarks/benchmark_optimization_comparison.rb +0 -168
- data/benchmarks/benchmark_parsing.rb +0 -153
- data/benchmarks/benchmark_ragel_removal.rb +0 -56
- data/benchmarks/benchmark_runner.rb +0 -70
- data/benchmarks/benchmark_serialization.rb +0 -180
- data/benchmarks/benchmark_shorthand.rb +0 -109
- data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
- data/benchmarks/benchmark_specificity.rb +0 -124
- data/benchmarks/benchmark_string_allocation.rb +0 -151
- data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
- data/benchmarks/benchmark_to_s_cached.rb +0 -55
- data/benchmarks/benchmark_value_splitter.rb +0 -54
- data/benchmarks/benchmark_yjit.rb +0 -158
- data/benchmarks/benchmark_yjit_workers.rb +0 -61
- data/benchmarks/profile_to_s.rb +0 -23
- data/benchmarks/speedup_calculator.rb +0 -83
- data/benchmarks/system_metadata.rb +0 -81
- data/benchmarks/templates/benchmarks.md.erb +0 -221
- data/benchmarks/yjit_tests.rb +0 -141
- data/scripts/fuzzer/run.rb +0 -828
- data/scripts/fuzzer/worker.rb +0 -99
- data/scripts/generate_benchmarks_md.rb +0 -155
data/README.md
CHANGED
|
@@ -34,6 +34,32 @@ gem install cataract
|
|
|
34
34
|
|
|
35
35
|
- Ruby >= 3.1.0
|
|
36
36
|
|
|
37
|
+
### Pure Ruby Implementation
|
|
38
|
+
|
|
39
|
+
Cataract includes a pure Ruby implementation alongside the C extension. This is useful for:
|
|
40
|
+
- Platforms where C extensions cannot be compiled
|
|
41
|
+
- Development/debugging without needing to recompile C code
|
|
42
|
+
- Environments with restricted native code execution
|
|
43
|
+
|
|
44
|
+
**In your Gemfile:**
|
|
45
|
+
```ruby
|
|
46
|
+
gem 'cataract', require: 'cataract/pure'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Or set environment variable:**
|
|
50
|
+
```ruby
|
|
51
|
+
# For CI, testing, or one-off usage
|
|
52
|
+
ENV['CATARACT_PURE'] = '1'
|
|
53
|
+
require 'cataract'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Check which implementation is loaded:**
|
|
57
|
+
```ruby
|
|
58
|
+
Cataract::IMPLEMENTATION # => :native or :ruby
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Note:** The pure Ruby implementation does not include color conversion functionality (`convert_colors!`), which is only available in the C extension.
|
|
62
|
+
|
|
37
63
|
## Usage
|
|
38
64
|
|
|
39
65
|
### Basic Parsing
|
|
@@ -142,9 +168,11 @@ See [BENCHMARKS.md](BENCHMARKS.md) for detailed performance comparisons.
|
|
|
142
168
|
|
|
143
169
|
## CSS Support
|
|
144
170
|
|
|
145
|
-
Cataract
|
|
171
|
+
Cataract parses and preserves all standard CSS including:
|
|
146
172
|
- **Selectors**: All CSS2/CSS3 selectors (type, class, ID, attribute, pseudo-classes, pseudo-elements, combinators)
|
|
147
|
-
- **At-rules**:
|
|
173
|
+
- **At-rules**:
|
|
174
|
+
- **`@media`**: Special handling with indexing and filtering API (`with_media(:print)`, `with_media(:all)`)
|
|
175
|
+
- **Others** (`@font-face`, `@keyframes`, `@supports`, `@page`, `@layer`, `@container`, `@property`, `@scope`, `@counter-style`): Parsed and preserved as-is (pass-through)
|
|
148
176
|
- **Media Queries**: Full support including nested queries and media features
|
|
149
177
|
- **Special syntax**: Data URIs, `calc()`, `url()`, CSS functions with parentheses
|
|
150
178
|
- **!important**: Full support with correct cascade behavior
|
|
@@ -153,7 +181,10 @@ Cataract aims to support all CSS specifications including:
|
|
|
153
181
|
|
|
154
182
|
Cataract supports converting colors between multiple CSS color formats with high precision.
|
|
155
183
|
|
|
156
|
-
**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>
|
|
157
188
|
|
|
158
189
|
```ruby
|
|
159
190
|
require 'cataract'
|
|
@@ -213,6 +244,8 @@ sheet.convert_colors!(to: :hex) # Converts all formats to hex
|
|
|
213
244
|
- CSS Color Level 5 features (`none`, `infinity`, relative color syntax with `from`) are preserved but not converted
|
|
214
245
|
- Unknown or future color functions are passed through unchanged
|
|
215
246
|
|
|
247
|
+
</details>
|
|
248
|
+
|
|
216
249
|
### `@import` Support
|
|
217
250
|
|
|
218
251
|
`@import` statements can be resolved with security controls:
|
|
@@ -273,7 +306,7 @@ Each `Rule` is a struct containing:
|
|
|
273
306
|
- `specificity`: Calculated CSS specificity (cached)
|
|
274
307
|
|
|
275
308
|
Implementation details:
|
|
276
|
-
- **C implementation**: Critical paths implemented in C (parsing,
|
|
309
|
+
- **C implementation**: Critical paths implemented in C (parsing, cascade/flatten, serialization)
|
|
277
310
|
- **Flat rule array**: All rules stored in a single array, preserving source order
|
|
278
311
|
- **Efficient media query handling**: O(1) lookup via internal media index
|
|
279
312
|
- **Memory efficient**: Minimal allocations, reuses string buffers where possible
|
data/Rakefile
CHANGED
|
@@ -28,23 +28,58 @@ end
|
|
|
28
28
|
# rake-compiler already adds: tmp/, lib/**/*.{so,bundle}, etc.
|
|
29
29
|
CLEAN.include('ext/**/Makefile', 'ext/**/*.o')
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
namespace :test do
|
|
32
|
+
desc 'Run tests with C extension (default)'
|
|
33
|
+
Rake::TestTask.new(:c) do |t|
|
|
34
|
+
t.libs << 'test'
|
|
35
|
+
t.libs << 'lib'
|
|
36
|
+
t.ruby_opts << '-rtest_helper'
|
|
37
|
+
t.test_files = FileList['test/**/test_*.rb'].exclude('test/css_parser_compat/**/*', 'test/color/**/*')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc 'Run tests with pure Ruby implementation'
|
|
41
|
+
task :pure do
|
|
42
|
+
# Run in subprocess to avoid conflicts with C extension
|
|
43
|
+
success = system({ 'CATARACT_PURE' => '1' }, 'rake', 'test:c')
|
|
44
|
+
abort('Pure Ruby tests failed!') unless success
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc 'Run tests for both C extension and pure Ruby'
|
|
48
|
+
task :all do
|
|
49
|
+
impls = ['pure ruby']
|
|
50
|
+
|
|
51
|
+
unless RUBY_ENGINE == 'jruby'
|
|
52
|
+
puts "\n#{'=' * 80}"
|
|
53
|
+
puts 'Running tests for C EXTENSION'
|
|
54
|
+
puts '=' * 80
|
|
55
|
+
impls << 'C extension'
|
|
56
|
+
|
|
57
|
+
Rake::Task['test:c'].invoke
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
puts "\n#{'=' * 80}"
|
|
61
|
+
puts 'Running tests for PURE RUBY implementation'
|
|
62
|
+
puts '=' * 80
|
|
63
|
+
|
|
64
|
+
Rake::Task['test:pure'].invoke
|
|
65
|
+
|
|
66
|
+
puts "\n#{'=' * 80}"
|
|
67
|
+
puts "✓ All tests passed for #{impls.join(' and ')}"
|
|
68
|
+
puts '=' * 80
|
|
69
|
+
end
|
|
38
70
|
end
|
|
39
71
|
|
|
72
|
+
# Default test task runs both implementations
|
|
73
|
+
desc 'Run tests for both C extension and pure Ruby (default)'
|
|
74
|
+
task test: 'test:all'
|
|
75
|
+
|
|
40
76
|
desc 'Run all benchmarks'
|
|
41
77
|
task :benchmark do
|
|
42
78
|
Rake::Task[:compile].invoke
|
|
43
79
|
Rake::Task['benchmark:parsing'].invoke
|
|
44
80
|
Rake::Task['benchmark:serialization'].invoke
|
|
45
81
|
Rake::Task['benchmark:specificity'].invoke
|
|
46
|
-
Rake::Task['benchmark:
|
|
47
|
-
Rake::Task['benchmark:yjit'].invoke
|
|
82
|
+
Rake::Task['benchmark:flattening'].invoke
|
|
48
83
|
puts "\n#{'-' * 80}"
|
|
49
84
|
puts 'All benchmarks complete!'
|
|
50
85
|
puts 'Generate documentation with: rake benchmark:generate_docs'
|
|
@@ -53,33 +88,27 @@ end
|
|
|
53
88
|
|
|
54
89
|
namespace :benchmark do
|
|
55
90
|
desc 'Benchmark CSS parsing performance'
|
|
56
|
-
task :
|
|
91
|
+
task parsing: :compile do
|
|
57
92
|
puts 'Running parsing benchmark...'
|
|
58
93
|
ruby 'benchmarks/benchmark_parsing.rb'
|
|
59
94
|
end
|
|
60
95
|
|
|
61
96
|
desc 'Benchmark CSS serialization (to_s) performance'
|
|
62
|
-
task :
|
|
97
|
+
task serialization: :compile do
|
|
63
98
|
puts 'Running serialization benchmark...'
|
|
64
99
|
ruby 'benchmarks/benchmark_serialization.rb'
|
|
65
100
|
end
|
|
66
101
|
|
|
67
102
|
desc 'Benchmark specificity calculation performance'
|
|
68
|
-
task :
|
|
103
|
+
task specificity: :compile do
|
|
69
104
|
puts 'Running specificity benchmark...'
|
|
70
105
|
ruby 'benchmarks/benchmark_specificity.rb'
|
|
71
106
|
end
|
|
72
107
|
|
|
73
|
-
desc 'Benchmark CSS
|
|
74
|
-
task :
|
|
75
|
-
puts 'Running
|
|
76
|
-
ruby 'benchmarks/
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
desc 'Benchmark Ruby-side operations with YJIT on vs off'
|
|
80
|
-
task :yjit do
|
|
81
|
-
puts 'Running YJIT benchmark...'
|
|
82
|
-
ruby 'benchmarks/benchmark_yjit.rb'
|
|
108
|
+
desc 'Benchmark CSS flattening performance'
|
|
109
|
+
task flattening: :compile do
|
|
110
|
+
puts 'Running flattening benchmark...'
|
|
111
|
+
ruby 'benchmarks/benchmark_flattening.rb'
|
|
83
112
|
end
|
|
84
113
|
|
|
85
114
|
desc 'Benchmark string allocation optimization (buffer vs dynamic)'
|
|
@@ -166,14 +195,27 @@ task :lint do
|
|
|
166
195
|
end
|
|
167
196
|
|
|
168
197
|
# Fuzz testing
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
175
214
|
end
|
|
176
215
|
|
|
216
|
+
desc 'Run fuzzer with both C extension and pure Ruby'
|
|
217
|
+
task fuzz: ['fuzz:c', 'fuzz:pure']
|
|
218
|
+
|
|
177
219
|
# Documentation generation with YARD
|
|
178
220
|
begin
|
|
179
221
|
require 'yard'
|
|
@@ -189,10 +231,8 @@ begin
|
|
|
189
231
|
|
|
190
232
|
desc 'Generate documentation and open in browser'
|
|
191
233
|
task docs: :generate_example do
|
|
192
|
-
# Generate YARD documentation
|
|
193
|
-
YARD::CLI::Yardoc.run
|
|
194
|
-
'--title', 'Cataract - Fast CSS Parser',
|
|
195
|
-
'lib/**/*.rb', 'ext/**/*.c', '-', 'docs/files/EXAMPLE.md')
|
|
234
|
+
# Generate YARD documentation (uses .yardopts for configuration)
|
|
235
|
+
YARD::CLI::Yardoc.run
|
|
196
236
|
|
|
197
237
|
# Open in browser (skip in CI)
|
|
198
238
|
unless ENV['CI']
|
data/cataract.gemspec
CHANGED
|
@@ -21,7 +21,10 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
|
|
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
|
-
`git ls-files -z`.split("\x0").reject
|
|
24
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
25
|
+
f.match(%r{^(test|spec|features|benchmarks|scripts|\.rubocop)/}) ||
|
|
26
|
+
f.match(/^(test|benchmark)_.*\.rb$/)
|
|
27
|
+
end
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
spec.bindir = 'exe'
|
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;
|
|
@@ -254,7 +255,7 @@ static void serialize_at_rule_formatted(VALUE result, VALUE at_rule, const char
|
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
// Helper to serialize a single rule with formatting (indented, multi-line)
|
|
257
|
-
static void serialize_rule_formatted(VALUE result, VALUE rule, const char *indent) {
|
|
258
|
+
static void serialize_rule_formatted(VALUE result, VALUE rule, const char *indent, int is_last) {
|
|
258
259
|
// Check if this is an AtRule
|
|
259
260
|
if (rb_obj_is_kind_of(rule, cAtRule)) {
|
|
260
261
|
serialize_at_rule_formatted(result, rule, indent);
|
|
@@ -277,9 +278,13 @@ static void serialize_rule_formatted(VALUE result, VALUE rule, const char *inden
|
|
|
277
278
|
serialize_declarations_formatted(result, declarations, decl_indent_ptr);
|
|
278
279
|
RB_GC_GUARD(decl_indent);
|
|
279
280
|
|
|
280
|
-
// Closing brace
|
|
281
|
+
// Closing brace - double newline for all except last rule
|
|
281
282
|
rb_str_cat2(result, indent);
|
|
282
|
-
|
|
283
|
+
if (is_last) {
|
|
284
|
+
rb_str_cat2(result, "}\n");
|
|
285
|
+
} else {
|
|
286
|
+
rb_str_cat2(result, "}\n\n");
|
|
287
|
+
}
|
|
283
288
|
}
|
|
284
289
|
|
|
285
290
|
// Context for building rule_to_media map
|
|
@@ -680,6 +685,7 @@ static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_i
|
|
|
680
685
|
VALUE rule = rb_ary_entry(rules_array, i);
|
|
681
686
|
VALUE rule_id = rb_struct_aref(rule, INT2FIX(RULE_ID));
|
|
682
687
|
VALUE rule_media = rb_hash_aref(rule_to_media, rule_id);
|
|
688
|
+
int is_first_rule = (i == 0);
|
|
683
689
|
|
|
684
690
|
if (NIL_P(rule_media)) {
|
|
685
691
|
// Not in any media query - close any open media block first
|
|
@@ -689,19 +695,24 @@ static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_i
|
|
|
689
695
|
current_media = Qnil;
|
|
690
696
|
}
|
|
691
697
|
|
|
692
|
-
//
|
|
693
|
-
|
|
698
|
+
// Add blank line prefix for non-first rules
|
|
699
|
+
if (!is_first_rule) {
|
|
700
|
+
rb_str_cat2(result, "\n");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Output rule with no indentation (always single newline suffix)
|
|
704
|
+
serialize_rule_formatted(result, rule, "", 1);
|
|
694
705
|
} else {
|
|
695
706
|
// This rule is in a media query
|
|
696
707
|
if (NIL_P(current_media) || !rb_equal(current_media, rule_media)) {
|
|
697
708
|
// Close previous media block if open
|
|
698
709
|
if (in_media_block) {
|
|
699
710
|
rb_str_cat2(result, "}\n");
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Add blank line prefix for non-first rules
|
|
714
|
+
if (!is_first_rule) {
|
|
715
|
+
rb_str_cat2(result, "\n");
|
|
705
716
|
}
|
|
706
717
|
|
|
707
718
|
// Open new media block
|
|
@@ -713,7 +724,8 @@ static VALUE stylesheet_to_formatted_s_original(VALUE rules_array, VALUE media_i
|
|
|
713
724
|
}
|
|
714
725
|
|
|
715
726
|
// Serialize rule inside media block with 2-space indentation
|
|
716
|
-
|
|
727
|
+
// Rules inside media blocks always get single newline (is_last=1)
|
|
728
|
+
serialize_rule_formatted(result, rule, " ", 1);
|
|
717
729
|
}
|
|
718
730
|
}
|
|
719
731
|
|
|
@@ -1013,42 +1025,31 @@ void Init_native_extension(void) {
|
|
|
1013
1025
|
eSizeError = rb_define_class_under(mCataract, "SizeError", eCataractError);
|
|
1014
1026
|
}
|
|
1015
1027
|
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
"Rule"
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
mCataract,
|
|
1032
|
-
|
|
1033
|
-
"
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
// - For @keyframes: content is Array of Rule (keyframe blocks)
|
|
1042
|
-
// - For @font-face: content is Array of Declaration
|
|
1043
|
-
cAtRule = rb_struct_define_under(
|
|
1044
|
-
mCataract,
|
|
1045
|
-
"AtRule",
|
|
1046
|
-
"id", // Integer (0-indexed position in @rules array)
|
|
1047
|
-
"selector", // String (e.g., "@keyframes fade", "@font-face")
|
|
1048
|
-
"content", // Array of Rule or Declaration
|
|
1049
|
-
"specificity", // Always nil for at-rules
|
|
1050
|
-
NULL
|
|
1051
|
-
);
|
|
1028
|
+
// Reuse Ruby-defined structs (they must be defined before loading this extension)
|
|
1029
|
+
// If they don't exist, someone required the extension directly instead of via lib/cataract.rb
|
|
1030
|
+
if (rb_const_defined(mCataract, rb_intern("Rule"))) {
|
|
1031
|
+
cRule = rb_const_get(mCataract, rb_intern("Rule"));
|
|
1032
|
+
} else {
|
|
1033
|
+
rb_raise(rb_eLoadError, "Cataract::Rule not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (rb_const_defined(mCataract, rb_intern("Declaration"))) {
|
|
1037
|
+
cDeclaration = rb_const_get(mCataract, rb_intern("Declaration"));
|
|
1038
|
+
} else {
|
|
1039
|
+
rb_raise(rb_eLoadError, "Cataract::Declaration not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (rb_const_defined(mCataract, rb_intern("AtRule"))) {
|
|
1043
|
+
cAtRule = rb_const_get(mCataract, rb_intern("AtRule"));
|
|
1044
|
+
} else {
|
|
1045
|
+
rb_raise(rb_eLoadError, "Cataract::AtRule not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (rb_const_defined(mCataract, rb_intern("ImportStatement"))) {
|
|
1049
|
+
cImportStatement = rb_const_get(mCataract, rb_intern("ImportStatement"));
|
|
1050
|
+
} else {
|
|
1051
|
+
rb_raise(rb_eLoadError, "Cataract::ImportStatement not defined. Do not require 'cataract/native_extension' directly, use require 'cataract'");
|
|
1052
|
+
}
|
|
1052
1053
|
|
|
1053
1054
|
// Define Declarations class and add to_s method
|
|
1054
1055
|
VALUE cDeclarations = rb_define_class_under(mCataract, "Declarations", rb_cObject);
|
|
@@ -1063,12 +1064,14 @@ void Init_native_extension(void) {
|
|
|
1063
1064
|
rb_define_module_function(mCataract, "_stylesheet_to_formatted_s", stylesheet_to_formatted_s_new, 4);
|
|
1064
1065
|
rb_define_module_function(mCataract, "parse_media_types", parse_media_types, 1);
|
|
1065
1066
|
rb_define_module_function(mCataract, "parse_declarations", new_parse_declarations, 1);
|
|
1066
|
-
rb_define_module_function(mCataract, "
|
|
1067
|
+
rb_define_module_function(mCataract, "flatten", cataract_flatten, 1);
|
|
1068
|
+
rb_define_module_function(mCataract, "merge", cataract_flatten, 1); // Deprecated alias for backwards compatibility
|
|
1067
1069
|
rb_define_module_function(mCataract, "extract_imports", extract_imports, 1);
|
|
1068
1070
|
rb_define_module_function(mCataract, "calculate_specificity", calculate_specificity, 1);
|
|
1071
|
+
rb_define_module_function(mCataract, "_expand_shorthand", cataract_expand_shorthand, 1);
|
|
1069
1072
|
|
|
1070
|
-
// Initialize
|
|
1071
|
-
|
|
1073
|
+
// Initialize flatten constants (cached property strings)
|
|
1074
|
+
init_flatten_constants();
|
|
1072
1075
|
|
|
1073
1076
|
// Export compile-time flags as a hash for runtime introspection
|
|
1074
1077
|
VALUE compile_flags = rb_hash_new();
|
|
@@ -1086,4 +1089,10 @@ void Init_native_extension(void) {
|
|
|
1086
1089
|
#endif
|
|
1087
1090
|
|
|
1088
1091
|
rb_define_const(mCataract, "COMPILE_FLAGS", compile_flags);
|
|
1092
|
+
|
|
1093
|
+
// Flag to indicate native extension is loaded (for pure Ruby fallback detection)
|
|
1094
|
+
rb_define_const(mCataract, "NATIVE_EXTENSION_LOADED", Qtrue);
|
|
1095
|
+
|
|
1096
|
+
// Implementation type constant
|
|
1097
|
+
rb_define_const(mCataract, "IMPLEMENTATION", ID2SYM(rb_intern("native")));
|
|
1089
1098
|
}
|
data/ext/cataract/cataract.h
CHANGED
|
@@ -12,6 +12,7 @@ extern VALUE cRule;
|
|
|
12
12
|
extern VALUE cDeclaration;
|
|
13
13
|
extern VALUE cAtRule;
|
|
14
14
|
extern VALUE cStylesheet;
|
|
15
|
+
extern VALUE cImportStatement;
|
|
15
16
|
|
|
16
17
|
// Error class references
|
|
17
18
|
extern VALUE eCataractError;
|
|
@@ -140,9 +141,9 @@ VALUE parse_css_new(VALUE self, VALUE css_string);
|
|
|
140
141
|
VALUE parse_css_new_impl(VALUE css_string, int rule_id_offset);
|
|
141
142
|
VALUE parse_media_types(VALUE self, VALUE media_query_sym);
|
|
142
143
|
|
|
143
|
-
//
|
|
144
|
-
VALUE
|
|
145
|
-
void
|
|
144
|
+
// Flatten (flatten.c)
|
|
145
|
+
VALUE cataract_flatten(VALUE self, VALUE rules_array);
|
|
146
|
+
void init_flatten_constants(void);
|
|
146
147
|
|
|
147
148
|
// Specificity (specificity.c)
|
|
148
149
|
VALUE calculate_specificity(VALUE self, VALUE selector);
|
|
@@ -162,6 +163,7 @@ VALUE cataract_expand_border_side(VALUE self, VALUE side, VALUE value);
|
|
|
162
163
|
VALUE cataract_expand_font(VALUE self, VALUE value);
|
|
163
164
|
VALUE cataract_expand_list_style(VALUE self, VALUE value);
|
|
164
165
|
VALUE cataract_expand_background(VALUE self, VALUE value);
|
|
166
|
+
VALUE cataract_expand_shorthand(VALUE self, VALUE decl);
|
|
165
167
|
VALUE cataract_create_margin_shorthand(VALUE self, VALUE properties);
|
|
166
168
|
VALUE cataract_create_padding_shorthand(VALUE self, VALUE properties);
|
|
167
169
|
VALUE cataract_create_border_width_shorthand(VALUE self, VALUE properties);
|