cataract 0.2.1 → 0.2.2
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.yml +1 -1
- data/.rubocop.yml +2 -0
- data/BENCHMARKS.md +41 -38
- data/CHANGELOG.md +13 -0
- data/README.md +9 -3
- data/ext/cataract/cataract.c +273 -92
- data/ext/cataract/cataract.h +4 -3
- data/ext/cataract/css_parser.c +125 -11
- data/ext/cataract/flatten.c +271 -16
- data/lib/cataract/declaration.rb +19 -0
- data/lib/cataract/pure/flatten.rb +103 -8
- data/lib/cataract/pure/parser.rb +203 -139
- data/lib/cataract/pure/serializer.rb +217 -115
- data/lib/cataract/pure.rb +4 -2
- data/lib/cataract/rule.rb +39 -3
- data/lib/cataract/stylesheet.rb +137 -14
- data/lib/cataract/stylesheet_scope.rb +11 -4
- data/lib/cataract/version.rb +1 -1
- metadata +1 -1
|
@@ -10,8 +10,9 @@ module Cataract
|
|
|
10
10
|
# @param media_index [Hash] Media query symbol => array of rule IDs
|
|
11
11
|
# @param charset [String, nil] @charset value
|
|
12
12
|
# @param has_nesting [Boolean] Whether any nested rules exist
|
|
13
|
+
# @param selector_lists [Hash] Selector list ID => array of rule IDs (for grouping)
|
|
13
14
|
# @return [String] Compact CSS string
|
|
14
|
-
def self._stylesheet_to_s(rules, media_index, charset, has_nesting)
|
|
15
|
+
def self._stylesheet_to_s(rules, media_index, charset, has_nesting, selector_lists = {})
|
|
15
16
|
result = +''
|
|
16
17
|
|
|
17
18
|
# Add @charset if present
|
|
@@ -21,7 +22,7 @@ module Cataract
|
|
|
21
22
|
|
|
22
23
|
# Fast path: no nesting - use simple algorithm
|
|
23
24
|
unless has_nesting
|
|
24
|
-
return stylesheet_to_s_original(rules, media_index, result)
|
|
25
|
+
return stylesheet_to_s_original(rules, media_index, result, selector_lists)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# Build parent-child relationships
|
|
@@ -81,55 +82,20 @@ module Cataract
|
|
|
81
82
|
result
|
|
82
83
|
end
|
|
83
84
|
|
|
84
|
-
# Helper: serialize rules without nesting support
|
|
85
|
-
def self.stylesheet_to_s_original(rules, media_index, result)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
rules.each do |rule|
|
|
99
|
-
rule_media = rule_to_media[rule.id]
|
|
100
|
-
|
|
101
|
-
if rule_media.nil?
|
|
102
|
-
# Not in any media query - close any open media block first
|
|
103
|
-
if in_media_block
|
|
104
|
-
result << "}\n"
|
|
105
|
-
in_media_block = false
|
|
106
|
-
current_media = nil
|
|
107
|
-
end
|
|
108
|
-
else
|
|
109
|
-
# This rule is in a media query
|
|
110
|
-
# Check if media query changed from previous rule
|
|
111
|
-
if current_media.nil? || current_media != rule_media
|
|
112
|
-
# Close previous media block if open
|
|
113
|
-
if in_media_block
|
|
114
|
-
result << "}\n"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Open new media block
|
|
118
|
-
current_media = rule_media
|
|
119
|
-
result << "@media #{current_media} {\n"
|
|
120
|
-
in_media_block = true
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
serialize_rule(result, rule)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Close final media block if still open
|
|
128
|
-
if in_media_block
|
|
129
|
-
result << "}\n"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
result
|
|
85
|
+
# Helper: serialize rules without nesting support (compact format)
|
|
86
|
+
def self.stylesheet_to_s_original(rules, media_index, result, selector_lists)
|
|
87
|
+
_serialize_stylesheet_with_grouping(
|
|
88
|
+
rules: rules,
|
|
89
|
+
media_index: media_index,
|
|
90
|
+
result: result,
|
|
91
|
+
selector_lists: selector_lists,
|
|
92
|
+
opening_brace: ' { ',
|
|
93
|
+
closing_brace: " }\n",
|
|
94
|
+
media_indent: '',
|
|
95
|
+
decl_indent_base: nil,
|
|
96
|
+
decl_indent_media: nil,
|
|
97
|
+
add_blank_lines: false
|
|
98
|
+
)
|
|
133
99
|
end
|
|
134
100
|
|
|
135
101
|
# Helper: serialize a rule with its nested children
|
|
@@ -236,6 +202,190 @@ module Cataract
|
|
|
236
202
|
end
|
|
237
203
|
end
|
|
238
204
|
|
|
205
|
+
# Helper: find all selectors from same list with matching declarations
|
|
206
|
+
# Returns array of selectors that can be grouped, marks rules as processed
|
|
207
|
+
def self.find_groupable_selectors(rule:, rules:, selector_lists:, processed_rule_ids:, rule_to_media:, current_media:)
|
|
208
|
+
list_id = rule.selector_list_id
|
|
209
|
+
rule_ids_in_list = selector_lists[list_id]
|
|
210
|
+
|
|
211
|
+
# If no other rules in this list, return just this selector
|
|
212
|
+
if rule_ids_in_list.nil? || rule_ids_in_list.size <= 1
|
|
213
|
+
processed_rule_ids[rule.id] = true
|
|
214
|
+
return [rule.selector]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Find all rules in this list that have identical declarations AND same media context
|
|
218
|
+
matching_selectors = []
|
|
219
|
+
rule_ids_in_list.each do |rid|
|
|
220
|
+
# Find the rule by ID
|
|
221
|
+
other_rule = rules.find { |r| r.id == rid }
|
|
222
|
+
next unless other_rule
|
|
223
|
+
next if processed_rule_ids[rid]
|
|
224
|
+
|
|
225
|
+
# Check same media context
|
|
226
|
+
next if rule_to_media[rid] != current_media
|
|
227
|
+
|
|
228
|
+
# Check declarations match (compare arrays directly for performance)
|
|
229
|
+
if declarations_equal?(rule.declarations, other_rule.declarations)
|
|
230
|
+
matching_selectors << other_rule.selector
|
|
231
|
+
processed_rule_ids[rid] = true
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
matching_selectors
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Private shared implementation for stylesheet serialization with optional selector list grouping
|
|
239
|
+
# All formatting behavior controlled by kwargs to avoid mode flags and if/else branches
|
|
240
|
+
def self._serialize_stylesheet_with_grouping(
|
|
241
|
+
rules:,
|
|
242
|
+
media_index:,
|
|
243
|
+
result:,
|
|
244
|
+
selector_lists:,
|
|
245
|
+
opening_brace:, # ' { ' (compact) vs " {\n" (formatted)
|
|
246
|
+
closing_brace:, # " }\n" (compact) vs "}\n" (formatted)
|
|
247
|
+
media_indent:, # '' (compact) vs ' ' (formatted)
|
|
248
|
+
decl_indent_base:, # nil (compact) vs ' ' (formatted base rules)
|
|
249
|
+
decl_indent_media:, # nil (compact) vs ' ' (formatted media rules)
|
|
250
|
+
add_blank_lines: # false (compact) vs true (formatted)
|
|
251
|
+
)
|
|
252
|
+
grouping_enabled = selector_lists && !selector_lists.empty?
|
|
253
|
+
|
|
254
|
+
# Build rule_id => media_symbol map
|
|
255
|
+
rule_to_media = {}
|
|
256
|
+
media_index.each do |media_sym, rule_ids|
|
|
257
|
+
rule_ids.each do |rule_id|
|
|
258
|
+
rule_to_media[rule_id] = media_sym
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Track processed rules to avoid duplicates when grouping
|
|
263
|
+
processed_rule_ids = {}
|
|
264
|
+
|
|
265
|
+
# Iterate through rules in insertion order, grouping consecutive media queries
|
|
266
|
+
current_media = nil
|
|
267
|
+
in_media_block = false
|
|
268
|
+
rule_index = 0
|
|
269
|
+
|
|
270
|
+
rules.each do |rule|
|
|
271
|
+
# Skip if already processed (when grouped)
|
|
272
|
+
next if processed_rule_ids[rule.id]
|
|
273
|
+
|
|
274
|
+
rule_media = rule_to_media[rule.id]
|
|
275
|
+
is_first_rule = (rule_index == 0)
|
|
276
|
+
|
|
277
|
+
if rule_media.nil?
|
|
278
|
+
# Not in any media query - close any open media block first
|
|
279
|
+
if in_media_block
|
|
280
|
+
result << "}\n"
|
|
281
|
+
in_media_block = false
|
|
282
|
+
current_media = nil
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Add blank line prefix for non-first rules (formatted only)
|
|
286
|
+
result << "\n" if add_blank_lines && !is_first_rule
|
|
287
|
+
|
|
288
|
+
# Try to group with other rules from same selector list
|
|
289
|
+
if grouping_enabled && rule.is_a?(Rule) && rule.selector_list_id
|
|
290
|
+
selectors = find_groupable_selectors(
|
|
291
|
+
rule: rule,
|
|
292
|
+
rules: rules,
|
|
293
|
+
selector_lists: selector_lists,
|
|
294
|
+
processed_rule_ids: processed_rule_ids,
|
|
295
|
+
rule_to_media: rule_to_media,
|
|
296
|
+
current_media: rule_media
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Serialize with grouped selectors
|
|
300
|
+
result << selectors.join(', ') << opening_brace
|
|
301
|
+
if decl_indent_base
|
|
302
|
+
serialize_declarations_formatted(result, rule.declarations, decl_indent_base)
|
|
303
|
+
else
|
|
304
|
+
serialize_declarations(result, rule.declarations)
|
|
305
|
+
end
|
|
306
|
+
result << closing_brace
|
|
307
|
+
else
|
|
308
|
+
# Serialize individual rule
|
|
309
|
+
if decl_indent_base
|
|
310
|
+
serialize_rule_formatted(result, rule, '', true)
|
|
311
|
+
else
|
|
312
|
+
serialize_rule(result, rule)
|
|
313
|
+
end
|
|
314
|
+
processed_rule_ids[rule.id] = true
|
|
315
|
+
end
|
|
316
|
+
else
|
|
317
|
+
# This rule is in a media query
|
|
318
|
+
if current_media.nil? || current_media != rule_media
|
|
319
|
+
# Close previous media block if open
|
|
320
|
+
if in_media_block
|
|
321
|
+
result << "}\n"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Add blank line prefix for non-first rules (formatted only)
|
|
325
|
+
result << "\n" if add_blank_lines && !is_first_rule
|
|
326
|
+
|
|
327
|
+
# Open new media block
|
|
328
|
+
current_media = rule_media
|
|
329
|
+
result << "@media #{current_media} {\n"
|
|
330
|
+
in_media_block = true
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Try to group with other rules from same selector list
|
|
334
|
+
if grouping_enabled && rule.is_a?(Rule) && rule.selector_list_id
|
|
335
|
+
selectors = find_groupable_selectors(
|
|
336
|
+
rule: rule,
|
|
337
|
+
rules: rules,
|
|
338
|
+
selector_lists: selector_lists,
|
|
339
|
+
processed_rule_ids: processed_rule_ids,
|
|
340
|
+
rule_to_media: rule_to_media,
|
|
341
|
+
current_media: rule_media
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Serialize with grouped selectors (with media indent)
|
|
345
|
+
result << media_indent << selectors.join(', ') << opening_brace
|
|
346
|
+
if decl_indent_media
|
|
347
|
+
serialize_declarations_formatted(result, rule.declarations, decl_indent_media)
|
|
348
|
+
else
|
|
349
|
+
serialize_declarations(result, rule.declarations)
|
|
350
|
+
end
|
|
351
|
+
result << media_indent << closing_brace
|
|
352
|
+
else
|
|
353
|
+
# Serialize individual rule inside media block
|
|
354
|
+
if decl_indent_media
|
|
355
|
+
serialize_rule_formatted(result, rule, media_indent, true)
|
|
356
|
+
else
|
|
357
|
+
serialize_rule(result, rule)
|
|
358
|
+
end
|
|
359
|
+
processed_rule_ids[rule.id] = true
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
rule_index += 1
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Close final media block if still open
|
|
367
|
+
if in_media_block
|
|
368
|
+
result << "}\n"
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
result
|
|
372
|
+
end
|
|
373
|
+
private_class_method :_serialize_stylesheet_with_grouping
|
|
374
|
+
|
|
375
|
+
# Helper: check if two declaration arrays are equal
|
|
376
|
+
def self.declarations_equal?(decls1, decls2)
|
|
377
|
+
return false if decls1.size != decls2.size
|
|
378
|
+
|
|
379
|
+
decls1.each_with_index do |d1, i|
|
|
380
|
+
d2 = decls2[i]
|
|
381
|
+
return false if d1.property != d2.property
|
|
382
|
+
return false if d1.value != d2.value
|
|
383
|
+
return false if d1.important != d2.important
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
true
|
|
387
|
+
end
|
|
388
|
+
|
|
239
389
|
# Helper: serialize a single rule
|
|
240
390
|
def self.serialize_rule(result, rule)
|
|
241
391
|
# Check if this is an AtRule
|
|
@@ -307,8 +457,9 @@ module Cataract
|
|
|
307
457
|
# @param media_index [Hash] Media query symbol => array of rule IDs
|
|
308
458
|
# @param charset [String, nil] @charset value
|
|
309
459
|
# @param has_nesting [Boolean] Whether any nested rules exist
|
|
460
|
+
# @param selector_lists [Hash] Selector list ID => array of rule IDs (for grouping)
|
|
310
461
|
# @return [String] Formatted CSS string
|
|
311
|
-
def self._stylesheet_to_formatted_s(rules, media_index, charset, has_nesting)
|
|
462
|
+
def self._stylesheet_to_formatted_s(rules, media_index, charset, has_nesting, selector_lists = {})
|
|
312
463
|
result = +''
|
|
313
464
|
|
|
314
465
|
# Add @charset if present
|
|
@@ -318,7 +469,7 @@ module Cataract
|
|
|
318
469
|
|
|
319
470
|
# Fast path: no nesting - use simple algorithm
|
|
320
471
|
unless has_nesting
|
|
321
|
-
return stylesheet_to_formatted_s_original(rules, media_index, result)
|
|
472
|
+
return stylesheet_to_formatted_s_original(rules, media_index, result, selector_lists)
|
|
322
473
|
end
|
|
323
474
|
|
|
324
475
|
# Build parent-child relationships
|
|
@@ -383,68 +534,19 @@ module Cataract
|
|
|
383
534
|
end
|
|
384
535
|
|
|
385
536
|
# Helper: formatted serialization without nesting support
|
|
386
|
-
def self.stylesheet_to_formatted_s_original(rules, media_index, result)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
rules.each do |rule|
|
|
401
|
-
rule_media = rule_to_media[rule.id]
|
|
402
|
-
is_first_rule = (rule_index == 0)
|
|
403
|
-
|
|
404
|
-
if rule_media.nil?
|
|
405
|
-
# Not in any media query - close any open media block first
|
|
406
|
-
if in_media_block
|
|
407
|
-
result << "}\n"
|
|
408
|
-
in_media_block = false
|
|
409
|
-
current_media = nil
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
# Add blank line prefix for non-first rules
|
|
413
|
-
result << "\n" unless is_first_rule
|
|
414
|
-
|
|
415
|
-
# Output rule with no indentation (always single newline suffix)
|
|
416
|
-
serialize_rule_formatted(result, rule, '', true)
|
|
417
|
-
else
|
|
418
|
-
# This rule is in a media query
|
|
419
|
-
if current_media.nil? || current_media != rule_media
|
|
420
|
-
# Close previous media block if open
|
|
421
|
-
if in_media_block
|
|
422
|
-
result << "}\n"
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
# Add blank line prefix for non-first rules
|
|
426
|
-
result << "\n" unless is_first_rule
|
|
427
|
-
|
|
428
|
-
# Open new media block
|
|
429
|
-
current_media = rule_media
|
|
430
|
-
result << "@media #{current_media} {\n"
|
|
431
|
-
in_media_block = true
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
# Serialize rule inside media block with 2-space indentation
|
|
435
|
-
# Rules inside media blocks always get single newline (is_last=true)
|
|
436
|
-
serialize_rule_formatted(result, rule, ' ', true)
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
rule_index += 1
|
|
440
|
-
end
|
|
441
|
-
|
|
442
|
-
# Close final media block if still open
|
|
443
|
-
if in_media_block
|
|
444
|
-
result << "}\n"
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
result
|
|
537
|
+
def self.stylesheet_to_formatted_s_original(rules, media_index, result, selector_lists)
|
|
538
|
+
_serialize_stylesheet_with_grouping(
|
|
539
|
+
rules: rules,
|
|
540
|
+
media_index: media_index,
|
|
541
|
+
result: result,
|
|
542
|
+
selector_lists: selector_lists,
|
|
543
|
+
opening_brace: " {\n",
|
|
544
|
+
closing_brace: "}\n",
|
|
545
|
+
media_indent: ' ',
|
|
546
|
+
decl_indent_base: ' ',
|
|
547
|
+
decl_indent_media: ' ',
|
|
548
|
+
add_blank_lines: true
|
|
549
|
+
)
|
|
448
550
|
end
|
|
449
551
|
|
|
450
552
|
# Helper: serialize a rule with nested children (formatted)
|
data/lib/cataract/pure.rb
CHANGED
|
@@ -84,14 +84,16 @@ module Cataract
|
|
|
84
84
|
#
|
|
85
85
|
# @api private
|
|
86
86
|
# @param css_string [String] CSS to parse
|
|
87
|
+
# @param parser_options [Hash] Parser configuration options
|
|
88
|
+
# @option parser_options [Boolean] :selector_lists (true) Track selector lists
|
|
87
89
|
# @return [Hash] {
|
|
88
90
|
# rules: Array<Rule>, # Flat array of Rule/AtRule structs
|
|
89
91
|
# _media_index: Hash, # Symbol => Array of rule IDs
|
|
90
92
|
# charset: String|nil, # @charset value if present
|
|
91
93
|
# _has_nesting: Boolean # Whether any nested rules exist
|
|
92
94
|
# }
|
|
93
|
-
def self._parse_css(css_string)
|
|
94
|
-
parser = Parser.new(css_string)
|
|
95
|
+
def self._parse_css(css_string, parser_options = {})
|
|
96
|
+
parser = Parser.new(css_string, parser_options: parser_options)
|
|
95
97
|
parser.parse
|
|
96
98
|
end
|
|
97
99
|
|
data/lib/cataract/rule.rb
CHANGED
|
@@ -24,16 +24,44 @@ module Cataract
|
|
|
24
24
|
# @attr [Integer, nil] specificity CSS specificity value (calculated lazily)
|
|
25
25
|
# @attr [Integer, nil] parent_rule_id Parent rule ID for nested rules
|
|
26
26
|
# @attr [Integer, nil] nesting_style 0=implicit, 1=explicit, nil=not nested
|
|
27
|
+
# @attr [Integer, nil] selector_list_id ID linking rules from same selector list (e.g., "h1, h2")
|
|
27
28
|
Rule = Struct.new(
|
|
28
29
|
:id,
|
|
29
30
|
:selector,
|
|
30
31
|
:declarations,
|
|
31
32
|
:specificity,
|
|
32
33
|
:parent_rule_id,
|
|
33
|
-
:nesting_style
|
|
34
|
+
:nesting_style,
|
|
35
|
+
:selector_list_id
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
class Rule
|
|
39
|
+
# Factory method for creating Rules with keyword arguments (for tests/readability).
|
|
40
|
+
# C code and hot paths should use positional arguments directly via Rule.new.
|
|
41
|
+
#
|
|
42
|
+
# @param id [Integer] The rule's position in the stylesheet
|
|
43
|
+
# @param selector [String] CSS selector
|
|
44
|
+
# @param declarations [Array<Declaration>] Array of declarations
|
|
45
|
+
# @param specificity [Integer, nil] Specificity value (nil to calculate lazily)
|
|
46
|
+
# @param parent_rule_id [Integer, nil] Parent rule ID for nested rules
|
|
47
|
+
# @param nesting_style [Integer, nil] Nesting style (0=implicit, 1=explicit, nil=not nested)
|
|
48
|
+
# @param selector_list_id [Integer, nil] Selector list ID for grouping
|
|
49
|
+
# @return [Rule] New rule instance
|
|
50
|
+
#
|
|
51
|
+
# @example Create a rule with keyword arguments
|
|
52
|
+
# Rule.make(
|
|
53
|
+
# id: 0,
|
|
54
|
+
# selector: '.foo',
|
|
55
|
+
# declarations: [Declaration.new('color', 'red', false)],
|
|
56
|
+
# specificity: 10,
|
|
57
|
+
# parent_rule_id: nil,
|
|
58
|
+
# nesting_style: nil,
|
|
59
|
+
# selector_list_id: nil
|
|
60
|
+
# )
|
|
61
|
+
def self.make(id:, selector:, declarations:, specificity: nil, parent_rule_id: nil, nesting_style: nil, selector_list_id: nil)
|
|
62
|
+
new(id, selector, declarations, specificity, parent_rule_id, nesting_style, selector_list_id)
|
|
63
|
+
end
|
|
64
|
+
|
|
37
65
|
# Silence warning about method redefinition. We redefine below to lazily calculate
|
|
38
66
|
# specificity
|
|
39
67
|
undef_method :specificity if method_defined?(:specificity)
|
|
@@ -87,6 +115,7 @@ module Cataract
|
|
|
87
115
|
#
|
|
88
116
|
# @param property [String] CSS property name to match
|
|
89
117
|
# @param value [String, nil] Optional value to match
|
|
118
|
+
# @param prefix_match [Boolean] Whether to match by prefix (default: false)
|
|
90
119
|
# @return [Boolean] true if rule has matching declaration
|
|
91
120
|
#
|
|
92
121
|
# @example Check for color property
|
|
@@ -94,9 +123,16 @@ module Cataract
|
|
|
94
123
|
#
|
|
95
124
|
# @example Check for specific property value
|
|
96
125
|
# rule.has_property?('color', 'red') #=> true
|
|
97
|
-
|
|
126
|
+
#
|
|
127
|
+
# @example Check for any margin-related property
|
|
128
|
+
# rule.has_property?('margin', prefix_match: true) #=> true if has margin, margin-top, etc.
|
|
129
|
+
def has_property?(property, value = nil, prefix_match: false)
|
|
98
130
|
declarations.any? do |decl|
|
|
99
|
-
property_matches =
|
|
131
|
+
property_matches = if prefix_match
|
|
132
|
+
decl.property.start_with?(property)
|
|
133
|
+
else
|
|
134
|
+
decl.property == property
|
|
135
|
+
end
|
|
100
136
|
value_matches = value.nil? || decl.value == value
|
|
101
137
|
property_matches && value_matches
|
|
102
138
|
end
|