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 --- 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 ++++++++++++ 7 files changed, 571 insertions(+) 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 (limited to 'lib/rdoc/ri') 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