markdown_ruby_documentation 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 +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
|