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,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