Module: Tap::Support::Lazydoc

Defined in:
lib/tap/support/lazydoc.rb,
lib/tap/support/lazydoc/config.rb,
lib/tap/support/lazydoc/method.rb,
lib/tap/support/lazydoc/comment.rb,
lib/tap/support/lazydoc/document.rb,
lib/tap/support/lazydoc/definition.rb

Overview

Lazydoc lazily pulls documentation out of source files and makes it available through LazyAttributes. Lazydoc can find two types of documentation, constant attributes and code comments. To illustrate, consider the following:

# Sample::key <value>
# This is the comment content.  A content
# string can span multiple lines...
#
#   code.is_allowed
#   much.as_in RDoc
#
# and stops at the next non-comment
# line, the next constant attribute,
# or an end key
class Sample
  extend Tap::Support::LazyAttributes
  self.source_file = __FILE__

  lazy_attr :key

  # comment content for a code comment
  # may similarly span multiple lines
  def method_one
  end
end

When a lazy attribute is called, Lazydoc scans source_file for the corresponding constant attribute and makes it available as a Lazydoc::Comment.

comment = Sample::key
comment.value       
# => "<value>"

comment.content       
# => [
# ["This is the comment content.  A content", "string can span multiple lines..."],
# [""],
# ["  code.is_allowed"],
# ["  much.as_in RDoc"],
# [""],
# ["and stops at the next non-comment", "line, the next constant attribute,", "or an end key"]]

"\n#{'.' * 30}\n" + comment.wrap(30) + "\n#{'.' * 30}\n"
# => %q{
# ..............................
# This is the comment content.
# A content string can span
# multiple lines...
# 
#   code.is_allowed
#   much.as_in RDoc
# 
# and stops at the next
# non-comment line, the next
# constant attribute, or an end
# key
# ..............................
#}

In addition, individual lines of code may be registered and resolved by Lazydoc:

doc = Sample.lazydoc.reset
comment = doc.register(/method_one/)

doc.resolve
comment.subject       # => "  def method_one"
comment.content       # => [["comment content for a code comment", "may similarly span multiple lines"]]

With these basics in mind, here are some details…

Constant Attributes

Constant attributes are like constants in Ruby, but with an extra ‘key’ that must consist of only lowercase letters and/or underscores. For example, these are constant attributes:

# Const::Name::key
# Const::Name::key_with_underscores
# ::key

While these are not:

# Const::Name::Key
# Const::Name::key2
# Const::Name::k@y

Lazydoc parses a Lazydoc::Comment for each constant attribute by using the remainder of the line as a value (ie subject) and scanning down for content. Scanning continues until a non-comment line, an end key, or a new attribute is reached; the comment is then stored by constant name and key.

str = %Q{
# Const::Name::key value for key
# comment for key
# parsed until a 
# non-comment line

# Const::Name::another value for another
# comment for another
# parsed to an end key
# Const::Name::another-
#
# ignored comment
}

doc = Lazydoc::Document.new
doc.resolve(str)

doc.to_hash {|comment| [comment.value, comment.to_s] } 
# => {
# 'Const::Name' => {
#   'key' =>     ['value for key', 'comment for key parsed until a non-comment line'],
#   'another' => ['value for another', 'comment for another parsed to an end key']}
# }

Constant attributes are only parsed from commented lines. To turn off attribute parsing for a section of documentation, use start/stop keys:

str = %Q{
Const::Name::not_parsed

# :::-
# Const::Name::not_parsed
# :::+
# Const::Name::parsed value
}

doc = Lazydoc::Document.new
doc.resolve(str)
doc.to_hash {|comment| comment.value }   # => {'Const::Name' => {'parsed' => 'value'}}

To hide attributes from RDoc, make use of the RDoc :startdoc: document modifier like this (note that spaces are added to prevent RDoc from hiding the example):

# :start doc::Const::Name::one hidden in RDoc
# * This line is visible in RDoc.
# :start doc::Const::Name::one-
# 
#-- 
# Const::Name::two
# You can hide attribute comments like this.
# Const::Name::two-
#++
#
# * This line is also visible in RDoc.

Here is the same text, for comparison if you are reading this as RDoc:

:startdoc::Const::Name::one hidden in RDoc

  • This line is visible in RDoc.

:startdoc::Const::Name::one-

– Const::Name::two You can hide attribute comments like this. Const::Name::two- ++

  • This line is also visible in RDoc.

As a side note, Const::Name::key is not a reference to the ‘key’ constant (as that would be invalid). In very idiomatic ruby Const::Name::key is equivalent to the method call Const::Name.key.

Code Comments

Code comments are lines registered for parsing if and when a Lazydoc gets resolved. Unlike constant attributes, the registered line is the comment subject (ie value) and contents are parsed up from it (basically mimicking the behavior of RDoc).

str = %Q{
# comment lines for
# the method
def method
end

# as in RDoc, the comment can be
# separated from the method

def another_method
end
}

doc = Lazydoc::Document.new
doc.register(3)
doc.register(9)
doc.resolve(str)

doc.comments.collect {|comment| [comment.subject, comment.to_s] } 
# => [
# ['def method', 'comment lines for the method'],
# ['def another_method', 'as in RDoc, the comment can be separated from the method']]

Comments may be registered to specific line numbers, or with a Proc or Regexp that will determine the line number during resolution. In the case of a Regexp, the first matching line is used; Procs receive an array of lines and should return the line number that should be used. See Lazydoc::Comment#resolve for more details.

Defined Under Namespace

Classes: Comment, Config, Definition, Document, Method

Constant Summary collapse

ATTRIBUTE_REGEXP =

A regexp matching an attribute start or end. After a match:

$1

const_name

$3

key

$4

end flag

/([A-Z][A-z]*(::[A-Z][A-z]*)*)?::([a-z_]+)(-?)/
CONSTANT_REGEXP =

A regexp matching constants from the ATTRIBUTE_REGEXP leader

/#.*?([A-Z][A-z]*(::[A-Z][A-z]*)*)?$/
CALLER_REGEXP =

A regexp matching a caller line, to extract the calling file and line number. After a match:

$1

file

$3

line number (as a string, obviously)

Note that line numbers in caller start at 1, not 0.

/^(([A-z]:)?[^:]+):(\d+)/

Class Method Summary collapse

Class Method Details

.[](source_file) ⇒ Object

Returns the lazydoc in registry for the specified source file. If no such lazydoc exists, one will be created for it.



240
241
242
243
244
245
246
247
248
# File 'lib/tap/support/lazydoc.rb', line 240

def [](source_file)
  source_file = File.expand_path(source_file.to_s)
  lazydoc = registry.find {|doc| doc.source_file == source_file }
  if lazydoc == nil
    lazydoc = Document.new(source_file)
    registry << lazydoc
  end
  lazydoc
end

.parse(str) ⇒ Object

Parses constant attributes from the string or StringScanner. Yields each (const_name, key, comment) triplet to the mandatory block and skips regions delimited by the stop and start keys :- and :+.

str = %Q{
# Const::Name::key subject for key
# comment for key

# :::-
# Ignored::key value
# :::+

# Ignored text before attribute ::another subject for another
# comment for another
}

results = []
Lazydoc.parse(str) do |const_name, key, comment|
  results << [const_name, key, comment.subject, comment.to_s]
end

results    
# => [
# ['Const::Name', 'key', 'subject for key', 'comment for key'], 
# ['', 'another', 'subject for another', 'comment for another']]

Returns the StringScanner used during scanning.



358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/tap/support/lazydoc.rb', line 358

def parse(str) # :yields: const_name, key, comment
  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
  
  scan(scanner, '[a-z_]+') do |const_name, key, value|
    comment = Comment.parse(scanner, false) do |line|
      if line =~ ATTRIBUTE_REGEXP
        # rewind to capture the next attribute unless an end is specified.
        scanner.unscan unless $4 == '-' && $3 == key && $1.to_s == const_name
        true
      else false
      end
    end
    comment.subject = value
    yield(const_name, key, comment)
  end
end

.register(source_file, line_number, comment_class = Comment) ⇒ Object

Register the specified line numbers to the lazydoc for source_file. Returns a comment_class instance corresponding to the line.



252
253
254
# File 'lib/tap/support/lazydoc.rb', line 252

def register(source_file, line_number, comment_class=Comment)
  Lazydoc[source_file].register(line_number, comment_class)
end

.registryObject

A hash of (source_file, lazydoc) pairs tracking the Lazydoc instance for the given source file.



234
235
236
# File 'lib/tap/support/lazydoc.rb', line 234

def registry
  @registry ||= []
end

.resolve_comments(comments) ⇒ Object

Resolves all lazydocs which include the specified code comments.



257
258
259
260
261
262
# File 'lib/tap/support/lazydoc.rb', line 257

def resolve_comments(comments)
  registry.each do |doc|
    next if (comments & doc.comments).empty?
    doc.resolve
  end
end

.scan(str, key) ⇒ Object

Scans the string or StringScanner for attributes matching the key (keys may be patterns, they are incorporated into a regexp). Yields each (const_name, key, value) triplet to the mandatory block and skips regions delimited by the stop and start keys :- and :+.

str = %Q{
# Const::Name::key value
# ::alt alt_value
#
# Ignored::Attribute::not_matched value
# :::-
# Also::Ignored::key value
# :::+
# Another::key another value

Ignored::key value
}

results = []
Lazydoc.scan(str, 'key|alt') do |const_name, key, value|
  results << [const_name, key, value]
end

results    
# => [
# ['Const::Name', 'key', 'value'], 
# ['', 'alt', 'alt_value'], 
# ['Another', 'key', 'another value']]

Returns the StringScanner used during scanning.



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
# File 'lib/tap/support/lazydoc.rb', line 307

def scan(str, key) # :yields: const_name, key, value
  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

  regexp = /^(.*?)::(:-|#{key})/
  while !scanner.eos?
    break if scanner.skip_until(regexp) == nil

    if scanner[2] == ":-"
      scanner.skip_until(/:::\+/)
    else
      next unless scanner[1] =~ CONSTANT_REGEXP
      key = scanner[2]
      yield($1.to_s, key, scanner.matched.strip) if scanner.scan(/[ \r\t].*$|$/)
    end
  end

  scanner
end

.scan_doc(source_file, key) ⇒ Object

Scans the specified file for attributes keyed by key and stores the resulting comments in the source_file lazydoc. Returns the lazydoc.



267
268
269
270
271
272
273
274
# File 'lib/tap/support/lazydoc.rb', line 267

def scan_doc(source_file, key)
  lazydoc = nil
  scan(File.read(source_file), key) do |const_name, attr_key, comment|
    lazydoc = self[source_file] unless lazydoc
    lazydoc[const_name][attr_key] = comment
  end
  lazydoc
end

.usage(path, cols = 80) ⇒ Object



379
380
381
382
383
# File 'lib/tap/support/lazydoc.rb', line 379

def usage(path, cols=80)
  scanner = StringScanner.new(File.read(path))
  scanner.scan(/^#!.*?$/)
  Comment.parse(scanner, false).wrap(cols, 2).strip
end