From c5bbcadbe64477433a243be191c41010c7ae10dc Mon Sep 17 00:00:00 2001 From: dave Date: Tue, 16 Dec 2003 05:44:25 +0000 Subject: 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 --- MANIFEST | 10 ++ bin/ri | 201 ++++++++++++++++++++++++++++++ lib/rdoc/generators/ri_generator.rb | 202 +++++++++++++++++++++++++++++++ lib/rdoc/markup/simple_markup/to_flow.rb | 187 ++++++++++++++++++++++++++++ lib/rdoc/options.rb | 41 +++++-- lib/rdoc/ri/ri_cache.rb | 145 ++++++++++++++++++++++ lib/rdoc/ri/ri_descriptions.rb | 58 +++++++++ lib/rdoc/ri/ri_formatter.rb | 170 ++++++++++++++++++++++++++ lib/rdoc/ri/ri_paths.rb | 41 +++++++ lib/rdoc/ri/ri_reader.rb | 46 +++++++ lib/rdoc/ri/ri_util.rb | 63 ++++++++++ lib/rdoc/ri/ri_writer.rb | 48 ++++++++ 12 files changed, 1203 insertions(+), 9 deletions(-) create mode 100755 bin/ri create mode 100644 lib/rdoc/generators/ri_generator.rb create mode 100644 lib/rdoc/markup/simple_markup/to_flow.rb create mode 100644 lib/rdoc/ri/ri_cache.rb create mode 100644 lib/rdoc/ri/ri_descriptions.rb create mode 100644 lib/rdoc/ri/ri_formatter.rb create mode 100644 lib/rdoc/ri/ri_paths.rb create mode 100644 lib/rdoc/ri/ri_reader.rb create mode 100644 lib/rdoc/ri/ri_util.rb create mode 100644 lib/rdoc/ri/ri_writer.rb diff --git a/MANIFEST b/MANIFEST index 40c60ae64a..32294deb09 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 0000000000..899fc2dd28 --- /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 0000000000..db86d744af --- /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 <\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 0000000000..b91bb12e1f --- /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 => [ "" ], + ListBase::NUMBER => [ "
    ", "
" ], + ListBase::UPPERALPHA => [ "
    ", "
" ], + ListBase::LOWERALPHA => [ "
    ", "
" ], + ListBase::LABELED => [ "
", "
" ], + ListBase::NOTE => [ "", "
" ], + } + + 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), "", ""), + InlineTag.new(SM::Attribute.bitmap_for(:TT), "", ""), + InlineTag.new(SM::Attribute.bitmap_for(: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("

") << "\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 fe75b10ce8..36b82eab7d 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 0000000000..4ca976c4b9 --- /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 0000000000..e80b4ebe05 --- /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 0000000000..052ac87906 --- /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{(.*?)}) { "+#$1+" } . + gsub(%r{(.*?)}) { "*#$1*" } . + gsub(%r{(.*?)}) { "_#$1_" } . + gsub(/>/, '>'). + gsub(/</, '<'). + gsub(/"/, '"'). + gsub(/&/, '&') + + 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 0000000000..14288d9a94 --- /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//doc/rdoc + # site:: $prefix/lib/ruby/site_dir//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 0000000000..f7e9e3074c --- /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 0000000000..07f79b1d62 --- /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 0000000000..072b3acfea --- /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 -- cgit v1.2.3