cataract 0.3.0 → 0.5.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.
@@ -4,159 +4,188 @@
4
4
  # NO REGEXP ALLOWED - char-by-char parsing only
5
5
 
6
6
  module Cataract
7
- # Legacy CSS2 pseudo-elements written with a single colon (e.g. :before)
8
- # that must still be counted as pseudo-elements, not pseudo-classes.
9
- PSEUDO_ELEMENT_KEYWORDS = %w[before after first-line first-letter selection].freeze
10
-
11
- # Calculate CSS specificity for a selector
12
- #
13
- # @param selector [String] CSS selector
14
- # @return [Integer] Specificity value
15
- #
16
- # Specificity calculation (per CSS spec):
17
- # - Count IDs (#id) - each worth 100
18
- # - Count classes/attributes/pseudo-classes (.class, [attr], :pseudo) - each worth 10
19
- # - Count elements/pseudo-elements (div, ::before) - each worth 1
20
- def self.calculate_specificity(selector)
21
- return 0 if selector.nil? || selector.empty?
22
-
23
- # Counters for specificity components
24
- id_count = 0
25
- class_count = 0
26
- attr_count = 0
27
- pseudo_class_count = 0
28
- pseudo_element_count = 0
29
- element_count = 0
30
-
31
- i = 0
32
- len = selector.length
33
-
34
- while i < len
35
- byte = selector.getbyte(i)
36
-
37
- if byte == BYTE_HASH
38
- id_count += 1
39
- i = skip_identifier(selector, i + 1, len)
40
- elsif byte == BYTE_DOT
41
- class_count += 1
42
- i = skip_identifier(selector, i + 1, len)
43
- elsif byte == BYTE_LBRACKET
44
- attr_count += 1
45
- i = skip_attribute_selector(selector, i, len)
46
- elsif byte == BYTE_COLON
47
- i, is_not, not_content, counts_as_element = parse_pseudo(selector, i, len)
48
- if not_content
49
- # :not() doesn't count itself, but its content does
50
- not_specificity = calculate_specificity(not_content)
51
- id_count += not_specificity / 100
52
- class_count += (not_specificity % 100) / 10
53
- element_count += not_specificity % 10
54
- elsif !is_not
55
- if counts_as_element
56
- pseudo_element_count += 1
7
+ module Backends
8
+ class PureImpl
9
+ # Legacy CSS2 pseudo-elements written with a single colon (e.g. :before)
10
+ # that must still be counted as pseudo-elements, not pseudo-classes.
11
+ PSEUDO_ELEMENT_KEYWORDS = %w[before after first-line first-letter selection].freeze
12
+
13
+ # Check if byte is a letter (a-z, A-Z)
14
+ # @param byte [Integer] Byte value from String#getbyte
15
+ # @return [Boolean] true if letter
16
+ def letter?(byte)
17
+ (byte >= BYTE_LOWER_A && byte <= BYTE_LOWER_Z) ||
18
+ (byte >= BYTE_UPPER_A && byte <= BYTE_UPPER_Z)
19
+ end
20
+ private :letter?
21
+
22
+ # Check if byte is alphanumeric, hyphen, or underscore (CSS identifier char)
23
+ # @param byte [Integer] Byte value from String#getbyte
24
+ # @return [Boolean] true if valid identifier character
25
+ def ident_char?(byte)
26
+ letter?(byte) || (byte >= BYTE_DIGIT_0 && byte <= BYTE_DIGIT_9) || byte == BYTE_HYPHEN || byte == BYTE_UNDERSCORE
27
+ end
28
+ private :ident_char?
29
+
30
+ # Calculate CSS specificity for a selector
31
+ #
32
+ # @param selector [String] CSS selector
33
+ # @return [Integer] Specificity value
34
+ #
35
+ # Specificity calculation (per CSS spec):
36
+ # - Count IDs (#id) - each worth 100
37
+ # - Count classes/attributes/pseudo-classes (.class, [attr], :pseudo) - each worth 10
38
+ # - Count elements/pseudo-elements (div, ::before) - each worth 1
39
+ def calculate_specificity(selector)
40
+ return 0 if selector.nil? || selector.empty?
41
+
42
+ # Counters for specificity components
43
+ id_count = 0
44
+ class_count = 0
45
+ attr_count = 0
46
+ pseudo_class_count = 0
47
+ pseudo_element_count = 0
48
+ element_count = 0
49
+
50
+ i = 0
51
+ len = selector.length
52
+
53
+ while i < len
54
+ byte = selector.getbyte(i)
55
+
56
+ if byte == BYTE_HASH
57
+ id_count += 1
58
+ i = skip_identifier(selector, i + 1, len)
59
+ elsif byte == BYTE_DOT
60
+ class_count += 1
61
+ i = skip_identifier(selector, i + 1, len)
62
+ elsif byte == BYTE_LBRACKET
63
+ attr_count += 1
64
+ i = skip_attribute_selector(selector, i, len)
65
+ elsif byte == BYTE_COLON
66
+ i, is_not, not_content, counts_as_element = parse_pseudo(selector, i, len)
67
+ if not_content
68
+ # :not() doesn't count itself, but its content does
69
+ not_specificity = calculate_specificity(not_content)
70
+ id_count += not_specificity / 100
71
+ class_count += (not_specificity % 100) / 10
72
+ element_count += not_specificity % 10
73
+ elsif !is_not
74
+ if counts_as_element
75
+ pseudo_element_count += 1
76
+ else
77
+ pseudo_class_count += 1
78
+ end
79
+ end
80
+ elsif letter?(byte)
81
+ ident_end = skip_identifier(selector, i + 1, len)
82
+ if ident_end < len && selector.getbyte(ident_end) == BYTE_PIPE
83
+ # Namespace prefix (ns|E, css-namespaces-3 qname grammar) -
84
+ # contributes nothing itself; the local name after '|' is the
85
+ # real type selector and gets counted on the next iteration.
86
+ i = ident_end + 1
87
+ else
88
+ element_count += 1
89
+ i = ident_end
90
+ end
57
91
  else
58
- pseudo_class_count += 1
92
+ # Whitespace, combinators, and the universal selector (*) all have
93
+ # zero specificity - just skip a single byte.
94
+ i += 1
59
95
  end
60
96
  end
61
- elsif letter?(byte)
62
- element_count += 1
63
- i = skip_identifier(selector, i + 1, len)
64
- else
65
- # Whitespace, combinators, and the universal selector (*) all have
66
- # zero specificity - just skip a single byte.
67
- i += 1
97
+
98
+ # Calculate specificity using W3C formula
99
+ (id_count * 100) +
100
+ ((class_count + attr_count + pseudo_class_count) * 10) +
101
+ ((element_count + pseudo_element_count) * 1)
68
102
  end
69
- end
70
103
 
71
- # Calculate specificity using W3C formula
72
- (id_count * 100) +
73
- ((class_count + attr_count + pseudo_class_count) * 10) +
74
- ((element_count + pseudo_element_count) * 1)
75
- end
104
+ # Advances past an identifier (used after #id, .class, element names, and
105
+ # pseudo names), returning the index of the first non-identifier byte.
106
+ def skip_identifier(selector, pos, len)
107
+ pos += 1 while pos < len && ident_char?(selector.getbyte(pos))
108
+ pos
109
+ end
110
+ private :skip_identifier
76
111
 
77
- # Advances past an identifier (used after #id, .class, element names, and
78
- # pseudo names), returning the index of the first non-identifier byte.
79
- def self.skip_identifier(selector, pos, len)
80
- pos += 1 while pos < len && ident_char?(selector.getbyte(pos))
81
- pos
82
- end
83
- private_class_method :skip_identifier
112
+ # Advances past a bracketed [attr] selector, honoring nested brackets,
113
+ # returning the index just after the matching closing bracket.
114
+ def skip_attribute_selector(selector, pos, len)
115
+ skip_balanced(selector, pos + 1, len, BYTE_LBRACKET, BYTE_RBRACKET)
116
+ end
117
+ private :skip_attribute_selector
118
+
119
+ # Advances past a balanced open/close byte pair (already past the opening
120
+ # byte, with depth 1), returning the index just after the matching close.
121
+ def skip_balanced(selector, pos, len, open_byte, close_byte)
122
+ depth = 1
123
+ while pos < len && depth > 0
124
+ b = selector.getbyte(pos)
125
+ depth += 1 if b == open_byte
126
+ depth -= 1 if b == close_byte
127
+ pos += 1
128
+ end
129
+ pos
130
+ end
131
+ private :skip_balanced
132
+
133
+ # Advances past a balanced open/close byte pair (already past the opening
134
+ # byte, with depth 1), returning the index OF the matching close byte
135
+ # (rather than past it), so the caller can capture the content in between.
136
+ def find_balanced_close(selector, pos, len, open_byte, close_byte)
137
+ depth = 1
138
+ while pos < len && depth > 0
139
+ b = selector.getbyte(pos)
140
+ depth += 1 if b == open_byte
141
+ depth -= 1 if b == close_byte
142
+ pos += 1 if depth > 0
143
+ end
144
+ pos
145
+ end
146
+ private :find_balanced_close
147
+
148
+ # Parses a :pseudo-class, ::pseudo-element, or :not(...) token starting at
149
+ # the colon byte. Returns [new_pos, is_not, not_content, counts_as_element]:
150
+ # - new_pos: index just after the fully-consumed token (incl. any (...) args)
151
+ # - is_not: whether this token is :not (which never counts itself)
152
+ # - not_content: the non-empty content of :not(...), or nil otherwise
153
+ # - counts_as_element: whether this token counts toward pseudo-elements
154
+ # (::foo, or a legacy single-colon pseudo-element like :before) rather
155
+ # than pseudo-classes
156
+ def parse_pseudo(selector, pos, len)
157
+ pos += 1
158
+ is_pseudo_element = false
159
+ if pos < len && selector.getbyte(pos) == BYTE_COLON
160
+ is_pseudo_element = true
161
+ pos += 1
162
+ end
84
163
 
85
- # Advances past a bracketed [attr] selector, honoring nested brackets,
86
- # returning the index just after the matching closing bracket.
87
- def self.skip_attribute_selector(selector, pos, len)
88
- skip_balanced(selector, pos + 1, len, BYTE_LBRACKET, BYTE_RBRACKET)
89
- end
90
- private_class_method :skip_attribute_selector
91
-
92
- # Advances past a balanced open/close byte pair (already past the opening
93
- # byte, with depth 1), returning the index just after the matching close.
94
- def self.skip_balanced(selector, pos, len, open_byte, close_byte)
95
- depth = 1
96
- while pos < len && depth > 0
97
- b = selector.getbyte(pos)
98
- depth += 1 if b == open_byte
99
- depth -= 1 if b == close_byte
100
- pos += 1
101
- end
102
- pos
103
- end
104
- private_class_method :skip_balanced
105
-
106
- # Advances past a balanced open/close byte pair (already past the opening
107
- # byte, with depth 1), returning the index OF the matching close byte
108
- # (rather than past it), so the caller can capture the content in between.
109
- def self.find_balanced_close(selector, pos, len, open_byte, close_byte)
110
- depth = 1
111
- while pos < len && depth > 0
112
- b = selector.getbyte(pos)
113
- depth += 1 if b == open_byte
114
- depth -= 1 if b == close_byte
115
- pos += 1 if depth > 0
116
- end
117
- pos
118
- end
119
- private_class_method :find_balanced_close
120
-
121
- # Parses a :pseudo-class, ::pseudo-element, or :not(...) token starting at
122
- # the colon byte. Returns [new_pos, is_not, not_content, counts_as_element]:
123
- # - new_pos: index just after the fully-consumed token (incl. any (...) args)
124
- # - is_not: whether this token is :not (which never counts itself)
125
- # - not_content: the non-empty content of :not(...), or nil otherwise
126
- # - counts_as_element: whether this token counts toward pseudo-elements
127
- # (::foo, or a legacy single-colon pseudo-element like :before) rather
128
- # than pseudo-classes
129
- def self.parse_pseudo(selector, pos, len)
130
- pos += 1
131
- is_pseudo_element = false
132
- if pos < len && selector.getbyte(pos) == BYTE_COLON
133
- is_pseudo_element = true
134
- pos += 1
135
- end
164
+ pseudo_start = pos
165
+ pos = skip_identifier(selector, pos, len)
166
+ pseudo_name = selector[pseudo_start...pos]
167
+
168
+ is_legacy_pseudo_element = !is_pseudo_element && !pseudo_name.empty? &&
169
+ PSEUDO_ELEMENT_KEYWORDS.include?(pseudo_name)
170
+ is_not = (pseudo_name == 'not')
171
+ not_content = nil
172
+
173
+ if pos < len && selector.getbyte(pos) == BYTE_LPAREN
174
+ pos += 1
175
+ if is_not
176
+ content_start = pos
177
+ pos = find_balanced_close(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
178
+ content = selector[content_start...pos]
179
+ not_content = content unless content.empty?
180
+ pos += 1 # Skip closing paren
181
+ else
182
+ pos = skip_balanced(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
183
+ end
184
+ end
136
185
 
137
- pseudo_start = pos
138
- pos = skip_identifier(selector, pos, len)
139
- pseudo_name = selector[pseudo_start...pos]
140
-
141
- is_legacy_pseudo_element = !is_pseudo_element && !pseudo_name.empty? &&
142
- PSEUDO_ELEMENT_KEYWORDS.include?(pseudo_name)
143
- is_not = (pseudo_name == 'not')
144
- not_content = nil
145
-
146
- if pos < len && selector.getbyte(pos) == BYTE_LPAREN
147
- pos += 1
148
- if is_not
149
- content_start = pos
150
- pos = find_balanced_close(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
151
- content = selector[content_start...pos]
152
- not_content = content unless content.empty?
153
- pos += 1 # Skip closing paren
154
- else
155
- pos = skip_balanced(selector, pos, len, BYTE_LPAREN, BYTE_RPAREN)
186
+ [pos, is_not, not_content, is_pseudo_element || is_legacy_pseudo_element]
156
187
  end
188
+ private :parse_pseudo
157
189
  end
158
-
159
- [pos, is_not, not_content, is_pseudo_element || is_legacy_pseudo_element]
160
190
  end
161
- private_class_method :parse_pseudo
162
191
  end
data/lib/cataract/pure.rb CHANGED
@@ -7,16 +7,24 @@
7
7
  # NO REGEXP ALLOWED - consume chars one at a time like the C version.
8
8
  # ==================================================================
9
9
  #
10
- # Load this instead of the C extension with:
11
- # require 'cataract/pure'
10
+ # Load this instead of the C extension by setting CATARACT_PURE before
11
+ # requiring 'cataract' (not by requiring this file directly - lib/cataract.rb
12
+ # is what sets up Cataract::Backends.active and the top-level identity
13
+ # constants like IMPLEMENTATION; this file alone does not):
14
+ # ENV['CATARACT_PURE'] = '1'
15
+ # require 'cataract'
12
16
  #
13
17
  # Or run tests with:
14
18
  # 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
19
+ #
20
+ # Everything backend-specific here lives under Cataract::Backends::Pure -
21
+ # nothing is defined directly on Cataract itself, so this file can be loaded
22
+ # in the same process as the native backend without either clobbering the
23
+ # other. The one exception is Stylesheet#convert_colors! below: color
24
+ # conversion has no pure-Ruby implementation at all, so the stub simply
25
+ # raises - native's real implementation (ext/cataract_color, loaded
26
+ # separately and only opt-in) defines its own convert_colors! and is
27
+ # unaffected by this file being loaded or not.
20
28
 
21
29
  require_relative 'error'
22
30
 
@@ -24,126 +32,91 @@ require_relative 'version'
24
32
  require_relative 'constants'
25
33
 
26
34
  # Load struct definitions and supporting files
27
- # (These are also loaded by lib/cataract.rb, but we need them here for direct require)
35
+ # (These are also loaded by lib/cataract/native.rb, but we need them here for direct require)
28
36
  require_relative 'declaration'
29
37
  require_relative 'rule'
30
38
  require_relative 'at_rule'
31
39
  require_relative 'media_query'
40
+ require_relative 'conditional_group'
32
41
  require_relative 'import_statement'
33
42
  require_relative 'stylesheet_scope'
34
43
  require_relative 'stylesheet'
35
44
  require_relative 'declarations'
36
45
  require_relative 'import_resolver'
37
46
 
38
- # Add to_s method to Declarations class for pure Ruby mode
39
- module Cataract
40
- class Declarations
41
- # Serialize declarations to CSS string
42
- def to_s
43
- result = String.new
44
- @values.each_with_index do |decl, i|
45
- result << decl.property
46
- result << ': '
47
- result << decl.value
48
- result << ' !important' if decl.important
49
- result << ';'
50
- result << ' ' if i < @values.length - 1 # Add space after semicolon except for last
51
- end
52
- result
53
- end
54
- end
55
- end
56
-
57
47
  # Load pure Ruby implementation modules
58
48
  require_relative 'pure/byte_constants'
59
- require_relative 'pure/helpers'
60
49
  require_relative 'pure/specificity'
61
50
  require_relative 'pure/serializer'
62
51
  require_relative 'pure/parser'
63
52
  require_relative 'pure/flatten'
53
+ require_relative 'pure/declarations'
64
54
 
65
55
  module Cataract
66
- # Flag to indicate pure Ruby version is loaded
67
- PURE_RUBY_LOADED = true
68
-
69
- # Implementation type constant
70
- IMPLEMENTATION = :ruby
71
-
72
- # Compile flags (mimic C version)
73
- COMPILE_FLAGS = {
74
- debug: false,
75
- str_buf_optimization: false,
76
- pure_ruby: true
77
- }.freeze
78
-
79
- # Parse CSS string and return hash with rules, media_index, charset, etc.
80
- #
81
- # @api private
82
- # @param css_string [String] CSS to parse
83
- # @param parser_options [Hash] Parser configuration options
84
- # @option parser_options [Boolean] :selector_lists (true) Track selector lists
85
- # @return [Hash] {
86
- # rules: Array<Rule>, # Flat array of Rule/AtRule structs
87
- # _media_index: Hash, # Symbol => Array of rule IDs
88
- # charset: String|nil, # @charset value if present
89
- # _has_nesting: Boolean # Whether any nested rules exist
90
- # }
91
- def self._parse_css(css_string, parser_options = {})
92
- parser = Parser.new(css_string, parser_options: parser_options)
93
- parser.parse
94
- end
95
-
96
- # NOTE: Copied from cataract.rb
97
- # Need to untangle this eventually
98
- def self.parse_css(css, **options)
99
- Stylesheet.parse(css, **options)
100
- end
101
-
102
- # Flatten stylesheet rules according to CSS cascade rules
103
- #
104
- # @param stylesheet [Stylesheet] Stylesheet to flatten
105
- # @return [Stylesheet] New stylesheet with flattened rules
106
- def self.flatten(stylesheet)
107
- Flatten.flatten(stylesheet, mutate: false)
108
- end
109
-
110
- # Flatten stylesheet rules in-place (mutates receiver)
111
- #
112
- # @param stylesheet [Stylesheet] Stylesheet to flatten
113
- # @return [Stylesheet] Same stylesheet (mutated)
114
- def self.flatten!(stylesheet)
115
- Flatten.flatten(stylesheet, mutate: true)
116
- end
56
+ module Backends
57
+ class PureImpl
58
+ # Flag to indicate the pure Ruby backend is loaded
59
+ PURE_RUBY_LOADED = true
60
+
61
+ # Implementation type constant
62
+ IMPLEMENTATION = :ruby
63
+
64
+ # Compile flags (mimic C version)
65
+ COMPILE_FLAGS = {
66
+ debug: false,
67
+ str_buf_optimization: false,
68
+ pure_ruby: true
69
+ }.freeze
70
+
71
+ # Parse CSS string and return hash with rules, media_index, charset, etc.
72
+ # Called by Stylesheet#add_block - not meant for direct use.
73
+ #
74
+ # @param css_string [String] CSS to parse
75
+ # @param parser_options [Hash] Parser configuration options
76
+ # @option parser_options [Boolean] :selector_lists (true) Track selector lists
77
+ # @return [Hash] {
78
+ # rules: Array<Rule>, # Flat array of Rule/AtRule structs
79
+ # _media_index: Hash, # Symbol => Array of rule IDs
80
+ # charset: String|nil, # @charset value if present
81
+ # _has_nesting: Boolean # Whether any nested rules exist
82
+ # }
83
+ def parse(css_string, parser_options = {})
84
+ parser = Parser.new(css_string, parser_options: parser_options)
85
+ parser.parse
86
+ end
117
87
 
118
- # Deprecated: Use flatten instead
119
- def self.merge(stylesheet)
120
- warn 'Cataract.merge is deprecated, use Cataract.flatten instead', uplevel: 1
121
- flatten(stylesheet)
122
- end
88
+ # Flatten stylesheet rules according to CSS cascade rules
89
+ #
90
+ # @param stylesheet [Stylesheet] Stylesheet to flatten
91
+ # @return [Stylesheet] New stylesheet with flattened rules
92
+ def flatten(stylesheet)
93
+ Flatten.flatten(stylesheet, mutate: false)
94
+ end
123
95
 
124
- # Deprecated: Use flatten! instead
125
- def self.merge!(stylesheet)
126
- warn 'Cataract.merge! is deprecated, use Cataract.flatten! instead', uplevel: 1
127
- flatten!(stylesheet)
128
- end
96
+ # Expand a single shorthand declaration into longhand declarations.
97
+ # Called by Rule#expanded_declarations - not meant for direct use.
98
+ #
99
+ # @param decl [Declaration] Declaration to expand
100
+ # @return [Array<Declaration>] Array of expanded longhand declarations
101
+ def expand_shorthand(decl)
102
+ Flatten.expand_shorthand(decl)
103
+ end
104
+ end
129
105
 
130
- # Expand a single shorthand declaration into longhand declarations.
131
- # Underscore prefix indicates semi-private API - use with caution.
132
- #
133
- # @param decl [Declaration] Declaration to expand
134
- # @return [Array<Declaration>] Array of expanded longhand declarations
135
- # @api private
136
- def self.expand_shorthand(decl)
137
- Flatten.expand_shorthand(decl)
106
+ # The active-facing constant is a single frozen instance, not the class
107
+ # itself - none of PureImpl's methods touch instance state, so one shared
108
+ # instance is exactly as safe as the bare module this replaces, while
109
+ # giving its methods genuine instance-level `private` instead of
110
+ # `private_class_method`.
111
+ Pure = PureImpl.new.freeze
138
112
  end
139
113
 
140
- # Add stub method to Stylesheet for pure Ruby implementation
141
114
  class Stylesheet
142
- # Color conversion is only available in the native C extension
115
+ # Color conversion has no pure-Ruby implementation.
143
116
  #
144
- # @raise [NotImplementedError] Always raises - color conversion requires C extension
117
+ # @raise [NotImplementedError] Always raises - not implemented for pure Ruby
145
118
  def convert_colors!(*_args)
146
- raise NotImplementedError, 'convert_colors! is only available in the native C extension'
119
+ raise NotImplementedError, 'convert_colors! is not yet implemented in Cataract'
147
120
  end
148
121
  end
149
122
  end
data/lib/cataract/rule.rb CHANGED
@@ -26,6 +26,7 @@ module Cataract
26
26
  # @attr [Integer, nil] nesting_style 0=implicit, 1=explicit, nil=not nested
27
27
  # @attr [Integer, nil] selector_list_id ID linking rules from same selector list (e.g., "h1, h2")
28
28
  # @attr [Integer, nil] media_query_id ID of the MediaQuery this rule belongs to (nil if not in media query)
29
+ # @attr [Integer, nil] conditional_group_id ID of the ConditionalGroup (@supports/@layer/@container/@scope) this rule belongs to (nil if not in one)
29
30
  Rule = Struct.new(
30
31
  :id,
31
32
  :selector,
@@ -34,7 +35,8 @@ module Cataract
34
35
  :parent_rule_id,
35
36
  :nesting_style,
36
37
  :selector_list_id,
37
- :media_query_id
38
+ :media_query_id,
39
+ :conditional_group_id
38
40
  )
39
41
 
40
42
  class Rule
@@ -49,6 +51,7 @@ module Cataract
49
51
  # @param nesting_style [Integer, nil] Nesting style (0=implicit, 1=explicit, nil=not nested)
50
52
  # @param selector_list_id [Integer, nil] Selector list ID for grouping
51
53
  # @param media_query_id [Integer, nil] MediaQuery ID for rules in media queries
54
+ # @param conditional_group_id [Integer, nil] ConditionalGroup ID for rules in @supports/@layer/@container/@scope
52
55
  # @return [Rule] New rule instance
53
56
  #
54
57
  # @example Create a rule with keyword arguments
@@ -60,10 +63,11 @@ module Cataract
60
63
  # parent_rule_id: nil,
61
64
  # nesting_style: nil,
62
65
  # selector_list_id: nil,
63
- # media_query_id: nil
66
+ # media_query_id: nil,
67
+ # conditional_group_id: nil
64
68
  # )
65
- def self.make(id:, selector:, declarations:, specificity: nil, parent_rule_id: nil, nesting_style: nil, selector_list_id: nil, media_query_id: nil)
66
- new(id, selector, declarations, specificity, parent_rule_id, nesting_style, selector_list_id, media_query_id)
69
+ def self.make(id:, selector:, declarations:, specificity: nil, parent_rule_id: nil, nesting_style: nil, selector_list_id: nil, media_query_id: nil, conditional_group_id: nil)
70
+ new(id, selector, declarations, specificity, parent_rule_id, nesting_style, selector_list_id, media_query_id, conditional_group_id)
67
71
  end
68
72
 
69
73
  # Silence warning about method redefinition. We redefine below to lazily calculate
@@ -87,8 +91,11 @@ module Cataract
87
91
  def specificity
88
92
  return self[:specificity] unless self[:specificity].nil?
89
93
 
90
- # Calculate and cache
91
- calculated = Cataract.calculate_specificity(selector)
94
+ # Calculate and cache. Specificity is a pure function of the selector
95
+ # string, identical across backends by design, so this always goes
96
+ # through the process's active backend regardless of which backend
97
+ # actually produced this Rule.
98
+ calculated = Cataract::Backends.active.calculate_specificity(selector)
92
99
  self[:specificity] = calculated
93
100
  calculated
94
101
  end
@@ -215,7 +222,7 @@ module Cataract
215
222
  # rubocop:disable Naming/MemoizedInstanceVariableName
216
223
  def expanded_declarations
217
224
  @_expanded_declarations ||= begin
218
- expanded = declarations.flat_map { |decl| Cataract.expand_shorthand(decl) }
225
+ expanded = declarations.flat_map { |decl| Cataract::Backends.active.expand_shorthand(decl) }
219
226
  expanded.sort_by! { |d| [d.property, d.value, d.important ? 1 : 0] }
220
227
  expanded
221
228
  end