# # 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. # # Method descriptions appear in whatever entity (file, class, # or module) that contains them. # # We generate files in a structure below a specified subdirectory, # normally +doc+. # # opdir # | # |___ files # | |__ per file summaries # | # |___ classes # |__ per class/module descriptions # # HTML is generated using the Template class. # require 'ftools' require 'rdoc/options' require 'rdoc/template' require 'rdoc/markup/simple_markup' require 'rdoc/markup/simple_markup/to_html' require 'cgi' module Generators # Name of sub-direcories that hold file and class/module descriptions FILE_DIR = "files" CLASS_DIR = "classes" CSS_NAME = "stylesheet.css" ## # 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 ## # Subclass of the SM::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 < SM::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) super() @from_path = from_path @parent_name = context.parent_name @parent_name += "::" if @parent_name @context = context 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 Generators:: # prefix, because we look for it in module Generators first. def handle_special_CROSSREF(special) name = special.text if name[0,1] == '#' lookup = name[1..-1] name = lookup unless Options.instance.show_hash else lookup = name end if /([A-Z].*)[.\#](.*)/ =~ 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 #it does not need to be a link 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" url = path end if (type == "http" || type == "link") && url =~ /\.(gif|png|jpg|jpeg|bmp)$/ "" elsif (type == "http" || type == "link") "#{text}" 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 def style_url(path, css_name=nil) css_name ||= CSS_NAME 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 ContextUser include MarkUp attr_reader :context def initialize(context, options) @context = context @options = options end # convenience method to build a hyperlink # Where's the DRY in this?? Put this in the template where it belongs def href(link, cls, name) %{"#{name}"} 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" => meth.aref, "href" => meth.path } 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 = ref.path res << { "name" => h_name, "href" => path } else res << { "name" => h_name, "href" => "" } 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["href"] = m.path row["m_seq"] = m.seq 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, 'href' => other.viewer.path, 'aref' => other.viewer.aref } 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(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(cls.viewer.path, "link", cls.full_name) << "
\n" << build_class_list(level + 1, cls, section, infile) end end res end def document_self @context.document_self end def diagram_reference(diagram) res = diagram.gsub(/((?:src|href)=")(.*?)"/) { $1 + $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 HtmlClass < ContextUser @@c_seq = "C00000000" attr_reader :path def initialize(context, html_file, prefix, options) super(context, options) @@c_seq = @@c_seq.succ @c_seq = @@c_seq @html_file = html_file @is_module = context.is_module? @values = {} context.viewer = self @path = http_url(context.full_name, prefix) collect_methods AllReferences.add(name, self) end # return 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 seq @c_seq end def aref @c_seq end def scope a = @context.full_name.split("::") if a.length > 1 a.pop a.join("::") else "" end end def name @context.full_name.gsub("#{scope}::", '') end def full_name @context.full_name end def parent_name @context.parent.full_name end def write_on(f) value_hash template = TemplatePage.new(RDoc::Page::BODY, RDoc::Page::CLASS_PAGE, RDoc::Page::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) # Convert README to html unless File.exist?('files/README.html') File.open('files/README.html', 'w') do |file| file << markup(File.read(File.expand_path(@options.main_page))) end end d = markup(@context.comment) @values["description"] = d unless d.empty? ml = build_method_summary_list @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["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 @values["class_seq"] = seq 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"] = 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"] = 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 @@f_seq = "F00000000" attr_reader :path attr_reader :name def initialize(context, options, file_dir) super(context, options) @@f_seq = @@f_seq.succ @f_seq = @@f_seq @values = {} @path = http_url(file_dir) @source_file_path = File.expand_path(@context.file_relative_name).gsub("\/doc\/", "/") @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(/%|\/|\?|\#/) {|s| '%' + ("%x" % s[0]) } end def seq @f_seq end def aref @f_seq end def name full_path = @context.file_absolute_name short_name = File.basename(full_path) end def full_name @context.file_absolute_name end def scope @context.file_relative_name.gsub(/\/#{name}$/, '') end def parent_name nil end def full_file_source ret_str = "" File.open(@source_file_path, 'r') do |f| while(!f.eof?) do ret_str += f.readline() end end ret_str rescue "file not found -#{@source_file_path}-" #@source_file_path 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) @values["file_seq"] = seq #pulling in the source for this file #@values["source_code"] = @context.token_stream @values["file_source_code"] = CGI.escapeHTML(full_file_source) 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? file_context = @context @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 = TemplatePage.new(RDoc::Page::SRC_BODY,RDoc::Page::FILE_PAGE, RDoc::Page::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 @@m_seq = "M000000" @@all_methods = [] def HtmlMethod::reset @@all_methods = [] end def initialize(context, html_class, options) @context = context @html_class = html_class @options = options @@m_seq = @@m_seq.succ @m_seq = @@m_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 = MERBGenerator.gen_url(path, 'source.png') #end end AllReferences.add(name, self) end def seq @m_seq end def aref @m_seq end def scope @html_class.full_name end # return 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 name @context.name end def section @context.section end def parent_name if @context.parent.parent @context.parent.parent.full_name else nil end end def path @html_class.path 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 p = @context.params 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') File.makedirs(meth_path) file_path = File.join(meth_path, seq) + ".html" template = TemplatePage.new(RDoc::Page::SRC_PAGE) File.open(file_path, "w") do |f| values = { 'title' => CGI.escapeHTML(name), 'code' => code_body, 'style_url' => style_url(file_path, @options.css), 'charset' => @options.charset } template.write_html_on(f, values) end file_path end def HtmlMethod.all_methods @@all_methods 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) 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 ##################################################################### class MERBGenerator include MarkUp # Generators may need to return specific subclasses depending # on the options they are passed. Because of this # we create them using a factory def MERBGenerator.for(options) AllReferences::reset HtmlMethod::reset MERBGenerator.new(options) end class < "helvetica"} #this is not used anywhere but the template function demands a hash of values template.write_html_on(f, values) end end def write_javascript #Argh... I couldn't figure out how to copy these from the template dir so they were copied into # the template file "ajax.rb" and processed similarlly to the style sheets. Not exactly a good thing to do with # external library code. Not very DRY. File.open("api_grease.js", "w") do |f| f << RDoc::Page::API_GREASE_JS end File.open("prototype.js", "w") do |f| f << RDoc::Page::PROTOTYPE_JS end rescue LoadError $stderr.puts "Could not find AJAX template" exit 99 end ## # See the comments at the top for a description of the # directory structure def gen_sub_directories File.makedirs(FILE_DIR, CLASS_DIR) rescue $stderr.puts $!.message exit 1 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 # 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 # 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 File.makedirs(File.dirname(op_file)) File.open(op_file, "w") { |file| item.write_on(file) } end end end def gen_file_index gen_an_index(@files, 'Files', RDoc::Page::FILE_INDEX, "fr_file_index.html") end def gen_class_index gen_an_index(@classes, 'Classes', RDoc::Page::CLASS_INDEX, "fr_class_index.html") end def gen_method_index gen_an_index(HtmlMethod.all_methods, 'Methods', RDoc::Page::METHOD_INDEX, "fr_method_index.html") end def gen_an_index(collection, title, template, filename) template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template) res = [] collection.sort.each do |f| if f.document_self res << { "href" => f.path, "name" => f.name, "scope" => f.scope, "seq_id" => f.seq } 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 = TemplatePage.new(RDoc::Page::INDEX) File.open("index.html", "w") do |f| tStr = "" #File.open(main_url, 'r') do |g| # tStr = markup(g) #end values = { "initial_page" => tStr, 'title' => CGI.escapeHTML(@options.title), 'charset' => @options.charset, 'content' => File.read('files/README.html') } values['inline_source'] = true template.write_html_on(f, values) end end # return the url of the main page def main_url "files/README.html" end end end