readme-score 0.0.2
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 +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +68 -0
- data/LICENSE.txt +22 -0
- data/README.md +87 -0
- data/Rakefile +133 -0
- data/data/seed.json +58 -0
- data/lib/readme-score.rb +47 -0
- data/lib/readme-score/document.rb +37 -0
- data/lib/readme-score/document/filter.rb +70 -0
- data/lib/readme-score/document/loader.rb +88 -0
- data/lib/readme-score/document/loader/github_readme_finder.rb +39 -0
- data/lib/readme-score/document/metrics.rb +125 -0
- data/lib/readme-score/document/parser.rb +24 -0
- data/lib/readme-score/document/score.rb +104 -0
- data/lib/readme-score/util.rb +14 -0
- data/lib/readme-score/version.rb +3 -0
- data/readme-score.gemspec +30 -0
- data/spec/document/filter_spec.rb +111 -0
- data/spec/document/loader_spec.rb +139 -0
- data/spec/document/metrics_spec.rb +95 -0
- data/spec/readme-score_spec.rb +34 -0
- data/spec/spec_helper.rb +17 -0
- metadata +199 -0
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            require 'readme-score/document/filter'
         | 
| 2 | 
            +
            require 'readme-score/document/metrics'
         | 
| 3 | 
            +
            require 'readme-score/document/loader'
         | 
| 4 | 
            +
            require 'readme-score/document/parser'
         | 
| 5 | 
            +
            require 'readme-score/document/score'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module ReadmeScore
         | 
| 8 | 
            +
              class Document
         | 
| 9 | 
            +
                attr_accessor :html, :filter, :metrics
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def self.load(url)
         | 
| 12 | 
            +
                  loader = Loader.new(url)
         | 
| 13 | 
            +
                  loader.load!
         | 
| 14 | 
            +
                  new(loader.html)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def initialize(html)
         | 
| 18 | 
            +
                  @html = html
         | 
| 19 | 
            +
                  @noko = Nokogiri::HTML.fragment(@html)
         | 
| 20 | 
            +
                  @metrics = Document::Metrics.new(html_for_analysis)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                # @return [String] HTML string ready for analysis
         | 
| 24 | 
            +
                def html_for_analysis
         | 
| 25 | 
            +
                  @filter ||= Document::Filter.new(@noko)
         | 
| 26 | 
            +
                  @filter.filtered_html!
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def score
         | 
| 30 | 
            +
                  @score ||= Score.new(metrics)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def inspect
         | 
| 34 | 
            +
                  "#<#{self.class}>"
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
            end
         | 
| @@ -0,0 +1,70 @@ | |
| 1 | 
            +
            module ReadmeScore
         | 
| 2 | 
            +
              class Document
         | 
| 3 | 
            +
                class Filter
         | 
| 4 | 
            +
                  SERVICES = ["travis-ci.org", "codeclimate.com", "gemnasium.com", "cocoadocs.org", "readme-score-api.herokuapp.com"]
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(noko_or_html)
         | 
| 7 | 
            +
                    @noko = Util.to_noko(noko_or_html, true)
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def filtered_html!
         | 
| 11 | 
            +
                    remove_license!
         | 
| 12 | 
            +
                    remove_contact!
         | 
| 13 | 
            +
                    remove_service_images!
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    @noko.to_s
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def remove_license!
         | 
| 19 | 
            +
                    remove_heading_sections_named("license")
         | 
| 20 | 
            +
                    remove_heading_sections_named("licensing")
         | 
| 21 | 
            +
                    remove_heading_sections_named("copyright")
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def remove_contact!
         | 
| 25 | 
            +
                    remove_heading_sections_named("contact")
         | 
| 26 | 
            +
                    remove_heading_sections_named("author")
         | 
| 27 | 
            +
                    remove_heading_sections_named("credits")
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  def remove_service_images!
         | 
| 31 | 
            +
                    SERVICES.each {|service|
         | 
| 32 | 
            +
                      remove_anchor_images_containing_url(service)
         | 
| 33 | 
            +
                    }
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  private
         | 
| 37 | 
            +
                    def remove_heading_sections_named(prefix)
         | 
| 38 | 
            +
                      any_hit = false
         | 
| 39 | 
            +
                      selectors = (1..5).map {|i| "h#{i}"}
         | 
| 40 | 
            +
                      selectors.each { |h|
         | 
| 41 | 
            +
                        @noko.search(h).each { |heading|
         | 
| 42 | 
            +
                          if heading.content.downcase == prefix
         | 
| 43 | 
            +
                            # hit - remove everything until the next heading
         | 
| 44 | 
            +
                            while sibling = heading.next_sibling
         | 
| 45 | 
            +
                              if sibling.name.downcase.start_with?(heading.name)
         | 
| 46 | 
            +
                                break
         | 
| 47 | 
            +
                              else
         | 
| 48 | 
            +
                                sibling.remove
         | 
| 49 | 
            +
                              end
         | 
| 50 | 
            +
                            end
         | 
| 51 | 
            +
                            heading.remove
         | 
| 52 | 
            +
                            any_hit = true
         | 
| 53 | 
            +
                            break
         | 
| 54 | 
            +
                          end
         | 
| 55 | 
            +
                        }
         | 
| 56 | 
            +
                      }
         | 
| 57 | 
            +
                      any_hit
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    def remove_anchor_images_containing_url(url_fragment)
         | 
| 61 | 
            +
                      @noko.search('a').each {|a|
         | 
| 62 | 
            +
                        href = a['href']
         | 
| 63 | 
            +
                        if href && href.downcase.include?(url_fragment.downcase)
         | 
| 64 | 
            +
                          a.remove unless a.search('img').empty?
         | 
| 65 | 
            +
                        end
         | 
| 66 | 
            +
                      }
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
            end
         | 
| @@ -0,0 +1,88 @@ | |
| 1 | 
            +
            require 'uri/http'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'readme-score/document/loader/github_readme_finder'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module ReadmeScore
         | 
| 6 | 
            +
              class Document
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                class Loader
         | 
| 9 | 
            +
                  MARKDOWN_EXTENSIONS = %w{md mdown markdown}
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  attr_accessor :request, :markdown
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def self.github_repo_name(url)
         | 
| 14 | 
            +
                    uri = URI.parse(url)
         | 
| 15 | 
            +
                    return nil unless ["github.com", "www.github.com"].include?(uri.host)
         | 
| 16 | 
            +
                    path_components = uri.path.split("/")
         | 
| 17 | 
            +
                    return nil if path_components.reject(&:empty?).count != 2
         | 
| 18 | 
            +
                    path_components[-2..-1].join("/")
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def self.is_github_repo_slug?(possible_repo)
         | 
| 22 | 
            +
                    !!(/^(\w|-)+\/(\w|-|\.)+$/.match(possible_repo))
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def self.is_url?(possible_url)
         | 
| 26 | 
            +
                    !!(/https?:\/\//.match(possible_url))
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def self.markdown_url?(url)
         | 
| 30 | 
            +
                    MARKDOWN_EXTENSIONS.select {|ext| url.downcase.end_with?(".#{ext}")}.any?
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  attr_accessor :response
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def initialize(url)
         | 
| 36 | 
            +
                    @url = url
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def github_repo_name
         | 
| 40 | 
            +
                    Loader.github_repo_name(@url)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def load!
         | 
| 44 | 
            +
                    if github_repo_name
         | 
| 45 | 
            +
                      if ReadmeScore.use_github_api?
         | 
| 46 | 
            +
                        load_via_github_api!
         | 
| 47 | 
            +
                      else
         | 
| 48 | 
            +
                        # take a guess at the raw file name
         | 
| 49 | 
            +
                        load_via_github_approximation!
         | 
| 50 | 
            +
                      end
         | 
| 51 | 
            +
                    else
         | 
| 52 | 
            +
                      @markdown = Loader.markdown_url?(@url)
         | 
| 53 | 
            +
                      @response ||= Unirest.get @url
         | 
| 54 | 
            +
                    end
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def load_via_github_api!
         | 
| 58 | 
            +
                    @markdown = false
         | 
| 59 | 
            +
                    @response ||= OpenStruct.new.tap {|o|
         | 
| 60 | 
            +
                      @@client ||= Octokit::Client.new(access_token: ReadmeScore.github_api_token)
         | 
| 61 | 
            +
                      o.body = @@client.readme(github_repo_name, :accept => 'application/vnd.github.html').force_encoding("UTF-8")
         | 
| 62 | 
            +
                    }
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def load_via_github_approximation!
         | 
| 66 | 
            +
                    @github_approximation_url ||= GithubReadmeFinder.new(url).find_url
         | 
| 67 | 
            +
                    @markdown = Loader.markdown_url?(@github_approximation_url)
         | 
| 68 | 
            +
                    @response ||= Unirest.get @github_approximation_url
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def html
         | 
| 72 | 
            +
                    if markdown?
         | 
| 73 | 
            +
                      parse_markdown(@response.body)
         | 
| 74 | 
            +
                    else
         | 
| 75 | 
            +
                      @response.body
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  def parse_markdown(markdown)
         | 
| 80 | 
            +
                    Parser.new(@response.body).to_html
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  def markdown?
         | 
| 84 | 
            +
                    @markdown == true
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
              end
         | 
| 88 | 
            +
            end
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            require 'net/http'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module ReadmeScore
         | 
| 4 | 
            +
              class Document
         | 
| 5 | 
            +
                class Loader
         | 
| 6 | 
            +
                  class GithubReadmeFinder
         | 
| 7 | 
            +
                    POSSIBLE_README_FILES = %w{README.md readme.md README readme ReadMe ReadMe.md}
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    def initialize(github_repo_url)
         | 
| 10 | 
            +
                      @repo_url = github_repo_url
         | 
| 11 | 
            +
                    end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def find_url
         | 
| 14 | 
            +
                      uri = URI.parse(@repo_url)
         | 
| 15 | 
            +
                      uri.scheme = "https"
         | 
| 16 | 
            +
                      uri.host = "raw.githubusercontent.com"
         | 
| 17 | 
            +
                      original_path = uri.path
         | 
| 18 | 
            +
                      readme_url = nil
         | 
| 19 | 
            +
                      POSSIBLE_README_FILES.each {|f|
         | 
| 20 | 
            +
                        uri.path = File.join(original_path, "master/#{f}")
         | 
| 21 | 
            +
                        readme_url = uri.to_s if reachable?(uri.to_s)
         | 
| 22 | 
            +
                        break if readme_url
         | 
| 23 | 
            +
                      }
         | 
| 24 | 
            +
                      readme_url
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    private
         | 
| 28 | 
            +
                      def reachable?(url)
         | 
| 29 | 
            +
                        begin
         | 
| 30 | 
            +
                          RestClient::Request.execute(method: :head, url: url)
         | 
| 31 | 
            +
                          true
         | 
| 32 | 
            +
                        rescue RestClient::Exception
         | 
| 33 | 
            +
                          false
         | 
| 34 | 
            +
                        end
         | 
| 35 | 
            +
                      end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
            end
         | 
| @@ -0,0 +1,125 @@ | |
| 1 | 
            +
            module ReadmeScore
         | 
| 2 | 
            +
              class Document
         | 
| 3 | 
            +
                class Metrics
         | 
| 4 | 
            +
                  EQUATION_METRICS = [
         | 
| 5 | 
            +
                    :cumulative_code_block_length
         | 
| 6 | 
            +
                  ]
         | 
| 7 | 
            +
                  def initialize(noko_or_html)
         | 
| 8 | 
            +
                    @noko = Util.to_noko(noko_or_html)
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def cumulative_code_block_length
         | 
| 12 | 
            +
                    all_code_blocks.inner_html.length
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def number_of_links
         | 
| 16 | 
            +
                    all_links.length
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def number_of_code_blocks
         | 
| 20 | 
            +
                    all_code_blocks.length
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def number_of_paragraphs
         | 
| 24 | 
            +
                    all_paragraphs.length
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def number_of_non_code_sections
         | 
| 28 | 
            +
                    (all_paragraphs + all_lists).length
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def code_block_to_paragraph_ratio
         | 
| 32 | 
            +
                    if number_of_paragraphs.to_f == 0.0
         | 
| 33 | 
            +
                      return 0
         | 
| 34 | 
            +
                    end
         | 
| 35 | 
            +
                    number_of_code_blocks.to_f / number_of_paragraphs.to_f
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  def number_of_internal_links
         | 
| 39 | 
            +
                    all_links.select {|a|
         | 
| 40 | 
            +
                      internal_link?(a)
         | 
| 41 | 
            +
                    }.count
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  def number_of_external_links
         | 
| 45 | 
            +
                    all_links.reject {|a|
         | 
| 46 | 
            +
                      internal_link?(a)
         | 
| 47 | 
            +
                    }.count
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def has_lists?
         | 
| 51 | 
            +
                    all_lists.length > 0
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  def has_images?
         | 
| 55 | 
            +
                    number_of_images > 0
         | 
| 56 | 
            +
                  end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  def number_of_images
         | 
| 59 | 
            +
                    all_images.count
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  def has_gifs?
         | 
| 63 | 
            +
                    number_of_gifs > 0
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def number_of_gifs
         | 
| 67 | 
            +
                    all_gifs.length
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def has_tables?
         | 
| 71 | 
            +
                    !all_tables.empty?
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  def inspect
         | 
| 75 | 
            +
                    "#<#{self.class}>"
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  private
         | 
| 79 | 
            +
                    def all_links
         | 
| 80 | 
            +
                      @noko.search('a')
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    def all_code_blocks
         | 
| 84 | 
            +
                      @noko.search('pre')
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                    def all_paragraphs
         | 
| 88 | 
            +
                      @noko.search('p')
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    def all_lists
         | 
| 92 | 
            +
                      @noko.search('ol') + @noko.search('ul')
         | 
| 93 | 
            +
                    end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    def all_images
         | 
| 96 | 
            +
                      @noko.search('img')
         | 
| 97 | 
            +
                    end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    def all_gifs
         | 
| 100 | 
            +
                      all_images.select {|a|
         | 
| 101 | 
            +
                        source_attributes = ['src', 'data-canonical-src']
         | 
| 102 | 
            +
                        source_attributes.map {|_attr|
         | 
| 103 | 
            +
                          a[_attr] && a[_attr].downcase.include?(".gif")
         | 
| 104 | 
            +
                        }.any?
         | 
| 105 | 
            +
                      }
         | 
| 106 | 
            +
                    end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                    def all_tables
         | 
| 109 | 
            +
                      @noko.search('table')
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                    def internal_link?(a)
         | 
| 113 | 
            +
                      external_prefixes = %w{http}
         | 
| 114 | 
            +
                      href = a['href'].downcase
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                      return true if href.include?("://github") || href.include?("github.io")
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                      external_prefixes.select {|prefix|
         | 
| 119 | 
            +
                        href.start_with?(prefix)
         | 
| 120 | 
            +
                      }.any?
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                end
         | 
| 124 | 
            +
              end
         | 
| 125 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            module ReadmeScore
         | 
| 2 | 
            +
              class Document
         | 
| 3 | 
            +
                class Parser
         | 
| 4 | 
            +
                  def initialize(markdown)
         | 
| 5 | 
            +
                    @markdown = markdown
         | 
| 6 | 
            +
                  end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def to_html
         | 
| 9 | 
            +
                    parser.render(@markdown)
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def parser
         | 
| 13 | 
            +
                    @@parser ||= Redcarpet::Markdown.new(
         | 
| 14 | 
            +
                      Redcarpet::Render::HTML,
         | 
| 15 | 
            +
                      no_intra_emphasis: true,
         | 
| 16 | 
            +
                      autolink: true,
         | 
| 17 | 
            +
                      fenced_code_blocks: true,
         | 
| 18 | 
            +
                      tables: true,
         | 
| 19 | 
            +
                      strikethrough: true
         | 
| 20 | 
            +
                    )
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,104 @@ | |
| 1 | 
            +
            module ReadmeScore
         | 
| 2 | 
            +
              class Document
         | 
| 3 | 
            +
                class Score
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  SCORE_METRICS = [
         | 
| 6 | 
            +
                    {
         | 
| 7 | 
            +
                      metric: :number_of_code_blocks,
         | 
| 8 | 
            +
                      description: "Number of code blocks",
         | 
| 9 | 
            +
                      value_per: 5,
         | 
| 10 | 
            +
                      max: 40
         | 
| 11 | 
            +
                    },
         | 
| 12 | 
            +
                    {
         | 
| 13 | 
            +
                      metric: :number_of_non_code_sections,
         | 
| 14 | 
            +
                      description: "Number of non-code sections",
         | 
| 15 | 
            +
                      value_per: 5,
         | 
| 16 | 
            +
                      max: 30
         | 
| 17 | 
            +
                    },
         | 
| 18 | 
            +
                    {
         | 
| 19 | 
            +
                      metric: :has_lists?,
         | 
| 20 | 
            +
                      description: "Has any lists?",
         | 
| 21 | 
            +
                      value: 10
         | 
| 22 | 
            +
                    },
         | 
| 23 | 
            +
                    {
         | 
| 24 | 
            +
                      metric: :number_of_images,
         | 
| 25 | 
            +
                      description: "Number of images",
         | 
| 26 | 
            +
                      value_per: 5,
         | 
| 27 | 
            +
                      max: 15
         | 
| 28 | 
            +
                    },
         | 
| 29 | 
            +
                    {
         | 
| 30 | 
            +
                      metric: :number_of_gifs,
         | 
| 31 | 
            +
                      description: "Number of GIFs",
         | 
| 32 | 
            +
                      value_per: 5,
         | 
| 33 | 
            +
                      max: 15
         | 
| 34 | 
            +
                    },
         | 
| 35 | 
            +
                    {
         | 
| 36 | 
            +
                      metric: :cumulative_code_block_length,
         | 
| 37 | 
            +
                      description: "Amount of code",
         | 
| 38 | 
            +
                      value_per: 0.0009475244447271192,
         | 
| 39 | 
            +
                      max: 10
         | 
| 40 | 
            +
                    },
         | 
| 41 | 
            +
                    {
         | 
| 42 | 
            +
                      metric_name: :low_code_block_penalty,
         | 
| 43 | 
            +
                      description: "Penalty for lack of code blocks",
         | 
| 44 | 
            +
                      metric: :number_of_code_blocks,
         | 
| 45 | 
            +
                      if_less_than: 3,
         | 
| 46 | 
            +
                      value: -10
         | 
| 47 | 
            +
                    }
         | 
| 48 | 
            +
                  ]
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  attr_accessor :metrics
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  def initialize(metrics)
         | 
| 53 | 
            +
                    @metrics = metrics
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def score_breakdown(as_description = false)
         | 
| 57 | 
            +
                    breakdown = {}
         | 
| 58 | 
            +
                    SCORE_METRICS.each { |h|
         | 
| 59 | 
            +
                      metric_option = OpenStruct.new(h)
         | 
| 60 | 
            +
                      metric_name = metric_option.metric_name || metric_option.metric
         | 
| 61 | 
            +
                      metric_score_value = 0
         | 
| 62 | 
            +
                      # points for each occurance
         | 
| 63 | 
            +
                      if metric_option.value_per
         | 
| 64 | 
            +
                        metric_score_value = [metrics.send(metric_option.metric) * metric_option.value_per, metric_option.max].min
         | 
| 65 | 
            +
                      elsif metric_option.if_less_than
         | 
| 66 | 
            +
                        if metrics.send(metric_option.metric) < metric_option.if_less_than
         | 
| 67 | 
            +
                          metric_score_value = metric_option.value
         | 
| 68 | 
            +
                        end
         | 
| 69 | 
            +
                      else
         | 
| 70 | 
            +
                        metric_score_value = metrics.send(metric_option.metric) ? metric_option.value : 0
         | 
| 71 | 
            +
                      end
         | 
| 72 | 
            +
                      if as_description
         | 
| 73 | 
            +
                        breakdown[metric_option.description] = [metric_score_value, metric_option.max || metric_option.value]
         | 
| 74 | 
            +
                      else
         | 
| 75 | 
            +
                        breakdown[metric_name] = metric_score_value
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
                    }
         | 
| 78 | 
            +
                    breakdown
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
                  alias_method :breakdown, :score_breakdown
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  def human_breakdown
         | 
| 83 | 
            +
                    score_breakdown(true)
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  def total_score
         | 
| 87 | 
            +
                    score = 0
         | 
| 88 | 
            +
                    score_breakdown.each {|metric, points|
         | 
| 89 | 
            +
                      score += points.to_i
         | 
| 90 | 
            +
                    }
         | 
| 91 | 
            +
                    [[score, 100].min, 0].max
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
                  alias_method :to_i, :total_score
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  def to_f
         | 
| 96 | 
            +
                    to_i.to_f
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  def inspect
         | 
| 100 | 
            +
                    "#<#{self.class} - #{total_score}>"
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
            end
         |