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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6abcfc4cf2f5468f2fa82b1bfedc3eb7b9555d92
|
4
|
+
data.tar.gz: 85c19fb0d3993629cda8586a41a6f7b53d380e2c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 55e74e0702da7383a1d2259d5ea73eafc182c983ffc2d1ecd3d77e7a51a73fe4f39a0a37a66cda847f2e3c8a69fae5be3668f210fb600b8700b593a27f412136
|
7
|
+
data.tar.gz: b77a5b69826a4458e415e1c94dd5ed0b6a38bae4e95a34b1d273e59bc274bb4e8a7fe7d01c3b986d3ff5dc8adf49f8e1f82b93267fa3c2e881c6fc7a1311be55
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Attila Večerek
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# CSS Compare
|
2
|
+
|
3
|
+
Processes, evaluates and compares 2 CSS files based on their AST. The repository has been created in order to be able to test the [less2sass](https://github.com/vecerek/less2sass) project. The program returns `true` or `false` to the `$stdout`, so far.
|
4
|
+
Uses the Sass parser to get the CSS files' AST.
|
5
|
+
|
6
|
+
Supported CSS features:
|
7
|
+
- all types of selectors (they are normalized - duplicity removal and logical/alphabetical ordering)
|
8
|
+
- @media, partially
|
9
|
+
- @import (lazy loading of imported css files, that can be found, otherwise ignored)
|
10
|
+
- @font-face
|
11
|
+
- @namespace
|
12
|
+
- @charset
|
13
|
+
- @keyframes
|
14
|
+
- @page
|
15
|
+
- @supports, partially
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'css_compare'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install css_compare
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
Command line usage:
|
35
|
+
|
36
|
+
$ css_compare <CSS file> <CSS file>
|
37
|
+
|
38
|
+
Programmatic usage:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
opts = {
|
42
|
+
:operands => ["path/to/file1.css", "path/to/file2.css"]
|
43
|
+
}
|
44
|
+
result = CssCompare::Engine.new(opts)
|
45
|
+
.parse!
|
46
|
+
.equal?
|
47
|
+
```
|
48
|
+
|
49
|
+
## TODO
|
50
|
+
|
51
|
+
- Evaluate shorthand properties, so the values of base properties get overridden.
|
52
|
+
- Evaluate @media rule's and @supports rule's conditions.
|
53
|
+
- Output the difference, optionally.
|
54
|
+
|
55
|
+
## Contributing
|
56
|
+
|
57
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/vecerek/css_compare.
|
58
|
+
|
59
|
+
|
60
|
+
## License
|
61
|
+
|
62
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
63
|
+
|
data/Rakefile
ADDED
data/bin/css_compare
ADDED
data/lib/css_compare.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'css_compare/css/component/base'
|
2
|
+
require 'css_compare/css/component/value'
|
3
|
+
require 'css_compare/css/component/property'
|
4
|
+
require 'css_compare/css/component/selector'
|
5
|
+
require 'css_compare/css/component/keyframes_selector'
|
6
|
+
require 'css_compare/css/component/keyframes'
|
7
|
+
require 'css_compare/css/component/supports'
|
8
|
+
require 'css_compare/css/component/margin_box'
|
9
|
+
require 'css_compare/css/component/page_selector'
|
10
|
+
require 'css_compare/css/component/font_face'
|
11
|
+
|
12
|
+
module CssCompare
|
13
|
+
module CSS
|
14
|
+
module Component
|
15
|
+
# Creates a new {Sass::Tree::RootNode}.
|
16
|
+
#
|
17
|
+
# @param [Array<Sass::Tree::Node>] children the child nodes
|
18
|
+
# of the newly created node.
|
19
|
+
# @param [Hash] options node options
|
20
|
+
# @return [Sass::Tree::RootNode]
|
21
|
+
def root_node(children, options)
|
22
|
+
root = Sass::Engine.new('').to_tree
|
23
|
+
root.options = options
|
24
|
+
root.children = children.is_a?(Array) ? children : [children]
|
25
|
+
root
|
26
|
+
end
|
27
|
+
|
28
|
+
# Creates a new {Sass::Tree::MediaNode} from scratch.
|
29
|
+
#
|
30
|
+
# @param [Array<String, Sass::Media::Query>] query the
|
31
|
+
# list of media queries
|
32
|
+
# @param [Sass::Tree::Node] children (see #root_node)
|
33
|
+
# @param [Hash] options (see #root_node)
|
34
|
+
# @return [Sass::Tree::MediaNode]
|
35
|
+
def media_node(query, children, options)
|
36
|
+
media_node = Sass::Tree::MediaNode.new(query)
|
37
|
+
media_node.options = options
|
38
|
+
media_node.line = 1
|
39
|
+
media_node = Sass::Tree::Visitors::Perform.visit(media_node)
|
40
|
+
media_node.children = children
|
41
|
+
media_node
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CssCompare
|
2
|
+
module CSS
|
3
|
+
module Component
|
4
|
+
class Base
|
5
|
+
# Checks, whether two hashes are equal.
|
6
|
+
#
|
7
|
+
# They are equal, if they contain the same keys
|
8
|
+
# and also have the same values assigned.
|
9
|
+
#
|
10
|
+
# @param [Hash] this first hash to compare
|
11
|
+
# @param [Hash] that second hash to compare
|
12
|
+
# @return [Boolean]
|
13
|
+
def ==(this, that)
|
14
|
+
keys = this.keys + that.keys
|
15
|
+
keys.uniq!
|
16
|
+
keys.all? { |key| this[key] && that[key] && this[key] == that[key] }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
module CssCompare
|
2
|
+
module CSS
|
3
|
+
module Component
|
4
|
+
# Represents the @font-face directive.
|
5
|
+
#
|
6
|
+
# Multiple @font-face rules can be used to construct
|
7
|
+
# font families with a variety of faces. Each @font-face
|
8
|
+
# rule specifies a value for every font descriptor,
|
9
|
+
# either implicitly or explicitly. Those not given explicit
|
10
|
+
# values in the rule take the initial value listed with
|
11
|
+
# each descriptor in the w3.org specification.
|
12
|
+
#
|
13
|
+
# If multiple declarations of @font-face rules, that share
|
14
|
+
# the same `font-family` and `src` values are present, the last
|
15
|
+
# declaration overrides the other.
|
16
|
+
#
|
17
|
+
# `Font-family` property is saved downcased, since the user
|
18
|
+
# agents, when matching font-family names, do it in
|
19
|
+
# a case-insensitive manner.
|
20
|
+
#
|
21
|
+
# @see https://www.w3.org/TR/css-fonts-3/#font-face-rule
|
22
|
+
class FontFace < Base
|
23
|
+
attr_reader :declarations
|
24
|
+
|
25
|
+
# @param [Array<Sass::Tree::PropNode>] children font properties
|
26
|
+
def initialize(children)
|
27
|
+
@declarations = {}
|
28
|
+
init_declarations
|
29
|
+
process_declarations(children)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Checks, whether two @font-face declarations are equal.
|
33
|
+
#
|
34
|
+
# No need to check, whether both font-faces have the same
|
35
|
+
# keys, since they are also initialized with the default
|
36
|
+
# values.
|
37
|
+
#
|
38
|
+
# @param [FontFace] other the @font-face to compare this
|
39
|
+
# with.
|
40
|
+
def ==(other)
|
41
|
+
@declarations.all? { |k, _| @declarations[k] == other.declarations[k] }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Tells, whether this rule is valid or not.
|
45
|
+
#
|
46
|
+
# @font-face rules require a font-family and src descriptor;
|
47
|
+
# if either of these are missing, the @font-face rule is
|
48
|
+
# invalid and must be ignored entirely.
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
def valid?
|
52
|
+
family && src
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [String, nil] font-family name, if set
|
56
|
+
def family
|
57
|
+
@declarations['font-family']
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String, nil] the source of the font if set
|
61
|
+
def src
|
62
|
+
@declarations['src']
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates the JSON representation of this object.
|
66
|
+
#
|
67
|
+
# @return [Hash]
|
68
|
+
def to_json
|
69
|
+
@declarations
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
INITIAL_VALUES = {
|
75
|
+
:font_family => {
|
76
|
+
:default => nil
|
77
|
+
},
|
78
|
+
:src => {
|
79
|
+
:default => nil
|
80
|
+
},
|
81
|
+
:font_style => {
|
82
|
+
:default => 'normal',
|
83
|
+
:allowed => %w(normal italic oblique)
|
84
|
+
},
|
85
|
+
:font_weight => {
|
86
|
+
:default => '400',
|
87
|
+
:allowed => %w(normal bold 100 200 300 400 500 600 700 800 900),
|
88
|
+
:synonyms => {
|
89
|
+
:normal => '400',
|
90
|
+
:bold => '600'
|
91
|
+
}
|
92
|
+
},
|
93
|
+
:font_stretch => {
|
94
|
+
:default => 'normal',
|
95
|
+
:allowed => %w(normal ultra-condensed extra-condensed condensed semi-condensed semi-expanded expanded
|
96
|
+
extra-expanded ultra-expanded)
|
97
|
+
},
|
98
|
+
:unicode_range => {
|
99
|
+
:default => 'U+0-10FFFF'
|
100
|
+
},
|
101
|
+
:font_variant => {
|
102
|
+
:default => 'normal'
|
103
|
+
},
|
104
|
+
:font_feature_settings => {
|
105
|
+
:default => 'normal'
|
106
|
+
},
|
107
|
+
:font_kerning => {
|
108
|
+
:default => 'auto',
|
109
|
+
:allowed => %w(auto normal none)
|
110
|
+
},
|
111
|
+
:font_variant_ligatures => {
|
112
|
+
:default => 'normal'
|
113
|
+
},
|
114
|
+
:font_variant_position => {
|
115
|
+
:default => 'normal',
|
116
|
+
:allowed => %w(normal sub super)
|
117
|
+
},
|
118
|
+
:font_variant_caps => {
|
119
|
+
:default => 'normal',
|
120
|
+
:allowed => %w(normal small-caps all-small-caps petite-caps all-petite-caps unicase titling-caps)
|
121
|
+
},
|
122
|
+
:font_variant_numeric => {
|
123
|
+
:default => 'normal'
|
124
|
+
},
|
125
|
+
:font_variant_alternates => {
|
126
|
+
:default => 'normal'
|
127
|
+
},
|
128
|
+
:font_variant_east_asian => {
|
129
|
+
:default => 'normal'
|
130
|
+
},
|
131
|
+
:font_language_override => {
|
132
|
+
:default => 'normal'
|
133
|
+
}
|
134
|
+
|
135
|
+
}.freeze
|
136
|
+
|
137
|
+
# Initializes the font-face with values from
|
138
|
+
# the official specifications.
|
139
|
+
#
|
140
|
+
# @return [Void]
|
141
|
+
def init_declarations
|
142
|
+
INITIAL_VALUES.each { |k, v| @declarations[k.to_s.tr('_', '-')] = v[:default] }
|
143
|
+
end
|
144
|
+
|
145
|
+
# Processes the @font-face declarations and set
|
146
|
+
# the values if:
|
147
|
+
# 1. the property processed is a valid font-face property
|
148
|
+
# 2. the property has a valid value
|
149
|
+
# If the property's value is not valid, it falls back to
|
150
|
+
# the default one.
|
151
|
+
#
|
152
|
+
# @param [Array<Sass::Tree::PropNode>] children
|
153
|
+
# @return [Void]
|
154
|
+
def process_declarations(children)
|
155
|
+
children.each do |child|
|
156
|
+
next unless child.is_a?(Sass::Tree::PropNode)
|
157
|
+
name = child.resolved_name
|
158
|
+
value = child.resolved_value
|
159
|
+
key = name.tr('-', '_').to_sym
|
160
|
+
property = INITIAL_VALUES[key]
|
161
|
+
next unless property
|
162
|
+
@declarations[name] = value.downcase if name == 'font-family'
|
163
|
+
@declarations[name] = value.gsub(/'|"/, '') if name == 'src'
|
164
|
+
next if name == 'font-family' || name == 'src'
|
165
|
+
@declarations[name] = value
|
166
|
+
allowed_values = property[:allowed]
|
167
|
+
next unless allowed_values
|
168
|
+
if allowed_values.include?(value)
|
169
|
+
@declarations[name] = get_synonym(property, value.to_sym) || value
|
170
|
+
else
|
171
|
+
@declaration[name] = property[:default]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns the synonym for a property's value.
|
177
|
+
#
|
178
|
+
# @example
|
179
|
+
# get_synonym(INITIAL_VALUES[:font-weight], [:bold]) #=> '600'
|
180
|
+
#
|
181
|
+
# @return [String, nil] a string, if a synonym exists
|
182
|
+
# nil otherwise
|
183
|
+
def get_synonym(property, key)
|
184
|
+
return unless property[:synonyms] && property[:synonyms].include?(key)
|
185
|
+
property[:synonyms][key]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module CssCompare
|
2
|
+
module CSS
|
3
|
+
module Component
|
4
|
+
# Represents an @keyframes declaration
|
5
|
+
#
|
6
|
+
# @see https://www.w3.org/TR/css3-animations/#keyframes
|
7
|
+
class Keyframes < Base
|
8
|
+
# The name of the keyframes.
|
9
|
+
#
|
10
|
+
# @return [String] the name
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# The rules of the keyframe grouped by
|
14
|
+
# @media query conditions.
|
15
|
+
#
|
16
|
+
# Rules' example structure:
|
17
|
+
# {
|
18
|
+
# 'all' => {
|
19
|
+
# '0%' => {KeyframeSelector},
|
20
|
+
# '100%' => {KeyframeSelector}
|
21
|
+
# },
|
22
|
+
# '(max-width: 600px)' => {
|
23
|
+
# '0%' => {KeyframeSelector},
|
24
|
+
# '50%' => {KeyframeSelector},
|
25
|
+
# '100%' => {KeyframeSelector}
|
26
|
+
# }
|
27
|
+
# }
|
28
|
+
#
|
29
|
+
# @return [Hash{String => Hash{String => KeyframeSelector}}]
|
30
|
+
attr_reader :rules
|
31
|
+
|
32
|
+
def initialize(node, conditions)
|
33
|
+
@name = node.value[1]
|
34
|
+
process_conditions(conditions, process_rules(node.children))
|
35
|
+
end
|
36
|
+
|
37
|
+
# Checks, whether two @keyframes are equal.
|
38
|
+
#
|
39
|
+
# Two @keyframes are only equal, if they both have equal
|
40
|
+
# keyframe selectors under each and every condition.
|
41
|
+
# If a condition or frame is missing from one or another,
|
42
|
+
# the @keyframes are not equal.
|
43
|
+
#
|
44
|
+
# @param [Keyframes] other the @keyframe to compare this
|
45
|
+
# with.
|
46
|
+
# @param [Boolean]
|
47
|
+
def ==(other)
|
48
|
+
conditions = @rules.keys + other.rules.keys
|
49
|
+
conditions.uniq!
|
50
|
+
conditions.all? do |condition|
|
51
|
+
return false unless @rules[condition] && other.rules[condition]
|
52
|
+
super(@rules[condition], other.rules[condition])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Merges this selector with another one.
|
57
|
+
#
|
58
|
+
# The new declaration of the keyframe under the same
|
59
|
+
# condition rewrites the previous one. No deep_copy
|
60
|
+
# needs to be made and the value can be passed by
|
61
|
+
# reference.
|
62
|
+
#
|
63
|
+
# @param [Keyframes] keyframes the keyframes to
|
64
|
+
# extend this one.
|
65
|
+
# @return [Void]
|
66
|
+
def merge(keyframes)
|
67
|
+
keyframes.rules.each do |condition, selector|
|
68
|
+
@rules[condition] = selector
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def deep_copy(name = @name)
|
73
|
+
copy = dup
|
74
|
+
copy.name = name
|
75
|
+
copy.rules = @rules.inject({}) do |result, (k, v)|
|
76
|
+
result.update(k => v.deep_copy)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Creates the JSON representation of this keyframes.
|
81
|
+
#
|
82
|
+
# @return [Hash]
|
83
|
+
def to_json
|
84
|
+
json = { :name => @name.to_sym, :rules => {} }
|
85
|
+
@rules.each_with_object(json[:rules]) do |(cond, rules), frames|
|
86
|
+
rules.each_with_object(frames[cond.to_sym] = {}) do |(value, rule), result|
|
87
|
+
result.update(value.to_sym => rule.to_json)
|
88
|
+
end
|
89
|
+
frames
|
90
|
+
end
|
91
|
+
json
|
92
|
+
end
|
93
|
+
|
94
|
+
# Assigns the processed rules to the passed conditions
|
95
|
+
# By reference. No deep copy needed, as the {KeyframeSelector}s
|
96
|
+
# won't be altered or merged with another {KeyframeSelector},
|
97
|
+
# since this feature is missing at @keyframe directives.
|
98
|
+
#
|
99
|
+
# @return [Hash{String => Hash}]
|
100
|
+
# @see `@rules`
|
101
|
+
def process_conditions(conditions, keyframe_rules)
|
102
|
+
@rules = conditions.inject({}) do |kf, condition|
|
103
|
+
kf.update(condition => keyframe_rules)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Processes the keyframe rules and creates their
|
108
|
+
# internal representation.
|
109
|
+
#
|
110
|
+
# @return [Hash{String => KeyframeSelector}]
|
111
|
+
def process_rules(rule_nodes)
|
112
|
+
rule_nodes.each_with_object({}) do |node, rules|
|
113
|
+
if node.is_a?(Sass::Tree::KeyframeRuleNode)
|
114
|
+
rule = Component::KeyframesSelector.new(node)
|
115
|
+
rules.update(rule.value => rule)
|
116
|
+
end
|
117
|
+
rules
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|