lazydoc 0.1.0

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