markdown_ruby_documentation 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/.gitignore +9 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +134 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/github-markdown.css +656 -0
- data/lib/markdown_ruby_documentation.rb +29 -0
- data/lib/markdown_ruby_documentation/constants_presenter.rb +38 -0
- data/lib/markdown_ruby_documentation/default_erb_methods.rb +11 -0
- data/lib/markdown_ruby_documentation/generate.rb +107 -0
- data/lib/markdown_ruby_documentation/git_hub_link.rb +74 -0
- data/lib/markdown_ruby_documentation/git_hub_project.rb +29 -0
- data/lib/markdown_ruby_documentation/instance_to_class_methods.rb +44 -0
- data/lib/markdown_ruby_documentation/markdown_presenter.rb +36 -0
- data/lib/markdown_ruby_documentation/method.rb +101 -0
- data/lib/markdown_ruby_documentation/method/class_method.rb +13 -0
- data/lib/markdown_ruby_documentation/method/instance_method.rb +11 -0
- data/lib/markdown_ruby_documentation/method/null_method.rb +28 -0
- data/lib/markdown_ruby_documentation/method_linker.rb +41 -0
- data/lib/markdown_ruby_documentation/print_method_source.rb +19 -0
- data/lib/markdown_ruby_documentation/reject_blank_methods.rb +9 -0
- data/lib/markdown_ruby_documentation/summary.rb +32 -0
- data/lib/markdown_ruby_documentation/template_parser.rb +348 -0
- data/lib/markdown_ruby_documentation/version.rb +3 -0
- data/lib/markdown_ruby_documentation/write_markdown_to_disk.rb +29 -0
- data/markdown_ruby_documenation.gemspec +30 -0
- metadata +162 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
module MarkdownRubyDocumentation
|
2
|
+
class NullMethod < Method
|
3
|
+
|
4
|
+
def self.type_symbol
|
5
|
+
""
|
6
|
+
end
|
7
|
+
|
8
|
+
def name
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def type
|
13
|
+
raise "Does not have a type"
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_proc
|
17
|
+
raise "Not convertible to a proc"
|
18
|
+
end
|
19
|
+
|
20
|
+
def context
|
21
|
+
method_reference.constantize
|
22
|
+
end
|
23
|
+
|
24
|
+
def context_name
|
25
|
+
method_reference
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module MarkdownRubyDocumentation
|
2
|
+
class MethodLinker
|
3
|
+
|
4
|
+
attr_reader :text, :section_key, :root_path
|
5
|
+
|
6
|
+
def initialize(section_key:, root_path:)
|
7
|
+
|
8
|
+
@section_key = section_key
|
9
|
+
@root_path = root_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(text=nil)
|
13
|
+
@text = text
|
14
|
+
generate
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def generate
|
20
|
+
text.scan(/(?<!\^`)`{1}([\w:_\.#?]*[^`\n])\`/).each do |r|
|
21
|
+
r = r.first
|
22
|
+
if r =~ /(\w*::\w*)+#[\w|\?]+/ # constant with an instance method
|
23
|
+
parts = r.split("#")
|
24
|
+
meths = parts[-1]
|
25
|
+
const = parts[0]
|
26
|
+
str = "[#{meths.titleize}](#{root_path}#{const.underscore.gsub("/", "-")}##{md_id meths})"
|
27
|
+
elsif r =~ /\w*::\w*/ # is constant
|
28
|
+
str = "[#{r.gsub("::", " ").titleize}](#{root_path}#{md_id r})"
|
29
|
+
else # a method
|
30
|
+
str = "[#{r.titleize}](##{md_id r})"
|
31
|
+
end
|
32
|
+
@text = text.gsub("^`#{r}`", str)
|
33
|
+
end
|
34
|
+
text
|
35
|
+
end
|
36
|
+
|
37
|
+
def md_id(str)
|
38
|
+
str.downcase.dasherize.delete(" ").delete('?')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module MarkdownRubyDocumentation
|
2
|
+
class PrintMethodSource
|
3
|
+
def initialize(method:)
|
4
|
+
@method_object = method
|
5
|
+
end
|
6
|
+
|
7
|
+
def print
|
8
|
+
method_object.to_proc
|
9
|
+
.source
|
10
|
+
.split("\n")[1..-2]
|
11
|
+
.map(&:lstrip)
|
12
|
+
.join("\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :method_object
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Summary
|
2
|
+
attr_reader :erb_methods_class, :subject
|
3
|
+
|
4
|
+
def initialize(subject:, erb_methods_class:)
|
5
|
+
@subject = subject
|
6
|
+
@erb_methods_class = erb_methods_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def title
|
10
|
+
ancestors = subject.ancestors.select do |klass|
|
11
|
+
klass.is_a?(Class) && ![BasicObject, Object, subject].include?(klass)
|
12
|
+
end
|
13
|
+
[format_class(subject), *ancestors.map { |a| create_link(a) }].join(" < ")
|
14
|
+
end
|
15
|
+
|
16
|
+
def summary
|
17
|
+
descendants = ObjectSpace.each_object(Class).select { |klass| klass < subject && !klass.name.include?("InstanceToClassMethods") }
|
18
|
+
|
19
|
+
descendants_links = descendants.map { |d| create_link(d) }.join(", ")
|
20
|
+
"Descendants: #{descendants_links}" if descendants.count >= 1
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def create_link(klass)
|
26
|
+
erb_methods_class.link_to_markdown(klass.to_s, title: format_class(klass))
|
27
|
+
end
|
28
|
+
|
29
|
+
def format_class(klass)
|
30
|
+
klass.name.titleize.split("/").last
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,348 @@
|
|
1
|
+
module MarkdownRubyDocumentation
|
2
|
+
class TemplateParser
|
3
|
+
|
4
|
+
def initialize(ruby_class, methods)
|
5
|
+
@ruby_class = ruby_class
|
6
|
+
@methods = methods.map { |method| method.is_a?(Symbol) ? InstanceMethod.new("##{method}", context: ruby_class) : method }
|
7
|
+
@erb_methods_class = erb_methods_class
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_hash(*args)
|
11
|
+
parser
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :call, :to_hash
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
IGNORE_METHODS = %w(
|
19
|
+
initialize
|
20
|
+
inherited
|
21
|
+
included
|
22
|
+
extended
|
23
|
+
prepended
|
24
|
+
method_added
|
25
|
+
method_undefined
|
26
|
+
alias_method
|
27
|
+
append_features
|
28
|
+
attr
|
29
|
+
attr_accessor
|
30
|
+
attr_reader
|
31
|
+
attr_writer
|
32
|
+
define_method
|
33
|
+
extend_object
|
34
|
+
method_removed
|
35
|
+
module_function
|
36
|
+
prepend_features
|
37
|
+
private
|
38
|
+
protected
|
39
|
+
public
|
40
|
+
refine
|
41
|
+
remove_const
|
42
|
+
remove_method
|
43
|
+
undef_method
|
44
|
+
using
|
45
|
+
)
|
46
|
+
|
47
|
+
attr_reader :ruby_class, :methods, :erb_methods_class
|
48
|
+
|
49
|
+
def parser
|
50
|
+
@parser ||= methods.each_with_object({}) do |method, hash|
|
51
|
+
begin
|
52
|
+
value = parse_erb(insert_method_name(strip_comment_hash(extract_dsl_comment_from_method(method)), method), method)
|
53
|
+
rescue MethodSource::SourceNotFoundError => e
|
54
|
+
value = false
|
55
|
+
puts e.message unless IGNORE_METHODS.any? { |im| e.message.include? im }
|
56
|
+
end
|
57
|
+
if value
|
58
|
+
hash[method.name] = { text: value, method_object: method }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module CommentMacros
|
64
|
+
# @param [String] str
|
65
|
+
# @example
|
66
|
+
# @return [String] of any comments proceeding a method def
|
67
|
+
def print_raw_comment(str)
|
68
|
+
strip_comment_hash(ruby_class_meth_comment(Method.create(str, context: ruby_class)))
|
69
|
+
end
|
70
|
+
|
71
|
+
# @param [String] str
|
72
|
+
# @example
|
73
|
+
# @return [String]
|
74
|
+
def print_mark_doc_from(str)
|
75
|
+
method = Method.create(str, context: ruby_class)
|
76
|
+
parse_erb(insert_method_name(extract_dsl_comment(print_raw_comment(str)), method), method)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @param [String] str
|
80
|
+
# @example
|
81
|
+
# @return [Object] anything that the evaluated method would return.
|
82
|
+
def eval_method(str)
|
83
|
+
case (method = Method.create(str, context: ruby_class))
|
84
|
+
when ClassMethod
|
85
|
+
method.context.public_send(method.name)
|
86
|
+
when InstanceMethod
|
87
|
+
InstanceToClassMethods.new(method: method).eval_instance_method
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# @param [String] input
|
92
|
+
# @return [String] the source of a method block is returned as text.
|
93
|
+
def print_method_source(input)
|
94
|
+
method = Method.create(input.dup, context: ruby_class)
|
95
|
+
PrintMethodSource.new(method: method).print
|
96
|
+
end
|
97
|
+
|
98
|
+
def git_hub_method_url(input)
|
99
|
+
method = Method.create(input.dup, context: ruby_class)
|
100
|
+
GitHubLink::MethodUrl.new(subject: method.context, method_object: method)
|
101
|
+
end
|
102
|
+
|
103
|
+
def git_hub_file_url(file_path)
|
104
|
+
if file_path.include?("/")
|
105
|
+
GitHubLink::FileUrl.new(file_path: file_path)
|
106
|
+
else
|
107
|
+
const = Object.const_get(file_path)
|
108
|
+
a_method = const.public_instance_methods.first
|
109
|
+
git_hub_method_url("#{file_path}##{a_method}")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def pretty_code(source_code, humanize: true)
|
114
|
+
source_code = ternary_to_if_else(source_code)
|
115
|
+
source_code = pretty_early_return(source_code)
|
116
|
+
source_code.gsub!(/@[a-z][a-z0-9_]+ \|\|=?\s/, "") # @memoized_vars ||=
|
117
|
+
source_code.gsub!(":", '')
|
118
|
+
source_code.gsub!("&&", "and")
|
119
|
+
source_code.gsub!(">=", "is greater than or equal to")
|
120
|
+
source_code.gsub!("<=", "is less than or equal to")
|
121
|
+
source_code.gsub!(" < ", " is less than ")
|
122
|
+
source_code.gsub!(" > ", " is greater than ")
|
123
|
+
source_code.gsub!(" == ", " Equal to ")
|
124
|
+
source_code.gsub!("nil?", "is missing?")
|
125
|
+
source_code.gsub!("elsif", "else if")
|
126
|
+
source_code.gsub!("||", "or")
|
127
|
+
source_code.gsub!(/([0-9][0-9_]+[0-9]+)/) do |match|
|
128
|
+
match.gsub("_", ",")
|
129
|
+
end
|
130
|
+
if humanize
|
131
|
+
source_code.gsub!(/["']?[a-z_A-Z?!0-9]*["']?/) do |s|
|
132
|
+
if s.include?("_") && !(/["'][a-z_A-Z?!0-9]*["']/ =~ s)
|
133
|
+
"'#{s.humanize}'"
|
134
|
+
else
|
135
|
+
s.humanize(capitalize: false)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
source_code
|
140
|
+
end
|
141
|
+
|
142
|
+
def readable_ruby_numbers(source_code, &block)
|
143
|
+
source_code.gsub(/([0-9][0-9_]+[0-9]+)/) do |match|
|
144
|
+
value = eval(match)
|
145
|
+
if block_given?
|
146
|
+
block.call(value)
|
147
|
+
else
|
148
|
+
ActiveSupport::NumberHelper.number_to_delimited(value)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def link_local_methods_from_pretty_code(pretty_code, include: nil)
|
154
|
+
pretty_code.gsub(/([`][a-zA-Z_0-9!?\s]+[`])/) do |match|
|
155
|
+
include_code(include, match) do
|
156
|
+
variables_as_local_links match.underscore.gsub(" ", "_").gsub(/`/, "")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def include_code(include, text, &do_action)
|
162
|
+
if include.nil? || (include.present? && include.include?(remove_quotes(text)))
|
163
|
+
do_action.call(text)
|
164
|
+
else
|
165
|
+
text
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
private :include_code
|
170
|
+
|
171
|
+
def convert_early_return_to_if_else(source_code)
|
172
|
+
source_code.gsub(/(.+) if (.+)/, "if \\2\n\\1\nend")
|
173
|
+
end
|
174
|
+
|
175
|
+
def pretty_early_return(source_code)
|
176
|
+
source_code.gsub(/return (unless|if)/, 'return nothing \1')
|
177
|
+
end
|
178
|
+
|
179
|
+
def ternary_to_if_else(ternary)
|
180
|
+
ternary.gsub(/(.*) \? (.*) \: (.*)/, "if \\1\n\\2\nelse\n\\3\nend")
|
181
|
+
end
|
182
|
+
|
183
|
+
def format_link(title, link_ref)
|
184
|
+
path, anchor = *link_ref.to_s.split("#")
|
185
|
+
formatted_path = [path, anchor.try!(:dasherize).try!(:delete, "?")].compact.join("#")
|
186
|
+
"[#{title}](#{formatted_path})"
|
187
|
+
end
|
188
|
+
|
189
|
+
def title_from_link(link_ref)
|
190
|
+
[link_ref.split("/").last.split("#").last.to_s.humanize, link_ref]
|
191
|
+
end
|
192
|
+
|
193
|
+
def link_to_markdown(klass, title:)
|
194
|
+
return super if defined? super
|
195
|
+
raise "Client needs to define MarkdownRubyDocumentation::TemplateParser::CommentMacros#link_to_markdown"
|
196
|
+
end
|
197
|
+
|
198
|
+
def method_as_local_links(ruby_source)
|
199
|
+
ruby_source.gsub(/(\b(?<!['"])[a-z_][a-z_0-9?!]*(?!['"]))/) do |match|
|
200
|
+
is_a_method_on_ruby_class?(match) ? "^`#{match}`" : match
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
alias_method(:variables_as_local_links, :method_as_local_links)
|
205
|
+
|
206
|
+
def quoted_strings_as_local_links(text, include: nil)
|
207
|
+
text.gsub(/(['|"][a-zA-Z_0-9!?\s]+['|"])/) do |match|
|
208
|
+
include_code(include, match) do
|
209
|
+
"^`#{remove_quotes(match).underscore.gsub(" ", "_")}`"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def constants_with_name_and_value(ruby_source)
|
215
|
+
ruby_source.gsub(/([A-Z]+[A-Z_0-9]+)/) do |match|
|
216
|
+
value = ruby_class.const_get(match)
|
217
|
+
"`#{match} => #{value.inspect}`"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def ruby_to_markdown(ruby_source)
|
222
|
+
ruby_source = readable_ruby_numbers(ruby_source)
|
223
|
+
ruby_source = pretty_early_return(ruby_source)
|
224
|
+
ruby_source = convert_early_return_to_if_else(ruby_source)
|
225
|
+
ruby_source = ternary_to_if_else(ruby_source)
|
226
|
+
ruby_source = ruby_if_statement_to_md(ruby_source)
|
227
|
+
ruby_source = ruby_case_statement_to_md(ruby_source)
|
228
|
+
remove_end_keyword(ruby_source)
|
229
|
+
end
|
230
|
+
|
231
|
+
def remove_end_keyword(ruby_source)
|
232
|
+
ruby_source.gsub!(/^[\s]*end\n?/, "")
|
233
|
+
end
|
234
|
+
|
235
|
+
def ruby_if_statement_to_md(ruby_source)
|
236
|
+
ruby_source.gsub!(/else if(.*)/, "* __ElseIf__\\1\n__Then__")
|
237
|
+
ruby_source.gsub!(/elsif(.*)/, "* __ElseIf__\\1\n__Then__")
|
238
|
+
ruby_source.gsub!(/if(.*)/, "* __If__\\1\n__Then__")
|
239
|
+
ruby_source.gsub!("else", "* __Else__")
|
240
|
+
ruby_source
|
241
|
+
end
|
242
|
+
|
243
|
+
def ruby_case_statement_to_md(ruby_source)
|
244
|
+
ruby_source.gsub!(/case(.*)/, "* __Given__\\1")
|
245
|
+
ruby_source.gsub!(/when(.*)/, "* __When__\\1\n__Then__")
|
246
|
+
ruby_source.gsub!("else", "* __Else__")
|
247
|
+
ruby_source
|
248
|
+
end
|
249
|
+
|
250
|
+
def hash_to_markdown_table(hash, key_name:, value_name:)
|
251
|
+
key_max_length = [hash.keys.group_by(&:size).max.first, key_name.size + 1].max
|
252
|
+
value_max_length = [hash.values.group_by(&:size).max.first, value_name.size + 1].max
|
253
|
+
header = markdown_table_header([[key_name, key_max_length+2], [value_name, value_max_length+2]])
|
254
|
+
rows = hash.map { |key, value| "| #{key.to_s.ljust(key_max_length)} | #{value.to_s.ljust(value_max_length)}|" }.join("\n")
|
255
|
+
[header, rows].join("\n")
|
256
|
+
end
|
257
|
+
|
258
|
+
def array_to_markdown_table(array, key_name:)
|
259
|
+
key_max_length = [array.group_by(&:size).max.first, key_name.size + 1].max
|
260
|
+
header = markdown_table_header([[key_name, key_max_length+3]])
|
261
|
+
rows = array.map { |key| "| #{key.to_s.ljust(key_max_length)} |" }.join("\n")
|
262
|
+
[header, rows].join("\n")
|
263
|
+
end
|
264
|
+
|
265
|
+
def markdown_table_header(array_headers)
|
266
|
+
parts = array_headers.map { |header, pad_length=0| " #{header.ljust(pad_length-1)}" }
|
267
|
+
bar = parts.map(&:length).map { |length| ("-" * (length)) }.join("|")
|
268
|
+
bar[-1] = "|"
|
269
|
+
header = parts.join("|")
|
270
|
+
header[-1] = "|"
|
271
|
+
[("|" + header), ("|" + bar)].join("\n")
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
|
276
|
+
def is_a_method_on_ruby_class?(method)
|
277
|
+
[*ruby_class.public_instance_methods, *ruby_class.private_instance_methods].include?(remove_quotes(method).to_sym)
|
278
|
+
end
|
279
|
+
|
280
|
+
def remove_quotes(string)
|
281
|
+
string.gsub(/['|"]/, "")
|
282
|
+
end
|
283
|
+
|
284
|
+
def insert_method_name(string, method)
|
285
|
+
string.gsub("__method__", "'#{method.to_s}'")
|
286
|
+
end
|
287
|
+
|
288
|
+
def parse_erb(str, method)
|
289
|
+
filename, lineno = ruby_class_meth_source_location(method)
|
290
|
+
|
291
|
+
ruby_class.module_eval(<<-RUBY, __FILE__, __LINE__+1)
|
292
|
+
def self.get_binding
|
293
|
+
self.send(:binding)
|
294
|
+
end
|
295
|
+
RUBY
|
296
|
+
ruby_class.extend(CommentMacros)
|
297
|
+
erb = ERB.new(str, nil, "-")
|
298
|
+
erb.result(ruby_class.get_binding)
|
299
|
+
rescue => e
|
300
|
+
raise e.class, e.message, ["#{filename}:#{lineno}:in `#{method.name}'", *e.backtrace]
|
301
|
+
end
|
302
|
+
|
303
|
+
def strip_comment_hash(str)
|
304
|
+
str.gsub(/^#[\s]?/, "")
|
305
|
+
end
|
306
|
+
|
307
|
+
def ruby_class_meth_comment(method)
|
308
|
+
method.context.public_send(method.type, method.name).comment
|
309
|
+
|
310
|
+
rescue MethodSource::SourceNotFoundError => e
|
311
|
+
raise e.class, "#{ method.context}#{method.type_symbol}#{method.name}, \n#{e.message}"
|
312
|
+
end
|
313
|
+
|
314
|
+
def ruby_class_meth_source_location(method)
|
315
|
+
method.context.public_send(method.type, method.name).source_location
|
316
|
+
end
|
317
|
+
|
318
|
+
def extract_dsl_comment(comment_string)
|
319
|
+
if (v = when_start_and_end(comment_string))
|
320
|
+
v
|
321
|
+
elsif (x = when_only_start(comment_string))
|
322
|
+
x << "[//]: # (This method has no mark_end)"
|
323
|
+
else
|
324
|
+
""
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def when_start_and_end(comment_string)
|
329
|
+
v = /#{START_TOKEN}\n((.|\n)*)#{END_TOKEN}/.match(comment_string)
|
330
|
+
v.try!(:captures).try!(:first)
|
331
|
+
end
|
332
|
+
|
333
|
+
def when_only_start(comment_string)
|
334
|
+
v = /#{START_TOKEN}\n((.|\n)*)/.match(comment_string)
|
335
|
+
v.try!(:captures).try!(:first)
|
336
|
+
end
|
337
|
+
|
338
|
+
def extract_dsl_comment_from_method(method)
|
339
|
+
extract_dsl_comment strip_comment_hash(ruby_class_meth_comment(method))
|
340
|
+
end
|
341
|
+
|
342
|
+
def ruby_class
|
343
|
+
@ruby_class || self
|
344
|
+
end
|
345
|
+
end
|
346
|
+
include CommentMacros
|
347
|
+
end
|
348
|
+
end
|