css_compare 0.1.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.
@@ -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