markdown-helpers 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7d195ce1a339bdbd1d82427afd58d0634bd9af51
4
+ data.tar.gz: 35b15149f335408471568967cdbc2a178a814690
5
+ SHA512:
6
+ metadata.gz: 6af36d716dc6b80b54ff3b0742a51a0034750f913c1e08f8ab9feef2fd17cb234f9fde2d5317a46f988ec738224a873c1c41eb9aa22412f54f652c9f81951368
7
+ data.tar.gz: 74b256a38c0a59e61ac6b9cba1905d386ef84451a0f30555689da285f7d519371a6d221b448040e7649af79ad7b8c29bbe441eee5c0b19e31a9dad36e2656d6a
data/bin/markdownh ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ require 'markdown-helpers'
3
+ require 'thor'
4
+
5
+ # 'markdownh' CLI
6
+ class MarkdownH < Thor
7
+ desc 'check_links CONFIG', 'check for broken links'
8
+ def check_links(config)
9
+ LinkChecker.new(config).check
10
+ end
11
+
12
+ desc 'generate_index CONFIG', 'generate a markdown link tree'
13
+ def generate_index(config)
14
+ doc_builder = DocBuilder.new(config)
15
+ doc_builder.generate_index
16
+ doc_builder.write
17
+ end
18
+ end
19
+
20
+ MarkdownH.start(ARGV)
@@ -0,0 +1,104 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+
4
+ # Class for building an index tree from
5
+ # erb and directory root
6
+ class DocBuilder
7
+ def initialize(config_file)
8
+ @config = YAML.load_file(config_file)
9
+ @config['base_header'] ||= ''
10
+ @config['base_level'] ||= 1
11
+ if @config['base_level'] < 1
12
+ puts ':base_level must be greater than 0'
13
+ exit
14
+ end
15
+ @config['ignore'] ||= []
16
+ @doc_index = ''
17
+ end
18
+
19
+ # recursive call on directories starting at @doc_directory
20
+ # writes indented list block for each directory
21
+ def generate_index(
22
+ options = {
23
+ directory: @config['doc_directory'],
24
+ level: @config['base_level'],
25
+ header_level: @config['base_header']
26
+ }
27
+ )
28
+ sub_directories = Dir.entries(options[:directory])
29
+ directory_last = true
30
+ sub_directories.each do |filename|
31
+ next if ignore_file?(filename, options[:directory])
32
+ path = File.join(options[:directory], filename)
33
+ if File.directory?(path)
34
+ # case where two directories are back to back
35
+ options[:level] = options[:level] - 1 if directory_last && options[:level] > 0
36
+ concat_directory(path, options[:level] + 1, options[:header_level])
37
+ directory_last = true
38
+ else
39
+ concat_file(path, options[:level])
40
+ directory_last = false
41
+ end
42
+ end
43
+ end
44
+
45
+ def write
46
+ template = File.read(@config['erb_path'])
47
+ erb = ERB.new(template)
48
+ File.write(@config['output_file'], erb.result(binding))
49
+ end
50
+
51
+ private
52
+
53
+ # format and concat directory
54
+ def concat_directory(path, level, header_level)
55
+ indent = ' ' * (level)
56
+ description = File.basename(path)
57
+ .split('_') # split on underscores
58
+ .map(&:capitalize) # capitalize each word
59
+ .join(' ') # make it one string
60
+ repo_path = path.sub(File.dirname(@config['output_file']), '.')
61
+ # return if nothing below directory
62
+ return if empty_directory?(path)
63
+ @doc_index << "\n#{indent}#{header_level} [#{description}](#{repo_path})\n"
64
+ generate_index(directory: path, level: level, header_level: header_level + '#')
65
+ end
66
+
67
+ # format and concat file
68
+ def concat_file(path, level)
69
+ indent = ' ' * (level)
70
+ line = File.open(path, &:readline)
71
+ repo_path = path.sub(File.dirname(@config['output_file']), '.')
72
+ description = line.sub(/^#* */, '').chomp
73
+ @doc_index << "#{indent}- [#{description}](#{repo_path})\n"
74
+ end
75
+
76
+ # empty == no subdirectories with .md files
77
+ def empty_directory?(directory)
78
+ Dir.entries(directory).each do |filename|
79
+ next if ignore_file?(filename, directory)
80
+ return false if filename.include?('.md')
81
+ path = File.join(directory, filename)
82
+ return false if File.directory?(path) && !empty_directory?(path)
83
+ end
84
+ return true
85
+ end
86
+
87
+ def ignore_file?(filename, directory)
88
+ path = File.join(directory, filename)
89
+ # current or parent dir
90
+ if filename == '.' || filename == '..'
91
+ return true
92
+ # explicit ignore in config
93
+ elsif @config['ignore'].include?(filename)
94
+ return true
95
+ # symlinks, because of infinite loops on dir's
96
+ elsif File.symlink?(path)
97
+ return true
98
+ # is it a non-md file?
99
+ elsif File.file?(path) && !filename.include?('.md')
100
+ return true
101
+ end
102
+ return false
103
+ end
104
+ end
@@ -0,0 +1,162 @@
1
+ require 'net/http'
2
+ require 'octokit'
3
+ require 'yaml'
4
+
5
+ # Class to help verify local and external links
6
+ class LinkChecker
7
+ HTTP_ERRORS = [
8
+ EOFError,
9
+ Errno::ECONNRESET,
10
+ Errno::EINVAL,
11
+ Net::HTTPBadResponse,
12
+ Net::HTTPHeaderSyntaxError,
13
+ Net::ProtocolError,
14
+ Timeout::Error,
15
+ SocketError
16
+ ]
17
+ def initialize(config_file)
18
+ @config = YAML.load_file(config_file)
19
+ unless @config['include'] && @config['include'].any?
20
+ puts "Must declare files in 'include' of config".red
21
+ exit 1
22
+ end
23
+ @broken_links = []
24
+ end
25
+
26
+ def check
27
+ @config['include'].each do |included|
28
+ included_object = IncludeItem.new(included)
29
+ included_object.check_paths
30
+ @broken_links << included_object.broken_links.flatten
31
+ end
32
+ @broken_links.flatten!
33
+ if @broken_links.any?
34
+ @broken_links.each do |link_hash|
35
+ output = "Broken link: '#{link_hash['link']}'\n" \
36
+ " in file: '#{link_hash[:file]}'\n" \
37
+ " on line '#{link_hash[:line_number]}'"
38
+ puts output.red
39
+ end
40
+ exit(1)
41
+ else
42
+ puts 'No broken links :)'
43
+ end
44
+ end
45
+
46
+ # Helper class for dealing with each included item in config
47
+ class IncludeItem
48
+ attr_reader :broken_links
49
+
50
+ def initialize(config)
51
+ @config = config
52
+ @broken_links = []
53
+ @config['replacements'] ||= {}
54
+ @config['private_github'] ||= false
55
+ return unless @config['private_github']
56
+ unless ENV['GITHUB_OAUTH_TOKEN']
57
+ puts "Must specify 'GITHUB_OAUTH_TOKEN' env variable to use 'private_github' config option"
58
+ exit(1)
59
+ end
60
+ @github_client = Octokit::Client.new(access_token: ENV['GITHUB_OAUTH_TOKEN'])
61
+ end
62
+
63
+ # iterate over list of paths
64
+ def check_paths
65
+ @config['paths'].each do |path|
66
+ check_path(path)
67
+ end
68
+ end
69
+
70
+ # glob each path
71
+ def check_path(path)
72
+ files = Dir.glob(path).select { |f| File.file?(f) } # only want files
73
+ files.each do |filename|
74
+ check_file(filename)
75
+ end
76
+ end
77
+
78
+ def check_file(filename)
79
+ file = File.open(filename, 'r')
80
+ file.each_with_index do |line, index|
81
+ return false if @config['exclude_comment'] && line.include?(@config['exclude_comment'])
82
+ links = line.scan(@config['pattern']).flatten
83
+ check_links(links, filename, index + 1) if links.any?
84
+ end
85
+ end
86
+
87
+ def check_links(links, file, line_number)
88
+ links.each do |link|
89
+ link = replace_values(link)
90
+ link = link.sub(/#.*$/, '') # scrub the anchor
91
+ next if check_link(link, file)
92
+ @broken_links << {
93
+ 'link' => link,
94
+ :file => file,
95
+ :line_number => line_number
96
+ }
97
+ end
98
+ end
99
+
100
+ def check_link(link, file)
101
+ if link.match(%r{https://github.com}) && @config['private_github']
102
+ check_github_link(link)
103
+ elsif link.match(/^http.*/)
104
+ check_external_link(link)
105
+ elsif link.match(/^#.*/)
106
+ check_section_link(link, file)
107
+ else
108
+ check_internal_link(link, file)
109
+ end
110
+ end
111
+
112
+ def check_github_link(link)
113
+ repo = link.match(%r{github\.com/([^/]*/[^/]*)/.*})[1]
114
+ path = link.match(%r{github\.com/.*/.*/blob/[^/]*/(.*)})[1]
115
+ begin
116
+ @github_client.rate_limit
117
+ @github_client.contents(repo, path: path)
118
+ rescue Octokit::NotFound
119
+ return false
120
+ end
121
+ true
122
+ end
123
+
124
+ def check_external_link(link)
125
+ uri = URI(link)
126
+ begin
127
+ response = Net::HTTP.get_response(uri)
128
+ rescue *HTTP_ERRORS
129
+ puts "Error querying #{link}".red
130
+ return false
131
+ end
132
+ response.is_a?(Net::HTTPSuccess) ? true : false
133
+ end
134
+
135
+ def check_section_link(link, file)
136
+ section = link.sub(%r{/#*/}, '').split('-').each(&:capitalize).join(' ')
137
+ File.readlines(file).grep(/#{section}/i).size > 0
138
+ end
139
+
140
+ def check_internal_link(link, file)
141
+ File.exist?(File.join(File.dirname(file), link)) ? true : false
142
+ end
143
+
144
+ def replace_values(link)
145
+ @config['replacements'].each do |pair|
146
+ link = link.sub(pair['match'], pair['value'])
147
+ end
148
+ link
149
+ end
150
+ end
151
+ end
152
+
153
+ # Add colorize to the String class
154
+ class String
155
+ def colorize(color_code)
156
+ "\e[#{color_code}m#{self}\e[0m"
157
+ end
158
+
159
+ def red
160
+ colorize(31)
161
+ end
162
+ end
@@ -0,0 +1,2 @@
1
+ require_relative './markdown-helpers/link_checker.rb'
2
+ require_relative './markdown-helpers/doc_tree_builder.rb'
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: markdown-helpers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mike
8
+ - Wood
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-10-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: octokit
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '3.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '3.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: thor
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.19'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.19'
42
+ description: some helpers for working with github markdown
43
+ email: michael.wood@optimizely.com
44
+ executables:
45
+ - markdownh
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - bin/markdownh
50
+ - lib/markdown-helpers.rb
51
+ - lib/markdown-helpers/doc_tree_builder.rb
52
+ - lib/markdown-helpers/link_checker.rb
53
+ homepage: https://rubygems.org/gems/markdown-helpers
54
+ licenses: []
55
+ metadata: {}
56
+ post_install_message:
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 2.2.2
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: some helpers for working with github markdown
76
+ test_files: []