require 'fileutils' require 'rdoc/generator' require 'rdoc/markup/to_html' ## # 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 ## # Generator may need to return specific subclasses depending on the # options they are passed. Because of this we create them using a factory def self.for(options) RDoc::Generator::AllReferences.reset RDoc::Generator::Method.reset if options.all_one_file RDoc::Generator::HTMLInOne.new options else new options end end class << self protected :new end ## # Set up a new HTML generator. Basically all we do here is load up the # correct output temlate def initialize(options) #:not-new: @options = options load_html_template end ## # Build the initial indices and output objects # based on an array of TopLevel objects containing # the extracted information. def generate(toplevels) @toplevels = toplevels @files = [] @classes = [] write_style_sheet gen_sub_directories build_indices generate_html end private ## # Load up the HTML template specified in the options. # If the template name contains a slash, use it literally def load_html_template # # If the template is not a path, first look for it # in rdoc's HTML template directory. Perhaps this behavior should # be reversed (first try to include the template and, only if that # fails, try to include it in the default template directory). # One danger with reversing the behavior, however, is that # if something like require 'html' could load up an # unrelated file in the standard library or in a gem. # template = @options.template unless template =~ %r{/|\\} then template = File.join('rdoc', 'generator', @options.generator.key, template) end begin require template @template = self.class.const_get @options.template.upcase @options.template_class = @template rescue LoadError => e # # The template did not exist in the default template directory, so # see if require can find the template elsewhere (in a gem, for # instance). # if(e.message[template] && template != @options.template) template = @options.template retry end $stderr.puts "Could not find HTML template '#{template}': #{e.message}" exit 99 end end ## # Write out the style sheet used by the main frames def write_style_sheet return unless @template.constants.include? :STYLE or @template.constants.include? 'STYLE' template = RDoc::TemplatePage.new @template::STYLE unless @options.css then open RDoc::Generator::CSS_NAME, 'w' do |f| values = {} if @template.constants.include? :FONTS or @template.constants.include? 'FONTS' then values["fonts"] = @template::FONTS end template.write_html_on(f, values) end end end ## # See the comments at the top for a description of the directory structure 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 def build_indices @files, @classes = RDoc::Generator::Context.build_indices(@toplevels, @options) end ## # Generate all the HTML def generate_html @main_url = main_url # 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 # this method is defined in the template file values = { 'title_suffix' => CGI.escapeHTML("[#{@options.title}]"), 'charset' => @options.charset, 'style_url' => style_url('', @options.css), } @template.write_extra_pages(values) if @template.respond_to?(:write_extra_pages) end def gen_into(list) # # The file, class, and method lists technically should be regenerated # for every output file, in order that the relative links be correct # (we are worried here about frameless templates, which need this # information for every generated page). Doing this is a bit slow, # however. For a medium-sized gem, this increased rdoc's runtime by # about 5% (using the 'time' command-line utility). While this is not # necessarily a problem, I do not want to pessimize rdoc for large # projects, however, and so we only regenerate the lists when the # directory of the output file changes, which seems like a reasonable # optimization. # file_list = {} class_list = {} method_list = {} prev_op_dir = nil list.each do |item| next unless item.document_self op_file = item.path op_dir = File.dirname(op_file) if(op_dir != prev_op_dir) file_list = index_to_links op_file, @files class_list = index_to_links op_file, @classes method_list = index_to_links op_file, RDoc::Generator::Method.all_methods end prev_op_dir = op_dir FileUtils.mkdir_p op_dir open op_file, 'w' do |io| item.write_on io, file_list, class_list, method_list end file_list.clear class_list.clear method_list.clear 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, 'title' => CGI.escapeHTML("#{title} [#{@options.title}]"), '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 ## # 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 if @template.const_defined? :FRAMELESS then # # If we're using a template without frames, then just redirect # to it from index.html. # # One alternative to this, expanding the main page's template into # index.html, is tricky because the relative URLs will be different # (since index.html is located in at the site's root, # rather than within a files or a classes subdirectory). # open 'index.html', 'w' do |f| f.puts(%{}) f.puts(%{}) f.puts(%{}) f.puts(%{#{CGI.escapeHTML(@options.title)}}) f.puts(%{}) f.puts(%{}) f.puts(%{}) f.puts(%{}) end else main = RDoc::TemplatePage.new @template::INDEX open 'index.html', 'w' do |f| style_url = style_url '', @options.css classes = @classes.sort.map { |klass| klass.value_hash } values = { '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 main.write_html_on f, values end end end def index_to_links(output_path, collection) collection.sort.map do |f| next unless f.document_self { "href" => RDoc::Markup::ToHtml.gen_relative_url(output_path, f.path), "name" => f.index_name } end.compact end ## # Returns the url of the main page def main_url main_page = @options.main_page # # If a main page has been specified (--main), then search for it # in the AllReferences array. This allows either files or classes # to be used for the main page. # if main_page then main_page_ref = RDoc::Generator::AllReferences[main_page] if main_page_ref then return main_page_ref.path else $stderr.puts "Could not find main page #{main_page}" end end # # No main page has been specified, so just use the README. # @files.each do |file| if file.name =~ /^README/ then return file.path end end # # There's no README (shame! shame!). Just use the first file # that will be documented. # @files.each do |file| if file.document_self then return file.path end end # # There are no files to be documented... Something seems very wrong. # raise RDoc::Error, "Couldn't find anything to document (perhaps :stopdoc: has been used in all classes)!" end private :main_url 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), } 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 end