tomdoc 0.1.0

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