Class: Tap::Support::Lazydoc::Comment

Inherits:
Object
  • Object
show all
Defined in:
lib/tap/support/lazydoc/comment.rb

Overview

Comment represents a code comment parsed by Lazydoc. Comments consist of a subject and content.

sample_comment = %Q{
# this is the content
#
# content may stretch across
# multiple lines
this is the subject
}

Normally the subject is the first non-comment line following the content, although in some cases the subject will be manually set to something else (as in a Lazydoc constant attribute). The content is an array of comment fragments organized by line:

c = Comment.parse(sample_comment)
c.subject      # => "this is the subject"
c.content      
# => [
# ["this is the content"], 
# [""], 
# ["content may stretch across", "multiple lines"]]

Comments may be initialized to the subject line and then resolved later:

doc = %Q{
module Sample
  # this is the content of the comment
  # for method_one
  def method_one
  end

  # this is the content of the comment
  # for method_two
  def method_two
  end
end}

c1 = Comment.new(4).resolve(doc)
c1.subject     # => "  def method_one"
c1.content     # => [["this is the content of the comment", "for method_one"]]

c2 = Comment.new(9).resolve(doc)
c2.subject     # => "  def method_two"
c2.content     # => [["this is the content of the comment", "for method_two"]]

A Regexp (or Proc) may be used in place of a line number; during resolve, the lines will be scanned and the first matching line will be used.

c3 = Comment.new(/def method_two/).resolve(doc)
c3.subject     # => "  def method_two"
c3.content     # => [["this is the content of the comment", "for method_two"]]

Direct Known Subclasses

Config, Definition, Method

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(line_number = nil) ⇒ Comment

Returns a new instance of Comment.



229
230
231
232
233
# File 'lib/tap/support/lazydoc/comment.rb', line 229

def initialize(line_number=nil)
  @content = []
  @subject = nil
  @line_number = line_number
end

Instance Attribute Details

#contentObject (readonly)

An array of comment fragments organized into lines



216
217
218
# File 'lib/tap/support/lazydoc/comment.rb', line 216

def content
  @content
end

#line_numberObject

Returns the line number for the subject line, if known. Although normally an integer, line_number may be set to a Regexp or Proc to dynamically determine the subject line during resolve



227
228
229
# File 'lib/tap/support/lazydoc/comment.rb', line 227

def line_number
  @line_number
end

#subjectObject

The subject of the comment (normally set to the next non-comment line after the content ends; ie the line that would receive the comment in RDoc documentation)



221
222
223
# File 'lib/tap/support/lazydoc/comment.rb', line 221

def subject
  @subject
end

Class Method Details

.parse(str, parse_subject = true) ⇒ Object

Parses the input string into a comment. Takes a string or a StringScanner and returns the comment.

comment_string = %Q{
# comments spanning multiple
# lines are collected
#
#   while indented lines
#   are preserved individually
#    
this is the subject line

# this line is not parsed
}

c = Comment.parse(comment_string)
c.content   
# => [
# ['comments spanning multiple', 'lines are collected'],
# [''],
# ['  while indented lines'],
# ['  are preserved individually'],
# [''],
# []]
c.subject   # => "this is the subject line"

Parsing may be manually ended by providing a block; parse yields each line fragment to the block and stops parsing when the block returns true. Note that no subject will be parsed under these circumstances.

c = Comment.parse(comment_string) {|frag| frag.strip.empty? }
c.content   
# => [
# ['comments spanning multiple', 'lines are collected']]
c.subject   # => nil

Subject parsing may also be suppressed by setting parse_subject to false.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/tap/support/lazydoc/comment.rb', line 103

def parse(str, parse_subject=true) # :yields: fragment
  scanner = case str
  when StringScanner then str
  when String then StringScanner.new(str)
  else raise TypeError, "can't convert #{str.class} into StringScanner or String"
  end
        
  comment = Comment.new
  while scanner.scan(/\r?\n?[ \t]*#[ \t]?(([ \t]*).*?)\r?$/)
    fragment = scanner[1]
    indent = scanner[2]
  
    # collect continuous description line
    # fragments and join into a single line
    if block_given? && yield(fragment)
      # break on comment if the description end is reached
      parse_subject = false
      break
    else
      categorize(fragment, indent) {|f| comment.push(f) }
    end
  end
        
  if parse_subject
    scanner.skip(/\s+/)
    unless scanner.peek(1) == '#'
      comment.subject = scanner.scan(/.+?$/) 
      comment.subject.strip! unless comment.subject == nil
    end
  end
        
  comment
end

.scan(line) ⇒ Object

Scan determines if and how to add a line fragment to a comment and yields the appropriate fragments to the block. Returns true if fragments are yielded and false otherwise.

Content may be built from an array of lines using scan like so:

lines = [
  "# comments spanning multiple",
  "# lines are collected",
  "#",
  "#   while indented lines",
  "#   are preserved individually",
  "#    ",
  "not a comment line",
  "# skipped since the loop breaks",
  "# at the first non-comment line"]

c = Comment.new
lines.each do |line|
  break unless Comment.scan(line) do |fragment|
    c.push(fragment)  
  end
end

c.content   
# => [
# ['comments spanning multiple', 'lines are collected'],
# [''],
# ['  while indented lines'],
# ['  are preserved individually'],
# [''],
# []]


170
171
172
173
174
175
176
# File 'lib/tap/support/lazydoc/comment.rb', line 170

def scan(line) # :yields: fragment
  return false unless line =~ /^[ \t]*#[ \t]?(([ \t]*).*?)\r?$/
  categorize($1, $2) do |fragment|
    yield(fragment)
  end
  true
end

.wrap(line, cols = 80, tabsize = 2) ⇒ Object

Splits a line of text along whitespace breaks into fragments of cols width. Tabs in the line will be expanded into tabsize spaces; fragments are rstripped of whitespace.

Comment.wrap("some line that will wrap", 10)       # => ["some line", "that will", "wrap"]
Comment.wrap("     line that will wrap    ", 10)   # => ["     line", "that will", "wrap"]
Comment.wrap("                            ", 10)   # => []

The wrapping algorithm is slightly modified from: blog.macromates.com/2006/wrapping-text-with-regular-expressions/



188
189
190
191
# File 'lib/tap/support/lazydoc/comment.rb', line 188

def wrap(line, cols=80, tabsize=2)
  line = line.gsub(/\t/, " " * tabsize) unless tabsize == nil
  line.gsub(/(.{1,#{cols}})( +|$\r?\n?)|(.{1,#{cols}})/, "\\1\\3\n").split(/\s*?\n/)
end

Instance Method Details

#<<(fragment) ⇒ Object

Alias for push.



275
276
277
# File 'lib/tap/support/lazydoc/comment.rb', line 275

def <<(fragment)
  push(fragment)
end

#==(another) ⇒ Object

Returns true if another is a Comment with the same line_number, subject, and content as self



483
484
485
486
487
488
# File 'lib/tap/support/lazydoc/comment.rb', line 483

def ==(another)
  another.kind_of?(Comment) && 
  self.line_number == another.line_number &&
  self.subject == another.subject &&
  self.content == another.content
end

#append(line) ⇒ Object

Scans the comment line using Comment.scan and pushes the appropriate fragments onto self. Used to build a content by scanning down a set of lines.

lines = [
  "# comment spanning multiple",
  "# lines",
  "#",
  "#   indented line one",
  "#   indented line two",
  "#    ",
  "not a comment line"]

c = Comment.new
lines.each {|line| c.append(line) }

c.content 
# => [
# ['comment spanning multiple', 'lines'],
# [''],
# ['  indented line one'],
# ['  indented line two'],
# [''],
# []]


304
305
306
# File 'lib/tap/support/lazydoc/comment.rb', line 304

def append(line)
  Comment.scan(line) {|f| push(f) }
end

#empty?Boolean

True if all lines in content are empty.

Returns:

  • (Boolean)


456
457
458
# File 'lib/tap/support/lazydoc/comment.rb', line 456

def empty?
  !content.find {|line| !line.empty?}
end

#prepend(line) ⇒ Object

Scans the comment line using Comment.scan and unshifts the appropriate fragments onto self. Used to build a content by scanning up a set of lines.

lines = [
  "# comment spanning multiple",
  "# lines",
  "#",
  "#   indented line one",
  "#   indented line two",
  "#    ",
  "not a comment line"]

c = Comment.new
lines.reverse_each {|line| c.prepend(line) }

c.content 
# => [
# ['comment spanning multiple', 'lines'],
# [''],
# ['  indented line one'],
# ['  indented line two'],
# ['']]


361
362
363
# File 'lib/tap/support/lazydoc/comment.rb', line 361

def prepend(line)
  Comment.scan(line) {|f| unshift(f) }
end

#push(fragment) ⇒ Object

Pushes the fragment onto the last line array of content. If the fragment is an array itself then it will be pushed onto content as a new line.

c = Comment.new
c.push "some line"
c.push "fragments"
c.push ["a", "whole", "new line"]

c.content         
# => [
# ["some line", "fragments"], 
# ["a", "whole", "new line"]]


259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/tap/support/lazydoc/comment.rb', line 259

def push(fragment)
  content << [] if content.empty?

  case fragment
  when Array
    if content[-1].empty? 
      content[-1] = fragment
    else
      content.push fragment
    end
  else
     content[-1].push fragment
  end
end

#resolve(lines) ⇒ Object

Builds the subject and content of self using lines; resolve sets the subject to the line at line_number, and parses content up from there. Any previously set subject and content is overridden.

Returns self.

document = %Q{
module Sample
  # this is the content of the comment
  # for method_one
  def method_one
  end

  # this is the content of the comment
  # for method_two
  def method_two
  end
end}

c = Comment.new 4
c.resolve(document)
c.subject     # => "  def method_one"
c.content     # => [["this is the content of the comment", "for method_one"]]

Lines may be an array or a string; string inputs are split into an array along newline boundaries.

dynamic line numbers

The line_number used by resolve may be determined dynamically from lines by setting line_number to a Regexp and Proc. In the case of a Regexp, the first line matching the regexp is used:

c = Comment.new(/def method/)
c.resolve(document)
c.line_number = 4
c.subject     # => "  def method_one"
c.content     # => [["this is the content of the comment", "for method_one"]]

Procs are called with lines and are expected to return the actual line number.

c = Comment.new lambda {|lines| 9 }
c.resolve(document)
c.line_number = 9
c.subject     # => "  def method_two"
c.content     # => [["this is the content of the comment", "for method_two"]]

As shown in the examples, in both cases the dynamically determined line_number overwrites the Regexp or Proc.



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/tap/support/lazydoc/comment.rb', line 413

def resolve(lines)
  lines = lines.split(/\r?\n/) if lines.kind_of?(String)

  # resolve late-evaluation line numbers
  n = case line_number
  when Regexp then match_index(line_number, lines)
  when Proc then line_number.call(lines)
  else line_number
  end
 
  # quietly exit if a line number was not found
  return self unless n.kind_of?(Integer)
  
  unless n < lines.length
    raise RangeError, "line_number outside of lines: #{line_number} (#{lines.length})"
  end

  self.line_number = n
  self.subject = lines[n]
  self.content.clear

  # remove whitespace lines
  n -= 1
  n -= 1 while n >=0 && lines[n].strip.empty?

  # put together the comment
  while n >= 0
    break unless prepend(lines[n])
    n -= 1
  end
 
  self
end

#to_s(fragment_sep = " ", line_sep = "\n", strip = true) ⇒ Object

Returns content as a string where line fragments are joined by fragment_sep and lines are joined by line_sep.



462
463
464
465
466
467
468
469
470
471
472
# File 'lib/tap/support/lazydoc/comment.rb', line 462

def to_s(fragment_sep=" ", line_sep="\n", strip=true)
  lines = content.collect {|line| line.join(fragment_sep)}

  # strip leading an trailing whitespace lines
  if strip
    lines.shift while !lines.empty? && lines[0].empty?
    lines.pop while !lines.empty? && lines[-1].empty?
  end

  line_sep ? lines.join(line_sep) : lines
end

#trimObject

Removes leading and trailing lines from content that are empty ([]) or whitespace ([”]). Returns self.



449
450
451
452
453
# File 'lib/tap/support/lazydoc/comment.rb', line 449

def trim
  content.shift while !content.empty? && (content[0].empty? || content[0].join.strip.empty?)
  content.pop   while !content.empty? && (content[-1].empty? || content[-1].join.strip.empty?)
  self
end

#unshift(fragment) ⇒ Object

Unshifts the fragment to the first line array of content. If the fragment is an array itself then it will be unshifted onto content as a new line.

c = Comment.new
c.unshift "some line"
c.unshift "fragments"
c.unshift ["a", "whole", "new line"]

c.content         
# => [
# ["a", "whole", "new line"], 
# ["fragments", "some line"]]


322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/tap/support/lazydoc/comment.rb', line 322

def unshift(fragment)
  content << [] if content.empty?

  case fragment
  when Array
    if content[0].empty? 
      content[0] = fragment
    else
      content.unshift fragment
    end
  else
     content[0].unshift fragment
  end
end

#valueObject

Alias for subject



236
237
238
# File 'lib/tap/support/lazydoc/comment.rb', line 236

def value
  subject
end

#value=(value) ⇒ Object

Alias for subject=



241
242
243
# File 'lib/tap/support/lazydoc/comment.rb', line 241

def value=(value)
  self.subject = value
end

#wrap(cols = 80, tabsize = 2, line_sep = "\n", fragment_sep = " ", strip = true) ⇒ Object

Like to_s, but wraps the content to the specified number of cols and expands tabs to tabsize spaces.



476
477
478
479
# File 'lib/tap/support/lazydoc/comment.rb', line 476

def wrap(cols=80, tabsize=2, line_sep="\n", fragment_sep=" ", strip=true)
  lines = Comment.wrap(to_s(fragment_sep, "\n", strip), cols, tabsize)
  line_sep ? lines.join(line_sep) : lines
end