css_parser 1.7.0 → 1.10.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 +4 -4
- data/lib/css_parser.rb +23 -33
- data/lib/css_parser/parser.rb +95 -99
- data/lib/css_parser/regexps.rb +53 -32
- data/lib/css_parser/rule_set.rb +374 -240
- data/lib/css_parser/version.rb +3 -1
- metadata +120 -6
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5de40b5ed5a0e298dcf2bbb5461b0643a2044f0ee314f1211695da1e8b0c15a1
         | 
| 4 | 
            +
              data.tar.gz: 8db5565c440bb65113054f58354d8dcfb4c761b2e3a1b1c4d4d758c9f74aa3f8
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 28b3ff2dcd11c1803a74175fdebd8592aab0da5b5df608a92923a9c56bb1dd41fba217b73ccc1a919b4ba6468c9ecb1e5a57f4fd4cce989307f08acf0c7d28ac
         | 
| 7 | 
            +
              data.tar.gz: 3a0bcb5fb3c71f057ce7bdcf506aeca30829bb22558787d1d73c8a3a539f65cc9bcef9ead07c87f379378ec6885cc31b919582c1961eb8e921f81b04c620e949
         | 
    
        data/lib/css_parser.rb
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            require 'addressable/uri'
         | 
| 3 4 | 
             
            require 'uri'
         | 
| 4 5 | 
             
            require 'net/https'
         | 
| @@ -13,7 +14,6 @@ require 'css_parser/regexps' | |
| 13 14 | 
             
            require 'css_parser/parser'
         | 
| 14 15 |  | 
| 15 16 | 
             
            module CssParser
         | 
| 16 | 
            -
             | 
| 17 17 | 
             
              # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
         | 
| 18 18 | 
             
              # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
         | 
| 19 19 | 
             
              #
         | 
| @@ -56,10 +56,10 @@ module CssParser | |
| 56 56 | 
             
                @folded_declaration_cache = {}
         | 
| 57 57 |  | 
| 58 58 | 
             
                # in case called like CssParser.merge([rule_set, rule_set])
         | 
| 59 | 
            -
                rule_sets.flatten! if rule_sets[0]. | 
| 59 | 
            +
                rule_sets.flatten! if rule_sets[0].is_a?(Array)
         | 
| 60 60 |  | 
| 61 | 
            -
                unless rule_sets.all? {|rs| rs. | 
| 62 | 
            -
                  raise ArgumentError,  | 
| 61 | 
            +
                unless rule_sets.all? { |rs| rs.is_a?(CssParser::RuleSet) }
         | 
| 62 | 
            +
                  raise ArgumentError, 'all parameters must be CssParser::RuleSets.'
         | 
| 63 63 | 
             
                end
         | 
| 64 64 |  | 
| 65 65 | 
             
                return rule_sets[0] if rule_sets.length == 1
         | 
| @@ -71,38 +71,27 @@ module CssParser | |
| 71 71 | 
             
                  rule_set.expand_shorthand!
         | 
| 72 72 |  | 
| 73 73 | 
             
                  specificity = rule_set.specificity
         | 
| 74 | 
            -
                   | 
| 75 | 
            -
                    if rule_set.selectors.length == 0
         | 
| 76 | 
            -
                      specificity = 0
         | 
| 77 | 
            -
                    else
         | 
| 78 | 
            -
                      specificity = rule_set.selectors.map { |s| calculate_specificity(s) }.compact.max || 0
         | 
| 79 | 
            -
                    end
         | 
| 80 | 
            -
                  end
         | 
| 74 | 
            +
                  specificity ||= rule_set.selectors.map { |s| calculate_specificity(s) }.compact.max || 0
         | 
| 81 75 |  | 
| 82 76 | 
             
                  rule_set.each_declaration do |property, value, is_important|
         | 
| 83 77 | 
             
                    # Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
         | 
| 84 | 
            -
                    if not properties. | 
| 85 | 
            -
                      properties[property] = {: | 
| 78 | 
            +
                    if not properties.key?(property)
         | 
| 79 | 
            +
                      properties[property] = {value: value, specificity: specificity, is_important: is_important}
         | 
| 86 80 | 
             
                    elsif is_important
         | 
| 87 81 | 
             
                      if not properties[property][:is_important] or properties[property][:specificity] <= specificity
         | 
| 88 | 
            -
                        properties[property] = {: | 
| 82 | 
            +
                        properties[property] = {value: value, specificity: specificity, is_important: is_important}
         | 
| 89 83 | 
             
                      end
         | 
| 90 84 | 
             
                    elsif properties[property][:specificity] < specificity or properties[property][:specificity] == specificity
         | 
| 91 85 | 
             
                      unless properties[property][:is_important]
         | 
| 92 | 
            -
                        properties[property] = {: | 
| 86 | 
            +
                        properties[property] = {value: value, specificity: specificity, is_important: is_important}
         | 
| 93 87 | 
             
                      end
         | 
| 94 88 | 
             
                    end
         | 
| 95 | 
            -
             | 
| 89 | 
            +
                  end
         | 
| 96 90 | 
             
                end
         | 
| 97 91 |  | 
| 98 | 
            -
                merged = RuleSet.new(nil, nil)
         | 
| 99 | 
            -
             | 
| 100 | 
            -
             | 
| 101 | 
            -
                  if details[:is_important]
         | 
| 102 | 
            -
                    merged[property.strip] = details[:value].strip.gsub(/\;\Z/, '') + '!important'
         | 
| 103 | 
            -
                  else
         | 
| 104 | 
            -
                    merged[property.strip] = details[:value].strip
         | 
| 105 | 
            -
                  end
         | 
| 92 | 
            +
                merged = properties.each_with_object(RuleSet.new(nil, nil)) do |(property, details), rule_set|
         | 
| 93 | 
            +
                  value = details[:value].strip
         | 
| 94 | 
            +
                  rule_set[property.strip] = details[:is_important] ? "#{value.gsub(/;\Z/, '')}!important" : value
         | 
| 106 95 | 
             
                end
         | 
| 107 96 |  | 
| 108 97 | 
             
                merged.create_shorthand!
         | 
| @@ -128,7 +117,7 @@ module CssParser | |
| 128 117 |  | 
| 129 118 | 
             
                "#{a}#{b}#{c}#{d}".to_i
         | 
| 130 119 | 
             
              rescue
         | 
| 131 | 
            -
                 | 
| 120 | 
            +
                0
         | 
| 132 121 | 
             
              end
         | 
| 133 122 |  | 
| 134 123 | 
             
              # Make <tt>url()</tt> links absolute.
         | 
| @@ -145,23 +134,24 @@ module CssParser | |
| 145 134 | 
             
              #               "http://example.org/style/basic.css").inspect
         | 
| 146 135 | 
             
              #  => "body { background: url('http://example.org/style/yellow.png?abc=123') };"
         | 
| 147 136 | 
             
              def self.convert_uris(css, base_uri)
         | 
| 148 | 
            -
                base_uri = Addressable::URI.parse(base_uri) unless base_uri. | 
| 137 | 
            +
                base_uri = Addressable::URI.parse(base_uri) unless base_uri.is_a?(Addressable::URI)
         | 
| 149 138 |  | 
| 150 139 | 
             
                css.gsub(URI_RX) do
         | 
| 151 | 
            -
                  uri =  | 
| 152 | 
            -
                  uri.gsub!(/["']+/, '')
         | 
| 140 | 
            +
                  uri = Regexp.last_match(1).to_s.gsub(/["']+/, '')
         | 
| 153 141 | 
             
                  # Don't process URLs that are already absolute
         | 
| 154 | 
            -
                  unless uri | 
| 142 | 
            +
                  unless uri.match(%r{^[a-z]+://}i)
         | 
| 155 143 | 
             
                    begin
         | 
| 156 | 
            -
                      uri = base_uri | 
| 157 | 
            -
                    rescue | 
| 144 | 
            +
                      uri = base_uri.join(uri)
         | 
| 145 | 
            +
                    rescue
         | 
| 146 | 
            +
                      nil
         | 
| 147 | 
            +
                    end
         | 
| 158 148 | 
             
                  end
         | 
| 159 | 
            -
                  "url('#{uri | 
| 149 | 
            +
                  "url('#{uri}')"
         | 
| 160 150 | 
             
                end
         | 
| 161 151 | 
             
              end
         | 
| 162 152 |  | 
| 163 153 | 
             
              def self.sanitize_media_query(raw)
         | 
| 164 | 
            -
                mq = raw.to_s.gsub( | 
| 154 | 
            +
                mq = raw.to_s.gsub(/\s+/, ' ')
         | 
| 165 155 | 
             
                mq.strip!
         | 
| 166 156 | 
             
                mq = 'all' if mq.empty?
         | 
| 167 157 | 
             
                mq.to_sym
         | 
    
        data/lib/css_parser/parser.rb
    CHANGED
    
    | @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module CssParser
         | 
| 3 4 | 
             
              # Exception class used for any errors encountered while downloading remote files.
         | 
| 4 5 | 
             
              class RemoteFileError < IOError; end
         | 
| @@ -15,13 +16,13 @@ module CssParser | |
| 15 16 | 
             
              # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
         | 
| 16 17 | 
             
              # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
         | 
| 17 18 | 
             
              class Parser
         | 
| 18 | 
            -
                USER_AGENT | 
| 19 | 
            +
                USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)"
         | 
| 19 20 |  | 
| 20 | 
            -
                STRIP_CSS_COMMENTS_RX =  | 
| 21 | 
            -
                STRIP_HTML_COMMENTS_RX =  | 
| 21 | 
            +
                STRIP_CSS_COMMENTS_RX = %r{/\*.*?\*/}m.freeze
         | 
| 22 | 
            +
                STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze
         | 
| 22 23 |  | 
| 23 24 | 
             
                # Initial parsing
         | 
| 24 | 
            -
                RE_AT_IMPORT_RULE =  | 
| 25 | 
            +
                RE_AT_IMPORT_RULE = /@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s)]*)["']?\)?([\w\s,^\]()]*)\)?[;\n]?/.freeze
         | 
| 25 26 |  | 
| 26 27 | 
             
                MAX_REDIRECTS = 3
         | 
| 27 28 |  | 
| @@ -35,10 +36,10 @@ module CssParser | |
| 35 36 | 
             
                class << self; attr_reader :folded_declaration_cache; end
         | 
| 36 37 |  | 
| 37 38 | 
             
                def initialize(options = {})
         | 
| 38 | 
            -
                  @options = {: | 
| 39 | 
            -
                              : | 
| 40 | 
            -
                              : | 
| 41 | 
            -
                              : | 
| 39 | 
            +
                  @options = {absolute_paths: false,
         | 
| 40 | 
            +
                              import: true,
         | 
| 41 | 
            +
                              io_exceptions: true,
         | 
| 42 | 
            +
                              capture_offsets: false}.merge(options)
         | 
| 42 43 |  | 
| 43 44 | 
             
                  # array of RuleSets
         | 
| 44 45 | 
             
                  @rules = []
         | 
| @@ -70,21 +71,20 @@ module CssParser | |
| 70 71 | 
             
                # Returns an array of declarations.
         | 
| 71 72 | 
             
                def find_by_selector(selector, media_types = :all)
         | 
| 72 73 | 
             
                  out = []
         | 
| 73 | 
            -
                  each_selector(media_types) do |sel, dec,  | 
| 74 | 
            +
                  each_selector(media_types) do |sel, dec, _spec|
         | 
| 74 75 | 
             
                    out << dec if sel.strip == selector.strip
         | 
| 75 76 | 
             
                  end
         | 
| 76 77 | 
             
                  out
         | 
| 77 78 | 
             
                end
         | 
| 78 | 
            -
                 | 
| 79 | 
            +
                alias [] find_by_selector
         | 
| 79 80 |  | 
| 80 81 | 
             
                # Finds the rule sets that match the given selectors
         | 
| 81 82 | 
             
                def find_rule_sets(selectors, media_types = :all)
         | 
| 82 83 | 
             
                  rule_sets = []
         | 
| 83 84 |  | 
| 84 85 | 
             
                  selectors.each do |selector|
         | 
| 85 | 
            -
                    selector.gsub | 
| 86 | 
            -
                     | 
| 87 | 
            -
                    each_rule_set(media_types) do |rule_set, media_type|
         | 
| 86 | 
            +
                    selector = selector.gsub(/\s+/, ' ').strip
         | 
| 87 | 
            +
                    each_rule_set(media_types) do |rule_set, _media_type|
         | 
| 88 88 | 
             
                      if !rule_sets.member?(rule_set) && rule_set.selectors.member?(selector)
         | 
| 89 89 | 
             
                        rule_sets << rule_set
         | 
| 90 90 | 
             
                      end
         | 
| @@ -115,9 +115,9 @@ module CssParser | |
| 115 115 | 
             
                #   parser = CssParser::Parser.new
         | 
| 116 116 | 
             
                #   parser.add_block!(css)
         | 
| 117 117 | 
             
                def add_block!(block, options = {})
         | 
| 118 | 
            -
                  options = {: | 
| 119 | 
            -
                  options[:media_types] = [options[:media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
         | 
| 120 | 
            -
                  options[:only_media_types] = [options[:only_media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
         | 
| 118 | 
            +
                  options = {base_uri: nil, base_dir: nil, charset: nil, media_types: :all, only_media_types: :all}.merge(options)
         | 
| 119 | 
            +
                  options[:media_types] = [options[:media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
         | 
| 120 | 
            +
                  options[:only_media_types] = [options[:only_media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
         | 
| 121 121 |  | 
| 122 122 | 
             
                  block = cleanup_block(block, options)
         | 
| 123 123 |  | 
| @@ -129,19 +129,19 @@ module CssParser | |
| 129 129 | 
             
                  if @options[:import]
         | 
| 130 130 | 
             
                    block.scan(RE_AT_IMPORT_RULE).each do |import_rule|
         | 
| 131 131 | 
             
                      media_types = []
         | 
| 132 | 
            -
                      if media_string = import_rule[-1]
         | 
| 133 | 
            -
                        media_string.split( | 
| 132 | 
            +
                      if (media_string = import_rule[-1])
         | 
| 133 | 
            +
                        media_string.split(/,/).each do |t|
         | 
| 134 134 | 
             
                          media_types << CssParser.sanitize_media_query(t) unless t.empty?
         | 
| 135 135 | 
             
                        end
         | 
| 136 136 | 
             
                      else
         | 
| 137 137 | 
             
                        media_types = [:all]
         | 
| 138 138 | 
             
                      end
         | 
| 139 139 |  | 
| 140 | 
            -
                      next unless options[:only_media_types].include?(:all) or media_types. | 
| 140 | 
            +
                      next unless options[:only_media_types].include?(:all) or media_types.empty? or !(media_types & options[:only_media_types]).empty?
         | 
| 141 141 |  | 
| 142 142 | 
             
                      import_path = import_rule[0].to_s.gsub(/['"]*/, '').strip
         | 
| 143 143 |  | 
| 144 | 
            -
                      import_options = { | 
| 144 | 
            +
                      import_options = {media_types: media_types}
         | 
| 145 145 | 
             
                      import_options[:capture_offsets] = true if options[:capture_offsets]
         | 
| 146 146 |  | 
| 147 147 | 
             
                      if options[:base_uri]
         | 
| @@ -183,21 +183,21 @@ module CssParser | |
| 183 183 | 
             
                #
         | 
| 184 184 | 
             
                # +media_types+ can be a symbol or an array of symbols.
         | 
| 185 185 | 
             
                def add_rule_set!(ruleset, media_types = :all)
         | 
| 186 | 
            -
                  raise ArgumentError unless ruleset. | 
| 186 | 
            +
                  raise ArgumentError unless ruleset.is_a?(CssParser::RuleSet)
         | 
| 187 187 |  | 
| 188 | 
            -
                  media_types = [media_types] unless Array | 
| 189 | 
            -
                  media_types = media_types.flat_map { |mt| CssParser.sanitize_media_query(mt)}
         | 
| 188 | 
            +
                  media_types = [media_types] unless media_types.is_a?(Array)
         | 
| 189 | 
            +
                  media_types = media_types.flat_map { |mt| CssParser.sanitize_media_query(mt) }
         | 
| 190 190 |  | 
| 191 | 
            -
                  @rules << {: | 
| 191 | 
            +
                  @rules << {media_types: media_types, rules: ruleset}
         | 
| 192 192 | 
             
                end
         | 
| 193 193 |  | 
| 194 194 | 
             
                # Remove a CssParser RuleSet object.
         | 
| 195 195 | 
             
                #
         | 
| 196 196 | 
             
                # +media_types+ can be a symbol or an array of symbols.
         | 
| 197 197 | 
             
                def remove_rule_set!(ruleset, media_types = :all)
         | 
| 198 | 
            -
                  raise ArgumentError unless ruleset. | 
| 198 | 
            +
                  raise ArgumentError unless ruleset.is_a?(CssParser::RuleSet)
         | 
| 199 199 |  | 
| 200 | 
            -
                  media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
         | 
| 200 | 
            +
                  media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
         | 
| 201 201 |  | 
| 202 202 | 
             
                  @rules.reject! do |rule|
         | 
| 203 203 | 
             
                    rule[:media_types] == media_types && rule[:rules].to_s == ruleset.to_s
         | 
| @@ -209,7 +209,7 @@ module CssParser | |
| 209 209 | 
             
                # +media_types+ can be a symbol or an array of symbols.
         | 
| 210 210 | 
             
                def each_rule_set(media_types = :all) # :yields: rule_set, media_types
         | 
| 211 211 | 
             
                  media_types = [:all] if media_types.nil?
         | 
| 212 | 
            -
                  media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
         | 
| 212 | 
            +
                  media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
         | 
| 213 213 |  | 
| 214 214 | 
             
                  @rules.each do |block|
         | 
| 215 215 | 
             
                    if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) }
         | 
| @@ -222,7 +222,7 @@ module CssParser | |
| 222 222 | 
             
                def to_h(which_media = :all)
         | 
| 223 223 | 
             
                  out = {}
         | 
| 224 224 | 
             
                  styles_by_media_types = {}
         | 
| 225 | 
            -
                  each_selector(which_media) do |selectors, declarations,  | 
| 225 | 
            +
                  each_selector(which_media) do |selectors, declarations, _specificity, media_types|
         | 
| 226 226 | 
             
                    media_types.each do |media_type|
         | 
| 227 227 | 
             
                      styles_by_media_types[media_type] ||= []
         | 
| 228 228 | 
             
                      styles_by_media_types[media_type] << [selectors, declarations]
         | 
| @@ -244,7 +244,7 @@ module CssParser | |
| 244 244 | 
             
                # +media_types+ can be a symbol or an array of symbols.
         | 
| 245 245 | 
             
                # See RuleSet#each_selector for +options+.
         | 
| 246 246 | 
             
                def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types
         | 
| 247 | 
            -
                  return to_enum( | 
| 247 | 
            +
                  return to_enum(__method__, all_media_types, options) unless block_given?
         | 
| 248 248 |  | 
| 249 249 | 
             
                  each_rule_set(all_media_types) do |rule_set, media_types|
         | 
| 250 250 | 
             
                    rule_set.each_selector(options) do |selectors, declarations, specificity|
         | 
| @@ -255,9 +255,10 @@ module CssParser | |
| 255 255 |  | 
| 256 256 | 
             
                # Output all CSS rules as a single stylesheet.
         | 
| 257 257 | 
             
                def to_s(which_media = :all)
         | 
| 258 | 
            -
                  out =  | 
| 258 | 
            +
                  out = []
         | 
| 259 259 | 
             
                  styles_by_media_types = {}
         | 
| 260 | 
            -
             | 
| 260 | 
            +
             | 
| 261 | 
            +
                  each_selector(which_media) do |selectors, declarations, _specificity, media_types|
         | 
| 261 262 | 
             
                    media_types.each do |media_type|
         | 
| 262 263 | 
             
                      styles_by_media_types[media_type] ||= []
         | 
| 263 264 | 
             
                      styles_by_media_types[media_type] << [selectors, declarations]
         | 
| @@ -266,20 +267,21 @@ module CssParser | |
| 266 267 |  | 
| 267 268 | 
             
                  styles_by_media_types.each_pair do |media_type, media_styles|
         | 
| 268 269 | 
             
                    media_block = (media_type != :all)
         | 
| 269 | 
            -
                    out << "@media #{media_type} { | 
| 270 | 
            +
                    out << "@media #{media_type} {" if media_block
         | 
| 270 271 |  | 
| 271 272 | 
             
                    media_styles.each do |media_style|
         | 
| 272 273 | 
             
                      if media_block
         | 
| 273 | 
            -
                        out | 
| 274 | 
            +
                        out.push("  #{media_style[0]} {\n    #{media_style[1]}\n  }")
         | 
| 274 275 | 
             
                      else
         | 
| 275 | 
            -
                        out | 
| 276 | 
            +
                        out.push("#{media_style[0]} {\n#{media_style[1]}\n}")
         | 
| 276 277 | 
             
                      end
         | 
| 277 278 | 
             
                    end
         | 
| 278 279 |  | 
| 279 | 
            -
                    out <<  | 
| 280 | 
            +
                    out << '}' if media_block
         | 
| 280 281 | 
             
                  end
         | 
| 281 282 |  | 
| 282 | 
            -
                  out
         | 
| 283 | 
            +
                  out << ''
         | 
| 284 | 
            +
                  out.join("\n")
         | 
| 283 285 | 
             
                end
         | 
| 284 286 |  | 
| 285 287 | 
             
                # A hash of { :media_query => rule_sets }
         | 
| @@ -287,7 +289,7 @@ module CssParser | |
| 287 289 | 
             
                  rules_by_media = {}
         | 
| 288 290 | 
             
                  @rules.each do |block|
         | 
| 289 291 | 
             
                    block[:media_types].each do |mt|
         | 
| 290 | 
            -
                      unless rules_by_media. | 
| 292 | 
            +
                      unless rules_by_media.key?(mt)
         | 
| 291 293 | 
             
                        rules_by_media[mt] = []
         | 
| 292 294 | 
             
                      end
         | 
| 293 295 | 
             
                      rules_by_media[mt] << block[:rules]
         | 
| @@ -299,15 +301,13 @@ module CssParser | |
| 299 301 |  | 
| 300 302 | 
             
                # Merge declarations with the same selector.
         | 
| 301 303 | 
             
                def compact! # :nodoc:
         | 
| 302 | 
            -
                   | 
| 303 | 
            -
             | 
| 304 | 
            -
                  compacted
         | 
| 304 | 
            +
                  []
         | 
| 305 305 | 
             
                end
         | 
| 306 306 |  | 
| 307 307 | 
             
                def parse_block_into_rule_sets!(block, options = {}) # :nodoc:
         | 
| 308 308 | 
             
                  current_media_queries = [:all]
         | 
| 309 309 | 
             
                  if options[:media_types]
         | 
| 310 | 
            -
                    current_media_queries = options[:media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
         | 
| 310 | 
            +
                    current_media_queries = options[:media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
         | 
| 311 311 | 
             
                  end
         | 
| 312 312 |  | 
| 313 313 | 
             
                  in_declarations = 0
         | 
| @@ -326,7 +326,7 @@ module CssParser | |
| 326 326 | 
             
                  rule_start = nil
         | 
| 327 327 | 
             
                  offset = nil
         | 
| 328 328 |  | 
| 329 | 
            -
                  block.scan(/\s | 
| 329 | 
            +
                  block.scan(/\s+|\\{2,}|\\?[{}\s"]|.[^\s"{}\\]*/) do |token|
         | 
| 330 330 | 
             
                    # save the regex offset so that we know where in the file we are
         | 
| 331 331 | 
             
                    offset = Regexp.last_match.offset(0) if options[:capture_offsets]
         | 
| 332 332 |  | 
| @@ -349,7 +349,7 @@ module CssParser | |
| 349 349 | 
             
                      current_declarations << token
         | 
| 350 350 |  | 
| 351 351 | 
             
                      if !in_string && token.include?('}')
         | 
| 352 | 
            -
                        current_declarations.gsub!(/\} | 
| 352 | 
            +
                        current_declarations.gsub!(/\}\s*$/, '')
         | 
| 353 353 |  | 
| 354 354 | 
             
                        in_declarations -= 1
         | 
| 355 355 | 
             
                        current_declarations.strip!
         | 
| @@ -374,7 +374,7 @@ module CssParser | |
| 374 374 | 
             
                      current_media_queries = []
         | 
| 375 375 | 
             
                    elsif in_at_media_rule
         | 
| 376 376 | 
             
                      if token.include?('{')
         | 
| 377 | 
            -
                        block_depth  | 
| 377 | 
            +
                        block_depth += 1
         | 
| 378 378 | 
             
                        in_at_media_rule = false
         | 
| 379 379 | 
             
                        in_media_block = true
         | 
| 380 380 | 
             
                        current_media_queries << CssParser.sanitize_media_query(current_media_query)
         | 
| @@ -393,38 +393,34 @@ module CssParser | |
| 393 393 | 
             
                    elsif in_charset or token =~ /@charset/i
         | 
| 394 394 | 
             
                      # iterate until we are out of the charset declaration
         | 
| 395 395 | 
             
                      in_charset = !token.include?(';')
         | 
| 396 | 
            -
                     | 
| 397 | 
            -
                       | 
| 398 | 
            -
                        block_depth = block_depth - 1
         | 
| 396 | 
            +
                    elsif !in_string && token.include?('}')
         | 
| 397 | 
            +
                      block_depth -= 1
         | 
| 399 398 |  | 
| 400 | 
            -
             | 
| 401 | 
            -
             | 
| 402 | 
            -
             | 
| 403 | 
            -
             | 
| 404 | 
            -
                        end
         | 
| 405 | 
            -
                      else
         | 
| 406 | 
            -
                        if !in_string && token.include?('{')
         | 
| 407 | 
            -
                          current_selectors.strip!
         | 
| 408 | 
            -
                          in_declarations += 1
         | 
| 409 | 
            -
                        else
         | 
| 410 | 
            -
                          # if we are in a selector, add the token to the current selectors
         | 
| 411 | 
            -
                          current_selectors << token
         | 
| 412 | 
            -
             | 
| 413 | 
            -
                          # mark this as the beginning of the selector unless we have already marked it
         | 
| 414 | 
            -
                          rule_start = offset.first if options[:capture_offsets] && rule_start.nil? && token =~ /^[^\s]+$/
         | 
| 415 | 
            -
                        end
         | 
| 399 | 
            +
                      # reset the current media query scope
         | 
| 400 | 
            +
                      if in_media_block
         | 
| 401 | 
            +
                        current_media_queries = [:all]
         | 
| 402 | 
            +
                        in_media_block = false
         | 
| 416 403 | 
             
                      end
         | 
| 404 | 
            +
                    elsif !in_string && token.include?('{')
         | 
| 405 | 
            +
                      current_selectors.strip!
         | 
| 406 | 
            +
                      in_declarations += 1
         | 
| 407 | 
            +
                    else
         | 
| 408 | 
            +
                      # if we are in a selector, add the token to the current selectors
         | 
| 409 | 
            +
                      current_selectors << token
         | 
| 410 | 
            +
             | 
| 411 | 
            +
                      # mark this as the beginning of the selector unless we have already marked it
         | 
| 412 | 
            +
                      rule_start = offset.first if options[:capture_offsets] && rule_start.nil? && token =~ /^[^\s]+$/
         | 
| 417 413 | 
             
                    end
         | 
| 418 414 | 
             
                  end
         | 
| 419 415 |  | 
| 420 416 | 
             
                  # check for unclosed braces
         | 
| 421 | 
            -
                   | 
| 422 | 
            -
             | 
| 423 | 
            -
             | 
| 424 | 
            -
                     | 
| 425 | 
            -
                      add_rule!(current_selectors, current_declarations, current_media_queries)
         | 
| 426 | 
            -
                    end
         | 
| 417 | 
            +
                  return unless in_declarations > 0
         | 
| 418 | 
            +
             | 
| 419 | 
            +
                  unless options[:capture_offsets]
         | 
| 420 | 
            +
                    return add_rule!(current_selectors, current_declarations, current_media_queries)
         | 
| 427 421 | 
             
                  end
         | 
| 422 | 
            +
             | 
| 423 | 
            +
                  add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
         | 
| 428 424 | 
             
                end
         | 
| 429 425 |  | 
| 430 426 | 
             
                # Load a remote CSS file.
         | 
| @@ -437,7 +433,7 @@ module CssParser | |
| 437 433 | 
             
                def load_uri!(uri, options = {}, deprecated = nil)
         | 
| 438 434 | 
             
                  uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme
         | 
| 439 435 |  | 
| 440 | 
            -
                  opts = {: | 
| 436 | 
            +
                  opts = {base_uri: nil, media_types: :all}
         | 
| 441 437 |  | 
| 442 438 | 
             
                  if options.is_a? Hash
         | 
| 443 439 | 
             
                    opts.merge!(options)
         | 
| @@ -457,14 +453,13 @@ module CssParser | |
| 457 453 | 
             
                  opts[:filename] = uri.to_s if opts[:capture_offsets]
         | 
| 458 454 |  | 
| 459 455 | 
             
                  src, = read_remote_file(uri) # skip charset
         | 
| 460 | 
            -
             | 
| 461 | 
            -
             | 
| 462 | 
            -
                  end
         | 
| 456 | 
            +
             | 
| 457 | 
            +
                  add_block!(src, opts) if src
         | 
| 463 458 | 
             
                end
         | 
| 464 459 |  | 
| 465 460 | 
             
                # Load a local CSS file.
         | 
| 466 461 | 
             
                def load_file!(file_name, options = {}, deprecated = nil)
         | 
| 467 | 
            -
                  opts = {: | 
| 462 | 
            +
                  opts = {base_dir: nil, media_types: :all}
         | 
| 468 463 |  | 
| 469 464 | 
             
                  if options.is_a? Hash
         | 
| 470 465 | 
             
                    opts.merge!(options)
         | 
| @@ -487,7 +482,7 @@ module CssParser | |
| 487 482 |  | 
| 488 483 | 
             
                # Load a local CSS string.
         | 
| 489 484 | 
             
                def load_string!(src, options = {}, deprecated = nil)
         | 
| 490 | 
            -
                  opts = {: | 
| 485 | 
            +
                  opts = {base_dir: nil, media_types: :all}
         | 
| 491 486 |  | 
| 492 487 | 
             
                  if options.is_a? Hash
         | 
| 493 488 | 
             
                    opts.merge!(options)
         | 
| @@ -499,9 +494,8 @@ module CssParser | |
| 499 494 | 
             
                  add_block!(src, opts)
         | 
| 500 495 | 
             
                end
         | 
| 501 496 |  | 
| 502 | 
            -
             | 
| 503 | 
            -
             | 
| 504 497 | 
             
              protected
         | 
| 498 | 
            +
             | 
| 505 499 | 
             
                # Check that a path hasn't been loaded already
         | 
| 506 500 | 
             
                #
         | 
| 507 501 | 
             
                # Raises a CircularReferenceError exception if io_exceptions are on,
         | 
| @@ -510,10 +504,11 @@ module CssParser | |
| 510 504 | 
             
                  path = path.to_s
         | 
| 511 505 | 
             
                  if @loaded_uris.include?(path)
         | 
| 512 506 | 
             
                    raise CircularReferenceError, "can't load #{path} more than once" if @options[:io_exceptions]
         | 
| 513 | 
            -
             | 
| 507 | 
            +
             | 
| 508 | 
            +
                    false
         | 
| 514 509 | 
             
                  else
         | 
| 515 510 | 
             
                    @loaded_uris << path
         | 
| 516 | 
            -
                     | 
| 511 | 
            +
                    true
         | 
| 517 512 | 
             
                  end
         | 
| 518 513 | 
             
                end
         | 
| 519 514 |  | 
| @@ -533,7 +528,7 @@ module CssParser | |
| 533 528 | 
             
                # Returns a string.
         | 
| 534 529 | 
             
                def cleanup_block(block, options = {}) # :nodoc:
         | 
| 535 530 | 
             
                  # Strip CSS comments
         | 
| 536 | 
            -
                  utf8_block = block.encode('UTF-8', ' | 
| 531 | 
            +
                  utf8_block = block.encode('UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: ' ')
         | 
| 537 532 | 
             
                  utf8_block = ignore_pattern(utf8_block, STRIP_CSS_COMMENTS_RX, options)
         | 
| 538 533 |  | 
| 539 534 | 
             
                  # Strip HTML comments - they shouldn't really be in here but
         | 
| @@ -541,7 +536,7 @@ module CssParser | |
| 541 536 | 
             
                  utf8_block = ignore_pattern(utf8_block, STRIP_HTML_COMMENTS_RX, options)
         | 
| 542 537 |  | 
| 543 538 | 
             
                  # Strip lines containing just whitespace
         | 
| 544 | 
            -
                  utf8_block.gsub!(/^\s+$/,  | 
| 539 | 
            +
                  utf8_block.gsub!(/^\s+$/, '') unless options[:capture_offsets]
         | 
| 545 540 |  | 
| 546 541 | 
             
                  utf8_block
         | 
| 547 542 | 
             
                end
         | 
| @@ -577,11 +572,8 @@ module CssParser | |
| 577 572 | 
             
                    if uri.scheme == 'file'
         | 
| 578 573 | 
             
                      # local file
         | 
| 579 574 | 
             
                      path = uri.path
         | 
| 580 | 
            -
                      path.gsub!( | 
| 581 | 
            -
                       | 
| 582 | 
            -
                      src = fh.read
         | 
| 583 | 
            -
                      charset = fh.respond_to?(:charset) ? fh.charset : 'utf-8'
         | 
| 584 | 
            -
                      fh.close
         | 
| 575 | 
            +
                      path.gsub!(%r{^/}, '') if Gem.win_platform?
         | 
| 576 | 
            +
                      src = File.read(path, mode: 'rb')
         | 
| 585 577 | 
             
                    else
         | 
| 586 578 | 
             
                      # remote file
         | 
| 587 579 | 
             
                      if uri.scheme == 'https'
         | 
| @@ -599,21 +591,22 @@ module CssParser | |
| 599 591 |  | 
| 600 592 | 
             
                      if res.code.to_i >= 400
         | 
| 601 593 | 
             
                        @redirect_count = nil
         | 
| 602 | 
            -
                        raise RemoteFileError | 
| 594 | 
            +
                        raise RemoteFileError, uri.to_s if @options[:io_exceptions]
         | 
| 595 | 
            +
             | 
| 603 596 | 
             
                        return '', nil
         | 
| 604 597 | 
             
                      elsif res.code.to_i >= 300 and res.code.to_i < 400
         | 
| 605 | 
            -
                         | 
| 598 | 
            +
                        unless res['Location'].nil?
         | 
| 606 599 | 
             
                          return read_remote_file Addressable::URI.parse(Addressable::URI.escape(res['Location']))
         | 
| 607 600 | 
             
                        end
         | 
| 608 601 | 
             
                      end
         | 
| 609 602 |  | 
| 610 603 | 
             
                      case res['content-encoding']
         | 
| 611 | 
            -
             | 
| 612 | 
            -
             | 
| 613 | 
            -
             | 
| 614 | 
            -
             | 
| 615 | 
            -
             | 
| 616 | 
            -
             | 
| 604 | 
            +
                      when 'gzip'
         | 
| 605 | 
            +
                        io = Zlib::GzipReader.new(StringIO.new(res.body))
         | 
| 606 | 
            +
                        src = io.read
         | 
| 607 | 
            +
                      when 'deflate'
         | 
| 608 | 
            +
                        io = Zlib::Inflate.new
         | 
| 609 | 
            +
                        src = io.inflate(res.body)
         | 
| 617 610 | 
             
                      end
         | 
| 618 611 | 
             
                    end
         | 
| 619 612 |  | 
| @@ -627,15 +620,17 @@ module CssParser | |
| 627 620 | 
             
                    end
         | 
| 628 621 | 
             
                  rescue
         | 
| 629 622 | 
             
                    @redirect_count = nil
         | 
| 630 | 
            -
                    raise RemoteFileError | 
| 623 | 
            +
                    raise RemoteFileError, uri.to_s if @options[:io_exceptions]
         | 
| 624 | 
            +
             | 
| 631 625 | 
             
                    return nil, nil
         | 
| 632 626 | 
             
                  end
         | 
| 633 627 |  | 
| 634 628 | 
             
                  @redirect_count = nil
         | 
| 635 | 
            -
                   | 
| 629 | 
            +
                  [src, charset]
         | 
| 636 630 | 
             
                end
         | 
| 637 631 |  | 
| 638 632 | 
             
              private
         | 
| 633 | 
            +
             | 
| 639 634 | 
             
                # Save a folded declaration block to the internal cache.
         | 
| 640 635 | 
             
                def save_folded_declaration(block_hash, folded_declaration) # :nodoc:
         | 
| 641 636 | 
             
                  @folded_declaration_cache[block_hash] = folded_declaration
         | 
| @@ -643,7 +638,7 @@ module CssParser | |
| 643 638 |  | 
| 644 639 | 
             
                # Retrieve a folded declaration block from the internal cache.
         | 
| 645 640 | 
             
                def get_folded_declaration(block_hash) # :nodoc:
         | 
| 646 | 
            -
                   | 
| 641 | 
            +
                  @folded_declaration_cache[block_hash] ||= nil
         | 
| 647 642 | 
             
                end
         | 
| 648 643 |  | 
| 649 644 | 
             
                def reset! # :nodoc:
         | 
| @@ -657,14 +652,15 @@ module CssParser | |
| 657 652 | 
             
                # passed hash
         | 
| 658 653 | 
             
                def css_node_to_h(hash, key, val)
         | 
| 659 654 | 
             
                  hash[key.strip] = '' and return hash if val.nil?
         | 
| 655 | 
            +
             | 
| 660 656 | 
             
                  lines = val.split(';')
         | 
| 661 657 | 
             
                  nodes = {}
         | 
| 662 658 | 
             
                  lines.each do |line|
         | 
| 663 659 | 
             
                    parts = line.split(':', 2)
         | 
| 664 | 
            -
                    if  | 
| 660 | 
            +
                    if parts[1] =~ /:/
         | 
| 665 661 | 
             
                      nodes[parts[0]] = css_node_to_h(hash, parts[0], parts[1])
         | 
| 666 662 | 
             
                    else
         | 
| 667 | 
            -
                      nodes[parts[0].to_s.strip] =parts[1].to_s.strip
         | 
| 663 | 
            +
                      nodes[parts[0].to_s.strip] = parts[1].to_s.strip
         | 
| 668 664 | 
             
                    end
         | 
| 669 665 | 
             
                  end
         | 
| 670 666 | 
             
                  hash[key.strip] = nodes
         |