lib/lazydoc/document.rb in lazydoc-0.2.0 vs lib/lazydoc/document.rb in lazydoc-0.3.0

- old
+ new

@@ -1,172 +1,248 @@ require 'lazydoc/comment' require 'lazydoc/method' module Lazydoc + autoload(:Attributes, 'lazydoc/attributes') + autoload(:Arguments, 'lazydoc/arguments') + autoload(:Subject, 'lazydoc/subject') + autoload(:Trailer, 'lazydoc/trailer') - # A Document tracks constant attributes and code comments for a particular - # source file. Documents may be assigned a default_const_name to be used + # A regexp matching an attribute start or end. After a match: + # + # $1:: const_name + # $3:: key + # $4:: end flag + # + ATTRIBUTE_REGEXP = /([A-Z][A-z]*(::[A-Z][A-z]*)*)?::([a-z_]+)(-?)/ + + # A regexp matching constants from the ATTRIBUTE_REGEXP leader + CONSTANT_REGEXP = /#.*?([A-Z][A-z]*(::[A-Z][A-z]*)*)?$/ + + # 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. + CALLER_REGEXP = /^(([A-z]:)?[^:]+):(\d+)/ + + # A Document resolves constant attributes and code comments for a particular + # source file. Documents may be assigned a default_const_name to be used # when a constant attribute does not specify a constant. # - # # KeyWithConst::key value a + # # Const::Name::key value a # # ::key value b # - # doc = Document.new(__FILE__, 'DefaultConst') + # doc = Document.new(__FILE__, 'Default') # doc.resolve - # doc['KeyWithConst']['key'].value # => 'value a' - # doc['DefaultConst']['key'].value # => 'value b' # + # Document['Const::Name']['key'].value # => 'value a' + # Document['Default']['key'].value # => 'value b' + # + # As shown in the example, constant attibutes for all documents are cached in + # the class-level const_attrs hash and are normally consumed through Document + # itself. class Document + class << self + + # A nested hash of (const_name, (key, comment)) pairs tracking + # the constant attributes assigned to a constant name. + def const_attrs + @const_attrs ||= {} + end + + # Returns the hash of (key, comment) pairs for const_name stored + # in const_attrs. If no such hash exists, one will be created. + def [](const_name) + const_attrs[const_name] ||= {} + end + + # Scans the string or StringScanner for attributes matching the key + # (keys may be patterns; they are incorporated into a regexp). + # Regions delimited by the stop and start keys <tt>:::-</tt> and + # <tt>:::+</tt> are skipped. Yields each (const_name, key, value) + # triplet to the block. + # + # str = %Q{ + # # Name::Space::key value + # # ::alt alt_value + # # + # # Ignored::Attribute::not_matched value + # # :::- + # # Also::Ignored::key value + # # :::+ + # # Another::key another value + # + # Ignored::key value + # } + # + # results = [] + # Document.scan(str, 'key|alt') do |const_name, key, value| + # results << [const_name, key, value] + # end + # + # results + # # => [ + # # ['Name::Space', '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 + end # The source file for self, used during resolve attr_reader :source_file - - # An array of Comment objects identifying lines - # resolved or to-be-resolved - attr_reader :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 - + # The default constant name used when no constant name # is specified for a constant attribute attr_reader :default_const_name + # An array of Comment objects registered to self + attr_reader :comments + # Flag indicating whether or not self has been resolved attr_accessor :resolved - - def initialize(source_file=nil, default_const_name='') + + def initialize(source_file=nil, default_const_name=nil) self.source_file = source_file @default_const_name = default_const_name @comments = [] - @const_attrs = {} @resolved = false - self.reset end - - # Resets self by clearing const_attrs, comments, and setting - # resolved to false. Generally NOT recommended as this - # clears any work you've done registering lines; to simply - # allow resolve to re-scan a document, manually set - # resolved to false. - def reset - @const_attrs.clear - @comments.clear - @resolved = false - self + + # Returns the attributes for the specified const_name. If an empty + # const_name ('') is specified, and a default_const_name is set, + # the default_const_name will be used instead. + def [](const_name) + const_name = default_const_name if default_const_name && const_name == '' + Document[const_name] end - # Sets the source file for self. Expands the source file path if necessary. + # Expands and sets the source file for self. def source_file=(source_file) @source_file = source_file == nil ? nil : File.expand_path(source_file) end - - # Sets the default_const_name for self. Any const_attrs assigned to - # the previous default will be removed and merged with those already - # assigned to the new default. - def default_const_name=(const_name) - self[const_name].merge!(const_attrs.delete(@default_const_name) || {}) - @default_const_name = const_name - end - - # Returns the attributes for the specified const_name. - def [](const_name) - const_attrs[const_name] ||= {} - end - # Returns an array of the const_names in self with at - # least one attribute. - def const_names - names = [] - const_attrs.each_pair do |const_name, attrs| - names << const_name unless attrs.empty? - end - names - end - - # Register the specified line number to self. Register - # may take an integer or a regexp for late-evaluation. - # See Comment#resolve for more details. + # Registers the specified line number to self. Register may take an + # integer or a regexp for dynamic evaluation. See Comment#resolve for + # more details. # - # Returns a comment_class instance corresponding to the line. + # Returns the newly registered comment. def register(line_number, comment_class=Comment) - comment = comments.find {|c| c.class == comment_class && c.line_number == line_number } - - if comment == nil - comment = comment_class.new(line_number) - comments << comment - end - + comment = comment_class.new(line_number, self) + comments << comment comment end - # Registers a regexp matching the first method by the specified name. - def register_method(method_name, comment_class=Method) - register(Method.method_regexp(method_name), comment_class) - end - # Registers the next comment. # # lazydoc = Document.new(__FILE__) # - # lazydoc.register___ + # c = lazydoc.register___ # # this is the comment # # that is registered # def method(a,b,c) # end # # lazydoc.resolve - # m = lazydoc.comments[0] - # m.subject # => "def method(a,b,c)" - # m.to_s # => "this is the comment that is registered" # + # c.subject # => "def method(a,b,c)" + # c.comment # => "this is the comment that is registered" + # def register___(comment_class=Comment, caller_index=0) caller[caller_index] =~ CALLER_REGEXP - block = lambda do |lines| + block = lambda do |scanner, lines| n = $3.to_i n += 1 while lines[n] =~ /^\s*(#.*)?$/ n end register(block, comment_class) end - # Scans str for constant attributes and adds them to to self. Code - # comments are also resolved against str. If no str is specified, - # the contents of source_file are used instead. + # Scans str for constant attributes and adds them to Document.const_attrs. + # Comments registered with self are also resolved against str. If no str + # is specified, the contents of source_file are used instead. # - # Resolve does nothing if resolved == true. Returns true if str - # was resolved, or false otherwise. - def resolve(str=nil) - return(false) if resolved - + # Resolve does nothing if resolved == true, unless force is also specified. + # Returns true if str was resolved, or false otherwise. + def resolve(str=nil, force=false) + return false if resolved && !force + @resolved = true + str = File.read(source_file) if str == nil - Lazydoc.parse(str) do |const_name, key, comment| - const_name = default_const_name if const_name.empty? - self[const_name][key] = comment - end - - unless comments.empty? - lines = str.split(/\r?\n/) - comments.each do |comment| - comment.resolve(lines) + lines = Utils.split_lines(str) + scanner = Utils.convert_to_scanner(str) + + Document.scan(scanner, '[a-z_]+') do |const_name, key, value| + # get or initialize the comment that will be parsed + comment = (self[const_name][key] ||= Subject.new(nil, self)) + + # skip non-comment constant attributes + next unless comment.kind_of?(Comment) + + # parse the comment + comment.parse_down(scanner, lines) 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 + + # set the subject + comment.subject = value end - - @resolved = true + + # resolve registered comments + comments.each do |comment| + comment.parse_up(scanner, lines) + + n = comment.line_number + comment.subject = n.kind_of?(Integer) ? lines[n] : nil + end + + true end - - def to_hash + + # Summarizes constant attributes registered to self by collecting them + # into a nested hash of (const_name, (key, comment)) pairs. A block + # may be provided to collect values from the comments; each comment is + # yielded to the block and the return stored in it's place. + def summarize const_hash = {} - const_attrs.each_pair do |const_name, attributes| + Document.const_attrs.each_pair do |const_name, attributes| next if attributes.empty? - - attr_hash = {} + + const_hash[const_name] = attr_hash = {} attributes.each_pair do |key, comment| + next unless comment.document == self attr_hash[key] = (block_given? ? yield(comment) : comment) end - const_hash[const_name] = attr_hash end const_hash end end end