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 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. # # # Const::Name::key value a # # ::key value b # # doc = Document.new(__FILE__, 'Default') # doc.resolve # # 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 :::- and # :::+ 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='[a-z_]+') # :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 # 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=nil) self.source_file = source_file @default_const_name = default_const_name @comments = [] @resolved = false end # 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 # 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. Raises an error if default_const_name is # already set to a different value. def default_const_name=(input) @default_const_name = case @default_const_name when nil, input then input else raise ArgumentError, "default_const_name cannot be overridden #{source_file}: #{@default_const_name.inspect} != #{input.inspect}" end end # 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 the newly registered comment. def register(line_number, comment_class=Comment) comment = comment_class.new(line_number, self) comments << comment comment end # Registers the next comment. # # lazydoc = Document.new(__FILE__) # # c = lazydoc.register___ # # this is the comment # # that is registered # def method(a,b,c) # end # # lazydoc.resolve # # 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 |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 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, 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 lines = Utils.split_lines(str) scanner = Utils.convert_to_scanner(str) Document.scan(scanner) 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 # 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 # 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 = {} Document.const_attrs.each_pair do |const_name, attributes| next if attributes.empty? 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 end const_hash end end end