lazydoc 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,128 +2,179 @@ require 'lazydoc/comment'
2
2
  require 'lazydoc/method'
3
3
 
4
4
  module Lazydoc
5
+ autoload(:Attributes, 'lazydoc/attributes')
6
+ autoload(:Arguments, 'lazydoc/arguments')
7
+ autoload(:Subject, 'lazydoc/subject')
8
+ autoload(:Trailer, 'lazydoc/trailer')
5
9
 
6
- # A Document tracks constant attributes and code comments for a particular
7
- # source file. Documents may be assigned a default_const_name to be used
10
+ # A regexp matching an attribute start or end. After a match:
11
+ #
12
+ # $1:: const_name
13
+ # $3:: key
14
+ # $4:: end flag
15
+ #
16
+ ATTRIBUTE_REGEXP = /([A-Z][A-z]*(::[A-Z][A-z]*)*)?::([a-z_]+)(-?)/
17
+
18
+ # A regexp matching constants from the ATTRIBUTE_REGEXP leader
19
+ CONSTANT_REGEXP = /#.*?([A-Z][A-z]*(::[A-Z][A-z]*)*)?$/
20
+
21
+ # A regexp matching a caller line, to extract the calling file
22
+ # and line number. After a match:
23
+ #
24
+ # $1:: file
25
+ # $3:: line number (as a string, obviously)
26
+ #
27
+ # Note that line numbers in caller start at 1, not 0.
28
+ CALLER_REGEXP = /^(([A-z]:)?[^:]+):(\d+)/
29
+
30
+ # A Document resolves constant attributes and code comments for a particular
31
+ # source file. Documents may be assigned a default_const_name to be used
8
32
  # when a constant attribute does not specify a constant.
9
33
  #
10
- # # KeyWithConst::key value a
34
+ # # Const::Name::key value a
11
35
  # # ::key value b
12
36
  #
13
- # doc = Document.new(__FILE__, 'DefaultConst')
37
+ # doc = Document.new(__FILE__, 'Default')
14
38
  # doc.resolve
15
- # doc['KeyWithConst']['key'].value # => 'value a'
16
- # doc['DefaultConst']['key'].value # => 'value b'
17
39
  #
40
+ # Document['Const::Name']['key'].value # => 'value a'
41
+ # Document['Default']['key'].value # => 'value b'
42
+ #
43
+ # As shown in the example, constant attibutes for all documents are cached in
44
+ # the class-level const_attrs hash and are normally consumed through Document
45
+ # itself.
18
46
  class Document
47
+ class << self
48
+
49
+ # A nested hash of (const_name, (key, comment)) pairs tracking
50
+ # the constant attributes assigned to a constant name.
51
+ def const_attrs
52
+ @const_attrs ||= {}
53
+ end
54
+
55
+ # Returns the hash of (key, comment) pairs for const_name stored
56
+ # in const_attrs. If no such hash exists, one will be created.
57
+ def [](const_name)
58
+ const_attrs[const_name] ||= {}
59
+ end
60
+
61
+ # Scans the string or StringScanner for attributes matching the key
62
+ # (keys may be patterns; they are incorporated into a regexp).
63
+ # Regions delimited by the stop and start keys <tt>:::-</tt> and
64
+ # <tt>:::+</tt> are skipped. Yields each (const_name, key, value)
65
+ # triplet to the block.
66
+ #
67
+ # str = %Q{
68
+ # # Name::Space::key value
69
+ # # ::alt alt_value
70
+ # #
71
+ # # Ignored::Attribute::not_matched value
72
+ # # :::-
73
+ # # Also::Ignored::key value
74
+ # # :::+
75
+ # # Another::key another value
76
+ #
77
+ # Ignored::key value
78
+ # }
79
+ #
80
+ # results = []
81
+ # Document.scan(str, 'key|alt') do |const_name, key, value|
82
+ # results << [const_name, key, value]
83
+ # end
84
+ #
85
+ # results
86
+ # # => [
87
+ # # ['Name::Space', 'key', 'value'],
88
+ # # ['', 'alt', 'alt_value'],
89
+ # # ['Another', 'key', 'another value']]
90
+ #
91
+ # Returns the StringScanner used during scanning.
92
+ def scan(str, key) # :yields: const_name, key, value
93
+ scanner = case str
94
+ when StringScanner then str
95
+ when String then StringScanner.new(str)
96
+ else raise TypeError, "can't convert #{str.class} into StringScanner or String"
97
+ end
98
+
99
+ regexp = /^(.*?)::(:-|#{key})/
100
+ while !scanner.eos?
101
+ break if scanner.skip_until(regexp) == nil
102
+
103
+ if scanner[2] == ":-"
104
+ scanner.skip_until(/:::\+/)
105
+ else
106
+ next unless scanner[1] =~ CONSTANT_REGEXP
107
+ key = scanner[2]
108
+ yield($1.to_s, key, scanner.matched.strip) if scanner.scan(/[ \r\t].*$|$/)
109
+ end
110
+ end
111
+
112
+ scanner
113
+ end
114
+ end
19
115
 
20
116
  # The source file for self, used during resolve
21
117
  attr_reader :source_file
22
-
23
- # An array of Comment objects identifying lines
24
- # resolved or to-be-resolved
25
- attr_reader :comments
26
-
27
- # A hash of [const_name, attributes] pairs tracking the constant
28
- # attributes resolved or to-be-resolved for self. Attributes
29
- # are hashes of [key, comment] pairs.
30
- attr_reader :const_attrs
31
-
118
+
32
119
  # The default constant name used when no constant name
33
120
  # is specified for a constant attribute
34
121
  attr_reader :default_const_name
35
122
 
123
+ # An array of Comment objects registered to self
124
+ attr_reader :comments
125
+
36
126
  # Flag indicating whether or not self has been resolved
37
127
  attr_accessor :resolved
38
-
39
- def initialize(source_file=nil, default_const_name='')
128
+
129
+ def initialize(source_file=nil, default_const_name=nil)
40
130
  self.source_file = source_file
41
131
  @default_const_name = default_const_name
42
132
  @comments = []
43
- @const_attrs = {}
44
133
  @resolved = false
45
- self.reset
46
134
  end
47
-
48
- # Resets self by clearing const_attrs, comments, and setting
49
- # resolved to false. Generally NOT recommended as this
50
- # clears any work you've done registering lines; to simply
51
- # allow resolve to re-scan a document, manually set
52
- # resolved to false.
53
- def reset
54
- @const_attrs.clear
55
- @comments.clear
56
- @resolved = false
57
- self
135
+
136
+ # Returns the attributes for the specified const_name. If an empty
137
+ # const_name ('') is specified, and a default_const_name is set,
138
+ # the default_const_name will be used instead.
139
+ def [](const_name)
140
+ const_name = default_const_name if default_const_name && const_name == ''
141
+ Document[const_name]
58
142
  end
59
143
 
60
- # Sets the source file for self. Expands the source file path if necessary.
144
+ # Expands and sets the source file for self.
61
145
  def source_file=(source_file)
62
146
  @source_file = source_file == nil ? nil : File.expand_path(source_file)
63
147
  end
64
-
65
- # Sets the default_const_name for self. Any const_attrs assigned to
66
- # the previous default will be removed and merged with those already
67
- # assigned to the new default.
68
- def default_const_name=(const_name)
69
- self[const_name].merge!(const_attrs.delete(@default_const_name) || {})
70
- @default_const_name = const_name
71
- end
72
-
73
- # Returns the attributes for the specified const_name.
74
- def [](const_name)
75
- const_attrs[const_name] ||= {}
76
- end
77
148
 
78
- # Returns an array of the const_names in self with at
79
- # least one attribute.
80
- def const_names
81
- names = []
82
- const_attrs.each_pair do |const_name, attrs|
83
- names << const_name unless attrs.empty?
84
- end
85
- names
86
- end
87
-
88
- # Register the specified line number to self. Register
89
- # may take an integer or a regexp for late-evaluation.
90
- # See Comment#resolve for more details.
149
+ # Registers the specified line number to self. Register may take an
150
+ # integer or a regexp for dynamic evaluation. See Comment#resolve for
151
+ # more details.
91
152
  #
92
- # Returns a comment_class instance corresponding to the line.
153
+ # Returns the newly registered comment.
93
154
  def register(line_number, comment_class=Comment)
94
- comment = comments.find {|c| c.class == comment_class && c.line_number == line_number }
95
-
96
- if comment == nil
97
- comment = comment_class.new(line_number)
98
- comments << comment
99
- end
100
-
155
+ comment = comment_class.new(line_number, self)
156
+ comments << comment
101
157
  comment
102
158
  end
103
159
 
104
- # Registers a regexp matching the first method by the specified name.
105
- def register_method(method_name, comment_class=Method)
106
- register(Method.method_regexp(method_name), comment_class)
107
- end
108
-
109
160
  # Registers the next comment.
110
161
  #
111
162
  # lazydoc = Document.new(__FILE__)
112
163
  #
113
- # lazydoc.register___
164
+ # c = lazydoc.register___
114
165
  # # this is the comment
115
166
  # # that is registered
116
167
  # def method(a,b,c)
117
168
  # end
118
169
  #
119
170
  # lazydoc.resolve
120
- # m = lazydoc.comments[0]
121
- # m.subject # => "def method(a,b,c)"
122
- # m.to_s # => "this is the comment that is registered"
171
+ #
172
+ # c.subject # => "def method(a,b,c)"
173
+ # c.comment # => "this is the comment that is registered"
123
174
  #
124
175
  def register___(comment_class=Comment, caller_index=0)
125
176
  caller[caller_index] =~ CALLER_REGEXP
126
- block = lambda do |lines|
177
+ block = lambda do |scanner, lines|
127
178
  n = $3.to_i
128
179
  n += 1 while lines[n] =~ /^\s*(#.*)?$/
129
180
  n
@@ -131,41 +182,66 @@ module Lazydoc
131
182
  register(block, comment_class)
132
183
  end
133
184
 
134
- # Scans str for constant attributes and adds them to to self. Code
135
- # comments are also resolved against str. If no str is specified,
136
- # the contents of source_file are used instead.
185
+ # Scans str for constant attributes and adds them to Document.const_attrs.
186
+ # Comments registered with self are also resolved against str. If no str
187
+ # is specified, the contents of source_file are used instead.
137
188
  #
138
- # Resolve does nothing if resolved == true. Returns true if str
139
- # was resolved, or false otherwise.
140
- def resolve(str=nil)
141
- return(false) if resolved
142
-
189
+ # Resolve does nothing if resolved == true, unless force is also specified.
190
+ # Returns true if str was resolved, or false otherwise.
191
+ def resolve(str=nil, force=false)
192
+ return false if resolved && !force
193
+ @resolved = true
194
+
143
195
  str = File.read(source_file) if str == nil
144
- Lazydoc.parse(str) do |const_name, key, comment|
145
- const_name = default_const_name if const_name.empty?
146
- self[const_name][key] = comment
147
- end
148
-
149
- unless comments.empty?
150
- lines = str.split(/\r?\n/)
151
- comments.each do |comment|
152
- comment.resolve(lines)
196
+ lines = Utils.split_lines(str)
197
+ scanner = Utils.convert_to_scanner(str)
198
+
199
+ Document.scan(scanner, '[a-z_]+') do |const_name, key, value|
200
+ # get or initialize the comment that will be parsed
201
+ comment = (self[const_name][key] ||= Subject.new(nil, self))
202
+
203
+ # skip non-comment constant attributes
204
+ next unless comment.kind_of?(Comment)
205
+
206
+ # parse the comment
207
+ comment.parse_down(scanner, lines) do |line|
208
+ if line =~ ATTRIBUTE_REGEXP
209
+ # rewind to capture the next attribute unless an end is specified.
210
+ scanner.unscan unless $4 == '-' && $3 == key && $1.to_s == const_name
211
+ true
212
+ else false
213
+ end
153
214
  end
215
+
216
+ # set the subject
217
+ comment.subject = value
154
218
  end
155
-
156
- @resolved = true
219
+
220
+ # resolve registered comments
221
+ comments.each do |comment|
222
+ comment.parse_up(scanner, lines)
223
+
224
+ n = comment.line_number
225
+ comment.subject = n.kind_of?(Integer) ? lines[n] : nil
226
+ end
227
+
228
+ true
157
229
  end
158
-
159
- def to_hash
230
+
231
+ # Summarizes constant attributes registered to self by collecting them
232
+ # into a nested hash of (const_name, (key, comment)) pairs. A block
233
+ # may be provided to collect values from the comments; each comment is
234
+ # yielded to the block and the return stored in it's place.
235
+ def summarize
160
236
  const_hash = {}
161
- const_attrs.each_pair do |const_name, attributes|
237
+ Document.const_attrs.each_pair do |const_name, attributes|
162
238
  next if attributes.empty?
163
-
164
- attr_hash = {}
239
+
240
+ const_hash[const_name] = attr_hash = {}
165
241
  attributes.each_pair do |key, comment|
242
+ next unless comment.document == self
166
243
  attr_hash[key] = (block_given? ? yield(comment) : comment)
167
244
  end
168
- const_hash[const_name] = attr_hash
169
245
  end
170
246
  const_hash
171
247
  end
@@ -10,88 +10,14 @@ module Lazydoc
10
10
  # end
11
11
  # }
12
12
  #
13
- # m = Method.parse(sample_method)
13
+ # m = Document.new.register(2, Method)
14
+ # m.resolve(sample_method)
14
15
  # m.method_name # => "method_name"
15
16
  # m.arguments # => ["a", "b='default'", "&c"]
16
17
  # m.trailer # => "trailing comment"
17
18
  # m.to_s # => "This is the comment body"
18
19
  #
19
20
  class Method < Comment
20
- class << self
21
-
22
- # Generates a regexp matching a standard definition of the
23
- # specified method.
24
- #
25
- # m = Method.method_regexp("method")
26
- # m =~ "def method" # => true
27
- # m =~ "def method(with, args, &block)" # => true
28
- # m !~ "def some_other_method" # => true
29
- #
30
- def method_regexp(method_name)
31
- /^\s*def\s+#{method_name}(\W|$)/
32
- end
33
-
34
- # Parses an argument string (anything following the method name in a
35
- # standard method definition, including parenthesis/comments/default
36
- # values etc) into an array of strings.
37
- #
38
- # Method.parse_args("(a, b='default', &block)")
39
- # # => ["a", "b='default'", "&block"]
40
- #
41
- # Note the %-syntax for strings and arrays is not fully supported,
42
- # ie %w, %Q, %q, etc. may not parse correctly. The same is true
43
- # for multiline argument strings.
44
- def parse_args(str)
45
- scanner = case str
46
- when StringScanner then str
47
- when String then StringScanner.new(str)
48
- else raise TypeError, "can't convert #{str.class} into StringScanner or String"
49
- end
50
- str = scanner.string
51
-
52
- # skip whitespace and leading LPAREN
53
- scanner.skip(/\s*\(?\s*/)
54
-
55
- args = []
56
- brakets = braces = parens = 0
57
- start = scanner.pos
58
- broke = while scanner.skip(/.*?['"#,\(\)\{\}\[\]]/)
59
- pos = scanner.pos - 1
60
-
61
- case str[pos]
62
- when ?,,nil
63
- # skip if in brakets, braces, or parenthesis
64
- next if parens > 0 || brakets > 0 || braces > 0
65
-
66
- # ok, found an arg
67
- args << str[start, pos-start].strip
68
- start = pos + 1
69
-
70
- when ?# then break(true) # break on a comment
71
- when ?' then skip_quote(scanner, /'/) # parse over quoted strings
72
- when ?" then skip_quote(scanner, /"/) # parse over double-quoted string
73
-
74
- when ?( then parens += 1 # for brakets, braces, and parenthesis
75
- when ?) # simply track the nesting EXCEPT for
76
- break(true) if parens == 0 # RPAREN. If the closing parenthesis
77
- parens -= 1 # is found, break.
78
- when ?[ then braces += 1
79
- when ?] then braces -= 1
80
- when ?{ then brakets += 1
81
- when ?} then brakets -= 1
82
- end
83
- end
84
-
85
- # parse out the final arg. if the loop broke (ie
86
- # a comment or the closing parenthesis was found)
87
- # then the end position is determined by the
88
- # scanner, otherwise take all that remains
89
- pos = broke ? scanner.pos-1 : str.length
90
- args << str[start, pos-start].strip
91
-
92
- args
93
- end
94
- end
95
21
 
96
22
  # Matches a standard method definition. After the match:
97
23
  #
@@ -120,7 +46,7 @@ module Lazydoc
120
46
  end
121
47
 
122
48
  @method_name = $1
123
- @arguments = Method.parse_args($2)
49
+ @arguments = scan_args($2)
124
50
 
125
51
  super
126
52
  end
@@ -0,0 +1,19 @@
1
+ module Lazydoc
2
+
3
+ # A special type of self-resolving Comment whose to_s returns the
4
+ # subject, or an empty string if subject is nil.
5
+ #
6
+ # s = Subject.new
7
+ # s.subject = "subject string"
8
+ # s.to_s # => "subject string"
9
+ #
10
+ class Subject < Comment
11
+
12
+ # Self-resolves and returns subject, or an empty
13
+ # string if subject is nil.
14
+ def to_s
15
+ resolve
16
+ subject.to_s
17
+ end
18
+ end
19
+ end