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.
@@ -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