From 455b051a008aa859a7b0d6fca56de69a38cc1f8d Mon Sep 17 00:00:00 2001 From: drbrain Date: Sun, 10 Feb 2008 03:59:08 +0000 Subject: * lib/rdoc/code_objects.rb: Make some attributes accessible for reuse. * lib/rdoc/generator/html.rb: Pull out ContextUser classes and related methods for reuse. * lib/rdoc/generator.rb: Move ContextUser classes to RDoc::Generator::Context for reuse. * lib/rdoc/rdoc.rb: Make RDoc::RDoc initialization a little easier. * lib/rdoc/options.rb: Make RDoc::Options easier to use without parsing an ARGV. * lib/rdoc/markup/to_*.rb: Subclass RDoc::Markup::Formatter. * lib/rdoc/markup/formatter.rb: Add RDoc::Markup::Formatter to make RDoc markup conversion easier. * lib/rdoc/markup/fragments.rb: Make RDoc::Markup::ListItem easier to test. * lib/rdoc/markup/to_html_hyperlink.rb: Pulled out of the HTML generator for easier reusability. * lib/rdoc/markup.rb: Fix bug with labeled lists containing bullet lists. * lib/rdoc/generators/html/html.rb: Fix Constant display. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@15421 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- lib/rdoc/generator.rb | 1024 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1023 insertions(+), 1 deletion(-) (limited to 'lib/rdoc/generator.rb') diff --git a/lib/rdoc/generator.rb b/lib/rdoc/generator.rb index 7db23058e3..b62cd00506 100644 --- a/lib/rdoc/generator.rb +++ b/lib/rdoc/generator.rb @@ -1,7 +1,7 @@ require 'cgi' require 'rdoc' require 'rdoc/options' -require 'rdoc/markup' +require 'rdoc/markup/to_html_hyperlink' require 'rdoc/template' module RDoc::Generator @@ -21,5 +21,1027 @@ module RDoc::Generator CSS_NAME = "rdoc-style.css" + ## + # Converts a target url to one that is relative to a given path + + def self.gen_url(path, target) + from = ::File.dirname path + to, to_file = ::File.split target + + from = from.split "/" + to = to.split "/" + + while from.size > 0 and to.size > 0 and from[0] == to[0] do + from.shift + to.shift + end + + from.fill ".." + from.concat to + from << to_file + ::File.join(*from) + end + + ## + # Build a hash of all items that can be cross-referenced. This is used when + # we output required and included names: if the names appear in this hash, + # we can generate an html cross reference to the appropriate description. + # We also use this when parsing comment blocks: any decorated words matching + # an entry in this list are hyperlinked. + + class AllReferences + @@refs = {} + + def AllReferences::reset + @@refs = {} + end + + def AllReferences.add(name, html_class) + @@refs[name] = html_class + end + + def AllReferences.[](name) + @@refs[name] + end + + def AllReferences.keys + @@refs.keys + end + end + + ## + # Handle common markup tasks for the various Context subclasses + + module MarkUp + + ## + # Convert a string in markup format into HTML. + + def markup(str, remove_para = false) + return '' unless str + + unless defined? @formatter then + @formatter = RDoc::Markup::ToHtmlHyperlink.new(path, self, + @options.show_hash) + end + + # Convert leading comment markers to spaces, but only if all non-blank + # lines have them + if str =~ /^(?>\s*)[^\#]/ then + content = str + else + content = str.gsub(/^\s*(#+)/) { $1.tr '#', ' ' } + end + + res = @formatter.convert content + + if remove_para then + res.sub!(/^

/, '') + res.sub!(/<\/p>$/, '') + end + + res + end + + ## + # Qualify a stylesheet URL; if if +css_name+ does not begin with '/' or + # 'http[s]://', prepend a prefix relative to +path+. Otherwise, return it + # unmodified. + + def style_url(path, css_name=nil) +# $stderr.puts "style_url( #{path.inspect}, #{css_name.inspect} )" + css_name ||= CSS_NAME + if %r{^(https?:/)?/} =~ css_name + css_name + else + RDoc::Generator.gen_url path, css_name + end + end + + ## + # Build a webcvs URL with the given 'url' argument. URLs with a '%s' in them + # get the file's path sprintfed into them; otherwise they're just catenated + # together. + + def cvs_url(url, full_path) + if /%s/ =~ url + return sprintf( url, full_path ) + else + return url + full_path + end + end + + end + + ## + # A Context is built by the parser to represent a container: contexts hold + # classes, modules, methods, require lists and include lists. ClassModule + # and TopLevel are the context objects we process here + + class Context + + include MarkUp + + attr_reader :context + + ## + # Generate: + # + # * a list of RDoc::Generator::File objects for each TopLevel object + # * a list of RDoc::Generator::Class objects for each first level class or + # module in the TopLevel objects + # * a complete list of all hyperlinkable terms (file, class, module, and + # method names) + + def self.build_indicies(toplevels, options) + files = [] + classes = [] + + toplevels.each do |toplevel| + files << RDoc::Generator::File.new(toplevel, options, + RDoc::Generator::FILE_DIR) + end + + RDoc::TopLevel.all_classes_and_modules.each do |cls| + build_class_list(classes, options, cls, files[0], + RDoc::Generator::CLASS_DIR) + end + + return files, classes + end + + def self.build_class_list(classes, options, from, html_file, class_dir) + classes << RDoc::Generator::Class.new(from, html_file, class_dir, options) + + from.each_classmodule do |mod| + build_class_list(classes, options, mod, html_file, class_dir) + end + end + + def initialize(context, options) + @context = context + @options = options + + # HACK ugly + @template = options.template_class + end + + ## + # convenience method to build a hyperlink + + def href(link, cls, name) + %{#{name}} #" + end + + ## + # Returns a reference to outselves to be used as an href= the form depends + # on whether we're all in one file or in multiple files + + def as_href(from_path) + if @options.all_one_file + "#" + path + else + RDoc::Generator.gen_url from_path, path + end + end + + ## + # Create a list of Method objects for each method in the corresponding + # context object. If the @options.show_all variable is set (corresponding + # to the --all option, we include all methods, otherwise just the + # public ones. + + def collect_methods + list = @context.method_list + + unless @options.show_all then + list = list.find_all do |m| + m.visibility == :public or + m.visibility == :protected or + m.force_documentation + end + end + + @methods = list.collect do |m| + RDoc::Generator::Method.new m, self, @options + end + end + + ## + # Build a summary list of all the methods in this context + + def build_method_summary_list(path_prefix="") + collect_methods unless @methods + meths = @methods.sort + res = [] + meths.each do |meth| + res << { + "name" => CGI.escapeHTML(meth.name), + "aref" => "#{path_prefix}\##{meth.aref}" + } + end + res + end + + ## + # Build a list of aliases for which we couldn't find a + # corresponding method + + def build_alias_summary_list(section) + values = [] + @context.aliases.each do |al| + next unless al.section == section + res = { + 'old_name' => al.old_name, + 'new_name' => al.new_name, + } + if al.comment && !al.comment.empty? + res['desc'] = markup(al.comment, true) + end + values << res + end + values + end + + ## + # Build a list of constants + + def build_constants_summary_list(section) + values = [] + @context.constants.each do |co| + next unless co.section == section + res = { + 'name' => co.name, + 'value' => CGI.escapeHTML(co.value) + } + res['desc'] = markup(co.comment, true) if co.comment && !co.comment.empty? + values << res + end + values + end + + def build_requires_list(context) + potentially_referenced_list(context.requires) {|fn| [fn + ".rb"] } + end + + def build_include_list(context) + potentially_referenced_list(context.includes) + end + + ## + # Build a list from an array of Context items. Look up each in the + # AllReferences hash: if we find a corresponding entry, we generate a + # hyperlink to it, otherwise just output the name. However, some names + # potentially need massaging. For example, you may require a Ruby file + # without the .rb extension, but the file names we know about may have it. + # To deal with this, we pass in a block which performs the massaging, + # returning an array of alternative names to match + + def potentially_referenced_list(array) + res = [] + array.each do |i| + ref = AllReferences[i.name] +# if !ref +# container = @context.parent +# while !ref && container +# name = container.name + "::" + i.name +# ref = AllReferences[name] +# container = container.parent +# end +# end + + ref = @context.find_symbol(i.name) + ref = ref.viewer if ref + + if !ref && block_given? + possibles = yield(i.name) + while !ref and !possibles.empty? + ref = AllReferences[possibles.shift] + end + end + h_name = CGI.escapeHTML(i.name) + if ref and ref.document_self + path = url(ref.path) + res << { "name" => h_name, "aref" => path } + else + res << { "name" => h_name } + end + end + res + end + + ## + # Build an array of arrays of method details. The outer array has up + # to six entries, public, private, and protected for both class + # methods, the other for instance methods. The inner arrays contain + # a hash for each method + + def build_method_detail_list(section) + outer = [] + + methods = @methods.sort + for singleton in [true, false] + for vis in [ :public, :protected, :private ] + res = [] + methods.each do |m| + if m.section == section and + m.document_self and + m.visibility == vis and + m.singleton == singleton + row = {} + if m.call_seq + row["callseq"] = m.call_seq.gsub(/->/, '→') + else + row["name"] = CGI.escapeHTML(m.name) + row["params"] = m.params + end + desc = m.description.strip + row["m_desc"] = desc unless desc.empty? + row["aref"] = m.aref + row["visibility"] = m.visibility.to_s + + alias_names = [] + m.aliases.each do |other| + if other.viewer # won't be if the alias is private + alias_names << { + 'name' => other.name, + 'aref' => other.viewer.as_href(path) + } + end + end + unless alias_names.empty? + row["aka"] = alias_names + end + + if @options.inline_source + code = m.source_code + row["sourcecode"] = code if code + else + code = m.src_url + if code + row["codeurl"] = code + row["imgurl"] = m.img_url + end + end + res << row + end + end + if res.size > 0 + outer << { + "type" => vis.to_s.capitalize, + "category" => singleton ? "Class" : "Instance", + "methods" => res + } + end + end + end + outer + end + + ## + # Build the structured list of classes and modules contained + # in this context. + + def build_class_list(level, from, section, infile=nil) + res = "" + prefix = "  ::" * level; + + from.modules.sort.each do |mod| + next unless mod.section == section + next if infile && !mod.defined_in?(infile) + if mod.document_self + res << + prefix << + "Module " << + href(url(mod.viewer.path), "link", mod.full_name) << + "
\n" << + build_class_list(level + 1, mod, section, infile) + end + end + + from.classes.sort.each do |cls| + next unless cls.section == section + next if infile && !cls.defined_in?(infile) + if cls.document_self + res << + prefix << + "Class " << + href(url(cls.viewer.path), "link", cls.full_name) << + "
\n" << + build_class_list(level + 1, cls, section, infile) + end + end + + res + end + + def url(target) + RDoc::Generator.gen_url path, target + end + + def aref_to(target) + if @options.all_one_file + "#" + target + else + url(target) + end + end + + def document_self + @context.document_self + end + + def diagram_reference(diagram) + res = diagram.gsub(/((?:src|href)=")(.*?)"/) { + $1 + url($2) + '"' + } + res + end + + ## + # Find a symbol in ourselves or our parent + + def find_symbol(symbol, method=nil) + res = @context.find_symbol(symbol, method) + if res + res = res.viewer + end + res + end + + ## + # create table of contents if we contain sections + + def add_table_of_sections + toc = [] + @context.sections.each do |section| + if section.title + toc << { + 'secname' => section.title, + 'href' => section.sequence + } + end + end + + @values['toc'] = toc unless toc.empty? + end + + end + + ## + # Wrap a ClassModule context + + class Class < Context + + attr_reader :methods + attr_reader :path + + def initialize(context, html_file, prefix, options) + super(context, options) + + @html_file = html_file + @is_module = context.is_module? + @values = {} + + context.viewer = self + + if options.all_one_file + @path = context.full_name + else + @path = http_url(context.full_name, prefix) + end + + collect_methods + + AllReferences.add(name, self) + end + + ## + # Returns the relative file name to store this class in, which is also its + # url + + def http_url(full_name, prefix) + path = full_name.dup + + path.gsub!(/<<\s*(\w*)/) { "from-#$1" } if path['<<'] + + ::File.join(prefix, path.split("::")) + ".html" + end + + def name + @context.full_name + end + + def parent_name + @context.parent.full_name + end + + def index_name + name + end + + def write_on(f) + value_hash + template = RDoc::TemplatePage.new(@template::BODY, + @template::CLASS_PAGE, + @template::METHOD_LIST) + template.write_html_on(f, @values) + end + + def value_hash + class_attribute_values + add_table_of_sections + + @values["charset"] = @options.charset + @values["style_url"] = style_url(path, @options.css) + + d = markup(@context.comment) + @values["description"] = d unless d.empty? + + ml = build_method_summary_list @path + @values["methods"] = ml unless ml.empty? + + il = build_include_list(@context) + @values["includes"] = il unless il.empty? + + @values["sections"] = @context.sections.map do |section| + + secdata = { + "sectitle" => section.title, + "secsequence" => section.sequence, + "seccomment" => markup(section.comment) + } + + al = build_alias_summary_list(section) + secdata["aliases"] = al unless al.empty? + + co = build_constants_summary_list(section) + secdata["constants"] = co unless co.empty? + + al = build_attribute_list(section) + secdata["attributes"] = al unless al.empty? + + cl = build_class_list(0, @context, section) + secdata["classlist"] = cl unless cl.empty? + + mdl = build_method_detail_list(section) + secdata["method_list"] = mdl unless mdl.empty? + + secdata + end + + @values + end + + def build_attribute_list(section) + atts = @context.attributes.sort + res = [] + atts.each do |att| + next unless att.section == section + if att.visibility == :public || att.visibility == :protected || @options.show_all + entry = { + "name" => CGI.escapeHTML(att.name), + "rw" => att.rw, + "a_desc" => markup(att.comment, true) + } + unless att.visibility == :public || att.visibility == :protected + entry["rw"] << "-" + end + res << entry + end + end + res + end + + def class_attribute_values + h_name = CGI.escapeHTML(name) + + @values["path"] = @path + @values["classmod"] = @is_module ? "Module" : "Class" + @values["title"] = "#{@values['classmod']}: #{h_name}" + + c = @context + c = c.parent while c and !c.diagram + if c && c.diagram + @values["diagram"] = diagram_reference(c.diagram) + end + + @values["full_name"] = h_name + + parent_class = @context.superclass + + if parent_class + @values["parent"] = CGI.escapeHTML(parent_class) + + if parent_name + lookup = parent_name + "::" + parent_class + else + lookup = parent_class + end + + parent_url = AllReferences[lookup] || AllReferences[parent_class] + + if parent_url and parent_url.document_self + @values["par_url"] = aref_to(parent_url.path) + end + end + + files = [] + @context.in_files.each do |f| + res = {} + full_path = CGI.escapeHTML(f.file_absolute_name) + + res["full_path"] = full_path + res["full_path_url"] = aref_to(f.viewer.path) if f.document_self + + if @options.webcvs + res["cvsurl"] = cvs_url( @options.webcvs, full_path ) + end + + files << res + end + + @values['infiles'] = files + end + + def <=>(other) + self.name <=> other.name + end + + end + + ## + # Handles the mapping of a file's information to HTML. In reality, a file + # corresponds to a +TopLevel+ object, containing modules, classes, and + # top-level methods. In theory it _could_ contain attributes and aliases, + # but we ignore these for now. + + class File < Context + + attr_reader :path + attr_reader :name + + def initialize(context, options, file_dir) + super(context, options) + + @values = {} + + if options.all_one_file + @path = filename_to_label + else + @path = http_url(file_dir) + end + + @name = @context.file_relative_name + + collect_methods + AllReferences.add(name, self) + context.viewer = self + end + + def http_url(file_dir) + ::File.join file_dir, "#{@context.file_relative_name.tr '.', '_'}.html" + end + + def filename_to_label + @context.file_relative_name.gsub(/%|\/|\?|\#/) do |s| + '%%%x' % s[0].unpack('C') + end + end + + def index_name + name + end + + def parent_name + nil + end + + def value_hash + file_attribute_values + add_table_of_sections + + @values["charset"] = @options.charset + @values["href"] = path + @values["style_url"] = style_url(path, @options.css) + + if @context.comment + d = markup(@context.comment) + @values["description"] = d if d.size > 0 + end + + ml = build_method_summary_list + @values["methods"] = ml unless ml.empty? + + il = build_include_list(@context) + @values["includes"] = il unless il.empty? + + rl = build_requires_list(@context) + @values["requires"] = rl unless rl.empty? + + if @options.promiscuous + file_context = nil + else + file_context = @context + end + + + @values["sections"] = @context.sections.map do |section| + + secdata = { + "sectitle" => section.title, + "secsequence" => section.sequence, + "seccomment" => markup(section.comment) + } + + cl = build_class_list(0, @context, section, file_context) + @values["classlist"] = cl unless cl.empty? + + mdl = build_method_detail_list(section) + secdata["method_list"] = mdl unless mdl.empty? + + al = build_alias_summary_list(section) + secdata["aliases"] = al unless al.empty? + + co = build_constants_summary_list(section) + @values["constants"] = co unless co.empty? + + secdata + end + + @values + end + + def write_on(f) + value_hash + + template = RDoc::TemplatePage.new(@template::BODY, + @template::FILE_PAGE, + @template::METHOD_LIST) + + template.write_html_on(f, @values) + end + + def file_attribute_values + full_path = @context.file_absolute_name + short_name = ::File.basename full_path + + @values["title"] = CGI.escapeHTML("File: #{short_name}") + + if @context.diagram then + @values["diagram"] = diagram_reference(@context.diagram) + end + + @values["short_name"] = CGI.escapeHTML(short_name) + @values["full_path"] = CGI.escapeHTML(full_path) + @values["dtm_modified"] = @context.file_stat.mtime.to_s + + if @options.webcvs then + @values["cvsurl"] = cvs_url @options.webcvs, @values["full_path"] + end + end + + def <=>(other) + self.name <=> other.name + end + + end + + class Method + + include MarkUp + + attr_reader :context + attr_reader :src_url + attr_reader :img_url + attr_reader :source_code + + @@seq = "M000000" + + @@all_methods = [] + + def self.all_methods + @@all_methods + end + + def self.reset + @@all_methods = [] + end + + def initialize(context, html_class, options) + @context = context + @html_class = html_class + @options = options + + # HACK ugly + @template = options.template_class + + @@seq = @@seq.succ + @seq = @@seq + @@all_methods << self + + context.viewer = self + + if (ts = @context.token_stream) + @source_code = markup_code(ts) + unless @options.inline_source + @src_url = create_source_code_file(@source_code) + @img_url = RDoc::Generator.gen_url path, 'source.png' + end + end + + AllReferences.add(name, self) + end + + ## + # Returns a reference to outselves to be used as an href= the form depends + # on whether we're all in one file or in multiple files + + def as_href(from_path) + if @options.all_one_file + "#" + path + else + RDoc::Generator.gen_url from_path, path + end + end + + def name + @context.name + end + + def section + @context.section + end + + def index_name + "#{@context.name} (#{@html_class.name})" + end + + def parent_name + if @context.parent.parent + @context.parent.parent.full_name + else + nil + end + end + + def aref + @seq + end + + def path + if @options.all_one_file + aref + else + @html_class.path + "#" + aref + end + end + + def description + markup(@context.comment) + end + + def visibility + @context.visibility + end + + def singleton + @context.singleton + end + + def call_seq + cs = @context.call_seq + if cs + cs.gsub(/\n/, "
\n") + else + nil + end + end + + def params + # params coming from a call-seq in 'C' will start with the + # method name + if p !~ /^\w/ + p = @context.params.gsub(/\s*\#.*/, '') + p = p.tr("\n", " ").squeeze(" ") + p = "(" + p + ")" unless p[0] == ?( + + if (block = @context.block_params) + # If this method has explicit block parameters, remove any + # explicit &block + + p.sub!(/,?\s*&\w+/, '') + + block.gsub!(/\s*\#.*/, '') + block = block.tr("\n", " ").squeeze(" ") + if block[0] == ?( + block.sub!(/^\(/, '').sub!(/\)/, '') + end + p << " {|#{block.strip}| ...}" + end + end + CGI.escapeHTML(p) + end + + def create_source_code_file(code_body) + meth_path = @html_class.path.sub(/\.html$/, '.src') + FileUtils.mkdir_p(meth_path) + file_path = ::File.join meth_path, "#{@seq}.html" + + template = RDoc::TemplatePage.new(@template::SRC_PAGE) + + open file_path, 'w' do |f| + values = { + 'title' => CGI.escapeHTML(index_name), + 'code' => code_body, + 'style_url' => style_url(file_path, @options.css), + 'charset' => @options.charset + } + template.write_html_on(f, values) + end + + RDoc::Generator.gen_url path, file_path + end + + def <=>(other) + @context <=> other.context + end + + ## + # Given a sequence of source tokens, mark up the source code + # to make it look purty. + + def markup_code(tokens) + src = "" + tokens.each do |t| + next unless t + # p t.class +# style = STYLE_MAP[t.class] + style = case t + when RubyToken::TkCONSTANT then "ruby-constant" + when RubyToken::TkKW then "ruby-keyword kw" + when RubyToken::TkIVAR then "ruby-ivar" + when RubyToken::TkOp then "ruby-operator" + when RubyToken::TkId then "ruby-identifier" + when RubyToken::TkNode then "ruby-node" + when RubyToken::TkCOMMENT then "ruby-comment cmt" + when RubyToken::TkREGEXP then "ruby-regexp re" + when RubyToken::TkSTRING then "ruby-value str" + when RubyToken::TkVal then "ruby-value" + else + nil + end + + text = CGI.escapeHTML(t.text) + + if style + src << "#{text}" + else + src << text + end + end + + add_line_numbers(src) if @options.include_line_numbers + src + end + + ## + # We rely on the fact that the first line of a source code listing has + # # File xxxxx, line dddd + + def add_line_numbers(src) + if src =~ /\A.*, line (\d+)/ + first = $1.to_i - 1 + last = first + src.count("\n") + size = last.to_s.length + real_fmt = "%#{size}d: " + fmt = " " * (size+2) + src.gsub!(/^/) do + res = sprintf(fmt, first) + first += 1 + fmt = real_fmt + res + end + end + end + + def document_self + @context.document_self + end + + def aliases + @context.aliases + end + + def find_symbol(symbol, method=nil) + res = @context.parent.find_symbol(symbol, method) + if res + res = res.viewer + end + res + end + + end + end -- cgit v1.2.3