summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordave <dave@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2003-12-16 05:44:25 +0000
committerdave <dave@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>2003-12-16 05:44:25 +0000
commitc5bbcadbe64477433a243be191c41010c7ae10dc (patch)
tree0d09db2cbe31c84eac3c29575e7008c9d7a6d57b
parentdcd30a1236cdb2e06b6dd1a74a4c0a0c29549be6 (diff)
Initial load of support for ri/rdoc integration
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@5199 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
-rw-r--r--MANIFEST10
-rwxr-xr-xbin/ri201
-rw-r--r--lib/rdoc/generators/ri_generator.rb202
-rw-r--r--lib/rdoc/markup/simple_markup/to_flow.rb187
-rw-r--r--lib/rdoc/options.rb41
-rw-r--r--lib/rdoc/ri/ri_cache.rb145
-rw-r--r--lib/rdoc/ri/ri_descriptions.rb58
-rw-r--r--lib/rdoc/ri/ri_formatter.rb170
-rw-r--r--lib/rdoc/ri/ri_paths.rb41
-rw-r--r--lib/rdoc/ri/ri_reader.rb46
-rw-r--r--lib/rdoc/ri/ri_util.rb63
-rw-r--r--lib/rdoc/ri/ri_writer.rb48
12 files changed, 1203 insertions, 9 deletions
diff --git a/MANIFEST b/MANIFEST
index 40c60ae..32294de 100644
--- a/MANIFEST
+++ b/MANIFEST
@@ -84,6 +84,7 @@ bcc32/setup.mak
bin/erb
bin/irb
bin/rdoc
+bin/ri
bin/testrb
cygwin/GNUmakefile.in
djgpp/GNUmakefile.in
@@ -231,6 +232,7 @@ lib/rdoc/tokenstream.rb
lib/rdoc/dot/dot.rb
lib/rdoc/generators/chm_generator.rb
lib/rdoc/generators/html_generator.rb
+lib/rdoc/generators/ri_generator.rb
lib/rdoc/generators/xml_generator.rb
lib/rdoc/generators/template/chm/chm.rb
lib/rdoc/generators/template/html/css2.rb
@@ -246,6 +248,7 @@ lib/rdoc/markup/simple_markup/fragments.rb
lib/rdoc/markup/simple_markup/inline.rb
lib/rdoc/markup/simple_markup/lines.rb
lib/rdoc/markup/simple_markup/preprocess.rb
+lib/rdoc/markup/simple_markup/to_flow.rb
lib/rdoc/markup/simple_markup/to_html.rb
lib/rdoc/markup/simple_markup/to_latex.rb
lib/rdoc/markup/test/AllTests.rb
@@ -256,6 +259,13 @@ lib/rdoc/parsers/parse_f95.rb
lib/rdoc/parsers/parse_rb.rb
lib/rdoc/parsers/parse_simple.rb
lib/rdoc/parsers/parserfactory.rb
+lib/rdoc/ri/ri_cache.rb
+lib/rdoc/ri/ri_descriptions.rb
+lib/rdoc/ri/ri_formatter.rb
+lib/rdoc/ri/ri_paths.rb
+lib/rdoc/ri/ri_reader.rb
+lib/rdoc/ri/ri_util.rb
+lib/rdoc/ri/ri_writer.rb
lib/readbytes.rb
lib/resolv-replace.rb
lib/resolv.rb
diff --git a/bin/ri b/bin/ri
new file mode 100755
index 0000000..899fc2d
--- /dev/null
+++ b/bin/ri
@@ -0,0 +1,201 @@
+#!/usr/bin/env ruby
+# usage:
+#
+# ri name...
+#
+# where name can be
+#
+# Class | Class::method | Class#method | Class.method | method
+#
+# All names may be abbreviated to their minimum unbiguous form. If a name
+# _is_ ambiguous, all valid options will be listed.
+#
+
+require 'rdoc/ri/ri_paths'
+require 'rdoc/ri/ri_cache'
+require 'rdoc/ri/ri_util'
+require 'rdoc/ri/ri_reader'
+require 'rdoc/ri/ri_formatter'
+
+######################################################################
+
+def display_usage
+ File.open(__FILE__) do |f|
+ f.gets
+ puts $1 while (f.gets =~ /^# ?(.*)/)
+ end
+ exit
+end
+
+
+######################################################################
+
+class RiDisplay
+
+ def initialize
+ paths = RI::Paths::PATH
+ if paths.empty?
+ $stderr.puts "No ri documentation found in:"
+ [ RI::Paths::SYSDIR, RI::Paths::SITEDIR, RI::Paths::HOMEDIR].each do |d|
+ $stderr.puts " #{d}"
+ end
+ $stderr.puts "\nIs ri correctly installed?"
+ exit 1
+ end
+ @ri_reader = RI::RiReader.new(RI::RiCache.new(paths))
+ @formatter = RI::RiFormatter.new(72, " ")
+ end
+
+ ######################################################################
+
+ def display_params(method)
+ params = method.params
+ if params[0,1] == "("
+ if method.is_singleton
+ params = method.full_name + params
+ else
+ params = method.name + params
+ end
+ end
+ @formatter.wrap(params)
+ end
+
+ ######################################################################
+
+ def display_flow(flow)
+ if !flow || flow.empty?
+ @formatter.wrap("(no description...)")
+ else
+ @formatter.display_flow(flow)
+ end
+ end
+
+######################################################################
+
+def display_method_info(method_entry)
+ method = @ri_reader.get_method(method_entry)
+ @formatter.draw_line(method.full_name)
+ display_params(method)
+ @formatter.draw_line
+ display_flow(method.comment)
+ if method.aliases && !method.aliases.empty?
+ @formatter.blankline
+ aka = "(also known as "
+ aka << method.aliases.map {|a| a.name }.join(", ")
+ aka << ")"
+ @formatter.wrap(aka)
+ end
+end
+
+######################################################################
+
+def display_class_info(class_entry)
+ klass = @ri_reader.get_class(class_entry)
+ @formatter.draw_line("Class: " + klass.full_name)
+ display_flow(klass.comment)
+ @formatter.draw_line
+
+ unless klass.constants.empty?
+ @formatter.blankline
+ @formatter.wrap("Constants:", "")
+ len = 0
+ klass.constants.each { |c| len = c.name.length if c.name.length > len }
+ len += 2
+ klass.constants.each do |c|
+ @formatter.wrap(c.value,
+ @formatter.indent+((c.name+":").ljust(len)))
+ end
+ end
+
+ unless klass.method_list.empty?
+ @formatter.blankline
+ @formatter.wrap("Methods:", "")
+ @formatter.wrap(klass.method_list.map{|m| m.name}.sort.join(', '))
+ end
+
+ unless klass.attributes.empty?
+ @formatter.blankline
+ @formatter.wrap("Attributes:", "")
+ @formatter.wrap(klass.attributes.map{|a| a.name}.sort.join(', '))
+ end
+end
+
+######################################################################
+
+# If the list of matching methods contains exactly one entry, or
+# if it contains an entry that exactly matches the requested method,
+# then display that entry, otherwise display the list of
+# matching method names
+
+def report_method_stuff(requested_method_name, methods)
+ if methods.size == 1
+ display_method_info(methods[0])
+ elsif (entry = methods.find {|m| m.name == requested_method_name})
+ display_method_info(entry)
+ else
+ puts "More than one method matched your request. You can refine"
+ puts "your search by asking for information on one of:\n\n"
+ @formatter.wrap(methods.map {|m| m.full_name} .join(", "))
+ end
+end
+
+######################################################################
+
+def report_class_stuff(namespaces)
+ if namespaces.size > 1
+ puts "More than one class or module matched your request. You can refine"
+ puts "your search by asking for information on one of:\n\n"
+ puts @formatter.wrap("", namespaces.map {|m| m.full_name} .join(", "))
+ else
+ class_desc = @ri_reader.get_class(namespaces[0])
+ display_class_info(namespaces[0])
+ end
+end
+
+######################################################################
+
+
+def display_info_for(arg)
+ desc = NameDescriptor.new(arg)
+
+ namespaces = @ri_reader.top_level_namespace
+
+ for class_name in desc.class_names
+ namespaces = @ri_reader.lookup_namespace_in(class_name, namespaces)
+ if namespaces.empty?
+ raise RiError.new("Nothing known about #{arg}")
+ end
+ end
+
+ if desc.method_name.nil?
+ report_class_stuff(namespaces)
+ else
+ methods = @ri_reader.find_methods(desc.method_name,
+ desc.is_class_method,
+ namespaces)
+
+ if methods.empty?
+ raise RiError.new("Nothing known about #{arg}")
+ else
+ report_method_stuff(desc.method_name, methods)
+ end
+ end
+
+end
+end
+
+######################################################################
+
+if ARGV.size.zero?
+ display_usage
+else
+ ri = RiDisplay.new
+ begin
+ ARGV.each do |arg|
+ ri.display_info_for(arg)
+ end
+ rescue RiError => e
+ $stderr.puts(e.message)
+ exit(1)
+ end
+end
diff --git a/lib/rdoc/generators/ri_generator.rb b/lib/rdoc/generators/ri_generator.rb
new file mode 100644
index 0000000..db86d74
--- /dev/null
+++ b/lib/rdoc/generators/ri_generator.rb
@@ -0,0 +1,202 @@
+# We're responsible for generating all the HTML files
+# from the object tree defined in code_objects.rb. We
+# generate:
+#
+# [files] an html file for each input file given. These
+# input files appear as objects of class
+# TopLevel
+#
+# [classes] an html file for each class or module encountered.
+# These classes are not grouped by file: if a file
+# contains four classes, we'll generate an html
+# file for the file itself, and four html files
+# for the individual classes.
+#
+# [indices] we generate three indices for files, classes,
+# and methods. These are displayed in a browser
+# like window with three index panes across the
+# top and the selected description below
+#
+# Method descriptions appear in whatever entity (file, class,
+# or module) that contains them.
+#
+# We generate files in a structure below a specified subdirectory,
+# normally +doc+.
+#
+# opdir
+# |
+# |___ files
+# | |__ per file summaries
+# |
+# |___ classes
+# |__ per class/module descriptions
+#
+# HTML is generated using the Template class.
+#
+
+require 'ftools'
+
+require 'rdoc/options'
+require 'rdoc/template'
+require 'rdoc/markup/simple_markup'
+require 'rdoc/markup/simple_markup/to_flow'
+require 'cgi'
+
+require 'rdoc/ri/ri_writer'
+require 'rdoc/ri/ri_descriptions'
+
+module Generators
+
+
+ class RIGenerator
+
+ # Generators may need to return specific subclasses depending
+ # on the options they are passed. Because of this
+ # we create them using a factory
+
+ def RIGenerator.for(options)
+ new(options)
+ end
+
+ class <<self
+ protected :new
+ end
+
+ # Set up a new HTML generator. Basically all we do here is load
+ # up the correct output temlate
+
+ def initialize(options) #:not-new:
+ @options = options
+ @ri_writer = RI::RiWriter.new(options.op_dir)
+ @markup = SM::SimpleMarkup.new
+ @to_flow = SM::ToFlow.new
+ end
+
+
+ ##
+ # Build the initial indices and output objects
+ # based on an array of TopLevel objects containing
+ # the extracted information.
+
+ def generate(toplevels)
+ RDoc::TopLevel.all_classes_and_modules.each do |cls|
+ process_class(cls)
+ end
+ end
+
+ def process_class(from_class)
+ generate_class_info(from_class)
+
+ # now recure into this classes constituent classess
+ from_class.each_classmodule do |mod|
+ process_class(mod)
+ end
+ end
+
+ def generate_class_info(cls)
+ cls_desc = RI::ClassDescription.new
+ cls_desc.name = cls.name
+ cls_desc.full_name = cls.full_name
+ cls_desc.superclass = cls.superclass
+ cls_desc.comment = markup(cls.comment)
+
+ cls_desc.method_list = method_list(cls)
+
+ cls_desc.attributes =cls.attributes.sort.map do |a|
+ RI::Attribute.new(a.name, a.rw, markup(a.comment))
+ end
+
+ cls_desc.constants = cls.constants.map do |c|
+ RI::Constant.new(c.name, c.value, markup(c.comment))
+ end
+
+ cls_desc.includes = cls.includes.map do |i|
+ RI::IncludedModule.new(i.name)
+ end
+
+ methods = method_list(cls)
+
+ cls_desc.method_list = methods.map do |m|
+ RI::MethodSummary.new(m.name)
+ end
+
+ @ri_writer.remove_class(cls_desc)
+ @ri_writer.add_class(cls_desc)
+
+ methods.each do |m|
+ generate_method_info(cls_desc, m)
+ end
+ end
+
+
+ def generate_method_info(cls_desc, method)
+ meth_desc = RI::MethodDescription.new
+ meth_desc.name = method.name
+ meth_desc.full_name = cls_desc.full_name
+ if method.singleton
+ meth_desc.full_name += "::"
+ else
+ meth_desc.full_name += "#"
+ end
+ meth_desc.full_name << method.name
+
+ meth_desc.comment = markup(method.comment)
+ meth_desc.params = params_of(method)
+ meth_desc.visibility = method.visibility.to_s
+ meth_desc.is_singleton = method.singleton
+ meth_desc.block_params = method.block_params
+
+ meth_desc.aliases = method.aliases.map do |a|
+ RI::AliasName.new(a.name)
+ end
+
+ @ri_writer.add_method(cls_desc, meth_desc)
+ end
+
+ private
+
+ # return a list of methods that we'll be documenting
+
+ def method_list(cls)
+ list = cls.method_list
+ unless @options.show_all
+ list = list.find_all do |m|
+ m.visibility == :public || m.force_documentation
+ end
+ end
+
+ list.sort
+ end
+
+ def params_of(method)
+ p = method.params.gsub(/\s*\#.*/, '')
+ p = p.tr("\n", " ").squeeze(" ")
+ p = "(" + p + ")" unless p[0] == ?(
+
+ if (block = method.block_params)
+ block.gsub!(/\s*\#.*/, '')
+ block = block.tr("\n", " ").squeeze(" ")
+ if block[0] == ?(
+ block.sub!(/^\(/, '').sub!(/\)/, '')
+ end
+ p << " {|#{block.strip}| ...}"
+ end
+ p
+ end
+
+ def markup(comment)
+ return nil if !comment || comment.empty?
+
+ # Convert leading comment markers to spaces, but only
+ # if all non-blank lines have them
+
+ if comment =~ /^(?>\s*)[^\#]/
+ content = comment
+ else
+ content = comment.gsub(/^\s*(#+)/) { $1.tr('#',' ') }
+ end
+ @markup.convert(content, @to_flow)
+ end
+
+ end
+end
diff --git a/lib/rdoc/markup/simple_markup/to_flow.rb b/lib/rdoc/markup/simple_markup/to_flow.rb
new file mode 100644
index 0000000..b91bb12
--- /dev/null
+++ b/lib/rdoc/markup/simple_markup/to_flow.rb
@@ -0,0 +1,187 @@
+require 'rdoc/markup/simple_markup/fragments'
+require 'rdoc/markup/simple_markup/inline'
+
+
+module SM
+
+ module Flow
+ P = Struct.new(:body)
+ VERB = Struct.new(:body)
+ RULE = Struct.new(:width)
+ class LIST
+ attr_reader :type, :contents
+ def initialize(type)
+ @type = type
+ @contents = []
+ end
+ def <<(stuff)
+ @contents << stuff
+ end
+ end
+ LI = Struct.new(:label, :body)
+ H = Struct.new(:level, :text)
+ end
+
+ class ToFlow
+
+ LIST_TYPE_TO_HTML = {
+ ListBase::BULLET => [ "<ul>", "</ul>" ],
+ ListBase::NUMBER => [ "<ol>", "</ol>" ],
+ ListBase::UPPERALPHA => [ "<ol>", "</ol>" ],
+ ListBase::LOWERALPHA => [ "<ol>", "</ol>" ],
+ ListBase::LABELED => [ "<dl>", "</dl>" ],
+ ListBase::NOTE => [ "<table>", "</table>" ],
+ }
+
+ InlineTag = Struct.new(:bit, :on, :off)
+
+ def initialize
+ init_tags
+ end
+
+ ##
+ # Set up the standard mapping of attributes to HTML tags
+ #
+ def init_tags
+ @attr_tags = [
+ InlineTag.new(SM::Attribute.bitmap_for(:BOLD), "<b>", "</b>"),
+ InlineTag.new(SM::Attribute.bitmap_for(:TT), "<tt>", "</tt>"),
+ InlineTag.new(SM::Attribute.bitmap_for(:EM), "<em>", "</em>"),
+ ]
+ end
+
+ ##
+ # Add a new set of HTML tags for an attribute. We allow
+ # separate start and end tags for flexibility
+ #
+ def add_tag(name, start, stop)
+ @attr_tags << InlineTag.new(SM::Attribute.bitmap_for(name), start, stop)
+ end
+
+ ##
+ # Given an HTML tag, decorate it with class information
+ # and the like if required. This is a no-op in the base
+ # class, but is overridden in HTML output classes that
+ # implement style sheets
+
+ def annotate(tag)
+ tag
+ end
+
+ ##
+ # Here's the client side of the visitor pattern
+
+ def start_accepting
+ @res = []
+ @list_stack = []
+ end
+
+ def end_accepting
+ @res
+ end
+
+ def accept_paragraph(am, fragment)
+ @res << Flow::P.new((convert_flow(am.flow(fragment.txt))))
+ end
+
+ def accept_verbatim(am, fragment)
+ @res << Flow::VERB.new((convert_flow(am.flow(fragment.txt))))
+ end
+
+ def accept_rule(am, fragment)
+ @res << Rule.new(size)
+ end
+
+ def accept_list_start(am, fragment)
+ @list_stack.push(@res)
+ list = Flow::LIST.new(fragment.type)
+ @res << list
+ @res = list
+ end
+
+ def accept_list_end(am, fragment)
+ @res = @list_stack.pop
+ end
+
+ def accept_list_item(am, fragment)
+ @res << Flow::LI.new(fragment.param, convert_flow(am.flow(fragment.txt)))
+ end
+
+ def accept_blank_line(am, fragment)
+ # @res << annotate("<p />") << "\n"
+ end
+
+ def accept_heading(am, fragment)
+ @res << Flow::H.new(fragment.head_level, am.flow(fragment.txt))
+ end
+
+
+ #######################################################################
+
+ private
+
+ #######################################################################
+
+ def on_tags(res, item)
+ attr_mask = item.turn_on
+ return if attr_mask.zero?
+
+ @attr_tags.each do |tag|
+ if attr_mask & tag.bit != 0
+ res << annotate(tag.on)
+ end
+ end
+ end
+
+ def off_tags(res, item)
+ attr_mask = item.turn_off
+ return if attr_mask.zero?
+
+ @attr_tags.reverse_each do |tag|
+ if attr_mask & tag.bit != 0
+ res << annotate(tag.off)
+ end
+ end
+ end
+
+ def convert_flow(flow)
+ res = ""
+ flow.each do |item|
+ case item
+ when String
+ res << convert_string(item)
+ when AttrChanger
+ off_tags(res, item)
+ on_tags(res, item)
+ when Special
+ res << convert_special(item)
+ else
+ raise "Unknown flow element: #{item.inspect}"
+ end
+ end
+ res
+ end
+
+ # some of these patterns are taken from SmartyPants...
+
+ def convert_string(item)
+ CGI.escapeHTML(item)
+ end
+
+ def convert_special(special)
+ handled = false
+ Attribute.each_name_of(special.type) do |name|
+ method_name = "handle_special_#{name}"
+ if self.respond_to? method_name
+ special.text = send(method_name, special)
+ handled = true
+ end
+ end
+ raise "Unhandled special: #{special}" unless handled
+ special.text
+ end
+
+
+ end
+
+end
diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb
index fe75b10..36b82ea 100644
--- a/lib/rdoc/options.rb
+++ b/lib/rdoc/options.rb
@@ -1,6 +1,8 @@
# We handle the parsing of options, and subsequently as a singleton
# object to be queried for option values
+require "rdoc/ri/ri_paths"
+
class Options
require 'singleton'
@@ -178,6 +180,17 @@ class Options
[ "--quiet", "-q", nil,
"don't show progress as we parse" ],
+ [ "--ri", "-r", nil,
+ "generate output for use by 'ri.' The files are\n" +
+ "stored in the '.rdoc' directory under your home\n"+
+ "directory unless overridden by a subsequent\n" +
+ "--op parameter, so no special privileges are needed." ],
+
+ [ "--ri-site", "-R", nil,
+ "generate output for use by 'ri.' The files are\n" +
+ "stored in a site-wide directory, making them accessible\n"+
+ "to others, so special privileges are needed." ],
+
[ "--show-hash", "-H", nil,
"A name of the form #name in a comment\n" +
"is a possible hyperlink to an instance\n" +
@@ -391,15 +404,7 @@ class Options
when "--fmt"
@generator_name = arg.downcase
- @generator = generators[@generator_name]
- if !@generator
- OptionList.error("Invalid output formatter")
- end
-
- if @generator_name == "xml"
- @all_one_file = true
- @inline_source = true
- end
+ setup_generator(generators)
when "--help"
OptionList.usage(generators.keys)
@@ -417,6 +422,11 @@ class Options
when "--include"
@rdoc_include.concat arg.split(/\s*,\s*/)
+ when "--ri", "--ri-site"
+ @generator_name = "ri"
+ @op_dir = opt == "--ri" ? RI::Paths::HOMEDIR : RI::Paths::SITEDIR
+ setup_generator(generators)
+
when "--tab-width"
begin
@tab_width = Integer(arg)
@@ -479,6 +489,19 @@ class Options
private
+ # Set up an output generator for the format in @generator_name
+ def setup_generator(generators)
+ @generator = generators[@generator_name]
+ if !@generator
+ OptionList.error("Invalid output formatter")
+ end
+
+ if @generator_name == "xml"
+ @all_one_file = true
+ @inline_source = true
+ end
+ end
+
# Check that the right version of 'dot' is available.
# Unfortuately this doesn't work correctly under Windows NT,
# so we'll bypass the test under Windows
diff --git a/lib/rdoc/ri/ri_cache.rb b/lib/rdoc/ri/ri_cache.rb
new file mode 100644
index 0000000..4ca976c
--- /dev/null
+++ b/lib/rdoc/ri/ri_cache.rb
@@ -0,0 +1,145 @@
+module RI
+
+ class ClassEntry
+
+ attr_reader :name
+ attr_reader :path_name
+
+ def initialize(path_name, name, in_class)
+ @path_name = path_name
+ @name = name
+ @in_class = in_class
+ @class_methods = []
+ @instance_methods = []
+ @inferior_classes = []
+ 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 = external_name
+ list = is_class_method ? @class_methods : @instance_methods
+ path = File.join(dir, name)
+ list << MethodEntry.new(path, internal_name, is_class_method, self)
+ else
+ full_name = File.join(dir, name)
+ if File.directory?(full_name)
+ inf_class = ClassEntry.new(full_name, name, self)
+ inf_class.load_from(full_name)
+ @inferior_classes << inf_class
+ 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
+
+ # 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)
+ local_methods_matching(name)
+ end
+
+ # Find methods matching 'name' in ourselves and in
+ # any classes we contain
+ def recursively_find_methods_matching(name)
+ res = local_methods_matching(name)
+ @inferior_classes.each do |c|
+ res.concat(c.recursively_find_methods_matching(name))
+ end
+ res
+ end
+
+
+ # Return our full name
+ def full_name
+ res = @in_class.full_name
+ res << "::" unless res.empty?
+ res << @name
+ end
+
+ private
+
+ # Return a list of all our methods matching a given string
+ def local_methods_matching(name)
+ @class_methods.find_all {|m| m.name[name] } +
+ @instance_methods.find_all {|m| 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 TopLevelEntry < ClassEntry
+ def methods_matching(name)
+ res = recursively_find_methods_matching(name)
+ end
+
+ def full_name
+ ""
+ end
+ end
+
+ class 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 know about all 'ri' files
+ # accessible to this program
+
+ class RiCache
+
+ attr_reader :toplevel
+
+ def initialize(dirs)
+ # At the top level we have a dummy module holding the
+ # overall namespace
+ @toplevel = TopLevelEntry.new('', '::', nil)
+
+ dirs.each do |dir|
+ @toplevel.load_from(dir)
+ end
+ end
+
+ end
+end
diff --git a/lib/rdoc/ri/ri_descriptions.rb b/lib/rdoc/ri/ri_descriptions.rb
new file mode 100644
index 0000000..e80b4eb
--- /dev/null
+++ b/lib/rdoc/ri/ri_descriptions.rb
@@ -0,0 +1,58 @@
+require 'yaml'
+
+module RI
+ Alias = Struct.new(:old_name, :new_name)
+ AliasName = Struct.new(:name)
+ Attribute = Struct.new(:name, :rw, :comment)
+ Constant = Struct.new(:name, :value, :comment)
+ IncludedModule = Struct.new(:name)
+
+ class MethodSummary
+ attr_accessor :name
+ def initialize(name="")
+ @name = name
+ end
+
+ def <=>(other)
+ self.name <=> other.name
+ end
+ end
+
+
+ class Description
+ attr_accessor :name
+ attr_accessor :full_name
+ attr_accessor :comment
+
+ def serialize
+ self.to_yaml
+ end
+
+ def Description.deserialize(from)
+ YAML.load(from)
+ end
+ end
+
+ class ClassDescription < Description
+
+ attr_accessor :method_list
+ attr_accessor :attributes
+ attr_accessor :constants
+ attr_accessor :superclass
+ attr_accessor :includes
+
+ end
+
+ class MethodDescription < 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
+
+ end
+
+end
diff --git a/lib/rdoc/ri/ri_formatter.rb b/lib/rdoc/ri/ri_formatter.rb
new file mode 100644
index 0000000..052ac87
--- /dev/null
+++ b/lib/rdoc/ri/ri_formatter.rb
@@ -0,0 +1,170 @@
+module RI
+ class RiFormatter
+
+ attr_reader :indent
+
+ def initialize(width, indent)
+ @width = width
+ @indent = indent
+ end
+
+
+ ######################################################################
+
+ def draw_line(label=nil)
+ len = @width
+ len -= (label.size+1) if label
+ print "-"*len
+ print(" ", label) if label
+ puts
+ end
+
+ ######################################################################
+
+ def wrap(txt, prefix=@indent, linelen=@width)
+ return unless txt && !txt.empty?
+ work = txt.dup
+ 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?
+ puts (prefix + res.join("\n" + next_prefix))
+ end
+
+ ######################################################################
+
+ def blankline
+ puts
+ end
+
+ ######################################################################
+
+ # convert HTML entities back to ASCII
+ def conv_html(txt)
+ txt.
+ gsub(%r{<tt>(.*?)</tt>}) { "+#$1+" } .
+ gsub(%r{<b>(.*?)</b>}) { "*#$1*" } .
+ gsub(%r{<i>(.*?)</i>}) { "_#$1_" } .
+ gsub(/&gt;/, '>').
+ gsub(/&lt;/, '<').
+ gsub(/&quot;/, '"').
+ gsub(/&amp;/, '&')
+
+ end
+
+ ######################################################################
+
+ def display_list(list)
+ case list.type
+
+ when SM::ListBase::BULLET
+ prefixer = proc { |ignored| @indent + "* " }
+
+ when SM::ListBase::NUMBER,
+ SM::ListBase::UPPERALPHA,
+ SM::ListBase::LOWERALPHA
+
+ start = case list.type
+ when SM::ListBase::NUMBER then 1
+ when SM::ListBase::UPPERALPHA then 'A'
+ when SM::ListBase::LOWERALPHA then 'a'
+ end
+ prefixer = proc do |ignored|
+ res = @indent + "#{start}.".ljust(4)
+ start = start.succ
+ res
+ end
+
+ when SM::ListBase::LABELED
+ prefixer = proc do |li|
+ li.label
+ end
+
+ when SM::ListBase::NOTE
+ longest = 0
+ list.contents.each do |item|
+ if item.kind_of?(SM::Flow::LI) && item.label.length > longest
+ longest = item.label.length
+ end
+ end
+
+ prefixer = proc do |li|
+ @indent + li.label.ljust(longest+1)
+ end
+
+ else
+ fail "unknown list type"
+
+ end
+
+ list.contents.each do |item|
+ if item.kind_of? SM::Flow::LI
+ 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 SM::Flow::P, SM::Flow::LI
+ wrap(conv_html(item.body), prefix)
+ blankline
+
+ when SM::Flow::LIST
+ display_list(item)
+
+ when SM::Flow::VERB
+ item.body.split(/\n/).each do |line|
+ print @indent, conv_html(line), "\n"
+ end
+ blankline
+
+ when SM::Flow::H
+ text = conv_html(item.text.join)
+ case item.level
+ when 1
+ ul = "=" * text.length
+ puts
+ puts text.upcase
+ puts ul
+ puts
+
+ when 2
+ ul = "-" * text.length
+ puts
+ puts text
+ puts ul
+ puts
+ else
+ print "\n", @indent, text, "\n\n"
+ end
+ else
+ fail "Unknown flow element: #{item.class}"
+ end
+ end
+
+ ######################################################################
+
+ def display_flow(flow)
+ flow.each do |f|
+ display_flow_item(f)
+ end
+ end
+ end
+end
diff --git a/lib/rdoc/ri/ri_paths.rb b/lib/rdoc/ri/ri_paths.rb
new file mode 100644
index 0000000..14288d9
--- /dev/null
+++ b/lib/rdoc/ri/ri_paths.rb
@@ -0,0 +1,41 @@
+module 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:: $prefix/lib/ruby/<version>/doc/rdoc
+ # site:: $prefix/lib/ruby/site_dir/<version>/doc/rdoc
+ # user:: ~/.rdoc
+
+ module Paths
+
+ #:stopdoc:
+ require 'rbconfig'
+
+ DOC_DIR = "doc/rdoc"
+
+ SYSDIR = File.join(Config::CONFIG['rubylibdir'], DOC_DIR)
+ SITEDIR = File.join(Config::CONFIG['sitelibdir'], DOC_DIR)
+ homedir = ENV['HOME'] || ENV['USERPROFILE'] || ENV['HOMEPATH']
+
+ if homedir
+ HOMEDIR = File.join(homedir, ".rdoc")
+ else
+ HOMEDIR = nil
+ end
+
+ PATH = [ SYSDIR, SITEDIR, HOMEDIR ].find_all {|p| p && File.directory?(p)}
+ end
+end
diff --git a/lib/rdoc/ri/ri_reader.rb b/lib/rdoc/ri/ri_reader.rb
new file mode 100644
index 0000000..f7e9e30
--- /dev/null
+++ b/lib/rdoc/ri/ri_reader.rb
@@ -0,0 +1,46 @@
+require 'rdoc/ri/ri_descriptions'
+require 'rdoc/ri/ri_writer'
+require 'rdoc/markup/simple_markup/to_flow'
+
+module RI
+ class RiReader
+
+ 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_methods(name, is_class_method, namespaces)
+ result = []
+ namespaces.each do |ns|
+ result.concat ns.methods_matching(name)
+ 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| RI::Description.deserialize(f) }
+ end
+
+ # Return a class description
+ def get_class(class_entry)
+ path = RiWriter.class_desc_path(class_entry.path_name, class_entry)
+ File.open(path) {|f| RI::Description.deserialize(f) }
+ end
+
+ end
+end
diff --git a/lib/rdoc/ri/ri_util.rb b/lib/rdoc/ri/ri_util.rb
new file mode 100644
index 0000000..07f79b1
--- /dev/null
+++ b/lib/rdoc/ri/ri_util.rb
@@ -0,0 +1,63 @@
+######################################################################
+
+class RiError < Exception; end
+#
+# Break argument into its constituent class or module names, an
+# optional method type, and a method name
+
+class NameDescriptor
+
+ attr_reader :class_names
+ attr_reader :method_name
+ 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 = "."
+
+ tokens = arg.split(/\b/)
+
+ # 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
+ 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 RiError.new("Bad argument: #{arg}")
+ end
+ @is_class_method = separator == "::"
+ end
+ end
+end
diff --git a/lib/rdoc/ri/ri_writer.rb b/lib/rdoc/ri/ri_writer.rb
new file mode 100644
index 0000000..072b3ac
--- /dev/null
+++ b/lib/rdoc/ri/ri_writer.rb
@@ -0,0 +1,48 @@
+require 'fileutils'
+
+module RI
+ class RiWriter
+
+ def RiWriter.class_desc_path(dir, class_desc)
+ File.join(dir, "cdesc-" + class_desc.name + ".yaml")
+ 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 = RiWriter.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)
+ meth_file_name = File.join(dir, method_desc.name)
+ if method_desc.is_class_method
+ 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
+end