lib/css_parser/parser.rb in css_parser-1.4.10 vs lib/css_parser/parser.rb in css_parser-1.5.0.pre

- old
+ new

@@ -34,11 +34,12 @@ class << self; attr_reader :folded_declaration_cache; end 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 @@ -115,11 +116,11 @@ def add_block!(block, options = {}) options = {:base_uri => nil, :base_dir => nil, :charset => nil, :media_types => :all, :only_media_types => :all}.merge(options) 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]) end @@ -137,21 +138,26 @@ next unless options[:only_media_types].include?(:all) or media_types.length < 1 or (media_types & options[:only_media_types]).length > 0 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 # Add a CSS rule by setting the +selectors+, +declarations+ and +media_types+. @@ -160,10 +166,20 @@ def add_rule!(selectors, declarations, media_types = :all) rule_set = RuleSet.new(selectors, declarations) 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. def add_rule_set!(ruleset, media_types = :all) raise ArgumentError unless ruleset.kind_of?(CssParser::RuleSet) @@ -287,13 +303,20 @@ current_selectors = '' current_media_query = '' current_declarations = '' - block.scan(/(([\\]{2,})|([\\]?[{}\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 if in_declarations > 0 @@ -314,15 +337,22 @@ current_declarations.gsub!(/\}[\s]*$/, '') 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 in_at_media_rule = true current_media_queries = [] @@ -354,23 +384,30 @@ current_media_queries = [:all] in_media_block = false 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 end # 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 # Load a remote CSS file. # @@ -379,11 +416,10 @@ # See add_block! for options. # # 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} if options.is_a? Hash opts.merge!(options) @@ -397,31 +433,54 @@ uri.scheme = 'file' end opts[:base_uri] = uri if opts[:base_uri].nil? + # 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 protected @@ -438,24 +497,35 @@ @loaded_uris << path return true 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 - utf8_block = block.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') - utf8_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... - utf8_block.gsub!(STRIP_HTML_COMMENTS_RX, '') + utf8_block = ignore_pattern(utf8_block, STRIP_HTML_COMMENTS_RX, options) # Strip lines containing just whitespace - utf8_block.gsub!(/^\s+$/, "") + utf8_block.gsub!(/^\s+$/, "") unless options[:capture_offsets] utf8_block end # Download a file into a string. @@ -481,16 +551,20 @@ return nil, nil end src = '', charset = nil - uri = Addressable::URI.parse(uri.to_s) begin + uri = Addressable::URI.parse(uri.to_s) + 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 if uri.scheme == 'https' uri.port = 443 unless uri.port @@ -501,10 +575,10 @@ http = Net::HTTP.new(uri.host, uri.port) end 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 @redirect_count = nil raise RemoteFileError.new(uri.to_s) if @options[:io_exceptions] return '', nil