From 87762adcb0d38d6c575448f67c2906964215f3a1 Mon Sep 17 00:00:00 2001 From: dave Date: Mon, 1 Dec 2003 07:12:49 +0000 Subject: Add RDoc git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@5073 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 4 + bin/rdoc | 67 + lib/rdoc/README | 445 +++++ lib/rdoc/code_objects.rb | 653 +++++++ lib/rdoc/diagram.rb | 333 ++++ lib/rdoc/generators/chm_generator.rb | 112 ++ lib/rdoc/generators/html_generator.rb | 1370 +++++++++++++++ lib/rdoc/generators/template/chm/chm.rb | 86 + lib/rdoc/generators/template/html/css2.rb | 631 +++++++ lib/rdoc/generators/template/html/hefss.rb | 418 +++++ lib/rdoc/generators/template/html/html.rb | 762 ++++++++ lib/rdoc/generators/template/html/kilmer.rb | 398 +++++ lib/rdoc/generators/template/xml/rdf.rb | 112 ++ lib/rdoc/generators/template/xml/xml.rb | 112 ++ lib/rdoc/generators/xml_generator.rb | 130 ++ lib/rdoc/markup/sample/rdoc2latex.rb | 16 + lib/rdoc/markup/sample/sample.rb | 42 + lib/rdoc/markup/simple_markup.rb | 477 +++++ lib/rdoc/markup/simple_markup/fragments.rb | 328 ++++ lib/rdoc/markup/simple_markup/inline.rb | 348 ++++ lib/rdoc/markup/simple_markup/lines.rb | 151 ++ lib/rdoc/markup/simple_markup/preprocess.rb | 68 + lib/rdoc/markup/simple_markup/to_html.rb | 289 ++++ lib/rdoc/markup/simple_markup/to_latex.rb | 333 ++++ lib/rdoc/markup/test/AllTests.rb | 2 + lib/rdoc/markup/test/TestInline.rb | 151 ++ lib/rdoc/markup/test/TestParse.rb | 503 ++++++ lib/rdoc/options.rb | 526 ++++++ lib/rdoc/parsers/parse_c.rb | 287 +++ lib/rdoc/parsers/parse_f95.rb | 118 ++ lib/rdoc/parsers/parse_rb.rb | 2494 +++++++++++++++++++++++++++ lib/rdoc/parsers/parse_simple.rb | 37 + lib/rdoc/parsers/parserfactory.rb | 86 + lib/rdoc/rdoc.rb | 219 +++ lib/rdoc/template.rb | 234 +++ lib/rdoc/tokenstream.rb | 25 + 36 files changed, 12367 insertions(+) create mode 100644 bin/rdoc create mode 100644 lib/rdoc/README create mode 100644 lib/rdoc/code_objects.rb create mode 100644 lib/rdoc/diagram.rb create mode 100644 lib/rdoc/generators/chm_generator.rb create mode 100644 lib/rdoc/generators/html_generator.rb create mode 100644 lib/rdoc/generators/template/chm/chm.rb create mode 100644 lib/rdoc/generators/template/html/css2.rb create mode 100644 lib/rdoc/generators/template/html/hefss.rb create mode 100644 lib/rdoc/generators/template/html/html.rb create mode 100644 lib/rdoc/generators/template/html/kilmer.rb create mode 100644 lib/rdoc/generators/template/xml/rdf.rb create mode 100644 lib/rdoc/generators/template/xml/xml.rb create mode 100644 lib/rdoc/generators/xml_generator.rb create mode 100644 lib/rdoc/markup/sample/rdoc2latex.rb create mode 100644 lib/rdoc/markup/sample/sample.rb create mode 100644 lib/rdoc/markup/simple_markup.rb create mode 100644 lib/rdoc/markup/simple_markup/fragments.rb create mode 100644 lib/rdoc/markup/simple_markup/inline.rb create mode 100644 lib/rdoc/markup/simple_markup/lines.rb create mode 100644 lib/rdoc/markup/simple_markup/preprocess.rb create mode 100644 lib/rdoc/markup/simple_markup/to_html.rb create mode 100644 lib/rdoc/markup/simple_markup/to_latex.rb create mode 100644 lib/rdoc/markup/test/AllTests.rb create mode 100644 lib/rdoc/markup/test/TestInline.rb create mode 100644 lib/rdoc/markup/test/TestParse.rb create mode 100644 lib/rdoc/options.rb create mode 100644 lib/rdoc/parsers/parse_c.rb create mode 100644 lib/rdoc/parsers/parse_f95.rb create mode 100644 lib/rdoc/parsers/parse_rb.rb create mode 100644 lib/rdoc/parsers/parse_simple.rb create mode 100644 lib/rdoc/parsers/parserfactory.rb create mode 100644 lib/rdoc/rdoc.rb create mode 100644 lib/rdoc/template.rb create mode 100644 lib/rdoc/tokenstream.rb diff --git a/ChangeLog b/ChangeLog index 33ed913651..5a108a2cad 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +Mon Dec 1 16:10:52 2003 Dave Thomas + + * lib/rdoc/rdoc.rb: (etc) initial merge into main tree. + Mon Dec 1 14:17:49 2003 Minero Aoki * lib/fileutils.rb (fu_each_src_dest0): call #to_str to allow diff --git a/bin/rdoc b/bin/rdoc new file mode 100644 index 0000000000..fe619137fd --- /dev/null +++ b/bin/rdoc @@ -0,0 +1,67 @@ +#!/usr/bin/env ruby +# +# RDoc: Documentation tool for source code +# (see lib/rdoc/rdoc.rb for more information) +# +# Copyright (c) 2003 Dave Thomas +# Released under the same terms as Ruby +# +# $Revision$ + +## Transitional Hack #### +# +# RDoc was initially distributed independently, and installed +# itself into /lib/ruby/site_ruby//rdoc... +# +# Now that RDoc is part of the distribution, it's installed into +# /lib/ruby/, which unfortunately appears later in the +# search path. This means that if you have previously installed RDoc, +# and then install from ruby-lang, you'll pick up the old one by +# default. This hack checks for the condition, and readjusts the +# search path if necessary. + +def adjust_for_existing_rdoc(path) + + $stderr.puts %{ + It seems as if you have a previously-installed RDoc in + the directory #{path}. + + Because this is now out-of-date, you might want to consider + removing the directories: + + #{File.join(path, "rdoc")} + + and + + #{File.join(path, "markup")} + + } + + # Move all the site_ruby directories to the end + p $: + $:.replace($:.partition {|path| /site_ruby/ !~ path}.flatten) + p $: +end + +$:.each do |path| + if /site_ruby/ =~ path + rdoc_path = File.join(path, 'rdoc', 'rdoc.rb') + if File.exists?(rdoc_path) + adjust_for_existing_rdoc(path) + break + end + end +end + +## End of Transitional Hack ## + + +require 'rdoc/rdoc' + +begin + r = RDoc::RDoc.new + r.document(ARGV) +rescue RDoc::RDocError => e + $stderr.puts e.message + exit(1) +end diff --git a/lib/rdoc/README b/lib/rdoc/README new file mode 100644 index 0000000000..39ce8bb75b --- /dev/null +++ b/lib/rdoc/README @@ -0,0 +1,445 @@ += RDOC - Ruby Documentation System + +This package contains Rdoc and SimpleMarkup. Rdoc is an application +that produces documentation for one or more Ruby source files. We work +similarly to JavaDoc, parsing the source, and extracting the +definition for classes, modules, and methods (along with includes and +requires). We associate with these optional documentation contained +in the immediately preceding comment block, and then render the result +using a pluggable output formatter. (Currently, HTML is the only +supported format. Markup is a library that converts plain text into +various output formats. The Markup library is used to interpret the +comment blocks that Rdoc uses to document methods, classes, and so on. + +This library contains two packages, rdoc itself and a text markup +library, 'markup'. + +== Roadmap + +* If you want to use Rdoc to create documentation for your Ruby source + files, read on. +* If you want to include extensions written in C, see rdoc/parsers/parse_c.rb. +* For information on the various markups available in comment + blocks, see markup/simple_markup.rb. +* If you want to drive Rdoc programatically, see RDoc::RDoc. +* If you want to use the library to format text blocks into HTML, + have a look at SM::SimpleMarkup. +* If you want to try writing your own HTML output template, see + RDoc::Page. + +== Summary + +Once installed, you can create documentation using the 'rdoc' command +(the command is 'rdoc.bat' under Windows) + + % rdoc [options] [names...] + +Type "rdoc --help" for an up-to-date option summary. + +A typical use might be to generate documentation for a package of Ruby +source (such as rdoc itself). + + % rdoc + +This command generates documentation for all the Ruby and C source +files in and below the current directory. These will be stored in a +documentation tree starting in the subdirectory 'doc'. + +You can make this slightly more useful for your readers by having the +index page contain the documentation for the primary file. In our +case, we could type + + % rdoc --main rdoc/rdoc.rb + +You'll find information on the various formatting tricks you can use +in comment blocks in the documentation this generates. + +RDoc uses file extensions to determine how to process each file. File +names ending .rb and .rbw are assumed to be Ruby +source. Files ending .c are parsed as C files. All other +files are assumed to contain just SimpleMarkup-style markup (with or +without leading '#' comment markers). If directory names are passed to +RDoc, they are scanned recursively for C and Ruby source files only. + +== Credits + +* The Ruby parser in rdoc/parse.rb is based heavily on the outstanding + work of Keiju ISHITSUKA of Nippon Rational Inc, who produced the Ruby + parser for irb and the rtags package. + +* Code to diagram classes and modules was written by Sergey A Yanovitsky + (Jah) of Enticla. + +* Charset patch from MoonWolf. + +* Rich Kilmer wrote the kilmer.rb output template. + +* Dan Brickley led the design of the RDF format. + +== License + +RDoc is Copyright (c) 2001-2003 Dave Thomas, The Pragmatic Programmers. It +is free software, and may be redistributed under the terms specified +in the README file of the Ruby distribution. + + +---- + += Usage + +RDoc is invoked from the command line using: + + % rdoc [name...] + +Files are parsed, and the information they contain collected, before +any output is produced. This allows cross references between all files +to be resolved. If a name is a directory, it is traversed. If no +names are specified, all Ruby files in the current directory (and +subdirectories) are processed. + +Options are: + +[--accessor name[,name...]] + specifies the name(s) of additional methods that should be treated + as if they were attr_xxx methods. Specifying + "--accessor db_opt" means lines such as + + db_opt :name, :age + + will get parsed and displayed in the documentation. Each name may have an + optional "=flagtext" appended, in which case the given flagtext will appear + where (for example) the 'rw' appears for attr_accessor. + +[--all] + include protected and private methods in the output (by default + only public methods are included) + +[--charset _charset_] + Set the character set for the generated HTML. + +[--diagram] + include diagrams showing modules and classes. This is currently + an experimental feature, and may not be supported by all output + templates. You need dot V1.8.6 or later to use the --diagram + option correctly (http://www.research.att.com/sw/tools/graphviz/). + +[--exclude pattern] + exclude files and directories matching this pattern from processing + +[--extension new=old] + treat files ending .new as if they ended + .old. Saying '--extension cgi=rb' causes RDoc to treat .cgi + files as Ruby source. + +[fileboxes] + Classes are put in boxes which represents files, where these + classes reside. Classes shared between more than one file are + shown with list of files that sharing them. Silently discarded if + --diagram is not given Experimental. + +[--fmt _fmt_] + generate output in a particular format. + +[--help] + generate a usage summary. + +[--help-output] + explain the various output options. + +[--image-format gif/png/jpg/jpeg] + sets output image format for diagrams. Can be png, gif, jpeg, + jpg. If this option is omitted, png is used. Requires --diagram. + +[--include dir,...] + specify one or more directories to be searched when satisfying + :+include+: directives. Multiple --include options may be + given. The directory containing the file currently being processed + is always searched. + +[--inline-source] + By default, the source code of methods is shown in a popup. With + this option, it's displayed inline. + +[line-numbers] + include line numbers in the source code + +[--main _name_] + set the class, module, or file to appear on the index page + +[--one-file] + place all the output into a single file + +[--op _dir_] + set the output directory to _dir_ (the default is the directory + "doc") + +[--op-name _name_] + set the name of the output. Has no effect for HTML. + "doc") + +[--opname _name_] + set the output name (has no effect for HTML). + +[--promiscuous] + If a module or class is defined in more than one source file, and + you click on a particular file's name in the top navigation pane, + RDoc will normally only show you the inner classes and modules of + that class that are defined in the particular file. Using this + option makes it show all classes and modules defined in the class, + regardless of the file they were defined in. + +[--quiet] + do not display progress messages + +[--show-hash] + A name of the form #name in a comment is a possible hyperlink to + an instance method name. When displayed, the '#' is removed unless + this option is specified + +[--style stylesheet url] + specifies the URL of an external stylesheet to use (rather than + generating one of our own) + +[tab-width _n_] + set the width of tab characters (default 8) + +[--template name] + specify an alternate template to use when generating output (the + default is 'standard'). This template should be in a directory + accessible via $: as rdoc/generators/xxxx_template, where 'xxxx' + depends on the output formatter. + +[--version] + display RDoc's version + +[--webcvs _url_] + Specify a URL for linking to a web frontend to CVS. If the URL + contains a '\%s', the name of the current file will be + substituted; if the URL doesn't contain a '\%s', the filename will + be appended to it. + += Example + +A typical small Ruby program commented using RDoc might be as follows. You +can see the formatted result in EXAMPLE.rb and Anagram. + + :include: EXAMPLE.rb + += Markup + +Comment blocks can be written fairly naturally. + +Paragraphs are lines that share the left margin. Text indented past +this margin are formatted verbatim. + +1. Lists are typed as indented paragraphs with: + * a '*' or '-' (for bullet lists) + * a digit followed by a period for + numbered lists + * an upper or lower case letter followed + by a period for alpha lists. + + For example, the input that produced the above paragraph looked like + 1. Lists are typed as indented + paragraphs with: + * a '*' or '-' (for bullet lists) + * a digit followed by a period for + numbered lists + * an upper or lower case letter followed + by a period for alpha lists. + +2. Labeled lists (sometimes called description + lists) are typed using square brackets for the label. + [cat] small domestic animal + [+cat+] command to copy standard input + +3. Labeled lists may also be produced by putting a double colon + after the label. This sets the result in tabular form, so the + descriptions all line up. This was used to create the 'author' + block at the bottom of this description. + cat:: small domestic animal + +cat+:: command to copy standard input + + For both kinds of labeled lists, if the body text starts on the same + line as the label, then the start of that text determines the block + indent for the rest of the body. The text may also start on the line + following the label, indented from the start of the label. This is + often preferable if the label is long. Both the following are + valid labeled list entries: + + --output name [, name]:: + specify the name of one or more output files. If multiple + files are present, the first is used as the index. + + --quiet::: do not output the names, sizes, byte counts, + index areas, or bit ratios of units as + they are processed. + +4. Headings are entered using equals signs + + = Level One Heading + == Level Two Heading + and so on + +5. Rules (horizontal lines) are entered using three or + more hyphens. + +6. Non-verbatim text can be marked up: + + _italic_:: \_word_ or \text + *bold*:: \*word* or \text + +typewriter+:: \+word+ or \text + + The first form only works around 'words', where a word is a + sequence of upper and lower case letters and underscores. Putting a + backslash before inline markup stops it being interpreted, which is + how I created the table above: + + _italic_:: \_word_ or \text + *bold*:: \*word* or \text + +typewriter+:: \+word+ or \text + +7. Names of classes, source files, and any method names + containing an underscore or preceded by a hash + character are automatically hyperlinked from + comment text to their description. + +8. Hyperlinks to the web starting http:, mailto:, ftp:, or www. are + recognized. An HTTP url that references an external image file is + converted into an inline . Hyperlinks starting 'link:' are + assumed to refer to local files whose path is relative to the --op + directory. + + Hyperlinks can also be of the form label[url], in which + case the label is used in the displayed text, and url is + used as the target. + +9. Method parameter lists are extracted and displayed with + the method description. If a method calls +yield+, then + the parameters passed to yield will also be displayed: + + def fred + ... + yield line, address + + This will get documented as + + fred() { |line, address| ... } + + You can override this using a comment containing + ':yields: ...' immediately after the method definition + + def fred # :yields: index, position + ... + yield line, address + + which will get documented as + + fred() { |index, position| ... } + + +10. ':yields:' is an example of a documentation modifier. These appear + immediately after the start of the document element they are modifying. + Other modifiers include + + [:nodoc:[all]] + don't include this element in the documentation. For classes + and modules, the methods, aliases, constants, and attributes + directly within the affected class or module will also be + omitted. By default, though, modules and classes within that + class of module _will_ be documented. This is turned off by + adding the +all+ modifier. + + module SM #:nodoc: + class Input + end + end + module Markup #:nodoc: all + class Output + end + end + + In the above code, only class SM::Input will be + documented. + + [:doc:] + force a method or attribute to be documented even if it + wouldn't otherwise be. Useful if, for example, you want to + include documentation of a particular private method. + + [:notnew:] + only applicable to the +initialize+ instance method. Normally + RDoc assumes that the documentation and parameters for + #initialize are actually for the ::new method, and so fakes + out a ::new for the class. THe :notnew: modifier stops + this. Remember that #initialize is protected, so you won't + see the documentation unless you use the -a command line + option. + + +11. RDoc stops processing comments if it finds a comment + line containing '#--'. This can be used to + separate external from internal comments, or + to stop a comment being associated with a method, + class, or module. Commenting can be turned back on with + a line that starts '#++'. + + # Extract the age and calculate the + # date-of-birth. + #-- + # FIXME: fails if the birthday falls on + # February 29th + #++ + # The DOB is returned as a Time object. + + def get_dob(person) + ... + +12. Comment blocks can contain other directives: + + [:include:filename] + include the contents of the named file at this point. The + file will be searched for in the directories listed by + the --include option, or in the current + directory by default. The contents of the file will be + shifted to have the same indentation as the ':' at the + start of the :include: directive. + + [:title:text] + Sets the title for the document. Equivalent to the --title command + line parameter. (The command line parameter overrides any :title: + directive in the source). + + [:enddoc:] + Document nothing further at the current level. + + [:main:name] + Equivalent to the --main command line parameter. + + [:stopdoc: / :startdoc:] + Stop and start adding new documentation elements to the + current container. For example, if a class has a number of + constants that you don't want to document, put a + :stopdoc: before the first, and a + :startdoc: after the last. If you don't specifiy a + :startdoc: by the end of the container, disables + documentation for the entire class or module. + + +--- + +See also markup/simple_markup.rb. + += Other stuff + +Author:: Dave Thomas +Requires:: Ruby 1.8.1 or later +License:: Copyright (c) 2001-2003 Dave Thomas. + Released under the same license as Ruby. + +== Warranty + +This software is provided "as is" and without any express or +implied warranties, including, without limitation, the implied +warranties of merchantibility and fitness for a particular +purpose. diff --git a/lib/rdoc/code_objects.rb b/lib/rdoc/code_objects.rb new file mode 100644 index 0000000000..7fdbbde027 --- /dev/null +++ b/lib/rdoc/code_objects.rb @@ -0,0 +1,653 @@ +# We represent the various high-level code constructs that appear +# in Ruby programs: classes, modules, methods, and so on. + +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 + + # We are the model of the code, but we know that at some point + # we will be worked on by viewers. By implementing the Viewable + # protocol, viewers can associated themselves with these objects. + + attr_accessor :viewer + + # are we done documenting (ie, did we come across a :enddoc:)? + + attr_accessor :done_documenting + + + # do we document ourselves? + + attr_reader :document_self + + def document_self=(val) + @document_self = val + if !val + remove_methods_etc + end + end + + # set and cleared by :startdoc: and :enddoc:, this is used to toggle + # the capturing of documentation + def start_doc + @document_self = true + @document_children = true + end + + def stop_doc + @document_self = false + @document_children = false + end + + # do we document ourselves and our children + + attr_reader :document_children + + def document_children=(val) + @document_children = val + if !val + remove_classes_and_modules + end + end + + # Do we _force_ documentation, even is we wouldn't normally show the entity + attr_accessor :force_documentation + + # Default callbacks to nothing, but this is overridden for classes + # and modules + def remove_classes_and_modules + end + + def remove_methods_etc + end + + def initialize + @document_self = true + @document_children = true + @force_documentation = false + @done_documenting = false + end + + # Access the code object's comment + attr_reader :comment + + # Update the comment, but don't overwrite a real comment + # with an empty one + def comment=(comment) + @comment = comment unless comment.empty? + end + + # There's a wee trick we pull. Comment blocks can have directives that + # override the stuff we extract during the parse. So, we have a special + # class method, attr_overridable, that lets code objects list + # those directives. Wehn a comment is assigned, we then extract + # out any matching directives and update our object + + def CodeObject.attr_overridable(name, *aliases) + @overridables ||= {} + + attr_accessor name + + aliases.unshift name + aliases.each do |directive_name| + @overridables[directive_name.to_s] = name + end + end + + end + + # A Context is something that can hold modules, classes, methods, + # attributes, aliases, requires, and includes. Classes, modules, and + # files are all Contexts. + + class Context < CodeObject + attr_reader :name, :method_list, :attributes, :aliases, :constants + attr_reader :requires, :includes, :in_files, :visibility + + + def initialize + super() + + @in_files = [] + + @name ||= "unknown" + @comment ||= "" + @parent = nil + @visibility = :public + + initialize_methods_etc + initialize_classes_and_modules + end + + # map the class hash to an array externally + def classes + @classes.values + end + + # map the module hash to an array externally + def modules + @modules.values + end + + # Change the default visibility for new methods + def ongoing_visibility=(vis) + @visibility = vis + end + + # Given an array +methods+ of method names, set the + # visibility of the corresponding AnyMethod object + + def set_visibility_for(methods, vis, singleton=false) + @method_list.each_with_index do |m,i| + if methods.include?(m.name) && m.singleton == singleton + m.visibility = vis + end + end + end + + # Record the file that we happen to find it in + def record_location(toplevel) + @in_files << toplevel unless @in_files.include?(toplevel) + end + + # Return true if at least part of this thing was defined in +file+ + def defined_in?(file) + @in_files.include?(file) + end + + def add_class(class_type, name, superclass) + add_class_or_module(@classes, class_type, name, superclass) + end + + def add_module(class_type, name) + add_class_or_module(@modules, class_type, name, nil) + end + + def add_method(a_method) + puts "Adding #@visibility method #{a_method.name} to #@name" if $DEBUG + a_method.visibility = @visibility + add_to(@method_list, a_method) + end + + def add_attribute(an_attribute) + add_to(@attributes, an_attribute) + end + + def add_alias(an_alias) + meth = find_method_named(an_alias.old_name) + if meth + new_meth = AnyMethod.new(an_alias.text, an_alias.new_name) + new_meth.is_alias_for = meth + new_meth.singleton = meth.singleton + new_meth.params = meth.params + new_meth.comment = "Alias for \##{meth.name}" + meth.add_alias(new_meth) + add_method(new_meth) + else + add_to(@aliases, an_alias) + end + end + + def add_include(an_include) + add_to(@includes, an_include) + end + + def add_constant(const) + add_to(@constants, const) + end + + # Requires always get added to the top-level (file) context + def add_require(a_require) + if self.kind_of? TopLevel + add_to(@requires, a_require) + else + parent.add_require(a_require) + end + end + + def add_class_or_module(collection, class_type, name, superclass=nil) + cls = collection[name] + if cls + puts "Reusing class/module #{name}" if $DEBUG + else + cls = class_type.new(name, superclass) + puts "Adding class/module #{name} to #@name" if $DEBUG + collection[name] = cls + cls.parent = self + end + cls + end + + def add_to(array, thing) + array << thing if @document_self + thing.parent = self + end + + # If a class's documentation is turned off after we've started + # collecting methods etc., we need to remove the ones + # we have + + def remove_methods_etc + initialize_methods_etc + end + + def initialize_methods_etc + @method_list = [] + @attributes = [] + @aliases = [] + @requires = [] + @includes = [] + @constants = [] + end + + # and remove classes and modules when we see a :nodoc: all + def remove_classes_and_modules + initialize_classes_and_modules + end + + def initialize_classes_and_modules + @classes = {} + @modules = {} + end + + # Find a named module + def find_module_named(name) + return self if self.name == name + res = @modules[name] || @classes[name] + return res if res + parent && parent.find_module_named(name) + end + + # Iterate over all the classes and modules in + # this object + + def each_classmodule + @modules.each_value {|m| yield m} + @classes.each_value {|c| yield c} + end + + def each_method + @method_list.each {|m| yield m} + end + + def each_attribute + @attributes.each {|a| yield a} + end + + def each_constant + @constants.each {|c| yield c} + end + + # Return the toplevel that owns us + + def toplevel + return @toplevel if defined? @toplevel + @toplevel = self + @toplevel = @toplevel.parent until TopLevel === @toplevel + @toplevel + end + + # allow us to sort modules by name + def <=>(other) + name <=> other.name + end + + # Look up the given symbol. If method is non-nil, then + # we assume the symbol references a module that + # contains that method + def find_symbol(symbol, method=nil) + result = nil + case symbol + when /^::(.*)/ + result = toplevel.find_symbol(symbol) + when /::/ + modules = symbol.split(/::/) + unless modules.empty? + module_name = modules.shift + result = find_module_named(module_name) + if result + modules.each do |module_name| + result = result.find_module_named(module_name) + end + end + end + else + result = find_local_symbol(symbol) + if result.nil? + if symbol =~ /^[A-Z]/ + result = parent + while result && result.name != symbol + result = result.parent + end + end + end + end + if result && method + result = result.find_local_symbol(method) + end + result + end + + def find_local_symbol(symbol) + res = find_method_named(symbol) || + find_constant_named(symbol) || + find_attribute_named(symbol) || + find_module_named(symbol) + end + + private + + # Find a named method, or return nil + def find_method_named(name) + @method_list.find {|meth| meth.name == name} + end + + # Find a named constant, or return nil + def find_constant_named(name) + @constants.find {|m| m.name == name} + end + + # Find a named attribute, or return nil + def find_attribute_named(name) + @attributes.find {|m| m.name == name} + end + + end + + + # A TopLevel context is a source file + + class TopLevel < Context + attr_accessor :file_stat + attr_accessor :file_relative_name + attr_accessor :file_absolute_name + attr_accessor :diagram + + @@all_classes = {} + @@all_modules = {} + + def TopLevel::reset + @@all_classes = {} + @@all_modules = {} + end + + def initialize(file_name) + super() + @name = "TopLevel" + @file_relative_name = file_name + @file_absolute_name = file_name + @file_stat = File.stat(file_name) + @diagram = nil + end + + def full_name + nil + end + + # Adding a class or module to a TopLevel is special, as we only + # want one copy of a particular top-level class. For example, + # if both file A and file B implement class C, we only want one + # ClassModule object for C. This code arranges to share + # classes and modules between files. + + def add_class_or_module(collection, class_type, name, superclass) + cls = collection[name] + if cls + puts "Reusing class/module #{name}" if $DEBUG + else + if class_type == NormalModule + all = @@all_modules + else + all = @@all_classes + end + cls = all[name] + if !cls + cls = class_type.new(name, superclass) + all[name] = cls + end + puts "Adding class/module #{name} to #@name" if $DEBUG + collection[name] = cls + cls.parent = self + end + cls + end + + def TopLevel.all_classes_and_modules + @@all_classes.values + @@all_modules.values + end + + def TopLevel.find_class_named(name) + @@all_classes.each_value do |c| + res = c.find_class_named(name) + return res if res + end + nil + end + + def find_local_symbol(symbol) + find_class_or_module_named(symbol) || super + end + + def find_class_or_module_named(symbol) + @@all_classes.each_value {|c| return c if c.name == symbol} + @@all_modules.each_value {|m| return m if m.name == symbol} + nil + end + + end + + # ClassModule is the base class for objects representing either a + # class or a module. + + class ClassModule < Context + + attr_reader :superclass + attr_accessor :diagram + + def initialize(name, superclass = nil) + @name = name + @diagram = nil + @superclass = superclass + @comment = "" + super() + end + + # Return the fully qualified name of this class or module + def full_name + if @parent && @parent.full_name + @parent.full_name + "::" + @name + else + @name + end + end + + def http_url(prefix) + path = full_name.split("::") + File.join(prefix, *path) + ".html" + end + + # Return +true+ if this object represents a module + def is_module? + false + end + + # to_s is simply for debugging + def to_s + res = self.class.name + ": " + @name + res << @comment.to_s + res << super + res + end + + def find_class_named(name) + return self if full_name == name + @classes.each_value {|c| return c if c.find_class_named(name) } + nil + end + end + + # Anonymous classes + class AnonClass < ClassModule + end + + # Normal classes + class NormalClass < ClassModule + end + + # Singleton classes + class SingleClass < ClassModule + end + + # Module + class NormalModule < ClassModule + def is_module? + true + end + end + + + # AnyMethod is the base class for objects representing methods + + class AnyMethod < CodeObject + attr_accessor :name + attr_accessor :visibility + 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_overridable :params, :param, :parameters, :parameter + + include TokenStream + + def initialize(text, name) + super() + @text = text + @name = name + @token_stream = nil + @visibility = :public + @dont_rename_initialize = false + @block_params = nil + @aliases = [] + @is_alias_for = nil + @comment = "" + end + + def <=>(other) + @name <=> other.name + end + + def to_s + res = self.class.name + ": " + @name + " (" + @text + ")\n" + res << @comment.to_s + res + end + + def param_seq + p = params.gsub(/\s*\#.*/, '') + p = p.tr("\n", " ").squeeze(" ") + p = "(" + p + ")" unless p[0] == ?( + + if (block = block_params) + block.gsub!(/\s*\#.*/, '') + block = block.tr("\n", " ").squeeze(" ") + if block[0] == ?( + block.sub!(/^\(/, '').sub!(/\)/, '') + end + p << " {|#{block}| ...}" + end + p + end + + def add_alias(method) + @aliases << method + end + end + + + # Represent an alias, which is an old_name/ new_name pair associated + # with a particular context + class Alias < CodeObject + attr_accessor :text, :old_name, :new_name, :comment + + def initialize(text, old_name, new_name, comment) + super() + @text = text + @old_name = old_name + @new_name = new_name + self.comment = comment + end + + def to_s + "alias: #{self.old_name} -> #{self.new_name}\n#{self.comment}" + end + end + + # Represent a constant + class Constant < CodeObject + attr_accessor :name, :value + + def initialize(name, value, comment) + super() + @name = name + @value = value + self.comment = comment + end + end + + # Represent attributes + class Attr < CodeObject + attr_accessor :text, :name, :rw + + def initialize(text, name, rw, comment) + super() + @text = text + @name = name + @rw = rw + self.comment = comment + end + + def to_s + "attr: #{self.name} #{self.rw}\n#{self.comment}" + end + + def <=>(other) + self.name <=> other.name + end + end + + # a required file + + class Require < CodeObject + attr_accessor :name + + def initialize(name, comment) + super() + @name = name.gsub(/'|"/, "") #' + self.comment = comment + end + + end + + # an included module + class Include < CodeObject + attr_accessor :name + + def initialize(name, comment) + super() + @name = name + self.comment = comment + end + + end + +end diff --git a/lib/rdoc/diagram.rb b/lib/rdoc/diagram.rb new file mode 100644 index 0000000000..3c7df03417 --- /dev/null +++ b/lib/rdoc/diagram.rb @@ -0,0 +1,333 @@ +# A wonderful hack by to draw package diagrams using the dot package. +# Originally written by Jah, team Enticla. +# +# You must have the V1.7 or later in your path +# http://www.research.att.com/sw/tools/graphviz/ + +require "dot/dot" +require 'rdoc/options' + +module RDoc + + # Draw a set of diagrams representing the modules and classes in the + # system. We draw one diagram for each file, and one for each toplevel + # class or module. This means there will be overlap. However, it also + # means that you'll get better context for objects. + # + # To use, simply + # + # d = Diagram.new(info) # pass in collection of top level infos + # d.draw + # + # The results will be written to the +dot+ subdirectory. The process + # also sets the +diagram+ attribute in each object it graphs to + # the name of the file containing the image. This can be used + # by output generators to insert images. + + class Diagram + + FONT = "Arial" + + DOT_PATH = "dot" + + # Pass in the set of top level objects. The method also creates + # the subdirectory to hold the images + + def initialize(info, options) + @info = info + @options = options + @counter = 0 + File.makedirs(DOT_PATH) + end + + # Draw the diagrams. We traverse the files, drawing a diagram for + # each. We also traverse each top-level class and module in that + # file drawing a diagram for these too. + + def draw + unless @options.quiet + $stderr.print "Diagrams: " + $stderr.flush + end + + @info.each_with_index do |i, file_count| + @done_modules = {} + @local_names = find_names(i) + @global_names = [] + @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel', + 'label' => i.file_absolute_name, + 'fontname' => FONT, + 'fontsize' => '8', + 'bgcolor' => 'lightcyan1', + 'compound' => 'true') + + # it's a little hack %) i'm too lazy to create a separate class + # for default node + graph << DOT::DOTNode.new('name' => 'node', + 'fontname' => FONT, + 'color' => 'black', + 'fontsize' => 8) + + i.modules.each do |mod| + draw_module(mod, graph, true, i.file_relative_name) + end + add_classes(i, graph, i.file_relative_name) + + i.diagram = convert_to_png("f_#{file_count}", graph, i.name) + + # now go through and document each top level class and + # module independently + i.modules.each_with_index do |mod, count| + @done_modules = {} + @local_names = find_names(mod) + @global_names = [] + + @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel', + 'label' => i.full_name, + 'fontname' => FONT, + 'fontsize' => '8', + 'bgcolor' => 'lightcyan1', + 'compound' => 'true') + + graph << DOT::DOTNode.new('name' => 'node', + 'fontname' => FONT, + 'color' => 'black', + 'fontsize' => 8) + draw_module(mod, graph, true) + mod.diagram = convert_to_png("m_#{file_count}_#{count}", + graph, + "Module: #{mod.name}") + end + end + $stderr.puts unless @options.quiet + end + + ####### + private + ####### + + def find_names(mod) + return [mod.full_name] + mod.classes.collect{|cl| cl.full_name} + + mod.modules.collect{|m| find_names(m)}.flatten + end + + def find_full_name(name, mod) + full_name = name.dup + return full_name if @local_names.include?(full_name) + mod_path = mod.full_name.split('::')[0..-2] + unless mod_path.nil? + until mod_path.empty? + full_name = mod_path.pop + '::' + full_name + return full_name if @local_names.include?(full_name) + end + end + return name + end + + def draw_module(mod, graph, toplevel = false, file = nil) + return if @done_modules[mod.full_name] and not toplevel + + @counter += 1 + url = mod.http_url("classes") + m = DOT::DOTSubgraph.new('name' => "cluster_#{mod.full_name.gsub( /:/,'_' )}", + 'label' => mod.name, + 'fontname' => FONT, + 'color' => 'blue', + 'style' => 'filled', + 'URL' => %{"#{url}"}, + 'fillcolor' => toplevel ? 'palegreen1' : 'palegreen3') + + @done_modules[mod.full_name] = m + add_classes(mod, m, file) + graph << m + + unless mod.includes.empty? + mod.includes.each do |m| + m_full_name = find_full_name(m.name, mod) + if @local_names.include?(m_full_name) + @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}", + 'to' => "#{mod.full_name.gsub( /:/,'_' )}", + 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}", + 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}") + else + unless @global_names.include?(m_full_name) + path = m_full_name.split("::") + url = File.join('classes', *path) + ".html" + @global_graph << DOT::DOTNode.new('name' => "#{m_full_name.gsub( /:/,'_' )}", + 'shape' => 'box', + 'label' => "#{m_full_name}", + 'URL' => %{"#{url}"}) + @global_names << m_full_name + end + @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}", + 'to' => "#{mod.full_name.gsub( /:/,'_' )}", + 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}") + end + end + end + end + + def add_classes(container, graph, file = nil ) + + use_fileboxes = Options.instance.fileboxes + + files = {} + + # create dummy node (needed if empty and for module includes) + if container.full_name + graph << DOT::DOTNode.new('name' => "#{container.full_name.gsub( /:/,'_' )}", + 'label' => "", + 'width' => (container.classes.empty? and + container.modules.empty?) ? + '0.75' : '0.01', + 'height' => '0.01', + 'shape' => 'plaintext') + end + container.classes.each_with_index do |cl, cl_index| + last_file = cl.in_files[-1].file_relative_name + + if use_fileboxes && !files.include?(last_file) + @counter += 1 + files[last_file] = + DOT::DOTSubgraph.new('name' => "cluster_#{@counter}", + 'label' => "#{last_file}", + 'fontname' => FONT, + 'color'=> + last_file == file ? 'red' : 'black') + end + + next if cl.name == 'Object' || cl.name[0,2] == "<<" + + url = cl.http_url("classes") + + label = cl.name.dup + if use_fileboxes && cl.in_files.length > 1 + label << '\n[' + + cl.in_files.collect {|i| + i.file_relative_name + }.sort.join( '\n' ) + + ']' + end + + attrs = { + 'name' => "#{cl.full_name.gsub( /:/, '_' )}", + 'fontcolor' => 'black', + 'style'=>'filled', + 'color'=>'palegoldenrod', + 'label' => label, + 'shape' => 'ellipse', + 'URL' => %{"#{url}"} + } + + c = DOT::DOTNode.new(attrs) + + if use_fileboxes + files[last_file].push c + else + graph << c + end + end + + if use_fileboxes + files.each_value do |val| + graph << val + end + end + + unless container.classes.empty? + container.classes.each_with_index do |cl, cl_index| + cl.includes.each do |m| + m_full_name = find_full_name(m.name, cl) + if @local_names.include?(m_full_name) + @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}", + 'to' => "#{cl.full_name.gsub( /:/,'_' )}", + 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}") + else + unless @global_names.include?(m_full_name) + path = m_full_name.split("::") + url = File.join('classes', *path) + ".html" + @global_graph << DOT::DOTNode.new('name' => "#{m_full_name.gsub( /:/,'_' )}", + 'shape' => 'box', + 'label' => "#{m_full_name}", + 'URL' => %{"#{url}"}) + @global_names << m_full_name + end + @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}", + 'to' => "#{cl.full_name.gsub( /:/, '_')}") + end + end + + sclass = cl.superclass + next if sclass.nil? || sclass == 'Object' + sclass_full_name = find_full_name(sclass,cl) + unless @local_names.include?(sclass_full_name) or @global_names.include?(sclass_full_name) + path = sclass_full_name.split("::") + url = File.join('classes', *path) + ".html" + @global_graph << DOT::DOTNode.new( + 'name' => "#{sclass_full_name.gsub( /:/, '_' )}", + 'label' => sclass_full_name, + 'URL' => %{"#{url}"}) + @global_names << sclass_full_name + end + @global_graph << DOT::DOTEdge.new('from' => "#{sclass_full_name.gsub( /:/,'_' )}", + 'to' => "#{cl.full_name.gsub( /:/, '_')}") + end + end + + container.modules.each do |submod| + draw_module(submod, graph) + end + + end + + def convert_to_png(file_base, graph, name) + op_type = Options.instance.image_format + dotfile = File.join(DOT_PATH, file_base) + src = dotfile + ".dot" + dot = dotfile + "." + op_type + + unless @options.quiet + $stderr.print "." + $stderr.flush + end + + File.open(src, 'w+' ) do |f| + f << graph.to_s << "\n" + end + + system "dot -T#{op_type} #{src} -o #{dot}" + + # Now construct the imagemap wrapper around + # that png + + return wrap_in_image_map(src, dot, name) + end + + # Extract the client-side image map from dot, and use it + # to generate the imagemap proper. Return the whole + # .. combination, suitable for inclusion on + # the page + + def wrap_in_image_map(src, dot, name) + res = %{\n} + dot_map = `dot -Tismap #{src}` + dot_map.each do |area| + unless area =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) ([\/\w.]+)\s*(.*)/ + $stderr.puts "Unexpected output from dot:\n#{area}" + return nil + end + + blx = $1; bly = $2 + trx = $3; try = $4 + url = $5; area_name = $6 + res << %{ #{area_name}\n} + end + res << "\n" +# map_file = src.sub(/.dot/, '.map') +# system("dot -Timap #{src} -o #{map_file}") + res << %{#{name}} + return res + end + end +end diff --git a/lib/rdoc/generators/chm_generator.rb b/lib/rdoc/generators/chm_generator.rb new file mode 100644 index 0000000000..1c46769cc2 --- /dev/null +++ b/lib/rdoc/generators/chm_generator.rb @@ -0,0 +1,112 @@ +require 'rdoc/generators/html_generator' + +module Generators + + class CHMGenerator < HTMLGenerator + + HHC_PATH = "c:\\Program Files\\HTML Help Workshop\\hhc.exe" + + # Standard generator factory + def CHMGenerator.for(options) + CHMGenerator.new(options) + end + + + def initialize(*args) + super + @op_name = @options.op_name || "rdoc" + check_for_html_help_workshop + end + + def check_for_html_help_workshop + stat = File.stat(HHC_PATH) + rescue + $stderr << + "\n.chm output generation requires that Microsoft's Html Help\n" << + "Workshop is installed. RDoc looks for it in:\n\n " << + HHC_PATH << + "\n\nYou can download a copy for free from:\n\n" << + " http://msdn.microsoft.com/library/default.asp?" << + "url=/library/en-us/htmlhelp/html/hwMicrosoftHTMLHelpDownloads.asp\n\n" + + exit 99 + end + + # Generate the html as normal, then wrap it + # in a help project + def generate(info) + super + @project_name = @op_name + ".hhp" + create_help_project + end + + # The project contains the project file, a table of contents + # and an index + def create_help_project + create_project_file + create_contents_and_index + compile_project + end + + # The project file links together all the various + # files that go to make up the help. + + def create_project_file + template = TemplatePage.new(RDoc::Page::HPP_FILE) + values = { "title" => @options.title, "opname" => @op_name } + files = [] + @files.each do |f| + files << { "html_file_name" => f.path } + end + + values['all_html_files'] = files + + File.open(@project_name, "w") do |f| + template.write_html_on(f, values) + end + end + + # The contents is a list of all files and modules. + # For each we include as sub-entries the list + # of methods they contain. As we build the contents + # we also build an index file + + def create_contents_and_index + contents = [] + index = [] + + (@files+@classes).sort.each do |entry| + content_entry = { "c_name" => entry.name, "ref" => entry.path } + index << { "name" => entry.name, "aref" => entry.path } + + internals = [] + + methods = entry.build_method_summary_list(entry.path) + + content_entry["methods"] = methods unless methods.empty? + contents << content_entry + index.concat methods + end + + values = { "contents" => contents } + template = TemplatePage.new(RDoc::Page::CONTENTS) + File.open("contents.hhc", "w") do |f| + template.write_html_on(f, values) + end + + values = { "index" => index } + template = TemplatePage.new(RDoc::Page::CHM_INDEX) + File.open("index.hhk", "w") do |f| + template.write_html_on(f, values) + end + end + + # Invoke the windows help compiler to compiler the project + def compile_project + system("\"#{HHC_PATH}\" #@project_name") + end + + end + + +end diff --git a/lib/rdoc/generators/html_generator.rb b/lib/rdoc/generators/html_generator.rb new file mode 100644 index 0000000000..36e2f351da --- /dev/null +++ b/lib/rdoc/generators/html_generator.rb @@ -0,0 +1,1370 @@ +# 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. +# + +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 = "rdoc-style.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 + 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 + 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 + 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 = HTMLGenerator.gen_url(@from_path, path) + end + end + + if (type == "http" || type == "link") && + url =~ /\.(gif|png|jpg|jpeg|bmp)$/ + + "" + else + "#{url.sub(%r{^\w+:/*}, '')}" + end + end + + # HEre's a hypedlink where the label is different to the URL + #