lazydoc 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.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008, Regents of the University of Colorado.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
+ software and associated documentation files (the "Software"), to deal in the Software
5
+ without restriction, including without limitation the rights to use, copy, modify, merge,
6
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7
+ to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or
10
+ substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
16
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,214 @@
1
+ = Lazydoc[http://tap.rubyforge.org/lazydoc]
2
+
3
+ Lazydoc lazily pulls documentation out of source files and makes it
4
+ available in code through lazy attributes. Lazydoc is used by the
5
+ Tap[http://tap.rubyforge.org] framework.
6
+
7
+ == Description
8
+
9
+ Lazydoc can find two types of documentation: constant attributes and
10
+ code comments. To illustrate, consider the following:
11
+
12
+ # Sample::key <value>
13
+ # This is the comment content. A content
14
+ # string can span multiple lines...
15
+ #
16
+ # code.is_allowed
17
+ # much.as_in RDoc
18
+ #
19
+ # and stops at the next non-comment
20
+ # line, the next constant attribute,
21
+ # or an end key
22
+ class Sample
23
+ extend Lazydoc::Attributes
24
+ self.source_file = __FILE__
25
+
26
+ lazy_attr :key
27
+
28
+ # comment content for a code comment
29
+ # may similarly span multiple lines
30
+ def method_one
31
+ end
32
+ end
33
+
34
+ When a lazy attribute is called, Lazydoc scans <tt>source_file</tt> for
35
+ the corresponding constant attribute and makes it available as a
36
+ Lazydoc::Comment.
37
+
38
+ comment = Sample::key
39
+ comment.value
40
+ # => "<value>"
41
+
42
+ comment.content
43
+ # => [
44
+ # ["This is the comment content. A content", "string can span multiple lines..."],
45
+ # [""],
46
+ # [" code.is_allowed"],
47
+ # [" much.as_in RDoc"],
48
+ # [""],
49
+ # ["and stops at the next non-comment", "line, the next constant attribute,", "or an end key"]]
50
+
51
+ "\n#{'.' * 30}\n" + comment.wrap(30) + "\n#{'.' * 30}\n"
52
+ # => %q{
53
+ # ..............................
54
+ # This is the comment content.
55
+ # A content string can span
56
+ # multiple lines...
57
+ #
58
+ # code.is_allowed
59
+ # much.as_in RDoc
60
+ #
61
+ # and stops at the next
62
+ # non-comment line, the next
63
+ # constant attribute, or an end
64
+ # key
65
+ # ..............................
66
+ #}
67
+
68
+ In addition, individual lines of code may be registered and resolved by Lazydoc:
69
+
70
+ doc = Sample.lazydoc.reset
71
+ comment = doc.register(/method_one/)
72
+
73
+ doc.resolve
74
+ comment.subject # => " def method_one"
75
+ comment.content # => [["comment content for a code comment", "may similarly span multiple lines"]]
76
+
77
+ Check out these links for development, and bug tracking.
78
+
79
+ * Website[http://tap.rubyforge.org/lazydoc]
80
+ * Github[http://github.com/bahuvrihi/lazydoc/tree/master]
81
+ * {Google Group}[http://groups.google.com/group/ruby-on-tap]
82
+
83
+ == Usage
84
+
85
+ === Constant Attributes
86
+
87
+ Constant attributes are like constants in Ruby, but with an extra 'key'
88
+ that must consist of only lowercase letters and/or underscores. For
89
+ example, these are constant attributes:
90
+
91
+ # Const::Name::key
92
+ # Const::Name::key_with_underscores
93
+ # ::key
94
+
95
+ While these are not:
96
+
97
+ # Const::Name::Key
98
+ # Const::Name::key2
99
+ # Const::Name::k@y
100
+
101
+ Lazydoc parses a Lazydoc::Comment for each constant attribute by using the
102
+ remainder of the line as a value (ie subject) and scanning down for content.
103
+ Scanning continues until a non-comment line, an end key, or a new attribute
104
+ is reached; the comment is then stored by constant name and key.
105
+
106
+ str = %Q{
107
+ # Const::Name::key value for key
108
+ # comment for key
109
+ # parsed until a
110
+ # non-comment line
111
+
112
+ # Const::Name::another value for another
113
+ # comment for another
114
+ # parsed to an end key
115
+ # Const::Name::another-
116
+ #
117
+ # ignored comment
118
+ }
119
+
120
+ doc = Lazydoc::Document.new
121
+ doc.resolve(str)
122
+
123
+ doc.to_hash {|comment| [comment.value, comment.to_s] }
124
+ # => {
125
+ # 'Const::Name' => {
126
+ # 'key' => ['value for key', 'comment for key parsed until a non-comment line'],
127
+ # 'another' => ['value for another', 'comment for another parsed to an end key']}
128
+ # }
129
+
130
+ Constant attributes are only parsed from commented lines. To turn off
131
+ attribute parsing for a section of documentation, use start/stop keys:
132
+
133
+ str = %Q{
134
+ Const::Name::not_parsed
135
+
136
+ # :::-
137
+ # Const::Name::not_parsed
138
+ # :::+
139
+ # Const::Name::parsed value
140
+ }
141
+
142
+ doc = Lazydoc::Document.new
143
+ doc.resolve(str)
144
+ doc.to_hash {|comment| comment.value } # => {'Const::Name' => {'parsed' => 'value'}}
145
+
146
+ To hide attributes from RDoc, make use of the RDoc <tt>:startdoc:</tt>
147
+ document modifier like this (note that spaces are added to prevent RDoc
148
+ from hiding the example):
149
+
150
+ # :start doc::Const::Name::one hidden in RDoc
151
+ # * This line is visible in RDoc.
152
+ # :start doc::Const::Name::one-
153
+ #
154
+ #--
155
+ # Const::Name::two
156
+ # You can hide attribute comments like this.
157
+ # Const::Name::two-
158
+ #++
159
+ #
160
+ # * This line is also visible in RDoc.
161
+
162
+ As a side note, <tt>Const::Name::key</tt> is not a reference to the 'key'
163
+ constant (as that would be invalid). In *very* idiomatic ruby
164
+ <tt>Const::Name::key</tt> is equivalent to the method call
165
+ <tt>Const::Name.key</tt>.
166
+
167
+ === Code Comments
168
+
169
+ Code comments are lines registered for parsing if and when a Lazydoc gets
170
+ resolved. Unlike constant attributes, the registered line is the comment
171
+ subject (ie value) and contents are parsed up from it (basically mimicking
172
+ the behavior of RDoc).
173
+
174
+ str = %Q{
175
+ # comment lines for
176
+ # the method
177
+ def method
178
+ end
179
+
180
+ # as in RDoc, the comment can be
181
+ # separated from the method
182
+
183
+ def another_method
184
+ end
185
+ }
186
+
187
+ doc = Lazydoc::Document.new
188
+ doc.register(3)
189
+ doc.register(9)
190
+ doc.resolve(str)
191
+
192
+ doc.comments.collect {|comment| [comment.subject, comment.to_s] }
193
+ # => [
194
+ # ['def method', 'comment lines for the method'],
195
+ # ['def another_method', 'as in RDoc, the comment can be separated from the method']]
196
+
197
+ Comments may be registered to specific line numbers, or with a Proc or
198
+ Regexp that will determine the line number during resolution. In the case
199
+ of a Regexp, the first matching line is used; Procs receive an array of
200
+ lines and should return the line number that should be used. See
201
+ Lazydoc::Comment#resolve for more details.
202
+
203
+ == Installation
204
+
205
+ Lazydoc is available as a gem on RubyForge[http://rubyforge.org/projects/tap]. Use:
206
+
207
+ % gem install lazydoc
208
+
209
+ == Info
210
+
211
+ Copyright (c) 2008, Regents of the University of Colorado.
212
+ Developer:: {Simon Chiang}[http://bahuvrihi.wordpress.com], {Biomolecular Structure Program}[http://biomol.uchsc.edu/], {Hansen Lab}[http://hsc-proteomics.uchsc.edu/hansenlab/]
213
+ Support:: CU Denver School of Medicine Deans Academic Enrichment Fund
214
+ Licence:: {MIT-Style}[link:files/MIT-LICENSE.html]
@@ -0,0 +1,42 @@
1
+ module Lazydoc
2
+ # Attributes adds methods to declare class-level accessors
3
+ # for Lazydoc attributes. The source_file for the class must
4
+ # be set manually.
5
+ #
6
+ # # ConstName::key value
7
+ # class ConstName
8
+ # class << self
9
+ # include Lazydoc::Attributes
10
+ # end
11
+ #
12
+ # self.source_file = __FILE__
13
+ # lazy_attr :key
14
+ # end
15
+ #
16
+ # ConstName::key.subject # => 'value'
17
+ #
18
+ module Attributes
19
+
20
+ # The source_file for self. Must be set independently.
21
+ attr_accessor :source_file
22
+
23
+ # Returns the lazydoc for source_file
24
+ def lazydoc(resolve=true)
25
+ lazydoc = Lazydoc[source_file]
26
+ lazydoc.resolve if resolve
27
+ lazydoc
28
+ end
29
+
30
+ # Creates a lazy attribute accessor for the specified attribute.
31
+ def lazy_attr(key, attribute=key)
32
+ instance_eval %Q{
33
+ def #{key}
34
+ lazydoc[to_s]['#{attribute}'] ||= Lazydoc::Comment.new
35
+ end
36
+
37
+ def #{key}=(comment)
38
+ Lazydoc[source_file][to_s]['#{attribute}'] = comment
39
+ end}
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,499 @@
1
+ require 'strscan'
2
+
3
+ module Lazydoc
4
+ # Comment represents a code comment parsed by Lazydoc. Comments consist
5
+ # of a subject and content.
6
+ #
7
+ # sample_comment = %Q{
8
+ # # this is the content
9
+ # #
10
+ # # content may stretch across
11
+ # # multiple lines
12
+ # this is the subject
13
+ # }
14
+ #
15
+ # Normally the subject is the first non-comment line following the content,
16
+ # although in some cases the subject will be manually set to something else
17
+ # (as in a Lazydoc constant attribute). The content is an array of comment
18
+ # fragments organized by line:
19
+ #
20
+ # c = Comment.parse(sample_comment)
21
+ # c.subject # => "this is the subject"
22
+ # c.content
23
+ # # => [
24
+ # # ["this is the content"],
25
+ # # [""],
26
+ # # ["content may stretch across", "multiple lines"]]
27
+ #
28
+ # Comments may be initialized to the subject line and then resolved later:
29
+ #
30
+ # doc = %Q{
31
+ # module Sample
32
+ # # this is the content of the comment
33
+ # # for method_one
34
+ # def method_one
35
+ # end
36
+ #
37
+ # # this is the content of the comment
38
+ # # for method_two
39
+ # def method_two
40
+ # end
41
+ # end}
42
+ #
43
+ # c1 = Comment.new(4).resolve(doc)
44
+ # c1.subject # => " def method_one"
45
+ # c1.content # => [["this is the content of the comment", "for method_one"]]
46
+ #
47
+ # c2 = Comment.new(9).resolve(doc)
48
+ # c2.subject # => " def method_two"
49
+ # c2.content # => [["this is the content of the comment", "for method_two"]]
50
+ #
51
+ # A Regexp (or Proc) may be used in place of a line number; during resolve,
52
+ # the lines will be scanned and the first matching line will be used.
53
+ #
54
+ # c3 = Comment.new(/def method_two/).resolve(doc)
55
+ # c3.subject # => " def method_two"
56
+ # c3.content # => [["this is the content of the comment", "for method_two"]]
57
+ #
58
+ class Comment
59
+
60
+ class << self
61
+
62
+ # Parses the input string into a comment. Takes a string or a
63
+ # StringScanner and returns the comment.
64
+ #
65
+ # comment_string = %Q{
66
+ # # comments spanning multiple
67
+ # # lines are collected
68
+ # #
69
+ # # while indented lines
70
+ # # are preserved individually
71
+ # #
72
+ # this is the subject line
73
+ #
74
+ # # this line is not parsed
75
+ # }
76
+ #
77
+ # c = Comment.parse(comment_string)
78
+ # c.content
79
+ # # => [
80
+ # # ['comments spanning multiple', 'lines are collected'],
81
+ # # [''],
82
+ # # [' while indented lines'],
83
+ # # [' are preserved individually'],
84
+ # # [''],
85
+ # # []]
86
+ # c.subject # => "this is the subject line"
87
+ #
88
+ # Parsing may be manually ended by providing a block; parse yields
89
+ # each line fragment to the block and stops parsing when the block
90
+ # returns true. Note that no subject will be parsed under these
91
+ # circumstances.
92
+ #
93
+ # c = Comment.parse(comment_string) {|frag| frag.strip.empty? }
94
+ # c.content
95
+ # # => [
96
+ # # ['comments spanning multiple', 'lines are collected']]
97
+ # c.subject # => nil
98
+ #
99
+ # Subject parsing may also be suppressed by setting parse_subject
100
+ # to false.
101
+ def parse(str, parse_subject=true) # :yields: fragment
102
+ scanner = case str
103
+ when StringScanner then str
104
+ when String then StringScanner.new(str)
105
+ else raise TypeError, "can't convert #{str.class} into StringScanner or String"
106
+ end
107
+
108
+ comment = Comment.new
109
+ while scanner.scan(/\r?\n?[ \t]*#[ \t]?(([ \t]*).*?)\r?$/)
110
+ fragment = scanner[1]
111
+ indent = scanner[2]
112
+
113
+ # collect continuous description line
114
+ # fragments and join into a single line
115
+ if block_given? && yield(fragment)
116
+ # break on comment if the description end is reached
117
+ parse_subject = false
118
+ break
119
+ else
120
+ categorize(fragment, indent) {|f| comment.push(f) }
121
+ end
122
+ end
123
+
124
+ if parse_subject
125
+ scanner.skip(/\s+/)
126
+ unless scanner.peek(1) == '#'
127
+ comment.subject = scanner.scan(/.+?$/)
128
+ comment.subject.strip! unless comment.subject == nil
129
+ end
130
+ end
131
+
132
+ comment
133
+ end
134
+
135
+ # Scan determines if and how to add a line fragment to a comment and
136
+ # yields the appropriate fragments to the block. Returns true if
137
+ # fragments are yielded and false otherwise.
138
+ #
139
+ # Content may be built from an array of lines using scan like so:
140
+ #
141
+ # lines = [
142
+ # "# comments spanning multiple",
143
+ # "# lines are collected",
144
+ # "#",
145
+ # "# while indented lines",
146
+ # "# are preserved individually",
147
+ # "# ",
148
+ # "not a comment line",
149
+ # "# skipped since the loop breaks",
150
+ # "# at the first non-comment line"]
151
+ #
152
+ # c = Comment.new
153
+ # lines.each do |line|
154
+ # break unless Comment.scan(line) do |fragment|
155
+ # c.push(fragment)
156
+ # end
157
+ # end
158
+ #
159
+ # c.content
160
+ # # => [
161
+ # # ['comments spanning multiple', 'lines are collected'],
162
+ # # [''],
163
+ # # [' while indented lines'],
164
+ # # [' are preserved individually'],
165
+ # # [''],
166
+ # # []]
167
+ #
168
+ def scan(line) # :yields: fragment
169
+ return false unless line =~ /^[ \t]*#[ \t]?(([ \t]*).*?)\r?$/
170
+ categorize($1, $2) do |fragment|
171
+ yield(fragment)
172
+ end
173
+ true
174
+ end
175
+
176
+ # Splits a line of text along whitespace breaks into fragments of cols
177
+ # width. Tabs in the line will be expanded into tabsize spaces;
178
+ # fragments are rstripped of whitespace.
179
+ #
180
+ # Comment.wrap("some line that will wrap", 10) # => ["some line", "that will", "wrap"]
181
+ # Comment.wrap(" line that will wrap ", 10) # => [" line", "that will", "wrap"]
182
+ # Comment.wrap(" ", 10) # => []
183
+ #
184
+ # The wrapping algorithm is slightly modified from:
185
+ # http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
186
+ def wrap(line, cols=80, tabsize=2)
187
+ line = line.gsub(/\t/, " " * tabsize) unless tabsize == nil
188
+ line.gsub(/(.{1,#{cols}})( +|$\r?\n?)|(.{1,#{cols}})/, "\\1\\3\n").split(/\s*?\n/)
189
+ end
190
+
191
+ private
192
+
193
+ # utility method used by scan to categorize and yield
194
+ # the appropriate objects to add the fragment to a
195
+ # comment
196
+ def categorize(fragment, indent) # :nodoc:
197
+ case
198
+ when fragment == indent
199
+ # empty comment line
200
+ yield [""]
201
+ yield []
202
+ when indent.empty?
203
+ # continuation line
204
+ yield fragment.rstrip
205
+ else
206
+ # indented line
207
+ yield [fragment.rstrip]
208
+ yield []
209
+ end
210
+ end
211
+ end
212
+
213
+ # An array of comment fragments organized into lines
214
+ attr_reader :content
215
+
216
+ # The subject of the comment (normally set to the next
217
+ # non-comment line after the content ends; ie the line
218
+ # that would receive the comment in RDoc documentation)
219
+ attr_accessor :subject
220
+
221
+ # Returns the line number for the subject line, if known.
222
+ # Although normally an integer, line_number may be
223
+ # set to a Regexp or Proc to dynamically determine
224
+ # the subject line during resolve
225
+ attr_accessor :line_number
226
+
227
+ def initialize(line_number=nil)
228
+ @content = []
229
+ @subject = nil
230
+ @line_number = line_number
231
+ end
232
+
233
+ # Alias for subject
234
+ def value
235
+ subject
236
+ end
237
+
238
+ # Alias for subject=
239
+ def value=(value)
240
+ self.subject = value
241
+ end
242
+
243
+ # Pushes the fragment onto the last line array of content. If the
244
+ # fragment is an array itself then it will be pushed onto content
245
+ # as a new line.
246
+ #
247
+ # c = Comment.new
248
+ # c.push "some line"
249
+ # c.push "fragments"
250
+ # c.push ["a", "whole", "new line"]
251
+ #
252
+ # c.content
253
+ # # => [
254
+ # # ["some line", "fragments"],
255
+ # # ["a", "whole", "new line"]]
256
+ #
257
+ def push(fragment)
258
+ content << [] if content.empty?
259
+
260
+ case fragment
261
+ when Array
262
+ if content[-1].empty?
263
+ content[-1] = fragment
264
+ else
265
+ content.push fragment
266
+ end
267
+ else
268
+ content[-1].push fragment
269
+ end
270
+ end
271
+
272
+ # Alias for push.
273
+ def <<(fragment)
274
+ push(fragment)
275
+ end
276
+
277
+ # Scans the comment line using Comment.scan and pushes the appropriate
278
+ # fragments onto self. Used to build a content by scanning down a set
279
+ # of lines.
280
+ #
281
+ # lines = [
282
+ # "# comment spanning multiple",
283
+ # "# lines",
284
+ # "#",
285
+ # "# indented line one",
286
+ # "# indented line two",
287
+ # "# ",
288
+ # "not a comment line"]
289
+ #
290
+ # c = Comment.new
291
+ # lines.each {|line| c.append(line) }
292
+ #
293
+ # c.content
294
+ # # => [
295
+ # # ['comment spanning multiple', 'lines'],
296
+ # # [''],
297
+ # # [' indented line one'],
298
+ # # [' indented line two'],
299
+ # # [''],
300
+ # # []]
301
+ #
302
+ def append(line)
303
+ Comment.scan(line) {|f| push(f) }
304
+ end
305
+
306
+ # Unshifts the fragment to the first line array of content. If the
307
+ # fragment is an array itself then it will be unshifted onto content
308
+ # as a new line.
309
+ #
310
+ # c = Comment.new
311
+ # c.unshift "some line"
312
+ # c.unshift "fragments"
313
+ # c.unshift ["a", "whole", "new line"]
314
+ #
315
+ # c.content
316
+ # # => [
317
+ # # ["a", "whole", "new line"],
318
+ # # ["fragments", "some line"]]
319
+ #
320
+ def unshift(fragment)
321
+ content << [] if content.empty?
322
+
323
+ case fragment
324
+ when Array
325
+ if content[0].empty?
326
+ content[0] = fragment
327
+ else
328
+ content.unshift fragment
329
+ end
330
+ else
331
+ content[0].unshift fragment
332
+ end
333
+ end
334
+
335
+ # Scans the comment line using Comment.scan and unshifts the appropriate
336
+ # fragments onto self. Used to build a content by scanning up a set of
337
+ # lines.
338
+ #
339
+ # lines = [
340
+ # "# comment spanning multiple",
341
+ # "# lines",
342
+ # "#",
343
+ # "# indented line one",
344
+ # "# indented line two",
345
+ # "# ",
346
+ # "not a comment line"]
347
+ #
348
+ # c = Comment.new
349
+ # lines.reverse_each {|line| c.prepend(line) }
350
+ #
351
+ # c.content
352
+ # # => [
353
+ # # ['comment spanning multiple', 'lines'],
354
+ # # [''],
355
+ # # [' indented line one'],
356
+ # # [' indented line two'],
357
+ # # ['']]
358
+ #
359
+ def prepend(line)
360
+ Comment.scan(line) {|f| unshift(f) }
361
+ end
362
+
363
+ # Builds the subject and content of self using lines; resolve sets
364
+ # the subject to the line at line_number, and parses content up
365
+ # from there. Any previously set subject and content is overridden.
366
+ # Returns self.
367
+ #
368
+ # document = %Q{
369
+ # module Sample
370
+ # # this is the content of the comment
371
+ # # for method_one
372
+ # def method_one
373
+ # end
374
+ #
375
+ # # this is the content of the comment
376
+ # # for method_two
377
+ # def method_two
378
+ # end
379
+ # end}
380
+ #
381
+ # c = Comment.new 4
382
+ # c.resolve(document)
383
+ # c.subject # => " def method_one"
384
+ # c.content # => [["this is the content of the comment", "for method_one"]]
385
+ #
386
+ # Lines may be an array or a string; string inputs are split into an
387
+ # array along newline boundaries.
388
+ #
389
+ # === dynamic line numbers
390
+ # The line_number used by resolve may be determined dynamically from
391
+ # lines by setting line_number to a Regexp and Proc. In the case
392
+ # of a Regexp, the first line matching the regexp is used:
393
+ #
394
+ # c = Comment.new(/def method/)
395
+ # c.resolve(document)
396
+ # c.line_number = 4
397
+ # c.subject # => " def method_one"
398
+ # c.content # => [["this is the content of the comment", "for method_one"]]
399
+ #
400
+ # Procs are called with lines and are expected to return the
401
+ # actual line number.
402
+ #
403
+ # c = Comment.new lambda {|lines| 9 }
404
+ # c.resolve(document)
405
+ # c.line_number = 9
406
+ # c.subject # => " def method_two"
407
+ # c.content # => [["this is the content of the comment", "for method_two"]]
408
+ #
409
+ # As shown in the examples, in both cases the dynamically determined
410
+ # line_number overwrites the Regexp or Proc.
411
+ def resolve(lines)
412
+ lines = lines.split(/\r?\n/) if lines.kind_of?(String)
413
+
414
+ # resolve late-evaluation line numbers
415
+ n = case line_number
416
+ when Regexp then match_index(line_number, lines)
417
+ when Proc then line_number.call(lines)
418
+ else line_number
419
+ end
420
+
421
+ # quietly exit if a line number was not found
422
+ return self unless n.kind_of?(Integer)
423
+
424
+ unless n < lines.length
425
+ raise RangeError, "line_number outside of lines: #{line_number} (#{lines.length})"
426
+ end
427
+
428
+ self.line_number = n
429
+ self.subject = lines[n]
430
+ self.content.clear
431
+
432
+ # remove whitespace lines
433
+ n -= 1
434
+ n -= 1 while n >=0 && lines[n].strip.empty?
435
+
436
+ # put together the comment
437
+ while n >= 0
438
+ break unless prepend(lines[n])
439
+ n -= 1
440
+ end
441
+
442
+ self
443
+ end
444
+
445
+ # Removes leading and trailing lines from content that are
446
+ # empty ([]) or whitespace (['']). Returns self.
447
+ def trim
448
+ content.shift while !content.empty? && (content[0].empty? || content[0].join.strip.empty?)
449
+ content.pop while !content.empty? && (content[-1].empty? || content[-1].join.strip.empty?)
450
+ self
451
+ end
452
+
453
+ # True if all lines in content are empty.
454
+ def empty?
455
+ !content.find {|line| !line.empty?}
456
+ end
457
+
458
+ # Returns content as a string where line fragments are joined by
459
+ # fragment_sep and lines are joined by line_sep.
460
+ def to_s(fragment_sep=" ", line_sep="\n", strip=true)
461
+ lines = content.collect {|line| line.join(fragment_sep)}
462
+
463
+ # strip leading an trailing whitespace lines
464
+ if strip
465
+ lines.shift while !lines.empty? && lines[0].empty?
466
+ lines.pop while !lines.empty? && lines[-1].empty?
467
+ end
468
+
469
+ line_sep ? lines.join(line_sep) : lines
470
+ end
471
+
472
+ # Like to_s, but wraps the content to the specified number of cols
473
+ # and expands tabs to tabsize spaces.
474
+ def wrap(cols=80, tabsize=2, line_sep="\n", fragment_sep=" ", strip=true)
475
+ lines = Comment.wrap(to_s(fragment_sep, "\n", strip), cols, tabsize)
476
+ line_sep ? lines.join(line_sep) : lines
477
+ end
478
+
479
+ # Returns true if another is a Comment with the same
480
+ # line_number, subject, and content as self
481
+ def ==(another)
482
+ another.kind_of?(Comment) &&
483
+ self.line_number == another.line_number &&
484
+ self.subject == another.subject &&
485
+ self.content == another.content
486
+ end
487
+
488
+ private
489
+
490
+ # utility method used to by resolve to find the index
491
+ # of a line matching a regexp line_number.
492
+ def match_index(regexp, lines) # :nodoc:
493
+ lines.each_with_index do |line, index|
494
+ return index if line =~ regexp
495
+ end
496
+ nil
497
+ end
498
+ end
499
+ end
@@ -0,0 +1,148 @@
1
+ require 'lazydoc/comment'
2
+
3
+ module Lazydoc
4
+
5
+ # A Document tracks constant attributes and code comments for a particular
6
+ # source file. Documents may be assigned a default_const_name to be used
7
+ # when a constant attribute does not specify a constant.
8
+ #
9
+ # # KeyWithConst::key value a
10
+ # # ::key value b
11
+ #
12
+ # doc = Document.new(__FILE__, 'DefaultConst')
13
+ # doc.resolve
14
+ # doc['KeyWithConst']['key'].value # => 'value a'
15
+ # doc['DefaultConst']['key'].value # => 'value b'
16
+ #
17
+ class Document
18
+
19
+ # The source file for self, used during resolve
20
+ attr_reader :source_file
21
+
22
+ # An array of Comment objects identifying lines
23
+ # resolved or to-be-resolved
24
+ attr_reader :comments
25
+
26
+ # A hash of [const_name, attributes] pairs tracking the constant
27
+ # attributes resolved or to-be-resolved for self. Attributes
28
+ # are hashes of [key, comment] pairs.
29
+ attr_reader :const_attrs
30
+
31
+ # The default constant name used when no constant name
32
+ # is specified for a constant attribute
33
+ attr_reader :default_const_name
34
+
35
+ # Flag indicating whether or not self has been resolved
36
+ attr_accessor :resolved
37
+
38
+ def initialize(source_file=nil, default_const_name='')
39
+ self.source_file = source_file
40
+ @default_const_name = default_const_name
41
+ @comments = []
42
+ @const_attrs = {}
43
+ @resolved = false
44
+ self.reset
45
+ end
46
+
47
+ # Resets self by clearing const_attrs, comments, and setting
48
+ # resolved to false. Generally NOT recommended as this
49
+ # clears any work you've done registering lines; to simply
50
+ # allow resolve to re-scan a document, manually set
51
+ # resolved to false.
52
+ def reset
53
+ @const_attrs.clear
54
+ @comments.clear
55
+ @resolved = false
56
+ self
57
+ end
58
+
59
+ # Sets the source file for self. Expands the source file path if necessary.
60
+ def source_file=(source_file)
61
+ @source_file = source_file == nil ? nil : File.expand_path(source_file)
62
+ end
63
+
64
+ # Sets the default_const_name for self. Any const_attrs assigned to
65
+ # the previous default will be removed and merged with those already
66
+ # assigned to the new default.
67
+ def default_const_name=(const_name)
68
+ self[const_name].merge!(const_attrs.delete(@default_const_name) || {})
69
+ @default_const_name = const_name
70
+ end
71
+
72
+ # Returns the attributes for the specified const_name.
73
+ def [](const_name)
74
+ const_attrs[const_name] ||= {}
75
+ end
76
+
77
+ # Returns an array of the const_names in self with at
78
+ # least one attribute.
79
+ def const_names
80
+ names = []
81
+ const_attrs.each_pair do |const_name, attrs|
82
+ names << const_name unless attrs.empty?
83
+ end
84
+ names
85
+ end
86
+
87
+ # Register the specified line number to self. Register
88
+ # may take an integer or a regexp for late-evaluation.
89
+ # See Comment#resolve for more details.
90
+ #
91
+ # Returns a comment_class instance corresponding to the line.
92
+ def register(line_number, comment_class=Comment)
93
+ comment = comments.find {|c| c.class == comment_class && c.line_number == line_number }
94
+
95
+ if comment == nil
96
+ comment = comment_class.new(line_number)
97
+ comments << comment
98
+ end
99
+
100
+ comment
101
+ end
102
+
103
+ # Registers a regexp matching methods by the specified
104
+ # name.
105
+ def register_method(method, comment_class=Comment)
106
+ register(/^\s*def\s+#{method}(\W|$)/, comment_class)
107
+ end
108
+
109
+ # Scans str for constant attributes and adds them to to self. Code
110
+ # comments are also resolved against str. If no str is specified,
111
+ # the contents of source_file are used instead.
112
+ #
113
+ # Resolve does nothing if resolved == true. Returns true if str
114
+ # was resolved, or false otherwise.
115
+ def resolve(str=nil)
116
+ return(false) if resolved
117
+
118
+ str = File.read(source_file) if str == nil
119
+ Lazydoc.parse(str) do |const_name, key, comment|
120
+ const_name = default_const_name if const_name.empty?
121
+ self[const_name][key] = comment
122
+ end
123
+
124
+ unless comments.empty?
125
+ lines = str.split(/\r?\n/)
126
+ comments.each do |comment|
127
+ comment.resolve(lines)
128
+ end
129
+ end
130
+
131
+ @resolved = true
132
+ end
133
+
134
+ def to_hash
135
+ const_hash = {}
136
+ const_attrs.each_pair do |const_name, attributes|
137
+ next if attributes.empty?
138
+
139
+ attr_hash = {}
140
+ attributes.each_pair do |key, comment|
141
+ attr_hash[key] = (block_given? ? yield(comment) : comment)
142
+ end
143
+ const_hash[const_name] = attr_hash
144
+ end
145
+ const_hash
146
+ end
147
+ end
148
+ end
data/lib/lazydoc.rb ADDED
@@ -0,0 +1,180 @@
1
+ require 'lazydoc/document'
2
+
3
+ module Lazydoc
4
+ autoload(:Attributes, 'lazydoc/attributes')
5
+
6
+ # A regexp matching an attribute start or end. After a match:
7
+ #
8
+ # $1:: const_name
9
+ # $3:: key
10
+ # $4:: end flag
11
+ #
12
+ ATTRIBUTE_REGEXP = /([A-Z][A-z]*(::[A-Z][A-z]*)*)?::([a-z_]+)(-?)/
13
+
14
+ # A regexp matching constants from the ATTRIBUTE_REGEXP leader
15
+ CONSTANT_REGEXP = /#.*?([A-Z][A-z]*(::[A-Z][A-z]*)*)?$/
16
+
17
+ # A regexp matching a caller line, to extract the calling file
18
+ # and line number. After a match:
19
+ #
20
+ # $1:: file
21
+ # $3:: line number (as a string, obviously)
22
+ #
23
+ # Note that line numbers in caller start at 1, not 0.
24
+ CALLER_REGEXP = /^(([A-z]:)?[^:]+):(\d+)/
25
+
26
+ module_function
27
+
28
+ # A hash of (source_file, lazydoc) pairs tracking the
29
+ # Lazydoc instance for the given source file.
30
+ def registry
31
+ @registry ||= []
32
+ end
33
+
34
+ # Returns the lazydoc in registry for the specified source file.
35
+ # If no such lazydoc exists, one will be created for it.
36
+ def [](source_file)
37
+ source_file = File.expand_path(source_file.to_s)
38
+ lazydoc = registry.find {|doc| doc.source_file == source_file }
39
+ if lazydoc == nil
40
+ lazydoc = Document.new(source_file)
41
+ registry << lazydoc
42
+ end
43
+ lazydoc
44
+ end
45
+
46
+ # Register the specified line numbers to the lazydoc for source_file.
47
+ # Returns a comment_class instance corresponding to the line.
48
+ def register(source_file, line_number, comment_class=Comment)
49
+ Lazydoc[source_file].register(line_number, comment_class)
50
+ end
51
+
52
+ # Resolves all lazydocs which include the specified code comments.
53
+ def resolve_comments(comments)
54
+ registry.each do |doc|
55
+ next if (comments & doc.comments).empty?
56
+ doc.resolve
57
+ end
58
+ end
59
+
60
+ # Scans the specified file for attributes keyed by key and stores
61
+ # the resulting comments in the source_file lazydoc. Returns the
62
+ # lazydoc.
63
+ def scan_doc(source_file, key)
64
+ lazydoc = nil
65
+ scan(File.read(source_file), key) do |const_name, attr_key, comment|
66
+ lazydoc = self[source_file] unless lazydoc
67
+ lazydoc[const_name][attr_key] = comment
68
+ end
69
+ lazydoc
70
+ end
71
+
72
+ # Scans the string or StringScanner for attributes matching the key
73
+ # (keys may be patterns, they are incorporated into a regexp). Yields
74
+ # each (const_name, key, value) triplet to the mandatory block and
75
+ # skips regions delimited by the stop and start keys <tt>:-</tt>
76
+ # and <tt>:+</tt>.
77
+ #
78
+ # str = %Q{
79
+ # # Const::Name::key value
80
+ # # ::alt alt_value
81
+ # #
82
+ # # Ignored::Attribute::not_matched value
83
+ # # :::-
84
+ # # Also::Ignored::key value
85
+ # # :::+
86
+ # # Another::key another value
87
+ #
88
+ # Ignored::key value
89
+ # }
90
+ #
91
+ # results = []
92
+ # Lazydoc.scan(str, 'key|alt') do |const_name, key, value|
93
+ # results << [const_name, key, value]
94
+ # end
95
+ #
96
+ # results
97
+ # # => [
98
+ # # ['Const::Name', 'key', 'value'],
99
+ # # ['', 'alt', 'alt_value'],
100
+ # # ['Another', 'key', 'another value']]
101
+ #
102
+ # Returns the StringScanner used during scanning.
103
+ def scan(str, key) # :yields: const_name, key, value
104
+ scanner = case str
105
+ when StringScanner then str
106
+ when String then StringScanner.new(str)
107
+ else raise TypeError, "can't convert #{str.class} into StringScanner or String"
108
+ end
109
+
110
+ regexp = /^(.*?)::(:-|#{key})/
111
+ while !scanner.eos?
112
+ break if scanner.skip_until(regexp) == nil
113
+
114
+ if scanner[2] == ":-"
115
+ scanner.skip_until(/:::\+/)
116
+ else
117
+ next unless scanner[1] =~ CONSTANT_REGEXP
118
+ key = scanner[2]
119
+ yield($1.to_s, key, scanner.matched.strip) if scanner.scan(/[ \r\t].*$|$/)
120
+ end
121
+ end
122
+
123
+ scanner
124
+ end
125
+
126
+ # Parses constant attributes from the string or StringScanner. Yields
127
+ # each (const_name, key, comment) triplet to the mandatory block
128
+ # and skips regions delimited by the stop and start keys <tt>:-</tt>
129
+ # and <tt>:+</tt>.
130
+ #
131
+ # str = %Q{
132
+ # # Const::Name::key subject for key
133
+ # # comment for key
134
+ #
135
+ # # :::-
136
+ # # Ignored::key value
137
+ # # :::+
138
+ #
139
+ # # Ignored text before attribute ::another subject for another
140
+ # # comment for another
141
+ # }
142
+ #
143
+ # results = []
144
+ # Lazydoc.parse(str) do |const_name, key, comment|
145
+ # results << [const_name, key, comment.subject, comment.to_s]
146
+ # end
147
+ #
148
+ # results
149
+ # # => [
150
+ # # ['Const::Name', 'key', 'subject for key', 'comment for key'],
151
+ # # ['', 'another', 'subject for another', 'comment for another']]
152
+ #
153
+ # Returns the StringScanner used during scanning.
154
+ def parse(str) # :yields: const_name, key, comment
155
+ scanner = case str
156
+ when StringScanner then str
157
+ when String then StringScanner.new(str)
158
+ else raise TypeError, "can't convert #{str.class} into StringScanner or String"
159
+ end
160
+
161
+ scan(scanner, '[a-z_]+') do |const_name, key, value|
162
+ comment = Comment.parse(scanner, false) do |line|
163
+ if line =~ ATTRIBUTE_REGEXP
164
+ # rewind to capture the next attribute unless an end is specified.
165
+ scanner.unscan unless $4 == '-' && $3 == key && $1.to_s == const_name
166
+ true
167
+ else false
168
+ end
169
+ end
170
+ comment.subject = value
171
+ yield(const_name, key, comment)
172
+ end
173
+ end
174
+
175
+ def usage(path, cols=80)
176
+ scanner = StringScanner.new(File.read(path))
177
+ scanner.scan(/^#!.*?$/)
178
+ Comment.parse(scanner, false).wrap(cols, 2).strip
179
+ end
180
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lazydoc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Simon Chiang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-12 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: tap
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.11.1
24
+ version:
25
+ description:
26
+ email: simon.a.chiang@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README
33
+ - MIT-LICENSE
34
+ files:
35
+ - lib/lazydoc.rb
36
+ - lib/lazydoc/attributes.rb
37
+ - lib/lazydoc/comment.rb
38
+ - lib/lazydoc/document.rb
39
+ - README
40
+ - MIT-LICENSE
41
+ has_rdoc: true
42
+ homepage: http://tap.rubyforge.org/lazydoc
43
+ post_install_message:
44
+ rdoc_options: []
45
+
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project: tap
63
+ rubygems_version: 1.2.0
64
+ signing_key:
65
+ specification_version: 2
66
+ summary: lazydoc
67
+ test_files: []
68
+