tomdoc 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.
- data/LICENSE +20 -0
- data/README.md +104 -0
- data/Rakefile +80 -0
- data/bin/tomdoc +6 -0
- data/lib/tomdoc.rb +25 -0
- data/lib/tomdoc/arg.rb +14 -0
- data/lib/tomdoc/cli.rb +150 -0
- data/lib/tomdoc/generator.rb +138 -0
- data/lib/tomdoc/generators/console.rb +68 -0
- data/lib/tomdoc/generators/html.rb +42 -0
- data/lib/tomdoc/method.rb +21 -0
- data/lib/tomdoc/scope.rb +46 -0
- data/lib/tomdoc/source_parser.rb +145 -0
- data/lib/tomdoc/tomdoc.rb +133 -0
- data/lib/tomdoc/version.rb +3 -0
- data/man/tomdoc.5 +320 -0
- data/man/tomdoc.5.html +285 -0
- data/man/tomdoc.5.ronn +203 -0
- data/test/console_generator_test.rb +20 -0
- data/test/fixtures/chimney.rb +711 -0
- data/test/fixtures/multiplex.rb +47 -0
- data/test/fixtures/simple.rb +10 -0
- data/test/generator_test.rb +47 -0
- data/test/helper.rb +21 -0
- data/test/html_generator_test.rb +18 -0
- data/test/source_parser_test.rb +66 -0
- data/test/tomdoc_parser_test.rb +127 -0
- metadata +143 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
module TomDoc
|
2
|
+
module Generators
|
3
|
+
class Console < Generator
|
4
|
+
def highlight(text)
|
5
|
+
pygments(text, '-l', 'ruby')
|
6
|
+
end
|
7
|
+
|
8
|
+
def write_scope_header(scope, prefix = '')
|
9
|
+
return if scope.tomdoc.to_s.empty?
|
10
|
+
write_method(scope, prefix)
|
11
|
+
end
|
12
|
+
|
13
|
+
def write_method(method, prefix = '')
|
14
|
+
write '-' * 80
|
15
|
+
write "#{prefix}#{method.name}#{args(method)}".bold, ''
|
16
|
+
write format_comment(method.tomdoc)
|
17
|
+
end
|
18
|
+
|
19
|
+
def args(method)
|
20
|
+
return '' if !method.respond_to?(:args)
|
21
|
+
if method.args.any?
|
22
|
+
'(' + method.args.join(', ') + ')'
|
23
|
+
else
|
24
|
+
''
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def format_comment(comment)
|
29
|
+
comment = comment.to_s
|
30
|
+
|
31
|
+
# Strip leading comments
|
32
|
+
comment.gsub!(/^# ?/, '')
|
33
|
+
|
34
|
+
# Example code
|
35
|
+
comment.gsub!(/(\s*Examples\s*(.+?)\s*Returns)/m) do
|
36
|
+
$1.sub($2, highlight($2))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Param list
|
40
|
+
comment.gsub!(/^(\s*(\w+) +- )/) do
|
41
|
+
param = $2
|
42
|
+
$1.sub(param, param.green)
|
43
|
+
end
|
44
|
+
|
45
|
+
# true/false/nil
|
46
|
+
comment.gsub!(/(true|false|nil)/, '\1'.magenta)
|
47
|
+
|
48
|
+
# Strings
|
49
|
+
comment.gsub!(/('.+?')/, '\1'.yellow)
|
50
|
+
comment.gsub!(/(".+?")/, '\1'.yellow)
|
51
|
+
|
52
|
+
# Symbols
|
53
|
+
comment.gsub!(/(\s+:\w+)/, '\1'.red)
|
54
|
+
|
55
|
+
# Constants
|
56
|
+
comment.gsub!(/(([A-Z]\w+(::)?)+)/) do
|
57
|
+
if constant?($1.strip)
|
58
|
+
$1.split('::').map { |part| part.cyan }.join('::')
|
59
|
+
else
|
60
|
+
$1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
comment
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module TomDoc
|
2
|
+
module Generators
|
3
|
+
class HTML < Generator
|
4
|
+
def highlight(text)
|
5
|
+
pygments(text, '-l', 'ruby', '-f', 'html')
|
6
|
+
end
|
7
|
+
|
8
|
+
def write_scope_header(scope, prefix)
|
9
|
+
end
|
10
|
+
|
11
|
+
def write_scope_footer(scope, prefix)
|
12
|
+
end
|
13
|
+
|
14
|
+
def write_class_methods(scope, prefix)
|
15
|
+
out = '<ul>'
|
16
|
+
out << super.to_s
|
17
|
+
write out
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_instance_methods(scope, prefix)
|
21
|
+
out = ''
|
22
|
+
out << super.to_s
|
23
|
+
out << '</ul>'
|
24
|
+
write out
|
25
|
+
end
|
26
|
+
|
27
|
+
def write_method(method, prefix = '')
|
28
|
+
if method.args.any?
|
29
|
+
args = '(' + method.args.join(', ') + ')'
|
30
|
+
end
|
31
|
+
out = '<li>'
|
32
|
+
out << "<b>#{prefix}#{method.to_s}#{args}</b>"
|
33
|
+
|
34
|
+
out << '<pre>'
|
35
|
+
out << method.tomdoc.tomdoc
|
36
|
+
out << '</pre>'
|
37
|
+
|
38
|
+
out << '</li>'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TomDoc
|
2
|
+
# A Method can be instance or class level.
|
3
|
+
class Method
|
4
|
+
attr_accessor :name, :comment, :args
|
5
|
+
|
6
|
+
def initialize(name, comment = '', args = [])
|
7
|
+
@name = name
|
8
|
+
@comment = comment
|
9
|
+
@args = args || []
|
10
|
+
end
|
11
|
+
alias_method :to_s, :name
|
12
|
+
|
13
|
+
def tomdoc
|
14
|
+
@tomdoc ||= TomDoc.new(@comment)
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
"#{name}(#{args.join(', ')})"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/tomdoc/scope.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module TomDoc
|
2
|
+
# A Scope is a Module or Class.
|
3
|
+
# It may contain other scopes.
|
4
|
+
class Scope
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_accessor :name, :comment, :instance_methods, :class_methods
|
8
|
+
attr_accessor :scopes
|
9
|
+
|
10
|
+
def initialize(name, comment = '', instance_methods = [], class_methods = [])
|
11
|
+
@name = name
|
12
|
+
@comment = comment
|
13
|
+
@instance_methods = instance_methods
|
14
|
+
@class_methods = class_methods
|
15
|
+
@scopes = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def tomdoc
|
19
|
+
@tomdoc ||= TomDoc.new(@comment)
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](scope)
|
23
|
+
@scopes[scope]
|
24
|
+
end
|
25
|
+
|
26
|
+
def keys
|
27
|
+
@scopes.keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def each(&block)
|
31
|
+
@scopes.each(&block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
inspect
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
scopes = @scopes.keys.join(', ')
|
40
|
+
imethods = @instance_methods.inspect
|
41
|
+
cmethods = @class_methods.inspect
|
42
|
+
|
43
|
+
"<#{name} scopes:[#{scopes}] :#{cmethods}: ##{imethods}#>"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module TomDoc
|
2
|
+
class SourceParser
|
3
|
+
# Converts Ruby code into a data structure.
|
4
|
+
#
|
5
|
+
# text - A String of Ruby code.
|
6
|
+
#
|
7
|
+
# Returns a Hash with each key a namespace and each value another
|
8
|
+
# Hash or a TomDoc::Scope.
|
9
|
+
def self.parse(text)
|
10
|
+
new.parse(text)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :parser, :scopes, :options
|
14
|
+
|
15
|
+
# Each instance of SourceParser accumulates scopes with each
|
16
|
+
# parse, making it easy to parse an entire project in chunks but
|
17
|
+
# more difficult to parse disparate files in one go. Create
|
18
|
+
# separate instances for separate global scopes.
|
19
|
+
#
|
20
|
+
# Returns an instance of TomDoc::SourceParser
|
21
|
+
def initialize(options = {})
|
22
|
+
@options = {}
|
23
|
+
@parser = RubyParser.new
|
24
|
+
@scopes = {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Resets the state of the parser to a pristine one. Maintains options.
|
28
|
+
#
|
29
|
+
# Returns nothing.
|
30
|
+
def reset
|
31
|
+
initialize(@options)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Converts Ruby code into a data structure. Note that at the
|
35
|
+
# instance level scopes accumulate, which makes it easy to parse
|
36
|
+
# multiple files in a single project but harder to parse files
|
37
|
+
# that have no connection.
|
38
|
+
#
|
39
|
+
# text - A String of Ruby code.
|
40
|
+
#
|
41
|
+
# Examples
|
42
|
+
# @parser = TomDoc::SourceParser.new
|
43
|
+
# files.each do |file|
|
44
|
+
# @parser.parse(File.read(file))
|
45
|
+
# end
|
46
|
+
# pp @parser.scopes
|
47
|
+
#
|
48
|
+
# Returns a Hash with each key a namespace and each value another
|
49
|
+
# Hash or a TomDoc::Scope.
|
50
|
+
def parse(text)
|
51
|
+
process(tokenize(sexp(text)))
|
52
|
+
@scopes
|
53
|
+
end
|
54
|
+
|
55
|
+
# Converts Ruby sourcecode into an AST.
|
56
|
+
#
|
57
|
+
# text - A String of Ruby source.
|
58
|
+
#
|
59
|
+
# Returns a Sexp representing the AST.
|
60
|
+
def sexp(text)
|
61
|
+
@parser.parse(text)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Converts a tokenized Array of classes, modules, and methods into
|
65
|
+
# Scopes and Methods, adding them to the @scopes instance variable
|
66
|
+
# as it works.
|
67
|
+
#
|
68
|
+
# ast - Tokenized Array produced by calling `tokenize`.
|
69
|
+
# scope - An optional Scope object for nested classes or modules.
|
70
|
+
#
|
71
|
+
# Returns nothing.
|
72
|
+
def process(ast, scope = nil)
|
73
|
+
case Array(ast)[0]
|
74
|
+
when :module, :class
|
75
|
+
name = ast[1]
|
76
|
+
new_scope = Scope.new(name, ast[2])
|
77
|
+
|
78
|
+
if scope
|
79
|
+
scope.scopes[name] = new_scope
|
80
|
+
elsif @scopes[name]
|
81
|
+
new_scope = @scopes[name]
|
82
|
+
else
|
83
|
+
@scopes[name] = new_scope
|
84
|
+
end
|
85
|
+
|
86
|
+
process(ast[3], new_scope)
|
87
|
+
when :imethod
|
88
|
+
ast.shift
|
89
|
+
scope.instance_methods << Method.new(*ast)
|
90
|
+
when :cmethod
|
91
|
+
ast.shift
|
92
|
+
scope.class_methods << Method.new(*ast)
|
93
|
+
when Array
|
94
|
+
ast.map { |a| process(a, scope) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Converts a Ruby AST-style Sexp into an Array of more useful tokens.
|
99
|
+
#
|
100
|
+
# node - A Ruby AST Sexp or Array
|
101
|
+
#
|
102
|
+
# Examples
|
103
|
+
#
|
104
|
+
# [:module, :Math, "",
|
105
|
+
# [:class, :Multiplexer, "# Class Comment",
|
106
|
+
# [:cmethod,
|
107
|
+
# :multiplex, "# Class Method Comment", [:text]],
|
108
|
+
# [:imethod,
|
109
|
+
# :multiplex, "# Instance Method Comment", [:text, :count]]]]
|
110
|
+
#
|
111
|
+
# # In others words:
|
112
|
+
# # [ :type, :name, :comment, other ]
|
113
|
+
#
|
114
|
+
# Returns an Array in the above format.
|
115
|
+
def tokenize(node)
|
116
|
+
case Array(node)[0]
|
117
|
+
when :module
|
118
|
+
name = node[1]
|
119
|
+
[ :module, name, node.comments, tokenize(node[2]) ]
|
120
|
+
when :class
|
121
|
+
name = node[1]
|
122
|
+
[ :class, name, node.comments, tokenize(node[3]) ]
|
123
|
+
when :defn
|
124
|
+
name = node[1]
|
125
|
+
args = args_for_node(node[2])
|
126
|
+
[ :imethod, name, node.comments, args ]
|
127
|
+
when :defs
|
128
|
+
name = node[2]
|
129
|
+
args = args_for_node(node[3])
|
130
|
+
[ :cmethod, name, node.comments, args ]
|
131
|
+
when :block
|
132
|
+
tokenize(node[1..-1])
|
133
|
+
when :scope
|
134
|
+
tokenize(node[1])
|
135
|
+
when Array
|
136
|
+
node.map { |n| tokenize(n) }.compact
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Given a method sexp, returns an array of the args.
|
141
|
+
def args_for_node(node)
|
142
|
+
Array(node)[1..-1].select { |arg| arg.is_a? Symbol }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module TomDoc
|
2
|
+
class InvalidTomDoc < RuntimeError
|
3
|
+
def initialize(doc)
|
4
|
+
@doc = doc
|
5
|
+
end
|
6
|
+
|
7
|
+
def message
|
8
|
+
@doc
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@doc
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class TomDoc
|
17
|
+
attr_accessor :raw
|
18
|
+
|
19
|
+
def initialize(text)
|
20
|
+
@raw = text.to_s.strip
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
@raw
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.valid?(text)
|
28
|
+
new(text).valid?
|
29
|
+
end
|
30
|
+
|
31
|
+
def valid?
|
32
|
+
validate
|
33
|
+
rescue InvalidTomDoc
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate
|
38
|
+
if !raw.include?('Returns')
|
39
|
+
raise InvalidTomDoc.new("No `Returns' statement.")
|
40
|
+
end
|
41
|
+
|
42
|
+
if tomdoc.split("\n\n").size < 2
|
43
|
+
raise InvalidTomDoc.new("No description section found.")
|
44
|
+
end
|
45
|
+
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def tomdoc
|
50
|
+
clean = raw.split("\n").map do |line|
|
51
|
+
line =~ /^(\s*# ?)/ ? line.sub($1, '') : nil
|
52
|
+
end.compact.join("\n")
|
53
|
+
|
54
|
+
clean
|
55
|
+
end
|
56
|
+
|
57
|
+
def sections
|
58
|
+
tomdoc.split("\n\n")
|
59
|
+
end
|
60
|
+
|
61
|
+
def description
|
62
|
+
sections.first
|
63
|
+
end
|
64
|
+
|
65
|
+
def args
|
66
|
+
args = []
|
67
|
+
last_indent = nil
|
68
|
+
|
69
|
+
sections[1].split("\n").each do |line|
|
70
|
+
next if line.strip.empty?
|
71
|
+
indent = line.scan(/^\s*/)[0].to_s.size
|
72
|
+
|
73
|
+
if last_indent && indent > last_indent
|
74
|
+
args.last.description += line.squeeze(" ")
|
75
|
+
else
|
76
|
+
param, desc = line.split(" - ")
|
77
|
+
args << Arg.new(param.strip, desc.strip) if param && desc
|
78
|
+
end
|
79
|
+
|
80
|
+
last_indent = indent
|
81
|
+
end
|
82
|
+
|
83
|
+
args
|
84
|
+
end
|
85
|
+
|
86
|
+
def examples
|
87
|
+
if tomdoc =~ /(\s*Examples\s*(.+?)\s*(?:Returns|Raises))/m
|
88
|
+
$2.split("\n\n")
|
89
|
+
else
|
90
|
+
[]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def returns
|
95
|
+
if tomdoc =~ /^\s*(Returns.+)/m
|
96
|
+
lines = $1.split("\n")
|
97
|
+
statements = []
|
98
|
+
|
99
|
+
lines.each do |line|
|
100
|
+
next if line =~ /^\s*Raises/
|
101
|
+
if line =~ /^\s+/
|
102
|
+
statements.last << line.squeeze(' ')
|
103
|
+
else
|
104
|
+
statements << line
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
statements
|
109
|
+
else
|
110
|
+
[]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def raises
|
115
|
+
if tomdoc =~ /^\s*(Raises.+)/m
|
116
|
+
lines = $1.split("\n")
|
117
|
+
statements = []
|
118
|
+
|
119
|
+
lines.each do |line|
|
120
|
+
if line =~ /^\s+/
|
121
|
+
statements.last << line.squeeze(' ')
|
122
|
+
else
|
123
|
+
statements << line
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
statements
|
128
|
+
else
|
129
|
+
[]
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|