[DRE-commits] [ruby-css-parser] 01/11: New upstream version 1.5.0.pre2

Lucas Kanashiro kanashiro at moszumanska.debian.org
Sun Aug 27 23:40:23 UTC 2017


This is an automated email from the git hooks/post-receive script.

kanashiro pushed a commit to branch master
in repository ruby-css-parser.

commit 4d9fe0be43d651e5b946f8df47c01e42d5c6097c
Author: Lucas Kanashiro <kanashiro at debian.org>
Date:   Fri Aug 11 12:58:52 2017 -0300

    New upstream version 1.5.0.pre2
---
 .editorconfig                             |  10 +
 .gitignore                                |   2 +
 .jrubyrc                                  |   1 +
 .travis.yml                               |  13 +-
 CHANGELOG.md                              |  63 ++++
 Gemfile                                   |   5 +-
 Gemfile.lock                              |  23 +-
 Readme.md => README.md                    |  29 +-
 Rakefile                                  |  26 ++
 css_parser.gemspec                        |   7 +-
 lib/css_parser/parser.rb                  | 224 ++++++++++---
 lib/css_parser/regexps.rb                 | 166 +++++++++-
 lib/css_parser/rule_set.rb                | 103 ++++--
 lib/css_parser/version.rb                 |   2 +-
 test/fixtures/complex.css                 | 505 ++++++++++++++++++++++++++++++
 test/fixtures/import-malformed.css        |  35 +++
 test/test_css_parser_basic.rb             |  17 +-
 test/test_css_parser_loading.rb           |  75 ++++-
 test/test_css_parser_media_types.rb       |   8 +-
 test/test_css_parser_misc.rb              |  24 +-
 test/test_css_parser_offset_capture.rb    | 109 +++++++
 test/test_css_parser_regexps.rb           |  23 +-
 test/test_helper.rb                       |   5 +-
 test/test_merging.rb                      |   4 +-
 test/test_rule_set.rb                     |  12 +-
 test/test_rule_set_creating_shorthand.rb  |  40 ++-
 test/test_rule_set_expanding_shorthand.rb |  46 ++-
 27 files changed, 1432 insertions(+), 145 deletions(-)

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..0f09989
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,10 @@
+# editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..417d5f1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/pkg/
+
diff --git a/.jrubyrc b/.jrubyrc
new file mode 100644
index 0000000..250bfe2
--- /dev/null
+++ b/.jrubyrc
@@ -0,0 +1 @@
+cext.enabled=true
diff --git a/.travis.yml b/.travis.yml
index b7ec9c1..d110dec 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,11 @@
-notifications:
-  disabled: true
+language: ruby
+sudo: false
+cache: bundler
+before_install: rm Gemfile.lock
 rvm:
-  - 1.9.2
   - 1.9.3
-  - 2.0.0
-  - 2.1.0
+  - 2.0.0-p647
+  - 2.1.7
+  - 2.2.3
+  - 2.3.1
   - jruby
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f229f6c..9df8c5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,68 @@
 ## Ruby CSS Parser CHANGELOG
 
+## Version 1.5.0.pre2
+
+ * Extended color keywords support (https://www.w3.org/TR/css3-color/).
+ * remove_rule_set! method.
+
+### Version 1.5.0.pre
+
+ * capture_offsets feature.
+
+### Version 1.4.10
+
+ * Include uri in RemoteFileError message.
+ * Prevent to convert single declarations to their respective shorthand.
+ * Fix Ruby warnings.
+
+### Version 1.4.9
+
+ * Support for vrem, vh, vw, vmin, vmax and vm box model units.
+ * Replace obsolete calls with actual ones.
+ * Fix some Ruby warnings.
+
+### Version 1.4.8
+
+ * Allow to get CSS rules as Hash using `to_hash` method.
+ * Updates to support Ruby 1.9 and JRuby.
+ * utf-8 related update.
+
+### Version 1.4.7
+
+ * background-position shorthand fix.
+
+### Version 1.4.6
+
+ * Normalize whitespace in selectors and queries.
+ * Strip spaces from keys.
+ * More checks on ordering.
+
+### Version 1.4.5
+
+ * Maintenance release.
+
+### Version 1.4.4
+
+ * More robust redirection handling, refs #47.
+
+### Version 1.4.3
+
+ * Look for redirects, MAX_REDIRECTS set to 3, refs #36.
+ * Fix border style expanding, refs #58.
+ * load_string! described, refs #70.
+
+### Version 1.4.2
+
+ * Ship license with package, refs #69.
+
+### Version 1.4.1
+
+ * Fix background shorthands, refs #66.
+
+### Version 1.4.0
+
+ * Add support for background-size in the shorthand property @mitio
+
 ### Version 1.3.6
 
  * Fix bug not setting general rules after media query @jievans.
diff --git a/Gemfile b/Gemfile
index bdbf92c..e033093 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,10 +1,13 @@
+# Keep Gemfile.lock in repo. Reason: https://grosser.it/2015/08/14/check-in-your-gemfile-lock/
+
 source "https://rubygems.org"
 
 gemspec
 
 gem 'rake'
 gem 'bump'
-gem 'test-unit', '>= 2.5.3'
+gem 'maxitest'
+gem 'public_suffix', '~> 1.4.0', platform: [:ruby_19, :jruby]
 
 platforms :jruby do
   gem 'jruby-openssl'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2d6d15a..40169e1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,27 +1,30 @@
 PATH
   remote: .
   specs:
-    css_parser (1.3.6)
+    css_parser (1.5.0.pre2)
       addressable
 
 GEM
   remote: https://rubygems.org/
   specs:
-    addressable (2.3.6)
-    bouncy-castle-java (1.5.0146.1)
-    bump (0.3.12)
-    jruby-openssl (0.7.6.1)
-      bouncy-castle-java (>= 1.5.0146.1)
-    rake (0.9.2.2)
-    test-unit (2.5.4)
+    addressable (2.4.0)
+    bump (0.5.3)
+    maxitest (2.4.0)
+      minitest (>= 5.0.0, < 5.11.0)
+    minitest (5.10.1)
+    public_suffix (1.4.6)
+    rake (12.0.0)
 
 PLATFORMS
-  java
   ruby
 
 DEPENDENCIES
   bump
   css_parser!
   jruby-openssl
+  maxitest
+  public_suffix (~> 1.4.0)
   rake
-  test-unit (>= 2.5.3)
+
+BUNDLED WITH
+   1.14.6
diff --git a/Readme.md b/README.md
similarity index 53%
rename from Readme.md
rename to README.md
index 2c74612..5078793 100644
--- a/Readme.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Ruby CSS Parser
+# Ruby CSS Parser [![Build Status](https://travis-ci.org/premailer/css_parser.png?branch=master)](https://travis-ci.org/premailer/css_parser) [![Gem Version](https://badge.fury.io/rb/css_parser.svg)](https://badge.fury.io/rb/css_parser)
 
 Load, parse and cascade CSS rule sets in Ruby.
 
@@ -21,11 +21,15 @@ parser = CssParser::Parser.new
 parser.load_uri!('file://home/user/styles/style.css')
 
 # load a remote file, setting the base_uri and media_types
-parser.load_uri!('../style.css', {:base_uri => 'http://example.com/styles/inc/', :media_types => [:screen, :handheld])
+parser.load_uri!('../style.css', {base_uri: 'http://example.com/styles/inc/', media_types: [:screen, :handheld]})
 
 # load a local file, setting the base_dir and media_types
 parser.load_file!('print.css', '~/styles/', :print)
 
+# load a string
+parser = CssParser::Parser.new
+parser.load_string! 'a { color: hotpink; }'
+
 # lookup a rule by a selector
 parser.find_by_selector('#content')
 #=> 'font-size: 13px; line-height: 1.2;'
@@ -49,6 +53,22 @@ parser.add_block!(css)
 parser.to_s
 => #content { font-size: 13px; line-height: 1.2; }
    body { margin: 0 1em; }
+
+# capturing byte offsets within a file
+parser.load_uri!('../style.css', {base_uri: 'http://example.com/styles/inc/', capture_offsets: true)
+content_rule = parser.find_rule_sets(['#content']).first
+content_rule.filename
+#=> 'http://example.com/styles/styles.css'
+content_rule.offset
+#=> 10703..10752
+
+# capturing byte offsets within a string
+parser.load_string!('a { color: hotpink; }', {filename: 'index.html', capture_offsets: true)
+content_rule = parser.find_rule_sets(['a']).first
+content_rule.filename
+#=> 'index.html'
+content_rule.offset
+#=> 0..21
 ```
 
 # Testing
@@ -66,9 +86,6 @@ By Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-11.
 
 License: MIT
 
-Thanks to [all the wonderful contributors](http://github.com/alexdunae/css_parser/contributors) for their updates.
+Thanks to [all the wonderful contributors](http://github.com/premailer/css_parser/contributors) for their updates.
 
 Made on Vancouver Island.
-
-[![Build Status](https://travis-ci.org/alexdunae/css_parser.png)](https://travis-ci.org/alexdunae/css_parser)
-
diff --git a/Rakefile b/Rakefile
index c9d7b1b..0db3ed6 100644
--- a/Rakefile
+++ b/Rakefile
@@ -7,3 +7,29 @@ desc 'Run the unit tests.'
 Rake::TestTask.new(:default) do |test|
   test.verbose = true
 end
+
+desc 'Run a performance evaluation.'
+task :benchmark do
+  require 'benchmark'
+  require 'css_parser'
+
+  base_dir = File.dirname(__FILE__) + '/test/fixtures'
+
+  # parse the import1 file to benchmark file loading
+  time = Benchmark.measure do
+    10000.times do
+      parser = CssParser::Parser.new
+      parser.load_file!('import1.css', base_dir)
+    end
+  end
+  puts "Parsing 'import1.css' 10 000 times took #{time.real.round(4)} seconds"
+
+  # parse the import1 file to benchmark rule parsing
+  time = Benchmark.measure do
+    1000.times do
+      parser = CssParser::Parser.new
+      parser.load_file!('complex.css', base_dir)
+    end
+  end
+  puts "Parsing 'complex.css' 1 000 times took #{time.real.round(4)} seconds"
+end
diff --git a/css_parser.gemspec b/css_parser.gemspec
index eb2067d..32927fa 100644
--- a/css_parser.gemspec
+++ b/css_parser.gemspec
@@ -1,14 +1,13 @@
-$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
 name = "css_parser"
-require "#{name}/version"
+require "./lib/#{name}/version"
 
-Gem::Specification.new name, CssParser::VERSION.dup do |s|
+Gem::Specification.new name, CssParser::VERSION do |s|
   s.summary  = "Ruby CSS parser."
   s.description  = "A set of classes for parsing CSS in Ruby."
   s.email    = "code at dunae.ca"
   s.homepage = "https://github.com/premailer/#{name}"
   s.author  = "Alex Dunae"
   s.add_runtime_dependency 'addressable'
-  s.files = Dir.glob("lib/**/*")
+  s.files = Dir.glob("lib/**/*") + ["MIT-LICENSE"]
   s.license = "MIT"
 end
diff --git a/lib/css_parser/parser.rb b/lib/css_parser/parser.rb
index d6207a0..4312c87 100644
--- a/lib/css_parser/parser.rb
+++ b/lib/css_parser/parser.rb
@@ -14,15 +14,17 @@ module CssParser
   # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
   # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
   class Parser
-    USER_AGENT   = "Ruby CSS Parser/#{CssParser::VERSION} (http://github.com/alexdunae/css_parser)"
+    USER_AGENT   = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)"
 
     STRIP_CSS_COMMENTS_RX = /\/\*.*?\*\//m
     STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
 
     # Initial parsing
-    RE_AT_IMPORT_RULE = /\@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s\)]*)["']?\)?([\w\s\,^\]\(\))]*)\)?[;\n]?/
+    RE_AT_IMPORT_RULE = /\@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s\)]*)["']?\)?([\w\s\,^\]\(\)]*)\)?[;\n]?/
 
-     # Array of CSS files that have been loaded.
+    MAX_REDIRECTS = 3
+
+    # Array of CSS files that have been loaded.
     attr_reader   :loaded_uris
 
     #--
@@ -34,11 +36,13 @@ module CssParser
     def initialize(options = {})
       @options = {:absolute_paths => false,
                   :import => true,
-                  :io_exceptions => true}.merge(options)
+                  :io_exceptions => true,
+                  :capture_offsets => false}.merge(options)
 
       # array of RuleSets
       @rules = []
 
+      @redirect_count = nil
 
       @loaded_uris = []
 
@@ -77,6 +81,8 @@ module CssParser
       rule_sets = []
 
       selectors.each do |selector|
+        selector.gsub!(/\s+/, ' ')
+        selector.strip!
         each_rule_set(media_types) do |rule_set, media_type|
           if !rule_sets.member?(rule_set) && rule_set.selectors.member?(selector)
             rule_sets << rule_set
@@ -112,7 +118,7 @@ module CssParser
       options[:media_types] = [options[:media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
       options[:only_media_types] = [options[:only_media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
 
-      block = cleanup_block(block)
+      block = cleanup_block(block, options)
 
       if options[:base_uri] and @options[:absolute_paths]
         block = CssParser.convert_uris(block, options[:base_uri])
@@ -134,17 +140,22 @@ module CssParser
 
           import_path = import_rule[0].to_s.gsub(/['"]*/, '').strip
 
+          import_options = { :media_types => media_types }
+          import_options[:capture_offsets] = true if options[:capture_offsets]
+
           if options[:base_uri]
             import_uri = Addressable::URI.parse(options[:base_uri].to_s) + Addressable::URI.parse(import_path)
-            load_uri!(import_uri, options[:base_uri], media_types)
+            import_options[:base_uri] = options[:base_uri]
+            load_uri!(import_uri, import_options)
           elsif options[:base_dir]
-            load_file!(import_path, options[:base_dir], media_types)
+            import_options[:base_dir] = options[:base_dir]
+            load_file!(import_path, import_options)
           end
         end
       end
 
       # Remove @import declarations
-      block.gsub!(RE_AT_IMPORT_RULE, '')
+      block = ignore_pattern(block, RE_AT_IMPORT_RULE, options)
 
       parse_block_into_rule_sets!(block, options)
     end
@@ -157,6 +168,16 @@ module CssParser
       add_rule_set!(rule_set, media_types)
     end
 
+    # Add a CSS rule by setting the +selectors+, +declarations+, +filename+, +offset+ and +media_types+.
+    #
+    # +filename+ can be a string or uri pointing to the file or url location.
+    # +offset+ should be Range object representing the start and end byte locations where the rule was found in the file.
+    # +media_types+ can be a symbol or an array of symbols.
+    def add_rule_with_offsets!(selectors, declarations, filename, offset, media_types = :all)
+      rule_set = OffsetAwareRuleSet.new(filename, offset, selectors, declarations)
+      add_rule_set!(rule_set, media_types)
+    end
+
     # Add a CssParser RuleSet object.
     #
     # +media_types+ can be a symbol or an array of symbols.
@@ -168,6 +189,19 @@ module CssParser
       @rules << {:media_types => media_types, :rules => ruleset}
     end
 
+    # Remove a CssParser RuleSet object.
+    #
+    # +media_types+ can be a symbol or an array of symbols.
+    def remove_rule_set!(ruleset, media_types = :all)
+      raise ArgumentError unless ruleset.kind_of?(CssParser::RuleSet)
+
+      media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
+
+      @rules.reject! do |rule|
+        rule[:media_types] == media_types && rule[:rules].to_s == ruleset.to_s
+      end
+    end
+
     # Iterate through RuleSet objects.
     #
     # +media_types+ can be a symbol or an array of symbols.
@@ -182,12 +216,33 @@ module CssParser
       end
     end
 
+    # Output all CSS rules as a Hash
+    def to_h(which_media = :all)
+      out = {}
+      styles_by_media_types = {}
+      each_selector(which_media) do |selectors, declarations, specificity, media_types|
+        media_types.each do |media_type|
+          styles_by_media_types[media_type] ||= []
+          styles_by_media_types[media_type] << [selectors, declarations]
+        end
+      end
+
+      styles_by_media_types.each_pair do |media_type, media_styles|
+        ms = {}
+        media_styles.each do |media_style|
+          ms = css_node_to_h(ms, media_style[0], media_style[1])
+        end
+        out[media_type.to_s] = ms
+      end
+      out
+    end
+
     # Iterate through CSS selectors.
     #
     # +media_types+ can be a symbol or an array of symbols.
     # See RuleSet#each_selector for +options+.
-    def each_selector(media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types
-      each_rule_set(media_types) do |rule_set, media_types|
+    def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types
+      each_rule_set(all_media_types) do |rule_set, media_types|
         rule_set.each_selector(options) do |selectors, declarations, specificity|
           yield selectors, declarations, specificity, media_types
         end
@@ -195,10 +250,10 @@ module CssParser
     end
 
     # Output all CSS rules as a single stylesheet.
-    def to_s(media_types = :all)
+    def to_s(which_media = :all)
       out = ''
       styles_by_media_types = {}
-      each_selector(media_types) do |selectors, declarations, specificity, media_types|
+      each_selector(which_media) do |selectors, declarations, specificity, media_types|
         media_types.each do |media_type|
           styles_by_media_types[media_type] ||= []
           styles_by_media_types[media_type] << [selectors, declarations]
@@ -263,9 +318,16 @@ module CssParser
       current_media_query = ''
       current_declarations = ''
 
-      block.scan(/([\\]?[{}\s"]|(.[^\s"{}\\]*))/).each do |matches|
+      # once we are in a rule, we will use this to store where we started if we are capturing offsets
+      rule_start = nil
+      offset = nil
+
+      block.scan(/(([\\]{2,})|([\\]?[{}\s"])|(.[^\s"{}\\]*))/) do |matches|
         token = matches[0]
 
+        # save the regex offset so that we know where in the file we are
+        offset = Regexp.last_match.offset(0) if options[:capture_offsets]
+
         if token =~ /\A"/ # found un-escaped double quote
           in_string = !in_string
         end
@@ -277,7 +339,7 @@ module CssParser
             next
           end
 
-          if token =~ /\{/
+          if token =~ /\{/ and not in_string
             in_declarations += 1
             next
           end
@@ -290,11 +352,18 @@ module CssParser
             in_declarations -= 1
 
             unless current_declarations.strip.empty?
-              add_rule!(current_selectors, current_declarations, current_media_queries)
+              if options[:capture_offsets]
+                add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
+              else
+                add_rule!(current_selectors, current_declarations, current_media_queries)
+              end
             end
 
             current_selectors = ''
             current_declarations = ''
+
+            # restart our search for selectors and declarations
+            rule_start = nil if options[:capture_offsets]
           end
         elsif token =~ /@media/i
           # found '@media', reset current media_types
@@ -330,11 +399,14 @@ module CssParser
             end
           else
             if token =~ /\{/ and not in_string
-              current_selectors.gsub!(/^[\s]*/, '')
-              current_selectors.gsub!(/[\s]*$/, '')
+              current_selectors.strip!
               in_declarations += 1
             else
+              # if we are in a selector, add the token to the current selectors
               current_selectors += token
+
+              # mark this as the beginning of the selector unless we have already marked it
+              rule_start = offset.first if options[:capture_offsets] && rule_start.nil? && token =~ /^[^\s]+$/
             end
           end
         end
@@ -342,7 +414,11 @@ module CssParser
 
       # check for unclosed braces
       if in_declarations > 0
-        add_rule!(current_selectors, current_declarations, current_media_queries)
+        if options[:capture_offsets]
+          add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
+        else
+          add_rule!(current_selectors, current_declarations, current_media_queries)
+        end
       end
     end
 
@@ -355,7 +431,6 @@ module CssParser
     # Deprecated: originally accepted three params: `uri`, `base_uri` and `media_types`
     def load_uri!(uri, options = {}, deprecated = nil)
       uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme
-      #base_uri = nil, media_types = :all, options = {}
 
       opts = {:base_uri => nil, :media_types => :all}
 
@@ -373,27 +448,50 @@ module CssParser
 
       opts[:base_uri] = uri if opts[:base_uri].nil?
 
-      src, charset = read_remote_file(uri)
+      # pass on the uri if we are capturing file offsets
+      opts[:filename] = uri.to_s if opts[:capture_offsets]
+
+      src, = read_remote_file(uri) # skip charset
       if src
         add_block!(src, opts)
       end
     end
 
     # Load a local CSS file.
-    def load_file!(file_name, base_dir = nil, media_types = :all)
-      file_name = File.expand_path(file_name, base_dir)
+    def load_file!(file_name, options = {}, deprecated = nil)
+      opts = {:base_dir => nil, :media_types => :all}
+
+      if options.is_a? Hash
+        opts.merge!(options)
+      else
+        opts[:base_dir] = options if options.is_a? String
+        opts[:media_types] = deprecated if deprecated
+      end
+
+      file_name = File.expand_path(file_name, opts[:base_dir])
       return unless File.readable?(file_name)
       return unless circular_reference_check(file_name)
 
       src = IO.read(file_name)
-      base_dir = File.dirname(file_name)
 
-      add_block!(src, {:media_types => media_types, :base_dir => base_dir})
+      opts[:filename] = file_name if opts[:capture_offsets]
+      opts[:base_dir] = File.dirname(file_name)
+
+      add_block!(src, opts)
     end
 
     # Load a local CSS string.
-    def load_string!(src, base_dir = nil, media_types = :all)
-      add_block!(src, {:media_types => media_types, :base_dir => base_dir})
+    def load_string!(src, options = {}, deprecated = nil)
+      opts = {:base_dir => nil, :media_types => :all}
+
+      if options.is_a? Hash
+        opts.merge!(options)
+      else
+        opts[:base_dir] = options if options.is_a? String
+        opts[:media_types] = deprecated if deprecated
+      end
+
+      add_block!(src, opts)
     end
 
 
@@ -414,21 +512,33 @@ module CssParser
       end
     end
 
+    # Remove a pattern from a given string
+    #
+    # Returns a string.
+    def ignore_pattern(css, regex, options)
+      # if we are capturing file offsets, replace the characters with spaces to retail the original positions
+      return css.gsub(regex) { |m| ' ' * m.length } if options[:capture_offsets]
+
+      # otherwise just strip it out
+      css.gsub(regex, '')
+    end
+
     # Strip comments and clean up blank lines from a block of CSS.
     #
     # Returns a string.
-    def cleanup_block(block) # :nodoc:
+    def cleanup_block(block, options = {}) # :nodoc:
       # Strip CSS comments
-      block.gsub!(STRIP_CSS_COMMENTS_RX, '')
+      utf8_block = block.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: ' ')
+      utf8_block = ignore_pattern(utf8_block, STRIP_CSS_COMMENTS_RX, options)
 
       # Strip HTML comments - they shouldn't really be in here but
       # some people are just crazy...
-      block.gsub!(STRIP_HTML_COMMENTS_RX, '')
+      utf8_block = ignore_pattern(utf8_block, STRIP_HTML_COMMENTS_RX, options)
 
       # Strip lines containing just whitespace
-      block.gsub!(/^\s+$/, "")
+      utf8_block.gsub!(/^\s+$/, "") unless options[:capture_offsets]
 
-      block
+      utf8_block
     end
 
     # Download a file into a string.
@@ -438,7 +548,21 @@ module CssParser
     # TODO: add option to fail silently or throw and exception on a 404
     #++
     def read_remote_file(uri) # :nodoc:
-      return nil, nil unless circular_reference_check(uri.to_s)
+      if @redirect_count.nil?
+        @redirect_count = 0
+      else
+        @redirect_count += 1
+      end
+
+      unless circular_reference_check(uri.to_s)
+        @redirect_count = nil
+        return nil, nil
+      end
+
+      if @redirect_count > MAX_REDIRECTS
+        @redirect_count = nil
+        return nil, nil
+      end
 
       src = '', charset = nil
 
@@ -447,8 +571,11 @@ module CssParser
 
         if uri.scheme == 'file'
           # local file
-          fh = open(uri.path, 'rb')
+          path = uri.path
+          path.gsub!(/^\//, '') if Gem.win_platform?
+          fh = open(path, 'rb')
           src = fh.read
+          charset = fh.respond_to?(:charset) ? fh.charset : 'utf-8'
           fh.close
         else
           # remote file
@@ -463,11 +590,16 @@ module CssParser
 
           res = http.get(uri.request_uri, {'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip'})
           src = res.body
-          charset = fh.respond_to?(:charset) ? fh.charset : 'utf-8'
+          charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'
 
           if res.code.to_i >= 400
-            raise RemoteFileError if @options[:io_exceptions]
+            @redirect_count = nil
+            raise RemoteFileError.new(uri.to_s) if @options[:io_exceptions]
             return '', nil
+          elsif res.code.to_i >= 300 and res.code.to_i < 400
+            if res['Location'] != nil
+              return read_remote_file Addressable::URI.parse(Addressable::URI.escape(res['Location']))
+            end
           end
 
           case res['content-encoding']
@@ -489,10 +621,12 @@ module CssParser
           end
         end
       rescue
-        raise RemoteFileError if @options[:io_exceptions]
+        @redirect_count = nil
+        raise RemoteFileError.new(uri.to_s)if @options[:io_exceptions]
         return nil, nil
       end
 
+      @redirect_count = nil
       return src, charset
     end
 
@@ -513,5 +647,23 @@ module CssParser
       @css_rules = []
       @css_warnings = []
     end
+
+    # recurse through nested nodes and return them as Hashes nested in
+    # passed hash
+    def css_node_to_h(hash, key, val)
+      hash[key.strip] = '' and return hash if val.nil?
+      lines = val.split(';')
+      nodes = {}
+      lines.each do |line|
+        parts = line.split(':', 2)
+        if (parts[1] =~ /:/)
+          nodes[parts[0]] = css_node_to_h(hash, parts[0], parts[1])
+        else
+          nodes[parts[0].to_s.strip] =parts[1].to_s.strip
+        end
+      end
+      hash[key.strip] = nodes
+      hash
+    end
   end
 end
diff --git a/lib/css_parser/regexps.rb b/lib/css_parser/regexps.rb
index 68668db..1986c39 100644
--- a/lib/css_parser/regexps.rb
+++ b/lib/css_parser/regexps.rb
@@ -23,7 +23,7 @@ module CssParser
   RE_GRADIENT = /[-a-z]*gradient\([-a-z0-9 .,#%()]*\)/im
 
   # Initial parsing
-  RE_AT_IMPORT_RULE = /\@import[\s]+(url\()?["''"]?(.[^'"\s"']*)["''"]?\)?([\w\s\,^\])]*)\)?;?/
+  RE_AT_IMPORT_RULE = /\@import\s+(url\()?["']?(.[^'"\s]*)["']?\)?([\w\s\,^\])]*)\)?;?/
 
   #--
   #RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')
@@ -44,9 +44,10 @@ module CssParser
   STRIP_HTML_COMMENTS_RX = /\<\!\-\-|\-\-\>/m
 
   # Special units
-  BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx
+  BOX_MODEL_UNITS_RX = /(auto|inherit|0|([\-]*([0-9]+|[0-9]*\.[0-9]+)(rem|vw|vh|vm|vmin|vmax|e[mx]+|px|[cm]+m|p[tc+]|in|\%)))([\s;]|\Z)/imx
   RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
   RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
+  RE_BACKGROUND_SIZE = Regexp.new("\\s*/\\s*((((#{RE_LENGTH_OR_PERCENTAGE})|auto|cover|contain|initial|inherit)[\\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
   FONT_UNITS_RX = /(([x]+\-)*small|medium|large[r]*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%)*)/i
   RE_BORDER_STYLE = /([\s]*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)([\s]*$)?/imx
   RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
@@ -82,10 +83,163 @@ module CssParser
   )/ix
 
   # Colours
-  RE_COLOUR_NUMERIC = Regexp.new('((hsl|rgb)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
-  RE_COLOUR_NUMERIC_ALPHA = Regexp.new('((hsla|rgba)[\s]*\([\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*,[\s-]*[\d]+(\.[\d]+)?[%\s]*\))', Regexp::IGNORECASE)
-  RE_COLOUR_HEX = /(#([0-9a-f]{6}|[0-9a-f]{3})([\s;]|$))/i
-  RE_COLOUR_NAMED = /([\s]*^)?(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|transparent)([\s]*$)?/i
+  NAMED_COLOURS = %w[
+    aliceblue
+    antiquewhite
+    aqua
+    aquamarine
+    azure
+    beige
+    bisque
+    black
+    blanchedalmond
+    blue
+    blueviolet
+    brown
+    burlywood
+    cadetblue
+    chartreuse
+    chocolate
+    coral
+    cornflowerblue
+    cornsilk
+    crimson
+    cyan
+    darkblue
+    darkcyan
+    darkgoldenrod
+    darkgray
+    darkgreen
+    darkgrey
+    darkkhaki
+    darkmagenta
+    darkolivegreen
+    darkorange
+    darkorchid
+    darkred
+    darksalmon
+    darkseagreen
+    darkslateblue
+    darkslategray
+    darkslategrey
+    darkturquoise
+    darkviolet
+    deeppink
+    deepskyblue
+    dimgray
+    dimgrey
+    dodgerblue
+    firebrick
+    floralwhite
+    forestgreen
+    fuchsia
+    gainsboro
+    ghostwhite
+    gold
+    goldenrod
+    gray
+    green
+    greenyellow
+    grey
+    honeydew
+    hotpink
+    indianred
+    indigo
+    ivory
+    khaki
+    lavender
+    lavenderblush
+    lawngreen
+    lemonchiffon
+    lightblue
+    lightcoral
+    lightcyan
+    lightgoldenrodyellow
+    lightgray
+    lightgreen
+    lightgrey
+    lightpink
+    lightsalmon
+    lightseagreen
+    lightskyblue
+    lightslategray
+    lightslategrey
+    lightsteelblue
+    lightyellow
+    lime
+    limegreen
+    linen
+    magenta
+    maroon
+    mediumaquamarine
+    mediumblue
+    mediumorchid
+    mediumpurple
+    mediumseagreen
+    mediumslateblue
+    mediumspringgreen
+    mediumturquoise
+    mediumvioletred
+    midnightblue
+    mintcream
+    mistyrose
+    moccasin
+    navajowhite
+    navy
+    oldlace
+    olive
+    olivedrab
+    orange
+    orangered
+    orchid
+    palegoldenrod
+    palegreen
+    paleturquoise
+    palevioletred
+    papayawhip
+    peachpuff
+    peru
+    pink
+    plum
+    powderblue
+    purple
+    red
+    rosybrown
+    royalblue
+    saddlebrown
+    salmon
+    sandybrown
+    seagreen
+    seashell
+    sienna
+    silver
+    skyblue
+    slateblue
+    slategray
+    slategrey
+    snow
+    springgreen
+    steelblue
+    tan
+    teal
+    thistle
+    tomato
+    turquoise
+    violet
+    wheat
+    white
+    whitesmoke
+    yellow
+    yellowgreen
+
+    transparent
+    inherit
+    currentColor
+  ]
+  RE_COLOUR_NUMERIC = /\b(hsl|rgb)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i
+  RE_COLOUR_NUMERIC_ALPHA = /\b(hsla|rgba)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i
+  RE_COLOUR_HEX = /\s*#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/
+  RE_COLOUR_NAMED = /\s*\b(#{NAMED_COLOURS.join('|')})\b/i
   RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)
   # :startdoc:
 end
diff --git a/lib/css_parser/rule_set.rb b/lib/css_parser/rule_set.rb
index 199cd2a..e06c46a 100644
--- a/lib/css_parser/rule_set.rb
+++ b/lib/css_parser/rule_set.rb
@@ -4,12 +4,12 @@ module CssParser
     RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s\+\>]+)[\w]+|\:(first\-line|first\-letter|before|after))/i
     RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.[\w]+)|(\[[\w]+)|(\:(link|first\-child|lang))/i
 
-    BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-attachment']
+    BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment']
     LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image']
 
     # Array of selector strings.
     attr_reader   :selectors
-    
+
     # Integer with the specificity to use for this RuleSet.
     attr_accessor   :specificity
 
@@ -55,7 +55,7 @@ module CssParser
         @declarations.delete(property)
         return
       end
-      
+
       value.gsub!(/;\Z/, '')
       is_important = !value.gsub!(CssParser::IMPORTANT_IN_PROPERTY_RX, '').nil?
       property = property.downcase.strip
@@ -149,11 +149,18 @@ module CssParser
       split_declaration('background', 'background-attachment', value.slice!(CssParser::RE_SCROLL_FIXED))
       split_declaration('background', 'background-repeat', value.slice!(CssParser::RE_REPEAT))
       split_declaration('background', 'background-color', value.slice!(CssParser::RE_COLOUR))
+      split_declaration('background', 'background-size', extract_background_size_from(value))
       split_declaration('background', 'background-position', value.slice(CssParser::RE_BACKGROUND_POSITION))
 
       @declarations.delete('background')
     end
 
+    def extract_background_size_from(value)
+      size = value.slice!(CssParser::RE_BACKGROUND_SIZE)
+
+      size.sub(/^\s*\/\s*/, '') if size
+    end
+
     # Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
     # Additional splitting happens in expand_dimensions_shorthand!
     def expand_border_shorthand! # :nodoc:
@@ -166,7 +173,7 @@ module CssParser
         split_declaration(k, "#{k}-color", value.slice!(CssParser::RE_COLOUR))
         split_declaration(k, "#{k}-style", value.slice!(CssParser::RE_BORDER_STYLE))
 
-        @declarations.delete(k)   
+        @declarations.delete(k)
       end
     end
 
@@ -175,22 +182,22 @@ module CssParser
     def expand_dimensions_shorthand! # :nodoc:
       {'margin'       => 'margin-%s',
        'padding'      => 'padding-%s',
-       'border-color' => 'border-%s-color', 
-       'border-style' => 'border-%s-style', 
+       'border-color' => 'border-%s-color',
+       'border-style' => 'border-%s-style',
        'border-width' => 'border-%s-width'}.each do |property, expanded|
 
         next unless @declarations.has_key?(property)
-        
+
         value = @declarations[property][:value]
 
         # RGB and HSL values in borders are the only units that can have spaces (within params).
-        # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we 
+        # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
         # can split easily on spaces.
         #
         # TODO: rgba, hsl, hsla
-        value.gsub!(RE_COLOUR) { |c| c.gsub(/[\s]+/, '') }
+        value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*\,\s*)/, ',') }
 
-        matches = value.strip.split(/[\s]+/)
+        matches = value.strip.split(/\s+/)
 
         t, r, b, l = nil
 
@@ -310,14 +317,19 @@ module CssParser
     # Combine several properties into a shorthand one
     def create_shorthand_properties! properties, shorthand_property # :nodoc:
       values = []
+      properties_to_delete = []
       properties.each do |property|
-         if @declarations.has_key?(property) and not @declarations[property][:is_important]
+        if @declarations.has_key?(property) and not @declarations[property][:is_important]
           values << @declarations[property][:value]
-           @declarations.delete(property)
-         end
-       end
+          properties_to_delete << property
+        end
+      end
+
+      if values.length > 1
+        properties_to_delete.each do |property|
+          @declarations.delete(property)
+        end
 
-      unless values.empty?
         @declarations[shorthand_property] = {:value => values.join(' ')}
       end
     end
@@ -327,16 +339,28 @@ module CssParser
     #
     # Leaves properties declared !important alone.
     def create_background_shorthand! # :nodoc:
+      # When we have a background-size property we must separate it and distinguish it from
+      # background-position by preceeding it with a backslash. In this case we also need to
+      # have a background-position property, so we set it if it's missing.
+      # http://www.w3schools.com/cssref/css3_pr_background.asp
+      if @declarations.has_key?('background-size') and not @declarations['background-size'][:is_important]
+        unless @declarations.has_key?('background-position')
+          @declarations['background-position'] = {:value => '0% 0%'}
+        end
+
+        @declarations['background-size'][:value] = "/ #{@declarations['background-size'][:value]}"
+      end
+
       create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'
     end
-    
+
     # Combine border-color, border-style and border-width into border
     # Should be run after create_dimensions_shorthand!
     #
     # TODO: this is extremely similar to create_background_shorthand! and should be combined
     def create_border_shorthand! # :nodoc:
       values = []
-      
+
       ['border-width', 'border-style', 'border-color'].each do |property|
         if @declarations.has_key?(property) and not @declarations[property][:is_important]
           # can't merge if any value contains a space (i.e. has multiple values)
@@ -354,16 +378,16 @@ module CssParser
         @declarations['border'] = {:value => values.join(' ')}
       end
     end
-    
-    # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width) 
+
+    # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
     # and converts them into shorthand CSS properties.
     def create_dimensions_shorthand! # :nodoc:
       directions = ['top', 'right', 'bottom', 'left']
 
       {'margin'       => 'margin-%s',
        'padding'      => 'padding-%s',
-       'border-color' => 'border-%s-color', 
-       'border-style' => 'border-%s-style', 
+       'border-color' => 'border-%s-color',
+       'border-style' => 'border-%s-style',
        'border-width' => 'border-%s-width'}.each do |property, expanded|
 
         top, right, bottom, left = ['top', 'right', 'bottom', 'left'].map { |side| expanded % side }
@@ -377,7 +401,7 @@ module CssParser
           directions.each { |d| values[d.to_sym] = @declarations[expanded % d][:value].downcase.strip }
 
           if values[:left] == values[:right]
-            if values[:top] == values[:bottom] 
+            if values[:top] == values[:bottom]
               if values[:top] == values[:left] # All four sides are equal
                 new_value = values[:top]
               else # Top and bottom are equal, left and right are equal
@@ -400,8 +424,8 @@ module CssParser
     end
 
 
-    # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and 
-    # tries to convert them into a shorthand CSS <tt>font</tt> property.  All 
+    # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
+    # tries to convert them into a shorthand CSS <tt>font</tt> property.  All
     # font properties must be present in order to create a shorthand declaration.
     def create_font_shorthand! # :nodoc:
       ['font-style', 'font-variant', 'font-weight', 'font-size',
@@ -449,17 +473,17 @@ module CssParser
 
       if @declarations.has_key?(dest)
         #puts "dest #{dest} already exists"
-      
-        if @declarations[dest][:order] > @declarations[src][:order]
-          #puts "skipping #{dest}:#{v} due to order "        
+
+        if @declarations[src][:order].nil? || (!@declarations[dest][:order].nil? && @declarations[dest][:order] > @declarations[src][:order])
+          #puts "skipping #{dest}:#{v} due to order "
           return
         else
-          @declarations[dest] = {}    
+          @declarations[dest] = {}
         end
       end
-      @declarations[dest] = @declarations[src].merge({:value => v.to_s.strip})    
+      @declarations[dest] = @declarations[src].merge({:value => v.to_s.strip})
     end
-  
+
     def parse_declarations!(block) # :nodoc:
       @declarations = {}
 
@@ -474,7 +498,7 @@ module CssParser
           continuation = decs + ';'
 
         elsif matches = decs.match(/(.[^:]*)\s*:\s*(.+)(;?\s*\Z)/i)
-          property, value, end_of_declaration = matches.captures
+          property, value, = matches.captures # skip end_of_declaration
 
           add_declaration!(property, value)
           continuation = ''
@@ -486,7 +510,22 @@ module CssParser
     # TODO: way too simplistic
     #++
     def parse_selectors!(selectors) # :nodoc:
-      @selectors = selectors.split(',').map { |s| s.strip }
+      @selectors = selectors.split(',').map { |s| s.gsub(/\s+/, ' ').strip }
+    end
+  end
+
+  class OffsetAwareRuleSet < RuleSet
+
+    # File offset range
+    attr_reader :offset
+
+    # the local or remote location
+    attr_accessor :filename
+
+    def initialize(filename, offset, selectors, block, specificity = nil)
+      super(selectors, block, specificity)
+      @offset = offset
+      @filename = filename
     end
   end
 end
diff --git a/lib/css_parser/version.rb b/lib/css_parser/version.rb
index 08e2dbd..3514ca5 100644
--- a/lib/css_parser/version.rb
+++ b/lib/css_parser/version.rb
@@ -1,3 +1,3 @@
 module CssParser
-  VERSION = "1.3.6".freeze
+  VERSION = "1.5.0.pre2".freeze
 end
diff --git a/test/fixtures/complex.css b/test/fixtures/complex.css
new file mode 100644
index 0000000..65764ea
--- /dev/null
+++ b/test/fixtures/complex.css
@@ -0,0 +1,505 @@
+/*
+Fonts:
+	font-family:'Caslon 540 LT W01 Italic';
+	font-family:'Caslon 540 LT W01 Roman';
+	font-family:'Univers LT W01 53 Extended';
+	font-family:'Univers LT W01 57 Condensed';
+	font-family:'Univers LT W01 65 Bold';
+*/
+
+
+/*!
+   http://meyerweb.com/eric/tools/css/reset/ 
+   v2.0 | 20110126
+   License: none (public domain)
+*/
+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;}
+/* HTML5 display-role reset for older browsers */
+article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block;}
+body{line-height:1;}
+ol,ul{list-style:none;}
+blockquote,q{quotes:none;}
+blockquote:before,blockquote:after,
+q:before,q:after{content:'';content:none;}
+table{border-collapse:collapse;border-spacing:0;}
+
+/*!
+ * Copyright (c) 2008, Yahoo! Inc. All rights reserved.
+ * Code licensed under the BSD License:
+ * http://developer.yahoo.net/yui/license.txt
+ * version: 2.6.0
+*/
+body{font:13px/1.231 helvetica,arial,clean,sans-serif;*font-size:small;*font:x-small;}select,input,button,textarea{font:99% arial,helvetica,clean,sans-serif;}table{font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;}
+
+body {
+	font: normal 11px/20px sans-serif;
+	color: #3C3C46;
+	background: #FFF none;
+
+}
+
+
+.debug #page {background-image:url("grid.png?1");}
+
+.debug img { opacity: .5;}
+.debug .cyclenav, .debug .cycleprev, .debug .services li { background-color: rgba(100,100,100,0.5);}
+
+/** Frame **/
+body:after, #page:after, .features:after, section:after, #study_viewer:after, #study:after,#study .gallerywrap:after, footer:after, #name_case_converter:after, footer div:after, form .field:after{clear:both;display:block;visibility:hidden;overflow:hidden;height:0;content:"\0020";}
+
+#container {
+	margin: 0 auto;
+	background: #FFF none;
+}
+
+#page {
+	width: 940px;
+	margin: 0 auto;
+}
+
+
+
+
+header{width:940px;margin:60px auto;background:#fff url("header_bg.png") 0 50% repeat-x;}
+header li{display:block;float:left;width:180px;height:10px;padding:50px 0;line-height: 10px;}
+#nav{width:590px;height:110px;margin:0 auto;font:normal 10px/10px "Univers LT W01 53 Extended",sans-serif;letter-spacing:4px;text-transform:uppercase;}
+#logo,#logo a{display:block;width:110px;height:110px;margin:0 auto;padding:0;}
+#logo{padding:0 55px;}
+#logo a{background:transparent url("sprites.png") -380px 0;text-indent:-9999em;}
+#logo a:hover,#logo a:focus{background-position:-380px -109px;}
+#logo a:active{background-position:-380px -220px;}
+#nav_work{text-align:right;}
+
+
+
+header, footer, nav, section{clear:both;zoom:1;}
+em, i { font-style: italic;}
+b, strong {font-weight: bold;}
+a { color: #000; text-decoration: none;}
+img {display: block;}
+pre {
+	margin: 20px;
+	line-height: 1.5;
+}
+
+h1 {
+	margin-top: 3px;
+	margin-bottom: 17px;
+	font: normal 70px/80px "Caslon 540 LT W01 Roman", Georgia, serif;
+	text-align: center;
+
+	text-shadow: -2px 2px 1px #FFFFFF, -3px 3px 1px #DCD7D2;
+
+	/*filter: dropshadow(color=#cccccc, offx=3, offy=2);*/
+	/*filter: Shadow(Color=#cccccc,Direction=135,Strenth=1);*/
+}
+
+h2 {
+	height: 20px;
+	margin: 50px 0 23px;
+	font: normal 9px/20px "Univers LT W01 53 Extended", sans-serif;
+	background: #FFF url("dashdots.png") 0 50% repeat-x;
+	text-align: center;
+	text-transform: uppercase;
+	letter-spacing: 5px;
+}
+
+h2 span { padding: 0 30px;background: #FFF none;}
+
+h3 {
+	margin-bottom: 12px;
+	font: normal 16px/20px "Univers LT W01 57 Condensed", sans-serif;
+}
+
+
+ol, ul { margin-bottom: 20px; list-style-position: inside; font: italic 11px/20px Arial, sans-serif;}
+
+blockquote {
+	margin: 20px;
+	font: italic 11px/20px Arial, sans-serif;
+}
+
+ul {
+	list-style-type: disc;
+}
+
+li {
+	margin: 0 0 20px;
+	
+}
+
+p {
+	margin-bottom: 20px;
+}
+
+
+.lede {
+	width: 820px;
+	margin: 0 auto 100px;
+	font: normal 18px/30px "Caslon 540 LT W01 Roman", Georgia, serif;
+	text-align: center;
+}
+
+.lede em, .lede i, .lede a {
+		font-family: "Garamond W01 Italic", serif;
+}
+
+a {
+	outline: none;
+	font-style: italic;
+}
+
+a:hover, a:focus {
+	text-decoration: underline;
+}
+
+#nav a, .features a, footer a { font-style: normal;}
+
+
+/* Feature boxes */
+.features {
+	clear: both;
+	display: block;
+	width: 900px;
+	margin-bottom: 50px;
+	padding: 0 20px;
+	list-style: none;
+	font-style: normal;
+	background-color: transparent !important;
+}
+
+.features a:hover, .features a:focus {
+	text-decoration: none;
+}
+
+.features li {
+	float: left;
+	width: 180px;
+	margin: 0 60px 50px 0;
+}
+
+#contact .features h3 {font-size: 16px;}
+
+#contact .features li {text-align: center;}
+
+#studies .features, #contact .features {
+	width: 940px;
+	padding: 0 60px;
+}
+#studies .features li, #contact .features li {
+	width: 220px;
+	margin-right: 80px;
+}
+#studies .lede {margin: 0 auto 50px;
+}
+
+#studies nav {
+	width: 940px;
+	margin: 0;
+}
+
+#studies .fade_l {
+	position: absolute;
+	left: 0;
+	top: 0;
+	width: 40px;
+	height: 100%;
+	z-index: 1000;
+	background: transparent url("fade_l.png") repeat-y;
+}
+
+
+
+#studies .fade_r {
+	position: absolute;
+	right: 0;
+	top: 0;
+	width: 40px;
+	height: 100%;
+	z-index: 1000;
+	background: transparent url("fade_r.png") repeat-y;
+}
+
+.features h3 { margin-bottom: 3px;}
+
+.services h3 {
+	margin-left: -5px;
+	margin-bottom: 12px;
+	padding-left: 40px;
+	background: #FFF url("sprites.png")  0 -250px no-repeat;
+}
+
+
+.services .strategy h3  {
+	background-position: 0 -297px;
+}
+
+.services .id h3  {
+	padding-left: 43px;
+	background-position: 0 -347px;
+}
+
+.services .review h3  {
+	padding-left: 45px;
+	background-position: 0 -397px;
+}
+
+.services p {
+	padding: 0 7px;
+}
+
+.features p { margin-bottom:0;}
+.features .img {
+	display: block;
+	width: 228px;
+	height: 168px;
+	margin: 0 -5px 15px;
+	border: 1px solid #e4e4e4;
+}
+.features .img:active {
+background-color: #e4e4e4;	
+}
+
+/*.features .img:hover img {opacity: 1;}*/
+
+.features img { margin: 4px; }
+.js .features img {opacity: 1;}
+.features .first { clear: both;}
+.features .last { margin-right: 0 !important;}
+
+.features .meta {
+	margin-bottom: 0;
+	font: italic 12px/30px "Caslon 540 LT W01 Italic", Georgia, sans-serif;
+}
+
+
+
+#contact .email { font-style: italic;}
+#content .tel a { font-style: normal;}
+#contact a:hover, #contact a:focus { text-decoration: underline;}
+
+
+footer {
+	width: 940px;
+	margin: 0 auto 0;
+	padding-bottom: 100px;
+}
+
+
+footer nav {
+	width: 100%;
+	height: 10px;
+	margin: 0 0 50px;
+	padding: 25px 0;
+	text-align: center;
+	font: normal 8px/10px "Univers LT W01 53 Extended", sans-serif;
+	letter-spacing: 3px;
+	text-transform: uppercase;
+	background: #fff url("header_bg.png") 0 50% repeat-x;
+}
+
+footer nav a {
+	display: inline;
+	padding: 0 32px;
+	text-align: center;
+	
+}
+
+footer div {
+	margin: 0 auto;
+	font-size: 10px;
+	text-align: center;
+
+	color: #B3B3B3;
+}
+
+footer a { font-style: normal;}
+
+footer img {
+	display: block;
+	margin: 20px auto;
+}
+
+
+
+
+#study h1, .code h1, .inside h1 {
+	font-size: 50px;
+	line-height: 60px;
+	text-shadow: none;
+}
+
+.focus h2 {
+	height: auto;
+	margin: 0 0 12px;
+	border: 0;
+	font: normal 15px/20px "Univers LT W01 57 Condensed", sans-serif;
+	text-transform: none;
+	text-align: left;
+	letter-spacing: 0;
+	background: none;
+}
+
+.focus ul { list-style: none;}
+
+.focus li {
+	margin: 5px 0 10px;
+	font: normal 12px/15px "Univers LT W01 65 Bold", sans-serif;
+}
+
+#study { 	margin-bottom: 80px;}
+#study .prose, #study .focus {
+	float: left;
+}
+
+#study .prose {
+	width: 460px;
+	margin: 0 40px 0 120px;
+	font: normal 15px/20px "Caslon 540 LT W01 Roman", Georgia, serif;
+}
+
+#study .prose em, #study .prose i {
+	font-family: "Garamond W01 Italic", serif;
+}
+
+#study .focus {
+	width: 200px;
+}
+
+#study .visit {
+	margin: 20px 0;
+	font: normal 15px/20px "Univers LT W01 57 Condensed", sans-serif;
+	text-transform: lowercase;
+}
+
+#study .visit a { text-decoration:none;font-style: normal;}
+
+
+.prose a { text-decoration: underline;}
+
+
+
+.gallerywrap .gallery,.cycleprev,.cyclenext{float:left;}
+.cycleprev,.cyclenext{display:block;width:26px;height:26px;text-indent:-9999em;cursor:pointer;background:#FFF url("sprites.png") no-repeat;}
+.cycleprev{margin-left:14px;margin-right:80px;background-position:-40px -60px;}
+.cycleprev:hover,.cycleprev:focus{background-position:-70px -60px;}
+.cyclenext{margin-right:14px;margin-left:80px;background-position:-100px -60px;}
+.cyclenext:hover,.cyclenext:focus{background-position:-130px -60px;}
+
+
+.cyclenav{width:700px;height:12px;margin:50px auto 50px;text-align:center;}
+.cyclenav a{display:inline-block;width:12px;height:12px;margin:0 5px;text-indent:-9999em;background:#fff url("sprites.png") 0 -60px no-repeat;}
+.cyclenav a:hover,.cyclenav a:focus,.cyclenav a.activeSlide{background-position:-15px -60px;}
+#study .cyclenav { margin-top: 40px;}
+#studies .studywrap .cycleprev { margin: 25px 40px 35px 34px;}
+#studies .studywrap .cyclenext { margin: 25px 34px 35px 40px;}
+#studies .studywrap .cyclenav { float: left; width: 740px;height: 26px;margin:30px auto 45px;}
+#studies nav ul.features { background-color: #FFF;}
+#studies nav { margin-bottom: 20px; background-color: #FFF;}
+
+#study .cycleprev,#study .cyclenext{margin-top:235px;}
+
+
+
+.gallerywrap {
+	width: 100%;
+}
+
+.gallery {
+	width: 700px;
+	margin: 0 auto 50px;
+}
+
+.js .gallery img {
+	display: none;
+}
+
+.js .gallery img:first-child {
+	display: block;
+}
+
+#study .gallery img {
+	width: 700px;
+	height: 500px;
+	margin: 0 auto;
+}
+
+
+
+
+/* Forms and code */
+form {
+	width: 700px;
+	margin: 45px auto;
+}
+
+form .field {
+	clear: left;
+	margin: 0 0 50px;
+}
+
+label {
+	display: block;
+	float: left;
+	width: 280px;
+	margin: 0 20px 5px 0;
+	font: normal 20px/20px "Univers LT W01 57 Condensed", sans-serif;
+}
+
+label .pull {
+	display: inline-block;
+	width: 25px;
+	margin-left: -30px;
+}
+
+form .sublabel {
+	display: block;
+	margin: 10px 0 5px;
+	font: normal 12px/15px sans-serif;
+}
+
+form .hint {
+	margin: 5px 0;
+	font: italic 11px/15px sans-serif;
+}
+
+
+input.text, textarea {
+	width: 378px;
+	padding: 0 10px;
+	border: 1px solid #E4E4E4;
+}
+
+input.text {
+		height: 38px;
+}
+
+textarea { padding: 10px; line-height: 20px; height: 218px;}
+
+form div.button {
+	padding-left: 300px;
+}
+
+button.submit {
+	display: block;
+	width: 85px;
+	height: 85px;
+	border: 0;
+	text-align: left;
+	text-indent: -9999em;
+	background: #FFF url("sprites.png") 0 -150px;
+	cursor: pointer;
+}
+
+button.submit:hover, button.submit:focus {
+	background-position: -100px -150px;
+}
+
+button.submit:active {
+	background-position: -200px -150px;
+}
+
+
+#name_case_converter textarea {
+	height: 400px;
+}
\ No newline at end of file
diff --git a/test/fixtures/import-malformed.css b/test/fixtures/import-malformed.css
new file mode 100644
index 0000000..3824495
--- /dev/null
+++ b/test/fixtures/import-malformed.css
@@ -0,0 +1,35 @@
+.malformed.one:before {
+  content: "\\";
+  color: "red";
+}
+
+.wellformed.one {
+  color: "green";
+}
+
+.malformed.two:before {
+  content: "\"";
+  color: "red";
+}
+
+.wellformed.two {
+  color: "green";
+}
+
+.malformed.three:before {
+  content: "{";
+  color: "red";
+}
+
+.wellformed.three {
+  color: "green";
+}
+
+.malformed.four:before {
+  content: "}";
+  color: "red";
+}
+
+.wellformed.four {
+  color: "green";
+}
\ No newline at end of file
diff --git a/test/test_css_parser_basic.rb b/test/test_css_parser_basic.rb
index 5e9027d..7f3b7cf 100644
--- a/test/test_css_parser_basic.rb
+++ b/test/test_css_parser_basic.rb
@@ -1,7 +1,7 @@
 require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 
 # Test cases for reading and generating CSS shorthand properties
-class CssParserBasicTests < Test::Unit::TestCase
+class CssParserBasicTests < Minitest::Test
   include CssParser
 
   def setup
@@ -43,6 +43,14 @@ class CssParserBasicTests < Test::Unit::TestCase
     assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')
   end
 
+  def test_removing_a_rule_set
+    rs = CssParser::RuleSet.new('div', 'color: blue;')
+    @cp.add_rule_set!(rs)
+    rs2 = CssParser::RuleSet.new('div', 'color: blue;')
+    @cp.remove_rule_set!(rs2)
+    assert_equal '', @cp.find_by_selector('div').join(' ')
+  end
+
   def test_toggling_uri_conversion
     # with conversion
     cp_with_conversion = Parser.new(:absolute_paths => true)
@@ -60,4 +68,11 @@ class CssParserBasicTests < Test::Unit::TestCase
     assert_equal "background: url('../style/yellow.png?abc=123');",
       cp_without_conversion['body'].join(' ')
   end
+
+  def test_converting_to_hash
+    rs = CssParser::RuleSet.new('div', 'color: blue;')
+    @cp.add_rule_set!(rs)
+    hash = @cp.to_h
+    assert_equal 'blue', hash['all']['div']['color']
+  end
 end
diff --git a/test/test_css_parser_loading.rb b/test/test_css_parser_loading.rb
index 20fc87c..5177475 100644
--- a/test/test_css_parser_loading.rb
+++ b/test/test_css_parser_loading.rb
@@ -1,7 +1,7 @@
 require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 
 # Test cases for the CssParser's loading functions.
-class CssParserLoadingTests < Test::Unit::TestCase
+class CssParserLoadingTests < Minitest::Test
   include CssParser
   include WEBrick
 
@@ -15,6 +15,14 @@ class CssParserLoadingTests < Test::Unit::TestCase
 
     @server_thread = Thread.new do
       s = WEBrick::HTTPServer.new(:Port => 12000, :DocumentRoot => @www_root, :Logger => Log.new(nil, BasicLog::FATAL), :AccessLog => [])
+      s.mount_proc('/redirect301') do |request, response|
+        response['Location'] = '/simple.css'
+        raise WEBrick::HTTPStatus::MovedPermanently
+      end
+      s.mount_proc('/redirect302') do |request, response|
+        response['Location'] = '/simple.css'
+        raise WEBrick::HTTPStatus::TemporaryRedirect
+      end
       @port = s.config[:Port]
       begin
         s.start
@@ -32,6 +40,16 @@ class CssParserLoadingTests < Test::Unit::TestCase
     @server_thread = nil
   end
 
+  def test_loading_301_redirect
+    @cp.load_uri!("#{@uri_base}/redirect301")
+    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
+  end
+
+  def test_loading_302_redirect
+    @cp.load_uri!("#{@uri_base}/redirect302")
+    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
+  end
+
   def test_loading_a_local_file
     file_name = File.dirname(__FILE__) + '/fixtures/simple.css'
     @cp.load_file!(file_name)
@@ -49,11 +67,15 @@ class CssParserLoadingTests < Test::Unit::TestCase
     assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
   end
 
-  # http://github.com/alexdunae/css_parser/issues#issue/4
+  # http://github.com/premailer/css_parser/issues#issue/4
   def test_loading_a_remote_file_over_ssl
     # TODO: test SSL locally
-    @cp.load_uri!("https://dialect.ca/inc/screen.css")
-    assert_match /margin\: 0\;/, @cp.find_by_selector('body').join(' ')
+    if RUBY_PLATFORM == 'java'
+      skip "SSL: does not work on jruby"
+    else
+      @cp.load_uri!("https://dialect.ca/inc/screen.css")
+      assert_includes( @cp.find_by_selector('body').join(' '), "margin: 0;" )
+    end
   end
 
   def test_loading_a_string
@@ -102,14 +124,41 @@ class CssParserLoadingTests < Test::Unit::TestCase
     assert_equal '', cp.find_by_selector('p').join(' ')
   end
 
+  def test_following_remote_import_rules
+    css_block = '@import "http://example.com/css";'
+
+    assert_raises CssParser::RemoteFileError do
+      @cp.add_block!(css_block, :base_uri => "#{@uri_base}/subdir/")
+    end
+  end
+
   def test_following_badly_escaped_import_rules
     css_block = '@import "http://example.com/css?family=Droid+Sans:regular,bold|Droid+Serif:regular,italic,bold,bolditalic&subset=latin";'
 
-    assert_nothing_raised do
+    assert_raises CssParser::RemoteFileError do
       @cp.add_block!(css_block, :base_uri => "#{@uri_base}/subdir/")
     end
   end
 
+  def test_loading_malformed_content_strings
+    file_name = File.dirname(__FILE__) + '/fixtures/import-malformed.css'
+    @cp.load_file!(file_name)
+    @cp.each_selector do |sel, dec, spec|
+      assert_nil dec =~ /wellformed/
+    end
+  end
+
+  def test_loading_malformed_css_brackets
+    file_name = File.dirname(__FILE__) + '/fixtures/import-malformed.css'
+    @cp.load_file!(file_name)
+    selector_count = 0
+    @cp.each_selector do |sel, dec, spec|
+      selector_count += 1
+    end
+
+    assert_equal 8, selector_count
+  end
+
   def test_following_at_import_rules_from_add_block
     css_block = '@import "../simple.css";'
 
@@ -128,13 +177,13 @@ class CssParserLoadingTests < Test::Unit::TestCase
   end
 
   def test_local_circular_reference_exception
-    assert_raise CircularReferenceError do
+    assert_raises CircularReferenceError do
       @cp.load_file!(File.dirname(__FILE__) + '/fixtures/import-circular-reference.css')
     end
   end
 
   def test_remote_circular_reference_exception
-    assert_raise CircularReferenceError do
+    assert_raises CircularReferenceError do
       @cp.load_uri!("#{@uri_base}/import-circular-reference.css")
     end
   end
@@ -142,22 +191,20 @@ class CssParserLoadingTests < Test::Unit::TestCase
   def test_suppressing_circular_reference_exceptions
     cp_without_exceptions = Parser.new(:io_exceptions => false)
 
-    assert_nothing_raised CircularReferenceError do
-      cp_without_exceptions.load_uri!("#{@uri_base}/import-circular-reference.css")
-    end
+    cp_without_exceptions.load_uri!("#{@uri_base}/import-circular-reference.css")
   end
 
   def test_toggling_not_found_exceptions
     cp_with_exceptions = Parser.new(:io_exceptions => true)
 
-    assert_raise RemoteFileError do
+    err = assert_raises RemoteFileError do
       cp_with_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
     end
 
+    assert_includes err.message, "#{@uri_base}/no-exist.xyz"
+
     cp_without_exceptions = Parser.new(:io_exceptions => false)
 
-    assert_nothing_raised RemoteFileError do
-      cp_without_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
-    end
+    cp_without_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
   end
 end
diff --git a/test/test_css_parser_media_types.rb b/test/test_css_parser_media_types.rb
index 7573fe0..43dff16 100644
--- a/test/test_css_parser_media_types.rb
+++ b/test/test_css_parser_media_types.rb
@@ -1,7 +1,7 @@
 require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 
 # Test cases for the handling of media types
-class CssParserMediaTypesTests < Test::Unit::TestCase
+class CssParserMediaTypesTests < Minitest::Test
   include CssParser
 
   def setup
@@ -78,7 +78,7 @@ class CssParserMediaTypesTests < Test::Unit::TestCase
       body { color: black; }
     CSS
 
-    assert_match 'color: black;', @cp.to_s
+    assert_includes @cp.to_s, 'color: black;'
   end
 
   def test_adding_block_and_limiting_media_types1
@@ -100,7 +100,7 @@ class CssParserMediaTypesTests < Test::Unit::TestCase
     base_dir = File.dirname(__FILE__)  + '/fixtures/'
 
     @cp.add_block!(css, :only_media_types => 'print and (color)', :base_dir => base_dir)
-    assert_match 'color: lime', @cp.find_by_selector('div').join(' ')
+    assert_includes @cp.find_by_selector('div').join(' '), 'color: lime'
   end
 
   def test_adding_block_and_limiting_media_types
@@ -110,7 +110,7 @@ class CssParserMediaTypesTests < Test::Unit::TestCase
 
     base_dir = File.dirname(__FILE__)  + '/fixtures/'
     @cp.add_block!(css, :only_media_types => :print, :base_dir => base_dir)
-    assert_match '', @cp.find_by_selector('div').join(' ')
+    assert_equal '', @cp.find_by_selector('div').join(' ')
   end
 
   def test_adding_rule_set_with_media_type
diff --git a/test/test_css_parser_misc.rb b/test/test_css_parser_misc.rb
index ea3a7fc..7f023ae 100644
--- a/test/test_css_parser_misc.rb
+++ b/test/test_css_parser_misc.rb
@@ -1,7 +1,7 @@
 require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 
 # Test cases for the CssParser.
-class CssParserTests < Test::Unit::TestCase
+class CssParserTests < Minitest::Test
   include CssParser
 
   def setup
@@ -53,14 +53,14 @@ class CssParserTests < Test::Unit::TestCase
     # dervived from http://www.w3.org/TR/CSS21/syndata.html#rule-sets
     css = <<-CSS
       div[name='test'] {
-      
+
       color:
-      
+
       red;
-      
+
       }div:hover{coloR:red;
          }div:first-letter{color:red;/*color:blue;}"commented out"*/}
-      
+
       p[example="public class foo\
       {\
           private string x;\
@@ -71,7 +71,7 @@ class CssParserTests < Test::Unit::TestCase
           }\
       \
       }"] { color: red }
-      
+
       p { color:red}
     CSS
 
@@ -106,11 +106,16 @@ class CssParserTests < Test::Unit::TestCase
       h1, h2 { color: blue; }
       h1 { font-size: 10px; }
       h2 { font-size: 5px; }
+      article  h3  { color: black; }
+      article
+      h3 { background-color: white; }
     CSS
 
     @cp.add_block!(css)
     assert_equal 2, @cp.find_rule_sets(["h2"]).size
     assert_equal 3, @cp.find_rule_sets(["h1", "h2"]).size
+    assert_equal 2, @cp.find_rule_sets(["article h3"]).size
+    assert_equal 2, @cp.find_rule_sets(["  article \t  \n  h3 \n "]).size
   end
 
   def test_calculating_specificity
@@ -152,7 +157,7 @@ class CssParserTests < Test::Unit::TestCase
   end
 
   def test_ruleset_with_braces
-=begin  
+=begin
     parser = Parser.new
     parser.add_block!("div { background-color: black !important; }")
     parser.add_block!("div { background-color: red; }")
@@ -172,4 +177,9 @@ class CssParserTests < Test::Unit::TestCase
 
     assert_equal 'div { background-color: black !important; }', new_rule.to_s
   end
+
+  def test_content_with_data
+    rule = RuleSet.new('div', '{content: url(data:image/png;base64,LOTSOFSTUFF)}')
+    assert_includes rule.to_s, "image/png;base64,LOTSOFSTUFF"
+  end
 end
diff --git a/test/test_css_parser_offset_capture.rb b/test/test_css_parser_offset_capture.rb
new file mode 100644
index 0000000..60ae33b
--- /dev/null
+++ b/test/test_css_parser_offset_capture.rb
@@ -0,0 +1,109 @@
+require File.expand_path(File.dirname(__FILE__) + '/test_helper')
+
+# Test cases for the CssParser's loading functions.
+class CssParserOffsetCaptureTests < Minitest::Test
+  include CssParser
+
+  def setup
+    @cp = Parser.new
+  end
+
+  def test_capturing_offsets_for_local_file
+    file_name = File.dirname(__FILE__) + '/fixtures/simple.css'
+    @cp.load_file!(file_name, :capture_offsets => true)
+
+    rules = @cp.find_rule_sets(['body', 'p'])
+
+    # check that we found the body rule where we expected
+    assert_equal 0, rules[0].offset.first
+    assert_equal 43, rules[0].offset.last
+    assert_equal file_name, rules[0].filename
+
+    # and the p rule
+    assert_equal 45, rules[1].offset.first
+    assert_equal 63, rules[1].offset.last
+    assert_equal file_name, rules[1].filename
+  end
+
+  # http://github.com/premailer/css_parser/issues#issue/4
+  def test_capturing_offsets_from_remote_file
+    # TODO: test SSL locally
+    if RUBY_PLATFORM == 'java'
+      skip "SSL: does not work on jruby"
+    else
+      @cp.load_uri!("https://dialect.ca/inc/screen.css", :capture_offsets => true)
+
+      # there are a lot of rules in this file, but check some rule offsets
+      rules = @cp.find_rule_sets(['#container', '#name_case_converter textarea'])
+      assert_equal 2, rules.count
+
+      assert_equal 2172, rules.first.offset.first
+      assert_equal 2227, rules.first.offset.last
+      assert_equal 'https://dialect.ca/inc/screen.css', rules.first.filename
+
+      assert_equal 10703, rules.last.offset.first
+      assert_equal 10752, rules.last.offset.last
+      assert_equal 'https://dialect.ca/inc/screen.css', rules.last.filename
+    end
+  end
+
+  def test_capturing_offsets_from_string
+    css = <<-CSS
+      body { margin: 0px; }
+      p { padding: 0px; }
+      #content { font: 12px/normal sans-serif; }
+      .content { color: red; }
+    CSS
+    @cp.load_string!(css, :capture_offsets => true, :filename => 'index.html')
+
+    rules = @cp.find_rule_sets(['body', 'p', '#content', '.content'])
+    assert_equal 4, rules.count
+
+    assert_equal 6, rules[0].offset.first
+    assert_equal 27, rules[0].offset.last
+    assert_equal 'index.html', rules[0].filename
+
+    assert_equal 34, rules[1].offset.first
+    assert_equal 53, rules[1].offset.last
+    assert_equal 'index.html', rules[1].filename
+
+    assert_equal 60, rules[2].offset.first
+    assert_equal 102, rules[2].offset.last
+    assert_equal 'index.html', rules[2].filename
+
+    assert_equal 109, rules[3].offset.first
+    assert_equal 133, rules[3].offset.last
+    assert_equal 'index.html', rules[3].filename
+  end
+
+  def test_capturing_offsets_with_imports
+    base_dir = File.dirname(__FILE__) + '/fixtures'
+    @cp.load_file!('import1.css', :base_dir => base_dir, :capture_offsets => true)
+
+    rules = @cp.find_rule_sets(['div', 'a', 'body', 'p'])
+
+    # check that we found the div rule where we expected in the primary file
+    assert_equal 'div', rules[0].selectors.join
+    assert_equal 31, rules[0].offset.first
+    assert_equal 51, rules[0].offset.last
+    assert_equal base_dir + '/import1.css', rules[0].filename
+
+    # check that the a rule in the first import is where we expect
+    assert_equal 'a', rules[1].selectors.join
+    assert_equal 26, rules[1].offset.first
+    assert_equal 54, rules[1].offset.last
+    assert_equal base_dir + '/subdir/import2.css', rules[1].filename
+
+    # and the body rule in the second import
+    assert_equal 'body', rules[2].selectors.join
+    assert_equal 0, rules[2].offset.first
+    assert_equal 43, rules[2].offset.last
+    assert_equal base_dir + '/simple.css', rules[2].filename
+
+    # as well as the p rule in the second import
+    assert_equal 'p', rules[3].selectors.join
+    assert_equal 45, rules[3].offset.first
+    assert_equal 63, rules[3].offset.last
+    assert_equal base_dir + '/simple.css', rules[3].filename
+  end
+end
diff --git a/test/test_css_parser_regexps.rb b/test/test_css_parser_regexps.rb
index 99f0008..61a33f4 100644
--- a/test/test_css_parser_regexps.rb
+++ b/test/test_css_parser_regexps.rb
@@ -5,13 +5,13 @@ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 #
 # see http://www.w3.org/TR/CSS21/syndata.html and
 # http://www.w3.org/TR/CSS21/grammar.html
-class CssParserRegexpTests < Test::Unit::TestCase
+class CssParserRegexpTests < Minitest::Test
   def test_strings
     # complete matches
     [
-      '"abcd"', '" A sd s�drcv \'dsf\' asd rfg asd"', '"A\ d??ef 123!"',
+      '"abcd"', '" A sd sédrcv \'dsf\' asd rfg asd"', '"A\ d??ef 123!"',
       "\"this is\\\n a test\"", '"back\67round"', '"r\000065 ed"',
-      "'abcd'", "' A sd sedrcv \"dsf\" asd rf�&23$%#%$g asd'", "'A\\\n def 123!'",
+      "'abcd'", "' A sd sedrcv \"dsf\" asd rf—&23$%#%$g asd'", "'A\\\n def 123!'",
       "'this is\\\n a test'", "'back\\67round'", "'r\\000065 ed'"
     ].each do |str|
       assert_equal str, str.match(CssParser::RE_STRING).to_s
@@ -21,6 +21,12 @@ class CssParserRegexpTests < Test::Unit::TestCase
     assert_equal "\"url\\.'p'ng\"", test_string.match(CssParser::RE_STRING).to_s
   end
 
+  def test_box_model_units
+    %w( auto inherit 80px 90pt 80pc 80rem 80vh 70vm 60vw 1vmin 2vmax 0 2em 3ex 1cm 100mm 2in 120% ).each do |str|
+      assert_match(CssParser::BOX_MODEL_UNITS_RX, str)
+    end
+  end
+
   def test_unicode
     ['back\67round', 'r\000065 ed', '\00006C'].each do |str|
       assert_match(Regexp.new(CssParser::RE_UNICODE), str)
@@ -31,7 +37,9 @@ class CssParserRegexpTests < Test::Unit::TestCase
     [
       'color: #fff', 'color:#f0a09c;', 'color: #04A', 'color: #04a9CE',
       'color: rgb(100, -10%, 300);', 'color: rgb(10,10,10)', 'color:rgb(12.7253%, -12%,0)',
-      'color: black', 'color:Red;', 'color: AqUa;', 'color: blue   ', 'color: transparent'
+      'color: hsla(-15, -77%, 19%, 5%);',
+      'color: black', 'color:Red;', 'color: AqUa;', 'color: blue   ', 'color: transparent',
+      'color: darkslategray'
     ].each do |colour|
       assert_match(CssParser::RE_COLOUR, colour)
     end
@@ -39,9 +47,10 @@ class CssParserRegexpTests < Test::Unit::TestCase
     [
       'color: #fa', 'color:#f009c;', 'color: #04G', 'color: #04a9Cq',
       'color: rgb 100, -10%, 300;', 'color: rgb 10,10,10', 'color:rgb(12px, -12%,0)',
-      'color:fuscia;', 'color: thick'
+      'color:fuscia;', 'color: thick',
+      'color:  alice_blue'
     ].each do |colour|
-      assert_no_match(CssParser::RE_COLOUR, colour)
+      refute_match(CssParser::RE_COLOUR, colour)
     end
   end
 
@@ -74,7 +83,7 @@ class CssParserRegexpTests < Test::Unit::TestCase
 
   def test_important
     assert_match(CssParser::IMPORTANT_IN_PROPERTY_RX, "color: #f00 !important   ;")
-    assert_no_match(CssParser::IMPORTANT_IN_PROPERTY_RX, "color: #f00 !importantish;")
+    refute_match(CssParser::IMPORTANT_IN_PROPERTY_RX, "color: #f00 !importantish;")
   end
 
   protected
diff --git a/test/test_helper.rb b/test/test_helper.rb
index ffe7a2e..05aab4c 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,6 +1,5 @@
-require 'rubygems'
 require 'bundler/setup'
-require 'test/unit'
+require 'maxitest/autorun'
 require 'net/http'
 require 'webrick'
-require File.dirname(__FILE__) + '/../lib/css_parser'
+require 'css_parser'
diff --git a/test/test_merging.rb b/test/test_merging.rb
index 1a6e8b7..3ebbcd0 100644
--- a/test/test_merging.rb
+++ b/test/test_merging.rb
@@ -1,6 +1,6 @@
 require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 
-class MergingTests < Test::Unit::TestCase
+class MergingTests < Minitest::Test
   include CssParser
 
   def setup
@@ -89,7 +89,7 @@ class MergingTests < Test::Unit::TestCase
   end
 
   def test_raising_error_on_bad_type
-    assert_raise ArgumentError do
+    assert_raises ArgumentError do
       CssParser.merge([1,2,3])
     end
   end
diff --git a/test/test_rule_set.rb b/test/test_rule_set.rb
index 14d13e8..28273b8 100644
--- a/test/test_rule_set.rb
+++ b/test/test_rule_set.rb
@@ -2,7 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 require "set"
 
 # Test cases for parsing CSS blocks
-class RuleSetTests < Test::Unit::TestCase
+class RuleSetTests < Minitest::Test
   include CssParser
 
   def setup
@@ -106,4 +106,14 @@ class RuleSetTests < Test::Unit::TestCase
       assert_equal 1000, spec
     end
   end
+
+  def test_not_raised_issue68
+    ok = true
+    begin
+      RuleSet.new('td', 'border-top: 5px solid; border-color: #fffff0;')
+    rescue
+      ok = false
+    end
+    assert_equal true, ok
+  end
 end
diff --git a/test/test_rule_set_creating_shorthand.rb b/test/test_rule_set_creating_shorthand.rb
index 711a59b..c598495 100644
--- a/test/test_rule_set_creating_shorthand.rb
+++ b/test/test_rule_set_creating_shorthand.rb
@@ -1,7 +1,7 @@
 require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 
 # Test cases for reading and generating CSS shorthand properties
-class RuleSetCreatingShorthandTests < Test::Unit::TestCase
+class RuleSetCreatingShorthandTests < Minitest::Test
   include CssParser
 
   def setup
@@ -35,6 +35,10 @@ class RuleSetCreatingShorthandTests < Test::Unit::TestCase
     properties = {'border-top-style' => 'none', 'border-right-style' => 'none', 'border-bottom-style' => 'none', 'border-left-style' => 'none'}
     combined = create_shorthand(properties)
     assert_equal 'none;', combined['border']
+
+    properties = {'border-top-color' => '#bada55', 'border-right-color' => '#000000', 'border-bottom-color' => '#ffffff', 'border-left-color' => '#ff0000'}
+    combined = create_shorthand(properties)
+    assert_equal '#bada55 #000000 #ffffff #ff0000;', combined['border-color']
   end
 
   # Dimensions shorthand
@@ -102,6 +106,31 @@ class RuleSetCreatingShorthandTests < Test::Unit::TestCase
     assert_properties_are_deleted(combined, properties)
   end
 
+  def test_combining_background_with_size_into_shorthand
+    properties = {'background-image' => 'url(\'chess.png\')', 'background-color' => 'gray',
+      'background-position' => 'center -10.2%', 'background-attachment' => 'fixed',
+      'background-repeat' => 'no-repeat', 'background-size' => '50% 100%'}
+
+    combined = create_shorthand(properties)
+
+    assert_equal('gray url(\'chess.png\') no-repeat center -10.2% / 50% 100% fixed;', combined['background'])
+
+    # after creating shorthand, all long-hand properties should be deleted
+    assert_properties_are_deleted(combined, properties)
+  end
+
+  def test_combining_background_with_size_and_no_position_into_shorthand
+    properties = {'background-image' => 'url(\'chess.png\')', 'background-color' => 'gray',
+      'background-attachment' => 'fixed', 'background-repeat' => 'no-repeat',
+      'background-size' => '50% 100%'}
+
+    combined = create_shorthand(properties)
+
+    assert_equal('gray url(\'chess.png\') no-repeat 0% 0% / 50% 100% fixed;', combined['background'])
+
+    # after creating shorthand, all long-hand properties should be deleted
+    assert_properties_are_deleted(combined, properties)
+  end
 
   # List-style shorthand
   def test_combining_list_style_into_shorthand
@@ -125,6 +154,15 @@ class RuleSetCreatingShorthandTests < Test::Unit::TestCase
     assert_equal('url(http://example.com/1528/www/top-logo.jpg) no-repeat top right;', rs['background'])
   end
 
+
+  def test_a_single_property_is_not_shorted
+    properties = {'background-color' => 'gray'}
+    combined = create_shorthand(properties)
+
+    assert_equal('gray;', combined['background-color'])
+    assert_equal('', combined['background'])
+  end
+
   protected
 
   def assert_properties_are_deleted(ruleset, properties)
diff --git a/test/test_rule_set_expanding_shorthand.rb b/test/test_rule_set_expanding_shorthand.rb
index af70be7..a0069fa 100644
--- a/test/test_rule_set_expanding_shorthand.rb
+++ b/test/test_rule_set_expanding_shorthand.rb
@@ -1,6 +1,6 @@
 require File.expand_path(File.dirname(__FILE__) + '/test_helper')
 
-class RuleSetExpandingShorthandTests < Test::Unit::TestCase
+class RuleSetExpandingShorthandTests < Minitest::Test
   include CssParser
 
   def setup
@@ -21,6 +21,27 @@ class RuleSetExpandingShorthandTests < Test::Unit::TestCase
     assert_equal 'rgb(2%,2%,2%)', declarations['border-bottom-color']
     assert_equal 'hsla(255,0,0,5)', declarations['border-left-color']
 
+    declarations = expand_declarations('border-color: #000000 #bada55 #ffffff #ff0000')
+
+    assert_equal '#000000', declarations['border-top-color']
+    assert_equal '#bada55', declarations['border-right-color']
+    assert_equal '#ffffff', declarations['border-bottom-color']
+    assert_equal '#ff0000', declarations['border-left-color']
+
+    declarations = expand_declarations('border-color: #000000 #bada55 #ffffff')
+
+    assert_equal '#000000', declarations['border-top-color']
+    assert_equal '#bada55', declarations['border-right-color']
+    assert_equal '#ffffff', declarations['border-bottom-color']
+    assert_equal '#bada55', declarations['border-left-color']
+
+    declarations = expand_declarations('border-color: #000000 #bada55')
+
+    assert_equal '#000000', declarations['border-top-color']
+    assert_equal '#bada55', declarations['border-right-color']
+    assert_equal '#000000', declarations['border-bottom-color']
+    assert_equal '#bada55', declarations['border-left-color']
+
     declarations = expand_declarations('border: thin dot-dot-dash')
     assert_equal 'dot-dot-dash', declarations['border-left-style']
     assert_equal 'thin', declarations['border-left-width']
@@ -83,10 +104,13 @@ class RuleSetExpandingShorthandTests < Test::Unit::TestCase
     shorthand = "font: small-caps italic 12px sans-serif;"
     declarations = expand_declarations(shorthand)
     assert_equal('small-caps', declarations['font-variant'])
+  end
 
-    # ensure normal is the default state
-    ['font: normal italic 12px sans-serif;', 'font: italic 12px sans-serif;',
-      'font: normal 12px sans-serif;', 'font: 12px/16px sans-serif;'].each do |shorthand|
+  def test_getting_font_variant_from_shorthand_ensure_normal_is_the_default_state
+    [
+      'font: normal italic 12px sans-serif;', 'font: italic 12px sans-serif;',
+      'font: normal 12px sans-serif;', 'font: 12px/16px sans-serif;'
+    ].each do |shorthand|
       declarations = expand_declarations(shorthand)
       assert_equal('normal', declarations['font-variant'], shorthand)
     end
@@ -147,6 +171,20 @@ class RuleSetExpandingShorthandTests < Test::Unit::TestCase
     end
   end
 
+  def test_getting_background_size_from_shorthand
+    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|
+      shorthand = "background: url('chess.png') gray 30% -0.20/-0.15#{unit} auto repeat fixed;"
+      declarations = expand_declarations(shorthand)
+      assert_equal("-0.15#{unit} auto", declarations['background-size'])
+    end
+
+    ['cover', 'contain', 'auto', 'initial', 'inherit'].each do |size|
+      shorthand = "background: url('chess.png') #000fff 0% 50% / #{size} no-repeat fixed;"
+      declarations = expand_declarations(shorthand)
+      assert_equal(size, declarations['background-size'])
+    end
+  end
+
   def test_getting_background_colour_from_shorthand
     ['blue', 'lime', 'rgb(10,10,10)', 'rgb (  -10%, 99, 300)', '#ffa0a0', '#03c', 'trAnsparEnt', 'inherit'].each do |colour|
       shorthand = "background:#{colour} url('chess.png') center repeat fixed ;"

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-css-parser.git



More information about the Pkg-ruby-extras-commits mailing list