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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +63 -0
- data/Rakefile +6 -0
- data/bin/css_compare +11 -0
- data/lib/css_compare.rb +6 -0
- data/lib/css_compare/constants.rb +5 -0
- data/lib/css_compare/css.rb +3 -0
- data/lib/css_compare/css/component.rb +45 -0
- data/lib/css_compare/css/component/base.rb +21 -0
- data/lib/css_compare/css/component/font_face.rb +190 -0
- data/lib/css_compare/css/component/keyframes.rb +123 -0
- data/lib/css_compare/css/component/keyframes_selector.rb +94 -0
- data/lib/css_compare/css/component/margin_box.rb +40 -0
- data/lib/css_compare/css/component/page_selector.rb +126 -0
- data/lib/css_compare/css/component/property.rb +155 -0
- data/lib/css_compare/css/component/selector.rb +118 -0
- data/lib/css_compare/css/component/supports.rb +136 -0
- data/lib/css_compare/css/component/value.rb +51 -0
- data/lib/css_compare/css/engine.rb +567 -0
- data/lib/css_compare/css/parser.rb +28 -0
- data/lib/css_compare/engine.rb +31 -0
- data/lib/css_compare/exec.rb +111 -0
- data/lib/css_compare/util.rb +13 -0
- metadata +112 -0
@@ -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
|