diff --git a/ChangeLog b/ChangeLog index 2a6aee983e..104835a3d5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,24 @@ +Sun Feb 10 12:58:33 2008 Eric Hodel + + * 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. + Sat Feb 9 23:44:29 2008 Tanaka Akira * missing/tgamma.c (tgamma): use lgamma_r if available. diff --git a/lib/rdoc/code_objects.rb b/lib/rdoc/code_objects.rb index dfc0fff9cc..da383d69c4 100644 --- a/lib/rdoc/code_objects.rb +++ b/lib/rdoc/code_objects.rb @@ -5,10 +5,10 @@ require 'rdoc/tokenstream' module RDoc - + ## # We contain the common stuff for contexts (which are containers) # and other elements (methods, attributes and so on) - # + class CodeObject attr_accessor :parent @@ -82,8 +82,7 @@ module RDoc # Access the code object's comment attr_reader :comment - # Update the comment, but don't overwrite a real comment - # with an empty one + # Update the comment, but don't overwrite a real comment with an empty one def comment=(comment) @comment = comment unless comment.empty? end @@ -94,7 +93,7 @@ module RDoc # those directives. Wehn a comment is assigned, we then extract # out any matching directives and update our object - def CodeObject.attr_overridable(name, *aliases) + def self.attr_overridable(name, *aliases) @overridables ||= {} attr_accessor name @@ -623,7 +622,7 @@ module RDoc end end - + ## # AnyMethod is the base class for objects representing methods class AnyMethod < CodeObject @@ -632,14 +631,18 @@ module RDoc attr_accessor :block_params attr_accessor :dont_rename_initialize attr_accessor :singleton - attr_reader :aliases # list of other names for this method - attr_accessor :is_alias_for # or a method we're aliasing + attr_reader :text + + # list of other names for this method + attr_reader :aliases + + # method we're aliasing + attr_accessor :is_alias_for attr_overridable :params, :param, :parameters, :parameter attr_accessor :call_seq - include TokenStream def initialize(text, name) @@ -693,7 +696,6 @@ $stderr.puts p end end - # Represent an alias, which is an old_name/ new_name pair associated # with a particular context class Alias < CodeObject 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 diff --git a/lib/rdoc/generator/html.rb b/lib/rdoc/generator/html.rb index e6a6cd9ac9..95617725e9 100644 --- a/lib/rdoc/generator/html.rb +++ b/lib/rdoc/generator/html.rb @@ -3,1528 +3,367 @@ require 'fileutils' require 'rdoc/generator' require 'rdoc/markup/to_html' -module RDoc::Generator +## +# 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. + +class RDoc::Generator::HTML + + include RDoc::Generator::MarkUp ## - # 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. + # Generator may need to return specific subclasses depending on the + # options they are passed. Because of this we create them using a factory - class AllReferences - @@refs = {} + def self.for(options) + RDoc::Generator::AllReferences.reset + RDoc::Generator::Method.reset - 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 + if options.all_one_file + RDoc::Generator::HTMLInOne.new options + else + new options end end - ## - # Subclass of the RDoc::Markup::ToHtml class that supports looking up words - # in the AllReferences list. Those that are found (like AllReferences in - # this comment) will be hyperlinked - - class HyperlinkHtml < RDoc::Markup::ToHtml - - ## - # We need to record the html path of our caller so we can generate - # correct relative paths for any hyperlinks that we find - def initialize(from_path, context, options) - super() - - @from_path = from_path - - @parent_name = context.parent_name - @parent_name += "::" if @parent_name - @context = context - - @options = options - end - - ## - # We're invoked when any text matches the CROSSREF pattern - # (defined in MarkUp). If we fine the corresponding reference, - # generate a hyperlink. If the name we're looking for contains - # no punctuation, we look for it up the module/class chain. For - # example, HyperlinkHtml is found, even without the Generator:: - # prefix, because we look for it in module Generator first. - - def handle_special_CROSSREF(special) - name = special.text - if name[0,1] == '#' - lookup = name[1..-1] - name = lookup unless @options.show_hash - else - lookup = name - end - - # Find class, module, or method in class or module. - if /([A-Z]\w*)[.\#](\w+[!?=]?)/ =~ lookup - container = $1 - method = $2 - ref = @context.find_symbol(container, method) - elsif /([A-Za-z]\w*)[.\#](\w+(\([\.\w+\*\/\+\-\=\<\>]+\))?)/ =~ lookup - container = $1 - method = $2 - ref = @context.find_symbol(container, method) - else - ref = @context.find_symbol(lookup) - end - - if ref and ref.document_self - "#{name}" - else - name - end - end - - ## - # Generate a hyperlink for url, labeled with text. Handle the - # special cases for img: and link: described under handle_special_HYPEDLINK - - def gen_url(url, text) - if url =~ /([A-Za-z]+):(.*)/ - type = $1 - path = $2 - else - type = "http" - path = url - url = "http://#{url}" - end - - if type == "link" - if path[0,1] == '#' # is this meaningful? - url = path - else - url = HTML.gen_url(@from_path, path) - end - end - - if (type == "http" || type == "link") && - url =~ /\.(gif|png|jpg|jpeg|bmp)$/ - - "" - else - "#{text.sub(%r{^#{type}:/*}, '')}" - end - end - - ## - # And we're invoked with a potential external hyperlink mailto: - # just gets inserted. http: links are checked to see if they - # reference an image. If so, that image gets inserted using an - # tag. Otherwise a conventional is used. We also - # support a special type of hyperlink, link:, which is a reference - # to a local file whose path is relative to the --op directory. - - def handle_special_HYPERLINK(special) - url = special.text - gen_url(url, url) - end - - ## - # Here's a hypedlink where the label is different to the URL - #

/, '') - 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 - return css_name - else - return HTML.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 + # 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 + load_html_template 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 + # Build the initial indices and output objects + # based on an array of TopLevel objects containing + # the extracted information. - class ContextUser + def generate(toplevels) + @toplevels = toplevels + @files = [] + @classes = [] - include MarkUp + write_style_sheet + gen_sub_directories() + build_indices + generate_html + end - attr_reader :context + private - def initialize(context, options) - @context = context - @options = options + ## + # Load up the HTML template specified in the options. + # If the template name contains a slash, use it literally - # HACK ugly - @template = options.template_class + def load_html_template + template = @options.template + + unless template =~ %r{/|\\} then + template = File.join('rdoc', 'generator', @options.generator.key, + template) end - ## - # convenience method to build a hyperlink + require template - 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 - HTML.gen_url(from_path, path) - end - end - - ## - # Create a list of HtmlMethod 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 - list = list.find_all {|m| m.visibility == :public || m.visibility == :protected || m.force_documentation } - end - @methods = list.collect {|m| HtmlMethod.new(m, self, @options) } - 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 Htmlxxx 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) - HTML.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 + @template = self.class.const_get @options.template.upcase + @options.template_class = @template + rescue LoadError + $stderr.puts "Could not find HTML template '#{template}'" + exit 99 end ## - # Wrap a ClassModule context + # Write out the style sheet used by the main frames - class HtmlClass < ContextUser + def write_style_sheet + return unless @template.constants.include? :STYLE or + @template.constants.include? 'STYLE' - attr_reader :path + template = RDoc::TemplatePage.new @template::STYLE - def initialize(context, html_file, prefix, options) - super(context, options) + unless @options.css then + open RDoc::Generator::CSS_NAME, 'w' do |f| + values = {} - @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 - if path['<<'] - path.gsub!(/<<\s*(\w*)/) { "from-#$1" } - end - 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 + if @template.constants.include? :FONTS or + @template.constants.include? 'FONTS' then + values["fonts"] = @template::FONTS 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 HtmlFile < ContextUser - - 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 - @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 - @values["cvsurl"] = cvs_url( @options.webcvs, @values["full_path"] ) - end - end - - def <=>(other) - self.name <=> other.name - end - - end - - class HtmlMethod - - include MarkUp - - attr_reader :context - attr_reader :src_url - attr_reader :img_url - attr_reader :source_code - - @@seq = "M000000" - - @@all_methods = [] - - 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 = HTML.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 - HTML.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) - File.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 - HTML.gen_url(path, file_path) end + end - def self.all_methods - @@all_methods - end + ## + # See the comments at the top for a description of the directory structure - def <=>(other) - @context <=> other.context - end + def gen_sub_directories + FileUtils.mkdir_p RDoc::Generator::FILE_DIR + FileUtils.mkdir_p RDoc::Generator::CLASS_DIR + rescue + $stderr.puts $!.message + exit 1 + end - ## - # Given a sequence of source tokens, mark up the source code - # to make it look purty. + def build_indices + @files, @classes = RDoc::Generator::Context.build_indicies(@toplevels, + @options) + end - 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 + ## + # Generate all the HTML - text = CGI.escapeHTML(t.text) + def generate_html + # the individual descriptions for files and classes + gen_into(@files) + gen_into(@classes) + # and the index files + gen_file_index + gen_class_index + gen_method_index + gen_main_index - if style - src << "#{text}" - else - src << text - end + # this method is defined in the template file + write_extra_pages if defined? write_extra_pages + end + + def gen_into(list) + list.each do |item| + if item.document_self + op_file = item.path + FileUtils.mkdir_p(File.dirname(op_file)) + open(op_file, "w") { |file| item.write_on(file) } 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 + def gen_file_index + gen_an_index @files, 'Files', @template::FILE_INDEX, "fr_file_index.html" + end + + def gen_class_index + gen_an_index(@classes, 'Classes', @template::CLASS_INDEX, + "fr_class_index.html") + end + + def gen_method_index + gen_an_index(RDoc::Generator::Method.all_methods, 'Methods', + @template::METHOD_INDEX, "fr_method_index.html") + end + + def gen_an_index(collection, title, template, filename) + template = RDoc::TemplatePage.new @template::FR_INDEX_BODY, template + res = [] + collection.sort.each do |f| + if f.document_self + res << { "href" => f.path, "name" => f.index_name } + end + end + + values = { + "entries" => res, + 'list_title' => CGI.escapeHTML(title), + 'index_url' => main_url, + 'charset' => @options.charset, + 'style_url' => style_url('', @options.css), + } + + open filename, 'w' do |f| + template.write_html_on(f, values) + end + end + ## - # 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. + # The main index page is mostly a template frameset, but includes the + # initial page. If the --main option was given, we use this as + # our main page, otherwise we use the first file specified on the command + # line. - class HTML + def gen_main_index + template = RDoc::TemplatePage.new @template::INDEX - include MarkUp + open 'index.html', 'w' do |f| + classes = @classes.sort.map { |klass| klass.value_hash } - ## - # Converts a target url to one that is relative to a given path + values = { + 'main_page' => @main_page, + 'initial_page' => main_url, + 'style_url' => style_url('', @options.css), + 'title' => CGI.escapeHTML(@options.title), + 'charset' => @options.charset, + 'classes' => classes, + } - def self.gen_url(path, target) - from = File.dirname(path) - to, to_file = File.split(target) + values['inline_source'] = @options.inline_source - from = from.split("/") - to = to.split("/") - - while from.size > 0 and to.size > 0 and from[0] == to[0] - from.shift - to.shift - end - - from.fill("..") - from.concat(to) - from << to_file - File.join(*from) + template.write_html_on f, values end + end - ## - # Generator may need to return specific subclasses depending on the - # options they are passed. Because of this we create them using a factory + ## + # Returns the url of the main page - def self.for(options) - AllReferences.reset - HtmlMethod.reset - - if options.all_one_file - HTMLInOne.new(options) + def main_url + @main_page = @options.main_page + @main_page_ref = nil + if @main_page + @main_page_ref = AllReferences[@main_page] + if @main_page_ref then + @main_page_path = @main_page_ref.path else - HTML.new(options) + $stderr.puts "Could not find main page #{@main_page}" end end - class < f.path, "name" => f.index_name } - end - end - - values = { - "entries" => res, - 'list_title' => CGI.escapeHTML(title), - 'index_url' => main_url, - 'charset' => @options.charset, - 'style_url' => style_url('', @options.css), - } - - File.open(filename, "w") do |f| - template.write_html_on(f, values) - end - end - - ## - # The main index page is mostly a template frameset, but includes the - # initial page. If the --main option was given, we use this as - # our main page, otherwise we use the first file specified on the command - # line. - - def gen_main_index - template = RDoc::TemplatePage.new @template::INDEX - - open 'index.html', 'w' do |f| - classes = @classes.sort.map { |klass| klass.value_hash } - - values = { - 'main_page' => @main_page, - 'initial_page' => main_url, - 'style_url' => style_url('', @options.css), - 'title' => CGI.escapeHTML(@options.title), - 'charset' => @options.charset, - 'classes' => classes, - } - - values['inline_source'] = @options.inline_source - - template.write_html_on f, values - end - end - - ## - # Returns the url of the main page - - def main_url - @main_page = @options.main_page - @main_page_ref = nil - if @main_page - @main_page_ref = AllReferences[@main_page] - if @main_page_ref then - @main_page_path = @main_page_ref.path - else - $stderr.puts "Could not find main page #{@main_page}" - end - end - - unless @main_page_path then - file = @files.find { |file| file.document_self } - @main_page_path = file.path if file - end - - unless @main_page_path then - $stderr.puts "Couldn't find anything to document" - $stderr.puts "Perhaps you've used :stopdoc: in all classes" - exit 1 - end - - @main_page_path - end - - end - - class HTMLInOne < HTML - - def initialize(*args) - super - end - - ## - # Build the initial indices and output objects - # based on an array of TopLevel objects containing - # the extracted information. - - def generate(info) - @toplevels = info - @files = [] - @classes = [] - @hyperlinks = {} - - build_indices - generate_xml - end - - ## - # Generate: - # - # * a list of HtmlFile objects for each TopLevel object. - # * a list of HtmlClass 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 build_indices - - @toplevels.each do |toplevel| - @files << HtmlFile.new(toplevel, @options, FILE_DIR) - end - - RDoc::TopLevel.all_classes_and_modules.each do |cls| - build_class_list(cls, @files[0], CLASS_DIR) - end - end - - def build_class_list(from, html_file, class_dir) - @classes << HtmlClass.new(from, html_file, class_dir, @options) - from.each_classmodule do |mod| - build_class_list(mod, html_file, class_dir) - end - end - - ## - # Generate all the HTML. For the one-file case, we generate - # all the information in to one big hash - - def generate_xml - values = { - 'charset' => @options.charset, - 'files' => gen_into(@files), - 'classes' => gen_into(@classes), - 'title' => CGI.escapeHTML(@options.title), - } - - # this method is defined in the template file - write_extra_pages if defined? write_extra_pages - - template = RDoc::TemplatePage.new @template::ONE_PAGE - - if @options.op_name - opfile = File.open(@options.op_name, "w") - else - opfile = $stdout - end - template.write_html_on(opfile, values) - end - - def gen_into(list) - res = [] - list.each do |item| - res << item.value_hash - end - res - end - - def gen_file_index - gen_an_index(@files, 'Files') - end - - def gen_class_index - gen_an_index(@classes, 'Classes') - end - - def gen_method_index - gen_an_index(HtmlMethod.all_methods, 'Methods') - end - - def gen_an_index(collection, title) - res = [] - collection.sort.each do |f| - if f.document_self - res << { "href" => f.path, "name" => f.index_name } - end - end - - return { - "entries" => res, - 'list_title' => title, - 'index_url' => main_url, - } - end - + @main_page_path end end +class RDoc::Generator::HTMLInOne < RDoc::Generator::HTML + + def initialize(*args) + super + end + + ## + # Build the initial indices and output objects + # based on an array of TopLevel objects containing + # the extracted information. + + def generate(info) + @toplevels = info + @hyperlinks = {} + + build_indices + generate_xml + end + + ## + # 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 build_indices + @files, @classes = RDoc::Generator::Context.build_indices(@toplevels, + @options) + end + + ## + # Generate all the HTML. For the one-file case, we generate + # all the information in to one big hash + + def generate_xml + values = { + 'charset' => @options.charset, + 'files' => gen_into(@files), + 'classes' => gen_into(@classes), + 'title' => CGI.escapeHTML(@options.title), + } + + # this method is defined in the template file + write_extra_pages if defined? write_extra_pages + + template = RDoc::TemplatePage.new @template::ONE_PAGE + + if @options.op_name + opfile = open @options.op_name, 'w' + else + opfile = $stdout + end + template.write_html_on(opfile, values) + end + + def gen_into(list) + res = [] + list.each do |item| + res << item.value_hash + end + res + end + + def gen_file_index + gen_an_index(@files, 'Files') + end + + def gen_class_index + gen_an_index(@classes, 'Classes') + end + + def gen_method_index + gen_an_index(RDoc::Generator::Method.all_methods, 'Methods') + end + + def gen_an_index(collection, title) + res = [] + collection.sort.each do |f| + if f.document_self + res << { "href" => f.path, "name" => f.index_name } + end + end + + return { + "entries" => res, + 'list_title' => title, + 'index_url' => main_url, + } + end + +end + + diff --git a/lib/rdoc/generator/html/html.rb b/lib/rdoc/generator/html/html.rb index 0a0b754919..8343454d30 100644 --- a/lib/rdoc/generator/html/html.rb +++ b/lib/rdoc/generator/html/html.rb @@ -478,14 +478,14 @@ EOF

-<% values["constants"].each do |constants| $stderr.puts({ :constants => constants }.inspect) %> +<% values["constants"].each do |constants| %> - + - + <% if values["desc"] then %> - + <% end %> <% end # values["constants"] %> diff --git a/lib/rdoc/markup.rb b/lib/rdoc/markup.rb index 8f978dc7bc..9334329d6b 100644 --- a/lib/rdoc/markup.rb +++ b/lib/rdoc/markup.rb @@ -146,16 +146,16 @@ require 'rdoc' # end # end # -# p = RDoc::Markup.new -# p.add_word_pair("{", "}", :STRIKE) -# p.add_html("no", :STRIKE) +# m = RDoc::Markup.new +# m.add_word_pair("{", "}", :STRIKE) +# m.add_html("no", :STRIKE) # -# p.add_special(/\b([A-Z][a-z]+[A-Z]\w+)/, :WIKIWORD) +# m.add_special(/\b([A-Z][a-z]+[A-Z]\w+)/, :WIKIWORD) # # h = WikiHtml.new # h.add_tag(:STRIKE, "", "") # -# puts "" + p.convert(ARGF.read, h) + "" +# puts "" + m.convert(ARGF.read, h) + "" # #-- # Author:: Dave Thomas, dave@pragmaticprogrammer.com @@ -194,7 +194,7 @@ class RDoc::Markup # identify significant chunks. def initialize - @am = AttributeManager.new + @am = RDoc::Markup::AttributeManager.new @output = nil end @@ -234,15 +234,16 @@ class RDoc::Markup # display the result. def convert(str, op) - @lines = Lines.new(str.split(/\r?\n/).collect { |aLine| - Line.new(aLine) }) + lines = str.split(/\r?\n/).map { |line| Line.new line } + @lines = Lines.new lines + return "" if @lines.empty? @lines.normalize assign_types_to_lines group = group_lines # call the output formatter to handle the result - # group.to_a.each {|i| p i} - group.accept(@am, op) + #group.each { |line| p line } + group.accept @am, op end private @@ -252,9 +253,8 @@ class RDoc::Markup # Blank, a paragraph, a list element, or verbatim text. def assign_types_to_lines(margin = 0, level = 0) - while line = @lines.next - if line.isBlank? + if line.blank? then line.stamp :BLANK, level next end @@ -289,7 +289,6 @@ class RDoc::Markup # text following them (* xxx, - xxx, and dd. xxx) if SIMPLE_LIST_RE =~ active_line - offset = margin + $1.length prefix = $2 prefix_length = prefix.length @@ -308,7 +307,6 @@ class RDoc::Markup next end - if LABEL_LIST_RE =~ active_line offset = margin + $1.length prefix = $2 @@ -366,22 +364,23 @@ class RDoc::Markup prefix_length = prefix.length text = line.text flag = nil + case prefix - when /^\[/ + when /^\[/ then flag = :LABELED prefix = prefix[1, prefix.length-2] - when /:$/ + when /:$/ then flag = :NOTE prefix.chop! - else raise "Invalid List Type: #{self.inspect}" + else + raise "Invalid List Type: #{self.inspect}" end # body is on the next line - - if text.length <= offset + if text.length <= offset then original_line = line line = @lines.next - return(false) unless line + return false unless line text = line.text for i in 0..margin @@ -390,15 +389,24 @@ class RDoc::Markup return false end end + i = margin i += 1 while text[i] == SPACE - if i >= text.length + + if i >= text.length then @lines.unget return false else offset = i prefix_length = 0 - @lines.delete(original_line) + + if text[offset..-1] =~ SIMPLE_LIST_RE then + @lines.unget + line = original_line + line.text = '' + else + @lines.delete original_line + end end end @@ -418,24 +426,26 @@ class RDoc::Markup def group_lines @lines.rewind - inList = false - wantedType = wantedLevel = nil + in_list = false + wanted_type = wanted_level = nil block = LineCollection.new group = nil while line = @lines.next - if line.level == wantedLevel and line.type == wantedType + if line.level == wanted_level and line.type == wanted_type group.add_text(line.text) else group = block.fragment_for(line) block.add(group) + if line.type == :LIST - wantedType = :PARAGRAPH + wanted_type = :PARAGRAPH else - wantedType = line.type + wanted_type = line.type end - wantedLevel = line.type == :HEADING ? line.param : line.level + + wanted_level = line.type == :HEADING ? line.param : line.level end end @@ -462,4 +472,5 @@ class RDoc::Markup end require 'rdoc/markup/fragments' +require 'rdoc/markup/inline' require 'rdoc/markup/lines' diff --git a/lib/rdoc/markup/formatter.rb b/lib/rdoc/markup/formatter.rb new file mode 100644 index 0000000000..14cbae59f9 --- /dev/null +++ b/lib/rdoc/markup/formatter.rb @@ -0,0 +1,14 @@ +require 'rdoc/markup' + +class RDoc::Markup::Formatter + + def initialize + @markup = RDoc::Markup.new + end + + def convert(content) + @markup.convert content, self + end + +end + diff --git a/lib/rdoc/markup/fragments.rb b/lib/rdoc/markup/fragments.rb index 39b63cae22..1765861ad0 100644 --- a/lib/rdoc/markup/fragments.rb +++ b/lib/rdoc/markup/fragments.rb @@ -83,10 +83,16 @@ class RDoc::Markup class ListItem < ListBase type_name :LIST - # def label - # am = AttributeManager.new(@param) - # am.flow - # end + def to_s + text = if [:NOTE, :LABELED].include? type then + "#{@param}: #{@txt}" + else + @txt + end + + "L#@level: #{type} #{self.class.name.split('::')[-1]}\n#{text}" + end + end class ListStart < ListBase @@ -311,9 +317,8 @@ class RDoc::Markup def tidy_blank_lines (@fragments.size - 1).times do |i| - if @fragments[i].kind_of?(BlankLine) and - @fragments[i+1].kind_of?(ListEnd) - @fragments[i], @fragments[i+1] = @fragments[i+1], @fragments[i] + if BlankLine === @fragments[i] and ListEnd === @fragments[i+1] then + @fragments[i], @fragments[i+1] = @fragments[i+1], @fragments[i] end end diff --git a/lib/rdoc/markup/inline.rb b/lib/rdoc/markup/inline.rb index cbf5032a68..8945e14b83 100644 --- a/lib/rdoc/markup/inline.rb +++ b/lib/rdoc/markup/inline.rb @@ -12,9 +12,9 @@ class RDoc::Markup @@name_to_bitmap = { :_SPECIAL_ => SPECIAL } @@next_bitmap = 2 - def Attribute.bitmap_for(name) + def self.bitmap_for(name) bitmap = @@name_to_bitmap[name] - if !bitmap + unless bitmap then bitmap = @@next_bitmap @@next_bitmap <<= 1 @@name_to_bitmap[name] = bitmap @@ -22,7 +22,7 @@ class RDoc::Markup bitmap end - def Attribute.as_string(bitmap) + def self.as_string(bitmap) return "none" if bitmap.zero? res = [] @@name_to_bitmap.each do |name, bit| @@ -31,7 +31,7 @@ class RDoc::Markup res.join(",") end - def Attribute.each_name_of(bitmap) + def self.each_name_of(bitmap) @@name_to_bitmap.each do |name, bit| next if bit == SPECIAL yield name.to_s if (bitmap & bit) != 0 @@ -85,14 +85,15 @@ class RDoc::Markup self.text == o.text && self.type == o.type end - def to_s - "Special: type=#{type}, name=#{RDoc::Markup::Attribute.as_string type}, text=#{text.dump}" - end - def inspect "#" % [ object_id, @type, RDoc::Markup::Attribute.as_string(type), text.dump] end + + def to_s + "Special: type=#{type}, name=#{RDoc::Markup::Attribute.as_string type}, text=#{text.dump}" + end + end class AttributeManager @@ -165,8 +166,10 @@ class RDoc::Markup def convert_attrs(str, attrs) # first do matching ones tags = MATCHING_WORD_PAIRS.keys.join("") + re = "(^|\\W)([#{tags}])([A-Za-z_]+?)\\2(\\W|\$)" # re = "(^|\\W)([#{tags}])(\\S+?)\\2(\\W|\$)" + 1 while str.gsub!(Regexp.new(re)) { attr = MATCHING_WORD_PAIRS[$2]; attrs.set_attrs($`.length + $1.length + $2.length, $3.length, attr) @@ -185,9 +188,9 @@ class RDoc::Markup end def convert_html(str, attrs) - tags = HTML_TAGS.keys.join("|") - re = "<(#{tags})>(.*?)" - 1 while str.gsub!(Regexp.new(re, Regexp::IGNORECASE)) { + tags = HTML_TAGS.keys.join '|' + + 1 while str.gsub!(/<(#{tags})>(.*?)<\/\1>/i) { attr = HTML_TAGS[$1.downcase] html_length = $1.length + 2 seq = NULL * html_length @@ -210,7 +213,7 @@ class RDoc::Markup # A \ in front of a character that would normally be processed turns off # processing. We do this by turning \< into <#{PROTECT} - PROTECTABLE = [ "<" << "\\" ] #" + PROTECTABLE = [ "<" << "\\" ] def mask_protected_sequences @@ -300,7 +303,6 @@ class RDoc::Markup end def split_into_flow - display_attributes if $DEBUG_RDOC res = [] diff --git a/lib/rdoc/markup/lines.rb b/lib/rdoc/markup/lines.rb index 985304c225..069492122f 100644 --- a/lib/rdoc/markup/lines.rb +++ b/lib/rdoc/markup/lines.rb @@ -59,8 +59,8 @@ class RDoc::Markup end # Return true if this line is blank - def isBlank? - @text.length.zero? + def blank? + @text.empty? end # stamp a line with a type, a level, a prefix, and a flag diff --git a/lib/rdoc/markup/to_flow.rb b/lib/rdoc/markup/to_flow.rb index cb7da5fa88..3d87b3e9c3 100644 --- a/lib/rdoc/markup/to_flow.rb +++ b/lib/rdoc/markup/to_flow.rb @@ -1,3 +1,4 @@ +require 'rdoc/markup/formatter' require 'rdoc/markup/fragments' require 'rdoc/markup/inline' require 'cgi' @@ -22,7 +23,7 @@ class RDoc::Markup H = Struct.new(:level, :text) end - class ToFlow + class ToFlow < RDoc::Markup::Formatter LIST_TYPE_TO_HTML = { :BULLET => [ "
    ", "
" ], :NUMBER => [ "
    ", "
" ], @@ -35,6 +36,8 @@ class RDoc::Markup InlineTag = Struct.new(:bit, :on, :off) def initialize + super + init_tags end diff --git a/lib/rdoc/markup/to_html.rb b/lib/rdoc/markup/to_html.rb index 0238d5ae67..da42312ff5 100644 --- a/lib/rdoc/markup/to_html.rb +++ b/lib/rdoc/markup/to_html.rb @@ -1,22 +1,25 @@ +require 'rdoc/markup/formatter' require 'rdoc/markup/fragments' require 'rdoc/markup/inline' require 'cgi' -class RDoc::Markup::ToHtml +class RDoc::Markup::ToHtml < RDoc::Markup::Formatter LIST_TYPE_TO_HTML = { - :BULLET => [ "
    ", "
" ], - :NUMBER => [ "
    ", "
" ], - :UPPERALPHA => [ "
    ", "
" ], - :LOWERALPHA => [ "
    ", "
" ], - :LABELED => [ "
", "
" ], - :NOTE => [ "
<%= values["name"] %><%= constants["name"] %> =<%= values["value"] %><%= constants["value"] %> <%= values["desc"] %><%= constants["desc"] %>
", "
" ], + :BULLET => %w[
], + :NUMBER => %w[
], + :UPPERALPHA => %w[
], + :LOWERALPHA => %w[
], + :LABELED => %w[
], + :NOTE => %w[
], } InlineTag = Struct.new(:bit, :on, :off) def initialize + super + init_tags end @@ -94,8 +97,11 @@ class RDoc::Markup::ToHtml if tag = @in_list_entry.last @res << annotate(tag) << "\n" end + @res << list_item_start(am, fragment) + @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n" + @in_list_entry[-1] = list_end_for(fragment.type) end diff --git a/lib/rdoc/markup/to_html_hyperlink.rb b/lib/rdoc/markup/to_html_hyperlink.rb new file mode 100644 index 0000000000..58adaa8cb0 --- /dev/null +++ b/lib/rdoc/markup/to_html_hyperlink.rb @@ -0,0 +1,149 @@ +require 'rdoc/markup/to_html' + +## +# Subclass of the RDoc::Markup::ToHtml class that supports looking up words in +# the AllReferences list. Those that are found (like AllReferences in this +# comment) will be hyperlinked + +class RDoc::Markup::ToHtmlHyperlink < RDoc::Markup::ToHtml + + attr_accessor :context + + ## + # We need to record the html path of our caller so we can generate + # correct relative paths for any hyperlinks that we find + + def initialize(from_path, context, show_hash) + super() + + # class names, variable names, or instance variables + @markup.add_special(/( + # A::B.meth(**) (for operator in Fortran95) + \w+(::\w+)*[.\#]\w+(\([\.\w+\*\/\+\-\=\<\>]+\))? + # meth(**) (for operator in Fortran95) + | \#\w+(\([.\w\*\/\+\-\=\<\>]+\))? + | \b([A-Z]\w*(::\w+)*[.\#]\w+) # A::B.meth + | \b([A-Z]\w+(::\w+)*) # A::B + | \#\w+[!?=]? # #meth_name + | \\?\b\w+([_\/\.]+\w+)*[!?=]? # meth_name + )/x, + :CROSSREF) + + # external hyperlinks + @markup.add_special(/((link:|https?:|mailto:|ftp:|www\.)\S+\w)/, :HYPERLINK) + + # and links of the form [] + @markup.add_special(/(((\{.*?\})|\b\S+?)\[\S+?\.\S+?\])/, :TIDYLINK) + + @from_path = from_path + @context = context + @show_hash = show_hash + + @seen = {} + end + + ## + # We're invoked when any text matches the CROSSREF pattern + # (defined in MarkUp). If we fine the corresponding reference, + # generate a hyperlink. If the name we're looking for contains + # no punctuation, we look for it up the module/class chain. For + # example, HyperlinkHtml is found, even without the Generator:: + # prefix, because we look for it in module Generator first. + + def handle_special_CROSSREF(special) + name = special.text + + return @seen[name] if @seen.include? name + + if name[0,1] == '#' then + lookup = name[1..-1] + name = lookup unless @show_hash + else + lookup = name + end + + # Find class, module, or method in class or module. + if /([A-Z]\w*)[.\#](\w+[!?=]?)/ =~ lookup then + container = $1 + method = $2 + ref = @context.find_symbol container, method + elsif /([A-Za-z]\w*)[.\#](\w+(\([\.\w+\*\/\+\-\=\<\>]+\))?)/ =~ lookup then + container = $1 + method = $2 + ref = @context.find_symbol container, method + else + ref = @context.find_symbol lookup + end + + out = if lookup =~ /^\\/ then + $' + elsif ref and ref.document_self then + "#{name}" + else + name + end + + @seen[name] = out + + out + end + + ## + # Generate a hyperlink for url, labeled with text. Handle the + # special cases for img: and link: described under handle_special_HYPEDLINK + + def gen_url(url, text) + if url =~ /([A-Za-z]+):(.*)/ then + type = $1 + path = $2 + else + type = "http" + path = url + url = "http://#{url}" + end + + if type == "link" then + url = if path[0, 1] == '#' then # is this meaningful? + path + else + HTML.gen_url @from_path, path + end + end + + if (type == "http" or type == "link") and + url =~ /\.(gif|png|jpg|jpeg|bmp)$/ then + "" + else + "#{text.sub(%r{^#{type}:/*}, '')}" + end + end + + ## + # And we're invoked with a potential external hyperlink mailto: + # just gets inserted. http: links are checked to see if they + # reference an image. If so, that image gets inserted using an + # tag. Otherwise a conventional is used. We also + # support a special type of hyperlink, link:, which is a reference + # to a local file whose path is relative to the --op directory. + + def handle_special_HYPERLINK(special) + url = special.text + gen_url url, url + end + + ## + # Here's a hypedlink where the label is different to the URL + #