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,94 @@
|
|
1
|
+
module CssCompare
|
2
|
+
module CSS
|
3
|
+
module Component
|
4
|
+
# Represents a rule of the @keyframe directive.
|
5
|
+
#
|
6
|
+
# Examples:
|
7
|
+
# - from { top: 0; } // also meaning the same as 0%
|
8
|
+
# - 50% { top: 50; }
|
9
|
+
# - to { top: 100; } // also meaning the same as 100%
|
10
|
+
class KeyframesSelector < Base
|
11
|
+
# The value of the rule. Possible values: <'0%';'100%'>.
|
12
|
+
#
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :value
|
15
|
+
|
16
|
+
# The properties specified at this rule.
|
17
|
+
#
|
18
|
+
# @return [Hash{String => Property}]
|
19
|
+
attr_reader :properties
|
20
|
+
|
21
|
+
# @param [Sass::Tree::KeyframeRuleNode] node a rule node
|
22
|
+
# of the @keyframe directive.
|
23
|
+
def initialize(node)
|
24
|
+
@value = value(node.resolved_value)
|
25
|
+
@properties = {}
|
26
|
+
process_properties(node.children)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Checks, whether two keyframes selectors are equal.
|
30
|
+
#
|
31
|
+
# They are equal only if they have declared the same
|
32
|
+
# properties and they are also equal.
|
33
|
+
#
|
34
|
+
# @param [KeyframesSelector] other the keyframes selector
|
35
|
+
# to compare this with.
|
36
|
+
# @return [Boolean]
|
37
|
+
def ==(other)
|
38
|
+
super(@properties, other.properties)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns the value represented as percentage, even if
|
42
|
+
# declared with the well-known keywords.
|
43
|
+
#
|
44
|
+
# @return [String] the value of the rule
|
45
|
+
def value(value = nil)
|
46
|
+
aliases = { :from => '0%', :to => '100%' }
|
47
|
+
return aliases[value.to_sym] || value if value
|
48
|
+
@value
|
49
|
+
end
|
50
|
+
|
51
|
+
# Adds a new or rewrites an already existing property
|
52
|
+
# of this rule's set of properties.
|
53
|
+
#
|
54
|
+
# @return [Void]
|
55
|
+
def add_property(property)
|
56
|
+
name = property.name
|
57
|
+
if @properties[name]
|
58
|
+
@properties[name].merge(property)
|
59
|
+
else
|
60
|
+
@properties[name] = property
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def deep_copy(value = @value)
|
65
|
+
copy = dup
|
66
|
+
copy.value = value
|
67
|
+
copy.properties = @properties.inject({}) do |result, (k, v)|
|
68
|
+
result.update(k => v.deep_copy)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Creates the JSON representation of this keyframes
|
73
|
+
# selector.
|
74
|
+
#
|
75
|
+
# @return [Hash]
|
76
|
+
def to_json
|
77
|
+
@properties.inject({}) do |result, (name, prop)|
|
78
|
+
result.update(name.to_sym => prop.to_json)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Creates and puts te properties into the set of
|
83
|
+
# properties of this rule.
|
84
|
+
#
|
85
|
+
# @return [Void]
|
86
|
+
def process_properties(properties)
|
87
|
+
properties.each do |prop|
|
88
|
+
add_property(Property.new(prop, ['no condition'])) if prop.is_a?(Sass::Tree::PropNode)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CssCompare
|
2
|
+
module CSS
|
3
|
+
module Component
|
4
|
+
# Represents a @page's margin box declaration.
|
5
|
+
# A page margin box consists of a margin symbol, like
|
6
|
+
# `@top-left-corner` and a list of declarations.
|
7
|
+
#
|
8
|
+
# MarginBox inherits from the Selector class, since
|
9
|
+
# there are inevitable similarities. The specified margin
|
10
|
+
# symbol can be reached by the `value` property of the
|
11
|
+
# instance of this class.
|
12
|
+
#
|
13
|
+
# @see Selector
|
14
|
+
class MarginBox < Selector
|
15
|
+
IGNORED_CONDITIONS = %w(width height aspect-ratio orientation).freeze
|
16
|
+
|
17
|
+
# Looks for a `size` property to delete the values
|
18
|
+
# that should be ignored according to the @page
|
19
|
+
# W3 specification.
|
20
|
+
#
|
21
|
+
# If a size property declaration is qualified by a
|
22
|
+
# 'width', 'height', 'device-width', 'device-height',
|
23
|
+
# 'aspect-ratio', 'device-aspect-ratio' or 'orientation'
|
24
|
+
# media query (or other conditional on the size of the
|
25
|
+
# paper), then the declaration must be ignored.
|
26
|
+
#
|
27
|
+
# @see https://www.w3.org/TR/css3-page/#page-size
|
28
|
+
# ISSUE 3 and EXAMPLE 23
|
29
|
+
#
|
30
|
+
# @see Property#add_property
|
31
|
+
def add_property(prop, deep_copy = false)
|
32
|
+
prop.values.delete_if do |k, _|
|
33
|
+
IGNORED_CONDITIONS.any? { |condition| k.include?(condition) }
|
34
|
+
end if prop.name == 'size'
|
35
|
+
super(prop, deep_copy)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module CssCompare
|
2
|
+
module CSS
|
3
|
+
module Component
|
4
|
+
# Represents @page directive's page selector
|
5
|
+
# (`:right`, `LandscapeTable`, `CompanyLetterHead:first`)
|
6
|
+
# combined with the page body, that can contain a
|
7
|
+
# list of declarations and a list of page margin boxes.
|
8
|
+
#
|
9
|
+
# The declarations not assigned to any margin symbol
|
10
|
+
# will be automatically grouped under the global
|
11
|
+
# margin symbol.
|
12
|
+
#
|
13
|
+
# @see https://www.w3.org/TR/css3-page/
|
14
|
+
class PageSelector < Base
|
15
|
+
# The value of this page selector
|
16
|
+
#
|
17
|
+
# @return [String]
|
18
|
+
attr_accessor :value
|
19
|
+
|
20
|
+
# The margin box directives containing
|
21
|
+
# the declarations.
|
22
|
+
#
|
23
|
+
# @return [Hash{String => MarginBox}]
|
24
|
+
attr_accessor :margin_boxes
|
25
|
+
|
26
|
+
# The global margin symbol.
|
27
|
+
GLOBAL_MARGIN_SYMBOL = '@all'.freeze
|
28
|
+
|
29
|
+
# @param [String] value the page selector
|
30
|
+
# @param [Array<Sass::Tree::Node>] children page body
|
31
|
+
# @param [Array<String>] conditions applying media query conditions
|
32
|
+
def initialize(value, children, conditions)
|
33
|
+
@value = value
|
34
|
+
@margin_boxes = {}
|
35
|
+
process_margin_boxes(children, conditions)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Checks, whether two page selectors are equal.
|
39
|
+
#
|
40
|
+
# Two page selectors are equal only if both have
|
41
|
+
# declared the same margin boxes and they are also
|
42
|
+
# equal.
|
43
|
+
#
|
44
|
+
# @param [PageSelector] other the page selector to
|
45
|
+
# compare this with.
|
46
|
+
# @return [Boolean]
|
47
|
+
def ==(other)
|
48
|
+
super(@margin_boxes, other.margin_boxes)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Merges this page selector with another one.
|
52
|
+
#
|
53
|
+
# @param [PageSelector]
|
54
|
+
# @return [Void]
|
55
|
+
def merge(other)
|
56
|
+
other.margin_boxes.each do |_, prop|
|
57
|
+
add_margin_box(prop, true)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates a deep copy of this page selector.
|
62
|
+
#
|
63
|
+
# @param [String] value the new value of
|
64
|
+
# the selector's copy.
|
65
|
+
# @return [PageSelector] a copy of self
|
66
|
+
def deep_copy(value = @value)
|
67
|
+
copy = dup
|
68
|
+
copy.value = value
|
69
|
+
copy.margin_boxes = @margin_boxes.inject({}) do |result, (k, v)|
|
70
|
+
result.update(k => v.deep_copy)
|
71
|
+
end
|
72
|
+
copy
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates the JSON representation of this page selector.
|
76
|
+
#
|
77
|
+
# @return [Hash]
|
78
|
+
def to_json
|
79
|
+
json = { :selector => @value, :margin_boxes => [] }
|
80
|
+
@margin_boxes.inject(json[:margin_boxes]) do |result, (_k, v)|
|
81
|
+
result << v.to_json
|
82
|
+
end
|
83
|
+
json
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Adds a new or updates an already existing margin box.
|
89
|
+
#
|
90
|
+
# @param [MarginBox] margin_box the margin box to be added
|
91
|
+
# @param [Boolean] deep_copy checks whether the margin_box
|
92
|
+
# should be added by reference or its deep copied value.
|
93
|
+
def add_margin_box(margin_box, deep_copy = false)
|
94
|
+
if @margin_boxes[margin_box.name]
|
95
|
+
@margin_boxes[margin_box.name].merge(margin_box)
|
96
|
+
else
|
97
|
+
@margin_boxes[margin_box.name] = if deep_copy
|
98
|
+
margin_box.deep_copy
|
99
|
+
else
|
100
|
+
margin_box
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Processes and evaluates the page body.
|
106
|
+
#
|
107
|
+
# @param [Array<Sass::Tree::Node>] nodes (see #initialize)
|
108
|
+
# @param [Array<String>] conditions (see #initialize)
|
109
|
+
def process_margin_boxes(nodes, conditions)
|
110
|
+
nodes.each do |node|
|
111
|
+
if node.is_a?(Sass::Tree::PropNode)
|
112
|
+
margin_box = GLOBAL_MARGIN_SYMBOL
|
113
|
+
children = [node]
|
114
|
+
elsif node.is_a?(Sass::Tree::DirectiveNode)
|
115
|
+
margin_box = node.resolved_value
|
116
|
+
children = node.children
|
117
|
+
else
|
118
|
+
next # just ignore these nodes
|
119
|
+
end
|
120
|
+
add_margin_box(MarginBox.new(margin_box, children, conditions))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module CssCompare
|
2
|
+
module CSS
|
3
|
+
module Component
|
4
|
+
# Represents a CSS property tied to a specific selector.
|
5
|
+
# It can have several values based on the conditions
|
6
|
+
# specified by the @media queries.
|
7
|
+
class Property < Base
|
8
|
+
# @return [String] name of the property
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# Key-value pair of property values.
|
12
|
+
#
|
13
|
+
# The key represents the condition set by a media query
|
14
|
+
# and the value is the actual value of the property.
|
15
|
+
#
|
16
|
+
# @return [Hash{String=>Value}] name of the property
|
17
|
+
attr_accessor :values
|
18
|
+
|
19
|
+
# @note An optimization has been done here, as well.
|
20
|
+
# If there are several conditions, all should be
|
21
|
+
# paired with the same value but not the same object,
|
22
|
+
# since the value can be overridden later in the process
|
23
|
+
# of the stylesheet evaluation. A clone of the value is
|
24
|
+
# paired with each of the conditions.
|
25
|
+
#
|
26
|
+
# @param [Sass::Tree::PropNode] node the property node
|
27
|
+
# @param [Array<String>] conditions media query conditions
|
28
|
+
def initialize(node, conditions)
|
29
|
+
@name = node.resolved_name
|
30
|
+
@values = {}
|
31
|
+
value = Value.new(node.resolved_value)
|
32
|
+
conditions.each { |c| set_value(value.clone, c) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks whether two properties are equal.
|
36
|
+
#
|
37
|
+
# Properties are equal only if both contain
|
38
|
+
# the same keys and the values under those
|
39
|
+
# keys are also equal.
|
40
|
+
#
|
41
|
+
# @param [Property] other the property to compare this
|
42
|
+
# with.
|
43
|
+
# @return [Boolean]
|
44
|
+
def ==(other)
|
45
|
+
super(@values, other.values)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Merges the property with another one.
|
49
|
+
#
|
50
|
+
# @param [Property] property to be merged with `self`
|
51
|
+
# @return [Void]
|
52
|
+
def merge(property)
|
53
|
+
property.values.each { |cond, v| set_value(v, cond) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# Replaces the original value of the property under a certain
|
57
|
+
# media query condition.
|
58
|
+
#
|
59
|
+
# If the value does not exist under the specified condition,
|
60
|
+
# it is added into the set of property values. Otherwise, it
|
61
|
+
# rewrites the current value if it's not set as important.
|
62
|
+
# However, if the replacing value is also set as important,
|
63
|
+
# the current value will be replaced with the new one.
|
64
|
+
#
|
65
|
+
# If the condition does not exist but there is an important
|
66
|
+
# global value assigned to the property, the replacing value
|
67
|
+
# will be used only, if it's set to important, too. Otherwise,
|
68
|
+
# the global value should be cloned in there.
|
69
|
+
#
|
70
|
+
# @param [Value] val that should replace the current value
|
71
|
+
# @param [String] condition the circumstance, under which
|
72
|
+
# the property should take the new value.
|
73
|
+
# @return [Void]
|
74
|
+
def set_value(val, condition = 'all')
|
75
|
+
global_value = @values['all']
|
76
|
+
val_to_replace = value(condition)
|
77
|
+
# Check, whether the condition exists
|
78
|
+
if val_to_replace
|
79
|
+
@values[condition].value = val if val.important? || !val_to_replace.important?
|
80
|
+
else
|
81
|
+
@values[condition] = if global_value && global_value.important?
|
82
|
+
val.important? ? val : global_value.clone
|
83
|
+
else
|
84
|
+
val
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the property's value taken under a certain
|
90
|
+
# circumstance
|
91
|
+
#
|
92
|
+
# @return [#to_s]
|
93
|
+
def value(condition = 'all')
|
94
|
+
@values[condition]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Whether the property is a complex one.
|
98
|
+
# One property, that can be separated
|
99
|
+
# into smaller chunks of elementary properties.
|
100
|
+
#
|
101
|
+
# Example:
|
102
|
+
# `border: 1px solid black` => `{
|
103
|
+
# border-width: 1px;
|
104
|
+
# border-style: solid;
|
105
|
+
# border-color: black;
|
106
|
+
# }`
|
107
|
+
#
|
108
|
+
# @see #process
|
109
|
+
# @return [Boolean]
|
110
|
+
def complex?
|
111
|
+
COMPLEX_PROPERTIES.include?(@name)
|
112
|
+
false
|
113
|
+
end
|
114
|
+
|
115
|
+
# Creates a deep copy of this property.
|
116
|
+
#
|
117
|
+
# @return [Property]
|
118
|
+
def deep_copy
|
119
|
+
copy = dup
|
120
|
+
copy.values = {}
|
121
|
+
@values.each { |k, v| copy.values[k] = v.clone }
|
122
|
+
copy
|
123
|
+
end
|
124
|
+
|
125
|
+
# Creates the JSON representation of this property.
|
126
|
+
#
|
127
|
+
# @return [Hash]
|
128
|
+
def to_json
|
129
|
+
@values.inject({}) { |result, (k, v)| result.update(k => v.to_s) }
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
COMPLEX_PROPERTIES = [].freeze
|
135
|
+
|
136
|
+
# Checks, whether an unprocessed value is important
|
137
|
+
# or not
|
138
|
+
#
|
139
|
+
# @return [Boolean]
|
140
|
+
def important_value?(val)
|
141
|
+
val.to_s.include?('!important')
|
142
|
+
end
|
143
|
+
|
144
|
+
# Breaks down complex properties like `border` into
|
145
|
+
# smaller chunks (`border-width`, `border-style`, `border-color`)
|
146
|
+
#
|
147
|
+
# @return [Array<Property>]
|
148
|
+
def process
|
149
|
+
return nil unless complex?
|
150
|
+
[]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module CssCompare
|
2
|
+
module CSS
|
3
|
+
module Component
|
4
|
+
# Represents one simple selector sequence, like
|
5
|
+
# .a.b.c > div.f:first-child.
|
6
|
+
#
|
7
|
+
# @see https://www.w3.org/TR/css3-selectors/#selectors
|
8
|
+
class Selector < Base
|
9
|
+
# @return [String] selector's name
|
10
|
+
attr_accessor :name
|
11
|
+
|
12
|
+
# Hash of the selector's properties. Could have been
|
13
|
+
# an array but this structure has been chosen, so the
|
14
|
+
# properties' lookup gets optimized.
|
15
|
+
#
|
16
|
+
# @return [Hash{String => Component::Property}] properties
|
17
|
+
attr_accessor :properties
|
18
|
+
|
19
|
+
# @param [String] name of the selector
|
20
|
+
# @param [Array<Sass::Tree::PropNode>] properties to be included
|
21
|
+
# @param [Array<String>] conditions @media query conditions
|
22
|
+
def initialize(name, properties, conditions)
|
23
|
+
@name = name
|
24
|
+
@properties = {}
|
25
|
+
process_properties(properties, conditions)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Checks, whether two selector are equal.
|
29
|
+
#
|
30
|
+
# Two selectors are equal only if they both have declared
|
31
|
+
# the same properties and they are also equal.
|
32
|
+
#
|
33
|
+
# @param [Selector] other the selector to compare this with.
|
34
|
+
# @return [Boolean]
|
35
|
+
def ==(other)
|
36
|
+
super(@properties, other.properties)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Combines the selector's properties with the
|
40
|
+
# properties of another selector.
|
41
|
+
#
|
42
|
+
# @param [Property, Array<Property>] other
|
43
|
+
# the selector to be merged with
|
44
|
+
# @return [Void]
|
45
|
+
def merge(other)
|
46
|
+
other.properties.each do |_, prop|
|
47
|
+
add_property(prop, true)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Adds a property to the existing set of properties
|
52
|
+
# of this selector.
|
53
|
+
#
|
54
|
+
# If the property does not exist, it will be
|
55
|
+
# added. Otherwise the values of the properties
|
56
|
+
# will be merged.
|
57
|
+
#
|
58
|
+
# @see {Property#merge}
|
59
|
+
# @param [Property] prop the property to add
|
60
|
+
# @param [Boolean] deep_copy tells, whether a
|
61
|
+
# deep copy should be applied onto the property.
|
62
|
+
# @return [Void]
|
63
|
+
def add_property(prop, deep_copy = false)
|
64
|
+
name = prop.name
|
65
|
+
if @properties[name]
|
66
|
+
@properties[name].merge(prop)
|
67
|
+
else
|
68
|
+
@properties[name] = if deep_copy
|
69
|
+
prop.deep_copy
|
70
|
+
else
|
71
|
+
prop
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Creates a deep copy of this selector.
|
77
|
+
#
|
78
|
+
# @param [String] name the new name of
|
79
|
+
# the selector's copy.
|
80
|
+
# @return [Selector] a copy of self
|
81
|
+
def deep_copy(name = @name)
|
82
|
+
copy = dup
|
83
|
+
copy.name = name
|
84
|
+
copy.properties = {}
|
85
|
+
@properties.each { |k, v| copy.properties[k] = v.deep_copy }
|
86
|
+
copy
|
87
|
+
end
|
88
|
+
|
89
|
+
# Creates the JSON representation of this selector.
|
90
|
+
#
|
91
|
+
# @return [Hash]
|
92
|
+
def to_json
|
93
|
+
key = @name.to_sym
|
94
|
+
json = { key => {} }
|
95
|
+
@properties.inject(json[key]) do |result, (k, v)|
|
96
|
+
result.update(k => v.to_json)
|
97
|
+
end
|
98
|
+
json
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
# Walks through the property nodes ands merges them
|
104
|
+
# to the selector's set of properties.
|
105
|
+
#
|
106
|
+
# @param [Array<Sass::Tree::Node>] properties potential property
|
107
|
+
# nodes of the selector.
|
108
|
+
# @param [Array<String>] conditions @media query conditions
|
109
|
+
# @return [Void]
|
110
|
+
def process_properties(properties, conditions)
|
111
|
+
properties.each do |property|
|
112
|
+
add_property(Property.new(property, conditions)) if property.is_a?(Sass::Tree::PropNode)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|