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