css_compare 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|