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
|