css_compare 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,136 @@
1
+ module CssCompare
2
+ module CSS
3
+ module Component
4
+ # Represents the @support CSS rule.
5
+ #
6
+ # @see https://www.w3.org/TR/css3-conditional/#at-supports
7
+ class Supports < Base
8
+ include CssCompare::CSS::Component
9
+
10
+ # The name of the @support directive.
11
+ # Can be browser-prefixed.
12
+ #
13
+ # @return [String]
14
+ attr_reader :name
15
+
16
+ # The assigned rules grouped by the @supports'
17
+ # conditions.
18
+ #
19
+ # @supports can contain the same rules as a CSS
20
+ # stylesheet. Why not to create a new engine for it?
21
+ #
22
+ # @return [Hash{String => CssCompare::CSS::Engine}]
23
+ attr_accessor :rules
24
+
25
+ # @param [Sass::Tree::SupportsNode] node
26
+ # @param [Array<String>] query_list the query list of
27
+ # the parent node (the conditions under which this
28
+ # node is evaluated).
29
+ def initialize(node, query_list = [])
30
+ @name = node.name
31
+ @rules = {}
32
+ condition = node.condition.to_css.gsub(/\s*!important\s*/, '')
33
+ unless query_list.empty?
34
+ media_node = media_node([Engine::GLOBAL_QUERY], node.children, node.options)
35
+ node = root_node(media_node, node.options)
36
+ end
37
+ rules = CssCompare::CSS::Engine.new(node).evaluate(nil, query_list)
38
+ @rules[condition] = rules
39
+ end
40
+
41
+ # Checks, whether two @supports rules are equal.
42
+ #
43
+ # They are only equal, if all of their rules are
44
+ # equal.
45
+ #
46
+ # @param [Supports] other the supports rule to compare
47
+ # this to.
48
+ # @return [Boolean]
49
+ def ==(other)
50
+ super(@rules, other.rules)
51
+ end
52
+
53
+ # Merges this @supports rule with another one.
54
+ #
55
+ # @param [Supports] other
56
+ # @return [Void]
57
+ def merge(other)
58
+ other.rules.each do |cond, engine|
59
+ if @rules[cond]
60
+ merge_selectors(engine.selectors, cond)
61
+ merge_keyframes(engine.keyframes, cond)
62
+ merge_namespaces(engine.namespaces, cond)
63
+ merge_supports(engine.supports, cond)
64
+ else
65
+ @rules[cond] = engine
66
+ end
67
+ end
68
+ end
69
+
70
+ # Returns a deep copy of this object.
71
+ #
72
+ # @return [Supports]
73
+ def deep_copy(name = @name)
74
+ copy = dup
75
+ copy.name = name
76
+ copy.rules = {}
77
+ @rules.each { |k, v| copy.rules[k] = v.deep_copy }
78
+ copy
79
+ end
80
+
81
+ # Creates the JSON representation of this object.
82
+ #
83
+ # @return [Hash]
84
+ def to_json
85
+ json = { :name => @name.to_sym, :rules => {} }
86
+ @rules.inject(json[:rules]) do |result, (k, v)|
87
+ result.update(k => v.to_json)
88
+ end
89
+ json
90
+ end
91
+
92
+ private
93
+
94
+ def merge_selectors(selectors, cond)
95
+ loc_selectors = @rules[cond].selectors
96
+ selectors.each do |key, selector|
97
+ if loc_selectors[key]
98
+ loc_selectors[key].merge(selector)
99
+ else
100
+ loc_selectors[key] = selector.deep_copy
101
+ end
102
+ end
103
+ end
104
+
105
+ def merge_keyframes(keyframes, cond)
106
+ loc_keyframes = @rules[cond].keyframes
107
+ keyframes.each do |key, value|
108
+ if loc_keyframes[key]
109
+ loc_keyframes[key].merge(value)
110
+ else
111
+ loc_keyframes[key] = value.deep_copy
112
+ end
113
+ end
114
+ end
115
+
116
+ def merge_namespaces(namespaces, cond)
117
+ loc_namespaces = @rules[cond].namespaces
118
+ namespaces.each do |key, value|
119
+ loc_namespaces[key] = value
120
+ end
121
+ end
122
+
123
+ def merge_supports(supports, cond)
124
+ loc_supports = @rules[cond].supports
125
+ supports.each do |key, value|
126
+ if loc_supports[key]
127
+ loc_supports[key].merge(value)
128
+ else
129
+ loc_supports[key] = value.deep_copy
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,51 @@
1
+ module CssCompare
2
+ module CSS
3
+ module Component
4
+ # Represents the value of a CSS property under
5
+ # certain conditions declared by @media queries.
6
+ class Value < Base
7
+ # @return [#to_s]
8
+ attr_accessor :value
9
+
10
+ # @param [#to_s] val the value of the property
11
+ def initialize(val)
12
+ self.value = val
13
+ end
14
+
15
+ # Checks whether two values are equal.
16
+ # Equal values mean, that the actual value and
17
+ # the importance, as well, are set equally.
18
+ #
19
+ # @param [Value] other the value to compare this with
20
+ # @return [Boolean]
21
+ def ==(other)
22
+ @value.to_s == other.value.to_s
23
+ end
24
+
25
+ # Sets the value and the importance of
26
+ # the {Value} node.
27
+ #
28
+ # @private
29
+ def value=(value)
30
+ original_value = value = value.is_a?(Value) ? value.value : value
31
+ # Can't do gsub! because the String gets frozen and can't be further modified by strip
32
+ value = value.gsub(/\s*!important\s*/, '')
33
+ @is_important = value != original_value
34
+ @value = value
35
+ end
36
+
37
+ # Tells, whether or not the value is marked as !important
38
+ #
39
+ # @return [Bool]
40
+ def important?
41
+ @is_important
42
+ end
43
+
44
+ # @return [String] the String representation of this node
45
+ def to_s
46
+ @value.to_s + (@is_important ? ' !important' : '')
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,567 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'pathname'
4
+
5
+ module CssCompare
6
+ module CSS
7
+ # The CSS Engine that computes the values of the
8
+ # properties under all the declared parent_query_list in
9
+ # the stylesheet.
10
+ #
11
+ # It can handle:
12
+ # - simple property overriding
13
+ # - property overriding with !import
14
+ # - @import rules
15
+ # - @media queries - PARTIAL SUPPORT ONLY!!!
16
+ # - nested @media queries also know as nested conditional
17
+ # group rules
18
+ # - @keyframes rules
19
+ # - @namespace rules
20
+ # - @charset rules
21
+ # - @page rules
22
+ # - @supports rules
23
+ #
24
+ # However, the @media and @supports evaluations are not
25
+ # 100% reliable, since the parent_query_list of each directive
26
+ # are not interpreted and evaluated by the engine. Instead,
27
+ # they are stringified as a whole and used as the key
28
+ # for their selector-property pairs.
29
+ #
30
+ # "When multiple conditional group rules are nested, a rule
31
+ # inside of both of them applies only when all of the rules'
32
+ # parent_query_list are true."
33
+ # @see https://www.w3.org/TR/css3-conditional/#processing
34
+ #
35
+ # The imports are dynamically loaded and evaluated with
36
+ # the root document together. The result shows the final
37
+ # values of each CSS properties and rules, just like a
38
+ # browser would interpret the linked CSS stylesheets.
39
+ class Engine
40
+ include CssCompare::CSS::Component
41
+ # The inner representation of the computed properties
42
+ # of each selector under every condition specified by
43
+ # the declared @media directives.
44
+ #
45
+ # @return [Hash<Symbol, Array<Component::Selector, String>]
46
+ attr_accessor :engine
47
+
48
+ # A list of nodes, that could not be evaluated due to
49
+ # being not supported by this engine.
50
+ #
51
+ # @return [Array<Sass::Tree::Node>] unsupported CSS nodes
52
+ attr_accessor :unsupported
53
+
54
+ attr_accessor :selectors, :keyframes, :namespaces,
55
+ :pages, :supports, :charset
56
+
57
+ # @param [String, Sass::Tree::Node] input the source file of
58
+ # the CSS project, or its AST
59
+ def initialize(input)
60
+ @tree =
61
+ begin
62
+ if input.is_a?(String)
63
+ Parser.new(input).parse.freeze
64
+ elsif input.is_a?(Sass::Tree::Node)
65
+ input.freeze
66
+ else
67
+ raise ArgumentError, "The engine's input must be either a path, or a Sass::Tree::Node"
68
+ end
69
+ end
70
+ @filename = @tree.options[:filename]
71
+ @engine = {}
72
+ @selectors = {}
73
+ @font_faces = {}
74
+ @keyframes = {}
75
+ @namespaces = {}
76
+ @pages = {}
77
+ @supports = {}
78
+ @unsupported = []
79
+ @charset
80
+ end
81
+
82
+ # Checks, whether two engines are equal.
83
+ #
84
+ # They are equal only if the same symbols are defined
85
+ # and each and every component under those keys are
86
+ # also equal.
87
+ #
88
+ # @param [Engine] other the engine to compare this with.
89
+ # @return [Boolean]
90
+ def ==(other)
91
+ keys = @engine.keys + other.engine.keys
92
+ return false unless keys.uniq! # @todo this won't work
93
+ keys.all? { |key| @engine[key] == other.engine[key] }
94
+ end
95
+
96
+ # Computes the values of each declared selector's properties
97
+ # under each condition declared by the @media directives.
98
+ #
99
+ # @param [Sass::Tree::RootNode] tree the tree that needs to
100
+ # be evaluates. The default option is the engine's own tree.
101
+ # However, to support the @import directives, we'll have to
102
+ # be able to pass a tree in a parameter.
103
+ # @param [Array<String>] parent_query_list the list of parent
104
+ # queries
105
+ # @note the second parameter has been added to ensure multiply
106
+ # nested @media, @support and @import rules.
107
+ # @return [Void]
108
+ def evaluate(tree = @tree, parent_query_list = [])
109
+ tree = @tree unless tree # if nil is passed explicitly
110
+ tree.children.each do |node|
111
+ if node.is_a?(Sass::Tree::MediaNode)
112
+ process_media_node(node, parent_query_list)
113
+ elsif node.is_a?(Sass::Tree::RuleNode)
114
+ process_rule_node(node, parent_query_list)
115
+ elsif node.is_a?(Sass::Tree::DirectiveNode)
116
+ if node.is_a?(Sass::Tree::SupportsNode)
117
+ process_supports_node(node)
118
+ elsif node.is_a?(Sass::Tree::CssImportNode)
119
+ process_import_node(node, parent_query_list)
120
+ else
121
+ begin
122
+ case node.name
123
+ when '@keyframes'
124
+ process_keyframes_node(node, parent_query_list)
125
+ when '@namespace'
126
+ process_namespace_node(node)
127
+ when '@page'
128
+ process_page_node(node, parent_query_list)
129
+ when '@font-face'
130
+ process_font_face_node(node, parent_query_list)
131
+ else
132
+ # Unsupported DirectiveNodes, that have a name property
133
+ @unsupported << node
134
+ end
135
+ rescue NotImplementedError
136
+ # Unsupported DirectiveNodes, that do not implement a name getter
137
+ @unsupported << node
138
+ end
139
+ end
140
+ elsif node.is_a?(Sass::Tree::CharsetNode)
141
+ process_charset_node(node)
142
+ else
143
+ # Unsupported Node
144
+ @unsupported << node
145
+ end
146
+ end
147
+ @engine[:selectors] = @selectors
148
+ @engine[:font_faces] = @font_faces
149
+ @engine[:keyframes] = @keyframes
150
+ @engine[:namespaces] = @namespaces
151
+ @engine[:pages] = @pages
152
+ @engine[:supports] = @supports
153
+ @engine[:charset] = @charset
154
+ self
155
+ end
156
+
157
+ # Returns the inner representation of the processed
158
+ # CSS stylesheet.
159
+ #
160
+ # @return [Hash]
161
+ def to_json
162
+ engine = {
163
+ :selectors => [],
164
+ :font_faces => {},
165
+ :keyframes => [],
166
+ :namespaces => @namespaces,
167
+ :pages => [],
168
+ :supports => [],
169
+ :charset => @charset
170
+ }
171
+ @selectors.inject(engine[:selectors]) { |arr, (_, s)| arr << s.to_json }
172
+ @font_faces.each_with_object(engine[:font_faces]) do |(cond, font_families), arr|
173
+ arr[cond] = font_families.inject([]) do |font_faces, (_, font_family)|
174
+ font_faces + font_family.inject([]) do |sum, (_, font_face)|
175
+ sum << font_face.to_json
176
+ end
177
+ end
178
+ end
179
+ @keyframes.inject(engine[:keyframes]) { |arr, (_, k)| arr << k.to_json }
180
+ @pages.inject(engine[:pages]) { |arr, (_, p)| arr << p.to_json }
181
+ @supports.inject(engine[:supports]) { |arr, (_, s)| arr << s.to_json }
182
+ engine
183
+ end
184
+
185
+ # Creates a deep copy of this object.
186
+ #
187
+ # @return [Engine]
188
+ def deep_copy
189
+ copy = dup
190
+ copy.selectors = @selectors.inject({}) do |result, (k, v)|
191
+ result.update(k => v.deep_copy)
192
+ end
193
+ copy.keyframes = @keyframes.inject({}) do |result, (k, v)|
194
+ result.update(k => v.deep_copy)
195
+ end
196
+ copy.pages = @supports.inject({}) do |result, (k, v)|
197
+ result.update(k => v.deep_copy)
198
+ end
199
+ copy.supports = @supports.inject({}) do |result, (k, v)|
200
+ result.update(k => v.deep_copy)
201
+ end
202
+ copy.engine = {
203
+ :selectors => copy.selectors,
204
+ :keyframes => copy.keyframes,
205
+ :namespaces => copy.namespaces,
206
+ :pages => copy.pages,
207
+ :supports => copy.supports
208
+ }
209
+ copy
210
+ end
211
+
212
+ GLOBAL_QUERY = 'all'.freeze
213
+
214
+ private
215
+
216
+ # Processes the queries of the @media directive and
217
+ # starts processing its {Sass::Tree::RulesetNode}.
218
+ #
219
+ # These media queries are equal:
220
+ # @media all { ... }
221
+ # @media { ... }
222
+ #
223
+ # @todo The queries should be simplified and evaluated.
224
+ # For example, these are also equal queries:
225
+ # @media all and (min-width:500px) { ... }
226
+ # @media (min-width:500px) { ... }
227
+ # @see https://www.w3.org/TR/css3-mediaqueries/#media0
228
+ #
229
+ # @param [Sass::Tree::MediaNode] node the node
230
+ # representing the @media directive.
231
+ # @param [Array<String>] parent_query_list (see #evaluate)
232
+ # @return [Void]
233
+ def process_media_node(node, parent_query_list = [])
234
+ query_list = node.resolved_query.queries.inject([]) { |queries, q| queries << q.to_css }
235
+ query_list -= [GLOBAL_QUERY]
236
+ query_list = merge_nested_query_lists(parent_query_list, query_list) unless parent_query_list.empty?
237
+ evaluate(node, query_list)
238
+ end
239
+
240
+ # Merges the parent media queries with its child
241
+ # media queries resulting in their combination.
242
+ # Makes the nested @media queries possible to support
243
+ # in a limited manner. The parent-child relation is
244
+ # represented by a linking `>` character.
245
+ #
246
+ # @example:
247
+ # merge_nested_query_lists(["tv", "screen and (color)"], ["(color)", "(min-height: 100px)"]) #=>
248
+ # [
249
+ # "tv > (color)",
250
+ # "tv > (min-height: 100px)",
251
+ # "screen and (color) > (color)",
252
+ # "screen and (color) > (min-height: 100px)"
253
+ # ]
254
+ #
255
+ # @param [Array<String>] parent list of parent media queries
256
+ # @param [Array<String>] child list of child media queries
257
+ # @return [Array<String>] the combined media queries
258
+ def merge_nested_query_lists(parent, child)
259
+ if parent.empty?
260
+ child
261
+ elsif child.empty?
262
+ parent
263
+ else
264
+ parent.product(child).collect do |pair|
265
+ pair.first + ' > ' + pair.last
266
+ end
267
+ end
268
+ end
269
+
270
+ # Processes the {Sass::Tree::RuleNode} and saves the selectors
271
+ # with their properties accordingly to its parenting media queries
272
+ # in a reasonable data structure.
273
+ #
274
+ # @note Only one of the comma-separated selector sequences gets
275
+ # created as a new instance of {Component::Selector}. Since the
276
+ # whole group of selectors share the same property declaration
277
+ # block, there's no need to analyze the block again by instantiating
278
+ # each of the selectors. A deep copy should be done instead with
279
+ # a small change in the the selector's name.
280
+ #
281
+ # @param [Sass::Tree:RuleNode] node the Rule node
282
+ # @param [Array<String>] parent_query_list processed parent_query_list of the
283
+ # parent media node. If the rule is global, it will be assigned
284
+ # to the media query equal to `@media all {}`.
285
+ # @return [Void]
286
+ def process_rule_node(node, conditions)
287
+ conditions = [GLOBAL_QUERY] if conditions.empty?
288
+ selectors = selector_sequences(node)
289
+ selector = Component::Selector.new(selectors.shift, node.children, conditions)
290
+ save_selector(selector)
291
+ selectors.each do |name|
292
+ save_selector(selector.deep_copy(name))
293
+ end
294
+ end
295
+
296
+ # Saves the selector and its properties.
297
+ # If the selector already exists, it merges its properties
298
+ # with the existent selector's properties.
299
+ #
300
+ # @see {Component::Selector#merge}
301
+ # @return [Void]
302
+ def save_selector(selector)
303
+ if @selectors[selector.name]
304
+ @selectors[selector.name].merge(selector)
305
+ else
306
+ @selectors[selector.name] = selector
307
+ end
308
+ end
309
+
310
+ # Returns the comma-separated selectors.
311
+ #
312
+ # @param [Sass::Tree::RuleNode] node the node representing
313
+ # a CSS rule (group of selectors + declaration of properties).
314
+ # @return [Array<String>] array of selectors sharing the same
315
+ # block of properties.
316
+ def selector_sequences(node)
317
+ node.parsed_rules.members.inject([]) { |selectors, sequence| selectors << optimize_sequence(sequence) }
318
+ end
319
+
320
+ # Optimizes a CSS selector selector, a selector separated
321
+ # by empty strings, like input#id.class[type="text"]:first-child,
322
+ # in two ways:
323
+ #
324
+ # 1. gets rid of redundancy:
325
+ # Example:
326
+ # ".a.h.c.e.c" => ".a.h.c.e"
327
+ #
328
+ # 2. puts the simple sequences' nodes in alphabetical order
329
+ # Example:
330
+ # ".a.h.c > .div.elem[type='text'].col" => ".a.c.h > .col.div.elem[type='text']"
331
+ #
332
+ # `basket`'s keys are in a specific order.
333
+ # 1. Universal selector (*) should be the first in order.
334
+ # It shouldn't be followed by any element selector,
335
+ # whereas ids, classes and pseudo classes can follow it.
336
+ #
337
+ # 2. An element selector should go before any other
338
+ # selector, except the universal.
339
+ #
340
+ # 3. Id can follow an element selector, as well as
341
+ # a class selector. To unify the compared selectors
342
+ # a strict order had to be created.
343
+ #
344
+ # 4. Class selectors.
345
+ #
346
+ # 5. Placeholder selectors are a special type found in
347
+ # Sass code and are not a part of the CSS selectors.
348
+ # I included it just for the sake of completeness.
349
+ #
350
+ # 6. Pseudo selectors should be the last in the order.
351
+ #
352
+ # 7. Attribute selectors do not have their own place
353
+ # in the order. They get tied to the preceding
354
+ # selector.
355
+ #
356
+ # @param [Sass::Selector::Sequence] selector a node
357
+ # representing a selector sequence.
358
+ # @return [String] optimized selector.
359
+ def optimize_sequence(selector)
360
+ selector.members.inject([]) do |final, sequence|
361
+ if sequence.is_a?(Sass::Selector::SimpleSequence)
362
+ baskets = {
363
+ Sass::Selector::Universal => [],
364
+ Sass::Selector::Element => [],
365
+ Sass::Selector::Id => [],
366
+ Sass::Selector::Class => [],
367
+ Sass::Selector::Placeholder => [],
368
+ Sass::Selector::Pseudo => []
369
+ }
370
+ sequence.members.each_with_index do |simple, i|
371
+ last = i + 1 == sequence.members.length
372
+ if !last && sequence.members[i + 1].is_a?(Sass::Selector::Attribute)
373
+ baskets[simple.class] << simple.to_s + sequence.members[i + 1].to_s
374
+ sequence.members.delete_at(i + 1)
375
+ else
376
+ baskets[simple.class] << simple.to_s
377
+ end
378
+ end
379
+ final << baskets.values.inject([]) { |partial, b| partial + b.uniq.sort }.join('')
380
+ else
381
+ final << sequence.to_s
382
+ end
383
+ end.join(' ')
384
+ end
385
+
386
+ # Processes and evaluates the {Sass::Tree::KeyframeRuleNode}.
387
+ #
388
+ # An @keyframe directive can't be extended by later re-declarations.
389
+ # However, you can bend their behaviour by declaring keyframes
390
+ # under different @media queries. The browser then keeps track of
391
+ # different keyframes declarations under the same name. Like it would
392
+ # be namespaced. But still, the re-declarations do not extend the
393
+ # original @keyframe.
394
+ #
395
+ # Example:
396
+ # @keyframes my-value {
397
+ # from { top: 0px; }
398
+ # to { top: 100px; }
399
+ # }
400
+ # @media (max-width: 600px) {
401
+ # @keyframes my-value {
402
+ # 50% { top: 50px; }
403
+ # }
404
+ # }
405
+ #
406
+ # The keyframe under the media query WON'T be interpreted like this:
407
+ # @media (max-width: 600px) {
408
+ # @keyframes my-value {
409
+ # from { top: 0px; }
410
+ # 50% { top: 50px; }
411
+ # to { top: 100px; }
412
+ # }
413
+ # }
414
+ #
415
+ # @param [Sass::Tree::DirectiveNode] node the node containing
416
+ # information about and the keyframe rules of the @keyframes
417
+ # directive.
418
+ # @param conditions (see #process_rule_node)
419
+ # @return [Void]
420
+ def process_keyframes_node(node, conditions = ['all'])
421
+ keyframes = Component::Keyframes.new(node, conditions)
422
+ save_keyframes(keyframes)
423
+ end
424
+
425
+ # Saves the keyframes into its collection.
426
+ #
427
+ # @see #save_selector
428
+ def save_keyframes(keyframes)
429
+ if @keyframes[keyframes.name]
430
+ @keyframes[keyframes.name].merge(keyframes)
431
+ else
432
+ @keyframes[keyframes.name] = keyframes
433
+ end
434
+ end
435
+
436
+ # Processes the charset directive, if present.
437
+ def process_charset_node(node)
438
+ @charset = node.name
439
+ end
440
+
441
+ # Unifies the namespace by replacing the ' or " characters with an
442
+ # empty space if the namespace name is given by a URL.
443
+ # The namespaces declaration without any specified prefix value are
444
+ # automatically assigned to the default namespace.
445
+ #
446
+ # "If a namespace prefix or default namespace is declared more than
447
+ # once only the last declaration shall be used. Declaring a namespace
448
+ # prefix or default namespace more than once is nonconforming."
449
+ # @see https://www.w3.org/TR/css3-namespace/#prefixes
450
+ #
451
+ # @param [Sass::Tree::DirectiveNode] node the namespace node
452
+ # @return [Void]
453
+ def process_namespace_node(node)
454
+ values = node.value[1].strip.split(/\s+/)
455
+ values = values.unshift('default') if values.length == 1
456
+ values[1].gsub!(/("|')/, '') if values[1] =~ /^url\(("|').+("|')\)$/
457
+ @namespaces.update(values[0].to_sym => values[1])
458
+ end
459
+
460
+ # Processes the page node's all selectors. Instantiates one
461
+ # of them and creates a deep copy of itself for every
462
+ # leftover page selector.
463
+ # @see #process_rule_node
464
+ #
465
+ # @param [Sass::Tree::DirectiveNode] node
466
+ # @param parent_query_list (see #process_rule_node)
467
+ # @return [Void]
468
+ def process_page_node(node, parent_query_list = ['all'])
469
+ selectors = node.value[1].strip.split(/,\s+/)
470
+ page_selector = Component::PageSelector.new(selectors.shift, node.children, parent_query_list)
471
+ save_page_selector(page_selector)
472
+ selectors.each do |selector|
473
+ save_page_selector(page_selector.deep_copy(selector))
474
+ end
475
+ end
476
+
477
+ # Saves the page selector into its collection.
478
+ #
479
+ # @see #save_selector
480
+ def save_page_selector(page_selector)
481
+ if @pages[page_selector.value]
482
+ @pages[page_selector.value].merge(page_selector)
483
+ else
484
+ @pages[page_selector.value] = page_selector
485
+ end
486
+ end
487
+
488
+ # Processes and saves a {SupportsNode}.
489
+ #
490
+ # @see {Component::Supports}
491
+ # @param [Sass::Tree::SupportsNode] node
492
+ # @param [Array<String>] parent_query_list (see #evaluate)
493
+ # @return [Void]
494
+ def process_supports_node(node, parent_query_list = [])
495
+ supports = Component::Supports.new(node, parent_query_list)
496
+ save_supports(supports)
497
+ end
498
+
499
+ # Saves the supports rule into its collection.
500
+ #
501
+ # @see #save_selector
502
+ def save_supports(supports)
503
+ if @supports[supports.name]
504
+ @supports[supports.name].merge(supports)
505
+ else
506
+ @supports[supports.name] = supports
507
+ end
508
+ end
509
+
510
+ # Processes the @import rule, if the file can
511
+ # be found, otherwise it just skips the import
512
+ # file evaluation.
513
+ #
514
+ # @param [Sass::Tree::CssImportNode] node the
515
+ # @import rule to be processed
516
+ # @param [Array<String>] parent_query_list (see #evaluate)
517
+ # @return [Void]
518
+ def process_import_node(node, parent_query_list = [])
519
+ dir = Pathname.new(@filename).dirname
520
+ import_filename = node.resolved_uri.scan(/^[url\(]*['|"]*([^'")]+)[['|"]*\)*]*$/).first.first
521
+ import_filename = (dir + import_filename).cleanpath
522
+ if File.exist?(import_filename)
523
+ if node.query.empty?
524
+ evaluate(Parser.new(import_filename).parse.freeze, parent_query_list)
525
+ else
526
+ media_children = Parser.new(import_filename).parse.children
527
+ media_node = media_node(node.query, media_children, node.options)
528
+ root = root_node(media_node, media_node.options)
529
+ evaluate(root.freeze, parent_query_list)
530
+ end
531
+ end
532
+ end
533
+
534
+ # Processes the @font-face rule.
535
+ #
536
+ # @param [Sass::Tree::DirectiveNode] node the
537
+ # @font-face rule to be processed
538
+ # @param [Array<String>] parent_query_list (see #evaluate)
539
+ # @return [Void]
540
+ def process_font_face_node(node, parent_query_list = [])
541
+ save_font_face(Component::FontFace.new(node.children), parent_query_list)
542
+ end
543
+
544
+ # Save the @font-face rule to its collection
545
+ # grouped by:
546
+ # - the parent media query conditions
547
+ # - `font-family` value
548
+ # - `src` value
549
+ #
550
+ # @param [Component::FontFace] font_face the
551
+ # font-face to save
552
+ # @param [Array<String>] query_list (see #evaluate)
553
+ # @return [Void]
554
+ def save_font_face(font_face, query_list)
555
+ if font_face.valid?
556
+ family = font_face.family
557
+ src = font_face.src
558
+ query_list.each do |query|
559
+ @font_faces[query] ||= {}
560
+ @font_faces[query][family] ||= {}
561
+ @font_faces[query][family].update(src => font_face)
562
+ end
563
+ end
564
+ end
565
+ end
566
+ end
567
+ end