From 46580b51477355fece514573c88cb67030f4a502 Mon Sep 17 00:00:00 2001 From: drbrain Date: Thu, 1 Apr 2010 07:45:16 +0000 Subject: Import RDoc 2.5 git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@27147 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/rdoc/ri/cache.rb | 187 ------- lib/rdoc/ri/descriptions.rb | 156 ------ lib/rdoc/ri/display.rb | 392 --------------- lib/rdoc/ri/driver.rb | 1155 +++++++++++++++++++++++++++++-------------- lib/rdoc/ri/formatter.rb | 615 +---------------------- lib/rdoc/ri/gemdirs.rb | 28 -- lib/rdoc/ri/paths.rb | 148 ++++-- lib/rdoc/ri/reader.rb | 106 ---- lib/rdoc/ri/store.rb | 248 ++++++++++ lib/rdoc/ri/util.rb | 79 --- lib/rdoc/ri/writer.rb | 68 --- 11 files changed, 1116 insertions(+), 2066 deletions(-) delete mode 100644 lib/rdoc/ri/cache.rb delete mode 100644 lib/rdoc/ri/descriptions.rb delete mode 100644 lib/rdoc/ri/display.rb delete mode 100644 lib/rdoc/ri/gemdirs.rb delete mode 100644 lib/rdoc/ri/reader.rb create mode 100644 lib/rdoc/ri/store.rb delete mode 100644 lib/rdoc/ri/util.rb delete mode 100644 lib/rdoc/ri/writer.rb (limited to 'lib/rdoc/ri') diff --git a/lib/rdoc/ri/cache.rb b/lib/rdoc/ri/cache.rb deleted file mode 100644 index 06177a00de..0000000000 --- a/lib/rdoc/ri/cache.rb +++ /dev/null @@ -1,187 +0,0 @@ -require 'rdoc/ri' - -class RDoc::RI::ClassEntry - - attr_reader :name - attr_reader :path_names - - def initialize(path_name, name, in_class) - @path_names = [ path_name ] - @name = name - @in_class = in_class - @class_methods = [] - @instance_methods = [] - @inferior_classes = [] - end - - # We found this class in more than one place, so add - # in the name from there. - def add_path(path) - @path_names << path - end - - # read in our methods and any classes - # and modules in our namespace. Methods are - # stored in files called name-c|i.yaml, - # where the 'name' portion is the external - # form of the method name and the c|i is a class|instance - # flag - - def load_from(dir) - Dir.foreach(dir) do |name| - next if name =~ /^\./ - - # convert from external to internal form, and - # extract the instance/class flag - - if name =~ /^(.*?)-(c|i).yaml$/ - external_name = $1 - is_class_method = $2 == "c" - internal_name = RDoc::RI::Writer.external_to_internal(external_name) - list = is_class_method ? @class_methods : @instance_methods - path = File.join(dir, name) - list << RDoc::RI::MethodEntry.new(path, internal_name, is_class_method, self) - else - full_name = File.join(dir, name) - if File.directory?(full_name) - inf_class = @inferior_classes.find {|c| c.name == name } - if inf_class - inf_class.add_path(full_name) - else - inf_class = RDoc::RI::ClassEntry.new(full_name, name, self) - @inferior_classes << inf_class - end - inf_class.load_from(full_name) - end - end - end - end - - # Return a list of any classes or modules that we contain - # that match a given string - - def contained_modules_matching(name) - @inferior_classes.find_all {|c| c.name[name]} - end - - def classes_and_modules - @inferior_classes - end - - # Return an exact match to a particular name - def contained_class_named(name) - @inferior_classes.find {|c| c.name == name} - end - - # return the list of local methods matching name - # We're split into two because we need distinct behavior - # when called from the _toplevel_ - def methods_matching(name, is_class_method) - local_methods_matching(name, is_class_method) - end - - # Find methods matching 'name' in ourselves and in - # any classes we contain - def recursively_find_methods_matching(name, is_class_method) - res = local_methods_matching(name, is_class_method) - @inferior_classes.each do |c| - res.concat(c.recursively_find_methods_matching(name, is_class_method)) - end - res - end - - - # Return our full name - def full_name - res = @in_class.full_name - res << "::" unless res.empty? - res << @name - end - - # Return a list of all out method names - def all_method_names - res = @class_methods.map {|m| m.full_name } - @instance_methods.each {|m| res << m.full_name} - res - end - - private - - # Return a list of all our methods matching a given string. - # Is +is_class_methods+ if 'nil', we don't care if the method - # is a class method or not, otherwise we only return - # those methods that match - def local_methods_matching(name, is_class_method) - - list = case is_class_method - when nil then @class_methods + @instance_methods - when true then @class_methods - when false then @instance_methods - else fail "Unknown is_class_method: #{is_class_method.inspect}" - end - - list.find_all {|m| m.name; m.name[name]} - end -end - -## -# A TopLevelEntry is like a class entry, but when asked to search for methods -# searches all classes, not just itself - -class RDoc::RI::TopLevelEntry < RDoc::RI::ClassEntry - def methods_matching(name, is_class_method) - res = recursively_find_methods_matching(name, is_class_method) - end - - def full_name - "" - end - - def module_named(name) - - end - -end - -class RDoc::RI::MethodEntry - attr_reader :name - attr_reader :path_name - - def initialize(path_name, name, is_class_method, in_class) - @path_name = path_name - @name = name - @is_class_method = is_class_method - @in_class = in_class - end - - def full_name - res = @in_class.full_name - unless res.empty? - if @is_class_method - res << "::" - else - res << "#" - end - end - res << @name - end -end - -## -# We represent everything known about all 'ri' files accessible to this program - -class RDoc::RI::Cache - - attr_reader :toplevel - - def initialize(dirs) - # At the top level we have a dummy module holding the - # overall namespace - @toplevel = RDoc::RI::TopLevelEntry.new('', '::', nil) - - dirs.each do |dir| - @toplevel.load_from(dir) - end - end - -end diff --git a/lib/rdoc/ri/descriptions.rb b/lib/rdoc/ri/descriptions.rb deleted file mode 100644 index 467b7de2a9..0000000000 --- a/lib/rdoc/ri/descriptions.rb +++ /dev/null @@ -1,156 +0,0 @@ -require 'yaml' -require 'rdoc/markup/fragments' -require 'rdoc/ri' - -## -# Descriptions are created by RDoc (in ri_generator) and written out in -# serialized form into the documentation tree. ri then reads these to generate -# the documentation - -class RDoc::RI::NamedThing - attr_reader :name - def initialize(name) - @name = name - end - - def <=>(other) - @name <=> other.name - end - - def hash - @name.hash - end - - def eql?(other) - @name.eql?(other) - end -end - -class RDoc::RI::AliasName < RDoc::RI::NamedThing; end - -class RDoc::RI::Attribute < RDoc::RI::NamedThing - attr_reader :rw, :comment - - def initialize(name, rw, comment) - super(name) - @rw = rw - @comment = comment - end -end - -class RDoc::RI::Constant < RDoc::RI::NamedThing - attr_reader :value, :comment - - def initialize(name, value, comment) - super(name) - @value = value - @comment = comment - end -end - -class RDoc::RI::IncludedModule < RDoc::RI::NamedThing; end - -class RDoc::RI::MethodSummary < RDoc::RI::NamedThing - def initialize(name="") - super - end -end - -class RDoc::RI::Description - attr_accessor :name - attr_accessor :full_name - attr_accessor :comment - - def serialize - self.to_yaml - end - - def self.deserialize(from) - YAML.load(from) - end - - def <=>(other) - @name <=> other.name - end -end - -class RDoc::RI::ModuleDescription < RDoc::RI::Description - - attr_accessor :class_methods - attr_accessor :class_method_extensions - attr_accessor :instance_methods - attr_accessor :instance_method_extensions - attr_accessor :attributes - attr_accessor :constants - attr_accessor :includes - - # merge in another class description into this one - def merge_in(old) - merge(@class_methods, old.class_methods) - merge(@instance_methods, old.instance_methods) - merge(@attributes, old.attributes) - merge(@constants, old.constants) - merge(@includes, old.includes) - if @comment.nil? || @comment.empty? - @comment = old.comment - else - unless old.comment.nil? or old.comment.empty? then - if @comment.nil? or @comment.empty? then - @comment = old.comment - else - @comment << RDoc::Markup::Flow::RULE.new - @comment.concat old.comment - end - end - end - end - - def display_name - "Module" - end - - # the 'ClassDescription' subclass overrides this - # to format up the name of a parent - def superclass_string - nil - end - - private - - def merge(into, from) - names = {} - into.each {|i| names[i.name] = i } - from.each {|i| names[i.name] = i } - into.replace(names.keys.sort.map {|n| names[n]}) - end -end - -class RDoc::RI::ClassDescription < RDoc::RI::ModuleDescription - attr_accessor :superclass - - def display_name - "Class" - end - - def superclass_string - if @superclass && @superclass != "Object" - @superclass - else - nil - end - end -end - -class RDoc::RI::MethodDescription < RDoc::RI::Description - - attr_accessor :is_class_method - attr_accessor :visibility - attr_accessor :block_params - attr_accessor :is_singleton - attr_accessor :aliases - attr_accessor :is_alias_for - attr_accessor :params - attr_accessor :source_path - -end - diff --git a/lib/rdoc/ri/display.rb b/lib/rdoc/ri/display.rb deleted file mode 100644 index f6b647fbc1..0000000000 --- a/lib/rdoc/ri/display.rb +++ /dev/null @@ -1,392 +0,0 @@ -require 'rdoc/ri' - -# readline support might not be present, so be careful -# when requiring it. -begin - require('readline') - require('abbrev') - CAN_USE_READLINE = true # HACK use an RDoc namespace constant -rescue LoadError - CAN_USE_READLINE = false -end - -## -# This is a kind of 'flag' module. If you want to write your own 'ri' display -# module (perhaps because you're writing an IDE), you write a class which -# implements the various 'display' methods in RDoc::RI::DefaultDisplay, and -# include the RDoc::RI::Display module in that class. -# -# To access your class from the command line, you can do -# -# ruby -r ../ri .... - -module RDoc::RI::Display - - @@display_class = nil - - def self.append_features(display_class) - @@display_class = display_class - end - - def self.new(*args) - @@display_class.new(*args) - end - -end - -## -# A paging display module. Uses the RDoc::RI::Formatter class to do the actual -# presentation. - -class RDoc::RI::DefaultDisplay - - include RDoc::RI::Display - - def initialize(formatter, width, use_stdout, output = $stdout) - @use_stdout = use_stdout - @formatter = formatter.new output, width, " " - end - - ## - # Display information about +klass+. Fetches additional information from - # +ri_reader+ as necessary. - - def display_class_info(klass) - page do - superclass = klass.superclass - - if superclass - superclass = " < " + superclass - else - superclass = "" - end - - @formatter.draw_line(klass.display_name + ": " + - klass.full_name + superclass) - - display_flow(klass.comment) - @formatter.draw_line - - unless klass.includes.empty? - @formatter.blankline - @formatter.display_heading("Includes:", 2, "") - incs = [] - - klass.includes.each do |inc| - incs << inc.name - end - - @formatter.wrap(incs.sort.join(', ')) - end - - unless klass.constants.empty? - @formatter.blankline - @formatter.display_heading("Constants:", 2, "") - - constants = klass.constants.sort_by { |constant| constant.name } - - constants.each do |constant| - @formatter.wrap "#{constant.name} = #{constant.value}" - if constant.comment then - @formatter.indent do - @formatter.display_flow constant.comment - end - else - @formatter.break_to_newline - end - end - end - - unless klass.attributes.empty? then - @formatter.blankline - @formatter.display_heading 'Attributes:', 2, '' - - attributes = klass.attributes.sort_by { |attribute| attribute.name } - - attributes.each do |attribute| - if attribute.comment then - @formatter.wrap "#{attribute.name} (#{attribute.rw}):" - @formatter.indent do - @formatter.display_flow attribute.comment - end - else - @formatter.wrap "#{attribute.name} (#{attribute.rw})" - @formatter.break_to_newline - end - end - end - - return display_class_method_list(klass) - end - end - - ## - # Given a Hash mapping a class' methods to method types (returned by - # display_class_method_list), this method allows the user to - # choose one of the methods. - - def get_class_method_choice(method_map) - if CAN_USE_READLINE - # prepare abbreviations for tab completion - abbreviations = method_map.keys.abbrev - Readline.completion_proc = proc do |string| - abbreviations.values.uniq.grep(/^#{string}/) - end - end - - @formatter.raw_print_line "\nEnter the method name you want.\n" - @formatter.raw_print_line "Class methods can be preceeded by '::' and instance methods by '#'.\n" - - if CAN_USE_READLINE - @formatter.raw_print_line "You can use tab to autocomplete.\n" - @formatter.raw_print_line "Enter a blank line to exit.\n" - - choice_string = Readline.readline(">> ").strip - else - @formatter.raw_print_line "Enter a blank line to exit.\n" - @formatter.raw_print_line ">> " - choice_string = $stdin.gets.strip - end - - if choice_string == '' - return nil - else - class_or_instance = method_map[choice_string] - - if class_or_instance - # If the user's choice is not preceeded by a '::' or a '#', figure - # out whether they want a class or an instance method and decorate - # the choice appropriately. - if(choice_string =~ /^[a-zA-Z]/) - if(class_or_instance == :class) - choice_string = "::#{choice_string}" - else - choice_string = "##{choice_string}" - end - end - - return choice_string - else - @formatter.raw_print_line "No method matched '#{choice_string}'.\n" - return nil - end - end - end - - - ## - # Display methods on +klass+ - # Returns a hash mapping method name to method contents (HACK?) - - def display_class_method_list(klass) - method_map = {} - - class_data = [ - :class_methods, - :class_method_extensions, - :instance_methods, - :instance_method_extensions, - ] - - class_data.each do |data_type| - data = klass.send data_type - - unless data.nil? or data.empty? then - @formatter.blankline - - heading = data_type.to_s.split('_').join(' ').capitalize << ':' - @formatter.display_heading heading, 2, '' - - method_names = [] - data.each do |item| - method_names << item.name - - if(data_type == :class_methods || - data_type == :class_method_extensions) then - method_map["::#{item.name}"] = :class - method_map[item.name] = :class - else - # - # Since we iterate over instance methods after class methods, - # an instance method always will overwrite the unqualified - # class method entry for a class method of the same name. - # - method_map["##{item.name}"] = :instance - method_map[item.name] = :instance - end - end - method_names.sort! - - @formatter.wrap method_names.join(', ') - end - end - - method_map - end - private :display_class_method_list - - ## - # Display an Array of RDoc::Markup::Flow objects, +flow+. - - def display_flow(flow) - if flow and not flow.empty? then - @formatter.display_flow flow - else - @formatter.wrap '[no description]' - end - end - - ## - # Display information about +method+. - - def display_method_info(method) - page do - @formatter.draw_line(method.full_name) - display_params(method) - - @formatter.draw_line - display_flow(method.comment) - - if method.aliases and not method.aliases.empty? then - @formatter.blankline - aka = "(also known as #{method.aliases.map { |a| a.name }.join(', ')})" - @formatter.wrap aka - end - end - end - - ## - # Display the list of +methods+. - - def display_method_list(methods) - page do - @formatter.wrap "More than one method matched your request. You can refine your search by asking for information on one of:" - @formatter.blankline - - methods.each do |method| - @formatter.raw_print_line "#{method.full_name} [#{method.source_path}]\n" - end - end - end - - ## - # Display a list of +methods+ and allow the user to select one of them. - - def display_method_list_choice(methods) - page do - @formatter.wrap "More than one method matched your request. Please choose one of the possible matches." - @formatter.blankline - - methods.each_with_index do |method, index| - @formatter.raw_print_line "%3d %s [%s]\n" % [index + 1, method.full_name, method.source_path] - end - - @formatter.raw_print_line ">> " - - choice = $stdin.gets.strip! - - if(choice == '') - return - end - - choice = choice.to_i - - if ((choice == 0) || (choice > methods.size)) then - @formatter.raw_print_line "Invalid choice!\n" - else - method = methods[choice - 1] - display_method_info(method) - end - end - end - - ## - # Display the params for +method+. - - def display_params(method) - params = method.params - - if params[0,1] == "(" then - if method.is_singleton - params = method.full_name + params - else - params = method.name + params - end - end - - params.split(/\n/).each do |param| - @formatter.wrap param - @formatter.break_to_newline - end - - @formatter.blankline - @formatter.wrap("From #{method.source_path}") - end - - ## - # List the classes in +classes+. - - def list_known_classes(classes) - if classes.empty? - warn_no_database - else - page do - @formatter.draw_line "Known classes and modules" - @formatter.blankline - - @formatter.wrap classes.sort.join(', ') - end - end - end - - ## - # Paginates output through a pager program. - - def page - if pager = setup_pager then - begin - orig_output = @formatter.output - @formatter.output = pager - yield - ensure - @formatter.output = orig_output - pager.close - end - else - yield - end - rescue Errno::EPIPE - end - - ## - # Sets up a pager program to pass output through. - - def setup_pager - unless @use_stdout then - for pager in [ ENV['PAGER'], "less", "more", 'pager' ].compact.uniq - return IO.popen(pager, "w") rescue nil - end - @use_stdout = true - nil - end - end - - ## - # Displays a message that describes how to build RI data. - - def warn_no_database - output = @formatter.output - - output.puts "No ri data found" - output.puts - output.puts "If you've installed Ruby yourself, you need to generate documentation using:" - output.puts - output.puts " make install-doc" - output.puts - output.puts "from the same place you ran `make` to build ruby." - output.puts - output.puts "If you installed Ruby from a packaging system, then you may need to" - output.puts "install an additional package, or ask the packager to enable ri generation." - end - -end diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb index 89534a5972..1829de3432 100644 --- a/lib/rdoc/ri/driver.rb +++ b/lib/rdoc/ri/driver.rb @@ -1,89 +1,77 @@ +require 'abbrev' require 'optparse' -require 'yaml' + +begin + require 'readline' +rescue LoadError +end require 'rdoc/ri' require 'rdoc/ri/paths' -require 'rdoc/ri/formatter' -require 'rdoc/ri/display' -require 'fileutils' require 'rdoc/markup' -require 'rdoc/markup/to_flow' +require 'rdoc/markup/formatter' +require 'rdoc/text' -class RDoc::RI::Driver +## +# For RubyGems backwards compatibility - # - # This class offers both Hash and OpenStruct functionality. - # We convert from the Core Hash to this before calling any of - # the display methods, in order to give the display methods - # a cleaner API for accessing the data. - # - class OpenStructHash < Hash - # - # This method converts from a Hash to an OpenStructHash. - # - def self.convert(object) - case object - when Hash then - new_hash = new # Convert Hash -> OpenStructHash - - object.each do |key, value| - new_hash[key] = convert(value) - end +require 'rdoc/ri/formatter' - new_hash - when Array then - object.map do |element| - convert(element) - end - else - object - end - end - - def merge_enums(other) - other.each do |k, v| - if self[k] then - case v - when Array then - # HACK dunno - if String === self[k] and self[k].empty? then - self[k] = v - else - self[k] += v - end - when Hash then - self[k].update v - else - # do nothing - end - else - self[k] = v - end - end - end +## +# The RI driver implements the command-line ri tool. +# +# The driver supports: +# * loading RI data from: +# * Ruby's standard library +# * RubyGems +# * ~/.rdoc +# * A user-supplied directory +# * Paging output (uses RI_PAGER environment variable, PAGER environment +# variable or the less, more and pager programs) +# * Interactive mode with tab-completion +# * Abbreviated names (ri Zl shows Zlib documentation) +# * Colorized output +# * Merging output from multiple RI data sources - def method_missing method, *args - self[method.to_s] - end - end +class RDoc::RI::Driver + + ## + # Base Driver error class class Error < RDoc::RI::Error; end + ## + # Raised when a name isn't found in the ri data stores + class NotFoundError < Error - def message + + ## + # Name that wasn't found + + alias name message + + def message # :nodoc: "Nothing known about #{super}" end end - attr_accessor :homepath # :nodoc: + attr_accessor :stores + + ## + # Controls the user of the pager vs $stdout + + attr_accessor :use_stdout + + ## + # Default options for ri def self.default_options options = {} options[:use_stdout] = !$stdout.tty? options[:width] = 72 - options[:formatter] = RDoc::RI::Formatter.for 'plain' options[:interactive] = false options[:use_cache] = true + options[:profile] = false # By default all standard paths are used. options[:use_system] = true @@ -95,27 +83,33 @@ class RDoc::RI::Driver return options end - def self.process_args(argv) + ## + # Dump +data_path+ using pp + + def self.dump data_path + require 'pp' + + open data_path, 'rb' do |io| + pp Marshal.load(io.read) + end + end + + ## + # Parses +argv+ and returns a Hash of options + + def self.process_args argv options = default_options opts = OptionParser.new do |opt| + opt.accept File do |file,| + File.readable?(file) and not File.directory?(file) and file + end + opt.program_name = File.basename $0 opt.version = RDoc::VERSION opt.release = nil opt.summary_indent = ' ' * 4 - directories = [ - RDoc::RI::Paths::SYSDIR, - RDoc::RI::Paths::SITEDIR, - RDoc::RI::Paths::HOMEDIR - ] - - if RDoc::RI::Paths::GEMDIRS then - Gem.path.each do |dir| - directories << "#{dir}/doc/*/ri" - end - end - opt.banner = <<-EOT Usage: #{opt.program_name} [options] [names...] @@ -142,9 +136,9 @@ punctuation: #{opt.program_name} 'Array.[]' #{opt.program_name} compact\\! -By default ri searches for documentation in the following directories: +To see the default directories ri will search, run: - #{directories.join "\n "} + #{opt.program_name} --list-doc-dirs Specifying the --system, --site, --home, --gems or --doc-dir options will limit ri to searching only the specified directories. @@ -154,17 +148,60 @@ Options may also be set in the 'RI' environment variable. opt.separator nil opt.separator "Options:" + opt.separator nil - opt.on("--fmt=FORMAT", "--format=FORMAT", "-f", - RDoc::RI::Formatter::FORMATTERS.keys, - "Format to use when displaying output:", - " #{RDoc::RI::Formatter.list}", - "Use 'bs' (backspace) with most pager", - "programs. To use ANSI, either disable the", - "pager or tell the pager to allow control", - "characters.") do |value| - options[:formatter] = RDoc::RI::Formatter.for value + formatters = RDoc::Markup.constants.grep(/^To[A-Z][a-z]+$/).sort + formatters = formatters.sort.map do |formatter| + formatter.to_s.sub('To', '').downcase + end + + opt.on("--format=NAME", "-f", + "Uses the selected formatter. The default", + "formatter is bs for paged output and ansi", + "otherwise. Valid formatters are:", + formatters.join(' '), formatters) do |value| + options[:formatter] = RDoc::Markup.const_get "To#{value.capitalize}" + end + + opt.separator nil + + opt.on("--no-pager", "-T", + "Send output directly to stdout,", + "rather than to a pager.") do + options[:use_stdout] = true + end + + opt.separator nil + + opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger, + "Set the width of the output.") do |value| + options[:width] = value + end + + opt.separator nil + + opt.on("--interactive", "-i", + "In interactive mode you can repeatedly", + "look up methods with autocomplete.") do + options[:interactive] = true + end + + opt.separator nil + + opt.on("--[no-]profile", + "Run with the ruby profiler") do |value| + options[:profile] = value + end + + opt.separator nil + opt.separator "Data source options:" + opt.separator nil + + opt.on("--list-doc-dirs", + "List the directories from which ri will", + "source documentation on stdout and exit.") do + options[:list_doc_dirs] = true end opt.separator nil @@ -184,21 +221,11 @@ Options may also be set in the 'RI' environment variable. opt.separator nil - opt.on("--[no-]use-cache", - "Whether or not to use ri's cache.", - "True by default.") do |value| - options[:use_cache] = value - end - - opt.separator nil - opt.on("--no-standard-docs", "Do not include documentation from", "the Ruby standard library, site_lib,", "installed gems, or ~/.rdoc.", - "Equivalent to specifying", - "the options --no-system, --no-site, --no-gems,", - "and --no-home") do + "Use with --doc-dir") do options[:use_system] = false options[:use_site] = false options[:use_gems] = false @@ -239,38 +266,12 @@ Options may also be set in the 'RI' environment variable. end opt.separator nil - - opt.on("--list-doc-dirs", - "List the directories from which ri will", - "source documentation on stdout and exit.") do - options[:list_doc_dirs] = true - end - - opt.separator nil - - opt.on("--no-pager", "-T", - "Send output directly to stdout,", - "rather than to a pager.") do - options[:use_stdout] = true - end - - opt.on("--interactive", "-i", - "This makes ri go into interactive mode.", - "When ri is in interactive mode it will", - "allow the user to disambiguate lists of", - "methods in case multiple methods match", - "against a method search string. It also", - "will allow the user to enter in a method", - "name (with auto-completion, if readline", - "is supported) when viewing a class.") do - options[:interactive] = true - end - + opt.separator "Debug options:" opt.separator nil - opt.on("--width=WIDTH", "-w", OptionParser::DecimalInteger, - "Set the width of the output.") do |value| - options[:width] = value + opt.on("--dump=CACHE", File, + "Dumps data from an ri cache or data file") do |value| + options[:dump_path] = value end end @@ -280,7 +281,6 @@ Options may also be set in the 'RI' environment variable. options[:names] = argv - options[:formatter] ||= RDoc::RI::Formatter.for('plain') options[:use_stdout] ||= !$stdout.tty? options[:use_stdout] ||= options[:interactive] options[:width] ||= 72 @@ -294,376 +294,765 @@ Options may also be set in the 'RI' environment variable. exit 1 end - def self.run(argv = ARGV) + ## + # Runs the ri command line executable using +argv+ + + def self.run argv = ARGV options = process_args argv + + if options[:dump_path] then + dump options[:dump_path] + return + end + ri = new options ri.run end - def initialize(initial_options={}) + ## + # Creates a new driver using +initial_options+ from ::process_args + + def initialize initial_options = {} + @paging = false + @classes = nil + options = self.class.default_options.update(initial_options) + @formatter_klass = options[:formatter] + + require 'profile' if options[:profile] + @names = options[:names] - @class_cache_name = 'classes' - @doc_dirs = RDoc::RI::Paths.path(options[:use_system], - options[:use_site], - options[:use_home], - options[:use_gems], - options[:extra_doc_dirs]) + @doc_dirs = [] + @stores = [] - @homepath = RDoc::RI::Paths.raw_path(false, false, true, false).first - @homepath = @homepath.sub(/\.rdoc/, '.ri') - @sys_dir = RDoc::RI::Paths.raw_path(true, false, false, false).first - @list_doc_dirs = options[:list_doc_dirs] + RDoc::RI::Paths.each(options[:use_system], options[:use_site], + options[:use_home], options[:use_gems], + *options[:extra_doc_dirs]) do |path, type| + @doc_dirs << path - FileUtils.mkdir_p cache_file_path unless File.directory? cache_file_path - @cache_doc_dirs_path = File.join cache_file_path, ".doc_dirs" + store = RDoc::RI::Store.new path, type + store.load_cache + @stores << store + end - @use_cache = options[:use_cache] - @class_cache = nil + @list_doc_dirs = options[:list_doc_dirs] @interactive = options[:interactive] - @display = RDoc::RI::DefaultDisplay.new(options[:formatter], - options[:width], - options[:use_stdout]) + @use_stdout = options[:use_stdout] end - def class_cache - return @class_cache if @class_cache + ## + # Adds paths for undocumented classes +also_in+ to +out+ - # Get the documentation directories used to make the cache in order to see - # whether the cache is valid for the current ri instantiation. - if(File.readable?(@cache_doc_dirs_path)) - cache_doc_dirs = IO.read(@cache_doc_dirs_path).split("\n") - else - cache_doc_dirs = [] - end - - newest = map_dirs('created.rid') do |f| - File.mtime f if test ?f, f - end.max - - # An up to date cache file must have been created more recently than - # the last modification of any of the documentation directories. It also - # must have been created with the same documentation directories - # as those from which ri currently is sourcing documentation. - up_to_date = (File.exist?(class_cache_file_path) and - newest and newest < File.mtime(class_cache_file_path) and - (cache_doc_dirs == @doc_dirs)) - - if up_to_date and @use_cache then - open class_cache_file_path, 'rb' do |fp| - begin - @class_cache = Marshal.load fp.read - rescue - # - # This shouldn't be necessary, since the up_to_date logic above - # should force the cache to be recreated when a new version of - # rdoc is installed. This seems like a worthwhile enhancement - # to ri's robustness, however. - # - $stderr.puts "Error reading the class cache; recreating the class cache!" - @class_cache = create_class_cache - end - end - else - @class_cache = create_class_cache + def add_also_in out, also_in + return if also_in.empty? + + out << RDoc::Markup::Rule.new(1) + out << RDoc::Markup::Paragraph.new("Also found in:") + + paths = RDoc::Markup::Verbatim.new + also_in.each do |store| + paths.parts.push ' ', store.friendly_path, "\n" end + out << paths + end - @class_cache + ## + # Adds a class header to +out+ for class +name+ which is described in + # +classes+. + + def add_class out, name, classes + heading = if classes.all? { |klass| klass.module? } then + name + else + superclass = classes.map do |klass| + klass.superclass unless klass.module? + end.compact.shift || 'Object' + + "#{name} < #{superclass}" + end + + out << RDoc::Markup::Heading.new(1, heading) + out << RDoc::Markup::BlankLine.new end - def create_class_cache - class_cache = OpenStructHash.new + ## + # Adds "(from ...)" to +out+ for +store+ - if(@use_cache) - # Dump the documentation directories to a file in the cache, so that - # we only will use the cache for future instantiations with identical - # documentation directories. - File.open @cache_doc_dirs_path, "wb" do |fp| - fp << @doc_dirs.join("\n") - end - end + def add_from out, store + out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})") + end - classes = map_dirs('**/cdesc*.yaml') { |f| Dir[f] } - warn "Updating class cache with #{classes.size} classes..." - populate_class_cache class_cache, classes + ## + # Adds +includes+ to +out+ - write_cache class_cache, class_cache_file_path + def add_includes out, includes + return if includes.empty? - class_cache - end + out << RDoc::Markup::Rule.new(1) + out << RDoc::Markup::Heading.new(1, "Includes:") - def populate_class_cache(class_cache, classes, extension = false) - classes.each do |cdesc| - desc = read_yaml cdesc - klassname = desc["full_name"] + includes.each do |modules, store| + if modules.length == 1 then + include = modules.first + name = include.name + path = store.friendly_path + out << RDoc::Markup::Paragraph.new("#{name} (from #{path})") - unless class_cache.has_key? klassname then - desc["display_name"] = "Class" - desc["sources"] = [cdesc] - desc["instance_method_extensions"] = [] - desc["class_method_extensions"] = [] - class_cache[klassname] = desc + if include.comment then + out << RDoc::Markup::BlankLine.new + out << include.comment + end else - klass = class_cache[klassname] + out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})") + + wout, with = modules.partition { |incl| incl.comment.empty? } - if extension then - desc["instance_method_extensions"] = desc.delete "instance_methods" - desc["class_method_extensions"] = desc.delete "class_methods" + out << RDoc::Markup::BlankLine.new unless with.empty? + + with.each do |incl| + out << RDoc::Markup::Paragraph.new(incl.name) + out << RDoc::Markup::BlankLine.new + out << incl.comment end - klass.merge_enums desc - klass["sources"] << cdesc + unless wout.empty? then + verb = RDoc::Markup::Verbatim.new + + wout.each do |incl| + verb.push ' ', incl.name, "\n" + end + + out << verb + end end end end - def class_cache_file_path - File.join cache_file_path, @class_cache_name + ## + # Adds a list of +methods+ to +out+ with a heading of +name+ + + def add_method_list out, methods, name + return unless methods + + out << RDoc::Markup::Heading.new(1, "#{name}:") + out << RDoc::Markup::BlankLine.new + + out.push(*methods.map do |method| + RDoc::Markup::Verbatim.new ' ', method + end) + + out << RDoc::Markup::BlankLine.new end - def cache_file_for(klassname) - File.join cache_file_path, klassname.gsub(/:+/, "-") + ## + # Returns ancestor classes of +klass+ + + def ancestors_of klass + ancestors = [] + + unexamined = [klass] + seen = [] + + loop do + break if unexamined.empty? + current = unexamined.shift + seen << current + + stores = classes[current] + + break unless stores and not stores.empty? + + klasses = stores.map do |store| + store.ancestors[current] + end.flatten.uniq + + klasses = klasses - seen + + ancestors.push(*klasses) + unexamined.push(*klasses) + end + + ancestors.reverse end - def cache_file_path - File.join @homepath, 'cache' + ## + # For RubyGems backwards compatibility + + def class_cache # :nodoc: end - def display_class(name) - klass = class_cache[name] - @display.display_class_info klass + ## + # Hash mapping a known class or module to the stores it can be loaded from + + def classes + return @classes if @classes + + @classes = {} + + @stores.each do |store| + store.cache[:modules].each do |mod| + # using default block causes searched-for modules to be added + @classes[mod] ||= [] + @classes[mod] << store + end + end + + @classes end - def display_method(method) - @display.display_method_info method + ## + # Completes +name+ based on the caches. For Readline + + def complete name + klasses = classes.keys + completions = [] + + klass, selector, method = parse_name name + + # may need to include Foo when given Foo:: + klass_name = method ? name : klass + + if name !~ /#|\./ then + completions.push(*klasses.grep(/^#{klass_name}/)) + elsif selector then + completions << klass if classes.key? klass + elsif classes.key? klass_name then + completions << klass_name + end + + if completions.include? klass and name =~ /#|\.|::/ then + methods = list_methods_matching name + + if not methods.empty? then + # remove Foo if given Foo:: and a method was found + completions.delete klass + elsif selector then + # replace Foo with Foo:: as given + completions.delete klass + completions << "#{klass}#{selector}" + end + + completions.push(*methods) + end + + completions.sort end - def get_info_for(arg) - @names = [arg] - run + ## + # Converts +document+ to text and writes it to the pager + + def display document + page do |io| + text = document.accept formatter + + io.write text + end end - def load_cache_for(klassname) - path = cache_file_for klassname + ## + # Outputs formatted RI data for class +name+. Groups undocumented classes - cache = nil + def display_class name + return if name =~ /#|\./ - if File.exist? path and - File.mtime(path) >= File.mtime(class_cache_file_path) and - @use_cache then - open path, 'rb' do |fp| - begin - cache = Marshal.load fp.read - rescue - # - # The cache somehow is bad. Recreate the cache. - # - $stderr.puts "Error reading the cache for #{klassname}; recreating the cache!" - cache = create_cache_for klassname, path - end + klasses = [] + includes = [] + + found = @stores.map do |store| + begin + klass = store.load_class name + klasses << klass + includes << [klass.includes, store] if klass.includes + [store, klass] + rescue Errno::ENOENT end - else - cache = create_cache_for klassname, path + end.compact + + return if found.empty? + + also_in = [] + + includes.reject! do |modules,| modules.empty? end + + out = RDoc::Markup::Document.new + + add_class out, name, klasses + + add_includes out, includes + + found.each do |store, klass| + comment = klass.comment + class_methods = store.class_methods[klass.full_name] + instance_methods = store.instance_methods[klass.full_name] + attributes = store.attributes[klass.full_name] + + if comment.empty? and !(instance_methods or class_methods) then + also_in << store + next + end + + add_from out, store + + unless comment.empty? then + out << RDoc::Markup::Rule.new(1) + out << comment + end + + if class_methods or instance_methods or not klass.constants.empty? then + out << RDoc::Markup::Rule.new + end + + unless klass.constants.empty? then + out << RDoc::Markup::Heading.new(1, "Constants:") + out << RDoc::Markup::BlankLine.new + list = RDoc::Markup::List.new :NOTE + + constants = klass.constants.sort_by { |constant| constant.name } + + list.push(*constants.map do |constant| + parts = constant.comment.parts if constant.comment + parts << RDoc::Markup::Paragraph.new('[not documented]') if + parts.empty? + + RDoc::Markup::ListItem.new(constant.name, *parts) + end) + + out << list + end + + add_method_list out, class_methods, 'Class methods' + add_method_list out, instance_methods, 'Instance methods' + add_method_list out, attributes, 'Attributes' + + out << RDoc::Markup::BlankLine.new end - cache + add_also_in out, also_in + + display out end - def create_cache_for(klassname, path) - klass = class_cache[klassname] - return nil unless klass + ## + # Outputs formatted RI data for method +name+ + + def display_method name + found = load_methods_matching name - method_files = klass["sources"] - cache = OpenStructHash.new + raise NotFoundError, name if found.empty? - method_files.each do |f| - system_file = f.index(@sys_dir) == 0 - Dir[File.join(File.dirname(f), "*")].each do |yaml| - next unless yaml =~ /yaml$/ - next if yaml =~ /cdesc-[^\/]+yaml$/ + out = RDoc::Markup::Document.new - method = read_yaml yaml + out << RDoc::Markup::Heading.new(1, name) + out << RDoc::Markup::BlankLine.new - if system_file then - method["source_path"] = "Ruby #{RDoc::RI::Paths::VERSION}" - else - if(f =~ %r%gems/[\d.]+/doc/([^/]+)%) then - ext_path = "gem #{$1}" - else - ext_path = f - end + found.each do |store, methods| + methods.each do |method| + out << RDoc::Markup::Paragraph.new("(from #{store.friendly_path})") + + unless name =~ /^#{Regexp.escape method.parent_name}/ then + out << RDoc::Markup::Heading.new(3, "Implementation from #{method.parent_name}") + end + out << RDoc::Markup::Rule.new(1) + + if method.call_seq then + call_seq = method.call_seq.chomp.split "\n" + call_seq = call_seq.map { |line| [' ', line, "\n"] } + out << RDoc::Markup::Verbatim.new(*call_seq.flatten) + end - method["source_path"] = ext_path + if method.block_params then + out << RDoc::Markup::BlankLine.new if method.call_seq + params = "yields: #{method.block_params}" + out << RDoc::Markup::Verbatim.new(' ', params, "\n") end - name = method["full_name"] - cache[name] = method + out << RDoc::Markup::Rule.new(1) if + method.call_seq or method.block_params + + out << RDoc::Markup::BlankLine.new + out << method.comment + out << RDoc::Markup::BlankLine.new end end - write_cache cache, path + display out + end + + ## + # Outputs formatted RI data for the class or method +name+. + # + # Returns true if +name+ was found, false if it was not an alternative could + # be guessed, raises an error if +name+ couldn't be guessed. + + def display_name name + return true if display_class name + + display_method name if name =~ /::|#|\./ + + true + rescue NotFoundError + matches = list_methods_matching name if name =~ /::|#|\./ + matches = classes.keys.grep(/^#{name}/) if matches.empty? + + raise if matches.empty? + + page do |io| + io.puts "#{name} not found, maybe you meant:" + io.puts + io.puts matches.join("\n") + end + + false end ## - # Finds the next ancestor of +orig_klass+ after +klass+. + # Displays each name in +name+ + + def display_names names + names.each do |name| + name = expand_name name - def lookup_ancestor(klass, orig_klass) - # This is a bit hacky, but ri will go into an infinite - # loop otherwise, since Object has an Object ancestor - # for some reason. Depending on the documentation state, I've seen - # Kernel as an ancestor of Object and not as an ancestor of Object. - if ((orig_klass == "Object") && - ((klass == "Kernel") || (klass == "Object"))) - return nil + display_name name end + end + ## + # Expands abbreviated klass +klass+ into a fully-qualified class. "Zl::Da" + # will be expanded to Zlib::DataError. + + def expand_class klass + klass.split('::').inject '' do |expanded, klass_part| + expanded << '::' unless expanded.empty? + short = expanded << klass_part - cache = class_cache[orig_klass] + subset = classes.keys.select do |klass_name| + klass_name =~ /^#{expanded}[^:]*$/ + end - return nil unless cache + abbrevs = Abbrev.abbrev subset - ancestors = [orig_klass] - ancestors.push(*cache.includes.map { |inc| inc['name'] }) - ancestors << cache.superclass + expanded = abbrevs[short] - ancestor_index = ancestors.index(klass) + raise NotFoundError, short unless expanded - if ancestor_index - ancestor = ancestors[ancestors.index(klass) + 1] - return ancestor if ancestor + expanded.dup end + end + + ## + # Expands the class portion of +name+ into a fully-qualified class. See + # #expand_class. + + def expand_name name + klass, selector, method = parse_name name + + return [selector, method].join if klass.empty? - lookup_ancestor klass, cache.superclass + "#{expand_class klass}#{selector}#{method}" end ## - # Finds the method + # Yields items matching +name+ including the store they were found in, the + # class being searched for, the class they were found in (an ancestor) the + # types of methods to look up (from #method_type), and the method name being + # searched for + + def find_methods name + klass, selector, method = parse_name name + + types = method_type selector + + klasses = nil + ambiguous = klass.empty? + + if ambiguous then + klasses = classes.keys + else + klasses = ancestors_of klass + klasses.unshift klass + end + + methods = [] + + klasses.each do |ancestor| + ancestors = classes[ancestor] + + next unless ancestors + + klass = ancestor if ambiguous + + ancestors.each do |store| + methods << [store, klass, ancestor, types, method] + end + end + + methods = methods.sort_by do |_, k, a, _, m| + [k, a, m].compact + end - def lookup_method(name, klass) - cache = load_cache_for klass - return nil unless cache + methods.each do |item| + yield(*item) + end - method = cache[name.gsub('.', '#')] - method = cache[name.gsub('.', '::')] unless method - method + self end - def map_dirs(file_name) - @doc_dirs.map { |dir| yield File.join(dir, file_name) }.flatten.compact + ## + # Creates a new RDoc::Markup::Formatter. If a formatter is given with -f, + # use it. If we're outputting to a pager, use bs, otherwise ansi. + + def formatter + if @formatter_klass then + @formatter_klass.new + elsif paging? then + RDoc::Markup::ToBs.new + else + RDoc::Markup::ToAnsi.new + end end ## - # Extract the class and method name parts from +name+ like Foo::Bar#baz + # Runs ri interactively using Readline if it is available. - def parse_name(name) - parts = name.split(/(::|\#|\.)/) + def interactive + puts "\nEnter the method name you want to look up." - if parts[-2] != '::' or parts.last !~ /^[A-Z]/ then - meth = parts.pop - parts.pop + if defined? Readline then + Readline.completion_proc = method :complete + puts "You can use tab to autocomplete." end - klass = parts.join + puts "Enter a blank line to exit.\n\n" - [klass, meth] - end + loop do + name = if defined? Readline then + Readline.readline ">> " + else + print ">> " + $stdin.gets + end + + return if name.nil? or name.empty? - def read_yaml(path) - data = File.read path + name = expand_name name.strip + + begin + display_name name + rescue NotFoundError => e + puts e.message + end + end - # Necessary to be backward-compatible with documentation generated - # by earliar RDoc versions. - data = data.gsub(/ \!ruby\/(object|struct):(RDoc::RI|RI).*/, '') - data = data.gsub(/ \!ruby\/(object|struct):SM::(\S+)/, - ' !ruby/\1:RDoc::Markup::\2') - OpenStructHash.convert(YAML.load(data)) + rescue Interrupt + exit end - def run - if(@list_doc_dirs) - puts @doc_dirs.join("\n") - elsif @names.empty? then - @display.list_known_classes class_cache.keys.sort - else - @names.each do |name| - if class_cache.key? name then - method_map = display_class name - if(@interactive) - method_name = @display.get_class_method_choice(method_map) - - if(method_name != nil) - method = lookup_method "#{name}#{method_name}", name - display_method method - end - end - elsif name =~ /::|\#|\./ then - klass, = parse_name name + ## + # Lists classes known to ri - orig_klass = klass - orig_name = name + def list_known_classes + classes = [] - loop do - method = lookup_method name, klass + stores.each do |store| + classes << store.modules + end - break method if method + classes = classes.flatten.uniq.sort - ancestor = lookup_ancestor klass, orig_klass + page do |io| + if paging? or io.tty? then + io.puts "Classes and Modules known to ri:" + io.puts + end - break unless ancestor + io.puts classes.join("\n") + end + end - name = name.sub klass, ancestor - klass = ancestor - end + ## + # Returns an Array of methods matching +name+ + + def list_methods_matching name + found = [] + + find_methods name do |store, klass, ancestor, types, method| + if types == :instance or types == :both then + methods = store.instance_methods[ancestor] - raise NotFoundError, orig_name unless method - - display_method method - else - methods = select_methods(/#{name}/) - - if methods.size == 0 - raise NotFoundError, name - elsif methods.size == 1 - display_method methods[0] - else - if(@interactive) - @display.display_method_list_choice methods - else - @display.display_method_list methods - end + if methods then + matches = methods.grep(/^#{method}/) + + matches = matches.map do |match| + "#{klass}##{match}" end + + found.push(*matches) end end + + if types == :class or types == :both then + methods = store.class_methods[ancestor] + + next unless methods + matches = methods.grep(/^#{method}/) + + matches = matches.map do |match| + "#{klass}::#{match}" + end + + found.push(*matches) + end end - rescue NotFoundError => e - abort e.message + + found.uniq end - def select_methods(pattern) - methods = [] - class_cache.keys.sort.each do |klass| - class_cache[klass]["instance_methods"].map{|h|h["name"]}.grep(pattern) do |name| - method = load_cache_for(klass)[klass+'#'+name] - methods << method if method - end - class_cache[klass]["class_methods"].map{|h|h["name"]}.grep(pattern) do |name| - method = load_cache_for(klass)[klass+'::'+name] - methods << method if method + ## + # Loads RI data for method +name+ on +klass+ from +store+. +type+ and + # +cache+ indicate if it is a class or instance method. + + def load_method store, cache, klass, type, name + methods = store.send(cache)[klass] + + return unless methods + + method = methods.find do |method_name| + method_name == name + end + + return unless method + + store.load_method klass, "#{type}#{method}" + end + + ## + # Returns an Array of RI data for methods matching +name+ + + def load_methods_matching name + found = [] + + find_methods name do |store, klass, ancestor, types, method| + methods = [] + + methods << load_method(store, :class_methods, ancestor, '::', method) if + types == :class or types == :both + + methods << load_method(store, :instance_methods, ancestor, '#', method) if + types == :instance or types == :both + + found << [store, methods.compact] + end + + found.reject do |path, methods| methods.empty? end + end + + ## + # Returns the type of method (:both, :instance, :class) for +selector+ + + def method_type selector + case selector + when '.', nil then :both + when '#' then :instance + else :class + end + end + + ## + # Paginates output through a pager program. + + def page + if pager = setup_pager then + begin + yield pager + ensure + pager.close end + else + yield $stdout end - methods + rescue Errno::EPIPE + ensure + @paging = false + end + + ## + # Are we using a pager? + + def paging? + @paging end - def write_cache(cache, path) - if(@use_cache) - File.open path, "wb" do |cache_file| - Marshal.dump cache, cache_file + ## + # Extract the class, selector and method name parts from +name+ like + # Foo::Bar#baz. + # + # NOTE: Given Foo::Bar, Bar is considered a class even though it may be a + # method + + def parse_name(name) + parts = name.split(/(::|#|\.)/) + + if parts.length == 1 then + if parts.first =~ /^[a-z]/ then + type = '.' + meth = parts.pop + else + type = nil + meth = nil end + elsif parts.length == 2 or parts.last =~ /::|#|\./ then + type = parts.pop + meth = nil + elsif parts[-2] != '::' or parts.last !~ /^[A-Z]/ then + meth = parts.pop + type = parts.pop + end + + klass = parts.join + + [klass, type, meth] + end + + ## + # Looks up and displays ri data according to the options given. + + def run + if @list_doc_dirs then + puts @doc_dirs + elsif @interactive then + interactive + elsif @names.empty? then + list_known_classes + else + display_names @names + end + rescue NotFoundError => e + abort e.message + end + + ## + # Sets up a pager program to pass output through. Tries the RI_PAGER and + # PAGER environment variables followed by pager, less then more. + + def setup_pager + return if @use_stdout + + pagers = [ENV['RI_PAGER'], ENV['PAGER'], 'pager', 'less', 'more'] + + pagers.compact.uniq.each do |pager| + io = IO.popen(pager, "w") rescue next + + @paging = true + + return io end - cache + @use_stdout = true + + nil end end + diff --git a/lib/rdoc/ri/formatter.rb b/lib/rdoc/ri/formatter.rb index 258907d141..84d37a9d31 100644 --- a/lib/rdoc/ri/formatter.rb +++ b/lib/rdoc/ri/formatter.rb @@ -1,616 +1,5 @@ -require 'rdoc/ri' -require 'rdoc/markup' - -class RDoc::RI::Formatter - - attr_writer :indent - attr_accessor :output - - FORMATTERS = { } - - def self.for(name) - FORMATTERS[name.downcase] - end - - def self.list - FORMATTERS.keys.sort.join ", " - end - - def initialize(output, width, indent) - @output = output - @width = width - @indent = indent - @original_indent = indent.dup - end - - def draw_line(label=nil) - len = @width - len -= (label.size + 1) if label - - if len > 0 then - @output.print '-' * len - if label - @output.print ' ' - bold_print label - end - - @output.puts - else - @output.print '-' * @width - @output.puts - - @output.puts label - end - end - - def indent - return @indent unless block_given? - - begin - indent = @indent.dup - @indent += @original_indent - yield - ensure - @indent = indent - end - end - - def wrap(txt, prefix=@indent, linelen=@width) - return unless txt && !txt.empty? - - work = conv_markup(txt) - textLen = linelen - prefix.length - patt = Regexp.new("^(.{0,#{textLen}})[ \n]") - next_prefix = prefix.tr("^ ", " ") - - res = [] - - while work.length > textLen - if work =~ patt - res << $1 - work.slice!(0, $&.length) - else - res << work.slice!(0, textLen) - end - end - res << work if work.length.nonzero? - @output.puts(prefix + res.join("\n" + next_prefix)) - end - - def blankline - @output.puts - end - - ## - # Called when we want to ensure a new 'wrap' starts on a newline. Only - # needed for HtmlFormatter, because the rest do their own line breaking. - - def break_to_newline - end - - def bold_print(txt) - @output.print txt - end - - def raw_print_line(txt) - @output.print txt - end - - ## - # Convert HTML entities back to ASCII - - def conv_html(txt) - txt = txt.gsub(/>/, '>') - txt.gsub!(/</, '<') - txt.gsub!(/"/, '"') - txt.gsub!(/&/, '&') - txt - end - - ## - # Convert markup into display form - - def conv_markup(txt) - txt = txt.gsub(%r{(.*?)}, '+\1+') - txt.gsub!(%r{(.*?)}, '+\1+') - txt.gsub!(%r{(.*?)}, '*\1*') - txt.gsub!(%r{(.*?)}, '_\1_') - txt - end - - def display_list(list) - case list.type - when :BULLET - prefixer = proc { |ignored| @indent + "* " } - - when :NUMBER, :UPPERALPHA, :LOWERALPHA then - start = case list.type - when :NUMBER then 1 - when :UPPERALPHA then 'A' - when :LOWERALPHA then 'a' - end - - prefixer = proc do |ignored| - res = @indent + "#{start}.".ljust(4) - start = start.succ - res - end - - when :LABELED, :NOTE then - longest = 0 - - list.contents.each do |item| - if RDoc::Markup::Flow::LI === item and item.label.length > longest then - longest = item.label.length - end - end - - longest += 1 - - prefixer = proc { |li| @indent + li.label.ljust(longest) } - - else - raise ArgumentError, "unknown list type #{list.type}" - end - - list.contents.each do |item| - if RDoc::Markup::Flow::LI === item then - prefix = prefixer.call item - display_flow_item item, prefix - else - display_flow_item item - end - end - end - - def display_flow_item(item, prefix = @indent) - case item - when RDoc::Markup::Flow::P, RDoc::Markup::Flow::LI - wrap(conv_html(item.body), prefix) - blankline - - when RDoc::Markup::Flow::LIST - display_list(item) - - when RDoc::Markup::Flow::VERB - display_verbatim_flow_item(item, @indent) - - when RDoc::Markup::Flow::H - display_heading(conv_html(item.text), item.level, @indent) - - when RDoc::Markup::Flow::RULE - draw_line - - else - raise RDoc::Error, "Unknown flow element: #{item.class}" - end - end - - def display_verbatim_flow_item(item, prefix=@indent) - item.body.split(/\n/).each do |line| - @output.print @indent, conv_html(line), "\n" - end - blankline - end - - def display_heading(text, level, indent) - text = strip_attributes text - - case level - when 1 then - ul = "=" * text.length - @output.puts - @output.puts text.upcase - @output.puts ul - - when 2 then - ul = "-" * text.length - @output.puts - @output.puts text - @output.puts ul - else - @output.print indent, text, "\n" - end - - @output.puts - end - - def display_flow(flow) - flow.each do |f| - display_flow_item(f) - end - end - - def strip_attributes(text) - text.gsub(/(<\/?(?:b|code|em|i|tt)>)/, '') - end - -end - -## -# Handle text with attributes. We're a base class: there are different -# presentation classes (one, for example, uses overstrikes to handle bold and -# underlining, while another using ANSI escape sequences. - -class RDoc::RI::AttributeFormatter < RDoc::RI::Formatter - - BOLD = 1 - ITALIC = 2 - CODE = 4 - - ATTR_MAP = { - "b" => BOLD, - "code" => CODE, - "em" => ITALIC, - "i" => ITALIC, - "tt" => CODE - } - - AttrChar = Struct.new :char, :attr - - class AttributeString - attr_reader :txt - - def initialize - @txt = [] - @optr = 0 - end - - def <<(char) - @txt << char - end - - def empty? - @optr >= @txt.length - end - - # accept non space, then all following spaces - def next_word - start = @optr - len = @txt.length - - while @optr < len && @txt[@optr].char != " " - @optr += 1 - end - - while @optr < len && @txt[@optr].char == " " - @optr += 1 - end - - @txt[start...@optr] - end - end - - ## - # Overrides base class. Looks for ... etc sequences and generates - # an array of AttrChars. This array is then used as the basis for the - # split. - - def wrap(txt, prefix=@indent, linelen=@width) - return unless txt && !txt.empty? - - txt = add_attributes_to(txt) - next_prefix = prefix.tr("^ ", " ") - linelen -= prefix.size - - line = [] - - until txt.empty? - word = txt.next_word - if word.size + line.size > linelen - write_attribute_text(prefix, line) - prefix = next_prefix - line = [] - end - line.concat(word) - end - - write_attribute_text(prefix, line) if line.length > 0 - end - - protected - - def write_attribute_text(prefix, line) - @output.print prefix - line.each do |achar| - @output.print achar.char - end - @output.puts - end - - def bold_print(txt) - @output.print txt - end - - private - - def add_attributes_to(txt) - tokens = txt.split(%r{()}) - text = AttributeString.new - attributes = 0 - tokens.each do |tok| - case tok - when %r{^$} then attributes &= ~(ATTR_MAP[$1]||0) - when %r{^<(\w+)>$} then attributes |= (ATTR_MAP[$1]||0) - else - tok.split(//).each {|ch| text << AttrChar.new(ch, attributes)} - end - end - text - end - -end - -## -# This formatter generates overstrike-style formatting, which works with -# pagers such as man and less. - -class RDoc::RI::OverstrikeFormatter < RDoc::RI::AttributeFormatter - - BS = "\C-h" - - def write_attribute_text(prefix, line) - @output.print prefix - - line.each do |achar| - attr = achar.attr - @output.print "_", BS if (attr & (ITALIC + CODE)) != 0 - @output.print achar.char, BS if (attr & BOLD) != 0 - @output.print achar.char - end - - @output.puts - end - - ## - # Draw a string in bold - - def bold_print(text) - text.split(//).each do |ch| - @output.print ch, BS, ch - end - end - -end - -## -# This formatter uses ANSI escape sequences to colorize stuff works with -# pagers such as man and less. - -class RDoc::RI::AnsiFormatter < RDoc::RI::AttributeFormatter - - def initialize(*args) - super - @output.print "\033[0m" - end - - def write_attribute_text(prefix, line) - @output.print prefix - curr_attr = 0 - line.each do |achar| - attr = achar.attr - if achar.attr != curr_attr - update_attributes(achar.attr) - curr_attr = achar.attr - end - @output.print achar.char - end - update_attributes(0) unless curr_attr.zero? - @output.puts - end - - def bold_print(txt) - @output.print "\033[1m#{txt}\033[m" - end - - HEADINGS = { - 1 => ["\033[1;32m", "\033[m"], - 2 => ["\033[4;32m", "\033[m"], - 3 => ["\033[32m", "\033[m"], - } - - def display_heading(text, level, indent) - level = 3 if level > 3 - heading = HEADINGS[level] - @output.print indent - @output.print heading[0] - @output.print strip_attributes(text) - @output.puts heading[1] - end - - private - - ATTR_MAP = { - BOLD => "1", - ITALIC => "33", - CODE => "36" - } - - def update_attributes(attr) - str = "\033[" - for quality in [ BOLD, ITALIC, CODE] - unless (attr & quality).zero? - str << ATTR_MAP[quality] - end - end - @output.print str, "m" - end - -end - ## -# This formatter uses HTML. - -class RDoc::RI::HtmlFormatter < RDoc::RI::AttributeFormatter - - def write_attribute_text(prefix, line) - curr_attr = 0 - line.each do |achar| - attr = achar.attr - if achar.attr != curr_attr - update_attributes(curr_attr, achar.attr) - curr_attr = achar.attr - end - @output.print(escape(achar.char)) - end - update_attributes(curr_attr, 0) unless curr_attr.zero? - end - - def draw_line(label=nil) - if label != nil - bold_print(label) - end - @output.puts("
") - end - - def bold_print(txt) - tag("b") { txt } - end - - def blankline() - @output.puts("

") - end - - def break_to_newline - @output.puts("
") - end - - def display_heading(text, level, indent) - level = 4 if level > 4 - tag("h#{level}") { text } - @output.puts - end - - def display_list(list) - case list.type - when :BULLET then - list_type = "ul" - prefixer = proc { |ignored| "

  • " } - - when :NUMBER, :UPPERALPHA, :LOWERALPHA then - list_type = "ol" - prefixer = proc { |ignored| "
  • " } - - when :LABELED then - list_type = "dl" - prefixer = proc do |li| - "
    " + escape(li.label) + "
    " - end - - when :NOTE then - list_type = "table" - prefixer = proc do |li| - %{#{li.label.gsub(/ /, ' ')}} - end - else - fail "unknown list type" - end - - @output.print "<#{list_type}>" - list.contents.each do |item| - if item.kind_of? RDoc::Markup::Flow::LI - prefix = prefixer.call(item) - @output.print prefix - display_flow_item(item, prefix) - else - display_flow_item(item) - end - end - @output.print "" - end - - def display_verbatim_flow_item(item, prefix=@indent) - @output.print("
    ")
    -    item.body.split(/\n/).each do |line|
    -      @output.puts conv_html(line)
    -    end
    -    @output.puts("
    ") - end - - private - - ATTR_MAP = { - BOLD => "b>", - ITALIC => "i>", - CODE => "tt>" - } - - def update_attributes(current, wanted) - str = "" - # first turn off unwanted ones - off = current & ~wanted - for quality in [ BOLD, ITALIC, CODE] - if (off & quality) > 0 - str << "") - @output.print(yield) - @output.print("") - end - - def escape(str) - str = str.gsub(/&/n, '&') - str.gsub!(/\"/n, '"') - str.gsub!(/>/n, '>') - str.gsub!(/ ri_paths[name][0] - ri_paths[name] = [ver, dir] - end - end - end - - GEMDIRS = ri_paths.map { |k,v| v.last }.sort - rescue LoadError - GEMDIRS = [] - end -end diff --git a/lib/rdoc/ri/paths.rb b/lib/rdoc/ri/paths.rb index 399fbbab00..178142d35f 100644 --- a/lib/rdoc/ri/paths.rb +++ b/lib/rdoc/ri/paths.rb @@ -1,78 +1,118 @@ require 'rdoc/ri' ## -# Encapsulate all the strangeness to do with finding out where to find RDoc -# files -# -# We basically deal with three directories: -# -# 1. The 'system' documentation directory, which holds the documentation -# distributed with Ruby, and which is managed by the Ruby install process -# 2. The 'site' directory, which contains site-wide documentation added -# locally. -# 3. The 'user' documentation directory, stored under the user's own home -# directory. -# -# There's contention about all this, but for now: -# -# system:: $datadir/ri//system/... -# site:: $datadir/ri//site/... -# user:: ~/.rdoc +# The directories where ri data lives. module RDoc::RI::Paths #:stopdoc: require 'rbconfig' - DOC_DIR = "doc/rdoc" + version = RbConfig::CONFIG['ruby_version'] - VERSION = RbConfig::CONFIG['ruby_version'] + base = File.join RbConfig::CONFIG['datadir'], "ri", version + SYSDIR = File.join base, "system" + SITEDIR = File.join base, "site" + homedir = File.expand_path('~') || + ENV['HOME'] || ENV['USERPROFILE'] || ENV['HOMEPATH'] - if VERSION > '1.9.1' - if m = /ruby/.match(RbConfig::CONFIG['RUBY_INSTALL_NAME']) - m = [m.pre_match, m.post_match] - else - m = [""] * 2 + HOMEDIR = if homedir then + File.join homedir, ".rdoc" + end + #:startdoc: + + @gemdirs = nil + + ## + # Iterates over each selected path yielding the directory and type. + # + # Yielded types: + # :system:: Where Ruby's ri data is stored. Yielded when +system+ is + # true + # :site:: Where ri for installed libraries are stored. Yielded when + # +site+ is true. Normally no ri data is stored here. + # :home:: ~/.ri. Yielded when +home+ is true. + # :gem:: ri data for an installed gem. Yielded when +gems+ is true. + # :extra:: ri data directory from the command line. Yielded for each + # entry in +extra_dirs+ + + def self.each system, site, home, gems, *extra_dirs # :yields: directory, type + extra_dirs.each do |dir| + yield dir, :extra end - ri = "#{m[0]}ri#{m[1]}" - rdoc = "#{m[0]}rdoc#{m[1]}" - base = File.join(RbConfig::CONFIG['datadir'], ri, VERSION) - else - if m = /ruby/.match(RbConfig::CONFIG['RUBY_BASE_NAME']) - m = [m.pre_match, m.post_match] - else - m = [""] * 2 + + yield SYSDIR, :system if system + yield SITEDIR, :site if site + yield HOMEDIR, :home if home + + gemdirs.each do |dir| + yield dir, :gem + end if gems + + nil + end + + ## + # The latest installed gems' ri directories + + def self.gemdirs + return @gemdirs if @gemdirs + + require 'rubygems' unless defined?(Gem) and defined?(Gem::Enable) and + Gem::Enable + + # HACK dup'd from Gem.latest_partials and friends + all_paths = [] + + all_paths = Gem.path.map do |dir| + Dir[File.join(dir, 'doc', '*', 'ri')] + end.flatten + + ri_paths = {} + + all_paths.each do |dir| + base = File.basename File.dirname(dir) + if base =~ /(.*)-((\d+\.)*\d+)/ then + name, version = $1, $2 + ver = Gem::Version.new version + if ri_paths[name].nil? or ver > ri_paths[name][0] then + ri_paths[name] = [ver, dir] + end + end end - ri = "#{m[0]}ri#{m[1]}" - rdoc = "#{m[0]}rdoc#{m[1]}" - base = File.join(RbConfig::CONFIG['ridir'], VERSION) + + @gemdirs = ri_paths.map { |k,v| v.last }.sort + rescue LoadError + @gemdirs = [] end - SYSDIR = File.join(base, "system") - SITEDIR = File.join(base, "site") - HOMEDIR = (File.expand_path("~/.#{rdoc}") rescue nil) - autoload(:GEMDIRS, File.expand_path('../gemdirs.rb', __FILE__)) + ## + # Returns existing directories from the selected documentation directories + # as an Array. + # + # See also ::each - # Returns the selected documentation directories as an Array, or PATH if no - # overriding directories were given. + def self.path(system, site, home, gems, *extra_dirs) + path = raw_path system, site, home, gems, *extra_dirs - def self.path(use_system, use_site, use_home, use_gems, *extra_dirs) - path = raw_path(use_system, use_site, use_home, use_gems, *extra_dirs) - return path.select { |directory| File.directory? directory } + path.select { |directory| File.directory? directory } end - # Returns the selected documentation directories including nonexistent - # directories. Used to print out what paths were searched if no ri was - # found. + ## + # Returns selected documentation directories including nonexistent + # directories. + # + # See also ::each - def self.raw_path(use_system, use_site, use_home, use_gems, *extra_dirs) + def self.raw_path(system, site, home, gems, *extra_dirs) path = [] - path << extra_dirs unless extra_dirs.empty? - path << SYSDIR if use_system - path << SITEDIR if use_site - path << HOMEDIR if use_home - path << GEMDIRS if use_gems - return path.flatten.compact + each(system, site, home, gems, *extra_dirs) do |dir, type| + path << dir + end + + path.compact end + end + diff --git a/lib/rdoc/ri/reader.rb b/lib/rdoc/ri/reader.rb deleted file mode 100644 index de3c8d9afa..0000000000 --- a/lib/rdoc/ri/reader.rb +++ /dev/null @@ -1,106 +0,0 @@ -require 'rdoc/ri' -require 'rdoc/ri/descriptions' -require 'rdoc/ri/writer' -require 'rdoc/markup/to_flow' - -class RDoc::RI::Reader - - def initialize(ri_cache) - @cache = ri_cache - end - - def top_level_namespace - [ @cache.toplevel ] - end - - def lookup_namespace_in(target, namespaces) - result = [] - for n in namespaces - result.concat(n.contained_modules_matching(target)) - end - result - end - - def find_class_by_name(full_name) - names = full_name.split(/::/) - ns = @cache.toplevel - for name in names - ns = ns.contained_class_named(name) - return nil if ns.nil? - end - get_class(ns) - end - - def find_methods(name, is_class_method, namespaces) - result = [] - namespaces.each do |ns| - result.concat ns.methods_matching(name, is_class_method) - end - result - end - - ## - # Return the MethodDescription for a given MethodEntry by deserializing the - # YAML - - def get_method(method_entry) - path = method_entry.path_name - File.open(path) { |f| RDoc::RI::Description.deserialize(f) } - end - - ## - # Return a class description - - def get_class(class_entry) - result = nil - for path in class_entry.path_names - path = RDoc::RI::Writer.class_desc_path(path, class_entry) - desc = File.open(path) {|f| RDoc::RI::Description.deserialize(f) } - if result - result.merge_in(desc) - else - result = desc - end - end - result - end - - ## - # Return the names of all classes and modules - - def full_class_names - res = [] - find_classes_in(res, @cache.toplevel) - end - - ## - # Return a list of all classes, modules, and methods - - def all_names - res = [] - find_names_in(res, @cache.toplevel) - end - - private - - def find_classes_in(res, klass) - classes = klass.classes_and_modules - for c in classes - res << c.full_name - find_classes_in(res, c) - end - res - end - - def find_names_in(res, klass) - classes = klass.classes_and_modules - for c in classes - res << c.full_name - res.concat c.all_method_names - find_names_in(res, c) - end - res - end - -end - diff --git a/lib/rdoc/ri/store.rb b/lib/rdoc/ri/store.rb new file mode 100644 index 0000000000..81ffb7e674 --- /dev/null +++ b/lib/rdoc/ri/store.rb @@ -0,0 +1,248 @@ +require 'rdoc/code_objects' +require 'fileutils' + +## +# A set of ri data. +# +# The store manages reading and writing ri data for a project (gem, path, +# etc.) and maintains a cache of methods, classes and ancestors in the +# store. + +class RDoc::RI::Store + + ## + # Path this store reads or writes + + attr_accessor :path + + ## + # Type of ri datastore this was loaded from. See RDoc::RI::Driver, + # RDoc::RI::Paths. + + attr_accessor :type + + attr_reader :cache + + ## + # Creates a new Store of +type+ that will load or save to +path+ + + def initialize path, type = nil + @type = type + @path = path + + @cache = { + :class_methods => {}, + :instance_methods => {}, + :attributes => {}, + :modules => [], + :ancestors => {}, + } + end + + ## + # Ancestors cache accessor. Maps a klass name to an Array of its ancestors + # in this store. If Foo in this store inherits from Object, Kernel won't be + # listed (it will be included from ruby's ri store). + + def ancestors + @cache[:ancestors] + end + + ## + # Attributes cache accessor. Maps a class to an Array of its attributes. + + def attributes + @cache[:attributes] + end + + ## + # Path to the cache file + + def cache_path + File.join @path, 'cache.ri' + end + + ## + # Path to the ri data for +klass_name+ + + def class_file klass_name + name = klass_name.split('::').last + File.join class_path(klass_name), "cdesc-#{name}.ri" + end + + ## + # Class methods cache accessor. Maps a class to an Array of its class + # methods (not full name). + + def class_methods + @cache[:class_methods] + end + + ## + # Path where data for +klass_name+ will be stored (methods or class data) + + def class_path klass_name + File.join @path, *klass_name.split('::') + end + + ## + # Friendly rendition of #path + + def friendly_path + case type + when :gem then + sep = Regexp.union(*['/', File::ALT_SEPARATOR].compact) + @path =~ /#{sep}doc#{sep}(.*?)#{sep}ri$/ + "gem #{$1}" + when :home then '~/.ri' + when :site then 'ruby site' + when :system then 'ruby core' + else @path + end + end + + def inspect # :nodoc: + "#<%s:0x%x %s %p>" % [self.class, object_id, @path, modules.sort] + end + + ## + # Instance methods cache accessor. Maps a class to an Array of its + # instance methods (not full name). + + def instance_methods + @cache[:instance_methods] + end + + ## + # Loads cache file for this store + + def load_cache + open cache_path, 'rb' do |io| + @cache = Marshal.load io.read + end + rescue Errno::ENOENT + end + + ## + # Loads ri data for +klass_name+ + + def load_class klass_name + open class_file(klass_name), 'rb' do |io| + Marshal.load io.read + end + end + + ## + # Loads ri data for +method_name+ in +klass_name+ + + def load_method klass_name, method_name + open method_file(klass_name, method_name), 'rb' do |io| + Marshal.load io.read + end + end + + ## + # Path to the ri data for +method_name+ in +klass_name+ + + def method_file klass_name, method_name + method_name = method_name.split('::').last + method_name =~ /#(.*)/ + method_type = $1 ? 'i' : 'c' + method_name = $1 if $1 + + method_name = if ''.respond_to? :ord then + method_name.gsub(/\W/) { "%%%02x" % $&[0].ord } + else + method_name.gsub(/\W/) { "%%%02x" % $&[0] } + end + + File.join class_path(klass_name), "#{method_name}-#{method_type}.ri" + end + + ## + # Modules cache accessor. An Array of all the modules (and classes) in the + # store. + + def modules + @cache[:modules] + end + + ## + # Writes the cache file for this store + + def save_cache + # HACK mongrel-1.1.5 documents its files twice + @cache[:attributes]. each do |_, m| m.uniq!; m.sort! end + @cache[:class_methods]. each do |_, m| m.uniq!; m.sort! end + @cache[:instance_methods].each do |_, m| m.uniq!; m.sort! end + + open cache_path, 'wb' do |io| + Marshal.dump @cache, io + end + end + + ## + # Writes the ri data for +klass+ + + def save_class klass + FileUtils.mkdir_p class_path(klass.full_name) + + @cache[:modules] << klass.full_name + + path = class_file klass.full_name + + begin + disk_klass = nil + + open path, 'rb' do |io| + disk_klass = Marshal.load io.read + end + + klass.merge disk_klass + rescue Errno::ENOENT + end + + # BasicObject has no ancestors + ancestors = klass.ancestors.compact.map do |ancestor| + # HACK for classes we don't know about (class X < RuntimeError) + String === ancestor ? ancestor : ancestor.full_name + end + + @cache[:ancestors][klass.full_name] ||= [] + @cache[:ancestors][klass.full_name].push(*ancestors) + + attributes = klass.attributes.map do |attribute| + "#{attribute.type} #{attribute.name}" + end + + unless attributes.empty? then + @cache[:attributes][klass.full_name] ||= [] + @cache[:attributes][klass.full_name].push(*attributes) + end + + open path, 'wb' do |io| + Marshal.dump klass, io + end + end + + ## + # Writes the ri data for +method+ on +klass+ + + def save_method klass, method + FileUtils.mkdir_p class_path(klass.full_name) + + cache = if method.singleton then + @cache[:class_methods] + else + @cache[:instance_methods] + end + cache[klass.full_name] ||= [] + cache[klass.full_name] << method.name + + open method_file(klass.full_name, method.full_name), 'wb' do |io| + Marshal.dump method, io + end + end + +end + diff --git a/lib/rdoc/ri/util.rb b/lib/rdoc/ri/util.rb deleted file mode 100644 index 51cf881bdd..0000000000 --- a/lib/rdoc/ri/util.rb +++ /dev/null @@ -1,79 +0,0 @@ -require 'rdoc/ri' - -## -# Break argument into its constituent class or module names, an -# optional method type, and a method name - -class RDoc::RI::NameDescriptor - - attr_reader :class_names - attr_reader :method_name - - ## - # true and false have the obvious meaning. nil means we don't care - - attr_reader :is_class_method - - ## - # +arg+ may be - # - # 1. A class or module name (optionally qualified with other class or module - # names (Kernel, File::Stat etc) - # 2. A method name - # 3. A method name qualified by a optionally fully qualified class or module - # name - # - # We're fairly casual about delimiters: folks can say Kernel::puts, - # Kernel.puts, or Kernel\#puts for example. There's one exception: if you - # say IO::read, we look for a class method, but if you say IO.read, we look - # for an instance method - - def initialize(arg) - @class_names = [] - separator = nil - - tokens = arg.split(/(\.|::|#)/) - - # Skip leading '::', '#' or '.', but remember it might - # be a method name qualifier - separator = tokens.shift if tokens[0] =~ /^(\.|::|#)/ - - # Skip leading '::', but remember we potentially have an inst - - # leading stuff must be class names - - while tokens[0] =~ /^[A-Z]/ - @class_names << tokens.shift - unless tokens.empty? - separator = tokens.shift - break unless separator == "::" - end - end - - # Now must have a single token, the method name, or an empty array - unless tokens.empty? - @method_name = tokens.shift - # We may now have a trailing !, ?, or = to roll into - # the method name - if !tokens.empty? && tokens[0] =~ /^[!?=]$/ - @method_name << tokens.shift - end - - if @method_name =~ /::|\.|#/ or !tokens.empty? - raise RDoc::RI::Error.new("Bad argument: #{arg}") - end - if separator && separator != '.' - @is_class_method = separator == "::" - end - end - end - - # Return the full class name (with '::' between the components) or "" if - # there's no class name - - def full_class_name - @class_names.join("::") - end - -end - diff --git a/lib/rdoc/ri/writer.rb b/lib/rdoc/ri/writer.rb deleted file mode 100644 index 92aaa1c2da..0000000000 --- a/lib/rdoc/ri/writer.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'fileutils' -require 'rdoc/ri' - -class RDoc::RI::Writer - - def self.class_desc_path(dir, class_desc) - File.join(dir, "cdesc-" + class_desc.name + ".yaml") - end - - ## - # Convert a name from internal form (containing punctuation) to an external - # form (where punctuation is replaced by %xx) - - def self.internal_to_external(name) - if ''.respond_to? :ord then - name.gsub(/\W/) { "%%%02x" % $&[0].ord } - else - name.gsub(/\W/) { "%%%02x" % $&[0] } - end - end - - ## - # And the reverse operation - - def self.external_to_internal(name) - name.gsub(/%([0-9a-f]{2,2})/) { $1.to_i(16).chr } - end - - def initialize(base_dir) - @base_dir = base_dir - end - - def remove_class(class_desc) - FileUtils.rm_rf(path_to_dir(class_desc.full_name)) - end - - def add_class(class_desc) - dir = path_to_dir(class_desc.full_name) - FileUtils.mkdir_p(dir) - class_file_name = self.class.class_desc_path(dir, class_desc) - File.open(class_file_name, "w") do |f| - f.write(class_desc.serialize) - end - end - - def add_method(class_desc, method_desc) - dir = path_to_dir(class_desc.full_name) - file_name = self.class.internal_to_external(method_desc.name) - meth_file_name = File.join(dir, file_name) - if method_desc.is_singleton - meth_file_name += "-c.yaml" - else - meth_file_name += "-i.yaml" - end - - File.open(meth_file_name, "w") do |f| - f.write(method_desc.serialize) - end - end - - private - - def path_to_dir(class_name) - File.join(@base_dir, *class_name.split('::')) - end - -end - -- cgit v1.2.3