lazydoc 0.2.0 → 0.3.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.
@@ -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