css_compare 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/css_compare ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # A command line CSS comparison tool
3
+
4
+ begin
5
+ require_relative '../lib/css_compare'
6
+ rescue
7
+ require 'css_compare'
8
+ end
9
+
10
+ opts = CssCompare::Comparison.new(ARGV)
11
+ opts.parse!
@@ -0,0 +1,6 @@
1
+ dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
3
+
4
+ require 'css_compare/constants'
5
+ require 'css_compare/exec'
6
+ require 'css_compare/util'
@@ -0,0 +1,5 @@
1
+ module CssCompare
2
+ # The root directory of the Less2Sass source tree.
3
+ ROOT_DIR = File.expand_path(File.join(__FILE__, '../../..'))
4
+ VERSION = '0.1.0'.freeze
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'css_compare/css/component'
2
+ require 'css_compare/css/parser'
3
+ require 'css_compare/css/engine'
@@ -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