require 'lazydoc/document' module Lazydoc autoload(:Attributes, 'lazydoc/attributes') # 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+)/ module_function # 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 = Document.new(source_file) registry << lazydoc end lazydoc end # 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 # 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 # 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 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 :- # 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. 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 # 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. 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 def usage(path, cols=80) scanner = StringScanner.new(File.read(path)) scanner.scan(/^#!.*?$/) Comment.parse(scanner, false).wrap(cols, 2).strip end end