tomdoc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|