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.
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