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