lib/tap/support/lazydoc.rb in bahuvrihi-tap-0.10.4 vs lib/tap/support/lazydoc.rb in bahuvrihi-tap-0.10.5
- old
+ new
@@ -1,71 +1,146 @@
-require 'tap/support/comment'
+require 'tap/support/lazydoc/document'
module Tap
module Support
- # Lazydoc scans source files to pull out documentation. Lazydoc can find two
- # types of documentation, constant attributes and code comments.
+ # 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 <this is the subject line>
+ # # a constant attribute content string that
+ # # 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 <tt>source_file</tt> for
+ # the corresponding constant attribute and makes it available as a
+ # Lazydoc::Comment.
+ #
+ # comment = Sample::key
+ # comment.subject
+ # # => "<this is the subject line>"
+ #
+ # comment.content
+ # # => [
+ # # ["a constant attribute content string that", "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{
+ # # ..............................
+ # # a constant attribute content
+ # # string that 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:
#
- # Constant attributes are designated the same as constants in Ruby, but with
- # an extra 'key' constant that must consist of only lowercase letters and/or
- # underscores. Attributes are only parsed from comment lines.
+ # # Const::Name::key
+ # # Const::Name::key_with_underscores
+ # # ::key
#
- # When Lazydoc finds an attribute it parses a Comment value where the subject
- # is the remainder of the line, and comment lines are parsed down until a
- # non-comment line, an end key, or a new attribute is reached.
+ # 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 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 subject for key
# # comment for key
- # # parsed until a non-comment line
+ # # parsed until a
+ # # non-comment line
#
# # Const::Name::another subject for another
# # comment for another
# # parsed to an end key
# # Const::Name::another-
# #
# # ignored comment
# }
#
- # lazydoc = Lazydoc.new
- # lazydoc.resolve(str)
+ # doc = Lazydoc::Document.new
+ # doc.resolve(str)
#
- # lazydoc.to_hash {|comment| [comment.subject, comment.to_s] }
- # # => {'Const::Name' => {
- # # 'key' => ['subject for key', 'comment for key parsed until a non-comment line'],
- # # 'another' => ['subject for another', 'comment for another parsed to an end key']
- # # }}
+ # doc.to_hash {|comment| [comment.subject, comment.to_s] }
+ # # => {
+ # # 'Const::Name' => {
+ # # 'key' => ['subject for key', 'comment for key parsed until a non-comment line'],
+ # # 'another' => ['subject for another', 'comment for another parsed to an end key']}
+ # # }
#
- # A constant name does not need to be specified; when no constant name is
- # specified, Lazydoc will store the key as a default for the document. To
- # turn off attribute parsing for a section of documentation, use start/stop
- # keys:
+ # 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::not_parsed
- #
# # Const::Name::parsed subject
# }
#
- # lazydoc = Lazydoc.new
- # lazydoc.resolve(str)
- # lazydoc.to_hash {|comment| comment.subject } # => {'Const::Name' => {'parsed' => 'subject'}}
+ # doc = Lazydoc::Document.new
+ # doc.resolve(str)
+ # doc.to_hash {|comment| comment.subject } # => {'Const::Name' => {'parsed' => 'subject'}}
#
- # ==== startdoc
+ # To hide attributes from RDoc, make use of the RDoc <tt>:startdoc:</tt>
+ # document modifier like this (note that spaces are added to prevent RDoc
+ # from hiding the example):
#
- # Lazydoc is completely separate from RDoc, but the syntax of Lazydoc was developed
- # with RDoc in mind. To hide attributes in one line, make use of the RDoc
- # <tt>:startdoc:</tt> document modifier like this (spaces added to keep them in the
- # example):
- #
# # :start doc::Const::Name::one hidden in RDoc
# # * This line is visible in RDoc.
# # :start doc::Const::Name::one-
# #
# #--
@@ -74,11 +149,11 @@
# # Const::Name::two-
# #++
# #
# # * This line is also visible in RDoc.
#
- # Here is the same text, actually 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-
#
@@ -88,17 +163,21 @@
# Const::Name::two-
#++
#
# * This line is also visible in RDoc.
#
+ # As a side note, <tt>Const::Name::key</tt> is not a reference to the 'key'
+ # constant (as that would be invalid). In *very* idiomatic ruby
+ # <tt>Const::Name::key</tt> is equivalent to the method call
+ # <tt>Const::Name.key</tt>.
+ #
# === 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 and contents are parsed up from it (basically mimicking the
+ # behavior of RDoc).
#
- # Code comments are lines marked for parsing if and when a Lazydoc gets resolved.
- # Unlike constant attributes, the line is the subject of a code comment and
- # comment lines are parsed up from it (effectively mimicking the behavior of
- # RDoc).
- #
# str = %Q{
# # comment lines for
# # the method
# def method
# end
@@ -108,21 +187,27 @@
#
# def another_method
# end
# }
#
- # lazydoc = Lazydoc.new
- # lazydoc.register(3)
- # lazydoc.register(9)
- # lazydoc.resolve(str)
+ # doc = Lazydoc::Document.new
+ # doc.register(3)
+ # doc.register(9)
+ # doc.resolve(str)
#
- # lazydoc.code_comments.collect {|comment| [comment.subject, comment.to_s] }
+ # 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']]
#
- class Lazydoc
+ # 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.
+ #
+ module Lazydoc
# A regexp matching an attribute start or end. After a match:
#
# $1:: const_name
# $3:: key
@@ -140,330 +225,162 @@
# $3:: line number (as a string, obviously)
#
# Note that line numbers in caller start at 1, not 0.
CALLER_REGEXP = /^(([A-z]:)?[^:]+):(\d+)/
- class << self
-
- # A hash of (source_file, lazydoc) pairs tracking the
- # Lazydoc instance for the given source file.
- def registry
- @registry ||= []
- end
-
- # Returns the lazydoc in registry for the specified source file.
- # If no such lazydoc exists, one will be created for it.
- 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 = new(source_file)
- registry << lazydoc
- end
- lazydoc
- end
-
- # Register the specified line numbers to the lazydoc for source_file.
- # Returns a CodeComment corresponding to the line.
- def register(source_file, line_number)
- Lazydoc[source_file].register(line_number)
- end
-
- # Resolves all lazydocs which include the specified code comments.
- def resolve_comments(code_comments)
- registry.each do |doc|
- next if (code_comments & doc.code_comments).empty?
- doc.resolve
- end
- end
-
- # Scans the specified file for attributes keyed by key and stores
- # the resulting comments in the corresponding lazydoc.
- # Returns the lazydoc.
- 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.attributes(const_name)[attr_key] = comment
- end
- lazydoc
- end
-
- # 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 <tt>:-</tt>
- # and <tt>:+</tt>.
- #
- # 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.
- 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
+ module_function
- # 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 <tt>:-</tt>
- # and <tt>:+</tt>.
- #
- # 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.
- 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
+ # A hash of (source_file, lazydoc) pairs tracking the
+ # Lazydoc instance for the given source file.
+ def registry
+ @registry ||= []
end
- include Enumerable
-
- # The source file for self, used in resolving comments and
- # attributes.
- attr_reader :source_file
-
- # An array of Comment objects identifying lines resolved or
- # to-be-resolved for self.
- attr_reader :code_comments
-
- # A hash of (const_name, attributes) pairs tracking the constant
- # attributes resolved or to-be-resolved for self. Attributes
- # are hashes of (key, comment) pairs.
- attr_reader :const_attrs
-
- attr_reader :patterns
-
- def initialize(source_file=nil)
- self.source_file = source_file
- @code_comments = []
- @patterns = {}
- @const_attrs = {}
- @resolved = false
+ # Returns the lazydoc in registry for the specified source file.
+ # If no such lazydoc exists, one will be created for it.
+ 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
-
- # Sets the source file for self. Expands the source file path if necessary.
- def source_file=(source_file)
- @source_file = source_file == nil ? nil : File.expand_path(source_file)
- end
- # Returns the attributes for the specified const_name.
- def attributes(const_name)
- const_attrs[const_name] ||= {}
+ # Register the specified line numbers to the lazydoc for source_file.
+ # Returns a comment_class instance corresponding to the line.
+ def register(source_file, line_number, comment_class=Comment)
+ Lazydoc[source_file].register(line_number, comment_class)
end
- # Returns default document attributes (ie attributes(''))
- def default_attributes
- attributes('')
- end
-
- # Returns the attributes for const_name merged to default_attributes.
- # Set merge_defaults to false to get just the attributes for const_name.
- def [](const_name, merge_defaults=true)
- merge_defaults ? default_attributes.merge(attributes(const_name)) : attributes(const_name)
- end
-
- # Yields each (const_name, attributes) pair to the block; const_names where
- # the attributes are empty are skipped.
- def each
- const_attrs.each_pair do |const_name, attrs|
- yield(const_name, attrs) unless attrs.empty?
+ # Resolves all lazydocs which include the specified code comments.
+ def resolve_comments(comments)
+ registry.each do |doc|
+ next if (comments & doc.comments).empty?
+ doc.resolve
end
end
- # Returns true if the attributes for const_name are not empty.
- def has_const?(const_name)
- const_attrs.each_pair do |constname, attrs|
- next unless constname == const_name
- return !attrs.empty?
+ # Scans the specified file for attributes keyed by key and stores
+ # the resulting comments in the source_file lazydoc. Returns the
+ # lazydoc.
+ 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
-
- false
+ lazydoc
end
- # Returns an array of the constant names in self, for which
- # the constant attributes are not empty.
- def const_names
- names = []
- const_attrs.each_pair do |const_name, attrs|
- names << const_name unless attrs.empty?
+ # 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 <tt>:-</tt>
+ # and <tt>:+</tt>.
+ #
+ # 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.
+ 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
- names
- end
- # Register the specified line number to self. Returns a
- # Comment object corresponding to the line.
- def register(line_number)
- comment = code_comments.find {|c| c.line_number == line_number }
+ regexp = /^(.*?)::(:-|#{key})/
+ while !scanner.eos?
+ break if scanner.skip_until(regexp) == nil
- if comment == nil
- comment = Comment.new(line_number)
- code_comments << comment
- end
-
- comment
- end
-
- def register_pattern(key, regexp, &block) # :yields: comment, match
- patterns[key] = [regexp, block]
- end
-
- def register_method_pattern(key, method, range=0..-1)
- register_pattern(key, /^\s*def\s+#{method}(\((.*?)\))?/) do |comment, match|
- args = match[2].to_s.split(',').collect do |arg|
- arg = arg.strip.upcase
- case arg
- when /^&/ then nil
- when /^\*/ then arg[1..-1] + "..."
- else arg
- end
+ 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
-
- comment.subject = args[range].join(', ')
end
- end
- # Returns true if the code_comments for source_file are frozen.
- def resolved?
- @resolved
+ scanner
end
-
- attr_writer :resolved
-
- def resolve(str=nil)
- return(false) if resolved?
-
- if str == nil
- raise ArgumentError, "no source file specified" unless source_file && File.exists?(source_file)
- str = File.read(source_file)
+
+ # 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 <tt>:-</tt>
+ # and <tt>:+</tt>.
+ #
+ # 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.
+ 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
- Lazydoc.parse(str) do |const_name, key, comment|
- attributes(const_name)[key] = comment
- end
-
- lines = str.split(/\r?\n/)
-
- patterns.each_pair do |key, (regexp, block)|
- next if default_attributes.has_key?(key)
-
- lines.each_with_index do |line, line_number|
- next unless line =~ regexp
-
- comment = register(line_number)
- default_attributes[key] = comment
- break if block.call(comment, $~)
+ 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
- end unless patterns.empty?
-
- code_comments.collect! do |comment|
- line_number = comment.line_number
- comment.subject = lines[line_number] if comment.subject == nil
-
- # remove whitespace lines
- line_number -= 1
- while lines[line_number].strip.empty?
- line_number -= 1
- end
-
- # put together the comment
- while line_number >= 0
- break unless comment.prepend(lines[line_number])
- line_number -= 1
- end
-
- comment
+ comment.subject = value
+ yield(const_name, key, comment)
end
-
- @resolved = true
end
- def to_hash
- const_hash = {}
- const_names.sort.each do |const_name|
- attr_hash = {}
- self[const_name, false].each_pair do |key, comment|
- attr_hash[key] = (block_given? ? yield(comment) : comment)
- end
- const_hash[const_name] = attr_hash
- end
- const_hash
+ def usage(path, cols=80)
+ scanner = StringScanner.new(File.read(path))
+ scanner.scan(/^#!.*?$/)
+ Comment.parse(scanner, false).wrap(cols, 2).strip
end
end
end
end
\ No newline at end of file