cataract 0.1.2 → 0.1.4
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 +27 -0
- data/.overcommit.yml +1 -1
- data/.rubocop.yml +62 -0
- data/.rubocop_todo.yml +186 -0
- data/BENCHMARKS.md +60 -139
- data/CHANGELOG.md +14 -0
- data/README.md +30 -2
- data/Rakefile +49 -22
- data/cataract.gemspec +4 -1
- data/ext/cataract/cataract.c +47 -47
- data/ext/cataract/css_parser.c +17 -33
- data/ext/cataract/merge.c +58 -2
- data/lib/cataract/at_rule.rb +8 -9
- data/lib/cataract/declaration.rb +18 -0
- data/lib/cataract/import_resolver.rb +3 -4
- data/lib/cataract/pure/byte_constants.rb +69 -0
- data/lib/cataract/pure/helpers.rb +35 -0
- data/lib/cataract/pure/imports.rb +255 -0
- data/lib/cataract/pure/merge.rb +1146 -0
- data/lib/cataract/pure/parser.rb +1236 -0
- data/lib/cataract/pure/serializer.rb +590 -0
- data/lib/cataract/pure/specificity.rb +206 -0
- data/lib/cataract/pure.rb +130 -0
- data/lib/cataract/rule.rb +22 -13
- data/lib/cataract/stylesheet.rb +14 -9
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +18 -5
- metadata +12 -25
- 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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pure Ruby CSS parser - Specificity calculation
|
|
4
|
+
# NO REGEXP ALLOWED - char-by-char parsing only
|
|
5
|
+
|
|
6
|
+
module Cataract
|
|
7
|
+
# Calculate CSS specificity for a selector
|
|
8
|
+
#
|
|
9
|
+
# @param selector [String] CSS selector
|
|
10
|
+
# @return [Integer] Specificity value
|
|
11
|
+
#
|
|
12
|
+
# Specificity calculation (per CSS spec):
|
|
13
|
+
# - Count IDs (#id) - each worth 100
|
|
14
|
+
# - Count classes/attributes/pseudo-classes (.class, [attr], :pseudo) - each worth 10
|
|
15
|
+
# - Count elements/pseudo-elements (div, ::before) - each worth 1
|
|
16
|
+
def self.calculate_specificity(selector)
|
|
17
|
+
return 0 if selector.nil? || selector.empty?
|
|
18
|
+
|
|
19
|
+
# Counters for specificity components
|
|
20
|
+
id_count = 0
|
|
21
|
+
class_count = 0
|
|
22
|
+
attr_count = 0
|
|
23
|
+
pseudo_class_count = 0
|
|
24
|
+
pseudo_element_count = 0
|
|
25
|
+
element_count = 0
|
|
26
|
+
|
|
27
|
+
i = 0
|
|
28
|
+
len = selector.length
|
|
29
|
+
|
|
30
|
+
pseudo_element_kwords = %w[before after first-line first-letter selection]
|
|
31
|
+
|
|
32
|
+
while i < len
|
|
33
|
+
byte = selector.getbyte(i)
|
|
34
|
+
|
|
35
|
+
# Skip whitespace and combinators
|
|
36
|
+
if byte == BYTE_SPACE || byte == BYTE_TAB || byte == BYTE_NEWLINE || byte == BYTE_CR ||
|
|
37
|
+
byte == BYTE_GT || byte == BYTE_PLUS || byte == BYTE_TILDE || byte == BYTE_COMMA
|
|
38
|
+
i += 1
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# ID selector: #id
|
|
43
|
+
if byte == BYTE_HASH
|
|
44
|
+
id_count += 1
|
|
45
|
+
i += 1
|
|
46
|
+
# Skip the identifier
|
|
47
|
+
while i < len && ident_char?(selector.getbyte(i))
|
|
48
|
+
i += 1
|
|
49
|
+
end
|
|
50
|
+
next
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Class selector: .class
|
|
54
|
+
if byte == BYTE_DOT
|
|
55
|
+
class_count += 1
|
|
56
|
+
i += 1
|
|
57
|
+
# Skip the identifier
|
|
58
|
+
while i < len && ident_char?(selector.getbyte(i))
|
|
59
|
+
i += 1
|
|
60
|
+
end
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Attribute selector: [attr]
|
|
65
|
+
if byte == BYTE_LBRACKET
|
|
66
|
+
attr_count += 1
|
|
67
|
+
i += 1
|
|
68
|
+
# Skip to closing bracket
|
|
69
|
+
bracket_depth = 1
|
|
70
|
+
while i < len && bracket_depth > 0
|
|
71
|
+
b = selector.getbyte(i)
|
|
72
|
+
if b == BYTE_LBRACKET
|
|
73
|
+
bracket_depth += 1
|
|
74
|
+
elsif b == BYTE_RBRACKET
|
|
75
|
+
bracket_depth -= 1
|
|
76
|
+
end
|
|
77
|
+
i += 1
|
|
78
|
+
end
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Pseudo-element (::) or pseudo-class (:)
|
|
83
|
+
if byte == BYTE_COLON
|
|
84
|
+
i += 1
|
|
85
|
+
is_pseudo_element = false
|
|
86
|
+
|
|
87
|
+
# Check for double colon (::)
|
|
88
|
+
if i < len && selector.getbyte(i) == BYTE_COLON
|
|
89
|
+
is_pseudo_element = true
|
|
90
|
+
i += 1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Extract pseudo name
|
|
94
|
+
pseudo_start = i
|
|
95
|
+
while i < len && ident_char?(selector.getbyte(i))
|
|
96
|
+
i += 1
|
|
97
|
+
end
|
|
98
|
+
pseudo_name = selector[pseudo_start...i]
|
|
99
|
+
|
|
100
|
+
# Check for legacy pseudo-elements (single colon but should be double)
|
|
101
|
+
is_legacy_pseudo_element = false
|
|
102
|
+
if !is_pseudo_element && !pseudo_name.empty?
|
|
103
|
+
is_legacy_pseudo_element = pseudo_element_kwords.include?(pseudo_name)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check for :not() - it doesn't count itself, but its content does
|
|
107
|
+
is_not = (pseudo_name == 'not')
|
|
108
|
+
|
|
109
|
+
# Skip function arguments if present
|
|
110
|
+
if i < len && selector.getbyte(i) == BYTE_LPAREN
|
|
111
|
+
i += 1
|
|
112
|
+
paren_depth = 1
|
|
113
|
+
|
|
114
|
+
# If it's :not(), calculate specificity of the content
|
|
115
|
+
if is_not
|
|
116
|
+
not_content_start = i
|
|
117
|
+
|
|
118
|
+
# Find closing paren
|
|
119
|
+
while i < len && paren_depth > 0
|
|
120
|
+
b = selector.getbyte(i)
|
|
121
|
+
if b == BYTE_LPAREN
|
|
122
|
+
paren_depth += 1
|
|
123
|
+
elsif b == BYTE_RPAREN
|
|
124
|
+
paren_depth -= 1
|
|
125
|
+
end
|
|
126
|
+
i += 1 if paren_depth > 0
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
not_content = selector[not_content_start...i]
|
|
130
|
+
|
|
131
|
+
# Recursively calculate specificity of :not() content
|
|
132
|
+
unless not_content.empty?
|
|
133
|
+
not_specificity = calculate_specificity(not_content)
|
|
134
|
+
|
|
135
|
+
# Add :not() content's specificity to our counts
|
|
136
|
+
additional_a = not_specificity / 100
|
|
137
|
+
additional_b = (not_specificity % 100) / 10
|
|
138
|
+
additional_c = not_specificity % 10
|
|
139
|
+
|
|
140
|
+
id_count += additional_a
|
|
141
|
+
class_count += additional_b
|
|
142
|
+
element_count += additional_c
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
i += 1 # Skip closing paren
|
|
146
|
+
else
|
|
147
|
+
# Skip other function arguments
|
|
148
|
+
while i < len && paren_depth > 0
|
|
149
|
+
b = selector.getbyte(i)
|
|
150
|
+
if b == BYTE_LPAREN
|
|
151
|
+
paren_depth += 1
|
|
152
|
+
elsif b == BYTE_RPAREN
|
|
153
|
+
paren_depth -= 1
|
|
154
|
+
end
|
|
155
|
+
i += 1
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Count the pseudo-class/element
|
|
159
|
+
if is_pseudo_element || is_legacy_pseudo_element
|
|
160
|
+
pseudo_element_count += 1
|
|
161
|
+
else
|
|
162
|
+
pseudo_class_count += 1
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
# No function arguments - count the pseudo-class/element
|
|
167
|
+
if is_not
|
|
168
|
+
# :not without parens is invalid, but don't count it
|
|
169
|
+
elsif is_pseudo_element || is_legacy_pseudo_element
|
|
170
|
+
pseudo_element_count += 1
|
|
171
|
+
else
|
|
172
|
+
pseudo_class_count += 1
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
next
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Universal selector: *
|
|
179
|
+
if byte == BYTE_ASTERISK
|
|
180
|
+
# Universal selector has specificity 0, don't count
|
|
181
|
+
i += 1
|
|
182
|
+
next
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Type selector (element name): div, span, etc.
|
|
186
|
+
if letter?(byte)
|
|
187
|
+
element_count += 1
|
|
188
|
+
# Skip the identifier
|
|
189
|
+
while i < len && ident_char?(selector.getbyte(i))
|
|
190
|
+
i += 1
|
|
191
|
+
end
|
|
192
|
+
next
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Unknown character, skip it
|
|
196
|
+
i += 1
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Calculate specificity using W3C formula
|
|
200
|
+
specificity = (id_count * 100) +
|
|
201
|
+
((class_count + attr_count + pseudo_class_count) * 10) +
|
|
202
|
+
((element_count + pseudo_element_count) * 1)
|
|
203
|
+
|
|
204
|
+
specificity
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Pure Ruby implementation of Cataract CSS parser
|
|
4
|
+
#
|
|
5
|
+
# This is a character-by-character parser that closely mirrors the C implementation.
|
|
6
|
+
# ==================================================================
|
|
7
|
+
# NO REGEXP ALLOWED - consume chars one at a time like the C version.
|
|
8
|
+
# ==================================================================
|
|
9
|
+
#
|
|
10
|
+
# Load this instead of the C extension with:
|
|
11
|
+
# require 'cataract/pure'
|
|
12
|
+
#
|
|
13
|
+
# Or run tests with:
|
|
14
|
+
# CATARACT_PURE=1 rake test
|
|
15
|
+
|
|
16
|
+
# Check if C extension is already loaded
|
|
17
|
+
if defined?(Cataract::NATIVE_EXTENSION_LOADED)
|
|
18
|
+
raise LoadError, 'Cataract C extension is already loaded. Cannot load pure Ruby version.'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Define base module and error classes first
|
|
22
|
+
module Cataract
|
|
23
|
+
class Error < StandardError; end
|
|
24
|
+
class DepthError < Error; end
|
|
25
|
+
class SizeError < Error; end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require_relative 'version'
|
|
29
|
+
|
|
30
|
+
# Load struct definitions and supporting files
|
|
31
|
+
# (These are also loaded by lib/cataract.rb, but we need them here for direct require)
|
|
32
|
+
require_relative 'declaration'
|
|
33
|
+
require_relative 'rule'
|
|
34
|
+
require_relative 'at_rule'
|
|
35
|
+
require_relative 'stylesheet_scope'
|
|
36
|
+
require_relative 'stylesheet'
|
|
37
|
+
require_relative 'declarations'
|
|
38
|
+
require_relative 'import_resolver'
|
|
39
|
+
|
|
40
|
+
# Add to_s method to Declarations class for pure Ruby mode
|
|
41
|
+
module Cataract
|
|
42
|
+
class Declarations
|
|
43
|
+
# Serialize declarations to CSS string
|
|
44
|
+
def to_s
|
|
45
|
+
result = String.new
|
|
46
|
+
@values.each_with_index do |decl, i|
|
|
47
|
+
result << decl.property
|
|
48
|
+
result << ': '
|
|
49
|
+
result << decl.value
|
|
50
|
+
result << ' !important' if decl.important
|
|
51
|
+
result << ';'
|
|
52
|
+
result << ' ' if i < @values.length - 1 # Add space after semicolon except for last
|
|
53
|
+
end
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Load pure Ruby implementation modules
|
|
60
|
+
require_relative 'pure/byte_constants'
|
|
61
|
+
require_relative 'pure/helpers'
|
|
62
|
+
require_relative 'pure/specificity'
|
|
63
|
+
require_relative 'pure/imports'
|
|
64
|
+
require_relative 'pure/serializer'
|
|
65
|
+
require_relative 'pure/parser'
|
|
66
|
+
require_relative 'pure/merge'
|
|
67
|
+
|
|
68
|
+
module Cataract
|
|
69
|
+
# Flag to indicate pure Ruby version is loaded
|
|
70
|
+
PURE_RUBY_LOADED = true
|
|
71
|
+
|
|
72
|
+
# Implementation type constant
|
|
73
|
+
IMPLEMENTATION = :ruby
|
|
74
|
+
|
|
75
|
+
# Compile flags (mimic C version)
|
|
76
|
+
COMPILE_FLAGS = {
|
|
77
|
+
debug: false,
|
|
78
|
+
str_buf_optimization: false,
|
|
79
|
+
pure_ruby: true
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
# Parse CSS string and return hash with rules, media_index, charset, etc.
|
|
83
|
+
#
|
|
84
|
+
# @api private
|
|
85
|
+
# @param css_string [String] CSS to parse
|
|
86
|
+
# @return [Hash] {
|
|
87
|
+
# rules: Array<Rule>, # Flat array of Rule/AtRule structs
|
|
88
|
+
# _media_index: Hash, # Symbol => Array of rule IDs
|
|
89
|
+
# charset: String|nil, # @charset value if present
|
|
90
|
+
# _has_nesting: Boolean # Whether any nested rules exist
|
|
91
|
+
# }
|
|
92
|
+
def self._parse_css(css_string)
|
|
93
|
+
parser = Parser.new(css_string)
|
|
94
|
+
parser.parse
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# NOTE: Copied from cataract.rb
|
|
98
|
+
# Need to untangle this eventually
|
|
99
|
+
def self.parse_css(css, imports: false)
|
|
100
|
+
css = ImportResolver.resolve(css, imports) if imports
|
|
101
|
+
|
|
102
|
+
Stylesheet.parse(css)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Merge stylesheet rules according to CSS cascade rules
|
|
106
|
+
#
|
|
107
|
+
# @param stylesheet [Stylesheet] Stylesheet to merge
|
|
108
|
+
# @return [Stylesheet] New stylesheet with merged rules
|
|
109
|
+
def self.merge(stylesheet)
|
|
110
|
+
Merge.merge(stylesheet, mutate: false)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Merge stylesheet rules in-place (mutates receiver)
|
|
114
|
+
#
|
|
115
|
+
# @param stylesheet [Stylesheet] Stylesheet to merge
|
|
116
|
+
# @return [Stylesheet] Same stylesheet (mutated)
|
|
117
|
+
def self.merge!(stylesheet)
|
|
118
|
+
Merge.merge(stylesheet, mutate: true)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Add stub method to Stylesheet for pure Ruby implementation
|
|
122
|
+
class Stylesheet
|
|
123
|
+
# Color conversion is only available in the native C extension
|
|
124
|
+
#
|
|
125
|
+
# @raise [NotImplementedError] Always raises - color conversion requires C extension
|
|
126
|
+
def convert_colors!(*_args)
|
|
127
|
+
raise NotImplementedError, 'convert_colors! is only available in the native C extension'
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/cataract/rule.rb
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Cataract
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# Rule is a C struct defined as: `Struct.new(:id, :selector, :declarations, :specificity)`
|
|
7
|
-
#
|
|
8
|
-
# Rules are created by the parser and stored in Stylesheet objects. Each rule
|
|
9
|
-
# contains:
|
|
4
|
+
# Rules are created by the parser and stored in Stylesheet objects. Each rule contains:
|
|
10
5
|
# - An ID (position in the stylesheet)
|
|
11
6
|
# - A CSS selector string
|
|
12
7
|
# - An array of Declaration structs
|
|
13
8
|
# - A specificity value (calculated lazily)
|
|
9
|
+
# - Parent rule ID for nested rules (nil if top-level)
|
|
10
|
+
# - Nesting style (0=implicit, 1=explicit, nil=not nested)
|
|
14
11
|
#
|
|
15
12
|
# Media query information is stored separately in Stylesheet's media_index.
|
|
16
13
|
#
|
|
@@ -25,6 +22,17 @@ module Cataract
|
|
|
25
22
|
# @attr [String] selector The CSS selector (e.g., "body", ".class", "#id")
|
|
26
23
|
# @attr [Array<Declaration>] declarations Array of CSS property declarations
|
|
27
24
|
# @attr [Integer, nil] specificity CSS specificity value (calculated lazily)
|
|
25
|
+
# @attr [Integer, nil] parent_rule_id Parent rule ID for nested rules
|
|
26
|
+
# @attr [Integer, nil] nesting_style 0=implicit, 1=explicit, nil=not nested
|
|
27
|
+
Rule = Struct.new(
|
|
28
|
+
:id,
|
|
29
|
+
:selector,
|
|
30
|
+
:declarations,
|
|
31
|
+
:specificity,
|
|
32
|
+
:parent_rule_id,
|
|
33
|
+
:nesting_style
|
|
34
|
+
)
|
|
35
|
+
|
|
28
36
|
class Rule
|
|
29
37
|
# Silence warning about method redefinition. We redefine below to lazily calculate
|
|
30
38
|
# specificity
|
|
@@ -112,19 +120,20 @@ module Cataract
|
|
|
112
120
|
end
|
|
113
121
|
end
|
|
114
122
|
|
|
115
|
-
# Compare rules
|
|
123
|
+
# Compare rules for logical equality based on CSS semantics.
|
|
116
124
|
#
|
|
117
|
-
# Two rules are equal if they have the same
|
|
125
|
+
# Two rules are equal if they have the same selector and declarations.
|
|
126
|
+
# Internal implementation details (id, specificity) are not considered
|
|
127
|
+
# since they don't affect the CSS semantics. Specificity is derived from
|
|
128
|
+
# the selector, so if selectors match, specificity must match too.
|
|
118
129
|
#
|
|
119
130
|
# @param other [Object] Object to compare with
|
|
120
|
-
# @return [Boolean] true if rules have same
|
|
131
|
+
# @return [Boolean] true if rules have same selector and declarations
|
|
121
132
|
def ==(other)
|
|
122
133
|
return false unless other.is_a?(Rule)
|
|
123
134
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
declarations == other.declarations &&
|
|
127
|
-
specificity == other.specificity
|
|
135
|
+
selector == other.selector &&
|
|
136
|
+
declarations == other.declarations
|
|
128
137
|
end
|
|
129
138
|
alias eql? ==
|
|
130
139
|
end
|
data/lib/cataract/stylesheet.rb
CHANGED
|
@@ -48,6 +48,9 @@ module Cataract
|
|
|
48
48
|
@rules = [] # Flat array of Rule structs
|
|
49
49
|
@_media_index = {} # Hash: Symbol => Array of rule IDs
|
|
50
50
|
@charset = nil
|
|
51
|
+
@_has_nesting = nil # Set by parser (nil or boolean)
|
|
52
|
+
@_last_rule_id = nil # Tracks next rule ID for add_block
|
|
53
|
+
@selectors = nil # Memoized cache of selectors
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
# Parse CSS and return a new Stylesheet
|
|
@@ -297,12 +300,13 @@ module Cataract
|
|
|
297
300
|
Cataract._stylesheet_to_s(@rules, @_media_index, @charset, @_has_nesting || false)
|
|
298
301
|
else
|
|
299
302
|
# Collect all rule IDs that match the requested media types
|
|
300
|
-
matching_rule_ids =
|
|
303
|
+
matching_rule_ids = []
|
|
301
304
|
which_media_array.each do |media_sym|
|
|
302
305
|
if @_media_index[media_sym]
|
|
303
|
-
matching_rule_ids.
|
|
306
|
+
matching_rule_ids.concat(@_media_index[media_sym])
|
|
304
307
|
end
|
|
305
308
|
end
|
|
309
|
+
matching_rule_ids.uniq! # Dedupe: same rule can be in multiple media indexes
|
|
306
310
|
|
|
307
311
|
# Build filtered rules array (keep original IDs, no recreation needed)
|
|
308
312
|
filtered_rules = matching_rule_ids.sort.map! { |rule_id| @rules[rule_id] }
|
|
@@ -311,7 +315,7 @@ module Cataract
|
|
|
311
315
|
filtered_media_index = {}
|
|
312
316
|
which_media_array.each do |media_sym|
|
|
313
317
|
if @_media_index[media_sym]
|
|
314
|
-
filtered_media_index[media_sym] = @_media_index[media_sym] & matching_rule_ids
|
|
318
|
+
filtered_media_index[media_sym] = @_media_index[media_sym] & matching_rule_ids
|
|
315
319
|
end
|
|
316
320
|
end
|
|
317
321
|
|
|
@@ -353,20 +357,21 @@ module Cataract
|
|
|
353
357
|
Cataract._stylesheet_to_formatted_s(@rules, @_media_index, @charset, @_has_nesting || false)
|
|
354
358
|
else
|
|
355
359
|
# Collect all rule IDs that match the requested media types
|
|
356
|
-
matching_rule_ids =
|
|
360
|
+
matching_rule_ids = []
|
|
357
361
|
|
|
358
362
|
# Include rules not in any media query (they apply to all media)
|
|
359
363
|
media_rule_ids = @_media_index.values.flatten.uniq
|
|
360
364
|
all_rule_ids = (0...@rules.length).to_a
|
|
361
365
|
non_media_rule_ids = all_rule_ids - media_rule_ids
|
|
362
|
-
matching_rule_ids.
|
|
366
|
+
matching_rule_ids.concat(non_media_rule_ids)
|
|
363
367
|
|
|
364
368
|
# Include rules from requested media types
|
|
365
369
|
which_media_array.each do |media_sym|
|
|
366
370
|
if @_media_index[media_sym]
|
|
367
|
-
matching_rule_ids.
|
|
371
|
+
matching_rule_ids.concat(@_media_index[media_sym])
|
|
368
372
|
end
|
|
369
373
|
end
|
|
374
|
+
matching_rule_ids.uniq! # Dedupe: same rule can be in multiple media indexes
|
|
370
375
|
|
|
371
376
|
# Build filtered rules array (keep original IDs, no recreation needed)
|
|
372
377
|
filtered_rules = matching_rule_ids.sort.map! { |rule_id| @rules[rule_id] }
|
|
@@ -375,7 +380,7 @@ module Cataract
|
|
|
375
380
|
filtered_media_index = {}
|
|
376
381
|
which_media_array.each do |media_sym|
|
|
377
382
|
if @_media_index[media_sym]
|
|
378
|
-
filtered_media_index[media_sym] = @_media_index[media_sym] & matching_rule_ids
|
|
383
|
+
filtered_media_index[media_sym] = @_media_index[media_sym] & matching_rule_ids
|
|
379
384
|
end
|
|
380
385
|
end
|
|
381
386
|
|
|
@@ -499,7 +504,7 @@ module Cataract
|
|
|
499
504
|
filter_media = media_types ? Array(media_types).map(&:to_sym) : nil
|
|
500
505
|
|
|
501
506
|
# Find rules to remove
|
|
502
|
-
rules_to_remove =
|
|
507
|
+
rules_to_remove = []
|
|
503
508
|
@rules.each_with_index do |rule, rule_id|
|
|
504
509
|
# Check selector match
|
|
505
510
|
next if selector && rule.selector != selector
|
|
@@ -522,7 +527,7 @@ module Cataract
|
|
|
522
527
|
rules_to_remove << rule_id
|
|
523
528
|
end
|
|
524
529
|
|
|
525
|
-
# Remove rules and update media_index
|
|
530
|
+
# Remove rules and update media_index (sort in reverse to maintain indices during deletion)
|
|
526
531
|
rules_to_remove.sort.reverse_each do |rule_id|
|
|
527
532
|
@rules.delete_at(rule_id)
|
|
528
533
|
|
data/lib/cataract/version.rb
CHANGED
data/lib/cataract.rb
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'cataract/version'
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
# Load struct definitions first (before C extension or pure Ruby)
|
|
6
|
+
require_relative 'cataract/declaration'
|
|
5
7
|
require_relative 'cataract/rule'
|
|
6
8
|
require_relative 'cataract/at_rule'
|
|
9
|
+
|
|
10
|
+
# Load pure Ruby or C extension based on ENV var
|
|
11
|
+
if %w[1 true].include?(ENV.fetch('CATARACT_PURE', nil)) || RUBY_ENGINE == 'jruby'
|
|
12
|
+
require_relative 'cataract/pure'
|
|
13
|
+
else
|
|
14
|
+
require_relative 'cataract/native_extension'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Load supporting Ruby files (used by both implementations)
|
|
7
18
|
require_relative 'cataract/stylesheet_scope'
|
|
8
19
|
require_relative 'cataract/stylesheet'
|
|
9
20
|
require_relative 'cataract/declarations'
|
|
@@ -60,11 +71,13 @@ module Cataract
|
|
|
60
71
|
#
|
|
61
72
|
# @see Stylesheet#parse
|
|
62
73
|
# @see Stylesheet.parse
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
unless method_defined?(:parse_css)
|
|
75
|
+
def parse_css(css, imports: false)
|
|
76
|
+
# Resolve @import statements if requested
|
|
77
|
+
css = ImportResolver.resolve(css, imports) if imports
|
|
66
78
|
|
|
67
|
-
|
|
79
|
+
Stylesheet.parse(css)
|
|
80
|
+
end
|
|
68
81
|
end
|
|
69
82
|
|
|
70
83
|
# Merge CSS rules according to CSS cascade rules.
|
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.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Cook
|
|
@@ -21,12 +21,14 @@ extra_rdoc_files: []
|
|
|
21
21
|
files:
|
|
22
22
|
- ".clang-tidy"
|
|
23
23
|
- ".github/workflows/ci-macos.yml"
|
|
24
|
+
- ".github/workflows/ci-manual-rubies.yml"
|
|
24
25
|
- ".github/workflows/ci.yml"
|
|
25
26
|
- ".github/workflows/docs.yml"
|
|
26
27
|
- ".github/workflows/test.yml"
|
|
27
28
|
- ".gitignore"
|
|
28
29
|
- ".overcommit.yml"
|
|
29
30
|
- ".rubocop.yml"
|
|
31
|
+
- ".rubocop_todo.yml"
|
|
30
32
|
- BENCHMARKS.md
|
|
31
33
|
- CHANGELOG.md
|
|
32
34
|
- Gemfile
|
|
@@ -34,27 +36,6 @@ files:
|
|
|
34
36
|
- RAGEL_MIGRATION.md
|
|
35
37
|
- README.md
|
|
36
38
|
- Rakefile
|
|
37
|
-
- benchmarks/benchmark_harness.rb
|
|
38
|
-
- benchmarks/benchmark_merging.rb
|
|
39
|
-
- benchmarks/benchmark_optimization_comparison.rb
|
|
40
|
-
- benchmarks/benchmark_parsing.rb
|
|
41
|
-
- benchmarks/benchmark_ragel_removal.rb
|
|
42
|
-
- benchmarks/benchmark_runner.rb
|
|
43
|
-
- benchmarks/benchmark_serialization.rb
|
|
44
|
-
- benchmarks/benchmark_shorthand.rb
|
|
45
|
-
- benchmarks/benchmark_shorthand_expansion.rb
|
|
46
|
-
- benchmarks/benchmark_specificity.rb
|
|
47
|
-
- benchmarks/benchmark_string_allocation.rb
|
|
48
|
-
- benchmarks/benchmark_stylesheet_to_s.rb
|
|
49
|
-
- benchmarks/benchmark_to_s_cached.rb
|
|
50
|
-
- benchmarks/benchmark_value_splitter.rb
|
|
51
|
-
- benchmarks/benchmark_yjit.rb
|
|
52
|
-
- benchmarks/benchmark_yjit_workers.rb
|
|
53
|
-
- benchmarks/profile_to_s.rb
|
|
54
|
-
- benchmarks/speedup_calculator.rb
|
|
55
|
-
- benchmarks/system_metadata.rb
|
|
56
|
-
- benchmarks/templates/benchmarks.md.erb
|
|
57
|
-
- benchmarks/yjit_tests.rb
|
|
58
39
|
- cataract.gemspec
|
|
59
40
|
- cliff.toml
|
|
60
41
|
- docs/files/EXAMPLE.md
|
|
@@ -99,16 +80,22 @@ files:
|
|
|
99
80
|
- lib/cataract.rb
|
|
100
81
|
- lib/cataract/at_rule.rb
|
|
101
82
|
- lib/cataract/color_conversion.rb
|
|
83
|
+
- lib/cataract/declaration.rb
|
|
102
84
|
- lib/cataract/declarations.rb
|
|
103
85
|
- lib/cataract/import_resolver.rb
|
|
86
|
+
- lib/cataract/pure.rb
|
|
87
|
+
- lib/cataract/pure/byte_constants.rb
|
|
88
|
+
- lib/cataract/pure/helpers.rb
|
|
89
|
+
- lib/cataract/pure/imports.rb
|
|
90
|
+
- lib/cataract/pure/merge.rb
|
|
91
|
+
- lib/cataract/pure/parser.rb
|
|
92
|
+
- lib/cataract/pure/serializer.rb
|
|
93
|
+
- lib/cataract/pure/specificity.rb
|
|
104
94
|
- lib/cataract/rule.rb
|
|
105
95
|
- lib/cataract/stylesheet.rb
|
|
106
96
|
- lib/cataract/stylesheet_scope.rb
|
|
107
97
|
- lib/cataract/version.rb
|
|
108
98
|
- lib/tasks/gem.rake
|
|
109
|
-
- scripts/fuzzer/run.rb
|
|
110
|
-
- scripts/fuzzer/worker.rb
|
|
111
|
-
- scripts/generate_benchmarks_md.rb
|
|
112
99
|
homepage: https://github.com/jamescook/cataract
|
|
113
100
|
licenses:
|
|
114
101
|
- MIT
|