diff options
Diffstat (limited to 'lib')
68 files changed, 6229 insertions, 3212 deletions
diff --git a/lib/rdoc.rb b/lib/rdoc.rb index 7ce7b53a35..a453ee1039 100644 --- a/lib/rdoc.rb +++ b/lib/rdoc.rb @@ -18,14 +18,16 @@ $DEBUG_RDOC = nil # == Roadmap # # * If you want to use RDoc to create documentation for your Ruby source files, -# read on. +# read the summary below, and refer to <tt>rdoc --help</tt> for command line +# usage, and RDoc::Markup for a detailed description of RDoc's markup. # * If you want to generate documentation for extensions written in C, see # RDoc::Parser::C # * If you want to drive RDoc programmatically, see RDoc::RDoc. -# * If you want to use the library to format text blocks into HTML, have a look -# at RDoc::Markup. -# * If you want to try writing your own HTML output template, see -# RDoc::Generator::HTML +# * If you want to use the library to format text blocks into HTML, look at +# RDoc::Markup. +# * If you want to make an RDoc plugin such as a generator or directive +# handler see RDoc::RDoc. +# * If you want to try writing your own output generator see RDoc::Generator. # # == Summary # @@ -50,7 +52,7 @@ $DEBUG_RDOC = nil # index page contain the documentation for the primary file. In our # case, we could type # -# % rdoc --main rdoc.rb +# % rdoc --main README.txt # # You'll find information on the various formatting tricks you can use # in comment blocks in the documentation this generates. @@ -62,281 +64,9 @@ $DEBUG_RDOC = nil # markers). If directory names are passed to RDoc, they are scanned # recursively for C and Ruby source files only. # -# == \Options -# -# rdoc can be passed a variety of command-line options. In addition, -# options can be specified via the +RDOCOPT+ environment variable, which -# functions similarly to the +RUBYOPT+ environment variable. -# -# % export RDOCOPT="-S" -# -# will make rdoc default to inline method source code. Command-line options -# always will override those in +RDOCOPT+. -# -# Run: -# -# rdoc --help -# -# for full details on rdoc's options. -# -# == Documenting Source Code -# -# Comment blocks can be written fairly naturally, either using <tt>#</tt> on -# successive lines of the comment, or by including the comment in -# a =begin/=end block. If you use the latter form, the =begin line must be -# flagged with an RDoc tag: -# -# =begin rdoc -# Documentation to be processed by RDoc. -# -# ... -# =end -# -# RDoc stops processing comments if it finds a comment line containing -# a <tt>--</tt>. 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 with a -# <tt>++</tt>. -# -# ## -# # 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) -# # ... -# end -# -# Names of classes, files, and any method names containing an -# underscore or preceded by a hash character are automatically hyperlinked -# from comment text to their description. -# -# 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| ... } -# -# +:yields:+ is an example of a documentation directive. These appear -# immediately after the start of the document element they are modifying. -# -# RDoc automatically cross-references words with underscores or camel-case. -# To suppress cross-references, prefix the word with a \\ character. To -# include special characters like "\\n", you'll need to use two \\ -# characters like "\\\\\\n". -# -# == \Markup -# -# * The markup engine looks for a document's natural left margin. This is -# used as the initial margin for the document. -# -# * Consecutive lines starting at this margin are considered to be a -# paragraph. -# -# * If a paragraph starts with a "*", "-", or with "<digit>.", then it is -# taken to be the start of a list. The margin in increased to be the first -# non-space following the list start flag. Subsequent lines should be -# indented to this new margin until the list ends. For example: -# -# * this is a list with three paragraphs in -# the first item. This is the first paragraph. -# -# And this is the second paragraph. -# -# 1. This is an indented, numbered list. -# 2. This is the second item in that list -# -# This is the third conventional paragraph in the -# first list item. -# -# * This is the second item in the original list -# -# * You can also construct labeled lists, sometimes called description -# or definition lists. Do this by putting the label in square brackets -# and indenting the list body: -# -# [cat] a small furry mammal -# that seems to sleep a lot -# -# [ant] a little insect that is known -# to enjoy picnics -# -# A minor variation on labeled lists uses two colons to separate the -# label from the list body: -# -# cat:: a small furry mammal -# that seems to sleep a lot -# -# ant:: a little insect that is known -# to enjoy picnics -# -# This latter style guarantees that the list bodies' left margins are -# aligned: think of them as a two column table. -# -# * Any line that starts to the right of the current margin is treated -# as verbatim text. This is useful for code listings. The example of a -# list above is also verbatim text. -# -# * A line starting with an equals sign (=) is treated as a -# heading. Level one headings have one equals sign, level two headings -# have two,and so on. -# -# * A line starting with three or more hyphens (at the current indent) -# generates a horizontal rule. The more hyphens, the thicker the rule -# (within reason, and if supported by the output device) -# -# * You can use markup within text (except verbatim) to change the -# appearance of parts of that text. Out of the box, RDoc::Markup -# supports word-based and general markup. -# -# Word-based markup uses flag characters around individual words: -# -# [<tt>\*word*</tt>] displays word in a *bold* font -# [<tt>\_word_</tt>] displays word in an _emphasized_ font -# [<tt>\+word+</tt>] displays word in a +code+ font -# -# General markup affects text between a start delimiter and and end -# delimiter. Not surprisingly, these delimiters look like HTML markup. -# -# [<tt>\<b>text...</b></tt>] displays word in a *bold* font -# [<tt>\<em>text...</em></tt>] displays word in an _emphasized_ font -# [<tt>\<i>text...</i></tt>] displays word in an <i>italicized</i> font -# [<tt>\<tt>text...\</tt></tt>] displays word in a +code+ font -# -# Unlike conventional Wiki markup, general markup can cross line -# boundaries. You can turn off the interpretation of markup by -# preceding the first character with a backslash. This only works for -# simple markup, not HTML-style markup. -# -# * 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 \<IMG..>. 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 <tt>label</tt>[url], in which -# case the label is used in the displayed text, and +url+ is -# used as the target. If +label+ contains multiple words, -# put it in braces: <em>{multi word label}[</em>url<em>]</em>. -# -# Example hyperlinks: -# -# link:RDoc.html -# http://rdoc.rubyforge.org -# mailto:user@example.com -# {RDoc Documentation}[http://rdoc.rubyforge.org] -# {RDoc Markup}[link:RDoc/Markup.html] -# -# == Directives -# -# [+:nodoc:+ / +:nodoc:+ all] -# This directive prevents documentation for the element from -# being generated. For classes and modules, the methods, aliases, -# constants, and attributes directly within the affected class or -# module also will 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 MyModule # :nodoc: -# class Input -# end -# end -# -# module OtherModule # :nodoc: all -# class Output -# end -# end -# -# In the above code, only class <tt>MyModule::Input</tt> will be documented. -# The +:nodoc:+ directive is global across all files for the class or module -# to which it applies, so use +:stopdoc:+/+:startdoc:+ to suppress -# documentation only for a particular set of methods, etc. -# -# [+:doc:+] -# Forces a method or attribute to be documented even if it wouldn't be -# otherwise. 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 private, -# so you won't see the documentation unless you use the +-a+ command line -# option. -# -# Comment blocks can contain other directives: -# -# [<tt>:section: title</tt>] -# Starts a new section in the output. The title following +:section:+ is -# used as the section heading, and the remainder of the comment containing -# the section is used as introductory text. Subsequent methods, aliases, -# attributes, and classes will be documented in this section. A :section: -# comment block may have one or more lines before the :section: directive. -# These will be removed, and any identical lines at the end of the block are -# also removed. This allows you to add visual cues such as: -# -# # ---------------------------------------- -# # :section: My Section -# # This is the section that I wrote. -# # See it glisten in the noon-day sun. -# # ---------------------------------------- -# -# [+:call-seq:+] -# Lines up to the next blank line in the comment are treated as the method's -# calling sequence, overriding the default parsing of method parameters and -# yield arguments. -# -# [+: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 <tt>--title</tt> -# 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 <tt>--main</tt> 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 specify a +:startdoc:+ by the end of the container, -# disables documentation for the entire class or module. -# -# Further directives can be found in RDoc::Parser::Ruby and RDoc::Parser::C -# # == Other stuff # -# RDoc is currently being maintained by Eric Hodel <drbrain@segment7.net> +# RDoc is currently being maintained by Eric Hodel <drbrain@segment7.net>. # # Dave Thomas <dave@pragmaticprogrammer.com> is the original author of RDoc. # @@ -345,24 +75,6 @@ $DEBUG_RDOC = nil # * 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. -# -# * 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. -# -# == 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. module RDoc @@ -383,7 +95,12 @@ module RDoc ## # RDoc version you are using - VERSION = '2.5.8' + VERSION = '3.0' + + ## + # Method visibilities + + VISIBILITIES = [:public, :protected, :private] ## # Name of the dotfile that contains the description of files to be processed diff --git a/lib/rdoc/alias.rb b/lib/rdoc/alias.rb index 6cd9af09d4..fa433dc0a9 100644 --- a/lib/rdoc/alias.rb +++ b/lib/rdoc/alias.rb @@ -3,41 +3,77 @@ require 'rdoc/code_object' ## # Represent an alias, which is an old_name/new_name pair associated with a # particular context +#-- +# TODO implement Alias as a proxy to a method/attribute, inheriting from +# MethodAttr class RDoc::Alias < RDoc::CodeObject ## - # Allow comments to be overridden + # Aliased method's name - attr_writer :comment + attr_reader :new_name + + alias name new_name ## - # Aliased name + # Aliasee method's name - attr_accessor :new_name + attr_reader :old_name ## - # Aliasee's name + # Is this an alias declared in a singleton context? - attr_accessor :old_name + attr_accessor :singleton ## # Source file token stream - attr_accessor :text + attr_reader :text ## # Creates a new Alias with a token stream of +text+ that aliases +old_name+ - # to +new_name+ and has +comment+ + # to +new_name+, has +comment+ and is a +singleton+ context. - def initialize(text, old_name, new_name, comment) + def initialize(text, old_name, new_name, comment, singleton = false) super() + @text = text + @singleton = singleton @old_name = old_name @new_name = new_name self.comment = comment end + ## + # Order by #singleton then #new_name + + def <=>(other) + [@singleton ? 0 : 1, new_name] <=> [other.singleton ? 0 : 1, other.new_name] + end + + ## + # HTML fragment reference for this alias + + def aref + type = singleton ? 'c' : 'i' + "#alias-#{type}-#{html_name}" + end + + ## + # Full old name including namespace + + def full_old_name + @full_name || "#{parent.name}#{pretty_old_name}" + end + + ## + # HTML id-friendly version of +#new_name+. + + def html_name + CGI.escape(@new_name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '') + end + def inspect # :nodoc: parent_name = parent ? parent.name : '(unknown)' "#<%s:0x%x %s.alias_method %s, %s>" % [ @@ -46,8 +82,31 @@ class RDoc::Alias < RDoc::CodeObject ] end + ## + # '::' for the alias of a singleton method/attribute, '#' for instance-level. + + def name_prefix + singleton ? '::' : '#' + end + + ## + # Old name with prefix '::' or '#'. + + def pretty_old_name + "#{singleton ? '::' : '#'}#{@old_name}" + end + + ## + # New name with prefix '::' or '#'. + + def pretty_new_name + "#{singleton ? '::' : '#'}#{@new_name}" + end + + alias pretty_name pretty_new_name + def to_s # :nodoc: - "alias: #{self.old_name} -> #{self.new_name}\n#{self.comment}" + "alias: #{self.new_name} -> #{self.pretty_old_name} in: #{parent}" end end diff --git a/lib/rdoc/any_method.rb b/lib/rdoc/any_method.rb index d742daa7b8..f993621f8b 100644 --- a/lib/rdoc/any_method.rb +++ b/lib/rdoc/any_method.rb @@ -1,29 +1,12 @@ -require 'rdoc/code_object' -require 'rdoc/tokenstream' +require 'rdoc/method_attr' +require 'rdoc/token_stream' ## # AnyMethod is the base class for objects representing methods -class RDoc::AnyMethod < RDoc::CodeObject +class RDoc::AnyMethod < RDoc::MethodAttr - MARSHAL_VERSION = 1 # :nodoc: - - include Comparable - - ## - # Method name - - attr_writer :name - - ## - # public, protected, private - - attr_accessor :visibility - - ## - # Parameters yielded by the called block - - attr_accessor :block_params + MARSHAL_VERSION = 0 # :nodoc: ## # Don't rename \#initialize to \::new @@ -31,76 +14,49 @@ class RDoc::AnyMethod < RDoc::CodeObject attr_accessor :dont_rename_initialize ## - # Is this a singleton method? - - attr_accessor :singleton - - ## - # Source file token stream - - attr_reader :text - - ## - # Array of other names for this method - - attr_reader :aliases - - ## - # The method we're aliasing + # Different ways to call this method - attr_accessor :is_alias_for + attr_accessor :call_seq ## # Parameters for this method attr_accessor :params - ## - # Different ways to call this method - - attr_accessor :call_seq - include RDoc::TokenStream - def initialize(text, name) - super() + ## + # Creates a new AnyMethod with a token stream +text+ and +name+ - @text = text - @name = name + def initialize text, name + super - @aliases = [] - @block_params = nil - @call_seq = nil @dont_rename_initialize = false - @is_alias_for = nil - @params = nil - @parent_name = nil - @singleton = nil @token_stream = nil - @visibility = :public end ## - # Order by #singleton then #name + # Adds +an_alias+ as an alias for this method in +context+. - def <=>(other) - [@singleton ? 0 : 1, @name] <=> [other.singleton ? 0 : 1, other.name] - end - - ## - # Adds +method+ as an alias for this method + def add_alias(an_alias, context) + method = self.class.new an_alias.text, an_alias.new_name - def add_alias(method) + method.record_location an_alias.file + method.singleton = self.singleton + method.params = self.params + method.visibility = self.visibility + method.comment = an_alias.comment + method.is_alias_for = self @aliases << method + context.add_method method + method end ## - # HTML fragment reference for this method + # Prefix for +aref+ is 'method'. - def aref - type = singleton ? 'c' : 'i' - - "method-#{type}-#{CGI.escape name}" + def aref_prefix + 'method' end ## @@ -117,30 +73,6 @@ class RDoc::AnyMethod < RDoc::CodeObject end ## - # HTML id-friendly method name - - def html_name - @name.gsub(/[^a-z]+/, '-') - end - - def inspect # :nodoc: - alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil - "#<%s:0x%x %s (%s)%s>" % [ - self.class, object_id, - full_name, - visibility, - alias_for, - ] - end - - ## - # Full method name including namespace - - def full_name - @full_name ||= "#{@parent ? @parent.full_name : '(unknown)'}#{pretty_name}" - end - - ## # Dumps this AnyMethod for use by ri. See also #marshal_load def marshal_dump @@ -192,12 +124,14 @@ class RDoc::AnyMethod < RDoc::CodeObject end array[8].each do |new_name, comment| - add_alias RDoc::Alias.new(nil, @name, new_name, comment) + add_alias RDoc::Alias.new(nil, @name, new_name, comment, @singleton) end end ## # Method name + # + # If the method has no assigned name, it extracts it from #call_seq. def name return @name if @name @@ -229,62 +163,5 @@ class RDoc::AnyMethod < RDoc::CodeObject params end - ## - # Name of our parent with special handling for un-marshaled methods - - def parent_name - @parent_name || super - end - - ## - # Path to this method - - def path - "#{@parent.path}##{aref}" - end - - ## - # Method name with class/instance indicator - - def pretty_name - "#{singleton ? '::' : '#'}#{@name}" - end - - def pretty_print q # :nodoc: - alias_for = @is_alias_for ? "alias for #{@is_alias_for.name}" : nil - - q.group 2, "[#{self.class.name} #{full_name} #{visibility}", "]" do - if alias_for then - q.breakable - q.text alias_for - end - - if text then - q.breakable - q.text "text:" - q.breakable - q.pp @text - end - - unless comment.empty? then - q.breakable - q.text "comment:" - q.breakable - q.pp @comment - end - end - end - - def to_s # :nodoc: - "#{self.class.name}: #{full_name} (#{@text})\n#{@comment}" - end - - ## - # Type of method (class or instance) - - def type - singleton ? 'class' : 'instance' - end - end diff --git a/lib/rdoc/attr.rb b/lib/rdoc/attr.rb index 9b8c4562c2..97fac3222d 100644 --- a/lib/rdoc/attr.rb +++ b/lib/rdoc/attr.rb @@ -1,104 +1,71 @@ -require 'rdoc/code_object' +require 'rdoc/method_attr' ## # An attribute created by \#attr, \#attr_reader, \#attr_writer or # \#attr_accessor -class RDoc::Attr < RDoc::CodeObject +class RDoc::Attr < RDoc::MethodAttr - MARSHAL_VERSION = 0 # :nodoc: + MARSHAL_VERSION = 1 # :nodoc: ## - # Name of the attribute - - attr_accessor :name - - ## - # Is the attribute readable, writable or both? + # Is the attribute readable ('R'), writable ('W') or both ('RW')? attr_accessor :rw ## - # Source file token stream + # Creates a new Attr with body +text+, +name+, read/write status +rw+ and + # +comment+. +singleton+ marks this as a class attribute. - attr_accessor :text + def initialize(text, name, rw, comment, singleton = false) + super text, name - ## - # public, protected, private - - attr_accessor :visibility - - def initialize(text, name, rw, comment) - super() - @text = text - @name = name @rw = rw - @visibility = :public + @singleton = singleton self.comment = comment end ## - # Attributes are ordered by name - - def <=>(other) - self.name <=> other.name - end - - ## - # Attributes are equal when their names and rw is identical + # Attributes are equal when their names, singleton and rw are identical def == other self.class == other.class and self.name == other.name and - self.rw == other.rw + self.rw == other.rw and + self.singleton == other.singleton end ## - # Returns nil, for duck typing with RDoc::AnyMethod + # Add +an_alias+ as an attribute in +context+. - def arglists - end - - ## - # Returns nil, for duck typing with RDoc::AnyMethod + def add_alias(an_alias, context) + new_attr = self.class.new(self.text, an_alias.new_name, self.rw, + self.comment, self.singleton) - def block_params + new_attr.record_location an_alias.file + new_attr.visibility = self.visibility + new_attr.is_alias_for = self + @aliases << new_attr + context.add_attribute new_attr + new_attr end ## - # Returns nil, for duck typing with RDoc::AnyMethod + # The #aref prefix for attributes - def call_seq + def aref_prefix + 'attribute' end ## - # Partially bogus as Attr has no parent. For duck typing with - # RDoc::AnyMethod. + # Returns attr_reader, attr_writer or attr_accessor as appropriate. - def full_name - @full_name ||= "#{@parent ? @parent.full_name : '(unknown)'}##{name}" - end - - ## - # An HTML id-friendly representation of #name - - def html_name - @name.gsub(/[^a-z]+/, '-') - end - - def inspect # :nodoc: - attr = case rw - when 'RW' then :attr_accessor - when 'R' then :attr_reader - when 'W' then :attr_writer - else - " (#{rw})" - end - - "#<%s:0x%x %s.%s :%s>" % [ - self.class, object_id, - parent_name, attr, @name, - ] + def definition + case @rw + when 'RW' then 'attr_accessor' + when 'R' then 'attr_reader' + when 'W' then 'attr_writer' + end end ## @@ -111,11 +78,12 @@ class RDoc::Attr < RDoc::CodeObject @rw, @visibility, parse(@comment), + singleton, ] end ## - # Loads this AnyMethod from +array+. For a loaded AnyMethod the following + # Loads this Attr from +array+. For a loaded Attr the following # methods will return cached values: # # * #full_name @@ -127,51 +95,13 @@ class RDoc::Attr < RDoc::CodeObject @rw = array[3] @visibility = array[4] @comment = array[5] + @singleton = array[6] || false # MARSHAL_VERSION == 0 @parent_name = @full_name end - ## - # Name of our parent with special handling for un-marshaled methods - - def parent_name - @parent_name || super - end - - ## - # For duck typing with RDoc::AnyMethod, returns nil - - def params - nil - end - - ## - # URL path for this attribute - - def path - "#{@parent.path}##{@name}" - end - - ## - # For duck typing with RDoc::AnyMethod - - def singleton - false - end - def to_s # :nodoc: - "#{type} #{name}\n#{comment}" - end - - ## - # Returns attr_reader, attr_writer or attr_accessor as appropriate - - def type - case @rw - when 'RW' then 'attr_accessor' - when 'R' then 'attr_reader' - when 'W' then 'attr_writer' - end + "#{definition} #{name} in: #{parent}" end end diff --git a/lib/rdoc/class_module.rb b/lib/rdoc/class_module.rb index 16b85d7918..1e75699ffa 100644 --- a/lib/rdoc/class_module.rb +++ b/lib/rdoc/class_module.rb @@ -8,31 +8,114 @@ class RDoc::ClassModule < RDoc::Context MARSHAL_VERSION = 0 # :nodoc: - attr_accessor :diagram + ## + # Constants that are aliases for this class or module + + attr_accessor :constant_aliases + + attr_accessor :diagram # :nodoc: ## - # Creates a new ClassModule with +name+ with optional +superclass+ + # Class or module this constant is an alias for - def initialize(name, superclass = 'Object') - @diagram = nil - @full_name = nil - @name = name - @superclass = superclass + attr_accessor :is_alias_for + + ## + # Return a RDoc::ClassModule of class +class_type+ that is a copy + # of module +module+. Used to promote modules to classes. + + def self.from_module(class_type, mod) + klass = class_type.new(mod.name) + klass.comment = mod.comment + klass.parent = mod.parent + klass.section = mod.section + klass.viewer = mod.viewer + + klass.attributes.concat mod.attributes + klass.method_list.concat mod.method_list + klass.aliases.concat mod.aliases + klass.external_aliases.concat mod.external_aliases + klass.constants.concat mod.constants + klass.includes.concat mod.includes + + klass.methods_hash.update mod.methods_hash + klass.constants_hash.update mod.constants_hash + + klass.current_section = mod.current_section + klass.in_files.concat mod.in_files + klass.sections.concat mod.sections + klass.unmatched_alias_lists = mod.unmatched_alias_lists + klass.current_section = mod.current_section + klass.visibility = mod.visibility + + klass.classes_hash.update mod.classes_hash + klass.modules_hash.update mod.modules_hash + klass.metadata.update mod.metadata + + klass.document_self = mod.received_nodoc ? nil : mod.document_self + klass.document_children = mod.document_children + klass.force_documentation = mod.force_documentation + klass.done_documenting = mod.done_documenting + + # update the parent of all children + + (klass.attributes + + klass.method_list + + klass.aliases + + klass.external_aliases + + klass.constants + + klass.includes + + klass.classes + + klass.modules).each do |obj| + obj.parent = klass + obj.full_name = nil + end + + klass + end + + ## + # Creates a new ClassModule with +name+ with optional +superclass+ + # + # This is a constructor for subclasses, and must never be called directly. + + def initialize(name, superclass = nil) + @constant_aliases = [] + @diagram = nil + @is_alias_for = nil + @name = name + @superclass = superclass super() end ## - # Ancestors list for this ClassModule (abstract) + # Ancestors list for this ClassModule: the list of included modules + # (classes will add their superclass if any). + # + # Returns the included classes or modules, not the includes + # themselves. The returned values are either String or + # RDoc::NormalModule instances (see RDoc::Include#module). + # + # The values are returned in reverse order of their inclusion, + # which is the order suitable for searching methods/attributes + # in the ancestors. The superclass, if any, comes last. def ancestors - raise NotImplementedError + includes.map { |i| i.module }.reverse + end + + ## + # Clears the comment. Used by the ruby parser. + + def clear_comment + @comment = '' end ## # Appends +comment+ to the current comment, but separated by a rule. Works # more like <tt>+=</tt>. - def comment=(comment) + def comment= comment return if comment.empty? comment = "#{@comment}\n---\n#{normalize_comment comment}" unless @@ -42,9 +125,34 @@ class RDoc::ClassModule < RDoc::Context end ## + # Prepares this ClassModule for use by a generator. + # + # See RDoc::TopLevel::complete + + def complete min_visibility + update_aliases + remove_nodoc_children + update_includes + remove_invisible min_visibility + end + + ## + # Looks for a symbol in the #ancestors. See Context#find_local_symbol. + + def find_ancestor_local_symbol symbol + ancestors.each do |m| + next if m.is_a?(String) + res = m.find_local_symbol(symbol) + return res if res + end + + nil + end + + ## # Finds a class or module with +name+ in this namespace or its descendents - def find_class_named(name) + def find_class_named name return self if full_name == name return self if @name == name @@ -65,14 +173,8 @@ class RDoc::ClassModule < RDoc::Context end end - ## - # 'module' or 'class' - - def type - module? ? 'module' : 'class' - end - def marshal_dump # :nodoc: + # TODO must store the singleton attribute attrs = attributes.sort.map do |attr| [attr.name, attr.rw] end @@ -106,10 +208,13 @@ class RDoc::ClassModule < RDoc::Context end def marshal_load array # :nodoc: + # TODO must restore the singleton attribute initialize_methods_etc @document_self = true @done_documenting = false @current_section = nil + @parent = nil + @visibility = nil @name = array[1] @full_name = array[2] @@ -184,6 +289,15 @@ class RDoc::ClassModule < RDoc::Context end ## + # Allows overriding the initial name. + # + # Used for modules and classes that are constant aliases. + + def name= new_name + @name = new_name + end + + ## # Path to this class or module def path @@ -191,11 +305,51 @@ class RDoc::ClassModule < RDoc::Context end ## + # Name to use to generate the url: + # modules and classes that are aliases for another + # module or classe return the name of the latter. + + def name_for_path + is_alias_for ? is_alias_for.full_name : full_name + end + + ## + # Returns the classes and modules that are not constants + # aliasing another class or module. For use by formatters + # only (caches its result). + + def non_aliases + @non_aliases ||= classes_and_modules.reject { |cm| cm.is_alias_for } + end + + ## + # Updates the child modules or classes of class/module +parent+ by + # deleting the ones that have been removed from the documentation. + # + # +parent_hash+ is either <tt>parent.modules_hash</tt> or + # <tt>parent.classes_hash</tt> and +all_hash+ is ::all_modules_hash or + # ::all_classes_hash. + + def remove_nodoc_children + prefix = self.full_name + '::' + + modules_hash.each_key do |name| + full_name = prefix + name + modules_hash.delete name unless RDoc::TopLevel.all_modules_hash[full_name] + end + + classes_hash.each_key do |name| + full_name = prefix + name + classes_hash.delete name unless RDoc::TopLevel.all_classes_hash[full_name] + end + end + + ## # Get the superclass of this class. Attempts to retrieve the superclass # object, returns the name if it is not known. def superclass - RDoc::TopLevel.find_class_named_from(@superclass, parent) || @superclass + RDoc::TopLevel.find_class_named(@superclass) || @superclass end ## @@ -203,12 +357,72 @@ class RDoc::ClassModule < RDoc::Context def superclass=(superclass) raise NoMethodError, "#{full_name} is a module" if module? - - @superclass = superclass if @superclass.nil? or @superclass == 'Object' + @superclass = superclass end def to_s # :nodoc: - "#{self.class}: #{full_name} #{@comment} #{super}" + if is_alias_for then + "#{self.class.name} #{self.full_name} -> #{is_alias_for}" + else + super + end + end + + ## + # 'module' or 'class' + + def type + module? ? 'module' : 'class' + end + + ## + # Updates the child modules & classes by replacing the ones that are + # aliases through a constant. + # + # The aliased module/class is replaced in the children and in + # RDoc::TopLevel::all_modules_hash or RDoc::TopLevel::all_classes_hash + # by a copy that has <tt>RDoc::ClassModule#is_alias_for</tt> set to + # the aliased module/class, and this copy is added to <tt>#aliases</tt> + # of the aliased module/class. + # + # Formatters can use the #non_aliases method to retrieve children that + # are not aliases, for instance to list the namespace content, since + # the aliased modules are included in the constants of the class/module, + # that are listed separately. + + def update_aliases + constants.each do |const| + next unless cm = const.is_alias_for + cm_alias = cm.dup + cm_alias.name = const.name + cm_alias.parent = self + cm_alias.full_name = nil # force update for new parent + cm_alias.aliases.clear + cm_alias.is_alias_for = cm + + if cm.module? then + RDoc::TopLevel.all_modules_hash[cm_alias.full_name] = cm_alias + modules_hash[const.name] = cm_alias + else + RDoc::TopLevel.all_classes_hash[cm_alias.full_name] = cm_alias + classes_hash[const.name] = cm_alias + end + + cm.aliases << cm_alias + end + end + + ## + # Deletes from #includes those whose module has been removed from the + # documentation. + #-- + # FIXME: includes are not reliably removed, see _possible_bug test case + + def update_includes + includes.reject! do |include| + mod = include.module + !(String === mod) && RDoc::TopLevel.all_modules_hash[mod.full_name].nil? + end end end diff --git a/lib/rdoc/code_object.rb b/lib/rdoc/code_object.rb index f8c4e33f7e..02b2f2fcf6 100644 --- a/lib/rdoc/code_object.rb +++ b/lib/rdoc/code_object.rb @@ -12,15 +12,16 @@ require 'rdoc/text' # * RDoc::Context # * RDoc::TopLevel # * RDoc::ClassModule -# * RDoc::AnonClass +# * RDoc::AnonClass (never used so far) # * RDoc::NormalClass # * RDoc::NormalModule # * RDoc::SingleClass -# * RDoc::AnyMethod -# * RDoc::GhostMethod -# * RDoc::MetaMethod +# * RDoc::MethodAttr +# * RDoc::Attr +# * RDoc::AnyMethod +# * RDoc::GhostMethod +# * RDoc::MetaMethod # * RDoc::Alias -# * RDoc::Attr # * RDoc::Constant # * RDoc::Require # * RDoc::Include @@ -47,12 +48,17 @@ class RDoc::CodeObject ## # Are we done documenting (ie, did we come across a :enddoc:)? - attr_accessor :done_documenting + attr_reader :done_documenting + + ## + # Which file this code object was defined in + + attr_reader :file ## # Force documentation of this CodeObject - attr_accessor :force_documentation + attr_reader :force_documentation ## # Hash of arbitrary metadata for this CodeObject @@ -65,6 +71,11 @@ class RDoc::CodeObject attr_accessor :parent ## + # Did we ever receive a +:nodoc:+ directive? + + attr_reader :received_nodoc + + ## # Which section are we in attr_accessor :section @@ -80,15 +91,17 @@ class RDoc::CodeObject # Creates a new CodeObject that will document itself and its children def initialize - @metadata = {} - @comment = '' + @metadata = {} + @comment = '' + @parent = nil + @file = nil + @full_name = nil @document_children = true @document_self = true @done_documenting = false @force_documentation = false - - @parent = nil + @received_nodoc = false end ## @@ -108,28 +121,64 @@ class RDoc::CodeObject end ## - # Enables or disables documentation of this CodeObject's children. Calls - # remove_classes_and_modules when disabling. + # Enables or disables documentation of this CodeObject's children unless it + # has been turned off by :enddoc: def document_children=(document_children) - @document_children = document_children - remove_classes_and_modules unless document_children + @document_children = document_children unless @done_documenting end ## - # Enables or disables documentation of this CodeObject. Calls - # remove_methods_etc when disabling. + # Enables or disables documentation of this CodeObject unless it has been + # turned off by :enddoc:. If the argument is +nil+ it means the + # documentation is turned off by +:nodoc:+. def document_self=(document_self) + return if @done_documenting + @document_self = document_self - remove_methods_etc unless document_self + @received_nodoc = true if document_self.nil? end ## - # Does this class have a comment with content or is document_self false. + # Does this object have a comment with content or is #received_nodoc true? def documented? - !(@document_self and @comment.empty?) + @received_nodoc or !@comment.empty? + end + + ## + # Turns documentation on/off, and turns on/off #document_self + # and #document_children. + # + # Once documentation has been turned off (by +:enddoc:+), + # the object will refuse to turn #document_self or + # #document_children on, so +:doc:+ and +:start_doc:+ directives + # will have no effect in the current file. + + def done_documenting=(value) + @done_documenting = value + @document_self = !value + @document_children = @document_self + end + + ## + # Force the documentation of this object unless documentation + # has been turned off by :endoc: + #-- + # HACK untested, was assigning to an ivar + + def force_documentation=(value) + @force_documentation = value unless @done_documenting + end + + ## + # Sets the full_name overriding any computed full name. + # + # Set to +nil+ to clear RDoc's cached value + + def full_name= full_name + @full_name = full_name end ## @@ -147,23 +196,19 @@ class RDoc::CodeObject end ## - # Callback called upon disabling documentation of children. See - # #document_children= - - def remove_classes_and_modules - end - - ## - # Callback called upon disabling documentation of ourself. See - # #document_self= + # Records the RDoc::TopLevel (file) where this code object was defined - def remove_methods_etc + def record_location top_level + @file = top_level end ## - # Enable capture of documentation + # Enable capture of documentation unless documentation has been + # turned off by :endoc: def start_doc + return if @done_documenting + @document_self = true @document_children = true end diff --git a/lib/rdoc/constant.rb b/lib/rdoc/constant.rb index 908990855d..056ce130be 100644 --- a/lib/rdoc/constant.rb +++ b/lib/rdoc/constant.rb @@ -6,6 +6,13 @@ require 'rdoc/code_object' class RDoc::Constant < RDoc::CodeObject ## + # If this constant is an alias for a module or class, + # this is the RDoc::ClassModule it is an alias for. + # +nil+ otherwise. + + attr_accessor :is_alias_for + + ## # The constant's name attr_accessor :name @@ -22,6 +29,7 @@ class RDoc::Constant < RDoc::CodeObject super() @name = name @value = value + @is_alias_for = nil self.comment = comment end @@ -34,17 +42,28 @@ class RDoc::Constant < RDoc::CodeObject [parent_name, name] <=> [other.parent_name, other.name] end + ## + # Constants are equal when their #parent and #name is the same + def == other self.class == other.class and @parent == other.parent and @name == other.name end + ## + # A constant is documented if it has a comment, or is an alias + # for a documented class or module. + + def documented? + super or is_alias_for && is_alias_for.documented? + end + def inspect # :nodoc: - "#<%s:0x%x %s::%s>" % [ - self.class, object_id, - parent_name, @name, - ] + "#<%s:0x%x %s::%s>" % [ + self.class, object_id, + parent_name, @name, + ] end ## @@ -54,5 +73,14 @@ class RDoc::Constant < RDoc::CodeObject "#{@parent.path}##{@name}" end + def to_s # :nodoc: + parent_name = parent ? parent.full_name : '(unknown)' + if is_alias_for + "constant #{parent_name}::#@name -> #{is_alias_for}" + else + "constant #{parent_name}::#@name" + end + end + end diff --git a/lib/rdoc/context.rb b/lib/rdoc/context.rb index d55c5a9164..c424ef1676 100644 --- a/lib/rdoc/context.rb +++ b/lib/rdoc/context.rb @@ -15,17 +15,12 @@ class RDoc::Context < RDoc::CodeObject TYPES = %w[class instance] ## - # Method visibilities - - VISIBILITIES = [:public, :protected, :private] - - ## - # Aliased methods + # Class/module aliases attr_reader :aliases ## - # attr* methods + # All attr* methods attr_reader :attributes @@ -37,7 +32,7 @@ class RDoc::Context < RDoc::CodeObject ## # Current section of documentation - attr_reader :current_section + attr_accessor :current_section ## # Files this context is found in @@ -70,19 +65,37 @@ class RDoc::Context < RDoc::CodeObject attr_reader :sections ## - # Aliases that haven't been resolved to a method + # Hash <tt>old_name => [aliases]</tt>, for aliases + # that haven't (yet) been resolved to a method/attribute. + # (Not to be confused with the aliases of the context.) attr_accessor :unmatched_alias_lists ## + # Aliases that could not eventually be resolved. + + attr_reader :external_aliases + + ## # Current visibility of this context - attr_reader :visibility + attr_accessor :visibility + + ## + # Hash of registered methods. Attributes are also registered here, + # twice if they are RW. + + attr_reader :methods_hash + + ## + # Hash of registered constants. + + attr_reader :constants_hash ## # A per-comment section of documentation like: # - # # :SECTION: The title + # # :section: The title # # The body class Section @@ -137,14 +150,12 @@ class RDoc::Context < RDoc::CodeObject end ## - # Set the comment for this section from the original comment block If + # Set the comment for this section from the original comment block. If # the first line contains :section:, strip it and use the rest. # Otherwise remove lines up to the line containing :section:, and look # for those lines again at the end and remove them. This lets us write # - # # blah blah blah - # # - # # :SECTION: The title + # # :section: The title # # The body def set_comment(comment) @@ -169,7 +180,7 @@ class RDoc::Context < RDoc::CodeObject end ## - # Creates an unnamed empty context with public visibility + # Creates an unnamed empty context with public current visibility def initialize super @@ -184,16 +195,10 @@ class RDoc::Context < RDoc::CodeObject @current_section = Section.new self, nil, nil @sections = [@current_section] - initialize_methods_etc - initialize_classes_and_modules - end - - ## - # Sets the defaults for classes and modules - - def initialize_classes_and_modules @classes = {} @modules = {} + + initialize_methods_etc end ## @@ -206,10 +211,14 @@ class RDoc::Context < RDoc::CodeObject @requires = [] @includes = [] @constants = [] + @external_aliases = [] # This Hash maps a method name to a list of unmatched aliases (aliases of # a method not yet encountered). @unmatched_alias_lists = {} + + @methods_hash = {} + @constants_hash = {} end ## @@ -222,14 +231,18 @@ class RDoc::Context < RDoc::CodeObject ## # Adds +an_alias+ that is automatically resolved - def add_alias(an_alias) - meth = find_instance_method_named(an_alias.old_name) + def add_alias an_alias + return an_alias unless @document_self + + method_attr = find_method(an_alias.old_name, an_alias.singleton) || + find_attribute(an_alias.old_name, an_alias.singleton) - if meth then - add_alias_impl an_alias, meth + if method_attr then + method_attr.add_alias an_alias, self else - add_to @aliases, an_alias - unmatched_alias_list = @unmatched_alias_lists[an_alias.old_name] ||= [] + add_to @external_aliases, an_alias + unmatched_alias_list = + @unmatched_alias_lists[an_alias.pretty_old_name] ||= [] unmatched_alias_list.push an_alias end @@ -237,175 +250,279 @@ class RDoc::Context < RDoc::CodeObject end ## - # Turns +an_alias+ into an AnyMethod that points to +meth+ - - def add_alias_impl(an_alias, meth) - new_meth = RDoc::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 = an_alias.comment - - meth.add_alias new_meth - - add_method new_meth - - # aliases don't use ongoing visibility - new_meth.visibility = meth.visibility - - new_meth - end + # Adds +attribute+ if not already there. If it is (as method(s) or attribute), + # updates the comment if it was empty. + # + # The attribute is registered only if it defines a new method. + # For instance, <tt>attr_reader :foo</tt> will not be registered + # if method +foo+ exists, but <tt>attr_accessor :foo</tt> will be registered + # if method +foo+ exists, but <tt>foo=</tt> does not. + + def add_attribute attribute + return attribute unless @document_self + + # mainly to check for redefinition of an attribute as a method + # TODO find a policy for 'attr_reader :foo' + 'def foo=()' + register = false + + if attribute.rw.index('R') then + key = attribute.pretty_name + known = @methods_hash[key] + if known then + known.comment = attribute.comment if known.comment.empty? + else + @methods_hash[key] = attribute + register = true + end + end - ## - # Adds +attribute+ + if attribute.rw.index('W') + key = attribute.pretty_name << '=' + known = @methods_hash[key] + if known then + known.comment = attribute.comment if known.comment.empty? + else + @methods_hash[key] = attribute + register = true + end + end - def add_attribute(attribute) - add_to @attributes, attribute + if register then + attribute.visibility = @visibility + add_to @attributes, attribute + resolve_aliases attribute + end end ## - # Adds a class named +name+ with +superclass+. + # Adds a class named +given_name+ with +superclass+. + # + # Both +given_name+ and +superclass+ may contain '::', and are + # interpreted relative to the +self+ context. This allows handling correctly + # examples like these: + # class RDoc::Gauntlet < Gauntlet + # module Mod + # class Object # implies < ::Object + # class SubObject < Object # this is _not_ ::Object # # Given <tt>class Container::Item</tt> RDoc assumes +Container+ is a module - # unless it later sees <tt>class Container</tt>. add_class automatically - # upgrades +name+ to a class in this case. + # unless it later sees <tt>class Container</tt>. +add_class+ automatically + # upgrades +given_name+ to a class in this case. + + def add_class class_type, given_name, superclass = '::Object' + # superclass +nil+ is passed by the C parser in the following cases: + # - registering Object in 1.8 (correct) + # - registering BasicObject in 1.9 (correct) + # - registering RubyVM in 1.9 in iseq.c (incorrect: < Object in vm.c) + # + # If we later find a superclass for a registered class with a nil + # superclass, we must honor it. + + # find the name & enclosing context + if given_name =~ /^:+(\w+)$/ then + full_name = $1 + enclosing = top_level + name = full_name.split(/:+/).last + else + full_name = child_name given_name + + if full_name =~ /^(.+)::(\w+)$/ then + name = $2 + ename = $1 + enclosing = RDoc::TopLevel.classes_hash[ename] || + RDoc::TopLevel.modules_hash[ename] + # HACK: crashes in actionpack/lib/action_view/helpers/form_helper.rb (metaprogramming) + unless enclosing then + # try the given name at top level (will work for the above example) + enclosing = RDoc::TopLevel.classes_hash[given_name] || RDoc::TopLevel.modules_hash[given_name] + return enclosing if enclosing + # not found: create the parent(s) + names = ename.split('::') + enclosing = self + names.each do |n| + enclosing = enclosing.classes_hash[n] || + enclosing.modules_hash[n] || + enclosing.add_module(RDoc::NormalModule, n) + end + end + else + name = full_name + enclosing = self + end + end + + # find the superclass full name + if superclass then + if superclass =~ /^:+/ then + superclass = $' #' + else + if superclass =~ /^(\w+):+(.+)$/ then + suffix = $2 + mod = find_module_named($1) + superclass = mod.full_name + '::' + suffix if mod + else + mod = find_module_named(superclass) + superclass = mod.full_name if mod + end + end - def add_class(class_type, name, superclass = 'Object') - klass = add_class_or_module @classes, class_type, name, superclass + # did we believe it was a module? + mod = RDoc::TopLevel.modules_hash.delete superclass - existing = klass.superclass - existing = existing.name if existing and not String === existing + upgrade_to_class mod, RDoc::NormalClass, mod.parent if mod - if superclass != existing and superclass != 'Object' then - klass.superclass = superclass + # e.g., Object < Object + superclass = nil if superclass == full_name end - # If the parser encounters Container::Item before encountering - # Container, then it assumes that Container is a module. This may not - # be the case, so remove Container from the module list if present and - # transfer any contained classes and modules to the new class. + klass = RDoc::TopLevel.classes_hash[full_name] + + if klass then + # if TopLevel, it may not be registered in the classes: + enclosing.classes_hash[name] = klass + # update the superclass if needed + if superclass then + existing = klass.superclass + existing = existing.full_name unless existing.is_a?(String) if existing + if existing.nil? || + (existing == 'Object' && superclass != 'Object') then + klass.superclass = superclass + end + end + else + # this is a new class + mod = RDoc::TopLevel.modules_hash.delete full_name - mod = RDoc::TopLevel.modules_hash.delete klass.full_name + if mod then + klass = upgrade_to_class mod, RDoc::NormalClass, enclosing - if mod then - klass.classes_hash.update mod.classes_hash - klass.modules_hash.update mod.modules_hash - klass.method_list.concat mod.method_list + klass.superclass = superclass unless superclass.nil? + else + klass = class_type.new name, superclass - @modules.delete klass.name + enclosing.add_class_or_module(klass, enclosing.classes_hash, + RDoc::TopLevel.classes_hash) + end end - RDoc::TopLevel.classes_hash[klass.full_name] = klass - klass end ## - # Instantiates a +class_type+ named +name+ and adds it the modules or - # classes Hash +collection+. - - def add_class_or_module(collection, class_type, name, superclass = nil) - full_name = child_name name - - mod = collection[name] - - if mod then - mod.superclass = superclass unless mod.module? - else - all = if class_type == RDoc::NormalModule then - RDoc::TopLevel.modules_hash - else - RDoc::TopLevel.classes_hash - end - - mod = all[full_name] - - unless mod then - mod = class_type.new name, superclass - else - # If the class has been encountered already, check that its - # superclass has been set (it may not have been, depending on the - # context in which it was encountered). - if class_type == RDoc::NormalClass then - mod.superclass = superclass unless mod.superclass - end - end + # Adds the class or module +mod+ to the modules or + # classes Hash +self_hash+, and to +all_hash+ (either + # <tt>TopLevel::modules_hash</tt> or <tt>TopLevel::classes_hash</tt>), + # unless #done_documenting is +true+. Sets the #parent of +mod+ + # to +self+, and its #section to #current_section. Returns +mod+. - unless @done_documenting then - all[full_name] = mod - collection[name] = mod - end + def add_class_or_module mod, self_hash, all_hash + mod.section = @current_section # TODO declaring context? something is + # wrong here... + mod.parent = self - mod.section = @current_section - mod.parent = self + unless @done_documenting then + self_hash[mod.name] = mod + # this must be done AFTER adding mod to its parent, so that the full + # name is correct: + all_hash[mod.full_name] = mod end mod end ## - # Adds +constant+ + # Adds +constant+ if not already there. If it is, updates the comment, + # value and/or is_alias_for of the known constant if they were empty/nil. def add_constant(constant) - add_to @constants, constant + return constant unless @document_self + + # HACK: avoid duplicate 'PI' & 'E' in math.c (1.8.7 source code) + # (this is a #ifdef: should be handled by the C parser) + known = @constants_hash[constant.name] + if known + #$stderr.puts "\nconstant #{constant.name} already registered" + known.comment = constant.comment if known.comment.empty? + known.value = constant.value if known.value.nil? or known.value.strip.empty? + known.is_alias_for ||= constant.is_alias_for + else + @constants_hash[constant.name] = constant + add_to @constants, constant + end end ## - # Adds included module +include+ + # Adds included module +include+ which should be an RDoc::Include def add_include(include) add_to @includes, include end ## - # Adds +method+ + # Adds +method+ if not already there. If it is (as method or attribute), + # updates the comment if it was empty. def add_method(method) - method.visibility = @visibility - add_to @method_list, method - - unmatched_alias_list = @unmatched_alias_lists[method.name] - if unmatched_alias_list then - unmatched_alias_list.each do |unmatched_alias| - add_alias_impl unmatched_alias, method - @aliases.delete unmatched_alias - end - - @unmatched_alias_lists.delete method.name + return method unless @document_self + + # HACK: avoid duplicate 'new' in io.c & struct.c (1.8.7 source code) + key = method.pretty_name + known = @methods_hash[key] + if known + # TODO issue stderr messages if --verbose + #$stderr.puts "\n#{display(method)} already registered as #{display(known)}" + known.comment = method.comment if known.comment.empty? + else + @methods_hash[key] = method + method.visibility = @visibility + add_to @method_list, method + resolve_aliases method end end ## # Adds a module named +name+. If RDoc already knows +name+ is a class then - # that class is returned instead. See also #add_class + # that class is returned instead. See also #add_class. def add_module(class_type, name) - return @classes[name] if @classes.key? name + mod = @classes[name] || @modules[name] + return mod if mod + + full_name = child_name name + mod = RDoc::TopLevel.modules_hash[full_name] || class_type.new(name) - add_class_or_module @modules, class_type, name, nil + add_class_or_module(mod, @modules, RDoc::TopLevel.modules_hash) end ## - # Adds an alias from +from+ to +name+ + # Adds an alias from +from+ (a class or module) to +name+. def add_module_alias from, name - to_name = child_name name + return from if @done_documenting - unless @done_documenting then - if from.module? then - RDoc::TopLevel.modules_hash - else - RDoc::TopLevel.classes_hash - end[to_name] = from + to_name = child_name(name) - if from.module? then - @modules - else - @classes - end[name] = from + # if we already know this name, don't register an alias: + # see the metaprogramming in lib/active_support/basic_object.rb, + # where we already know BasicObject as a class when we find + # BasicObject = BlankSlate + return from if RDoc::TopLevel.find_class_or_module(to_name) + + if from.module? then + RDoc::TopLevel.modules_hash[to_name] = from + @modules[name] = from + else + RDoc::TopLevel.classes_hash[to_name] = from + @classes[name] = from end + # HACK: register a constant for this alias: + # constant value and comment will be updated after, + # when the Ruby parser adds the constant + const = RDoc::Constant.new(name, nil, '') + const.is_alias_for = from + add_constant const + from end @@ -413,6 +530,8 @@ class RDoc::Context < RDoc::CodeObject # Adds +require+ to this context's top level def add_require(require) + return require unless @document_self + if RDoc::TopLevel === self then add_to @requires, require else @@ -424,16 +543,37 @@ class RDoc::Context < RDoc::CodeObject # Adds +thing+ to the collection +array+ def add_to(array, thing) - array << thing if @document_self and not @done_documenting + array << thing if @document_self thing.parent = self thing.section = @current_section end ## + # Is there any content? + # This means any of: comment, aliases, methods, attributes, + # external aliases, require, constant. + # Includes are also checked unless <tt>includes == false</tt>. + + def any_content(includes = true) + @any_content ||= !( + @comment.empty? && + @method_list.empty? && + @attributes.empty? && + @aliases.empty? && + @external_aliases.empty? && + @requires.empty? && + @constants.empty? + ) + @any_content || (includes && !@includes.empty?) + end + + ## # Creates the full name for a child with +name+ def child_name name - if RDoc::TopLevel === self then + if name =~ /^:+/ + $' #' + elsif RDoc::TopLevel === self then name else "#{self.full_name}::#{name}" @@ -441,6 +581,20 @@ class RDoc::Context < RDoc::CodeObject end ## + # Class attributes + + def class_attributes + @class_attributes ||= attributes.select { |a| a.singleton } + end + + ## + # Class methods + + def class_method_list + @class_method_list ||= method_list.select { |a| a.singleton } + end + + ## # Array of classes in this context def classes @@ -468,6 +622,14 @@ class RDoc::Context < RDoc::CodeObject @in_files.include?(file) end + def display(method_attr) # :nodoc: + if method_attr.is_a? RDoc::Attr + "#{method_attr.definition} #{method_attr.pretty_name}" + else + "method #{method_attr.pretty_name}" + end + end + ## # Iterator for attributes @@ -504,10 +666,25 @@ class RDoc::Context < RDoc::CodeObject end ## + # Finds an attribute +name+ with singleton value +singleton+. + + def find_attribute(name, singleton) + name = $1 if name =~ /^(.*)=$/ + @attributes.find { |a| a.name == name && a.singleton == singleton } + end + + ## # Finds an attribute with +name+ in this context def find_attribute_named(name) - @attributes.find { |m| m.name == name } + case name + when /\A#/ then + find_attribute name[1..-1], false + when /\A::/ then + find_attribute name[2..-1], true + else + @attributes.find { |a| a.name == name } + end end ## @@ -532,6 +709,27 @@ class RDoc::Context < RDoc::CodeObject end ## + # Finds an external alias +name+ with singleton value +singleton+. + + def find_external_alias(name, singleton) + @external_aliases.find { |m| m.name == name && m.singleton == singleton } + end + + ## + # Finds an external alias with +name+ in this context + + def find_external_alias_named(name) + case name + when /\A#/ then + find_external_alias name[1..-1], false + when /\A::/ then + find_external_alias name[2..-1], true + else + @external_aliases.find { |a| a.name == name } + end + end + + ## # Finds a file with +name+ in this context def find_file_named(name) @@ -546,26 +744,34 @@ class RDoc::Context < RDoc::CodeObject end ## - # Finds a method, constant, attribute, module or files named +symbol+ in - # this context + # Finds a method, constant, attribute, external alias, module or file + # named +symbol+ in this context. def find_local_symbol(symbol) find_method_named(symbol) or find_constant_named(symbol) or find_attribute_named(symbol) or + find_external_alias_named(symbol) or find_module_named(symbol) or find_file_named(symbol) end ## + # Finds a method named +name+ with singleton value +singleton+. + + def find_method(name, singleton) + @method_list.find { |m| m.name == name && m.singleton == singleton } + end + + ## # Finds a instance or module method with +name+ in this context def find_method_named(name) case name when /\A#/ then - find_instance_method_named name[1..-1] + find_method name[1..-1], false when /\A::/ then - find_class_method_named name[2..-1] + find_method name[2..-1], true else @method_list.find { |meth| meth.name == name } end @@ -582,51 +788,42 @@ class RDoc::Context < RDoc::CodeObject end ## - # Look up +symbol+. If +method+ is non-nil, then we assume the symbol - # references a module that contains that method. + # Look up +symbol+, first as a module, then as a local symbol. + + def find_symbol(symbol) + find_symbol_module(symbol) || find_local_symbol(symbol) + end + + ## + # Look up a module named +symbol+. - def find_symbol(symbol, method = nil) + def find_symbol_module(symbol) result = nil + # look for a class or module 'symbol' case symbol - when /^::([A-Z].*)/ then - result = top_level.find_symbol($1) - when /::/ then - modules = symbol.split(/::/) - - unless modules.empty? then - module_name = modules.shift - result = find_module_named(module_name) - - if result then - modules.each do |name| - result = result.find_module_named name - break unless result - end - end + when /^::/ then + result = RDoc::TopLevel.find_class_or_module(symbol) + when /^(\w+):+(.+)$/ + suffix = $2 + top = $1 + searched = self + loop do + mod = searched.find_module_named(top) + break unless mod + result = RDoc::TopLevel.find_class_or_module(mod.full_name + '::' + suffix) + break if result || searched.is_a?(RDoc::TopLevel) + searched = searched.parent end - end - - unless result then - # if a method is specified, then we're definitely looking for - # a module, otherwise it could be any symbol - if method then - result = find_module_named symbol - else - result = find_local_symbol symbol - if result.nil? then - if symbol =~ /^[A-Z]/ then - result = parent - while result && result.name != symbol do - result = result.parent - end - end - end + else + searched = self + loop do + result = searched.find_module_named(symbol) + break if result || searched.is_a?(RDoc::TopLevel) + searched = searched.parent end end - result = result.find_local_symbol method if result and method - result end @@ -638,10 +835,22 @@ class RDoc::Context < RDoc::CodeObject end ## + # Does this context and its methods and constants all have documentation? + # + # (Yes, fully documented doesn't mean everything.) + + def fully_documented? + documented? and + attributes.all? { |a| a.documented? } and + method_list.all? { |m| m.documented? } and + constants.all? { |c| c.documented? } + end + + ## # URL for this with a +prefix+ def http_url(prefix) - path = full_name + path = name_for_path path = path.gsub(/<<\s*(\w*)/, 'from-\1') if path =~ /<</ path = [prefix] + path.split('::') @@ -649,15 +858,29 @@ class RDoc::Context < RDoc::CodeObject end ## + # Instance attributes + + def instance_attributes + @instance_attributes ||= attributes.reject { |a| a.singleton } + end + + ## + # Instance methods + + def instance_method_list + @instance_method_list ||= method_list.reject { |a| a.singleton } + end + + ## # Breaks method_list into a nested hash by type (class or instance) and - # visibility (public, protected private) + # visibility (public, protected, private) def methods_by_type methods = {} TYPES.each do |type| visibilities = {} - VISIBILITIES.each do |vis| + RDoc::VISIBILITIES.each do |vis| visibilities[vis] = [] end @@ -672,23 +895,11 @@ class RDoc::Context < RDoc::CodeObject end ## - # Yields Method and Attr entries matching the list of names in +methods+. - # Attributes are only returned when +singleton+ is false. + # Yields AnyMethod and Attr entries matching the list of names in +methods+. def methods_matching(methods, singleton = false) - count = 0 - - @method_list.each do |m| - if methods.include? m.name and m.singleton == singleton then - yield m - count += 1 - end - end - - return if count == methods.size || singleton - - @attributes.each do |a| - yield a if methods.include? a.name + (@method_list + @attributes).each do |m| + yield m if methods.include?(m.name) and m.singleton == singleton end end @@ -707,6 +918,14 @@ class RDoc::Context < RDoc::CodeObject end ## + # Name to use to generate the url. + # <tt>#full_name</tt> by default. + + def name_for_path + full_name + end + + ## # Changes the visibility for new methods to +visibility+ def ongoing_visibility=(visibility) @@ -714,35 +933,63 @@ class RDoc::Context < RDoc::CodeObject end ## - # Record which file +top_level+ is in + # Record +top_level+ as a file +self+ is in. def record_location(top_level) @in_files << top_level unless @in_files.include?(top_level) 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 + # Should we remove this context from the documentation? + # + # The answer is yes if: + # * #received_nodoc is +true+ + # * #any_content is +false+ (not counting includes) + # * All #includes are modules (not a string), and their module has + # <tt>#remove_from_documentation? == true</tt> + # * All classes and modules have <tt>#remove_from_documentation? == true</tt> + + def remove_from_documentation? + @remove_from_documentation ||= + @received_nodoc && + !any_content(false) && + @includes.all? { |i| !i.module.is_a?(String) && i.module.remove_from_documentation? } && + classes_and_modules.all? { |cm| cm.remove_from_documentation? } end ## - # Given an array +methods+ of method names, set the visibility of each to - # +visibility+ + # Removes methods and attributes with a visibility less than +min_visibility+. + #-- + # TODO mark the visibility of attributes in the template (if not public?) - def set_visibility_for(methods, visibility, singleton = false) - methods_matching methods, singleton do |m| - m.visibility = visibility + def remove_invisible(min_visibility) + return if min_visibility == :private + remove_invisible_in @method_list, min_visibility + remove_invisible_in @attributes, min_visibility + end + + def remove_invisible_in(array, min_visibility) # :nodoc: + if min_visibility == :public + array.reject! { |e| e.visibility != :public } + else + array.reject! { |e| e.visibility == :private } end end ## - # Removes classes and modules when we see a :nodoc: all + # Tries to resolve unmatched aliases when a method + # or attribute has just been added. - def remove_classes_and_modules - initialize_classes_and_modules + def resolve_aliases(added) + # resolve any pending unmatched aliases + key = added.pretty_name + unmatched_alias_list = @unmatched_alias_lists[key] + return unless unmatched_alias_list + unmatched_alias_list.each do |unmatched_alias| + added.add_alias unmatched_alias, self + @external_aliases.delete unmatched_alias + end + @unmatched_alias_lists.delete key end ## @@ -754,7 +1001,24 @@ class RDoc::Context < RDoc::CodeObject end ## + # Given an array +methods+ of method names, set the visibility of each to + # +visibility+ + + def set_visibility_for(methods, visibility, singleton = false) + methods_matching methods, singleton do |m| + m.visibility = visibility + end + end + + def to_s # :nodoc: + "#{self.class.name} #{self.full_name}" + end + + ## # Return the TopLevel that owns us + #-- + # FIXME we can be 'owned' by several TopLevel (see #record_location & + # #in_files) def top_level return @top_level if defined? @top_level @@ -763,5 +1027,20 @@ class RDoc::Context < RDoc::CodeObject @top_level end + ## + # Upgrades NormalModule +mod+ in +enclosing+ to a +class_type+ + + def upgrade_to_class mod, class_type, enclosing + enclosing.modules_hash.delete mod.name + + klass = RDoc::ClassModule.from_module class_type, mod + + # if it was there, then we keep it even if done_documenting + RDoc::TopLevel.classes_hash[mod.full_name] = klass + enclosing.classes_hash[mod.name] = klass + + klass + end + end diff --git a/lib/rdoc/encoding.rb b/lib/rdoc/encoding.rb new file mode 100644 index 0000000000..4f0779881c --- /dev/null +++ b/lib/rdoc/encoding.rb @@ -0,0 +1,79 @@ +require 'rdoc' + +## +# This class is a wrapper around File IO and Encoding that helps RDoc load +# files and convert them to the correct encoding. + +module RDoc::Encoding + + ## + # Reads the contents of +filename+ and handles any encoding directives in + # the file. + # + # The content will be converted to the +encoding+. If the file cannot be + # converted a warning will be printed and nil will be returned. + + def self.read_file filename, encoding + content = open filename, "rb" do |f| f.read end + + utf8 = content.sub!(/\A\xef\xbb\xbf/, '') + + RDoc::Encoding.set_encoding content + + if Object.const_defined? :Encoding then + encoding ||= Encoding.default_external + orig_encoding = content.encoding + + if utf8 then + content.force_encoding Encoding::UTF_8 + content.encode! encoding + else + # assume the content is in our output encoding + content.force_encoding encoding + end + + unless content.valid_encoding? then + # revert and try to transcode + content.force_encoding orig_encoding + content.encode! encoding + end + + unless content.valid_encoding? then + warn "unable to convert #{filename} to #{encoding}, skipping" + content = nil + end + end + + content + rescue ArgumentError => e + raise unless e.message =~ /unknown encoding name - (.*)/ + warn "unknown encoding name \"#{$1}\" for #{filename}, skipping" + nil + rescue Encoding::UndefinedConversionError => e + warn "unable to convert #{e.message} for #{filename}, skipping" + nil + rescue Errno::EISDIR, Errno::ENOENT + nil + end + + ## + # Sets the encoding of +string+ based on the magic comment + + def self.set_encoding string + return unless Object.const_defined? :Encoding + + first_line = string[/\A(?:#!.*\n)?.*\n/] + + name = case first_line + when /^<\?xml[^?]*encoding=(["'])(.*?)\1/ then $2 + when /\b(?:en)?coding[=:]\s*([^\s;]+)/i then $1 + else return + end + + enc = Encoding.find name + string.force_encoding enc if enc + end + +end + + diff --git a/lib/rdoc/erbio.rb b/lib/rdoc/erbio.rb new file mode 100644 index 0000000000..04a89fbd34 --- /dev/null +++ b/lib/rdoc/erbio.rb @@ -0,0 +1,37 @@ +require 'erb' + +## +# A subclass of ERB that writes directly to an IO. Credit to Aaron Patterson +# and Masatoshi SEKI. +# +# To use: +# +# erbio = RDoc::ERBIO.new '<%= "hello world" %>', nil, nil +# +# open 'hello.txt', 'w' do |io| +# erbio.result binding +# end +# +# Note that binding must enclose the io you wish to output on. + +class RDoc::ERBIO < ERB + + ## + # Defaults +eoutvar+ to 'io', otherwise is identical to ERB's initialize + + def initialize str, safe_level = nil, trim_mode = nil, eoutvar = 'io' + super + end + + ## + # Instructs +compiler+ how to write to +io_variable+ + + def set_eoutvar compiler, io_variable + compiler.put_cmd = "#{io_variable}.write" + compiler.insert_cmd = "#{io_variable}.write" + compiler.pre_cmd = [] + compiler.post_cmd = [] + end + +end + diff --git a/lib/rdoc/generator.rb b/lib/rdoc/generator.rb index b65002977a..d02a7538c0 100644 --- a/lib/rdoc/generator.rb +++ b/lib/rdoc/generator.rb @@ -1,7 +1,39 @@ require 'rdoc' ## -# Namespace for generators +# RDoc uses generators to turn parsed source code in the form of an +# RDoc::CodeObject tree into some form of output. RDoc comes with the HTML +# generator RDoc::Generator::Darkfish and an ri data generator +# RDoc::Generator::RI. +# +# = Registering a Generator +# +# Generators are registered by calling RDoc::RDoc.add_generator with the class +# of the generator: +# +# class My::Awesome::Generator +# RDoc::RDoc.add_generator self +# end +# +# = Adding Options to +rdoc+ +# +# Before option processing in +rdoc+, RDoc::Options will call ::setup_options +# on the generator class with an RDoc::Options instance. The generator can +# use RDoc::Options#option_parser to add command-line options to the +rdoc+ +# tool. See OptionParser for details on how to add options. +# +# You can extend the RDoc::Options instance with additional accesors for your +# generator. +# +# = Generator Instantiation +# +# After parsing, RDoc::RDoc will instantiate a generator by calling +# #initialize with an RDoc::Options instance. +# +# RDoc will then call #generate on the generator instance and pass in an Array +# of RDoc::TopLevel instances, each representing a parsed file. You can use +# the various class methods on RDoc::TopLevel and in the RDoc::CodeObject tree +# to create your desired output format. module RDoc::Generator end diff --git a/lib/rdoc/generator/darkfish.rb b/lib/rdoc/generator/darkfish.rb index f64641873e..e5a6e57424 100644 --- a/lib/rdoc/generator/darkfish.rb +++ b/lib/rdoc/generator/darkfish.rb @@ -1,15 +1,12 @@ # -*- mode: ruby; ruby-indent-level: 2; tab-width: 2 -*- -# vim: noet ts=2 sts=8 sw=2 require 'pathname' require 'fileutils' -require 'erb' +require 'rdoc/erbio' require 'rdoc/generator/markup' -$DARKFISH_DRYRUN = false # TODO make me non-global - -# +## # Darkfish RDoc HTML Generator # # $Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $ @@ -52,401 +49,314 @@ $DARKFISH_DRYRUN = false # TODO make me non-global # class RDoc::Generator::Darkfish - RDoc::RDoc.add_generator( self ) + RDoc::RDoc.add_generator self + + include ERB::Util + + # Path to this file's parent directory. Used to find templates and other + # resources. + + GENERATOR_DIR = File.join 'rdoc', 'generator' + + ## + # Release Version + + VERSION = '2' + + ## + # Initialize a few instance variables before we start - include ERB::Util + def initialize options + @options = options - # Subversion rev - SVNRev = %$Rev: 52 $ + @template_dir = Pathname.new options.template_dir + @template_cache = {} - # Subversion ID - SVNId = %$Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $ + @files = nil + @classes = nil - # Path to this file's parent directory. Used to find templates and other - # resources. - GENERATOR_DIR = File.join 'rdoc', 'generator' + @basedir = Pathname.pwd.expand_path + end - # Release Version - VERSION = '1.1.6' + ## + # The output directory - # Directory where generated classes live relative to the root - CLASS_DIR = nil + attr_reader :outputdir - # Directory where generated files live relative to the root - FILE_DIR = nil + ## + # Output progress information if debugging is enabled + def debug_msg *msg + return unless $DEBUG_RDOC + $stderr.puts(*msg) + end - ################################################################# - ### C L A S S M E T H O D S - ################################################################# + ## + # Directory where generated class HTML files live relative to the output + # dir. - ### Standard generator factory method - def self::for( options ) - new( options ) - end + def class_dir + nil + end + ## + # Directory where generated class HTML files live relative to the output + # dir. - ################################################################# - ### I N S T A N C E M E T H O D S - ################################################################# + def file_dir + nil + end - ### Initialize a few instance variables before we start - def initialize( options ) - @options = options + ## + # Create the directories the generated docs will live in if they don't + # already exist. - template = @options.template || 'darkfish' + def gen_sub_directories + @outputdir.mkpath + end - template_dir = $LOAD_PATH.map do |path| - File.join File.expand_path(path), GENERATOR_DIR, 'template', template - end.find do |dir| - File.directory? dir - end + ## + # Copy over the stylesheet into the appropriate place in the output + # directory. - raise RDoc::Error, "could not find template #{template.inspect}" unless - template_dir + def write_style_sheet + debug_msg "Copying static files" + options = { :verbose => $DEBUG_RDOC, :noop => @options.dry_run } - @template_dir = Pathname.new File.expand_path(template_dir) + FileUtils.cp @template_dir + 'rdoc.css', '.', options - @files = nil - @classes = nil + Dir[(@template_dir + "{js,images}/**/*").to_s].each do |path| + next if File.directory? path + next if File.basename(path) =~ /^\./ - @basedir = Pathname.pwd.expand_path - end + dst = Pathname.new(path).relative_path_from @template_dir - ###### - public - ###### + # I suck at glob + dst_dir = dst.dirname + FileUtils.mkdir_p dst_dir, options unless File.exist? dst_dir - # The output directory - attr_reader :outputdir + FileUtils.cp @template_dir + path, dst, options + end + end + ## + # Build the initial indices and output objects based on an array of TopLevel + # objects containing the extracted information. - ### Output progress information if debugging is enabled - def debug_msg( *msg ) - return unless $DEBUG_RDOC - $stderr.puts( *msg ) - end + def generate top_levels + @outputdir = Pathname.new(@options.op_dir).expand_path(@basedir) - def class_dir - CLASS_DIR - end + @files = top_levels.sort + @classes = RDoc::TopLevel.all_classes_and_modules.sort + @methods = @classes.map { |m| m.method_list }.flatten.sort + @modsort = get_sorted_module_list(@classes) - def file_dir - FILE_DIR - end + # Now actually write the output + write_style_sheet + generate_index + generate_class_files + generate_file_files - ### Create the directories the generated docs will live in if - ### they don't already exist. - def gen_sub_directories - @outputdir.mkpath - end + rescue StandardError => err + debug_msg "%s: %s\n %s" % [ + err.class.name, err.message, err.backtrace.join("\n ") + ] + + raise + end + + protected + + ## + # Return a list of the documented modules sorted by salience first, then + # by name. + + def get_sorted_module_list(classes) + nscounts = classes.inject({}) do |counthash, klass| + top_level = klass.full_name.gsub(/::.*/, '') + counthash[top_level] ||= 0 + counthash[top_level] += 1 + + counthash + end + + # Sort based on how often the top level namespace occurs, and then on the + # name of the module -- this works for projects that put their stuff into + # a namespace, of course, but doesn't hurt if they don't. + classes.sort_by do |klass| + top_level = klass.full_name.gsub( /::.*/, '' ) + [nscounts[top_level] * -1, klass.full_name] + end.select do |klass| + klass.document_self + end + end + + ## + # Generate an index page which lists all the classes which are documented. + + def generate_index + template_file = @template_dir + 'index.rhtml' + return unless template_file.exist? + + debug_msg "Rendering the index page..." + + out_file = @basedir + @options.op_dir + 'index.html' + + render_template template_file, out_file do |io| binding end + end + + ## + # Generate a documentation file for each class + + def generate_class_files + template_file = @template_dir + 'classpage.rhtml' + return unless template_file.exist? + debug_msg "Generating class documentation in #@outputdir" + + @classes.each do |klass| + debug_msg " working on %s (%s)" % [klass.full_name, klass.path] + out_file = @outputdir + klass.path + # suppress 1.9.3 warning + rel_prefix = rel_prefix = @outputdir.relative_path_from(out_file.dirname) + svninfo = svninfo = self.get_svninfo(klass) + + debug_msg " rendering #{out_file}" + render_template template_file, out_file do |io| binding end + end + end + + ## + # Generate a documentation file for each file + + def generate_file_files + template_file = @template_dir + 'filepage.rhtml' + return unless template_file.exist? + debug_msg "Generating file documentation in #@outputdir" + + @files.each do |file| + out_file = @outputdir + file.path + debug_msg " working on %s (%s)" % [ file.full_name, out_file ] + # suppress 1.9.3 warning + rel_prefix = rel_prefix = @outputdir.relative_path_from(out_file.dirname) + + debug_msg " rendering #{out_file}" + render_template template_file, out_file do |io| binding end + end + end + + ## + # Return a string describing the amount of time in the given number of + # seconds in terms a human can understand easily. + + def time_delta_string seconds + return 'less than a minute' if seconds < 60 + return "#{seconds / 60} minute#{seconds / 60 == 1 ? '' : 's'}" if + seconds < 3000 # 50 minutes + return 'about one hour' if seconds < 5400 # 90 minutes + return "#{seconds / 3600} hours" if seconds < 64800 # 18 hours + return 'one day' if seconds < 86400 # 1 day + return 'about one day' if seconds < 172800 # 2 days + return "#{seconds / 86400} days" if seconds < 604800 # 1 week + return 'about one week' if seconds < 1209600 # 2 week + return "#{seconds / 604800} weeks" if seconds < 7257600 # 3 months + return "#{seconds / 2419200} months" if seconds < 31536000 # 1 year + return "#{seconds / 31536000} years" + end + + # %q$Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $" + SVNID_PATTERN = / + \$Id:\s + (\S+)\s # filename + (\d+)\s # rev + (\d{4}-\d{2}-\d{2})\s # Date (YYYY-MM-DD) + (\d{2}:\d{2}:\d{2}Z)\s # Time (HH:MM:SSZ) + (\w+)\s # committer + \$$ + /x + + ## + # Try to extract Subversion information out of the first constant whose + # value looks like a subversion Id tag. If no matching constant is found, + # and empty hash is returned. + + def get_svninfo klass + constants = klass.constants or return {} + + constants.find { |c| c.value =~ SVNID_PATTERN } or return {} + + filename, rev, date, time, committer = $~.captures + commitdate = Time.parse "#{date} #{time}" + + return { + :filename => filename, + :rev => Integer(rev), + :commitdate => commitdate, + :commitdelta => time_delta_string(Time.now - commitdate), + :committer => committer, + } + end + + ## + # Load and render the erb template in the given +template_file+ and write + # it out to +out_file+. + # + # Both +template_file+ and +out_file+ should be Pathname-like objects. + # + # An io will be yielded which must be captured by binding in the caller. + + def render_template template_file, out_file # :yield: io + template = template_for template_file + + unless @options.dry_run then + debug_msg "Outputting to %s" % [out_file.expand_path] + + out_file.dirname.mkpath + out_file.open 'w', 0644 do |io| + io.set_encoding @options.encoding if Object.const_defined? :Encoding + + context = yield io + + template_result template, context, template_file + end + else + context = yield nil + + output = template_result template, context, template_file + + debug_msg " would have written %d characters to %s" % [ + output.length, out_file.expand_path + ] + end + end + + ## + # Creates the result for +template+ with +context+. If an error is raised a + # Pathname +template_file+ will indicate the file where the error occurred. + + def template_result template, context, template_file + template.filename = template_file.to_s + template.result context + rescue NoMethodError => e + raise RDoc::Error, "Error while evaluating %s: %s" % [ + template_file.expand_path, + e.message, + ], e.backtrace + end + + ## + # Retrieves a cache template for +file+, if present, or fills the cache. + + def template_for file + template = @template_cache[file] + + return template if template + + klass = @options.dry_run ? ERB : RDoc::ERBIO + + template = klass.new file.read, nil, '<>' + @template_cache[file] = template + template + end - ### Copy over the stylesheet into the appropriate place in the output - ### directory. - def write_style_sheet - debug_msg "Copying static files" - options = { :verbose => $DEBUG_RDOC, :noop => $DARKFISH_DRYRUN } - - FileUtils.cp @template_dir + 'rdoc.css', '.', options - - Dir[(@template_dir + "{js,images}/**/*").to_s].each do |path| - next if File.directory? path - next if path =~ /#{File::SEPARATOR}\./ - - dst = Pathname.new(path).relative_path_from @template_dir - - # I suck at glob - dst_dir = dst.dirname - FileUtils.mkdir_p dst_dir, options unless File.exist? dst_dir - - FileUtils.cp @template_dir + path, dst, options - end - end - - ### Build the initial indices and output objects - ### based on an array of TopLevel objects containing - ### the extracted information. - def generate( top_levels ) - @outputdir = Pathname.new( @options.op_dir ).expand_path( @basedir ) - - @files = top_levels.sort - @classes = RDoc::TopLevel.all_classes_and_modules.sort - @methods = @classes.map { |m| m.method_list }.flatten.sort - @modsort = get_sorted_module_list( @classes ) - - # Now actually write the output - write_style_sheet - generate_index - generate_class_files - generate_file_files - - rescue StandardError => err - debug_msg "%s: %s\n %s" % [ err.class.name, err.message, err.backtrace.join("\n ") ] - raise - end - - ######### - protected - ######### - - ### Return a list of the documented modules sorted by salience first, then - ### by name. - def get_sorted_module_list( classes ) - nscounts = classes.inject({}) do |counthash, klass| - top_level = klass.full_name.gsub( /::.*/, '' ) - counthash[top_level] ||= 0 - counthash[top_level] += 1 - - counthash - end - - # Sort based on how often the top level namespace occurs, and then on the - # name of the module -- this works for projects that put their stuff into - # a namespace, of course, but doesn't hurt if they don't. - classes.sort_by do |klass| - top_level = klass.full_name.gsub( /::.*/, '' ) - [ - nscounts[ top_level ] * -1, - klass.full_name - ] - end.select do |klass| - klass.document_self - end - end - - ### Generate an index page which lists all the classes which - ### are documented. - def generate_index - template_file = @template_dir + 'index.rhtml' - return unless template_file.exist? - - debug_msg "Rendering the index page..." - - template_src = template_file.read - template = ERB.new( template_src, nil, '<>' ) - template.filename = template_file.to_s - context = binding() - - output = nil - - begin - output = template.result( context ) - rescue NoMethodError => err - raise RDoc::Error, "Error while evaluating %s: %s (at %p)" % [ - template_file, - err.message, - eval( "_erbout[-50,50]", context ) - ], err.backtrace - end - - outfile = @basedir + @options.op_dir + 'index.html' - unless $DARKFISH_DRYRUN - debug_msg "Outputting to %s" % [outfile.expand_path] - outfile.open( 'w', 0644 ) do |fh| - fh.print( output ) - end - else - debug_msg "Would have output to %s" % [outfile.expand_path] - end - end - - ### Generate a documentation file for each class - def generate_class_files - template_file = @template_dir + 'classpage.rhtml' - return unless template_file.exist? - debug_msg "Generating class documentation in #@outputdir" - - @classes.each do |klass| - debug_msg " working on %s (%s)" % [ klass.full_name, klass.path ] - outfile = @outputdir + klass.path - rel_prefix = @outputdir.relative_path_from( outfile.dirname ) - svninfo = self.get_svninfo( klass ) - - debug_msg " rendering #{outfile}" - self.render_template( template_file, binding(), outfile ) - end - end - - ### Generate a documentation file for each file - def generate_file_files - template_file = @template_dir + 'filepage.rhtml' - return unless template_file.exist? - debug_msg "Generating file documentation in #@outputdir" - - @files.each do |file| - outfile = @outputdir + file.path - debug_msg " working on %s (%s)" % [ file.full_name, outfile ] - rel_prefix = @outputdir.relative_path_from( outfile.dirname ) - - debug_msg " rendering #{outfile}" - self.render_template( template_file, binding(), outfile ) - end - end - - - ### Return a string describing the amount of time in the given number of - ### seconds in terms a human can understand easily. - def time_delta_string( seconds ) - return 'less than a minute' if seconds < 1.minute - return (seconds / 1.minute).to_s + ' minute' + (seconds/60 == 1 ? '' : 's') if seconds < 50.minutes - return 'about one hour' if seconds < 90.minutes - return (seconds / 1.hour).to_s + ' hours' if seconds < 18.hours - return 'one day' if seconds < 1.day - return 'about one day' if seconds < 2.days - return (seconds / 1.day).to_s + ' days' if seconds < 1.week - return 'about one week' if seconds < 2.week - return (seconds / 1.week).to_s + ' weeks' if seconds < 3.months - return (seconds / 1.month).to_s + ' months' if seconds < 1.year - return (seconds / 1.year).to_s + ' years' - end - - - # %q$Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $" - SVNID_PATTERN = / - \$Id:\s - (\S+)\s # filename - (\d+)\s # rev - (\d{4}-\d{2}-\d{2})\s # Date (YYYY-MM-DD) - (\d{2}:\d{2}:\d{2}Z)\s # Time (HH:MM:SSZ) - (\w+)\s # committer - \$$ - /x - - ### Try to extract Subversion information out of the first constant whose value looks like - ### a subversion Id tag. If no matching constant is found, and empty hash is returned. - def get_svninfo( klass ) - constants = klass.constants or return {} - - constants.find {|c| c.value =~ SVNID_PATTERN } or return {} - - filename, rev, date, time, committer = $~.captures - commitdate = Time.parse( date + ' ' + time ) - - return { - :filename => filename, - :rev => Integer( rev ), - :commitdate => commitdate, - :commitdelta => time_delta_string( Time.now.to_i - commitdate.to_i ), - :committer => committer, - } - end - - - ### Load and render the erb template in the given +template_file+ within the - ### specified +context+ (a Binding object) and write it out to +outfile+. - ### Both +template_file+ and +outfile+ should be Pathname-like objects. - - def render_template( template_file, context, outfile ) - template_src = template_file.read - template = ERB.new( template_src, nil, '<>' ) - template.filename = template_file.to_s - - output = begin - template.result( context ) - rescue NoMethodError => err - raise RDoc::Error, "Error while evaluating %s: %s (at %p)" % [ - template_file.to_s, - err.message, - eval( "_erbout[-50,50]", context ) - ], err.backtrace - end - - unless $DARKFISH_DRYRUN - outfile.dirname.mkpath - outfile.open( 'w', 0644 ) do |ofh| - ofh.print( output ) - end - else - debug_msg " would have written %d bytes to %s" % - [ output.length, outfile ] - end - end - -end # Roc::Generator::Darkfish - -# :stopdoc: - -### Time constants -module TimeConstantMethods # :nodoc: - - ### Number of seconds (returns receiver unmodified) - def seconds - return self - end - alias_method :second, :seconds - - ### Returns number of seconds in <receiver> minutes - def minutes - return self * 60 - end - alias_method :minute, :minutes - - ### Returns the number of seconds in <receiver> hours - def hours - return self * 60.minutes - end - alias_method :hour, :hours - - ### Returns the number of seconds in <receiver> days - def days - return self * 24.hours - end - alias_method :day, :days - - ### Return the number of seconds in <receiver> weeks - def weeks - return self * 7.days - end - alias_method :week, :weeks - - ### Returns the number of seconds in <receiver> fortnights - def fortnights - return self * 2.weeks - end - alias_method :fortnight, :fortnights - - ### Returns the number of seconds in <receiver> months (approximate) - def months - return self * 30.days - end - alias_method :month, :months - - ### Returns the number of seconds in <receiver> years (approximate) - def years - return (self * 365.25.days).to_i - end - alias_method :year, :years - - - ### Returns the Time <receiver> number of seconds before the - ### specified +time+. E.g., 2.hours.before( header.expiration ) - def before( time ) - return time - self - end - - - ### Returns the Time <receiver> number of seconds ago. (e.g., - ### expiration > 2.hours.ago ) - def ago - return self.before( ::Time.now ) - end - - - ### Returns the Time <receiver> number of seconds after the given +time+. - ### E.g., 10.minutes.after( header.expiration ) - def after( time ) - return time + self - end - - # Reads best without arguments: 10.minutes.from_now - def from_now - return self.after( ::Time.now ) - end -end # module TimeConstantMethods - - -# Extend Numeric with time constants -class Numeric # :nodoc: - include TimeConstantMethods end diff --git a/lib/rdoc/generator/markup.rb b/lib/rdoc/generator/markup.rb index a90b15a1e7..482fd2b2a3 100644 --- a/lib/rdoc/generator/markup.rb +++ b/lib/rdoc/generator/markup.rb @@ -36,8 +36,9 @@ module RDoc::Generator::Markup return @formatter if defined? @formatter show_hash = RDoc::RDoc.current.options.show_hash + hyperlink_all = RDoc::RDoc.current.options.hyperlink_all this = RDoc::Context === self ? self : @parent - @formatter = RDoc::Markup::ToHtmlCrossref.new this.path, this, show_hash + @formatter = RDoc::Markup::ToHtmlCrossref.new this.path, this, show_hash, hyperlink_all end ## @@ -57,36 +58,65 @@ end class RDoc::AnyMethod + ## + # Maps RDoc::RubyToken classes to CSS class names + + STYLE_MAP = { + RDoc::RubyToken::TkCONSTANT => 'ruby-constant', + RDoc::RubyToken::TkKW => 'ruby-keyword', + RDoc::RubyToken::TkIVAR => 'ruby-ivar', + RDoc::RubyToken::TkOp => 'ruby-operator', + RDoc::RubyToken::TkId => 'ruby-identifier', + RDoc::RubyToken::TkNode => 'ruby-node', + RDoc::RubyToken::TkCOMMENT => 'ruby-comment', + RDoc::RubyToken::TkREGEXP => 'ruby-regexp', + RDoc::RubyToken::TkSTRING => 'ruby-string', + RDoc::RubyToken::TkVal => 'ruby-value', + } + include RDoc::Generator::Markup + @add_line_numbers = false + + class << self + ## + # Allows controlling whether <tt>#markup_code</tt> adds line numbers to + # the source code. + + attr_accessor :add_line_numbers + end + ## # Prepend +src+ with line numbers. Relies on the first line of a source # code listing having: # - # # File xxxxx, line dddd + # # File xxxxx, line dddd + # + # If it has, line numbers are added an ', line dddd' is removed. def add_line_numbers(src) - if src =~ /\A.*, line (\d+)/ then - first = $1.to_i - 1 - last = first + src.count("\n") - size = last.to_s.length - - line = first - src.gsub!(/^/) do - res = if line == first then - " " * (size + 2) - else - "%2$*1$d: " % [size, line] - end - - line += 1 - res - end + return unless src.sub!(/\A(.*)(, line (\d+))/, '\1') + first = $3.to_i - 1 + last = first + src.count("\n") + size = last.to_s.length + + line = first + src.gsub!(/^/) do + res = if line == first then + " " * (size + 1) + else + "<span class=\"line-num\">%2$*1$d</span> " % [size, line] + end + + line += 1 + res end end ## - # Turns the method's token stream into HTML + # Turns the method's token stream into HTML. + # + # Prepends line numbers if +add_line_numbers+ is true. def markup_code return '' unless @token_stream @@ -95,32 +125,32 @@ class RDoc::AnyMethod @token_stream.each do |t| next unless t - # style = STYLE_MAP[t.class] - style = case t - when RDoc::RubyToken::TkCONSTANT then "ruby-constant" - when RDoc::RubyToken::TkKW then "ruby-keyword kw" - when RDoc::RubyToken::TkIVAR then "ruby-ivar" - when RDoc::RubyToken::TkOp then "ruby-operator" - when RDoc::RubyToken::TkId then "ruby-identifier" - when RDoc::RubyToken::TkNode then "ruby-node" - when RDoc::RubyToken::TkCOMMENT then "ruby-comment cmt" - when RDoc::RubyToken::TkREGEXP then "ruby-regexp re" - when RDoc::RubyToken::TkSTRING then "ruby-value str" - when RDoc::RubyToken::TkVal then "ruby-value" - else - nil - end + + style = STYLE_MAP[t.class] text = CGI.escapeHTML t.text - if style + if style then src << "<span class=\"#{style}\">#{text}</span>" else src << text end end - add_line_numbers src + # dedent the source + indent = src.length + lines = src.lines.to_a + lines.shift if src =~ /\A.*#\ *File/i # remove '# File' comment + lines.each do |line| + if line =~ /^ *(?=\S)/ + n = $&.length + indent = n if n < indent + break if n == 0 + end + end + src.gsub!(/^#{' ' * indent}/, '') if indent > 0 + + add_line_numbers(src) if self.class.add_line_numbers src end @@ -133,6 +163,12 @@ class RDoc::Attr end +class RDoc::Alias + + include RDoc::Generator::Markup + +end + class RDoc::Constant include RDoc::Generator::Markup diff --git a/lib/rdoc/generator/ri.rb b/lib/rdoc/generator/ri.rb index 819eb52d40..fb52997e89 100644 --- a/lib/rdoc/generator/ri.rb +++ b/lib/rdoc/generator/ri.rb @@ -8,10 +8,6 @@ class RDoc::Generator::RI RDoc::RDoc.add_generator self - def self.for options - new options - end - ## # Set up a new ri generator @@ -20,6 +16,8 @@ class RDoc::Generator::RI @store = RDoc::RI::Store.new '.' @old_siginfo = nil @current = nil + + @store.dry_run = @options.dry_run end ## diff --git a/lib/rdoc/generator/template/darkfish/classpage.rhtml b/lib/rdoc/generator/template/darkfish/classpage.rhtml index 7151087988..72b86ec6a7 100644 --- a/lib/rdoc/generator/template/darkfish/classpage.rhtml +++ b/lib/rdoc/generator/template/darkfish/classpage.rhtml @@ -1,296 +1,289 @@ <?xml version="1.0" encoding="<%= @options.charset %>"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> - <meta content="text/html; charset=<%= @options.charset %>" http-equiv="Content-Type" /> + <meta content="text/html; charset=<%= @options.charset %>" http-equiv="Content-Type" /> - <title><%= klass.type.capitalize %>: <%= klass.full_name %></title> + <title><%= klass.type.capitalize %>: <%= klass.full_name %></title> - <link rel="stylesheet" href="<%= rel_prefix %>/rdoc.css" type="text/css" media="screen" /> + <link rel="stylesheet" href="<%= rel_prefix %>/rdoc.css" type="text/css" media="screen" /> - <script src="<%= rel_prefix %>/js/jquery.js" type="text/javascript" - charset="utf-8"></script> - <script src="<%= rel_prefix %>/js/thickbox-compressed.js" type="text/javascript" - charset="utf-8"></script> - <script src="<%= rel_prefix %>/js/quicksearch.js" type="text/javascript" - charset="utf-8"></script> - <script src="<%= rel_prefix %>/js/darkfish.js" type="text/javascript" - charset="utf-8"></script> + <script src="<%= rel_prefix %>/js/jquery.js" type="text/javascript" + charset="utf-8"></script> + <script src="<%= rel_prefix %>/js/thickbox-compressed.js" type="text/javascript" + charset="utf-8"></script> + <script src="<%= rel_prefix %>/js/quicksearch.js" type="text/javascript" + charset="utf-8"></script> + <script src="<%= rel_prefix %>/js/darkfish.js" type="text/javascript" + charset="utf-8"></script> </head> <body class="<%= klass.type %>"> - <div id="metadata"> - <div id="home-metadata"> - <div id="home-section" class="section"> + <div id="metadata"> + <div id="home-metadata"> + <div id="home-section" class="section"> <h3 class="section-header"> <a href="<%= rel_prefix %>/index.html">Home</a> <a href="<%= rel_prefix %>/index.html#classes">Classes</a> <a href="<%= rel_prefix %>/index.html#methods">Methods</a> </h3> - </div> - </div> - - <div id="file-metadata"> - <div id="file-list-section" class="section"> - <h3 class="section-header">In Files</h3> - <div class="section-body"> - <ul> - <% klass.in_files.each do |tl| %> - <li><a href="<%= rel_prefix %>/<%= h tl.path %>?TB_iframe=true&height=550&width=785" - class="thickbox" title="<%= h tl.absolute_name %>"><%= h tl.absolute_name %></a></li> - <% end %> - </ul> - </div> - </div> - - <% if !svninfo.empty? %> - <div id="file-svninfo-section" class="section"> - <h3 class="section-header">Subversion Info</h3> - <div class="section-body"> - <dl class="svninfo"> - <dt>Rev</dt> - <dd><%= svninfo[:rev] %></dd> - - <dt>Last Checked In</dt> - <dd><%= svninfo[:commitdate].strftime('%Y-%m-%d %H:%M:%S') %> - (<%= svninfo[:commitdelta] %> ago)</dd> - - <dt>Checked in by</dt> - <dd><%= svninfo[:committer] %></dd> - </dl> - </div> - </div> - <% end %> - </div> - - <div id="class-metadata"> - - <!-- Parent Class --> - <% if klass.type == 'class' %> - <div id="parent-class-section" class="section"> - <h3 class="section-header">Parent</h3> - <% unless String === klass.superclass %> - <p class="link"><a href="<%= klass.aref_to klass.superclass.path %>"><%= klass.superclass.full_name %></a></p> - <% else %> - <p class="link"><%= klass.superclass %></p> - <% end %> - </div> - <% end %> - - <!-- Namespace Contents --> - <% unless klass.classes_and_modules.empty? %> - <div id="namespace-list-section" class="section"> - <h3 class="section-header">Namespace</h3> - <ul class="link-list"> - <% (klass.modules.sort + klass.classes.sort).each do |mod| %> - <li><span class="type"><%= mod.type.upcase %></span> <a href="<%= klass.aref_to mod.path %>"><%= mod.full_name %></a></li> - <% end %> - </ul> - </div> - <% end %> - - <!-- Method Quickref --> - <% unless klass.method_list.empty? %> - <div id="method-list-section" class="section"> - <h3 class="section-header">Methods</h3> - <ul class="link-list"> - <% klass.each_method do |meth| %> - <li><a href="#<%= meth.aref %>"><%= meth.singleton ? '::' : '#' %><%= meth.name %></a></li> - <% end %> - </ul> - </div> - <% end %> - - <!-- Included Modules --> - <% unless klass.includes.empty? %> - <div id="includes-section" class="section"> - <h3 class="section-header">Included Modules</h3> - <ul class="link-list"> - <% klass.each_include do |inc| %> - <% unless String === inc.module %> - <li><a class="include" href="<%= klass.aref_to inc.module.path %>"><%= inc.module.full_name %></a></li> - <% else %> - <li><span class="include"><%= inc.name %></span></li> - <% end %> - <% end %> - </ul> - </div> - <% end %> - </div> - - <div id="project-metadata"> - <% simple_files = @files.select {|tl| tl.parser == RDoc::Parser::Simple } %> - <% unless simple_files.empty? then %> - <div id="fileindex-section" class="section project-section"> - <h3 class="section-header">Files</h3> - <ul> - <% simple_files.each do |file| %> - <li class="file"><a href="<%= rel_prefix %>/<%= file.path %>"><%= h file.base_name %></a></li> - <% end %> - </ul> - </div> - <% end %> - - <div id="classindex-section" class="section project-section"> - <h3 class="section-header">Class Index - <span class="search-toggle"><img src="<%= rel_prefix %>/images/find.png" - height="16" width="16" alt="[+]" - title="show/hide quicksearch" /></span></h3> - <form action="#" method="get" accept-charset="utf-8" class="initially-hidden"> - <fieldset> - <legend>Quicksearch</legend> - <input type="text" name="quicksearch" value="" - class="quicksearch-field" /> - </fieldset> - </form> - - <ul class="link-list"> - <% @modsort.each do |index_klass| %> - <li><a href="<%= rel_prefix %>/<%= index_klass.path %>"><%= index_klass.full_name %></a></li> - <% end %> - </ul> - <div id="no-class-search-results" style="display: none;">No matching classes.</div> - </div> - - <% if $DEBUG_RDOC %> - <div id="debugging-toggle"><img src="<%= rel_prefix %>/images/bug.png" - alt="toggle debugging" height="16" width="16" /></div> - <% end %> - </div> - </div> - - <div id="documentation"> - <h1 class="<%= klass.type %>"><%= klass.full_name %></h1> - - <div id="description"> - <%= klass.description %> - </div> - - <!-- Constants --> - <% unless klass.constants.empty? %> - <div id="constants-list" class="section"> - <h3 class="section-header">Constants</h3> - <dl> - <% klass.each_constant do |const| %> - <dt><a name="<%= const.name %>"><%= const.name %></a></dt> - <% if const.comment %> - <dd class="description"><%= const.description.strip %></dd> - <% else %> - <dd class="description missing-docs">(Not documented)</dd> - <% end %> - <% end %> - </dl> - </div> - <% end %> - - <!-- Attributes --> - <% unless klass.attributes.empty? %> - <div id="attribute-method-details" class="method-section section"> - <h3 class="section-header">Attributes</h3> - - <% klass.each_attribute do |attrib| %> - <div id="<%= attrib.html_name %>-attribute-method" class="method-detail"> - <a name="<%= h attrib.name %>"></a> - <% if attrib.rw =~ /w/i %> - <a name="<%= h attrib.name %>="></a> - <% end %> - <div class="method-heading attribute-method-heading"> - <span class="method-name"><%= h attrib.name %></span><span - class="attribute-access-type">[<%= attrib.rw %>]</span> - </div> - - <div class="method-description"> - <% if attrib.comment %> - <%= attrib.description.strip %> - <% else %> - <p class="missing-docs">(Not documented)</p> - <% end %> - </div> - </div> - <% end %> - </div> - <% end %> - - <!-- Methods --> - <% klass.methods_by_type.each do |type, visibilities| - next if visibilities.empty? - visibilities.each do |visibility, methods| - next if methods.empty? %> - <div id="<%= visibility %>-<%= type %>-method-details" class="method-section section"> - <h3 class="section-header"><%= visibility.to_s.capitalize %> <%= type.capitalize %> Methods</h3> - - <% methods.each do |method| %> - <div id="<%= method.html_name %>-method" class="method-detail <%= method.is_alias_for ? "method-alias" : '' %>"> - <a name="<%= h method.aref %>"></a> - - <div class="method-heading"> - <% if method.call_seq %> - <span class="method-callseq"><%= method.call_seq.strip.gsub(/->/, '→').gsub( /^\w.+\./m, '') %></span> - <span class="method-click-advice">click to toggle source</span> - <% else %> - <span class="method-name"><%= h method.name %></span><span - class="method-args"><%= method.params %></span> - <span class="method-click-advice">click to toggle source</span> - <% end %> - </div> - - <div class="method-description"> - <% if method.comment %> - <%= method.description.strip %> - <% else %> - <p class="missing-docs">(Not documented)</p> - <% end %> - - <% if method.token_stream %> - <div class="method-source-code" - id="<%= method.html_name %>-source"> + </div> + </div> + + <div id="file-metadata"> + <div id="file-list-section" class="section"> + <h3 class="section-header">In Files</h3> + <div class="section-body"> + <ul> + <% klass.in_files.each do |tl| %> + <li><a href="<%= rel_prefix %>/<%= h tl.path %>?TB_iframe=true&height=550&width=785" + class="thickbox" title="<%= h tl.absolute_name %>"><%= h tl.absolute_name %></a></li> + <% end %> + </ul> + </div> + </div> + + <% if !svninfo.empty? %> + <div id="file-svninfo-section" class="section"> + <h3 class="section-header">Subversion Info</h3> + <div class="section-body"> + <dl class="svninfo"> + <dt>Rev</dt> + <dd><%= svninfo[:rev] %></dd> + + <dt>Last Checked In</dt> + <dd><%= svninfo[:commitdate].strftime('%Y-%m-%d %H:%M:%S') %> + (<%= svninfo[:commitdelta] %> ago)</dd> + + <dt>Checked in by</dt> + <dd><%= svninfo[:committer] %></dd> + </dl> + </div> + </div> + <% end %> + </div> + + <div id="class-metadata"> + + <!-- Parent Class --> + <% if klass.type == 'class' %> + <div id="parent-class-section" class="section"> + <h3 class="section-header">Parent</h3> + <% if klass.superclass and not String === klass.superclass then %> + <p class="link"><a href="<%= klass.aref_to klass.superclass.path %>"><%= klass.superclass.full_name %></a></p> + <% else %> + <p class="link"><%= klass.superclass %></p> + <% end %> + </div> + <% end %> + + <!-- Namespace Contents --> + <% unless klass.classes_and_modules.empty? %> + <div id="namespace-list-section" class="section"> + <h3 class="section-header">Namespace</h3> + <ul class="link-list"> + <% (klass.modules.sort + klass.classes.sort).each do |mod| %> + <li><span class="type"><%= mod.type.upcase %></span> <a href="<%= klass.aref_to mod.path %>"><%= mod.full_name %></a></li> + <% end %> + </ul> + </div> + <% end %> + + <!-- Method Quickref --> + <% unless klass.method_list.empty? %> + <div id="method-list-section" class="section"> + <h3 class="section-header">Methods</h3> + <ul class="link-list"> + <% klass.each_method do |meth| %> + <li><a href="#<%= meth.aref %>"><%= meth.singleton ? '::' : '#' %><%= meth.name %></a></li> + <% end %> + </ul> + </div> + <% end %> + + <!-- Included Modules --> + <% unless klass.includes.empty? %> + <div id="includes-section" class="section"> + <h3 class="section-header">Included Modules</h3> + <ul class="link-list"> + <% klass.each_include do |inc| %> + <% unless String === inc.module %> + <li><a class="include" href="<%= klass.aref_to inc.module.path %>"><%= inc.module.full_name %></a></li> + <% else %> + <li><span class="include"><%= inc.name %></span></li> + <% end %> + <% end %> + </ul> + </div> + <% end %> + </div> + + <div id="project-metadata"> + <% simple_files = @files.select {|tl| tl.parser == RDoc::Parser::Simple } %> + <% unless simple_files.empty? then %> + <div id="fileindex-section" class="section project-section"> + <h3 class="section-header">Files</h3> + <ul> + <% simple_files.each do |file| %> + <li class="file"><a href="<%= rel_prefix %>/<%= file.path %>"><%= h file.base_name %></a></li> + <% end %> + </ul> + </div> + <% end %> + + <div id="classindex-section" class="section project-section"> + <h3 class="section-header">Class Index + <span class="search-toggle"><img src="<%= rel_prefix %>/images/find.png" + height="16" width="16" alt="[+]" + title="show/hide quicksearch" /></span></h3> + <form action="#" method="get" accept-charset="utf-8" class="initially-hidden"> + <fieldset> + <legend>Quicksearch</legend> + <input type="text" name="quicksearch" value="" + class="quicksearch-field" /> + </fieldset> + </form> + + <ul class="link-list"> + <% @modsort.each do |index_klass| %> + <li><a href="<%= rel_prefix %>/<%= index_klass.path %>"><%= index_klass.full_name %></a></li> + <% end %> + </ul> + <div id="no-class-search-results" style="display: none;">No matching classes.</div> + </div> + + <% if $DEBUG_RDOC %> + <div id="debugging-toggle"><img src="<%= rel_prefix %>/images/bug.png" + alt="toggle debugging" height="16" width="16" /></div> + <% end %> + </div> + </div> + + <div id="documentation"> + <h1 class="<%= klass.type %>"><%= klass.full_name %></h1> + + <div id="description"> + <%= klass.description %> + </div> + + <!-- Constants --> + <% unless klass.constants.empty? %> + <div id="constants-list" class="section"> + <h3 class="section-header">Constants</h3> + <dl> + <% klass.each_constant do |const| %> + <dt><a name="<%= const.name %>"><%= const.name %></a></dt> + <% if const.comment %> + <dd class="description"><%= const.description.strip %></dd> + <% else %> + <dd class="description missing-docs">(Not documented)</dd> + <% end %> + <% end %> + </dl> + </div> + <% end %> + + <!-- Attributes --> + <% unless klass.attributes.empty? %> + <div id="attribute-method-details" class="method-section section"> + <h3 class="section-header">Attributes</h3> + + <% klass.each_attribute do |attrib| %> + <div id="<%= attrib.html_name %>-attribute-method" class="method-detail"> + <a name="<%= h attrib.name %>"></a> + <% if attrib.rw =~ /w/i %> + <a name="<%= h attrib.name %>="></a> + <% end %> + <div class="method-heading attribute-method-heading"> + <span class="method-name"><%= h attrib.name %></span><span + class="attribute-access-type">[<%= attrib.rw %>]</span> + </div> + + <div class="method-description"> + <% if attrib.comment %> + <%= attrib.description.strip %> + <% else %> + <p class="missing-docs">(Not documented)</p> + <% end %> + </div> + </div> + <% end %> + </div> + <% end %> + + <!-- Methods --> + <% klass.methods_by_type.each do |type, visibilities| + next if visibilities.empty? + visibilities.each do |visibility, methods| + next if methods.empty? %> + <div id="<%= visibility %>-<%= type %>-method-details" class="method-section section"> + <h3 class="section-header"><%= visibility.to_s.capitalize %> <%= type.capitalize %> Methods</h3> + + <% methods.each do |method| %> + <div id="<%= method.html_name %>-method" class="method-detail <%= method.is_alias_for ? "method-alias" : '' %>"> + <a name="<%= h method.aref %>"></a> + + <div class="method-heading"> + <% if method.call_seq %> + <span class="method-callseq"><%= method.call_seq.strip.gsub(/->/, '→').gsub( /^\w.+\./m, '') %></span> + <span class="method-click-advice">click to toggle source</span> + <% else %> + <span class="method-name"><%= h method.name %></span><span + class="method-args"><%= method.params %></span> + <span class="method-click-advice">click to toggle source</span> + <% end %> + </div> + + <div class="method-description"> + <% if method.comment %> + <%= method.description.strip %> + <% else %> + <p class="missing-docs">(Not documented)</p> + <% end %> + + <% if method.token_stream %> + <div class="method-source-code" + id="<%= method.html_name %>-source"> <pre> <%= method.markup_code %> </pre> - </div> - <% end %> - </div> - - <% unless method.aliases.empty? %> - <div class="aliases"> - Also aliased as: <%= method.aliases.map do |aka| - %{<a href="#{ klass.aref_to aka.path}">#{h aka.name}</a>} - end.join(", ") %> - </div> - <% end %> - - <% if method.is_alias_for then %> - <div class="aliases"> + </div> + <% end %> + </div> + + <% unless method.aliases.empty? %> + <div class="aliases"> + Also aliased as: <%= method.aliases.map do |aka| + if aka.parent then # HACK lib/rexml/encodings + %{<a href="#{klass.aref_to aka.path}">#{h aka.name}</a>} + else + h aka.name + end + end.join ", " %> + </div> + <% end %> + + <% if method.is_alias_for then %> + <div class="aliases"> Alias for: <a href="<%= klass.aref_to method.is_alias_for.path %>"><%= h method.is_alias_for.name %></a> - </div> - <% end %> - </div> - - <% end %> - </div> - <% end - end %> - - </div> - - - <div id="rdoc-debugging-section-dump" class="debugging-section"> - <% if $DEBUG_RDOC - require 'pp' %> -<pre><%= h PP.pp(klass, _erbout) %></pre> - </div> - <% else %> - <p>Disabled; run with --debug to generate this.</p> - <% end %> - </div> - - <div id="validator-badges"> - <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> - <p><small>Generated with the <a href="http://deveiate.org/projects/Darkfish-Rdoc/">Darkfish - Rdoc Generator</a> <%= RDoc::Generator::Darkfish::VERSION %></small>.</p> - </div> + </div> + <% end %> + </div> + + <% end %> + </div> + <% end + end %> + + </div> + + <div id="validator-badges"> + <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> + <p><small>Generated with the <a href="http://deveiate.org/projects/Darkfish-Rdoc/">Darkfish + Rdoc Generator</a> <%= RDoc::Generator::Darkfish::VERSION %></small>.</p> + </div> </body> </html> diff --git a/lib/rdoc/generator/template/darkfish/filepage.rhtml b/lib/rdoc/generator/template/darkfish/filepage.rhtml index 33216dc8f1..b230a456a3 100644 --- a/lib/rdoc/generator/template/darkfish/filepage.rhtml +++ b/lib/rdoc/generator/template/darkfish/filepage.rhtml @@ -1,123 +1,123 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> - <meta content="text/html; charset=<%= @options.charset %>" http-equiv="Content-Type" /> + <meta content="text/html; charset=<%= @options.charset %>" http-equiv="Content-Type" /> - <title>File: <%= file.base_name %> [<%= @options.title %>]</title> + <title>File: <%= file.base_name %> [<%= @options.title %>]</title> - <link type="text/css" media="screen" href="<%= rel_prefix %>/rdoc.css" rel="stylesheet" /> + <link type="text/css" media="screen" href="<%= rel_prefix %>/rdoc.css" rel="stylesheet" /> - <script src="<%= rel_prefix %>/js/jquery.js" type="text/javascript" - charset="utf-8"></script> - <script src="<%= rel_prefix %>/js/thickbox-compressed.js" type="text/javascript" - charset="utf-8"></script> - <script src="<%= rel_prefix %>/js/quicksearch.js" type="text/javascript" - charset="utf-8"></script> - <script src="<%= rel_prefix %>/js/darkfish.js" type="text/javascript" - charset="utf-8"></script> + <script src="<%= rel_prefix %>/js/jquery.js" type="text/javascript" + charset="utf-8"></script> + <script src="<%= rel_prefix %>/js/thickbox-compressed.js" type="text/javascript" + charset="utf-8"></script> + <script src="<%= rel_prefix %>/js/quicksearch.js" type="text/javascript" + charset="utf-8"></script> + <script src="<%= rel_prefix %>/js/darkfish.js" type="text/javascript" + charset="utf-8"></script> </head> <% if file.parser == RDoc::Parser::Simple %> <body class="file"> - <div id="metadata"> - <div id="home-metadata"> - <div id="home-section" class="section"> + <div id="metadata"> + <div id="home-metadata"> + <div id="home-section" class="section"> <h3 class="section-header"> <a href="<%= rel_prefix %>/index.html">Home</a> <a href="<%= rel_prefix %>/index.html#classes">Classes</a> <a href="<%= rel_prefix %>/index.html#methods">Methods</a> </h3> - </div> - </div> + </div> + </div> - <div id="project-metadata"> - <% simple_files = @files.select { |f| f.parser == RDoc::Parser::Simple } %> - <% unless simple_files.empty? then %> - <div id="fileindex-section" class="section project-section"> - <h3 class="section-header">Files</h3> - <ul> - <% simple_files.each do |f| %> - <li class="file"><a href="<%= rel_prefix %>/<%= f.path %>"><%= h f.base_name %></a></li> - <% end %> - </ul> - </div> - <% end %> + <div id="project-metadata"> + <% simple_files = @files.select { |f| f.parser == RDoc::Parser::Simple } %> + <% unless simple_files.empty? then %> + <div id="fileindex-section" class="section project-section"> + <h3 class="section-header">Files</h3> + <ul> + <% simple_files.each do |f| %> + <li class="file"><a href="<%= rel_prefix %>/<%= f.path %>"><%= h f.base_name %></a></li> + <% end %> + </ul> + </div> + <% end %> - <div id="classindex-section" class="section project-section"> - <h3 class="section-header">Class Index - <span class="search-toggle"><img src="<%= rel_prefix %>/images/find.png" - height="16" width="16" alt="[+]" - title="show/hide quicksearch" /></span></h3> - <form action="#" method="get" accept-charset="utf-8" class="initially-hidden"> - <fieldset> - <legend>Quicksearch</legend> - <input type="text" name="quicksearch" value="" - class="quicksearch-field" /> - </fieldset> - </form> + <div id="classindex-section" class="section project-section"> + <h3 class="section-header">Class Index + <span class="search-toggle"><img src="<%= rel_prefix %>/images/find.png" + height="16" width="16" alt="[+]" + title="show/hide quicksearch" /></span></h3> + <form action="#" method="get" accept-charset="utf-8" class="initially-hidden"> + <fieldset> + <legend>Quicksearch</legend> + <input type="text" name="quicksearch" value="" + class="quicksearch-field" /> + </fieldset> + </form> - <ul class="link-list"> - <% @modsort.each do |index_klass| %> - <li><a href="<%= rel_prefix %>/<%= index_klass.path %>"><%= index_klass.full_name %></a></li> - <% end %> - </ul> - <div id="no-class-search-results" style="display: none;">No matching classes.</div> - </div> + <ul class="link-list"> + <% @modsort.each do |index_klass| %> + <li><a href="<%= rel_prefix %>/<%= index_klass.path %>"><%= index_klass.full_name %></a></li> + <% end %> + </ul> + <div id="no-class-search-results" style="display: none;">No matching classes.</div> + </div> - <% if $DEBUG_RDOC %> - <div id="debugging-toggle"><img src="<%= rel_prefix %>/images/bug.png" - alt="toggle debugging" height="16" width="16" /></div> - <% end %> - </div> - </div> + <% if $DEBUG_RDOC %> + <div id="debugging-toggle"><img src="<%= rel_prefix %>/images/bug.png" + alt="toggle debugging" height="16" width="16" /></div> + <% end %> + </div> + </div> - <div id="documentation"> - <%= file.description %> - </div> + <div id="documentation"> + <%= file.description %> + </div> - <div id="validator-badges"> - <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> - <p><small>Generated with the <a href="http://deveiate.org/projects/Darkfish-Rdoc/">Darkfish - Rdoc Generator</a> <%= RDoc::Generator::Darkfish::VERSION %></small>.</p> - </div> + <div id="validator-badges"> + <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> + <p><small>Generated with the <a href="http://deveiate.org/projects/Darkfish-Rdoc/">Darkfish + Rdoc Generator</a> <%= RDoc::Generator::Darkfish::VERSION %></small>.</p> + </div> </body> <% else %> <body class="file file-popup"> - <div id="metadata"> - <dl> - <dt class="modified-date">Last Modified</dt> - <dd class="modified-date"><%= file.last_modified %></dd> + <div id="metadata"> + <dl> + <dt class="modified-date">Last Modified</dt> + <dd class="modified-date"><%= file.last_modified %></dd> - <% if file.requires %> - <dt class="requires">Requires</dt> - <dd class="requires"> - <ul> - <% file.requires.each do |require| %> - <li><%= require.name %></li> - <% end %> - </ul> - </dd> - <% end %> + <% if file.requires %> + <dt class="requires">Requires</dt> + <dd class="requires"> + <ul> + <% file.requires.each do |require| %> + <li><%= require.name %></li> + <% end %> + </ul> + </dd> + <% end %> - <% if @options.webcvs %> - <dt class="scs-url">Trac URL</dt> - <dd class="scs-url"><a target="_top" - href="<%= file.cvs_url %>"><%= file.cvs_url %></a></dd> - <% end %> - </dl> - </div> + <% if @options.webcvs %> + <dt class="scs-url">Trac URL</dt> + <dd class="scs-url"><a target="_top" + href="<%= file.cvs_url %>"><%= file.cvs_url %></a></dd> + <% end %> + </dl> + </div> - <div id="documentation"> - <% if file.comment %> - <div class="description"> - <h2>Description</h2> - <%= file.description %> - </div> - <% end %> - </div> + <div id="documentation"> + <% if file.comment %> + <div class="description"> + <h2>Description</h2> + <%= file.description %> + </div> + <% end %> + </div> </body> <% end %> </html> diff --git a/lib/rdoc/generator/template/darkfish/index.rhtml b/lib/rdoc/generator/template/darkfish/index.rhtml index e853235ddb..3198246f8a 100644 --- a/lib/rdoc/generator/template/darkfish/index.rhtml +++ b/lib/rdoc/generator/template/darkfish/index.rhtml @@ -1,64 +1,64 @@ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" - "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> + "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> - <meta content="text/html; charset=<%= @options.charset %>" http-equiv="Content-Type" /> + <meta content="text/html; charset=<%= @options.charset %>" http-equiv="Content-Type" /> - <title><%= h @options.title %></title> + <title><%= h @options.title %></title> - <link type="text/css" media="screen" href="rdoc.css" rel="stylesheet" /> + <link type="text/css" media="screen" href="rdoc.css" rel="stylesheet" /> - <script src="js/jquery.js" type="text/javascript" charset="utf-8"></script> - <script src="js/thickbox-compressed.js" type="text/javascript" charset="utf-8"></script> - <script src="js/quicksearch.js" type="text/javascript" charset="utf-8"></script> - <script src="js/darkfish.js" type="text/javascript" charset="utf-8"></script> + <script src="js/jquery.js" type="text/javascript" charset="utf-8"></script> + <script src="js/thickbox-compressed.js" type="text/javascript" charset="utf-8"></script> + <script src="js/quicksearch.js" type="text/javascript" charset="utf-8"></script> + <script src="js/darkfish.js" type="text/javascript" charset="utf-8"></script> </head> <body class="indexpage"> - <% $stderr.sync = true %> - <h1><%= h @options.title %></h1> + <% $stderr.sync = true %> + <h1><%= h @options.title %></h1> - <% if @options.main_page && main_page = @files.find { |f| f.full_name == @options.main_page } %> - <div id="main"> - <%= main_page.description.sub(%r{^\s*<h1.*?/h1>}i, '') %> - </div> - <% else %> - <p>This is the API documentation for '<%= @options.title %>'.</p> - <% end %> + <% if @options.main_page && main_page = @files.find { |f| f.full_name == @options.main_page } then %> + <div id="main"> + <%= main_page.description.sub(%r{^\s*<h1.*?/h1>}i, '') %> + </div> + <% else %> + <p>This is the API documentation for '<%= @options.title %>'.</p> + <% end %> - <% simple_files = @files.select {|tl| tl.parser == RDoc::Parser::Simple } %> - <% unless simple_files.empty? then %> - <h2>Files</h2> - <ul> - <% simple_files.sort.each do |file| %> - <li class="file"><a href="<%= file.path %>"><%= h file.base_name %></a></li> - <% end %> - </ul> - <% end %> + <% simple_files = @files.select {|tl| tl.parser == RDoc::Parser::Simple } %> + <% unless simple_files.empty? then %> + <h2>Files</h2> + <ul> + <% simple_files.sort.each do |file| %> + <li class="file"><a href="<%= file.path %>"><%= h file.base_name %></a></li> + <% end %> + </ul> + <% end %> - <h2 id="classes">Classes/Modules</h2> - <ul> - <% @modsort.each do |klass| %> - <li class="<%= klass.type %>"><a href="<%= klass.path %>"><%= klass.full_name %></a></li> - <% end %> - </ul> + <h2 id="classes">Classes/Modules</h2> + <ul> + <% @modsort.each do |klass| %> + <li class="<%= klass.type %>"><a href="<%= klass.path %>"><%= klass.full_name %></a></li> + <% end %> + </ul> - <h2 id="methods">Methods</h2> - <ul> - <% RDoc::TopLevel.all_classes_and_modules.map do |mod| - mod.method_list - end.flatten.sort.each do |method| %> - <li><a href="<%= method.path %>"><%= method.pretty_name %> — <%= method.parent.full_name %></a></li> - <% end %> - </ul> + <h2 id="methods">Methods</h2> + <ul> + <% RDoc::TopLevel.all_classes_and_modules.map do |mod| + mod.method_list + end.flatten.sort.each do |method| %> + <li><a href="<%= method.path %>"><%= method.pretty_name %> — <%= method.parent.full_name %></a></li> + <% end %> + </ul> - <div id="validator-badges"> - <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> - <p><small>Generated with the <a href="http://deveiate.org/projects/Darkfish-Rdoc/">Darkfish - Rdoc Generator</a> <%= RDoc::Generator::Darkfish::VERSION %></small>.</p> - </div> + <div id="validator-badges"> + <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> + <p><small>Generated with the <a href="http://deveiate.org/projects/Darkfish-Rdoc/">Darkfish + Rdoc Generator</a> <%= RDoc::Generator::Darkfish::VERSION %></small>.</p> + </div> </body> </html> diff --git a/lib/rdoc/generator/template/darkfish/rdoc.css b/lib/rdoc/generator/template/darkfish/rdoc.css index ffe996001a..231f9b7f04 100644 --- a/lib/rdoc/generator/template/darkfish/rdoc.css +++ b/lib/rdoc/generator/template/darkfish/rdoc.css @@ -12,76 +12,76 @@ body { background: #efefef; - font: 14px "Helvetica Neue", Helvetica, Tahoma, sans-serif; + font: 14px "Helvetica Neue", Helvetica, Tahoma, sans-serif; } body.class, body.module, body.file { - margin-left: 40px; + margin-left: 40px; } body.file-popup { - font-size: 90%; - margin-left: 0; + font-size: 90%; + margin-left: 0; } h1 { - font-size: 300%; - text-shadow: rgba(135,145,135,0.65) 2px 2px 3px; - color: #6C8C22; + font-size: 300%; + text-shadow: rgba(135,145,135,0.65) 2px 2px 3px; + color: #6C8C22; } h2,h3,h4 { margin-top: 1.5em; } :link, :visited { - color: #6C8C22; - text-decoration: none; + color: #6C8C22; + text-decoration: none; } :link:hover, :visited:hover { - border-bottom: 1px dotted #6C8C22; + border-bottom: 1px dotted #6C8C22; } pre { - background: #ddd; - padding: 0.5em 0; + background: #ddd; + padding: 0.5em 0; } /* @group Generic Classes */ .initially-hidden { - display: none; + display: none; } .quicksearch-field { - width: 98%; - background: #ddd; - border: 1px solid #aaa; - height: 1.5em; - -webkit-border-radius: 4px; + width: 98%; + background: #ddd; + border: 1px solid #aaa; + height: 1.5em; + -webkit-border-radius: 4px; } .quicksearch-field:focus { - background: #f1edba; + background: #f1edba; } .missing-docs { - font-size: 120%; - background: white url(images/wrench_orange.png) no-repeat 4px center; - color: #ccc; - line-height: 2em; - border: 1px solid #d00; - opacity: 1; - padding-left: 20px; - text-indent: 24px; - letter-spacing: 3px; - font-weight: bold; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; + font-size: 120%; + background: white url(images/wrench_orange.png) no-repeat 4px center; + color: #ccc; + line-height: 2em; + border: 1px solid #d00; + opacity: 1; + padding-left: 20px; + text-indent: 24px; + letter-spacing: 3px; + font-weight: bold; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; } .target-section { - border: 2px solid #dcce90; - border-left-width: 8px; - padding: 0 1em; - background: #fff3c2; + border: 2px solid #dcce90; + border-left-width: 8px; + padding: 0 1em; + background: #fff3c2; } /* @end */ @@ -89,37 +89,37 @@ pre { /* @group Index Page, Standalone file pages */ body.indexpage { - margin: 1em 3em; + margin: 1em 3em; } body.indexpage p, body.indexpage div, body.file p { - margin: 1em 0; + margin: 1em 0; } .indexpage ul, .file #documentation ul { - line-height: 160%; - list-style: none; + line-height: 160%; + list-style: none; } .indexpage ul :link, .indexpage ul :visited { - font-size: 16px; + font-size: 16px; } .indexpage li, .file #documentation li { - padding-left: 20px; - background: url(images/bullet_black.png) no-repeat left 4px; + padding-left: 20px; + background: url(images/bullet_black.png) no-repeat left 4px; } .indexpage li.module { - background: url(images/package.png) no-repeat left 4px; + background: url(images/package.png) no-repeat left 4px; } .indexpage li.class { - background: url(images/ruby.png) no-repeat left 4px; + background: url(images/ruby.png) no-repeat left 4px; } .indexpage li.file { - background: url(images/page_white_text.png) no-repeat left 4px; + background: url(images/page_white_text.png) no-repeat left 4px; } .file li p, .indexpage li p { @@ -133,48 +133,48 @@ body.file p { .class #metadata, .file #metadata, .module #metadata { - float: left; - width: 260px; + float: left; + width: 260px; } .class #documentation, .file #documentation, .module #documentation { - margin: 2em 1em 5em 300px; - min-width: 340px; + margin: 2em 1em 5em 300px; + min-width: 340px; } .file #metadata { - margin: 0.8em; + margin: 0.8em; } #validator-badges { - clear: both; - margin: 1em 1em 2em; + clear: both; + margin: 1em 1em 2em; } /* @end */ /* @group Metadata Section */ #metadata .section { - background-color: #dedede; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border: 1px solid #aaa; - margin: 0 8px 16px; - font-size: 90%; - overflow: hidden; + background-color: #dedede; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border: 1px solid #aaa; + margin: 0 8px 16px; + font-size: 90%; + overflow: hidden; } #metadata h3.section-header { - margin: 0; - padding: 2px 8px; - background: #ccc; - color: #666; - -moz-border-radius-topleft: 4px; - -moz-border-radius-topright: 4px; - -webkit-border-top-left-radius: 4px; - -webkit-border-top-right-radius: 4px; - border-bottom: 1px solid #aaa; + margin: 0; + padding: 2px 8px; + background: #ccc; + color: #666; + -moz-border-radius-topleft: 4px; + -moz-border-radius-topright: 4px; + -webkit-border-top-left-radius: 4px; + -webkit-border-top-right-radius: 4px; + border-bottom: 1px solid #aaa; } #metadata #home-section h3.section-header { border-bottom: 0; @@ -183,33 +183,33 @@ body.file p { #metadata ul, #metadata dl, #metadata p { - padding: 8px; - list-style: none; + padding: 8px; + list-style: none; } #file-metadata ul { - padding-left: 28px; - list-style-image: url(images/page_green.png); + padding-left: 28px; + list-style-image: url(images/page_green.png); } dl.svninfo { - color: #666; - margin: 0; + color: #666; + margin: 0; } dl.svninfo dt { - font-weight: bold; + font-weight: bold; } ul.link-list li { - white-space: nowrap; + white-space: nowrap; } ul.link-list .type { - font-size: 8px; - text-transform: uppercase; - color: white; - background: #969696; - padding: 2px 4px; - -webkit-border-radius: 5px; + font-size: 8px; + text-transform: uppercase; + color: white; + background: #969696; + padding: 2px 4px; + -webkit-border-radius: 5px; } /* @end */ @@ -217,7 +217,7 @@ ul.link-list .type { /* @group Project Metadata Section */ #project-metadata { - margin-top: 3em; + margin-top: 3em; } .file #project-metadata { @@ -225,34 +225,34 @@ ul.link-list .type { } #project-metadata .section { - border: 1px solid #aaa; + border: 1px solid #aaa; } #project-metadata h3.section-header { - border-bottom: 1px solid #aaa; - position: relative; + border-bottom: 1px solid #aaa; + position: relative; } #project-metadata h3.section-header .search-toggle { - position: absolute; - right: 5px; + position: absolute; + right: 5px; } #project-metadata form { - color: #777; - background: #ccc; - padding: 8px 8px 16px; - border-bottom: 1px solid #bbb; + color: #777; + background: #ccc; + padding: 8px 8px 16px; + border-bottom: 1px solid #bbb; } #project-metadata fieldset { - border: 0; + border: 0; } #no-class-search-results { - margin: 0 auto 1em; - text-align: center; - font-size: 14px; - font-weight: bold; - color: #aaa; + margin: 0 auto 1em; + text-align: center; + font-size: 14px; + font-weight: bold; + color: #aaa; } /* @end */ @@ -260,12 +260,12 @@ ul.link-list .type { /* @group Documentation Section */ #description { - font-size: 100%; - color: #333; + font-size: 100%; + color: #333; } #description p { - margin: 1em 0.4em; + margin: 1em 0.4em; } #description li p { @@ -273,152 +273,152 @@ ul.link-list .type { } #description ul { - margin-left: 1.5em; + margin-left: 1.5em; } #description ul li { - line-height: 1.4em; + line-height: 1.4em; } #description dl, #documentation dl { - margin: 8px 1.5em; - border: 1px solid #ccc; + margin: 8px 1.5em; + border: 1px solid #ccc; } #description dl { - font-size: 14px; + font-size: 14px; } #description dt, #documentation dt { - padding: 2px 4px; - font-weight: bold; - background: #ddd; + padding: 2px 4px; + font-weight: bold; + background: #ddd; } #description dd, #documentation dd { - padding: 2px 12px; + padding: 2px 12px; } #description dd + dt, #documentation dd + dt { - margin-top: 0.7em; + margin-top: 0.7em; } #documentation .section { - font-size: 90%; + font-size: 90%; } #documentation h3.section-header { - margin-top: 2em; - padding: 0.75em 0.5em; - background-color: #dedede; - color: #333; - font-size: 150%; - border: 1px solid #bbb; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; + margin-top: 2em; + padding: 0.75em 0.5em; + background-color: #dedede; + color: #333; + font-size: 150%; + border: 1px solid #bbb; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; } #constants-list > dl, #attributes-list > dl { - margin: 1em 0 2em; - border: 0; + margin: 1em 0 2em; + border: 0; } #constants-list > dl dt, #attributes-list > dl dt { - padding-left: 0; - font-weight: bold; - font-family: Monaco, "Andale Mono"; - background: inherit; + padding-left: 0; + font-weight: bold; + font-family: Monaco, "Andale Mono"; + background: inherit; } #constants-list > dl dt a, #attributes-list > dl dt a { - color: inherit; + color: inherit; } #constants-list > dl dd, #attributes-list > dl dd { - margin: 0 0 1em 0; - padding: 0; - color: #666; + margin: 0 0 1em 0; + padding: 0; + color: #666; } /* @group Method Details */ #documentation .method-source-code { - display: none; + display: none; } #documentation .method-detail { - margin: 0.5em 0; - padding: 0.5em 0; - cursor: pointer; + margin: 0.5em 0; + padding: 0.5em 0; + cursor: pointer; } #documentation .method-detail:hover { - background-color: #f1edba; + background-color: #f1edba; } #documentation .method-heading { - position: relative; - padding: 2px 4px 0 20px; - font-size: 125%; - font-weight: bold; - color: #333; - background: url(images/brick.png) no-repeat left bottom; + position: relative; + padding: 2px 4px 0 20px; + font-size: 125%; + font-weight: bold; + color: #333; + background: url(images/brick.png) no-repeat left bottom; } #documentation .method-heading :link, #documentation .method-heading :visited { - color: inherit; + color: inherit; } #documentation .method-click-advice { - position: absolute; - top: 2px; - right: 5px; - font-size: 10px; - color: #9b9877; - visibility: hidden; - padding-right: 20px; - line-height: 20px; - background: url(images/zoom.png) no-repeat right top; + position: absolute; + top: 2px; + right: 5px; + font-size: 10px; + color: #9b9877; + visibility: hidden; + padding-right: 20px; + line-height: 20px; + background: url(images/zoom.png) no-repeat right top; } #documentation .method-detail:hover .method-click-advice { - visibility: visible; + visibility: visible; } #documentation .method-alias .method-heading { - color: #666; - background: url(images/brick_link.png) no-repeat left bottom; + color: #666; + background: url(images/brick_link.png) no-repeat left bottom; } #documentation .method-description, #documentation .aliases { - margin: 0 20px; - line-height: 1.2em; - color: #666; + margin: 0 20px; + line-height: 1.2em; + color: #666; } #documentation .aliases { - padding-top: 4px; - font-style: italic; - cursor: default; + padding-top: 4px; + font-style: italic; + cursor: default; } #documentation .method-description p { - padding: 0; + padding: 0; } #documentation .method-description p + p { - margin-bottom: 0.5em; + margin-bottom: 0.5em; } #documentation .method-description ul { margin-left: 1.5em; } #documentation .attribute-method-heading { - background: url(images/tag_green.png) no-repeat left bottom; + background: url(images/tag_green.png) no-repeat left bottom; } #documentation #attribute-method-details .method-detail:hover { - background-color: transparent; - cursor: default; + background-color: transparent; + cursor: default; } #documentation .attribute-access-type { - font-size: 60%; - text-transform: uppercase; - vertical-align: super; - padding: 0 2px; + font-size: 60%; + text-transform: uppercase; + vertical-align: super; + padding: 0 2px; } /* @end */ @@ -429,19 +429,19 @@ ul.link-list .type { /* @group Source Code */ div.method-source-code { - background: #262626; - color: #efefef; - margin: 1em; - padding: 0.5em; - border: 1px dashed #999; - overflow: hidden; + background: #262626; + color: #efefef; + margin: 1em; + padding: 0.5em; + border: 1px dashed #999; + overflow: hidden; } div.method-source-code pre { - background: inherit; - padding: 0; - color: white; - overflow: auto; + background: inherit; + padding: 0; + color: white; + overflow: auto; } /* @group Ruby keyword styles */ @@ -467,51 +467,51 @@ div.method-source-code pre { } .file-popup dl { - font-size: 80%; - padding: 0.75em; - background-color: #dedede; - color: #333; - border: 1px solid #bbb; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; + font-size: 80%; + padding: 0.75em; + background-color: #dedede; + color: #333; + border: 1px solid #bbb; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; } .file dt { - font-weight: bold; - padding-left: 22px; - line-height: 20px; - background: url(images/page_white_width.png) no-repeat left top; + font-weight: bold; + padding-left: 22px; + line-height: 20px; + background: url(images/page_white_width.png) no-repeat left top; } .file dt.modified-date { - background: url(images/date.png) no-repeat left top; + background: url(images/date.png) no-repeat left top; } .file dt.requires { - background: url(images/plugin.png) no-repeat left top; + background: url(images/plugin.png) no-repeat left top; } .file dt.scs-url { - background: url(images/wrench.png) no-repeat left top; + background: url(images/wrench.png) no-repeat left top; } .file dl dd { - margin: 0 0 1em 0; + margin: 0 0 1em 0; } .file #metadata dl dd ul { - list-style: circle; - margin-left: 20px; - padding-top: 0; + list-style: circle; + margin-left: 20px; + padding-top: 0; } .file #metadata dl dd ul li { } .file h2 { - margin-top: 2em; - padding: 0.75em 0.5em; - background-color: #dedede; - color: #333; - font-size: 120%; - border: 1px solid #bbb; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; + margin-top: 2em; + padding: 0.75em 0.5em; + background-color: #dedede; + color: #333; + font-size: 120%; + border: 1px solid #bbb; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; } /* @end */ @@ -521,13 +521,13 @@ div.method-source-code pre { /* @group ThickBox Styles */ #TB_window { - font: 12px Arial, Helvetica, sans-serif; - color: #333333; + font: 12px Arial, Helvetica, sans-serif; + color: #333333; } #TB_secondLine { - font: 10px Arial, Helvetica, sans-serif; - color:#666666; + font: 10px Arial, Helvetica, sans-serif; + color:#666666; } #TB_window :link, @@ -540,147 +540,147 @@ div.method-source-code pre { #TB_window :visited:focus { color: #666666; } #TB_overlay { - position: fixed; - z-index:100; - top: 0px; - left: 0px; - height:100%; - width:100%; + position: fixed; + z-index:100; + top: 0px; + left: 0px; + height:100%; + width:100%; } .TB_overlayMacFFBGHack {background: url(images/macFFBgHack.png) repeat;} .TB_overlayBG { - background-color:#000; - filter:alpha(opacity=75); - -moz-opacity: 0.75; - opacity: 0.75; + background-color:#000; + filter:alpha(opacity=75); + -moz-opacity: 0.75; + opacity: 0.75; } * html #TB_overlay { /* ie6 hack */ - position: absolute; - height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px'); + position: absolute; + height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px'); } #TB_window { - position: fixed; - background: #ffffff; - z-index: 102; - color:#000000; - display:none; - border: 4px solid #525252; - text-align:left; - top:50%; - left:50%; + position: fixed; + background: #ffffff; + z-index: 102; + color:#000000; + display:none; + border: 4px solid #525252; + text-align:left; + top:50%; + left:50%; } * html #TB_window { /* ie6 hack */ -position: absolute; -margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px'); + position: absolute; + margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px'); } #TB_window img#TB_Image { - display:block; - margin: 15px 0 0 15px; - border-right: 1px solid #ccc; - border-bottom: 1px solid #ccc; - border-top: 1px solid #666; - border-left: 1px solid #666; + display:block; + margin: 15px 0 0 15px; + border-right: 1px solid #ccc; + border-bottom: 1px solid #ccc; + border-top: 1px solid #666; + border-left: 1px solid #666; } #TB_caption{ - height:25px; - padding:7px 30px 10px 25px; - float:left; + height:25px; + padding:7px 30px 10px 25px; + float:left; } #TB_closeWindow{ - height:25px; - padding:11px 25px 10px 0; - float:right; + height:25px; + padding:11px 25px 10px 0; + float:right; } #TB_closeAjaxWindow{ - padding:7px 10px 5px 0; - margin-bottom:1px; - text-align:right; - float:right; + padding:7px 10px 5px 0; + margin-bottom:1px; + text-align:right; + float:right; } #TB_ajaxWindowTitle{ - float:left; - padding:7px 0 5px 10px; - margin-bottom:1px; - font-size: 22px; + float:left; + padding:7px 0 5px 10px; + margin-bottom:1px; + font-size: 22px; } #TB_title{ - background-color: #6C8C22; - color: #dedede; - height:40px; + background-color: #6C8C22; + color: #dedede; + height:40px; } #TB_title :link, #TB_title :visited { - color: white !important; - border-bottom: 1px dotted #dedede; + color: white !important; + border-bottom: 1px dotted #dedede; } #TB_ajaxContent{ - clear:both; - padding:2px 15px 15px 15px; - overflow:auto; - text-align:left; - line-height:1.4em; + clear:both; + padding:2px 15px 15px 15px; + overflow:auto; + text-align:left; + line-height:1.4em; } #TB_ajaxContent.TB_modal{ - padding:15px; + padding:15px; } #TB_ajaxContent p{ - padding:5px 0px 5px 0px; + padding:5px 0px 5px 0px; } #TB_load{ - position: fixed; - display:none; - height:13px; - width:208px; - z-index:103; - top: 50%; - left: 50%; - margin: -6px 0 0 -104px; /* -height/2 0 0 -width/2 */ + position: fixed; + display:none; + height:13px; + width:208px; + z-index:103; + top: 50%; + left: 50%; + margin: -6px 0 0 -104px; /* -height/2 0 0 -width/2 */ } * html #TB_load { /* ie6 hack */ -position: absolute; -margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px'); + position: absolute; + margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px'); } #TB_HideSelect{ - z-index:99; - position:fixed; - top: 0; - left: 0; - background-color:#fff; - border:none; - filter:alpha(opacity=0); - -moz-opacity: 0; - opacity: 0; - height:100%; - width:100%; + z-index:99; + position:fixed; + top: 0; + left: 0; + background-color:#fff; + border:none; + filter:alpha(opacity=0); + -moz-opacity: 0; + opacity: 0; + height:100%; + width:100%; } * html #TB_HideSelect { /* ie6 hack */ - position: absolute; - height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px'); + position: absolute; + height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px'); } #TB_iframeContent{ - clear:both; - border:none; - margin-bottom:-1px; - margin-top:1px; - _margin-bottom:1px; + clear:both; + border:none; + margin-bottom:-1px; + margin-top:1px; + _margin-bottom:1px; } /* @end */ @@ -688,17 +688,17 @@ margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = d /* @group Debugging Section */ #debugging-toggle { - text-align: center; + text-align: center; } #debugging-toggle img { - cursor: pointer; + cursor: pointer; } #rdoc-debugging-section-dump { - display: none; - margin: 0 2em 2em; - background: #ccc; - border: 1px solid #999; + display: none; + margin: 0 2em 2em; + background: #ccc; + border: 1px solid #999; } diff --git a/lib/rdoc/include.rb b/lib/rdoc/include.rb index 11a9bdc7ef..9cebd3d8ef 100644 --- a/lib/rdoc/include.rb +++ b/lib/rdoc/include.rb @@ -17,6 +17,7 @@ class RDoc::Include < RDoc::CodeObject super() @name = name self.comment = comment + @module = nil # cache for module if found end ## @@ -52,9 +53,47 @@ class RDoc::Include < RDoc::CodeObject ## # Attempts to locate the included module object. Returns the name if not # known. + # + # The scoping rules of Ruby to resolve the name of an included module are: + # - first look into the children of the current context; + # - if not found, look into the children of included modules, + # in reverse inclusion order; + # - if still not found, go up the hierarchy of names. def module - RDoc::TopLevel.find_module_named(@name) || @name + return @module if @module + + # search the current context + return @name unless parent + full_name = parent.child_name(@name) + @module = RDoc::TopLevel.modules_hash[full_name] + return @module if @module + return @name if @name =~ /^::/ + + # search the includes before this one, in reverse order + searched = parent.includes.take_while { |i| i != self }.reverse + searched.each do |i| + inc = i.module + next if String === inc + full_name = inc.child_name(@name) + @module = RDoc::TopLevel.modules_hash[full_name] + return @module if @module + end + + # go up the hierarchy of names + p = parent.parent + while p + full_name = p.child_name(@name) + @module = RDoc::TopLevel.modules_hash[full_name] + return @module if @module + p = p.parent + end + + @name + end + + def to_s # :nodoc: + "include #@name in: #{parent}" end end diff --git a/lib/rdoc/markup.rb b/lib/rdoc/markup.rb index 32c8179e0d..c8914fea11 100644 --- a/lib/rdoc/markup.rb +++ b/lib/rdoc/markup.rb @@ -59,12 +59,477 @@ require 'rdoc' # # puts "<body>#{wh.convert ARGF.read}</body>" # +# == Encoding +# +# Where Encoding support is available RDoc will automatically convert all +# documents to the same output encoding. The output encoding can be set via +# RDoc::Options#encoding and defaults to Encoding.default_external. +# +# = \RDoc Markup Reference +# +# == Block Markup +# +# === Paragraphs and Verbatim +# +# The markup engine looks for a document's natural left margin. This is +# used as the initial margin for the document. +# +# Consecutive lines starting at this margin are considered to be a +# paragraph. Empty lines separate paragraphs. +# +# Any line that starts to the right of the current margin is treated +# as verbatim text. This is useful for code listings: +# +# 3.times { puts "Ruby" } +# +# In verbatim text, two or more blank lines are collapsed into one, +# and trailing blank lines are removed: +# +# This is the first line +# +# +# This is the second non-blank line, +# after 2 blank lines in the source markup. +# +# +# There were two trailing blank lines right above this paragraph, that +# have been removed. In addition, the verbatim text has been shifted +# left, so the amount of indentation of verbatim text is unimportant. +# +# === Headers and Rules +# +# A line starting with an equal sign (=) is treated as a +# heading. Level one headings have one equals sign, level two headings +# have two, and so on until level six, which is the maximum +# (seven hyphens or more result in a level six heading). +# +# For example, the above header was obtained with: +# == Headers and Rules +# +# A line starting with three or more hyphens (at the current indent) +# generates a horizontal rule. The more hyphens, the thicker the rule +# (within reason, and if supported by the output device). +# +# In the case of HTML output, three dashes generate a 1-pixel high rule, +# four dashes result in 2 pixels, and so on. The actual height is limited +# to 10 pixels: +# +# --- +# ----- +# ----------------------------------------------------- +# +# produces: +# +# --- +# ----- +# ----------------------------------------------------- +# +# === Simple Lists +# +# If a paragraph starts with a "*", "-", "<digit>." or "<letter>.", +# then it is taken to be the start of a list. The margin in increased to be +# the first non-space following the list start flag. Subsequent lines +# should be indented to this new margin until the list ends. For example: +# +# * this is a list with three paragraphs in +# the first item. This is the first paragraph. +# +# And this is the second paragraph. +# +# 1. This is an indented, numbered list. +# 2. This is the second item in that list +# +# This is the third conventional paragraph in the +# first list item. +# +# * This is the second item in the original list +# +# produces: +# +# * this is a list with three paragraphs in +# the first item. This is the first paragraph. +# +# And this is the second paragraph. +# +# 1. This is an indented, numbered list. +# 2. This is the second item in that list +# +# This is the third conventional paragraph in the +# first list item. +# +# * This is the second item in the original list +# +# === Labeled Lists +# +# You can also construct labeled lists, sometimes called description +# or definition lists. Do this by putting the label in square brackets +# and indenting the list body: +# +# [cat] a small furry mammal +# that seems to sleep a lot +# +# [ant] a little insect that is known +# to enjoy picnics +# +# produces: +# +# [cat] a small furry mammal +# that seems to sleep a lot +# +# [ant] a little insect that is known +# to enjoy picnics +# +# If you want the list bodies to line up to the left of the labels, +# use two colons: +# +# cat:: a small furry mammal +# that seems to sleep a lot +# +# ant:: a little insect that is known +# to enjoy picnics +# +# produces: +# +# cat:: a small furry mammal +# that seems to sleep a lot +# +# ant:: a little insect that is known +# to enjoy picnics +# +# Notice that blank lines right after the label are ignored in labeled lists: +# +# [one] +# +# definition 1 +# +# [two] +# +# definition 2 +# +# produces the same output as +# +# [one] definition 1 +# [two] definition 2 +# +# +# === Lists and Verbatim +# +# If you want to introduce a verbatim section right after a list, it has to be +# less indented than the list item bodies, but more indented than the list +# label, letter, digit or bullet. For instance: +# +# * point 1 +# +# * point 2, first paragraph +# +# point 2, second paragraph +# verbatim text inside point 2 +# point 2, third paragraph +# verbatim text outside of the list (the list is therefore closed) +# regular paragraph after the list +# +# produces: +# +# * point 1 +# +# * point 2, first paragraph +# +# point 2, second paragraph +# verbatim text inside point 2 +# point 2, third paragraph +# verbatim text outside of the list (the list is therefore closed) +# regular paragraph after the list +# +# +# == Text Markup +# +# === Bold, Italic, Typewriter Text +# +# You can use markup within text (except verbatim) to change the +# appearance of parts of that text. Out of the box, RDoc::Markup +# supports word-based and general markup. +# +# Word-based markup uses flag characters around individual words: +# +# <tt>\*_word_\*</tt>:: displays _word_ in a *bold* font +# <tt>\__word_\_</tt>:: displays _word_ in an _emphasized_ font +# <tt>\+_word_\+</tt>:: displays _word_ in a +code+ font +# +# General markup affects text between a start delimiter and an end +# delimiter. Not surprisingly, these delimiters look like HTML markup. +# +# <tt>\<b>_text_</b></tt>:: displays _text_ in a *bold* font +# <tt>\<em>_text_</em></tt>:: displays _text_ in an _emphasized_ font +# (alternate tag: <tt>\<i></tt>) +# <tt>\<tt>_text_\</tt></tt>:: displays _text_ in a +code+ font +# (alternate tag: <tt>\<code></tt>) +# +# Unlike conventional Wiki markup, general markup can cross line +# boundaries. You can turn off the interpretation of markup by +# preceding the first character with a backslash (see <i>Escaping +# Text Markup</i>, below). +# +# === Hyperlinks +# +# Hyperlinks to the web starting with +http:+, +mailto:+, +ftp:+ or +www.+ +# are recognized. An HTTP url that references an external image file is +# converted into an inline <img...>. Hyperlinks starting with +link:+ are +# assumed to refer to local files whose path is relative to the <tt>--op</tt> +# directory. +# +# Hyperlinks can also be of the form _label_[_url_], in which +# case _label_ is used in the displayed text, and _url_ is +# used as the target. If _label_ contains multiple words, +# put it in braces: {<em>multi word label</em>}[url]. +# +# Example hyperlinks: +# +# link:RDoc.html +# http://rdoc.rubyforge.org +# mailto:user@example.com +# {RDoc Documentation}[http://rdoc.rubyforge.org] +# {RDoc Markup}[link:RDoc/Markup.html] +# +# === Escaping Text Markup +# +# Text markup can be escaped with a backslash, as in \<tt>, which was obtained +# with "<tt>\\<tt></tt>". Except in verbatim sections and between \<tt> tags, +# to produce a backslash, you have to double it unless it is followed by a +# space, tab or newline. Otherwise, the HTML formatter will discard it, as it +# is used to escape potential hyperlinks: +# +# * The \ must be doubled if not followed by white space: \\. +# * But not in \<tt> tags: in a Regexp, <tt>\S</tt> matches non-space. +# * This is a link to {ruby-lang}[www.ruby-lang.org]. +# * This is not a link, however: \{ruby-lang.org}[www.ruby-lang.org]. +# * This will not be hyperlinked to \RDoc::RDoc#document +# +# generates: +# +# * The \ must be doubled if not followed by white space: \\. +# * But not in \<tt> tags: in a Regexp, <tt>\S</tt> matches non-space. +# * This is a link to {ruby-lang}[www.ruby-lang.org] +# * This is not a link, however: \{ruby-lang.org}[www.ruby-lang.org] +# * This will not be hyperlinked to \RDoc::RDoc#document +# +# Inside \<tt> tags, more precisely, leading backslashes are removed +# only if followed by a markup character (<tt><*_+</tt>), a backslash, +# or a known hyperlink reference (a known class or method). So in the +# example above, the backslash of <tt>\S</tt> would be removed +# if there was a class or module named +S+ in the current context. +# +# This behavior is inherited from RDoc version 1, and has been kept +# for compatibility with existing RDoc documentation. +# +# === Conversion of characters +# +# HTML will convert two/three dashes to an em-dash. Other common characters are +# converted as well: +# +# em-dash:: -- or --- +# ellipsis:: ... +# +# single quotes:: 'text' or `text' +# double quotes:: "text" or ``text'' +# +# copyright:: (c) +# registered trademark:: (r) +# +# produces: +# +# em-dash:: -- or --- +# ellipsis:: ... +# +# single quotes:: 'text' or `text' +# double quotes:: "text" or ``text'' +# +# copyright:: (c) +# registered trademark:: (r) +# +# +# == Documenting Source Code +# +# Comment blocks can be written fairly naturally, either using <tt>#</tt> on +# successive lines of the comment, or by including the comment in +# a <tt>=begin</tt>/<tt>=end</tt> block. If you use the latter form, +# the <tt>=begin</tt> line _must_ be flagged with an +rdoc+ tag: +# +# =begin rdoc +# Documentation to be processed by RDoc. +# +# ... +# =end +# +# RDoc stops processing comments if it finds a comment line starting +# with <tt>--</tt> right after the <tt>#</tt> character (otherwise, +# it will be treated as a rule if it has three dashes or more). +# 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 with <tt>++</tt>. +# +# ## +# # 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) +# # ... +# end +# +# Names of classes, files, and any method names containing an +# underscore or preceded by a hash character are automatically hyperlinked +# from comment text to their description. This hyperlinking works inside +# the current class or module, and with ancestor methods (in included modules +# or in the superclass). +# +# 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| ... } +# +# +:yields:+ is an example of a documentation directive. These appear +# immediately after the start of the document element they are modifying. +# +# RDoc automatically cross-references words with underscores or camel-case. +# To suppress cross-references, prefix the word with a \ character. To +# include special characters like "<tt>\n</tt>", you'll need to use +# two \ characters in normal text, but only one in \<tt> text: +# +# "\\n" or "<tt>\n</tt>" +# +# produces: +# +# "\\n" or "<tt>\n</tt>" +# +# == Directives +# +# Directives are keywords surrounded by ":" characters. +# +# === Controlling what is documented +# +# [+:nodoc:+ / <tt>:nodoc: all</tt>] +# This directive prevents documentation for the element from +# being generated. For classes and modules, the methods, aliases, +# constants, and attributes directly within the affected class or +# module also will 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 MyModule # :nodoc: +# class Input +# end +# end +# +# module OtherModule # :nodoc: all +# class Output +# end +# end +# +# In the above code, only class <tt>MyModule::Input</tt> will be documented. +# +# The +:nodoc:+ directive, like +:enddoc:+, +:stopdoc:+ and +:startdoc:+ +# presented below, is local to the current file: if you do not want to +# document a module that appears in several files, specify +:nodoc:+ on each +# appearance, at least once per file. +# +# [+: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 specify a +:startdoc:+ by the end of the container, +# disables documentation for the rest of the current file. +# +# [+:doc:+] +# Forces a method or attribute to be documented even if it wouldn't be +# otherwise. Useful if, for example, you want to include documentation of a +# particular private method. +# +# [+:enddoc:+] +# Document nothing further at the current level: directives +:startdoc:+ and +# +:doc:+ that appear after this will not be honored for the current container +# (file, class or module), in the current file. +# +# [+:notnew:+ / +:not_new:+ / +:not-new:+ ] +# 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:+ directive stops this. Remember that +initialize+ is private, +# so you won't see the documentation unless you use the +-a+ command line +# option. +# +# === Other directives +# +# [+:include:+ _filename_] +# Include the contents of the named file at this point. This directive +# must appear alone on one line, possibly preceded by spaces. In this +# position, it can be escapd with a \ in front of the first colon. +# +# 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 <tt>--title</tt> +# command line parameter. (The command line parameter overrides any :title: +# directive in the source). +# +# [+:main:+ _name_] +# Equivalent to the <tt>--main</tt> command line parameter. +# +# [<tt>:section: title</tt>] +# Starts a new section in the output. The title following +:section:+ is +# used as the section heading, and the remainder of the comment containing +# the section is used as introductory text. Subsequent methods, aliases, +# attributes, and classes will be documented in this section. A :section: +# comment block may have one or more lines before the :section: directive. +# These will be removed, and any identical lines at the end of the block are +# also removed. This allows you to add visual cues such as: +# +# # ---------------------------------------- +# # :section: My Section +# # This is the section that I wrote. +# # See it glisten in the noon-day sun. +# # ---------------------------------------- +# +# <i>Note: Current formatters to not take sections into account.</i> +# +# [+:call-seq:+] +# Lines up to the next blank line in the comment are treated as the method's +# calling sequence, overriding the default parsing of method parameters and +# yield arguments. +# +# Further directives can be found in RDoc::Parser::Ruby and RDoc::Parser::C. #-- -# Author:: Dave Thomas, dave@pragmaticprogrammer.com -# License:: Ruby license +# Original Author:: Dave Thomas, dave@pragmaticprogrammer.com +# License:: Ruby license class RDoc::Markup + ## + # An AttributeManager which handles inline markup. + attr_reader :attribute_manager ## diff --git a/lib/rdoc/markup/attribute_manager.rb b/lib/rdoc/markup/attribute_manager.rb index e86e7f6812..2ee243ab0b 100644 --- a/lib/rdoc/markup/attribute_manager.rb +++ b/lib/rdoc/markup/attribute_manager.rb @@ -15,9 +15,12 @@ class RDoc::Markup::AttributeManager # optimistic #++ - A_PROTECT = 004 # :nodoc: + A_PROTECT = 004 # :nodoc: - PROTECT_ATTR = A_PROTECT.chr # :nodoc: + ## + # Special mask character to prevent inline markup handling + + PROTECT_ATTR = A_PROTECT.chr # :nodoc: ## # This maps delimiters that occur around words (such as *bold* or +tt+) @@ -56,7 +59,7 @@ class RDoc::Markup::AttributeManager def initialize @html_tags = {} @matching_word_pairs = {} - @protectable = %w[<\\] + @protectable = %w[<] @special = {} @word_pair_map = {} @@ -79,12 +82,19 @@ class RDoc::Markup::AttributeManager RDoc::Markup::AttrChanger.new turn_on, turn_off end - def change_attribute(current, new) + ## + # Changes the current attribute from +current+ to +new+ + + def change_attribute current, new diff = current ^ new attribute(new & diff, current & diff) end - def changed_attribute_by_name(current_set, new_set) + ## + # Used by the tests to change attributes by name from +current_set+ to + # +new_set+ + + def changed_attribute_by_name current_set, new_set current = new = 0 current_set.each do |name| current |= RDoc::Markup::Attribute.bitmap_for(name) @@ -97,6 +107,9 @@ class RDoc::Markup::AttributeManager change_attribute(current, new) end + ## + # Copies +start_pos+ to +end_pos+ from the current string + def copy_string(start_pos, end_pos) res = @str[start_pos...end_pos] res.gsub!(/\000/, '') @@ -112,7 +125,7 @@ class RDoc::Markup::AttributeManager # first do matching ones tags = @matching_word_pairs.keys.join("") - re = /(^|[^\w#{NULL}])([#{tags}])([#:\\]?[\w.\/-]+?\S?)\2(\W|$)/ + re = /(^|\W)([#{tags}])([#:\\]?[\w.\/-]+?\S?)\2(\W|$)/ 1 while str.gsub!(re) do attr = @matching_word_pairs[$2] @@ -164,6 +177,9 @@ class RDoc::Markup::AttributeManager # Escapes special sequences of text to prevent conversion to RDoc def mask_protected_sequences + # protect __send__, __FILE__, etc. + @str.gsub!(/__([a-z]+)__/i, + "_#{PROTECT_ATTR}_#{PROTECT_ATTR}\\1_#{PROTECT_ATTR}_#{PROTECT_ATTR}") @str.gsub!(/\\([#{Regexp.escape @protectable.join('')}])/, "\\1#{PROTECT_ATTR}") end @@ -228,8 +244,8 @@ class RDoc::Markup::AttributeManager @attrs = RDoc::Markup::AttrSpan.new @str.length - convert_html @str, @attrs convert_attrs @str, @attrs + convert_html @str, @attrs convert_specials @str, @attrs unmask_protected_sequences @@ -262,6 +278,9 @@ class RDoc::Markup::AttributeManager end end + ## + # Splits the string into chunks by attribute change + def split_into_flow res = [] current_attr = 0 diff --git a/lib/rdoc/markup/blank_line.rb b/lib/rdoc/markup/blank_line.rb index a8c07c8e57..5da0ac8d81 100644 --- a/lib/rdoc/markup/blank_line.rb +++ b/lib/rdoc/markup/blank_line.rb @@ -1,12 +1,20 @@ ## -# An empty line +# An empty line. This class is a singleton. class RDoc::Markup::BlankLine - def == other # :nodoc: - self.class == other.class + @instance = new + + ## + # RDoc::Markup::BlankLine is a singleton + + def self.new + @instance end + ## + # Calls #accept_blank_line on +visitor+ + def accept visitor visitor.accept_blank_line self end diff --git a/lib/rdoc/markup/document.rb b/lib/rdoc/markup/document.rb index 7963e9afe1..688e8e822e 100644 --- a/lib/rdoc/markup/document.rb +++ b/lib/rdoc/markup/document.rb @@ -39,6 +39,9 @@ class RDoc::Markup::Document self.class == other.class and @parts == other.parts end + ## + # Runs this document and all its #items through +visitor+ + def accept visitor visitor.start_accepting @@ -49,6 +52,9 @@ class RDoc::Markup::Document visitor.end_accepting end + ## + # Does this document have no parts? + def empty? @parts.empty? end diff --git a/lib/rdoc/markup/formatter.rb b/lib/rdoc/markup/formatter.rb index 993e523f0c..9308954de1 100644 --- a/lib/rdoc/markup/formatter.rb +++ b/lib/rdoc/markup/formatter.rb @@ -7,6 +7,10 @@ require 'rdoc/markup' class RDoc::Markup::Formatter + ## + # Tag for inline markup containing a +bit+ for the bitmask and the +on+ and + # +off+ triggers. + InlineTag = Struct.new(:bit, :on, :off) ## @@ -101,6 +105,9 @@ class RDoc::Markup::Formatter @in_tt > 0 end + ## + # Turns on tags for +item+ on +res+ + def on_tags res, item attr_mask = item.turn_on return if attr_mask.zero? @@ -113,6 +120,9 @@ class RDoc::Markup::Formatter end end + ## + # Turns off tags for +item+ on +res+ + def off_tags res, item attr_mask = item.turn_off return if attr_mask.zero? diff --git a/lib/rdoc/markup/formatter_test_case.rb b/lib/rdoc/markup/formatter_test_case.rb index 26c8d63332..dd755c55d1 100644 --- a/lib/rdoc/markup/formatter_test_case.rb +++ b/lib/rdoc/markup/formatter_test_case.rb @@ -4,14 +4,57 @@ require 'rdoc/markup/formatter' ## # Test case for creating new RDoc::Markup formatters. See # test/test_rdoc_markup_to_*.rb for examples. +# +# This test case adds a variety of tests to your subclass when +# #add_visitor_tests is called. Most tests set up a scenario then call a +# method you will provide to perform the assertion on the output. +# +# Your subclass must instantiate a visitor and assign it to <tt>@to</tt>. +# +# For example, test_accept_blank_line sets up a RDoc::Markup::BlockLine then +# calls accept_blank_line on your visitor. You are responsible for asserting +# that the output is correct. +# +# Example: +# +# class TestRDocMarkupToNewFormat < RDoc::Markup::FormatterTestCase +# +# add_visitor_tests +# +# def setup +# super +# +# @to = RDoc::Markup::ToNewFormat.new +# end +# +# def accept_blank_line +# assert_equal :junk, @to.res.join +# end +# +# # ... +# +# end class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase + ## + # Call #setup when inheriting from this test case. + # + # Provides the following instance variables: + # + # +@m+:: RDoc::Markup.new + # +@RM+:: RDoc::Markup # to reduce typing + # +@bullet_list+:: @RM::List.new :BULLET, # ... + # +@label_list+:: @RM::List.new :LABEL, # ... + # +@lalpha_list+:: @RM::List.new :LALPHA, # ... + # +@note_list+:: @RM::List.new :NOTE, # ... + # +@number_list+:: @RM::List.new :NUMBER, # ... + # +@ualpha_list+:: @RM::List.new :UALPHA, # ... + def setup super @m = RDoc::Markup.new - @am = RDoc::Markup::AttributeManager.new @RM = RDoc::Markup @bullet_list = @RM::List.new(:BULLET, @@ -39,14 +82,25 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase @RM::ListItem.new(nil, @RM::Paragraph.new('l2'))) end + ## + # Call to add the visitor tests to your test case + def self.add_visitor_tests self.class_eval do + + ## + # Calls start_accepting which needs to verify startup state + def test_start_accepting @to.start_accepting start_accepting end + ## + # Calls end_accepting on your test case which needs to call + # <tt>@to.end_accepting</tt> and verify document generation + def test_end_accepting @to.start_accepting @to.res << 'hi' @@ -54,6 +108,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase end_accepting end + ## + # Calls accept_blank_line + def test_accept_blank_line @to.start_accepting @@ -62,6 +119,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_blank_line end + ## + # Calls accept_heading with a level 5 RDoc::Markup::Heading + def test_accept_heading @to.start_accepting @@ -70,6 +130,79 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_heading end + ## + # Calls accept_heading_1 with a level 1 RDoc::Markup::Heading + + def test_accept_heading_1 + @to.start_accepting + + @to.accept_heading @RM::Heading.new(1, 'Hello') + + accept_heading_1 + end + + ## + # Calls accept_heading_2 with a level 2 RDoc::Markup::Heading + + def test_accept_heading_2 + @to.start_accepting + + @to.accept_heading @RM::Heading.new(2, 'Hello') + + accept_heading_2 + end + + ## + # Calls accept_heading_3 with a level 3 RDoc::Markup::Heading + + def test_accept_heading_3 + # HACK this doesn't belong here + skip "No String#chars, upgrade your ruby" unless ''.respond_to? :chars + + @to.start_accepting + + @to.accept_heading @RM::Heading.new(3, 'Hello') + + accept_heading_3 + end + + ## + # Calls accept_heading_4 with a level 4 RDoc::Markup::Heading + + def test_accept_heading_4 + @to.start_accepting + + @to.accept_heading @RM::Heading.new(4, 'Hello') + + accept_heading_4 + end + + ## + # Calls accept_heading_b with a bold level 1 RDoc::Markup::Heading + + def test_accept_heading_b + @to.start_accepting + + @to.accept_heading @RM::Heading.new(1, '*Hello*') + + accept_heading_b + end + + ## + # Calls accept_heading_suppressed_crossref with a level 1 + # RDoc::Markup::Heading containing a suppressed crossref + + def test_accept_heading_suppressed_crossref # HACK to_html_crossref test + @to.start_accepting + + @to.accept_heading @RM::Heading.new(1, '\\Hello') + + accept_heading_suppressed_crossref + end + + ## + # Calls accept_paragraph + def test_accept_paragraph @to.start_accepting @@ -78,15 +211,80 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_paragraph end + ## + # Calls accept_paragraph_b with a RDoc::Markup::Paragraph containing + # bold words + + def test_accept_paragraph_b + @to.start_accepting + + @to.accept_paragraph @RM::Paragraph.new('reg <b>bold words</b> reg') + + accept_paragraph_b + end + + ## + # Calls accept_paragraph_i with a RDoc::Markup::Paragraph containing + # emphasized words + + def test_accept_paragraph_i + @to.start_accepting + + @to.accept_paragraph @RM::Paragraph.new('reg <em>italic words</em> reg') + + accept_paragraph_i + end + + ## + # Calls accept_paragraph_plus with a RDoc::Markup::Paragraph containing + # teletype words + + def test_accept_paragraph_plus + @to.start_accepting + + @to.accept_paragraph @RM::Paragraph.new('reg +teletype+ reg') + + accept_paragraph_plus + end + + ## + # Calls accept_paragraph_star with a RDoc::Markup::Paragraph containing + # bold words + + def test_accept_paragraph_star + @to.start_accepting + + @to.accept_paragraph @RM::Paragraph.new('reg *bold* reg') + + accept_paragraph_star + end + + ## + # Calls accept_paragraph_underscore with a RDoc::Markup::Paragraph + # containing emphasized words + + def test_accept_paragraph_underscore + @to.start_accepting + + @to.accept_paragraph @RM::Paragraph.new('reg _italic_ reg') + + accept_paragraph_underscore + end + + ## + # Calls accept_verbatim with a RDoc::Markup::Verbatim + def test_accept_verbatim @to.start_accepting - @to.accept_verbatim @RM::Verbatim.new(' ', 'hi', "\n", - ' ', 'world', "\n") + @to.accept_verbatim @RM::Verbatim.new("hi\n", " world\n") accept_verbatim end + ## + # Calls accept_raw with a RDoc::Markup::Raw + def test_accept_raw @to.start_accepting @@ -99,6 +297,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_raw end + ## + # Calls accept_rule with a RDoc::Markup::Rule + def test_accept_rule @to.start_accepting @@ -107,6 +308,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_rule end + ## + # Calls accept_list_item_start_bullet + def test_accept_list_item_start_bullet @to.start_accepting @@ -117,6 +321,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_start_bullet end + ## + # Calls accept_list_item_start_label + def test_accept_list_item_start_label @to.start_accepting @@ -127,6 +334,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_start_label end + ## + # Calls accept_list_item_start_lalpha + def test_accept_list_item_start_lalpha @to.start_accepting @@ -137,6 +347,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_start_lalpha end + ## + # Calls accept_list_item_start_note + def test_accept_list_item_start_note @to.start_accepting @@ -147,6 +360,26 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_start_note end + ## + # Calls accept_list_item_start_note_2 + + def test_accept_list_item_start_note_2 + list = @RM::List.new(:NOTE, + @RM::ListItem.new('<tt>teletype</tt>', + @RM::Paragraph.new('teletype description'))) + + @to.start_accepting + + list.accept @to + + @to.end_accepting + + accept_list_item_start_note_2 + end + + ## + # Calls accept_list_item_start_number + def test_accept_list_item_start_number @to.start_accepting @@ -157,6 +390,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_start_number end + ## + # Calls accept_list_item_start_ualpha + def test_accept_list_item_start_ualpha @to.start_accepting @@ -167,6 +403,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_start_ualpha end + ## + # Calls accept_list_item_end_bullet + def test_accept_list_item_end_bullet @to.start_accepting @@ -179,6 +418,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_end_bullet end + ## + # Calls accept_list_item_end_label + def test_accept_list_item_end_label @to.start_accepting @@ -191,6 +433,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_end_label end + ## + # Calls accept_list_item_end_lalpha + def test_accept_list_item_end_lalpha @to.start_accepting @@ -203,6 +448,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_end_lalpha end + ## + # Calls accept_list_item_end_note + def test_accept_list_item_end_note @to.start_accepting @@ -215,6 +463,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_end_note end + ## + # Calls accept_list_item_end_number + def test_accept_list_item_end_number @to.start_accepting @@ -227,6 +478,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_end_number end + ## + # Calls accept_list_item_end_ualpha + def test_accept_list_item_end_ualpha @to.start_accepting @@ -239,6 +493,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_item_end_ualpha end + ## + # Calls accept_list_start_bullet + def test_accept_list_start_bullet @to.start_accepting @@ -247,6 +504,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_start_bullet end + ## + # Calls accept_list_start_label + def test_accept_list_start_label @to.start_accepting @@ -255,6 +515,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_start_label end + ## + # Calls accept_list_start_lalpha + def test_accept_list_start_lalpha @to.start_accepting @@ -263,6 +526,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_start_lalpha end + ## + # Calls accept_list_start_note + def test_accept_list_start_note @to.start_accepting @@ -271,6 +537,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_start_note end + ## + # Calls accept_list_start_number + def test_accept_list_start_number @to.start_accepting @@ -279,6 +548,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_start_number end + ## + # Calls accept_list_start_ualpha + def test_accept_list_start_ualpha @to.start_accepting @@ -287,6 +559,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_start_ualpha end + ## + # Calls accept_list_end_bullet + def test_accept_list_end_bullet @to.start_accepting @@ -297,6 +572,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_end_bullet end + ## + # Calls accept_list_end_label + def test_accept_list_end_label @to.start_accepting @@ -307,6 +585,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_end_label end + ## + # Calls accept_list_end_lalpha + def test_accept_list_end_lalpha @to.start_accepting @@ -317,6 +598,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_end_lalpha end + ## + # Calls accept_list_end_number + def test_accept_list_end_number @to.start_accepting @@ -327,6 +611,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_end_number end + ## + # Calls accept_list_end_note + def test_accept_list_end_note @to.start_accepting @@ -337,6 +624,9 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_end_note end + ## + # Calls accept_list_end_ulpha + def test_accept_list_end_ualpha @to.start_accepting @@ -346,6 +636,52 @@ class RDoc::Markup::FormatterTestCase < MiniTest::Unit::TestCase accept_list_end_ualpha end + + ## + # Calls list_nested with a two-level list + + def test_list_nested + doc = @RM::Document.new( + @RM::List.new(:BULLET, + @RM::ListItem.new(nil, + @RM::Paragraph.new('l1'), + @RM::List.new(:BULLET, + @RM::ListItem.new(nil, + @RM::Paragraph.new('l1.1')))), + @RM::ListItem.new(nil, + @RM::Paragraph.new('l2')))) + + doc.accept @to + + list_nested + end + + ## + # Calls list_verbatim with a list containing a verbatim block + + def test_list_verbatim # HACK overblown + doc = @RM::Document.new( + @RM::List.new(:BULLET, + @RM::ListItem.new(nil, + @RM::Paragraph.new('list', 'stuff'), + @RM::BlankLine.new, + @RM::Verbatim.new("* list\n", + " with\n", + "\n", + " second\n", + "\n", + " 1. indented\n", + " 2. numbered\n", + "\n", + " third\n", + "\n", + "* second\n")))) + + doc.accept @to + + list_verbatim + end + end end diff --git a/lib/rdoc/markup/heading.rb b/lib/rdoc/markup/heading.rb index 21e2574d68..081d637729 100644 --- a/lib/rdoc/markup/heading.rb +++ b/lib/rdoc/markup/heading.rb @@ -3,6 +3,9 @@ class RDoc::Markup::Heading < Struct.new :level, :text + ## + # Calls #accept_heading on +wisitor+ + def accept visitor visitor.accept_heading self end diff --git a/lib/rdoc/markup/inline.rb b/lib/rdoc/markup/inline.rb index 1b5eac45ae..f5bf98a071 100644 --- a/lib/rdoc/markup/inline.rb +++ b/lib/rdoc/markup/inline.rb @@ -1,3 +1,4 @@ +require 'rdoc' class RDoc::Markup ## @@ -14,6 +15,9 @@ class RDoc::Markup @@name_to_bitmap = { :_SPECIAL_ => SPECIAL } @@next_bitmap = 2 + ## + # Returns a unique bit for +name+ + def self.bitmap_for(name) bitmap = @@name_to_bitmap[name] unless bitmap then @@ -24,6 +28,9 @@ class RDoc::Markup bitmap end + ## + # Returns a string reperesentation of +bitmap+ + def self.as_string(bitmap) return "none" if bitmap.zero? res = [] @@ -33,6 +40,9 @@ class RDoc::Markup res.join(",") end + ## + # yields each attribute name in +bitmap+ + def self.each_name_of(bitmap) @@name_to_bitmap.each do |name, bit| next if bit == SPECIAL @@ -75,7 +85,7 @@ class RDoc::Markup end ## - # Acccesses flags for character +n+ + # Accesses flags for character +n+ def [](n) @attrs[n] diff --git a/lib/rdoc/markup/list.rb b/lib/rdoc/markup/list.rb index 75326ed836..820b4c9645 100644 --- a/lib/rdoc/markup/list.rb +++ b/lib/rdoc/markup/list.rb @@ -35,6 +35,9 @@ class RDoc::Markup::List @items == other.items end + ## + # Runs this list and all its #items through +visitor+ + def accept visitor visitor.accept_list_start self diff --git a/lib/rdoc/markup/list_item.rb b/lib/rdoc/markup/list_item.rb index 500e814fe1..d719c352ec 100644 --- a/lib/rdoc/markup/list_item.rb +++ b/lib/rdoc/markup/list_item.rb @@ -35,6 +35,9 @@ class RDoc::Markup::ListItem @parts == other.parts end + ## + # Runs this list item and all its #parts through +visitor+ + def accept visitor visitor.accept_list_item_start self diff --git a/lib/rdoc/markup/paragraph.rb b/lib/rdoc/markup/paragraph.rb index a9923ed24d..808430d576 100644 --- a/lib/rdoc/markup/paragraph.rb +++ b/lib/rdoc/markup/paragraph.rb @@ -3,6 +3,9 @@ class RDoc::Markup::Paragraph < RDoc::Markup::Raw + ## + # Calls #accept_paragraph on +visitor+ + def accept visitor visitor.accept_paragraph self end diff --git a/lib/rdoc/markup/parser.rb b/lib/rdoc/markup/parser.rb index 9fba69dc29..ea02ee3c5b 100644 --- a/lib/rdoc/markup/parser.rb +++ b/lib/rdoc/markup/parser.rb @@ -52,13 +52,13 @@ class RDoc::Markup::Parser attr_reader :tokens ## - # Parsers +str+ into a Document + # Parses +str+ into a Document def self.parse str parser = new - #parser.debug = true parser.tokenize str - RDoc::Markup::Document.new(*parser.parse) + doc = RDoc::Markup::Document.new + parser.parse doc end ## @@ -86,6 +86,7 @@ class RDoc::Markup::Parser # Builds a Heading of +level+ def build_heading level + _, text, = get # TEXT heading = RDoc::Markup::Heading.new level, text skip :NEWLINE @@ -105,38 +106,69 @@ class RDoc::Markup::Parser case type when :BULLET, :LABEL, :LALPHA, :NOTE, :NUMBER, :UALPHA then - list_type = type - if column < margin then + if column < margin || (list.type && list.type != type) then unget break end - if list.type and list.type != list_type then - unget - break - end - - list.type = list_type + list.type = type + peek_type, _, column, = peek_token case type when :NOTE, :LABEL then - _, indent, = get # SPACE - if :NEWLINE == peek_token.first then - get - peek_type, new_indent, peek_column, = peek_token - indent = new_indent if - peek_type == :INDENT and peek_column >= column - unget + if peek_type == :NEWLINE then + # description not on the same line as LABEL/NOTE + # skip the trailing newline & any blank lines below + while peek_type == :NEWLINE + get + peek_type, _, column, = peek_token + end + + # we may be: + # - at end of stream + # - at a column < margin: + # [text] + # blah blah blah + # - at the same column, but with a different type of list item + # [text] + # * blah blah + # - at the same column, with the same type of list item + # [one] + # [two] + # In all cases, we have an empty description. + # In the last case only, we continue. + if peek_type.nil? || column < margin then + empty = 1 + elsif column == margin then + case peek_type + when type + empty = 2 # continue + when *LIST_TOKENS + empty = 1 + else + empty = 0 + end + else + empty = 0 + end + + if empty > 0 then + item = RDoc::Markup::ListItem.new(data) + item << RDoc::Markup::BlankLine.new + list << item + break if empty == 1 + next + end end else data = nil - _, indent, = get end - list_item = build_list_item(margin + indent, data) + list_item = RDoc::Markup::ListItem.new data + parse list_item, column + list << list_item - list << list_item if list_item else unget break @@ -151,54 +183,6 @@ class RDoc::Markup::Parser end ## - # Builds a ListItem that is flush to +indent+ with type +item_type+ - - def build_list_item indent, item_type = nil - p :list_item_start => [indent, item_type] if @debug - - list_item = RDoc::Markup::ListItem.new item_type - - until @tokens.empty? do - type, data, column = get - - if column < indent and - not type == :NEWLINE and - (type != :INDENT or data < indent) then - unget - break - end - - case type - when :INDENT then - unget - list_item.push(*parse(indent)) - when :TEXT then - unget - list_item << build_paragraph(indent) - when :HEADER then - list_item << build_heading(data) - when :NEWLINE then - list_item << RDoc::Markup::BlankLine.new - when *LIST_TOKENS then - unget - list_item << build_list(column) - else - raise ParseError, "Unhandled token #{@current_token.inspect}" - end - end - - p :list_item_end => [indent, item_type] if @debug - - return nil if list_item.empty? - - list_item.parts.shift if - RDoc::Markup::BlankLine === list_item.parts.first and - list_item.length > 1 - - list_item - end - - ## # Builds a Paragraph that is flush to +margin+ def build_paragraph margin @@ -209,18 +193,7 @@ class RDoc::Markup::Parser until @tokens.empty? do type, data, column, = get - case type - when :INDENT then - next if data == margin and peek_token[0] == :TEXT - - unget - break - when :TEXT then - if column != margin then - unget - break - end - + if type == :TEXT && column == margin then paragraph << data skip :NEWLINE else @@ -235,67 +208,81 @@ class RDoc::Markup::Parser end ## - # Builds a Verbatim that is flush to +margin+ + # Builds a Verbatim that is indented from +margin+. + # + # The verbatim block is shifted left (the least indented lines start in + # column 0). Each part of the verbatim is one line of text, always + # terminated by a newline. Blank lines always consist of a single newline + # character, and there is never a single newline at the end of the verbatim. def build_verbatim margin p :verbatim_begin => margin if @debug verbatim = RDoc::Markup::Verbatim.new + min_indent = nil + generate_leading_spaces = true + line = '' + until @tokens.empty? do type, data, column, = get - case type - when :INDENT then - if margin >= data then - unget - break - end + if type == :NEWLINE then + line << data + verbatim << line + line = '' + generate_leading_spaces = true + next + end - indent = data - margin + if column <= margin + unget + break + end - verbatim << ' ' * indent - when :HEADER then - verbatim << '=' * data + if generate_leading_spaces then + indent = column - margin + line << ' ' * indent + min_indent = indent if min_indent.nil? || indent < min_indent + generate_leading_spaces = false + end + case type + when :HEADER then + line << '=' * data _, _, peek_column, = peek_token peek_column ||= column + data - verbatim << ' ' * (peek_column - column - data) + indent = peek_column - column - data + line << ' ' * indent when :RULE then width = 2 + data - verbatim << '-' * width - + line << '-' * width _, _, peek_column, = peek_token - peek_column ||= column + data + 2 - verbatim << ' ' * (peek_column - column - width) + peek_column ||= column + width + indent = peek_column - column - width + line << ' ' * indent when :TEXT then - verbatim << data - when *LIST_TOKENS then - if column <= margin then - unget - break - end - + line << data + else # *LIST_TOKENS list_marker = case type - when :BULLET then '*' - when :LABEL then "[#{data}]" - when :LALPHA, :NUMBER, :UALPHA then "#{data}." - when :NOTE then "#{data}::" + when :BULLET then data + when :LABEL then "[#{data}]" + when :NOTE then "#{data}::" + else # :LALPHA, :NUMBER, :UALPHA + "#{data}." end - - verbatim << list_marker - - _, data, = get - - verbatim << ' ' * (data - list_marker.length) - when :NEWLINE then - verbatim << data - break unless [:INDENT, :NEWLINE].include? peek_token[0] - else - unget - break + line << list_marker + peek_type, _, peek_column = peek_token + unless peek_type == :NEWLINE then + peek_column ||= column + list_marker.length + indent = peek_column - column - list_marker.length + line << ' ' * indent + end end + end + verbatim << line << "\n" unless line.empty? + verbatim.parts.each { |p| p.slice!(0, min_indent) unless p == "\n" } if min_indent > 0 verbatim.normalize p :verbatim_end => margin if @debug @@ -313,65 +300,60 @@ class RDoc::Markup::Parser end ## - # Parses the tokens into a Document - - def parse indent = 0 + # Parses the tokens into an array of RDoc::Markup::XXX objects, + # and appends them to the passed +parent+ RDoc::Markup::YYY object. + # + # Exits at the end of the token stream, or when it encounters a token + # in a column less than +indent+ (unless it is a NEWLINE). + # + # Returns +parent+. + + def parse parent, indent = 0 p :parse_start => indent if @debug - document = [] - until @tokens.empty? do type, data, column, = get - if type != :INDENT and column < indent then + if type == :NEWLINE then + # trailing newlines are skipped below, so this is a blank line + parent << RDoc::Markup::BlankLine.new + skip :NEWLINE, false + next + end + + # indentation change: break or verbattim + if column < indent then unget break + elsif column > indent then + unget + parent << build_verbatim(indent) + next end + # indentation is the same case type when :HEADER then - document << build_heading(data) - when :INDENT then - if indent > data then - unget - break - elsif indent == data then - next - end - - unget - document << build_verbatim(indent) - when :NEWLINE then - document << RDoc::Markup::BlankLine.new - skip :NEWLINE, false + parent << build_heading(data) when :RULE then - document << RDoc::Markup::Rule.new(data) + parent << RDoc::Markup::Rule.new(data) skip :NEWLINE when :TEXT then unget - document << build_paragraph(indent) - - # we're done with this paragraph (indent mismatch) - break if peek_token[0] == :TEXT + parent << build_paragraph(indent) when *LIST_TOKENS then unget - - list = build_list(indent) - - document << list if list - - # we're done with this list (indent mismatch) - break if LIST_TOKENS.include? peek_token.first and indent > 0 + parent << build_list(indent) else type, data, column, line = @current_token - raise ParseError, - "Unhandled token #{type} (#{data.inspect}) at #{line}:#{column}" + raise ParseError, "Unhandled token #{type} (#{data.inspect}) at #{line}:#{column}" end end p :parse_end => indent if @debug - document + parent + end ## @@ -384,63 +366,16 @@ class RDoc::Markup::Parser end ## - # Skips a token of +token_type+, optionally raising an error. + # Skips the next token if its type is +token_type+. + # + # Optionally raises an error if the next token is not of the expected type. def skip token_type, error = true type, = get - return unless type # end of stream - return @current_token if token_type == type - unget - - raise ParseError, "expected #{token_type} got #{@current_token.inspect}" if - error - end - - ## - # Consumes tokens until NEWLINE and turns them back into text - - def text - text = '' - - loop do - type, data, = get - - text << case type - when :BULLET then - _, space, = get # SPACE - "*#{' ' * (space - 1)}" - when :LABEL then - _, space, = get # SPACE - "[#{data}]#{' ' * (space - data.length - 2)}" - when :LALPHA, :NUMBER, :UALPHA then - _, space, = get # SPACE - "#{data}.#{' ' * (space - 2)}" - when :NOTE then - _, space = get # SPACE - "#{data}::#{' ' * (space - data.length - 2)}" - when :TEXT then - data - when :NEWLINE then - unget - break - when nil then - break - else - raise ParseError, "unhandled token #{@current_token.inspect}" - end - end - - text - end - - ## - # Calculates the column and line of the current token based on +offset+. - - def token_pos offset - [offset - @line_pos, @line] + raise ParseError, "expected #{token_type} got #{@current_token.inspect}" if error end ## @@ -455,51 +390,62 @@ class RDoc::Markup::Parser until s.eos? do pos = s.pos + # leading spaces will be reflected by the column of the next token + # the only thing we loose are trailing spaces at the end of the file + next if s.scan(/ +/) + + # note: after BULLET, LABEL, etc., + # indent will be the column of the next non-newline token + @tokens << case + # [CR]LF => :NEWLINE when s.scan(/\r?\n/) then token = [:NEWLINE, s.matched, *token_pos(pos)] @line_pos = s.pos @line += 1 token - when s.scan(/ +/) then - [:INDENT, s.matched_size, *token_pos(pos)] + # === text => :HEADER then :TEXT when s.scan(/(=+)\s*/) then level = s[1].length level = 6 if level > 6 @tokens << [:HEADER, level, *token_pos(pos)] - pos = s.pos s.scan(/.*/) - [:TEXT, s.matched, *token_pos(pos)] - when s.scan(/^(-{3,}) *$/) then + [:TEXT, s.matched.sub(/\r$/, ''), *token_pos(pos)] + # --- (at least 3) and nothing else on the line => :RULE + when s.scan(/(-{3,}) *$/) then [:RULE, s[1].length - 2, *token_pos(pos)] - when s.scan(/([*-])\s+/) then - @tokens << [:BULLET, :BULLET, *token_pos(pos)] - [:SPACE, s.matched_size, *token_pos(pos)] - when s.scan(/([a-z]|\d+)\.[ \t]+\S/i) then + # * or - followed by white space and text => :BULLET + when s.scan(/([*-]) +(\S)/) then + s.pos -= s[2].bytesize # unget \S + [:BULLET, s[1], *token_pos(pos)] + # A. text, a. text, 12. text => :UALPHA, :LALPHA, :NUMBER + when s.scan(/([a-z]|\d+)\. +(\S)/i) then + # FIXME if tab(s), the column will be wrong + # either support tabs everywhere by first expanding them to + # spaces, or assume that they will have been replaced + # before (and provide a check for that at least in debug + # mode) list_label = s[1] - width = s.matched_size - 1 - - s.pos -= 1 # unget \S - - list_type = case list_label - when /[a-z]/ then :LALPHA - when /[A-Z]/ then :UALPHA - when /\d/ then :NUMBER - else - raise ParseError, "BUG token #{list_label}" - end - - @tokens << [list_type, list_label, *token_pos(pos)] - [:SPACE, width, *token_pos(pos)] + s.pos -= s[2].bytesize # unget \S + list_type = + case list_label + when /[a-z]/ then :LALPHA + when /[A-Z]/ then :UALPHA + when /\d/ then :NUMBER + else + raise ParseError, "BUG token #{list_label}" + end + [list_type, list_label, *token_pos(pos)] + # [text] followed by spaces or end of line => :LABEL when s.scan(/\[(.*?)\]( +|$)/) then - @tokens << [:LABEL, s[1], *token_pos(pos)] - [:SPACE, s.matched_size, *token_pos(pos)] + [:LABEL, s[1], *token_pos(pos)] + # text:: followed by spaces or end of line => :NOTE when s.scan(/(.*?)::( +|$)/) then - @tokens << [:NOTE, s[1], *token_pos(pos)] - [:SPACE, s.matched_size, *token_pos(pos)] + [:NOTE, s[1], *token_pos(pos)] + # anything else: :TEXT else s.scan(/.*/) - [:TEXT, s.matched, *token_pos(pos)] + [:TEXT, s.matched.sub(/\r$/, ''), *token_pos(pos)] end end @@ -507,9 +453,17 @@ class RDoc::Markup::Parser end ## - # Returns the current token or +token+ to the token stream + # Calculates the column and line of the current token based on +offset+. + + def token_pos offset + [offset - @line_pos, @line] + end + + ## + # Returns the current token to the token stream - def unget token = @current_token + def unget + token = @current_token p :unget => token if @debug raise Error, 'too many #ungets' if token == @tokens.first @tokens.unshift token if token diff --git a/lib/rdoc/markup/preprocess.rb b/lib/rdoc/markup/pre_process.rb index cefb498916..e59bd227b7 100644 --- a/lib/rdoc/markup/preprocess.rb +++ b/lib/rdoc/markup/pre_process.rb @@ -1,12 +1,15 @@ require 'rdoc/markup' +require 'rdoc/encoding' ## # Handle common directives that can occur in a block of text: # -# : include : filename +# \:include: filename # -# RDoc plugin authors can register additional directives to be handled through -# RDoc::Markup::PreProcess::register +# Directives can be escaped by preceding them with a backslash. +# +# RDoc plugin authors can register additional directives to be handled by +# using RDoc::Markup::PreProcess::register class RDoc::Markup::PreProcess @@ -52,18 +55,25 @@ class RDoc::Markup::PreProcess # +code_object+. See RDoc::CodeObject#metadata def handle text, code_object = nil - text.gsub!(/^([ \t]*#?[ \t]*):(\w+):([ \t]*)(.+)?\n/) do - next $& if $3.empty? and $4 and $4[0, 1] == ':' + # regexp helper (square brackets for optional) + # $1 $2 $3 $4 $5 + # [prefix][\]:directive:[spaces][param]newline + text.gsub!(/^([ \t]*#?[ \t]*)(\\?):(\w+):([ \t]*)(.+)?\n/) do + # skip something like ':toto::' + next $& if $4.empty? and $5 and $5[0, 1] == ':' + + # skip if escaped + next "#$1:#$3:#$4#$5\n" unless $2.empty? prefix = $1 - directive = $2.downcase - param = $4 + directive = $3.downcase + param = $5 case directive when 'include' then filename = param.split[0] - include_file filename, prefix - + encoding = if defined?(Encoding) then text.encoding else nil end + include_file filename, prefix, encoding else result = yield directive, param if block_given? @@ -88,27 +98,38 @@ class RDoc::Markup::PreProcess end ## - # Include a file, indenting it correctly. - - def include_file(name, indent) - if full_name = find_include_file(name) then - content = if defined?(Encoding) then - File.binread full_name - else - File.read full_name - end - # HACK determine content type and force encoding - content = content.sub(/\A# .*coding[=:].*$/, '').lstrip - - # strip leading '#'s, but only if all lines start with them - if content =~ /^[^#]/ then - content.gsub(/^/, indent) - else - content.gsub(/^#?/, indent) - end - else + # Handles the <tt>:include: _filename_</tt> directive. + # + # If the first line of the included file starts with '#', and contains + # an encoding information in the form 'coding:' or 'coding=', it is + # removed. + # + # If all lines in the included file start with a '#', this leading '#' + # is removed before inclusion. The included content is indented like + # the <tt>:include:</tt> directive. + #-- + # so all content will be verbatim because of the likely space after '#'? + # TODO shift left the whole file content in that case + # TODO comment stop/start #-- and #++ in included file must be processed here + + def include_file name, indent, encoding + full_name = find_include_file name + + unless full_name then warn "Couldn't find file to include '#{name}' from #{@input_file_name}" - '' + return '' + end + + content = RDoc::Encoding.read_file full_name, encoding + + # strip magic comment + content = content.sub(/\A# .*coding[=:].*$/, '').lstrip + + # strip leading '#'s, but only if all lines start with them + if content =~ /^[^#]/ then + content.gsub(/^/, indent) + else + content.gsub(/^#?/, indent) end end diff --git a/lib/rdoc/markup/raw.rb b/lib/rdoc/markup/raw.rb index 1124be7cc8..ca877c79af 100644 --- a/lib/rdoc/markup/raw.rb +++ b/lib/rdoc/markup/raw.rb @@ -27,6 +27,9 @@ class RDoc::Markup::Raw self.class == other.class and text == other.text end + ## + # Calls #accept_raw+ on +visitor+ + def accept visitor visitor.accept_raw self end @@ -63,3 +66,4 @@ class RDoc::Markup::Raw end end + diff --git a/lib/rdoc/markup/rule.rb b/lib/rdoc/markup/rule.rb index 4fcd040d2b..b778f2bc09 100644 --- a/lib/rdoc/markup/rule.rb +++ b/lib/rdoc/markup/rule.rb @@ -3,6 +3,9 @@ class RDoc::Markup::Rule < Struct.new :weight + ## + # Calls #accept_rule on +visitor+ + def accept visitor visitor.accept_rule self end diff --git a/lib/rdoc/markup/text_formatter_test_case.rb b/lib/rdoc/markup/text_formatter_test_case.rb new file mode 100644 index 0000000000..ba9e7c6187 --- /dev/null +++ b/lib/rdoc/markup/text_formatter_test_case.rb @@ -0,0 +1,116 @@ +require 'rdoc/markup/formatter_test_case' + +## +# Test case for creating new plain-text RDoc::Markup formatters. See also +# RDoc::Markup::FormatterTestCase +# +# See test_rdoc_markup_to_rdoc.rb for a complete example. +# +# Example: +# +# class TestRDocMarkupToNewTextFormat < RDoc::Markup::TextFormatterTestCase +# +# add_visitor_tests +# add_text_tests +# +# def setup +# super +# +# @to = RDoc::Markup::ToNewTextFormat.new +# end +# +# def accept_blank_line +# assert_equal :junk, @to.res.join +# end +# +# # ... +# +# end + +class RDoc::Markup::TextFormatterTestCase < RDoc::Markup::FormatterTestCase + + ## + # Adds test cases to the calling TestCase. + + def self.add_text_tests + self.class_eval do + + ## + # Test case that calls <tt>@to.accept_heading</tt> + + def test_accept_heading_indent + @to.start_accepting + @to.indent = 3 + @to.accept_heading @RM::Heading.new(1, 'Hello') + + accept_heading_indent + end + + ## + # Test case that calls <tt>@to.accept_rule</tt> + + def test_accept_rule_indent + @to.start_accepting + @to.indent = 3 + @to.accept_rule @RM::Rule.new(1) + + accept_rule_indent + end + + ## + # Test case that calls <tt>@to.accept_verbatim</tt> + + def test_accept_verbatim_indent + @to.start_accepting + @to.indent = 2 + @to.accept_verbatim @RM::Verbatim.new("hi\n", " world\n") + + accept_verbatim_indent + end + + ## + # Test case that calls <tt>@to.accept_verbatim</tt> with a big indent + + def test_accept_verbatim_big_indent + @to.start_accepting + @to.indent = 2 + @to.accept_verbatim @RM::Verbatim.new("hi\n", "world\n") + + accept_verbatim_big_indent + end + + ## + # Test case that calls <tt>@to.accept_paragraph</tt> with an indent + + def test_accept_paragraph_indent + @to.start_accepting + @to.indent = 3 + @to.accept_paragraph @RM::Paragraph.new(('words ' * 30).strip) + + accept_paragraph_indent + end + + ## + # Test case that calls <tt>@to.accept_paragraph</tt> with a long line + + def test_accept_paragraph_wrap + @to.start_accepting + @to.accept_paragraph @RM::Paragraph.new(('words ' * 30).strip) + + accept_paragraph_wrap + end + + ## + # Test case that calls <tt>@to.attributes</tt> with an escaped + # cross-reference. If this test doesn't pass something may be very + # wrong. + + def test_attributes + assert_equal 'Dog', @to.attributes("\\Dog") + end + + end + end + +end + diff --git a/lib/rdoc/markup/to_ansi.rb b/lib/rdoc/markup/to_ansi.rb index 9a5be8babb..c9f874ea3c 100644 --- a/lib/rdoc/markup/to_ansi.rb +++ b/lib/rdoc/markup/to_ansi.rb @@ -1,10 +1,13 @@ -require 'rdoc/markup/inline' +require 'rdoc/markup/to_rdoc' ## # Outputs RDoc markup with vibrant ANSI color! class RDoc::Markup::ToAnsi < RDoc::Markup::ToRdoc + ## + # Creates a new ToAnsi visitor that is ready to output vibrant ANSI color! + def initialize super @@ -23,12 +26,15 @@ class RDoc::Markup::ToAnsi < RDoc::Markup::ToRdoc add_tag :EM, "\e[4m", "\e[m" end + ## + # Overrides indent width to ensure output lines up correctly. + def accept_list_item_end list_item width = case @list_type.last when :BULLET then 2 when :NOTE, :LABEL then - @res << "\n" + @res << "\n" unless res.length == 1 2 else bullet = @list_index.last.to_s @@ -39,6 +45,9 @@ class RDoc::Markup::ToAnsi < RDoc::Markup::ToRdoc @indent -= width end + ## + # Adds coloring to note and label list items + def accept_list_item_start list_item bullet = case @list_type.last when :BULLET then @@ -62,6 +71,9 @@ class RDoc::Markup::ToAnsi < RDoc::Markup::ToRdoc end end + ## + # Starts accepting with a reset screen + def start_accepting super diff --git a/lib/rdoc/markup/to_bs.rb b/lib/rdoc/markup/to_bs.rb index e7af129824..931edd81ea 100644 --- a/lib/rdoc/markup/to_bs.rb +++ b/lib/rdoc/markup/to_bs.rb @@ -1,4 +1,4 @@ -require 'rdoc/markup/inline' +require 'rdoc/markup/to_rdoc' ## # Outputs RDoc markup with hot backspace action! You will probably need a @@ -8,6 +8,9 @@ require 'rdoc/markup/inline' class RDoc::Markup::ToBs < RDoc::Markup::ToRdoc + ## + # Returns a new ToBs that is ready for hot backspace action! + def initialize super @@ -22,8 +25,12 @@ class RDoc::Markup::ToBs < RDoc::Markup::ToRdoc def init_tags add_tag :BOLD, '+b', '-b' add_tag :EM, '+_', '-_' + add_tag :TT, '' , '' # we need in_tt information maintained end + ## + # Makes heading text bold. + def accept_heading heading use_prefix or @res << ' ' * @indent @res << @headings[heading.level][0] @@ -44,7 +51,6 @@ class RDoc::Markup::ToBs < RDoc::Markup::ToRdoc when '+_' then @in_em = true when '-_' then @in_em = false end - '' end diff --git a/lib/rdoc/markup/to_html.rb b/lib/rdoc/markup/to_html.rb index 74e3137eb2..de723921e9 100644 --- a/lib/rdoc/markup/to_html.rb +++ b/lib/rdoc/markup/to_html.rb @@ -8,6 +8,8 @@ require 'cgi' class RDoc::Markup::ToHtml < RDoc::Markup::Formatter + include RDoc::Text + ## # Maps RDoc::Markup::Parser::LIST_TOKENS types to HTML tags @@ -15,7 +17,7 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter :BULLET => ['<ul>', '</ul>'], :LABEL => ['<dl>', '</dl>'], :LALPHA => ['<ol style="display: lower-alpha">', '</ol>'], - :NOTE => ['<table>', '</table>'], + :NOTE => ['<table class="rdoc-list">', '</table>'], :NUMBER => ['<ol>', '</ol>'], :UALPHA => ['<ol style="display: upper-alpha">', '</ol>'], } @@ -48,6 +50,9 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter File.join(*from) end + ## + # Creates a new formatter that will output HTML + def initialize super @@ -103,13 +108,15 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter end end + # :section: Special handling + ## - # And we're invoked with a potential external hyperlink mailto: - # just gets inserted. http: links are checked to see if they + # And we're invoked with a potential external hyperlink. <tt>mailto:</tt> + # just gets inserted. <tt>http:</tt> links are checked to see if they # reference an image. If so, that image gets inserted using an - # <img> tag. Otherwise a conventional <a href> 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. + # <tt><img></tt> tag. Otherwise a conventional <tt><a href></tt> is used. + # We also support a special type of hyperlink, <tt>link:</tt>, which is a + # reference to a local file whose path is relative to the --op directory. def handle_special_HYPERLINK(special) url = special.text @@ -118,7 +125,7 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter ## # Here's a hypedlink where the label is different to the URL - # <label>[url] or {long label}[url] + # <label>[url] or {long label}[url] def handle_special_TIDYLINK(special) text = special.text @@ -130,8 +137,10 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter gen_url url, label end + # :section: Utilities + ## - # This is a higher speed (if messier) version of wrap + # Wraps +txt+ to +line_len+ def wrap(txt, line_len = 76) res = [] @@ -159,173 +168,150 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter sp += 1 while sp < ep and txt[sp] == ?\s end - res.join + res.join.strip end - ## # :section: Visitor + ## + # Prepares the visitor for HTML generation + def start_accepting @res = [] @in_list_entry = [] @list = [] end + ## + # Returns the generated output + def end_accepting @res.join end + ## + # Adds +paragraph+ to the output + def accept_paragraph(paragraph) - @res << annotate("<p>") + "\n" - @res << wrap(convert_flow(@am.flow(paragraph.text))) - @res << annotate("</p>") + "\n" + @res << "\n<p>" + @res << wrap(to_html(paragraph.text)) + @res << "</p>\n" end + ## + # Adds +verbatim+ to the output + def accept_verbatim(verbatim) - @res << annotate("<pre>") << "\n" - @res << CGI.escapeHTML(verbatim.text) - @res << annotate("</pre>") << "\n" + @res << "\n<pre>" + @res << CGI.escapeHTML(verbatim.text.rstrip) + @res << "</pre>\n" end + ## + # Adds +rule+ to the output + def accept_rule(rule) size = rule.weight size = 10 if size > 10 - @res << "<hr style=\"height: #{size}px\"></hr>" + @res << "<hr style=\"height: #{size}px\">\n" end + ## + # Prepares the visitor for consuming +list+ + def accept_list_start(list) @list << list.type - @res << html_list_name(list.type, true) << "\n" + @res << html_list_name(list.type, true) @in_list_entry.push false end + ## + # Finishes consumption of +list+ + def accept_list_end(list) @list.pop if tag = @in_list_entry.pop - @res << annotate(tag) << "\n" + @res << tag end @res << html_list_name(list.type, false) << "\n" end + ## + # Prepares the visitor for consuming +list_item+ + def accept_list_item_start(list_item) if tag = @in_list_entry.last - @res << annotate(tag) << "\n" + @res << tag end @res << list_item_start(list_item, @list.last) end + ## + # Finishes consumption of +list_item+ + def accept_list_item_end(list_item) @in_list_entry[-1] = list_end_for(@list.last) end - def accept_blank_line(blank_line) - # @res << annotate("<p />") << "\n" - end - - def accept_heading(heading) - @res << convert_heading(heading.level, @am.flow(heading.text)) - end - - def accept_raw raw - @res << raw.parts.join("\n") - end - - private - ## - # Converts string +item+ + # Adds +blank_line+ to the output - def convert_string(item) - in_tt? ? convert_string_simple(item) : convert_string_fancy(item) + def accept_blank_line(blank_line) + # @res << annotate("<p />") << "\n" end ## - # Escapes HTML in +item+ + # Adds +heading+ to the output - def convert_string_simple(item) - CGI.escapeHTML item + def accept_heading(heading) + @res << "\n<h#{heading.level}>" + @res << to_html(heading.text) + @res << "</h#{heading.level}>\n" end ## - # Converts ampersand, dashes, elipsis, quotes, copyright and registered - # trademark symbols to HTML escaped Unicode. - - def convert_string_fancy(item) - # convert ampersand before doing anything else - item.gsub(/&/, '&'). - - # convert -- to em-dash, (-- to en-dash) - gsub(/---?/, '—'). #gsub(/--/, '–'). - - # convert ... to elipsis (and make sure .... becomes .<elipsis>) - gsub(/\.\.\.\./, '.…').gsub(/\.\.\./, '…'). + # Adds +raw+ to the output - # convert single closing quote - gsub(%r{([^ \t\r\n\[\{\(])\'}, '\1’'). # } - gsub(%r{\'(?=\W|s\b)}, '’'). - - # convert single opening quote - gsub(/'/, '‘'). - - # convert double closing quote - gsub(%r{([^ \t\r\n\[\{\(])\"(?=\W)}, '\1”'). # } - - # convert double opening quote - gsub(/"/, '“'). - - # convert copyright - gsub(/\(c\)/, '©'). - - # convert registered trademark - gsub(/\(r\)/, '®') + def accept_raw raw + @res << raw.parts.join("\n") end ## - # Converts headings to hN elements + # CGI escapes +text+ - def convert_heading(level, flow) - [annotate("<h#{level}>"), - convert_flow(flow), - annotate("</h#{level}>\n")].join + def convert_string(text) + CGI.escapeHTML text end ## - # Determins the HTML list element for +list_type+ and +open_tag+ + # Determines the HTML list element for +list_type+ and +open_tag+ def html_list_name(list_type, open_tag) tags = LIST_TYPE_TO_HTML[list_type] raise RDoc::Error, "Invalid list type: #{list_type.inspect}" unless tags - annotate tags[open_tag ? 0 : 1] + tags[open_tag ? 0 : 1] end ## - # Starts a list item + # Returns the HTML tag for +list_type+, possible using a label from + # +list_item+ def list_item_start(list_item, list_type) case list_type when :BULLET, :LALPHA, :NUMBER, :UALPHA then - annotate("<li>") - + "<li>" when :LABEL then - annotate("<dt>") + - convert_flow(@am.flow(list_item.label)) + - annotate("</dt>") + - annotate("<dd>") - + "<dt>#{to_html list_item.label}</dt>\n<dd>" when :NOTE then - annotate("<tr>") + - annotate("<td valign=\"top\">") + - convert_flow(@am.flow(list_item.label)) + - annotate("</td>") + - annotate("<td>") + "<tr><td class=\"rdoc-term\"><p>#{to_html list_item.label}</p></td>\n<td>" else raise RDoc::Error, "Invalid list type: #{list_type.inspect}" end end ## - # Ends a list item + # Returns the HTML end-tag for +list_type+ def list_end_for(list_type) case list_type @@ -340,5 +326,12 @@ class RDoc::Markup::ToHtml < RDoc::Markup::Formatter end end + ## + # Converts +item+ to HTML using RDoc::Text#to_html + + def to_html item + super convert_flow @am.flow item + end + end diff --git a/lib/rdoc/markup/to_html_crossref.rb b/lib/rdoc/markup/to_html_crossref.rb index 44e71486fb..a3feb848a2 100644 --- a/lib/rdoc/markup/to_html_crossref.rb +++ b/lib/rdoc/markup/to_html_crossref.rb @@ -9,10 +9,10 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml ## # Regular expression to match class references # - # 1) There can be a '\' in front of text to suppress any cross-references - # 2) There can be a '::' in front of class names to reference from the + # 1. There can be a '\\' in front of text to suppress the cross-reference + # 2. There can be a '::' in front of class names to reference from the # top-level namespace. - # 3) The method can be followed by parenthesis + # 3. The method can be followed by parenthesis (not recommended) CLASS_REGEXP_STR = '\\\\?((?:\:{2})?[A-Z]\w*(?:\:\:\w+)*)' @@ -34,10 +34,10 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml # A::B::C.meth #{CLASS_REGEXP_STR}(?:[.#]|::)#{METHOD_REGEXP_STR} - # Stand-alone method (proceeded by a #) + # Stand-alone method (preceeded by a #) | \\?\##{METHOD_REGEXP_STR} - # Stand-alone method (proceeded by ::) + # Stand-alone method (preceeded by ::) | ::#{METHOD_REGEXP_STR} # A::B::C @@ -51,9 +51,10 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml # In order that words like "can't" not # be flagged as potential cross-references, only # flag potential class cross-references if the character - # after the cross-referece is a space or sentence - # punctuation. - | #{CLASS_REGEXP_STR}(?=[\s\)\.\?\!\,\;]|\z) + # after the cross-referece is a space, sentence + # punctuation, tag start character, or attribute + # marker. + | #{CLASS_REGEXP_STR}(?=[\s\)\.\?\!\,\;<\000]|\z) # Things that look like filenames # The key thing is that there must be at least @@ -62,7 +63,29 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml | (?:\.\.\/)*[-\/\w]+[_\/\.][-\w\/\.]+ # Things that have markup suppressed - | \\[^\s] + # Don't process things like '\<' in \<tt>, though. + # TODO: including < is a hack, not very satisfying. + | \\[^\s<] + )/x + + ## + # Version of CROSSREF_REGEXP used when <tt>--hyperlink-all</tt> is specified. + + ALL_CROSSREF_REGEXP = /( + # A::B::C.meth + #{CLASS_REGEXP_STR}(?:[.#]|::)#{METHOD_REGEXP_STR} + + # Stand-alone method + | \\?#{METHOD_REGEXP_STR} + + # A::B::C + | #{CLASS_REGEXP_STR}(?=[\s\)\.\?\!\,\;<\000]|\z) + + # Things that look like filenames + | (?:\.\.\/)*[-\/\w]+[_\/\.][-\w\/\.]+ + + # Things that have markup suppressed + | \\[^\s<] )/x ## @@ -71,19 +94,28 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml attr_accessor :context ## + # Should we show '#' characters on method references? + + attr_accessor :show_hash + + ## # Creates a new crossref resolver that generates links relative to +context+ # which lives at +from_path+ in the generated files. '#' characters on - # references are removed unless +show_hash+ is true. + # references are removed unless +show_hash+ is true. Only method names + # preceded by '#' or '::' are hyperlinked, unless +hyperlink_all+ is true. - def initialize(from_path, context, show_hash) + def initialize(from_path, context, show_hash, hyperlink_all = false) raise ArgumentError, 'from_path cannot be nil' if from_path.nil? super() - @markup.add_special(CROSSREF_REGEXP, :CROSSREF) + crossref_re = hyperlink_all ? ALL_CROSSREF_REGEXP : CROSSREF_REGEXP + + @markup.add_special crossref_re, :CROSSREF @from_path = from_path @context = context @show_hash = show_hash + @hyperlink_all = hyperlink_all @seen = {} end @@ -92,22 +124,24 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml # We're invoked when any text matches the CROSSREF pattern. If we find 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. + # For example, ToHtml is found, even without the <tt>RDoc::Markup::</tt> + # prefix, because we look for it in module Markup first. def handle_special_CROSSREF(special) name = special.text - # This ensures that words entirely consisting of lowercase letters will - # not have cross-references generated (to suppress lots of erroneous - # cross-references to "new" in text, for instance) - return name if name =~ /\A[a-z]*\z/ + unless @hyperlink_all then + # This ensures that words entirely consisting of lowercase letters will + # not have cross-references generated (to suppress lots of erroneous + # cross-references to "new" in text, for instance) + return name if name =~ /\A[a-z]*\z/ + end return @seen[name] if @seen.include? name lookup = name - name = name[0, 1] unless @show_hash if name[0, 1] == '#' + name = name[1..-1] unless @show_hash if name[0, 1] == '#' # Find class, module, or method in class or module. # @@ -120,26 +154,47 @@ class RDoc::Markup::ToHtmlCrossref < RDoc::Markup::ToHtml # whether the string as a whole is a known symbol). if /#{CLASS_REGEXP_STR}([.#]|::)#{METHOD_REGEXP_STR}/ =~ lookup then - container = $1 type = $2 - type = '#' if type == '.' + type = '' if type == '.' # will find either #method or ::method method = "#{type}#{$3}" - ref = @context.find_symbol container, method + container = @context.find_symbol_module($1) + elsif /^([.#]|::)#{METHOD_REGEXP_STR}/ =~ lookup then + type = $1 + type = '' if type == '.' + method = "#{type}#{$2}" + container = @context + else + container = nil + end + + if container then + ref = container.find_local_symbol method + + unless ref || RDoc::TopLevel === container then + ref = container.find_ancestor_local_symbol method + end end ref = @context.find_symbol lookup unless ref + ref = nil if RDoc::Alias === ref # external alias: can't link to it out = if lookup == '\\' then lookup elsif lookup =~ /^\\/ then - $' - elsif ref and ref.document_self then - "<a href=\"#{ref.as_href @from_path}\">#{name}</a>" + # we remove the \ only in front of what we know: + # other backslashes are treated later, only outside of <tt> + ref ? $' : lookup + elsif ref then + if ref.document_self then + "<a href=\"#{ref.as_href @from_path}\">#{name}</a>" + else + name + end else - name + lookup end - @seen[name] = out + @seen[lookup] = out out end diff --git a/lib/rdoc/markup/to_rdoc.rb b/lib/rdoc/markup/to_rdoc.rb index 867715bb1e..b1ac59e5b0 100644 --- a/lib/rdoc/markup/to_rdoc.rb +++ b/lib/rdoc/markup/to_rdoc.rb @@ -1,3 +1,4 @@ +require 'rdoc/markup/formatter' require 'rdoc/markup/inline' ## @@ -5,21 +6,49 @@ require 'rdoc/markup/inline' class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter + ## + # Current indent amount for output in characters + attr_accessor :indent + + ## + # Output width in characters + + attr_accessor :width + + ## + # Stack of current list indexes for alphabetic and numeric lists + attr_reader :list_index + + ## + # Stack of list types + attr_reader :list_type + + ## + # Stack of list widths for indentation + attr_reader :list_width + + ## + # Prefix for the next list item. See #use_prefix + attr_reader :prefix + + ## + # Output accumulator + attr_reader :res + ## + # Creates a new formatter that will output (mostly) \RDoc markup + def initialize super - @markup.add_special(/\\[^\s]/, :SUPPRESSED_CROSSREF) - + @markup.add_special(/\\\S/, :SUPPRESSED_CROSSREF) @width = 78 - @prefix = '' - init_tags @headings = {} @@ -34,7 +63,7 @@ class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter end ## - # Maps attributes to ANSI sequences + # Maps attributes to HTML sequences def init_tags add_tag :BOLD, "<b>", "</b>" @@ -42,10 +71,16 @@ class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter add_tag :EM, "<em>", "</em>" end + ## + # Adds +blank_line+ to the output + def accept_blank_line blank_line @res << "\n" end + ## + # Adds +heading+ to the output + def accept_heading heading use_prefix or @res << ' ' * @indent @res << @headings[heading.level][0] @@ -54,12 +89,18 @@ class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter @res << "\n" end + ## + # Finishes consumption of +list+ + def accept_list_end list @list_index.pop @list_type.pop @list_width.pop end + ## + # Finishes consumption of +list_item+ + def accept_list_item_end list_item width = case @list_type.last when :BULLET then @@ -76,29 +117,29 @@ class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter @indent -= width end + ## + # Prepares the visitor for consuming +list_item+ + def accept_list_item_start list_item - bullet = case @list_type.last - when :BULLET then - '*' - when :NOTE, :LABEL then - attributes(list_item.label) + ":\n" - else - @list_index.last.to_s + '.' - end - - case @list_type.last + type = @list_type.last + + case type when :NOTE, :LABEL then + bullet = attributes(list_item.label) + ":\n" + @prefix = ' ' * @indent @indent += 2 - @prefix = bullet + (' ' * @indent) + @prefix << bullet + (' ' * @indent) else + bullet = type == :BULLET ? '*' : @list_index.last.to_s + '.' @prefix = (' ' * @indent) + bullet.ljust(bullet.length + 1) - width = bullet.length + 1 - @indent += width end end + ## + # Prepares the visitor for consuming +list+ + def accept_list_start list case list.type when :BULLET then @@ -123,14 +164,23 @@ class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter @list_type << list.type end + ## + # Adds +paragraph+ to the output + def accept_paragraph paragraph wrap attributes(paragraph.text) end + ## + # Adds +raw+ to the output + def accept_raw raw @res << raw.parts.join("\n") end + ## + # Adds +rule+ to the output + def accept_rule rule use_prefix or @res << ' ' * @indent @res << '-' * (@width - @indent) @@ -138,58 +188,46 @@ class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter end ## - # Outputs +verbatim+ flush left and indented 2 columns + # Outputs +verbatim+ indented 2 columns def accept_verbatim verbatim indent = ' ' * (@indent + 2) - lines = [] - current_line = [] - - # split into lines verbatim.parts.each do |part| - current_line << part - - if part == "\n" then - lines << current_line - current_line = [] - end + @res << indent unless part == "\n" + @res << part end - lines << current_line unless current_line.empty? - - # calculate margin - indented = lines.select { |line| line != ["\n"] } - margin = indented.map { |line| line.first.length }.min - - # flush left - indented.each { |line| line[0][0...margin] = '' } - - # output - use_prefix or @res << indent # verbatim is unlikely to have prefix - @res << lines.shift.join - - lines.each do |line| - @res << indent unless line == ["\n"] - @res << line.join - end - - @res << "\n" + @res << "\n" unless @res =~ /\n\z/ end + ## + # Applies attribute-specific markup to +text+ using RDoc::AttributeManager + def attributes text flow = @am.flow text.dup convert_flow flow end + ## + # Returns the generated output + def end_accepting @res.join end + ## + # Removes preceeding \\ from the suppressed crossref +special+ + def handle_special_SUPPRESSED_CROSSREF special - special.text.sub(/\\/, '') + text = special.text + text = text.sub('\\', '') unless in_tt? + text end + ## + # Prepares the visitor for text generation + def start_accepting @res = [""] @indent = 0 @@ -200,6 +238,10 @@ class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter @list_width = [] end + ## + # Adds the stored #prefix to the output and clears it. Lists generate a + # prefix for later consumption. + def use_prefix prefix = @prefix @prefix = nil @@ -208,6 +250,9 @@ class RDoc::Markup::ToRdoc < RDoc::Markup::Formatter prefix end + ## + # Wraps +text+ to #width + def wrap text return unless text && !text.empty? diff --git a/lib/rdoc/markup/to_test.rb b/lib/rdoc/markup/to_test.rb index 0afdb96a18..f79f9475f1 100644 --- a/lib/rdoc/markup/to_test.rb +++ b/lib/rdoc/markup/to_test.rb @@ -6,6 +6,8 @@ require 'rdoc/markup/formatter' class RDoc::Markup::ToTest < RDoc::Markup::Formatter + # :stopdoc: + ## # :section: Visitor @@ -22,8 +24,12 @@ class RDoc::Markup::ToTest < RDoc::Markup::Formatter @res << paragraph.text end + def accept_raw raw + @res << raw.parts.join + end + def accept_verbatim(verbatim) - @res << verbatim.text + @res << verbatim.text.gsub(/^(\S)/, ' \1') end def accept_list_start(list) @@ -60,5 +66,7 @@ class RDoc::Markup::ToTest < RDoc::Markup::Formatter @res << '-' * rule.weight end + # :startdoc: + end diff --git a/lib/rdoc/markup/verbatim.rb b/lib/rdoc/markup/verbatim.rb index c684d78765..8fe2184699 100644 --- a/lib/rdoc/markup/verbatim.rb +++ b/lib/rdoc/markup/verbatim.rb @@ -3,6 +3,9 @@ class RDoc::Markup::Verbatim < RDoc::Markup::Raw + ## + # Calls #accept_verbatim on +visitor+ + def accept visitor visitor.accept_verbatim self end @@ -17,16 +20,16 @@ class RDoc::Markup::Verbatim < RDoc::Markup::Raw @parts.each do |part| case part - when /\n/ then + when /^\s*\n/ then newlines += 1 - parts << part if newlines <= 2 + parts << part if newlines == 1 else newlines = 0 parts << part end end - parts.slice!(-1) if parts[-2..-1] == ["\n", "\n"] + parts.pop if parts.last =~ /\A\r?\n\z/ @parts = parts end diff --git a/lib/rdoc/method_attr.rb b/lib/rdoc/method_attr.rb new file mode 100644 index 0000000000..15924d9ed0 --- /dev/null +++ b/lib/rdoc/method_attr.rb @@ -0,0 +1,353 @@ +require 'rdoc/code_object' + +## +# Abstract class representing either a method or an attribute. + +class RDoc::MethodAttr < RDoc::CodeObject + + include Comparable + + ## + # Name of this method/attribute. + + attr_accessor :name + + ## + # public, protected, private + + attr_accessor :visibility + + ## + # Is this a singleton method/attribute? + + attr_accessor :singleton + + ## + # Source file token stream + + attr_reader :text + + ## + # Array of other names for this method/attribute + + attr_reader :aliases + + ## + # The method/attribute we're aliasing + + attr_accessor :is_alias_for + + #-- + # The attributes below are for AnyMethod only. + # They are left here for the time being to + # allow ri to operate. + # TODO modify ri to avoid calling these on attributes. + #++ + + ## + # Parameters yielded by the called block + + attr_reader :block_params + + ## + # Parameters for this method + + attr_accessor :params + + ## + # Different ways to call this method + + attr_accessor :call_seq + + ## + # The call_seq or the param_seq with method name, if there is no call_seq. + + attr_reader :arglists + + ## + # Pretty parameter list for this method + + attr_reader :param_seq + + + ## + # Creates a new MethodAttr from token stream +text+ and method or attribute + # name +name+. + # + # Usually this is called by super from a subclass. + + def initialize text, name + super() + + @text = text + @name = name + + @aliases = [] + @is_alias_for = nil + @parent_name = nil + @singleton = nil + @visibility = :public + @see = false + + @arglists = nil + @block_params = nil + @call_seq = nil + @param_seq = nil + @params = nil + end + + ## + # Order by #singleton then #name + + def <=>(other) + [@singleton ? 0 : 1, name] <=> [other.singleton ? 0 : 1, other.name] + end + + ## + # A method/attribute is documented if any of the following is true: + # - it was marked with :nodoc:; + # - it has a comment; + # - it is an alias for a documented method; + # - it has a +#see+ method that is documented. + + def documented? + super or + (is_alias_for and is_alias_for.documented?) or + (see and see.documented?) + end + + ## + # A method/attribute to look at, + # in particular if this method/attribute has no documentation. + # + # It can be a method/attribute of the superclass or of an included module, + # including the Kernel module, which is always appended to the included + # modules. + # + # Returns +nil+ if there is no such method/attribute. + # The +#is_alias_for+ method/attribute, if any, is not included. + # + # Templates may generate a "see also ..." if this method/attribute + # has documentation, and "see ..." if it does not. + + def see + @see = find_see if @see == false + @see + end + + def find_see # :nodoc: + return nil if singleton || is_alias_for + + # look for the method + other = find_method_or_attribute name + return other if other + + # if it is a setter, look for a getter + return nil unless name =~ /[a-z_]=$/i # avoid == or === + return find_method_or_attribute name[0..-2] + end + + def find_method_or_attribute name # :nodoc: + return nil unless parent.respond_to? :ancestors + + searched = parent.ancestors + kernel = RDoc::TopLevel.all_modules_hash['Kernel'] + + searched << kernel if kernel && + parent != kernel && !searched.include?(kernel) + + searched.each do |ancestor| + next if parent == ancestor + next if String === ancestor + + other = ancestor.find_method_named('#' << name) || + ancestor.find_attribute_named(name) + + return other if other + end + + nil + end + + ## + # Abstract method. Contexts in their building phase call this + # to register a new alias for this known method/attribute. + # + # - creates a new AnyMethod/Attribute +newa+ named an_alias.new_name; + # - adds +self+ as +newa.is_alias_for+; + # - adds +newa+ to #aliases + # - adds +newa+ to the methods/attributes of +context+. + + def add_alias(an_alias, context) + raise NotImplementedError + end + + ## + # HTML fragment reference for this method + + def aref + type = singleton ? 'c' : 'i' + # % characters are not allowed in html names => dash instead + "#{aref_prefix}-#{type}-#{html_name}" + end + + ## + # Prefix for +aref+, defined by subclasses. + + def aref_prefix + raise NotImplementedError + end + + ## + # Attempts to sanitize the content passed by the ruby parser: + # remove outer parentheses, etc. + + def block_params=(value) + # 'yield.to_s' or 'assert yield, msg' + return @block_params = '' if value =~ /^[\.,]/ + + # remove trailing 'if/unless ...' + return @block_params = '' if value =~ /^(if|unless)\s/ + + value = $1.strip if value =~ /^(.+)\s(if|unless)\s/ + + # outer parentheses + value = $1 if value =~ /^\s*\((.*)\)\s*$/ + value = value.strip + + # proc/lambda + return @block_params = $1 if value =~ /^(proc|lambda)(\s*\{|\sdo)/ + + # surrounding +...+ or [...] + value = $1.strip if value =~ /^\+(.*)\+$/ + value = $1.strip if value =~ /^\[(.*)\]$/ + + return @block_params = '' if value.empty? + + # global variable + return @block_params = 'str' if value =~ /^\$[&0-9]$/ + + # wipe out array/hash indices + value.gsub!(/(\w)\[[^\[]+\]/, '\1') + + # remove @ from class/instance variables + value.gsub!(/@@?([a-z0-9_]+)/, '\1') + + # method calls => method name + value.gsub!(/([A-Z:a-z0-9_]+)\.([a-z0-9_]+)(\s*\(\s*[a-z0-9_.,\s]*\s*\)\s*)?/) do + case $2 + when 'to_s' then $1 + when 'const_get' then 'const' + when 'new' then + $1.split('::').last. # ClassName => class_name + gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + downcase + else + $2 + end + end + + # class prefixes + value.gsub!(/[A-Za-z0-9_:]+::/, '') + + # simple expressions + value = $1 if value =~ /^([a-z0-9_]+)\s*[-*+\/]/ + + @block_params = value.strip + end + + ## + # HTML id-friendly method/attribute name + + def html_name + CGI.escape(@name.gsub('-', '-2D')).gsub('%','-').sub(/^-/, '') + end + + ## + # Full method/attribute name including namespace + + def full_name + @full_name || "#{parent_name}#{pretty_name}" + end + + ## + # '::' for a class method/attribute, '#' for an instance method. + + def name_prefix + singleton ? '::' : '#' + end + + ## + # Method/attribute name with class/instance indicator + + def pretty_name + "#{name_prefix}#{@name}" + end + + ## + # Type of method/attribute (class or instance) + + def type + singleton ? 'class' : 'instance' + end + + ## + # Path to this method + + def path + "#{@parent.path}##{aref}" + end + + ## + # Name of our parent with special handling for un-marshaled methods + + def parent_name + @parent_name || super + end + + def pretty_print q # :nodoc: + alias_for = @is_alias_for ? "alias for #{@is_alias_for.name}" : nil + + q.group 2, "[#{self.class.name} #{full_name} #{visibility}", "]" do + if alias_for then + q.breakable + q.text alias_for + end + + if text then + q.breakable + q.text "text:" + q.breakable + q.pp @text + end + + unless comment.empty? then + q.breakable + q.text "comment:" + q.breakable + q.pp @comment + end + end + end + + def inspect # :nodoc: + alias_for = @is_alias_for ? " (alias for #{@is_alias_for.name})" : nil + "#<%s:0x%x %s (%s)%s>" % [ + self.class, object_id, + full_name, + visibility, + alias_for, + ] + end + + def to_s # :nodoc: + if @is_alias_for + "#{self.class.name}: #{full_name} -> #{is_alias_for}" + else + "#{self.class.name}: #{full_name}" + end + end + +end + diff --git a/lib/rdoc/normal_class.rb b/lib/rdoc/normal_class.rb index e7ca6fffde..1ed8eaf974 100644 --- a/lib/rdoc/normal_class.rb +++ b/lib/rdoc/normal_class.rb @@ -6,10 +6,10 @@ require 'rdoc/class_module' class RDoc::NormalClass < RDoc::ClassModule ## - # Ancestor ClassModules + # Appends the superclass, if any, to the included modules. def ancestors - includes + [superclass] + superclass ? super + [superclass] : super end def inspect # :nodoc: @@ -20,6 +20,15 @@ class RDoc::NormalClass < RDoc::ClassModule ] end + def to_s # :nodoc: + display = "#{self.class.name} #{self.full_name}" + if superclass + display << ' < ' << (superclass.is_a?(String) ? superclass : superclass.full_name) + end + display << ' -> ' << is_alias_for.to_s if is_alias_for + display + end + def pretty_print q # :nodoc: superclass = @superclass ? " < #{@superclass}" : nil diff --git a/lib/rdoc/normal_module.rb b/lib/rdoc/normal_module.rb index 92abe6b440..74a31f2668 100644 --- a/lib/rdoc/normal_module.rb +++ b/lib/rdoc/normal_module.rb @@ -5,11 +5,6 @@ require 'rdoc/class_module' class RDoc::NormalModule < RDoc::ClassModule - ## - # Included NormalModules - - alias ancestors includes - def inspect # :nodoc: "#<%s:0x%x module %s includes: %p attributes: %p methods: %p aliases: %p>" % [ self.class, object_id, diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb index 90415f0aa4..810f7fac0a 100644 --- a/lib/rdoc/options.rb +++ b/lib/rdoc/options.rb @@ -8,9 +8,41 @@ require 'rdoc/ri/paths' class RDoc::Options ## - # Character-set + # The deprecated options. + + DEPRECATED = { + '--accessor' => 'support discontinued', + '--diagram' => 'support discontinued', + '--help-output' => 'support discontinued', + '--image-format' => 'was an option for --diagram', + '--inline-source' => 'source code is now always inlined', + '--merge' => 'ri now always merges class information', + '--one-file' => 'support discontinued', + '--op-name' => 'support discontinued', + '--opname' => 'support discontinued', + '--promiscuous' => 'files always only document their content', + '--ri-system' => 'Ruby installers use other techniques', + } - attr_reader :charset + ## + # Template option validator for OptionParser + + Template = nil + + ## + # Character-set for HTML output. #encoding is preferred over #charset + + attr_accessor :charset + + ## + # If true, RDoc will not write any files. + + attr_accessor :dry_run + + ## + # Encoding of output where. This is set via --encoding. + + attr_accessor :encoding if Object.const_defined? :Encoding ## # Files matching this pattern will be excluded @@ -23,9 +55,20 @@ class RDoc::Options attr_accessor :files ## + # Create the output even if the output directory does not look + # like an rdoc output directory + + attr_accessor :force_output + + ## # Scan newer sources than the flag file if true. - attr_reader :force_update + attr_accessor :force_update + + ## + # Formatter to mark up text with + + attr_accessor :formatter ## # Description of the output generator (set with the <tt>-fmt</tt> option) @@ -33,9 +76,21 @@ class RDoc::Options attr_accessor :generator ## - # Formatter to mark up text with + # Loaded generator options. Used to prevent --help from loading the same + # options multiple times. - attr_accessor :formatter + attr_accessor :generator_options + + ## + # Old rdoc behavior: hyperlink all words that match a method name, + # even if not preceded by '#' or '::' + + attr_accessor :hyperlink_all + + ## + # Include line numbers in the source code + + attr_accessor :line_numbers ## # Name of the file, class or module to display in the initial index page (if @@ -44,44 +99,54 @@ class RDoc::Options attr_accessor :main_page ## + # If true, only report on undocumented files + + attr_accessor :coverage_report + + ## # The name of the output directory attr_accessor :op_dir ## - # Is RDoc in pipe mode? + # The OptionParser for this instance - attr_accessor :pipe + attr_accessor :option_parser ## - # Array of directories to search for files to satisfy an :include: + # Is RDoc in pipe mode? - attr_reader :rdoc_include + attr_accessor :pipe ## - # Include private and protected methods in the output? + # Array of directories to search for files to satisfy an :include: - attr_accessor :show_all + attr_accessor :rdoc_include ## # Include the '#' at the front of hyperlinked instance method names - attr_reader :show_hash + attr_accessor :show_hash ## # The number of columns in a tab - attr_reader :tab_width + attr_accessor :tab_width ## # Template to be used when generating output - attr_reader :template + attr_accessor :template + + ## + # Directory the template lives in + + attr_accessor :template_dir ## # Documentation title - attr_reader :title + attr_accessor :title ## # Verbosity, zero means quiet @@ -91,29 +156,88 @@ class RDoc::Options ## # URL of web cvs frontend - attr_reader :webcvs + attr_accessor :webcvs + + ## + # Minimum visibility of a documented method. One of +:public+, + # +:protected+, +:private+. May be overridden on a per-method + # basis with the :doc: directive. + + attr_accessor :visibility def initialize # :nodoc: require 'rdoc/rdoc' - @op_dir = nil - @show_all = false - @main_page = nil + @dry_run = false @exclude = [] - @generators = RDoc::RDoc::GENERATORS - @generator = RDoc::Generator::Darkfish + @force_output = false + @force_update = true + @generator = nil @generator_name = nil + @generator_options = [] + @generators = RDoc::RDoc::GENERATORS + @hyperlink_all = false + @line_numbers = false + @main_page = nil + @coverage_report = false + @op_dir = nil + @pipe = false @rdoc_include = [] - @title = nil - @template = nil @show_hash = false + @stylesheet_url = nil @tab_width = 8 - @force_update = true + @template = nil + @template_dir = nil + @title = nil @verbosity = 1 - @pipe = false - + @visibility = :protected @webcvs = nil - @charset = 'utf-8' + if Object.const_defined? :Encoding then + @encoding = Encoding.default_external + @charset = @encoding.to_s + else + @charset = 'UTF-8' + end + end + + ## + # Check that the files on the command line exist + + def check_files + @files.delete_if do |file| + if File.exist? file then + if File.readable? file then + false + else + warn "file '#{file}' not readable" + + true + end + else + warn "file '#{file}' not found" + + true + end + end + end + + ## + # Ensure only one generator is loaded + + def check_generator + if @generator then + raise OptionParser::InvalidOption, + "generator already set to #{@generator_name}" + end + end + + ## + # Set the title, but only if not already set. Used to set the title + # from a source file, so that a title set from the command line + # will have the priority. + + def default_title=(string) + @title ||= string end ## @@ -122,7 +246,10 @@ class RDoc::Options def parse(argv) ignore_invalid = true + argv.insert(0, *ENV['RDOCOPT'].split) if ENV['RDOCOPT'] + opts = OptionParser.new do |opt| + @option_parser = opt opt.program_name = File.basename $0 opt.version = RDoc::VERSION opt.release = nil @@ -139,6 +266,14 @@ Usage: #{opt.program_name} [options] [names...] How RDoc generates output depends on the output formatter being used, and on the options you give. + Options can be specified via the RDOCOPT environment variable, which + functions similar to the RUBYOPT environment variable for ruby. + + $ export RDOCOPT="--show-hash" + + will make rdoc show hashes in method links by default. Command-line options + always will override those in RDOCOPT. + - Darkfish creates frameless HTML output by Michael Granger. - ri creates ri data files @@ -156,14 +291,44 @@ Usage: #{opt.program_name} [options] [names...] opt.banner << " - #{parser}: #{regexp.join ', '}\n" end + opt.banner << "\n The following options are deprecated:\n\n" + + name_length = DEPRECATED.keys.sort_by { |k| k.length }.last.length + + DEPRECATED.sort_by { |k,| k }.each do |name, reason| + opt.banner << " %*1$2$s %3$s\n" % [-name_length, name, reason] + end + + opt.accept Template do |template| + template_dir = template_dir_for template + + unless template_dir then + warn "could not find template #{template}" + nil + else + [template, template_dir] + end + end + opt.separator nil - opt.separator "Parsing Options:" + opt.separator "Parsing options:" opt.separator nil + if Object.const_defined? :Encoding then + opt.on("--encoding=ENCODING", "-e", Encoding.list.map { |e| e.name }, + "Specifies the output encoding. All files", + "read will be converted to this encoding.", + "Preferred over --charset") do |value| + @encoding = Encoding.find value + @charset = @encoding.to_s # may not be valid value + end + + opt.separator nil + end + opt.on("--all", "-a", - "Include all methods (not just public) in", - "the output.") do |value| - @show_all = value + "Synonym for --visibility=private.") do |value| + @visibility = :private end opt.separator nil @@ -207,20 +372,41 @@ Usage: #{opt.program_name} [options] [names...] end opt.separator nil - opt.separator "Generator Options:" + + opt.on("--tab-width=WIDTH", "-w", OptionParser::DecimalInteger, + "Set the width of tab characters.") do |value| + @tab_width = value + end + opt.separator nil - opt.on("--charset=CHARSET", "-c", - "Specifies the output HTML character-set.") do |value| - @charset = value + opt.on("--visibility=VISIBILITY", "-V", RDoc::VISIBILITIES, + "Minimum visibility to document a method.", + "One of 'public', 'protected' (the default)", + "or 'private'. Can be abbreviated.") do |value| + @visibility = value + end + + opt.separator nil + opt.separator "Common generator options:" + opt.separator nil + + opt.on("--force-output", "-O", + "Forces rdoc to write the output files,", + "even if the output directory exists", + "and does not seem to have been created", + "by rdoc.") do |value| + @force_output = value end opt.separator nil generator_text = @generators.keys.map { |name| " #{name}" }.sort - opt.on("--fmt=FORMAT", "--format=FORMAT", "-f", @generators.keys, + opt.on("-f", "--fmt=FORMAT", "--format=FORMAT", @generators.keys, "Set the output formatter. One of:", *generator_text) do |value| + check_generator + @generator_name = value.downcase setup_generator end @@ -236,9 +422,11 @@ Usage: #{opt.program_name} [options] [names...] opt.separator nil - opt.on("--main=NAME", "-m", - "NAME will be the initial page displayed.") do |value| - @main_page = value + opt.on("--[no-]coverage-report", "--[no-]dcov", "-C", + "Prints a report on undocumented items.", + "Does not generate files.") do |value| + @coverage_report = value + @force_update = true if value end opt.separator nil @@ -250,6 +438,51 @@ Usage: #{opt.program_name} [options] [names...] opt.separator nil + opt.on("-d", + "Deprecated --diagram option.", + "Prevents firing debug mode", + "with legacy invocation.") do |value| + end + + opt.separator nil + opt.separator 'HTML generator options:' + opt.separator nil + + opt.on("--charset=CHARSET", "-c", + "Specifies the output HTML character-set.", + "Use --encoding instead of --charset if", + "available.") do |value| + @charset = value + end + + opt.separator nil + + opt.on("--hyperlink-all", "-A", + "Generate hyperlinks for all words that", + "correspond to known methods, even if they", + "do not start with '#' or '::' (legacy", + "behavior).") do |value| + @hyperlink_all = value + end + + opt.separator nil + + opt.on("--main=NAME", "-m", + "NAME will be the initial page displayed.") do |value| + @main_page = value + end + + opt.separator nil + + opt.on("--[no-]line-numbers", "-N", + "Include line numbers in the source code.", + "By default, only the number of the first", + "line is displayed, in a leading comment.") do |value| + @line_numbers = value + end + + opt.separator nil + opt.on("--show-hash", "-H", "A name of the form #name in a comment is a", "possible hyperlink to an instance method", @@ -260,17 +493,12 @@ Usage: #{opt.program_name} [options] [names...] opt.separator nil - opt.on("--tab-width=WIDTH", "-w", OptionParser::DecimalInteger, - "Set the width of tab characters.") do |value| - @tab_width = value - end - - opt.separator nil - - opt.on("--template=NAME", "-T", + opt.on("--template=NAME", "-T", Template, "Set the template used when generating", - "output.") do |value| - @template = value + "output. The default depends on the", + "formatter used.") do |(template, template_dir)| + @template = template + @template_dir = template_dir end opt.separator nil @@ -292,11 +520,7 @@ Usage: #{opt.program_name} [options] [names...] end opt.separator nil - - opt.on("-d", "--diagram", "Prevents -d from tripping --debug") - - opt.separator nil - opt.separator "ri Generator Options:" + opt.separator "ri generator options:" opt.separator nil opt.on("--ri", "-r", @@ -305,6 +529,8 @@ Usage: #{opt.program_name} [options] [names...] "your home directory unless overridden by a", "subsequent --op parameter, so no special", "privileges are needed.") do |value| + check_generator + @generator_name = "ri" @op_dir ||= RDoc::RI::Paths::HOMEDIR setup_generator @@ -317,22 +543,30 @@ Usage: #{opt.program_name} [options] [names...] "are stored in a site-wide directory,", "making them accessible to others, so", "special privileges are needed.") do |value| + check_generator + @generator_name = "ri" @op_dir = RDoc::RI::Paths::SITEDIR setup_generator end opt.separator nil - opt.separator "Generic Options:" + opt.separator "Generic options:" opt.separator nil + opt.on("--[no-]dry-run", + "Don't write any files") do |value| + @dry_run = value + end + opt.on("-D", "--[no-]debug", "Displays lots on internal stuff.") do |value| $DEBUG_RDOC = value end opt.on("--[no-]ignore-invalid", - "Ignore invalid options and continue.") do |value| + "Ignore invalid options and continue", + "(default true).") do |value| ignore_invalid = value end @@ -342,38 +576,70 @@ Usage: #{opt.program_name} [options] [names...] end opt.on("--verbose", "-v", - "Display extra progress as we parse.") do |value| + "Display extra progress as RDoc parses") do |value| @verbosity = 2 end + opt.on("--help", + "Display this help") do + RDoc::RDoc::GENERATORS.each_key do |generator| + setup_generator generator + end + + puts opt.help + exit + end + opt.separator nil end - argv.insert(0, *ENV['RDOCOPT'].split) if ENV['RDOCOPT'] - ignored = [] + setup_generator 'darkfish' if + argv.grep(/\A(-f|--fmt|--format|-r|-R|--ri|--ri-site)\b/).empty? + + deprecated = [] + invalid = [] begin opts.parse! argv rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e - if ignore_invalid then - ignored << e.args.join(' ') - retry + if DEPRECATED[e.args.first] then + deprecated << e.args.first + elsif %w[--format --ri -r --ri-site -R].include? e.args.first then + raise else - $stderr.puts opts - $stderr.puts - $stderr.puts e - exit 1 + invalid << e.args.join(' ') end + + retry + end + + unless @generator then + @generator = RDoc::Generator::Darkfish + @generator_name = 'darkfish' end if @pipe and not argv.empty? then @pipe = false - ignored << '-p (with files)' + invalid << '-p (with files)' end - unless ignored.empty? or quiet then - $stderr.puts "invalid options: #{ignored.join ', '}" - $stderr.puts '(invalid options are ignored)' + unless quiet then + deprecated.each do |opt| + $stderr.puts 'option ' << opt << ' is deprecated: ' << DEPRECATED[opt] + end + + unless invalid.empty? then + invalid = "invalid options: #{invalid.join ', '}" + + if ignore_invalid then + $stderr.puts invalid + $stderr.puts '(invalid options are ignored)' + else + $stderr.puts opts + $stderr.puts invalid + exit 1 + end + end end @op_dir ||= 'doc' @@ -392,15 +658,10 @@ Usage: #{opt.program_name} [options] [names...] # If no template was specified, use the default template for the output # formatter - @template ||= @generator_name - end - - ## - # Set the title, but only if not already set. This means that a title set - # from the command line trumps one set in a source file - - def title=(string) - @title ||= string + unless @template then + @template = @generator_name + @template_dir = template_dir_for @template + end end ## @@ -410,30 +671,46 @@ Usage: #{opt.program_name} [options] [names...] @verbosity.zero? end - def quiet=(bool) + ## + # Set quietness to +bool+ + + def quiet= bool @verbosity = bool ? 0 : 1 end - private - ## - # Set up an output generator for the format in @generator_name + # Set up an output generator for the named +generator_name+. + # + # If the found generator responds to :setup_options it will be called with + # the options instance. This allows generators to add custom options or set + # default options. - def setup_generator - @generator = @generators[@generator_name] + def setup_generator generator_name = @generator_name + @generator = @generators[generator_name] unless @generator then - raise OptionParser::InvalidArgument, "Invalid output formatter" + raise OptionParser::InvalidArgument, + "Invalid output formatter #{generator_name}" end + + return if @generator_options.include? @generator + + @generator_name = generator_name + @generator_options << @generator + + @generator.setup_options self if @generator.respond_to? :setup_options end ## - # Check that the files on the command line exist + # Finds the template dir for +template+ - def check_files - @files.each do |f| - stat = File.stat f rescue next - raise RDoc::Error, "file '#{f}' not readable" unless stat.readable? + def template_dir_for template + template_path = File.join 'rdoc', 'generator', 'template', template + + $LOAD_PATH.map do |path| + File.join File.expand_path(path), template_path + end.find do |dir| + File.directory? dir end end diff --git a/lib/rdoc/parser.rb b/lib/rdoc/parser.rb index 1e4ab7c7a4..d218aba62a 100644 --- a/lib/rdoc/parser.rb +++ b/lib/rdoc/parser.rb @@ -1,6 +1,6 @@ require 'rdoc' require 'rdoc/code_objects' -require 'rdoc/markup/preprocess' +require 'rdoc/markup/pre_process' require 'rdoc/stats' ## @@ -43,7 +43,15 @@ class RDoc::Parser @parsers = [] class << self + + ## + # A Hash that maps file exetensions regular expressions to parsers that + # will consume them. + # + # Use parse_files_matching to register a parser's file extensions. + attr_reader :parsers + end ## @@ -67,18 +75,51 @@ class RDoc::Parser # content that an RDoc parser shouldn't try to consume. def self.binary?(file) + return false if file =~ /\.(rdoc|txt)$/ + s = File.read(file, 1024) or return false - if s[0, 2] == Marshal.dump('')[0, 2] then - true - elsif file =~ /erb\.rb$/ then - false - elsif s.scan(/<%|%>/).length >= 4 || s.index("\x00") then - true - elsif 0.respond_to? :fdiv then - s.count("\x00-\x7F", "^ -~\t\r\n").fdiv(s.size) > 0.3 - else # HACK 1.8.6 - (s.count("\x00-\x7F", "^ -~\t\r\n").to_f / s.size) > 0.3 + have_encoding = s.respond_to? :encoding + + if have_encoding then + return false if s.encoding != Encoding::ASCII_8BIT and s.valid_encoding? + end + + return true if s[0, 2] == Marshal.dump('')[0, 2] or s.index("\x00") + + if have_encoding then + s.force_encoding Encoding.default_external + + not s.valid_encoding? + else + if 0.respond_to? :fdiv then + s.count("\x00-\x7F", "^ -~\t\r\n").fdiv(s.size) > 0.3 + else # HACK 1.8.6 + (s.count("\x00-\x7F", "^ -~\t\r\n").to_f / s.size) > 0.3 + end + end + end + + ## + # Processes common directives for CodeObjects for the C and Ruby parsers. + # + # Applies +directive+'s +value+ to +code_object+, if appropriate + + def self.process_directive code_object, directive, value + case directive + when 'nodoc' then + code_object.document_self = nil # notify nodoc + code_object.document_children = value.downcase != 'all' + when 'doc' then + code_object.document_self = true + code_object.force_documentation = true + when 'yield', 'yields' then + # remove parameter &block + code_object.params.sub!(/,?\s*&\w+/, '') if code_object.params + + code_object.block_params = value + when 'arg', 'args' then + code_object.params = value end end @@ -143,6 +184,12 @@ class RDoc::Parser RDoc::Parser.parsers.unshift [regexp, self] end + ## + # Creates a new Parser storing +top_level+, +file_name+, +content+, + # +options+ and +stats+ in instance variables. + # + # Usually invoked by +super+ + def initialize(top_level, file_name, content, options, stats) @top_level = top_level @file_name = file_name diff --git a/lib/rdoc/parser/c.rb b/lib/rdoc/parser/c.rb index e218298dfe..60e9fefd61 100644 --- a/lib/rdoc/parser/c.rb +++ b/lib/rdoc/parser/c.rb @@ -3,9 +3,9 @@ require 'rdoc/parser/ruby' require 'rdoc/known_classes' ## -# We attempt to parse C extension files. Basically we look for +# RDoc::Parser::C attempts to parse C extension files. It looks for # the standard patterns that you find in extensions: <tt>rb_define_class, -# rb_define_method</tt> and so on. We also try to find the corresponding +# rb_define_method</tt> and so on. It tries to find the corresponding # C source for the methods and extract comments, but if we fail # we don't worry too much. # @@ -49,13 +49,26 @@ require 'rdoc/known_classes' # # The comment blocks may include special directives: # -# [Document-class: <i>name</i>] -# This comment block is documentation for the given class. Use this -# when the <tt>Init_xxx</tt> method is not named after the class. +# [Document-class: +name+] +# Documentation for the named class. # -# [Document-method: <i>name</i>] -# This comment documents the named method. Use when RDoc cannot -# automatically find the method from it's declaration +# [Document-module: +name+] +# Documentation for the named module. +# +# [Document-const: +name+] +# Documentation for the named +rb_define_const+. +# +# [Document-global: +name+] +# Documentation for the named +rb_define_global_const+ +# +# [Document-variable: +name+] +# Documentation for the named +rb_define_variable+ +# +# [Document-method: +name+] +# Documentation for the named method. +# +# [Document-attr: +name+] +# Documentation for the named attribute. # # [call-seq: <i>text up to an empty line</i>] # Because C source doesn't give descripive names to Ruby-level parameters, @@ -120,21 +133,61 @@ class RDoc::Parser::C < RDoc::Parser @known_classes = RDoc::KNOWN_CLASSES.dup @content = handle_tab_width handle_ifdefs_in(@content) @classes = Hash.new + @singleton_classes = Hash.new @file_dir = File.dirname(@file_name) end + ## + # Scans #content for rb_define_alias + def do_aliases - @content.scan(%r{rb_define_alias\s*\(\s*(\w+),\s*"([^"]+)",\s*"([^"]+)"\s*\)}m) do - |var_name, new_name, old_name| + @content.scan(/rb_define_alias\s*\( + \s*(\w+), + \s*"(.+?)", + \s*"(.+?)" + \s*\)/xm) do |var_name, new_name, old_name| class_name = @known_classes[var_name] || var_name - class_obj = find_class(var_name, class_name) + class_obj = find_class var_name, class_name + + al = RDoc::Alias.new '', old_name, new_name, '' + al.singleton = @singleton_classes.key?(var_name) + + comment = find_alias_comment var_name, new_name, old_name + comment = strip_stars comment + al.comment = comment + + class_obj.add_alias al + @stats.add_alias al + end + end - as = class_obj.add_alias RDoc::Alias.new("", old_name, new_name, "") + ## + # Scans #content for rb_attr and rb_define_attr + + def do_attrs + @content.scan(/rb_attr\s*\( + \s*(\w+), + \s*([\w"()]+), + \s*([01]), + \s*([01]), + \s*\w+\);/xm) do |var_name, attr_name, read, write| + handle_attr var_name, attr_name, read, write + end - @stats.add_alias as + @content.scan(%r%rb_define_attr\( + \s*([\w\.]+), + \s*"([^"]+)", + \s*(\d+), + \s*(\d+)\s*\); + %xm) do |var_name, attr_name, read, write| + handle_attr var_name, attr_name, read, write end end + ## + # Scans #content for rb_define_module, rb_define_class, boot_defclass, + # rb_define_module_under, rb_define_class_under and rb_singleton_class + def do_classes @content.scan(/(\w+)\s* = \s*rb_define_module\s*\(\s*"(\w+)"\s*\)/mx) do |var_name, class_name| @@ -165,35 +218,44 @@ class RDoc::Parser::C < RDoc::Parser end @content.scan(/([\w\.]+)\s* = \s*rb_define_class_under\s* - \( - \s*(\w+), - \s*"(\w+)", - \s*([\w\*\s\(\)\.\->]+)\s* # for SWIG - \s*\)/mx) do |var_name, in_module, class_name, parent| + \( + \s*(\w+), + \s*"(\w+)", + \s*([\w\*\s\(\)\.\->]+)\s* # for SWIG + \s*\)/mx) do |var_name, in_module, class_name, parent| handle_class_module(var_name, "class", class_name, parent, in_module) end + + @content.scan(/([\w\.]+)\s* = \s*rb_singleton_class\s* + \( + \s*(\w+) + \s*\)/mx) do |sclass_var, class_var| + handle_singleton sclass_var, class_var + end end + ## + # Scans #content for rb_define_variable, rb_define_readonly_variable, + # rb_define_const and rb_define_global_const + def do_constants - @content.scan(%r{\Wrb_define_ + @content.scan(%r%\Wrb_define_ ( variable | readonly_variable | const | - global_const | ) + global_const ) \s*\( (?:\s*(\w+),)? \s*"(\w+)", \s*(.*?)\s*\)\s*; - }xm) do |type, var_name, const_name, definition| + %xm) do |type, var_name, const_name, definition| var_name = "rb_cObject" if !var_name or var_name == "rb_mKernel" handle_constants type, var_name, const_name, definition end end ## - # Look for includes of the form: - # - # rb_include_module(rb_cArray, rb_mEnumerable); + # Scans #content for rb_include_module def do_includes @content.scan(/rb_include_module\s*\(\s*(\w+?),\s*(\w+?)\s*\)/) do |c,m| @@ -204,8 +266,13 @@ class RDoc::Parser::C < RDoc::Parser end end + ## + # Scans #content for rb_define_method, rb_define_singleton_method, + # rb_define_module_function, rb_define_private_method, + # rb_define_global_function and define_filetest_function + def do_methods - @content.scan(%r{rb_define_ + @content.scan(%r%rb_define_ ( singleton_method | method | @@ -217,8 +284,7 @@ class RDoc::Parser::C < RDoc::Parser \s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?, \s*(-?\w+)\s*\) (?:;\s*/[*/]\s+in\s+(\w+?\.[cy]))? - }xm) do - |type, var_name, meth_name, meth_body, param_count, source_file| + %xm) do |type, var_name, meth_name, meth_body, param_count, source_file| # Ignore top-object and weird struct.c dynamic stuff next if var_name == "ruby_top_self" @@ -231,44 +297,69 @@ class RDoc::Parser::C < RDoc::Parser source_file) end - @content.scan(%r{rb_define_attr\( - \s*([\w\.]+), - \s*"([^"]+)", - \s*(\d+), - \s*(\d+)\s*\); - }xm) do |var_name, attr_name, attr_reader, attr_writer| - #var_name = "rb_cObject" if var_name == "rb_mKernel" - handle_attr(var_name, attr_name, - attr_reader.to_i != 0, - attr_writer.to_i != 0) - end - - @content.scan(%r{rb_define_global_function\s*\( + @content.scan(%r%rb_define_global_function\s*\( \s*"([^"]+)", \s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?, \s*(-?\w+)\s*\) (?:;\s*/[*/]\s+in\s+(\w+?\.[cy]))? - }xm) do |meth_name, meth_body, param_count, source_file| + %xm) do |meth_name, meth_body, param_count, source_file| handle_method("method", "rb_mKernel", meth_name, meth_body, param_count, source_file) end @content.scan(/define_filetest_function\s*\( - \s*"([^"]+)", - \s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?, - \s*(-?\w+)\s*\)/xm) do - |meth_name, meth_body, param_count| + \s*"([^"]+)", + \s*(?:RUBY_METHOD_FUNC\(|VALUEFUNC\()?(\w+)\)?, + \s*(-?\w+)\s*\)/xm) do |meth_name, meth_body, param_count| handle_method("method", "rb_mFileTest", meth_name, meth_body, param_count) handle_method("singleton_method", "rb_cFile", meth_name, meth_body, param_count) end end - def find_attr_comment(attr_name) - if @content =~ %r{((?>/\*.*?\*/\s+)) - rb_define_attr\((?:\s*(\w+),)?\s*"#{attr_name}"\s*,.*?\)\s*;}xmi + ## + # Finds the comment for an alias on +class_name+ from +new_name+ to + # +old_name+ + + def find_alias_comment class_name, new_name, old_name + content =~ %r%((?>/\*.*?\*/\s+)) + rb_define_alias\(\s*#{Regexp.escape class_name}\s*, + \s*"#{Regexp.escape new_name}"\s*, + \s*"#{Regexp.escape old_name}"\s*\);%xm + + $1 || '' + end + + ## + # Finds a comment for rb_define_attr, rb_attr or Document-attr. + # + # +var_name+ is the C class variable the attribute is defined on. + # +attr_name+ is the attribute's name. + # + # +read+ and +write+ are the read/write flags ('1' or '0'). Either both or + # neither must be provided. + + def find_attr_comment var_name, attr_name, read = nil, write = nil + attr_name = Regexp.escape attr_name + + rw = if read and write then + /\s*#{read}\s*,\s*#{write}\s*/xm + else + /.*?/m + end + + if @content =~ %r%((?>/\*.*?\*/\s+)) + rb_define_attr\((?:\s*#{var_name},)?\s* + "#{attr_name}"\s*, + #{rw}\)\s*;%xm then + $1 + elsif @content =~ %r%((?>/\*.*?\*/\s+)) + rb_attr\(\s*#{var_name}\s*, + \s*#{attr_name}\s*, + #{rw},.*?\)\s*;%xm then $1 - elsif @content =~ %r{Document-attr:\s#{attr_name}\s*?\n((?>.*?\*/))}m + elsif @content =~ %r%Document-attr:\s#{attr_name}\s*?\n + ((?>.*?\*/))%xm then $1 else '' @@ -280,8 +371,10 @@ class RDoc::Parser::C < RDoc::Parser def find_body(class_name, meth_name, meth_obj, body, quiet = false) case body - when %r"((?>/\*.*?\*/\s*))((?:(?:static|SWIGINTERN)\s+)?(?:intern\s+)?VALUE\s+#{meth_name} - \s*(\([^)]*\))([^;]|$))"xm + when %r%((?>/\*.*?\*/\s*)) + ((?:(?:static|SWIGINTERN)\s+)? + (?:intern\s+)?VALUE\s+#{meth_name} + \s*(\([^)]*\))([^;]|$))%xm then comment = $1 body_text = $2 @@ -303,12 +396,13 @@ class RDoc::Parser::C < RDoc::Parser find_modifiers comment, meth_obj if comment + #meth_obj.params = params meth_obj.start_collecting_tokens tk = RDoc::RubyToken::Token.new nil, 1, 1 tk.set_text body_text meth_obj.add_token tk meth_obj.comment = strip_stars comment - when %r{((?>/\*.*?\*/\s*))^\s*(\#\s*define\s+#{meth_name}\s+(\w+))}m + when %r%((?>/\*.*?\*/\s*))^\s*(\#\s*define\s+#{meth_name}\s+(\w+))%m comment = $1 body_text = $2 find_body class_name, $3, meth_obj, body, true @@ -319,26 +413,29 @@ class RDoc::Parser::C < RDoc::Parser tk.set_text body_text meth_obj.add_token tk meth_obj.comment = strip_stars(comment) + meth_obj.comment.to_s - when %r{^\s*\#\s*define\s+#{meth_name}\s+(\w+)}m + when %r%^\s*\#\s*define\s+#{meth_name}\s+(\w+)%m unless find_body(class_name, $1, meth_obj, body, true) - warn "No definition for #{meth_name}" unless @options.quiet + warn "No definition for #{meth_name}" if @options.verbosity > 1 return false end - else - # No body, but might still have an override comment - comment = find_override_comment(class_name, meth_obj.name) + else # No body, but might still have an override comment + comment = find_override_comment class_name, meth_obj.name if comment - find_modifiers(comment, meth_obj) + find_modifiers comment, meth_obj meth_obj.comment = strip_stars comment else - warn "No definition for #{meth_name}" unless @options.quiet + warn "No definition for #{meth_name}" if @options.verbosity > 1 return false end end + true end + ## + # Finds a RDoc::NormalClass or RDoc::NormalModule for +raw_name+ + def find_class(raw_name, name) unless @classes[raw_name] if raw_name =~ /^rb_m/ @@ -382,16 +479,17 @@ class RDoc::Parser::C < RDoc::Parser def find_class_comment(class_name, class_mod) comment = nil - if @content =~ %r{ + if @content =~ %r% ((?>/\*.*?\*/\s+)) (static\s+)? void\s+ - Init_#{class_name}\s*(?:_\(\s*)?\(\s*(?:void\s*)?\)}xmi then # ) - comment = $1 - elsif @content =~ %r{Document-(?:class|module):\s+#{class_name}\s*?(?:<\s+[:,\w]+)?\n((?>.*?\*/))}m then + Init_#{class_name}\s*(?:_\(\s*)?\(\s*(?:void\s*)?\)%xmi then + comment = $1.sub(%r%Document-(?:class|module):\s+#{class_name}%, '') + elsif @content =~ %r%Document-(?:class|module):\s+#{class_name}\s*? + (?:<\s+[:,\w]+)?\n((?>.*?\*/))%xm then comment = $1 - elsif @content =~ %r{((?>/\*.*?\*/\s+)) - ([\w\.\s]+\s* = \s+)?rb_define_(class|module).*?"(#{class_name})"}xm then # " + elsif @content =~ %r%((?>/\*.*?\*/\s+)) + ([\w\.\s]+\s* = \s+)?rb_define_(class|module).*?"(#{class_name})"%xm then comment = $1 end @@ -409,10 +507,13 @@ class RDoc::Parser::C < RDoc::Parser # comment or in the matching Document- section. def find_const_comment(type, const_name) - if @content =~ %r{((?>^\s*/\*.*?\*/\s+)) - rb_define_#{type}\((?:\s*(\w+),)?\s*"#{const_name}"\s*,.*?\)\s*;}xmi + if @content =~ %r%((?>^\s*/\*.*?\*/\s+)) + rb_define_#{type}\((?:\s*(\w+),)?\s* + "#{const_name}"\s*, + .*?\)\s*;%xmi then $1 - elsif @content =~ %r{Document-(?:const|global|variable):\s#{const_name}\s*?\n((?>.*?\*/))}m + elsif @content =~ %r%Document-(?:const|global|variable):\s#{const_name} + \s*?\n((?>.*?\*/))%xm $1 else '' @@ -420,56 +521,111 @@ class RDoc::Parser::C < RDoc::Parser end ## - # If the comment block contains a section that looks like: + # Handles modifiers in +comment+ and updates +meth_obj+ as appropriate. + # + # If <tt>:nodoc:</tt> is found, documentation on +meth_obj+ is suppressed. + # + # If <tt>:yields:</tt> is followed by an argument list it is used for the + # #block_params of +meth_obj+. + # + # If the comment block contains a <tt>call-seq:</tt> section like: + # + # call-seq: + # ARGF.readlines(sep=$/) -> array + # ARGF.readlines(limit) -> array + # ARGF.readlines(sep, limit) -> array # - # call-seq: - # Array.new - # Array.new(10) + # ARGF.to_a(sep=$/) -> array + # ARGF.to_a(limit) -> array + # ARGF.to_a(sep, limit) -> array # - # use it for the parameters. + # it is used for the parameters of +meth_obj+. + + def find_modifiers comment, meth_obj + # we must handle situations like the above followed by an unindented first + # comment. The difficulty is to make sure not to match lines starting + # with ARGF at the same indent, but that are after the first description + # paragraph. + + if comment =~ /call-seq:(.*?[^\s\*].*?)^\s*\*?\s*$/m then + all_start, all_stop = $~.offset(0) + seq_start, seq_stop = $~.offset(1) + + # we get the following lines that start with the leading word at the + # same indent, even if they have blank lines before + if $1 =~ /(^\s*\*?\s*\n)+^(\s*\*?\s*\w+)/m then + leading = $2 # ' * ARGF' in the example above + re = %r% + \A( + (^\s*\*?\s*\n)+ + (^#{Regexp.escape leading}.*?\n)+ + )+ + ^\s*\*?\s*$ + %xm + if comment[seq_stop..-1] =~ re then + all_stop = seq_stop + $~.offset(0).last + seq_stop = seq_stop + $~.offset(1).last + end + end - def find_modifiers(comment, meth_obj) - if comment.sub!(/:nodoc:\s*^\s*\*?\s*$/m, '') or - comment.sub!(/\A\/\*\s*:nodoc:\s*\*\/\Z/, '') - meth_obj.document_self = false - end - if comment.sub!(/call-seq:(.*?)^\s*\*?\s*$/m, '') or - comment.sub!(/\A\/\*\s*call-seq:(.*?)\*\/\Z/, '') - seq = $1 - seq.gsub!(/^\s*\*\s*/, '') + seq = comment[seq_start..seq_stop] + seq.gsub!(/^(\s*\*?\s*?)(\S|\n)/m, '\2') + comment.slice! all_start...all_stop meth_obj.call_seq = seq + elsif comment.sub!(/\A\/\*\s*call-seq:(.*?)\*\/\Z/, '') then + meth_obj.call_seq = $1.strip + end + + if comment.sub!(/\s*:(nodoc|doc|yields?|args?):\s*(.*)/, '') then + RDoc::Parser.process_directive meth_obj, $1, $2 end end + ## + # Finds a <tt>Document-method</tt> override for +meth_name+ in +class_name+ + def find_override_comment(class_name, meth_name) name = Regexp.escape(meth_name) - if @content =~ %r{Document-method:\s+#{class_name}(?:\.|::|#)#{name}\s*?\n((?>.*?\*/))}m then + + if @content =~ %r%Document-method:\s+#{class_name}(?:\.|::|#)#{name}\s*?\n((?>.*?\*/))%m then $1 - elsif @content =~ %r{Document-method:\s#{name}\s*?\n((?>.*?\*/))}m then + elsif @content =~ %r%Document-method:\s#{name}\s*?\n((?>.*?\*/))%m then $1 end end - def handle_attr(var_name, attr_name, reader, writer) + ## + # Creates a new RDoc::Attr +attr_name+ on class +var_name+ that is either + # +read+, +write+ or both + + def handle_attr(var_name, attr_name, read, write) rw = '' - rw << 'R' if reader - rw << 'W' if writer + rw << 'R' if '1' == read + rw << 'W' if '1' == write class_name = @known_classes[var_name] return unless class_name - class_obj = find_class(var_name, class_name) + class_obj = find_class var_name, class_name + + return unless class_obj - if class_obj - comment = find_attr_comment(attr_name) - comment = strip_stars comment - att = RDoc::Attr.new '', attr_name, rw, comment - @stats.add_method att - class_obj.add_attribute(att) - end + comment = find_attr_comment var_name, attr_name + comment = strip_stars comment + + name = attr_name.gsub(/rb_intern\("([^"]+)"\)/, '\1') + + attr = RDoc::Attr.new '', name, rw, comment + + class_obj.add_attribute attr + @stats.add_attribute attr end + ## + # Creates a new RDoc::NormalClass or RDoc::NormalModule based on +type+ + # named +class_name+ in +parent+ which was assigned to the C +var_name+. + def handle_class_module(var_name, type, class_name, parent, in_module) parent_name = @known_classes[parent] || parent @@ -497,7 +653,7 @@ class RDoc::Parser::C < RDoc::Parser class_name end - if @content =~ %r{Document-class:\s+#{full_name}\s*<\s+([:,\w]+)} then + if @content =~ %r%Document-class:\s+#{full_name}\s*<\s+([:,\w]+)% then parent_name = $1 end @@ -519,15 +675,14 @@ class RDoc::Parser::C < RDoc::Parser end ## - # Adds constant comments. By providing some_value: at the start ofthe - # comment you can override the C value of the comment to give a friendly - # definition. + # Adds constants. By providing some_value: at the start of the comment you + # can override the C value of the comment to give a friendly definition. # # /* 300: The perfect score in bowling */ # rb_define_const(cFoo, "PERFECT", INT2FIX(300); # - # Will override +INT2FIX(300)+ with the value +300+ in the output RDoc. - # Values may include quotes and escaped colons (\:). + # Will override <tt>INT2FIX(300)</tt> with the value +300+ in the output + # RDoc. Values may include quotes and escaped colons (\:). def handle_constants(type, var_name, const_name, definition) class_name = @known_classes[var_name] @@ -588,22 +743,35 @@ class RDoc::Parser::C < RDoc::Parser body.gsub(/^#ifdef HAVE_PROTOTYPES.*?#else.*?\n(.*?)#endif.*?\n/m, '\1') end + ## + # Adds an RDoc::AnyMethod +meth_name+ defined on a class or module assigned + # to +var_name+. +type+ is the type of method definition function used. + # +singleton_method+ and +module_function+ create a singleton method. + def handle_method(type, var_name, meth_name, meth_body, param_count, source_file = nil) + singleton = false class_name = @known_classes[var_name] + unless class_name then + class_name = @singleton_classes[var_name] + singleton = true if class_name + end + return unless class_name class_obj = find_class var_name, class_name if class_obj then - if meth_name == "initialize" then - meth_name = "new" - type = "singleton_method" + if meth_name == 'initialize' then + meth_name = 'new' + singleton = true + type = 'method' # force public end meth_obj = RDoc::AnyMethod.new '', meth_name - meth_obj.singleton = %w[singleton_method module_function].include? type + meth_obj.singleton = + singleton || %w[singleton_method module_function].include?(type) p_count = Integer(param_count) rescue -1 @@ -627,7 +795,8 @@ class RDoc::Parser::C < RDoc::Parser body = @content end - if find_body(class_name, meth_body, meth_obj, body) and meth_obj.document_self then + if find_body(class_name, meth_body, meth_obj, body) and + meth_obj.document_self then class_obj.add_method meth_obj @stats.add_method meth_obj meth_obj.visibility = :private if 'private_method' == type @@ -635,13 +804,27 @@ class RDoc::Parser::C < RDoc::Parser end end + ## + # Registers a singleton class +sclass_var+ as a singleton of +class_var+ + + def handle_singleton sclass_var, class_var + class_name = @known_classes[class_var] + + @singleton_classes[sclass_var] = class_name + end + + ## + # Normalizes tabs in +body+ + def handle_tab_width(body) if /\t/ =~ body tab_width = @options.tab_width body.split(/\n/).map do |line| - 1 while line.gsub!(/\t+/) { ' ' * (tab_width*$&.length - $`.length % tab_width)} && $~ #` + 1 while line.gsub!(/\t+/) do + ' ' * (tab_width * $&.length - $`.length % tab_width) + end && $~ line - end .join("\n") + end.join "\n" else body end @@ -654,7 +837,7 @@ class RDoc::Parser::C < RDoc::Parser # * :title: My Awesome Project # */ # - # This routine modifies it's parameter + # This routine modifies its parameter def look_for_directives_in(context, comment) preprocess = RDoc::Markup::PreProcess.new @file_name, @options.rdoc_include @@ -665,29 +848,33 @@ class RDoc::Parser::C < RDoc::Parser @options.main_page = param '' when 'title' then - @options.title = param + @options.default_title = param if @options.respond_to? :default_title= '' end end comment end + ## # Removes lines that are commented out that might otherwise get picked up # when scanning for classes and methods def remove_commented_out_lines - @content.gsub!(%r{//.*rb_define_}, '//') + @content.gsub!(%r%//.*rb_define_%, '//') end + ## + # Removes private comments from +comment+ + def remove_private_comments(comment) comment.gsub!(/\/?\*--\n(.*?)\/?\*\+\+/m, '') comment.sub!(/\/?\*--\n.*/m, '') end ## - # Extract the classes/modules and methods from a C file and return the - # corresponding top-level object + # Extracts the classes, modules, methods, attributes, constants and aliases + # from a C file and returns an RDoc::TopLevel for this file def scan remove_commented_out_lines @@ -696,6 +883,7 @@ class RDoc::Parser::C < RDoc::Parser do_methods do_includes do_aliases + do_attrs @top_level end diff --git a/lib/rdoc/parser/perl.rb b/lib/rdoc/parser/perl.rb deleted file mode 100644 index 0023a013a6..0000000000 --- a/lib/rdoc/parser/perl.rb +++ /dev/null @@ -1,165 +0,0 @@ -require 'rdoc/parser' - -## -# -# This is an attamept to write a basic parser for Perl's -# POD (Plain old Documentation) format. Ruby code must -# co-exist with Perl, and some tasks are easier in Perl -# than Ruby because of existing libraries. -# -# One difficult is that Perl POD has no means of identifying -# the classes (packages) and methods (subs) with which it -# is associated, it is more like literate programming in so -# far as it just happens to be in the same place as the code, -# but need not be. -# -# We would like to support all the markup the POD provides -# so that it will convert happily to HTML. At the moment -# I don't think I can do that: time constraints. -# - -class RDoc::Parser::PerlPOD < RDoc::Parser - - parse_files_matching(/.p[lm]$/) - - ## - # Prepare to parse a perl file - - def initialize(top_level, file_name, content, options, stats) - super - - preprocess = RDoc::Markup::PreProcess.new @file_name, @options.rdoc_include - - preprocess.handle @content do |directive, param| - warn "Unrecognized directive '#{directive}' in #{@file_name}" - end - end - - ## - # Extract the Pod(-like) comments from the code. - # At its most basic there will ne no need to distinguish - # between the different types of header, etc. - # - # This uses a simple finite state machine, in a very - # procedural pattern. I could "replace case with polymorphism" - # but I think it would obscure the intent, scatter the - # code all over tha place. This machine is necessary - # because POD requires that directives be preceded by - # blank lines, so reading line by line is necessary, - # and preserving state about what is seen is necesary. - - def scan - - @top_level.comment ||= "" - state=:code_blank - line_number = 0 - line = nil - - # This started out as a really long nested case statement, - # which also led to repetitive code. I'd like to avoid that - # so I'm using a "table" instead. - - # Firstly we need some procs to do the transition and processing - # work. Because these are procs they are closures, and they can - # use variables in the local scope. - # - # First, the "nothing to see here" stuff. - code_noop = lambda do - if line =~ /^\s+$/ - state = :code_blank - end - end - - pod_noop = lambda do - if line =~ /^\s+$/ - state = :pod_blank - end - @top_level.comment += filter(line) - end - - begin_noop = lambda do - if line =~ /^\s+$/ - state = :begin_blank - end - @top_level.comment += filter(line) - end - - # Now for the blocks that process code and comments... - - transit_to_pod = lambda do - case line - when /^=(?:pod|head\d+)/ - state = :pod_no_blank - @top_level.comment += filter(line) - when /^=over/ - state = :over_no_blank - @top_level.comment += filter(line) - when /^=(?:begin|for)/ - state = :begin_no_blank - end - end - - process_pod = lambda do - case line - when /^\s*$/ - state = :pod_blank - @top_level.comment += filter(line) - when /^=cut/ - state = :code_no_blank - when /^=end/ - $stderr.puts "'=end' unexpected at #{line_number} in #{@file_name}" - else - @top_level.comment += filter(line) - end - end - - - process_begin = lambda do - case line - when /^\s*$/ - state = :begin_blank - @top_level.comment += filter(line) - when /^=end/ - state = :code_no_blank - when /^=cut/ - $stderr.puts "'=cut' unexpected at #{line_number} in #{@file_name}" - else - @top_level.comment += filter(line) - end - - end - - - transitions = { :code_no_blank => code_noop, - :code_blank => transit_to_pod, - :pod_no_blank => pod_noop, - :pod_blank => process_pod, - :begin_no_blank => begin_noop, - :begin_blank => process_begin} - @content.each_line do |l| - line = l - line_number += 1 - transitions[state].call - end # each line - - @top_level - end - - # Filter the perl markup that does the same as the rdoc - # filtering. Only basic for now. Will probably need a - # proper parser to cope with C<<...>> etc - def filter(comment) - return '' if comment =~ /^=pod\s*$/ - comment.gsub!(/^=pod/, '==') - comment.gsub!(/^=head(\d+)/) do - "=" * $1.to_i - end - comment.gsub!(/=item/, ''); - comment.gsub!(/C<(.*?)>/, '<tt>\1</tt>'); - comment.gsub!(/I<(.*?)>/, '<i>\1</i>'); - comment.gsub!(/B<(.*?)>/, '<b>\1</b>'); - comment - end - -end - diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb index 2874c47a20..e6f07d66da 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -11,8 +11,8 @@ require 'rdoc/ruby_token' require 'rdoc/ruby_lex' require 'rdoc/code_objects' -require 'rdoc/tokenstream' -require 'rdoc/markup/preprocess' +require 'rdoc/token_stream' +require 'rdoc/markup/pre_process' require 'rdoc/parser' require 'rdoc/parser/ruby_tools' @@ -162,6 +162,9 @@ class RDoc::Parser::Ruby < RDoc::Parser SINGLE = "<<" + ## + # Creates a new Ruby parser. + def initialize(top_level, file_name, content, options, stats) super @@ -209,10 +212,13 @@ class RDoc::Parser::Ruby < RDoc::Parser comment end + ## + # Aborts with +msg+ + def error(msg) msg = make_message msg - $stderr.puts msg - exit false + + abort msg end ## @@ -229,6 +235,10 @@ class RDoc::Parser::Ruby < RDoc::Parser meth end + ## + # Looks for a true or false token. Returns false if TkFALSE or TkNIL are + # found. + def get_bool skip_tkspace tk = get_tk @@ -245,20 +255,24 @@ class RDoc::Parser::Ruby < RDoc::Parser ## # Look for the name of a class of module (optionally with a leading :: or - # with :: separated named) and return the ultimate name and container + # with :: separated named) and return the ultimate name, the associated + # container, and the given name (with the ::). def get_class_or_module(container) skip_tkspace name_t = get_tk + given_name = '' # class ::A -> A is in the top level case name_t when TkCOLON2, TkCOLON3 then # bug name_t = get_tk container = @top_level + given_name << '::' end skip_tkspace false + given_name << name_t.name while TkCOLON2 === peek_tk do prev_container = container @@ -268,9 +282,10 @@ class RDoc::Parser::Ruby < RDoc::Parser end get_tk name_t = get_tk + given_name << '::' << name_t.name end skip_tkspace false - return [container, name_t] + return [container, name_t, given_name] end ## @@ -347,6 +362,9 @@ class RDoc::Parser::Ruby < RDoc::Parser name end + ## + # Extracts a name or symbol from the token stream. + def get_symbol_or_name tk = get_tk case tk @@ -361,7 +379,10 @@ class RDoc::Parser::Ruby < RDoc::Parser text when TkId, TkOp then tk.name - when TkSTRING, TkDSTRING then + when TkAMPER, + TkDSTRING, + TkSTAR, + TkSTRING then tk.text else raise RDoc::Error, "Name or symbol expected (got #{tk})" @@ -374,7 +395,7 @@ class RDoc::Parser::Ruby < RDoc::Parser # # :stopdoc: # # Don't display comment from this point forward # - # This routine modifies it's parameter + # This routine modifies its +comment+ parameter. def look_for_directives_in(context, comment) preprocess = RDoc::Markup::PreProcess.new @file_name, @options.rdoc_include @@ -382,9 +403,10 @@ class RDoc::Parser::Ruby < RDoc::Parser preprocess.handle comment, context do |directive, param| case directive when 'enddoc' then - throw :enddoc + context.done_documenting = true + '' when 'main' then - @options.main_page = param + @options.main_page = param if @options.respond_to? :main_page '' when 'method', 'singleton-method', 'attr', 'attr_accessor', 'attr_reader', 'attr_writer' then @@ -401,7 +423,7 @@ class RDoc::Parser::Ruby < RDoc::Parser context.stop_doc '' when 'title' then - @options.title = param + @options.default_title = param if @options.respond_to? :default_title= '' end end @@ -426,23 +448,28 @@ class RDoc::Parser::Ruby < RDoc::Parser def parse_attr(context, single, tk, comment) args = parse_symbol_arg 1 - if args.size > 0 + if args.size > 0 then name = args[0] rw = "R" skip_tkspace false tk = get_tk + if TkCOMMA === tk then rw = "RW" if get_bool else unget_tk tk end - att = RDoc::Attr.new get_tkread, name, rw, comment + + att = RDoc::Attr.new get_tkread, name, rw, comment, single == SINGLE + att.record_location @top_level + read_documentation_modifiers att, RDoc::ATTR_MODIFIERS - if att.document_self - context.add_attribute(att) - end + + context.add_attribute att if att.document_self + + @stats.add_attribute att else - warn("'attr' ignored - looks like a variable") + warn "'attr' ignored - looks like a variable" end end @@ -452,12 +479,8 @@ class RDoc::Parser::Ruby < RDoc::Parser def parse_attr_accessor(context, single, tk, comment) args = parse_symbol_arg - get_tkread - rw = "?" - # TODO If nodoc is given, don't document any of them - tmp = RDoc::CodeObject.new read_documentation_modifiers tmp, RDoc::ATTR_MODIFIERS return unless tmp.document_self @@ -471,17 +494,25 @@ class RDoc::Parser::Ruby < RDoc::Parser end for name in args - att = RDoc::Attr.new get_tkread, name, rw, comment + att = RDoc::Attr.new get_tkread, name, rw, comment, single == SINGLE + att.record_location @top_level + context.add_attribute att + @stats.add_attribute att end end + ## + # Parses an +alias+ in +context+ with +comment+ + def parse_alias(context, single, tk, comment) skip_tkspace + if TkLPAREN === peek_tk then get_tk skip_tkspace end + new_name = get_symbol_or_name @scanner.instance_eval { @lex_state = EXPR_FNAME } @@ -498,11 +529,20 @@ class RDoc::Parser::Ruby < RDoc::Parser return end - al = RDoc::Alias.new get_tkread, old_name, new_name, comment + al = RDoc::Alias.new(get_tkread, old_name, new_name, comment, + single == SINGLE) + al.record_location @top_level + read_documentation_modifiers al, RDoc::ATTR_MODIFIERS context.add_alias al if al.document_self + @stats.add_alias al + + al end + ## + # Extracts call parameters from the token stream. + def parse_call_parameters(tk) end_token = case tk when TkLPAREN, TkfLPAREN @@ -540,28 +580,33 @@ class RDoc::Parser::Ruby < RDoc::Parser res end + ## + # Parses a class in +context+ with +comment+ + def parse_class(container, single, tk, comment) - container, name_t = get_class_or_module container + declaration_context = container + container, name_t, given_name = get_class_or_module container case name_t when TkCONSTANT name = name_t.name - superclass = "Object" + superclass = '::Object' if TkLT === peek_tk then get_tk skip_tkspace superclass = get_class_specification - superclass = "<unknown>" if superclass.empty? + superclass = '(unknown)' if superclass.empty? end cls_type = single == SINGLE ? RDoc::SingleClass : RDoc::NormalClass - cls = container.add_class cls_type, name, superclass + cls = declaration_context.add_class cls_type, given_name, superclass read_documentation_modifiers cls, RDoc::CLASS_MODIFIERS cls.record_location @top_level - cls.comment = comment + cls.comment = comment if cls.document_self + @top_level.add_to_classes_or_modules cls @stats.add_class cls parse_statements cls @@ -569,7 +614,7 @@ class RDoc::Parser::Ruby < RDoc::Parser case name = get_class_specification when "self", container.name parse_statements container, SINGLE - when /\A[A-Z]/ + else other = RDoc::TopLevel.find_class_named name unless other then @@ -578,6 +623,15 @@ class RDoc::Parser::Ruby < RDoc::Parser other.comment = comment end + # notify :nodoc: all if not a constant-named class/module + # (and remove any comment) + unless name =~ /\A(::)?[A-Z]/ + other.document_self = nil + other.document_children = false + other.clear_comment + end + + @top_level.add_to_classes_or_modules other @stats.add_class other read_documentation_modifiers other, RDoc::CLASS_MODIFIERS @@ -589,9 +643,15 @@ class RDoc::Parser::Ruby < RDoc::Parser end end + ## + # Parses a constant in +context+ with +comment+ + def parse_constant(container, tk, comment) name = tk.name skip_tkspace false + + return unless name =~ /^\w+$/ + eq_tk = get_tk unless TkASSIGN === eq_tk then @@ -615,9 +675,9 @@ class RDoc::Parser::Ruby < RDoc::Parser loop do case tk when TkSEMICOLON then - break - when TkLPAREN, TkfLPAREN, TkLBRACE, TkLBRACK, TkDO, TkIF, TkUNLESS, - TkCASE then + break if nest <= 0 + when TkLPAREN, TkfLPAREN, TkLBRACE, TkfLBRACE, TkLBRACK, TkfLBRACK, + TkDO, TkIF, TkUNLESS, TkCASE, TkDEF, TkBEGIN then nest += 1 when TkRPAREN, TkRBRACE, TkRBRACK, TkEND then nest -= 1 @@ -654,10 +714,11 @@ class RDoc::Parser::Ruby < RDoc::Parser tk = get_tk end - res = get_tkread.tr("\n", " ").strip + res = get_tkread.gsub(/^[ \t]+/, '').strip res = "" if res == ";" con = RDoc::Constant.new name, res, comment + con.record_location @top_level read_documentation_modifiers con, RDoc::CONSTANT_MODIFIERS @stats.add_constant con @@ -679,6 +740,7 @@ class RDoc::Parser::Ruby < RDoc::Parser name = $1 unless $1.empty? meth = RDoc::GhostMethod.new get_tkread, name + meth.record_location @top_level meth.singleton = singleton meth.start_collecting_tokens @@ -709,13 +771,19 @@ class RDoc::Parser::Ruby < RDoc::Parser name = $3 unless $3.empty? + # TODO authorize 'singleton-attr...'? att = RDoc::Attr.new get_tkread, name, rw, comment + att.record_location @top_level + container.add_attribute att - @stats.add_method att + @stats.add_attribute att end end + ## + # Parses an +include+ in +context+ with +comment+ + def parse_include(context, comment) loop do skip_tkspace_comment @@ -759,8 +827,6 @@ class RDoc::Parser::Ruby < RDoc::Parser def parse_meta_attr(context, single, tk, comment) args = parse_symbol_arg - get_tkread - rw = "?" # If nodoc is given, don't document any of them @@ -779,12 +845,19 @@ class RDoc::Parser::Ruby < RDoc::Parser end if name then - att = RDoc::Attr.new get_tkread, name, rw, comment + att = RDoc::Attr.new get_tkread, name, rw, comment, single == SINGLE + att.record_location @top_level + context.add_attribute att + @stats.add_attribute att else args.each do |attr_name| - att = RDoc::Attr.new get_tkread, attr_name, rw, comment + att = RDoc::Attr.new(get_tkread, attr_name, rw, comment, + single == SINGLE) + att.record_location @top_level + context.add_attribute att + @stats.add_attribute att end end end @@ -825,6 +898,7 @@ class RDoc::Parser::Ruby < RDoc::Parser end meth = RDoc::MetaMethod.new get_tkread, name + meth.record_location @top_level meth.singleton = singleton remove_token_listener self @@ -882,7 +956,7 @@ class RDoc::Parser::Ruby < RDoc::Parser token_listener self do @scanner.instance_eval do @lex_state = EXPR_FNAME end - skip_tkspace false + skip_tkspace name_t = get_tk back_tk = skip_tkspace meth = nil @@ -922,11 +996,17 @@ class RDoc::Parser::Ruby < RDoc::Parser container.record_location @top_level end - when TkIDENTIFIER, TkIVAR then + when TkIDENTIFIER, TkIVAR, TkGVAR then dummy = RDoc::Context.new dummy.parent = container skip_method dummy return + when TkTRUE, TkFALSE, TkNIL then + klass_name = "#{name_t.name.capitalize}Class" + container = RDoc::TopLevel.find_class_named klass_name + container ||= @top_level.add_class RDoc::NormalClass, klass_name + + name = name_t2.name else warn "unexpected method name token #{name_t.inspect}" # break @@ -959,6 +1039,8 @@ class RDoc::Parser::Ruby < RDoc::Parser end end + meth.record_location @top_level + meth.start_collecting_tokens indent = TkSPACE.new nil, 1, 1 indent.set_text " " * column @@ -1001,6 +1083,9 @@ class RDoc::Parser::Ruby < RDoc::Parser @stats.add_method meth end + ## + # Extracts +yield+ parameters from +method+ + def parse_method_or_yield_parameters(method = nil, modifiers = RDoc::METHOD_MODIFIERS) skip_tkspace false @@ -1024,14 +1109,16 @@ class RDoc::Parser::Ruby < RDoc::Parser loop do case tk when TkSEMICOLON then - break - when TkLBRACE then + break if nest == 0 + when TkLBRACE, TkfLBRACE then nest += 1 when TkRBRACE then - # we might have a.each {|i| yield i } - unget_tk(tk) if nest.zero? nest -= 1 - break if nest <= 0 + if nest <= 0 + # we might have a.each { |i| yield i } + unget_tk(tk) if nest < 0 + break + end when TkLPAREN, TkfLPAREN then nest += 1 when end_token then @@ -1041,6 +1128,8 @@ class RDoc::Parser::Ruby < RDoc::Parser else break unless @scanner.continue end + when TkRPAREN then + nest -= 1 when method && method.block_params.nil? && TkCOMMENT then unget_tk tk read_documentation_modifiers method, modifiers @@ -1078,8 +1167,11 @@ class RDoc::Parser::Ruby < RDoc::Parser end end + ## + # Parses an RDoc::NormalModule in +container+ with +comment+ + def parse_module(container, single, tk, comment) - container, name_t = get_class_or_module container + container, name_t, = get_class_or_module container name = name_t.name @@ -1087,12 +1179,16 @@ class RDoc::Parser::Ruby < RDoc::Parser mod.record_location @top_level read_documentation_modifiers mod, RDoc::CLASS_MODIFIERS + mod.comment = comment if mod.document_self parse_statements(mod) - mod.comment = comment + @top_level.add_to_classes_or_modules mod @stats.add_module mod end + ## + # Parses an RDoc::Require in +context+ containing +comment+ + def parse_require(context, comment) skip_tkspace_comment tk = get_tk @@ -1105,7 +1201,7 @@ class RDoc::Parser::Ruby < RDoc::Parser name = tk.text if TkSTRING === tk if name then - context.add_require RDoc::Require.new(name, comment) + @top_level.add_require RDoc::Require.new(name, comment) else unget_tk tk end @@ -1206,7 +1302,7 @@ class RDoc::Parser::Ruby < RDoc::Parser # We can't solve the general case, but we can handle most occurrences by # ignoring a do at the end of a line. - when TkUNTIL, TkWHILE then + when TkUNTIL, TkWHILE then nest += 1 skip_optional_do_after_expression @@ -1275,9 +1371,14 @@ class RDoc::Parser::Ruby < RDoc::Parser end end + ## + # Parse up to +no+ symbol arguments + def parse_symbol_arg(no = nil) args = [] + skip_tkspace_comment + case tk = get_tk when TkLPAREN loop do @@ -1320,28 +1421,40 @@ class RDoc::Parser::Ruby < RDoc::Parser end end end + args end + ## + # Returns symbol text from the next token + def parse_symbol_in_arg case tk = get_tk when TkSYMBOL tk.text.sub(/^:/, '') when TkSTRING eval @read[-1] + when TkDSTRING, TkIDENTIFIER then + nil # ignore else warn("Expected symbol or string, got #{tk.inspect}") if $DEBUG_RDOC nil end end + ## + # Parses statements at the toplevel in +container+ + def parse_top_level_statements(container) comment = collect_first_comment look_for_directives_in(container, comment) - container.comment = comment unless comment.empty? + container.comment = comment if container.document_self unless comment.empty? parse_statements container, NORMAL, nil, comment end + ## + # Determines the visibility in +container+ from +tk+ + def parse_visibility(container, single, tk) singleton = (single == SINGLE) @@ -1383,7 +1496,8 @@ class RDoc::Parser::Ruby < RDoc::Parser container.methods_matching args do |m| s_m = m.dup - s_m.singleton = true if RDoc::AnyMethod === s_m + s_m.record_location @top_level + s_m.singleton = true s_m.visibility = :public module_functions << s_m end @@ -1403,6 +1517,9 @@ class RDoc::Parser::Ruby < RDoc::Parser end end + ## + # Determines the block parameter for +context+ + def parse_yield(context, single, tk, method) return if method.block_params @@ -1423,93 +1540,81 @@ class RDoc::Parser::Ruby < RDoc::Parser # # We return the directive name and any parameters as a two element array - def read_directive(allowed) + def read_directive allowed tk = get_tk - result = nil if TkCOMMENT === tk then - if tk.text =~ /\s*:?(\w+):\s*(.*)/ then - directive = $1.downcase - if allowed.include? directive then - result = [directive, $2] - end - end + return unless tk.text =~ /\s*:?(\w+):\s*(.*)/ + + directive = $1.downcase + + return [directive, $2] if allowed.include? directive else unget_tk tk end - - result end - def read_documentation_modifiers(context, allow) - dir = read_directive(allow) - - case dir[0] - when "notnew", "not_new", "not-new" then - context.dont_rename_initialize = true - - when "nodoc" then - context.document_self = false - if dir[1].downcase == "all" - context.document_children = false - end - - when "doc" then - context.document_self = true - context.force_documentation = true + ## + # Handles the directive for +context+ if the directive is listed in +allow+. + # This method is called for directives following a definition. - when "yield", "yields" then - unless context.params.nil? - context.params.sub!(/(,|)\s*&\w+/,'') # remove parameter &proc - end + def read_documentation_modifiers(context, allow) + directive, value = read_directive allow - context.block_params = dir[1] + return unless directive - when "arg", "args" then - context.params = dir[1] - end if dir + case directive + when 'notnew', 'not_new', 'not-new' then + context.dont_rename_initialize = true + else + RDoc::Parser.process_directive context, directive, value + end end + ## + # Removes private comments from +comment+ + def remove_private_comments(comment) - comment.gsub!(/^#--\n.*?^#\+\+/m, '') - comment.sub!(/^#--\n.*/m, '') + comment.gsub!(/^#--\n.*?^#\+\+\n?/m, '') + comment.sub!(/^#--\n.*\n?/m, '') end + ## + # Scans this ruby file for ruby constructs + def scan reset catch :eof do - catch :enddoc do - begin - parse_top_level_statements @top_level - rescue StandardError => e - bytes = '' - - 20.times do @scanner.ungetc end - count = 0 - 60.times do |i| - count = i - byte = @scanner.getc - break unless byte - bytes << byte - end - count -= 20 - count.times do @scanner.ungetc end + begin + parse_top_level_statements @top_level + rescue StandardError => e + bytes = '' + + 20.times do @scanner.ungetc end + count = 0 + 60.times do |i| + count = i + byte = @scanner.getc + break unless byte + bytes << byte + end + count -= 20 + count.times do @scanner.ungetc end - $stderr.puts <<-EOF + $stderr.puts <<-EOF #{self.class} failure around line #{@scanner.line_no} of #{@file_name} - EOF + EOF - unless bytes.empty? then - $stderr.puts - $stderr.puts bytes.inspect - end - - raise e + unless bytes.empty? then + $stderr.puts + $stderr.puts bytes.inspect end + + raise e end end @@ -1574,6 +1679,9 @@ class RDoc::Parser::Ruby < RDoc::Parser unget_tk(tk) unless TkIN === tk end + ## + # Skips the next method in +container+ + def skip_method container meth = RDoc::AnyMethod.new "", "anon" parse_method_parameters meth @@ -1591,6 +1699,9 @@ class RDoc::Parser::Ruby < RDoc::Parser end end + ## + # Prints +msg+ to +$stderr+ unless we're being quiet + def warn(msg) return if @options.quiet msg = make_message msg diff --git a/lib/rdoc/parser/ruby_tools.rb b/lib/rdoc/parser/ruby_tools.rb index 90c03307b4..3f6190884e 100644 --- a/lib/rdoc/parser/ruby_tools.rb +++ b/lib/rdoc/parser/ruby_tools.rb @@ -49,7 +49,6 @@ module RDoc::Parser::RubyTools obj.pop_token end if @token_listeners else - warn("':' not followed by identifier or operator") tk = tk1 end end @@ -62,6 +61,10 @@ module RDoc::Parser::RubyTools tk end + ## + # Reads and returns all tokens up to one of +tokens+. Leaves the matched + # token in the token list. + def get_tk_until(*tokens) read = [] diff --git a/lib/rdoc/parser/simple.rb b/lib/rdoc/parser/simple.rb index e99d2d4319..1e82eb5097 100644 --- a/lib/rdoc/parser/simple.rb +++ b/lib/rdoc/parser/simple.rb @@ -1,7 +1,6 @@ ## # Parse a non-source file. We basically take the whole thing as one big -# comment. If the first character in the file is '#', we strip leading pound -# signs. +# comment. class RDoc::Parser::Simple < RDoc::Parser @@ -32,10 +31,16 @@ class RDoc::Parser::Simple < RDoc::Parser @top_level end - def remove_private_comments(comment) - comment.gsub(/^--\n.*?^\+\+/m, '').sub(/^--\n.*/m, '') + ## + # Removes comments wrapped in <tt>--/++</tt> + + def remove_private_comments text + text.gsub(/^--\n.*?^\+\+/m, '').sub(/^--\n.*/m, '') end + ## + # Removes the encoding magic comment from +text+ + def remove_coding_comment text text.sub(/\A# .*coding[=:].*$/, '') end diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb index 8771b408d9..a885d8dded 100644 --- a/lib/rdoc/rdoc.rb +++ b/lib/rdoc/rdoc.rb @@ -1,5 +1,6 @@ require 'rdoc' +require 'rdoc/encoding' require 'rdoc/parser' # Simple must come first @@ -23,7 +24,28 @@ require 'time' # rdoc.document(args) # # Where +args+ is an array of strings, each corresponding to an argument you'd -# give rdoc on the command line. See rdoc/rdoc.rb for details. +# give rdoc on the command line. See <tt>rdoc --help<tt> for details. +# +# = Plugins +# +# When you <tt>require 'rdoc/rdoc'</tt> RDoc looks for 'rdoc/discover' files +# in your installed gems. This can be used to load alternate generators or +# add additional preprocessor directives. +# +# You will want to wrap your plugin loading in an RDoc version check. +# Something like: +# +# begin +# gem 'rdoc', '~> 3' +# require 'path/to/my/awesome/rdoc/plugin' +# rescue Gem::LoadError +# end +# +# The most obvious plugin type is a new output generator. See RDoc::Generator +# for details. +# +# You can also hook into RDoc::Markup to add new directives (:nodoc: is a +# directive). See RDoc::Markup::PreProcess::register for details. class RDoc::RDoc @@ -79,6 +101,10 @@ class RDoc::RDoc @current = rdoc end + ## + # Creates a new RDoc::RDoc instance. Call #document to parse files and + # generate documentation. + def initialize @current = nil @exclude = nil @@ -142,7 +168,9 @@ class RDoc::RDoc last = {} - if File.exist? dir then + if @options.dry_run then + # do nothing + elsif File.exist? dir then error "#{dir} exists and is not a directory" unless File.directory? dir begin @@ -167,7 +195,7 @@ you'll need to specify a different output directory name (using the --op <dir> option) ERROR - end + end unless @options.force_output else FileUtils.mkdir_p dir end @@ -179,6 +207,8 @@ option) # Update the flag file in an output directory. def update_output_dir(op_dir, time, last = {}) + return if @options.dry_run + open output_flag_file(op_dir), "w" do |f| f.puts time.rfc2822 last.each do |n, t| @@ -277,7 +307,9 @@ option) def parse_file filename @stats.add_file filename - content = read_file_contents filename + encoding = @options.encoding if defined?(Encoding) + + content = RDoc::Encoding.read_file filename, encoding return unless content @@ -288,11 +320,22 @@ option) return unless parser parser.scan + + # restart documentation for the classes & modules found + top_level.classes_or_modules.each do |cm| + cm.done_documenting = false + end + + top_level + rescue => e $stderr.puts <<-EOF Before reporting this, could you check that the file you're documenting -compiles cleanly--RDoc is not a full Ruby parser, and gets confused easily if -fed invalid programs. +has proper syntax: + + #{Gem.ruby} -c #{filename} + +RDoc is not a full Ruby parser and will fail when fed invalid ruby programs. The internal error was: @@ -300,7 +343,7 @@ The internal error was: EOF - $stderr.puts e.backtrace.join("\n\t") if $RDOC_DEBUG + $stderr.puts e.backtrace.join("\n\t") if $DEBUG_RDOC raise e nil @@ -344,11 +387,9 @@ The internal error was: # For simplicity, +argv+ is an array of strings, equivalent to the strings # that would be passed on the command line. (This isn't a coincidence, as # we _do_ pass in ARGV when running interactively). For a list of options, - # see rdoc/rdoc.rb. By default, output will be stored in a directory + # see <tt>rdoc --help</tt>. By default, output will be stored in a directory # called +doc+ below the current directory, so make sure you're somewhere # writable before invoking. - # - # Throws: RDoc::Error on error def document(argv) RDoc::TopLevel.reset @@ -364,29 +405,36 @@ The internal error was: @exclude = @options.exclude - @last_modified = setup_output_dir @options.op_dir, @options.force_update + unless @options.coverage_report then + @last_modified = setup_output_dir @options.op_dir, @options.force_update + end start_time = Time.now file_info = parse_files @options.files - @options.title = "RDoc Documentation" + @options.default_title = "RDoc Documentation" + + RDoc::TopLevel.complete @options.visibility - if file_info.empty? + if @options.coverage_report then + puts + puts @stats.report + elsif file_info.empty? $stderr.puts "\nNo newer files." unless @options.quiet else gen_klass = @options.generator - unless @options.quiet then - $stderr.puts "\nGenerating #{gen_klass.name.sub(/^.*::/, '')}..." - end - - @generator = gen_klass.for @options + @generator = gen_klass.new @options Dir.chdir @options.op_dir do begin self.class.current = self + unless @options.quiet then + $stderr.puts "\nGenerating #{gen_klass.name.sub(/^.*::/, '')} format into #{Dir.pwd}..." + end + @generator.generate file_info update_output_dir ".", start_time, @last_modified ensure @@ -397,26 +445,10 @@ The internal error was: unless @options.quiet or not @stats then puts - @stats.print - end - end - - def read_file_contents(filename) - content = open filename, "rb" do |f| f.read end - - utf8 = content.sub!(/\A\xef\xbb\xbf/, '') - if defined? Encoding then - if /coding[=:]\s*([^\s;]+)/i =~ content[%r"\A(?:#!.*\n)?.*\n"] - enc = ::Encoding.find($1) - end - if enc ||= (Encoding::UTF_8 if utf8) - content.force_encoding(enc) - end + puts @stats.summary end - content - rescue Errno::EISDIR, Errno::ENOENT - nil + exit @stats.fully_documented? if @options.coverage_report end ## diff --git a/lib/rdoc/require.rb b/lib/rdoc/require.rb index 407b55af35..65d3d464da 100644 --- a/lib/rdoc/require.rb +++ b/lib/rdoc/require.rb @@ -16,6 +16,7 @@ class RDoc::Require < RDoc::CodeObject def initialize(name, comment) super() @name = name.gsub(/'|"/, "") #' + @top_level = nil self.comment = comment end @@ -28,5 +29,25 @@ class RDoc::Require < RDoc::CodeObject ] end + def to_s # :nodoc: + "require #{name} in: #{parent}" + end + + ## + # The RDoc::TopLevel corresponding to this require, or +nil+ if not found. + + def top_level + @top_level ||= begin + tl = RDoc::TopLevel.all_files_hash[name + '.rb'] + + if tl.nil? and RDoc::TopLevel.all_files.first.full_name =~ %r(^lib/) then + # second chance + tl = RDoc::TopLevel.all_files_hash['lib/' + name + '.rb'] + end + + tl + end + end + end diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb index f3fd9539e3..9d61b1f243 100644 --- a/lib/rdoc/ri/driver.rb +++ b/lib/rdoc/ri/driver.rb @@ -6,6 +6,11 @@ begin rescue LoadError end +begin + require 'win32console' +rescue LoadError +end + require 'rdoc/ri' require 'rdoc/ri/paths' require 'rdoc/markup' @@ -55,6 +60,9 @@ class RDoc::RI::Driver end end + ## + # An RDoc::RI::Store for each entry in the RI path + attr_accessor :stores ## @@ -97,23 +105,10 @@ class RDoc::RI::Driver ## # Parses +argv+ and returns a Hash of options - def self.process_args argv = [] + def self.process_args argv options = default_options - opts = OptionParser.new - setup_options(opts, options) - argv = ENV['RI'].to_s.split.concat argv - opts.parse!(argv) - - fixup_options(options, argv) - - rescue OptionParser::ParseError => e - puts opts, nil, e - abort - end - - def self.setup_options(opt, options) - begin + opts = OptionParser.new do |opt| opt.accept File do |file,| File.readable?(file) and not File.directory?(file) and file end @@ -133,7 +128,7 @@ Where name can be: All class names may be abbreviated to their minimum unambiguous form. If a name is ambiguous, all valid options will be listed. -The form '.' method matches either class or instance methods, while #method +A '.' matches either class or instance methods, while #method matches only instance and ::method matches only class methods. For example: @@ -143,7 +138,7 @@ For example: #{opt.program_name} File.new #{opt.program_name} zip -Note that shell quoting may be required for method names containing +Note that shell quoting or escaping may be required for method names containing punctuation: #{opt.program_name} 'Array.[]' @@ -287,9 +282,11 @@ Options may also be set in the 'RI' environment variable. options[:dump_path] = value end end - end - def self.fixup_options(options, argv) + argv = ENV['RI'].to_s.split.concat argv + + opts.parse! argv + options[:names] = argv options[:use_stdout] ||= !$stdout.tty? @@ -297,6 +294,12 @@ Options may also be set in the 'RI' environment variable. options[:width] ||= 72 options + + rescue OptionParser::InvalidArgument, OptionParser::InvalidOption => e + puts opts + puts + puts e + exit 1 end ## @@ -359,7 +362,7 @@ Options may also be set in the 'RI' environment variable. paths = RDoc::Markup::Verbatim.new also_in.each do |store| - paths.parts.push ' ', store.friendly_path, "\n" + paths.parts.push store.friendly_path, "\n" end out << paths end @@ -427,7 +430,7 @@ Options may also be set in the 'RI' environment variable. verb = RDoc::Markup::Verbatim.new wout.each do |incl| - verb.push ' ', incl.name, "\n" + verb.push incl.name, "\n" end out << verb @@ -446,7 +449,7 @@ Options may also be set in the 'RI' environment variable. out << RDoc::Markup::BlankLine.new out.push(*methods.map do |method| - RDoc::Markup::Verbatim.new ' ', method + RDoc::Markup::Verbatim.new method end) out << RDoc::Markup::BlankLine.new @@ -664,8 +667,8 @@ Options may also be set in the 'RI' environment variable. if method.arglists then arglists = method.arglists.chomp.split "\n" - arglists = arglists.map { |line| [' ', line, "\n"] } - out << RDoc::Markup::Verbatim.new(*arglists.flatten) + arglists = arglists.map { |line| line + "\n" } + out << RDoc::Markup::Verbatim.new(*arglists) out << RDoc::Markup::Rule.new(1) end @@ -847,6 +850,17 @@ Options may also be set in the 'RI' environment variable. end ## + # Is +file+ in ENV['PATH']? + + def in_path? file + return true if file =~ %r%\A/% and File.exist? file + + ENV['PATH'].split(File::PATH_SEPARATOR).any? do |path| + File.exist? File.join(path, file) + end + end + + ## # Lists classes known to ri def list_known_classes @@ -1041,7 +1055,11 @@ Options may also be set in the 'RI' environment variable. pagers.compact.uniq.each do |pager| next unless pager - io = IO.popen pager, "w" rescue next + pager_cmd = pager.split.first + + next unless in_path? pager_cmd + + io = IO.popen(pager, 'w') rescue next next if $? and $?.exited? # pager didn't work diff --git a/lib/rdoc/ri/paths.rb b/lib/rdoc/ri/paths.rb index 9b338d7ad8..ec4d16b857 100644 --- a/lib/rdoc/ri/paths.rb +++ b/lib/rdoc/ri/paths.rb @@ -10,10 +10,21 @@ module RDoc::RI::Paths version = RbConfig::CONFIG['ruby_version'] - base = File.join RbConfig::CONFIG['ridir'], version + base = if RbConfig::CONFIG.key? 'ridir' then + File.join RbConfig::CONFIG['ridir'], version + else + File.join RbConfig::CONFIG['datadir'], 'ri', version + end + SYSDIR = File.join base, "system" SITEDIR = File.join base, "site" - HOMEDIR = (File.expand_path('~/.rdoc') rescue nil) + + homedir = File.expand_path('~') || + ENV['HOME'] || ENV['USERPROFILE'] || ENV['HOMEPATH'] + + HOMEDIR = if homedir then + File.join homedir, ".rdoc" + end #:startdoc: @gemdirs = nil diff --git a/lib/rdoc/ri/store.rb b/lib/rdoc/ri/store.rb index db02202f81..1fcd313d0f 100644 --- a/lib/rdoc/ri/store.rb +++ b/lib/rdoc/ri/store.rb @@ -11,6 +11,11 @@ require 'fileutils' class RDoc::RI::Store ## + # If true this Store will not write any files + + attr_accessor :dry_run + + ## # Path this store reads or writes attr_accessor :path @@ -21,14 +26,18 @@ class RDoc::RI::Store attr_accessor :type + ## + # The contents of the Store + attr_reader :cache ## # Creates a new Store of +type+ that will load or save to +path+ def initialize path, type = nil - @type = type - @path = path + @dry_run = false + @type = type + @path = path @cache = { :class_methods => {}, @@ -178,6 +187,8 @@ class RDoc::RI::Store @cache[:instance_methods].each do |_, m| m.uniq!; m.sort! end @cache[:modules].uniq!; @cache[:modules].sort! + return if @dry_run + open cache_path, 'wb' do |io| Marshal.dump @cache, io end @@ -187,7 +198,7 @@ class RDoc::RI::Store # Writes the ri data for +klass+ def save_class klass - FileUtils.mkdir_p class_path(klass.full_name) + FileUtils.mkdir_p class_path(klass.full_name) unless @dry_run @cache[:modules] << klass.full_name @@ -214,7 +225,7 @@ class RDoc::RI::Store @cache[:ancestors][klass.full_name].push(*ancestors) attributes = klass.attributes.map do |attribute| - "#{attribute.type} #{attribute.name}" + "#{attribute.definition} #{attribute.name}" end unless attributes.empty? then @@ -222,6 +233,8 @@ class RDoc::RI::Store @cache[:attributes][klass.full_name].push(*attributes) end + return if @dry_run + open path, 'wb' do |io| Marshal.dump klass, io end @@ -231,7 +244,7 @@ class RDoc::RI::Store # Writes the ri data for +method+ on +klass+ def save_method klass, method - FileUtils.mkdir_p class_path(klass.full_name) + FileUtils.mkdir_p class_path(klass.full_name) unless @dry_run cache = if method.singleton then @cache[:class_methods] @@ -241,6 +254,8 @@ class RDoc::RI::Store cache[klass.full_name] ||= [] cache[klass.full_name] << method.name + return if @dry_run + open method_file(klass.full_name, method.full_name), 'wb' do |io| Marshal.dump method, io end diff --git a/lib/rdoc/ruby_lex.rb b/lib/rdoc/ruby_lex.rb index fe289fb1a2..cbe3ec9061 100644 --- a/lib/rdoc/ruby_lex.rb +++ b/lib/rdoc/ruby_lex.rb @@ -92,9 +92,9 @@ class RDoc::RubyLex end def inspect # :nodoc: - "#<%s:0x%x lex_state %p space_seen %p>" % [ + "#<%s:0x%x pos %d lex_state %p space_seen %p>" % [ self.class, object_id, - @lex_state, @space_seen, + @io.pos, @lex_state, @space_seen, ] end @@ -149,6 +149,7 @@ class RDoc::RubyLex else @char_no += 1 end + c end @@ -674,7 +675,7 @@ class RDoc::RubyLex tk_c = TkLPAREN end @indent_stack.push tk_c - Token(tk_c) + Token tk_c end @OP.def_rule("[]", proc{|op, io| @lex_state == EXPR_FNAME}) do @@ -822,6 +823,12 @@ class RDoc::RubyLex end end + IDENT_RE = if defined? Encoding then + /[\w\u0080-\uFFFF]/u + else + /[\w\x80-\xFF]/ + end + def identify_identifier token = "" if peek(0) =~ /[$@]/ @@ -831,15 +838,7 @@ class RDoc::RubyLex end end - # HACK to avoid a warning the regexp is hidden behind an eval - # HACK need a better way to detect oniguruma - @identifier_re ||= if defined? Encoding then - eval '/[\p{Alnum}_]/u' - else - eval '/[\w\x80-\xff]/' - end - - while (ch = getc) =~ @identifier_re + while (ch = getc) =~ IDENT_RE do print " :#{ch}: " if RDoc::RubyLex.debug? token.concat ch end diff --git a/lib/rdoc/ruby_token.rb b/lib/rdoc/ruby_token.rb index 0c4f837193..93b7a5cbc8 100644 --- a/lib/rdoc/ruby_token.rb +++ b/lib/rdoc/ruby_token.rb @@ -178,7 +178,7 @@ module RDoc::RubyToken end class TkUnknownChar < Token - def initialize(seek, line_no, char_no, id) + def initialize(seek, line_no, char_no, name) super(seek, line_no, char_no) @name = name end @@ -253,7 +253,7 @@ module RDoc::RubyToken [:TkWHILE, TkKW, "while", EXPR_BEG, :TkWHILE_MOD], [:TkUNTIL, TkKW, "until", EXPR_BEG, :TkUNTIL_MOD], [:TkFOR, TkKW, "for", EXPR_BEG], - [:TkBREAK, TkKW, "break", EXPR_END], + [:TkBREAK, TkKW, "break", EXPR_MID], [:TkNEXT, TkKW, "next", EXPR_END], [:TkREDO, TkKW, "redo", EXPR_END], [:TkRETRY, TkKW, "retry", EXPR_END], diff --git a/lib/rdoc/single_class.rb b/lib/rdoc/single_class.rb index 1226d56f84..60336a759b 100644 --- a/lib/rdoc/single_class.rb +++ b/lib/rdoc/single_class.rb @@ -5,8 +5,9 @@ require 'rdoc/class_module' class RDoc::SingleClass < RDoc::ClassModule + # Adds the superclass to the included modules. def ancestors - includes + [superclass] + superclass ? super + [superclass] : super end end diff --git a/lib/rdoc/stats.rb b/lib/rdoc/stats.rb index 6a5d96faa8..e0af445539 100644 --- a/lib/rdoc/stats.rb +++ b/lib/rdoc/stats.rb @@ -1,247 +1,287 @@ require 'rdoc' ## -# RDoc stats collector +# RDoc statistics collector which prints a summary and report of a project's +# documentation totals. class RDoc::Stats - attr_reader :nodoc_constants - attr_reader :nodoc_methods + ## + # Count of files parsed during parsing - attr_reader :num_constants - attr_reader :num_files - attr_reader :num_methods + attr_reader :files_so_far - attr_reader :total_files + ## + # Total number of files found - def initialize(total_files, verbosity = 1) - @nodoc_constants = 0 - @nodoc_methods = 0 + attr_reader :num_files - @num_constants = 0 - @num_files = 0 - @num_methods = 0 + ## + # Creates a new Stats that will have +num_files+. +verbosity+ defaults to 1 + # which will create an RDoc::Stats::Normal outputter. - @total_files = total_files + def initialize num_files, verbosity = 1 + @files_so_far = 0 + @num_files = num_files + @fully_documented = nil @start = Time.now @display = case verbosity - when 0 then Quiet.new total_files - when 1 then Normal.new total_files - else Verbose.new total_files + when 0 then Quiet.new num_files + when 1 then Normal.new num_files + else Verbose.new num_files end end - def begin_adding - @display.begin_adding - end + ## + # Records the parsing of an alias +as+. - def add_alias(as) + def add_alias as @display.print_alias as - @num_methods += 1 - @nodoc_methods += 1 if as.document_self and as.comment.empty? end - def add_class(klass) + ## + # Records the parsing of an attribute +attribute+ + + def add_attribute attribute + @display.print_attribute attribute + end + + ## + # Records the parsing of a class +klass+ + + def add_class klass @display.print_class klass end - def add_constant(constant) + ## + # Records the parsing of +constant+ + + def add_constant constant @display.print_constant constant - @num_constants += 1 - @nodoc_constants += 1 if constant.document_self and constant.comment.empty? end + ## + # Records the parsing of +file+ + def add_file(file) - @display.print_file @num_files, file - @num_files += 1 + @files_so_far += 1 + @display.print_file @files_so_far, file end + ## + # Records the parsing of +method+ + def add_method(method) @display.print_method method - @num_methods += 1 - @nodoc_methods += 1 if method.document_self and method.comment.empty? end + ## + # Records the parsing of a module +mod+ + def add_module(mod) @display.print_module mod end - def done_adding - @display.done_adding - end + ## + # Call this to mark the beginning of parsing for display purposes - def print - classes = RDoc::TopLevel.classes - num_classes = classes.length - nodoc_classes = classes.select do |klass| - klass.document_self and klass.comment.empty? - end.length - - modules = RDoc::TopLevel.modules - num_modules = modules.length - nodoc_modules = modules.select do |mod| - mod.document_self and mod.comment.empty? - end.length - - items = num_classes + @num_constants + num_modules + @num_methods - doc_items = items - - nodoc_classes - @nodoc_constants - nodoc_modules - @nodoc_methods - - percent_doc = doc_items.to_f / items * 100 if items.nonzero? - - puts "Files: %5d" % @num_files - puts "Classes: %5d (%5d undocumented)" % [num_classes, nodoc_classes] - puts "Constants: %5d (%5d undocumented)" % - [@num_constants, @nodoc_constants] - puts "Modules: %5d (%5d undocumented)" % [num_modules, nodoc_modules] - puts "Methods: %5d (%5d undocumented)" % [@num_methods, @nodoc_methods] - puts "%6.2f%% documented" % percent_doc if percent_doc - puts - puts "Elapsed: %0.1fs" % (Time.now - @start) + def begin_adding + @display.begin_adding end ## - # Stats printer that prints nothing - - class Quiet + # Calculates documentation totals and percentages - def initialize total_files - @total_files = total_files - end - - ## - # Prints a message at the beginning of parsing + def calculate + return if @percent_doc - def begin_adding(*) end + ucm = RDoc::TopLevel.unique_classes_and_modules + constants = [] + ucm.each { |cm| constants.concat cm.constants } - ## - # Prints when an alias is added + methods = [] + ucm.each { |cm| methods.concat cm.method_list } - def print_alias(*) end + attributes = [] + ucm.each { |cm| attributes.concat cm.attributes } - ## - # Prints when a class is added + @num_attributes, @undoc_attributes = doc_stats attributes + @num_classes, @undoc_classes = doc_stats RDoc::TopLevel.unique_classes + @num_constants, @undoc_constants = doc_stats constants + @num_methods, @undoc_methods = doc_stats methods + @num_modules, @undoc_modules = doc_stats RDoc::TopLevel.unique_modules - def print_class(*) end + @num_items = + @num_attributes + + @num_classes + + @num_constants + + @num_methods + + @num_modules - ## - # Prints when a constant is added + @undoc_items = + @undoc_attributes + + @undoc_classes + + @undoc_constants + + @undoc_methods + + @undoc_modules - def print_constant(*) end + @doc_items = @num_items - @undoc_items - ## - # Prints when a file is added + @fully_documented = (@num_items - @doc_items) == 0 - def print_file(*) end - - ## - # Prints when a method is added + @percent_doc = @doc_items.to_f / @num_items * 100 if @num_items.nonzero? + end - def print_method(*) end + ## + # Returns the length and number of undocumented items in +collection+. - ## - # Prints when a module is added + def doc_stats collection + [collection.length, collection.count { |item| not item.documented? }] + end - def print_module(*) end + ## + # Call this to mark the end of parsing for display purposes - ## - # Prints when RDoc is done + def done_adding + @display.done_adding + end - def done_adding(*) end + ## + # The documentation status of this project. +true+ when 100%, +false+ when + # less than 100% and +nil+ when unknown. + # + # Set by calling #calculate + def fully_documented? + @fully_documented end ## - # Stats printer that prints just the files being documented with a progress - # bar + # Returns a report on which items are not documented - class Normal < Quiet + def report + report = [] - def begin_adding # :nodoc: - puts "Parsing sources..." - end + calculate - ## - # Prints a file with a progress bar - - def print_file(files_so_far, filename) - progress_bar = sprintf("%3d%% [%2d/%2d] ", - 100 * (files_so_far + 1) / @total_files, - files_so_far + 1, - @total_files) - - if $stdout.tty? - # Print a progress bar, but make sure it fits on a single line. Filename - # will be truncated if necessary. - terminal_width = (ENV['COLUMNS'] || 80).to_i - max_filename_size = terminal_width - progress_bar.size - if filename.size > max_filename_size - # Turn "some_long_filename.rb" to "...ong_filename.rb" - filename = filename[(filename.size - max_filename_size) .. -1] - filename[0..2] = "..." - end + if @num_items == @doc_items then + report << '100% documentation!' + report << nil + report << 'Great Job!' - # Pad the line with whitespaces so that leftover output from the - # previous line doesn't show up. - line = "#{progress_bar}#{filename}" - padding = terminal_width - line.size - line << (" " * padding) if padding > 0 - - $stdout.print("#{line}\r") - else - $stdout.puts "#{progress_bar} #{filename}" - end - $stdout.flush + return report.join "\n" end - def done_adding # :nodoc: - puts - end + report << 'The following items are not documented:' + report << nil + + ucm = RDoc::TopLevel.unique_classes_and_modules + + ucm.sort.each do |cm| + type = case cm # TODO #definition + when RDoc::NormalClass then 'class' + when RDoc::SingleClass then 'class <<' + when RDoc::NormalModule then 'module' + end + + if cm.fully_documented? then + next + elsif cm.in_files.empty? or + (cm.constants.empty? and cm.method_list.empty?) then + report << "# #{type} #{cm.full_name} is referenced but empty." + report << '#' + report << '# It probably came from another project. ' \ + 'I\'m sorry I\'m holding it against you.' + report << nil + + next + elsif cm.documented? then + report << "#{type} #{cm.full_name} # is documented" + else + report << '# in files:' - end + cm.in_files.each do |file| + report << "# #{file.full_name}" + end - ## - # Stats printer that prints everything documented, including the documented - # status + report << nil - class Verbose < Normal + report << "#{type} #{cm.full_name}" + end - ## - # Returns a marker for RDoc::CodeObject +co+ being undocumented + unless cm.constants.empty? then + report << nil - def nodoc co - " (undocumented)" unless co.documented? - end + cm.each_constant do |constant| + next if constant.documented? + report << " # in file #{constant.file.full_name}" + report << " #{constant.name} = nil" + end + end - def print_alias as # :nodoc: - puts "\t\talias #{as.new_name} #{as.old_name}#{nodoc as}" - end + unless cm.attributes.empty? then + report << nil - def print_class(klass) # :nodoc: - puts "\tclass #{klass.full_name}#{nodoc klass}" - end + cm.each_attribute do |attr| + next if attr.documented? + report << " #{attr.definition} #{attr.name} " \ + "# in file #{attr.file.full_name}" + end + end - def print_constant(constant) # :nodoc: - puts "\t\t#{constant.name}#{nodoc constant}" - end + unless cm.method_list.empty? then + report << nil - def print_file(files_so_far, file) # :nodoc: - super - puts - end + cm.each_method do |method| + next if method.documented? + report << " # in file #{method.file.full_name}" + report << " def #{method.name}#{method.params}; end" + report << nil + end + end - def print_method(method) # :nodoc: - puts "\t\t#{method.singleton ? '::' : '#'}#{method.name}#{nodoc method}" + report << 'end' + report << nil end - def print_module(mod) # :nodoc: - puts "\tmodule #{mod.full_name}#{nodoc mod}" - end + report.join "\n" + end + ## + # Returns a summary of the collected statistics. + + def summary + calculate + + report = [] + report << 'Files: %5d' % @num_files + report << nil + report << 'Classes: %5d (%5d undocumented)' % [@num_classes, + @undoc_classes] + report << 'Modules: %5d (%5d undocumented)' % [@num_modules, + @undoc_modules] + report << 'Constants: %5d (%5d undocumented)' % [@num_constants, + @undoc_constants] + report << 'Attributes: %5d (%5d undocumented)' % [@num_attributes, + @undoc_attributes] + report << 'Methods: %5d (%5d undocumented)' % [@num_methods, + @undoc_methods] + report << nil + report << 'Total: %5d (%5d undocumented)' % [@num_items, + @undoc_items] + + report << '%6.2f%% documented' % @percent_doc if @percent_doc + report << nil + report << 'Elapsed: %0.1fs' % (Time.now - @start) + + report.join "\n" end -end + autoload :Quiet, 'rdoc/stats/quiet' + autoload :Normal, 'rdoc/stats/normal' + autoload :Verbose, 'rdoc/stats/verbose' +end diff --git a/lib/rdoc/stats/normal.rb b/lib/rdoc/stats/normal.rb new file mode 100644 index 0000000000..2057332b2d --- /dev/null +++ b/lib/rdoc/stats/normal.rb @@ -0,0 +1,51 @@ +## +# Stats printer that prints just the files being documented with a progress +# bar + +class RDoc::Stats::Normal < RDoc::Stats::Quiet + + def begin_adding # :nodoc: + puts "Parsing sources..." + end + + ## + # Prints a file with a progress bar + + def print_file files_so_far, filename + progress_bar = sprintf("%3d%% [%2d/%2d] ", + 100 * files_so_far / @num_files, + files_so_far, + @num_files) + + if $stdout.tty? then + # Print a progress bar, but make sure it fits on a single line. Filename + # will be truncated if necessary. + terminal_width = (ENV['COLUMNS'] || 80).to_i + max_filename_size = terminal_width - progress_bar.size + + if filename.size > max_filename_size then + # Turn "some_long_filename.rb" to "...ong_filename.rb" + filename = filename[(filename.size - max_filename_size) .. -1] + filename[0..2] = "..." + end + + # Pad the line with whitespaces so that leftover output from the + # previous line doesn't show up. + line = "#{progress_bar}#{filename}" + padding = terminal_width - line.size + line << (" " * padding) if padding > 0 + + $stdout.print("#{line}\r") + else + $stdout.puts "#{progress_bar} #{filename}" + end + + $stdout.flush + end + + def done_adding # :nodoc: + puts + end + +end + diff --git a/lib/rdoc/stats/quiet.rb b/lib/rdoc/stats/quiet.rb new file mode 100644 index 0000000000..eed27b2a88 --- /dev/null +++ b/lib/rdoc/stats/quiet.rb @@ -0,0 +1,59 @@ +## +# Stats printer that prints nothing + +class RDoc::Stats::Quiet + + ## + # Creates a new Quiet that will print nothing + + def initialize num_files + @num_files = num_files + end + + ## + # Prints a message at the beginning of parsing + + def begin_adding(*) end + + ## + # Prints when an alias is added + + def print_alias(*) end + + ## + # Prints when an attribute is added + + def print_attribute(*) end + + ## + # Prints when a class is added + + def print_class(*) end + + ## + # Prints when a constant is added + + def print_constant(*) end + + ## + # Prints when a file is added + + def print_file(*) end + + ## + # Prints when a method is added + + def print_method(*) end + + ## + # Prints when a module is added + + def print_module(*) end + + ## + # Prints when RDoc is done + + def done_adding(*) end + +end + diff --git a/lib/rdoc/stats/verbose.rb b/lib/rdoc/stats/verbose.rb new file mode 100644 index 0000000000..430809ae07 --- /dev/null +++ b/lib/rdoc/stats/verbose.rb @@ -0,0 +1,45 @@ +## +# Stats printer that prints everything documented, including the documented +# status + +class RDoc::Stats::Verbose < RDoc::Stats::Normal + + ## + # Returns a marker for RDoc::CodeObject +co+ being undocumented + + def nodoc co + " (undocumented)" unless co.documented? + end + + def print_alias as # :nodoc: + puts " alias #{as.new_name} #{as.old_name}#{nodoc as}" + end + + def print_attribute attribute # :nodoc: + puts " #{attribute.definition} #{attribute.name}#{nodoc attribute}" + end + + def print_class(klass) # :nodoc: + puts " class #{klass.full_name}#{nodoc klass}" + end + + def print_constant(constant) # :nodoc: + puts " #{constant.name}#{nodoc constant}" + end + + def print_file(files_so_far, file) # :nodoc: + super + puts + end + + def print_method(method) # :nodoc: + puts " #{method.singleton ? '::' : '#'}#{method.name}#{nodoc method}" + end + + def print_module(mod) # :nodoc: + puts " module #{mod.full_name}#{nodoc mod}" + end + +end + + diff --git a/lib/rdoc/task.rb b/lib/rdoc/task.rb index f87ef7dc0e..005c516eed 100644 --- a/lib/rdoc/task.rb +++ b/lib/rdoc/task.rb @@ -21,13 +21,238 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ +require 'rubygems' +begin + gem 'rdoc' +rescue Gem::LoadError +end + +begin + gem 'rake' +rescue Gem::LoadError +end + require 'rdoc' require 'rake' -require 'rake/rdoctask' +require 'rake/tasklib' + +## +# Create a documentation task that will generate the RDoc files for a project. +# +# The RDoc::Task will create the following targets: +# +# [rdoc] +# Main task for this RDoc task. +# +# [clobber_rdoc] +# Delete all the rdoc files. This target is automatically added to the main +# clobber target. +# +# [rerdoc] +# Rebuild the rdoc files from scratch, even if they are not out of date. +# +# Simple Example: +# +# RDoc::Task.new do |rd| +# rd.main = "README.rdoc" +# rd.rdoc_files.include("README.rdoc", "lib/**/*.rb") +# end +# +# The +rd+ object passed to the block is an RDoc::Task object. See the +# attributes list for the RDoc::Task class for available customization options. +# +# == Specifying different task names +# +# You may wish to give the task a different name, such as if you are +# generating two sets of documentation. For instance, if you want to have a +# development set of documentation including private methods: +# +# RDoc::Task.new :rdoc_dev do |rd| +# rd.main = "README.doc" +# rd.rdoc_files.include("README.rdoc", "lib/**/*.rb") +# rd.options << "--all" +# end +# +# The tasks would then be named :<em>rdoc_dev</em>, +# :clobber_<em>rdoc_dev</em>, and :re<em>rdoc_dev</em>. +# +# If you wish to have completely different task names, then pass a Hash as +# first argument. With the <tt>:rdoc</tt>, <tt>:clobber_rdoc</tt> and +# <tt>:rerdoc</tt> options, you can customize the task names to your liking. +# +# For example: +# +# RDoc::Task.new(:rdoc => "rdoc", :clobber_rdoc => "rdoc:clean", +# :rerdoc => "rdoc:force") +# +# This will create the tasks <tt>:rdoc</tt>, <tt>:rdoc:clean</tt> and +# <tt>:rdoc:force</tt>. + +class RDoc::Task < Rake::TaskLib + + ## + # Name of the main, top level task. (default is :rdoc) + + attr_accessor :name + + ## + # Name of directory to receive the html output files. (default is "html") + + attr_accessor :rdoc_dir + + ## + # Title of RDoc documentation. (defaults to rdoc's default) + + attr_accessor :title + + ## + # Name of file to be used as the main, top level file of the RDoc. (default + # is none) + + attr_accessor :main + + ## + # Name of template to be used by rdoc. (defaults to rdoc's default) + + attr_accessor :template + + ## + # List of files to be included in the rdoc generation. (default is []) + + attr_accessor :rdoc_files + + ## + # Additional list of options to be passed rdoc. (default is []) + + attr_accessor :options + + ## + # Whether to run the rdoc process as an external shell (default is false) + + attr_accessor :external + + ## + # Create an RDoc task with the given name. See the RDoc::Task class overview + # for documentation. + + def initialize(name = :rdoc) # :yield: self + if name.is_a? Hash then + invalid_options = name.keys.map { |k| k.to_sym } - + [:rdoc, :clobber_rdoc, :rerdoc] + + unless invalid_options.empty? then + raise ArgumentError, "invalid options: #{invalid_options.join(", ")}" + end + end + + @name = name + @rdoc_files = Rake::FileList.new + @rdoc_dir = 'html' + @main = nil + @title = nil + @template = nil + @options = [] + yield self if block_given? + define + end + + ## + # Create the tasks defined by this task lib. + + def define + desc "Build RDoc HTML files" + task rdoc_task_name + + desc "Rebuild RDoc HTML files" + task rerdoc_task_name => [clobber_task_name, rdoc_task_name] + + desc "Remove RDoc HTML files" + task clobber_task_name do + rm_r @rdoc_dir rescue nil + end + + task :clobber => [clobber_task_name] + + directory @rdoc_dir + + rdoc_target_deps = [ + @rdoc_files, + Rake.application.rakefile + ].flatten.compact + + task rdoc_task_name => [rdoc_target] + file rdoc_target => rdoc_target_deps do + @before_running_rdoc.call if @before_running_rdoc + args = option_list + @rdoc_files + + if Rake.application.options.trace then + $stderr.puts "rdoc #{args.join ' '}" + end + require 'rdoc/rdoc' + RDoc::RDoc.new.document(args) + end + + self + end + + ## + # List of options that will be supplied to RDoc + + def option_list + result = @options.dup + result << "-o" << @rdoc_dir + result << "--main" << main if main + result << "--title" << title if title + result << "-T" << template if template + result + end + + ## + # The block passed to this method will be called just before running the + # RDoc generator. It is allowed to modify RDoc::Task attributes inside the + # block. + + def before_running_rdoc(&block) + @before_running_rdoc = block + end + + private + + def rdoc_target + "#{rdoc_dir}/index.html" + end + + def rdoc_task_name + case name + when Hash then (name[:rdoc] || "rdoc").to_s + else name.to_s + end + end + + def clobber_task_name + case name + when Hash then (name[:clobber_rdoc] || "clobber_rdoc").to_s + else "clobber_#{name}" + end + end + + def rerdoc_task_name + case name + when Hash then (name[:rerdoc] || "rerdoc").to_s + else "re#{name}" + end + end + +end # :stopdoc: -module RDoc - Task = Rake::RDocTask +module Rake + + ## + # For backwards compatibility + + RDocTask = RDoc::Task + end # :startdoc: diff --git a/lib/rdoc/text.rb b/lib/rdoc/text.rb index 5280aa0fd2..20cd8a258a 100644 --- a/lib/rdoc/text.rb +++ b/lib/rdoc/text.rb @@ -1,9 +1,44 @@ +# coding: utf-8 + +## +# For RDoc::Text#to_html + +require 'strscan' + ## # Methods for manipulating comment text module RDoc::Text ## + # Maps an encoding to a Hash of characters properly transcoded for that + # encoding. + # + # See also encode_fallback. + + TO_HTML_CHARACTERS = Hash.new do |h, encoding| + h[encoding] = { + :close_dquote => encode_fallback('”', encoding, '"'), + :close_squote => encode_fallback('’', encoding, '\''), + :copyright => encode_fallback('©', encoding, '(c)'), + :ellipsis => encode_fallback('…', encoding, '...'), + :em_dash => encode_fallback('—', encoding, '---'), + :en_dash => encode_fallback('–', encoding, '--'), + :open_dquote => encode_fallback('“', encoding, '"'), + :open_squote => encode_fallback('‘', encoding, '\''), + :trademark => encode_fallback('®', encoding, '(r)'), + } + end if Object.const_defined? :Encoding + + ## + # Transcodes +character+ to +encoding+ with a +fallback+ character. + + def self.encode_fallback character, encoding, fallback + character.encode(encoding, :fallback => { character => fallback }, + :undef => :replace, :replace => fallback) + end + + ## # Expands tab characters in +text+ to eight spaces def expand_tabs text @@ -43,8 +78,7 @@ module RDoc::Text end ## - # Convert a string in markup format into HTML. Removes the first paragraph - # tags if +remove_para+ is true. + # Convert a string in markup format into HTML. # # Requires the including class to implement #formatter @@ -105,7 +139,7 @@ http://rubyforge.org/tracker/?atid=2472&group_id=627&func=browse def strip_hashes text return text if text =~ /^(?>\s*)[^\#]/ - text.gsub(/^\s*(#+)/) { $1.tr '#',' ' } + text.gsub(/^\s*(#+)/) { $1.tr '#',' ' }.gsub(/^\s+$/, '') end ## @@ -123,7 +157,102 @@ http://rubyforge.org/tracker/?atid=2472&group_id=627&func=browse text.sub! %r%/\*+% do " " * $&.length end text.sub! %r%\*+/% do " " * $&.length end text.gsub! %r%^[ \t]*\*%m do " " * $&.length end - text + text.gsub(/^\s+$/, '') + end + + ## + # Converts ampersand, dashes, ellipsis, quotes, copyright and registered + # trademark symbols in +text+ to properly encoded characters. + + def to_html text + if Object.const_defined? :Encoding then + html = ''.encode text.encoding + + encoded = RDoc::Text::TO_HTML_CHARACTERS[text.encoding] + else + html = '' + encoded = { + :close_dquote => '”', + :close_squote => '’', + :copyright => '©', + :ellipsis => '…', + :em_dash => '—', + :en_dash => '–', + :open_dquote => '“', + :open_squote => '‘', + :trademark => '®', + } + end + + s = StringScanner.new text + insquotes = false + indquotes = false + after_word = nil + + until s.eos? do + case + when s.scan(/<tt>.*?<\/tt>/) then # skip contents of tt + html << s.matched.gsub('\\\\', '\\') + when s.scan(/<tt>.*?/) then + warn 'mismatched <tt> tag' # TODO signal file/line + html << s.matched + when s.scan(/<[^>]+\/?s*>/) then # skip HTML tags + html << s.matched + when s.scan(/\\(\S)/) then # unhandled suppressed crossref + html << s[1] + after_word = nil + when s.scan(/\.\.\.(\.?)/) then + html << s[1] << encoded[:ellipsis] + after_word = nil + when s.scan(/\(c\)/) then + html << encoded[:copyright] + after_word = nil + when s.scan(/\(r\)/) then + html << encoded[:trademark] + after_word = nil + when s.scan(/---/) then + html << encoded[:em_dash] + after_word = nil + when s.scan(/--/) then + html << encoded[:en_dash] + after_word = nil + when s.scan(/"|"/) then + html << encoded[indquotes ? :close_dquote : :open_dquote] + indquotes = !indquotes + after_word = nil + when s.scan(/``/) then # backtick double quote + html << encoded[:open_dquote] + after_word = nil + when s.scan(/''/) then # tick double quote + html << encoded[:close_dquote] + after_word = nil + when s.scan(/'/) then # single quote + if insquotes + html << encoded[:close_squote] + insquotes = false + elsif after_word + # Mary's dog, my parents' house: do not start paired quotes + html << encoded[:close_squote] + else + html << encoded[:open_squote] + insquotes = true + end + + after_word = nil + else # advance to the next potentially significant character + match = s.scan(/.+?(?=[<\\.("'`&-])/) #" + + if match then + html << match + after_word = match =~ /\w$/ + else + html << s.rest + break + end + end + end + + html end end diff --git a/lib/rdoc/tokenstream.rb b/lib/rdoc/token_stream.rb index b1e86543f7..fb887f2fa4 100644 --- a/lib/rdoc/tokenstream.rb +++ b/lib/rdoc/token_stream.rb @@ -1,5 +1,3 @@ -module RDoc; end - ## # A TokenStream is a list of tokens, gathered during the parse of some entity # (say a method). Entities populate these streams by being registered with the diff --git a/lib/rdoc/top_level.rb b/lib/rdoc/top_level.rb index 306790fc15..d0ea621f8c 100644 --- a/lib/rdoc/top_level.rb +++ b/lib/rdoc/top_level.rb @@ -20,7 +20,14 @@ class RDoc::TopLevel < RDoc::Context attr_accessor :absolute_name - attr_accessor :diagram + ## + # All the classes or modules that were declared in + # this file. These are assigned to either +#classes_hash+ + # or +#modules_hash+ once we know what they really are. + + attr_reader :classes_or_modules + + attr_accessor :diagram # :nodoc: ## # The parser that processed this file @@ -28,45 +35,110 @@ class RDoc::TopLevel < RDoc::Context attr_accessor :parser ## - # Returns all classes and modules discovered by RDoc + # Returns all classes discovered by RDoc - def self.all_classes_and_modules - classes_hash.values + modules_hash.values + def self.all_classes + @all_classes_hash.values end ## - # Returns all classes discovered by RDoc + # Returns all classes and modules discovered by RDoc - def self.classes - classes_hash.values + def self.all_classes_and_modules + @all_classes_hash.values + @all_modules_hash.values end ## # Hash of all classes known to RDoc - def self.classes_hash - @all_classes + def self.all_classes_hash + @all_classes_hash end ## # All TopLevels known to RDoc - def self.files - @all_files.values + def self.all_files + @all_files_hash.values end ## # Hash of all files known to RDoc - def self.files_hash - @all_files + def self.all_files_hash + @all_files_hash + end + + ## + # Returns all modules discovered by RDoc + + def self.all_modules + all_modules_hash.values + end + + ## + # Hash of all modules known to RDoc + + def self.all_modules_hash + @all_modules_hash + end + + ## + # Prepares the RDoc code object tree for use by a generator. + # + # It finds unique classes/modules defined, and replaces classes/modules that + # are aliases for another one by a copy with RDoc::ClassModule#is_alias_for + # set. + # + # It updates the RDoc::ClassModule#constant_aliases attribute of "real" + # classes or modules. + # + # It also completely removes the classes and modules that should be removed + # from the documentation and the methods that have a visibility below + # +min_visibility+, which is the <tt>--visibility</tt> option. + # + # See also RDoc::Context#remove_from_documentation? + + def self.complete min_visibility + fix_basic_object_inheritance + + # cache included modules before they are removed from the documentation + all_classes_and_modules.each { |cm| cm.ancestors } + + remove_nodoc @all_classes_hash + remove_nodoc @all_modules_hash + + @unique_classes = find_unique @all_classes_hash + @unique_modules = find_unique @all_modules_hash + + unique_classes_and_modules.each do |cm| + cm.complete min_visibility + end + + @all_files_hash.each_key do |file_name| + tl = @all_files_hash[file_name] + + unless RDoc::Parser::Simple === tl.parser then + tl.modules_hash.clear + tl.classes_hash.clear + + tl.classes_or_modules.each do |cm| + name = cm.full_name + if cm.type == 'class' then + tl.classes_hash[name] = cm if @all_classes_hash[name] + else + tl.modules_hash[name] = cm if @all_modules_hash[name] + end + end + end + end end ## # Finds the class with +name+ in all discovered classes def self.find_class_named(name) - classes_hash[name] + @all_classes_hash[name] end ## @@ -91,9 +163,7 @@ class RDoc::TopLevel < RDoc::Context # Finds the class or module with +name+ def self.find_class_or_module(name) - name =~ /^::/ - name = $' || name - + name = $' if name =~ /^::/ RDoc::TopLevel.classes_hash[name] || RDoc::TopLevel.modules_hash[name] end @@ -101,7 +171,7 @@ class RDoc::TopLevel < RDoc::Context # Finds the file with +name+ in all discovered files def self.find_file_named(name) - @all_files[name] + @all_files_hash[name] end ## @@ -112,26 +182,98 @@ class RDoc::TopLevel < RDoc::Context end ## - # Returns all modules discovered by RDoc + # Finds unique classes/modules defined in +all_hash+, + # and returns them as an array. Performs the alias + # updates in +all_hash+: see ::complete. + #-- + # TODO aliases should be registered by Context#add_module_alias - def self.modules - modules_hash.values + def self.find_unique(all_hash) + unique = [] + + all_hash.each_pair do |full_name, cm| + unique << cm if full_name == cm.full_name + end + + unique end ## - # Hash of all modules known to RDoc + # Fixes the erroneous <tt>BasicObject < Object</tt> in 1.9. + # + # Because we assumed all classes without a stated superclass + # inherit from Object, we have the above wrong inheritance. + # + # We fix BasicObject right away if we are running in a Ruby + # version >= 1.9. If not, we may be documenting 1.9 source + # while running under 1.8: we search the files of BasicObject + # for "object.c", and fix the inheritance if we find it. + + def self.fix_basic_object_inheritance + basic = all_classes_hash['BasicObject'] + return unless basic + if RUBY_VERSION >= '1.9' + basic.superclass = nil + elsif basic.in_files.any? { |f| File.basename(f.full_name) == 'object.c' } + basic.superclass = nil + end + end - def self.modules_hash - @all_modules + ## + # Removes from +all_hash+ the contexts that are nodoc or have no content. + # + # See RDoc::Context#remove_from_documentation? + + def self.remove_nodoc(all_hash) + all_hash.keys.each do |name| + context = all_hash[name] + all_hash.delete(name) if context.remove_from_documentation? + end end ## # Empties RDoc of stored class, module and file information def self.reset - @all_classes = {} - @all_modules = {} - @all_files = {} + @all_classes_hash = {} + @all_modules_hash = {} + @all_files_hash = {} + end + + ## + # Returns the unique classes discovered by RDoc. + # + # ::complete must have been called prior to using this method. + + def self.unique_classes + @unique_classes + end + + ## + # Returns the unique classes and modules discovered by RDoc. + # ::complete must have been called prior to using this method. + + def self.unique_classes_and_modules + @unique_classes + @unique_modules + end + + ## + # Returns the unique modules discovered by RDoc. + # ::complete must have been called prior to using this method. + + def self.unique_modules + @unique_modules + end + + class << self + alias classes all_classes + alias classes_hash all_classes_hash + + alias files all_files + alias files_hash all_files_hash + + alias modules all_modules + alias modules_hash all_modules_hash end reset @@ -148,17 +290,49 @@ class RDoc::TopLevel < RDoc::Context @diagram = nil @parser = nil + @classes_or_modules = [] + RDoc::TopLevel.files_hash[file_name] = self end ## - # Adds +method+ to Object instead of RDoc::TopLevel + # Adds +an_alias+ to +Object+ instead of +self+. + + def add_alias(an_alias) + return an_alias unless @document_self + object_class.add_alias an_alias + end + + ## + # Adds +constant+ to +Object+ instead of +self+. + + def add_constant(constant) + return constant unless @document_self + object_class.add_constant constant + end + + ## + # Adds +include+ to +Object+ instead of +self+. + + def add_include(include) + return include unless @document_self + object_class.add_include include + end + + ## + # Adds +method+ to +Object+ instead of +self+. def add_method(method) - object = self.class.find_class_named 'Object' - object = add_class RDoc::NormalClass, 'Object' unless object + return method unless @document_self + object_class.add_method method + end + + ## + # Adds class or module +mod+. Used in the building phase + # by the ruby parser. - object.add_method method + def add_to_classes_or_modules mod + @classes_or_modules << mod end ## @@ -168,8 +342,13 @@ class RDoc::TopLevel < RDoc::Context File.basename @absolute_name end + alias name base_name + ## - # See RDoc::TopLevel.find_class_or_module + # See RDoc::TopLevel::find_class_or_module + #-- + # TODO Why do we search through all classes/modules found, not just the + # ones of this instance? def find_class_or_module name RDoc::TopLevel.find_class_or_module name @@ -186,11 +365,11 @@ class RDoc::TopLevel < RDoc::Context # Finds a module or class with +name+ def find_module_named(name) - find_class_or_module(name) || find_enclosing_module_named(name) + find_class_or_module(name) end ## - # The name of this file + # Returns the relative name of this file def full_name @relative_name @@ -215,16 +394,24 @@ class RDoc::TopLevel < RDoc::Context end ## - # Date this file was last modified, if known + # Time this file was last modified, if known def last_modified - @file_stat ? file_stat.mtime.to_s : 'Unknown' + @file_stat ? file_stat.mtime : nil end ## - # Base name of this file - - alias name base_name + # Returns the NormalClass "Object", creating it if not found. + # + # Records +self+ as a location in "Object". + + def object_class + @object_class ||= begin + oc = self.class.find_class_named('Object') || add_class(RDoc::NormalClass, 'Object') + oc.record_location self + oc + end + end ## # Path to this file @@ -244,5 +431,9 @@ class RDoc::TopLevel < RDoc::Context end end + def to_s # :nodoc: + "file #{full_name}" + end + end |