markdown_ruby_documentation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +4 -0
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +134 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +7 -0
  12. data/lib/github-markdown.css +656 -0
  13. data/lib/markdown_ruby_documentation.rb +29 -0
  14. data/lib/markdown_ruby_documentation/constants_presenter.rb +38 -0
  15. data/lib/markdown_ruby_documentation/default_erb_methods.rb +11 -0
  16. data/lib/markdown_ruby_documentation/generate.rb +107 -0
  17. data/lib/markdown_ruby_documentation/git_hub_link.rb +74 -0
  18. data/lib/markdown_ruby_documentation/git_hub_project.rb +29 -0
  19. data/lib/markdown_ruby_documentation/instance_to_class_methods.rb +44 -0
  20. data/lib/markdown_ruby_documentation/markdown_presenter.rb +36 -0
  21. data/lib/markdown_ruby_documentation/method.rb +101 -0
  22. data/lib/markdown_ruby_documentation/method/class_method.rb +13 -0
  23. data/lib/markdown_ruby_documentation/method/instance_method.rb +11 -0
  24. data/lib/markdown_ruby_documentation/method/null_method.rb +28 -0
  25. data/lib/markdown_ruby_documentation/method_linker.rb +41 -0
  26. data/lib/markdown_ruby_documentation/print_method_source.rb +19 -0
  27. data/lib/markdown_ruby_documentation/reject_blank_methods.rb +9 -0
  28. data/lib/markdown_ruby_documentation/summary.rb +32 -0
  29. data/lib/markdown_ruby_documentation/template_parser.rb +348 -0
  30. data/lib/markdown_ruby_documentation/version.rb +3 -0
  31. data/lib/markdown_ruby_documentation/write_markdown_to_disk.rb +29 -0
  32. data/markdown_ruby_documenation.gemspec +30 -0
  33. metadata +162 -0
@@ -0,0 +1,13 @@
1
+ module MarkdownRubyDocumentation
2
+ class ClassMethod < Method
3
+
4
+ def self.type_symbol
5
+ "."
6
+ end
7
+
8
+ def type
9
+ :method
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module MarkdownRubyDocumentation
2
+ class InstanceMethod < Method
3
+ def self.type_symbol
4
+ "#"
5
+ end
6
+
7
+ def type
8
+ :instance_method
9
+ end
10
+ end
11
+ end
@@ -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,9 @@
1
+ module MarkdownRubyDocumentation
2
+ module RejectBlankMethod
3
+ def self.call(methods)
4
+ methods.reject do |_, hash|
5
+ hash[:text].nil? || hash[:text].blank?
6
+ end
7
+ end
8
+ end
9
+ 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