readme-score 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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