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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cataract
4
- VERSION = '0.2.5'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/cataract.rb CHANGED
@@ -1,34 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'cataract/version'
4
- require_relative 'cataract/error'
5
- require_relative 'cataract/constants'
6
-
7
- # Load struct definitions first (before C extension or pure Ruby)
8
- require_relative 'cataract/declaration'
9
- require_relative 'cataract/rule'
10
- require_relative 'cataract/at_rule'
11
- require_relative 'cataract/media_query'
12
- require_relative 'cataract/import_statement'
13
-
14
- # Load pure Ruby or C extension based on ENV var
15
- if %w[1 true].include?(ENV.fetch('CATARACT_PURE', nil)) || RUBY_ENGINE == 'jruby'
16
- require_relative 'cataract/pure'
17
- else
18
- require_relative 'cataract/native_extension'
19
- end
20
-
21
- # Load supporting Ruby files (used by both implementations)
22
- require_relative 'cataract/stylesheet_scope'
23
- require_relative 'cataract/stylesheet'
24
- require_relative 'cataract/declarations'
25
- require_relative 'cataract/import_resolver'
26
-
27
3
  # Cataract is a high-performance CSS parser written in C with a Ruby interface.
28
4
  #
29
5
  # It provides fast CSS parsing, rule querying, cascade merging, and serialization.
30
6
  # Designed for performance-critical applications that need to process large amounts of CSS.
31
7
  #
8
+ # Pure Ruby and native each live entirely under their own Backends::Pure /
9
+ # Backends::Native namespace (no shared method/constant is ever defined by
10
+ # a backend directly on Cataract or on a shared value type), so both can be
11
+ # required in the same process - e.g. to compare their output directly.
12
+ # Backends.active is the one this process picked by default (below); it's
13
+ # what shared value types (Stylesheet, Declarations, Rule) fall back to
14
+ # when they aren't otherwise told which backend produced them.
15
+ #
32
16
  # @example Basic usage
33
17
  # require 'cataract'
34
18
  #
@@ -44,6 +28,33 @@ require_relative 'cataract/import_resolver'
44
28
  # @see Stylesheet Main class for working with parsed CSS
45
29
  # @see Rule Represents individual CSS rules
46
30
  module Cataract
31
+ module Backends
32
+ class << self
33
+ attr_accessor :active
34
+ end
35
+ end
36
+ end
37
+
38
+ if %w[1 true].include?(ENV.fetch('CATARACT_PURE', nil)) || RUBY_ENGINE == 'jruby'
39
+ require_relative 'cataract/pure'
40
+ Cataract::Backends.active = Cataract::Backends::Pure
41
+ else
42
+ require_relative 'cataract/native'
43
+ Cataract::Backends.active = Cataract::Backends::Native
44
+ end
45
+
46
+ module Cataract
47
+ # Mirror the active backend's identity constants at the top level.
48
+ #
49
+ # Native is a Module (IMPLEMENTATION/COMPILE_FLAGS live on it directly);
50
+ # Pure is a frozen instance (they live on its class instead), so reach
51
+ # through .class only when active isn't already a Module itself.
52
+ backend_const_holder = Backends.active.is_a?(Module) ? Backends.active : Backends.active.class
53
+ IMPLEMENTATION = backend_const_holder::IMPLEMENTATION
54
+ COMPILE_FLAGS = backend_const_holder::COMPILE_FLAGS
55
+ NATIVE_EXTENSION_LOADED = true if defined?(Backends::Native) && Backends.active == Backends::Native
56
+ PURE_RUBY_LOADED = true if defined?(Backends::Pure) && Backends.active == Backends::Pure
57
+
47
58
  class << self
48
59
  # Parse a CSS string into a Stylesheet object.
49
60
  #
@@ -75,10 +86,8 @@ module Cataract
75
86
  #
76
87
  # @see Stylesheet#parse
77
88
  # @see Stylesheet.parse
78
- unless method_defined?(:parse_css)
79
- def parse_css(css, **options)
80
- Stylesheet.parse(css, **options)
81
- end
89
+ def parse_css(css, **options)
90
+ Stylesheet.parse(css, **options)
82
91
  end
83
92
 
84
93
  # Flatten CSS rules according to CSS cascade rules.
@@ -113,9 +122,11 @@ module Cataract
113
122
  #
114
123
  # @note This is a module-level convenience method. The same functionality is available
115
124
  # as an instance method: `stylesheet.flatten`
116
- # @note Implemented in C (see ext/cataract/flatten.c)
117
125
  #
118
126
  # @see Stylesheet#flatten
119
- # Cataract.flatten is defined in C via rb_define_module_function
127
+ def flatten(stylesheet_or_css)
128
+ stylesheet_or_css = Stylesheet.parse(stylesheet_or_css) if stylesheet_or_css.is_a?(String)
129
+ stylesheet_or_css.flatten
130
+ end
120
131
  end
121
132
  end
@@ -35,7 +35,7 @@ namespace :profile do
35
35
  puts "CSS size: #{css_content.bytesize} bytes"
36
36
  puts
37
37
 
38
- require_relative '../../lib/cataract/pure'
38
+ require_relative '../../lib/cataract'
39
39
  require 'json'
40
40
 
41
41
  # Use higher sampling rate (interval in microseconds, default is 1000)
@@ -74,6 +74,9 @@ namespace :profile do
74
74
  abort('stackprof gem not found. Install with: gem install stackprof')
75
75
  end
76
76
 
77
+ # Ensure we're using pure Ruby implementation
78
+ ENV['CATARACT_PURE'] = '1'
79
+
77
80
  fixture_path = File.expand_path('../../test/fixtures/bootstrap.css', __dir__)
78
81
  unless File.exist?(fixture_path)
79
82
  abort("Fixture not found: #{fixture_path}")
@@ -94,7 +97,7 @@ namespace :profile do
94
97
  puts "CSS size: #{css_content.bytesize} bytes"
95
98
  puts
96
99
 
97
- require_relative '../../lib/cataract/pure'
100
+ require_relative '../../lib/cataract'
98
101
  require 'json'
99
102
 
100
103
  # Parse once outside profiling to get stylesheet
@@ -162,7 +165,7 @@ namespace :profile do
162
165
  puts "CSS size: #{css_content.bytesize} bytes"
163
166
  puts
164
167
 
165
- require_relative '../../lib/cataract/pure'
168
+ require_relative '../../lib/cataract'
166
169
  require 'json'
167
170
 
168
171
  # Parse once outside profiling to get stylesheet
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cataract
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Cook
@@ -56,7 +56,6 @@ files:
56
56
  - ext/cataract/css_parser.c
57
57
  - ext/cataract/extconf.rb
58
58
  - ext/cataract/flatten.c
59
- - ext/cataract/import_scanner.c
60
59
  - ext/cataract/shorthand_expander.c
61
60
  - ext/cataract/specificity.c
62
61
  - ext/cataract/value_splitter.c
@@ -67,16 +66,6 @@ files:
67
66
  - ext/cataract_color/color_conversion_named.c
68
67
  - ext/cataract_color/color_conversion_oklab.c
69
68
  - ext/cataract_color/extconf.rb
70
- - ext/cataract_old/cataract.c
71
- - ext/cataract_old/cataract.h
72
- - ext/cataract_old/css_parser.c
73
- - ext/cataract_old/extconf.rb
74
- - ext/cataract_old/import_scanner.c
75
- - ext/cataract_old/merge.c
76
- - ext/cataract_old/shorthand_expander.c
77
- - ext/cataract_old/specificity.c
78
- - ext/cataract_old/stylesheet.c
79
- - ext/cataract_old/value_splitter.c
80
69
  - lib/cataract.rb
81
70
  - lib/cataract/at_rule.rb
82
71
  - lib/cataract/color_conversion.rb
@@ -87,10 +76,11 @@ files:
87
76
  - lib/cataract/import_resolver.rb
88
77
  - lib/cataract/import_statement.rb
89
78
  - lib/cataract/media_query.rb
79
+ - lib/cataract/native.rb
90
80
  - lib/cataract/pure.rb
91
81
  - lib/cataract/pure/byte_constants.rb
82
+ - lib/cataract/pure/declarations.rb
92
83
  - lib/cataract/pure/flatten.rb
93
- - lib/cataract/pure/helpers.rb
94
84
  - lib/cataract/pure/parser.rb
95
85
  - lib/cataract/pure/serializer.rb
96
86
  - lib/cataract/pure/specificity.rb
@@ -122,7 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
122
112
  - !ruby/object:Gem::Version
123
113
  version: '0'
124
114
  requirements: []
125
- rubygems_version: 3.6.9
115
+ rubygems_version: 4.0.10
126
116
  specification_version: 4
127
117
  summary: High-performance CSS parser with C extensions
128
118
  test_files: []
@@ -1,174 +0,0 @@
1
- #include <ruby.h>
2
- #include <ctype.h>
3
- #include <string.h>
4
- #include "cataract.h"
5
-
6
- /*
7
- * Scan CSS for @import statements
8
- *
9
- * Matches patterns:
10
- * @import url("path");
11
- * @import url('path');
12
- * @import "path";
13
- * @import 'path';
14
- * @import url("path") print; (with media query)
15
- *
16
- * Returns array of hashes: [{url: "...", media: "...", full_match: "..."}]
17
- */
18
- VALUE extract_imports(VALUE self, VALUE css_string) {
19
- Check_Type(css_string, T_STRING);
20
-
21
- const char *css = RSTRING_PTR(css_string);
22
- long css_len = RSTRING_LEN(css_string);
23
-
24
- VALUE imports = rb_ary_new();
25
-
26
- const char *p = css;
27
- const char *end = css + css_len;
28
-
29
- while (p < end) {
30
- // Skip whitespace and comments
31
- while (p < end) {
32
- if (IS_WHITESPACE(*p)) {
33
- p++;
34
- } else if (p + 2 <= end && p[0] == '/' && p[1] == '*') {
35
- // Skip /* */ comment
36
- p += 2;
37
- while (p + 1 < end && !(p[0] == '*' && p[1] == '/')) {
38
- p++;
39
- }
40
- if (p + 1 < end) p += 2; // Skip */
41
- } else {
42
- break;
43
- }
44
- }
45
-
46
- // Check for @import
47
- if (p + 7 <= end && strncasecmp(p, "@import", 7) == 0) {
48
- const char *import_start = p;
49
- p += 7;
50
-
51
- // Skip whitespace after @import
52
- while (p < end && IS_WHITESPACE(*p)) p++;
53
-
54
- // Check for optional url(
55
- int has_url_function = 0;
56
- if (p + 4 <= end && strncasecmp(p, "url(", 4) == 0) {
57
- has_url_function = 1;
58
- p += 4;
59
- while (p < end && IS_WHITESPACE(*p)) p++;
60
- }
61
-
62
- // Find opening quote
63
- if (p >= end || (*p != '"' && *p != '\'')) {
64
- // Invalid @import, skip to next semicolon
65
- while (p < end && *p != ';') p++;
66
- if (p < end) p++; // Skip semicolon
67
- continue;
68
- }
69
-
70
- char quote_char = *p;
71
- p++; // Skip opening quote
72
-
73
- const char *url_start = p;
74
-
75
- // Find closing quote (handle escaped quotes)
76
- while (p < end && *p != quote_char) {
77
- if (*p == '\\' && p + 1 < end) {
78
- p += 2; // Skip escaped character
79
- } else {
80
- p++;
81
- }
82
- }
83
-
84
- if (p >= end) {
85
- // Unterminated string
86
- break;
87
- }
88
-
89
- const char *url_end = p;
90
- p++; // Skip closing quote
91
-
92
- // Skip closing paren if we had url(
93
- if (has_url_function) {
94
- while (p < end && IS_WHITESPACE(*p)) p++;
95
- if (p < end && *p == ')') {
96
- p++;
97
- }
98
- }
99
-
100
- // Skip whitespace before optional media query or semicolon
101
- while (p < end && IS_WHITESPACE(*p)) p++;
102
-
103
- // Check for optional media query (everything until semicolon)
104
- const char *media_start = NULL;
105
- const char *media_end = NULL;
106
-
107
- if (p < end && *p != ';') {
108
- media_start = p;
109
-
110
- // Find semicolon
111
- while (p < end && *p != ';') p++;
112
-
113
- media_end = p;
114
-
115
- // Trim trailing whitespace from media query
116
- while (media_end > media_start && IS_WHITESPACE(*(media_end - 1))) {
117
- media_end--;
118
- }
119
- }
120
-
121
- // Skip semicolon
122
- if (p < end && *p == ';') p++;
123
-
124
- const char *import_end = p;
125
-
126
- // Build result hash
127
- VALUE import_hash = rb_hash_new();
128
-
129
- // Extract URL
130
- VALUE url = rb_str_new(url_start, url_end - url_start);
131
- rb_hash_aset(import_hash, ID2SYM(rb_intern("url")), url);
132
-
133
- // Extract media query (or nil)
134
- VALUE media = Qnil;
135
- if (media_start && media_end > media_start) {
136
- media = rb_str_new(media_start, media_end - media_start);
137
- }
138
- rb_hash_aset(import_hash, ID2SYM(rb_intern("media")), media);
139
-
140
- // Extract full match
141
- VALUE full_match = rb_str_new(import_start, import_end - import_start);
142
- rb_hash_aset(import_hash, ID2SYM(rb_intern("full_match")), full_match);
143
-
144
- rb_ary_push(imports, import_hash);
145
-
146
- RB_GC_GUARD(url);
147
- RB_GC_GUARD(media);
148
- RB_GC_GUARD(full_match);
149
- RB_GC_GUARD(import_hash);
150
- } else {
151
- // Not an @import, skip to next line or rule
152
- // Once we hit a non-@import rule (except @charset), stop looking
153
- // Per CSS spec, @import must be at the top
154
-
155
- // Skip @charset if present
156
- if (p + 8 <= end && strncasecmp(p, "@charset", 8) == 0) {
157
- // Skip to semicolon
158
- while (p < end && *p != ';') p++;
159
- if (p < end) p++; // Skip semicolon
160
- continue;
161
- }
162
-
163
- // If we hit any other content, stop scanning for imports
164
- if (p < end && !IS_WHITESPACE(*p)) {
165
- break;
166
- }
167
-
168
- p++;
169
- }
170
- }
171
-
172
- RB_GC_GUARD(imports);
173
- return imports;
174
- }