lib/headhunter/css_validator.rb in headhunter-0.1.4 vs lib/headhunter/css_validator.rb in headhunter-0.1.5

- old
+ new

@@ -1,75 +1,67 @@ require 'net/http' -require 'rexml/document' +require 'nokogiri/xml' module Headhunter - class LocalResponse - attr_reader :body + class CssValidator + VALIDATOR_PATH = Gem.loaded_specs['headhunter'].full_gem_path + '/lib/css-validator/' - def initialize(body) - @body = body - @headers = {'x-w3c-validator-status' => valid?(body)} - end + attr_reader :stylesheets, :responses - def [](key) - @headers[key] - end + def initialize(stylesheets = [], profile = 'css3', vextwarning = true) + @stylesheets = stylesheets + @profile = profile # TODO! + @vextwarning = vextwarning # TODO! - private - - def valid?(body) - REXML::Document.new(body).root.each_element('//m:validity') { |e| return e.text == 'true' } + @responses = @stylesheets.map do |stylesheet| + validate(stylesheet) + end end - end - class CssValidator - USE_LOCAL_VALIDATOR = true + def validate(uri) + # See http://stackoverflow.com/questions/1137884/is-there-an-open-source-css-validator-that-can-be-run-locally + # More config options see http://jigsaw.w3.org/css-validator/manual.html + results = if File.exists?(uri) + Dir.chdir(VALIDATOR_PATH) { `java -jar css-validator.jar --output=soap12 file:#{uri}` } + else + raise "Couldn't locate uri #{uri}" + end - def initialize(stylesheets = []) - @profile = 'css3' # TODO: Option for profile css1 and css21 - @stylesheets = stylesheets - @messages_per_stylesheet = {} + Response.new(results) end - def add_stylesheet(stylesheet) - @stylesheets << stylesheet + def valid_responses + @responses.select(&:valid?) end - def process! - @stylesheets.each do |stylesheet| - css = fetch(stylesheet) - css = ' ' if css.empty? # The validator returns a 500 error if it receives an empty string - - response = get_validation_response({text: css, profile: @profile, vextwarning: 'true'}) - unless response_indicates_valid?(response) - process_errors(stylesheet, css, response) - end - end + def invalid_responses + @responses.reject(&:valid?) end - def report - puts "Validated #{@stylesheets.size} stylesheets.".yellow - puts "#{x_stylesheets_be(@stylesheets.size - @messages_per_stylesheet.size)} valid.".green if @messages_per_stylesheet.size < @stylesheets.size - puts "#{x_stylesheets_be(@messages_per_stylesheet.size)} invalid.".red if @messages_per_stylesheet.size > 0 + def statistics + lines = [] - @messages_per_stylesheet.each_pair do |stylesheet, messages| - puts " #{extract_filename(stylesheet)}:".red + lines << "Validated #{responses.size} stylesheets.".yellow + lines << "All stylesheets are valid.".green if invalid_responses.size == 0 + lines << "#{x_stylesheets_be(invalid_responses.size)} invalid.".red if invalid_responses.size > 0 - messages.each { |message| puts " - #{message}".red } + invalid_responses.each do |response| + lines << " #{extract_filename(response.uri)}:".red + + response.errors.each do |error| + lines << " - #{error.to_s}".red + end end - puts + lines.join("\n") end - private - - # Converts a path like public/assets/application-d205d6f344d8623ca0323cb6f6bd7ca1.css to application.css def extract_filename(path) - if matches = path.match(/^public\/assets\/(.*)-?([a-z0-9]*)(\.css)/) - matches[1] + matches[3] + if matches = path.match(/public\/assets\/([a-z\-_]*)-([a-z0-9]{32})(\.css)$/) + matches[1] + matches[3] # application-d205d6f344d8623ca0323cb6f6bd7ca1.css becomes application.css else - raise "Unexpected path: #{path}" + File.basename(path) end end def x_stylesheets_be(size) if size <= 1 @@ -77,117 +69,74 @@ else "#{size} stylesheets are" end end - def process_errors(file, css, response) - @messages_per_stylesheet[file] = [] - - REXML::Document.new(response.body).root.each_element('//m:error') do |e| - @messages_per_stylesheet[file] << "Line #{extract_line_from_error(e)}: #{extract_message_from_error(e)}" + class Response + def initialize(response = nil) + @document = Nokogiri::XML(convert_soap_to_xml(response)) if response end - end - def extract_line_from_error(e) - e.elements['m:line'].text - end - - def extract_message_from_error(e) - e.elements['m:message'].get_text.value.strip[0..-2] - end - - def fetch(path) # TODO: Move to Headhunter! - loc = path - - begin - open(loc).read - rescue Errno::ENOENT - raise FetchError.new("#{loc} was not found") - rescue OpenURI::HTTPError => e - raise FetchError.new("retrieving #{loc} raised an HTTP error: #{e.message}") + def [](key) + @headers[key] end - end - def get_validation_response(query_params) - query_params.merge!({:output => 'soap12'}) - - if USE_LOCAL_VALIDATOR - call_local_validator(query_params) - else - call_remote_validator(query_params) + def valid? + @document.css('validity').text == 'true' end - end - def response_indicates_valid?(response) - response['x-w3c-validator-status'] == 'Valid' - end - - def call_remote_validator(query_params = {}) - boundary = Digest::MD5.hexdigest(Time.now.to_s) - data = encode_multipart_params(boundary, query_params) - response = http_start(validator_host).post2(validator_path, - data, - 'Content-type' => "multipart/form-data; boundary=#{boundary}") - - raise "HTTP error: #{response.code}" unless response.is_a? Net::HTTPSuccess - response - end - - def call_local_validator(query_params) - path = Gem.loaded_specs['headhunter'].full_gem_path + '/lib/css-validator/' - css_file = 'tmp.css' - results_file = 'results' - results = nil - - Dir.chdir(path) do - File.open(css_file, 'a') { |f| f.write query_params[:text] } - - # See http://stackoverflow.com/questions/1137884/is-there-an-open-source-css-validator-that-can-be-run-locally - if system "java -jar css-validator.jar --output=soap12 file:#{css_file} > #{results_file}" - results = IO.read results_file - else - raise 'Could not execute local validation!' + def errors + @document.css('errors error').map do |error| + Error.new( error.css('line').text.strip.to_i, + error.css('message').text.strip[0..-3], + errortype: error.css('errortype').text.strip, + context: error.css('context').text.strip, + errorsubtype: error.css('errorsubtype').text.strip, + skippedstring: error.css('skippedstring').text.strip + ) end + end - File.delete css_file - File.delete results_file + def uri + @document.css('cssvalidationresponse > uri').text end - LocalResponse.new(results) - end + private - def encode_multipart_params(boundary, params = {}) - ret = '' - params.each do |k,v| - unless v.empty? - ret << "\r\n--#{boundary}\r\n" - ret << "Content-Disposition: form-data; name=\"#{k.to_s}\"\r\n\r\n" - ret << v - end + def convert_soap_to_xml(soap) + sanitize_prefixed_tags_from( + remove_first_line_from(soap) + ) end - ret << "\r\n--#{boundary}--\r\n" - ret - end - def http_start(host) - if ENV['http_proxy'] - uri = URI.parse(ENV['http_proxy']) - proxy_user, proxy_pass = uri.userinfo.split(/:/) if uri.userinfo - Net::HTTP.start(host, nil, uri.host, uri.port, proxy_user, proxy_pass) - else - Net::HTTP.start(host) + # The first line of the validator's response contains parameter options like this: + # + # {vextwarning=false, output=soap, lang=en, warning=2, medium=all, profile=css3} + # + # We remove this so Nokogiri can parse the document as XML. + def remove_first_line_from(soap) + soap.split("\n")[1..-1].join("\n") end - end - def validator_host - 'jigsaw.w3.org' - end + # The validator's response contains strange SOAP tags like `m:error` or `env:body` which need to be sanitized for Nokogiri. + # + # We simply remove the `m:` and `env:` prefixes from the source, so e.g. `<env:body>` becomes `<body>`. + def sanitize_prefixed_tags_from(soap) + soap.gsub /(m|env):/, '' + end - def validator_path - '/css-validator/validator' - end + class Error + attr_reader :line, :message, :details - def error_line_prefix - 'Invalid css' + def initialize(line, message, details = {}) + @line = line + @message = message + @details = details + end + + def to_s + "Line #{@line}: #{@message}." + end + end end end end