markdown-helpers 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/markdownh +20 -0
- data/lib/markdown-helpers/doc_tree_builder.rb +104 -0
- data/lib/markdown-helpers/link_checker.rb +162 -0
- data/lib/markdown-helpers.rb +2 -0
- metadata +76 -0
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
|
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: []
|