cataract 0.2.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +6 -6
- data/.gitignore +17 -6
- data/.rubocop_todo.yml +11 -19
- data/BENCHMARKS.md +40 -40
- data/CHANGELOG.md +28 -1
- data/Gemfile +2 -2
- data/examples/css_analyzer/analyzer.rb +9 -3
- data/examples/css_analyzer/analyzers/base.rb +1 -1
- data/ext/cataract/cataract.c +312 -550
- data/ext/cataract/cataract.h +126 -16
- data/ext/cataract/css_parser.c +782 -806
- data/ext/cataract/extconf.rb +1 -2
- data/ext/cataract/flatten.c +279 -991
- data/ext/cataract/shorthand_expander.c +80 -102
- data/ext/cataract_color/color_conversion.c +1 -1
- data/lib/cataract/declarations.rb +26 -10
- data/lib/cataract/import_resolver.rb +36 -16
- data/lib/cataract/native.rb +30 -0
- data/lib/cataract/pure/byte_constants.rb +70 -67
- data/lib/cataract/pure/declarations.rb +125 -0
- data/lib/cataract/pure/flatten.rb +1195 -1182
- data/lib/cataract/pure/parser.rb +1721 -1652
- data/lib/cataract/pure/serializer.rb +575 -710
- data/lib/cataract/pure/specificity.rb +152 -175
- data/lib/cataract/pure.rb +73 -108
- data/lib/cataract/rule.rb +6 -3
- data/lib/cataract/stylesheet.rb +475 -415
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +41 -30
- data/lib/tasks/profile.rake +6 -3
- metadata +4 -14
- data/ext/cataract/import_scanner.c +0 -174
- data/ext/cataract_old/cataract.c +0 -393
- data/ext/cataract_old/cataract.h +0 -250
- data/ext/cataract_old/css_parser.c +0 -933
- data/ext/cataract_old/extconf.rb +0 -67
- data/ext/cataract_old/import_scanner.c +0 -174
- data/ext/cataract_old/merge.c +0 -776
- data/ext/cataract_old/shorthand_expander.c +0 -902
- data/ext/cataract_old/specificity.c +0 -213
- data/ext/cataract_old/stylesheet.c +0 -290
- data/ext/cataract_old/value_splitter.c +0 -116
- data/lib/cataract/pure/helpers.rb +0 -35
|
@@ -1,796 +1,661 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Pure Ruby CSS parser - Serialization
|
|
3
|
+
# Pure Ruby CSS parser - Serialization
|
|
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.
|
|
10
5
|
|
|
11
6
|
module Cataract
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
7
|
+
module Backends
|
|
8
|
+
class PureImpl
|
|
9
|
+
# Serializes one stylesheet's rules to a CSS string, compact or formatted.
|
|
10
|
+
# One instance per #stylesheet_to_s / #stylesheet_to_formatted_s call - the
|
|
11
|
+
# accumulating result, the rules/media being serialized, and the
|
|
12
|
+
# compact-vs-formatted knobs all live on ivars instead of being threaded
|
|
13
|
+
# through every helper as parameters.
|
|
14
|
+
class Serializer
|
|
15
|
+
def initialize(rules, charset, has_nesting, selector_lists, media_queries, media_query_lists, formatted:)
|
|
16
|
+
@rules = rules
|
|
17
|
+
@charset = charset
|
|
18
|
+
@has_nesting = has_nesting
|
|
19
|
+
@selector_lists = selector_lists || {}
|
|
20
|
+
@media_queries = media_queries || []
|
|
21
|
+
@media_query_lists = media_query_lists || {}
|
|
22
|
+
@formatted = formatted
|
|
23
|
+
@result = +''
|
|
24
|
+
|
|
25
|
+
if formatted
|
|
26
|
+
@opening_brace = " {\n"
|
|
27
|
+
@closing_brace = "}\n"
|
|
28
|
+
@media_indent = ' '
|
|
29
|
+
@decl_indent_base = ' '
|
|
30
|
+
@decl_indent_media = ' '
|
|
31
|
+
@add_blank_lines = true
|
|
32
|
+
else
|
|
33
|
+
@opening_brace = ' { '
|
|
34
|
+
@closing_brace = " }\n"
|
|
35
|
+
@media_indent = ''
|
|
36
|
+
@decl_indent_base = nil
|
|
37
|
+
@decl_indent_media = nil
|
|
38
|
+
@add_blank_lines = false
|
|
39
|
+
end
|
|
29
40
|
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
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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)
|
|
49
|
-
# @return [String] Compact CSS string
|
|
50
|
-
def self.stylesheet_to_s(rules, charset, has_nesting, selector_lists = {}, media_queries = [], media_query_lists = {})
|
|
51
|
-
result = +''
|
|
52
|
-
|
|
53
|
-
# Add @charset if present
|
|
54
|
-
unless charset.nil?
|
|
55
|
-
result << "@charset \"#{charset}\";\n"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Fast path: no nesting - use simple algorithm
|
|
59
|
-
unless has_nesting
|
|
60
|
-
return _stylesheet_to_s_without_nesting(rules, result, selector_lists, media_queries, media_query_lists)
|
|
61
|
-
end
|
|
42
|
+
# Serialize the stylesheet to a CSS string (compact or formatted,
|
|
43
|
+
# depending on how this Serializer was constructed).
|
|
44
|
+
#
|
|
45
|
+
# @return [String] CSS string
|
|
46
|
+
def to_s
|
|
47
|
+
@result << "@charset \"#{@charset}\";\n" unless @charset.nil?
|
|
62
48
|
|
|
63
|
-
|
|
64
|
-
rule_children = {}
|
|
65
|
-
rules.each do |rule|
|
|
66
|
-
next unless rule.parent_rule_id
|
|
67
|
-
|
|
68
|
-
parent_id = rule.parent_rule_id.is_a?(Integer) ? rule.parent_rule_id : rule.parent_rule_id.to_i
|
|
69
|
-
rule_children[parent_id] ||= []
|
|
70
|
-
rule_children[parent_id] << rule
|
|
71
|
-
end
|
|
49
|
+
build_mq_id_to_list_id!
|
|
72
50
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# Serialize top-level rules only (those without parent_rule_id)
|
|
80
|
-
current_media_query_list_id = nil
|
|
81
|
-
current_media_query = nil
|
|
82
|
-
in_media_block = false
|
|
83
|
-
|
|
84
|
-
rules.each do |rule|
|
|
85
|
-
# Skip rules that have a parent (they'll be serialized as nested)
|
|
86
|
-
next if rule.parent_rule_id
|
|
87
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
if rule_media_query.nil?
|
|
93
|
-
# Close any open media block
|
|
94
|
-
if in_media_block
|
|
95
|
-
result << "}\n"
|
|
96
|
-
in_media_block = false
|
|
97
|
-
current_media_query = nil
|
|
98
|
-
current_media_query_list_id = nil
|
|
99
|
-
end
|
|
100
|
-
else
|
|
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
|
|
113
|
-
if in_media_block
|
|
114
|
-
result << "}\n"
|
|
51
|
+
if @has_nesting
|
|
52
|
+
@formatted ? serialize_with_nesting_formatted : serialize_with_nesting_compact
|
|
53
|
+
else
|
|
54
|
+
serialize_with_grouping
|
|
115
55
|
end
|
|
116
|
-
current_media_query = rule_media_query
|
|
117
|
-
current_media_query_list_id = rule_media_query_list_id
|
|
118
56
|
|
|
119
|
-
|
|
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"
|
|
122
|
-
in_media_block = true
|
|
57
|
+
@result
|
|
123
58
|
end
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
_serialize_rule_with_nesting(result, rule, rule_children, media_queries)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
if in_media_block
|
|
130
|
-
result << "}\n"
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
result
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Helper: serialize rules without nesting support (compact format)
|
|
137
|
-
def self._stylesheet_to_s_without_nesting(rules, result, selector_lists, media_queries = [], media_query_lists = {})
|
|
138
|
-
_serialize_stylesheet_with_grouping(
|
|
139
|
-
rules: rules,
|
|
140
|
-
result: result,
|
|
141
|
-
selector_lists: selector_lists,
|
|
142
|
-
media_queries: media_queries,
|
|
143
|
-
media_query_lists: media_query_lists,
|
|
144
|
-
opening_brace: ' { ',
|
|
145
|
-
closing_brace: " }\n",
|
|
146
|
-
media_indent: '',
|
|
147
|
-
decl_indent_base: nil,
|
|
148
|
-
decl_indent_media: nil,
|
|
149
|
-
add_blank_lines: false
|
|
150
|
-
)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Helper: serialize a rule with its nested children
|
|
154
|
-
def self._serialize_rule_with_nesting(result, rule, rule_children, media_queries)
|
|
155
|
-
# Start selector
|
|
156
|
-
result << "#{rule.selector} { "
|
|
157
59
|
|
|
158
|
-
|
|
159
|
-
has_declarations = !rule.declarations.empty?
|
|
160
|
-
if has_declarations
|
|
161
|
-
_serialize_declarations(result, rule.declarations)
|
|
162
|
-
end
|
|
60
|
+
private
|
|
163
61
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
# - Add space between nested rules (not before first if no declarations)
|
|
172
|
-
if has_declarations || index > 0
|
|
173
|
-
result << ' '
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# Determine if we need to reconstruct the nested selector with &
|
|
177
|
-
nested_selector = _reconstruct_nested_selector(rule.selector, child.selector, child.nesting_style)
|
|
178
|
-
|
|
179
|
-
# Check if this child has @media nesting (parent_rule_id present but nesting_style is nil)
|
|
180
|
-
if child.nesting_style.nil? && child.media_query_id && media_queries[child.media_query_id]
|
|
181
|
-
# This is a nested @media rule
|
|
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)
|
|
190
|
-
|
|
191
|
-
# Recursively serialize any children of this @media rule
|
|
192
|
-
media_children = rule_children[child.id] || []
|
|
193
|
-
media_children.each_with_index do |media_child, media_idx|
|
|
194
|
-
result << ' ' if media_idx > 0 || !child.declarations.empty?
|
|
195
|
-
|
|
196
|
-
nested_media_selector = _reconstruct_nested_selector(
|
|
197
|
-
child.selector, media_child.selector,
|
|
198
|
-
media_child.nesting_style
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
result << "#{nested_media_selector} { "
|
|
202
|
-
_serialize_declarations(result, media_child.declarations)
|
|
203
|
-
result << ' }'
|
|
62
|
+
# Build reverse map: media_query_id => list_id (used by both the
|
|
63
|
+
# nesting-aware and selector-list-grouping paths)
|
|
64
|
+
def build_mq_id_to_list_id!
|
|
65
|
+
@mq_id_to_list_id = {}
|
|
66
|
+
@media_query_lists.each do |list_id, mq_ids|
|
|
67
|
+
mq_ids.each { |mq_id| @mq_id_to_list_id[mq_id] = list_id }
|
|
68
|
+
end
|
|
204
69
|
end
|
|
205
70
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
nested_grandchild_selector = _reconstruct_nested_selector(
|
|
218
|
-
child.selector,
|
|
219
|
-
grandchild.selector,
|
|
220
|
-
grandchild.nesting_style
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
result << "#{nested_grandchild_selector} { "
|
|
224
|
-
_serialize_declarations(result, grandchild.declarations)
|
|
225
|
-
result << ' }'
|
|
71
|
+
# Build id => rule lookup (used only by the selector-list-grouping
|
|
72
|
+
# path). @rules is NOT guaranteed to satisfy rules[i].id == i here -
|
|
73
|
+
# that invariant only holds for the full, freshly-parsed rules array;
|
|
74
|
+
# Stylesheet#to_s(media: ...) passes a filtered subset whenever the
|
|
75
|
+
# media filter isn't :all, so a plain array index would silently
|
|
76
|
+
# fetch the wrong rule (or even an AtRule, which has no
|
|
77
|
+
# #declarations) once any rule has been filtered out.
|
|
78
|
+
def build_rules_by_id!
|
|
79
|
+
@rules_by_id = {}
|
|
80
|
+
@rules.each { |rule| @rules_by_id[rule.id] = rule }
|
|
226
81
|
end
|
|
227
82
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
# Reconstruct nested selector representation
|
|
236
|
-
# If nesting_style == 1 (explicit), try to use & notation
|
|
237
|
-
# If nesting_style == 0 (implicit), use plain selector
|
|
238
|
-
def self._reconstruct_nested_selector(parent_selector, child_selector, nesting_style)
|
|
239
|
-
return child_selector if nesting_style.nil?
|
|
240
|
-
|
|
241
|
-
if nesting_style == 1 # NESTING_STYLE_EXPLICIT
|
|
242
|
-
# Try to reconstruct & notation
|
|
243
|
-
# ".parent .child" with parent ".parent" => "& .child"
|
|
244
|
-
# ".parent:hover" with parent ".parent" => "&:hover"
|
|
245
|
-
if child_selector.start_with?(parent_selector)
|
|
246
|
-
rest = child_selector[parent_selector.length..-1]
|
|
247
|
-
return "&#{rest}"
|
|
248
|
-
end
|
|
249
|
-
# More complex cases like ".parent .foo .child"
|
|
250
|
-
child_selector.sub(parent_selector, '&')
|
|
251
|
-
else # NESTING_STYLE_IMPLICIT
|
|
252
|
-
# Remove parent prefix for implicit nesting
|
|
253
|
-
# ".parent .child" with parent ".parent" => ".child"
|
|
254
|
-
if child_selector.start_with?(parent_selector)
|
|
255
|
-
rest = child_selector[parent_selector.length..-1]
|
|
256
|
-
return rest.lstrip
|
|
257
|
-
end
|
|
258
|
-
child_selector
|
|
259
|
-
end
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
# Helper: find all selectors from same list with matching declarations
|
|
263
|
-
# Returns array of selectors that can be grouped, marks rules as processed
|
|
264
|
-
def self._find_groupable_selectors(rule:, rules:, selector_lists:, processed_rule_ids:, current_media_query_id:)
|
|
265
|
-
list_id = rule.selector_list_id
|
|
266
|
-
rule_ids_in_list = selector_lists[list_id]
|
|
267
|
-
|
|
268
|
-
# If no other rules in this list, return just this selector
|
|
269
|
-
if rule_ids_in_list.nil? || rule_ids_in_list.size <= 1
|
|
270
|
-
processed_rule_ids[rule.id] = true
|
|
271
|
-
return [rule.selector]
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Find all rules in this list that have identical declarations AND same media context
|
|
275
|
-
matching_selectors = []
|
|
276
|
-
rule_ids_in_list.each do |rid|
|
|
277
|
-
# Direct array access (O(1)) - rules[i].id == i invariant is guaranteed by parser
|
|
278
|
-
other_rule = rules[rid]
|
|
279
|
-
next unless other_rule
|
|
280
|
-
next if processed_rule_ids[rid]
|
|
281
|
-
|
|
282
|
-
# Check same media context (compare media_query_id directly)
|
|
283
|
-
next if other_rule.media_query_id != current_media_query_id
|
|
284
|
-
|
|
285
|
-
# Check declarations match (compare arrays directly for performance)
|
|
286
|
-
if _declarations_equal?(rule.declarations, other_rule.declarations)
|
|
287
|
-
matching_selectors << other_rule.selector
|
|
288
|
-
processed_rule_ids[rid] = true
|
|
289
|
-
end
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
matching_selectors
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
# Private shared implementation for stylesheet serialization with optional selector list grouping
|
|
296
|
-
# All formatting behavior controlled by kwargs to avoid mode flags and if/else branches
|
|
297
|
-
def self._serialize_stylesheet_with_grouping(
|
|
298
|
-
rules:,
|
|
299
|
-
result:,
|
|
300
|
-
selector_lists:,
|
|
301
|
-
opening_brace:, # ' { ' (compact) vs " {\n" (formatted)
|
|
302
|
-
closing_brace:, # " }\n" (compact) vs "}\n" (formatted)
|
|
303
|
-
media_indent:, # '' (compact) vs ' ' (formatted)
|
|
304
|
-
decl_indent_base:, # nil (compact) vs ' ' (formatted base rules)
|
|
305
|
-
decl_indent_media:, # nil (compact) vs ' ' (formatted media rules)
|
|
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
|
|
309
|
-
)
|
|
310
|
-
grouping_enabled = selector_lists && !selector_lists.empty?
|
|
311
|
-
|
|
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 }
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
# Track processed rules to avoid duplicates when grouping
|
|
319
|
-
processed_rule_ids = {}
|
|
320
|
-
|
|
321
|
-
# Iterate through rules in insertion order, grouping consecutive media queries
|
|
322
|
-
current_media_query_list_id = nil
|
|
323
|
-
current_media_query = nil
|
|
324
|
-
in_media_block = false
|
|
325
|
-
rule_index = 0
|
|
83
|
+
# Build parent-child relationships (used only by the nesting-aware path)
|
|
84
|
+
def build_rule_children!
|
|
85
|
+
@rule_children = {}
|
|
86
|
+
@rules.each do |rule|
|
|
87
|
+
parent_rule_id = rule_parent_id(rule)
|
|
88
|
+
next unless parent_rule_id
|
|
326
89
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
334
|
-
is_first_rule = (rule_index == 0)
|
|
335
|
-
|
|
336
|
-
if rule_media_query.nil?
|
|
337
|
-
# Not in any media query - close any open media block first
|
|
338
|
-
if in_media_block
|
|
339
|
-
result << "}\n"
|
|
340
|
-
in_media_block = false
|
|
341
|
-
current_media_query = nil
|
|
342
|
-
current_media_query_list_id = nil
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# Add blank line prefix for non-first rules (formatted only)
|
|
346
|
-
result << "\n" if add_blank_lines && !is_first_rule
|
|
347
|
-
|
|
348
|
-
# Try to group with other rules from same selector list
|
|
349
|
-
if grouping_enabled && rule.is_a?(Rule) && rule.selector_list_id
|
|
350
|
-
selectors = _find_groupable_selectors(
|
|
351
|
-
rule: rule,
|
|
352
|
-
rules: rules,
|
|
353
|
-
selector_lists: selector_lists,
|
|
354
|
-
processed_rule_ids: processed_rule_ids,
|
|
355
|
-
current_media_query_id: rule_media_query_id
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
# Serialize with grouped selectors
|
|
359
|
-
result << selectors.join(', ') << opening_brace
|
|
360
|
-
if decl_indent_base
|
|
361
|
-
_serialize_declarations_formatted(result, rule.declarations, decl_indent_base)
|
|
362
|
-
else
|
|
363
|
-
_serialize_declarations(result, rule.declarations)
|
|
364
|
-
end
|
|
365
|
-
result << closing_brace
|
|
366
|
-
else
|
|
367
|
-
# Serialize individual rule
|
|
368
|
-
if decl_indent_base
|
|
369
|
-
_serialize_rule_formatted(result, rule, '', true)
|
|
370
|
-
else
|
|
371
|
-
_serialize_rule(result, rule)
|
|
90
|
+
parent_id = parent_rule_id.is_a?(Integer) ? parent_rule_id : parent_rule_id.to_i
|
|
91
|
+
@rule_children[parent_id] ||= []
|
|
92
|
+
@rule_children[parent_id] << rule
|
|
372
93
|
end
|
|
373
|
-
processed_rule_ids[rule.id] = true
|
|
374
94
|
end
|
|
375
|
-
else
|
|
376
|
-
# This rule is in a media query
|
|
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
|
|
388
|
-
# Close previous media block if open
|
|
389
|
-
if in_media_block
|
|
390
|
-
result << "}\n"
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
# Add blank line prefix for non-first rules (formatted only)
|
|
394
|
-
result << "\n" if add_blank_lines && !is_first_rule
|
|
395
95
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
result << "@media #{media_query_string} {\n"
|
|
403
|
-
in_media_block = true
|
|
96
|
+
# Read a rule's parent_rule_id, guarding against AtRule (which has no
|
|
97
|
+
# such member - only Rule participates in CSS nesting, AtRule never is
|
|
98
|
+
# nested via '&' nesting). Calling .parent_rule_id directly on an AtRule
|
|
99
|
+
# raises NoMethodError.
|
|
100
|
+
def rule_parent_id(rule)
|
|
101
|
+
rule.is_a?(Rule) ? rule.parent_rule_id : nil
|
|
404
102
|
end
|
|
405
103
|
|
|
406
|
-
#
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
104
|
+
# Build media query string from a MediaQuery object or list.
|
|
105
|
+
# @param media_query [MediaQuery] The MediaQuery object
|
|
106
|
+
# @param media_query_list_id [Integer, nil] Optional list ID if this is part of a comma-separated list
|
|
107
|
+
# @return [String] e.g. "screen", "screen, print", "screen and (min-width: 768px)"
|
|
108
|
+
def build_media_query_string(media_query, media_query_list_id)
|
|
109
|
+
if media_query_list_id
|
|
110
|
+
mq_ids = @media_query_lists[media_query_list_id]
|
|
111
|
+
mq_ids.map do |mq_id|
|
|
112
|
+
mq = @media_queries[mq_id]
|
|
113
|
+
if mq.conditions
|
|
114
|
+
mq.type == :all ? mq.conditions : "#{mq.type} and #{mq.conditions}"
|
|
115
|
+
else
|
|
116
|
+
mq.type.to_s
|
|
117
|
+
end
|
|
118
|
+
end.join(', ')
|
|
119
|
+
elsif media_query.conditions
|
|
120
|
+
media_query.type == :all ? media_query.conditions : "#{media_query.type} and #{media_query.conditions}"
|
|
420
121
|
else
|
|
421
|
-
|
|
122
|
+
media_query.type.to_s
|
|
422
123
|
end
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Does the rule's media query differ from the currently open media
|
|
127
|
+
# block, requiring a new "@media ... {" to be opened? Comma-separated
|
|
128
|
+
# lists are compared by list id; single queries are compared by content
|
|
129
|
+
# (type + conditions), since two separately-parsed queries with the same
|
|
130
|
+
# content should still be grouped under one block.
|
|
131
|
+
def needs_new_media_block?(current_media_query, current_media_query_list_id, rule_media_query,
|
|
132
|
+
rule_media_query_list_id)
|
|
133
|
+
if rule_media_query_list_id
|
|
134
|
+
current_media_query_list_id != rule_media_query_list_id
|
|
428
135
|
else
|
|
429
|
-
|
|
136
|
+
!current_media_query ||
|
|
137
|
+
current_media_query.type != rule_media_query.type ||
|
|
138
|
+
current_media_query.conditions != rule_media_query.conditions
|
|
430
139
|
end
|
|
431
|
-
processed_rule_ids[rule.id] = true
|
|
432
140
|
end
|
|
433
|
-
end
|
|
434
141
|
|
|
435
|
-
|
|
436
|
-
end
|
|
142
|
+
# ---- Nesting-aware path (has_nesting == true), compact format ----
|
|
437
143
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
result << "}\n"
|
|
441
|
-
end
|
|
144
|
+
def serialize_with_nesting_compact
|
|
145
|
+
build_rule_children!
|
|
442
146
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
147
|
+
current_media_query_list_id = nil
|
|
148
|
+
current_media_query = nil
|
|
149
|
+
in_media_block = false
|
|
446
150
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
151
|
+
@rules.each do |rule|
|
|
152
|
+
next if rule_parent_id(rule)
|
|
153
|
+
|
|
154
|
+
rule_media_query_id = rule.media_query_id
|
|
155
|
+
rule_media_query = rule_media_query_id ? @media_queries[rule_media_query_id] : nil
|
|
156
|
+
rule_media_query_list_id = rule_media_query_id ? @mq_id_to_list_id[rule_media_query_id] : nil
|
|
157
|
+
|
|
158
|
+
if rule_media_query.nil?
|
|
159
|
+
if in_media_block
|
|
160
|
+
@result << "}\n"
|
|
161
|
+
in_media_block = false
|
|
162
|
+
current_media_query = nil
|
|
163
|
+
current_media_query_list_id = nil
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
needs_new_block = needs_new_media_block?(current_media_query, current_media_query_list_id,
|
|
167
|
+
rule_media_query, rule_media_query_list_id)
|
|
168
|
+
|
|
169
|
+
if needs_new_block
|
|
170
|
+
@result << "}\n" if in_media_block
|
|
171
|
+
current_media_query = rule_media_query
|
|
172
|
+
current_media_query_list_id = rule_media_query_list_id
|
|
173
|
+
|
|
174
|
+
media_query_string = build_media_query_string(rule_media_query, rule_media_query_list_id)
|
|
175
|
+
@result << "@media #{media_query_string} {\n"
|
|
176
|
+
in_media_block = true
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
serialize_rule_with_nesting(rule)
|
|
181
|
+
end
|
|
450
182
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return false if d1.property != d2.property
|
|
454
|
-
return false if d1.value != d2.value
|
|
455
|
-
return false if d1.important != d2.important
|
|
456
|
-
end
|
|
183
|
+
@result << "}\n" if in_media_block
|
|
184
|
+
end
|
|
457
185
|
|
|
458
|
-
|
|
459
|
-
|
|
186
|
+
def serialize_rule_with_nesting(rule)
|
|
187
|
+
# AtRule can never have nested children of its own via @rule_children
|
|
188
|
+
# (only Rule participates in CSS nesting), so it always serializes the
|
|
189
|
+
# same way regardless of has_nesting.
|
|
190
|
+
if rule.is_a?(AtRule)
|
|
191
|
+
serialize_at_rule(rule)
|
|
192
|
+
return
|
|
193
|
+
end
|
|
460
194
|
|
|
461
|
-
|
|
462
|
-
def self._serialize_rule(result, rule)
|
|
463
|
-
# Check if this is an AtRule
|
|
464
|
-
if rule.is_a?(AtRule)
|
|
465
|
-
_serialize_at_rule(result, rule)
|
|
466
|
-
return
|
|
467
|
-
end
|
|
195
|
+
@result << "#{rule.selector} { "
|
|
468
196
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
_serialize_declarations(result, rule.declarations)
|
|
472
|
-
result << " }\n"
|
|
473
|
-
end
|
|
197
|
+
has_declarations = !rule.declarations.empty?
|
|
198
|
+
serialize_declarations(rule.declarations) if has_declarations
|
|
474
199
|
|
|
475
|
-
|
|
476
|
-
def self._serialize_declarations(result, declarations)
|
|
477
|
-
declarations.each_with_index do |decl, i|
|
|
478
|
-
important_suffix = decl.important ? ' !important;' : ';'
|
|
479
|
-
separator = i < declarations.length - 1 ? ' ' : ''
|
|
480
|
-
result << "#{decl.property}: #{decl.value}#{important_suffix}#{separator}"
|
|
481
|
-
end
|
|
482
|
-
end
|
|
200
|
+
serialize_children(rule.selector, @rule_children[rule.id] || [], has_declarations)
|
|
483
201
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
declarations.each do |decl|
|
|
487
|
-
result << indent
|
|
488
|
-
result << decl.property
|
|
489
|
-
result << ': '
|
|
490
|
-
result << decl.value
|
|
202
|
+
@result << " }\n"
|
|
203
|
+
end
|
|
491
204
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
205
|
+
# Recursively serialize a rule's nested children. CSS nesting can go as
|
|
206
|
+
# deep as the parser allows (MAX_PARSE_DEPTH), so this recurses rather
|
|
207
|
+
# than hand-unrolling a fixed number of levels - a nested rule's own
|
|
208
|
+
# nested rules are found the same way its parent's were, via
|
|
209
|
+
# @rule_children[id].
|
|
210
|
+
def serialize_children(parent_selector, children, parent_has_declarations)
|
|
211
|
+
children.each_with_index do |child, index|
|
|
212
|
+
# Add space before nested content - always if the parent had
|
|
213
|
+
# declarations, otherwise between nested rules (not before the first).
|
|
214
|
+
@result << ' ' if parent_has_declarations || index > 0
|
|
215
|
+
|
|
216
|
+
if child.nesting_style.nil? && child.media_query_id && @media_queries[child.media_query_id]
|
|
217
|
+
# Nested @media rule (parent_rule_id present but nesting_style is nil)
|
|
218
|
+
mq = @media_queries[child.media_query_id]
|
|
219
|
+
media_query_string = build_media_query_string(mq, nil)
|
|
220
|
+
@result << "@media #{media_query_string} { "
|
|
221
|
+
serialize_declarations(child.declarations)
|
|
222
|
+
|
|
223
|
+
media_children = @rule_children[child.id] || []
|
|
224
|
+
media_children.each_with_index do |media_child, media_idx|
|
|
225
|
+
@result << ' ' if media_idx > 0 || !child.declarations.empty?
|
|
226
|
+
|
|
227
|
+
nested_media_selector = reconstruct_nested_selector(
|
|
228
|
+
child.selector, media_child.selector, media_child.nesting_style
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
@result << "#{nested_media_selector} { "
|
|
232
|
+
serialize_declarations(media_child.declarations)
|
|
233
|
+
@result << ' }'
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
@result << ' }'
|
|
237
|
+
else
|
|
238
|
+
# Regular nested selector - reconstruct it with & if needed
|
|
239
|
+
nested_selector = reconstruct_nested_selector(parent_selector, child.selector, child.nesting_style)
|
|
240
|
+
|
|
241
|
+
@result << "#{nested_selector} { "
|
|
242
|
+
serialize_declarations(child.declarations)
|
|
243
|
+
|
|
244
|
+
serialize_children(child.selector, @rule_children[child.id] || [], !child.declarations.empty?)
|
|
245
|
+
|
|
246
|
+
@result << ' }'
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
495
250
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
251
|
+
# Reconstruct nested selector representation.
|
|
252
|
+
# If nesting_style == 1 (explicit), try to use & notation.
|
|
253
|
+
# If nesting_style == 0 (implicit), use plain selector.
|
|
254
|
+
def reconstruct_nested_selector(parent_selector, child_selector, nesting_style)
|
|
255
|
+
return child_selector if nesting_style.nil?
|
|
256
|
+
|
|
257
|
+
if nesting_style == 1 # NESTING_STYLE_EXPLICIT
|
|
258
|
+
# ".parent .child" with parent ".parent" => "& .child"
|
|
259
|
+
# ".parent:hover" with parent ".parent" => "&:hover"
|
|
260
|
+
if child_selector.start_with?(parent_selector)
|
|
261
|
+
rest = child_selector[parent_selector.length..-1]
|
|
262
|
+
return "&#{rest}"
|
|
263
|
+
end
|
|
264
|
+
# More complex cases like ".parent .foo .child"
|
|
265
|
+
child_selector.sub(parent_selector, '&')
|
|
266
|
+
else # NESTING_STYLE_IMPLICIT
|
|
267
|
+
# ".parent .child" with parent ".parent" => ".child"
|
|
268
|
+
if child_selector.start_with?(parent_selector)
|
|
269
|
+
rest = child_selector[parent_selector.length..-1]
|
|
270
|
+
return rest.lstrip
|
|
271
|
+
end
|
|
272
|
+
child_selector
|
|
273
|
+
end
|
|
274
|
+
end
|
|
499
275
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
276
|
+
# ---- Selector-list-grouping path (has_nesting == false) ----
|
|
277
|
+
# Shared between compact and formatted output - all formatting behavior
|
|
278
|
+
# is controlled by ivars set in #initialize, so there's no mode-flag
|
|
279
|
+
# kwarg pile to keep in sync.
|
|
503
280
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
281
|
+
def serialize_with_grouping
|
|
282
|
+
@processed_rule_ids = {}
|
|
283
|
+
grouping_enabled = @selector_lists && !@selector_lists.empty?
|
|
284
|
+
build_rules_by_id! if grouping_enabled
|
|
507
285
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
286
|
+
current_media_query_list_id = nil
|
|
287
|
+
current_media_query = nil
|
|
288
|
+
in_media_block = false
|
|
289
|
+
rule_index = 0
|
|
290
|
+
|
|
291
|
+
@rules.each do |rule|
|
|
292
|
+
next if @processed_rule_ids[rule.id]
|
|
293
|
+
|
|
294
|
+
rule_media_query_id = rule.media_query_id
|
|
295
|
+
rule_media_query = rule_media_query_id ? @media_queries[rule_media_query_id] : nil
|
|
296
|
+
rule_media_query_list_id = rule_media_query_id ? @mq_id_to_list_id[rule_media_query_id] : nil
|
|
297
|
+
is_first_rule = (rule_index == 0)
|
|
298
|
+
|
|
299
|
+
if rule_media_query.nil?
|
|
300
|
+
if in_media_block
|
|
301
|
+
@result << "}\n"
|
|
302
|
+
in_media_block = false
|
|
303
|
+
current_media_query = nil
|
|
304
|
+
current_media_query_list_id = nil
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
@result << "\n" if @add_blank_lines && !is_first_rule
|
|
308
|
+
|
|
309
|
+
if grouping_enabled && rule.is_a?(Rule) && rule.selector_list_id
|
|
310
|
+
selectors = find_groupable_selectors(rule, rule_media_query_id)
|
|
311
|
+
|
|
312
|
+
@result << selectors.join(', ') << @opening_brace
|
|
313
|
+
if @decl_indent_base
|
|
314
|
+
serialize_declarations_formatted(rule.declarations, @decl_indent_base)
|
|
315
|
+
else
|
|
316
|
+
serialize_declarations(rule.declarations)
|
|
317
|
+
end
|
|
318
|
+
@result << @closing_brace
|
|
319
|
+
else
|
|
320
|
+
if @decl_indent_base
|
|
321
|
+
serialize_rule_formatted(rule, '', true)
|
|
322
|
+
else
|
|
323
|
+
serialize_rule(rule)
|
|
324
|
+
end
|
|
325
|
+
@processed_rule_ids[rule.id] = true
|
|
326
|
+
end
|
|
327
|
+
else
|
|
328
|
+
needs_new_block = needs_new_media_block?(current_media_query, current_media_query_list_id,
|
|
329
|
+
rule_media_query, rule_media_query_list_id)
|
|
330
|
+
|
|
331
|
+
if needs_new_block
|
|
332
|
+
@result << "}\n" if in_media_block
|
|
333
|
+
@result << "\n" if @add_blank_lines && !is_first_rule
|
|
334
|
+
|
|
335
|
+
current_media_query = rule_media_query
|
|
336
|
+
current_media_query_list_id = rule_media_query_list_id
|
|
337
|
+
|
|
338
|
+
media_query_string = build_media_query_string(rule_media_query, rule_media_query_list_id)
|
|
339
|
+
@result << "@media #{media_query_string} {\n"
|
|
340
|
+
in_media_block = true
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
if grouping_enabled && rule.is_a?(Rule) && rule.selector_list_id
|
|
344
|
+
selectors = find_groupable_selectors(rule, rule_media_query_id)
|
|
345
|
+
|
|
346
|
+
@result << @media_indent << selectors.join(', ') << @opening_brace
|
|
347
|
+
if @decl_indent_media
|
|
348
|
+
serialize_declarations_formatted(rule.declarations, @decl_indent_media)
|
|
349
|
+
else
|
|
350
|
+
serialize_declarations(rule.declarations)
|
|
351
|
+
end
|
|
352
|
+
@result << @media_indent << @closing_brace
|
|
353
|
+
else
|
|
354
|
+
if @decl_indent_media
|
|
355
|
+
serialize_rule_formatted(rule, @media_indent, true)
|
|
356
|
+
else
|
|
357
|
+
serialize_rule(rule)
|
|
358
|
+
end
|
|
359
|
+
@processed_rule_ids[rule.id] = true
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
rule_index += 1
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
@result << "}\n" if in_media_block
|
|
514
367
|
end
|
|
515
|
-
else
|
|
516
|
-
# Serialize as declarations (e.g., @font-face)
|
|
517
|
-
result << ' '
|
|
518
|
-
_serialize_declarations(result, at_rule.content)
|
|
519
|
-
result << "\n"
|
|
520
|
-
end
|
|
521
|
-
end
|
|
522
368
|
|
|
523
|
-
|
|
524
|
-
|
|
369
|
+
# Find all selectors from the same selector list with matching
|
|
370
|
+
# declarations (and the same media context). Returns the array of
|
|
371
|
+
# selectors that can be grouped, marking them processed as it goes.
|
|
372
|
+
def find_groupable_selectors(rule, current_media_query_id)
|
|
373
|
+
list_id = rule.selector_list_id
|
|
374
|
+
rule_ids_in_list = @selector_lists[list_id]
|
|
525
375
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
# @param has_nesting [Boolean] Whether any nested rules exist
|
|
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)
|
|
534
|
-
# @return [String] Formatted CSS string
|
|
535
|
-
def self.stylesheet_to_formatted_s(rules, charset, has_nesting, selector_lists = {}, media_queries = [], media_query_lists = {})
|
|
536
|
-
result = +''
|
|
537
|
-
|
|
538
|
-
# Add @charset if present
|
|
539
|
-
unless charset.nil?
|
|
540
|
-
result << "@charset \"#{charset}\";\n"
|
|
541
|
-
end
|
|
376
|
+
if rule_ids_in_list.nil? || rule_ids_in_list.size <= 1
|
|
377
|
+
@processed_rule_ids[rule.id] = true
|
|
378
|
+
return [rule.selector]
|
|
379
|
+
end
|
|
542
380
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
381
|
+
matching_selectors = []
|
|
382
|
+
rule_ids_in_list.each do |rid|
|
|
383
|
+
other_rule = @rules_by_id[rid]
|
|
384
|
+
next unless other_rule
|
|
385
|
+
next if @processed_rule_ids[rid]
|
|
386
|
+
next if other_rule.media_query_id != current_media_query_id
|
|
387
|
+
|
|
388
|
+
if declarations_equal?(rule.declarations, other_rule.declarations)
|
|
389
|
+
matching_selectors << other_rule.selector
|
|
390
|
+
@processed_rule_ids[rid] = true
|
|
391
|
+
end
|
|
392
|
+
end
|
|
547
393
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
rules.each do |rule|
|
|
551
|
-
next unless rule.parent_rule_id
|
|
394
|
+
matching_selectors
|
|
395
|
+
end
|
|
552
396
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
rule_children[parent_id] << rule
|
|
556
|
-
end
|
|
397
|
+
def declarations_equal?(decls1, decls2)
|
|
398
|
+
return false if decls1.size != decls2.size
|
|
557
399
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
400
|
+
decls1.each_with_index do |d1, i|
|
|
401
|
+
d2 = decls2[i]
|
|
402
|
+
return false if d1.property != d2.property
|
|
403
|
+
return false if d1.value != d2.value
|
|
404
|
+
return false if d1.important != d2.important
|
|
405
|
+
end
|
|
563
406
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
current_media_query = nil
|
|
567
|
-
in_media_block = false
|
|
407
|
+
true
|
|
408
|
+
end
|
|
568
409
|
|
|
569
|
-
|
|
570
|
-
|
|
410
|
+
def serialize_rule(rule)
|
|
411
|
+
if rule.is_a?(AtRule)
|
|
412
|
+
serialize_at_rule(rule)
|
|
413
|
+
return
|
|
414
|
+
end
|
|
571
415
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
416
|
+
@result << "#{rule.selector} { "
|
|
417
|
+
serialize_declarations(rule.declarations)
|
|
418
|
+
@result << " }\n"
|
|
419
|
+
end
|
|
575
420
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
421
|
+
# Declarations, compact (single line)
|
|
422
|
+
def serialize_declarations(declarations)
|
|
423
|
+
declarations.each_with_index do |decl, i|
|
|
424
|
+
important_suffix = decl.important ? ' !important;' : ';'
|
|
425
|
+
separator = i < declarations.length - 1 ? ' ' : ''
|
|
426
|
+
@result << "#{decl.property}: #{decl.value}#{important_suffix}#{separator}"
|
|
427
|
+
end
|
|
582
428
|
end
|
|
583
429
|
|
|
584
|
-
#
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
600
|
-
if in_media_block
|
|
601
|
-
result << "}\n"
|
|
602
|
-
elsif result.length > 0
|
|
603
|
-
result << "\n"
|
|
430
|
+
# Declarations, formatted (one per line)
|
|
431
|
+
def serialize_declarations_formatted(declarations, indent)
|
|
432
|
+
declarations.each do |decl|
|
|
433
|
+
@result << indent
|
|
434
|
+
@result << decl.property
|
|
435
|
+
@result << ': '
|
|
436
|
+
@result << decl.value
|
|
437
|
+
@result << ' !important' if decl.important
|
|
438
|
+
@result << ";\n"
|
|
604
439
|
end
|
|
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"
|
|
610
|
-
in_media_block = true
|
|
611
440
|
end
|
|
612
441
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
442
|
+
# An at-rule (@keyframes, @font-face, etc), compact
|
|
443
|
+
def serialize_at_rule(at_rule)
|
|
444
|
+
@result << "#{at_rule.selector} {\n"
|
|
445
|
+
|
|
446
|
+
if at_rule.content.length > 0
|
|
447
|
+
first = at_rule.content[0]
|
|
448
|
+
|
|
449
|
+
if first.is_a?(Rule)
|
|
450
|
+
at_rule.content.each do |nested_rule|
|
|
451
|
+
@result << " #{nested_rule.selector} { "
|
|
452
|
+
serialize_declarations(nested_rule.declarations)
|
|
453
|
+
@result << " }\n"
|
|
454
|
+
end
|
|
455
|
+
else
|
|
456
|
+
@result << ' '
|
|
457
|
+
serialize_declarations(at_rule.content)
|
|
458
|
+
@result << "\n"
|
|
459
|
+
end
|
|
460
|
+
end
|
|
616
461
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
end
|
|
462
|
+
@result << "}\n"
|
|
463
|
+
end
|
|
620
464
|
|
|
621
|
-
|
|
622
|
-
end
|
|
465
|
+
# ---- Nesting-aware path (has_nesting == true), formatted ----
|
|
623
466
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
_serialize_stylesheet_with_grouping(
|
|
627
|
-
rules: rules,
|
|
628
|
-
result: result,
|
|
629
|
-
selector_lists: selector_lists,
|
|
630
|
-
media_queries: media_queries,
|
|
631
|
-
media_query_lists: media_query_lists,
|
|
632
|
-
opening_brace: " {\n",
|
|
633
|
-
closing_brace: "}\n",
|
|
634
|
-
media_indent: ' ',
|
|
635
|
-
decl_indent_base: ' ',
|
|
636
|
-
decl_indent_media: ' ',
|
|
637
|
-
add_blank_lines: true
|
|
638
|
-
)
|
|
639
|
-
end
|
|
467
|
+
def serialize_with_nesting_formatted
|
|
468
|
+
build_rule_children!
|
|
640
469
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
result << indent
|
|
645
|
-
result << rule.selector
|
|
646
|
-
result << " {\n"
|
|
470
|
+
current_media_query_list_id = nil
|
|
471
|
+
current_media_query = nil
|
|
472
|
+
in_media_block = false
|
|
647
473
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
474
|
+
@rules.each do |rule|
|
|
475
|
+
next if rule_parent_id(rule)
|
|
476
|
+
|
|
477
|
+
rule_media_query_id = rule.media_query_id
|
|
478
|
+
rule_media_query = rule_media_query_id ? @media_queries[rule_media_query_id] : nil
|
|
479
|
+
rule_media_query_list_id = rule_media_query_id ? @mq_id_to_list_id[rule_media_query_id] : nil
|
|
480
|
+
|
|
481
|
+
if rule_media_query.nil?
|
|
482
|
+
if in_media_block
|
|
483
|
+
@result << "}\n"
|
|
484
|
+
in_media_block = false
|
|
485
|
+
current_media_query = nil
|
|
486
|
+
current_media_query_list_id = nil
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Blank line before a base rule if we just closed a media block (ends with "}\n")
|
|
490
|
+
if @result.length > 1 && @result.getbyte(-1) == BYTE_NEWLINE && @result.getbyte(-2) == BYTE_RBRACE
|
|
491
|
+
@result << "\n"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
serialize_rule_with_nesting_formatted(rule, '')
|
|
495
|
+
else
|
|
496
|
+
needs_new_block = needs_new_media_block?(current_media_query, current_media_query_list_id,
|
|
497
|
+
rule_media_query, rule_media_query_list_id)
|
|
498
|
+
|
|
499
|
+
if needs_new_block
|
|
500
|
+
if in_media_block
|
|
501
|
+
@result << "}\n"
|
|
502
|
+
elsif @result.length > 0
|
|
503
|
+
@result << "\n"
|
|
504
|
+
end
|
|
505
|
+
current_media_query = rule_media_query
|
|
506
|
+
current_media_query_list_id = rule_media_query_list_id
|
|
507
|
+
media_query_string = build_media_query_string(rule_media_query, rule_media_query_list_id)
|
|
508
|
+
@result << "@media #{media_query_string} {\n"
|
|
509
|
+
in_media_block = true
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
serialize_rule_with_nesting_formatted(rule, ' ')
|
|
513
|
+
end
|
|
514
|
+
end
|
|
652
515
|
|
|
653
|
-
|
|
654
|
-
children = rule_children[rule.id] || []
|
|
655
|
-
|
|
656
|
-
# Serialize nested children
|
|
657
|
-
children.each do |child|
|
|
658
|
-
nested_selector = _reconstruct_nested_selector(rule.selector, child.selector, child.nesting_style)
|
|
659
|
-
|
|
660
|
-
if child.nesting_style.nil? && child.media_query_id && media_queries[child.media_query_id]
|
|
661
|
-
# Nested @media
|
|
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
|
|
668
|
-
result << indent
|
|
669
|
-
result << " @media #{media_query_string} {\n"
|
|
670
|
-
|
|
671
|
-
unless child.declarations.empty?
|
|
672
|
-
_serialize_declarations_formatted(result, child.declarations, "#{indent} ")
|
|
516
|
+
@result << "}\n" if in_media_block
|
|
673
517
|
end
|
|
674
518
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
child.selector,
|
|
680
|
-
media_child.selector,
|
|
681
|
-
media_child.nesting_style
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
result << indent
|
|
685
|
-
result << " #{nested_media_selector} {\n"
|
|
686
|
-
unless media_child.declarations.empty?
|
|
687
|
-
_serialize_declarations_formatted(result, media_child.declarations, "#{indent} ")
|
|
519
|
+
def serialize_rule_with_nesting_formatted(rule, indent)
|
|
520
|
+
if rule.is_a?(AtRule)
|
|
521
|
+
serialize_at_rule_formatted(rule, indent)
|
|
522
|
+
return
|
|
688
523
|
end
|
|
689
|
-
result << indent
|
|
690
|
-
result << " }\n"
|
|
691
|
-
end
|
|
692
524
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
525
|
+
@result << indent
|
|
526
|
+
@result << rule.selector
|
|
527
|
+
@result << " {\n"
|
|
528
|
+
|
|
529
|
+
serialize_declarations_formatted(rule.declarations, "#{indent} ") unless rule.declarations.empty?
|
|
530
|
+
|
|
531
|
+
serialize_children_formatted(rule.selector, @rule_children[rule.id] || [], "#{indent} ")
|
|
699
532
|
|
|
700
|
-
|
|
701
|
-
|
|
533
|
+
@result << indent
|
|
534
|
+
@result << "}\n"
|
|
702
535
|
end
|
|
703
536
|
|
|
704
|
-
# Recursively
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
537
|
+
# Recursively serialize a rule's nested children with indentation. CSS
|
|
538
|
+
# nesting can go as deep as the parser allows (MAX_PARSE_DEPTH), so this
|
|
539
|
+
# recurses rather than hand-unrolling a fixed number of levels - a nested
|
|
540
|
+
# rule's own nested rules are found the same way its parent's were, via
|
|
541
|
+
# @rule_children[id], with the indent growing by one level each call.
|
|
542
|
+
def serialize_children_formatted(parent_selector, children, indent)
|
|
543
|
+
children.each do |child|
|
|
544
|
+
if child.nesting_style.nil? && child.media_query_id && @media_queries[child.media_query_id]
|
|
545
|
+
mq = @media_queries[child.media_query_id]
|
|
546
|
+
media_query_string = build_media_query_string(mq, nil)
|
|
547
|
+
@result << indent
|
|
548
|
+
@result << "@media #{media_query_string} {\n"
|
|
549
|
+
|
|
550
|
+
serialize_declarations_formatted(child.declarations, "#{indent} ") unless child.declarations.empty?
|
|
551
|
+
|
|
552
|
+
media_children = @rule_children[child.id] || []
|
|
553
|
+
media_children.each do |media_child|
|
|
554
|
+
nested_media_selector = reconstruct_nested_selector(
|
|
555
|
+
child.selector, media_child.selector, media_child.nesting_style
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
@result << indent
|
|
559
|
+
@result << " #{nested_media_selector} {\n"
|
|
560
|
+
unless media_child.declarations.empty?
|
|
561
|
+
serialize_declarations_formatted(media_child.declarations, "#{indent} ")
|
|
562
|
+
end
|
|
563
|
+
@result << indent
|
|
564
|
+
@result << " }\n"
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
@result << indent
|
|
568
|
+
@result << "}\n"
|
|
569
|
+
else
|
|
570
|
+
nested_selector = reconstruct_nested_selector(parent_selector, child.selector, child.nesting_style)
|
|
571
|
+
|
|
572
|
+
@result << indent
|
|
573
|
+
@result << "#{nested_selector} {\n"
|
|
574
|
+
|
|
575
|
+
serialize_declarations_formatted(child.declarations, "#{indent} ") unless child.declarations.empty?
|
|
576
|
+
|
|
577
|
+
serialize_children_formatted(child.selector, @rule_children[child.id] || [], "#{indent} ")
|
|
578
|
+
|
|
579
|
+
@result << indent
|
|
580
|
+
@result << "}\n"
|
|
581
|
+
end
|
|
717
582
|
end
|
|
718
|
-
result << indent
|
|
719
|
-
result << " }\n"
|
|
720
583
|
end
|
|
721
584
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
# Closing brace
|
|
728
|
-
result << indent
|
|
729
|
-
result << "}\n"
|
|
730
|
-
end
|
|
585
|
+
def serialize_rule_formatted(rule, indent, is_last_rule = false)
|
|
586
|
+
if rule.is_a?(AtRule)
|
|
587
|
+
serialize_at_rule_formatted(rule, indent)
|
|
588
|
+
return
|
|
589
|
+
end
|
|
731
590
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if rule.is_a?(AtRule)
|
|
736
|
-
_serialize_at_rule_formatted(result, rule, indent)
|
|
737
|
-
return
|
|
738
|
-
end
|
|
591
|
+
@result << indent
|
|
592
|
+
@result << rule.selector
|
|
593
|
+
@result << " {\n"
|
|
739
594
|
|
|
740
|
-
|
|
741
|
-
# Selector line with opening brace
|
|
742
|
-
result << indent
|
|
743
|
-
result << rule.selector
|
|
744
|
-
result << " {\n"
|
|
595
|
+
serialize_declarations_formatted(rule.declarations, "#{indent} ")
|
|
745
596
|
|
|
746
|
-
|
|
747
|
-
|
|
597
|
+
# Closing brace - double newline for all except last rule
|
|
598
|
+
@result << indent
|
|
599
|
+
@result << (is_last_rule ? "}\n" : "}\n\n")
|
|
600
|
+
end
|
|
748
601
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
602
|
+
def serialize_at_rule_formatted(at_rule, indent)
|
|
603
|
+
@result << indent
|
|
604
|
+
@result << at_rule.selector
|
|
605
|
+
@result << " {\n"
|
|
606
|
+
|
|
607
|
+
if at_rule.content.length > 0
|
|
608
|
+
first = at_rule.content[0]
|
|
609
|
+
|
|
610
|
+
if first.is_a?(Rule)
|
|
611
|
+
at_rule.content.each do |nested_rule|
|
|
612
|
+
@result << indent
|
|
613
|
+
@result << ' '
|
|
614
|
+
@result << nested_rule.selector
|
|
615
|
+
@result << " {\n"
|
|
616
|
+
|
|
617
|
+
serialize_declarations_formatted(nested_rule.declarations, "#{indent} ")
|
|
618
|
+
|
|
619
|
+
@result << indent
|
|
620
|
+
@result << " }\n"
|
|
621
|
+
end
|
|
622
|
+
else
|
|
623
|
+
serialize_declarations_formatted(at_rule.content, "#{indent} ")
|
|
624
|
+
end
|
|
625
|
+
end
|
|
753
626
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
result << indent
|
|
757
|
-
result << at_rule.selector
|
|
758
|
-
result << " {\n"
|
|
759
|
-
|
|
760
|
-
# Check if content is rules or declarations
|
|
761
|
-
if at_rule.content.length > 0
|
|
762
|
-
first = at_rule.content[0]
|
|
763
|
-
|
|
764
|
-
if first.is_a?(Rule)
|
|
765
|
-
# Serialize as nested rules (e.g., @keyframes) with formatting
|
|
766
|
-
at_rule.content.each do |nested_rule|
|
|
767
|
-
# Nested selector with opening brace (2-space indent)
|
|
768
|
-
result << indent
|
|
769
|
-
result << ' '
|
|
770
|
-
result << nested_rule.selector
|
|
771
|
-
result << " {\n"
|
|
772
|
-
|
|
773
|
-
# Declarations (one per line, 4-space indent)
|
|
774
|
-
_serialize_declarations_formatted(result, nested_rule.declarations, "#{indent} ")
|
|
775
|
-
|
|
776
|
-
# Closing brace (2-space indent)
|
|
777
|
-
result << indent
|
|
778
|
-
result << " }\n"
|
|
627
|
+
@result << indent
|
|
628
|
+
@result << "}\n"
|
|
779
629
|
end
|
|
780
|
-
else
|
|
781
|
-
# Serialize as declarations (e.g., @font-face, one per line)
|
|
782
|
-
_serialize_declarations_formatted(result, at_rule.content, "#{indent} ")
|
|
783
630
|
end
|
|
784
|
-
end
|
|
785
631
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
632
|
+
# Serialize stylesheet to compact CSS string.
|
|
633
|
+
#
|
|
634
|
+
# @param rules [Array<Rule>] Array of rules
|
|
635
|
+
# @param charset [String, nil] @charset value
|
|
636
|
+
# @param has_nesting [Boolean] Whether any nested rules exist
|
|
637
|
+
# @param selector_lists [Hash] Selector list ID => array of rule IDs (for grouping)
|
|
638
|
+
# @param media_queries [Array<MediaQuery>] Array of MediaQuery objects
|
|
639
|
+
# @param media_query_lists [Hash] List ID => array of MediaQuery IDs (for comma-separated queries)
|
|
640
|
+
# @return [String] Compact CSS string
|
|
641
|
+
def stylesheet_to_s(rules, charset, has_nesting, selector_lists = {}, media_queries = [], media_query_lists = {})
|
|
642
|
+
Serializer.new(rules, charset, has_nesting, selector_lists, media_queries, media_query_lists, formatted: false).to_s
|
|
643
|
+
end
|
|
789
644
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
645
|
+
# Serialize stylesheet to formatted CSS string (with indentation).
|
|
646
|
+
#
|
|
647
|
+
# @param rules [Array<Rule>] Array of rules
|
|
648
|
+
# @param charset [String, nil] @charset value
|
|
649
|
+
# @param has_nesting [Boolean] Whether any nested rules exist
|
|
650
|
+
# @param selector_lists [Hash] Selector list ID => array of rule IDs (for grouping)
|
|
651
|
+
# @param media_queries [Array<MediaQuery>] Array of MediaQuery objects
|
|
652
|
+
# @param media_query_lists [Hash] List ID => array of MediaQuery IDs (for comma-separated queries)
|
|
653
|
+
# @return [String] Formatted CSS string
|
|
654
|
+
def stylesheet_to_formatted_s(rules, charset, has_nesting, selector_lists = {}, media_queries = [],
|
|
655
|
+
media_query_lists = {})
|
|
656
|
+
Serializer.new(rules, charset, has_nesting, selector_lists, media_queries, media_query_lists,
|
|
657
|
+
formatted: true).to_s
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
end
|
|
796
661
|
end
|