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