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