cataract 0.2.3 → 0.2.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/.rubocop.yml +7 -3
- data/BENCHMARKS.md +32 -32
- data/CHANGELOG.md +10 -0
- data/Gemfile +3 -0
- data/ext/cataract/cataract.c +219 -112
- data/ext/cataract/cataract.h +5 -1
- data/ext/cataract/css_parser.c +523 -33
- data/ext/cataract/flatten.c +233 -91
- data/ext/cataract/shorthand_expander.c +7 -0
- data/lib/cataract/at_rule.rb +2 -1
- data/lib/cataract/constants.rb +10 -0
- data/lib/cataract/import_resolver.rb +18 -87
- data/lib/cataract/import_statement.rb +29 -5
- data/lib/cataract/media_query.rb +98 -0
- data/lib/cataract/pure/byte_constants.rb +11 -0
- data/lib/cataract/pure/flatten.rb +127 -15
- data/lib/cataract/pure/parser.rb +637 -270
- data/lib/cataract/pure/serializer.rb +216 -115
- data/lib/cataract/pure.rb +6 -7
- data/lib/cataract/rule.rb +9 -5
- data/lib/cataract/stylesheet.rb +321 -99
- data/lib/cataract/stylesheet_scope.rb +10 -7
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +4 -8
- data/lib/tasks/profile.rake +210 -0
- metadata +4 -2
- data/lib/cataract/pure/imports.rb +0 -268
|
@@ -2,17 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
# Pure Ruby CSS parser - Serialization methods
|
|
4
4
|
# NO REGEXP ALLOWED - char-by-char parsing only
|
|
5
|
+
#
|
|
6
|
+
# @api private
|
|
7
|
+
# This module contains internal serialization methods for converting parsed CSS
|
|
8
|
+
# back to strings. These methods are called by Stylesheet#to_s and should not be
|
|
9
|
+
# used directly. The public API is through the Stylesheet class.
|
|
5
10
|
|
|
6
11
|
module Cataract
|
|
12
|
+
# @api private
|
|
13
|
+
# Helper: Build media query string from MediaQuery object or list
|
|
14
|
+
# @param media_query [MediaQuery] The MediaQuery object
|
|
15
|
+
# @param media_query_list_id [Integer, nil] Optional list ID if this is part of a comma-separated list
|
|
16
|
+
# @param media_query_lists [Hash] Hash mapping list_id => array of MediaQuery IDs
|
|
17
|
+
# @param media_queries [Array] Array of all MediaQuery objects
|
|
18
|
+
# @return [String] Serialized media query string (e.g., "screen", "screen, print", "screen and (min-width: 768px)")
|
|
19
|
+
def self._build_media_query_string(media_query, media_query_list_id, media_query_lists, media_queries)
|
|
20
|
+
if media_query_list_id
|
|
21
|
+
# Comma-separated list
|
|
22
|
+
mq_ids = media_query_lists[media_query_list_id]
|
|
23
|
+
mq_ids.map do |mq_id|
|
|
24
|
+
mq = media_queries[mq_id]
|
|
25
|
+
if mq.conditions
|
|
26
|
+
mq.type == :all ? mq.conditions : "#{mq.type} and #{mq.conditions}"
|
|
27
|
+
else
|
|
28
|
+
mq.type.to_s
|
|
29
|
+
end
|
|
30
|
+
end.join(', ')
|
|
31
|
+
else
|
|
32
|
+
# Single query
|
|
33
|
+
if media_query.conditions
|
|
34
|
+
media_query.type == :all ? media_query.conditions : "#{media_query.type} and #{media_query.conditions}"
|
|
35
|
+
else
|
|
36
|
+
media_query.type.to_s
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
7
41
|
# Serialize stylesheet to compact CSS string
|
|
8
42
|
#
|
|
9
43
|
# @param rules [Array<Rule>] Array of rules
|
|
10
|
-
# @param media_index [Hash] Media query symbol => array of rule IDs
|
|
11
44
|
# @param charset [String, nil] @charset value
|
|
12
45
|
# @param has_nesting [Boolean] Whether any nested rules exist
|
|
13
46
|
# @param selector_lists [Hash] Selector list ID => array of rule IDs (for grouping)
|
|
47
|
+
# @param media_queries [Array<MediaQuery>] Array of MediaQuery objects (optional, for proper serialization)
|
|
48
|
+
# @param media_query_lists [Hash] List ID => array of MediaQuery IDs (optional, for comma-separated queries)
|
|
14
49
|
# @return [String] Compact CSS string
|
|
15
|
-
def self.
|
|
50
|
+
def self.stylesheet_to_s(rules, charset, has_nesting, selector_lists = {}, media_queries = [], media_query_lists = {})
|
|
16
51
|
result = +''
|
|
17
52
|
|
|
18
53
|
# Add @charset if present
|
|
@@ -22,7 +57,7 @@ module Cataract
|
|
|
22
57
|
|
|
23
58
|
# Fast path: no nesting - use simple algorithm
|
|
24
59
|
unless has_nesting
|
|
25
|
-
return
|
|
60
|
+
return _stylesheet_to_s_without_nesting(rules, result, selector_lists, media_queries, media_query_lists)
|
|
26
61
|
end
|
|
27
62
|
|
|
28
63
|
# Build parent-child relationships
|
|
@@ -35,44 +70,60 @@ module Cataract
|
|
|
35
70
|
rule_children[parent_id] << rule
|
|
36
71
|
end
|
|
37
72
|
|
|
38
|
-
# Build
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
rule_to_media[rule_id] = media_sym
|
|
43
|
-
end
|
|
73
|
+
# Build reverse map: media_query_id => list_id
|
|
74
|
+
mq_id_to_list_id = {}
|
|
75
|
+
media_query_lists.each do |list_id, mq_ids|
|
|
76
|
+
mq_ids.each { |mq_id| mq_id_to_list_id[mq_id] = list_id }
|
|
44
77
|
end
|
|
45
78
|
|
|
46
79
|
# Serialize top-level rules only (those without parent_rule_id)
|
|
47
|
-
|
|
80
|
+
current_media_query_list_id = nil
|
|
81
|
+
current_media_query = nil
|
|
48
82
|
in_media_block = false
|
|
49
83
|
|
|
50
84
|
rules.each do |rule|
|
|
51
85
|
# Skip rules that have a parent (they'll be serialized as nested)
|
|
52
86
|
next if rule.parent_rule_id
|
|
53
87
|
|
|
54
|
-
|
|
88
|
+
rule_media_query_id = rule.is_a?(Rule) ? rule.media_query_id : nil
|
|
89
|
+
rule_media_query = rule_media_query_id ? media_queries[rule_media_query_id] : nil
|
|
90
|
+
rule_media_query_list_id = rule_media_query_id ? mq_id_to_list_id[rule_media_query_id] : nil
|
|
55
91
|
|
|
56
|
-
if
|
|
92
|
+
if rule_media_query.nil?
|
|
57
93
|
# Close any open media block
|
|
58
94
|
if in_media_block
|
|
59
95
|
result << "}\n"
|
|
60
96
|
in_media_block = false
|
|
61
|
-
|
|
97
|
+
current_media_query = nil
|
|
98
|
+
current_media_query_list_id = nil
|
|
62
99
|
end
|
|
63
100
|
else
|
|
64
|
-
#
|
|
65
|
-
|
|
101
|
+
# Check if we need to open a new media block
|
|
102
|
+
# For lists: compare list_id
|
|
103
|
+
# For single queries: compare by content (type + conditions)
|
|
104
|
+
needs_new_block = if rule_media_query_list_id
|
|
105
|
+
current_media_query_list_id != rule_media_query_list_id
|
|
106
|
+
else
|
|
107
|
+
!current_media_query ||
|
|
108
|
+
current_media_query.type != rule_media_query.type ||
|
|
109
|
+
current_media_query.conditions != rule_media_query.conditions
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if needs_new_block
|
|
66
113
|
if in_media_block
|
|
67
114
|
result << "}\n"
|
|
68
115
|
end
|
|
69
|
-
|
|
70
|
-
|
|
116
|
+
current_media_query = rule_media_query
|
|
117
|
+
current_media_query_list_id = rule_media_query_list_id
|
|
118
|
+
|
|
119
|
+
# Serialize the media query (or list)
|
|
120
|
+
media_query_string = _build_media_query_string(rule_media_query, rule_media_query_list_id, media_query_lists, media_queries)
|
|
121
|
+
result << "@media #{media_query_string} {\n"
|
|
71
122
|
in_media_block = true
|
|
72
123
|
end
|
|
73
124
|
end
|
|
74
125
|
|
|
75
|
-
|
|
126
|
+
_serialize_rule_with_nesting(result, rule, rule_children, media_queries)
|
|
76
127
|
end
|
|
77
128
|
|
|
78
129
|
if in_media_block
|
|
@@ -83,12 +134,13 @@ module Cataract
|
|
|
83
134
|
end
|
|
84
135
|
|
|
85
136
|
# Helper: serialize rules without nesting support (compact format)
|
|
86
|
-
def self.
|
|
137
|
+
def self._stylesheet_to_s_without_nesting(rules, result, selector_lists, media_queries = [], media_query_lists = {})
|
|
87
138
|
_serialize_stylesheet_with_grouping(
|
|
88
139
|
rules: rules,
|
|
89
|
-
media_index: media_index,
|
|
90
140
|
result: result,
|
|
91
141
|
selector_lists: selector_lists,
|
|
142
|
+
media_queries: media_queries,
|
|
143
|
+
media_query_lists: media_query_lists,
|
|
92
144
|
opening_brace: ' { ',
|
|
93
145
|
closing_brace: " }\n",
|
|
94
146
|
media_indent: '',
|
|
@@ -99,14 +151,14 @@ module Cataract
|
|
|
99
151
|
end
|
|
100
152
|
|
|
101
153
|
# Helper: serialize a rule with its nested children
|
|
102
|
-
def self.
|
|
154
|
+
def self._serialize_rule_with_nesting(result, rule, rule_children, media_queries)
|
|
103
155
|
# Start selector
|
|
104
156
|
result << "#{rule.selector} { "
|
|
105
157
|
|
|
106
158
|
# Serialize declarations
|
|
107
159
|
has_declarations = !rule.declarations.empty?
|
|
108
160
|
if has_declarations
|
|
109
|
-
|
|
161
|
+
_serialize_declarations(result, rule.declarations)
|
|
110
162
|
end
|
|
111
163
|
|
|
112
164
|
# Get nested children for this rule
|
|
@@ -122,27 +174,32 @@ module Cataract
|
|
|
122
174
|
end
|
|
123
175
|
|
|
124
176
|
# Determine if we need to reconstruct the nested selector with &
|
|
125
|
-
nested_selector =
|
|
177
|
+
nested_selector = _reconstruct_nested_selector(rule.selector, child.selector, child.nesting_style)
|
|
126
178
|
|
|
127
179
|
# Check if this child has @media nesting (parent_rule_id present but nesting_style is nil)
|
|
128
|
-
if child.nesting_style.nil? &&
|
|
180
|
+
if child.nesting_style.nil? && child.media_query_id && media_queries[child.media_query_id]
|
|
129
181
|
# This is a nested @media rule
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
182
|
+
mq = media_queries[child.media_query_id]
|
|
183
|
+
media_query_string = if mq.conditions
|
|
184
|
+
mq.type == :all ? mq.conditions : "#{mq.type} and #{mq.conditions}"
|
|
185
|
+
else
|
|
186
|
+
mq.type.to_s
|
|
187
|
+
end
|
|
188
|
+
result << "@media #{media_query_string} { "
|
|
189
|
+
_serialize_declarations(result, child.declarations)
|
|
133
190
|
|
|
134
191
|
# Recursively serialize any children of this @media rule
|
|
135
192
|
media_children = rule_children[child.id] || []
|
|
136
193
|
media_children.each_with_index do |media_child, media_idx|
|
|
137
194
|
result << ' ' if media_idx > 0 || !child.declarations.empty?
|
|
138
195
|
|
|
139
|
-
nested_media_selector =
|
|
196
|
+
nested_media_selector = _reconstruct_nested_selector(
|
|
140
197
|
child.selector, media_child.selector,
|
|
141
198
|
media_child.nesting_style
|
|
142
199
|
)
|
|
143
200
|
|
|
144
201
|
result << "#{nested_media_selector} { "
|
|
145
|
-
|
|
202
|
+
_serialize_declarations(result, media_child.declarations)
|
|
146
203
|
result << ' }'
|
|
147
204
|
end
|
|
148
205
|
|
|
@@ -150,21 +207,21 @@ module Cataract
|
|
|
150
207
|
else
|
|
151
208
|
# Regular nested selector
|
|
152
209
|
result << "#{nested_selector} { "
|
|
153
|
-
|
|
210
|
+
_serialize_declarations(result, child.declarations)
|
|
154
211
|
|
|
155
212
|
# Recursively serialize any children of this nested rule
|
|
156
213
|
grandchildren = rule_children[child.id] || []
|
|
157
214
|
grandchildren.each_with_index do |grandchild, grandchild_idx|
|
|
158
215
|
result << ' ' if grandchild_idx > 0 || !child.declarations.empty?
|
|
159
216
|
|
|
160
|
-
nested_grandchild_selector =
|
|
217
|
+
nested_grandchild_selector = _reconstruct_nested_selector(
|
|
161
218
|
child.selector,
|
|
162
219
|
grandchild.selector,
|
|
163
220
|
grandchild.nesting_style
|
|
164
221
|
)
|
|
165
222
|
|
|
166
223
|
result << "#{nested_grandchild_selector} { "
|
|
167
|
-
|
|
224
|
+
_serialize_declarations(result, grandchild.declarations)
|
|
168
225
|
result << ' }'
|
|
169
226
|
end
|
|
170
227
|
|
|
@@ -178,7 +235,7 @@ module Cataract
|
|
|
178
235
|
# Reconstruct nested selector representation
|
|
179
236
|
# If nesting_style == 1 (explicit), try to use & notation
|
|
180
237
|
# If nesting_style == 0 (implicit), use plain selector
|
|
181
|
-
def self.
|
|
238
|
+
def self._reconstruct_nested_selector(parent_selector, child_selector, nesting_style)
|
|
182
239
|
return child_selector if nesting_style.nil?
|
|
183
240
|
|
|
184
241
|
if nesting_style == 1 # NESTING_STYLE_EXPLICIT
|
|
@@ -204,7 +261,7 @@ module Cataract
|
|
|
204
261
|
|
|
205
262
|
# Helper: find all selectors from same list with matching declarations
|
|
206
263
|
# Returns array of selectors that can be grouped, marks rules as processed
|
|
207
|
-
def self.
|
|
264
|
+
def self._find_groupable_selectors(rule:, rules:, selector_lists:, processed_rule_ids:, current_media_query_id:)
|
|
208
265
|
list_id = rule.selector_list_id
|
|
209
266
|
rule_ids_in_list = selector_lists[list_id]
|
|
210
267
|
|
|
@@ -217,16 +274,16 @@ module Cataract
|
|
|
217
274
|
# Find all rules in this list that have identical declarations AND same media context
|
|
218
275
|
matching_selectors = []
|
|
219
276
|
rule_ids_in_list.each do |rid|
|
|
220
|
-
#
|
|
221
|
-
other_rule = rules
|
|
277
|
+
# Direct array access (O(1)) - rules[i].id == i invariant is guaranteed by parser
|
|
278
|
+
other_rule = rules[rid]
|
|
222
279
|
next unless other_rule
|
|
223
280
|
next if processed_rule_ids[rid]
|
|
224
281
|
|
|
225
|
-
# Check same media context
|
|
226
|
-
next if
|
|
282
|
+
# Check same media context (compare media_query_id directly)
|
|
283
|
+
next if other_rule.media_query_id != current_media_query_id
|
|
227
284
|
|
|
228
285
|
# Check declarations match (compare arrays directly for performance)
|
|
229
|
-
if
|
|
286
|
+
if _declarations_equal?(rule.declarations, other_rule.declarations)
|
|
230
287
|
matching_selectors << other_rule.selector
|
|
231
288
|
processed_rule_ids[rid] = true
|
|
232
289
|
end
|
|
@@ -239,7 +296,6 @@ module Cataract
|
|
|
239
296
|
# All formatting behavior controlled by kwargs to avoid mode flags and if/else branches
|
|
240
297
|
def self._serialize_stylesheet_with_grouping(
|
|
241
298
|
rules:,
|
|
242
|
-
media_index:,
|
|
243
299
|
result:,
|
|
244
300
|
selector_lists:,
|
|
245
301
|
opening_brace:, # ' { ' (compact) vs " {\n" (formatted)
|
|
@@ -247,23 +303,24 @@ module Cataract
|
|
|
247
303
|
media_indent:, # '' (compact) vs ' ' (formatted)
|
|
248
304
|
decl_indent_base:, # nil (compact) vs ' ' (formatted base rules)
|
|
249
305
|
decl_indent_media:, # nil (compact) vs ' ' (formatted media rules)
|
|
250
|
-
add_blank_lines
|
|
306
|
+
add_blank_lines:, # false (compact) vs true (formatted)
|
|
307
|
+
media_queries: [], # Array of MediaQuery objects
|
|
308
|
+
media_query_lists: {} # Hash: list_id => array of MediaQuery IDs
|
|
251
309
|
)
|
|
252
310
|
grouping_enabled = selector_lists && !selector_lists.empty?
|
|
253
311
|
|
|
254
|
-
# Build
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
rule_to_media[rule_id] = media_sym
|
|
259
|
-
end
|
|
312
|
+
# Build reverse map: media_query_id => list_id
|
|
313
|
+
mq_id_to_list_id = {}
|
|
314
|
+
media_query_lists.each do |list_id, mq_ids|
|
|
315
|
+
mq_ids.each { |mq_id| mq_id_to_list_id[mq_id] = list_id }
|
|
260
316
|
end
|
|
261
317
|
|
|
262
318
|
# Track processed rules to avoid duplicates when grouping
|
|
263
319
|
processed_rule_ids = {}
|
|
264
320
|
|
|
265
321
|
# Iterate through rules in insertion order, grouping consecutive media queries
|
|
266
|
-
|
|
322
|
+
current_media_query_list_id = nil
|
|
323
|
+
current_media_query = nil
|
|
267
324
|
in_media_block = false
|
|
268
325
|
rule_index = 0
|
|
269
326
|
|
|
@@ -271,15 +328,18 @@ module Cataract
|
|
|
271
328
|
# Skip if already processed (when grouped)
|
|
272
329
|
next if processed_rule_ids[rule.id]
|
|
273
330
|
|
|
274
|
-
|
|
331
|
+
rule_media_query_id = rule.is_a?(Rule) ? rule.media_query_id : nil
|
|
332
|
+
rule_media_query = rule_media_query_id ? media_queries[rule_media_query_id] : nil
|
|
333
|
+
rule_media_query_list_id = rule_media_query_id ? mq_id_to_list_id[rule_media_query_id] : nil
|
|
275
334
|
is_first_rule = (rule_index == 0)
|
|
276
335
|
|
|
277
|
-
if
|
|
336
|
+
if rule_media_query.nil?
|
|
278
337
|
# Not in any media query - close any open media block first
|
|
279
338
|
if in_media_block
|
|
280
339
|
result << "}\n"
|
|
281
340
|
in_media_block = false
|
|
282
|
-
|
|
341
|
+
current_media_query = nil
|
|
342
|
+
current_media_query_list_id = nil
|
|
283
343
|
end
|
|
284
344
|
|
|
285
345
|
# Add blank line prefix for non-first rules (formatted only)
|
|
@@ -287,35 +347,44 @@ module Cataract
|
|
|
287
347
|
|
|
288
348
|
# Try to group with other rules from same selector list
|
|
289
349
|
if grouping_enabled && rule.is_a?(Rule) && rule.selector_list_id
|
|
290
|
-
selectors =
|
|
350
|
+
selectors = _find_groupable_selectors(
|
|
291
351
|
rule: rule,
|
|
292
352
|
rules: rules,
|
|
293
353
|
selector_lists: selector_lists,
|
|
294
354
|
processed_rule_ids: processed_rule_ids,
|
|
295
|
-
|
|
296
|
-
current_media: rule_media
|
|
355
|
+
current_media_query_id: rule_media_query_id
|
|
297
356
|
)
|
|
298
357
|
|
|
299
358
|
# Serialize with grouped selectors
|
|
300
359
|
result << selectors.join(', ') << opening_brace
|
|
301
360
|
if decl_indent_base
|
|
302
|
-
|
|
361
|
+
_serialize_declarations_formatted(result, rule.declarations, decl_indent_base)
|
|
303
362
|
else
|
|
304
|
-
|
|
363
|
+
_serialize_declarations(result, rule.declarations)
|
|
305
364
|
end
|
|
306
365
|
result << closing_brace
|
|
307
366
|
else
|
|
308
367
|
# Serialize individual rule
|
|
309
368
|
if decl_indent_base
|
|
310
|
-
|
|
369
|
+
_serialize_rule_formatted(result, rule, '', true)
|
|
311
370
|
else
|
|
312
|
-
|
|
371
|
+
_serialize_rule(result, rule)
|
|
313
372
|
end
|
|
314
373
|
processed_rule_ids[rule.id] = true
|
|
315
374
|
end
|
|
316
375
|
else
|
|
317
376
|
# This rule is in a media query
|
|
318
|
-
|
|
377
|
+
# For lists: compare list_id
|
|
378
|
+
# For single queries: compare by content (type + conditions)
|
|
379
|
+
needs_new_block = if rule_media_query_list_id
|
|
380
|
+
current_media_query_list_id != rule_media_query_list_id
|
|
381
|
+
else
|
|
382
|
+
!current_media_query ||
|
|
383
|
+
current_media_query.type != rule_media_query.type ||
|
|
384
|
+
current_media_query.conditions != rule_media_query.conditions
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
if needs_new_block
|
|
319
388
|
# Close previous media block if open
|
|
320
389
|
if in_media_block
|
|
321
390
|
result << "}\n"
|
|
@@ -325,36 +394,39 @@ module Cataract
|
|
|
325
394
|
result << "\n" if add_blank_lines && !is_first_rule
|
|
326
395
|
|
|
327
396
|
# Open new media block
|
|
328
|
-
|
|
329
|
-
|
|
397
|
+
current_media_query = rule_media_query
|
|
398
|
+
current_media_query_list_id = rule_media_query_list_id
|
|
399
|
+
|
|
400
|
+
# Serialize the media query (or list)
|
|
401
|
+
media_query_string = _build_media_query_string(rule_media_query, rule_media_query_list_id, media_query_lists, media_queries)
|
|
402
|
+
result << "@media #{media_query_string} {\n"
|
|
330
403
|
in_media_block = true
|
|
331
404
|
end
|
|
332
405
|
|
|
333
406
|
# Try to group with other rules from same selector list
|
|
334
407
|
if grouping_enabled && rule.is_a?(Rule) && rule.selector_list_id
|
|
335
|
-
selectors =
|
|
408
|
+
selectors = _find_groupable_selectors(
|
|
336
409
|
rule: rule,
|
|
337
410
|
rules: rules,
|
|
338
411
|
selector_lists: selector_lists,
|
|
339
412
|
processed_rule_ids: processed_rule_ids,
|
|
340
|
-
|
|
341
|
-
current_media: rule_media
|
|
413
|
+
current_media_query_id: rule_media_query_id
|
|
342
414
|
)
|
|
343
415
|
|
|
344
416
|
# Serialize with grouped selectors (with media indent)
|
|
345
417
|
result << media_indent << selectors.join(', ') << opening_brace
|
|
346
418
|
if decl_indent_media
|
|
347
|
-
|
|
419
|
+
_serialize_declarations_formatted(result, rule.declarations, decl_indent_media)
|
|
348
420
|
else
|
|
349
|
-
|
|
421
|
+
_serialize_declarations(result, rule.declarations)
|
|
350
422
|
end
|
|
351
423
|
result << media_indent << closing_brace
|
|
352
424
|
else
|
|
353
425
|
# Serialize individual rule inside media block
|
|
354
426
|
if decl_indent_media
|
|
355
|
-
|
|
427
|
+
_serialize_rule_formatted(result, rule, media_indent, true)
|
|
356
428
|
else
|
|
357
|
-
|
|
429
|
+
_serialize_rule(result, rule)
|
|
358
430
|
end
|
|
359
431
|
processed_rule_ids[rule.id] = true
|
|
360
432
|
end
|
|
@@ -373,7 +445,7 @@ module Cataract
|
|
|
373
445
|
private_class_method :_serialize_stylesheet_with_grouping
|
|
374
446
|
|
|
375
447
|
# Helper: check if two declaration arrays are equal
|
|
376
|
-
def self.
|
|
448
|
+
def self._declarations_equal?(decls1, decls2)
|
|
377
449
|
return false if decls1.size != decls2.size
|
|
378
450
|
|
|
379
451
|
decls1.each_with_index do |d1, i|
|
|
@@ -387,21 +459,21 @@ module Cataract
|
|
|
387
459
|
end
|
|
388
460
|
|
|
389
461
|
# Helper: serialize a single rule
|
|
390
|
-
def self.
|
|
462
|
+
def self._serialize_rule(result, rule)
|
|
391
463
|
# Check if this is an AtRule
|
|
392
464
|
if rule.is_a?(AtRule)
|
|
393
|
-
|
|
465
|
+
_serialize_at_rule(result, rule)
|
|
394
466
|
return
|
|
395
467
|
end
|
|
396
468
|
|
|
397
469
|
# Regular Rule serialization
|
|
398
470
|
result << "#{rule.selector} { "
|
|
399
|
-
|
|
471
|
+
_serialize_declarations(result, rule.declarations)
|
|
400
472
|
result << " }\n"
|
|
401
473
|
end
|
|
402
474
|
|
|
403
475
|
# Helper: serialize declarations (compact, single line)
|
|
404
|
-
def self.
|
|
476
|
+
def self._serialize_declarations(result, declarations)
|
|
405
477
|
declarations.each_with_index do |decl, i|
|
|
406
478
|
important_suffix = decl.important ? ' !important;' : ';'
|
|
407
479
|
separator = i < declarations.length - 1 ? ' ' : ''
|
|
@@ -410,7 +482,7 @@ module Cataract
|
|
|
410
482
|
end
|
|
411
483
|
|
|
412
484
|
# Helper: serialize declarations (formatted, one per line)
|
|
413
|
-
def self.
|
|
485
|
+
def self._serialize_declarations_formatted(result, declarations, indent)
|
|
414
486
|
declarations.each do |decl|
|
|
415
487
|
result << indent
|
|
416
488
|
result << decl.property
|
|
@@ -426,7 +498,7 @@ module Cataract
|
|
|
426
498
|
end
|
|
427
499
|
|
|
428
500
|
# Helper: serialize an at-rule (@keyframes, @font-face, etc)
|
|
429
|
-
def self.
|
|
501
|
+
def self._serialize_at_rule(result, at_rule)
|
|
430
502
|
result << "#{at_rule.selector} {\n"
|
|
431
503
|
|
|
432
504
|
# Check if content is rules or declarations
|
|
@@ -437,13 +509,13 @@ module Cataract
|
|
|
437
509
|
# Serialize as nested rules (e.g., @keyframes)
|
|
438
510
|
at_rule.content.each do |nested_rule|
|
|
439
511
|
result << " #{nested_rule.selector} { "
|
|
440
|
-
|
|
512
|
+
_serialize_declarations(result, nested_rule.declarations)
|
|
441
513
|
result << " }\n"
|
|
442
514
|
end
|
|
443
515
|
else
|
|
444
516
|
# Serialize as declarations (e.g., @font-face)
|
|
445
517
|
result << ' '
|
|
446
|
-
|
|
518
|
+
_serialize_declarations(result, at_rule.content)
|
|
447
519
|
result << "\n"
|
|
448
520
|
end
|
|
449
521
|
end
|
|
@@ -454,12 +526,13 @@ module Cataract
|
|
|
454
526
|
# Serialize stylesheet to formatted CSS string (with indentation)
|
|
455
527
|
#
|
|
456
528
|
# @param rules [Array<Rule>] Array of rules
|
|
457
|
-
# @param media_index [Hash] Media query symbol => array of rule IDs
|
|
458
529
|
# @param charset [String, nil] @charset value
|
|
459
530
|
# @param has_nesting [Boolean] Whether any nested rules exist
|
|
460
531
|
# @param selector_lists [Hash] Selector list ID => array of rule IDs (for grouping)
|
|
532
|
+
# @param media_queries [Array<MediaQuery>] Array of MediaQuery objects (optional, for proper serialization)
|
|
533
|
+
# @param media_query_lists [Hash] List ID => array of MediaQuery IDs (optional, for comma-separated queries)
|
|
461
534
|
# @return [String] Formatted CSS string
|
|
462
|
-
def self.
|
|
535
|
+
def self.stylesheet_to_formatted_s(rules, charset, has_nesting, selector_lists = {}, media_queries = [], media_query_lists = {})
|
|
463
536
|
result = +''
|
|
464
537
|
|
|
465
538
|
# Add @charset if present
|
|
@@ -469,7 +542,7 @@ module Cataract
|
|
|
469
542
|
|
|
470
543
|
# Fast path: no nesting - use simple algorithm
|
|
471
544
|
unless has_nesting
|
|
472
|
-
return
|
|
545
|
+
return _stylesheet_to_formatted_s_without_nesting(rules, result, selector_lists, media_queries, media_query_lists)
|
|
473
546
|
end
|
|
474
547
|
|
|
475
548
|
# Build parent-child relationships
|
|
@@ -482,47 +555,62 @@ module Cataract
|
|
|
482
555
|
rule_children[parent_id] << rule
|
|
483
556
|
end
|
|
484
557
|
|
|
485
|
-
# Build
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
rule_to_media[rule_id] = media_sym
|
|
490
|
-
end
|
|
558
|
+
# Build reverse map: media_query_id => list_id
|
|
559
|
+
mq_id_to_list_id = {}
|
|
560
|
+
media_query_lists.each do |list_id, mq_ids|
|
|
561
|
+
mq_ids.each { |mq_id| mq_id_to_list_id[mq_id] = list_id }
|
|
491
562
|
end
|
|
492
563
|
|
|
493
564
|
# Serialize top-level rules only
|
|
494
|
-
|
|
565
|
+
current_media_query_list_id = nil
|
|
566
|
+
current_media_query = nil
|
|
495
567
|
in_media_block = false
|
|
496
568
|
|
|
497
569
|
rules.each do |rule|
|
|
498
570
|
next if rule.parent_rule_id
|
|
499
571
|
|
|
500
|
-
|
|
572
|
+
rule_media_query_id = rule.is_a?(Rule) ? rule.media_query_id : nil
|
|
573
|
+
rule_media_query = rule_media_query_id ? media_queries[rule_media_query_id] : nil
|
|
574
|
+
rule_media_query_list_id = rule_media_query_id ? mq_id_to_list_id[rule_media_query_id] : nil
|
|
501
575
|
|
|
502
|
-
if
|
|
576
|
+
if rule_media_query.nil?
|
|
503
577
|
if in_media_block
|
|
504
578
|
result << "}\n"
|
|
505
579
|
in_media_block = false
|
|
506
|
-
|
|
580
|
+
current_media_query = nil
|
|
581
|
+
current_media_query_list_id = nil
|
|
507
582
|
end
|
|
508
583
|
|
|
509
584
|
# Add blank line before base rule if we just closed a media block (ends with "}\n")
|
|
510
585
|
result << "\n" if result.length > 1 && result.getbyte(-1) == BYTE_NEWLINE && result.getbyte(-2) == BYTE_RBRACE
|
|
511
586
|
|
|
512
|
-
|
|
587
|
+
_serialize_rule_with_nesting_formatted(result, rule, rule_children, '', media_queries)
|
|
513
588
|
else
|
|
514
|
-
|
|
589
|
+
# For lists: compare list_id
|
|
590
|
+
# For single queries: compare by content (type + conditions)
|
|
591
|
+
needs_new_block = if rule_media_query_list_id
|
|
592
|
+
current_media_query_list_id != rule_media_query_list_id
|
|
593
|
+
else
|
|
594
|
+
!current_media_query ||
|
|
595
|
+
current_media_query.type != rule_media_query.type ||
|
|
596
|
+
current_media_query.conditions != rule_media_query.conditions
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
if needs_new_block
|
|
515
600
|
if in_media_block
|
|
516
601
|
result << "}\n"
|
|
517
602
|
elsif result.length > 0
|
|
518
603
|
result << "\n"
|
|
519
604
|
end
|
|
520
|
-
|
|
521
|
-
|
|
605
|
+
current_media_query = rule_media_query
|
|
606
|
+
current_media_query_list_id = rule_media_query_list_id
|
|
607
|
+
# Serialize the media query (or list)
|
|
608
|
+
media_query_string = _build_media_query_string(rule_media_query, rule_media_query_list_id, media_query_lists, media_queries)
|
|
609
|
+
result << "@media #{media_query_string} {\n"
|
|
522
610
|
in_media_block = true
|
|
523
611
|
end
|
|
524
612
|
|
|
525
|
-
|
|
613
|
+
_serialize_rule_with_nesting_formatted(result, rule, rule_children, ' ', media_queries)
|
|
526
614
|
end
|
|
527
615
|
end
|
|
528
616
|
|
|
@@ -534,12 +622,13 @@ module Cataract
|
|
|
534
622
|
end
|
|
535
623
|
|
|
536
624
|
# Helper: formatted serialization without nesting support
|
|
537
|
-
def self.
|
|
625
|
+
def self._stylesheet_to_formatted_s_without_nesting(rules, result, selector_lists, media_queries = [], media_query_lists = {})
|
|
538
626
|
_serialize_stylesheet_with_grouping(
|
|
539
627
|
rules: rules,
|
|
540
|
-
media_index: media_index,
|
|
541
628
|
result: result,
|
|
542
629
|
selector_lists: selector_lists,
|
|
630
|
+
media_queries: media_queries,
|
|
631
|
+
media_query_lists: media_query_lists,
|
|
543
632
|
opening_brace: " {\n",
|
|
544
633
|
closing_brace: "}\n",
|
|
545
634
|
media_indent: ' ',
|
|
@@ -550,7 +639,7 @@ module Cataract
|
|
|
550
639
|
end
|
|
551
640
|
|
|
552
641
|
# Helper: serialize a rule with nested children (formatted)
|
|
553
|
-
def self.
|
|
642
|
+
def self._serialize_rule_with_nesting_formatted(result, rule, rule_children, indent, media_queries)
|
|
554
643
|
# Selector line with opening brace
|
|
555
644
|
result << indent
|
|
556
645
|
result << rule.selector
|
|
@@ -558,7 +647,7 @@ module Cataract
|
|
|
558
647
|
|
|
559
648
|
# Serialize declarations (one per line)
|
|
560
649
|
unless rule.declarations.empty?
|
|
561
|
-
|
|
650
|
+
_serialize_declarations_formatted(result, rule.declarations, "#{indent} ")
|
|
562
651
|
end
|
|
563
652
|
|
|
564
653
|
# Get nested children
|
|
@@ -566,22 +655,27 @@ module Cataract
|
|
|
566
655
|
|
|
567
656
|
# Serialize nested children
|
|
568
657
|
children.each do |child|
|
|
569
|
-
nested_selector =
|
|
658
|
+
nested_selector = _reconstruct_nested_selector(rule.selector, child.selector, child.nesting_style)
|
|
570
659
|
|
|
571
|
-
if child.nesting_style.nil? &&
|
|
660
|
+
if child.nesting_style.nil? && child.media_query_id && media_queries[child.media_query_id]
|
|
572
661
|
# Nested @media
|
|
573
|
-
|
|
662
|
+
mq = media_queries[child.media_query_id]
|
|
663
|
+
media_query_string = if mq.conditions
|
|
664
|
+
mq.type == :all ? mq.conditions : "#{mq.type} and #{mq.conditions}"
|
|
665
|
+
else
|
|
666
|
+
mq.type.to_s
|
|
667
|
+
end
|
|
574
668
|
result << indent
|
|
575
|
-
result << " @media #{
|
|
669
|
+
result << " @media #{media_query_string} {\n"
|
|
576
670
|
|
|
577
671
|
unless child.declarations.empty?
|
|
578
|
-
|
|
672
|
+
_serialize_declarations_formatted(result, child.declarations, "#{indent} ")
|
|
579
673
|
end
|
|
580
674
|
|
|
581
675
|
# Recursively handle media children
|
|
582
676
|
media_children = rule_children[child.id] || []
|
|
583
677
|
media_children.each do |media_child|
|
|
584
|
-
nested_media_selector =
|
|
678
|
+
nested_media_selector = _reconstruct_nested_selector(
|
|
585
679
|
child.selector,
|
|
586
680
|
media_child.selector,
|
|
587
681
|
media_child.nesting_style
|
|
@@ -590,7 +684,7 @@ module Cataract
|
|
|
590
684
|
result << indent
|
|
591
685
|
result << " #{nested_media_selector} {\n"
|
|
592
686
|
unless media_child.declarations.empty?
|
|
593
|
-
|
|
687
|
+
_serialize_declarations_formatted(result, media_child.declarations, "#{indent} ")
|
|
594
688
|
end
|
|
595
689
|
result << indent
|
|
596
690
|
result << " }\n"
|
|
@@ -604,13 +698,13 @@ module Cataract
|
|
|
604
698
|
result << " #{nested_selector} {\n"
|
|
605
699
|
|
|
606
700
|
unless child.declarations.empty?
|
|
607
|
-
|
|
701
|
+
_serialize_declarations_formatted(result, child.declarations, "#{indent} ")
|
|
608
702
|
end
|
|
609
703
|
|
|
610
704
|
# Recursively handle grandchildren
|
|
611
705
|
grandchildren = rule_children[child.id] || []
|
|
612
706
|
grandchildren.each do |grandchild|
|
|
613
|
-
nested_grandchild_selector =
|
|
707
|
+
nested_grandchild_selector = _reconstruct_nested_selector(
|
|
614
708
|
child.selector,
|
|
615
709
|
grandchild.selector,
|
|
616
710
|
grandchild.nesting_style
|
|
@@ -619,7 +713,7 @@ module Cataract
|
|
|
619
713
|
result << indent
|
|
620
714
|
result << " #{nested_grandchild_selector} {\n"
|
|
621
715
|
unless grandchild.declarations.empty?
|
|
622
|
-
|
|
716
|
+
_serialize_declarations_formatted(result, grandchild.declarations, "#{indent} ")
|
|
623
717
|
end
|
|
624
718
|
result << indent
|
|
625
719
|
result << " }\n"
|
|
@@ -636,10 +730,10 @@ module Cataract
|
|
|
636
730
|
end
|
|
637
731
|
|
|
638
732
|
# Helper: serialize a single rule with formatting
|
|
639
|
-
def self.
|
|
733
|
+
def self._serialize_rule_formatted(result, rule, indent, is_last_rule = false)
|
|
640
734
|
# Check if this is an AtRule
|
|
641
735
|
if rule.is_a?(AtRule)
|
|
642
|
-
|
|
736
|
+
_serialize_at_rule_formatted(result, rule, indent)
|
|
643
737
|
return
|
|
644
738
|
end
|
|
645
739
|
|
|
@@ -650,7 +744,7 @@ module Cataract
|
|
|
650
744
|
result << " {\n"
|
|
651
745
|
|
|
652
746
|
# Declarations (one per line)
|
|
653
|
-
|
|
747
|
+
_serialize_declarations_formatted(result, rule.declarations, "#{indent} ")
|
|
654
748
|
|
|
655
749
|
# Closing brace - double newline for all except last rule
|
|
656
750
|
result << indent
|
|
@@ -658,7 +752,7 @@ module Cataract
|
|
|
658
752
|
end
|
|
659
753
|
|
|
660
754
|
# Helper: serialize an at-rule with formatting
|
|
661
|
-
def self.
|
|
755
|
+
def self._serialize_at_rule_formatted(result, at_rule, indent)
|
|
662
756
|
result << indent
|
|
663
757
|
result << at_rule.selector
|
|
664
758
|
result << " {\n"
|
|
@@ -677,7 +771,7 @@ module Cataract
|
|
|
677
771
|
result << " {\n"
|
|
678
772
|
|
|
679
773
|
# Declarations (one per line, 4-space indent)
|
|
680
|
-
|
|
774
|
+
_serialize_declarations_formatted(result, nested_rule.declarations, "#{indent} ")
|
|
681
775
|
|
|
682
776
|
# Closing brace (2-space indent)
|
|
683
777
|
result << indent
|
|
@@ -685,11 +779,18 @@ module Cataract
|
|
|
685
779
|
end
|
|
686
780
|
else
|
|
687
781
|
# Serialize as declarations (e.g., @font-face, one per line)
|
|
688
|
-
|
|
782
|
+
_serialize_declarations_formatted(result, at_rule.content, "#{indent} ")
|
|
689
783
|
end
|
|
690
784
|
end
|
|
691
785
|
|
|
692
786
|
result << indent
|
|
693
787
|
result << "}\n"
|
|
694
788
|
end
|
|
789
|
+
|
|
790
|
+
# Mark helper methods as private (public APIs: stylesheet_to_s, stylesheet_to_formatted_s)
|
|
791
|
+
private_class_method :_build_media_query_string, :_stylesheet_to_s_without_nesting, :_serialize_rule_with_nesting,
|
|
792
|
+
:_reconstruct_nested_selector, :_find_groupable_selectors, :_declarations_equal?,
|
|
793
|
+
:_serialize_rule, :_serialize_declarations, :_serialize_declarations_formatted,
|
|
794
|
+
:_serialize_at_rule, :_stylesheet_to_formatted_s_without_nesting, :_serialize_rule_with_nesting_formatted,
|
|
795
|
+
:_serialize_rule_formatted, :_serialize_at_rule_formatted
|
|
695
796
|
end
|