You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@buildr.apache.org by as...@apache.org on 2007/11/14 00:49:28 UTC

svn commit: r594723 - in /incubator/buildr/docter/trunk: ./ lib/ lib/docter/

Author: assaf
Date: Tue Nov 13 15:49:20 2007
New Revision: 594723

URL: http://svn.apache.org/viewvc?rev=594723&view=rev
Log:
Initial commit for use with Buildr

Added:
    incubator/buildr/docter/trunk/CHANGELOG
    incubator/buildr/docter/trunk/LICENSE
    incubator/buildr/docter/trunk/README
    incubator/buildr/docter/trunk/Rakefile
    incubator/buildr/docter/trunk/lib/
    incubator/buildr/docter/trunk/lib/docter/
    incubator/buildr/docter/trunk/lib/docter.rb
    incubator/buildr/docter/trunk/lib/docter/collection.rb
    incubator/buildr/docter/trunk/lib/docter/common.rb
    incubator/buildr/docter/trunk/lib/docter/page.rb
    incubator/buildr/docter/trunk/lib/docter/rake.rb
    incubator/buildr/docter/trunk/lib/docter/server.rb
    incubator/buildr/docter/trunk/lib/docter/template.rb
    incubator/buildr/docter/trunk/lib/docter/ultraviolet.rb

Added: incubator/buildr/docter/trunk/CHANGELOG
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/CHANGELOG?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/CHANGELOG (added)
+++ incubator/buildr/docter/trunk/CHANGELOG Tue Nov 13 15:49:20 2007
@@ -0,0 +1,10 @@
+1.0.2 (Pending)
+* Fixed: Sleek upload with changelog for each release courtesy of Anatol Pomozov.
+
+1.0.1 (6/6/2007)
+* Added: renumber_footnotes to template for handling footnote numbering when creating a single page. 
+* Changed: Ultraviolet no longer used as default syntax highlighting, must require separately.
+* Changed: footnote_links is now list_links and the new method eliminates duplicates (based on URL), sorts alphabetically (the text component) and capitalizes the text description.
+
+1.0.0 (6/3/2007)
+* First release of working code.

Added: incubator/buildr/docter/trunk/LICENSE
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/LICENSE?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/LICENSE (added)
+++ incubator/buildr/docter/trunk/LICENSE Tue Nov 13 15:49:20 2007
@@ -0,0 +1,176 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS

Added: incubator/buildr/docter/trunk/README
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/README?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/README (added)
+++ incubator/buildr/docter/trunk/README Tue Nov 13 15:49:20 2007
@@ -0,0 +1 @@
+

Added: incubator/buildr/docter/trunk/Rakefile
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/Rakefile?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/Rakefile (added)
+++ incubator/buildr/docter/trunk/Rakefile Tue Nov 13 15:49:20 2007
@@ -0,0 +1,119 @@
+require "rubygems"
+Gem::manage_gems
+require "rake/gempackagetask"
+require "spec/rake/spectask"
+require "rake/rdoctask"
+require "lib/docter"
+
+
+# Gem specification comes first, other tasks rely on it.
+spec = Gem::Specification.new do |spec|
+  spec.name         = "docter"
+  spec.version      = File.read(__FILE__.pathmap("%d/lib/docter.rb")).scan(/VERSION\s*=\s*(['"])(.*)\1/)[0][1]
+  spec.author       = "Apache Software Foundation"
+  #spec.email        = ""
+  spec.homepage     = "http://incubator.apache.org/buildr/"
+  spec.summary      = "We has docs"
+  spec.files        = FileList["lib/**/*", "CHANGELOG", "README", "LICENSE", "Rakefile", "html/**/*"].collect
+  spec.require_path = "lib"
+  spec.autorequire  = "docter.rb"
+  spec.has_rdoc     = true
+  spec.extra_rdoc_files = ["README", "CHANGELOG", "LICENSE"]
+  spec.rdoc_options << "--title" << "Docter -- #{spec.summary}" <<
+                       "--main" << "README" << "--line-numbers" << "-inline-source"
+  spec.rubyforge_project = "buildr"
+
+  # Tested against these dependencies.
+  spec.add_dependency "facets",   "~> 1.8"
+  spec.add_dependency "RedCloth", "~> 3.0"
+  spec.add_dependency "haml",     "~> 1.7"
+  spec.add_dependency "mongrel",  "~> 1.0"
+end
+
+
+# Testing is everything.
+desc "Run test cases"
+Spec::Rake::SpecTask.new(:test) do |task|
+  task.spec_files = FileList["test/**/*.rb"]
+  task.spec_opts = [ "--format", "specdoc", "--color", "--diff" ]
+end
+
+desc "Run test cases with rcov"
+Spec::Rake::SpecTask.new(:rcov) do |task|
+  task.spec_files = FileList["test/**/*.rb"]
+  task.spec_opts = [ "--format", "specdoc", "--color", "--diff" ]
+  task.rcov = true
+end
+
+
+# Packaging and local installation.
+Rake::GemPackageTask.new(spec) do |pkg|
+  pkg.need_tar = true
+  pkg.need_zip = true
+end
+
+desc "Install the package locally"
+task :install=>:package do |task|
+  system "gem", "install", "pkg/#{spec.name}-#{spec.version}.gem"
+end
+
+desc "Uninstall previously installed packaged"
+task :uninstall do |task|
+  system "gem", "uninstall", spec.name, "-v", spec.version.to_s
+end
+
+
+desc "Generate RDoc documentation"
+rdoc = Rake::RDocTask.new(:rdoc) do |rdoc|
+  rdoc.rdoc_dir = "rdoc"
+  rdoc.title    = spec.name
+  rdoc.options  = spec.rdoc_options
+  rdoc.rdoc_files.include("lib/**/*.rb")
+  rdoc.rdoc_files.include spec.extra_rdoc_files
+end
+
+task("clobber") { rm_rf [rdoc.rdoc_dir].map(&:to_s) }
+
+
+# Commit to SVN, upload and do the release cycle.
+namespace :svn do
+  task :clean? do |task|
+    status = `svn status`.reject { |line| line =~ /\s(pkg|html)$/ }
+    fail "Cannot release unless all local changes are in SVN:\n#{status}" unless status.empty?
+  end
+  
+  task :tag do |task|
+    cur_url = `svn info`.scan(/URL: (.*)/)[0][0]
+    new_url = cur_url.sub(/trunk$/, "tags/#{spec.version.to_s}")
+    system "svn", "remove", new_url, "-m", "Removing old copy" rescue nil
+    system "svn", "copy", cur_url, new_url, "-m", "Release #{spec.version.to_s}"
+  end
+end
+
+namespace :upload do
+  task :packages=>["rake:package"] do |task|
+    # Read the changes for this release.
+    pattern = /(^(\d+\.\d+(?:\.\d+)?)\s+\(\d+\/\d+\/\d+\)\s*((:?^[^\n]+\n)*))/
+    changelog = File.read(__FILE__.pathmap("%d/CHANGELOG"))
+    changes = changelog.scan(pattern).inject({}) { |hash, set| hash[set[1]] = set[2] ; hash }
+    current = changes[spec.version.to_s]
+    if !current && spec.version.to_s =~ /\.0$/
+      current = changes[spec.version.to_s.split(".")[0..-2].join(".")] 
+    end
+    fail "No changeset found for version #{spec.version}" unless current
+
+    puts "Uploading #{spec.name} #{spec.version}"
+    puts "Uploading #{spec.name} #{spec.version}"
+    files = %w( gem tgz zip ).map { |ext| "pkg/#{spec.name}-#{spec.version}.#{ext}" }
+    rubyforge = RubyForge.new
+    rubyforge.login    
+    File.open(".changes", 'w'){|f| f.write(current)}
+    rubyforge.userconfig.merge!("release_changes" => ".changes",  "preformatted" => true)
+    rubyforge.add_release spec.rubyforge_project.downcase, spec.name.downcase, spec.version, *files
+    rm ".changes"
+    puts "Release #{spec.version} uploaded"
+  end
+end
+
+desc "Upload release to RubyForge including docs, tag SVN"
+task :release=>[ "clobber", "svn:clean?", "test", "upload:packages" ]

Added: incubator/buildr/docter/trunk/lib/docter.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/lib/docter.rb?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/lib/docter.rb (added)
+++ incubator/buildr/docter/trunk/lib/docter.rb Tue Nov 13 15:49:20 2007
@@ -0,0 +1,32 @@
+# &:symbol goodness.
+require "facet/symbol/to_proc"
+# blank? on string and nil
+require "facet/string/blank"
+require "facet/nilclass/blank"
+# x.in?(y) is better than y.include?(x)
+require "facet/string/starts_with"
+require "facets/core/kernel/tap"
+require "facet/kernel/__DIR__"
+
+module Docter
+  VERSION = "1.0.2".freeze
+end
+
+$LOAD_PATH.unshift __DIR__
+
+require "cgi"
+require "erb"
+# All these Gems are optional.
+["redcloth", "haml", "mongrel", "uv"].each do |gem|
+  begin
+    require gem
+  rescue LoadError
+  end
+end
+
+require "docter/common.rb"
+require "docter/page.rb"
+require "docter/template.rb"
+require "docter/collection.rb"
+require "docter/server.rb" if defined?(Mongrel)
+require "docter/rake.rb" if defined?(Rake)

Added: incubator/buildr/docter/trunk/lib/docter/collection.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/lib/docter/collection.rb?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/lib/docter/collection.rb (added)
+++ incubator/buildr/docter/trunk/lib/docter/collection.rb Tue Nov 13 15:49:20 2007
@@ -0,0 +1,307 @@
+module Docter
+
+  class Collection
+
+    class ToCResource < Resource::Base
+
+      def initialize(collection, *args)
+        super(*args)
+        init_from *args
+        @collection = collection
+      end
+      
+      def page(path)
+        @collection.page(path)
+      end
+
+      def toc()
+        load
+        @toc
+      end
+
+      def title()
+        load
+        @title
+      end
+
+    protected
+
+      if defined?(::RedCloth)
+        def create_from_textile(text, options)
+          html = RedCloth.new(erb_this(text, binding), [:no_span_caps]).to_html(:textile)
+          create_from_html html, options
+        end
+
+        def create_from_markup(text, options)
+          html = RedCloth.new(erb_this(text, binding), [:no_span_caps]).to_html(:markup)
+          create_from_html html, options
+        end
+      else
+        def create_from_textile(text, options)
+          fail "You need to install RedCloth first:\n  gem install RedCloth"
+        end
+        alias :create_from_markup :create_from_textile
+      end
+
+      def create_from_html(html, options)
+        @toc = ToC.new.tap do |toc|
+          html.scan(regexp_element("ol")).each do |tag, attrs, list|
+            list.scan(regexp_element("li")).each do |tag, attrs, item|
+              if item[regexp_element("a")]
+                attrs, title = $2, $3
+                url = $3 if attrs =~ regexp_attribute("href")
+                if page = page(url)
+                  toc.add page.toc_entry 
+                else
+                  toc.add url, inner_text_from(title)
+                end
+              else
+                url, title = "#", inner_text_from(item)
+                if page = page(item.strip.gsub(/\s+/, " ").downcase + ".html")
+                  toc.add page.toc_entry 
+                else
+                  toc.add url, inner_text_from(title)
+                end
+              end
+            end
+          end
+          @title = inner_text_from($3) if html =~ regexp_element("h1")
+        end
+      end
+
+    end
+
+
+    include HTML
+
+    def initialize(title = nil)
+      @title = title
+      @sources = FileList[]
+    end
+
+    # The collection title (HTML encoded).
+    def title()
+      @title || if @toc_resource
+        @toc_resource.reload if @toc_resource.modified?
+        @toc_resource.title
+      end || ""
+    end
+
+    # :call-seq:
+    #   toc() => ToC
+    #
+    # Returns the collection's ToC.
+    def toc()
+      if @toc_resource
+        @toc_resource.reload if @toc_resource.modified?
+        @toc_resource.toc
+      else
+        @toc ||= pages.inject(ToC.new) { |toc, page| toc.add page.toc_entry ; toc }
+      end
+    end
+
+    # :call-seq:
+    #   using(toc) => self
+    #
+    # Specifies the ToC to use. You can load an exiting ToC object, or specify a filename
+    # containing the ToC.
+    def using(toc)
+      self.toc = toc
+      self
+    end
+
+    # :call-seq:
+    #   toc = toc|filename|nil
+    #
+    # Sets the ToC. You can provide an existing ToC, provide a ToC file name, or nil to create
+    # the default ToC from all pages in this collection.
+    def toc=(toc)
+      case toc
+      when String
+        @toc_resource = ToCResource.new(self, toc)
+      when ToCResource
+        @toc_resource = toc
+      when ToC, nil
+        @toc = toc
+      end
+    end
+
+    # :call-seq:
+    #   include(*paths) => self
+    #
+    # Adds files and directories to the collection.
+    def include(*paths)
+      @sources.include *paths.flatten
+      self
+    end
+
+    alias :add :include
+
+    # :call-seq:
+    #   exclude(*paths) => self
+    #
+    # Excludes files or directories from the collection.
+    def exclude(*paths)
+      @sources.exclude *paths.flatten
+      self
+    end
+
+    # :call-seq:
+    #   page(path) => Page
+    #
+    # Returns a page based on its path.
+    #
+    # For example:
+    #   collection.include("doc/pages")
+    #   collection.page("index.html")
+    # Will return a page generated from "doc/pages/index.textile".
+    def page(path)
+      pages.find { |page| page.path == path }
+    end
+
+    # :call-seq:
+    #   pages() => Pages
+    #
+    # Returns all the pages in this collection/
+    def pages()
+      @pages = @sources.map { |path| File.directory?(path) ? FileList[File.join(path, "**/*")] : path }.flatten.
+        inject(@pages || {}) { |pages, file| pages[file] ||= Page.new(file, :collection=>self) ; pages }
+      @pages.values
+    end
+
+    def prev(page)
+      pages = toc.map { |entry| page(entry.url) }.compact
+      index = pages.index(page)
+      pages[index - 1] if index > 0
+    end
+
+    def next(page)
+      pages = toc.map { |entry| page(entry.url) }.compact
+      pages[pages.index(page) + 1]
+    end
+
+    # :call-seq:
+    #   file(path) => filename
+    #
+    # Returns the full path based on the relative path. For example:
+    #   collection.include("pages", "LICENSE")
+    #   collection.find("index.textile") => "pages/index.textile"
+    #   collection.find("LICENSE") => "LICENSE"
+    def file(path)      
+      @sources.inject(nil) do |found, file|
+        break found if found
+        if File.directory?(file)
+          base = file + "/"
+          FileList[File.join(file, "**/*")].find { |file| file.sub(base, "") == path }
+        else
+          file if File.basename(file) == path
+        end
+      end
+    end
+
+    # :call-seq:
+    #   render(template, page, options?) => html
+    #   render(template, options?) => html
+    #
+    # Render page or collection using the template.
+    #
+    # The first form attempts to reload the page if modified and passes the page to the template
+    # context, returning a single HTML document generated from that page.
+    #
+    # The second form attempts to reload all modified pages from the ToC, and passes all these
+    # pages to the template context, returning a single HTML document generates from pages as
+    # ordered by the ToC.
+    def render(template, *args)
+      options = Hash === args.last ? args.pop : {}
+      template = Template.new(template) unless Template === template
+      template.reload if template.modified?
+      if page = args.shift
+        page.reload if page.modified?
+        template.render options.merge(:collection=>self, :page=>page, :one_page=>false)
+      else
+        pages = toc.map { |entry| page(entry.url) }.compact
+        pages.each { |page| page.reload if page.modified? }
+        html = template.render(options.merge(:collection=>self, :pages=>pages, :one_page=>true))
+
+        url_map = pages.inject({}) { |map, page| map[page.path] = "##{page.id}" ; map }
+        html.gsub(regexp_element("a")) do |link|
+          link.gsub(regexp_attribute("href")) do |href|
+            url = $3
+            url = url_map[url] || url.gsub(/^(.*?)#(.*?)$/) { |path| url_map[$1] ? "##{$2}" : path }
+            %{href="#{url}"}
+          end
+        end
+      end
+    end
+
+    # :call-seq:
+    #   generate(template, to_dir, :all?)
+    #   generate(template, to_dir, pages)
+    #   generate(template, to_dir, page)
+    #   generate(template, to_dir, :one_page)
+    #
+    # The first form generates all pages from the ToC into the destination directory. The :all argument
+    # is optional. In addition, it copies all other resource files to the destination directory.
+    #
+    # The second form works the same way but only for the specified pages, and the third form for
+    # a single page. Neither copies any resource files.
+    #
+    # The fourth form generates a single HTML document in the destination directory (index.html) with
+    # all pages from the ToC, and copies all resource files to the destination directory. It will
+    # typically use a different template than the first three forms.
+    def generate(template, to_dir, *args)
+      options = Hash === args.last ? args.pop : {}
+      if args.empty? || args.first == :all
+        generate template, to_dir, pages, options
+        template.copy_resources to_dir
+      elsif args.first == :one_page
+        mkpath to_dir
+        File.join(to_dir, "index.html").tap do |filename|
+          puts "Generating #{filename}" if verbose
+          html = render(template, options)
+          File.open(filename, "w") { |file| file.write html }
+        end
+        template.copy_resources to_dir
+      else
+        mkpath to_dir
+        args.flatten.each do |page|
+          File.join(to_dir, page.path).tap do |filename|
+            puts "Generating #{filename}" if verbose
+            html = render(template, page, options)
+            File.open(filename, "w") { |file| file.write html }
+          end
+        end
+      end
+    end
+
+    # :call-seq:
+    #
+    # Returns a list of dependencies (resource files, the template file, etc). Useful when creating
+    # a Rake task based on this template.
+    def dependencies() #:nodoc:
+      files = @sources.map { |path| File.directory?(path) ? FileList[path, File.join(path, "**/*")] : path }.flatten +
+        (@toc_resource ? [@toc_resource.filename] : [])
+    end
+
+  end
+
+
+  class << self
+
+    # :call-seq:
+    #   collection(title?) => Collection
+    #
+    # Creates and returns a new collection. The collection initially does not include any files.
+    # When created from a ToC file, the collection bases its ToC on that file, otherwise it creates
+    # a ToC form all pages included in the collection.
+    def collection(title = nil)
+      Collection.new(title)
+    end
+
+    def toc(*args)
+      Collection::ToCResource.new(*args)
+    end
+
+  end
+
+end

Added: incubator/buildr/docter/trunk/lib/docter/common.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/lib/docter/common.rb?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/lib/docter/common.rb (added)
+++ incubator/buildr/docter/trunk/lib/docter/common.rb Tue Nov 13 15:49:20 2007
@@ -0,0 +1,303 @@
+module Docter
+
+  module HTML
+
+    module_function
+
+    # :call-seq:
+    #   inner_text_from(html) => text
+    #
+    # Returns the inner text from some HTML text, effectively stripping the tags, normalizing whitespaces
+    # and stripping leading/trailing spaces.
+    def inner_text_from(html)
+      html.gsub(/<(\w*).*?>(.*?)<\/\1\s*>/m, "\\2").strip.gsub(/\s+/m, " ")
+    end
+
+    def regexp_element(name)
+      Regexp.new("<(#{name})\\s*(.*?)>(.*?)<\\/\\1\\s*>", Regexp::MULTILINE + Regexp::IGNORECASE)
+    end
+
+    def regexp_attribute(name)
+      Regexp.new("(#{name})=([\"'])(.*?)\\2", Regexp::MULTILINE + Regexp::IGNORECASE)
+    end
+
+  end
+
+
+  # Base class for resources like pages, templates, ToC, and anything else that you can create dynamically,
+  # or load from a file. It's the second usage that's more interesting: when coming from a file, the resource
+  # is created lazily, and you can detect when it is modified and reload it.
+  #
+  # A class that inherits from Resource must: a) call #load before using any value obtain from the resource
+  # (e.g page title), and b) implement one or more create_from_[format] methods for each content format it
+  # supports (e.g. create_from_textile).
+  module Resource
+
+    # Maps various filename extensions to the appropriate format. You only need to use this when the filename
+    # extension is not the same as the format, e.g. map ".txt" to :plain, but not necessary to map ".textile".
+    EXTENSIONS = { ""=>:plain, ".txt"=>:plain, ".text"=>:plain, ".thtml"=>:textile, ".mhtml"=>:markdown }
+
+    class << self
+
+      # :call-seq:
+      #   format_from(filename) => symbol
+      #
+      # Returns the format based on the filename. Basically uses the filename extension, possibly mapped
+      # using EXTENSIONS, and returns :plain if the filename has no extension.
+      def format_from(filename)
+        ext = File.extname(filename)
+        EXTENSIONS[ext] || ext[1..-1].to_sym
+      end
+
+    end
+
+
+    module Reloadable
+
+      # The filename, if this resource comes from a file, otherwise nil.
+      attr_reader :filename
+
+      # :call-seq:
+      #   modified() => time
+      #
+      # Returns the date/time this resource was last modified. If the resource comes from a file,
+      # the timestamp of the file, otherwise the when the resource was created.
+      def modified()
+        @filename ? File.stat(@filename).mtime : @modified
+      end
+
+      # :call-seq:
+      #   modified?() => boolean
+      #
+      # Returns true if the resource was modified since it was lase (re)loaded. Only applies to resources
+      # created from a file, all other resources return false.
+      def modified?()
+        @filename ? File.stat(@filename).mtime > @modified : false
+      end
+
+      # :call-seq:
+      #   reload()
+      #
+      # Reloads the resource. Only applies to resources created from a file, otherwise acts like load.
+      # You can safely call it for all resources, for example:
+      #   page.reload if page.modified?
+      def reload()
+        @loaded = false if @filename
+        load
+      end
+
+      def to_s() #:nodoc:
+        @filename || super
+      end
+
+    protected
+
+      # See Base::new.
+      def init_from(*args, &block)
+        options = Hash === args.last ? args.pop : {}
+        case args.first
+        when String
+          @filename = args.shift
+          raise ArgumentError, "Expecting file name and options, found too may arguments." unless args.empty?
+          # We'll load the file later, but we need to known the mtime in case someone calls modified?/reload first.
+          @modified = File.stat(@filename).mtime
+          @load_using = lambda do
+            puts "Loading #{filename}"
+            # We need to know when the file we're about to read was last modified, but only keep the new mtime
+            # if we managed to read it. We're avoiding race conditions with a user editing this file.
+            modified = File.stat(@filename).mtime
+            create Resource.format_from(@filename), File.read(@filename), options
+            @modified = modified
+          end
+        when Symbol
+          @modified = Time.now # Best guess
+          format, content = args.shift, args.shift
+          raise ArgumentError, "Expecting format (as symbol) followed by content (string), found too many arguments." unless args.empty?
+          @load_using = lambda { create format, content, options }
+        else
+          if args.empty? && block
+            @modified = Time.now # Best guess
+            @load_using = lambda { block.call options }
+          else
+            raise ArgumentError, "Expecting file name, or (format, content), not sure what to do with these arguments."
+          end
+        end
+      end
+
+      # :call-seq:
+      #   load()
+      #
+      # Loads the resource. Call this method before anything that depends on the content of the resource,
+      # for example:
+      #   def title()
+      #     load
+      #     @title # Created by load
+      #   end
+      def load()
+        unless @loaded
+          @load_using.call
+          @loaded = true
+        end
+      end
+
+      # :call-seq:
+      #   create(format, content, options)
+      #
+      # Creates the resource using the specified format, content and options passed during construction.
+      #
+      # This method may be called multiple times, specifically each time the resource is loaded. Override,
+      # if you need to perform any clean up to assure propert creation, etc. Otherwise, just let it delegate
+      # to a create_from_[format] method, such as create_from_textile.
+      def create(format, content, options)
+        method = "create_from_#{format}"
+        if respond_to?(method)
+          send method, content, options
+        else
+          raise ArgumentError, "Don't know how to create #{self} from :#{format}."
+        end
+      end
+
+      # :call-seq:
+      #   erb(content, binding?) => content
+      #
+      # Passes the content through ERB processing. Nothing fancy, but allows you to run filters,
+      # include files, generate timestamps, calculate sales tax, etc.
+      def erb_this(content, binding = nil)
+        ERB.new(content).result(binding)
+      end
+
+    end
+
+
+    class Base
+
+      include Reloadable, HTML
+
+      # :call-seq:
+      #   new(filename, options?)
+      #   new(format, content, options?)
+      #   new(options?) { |options| ... }
+      #
+      # The first form loads this resource from the specified file. Decides on the format based on the filename.
+      # You can then detect modification and reload as necessary, for example:
+      #   page.reload if page.modified?
+      #
+      # The second form creates this resource from content in the specified format. This one you cannot reload.
+      # For example:
+      #   Page.new(:plain, "HAI")
+      #
+      # The third form creates this resource by calling the block with the supplied options.
+      def initialize(*args, &block)
+        init_from *args, &block
+      end
+    
+    end
+
+  end
+
+
+  # Table of contents.
+  #
+  # A ToC is an array of entries, each entry providing a link to and a title, and may itself be a ToC.
+  #
+  # Supports the Enumerable methods for operating on the entries, in addition to the methods each,
+  # first/last, size, empty? and index/include?. Use #add to create new entries.
+  #
+  # Use #to_html to transform to an HTML ordered list.
+  class ToC
+
+    include Enumerable
+
+    # Array of entries.
+    attr_reader :entries
+
+    # Create new ToC with no entries.
+    def initialize()
+      @entries = []
+    end
+   
+    ARRAY_METHODS = ["each", "first", "last", "size", "empty?", "include?", "index", "[]"]
+    (Enumerable.instance_methods + ARRAY_METHODS - ["entries"]).each do |method|
+      class_eval "def #{method}(*args, &block) ; entries.send(:#{method}, *args, &block) ; end", __FILE__, __LINE__
+    end
+
+    # :call-seq:
+    #   add(url, title) => entry
+    #   add(entry) => entry
+    #
+    # Adds (and returns) a new entry. The first form creates an entry with a link (must be a valid URL,
+    # use CGI.escape if necessary) and HTML-encoded title. The second form adds an existing entry,
+    # for example to a page.
+    def add(*args)
+      if ToCEntry === args.first
+        entry = args.shift
+        raise ArgumentError, "Can only accept a ToCEntry argument." unless args.empty?
+      else
+        entry = ToCEntry.new(*args)
+      end
+      entries << entry
+      entry
+    end
+
+    # :call-seq:
+    #   to_html(options) => html
+    #
+    # Transforms this ToC into an HTML ordered list (OL) by calling to_html on each ToC entry.
+    #
+    # You can use the following options:
+    # * :nested -- For entries that are also ToC, expands them as well. You can specify how many
+    #   levels (e.g. 1 to expand only once), or true to expand all levels.
+    # * :class -- Class to apply to the OL element.
+    #
+    # The +options+ argument can take the form of a Hash, list of symbols or both. Symbols are
+    # treated as +true+ for example:
+    #   to_html(:nested, :class=>"toc")
+    # Is the same as:
+    #   to_html(:nested=>true, :class=>"toc")
+    def to_html(*args)
+      options = Hash === args.last ? args.pop.clone : {}
+      args.each { |arg| options[arg.to_sym] = true }
+      cls = %{ class="#{options[:class]}"} if options[:class]
+      %{<ol #{cls}>#{map { |entry| entry.to_html(options) }}</ol>}
+    end
+
+  end
+
+
+  # Table of contents entry.
+  class ToCEntry < ToC
+
+    # The URL for this entry.
+    attr_reader :url
+
+    # The title of this entry.
+    attr_reader :title
+
+    # :call-seq:
+    #   new(url, title)
+    #
+    # URL links to the ToC entry, and must be a valid URL (use CGI.escape is necessary). The title must
+    # be HTML-encoded (use CGI.escapeHTML if necessary).
+    def initialize(url, title)
+      super()
+      @url, @title = url, title
+    end
+   
+    # :call-seq:
+    #   to_html(nested?) => html
+    #
+    # Transforms this ToC entry into an HTML list item (LI). Depending on the nested argument,
+    # can also expand nested ToC.
+    def to_html(*args)
+      options = Hash === args.last ? args.pop.clone : {}
+      args.each { |arg| options[arg.to_sym] = true }
+      if options[:nested] && !empty?
+        nested = options[:nested].respond_to?(:to_i) && options[:nested].to_i > 0 ?
+          super(options.merge(:nested=>options[:nested] - 1)) : super(options)
+      end
+      %{<li><a href="#{url}">#{title}</a>#{nested}</li>}
+    end
+
+  end
+
+end

Added: incubator/buildr/docter/trunk/lib/docter/page.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/lib/docter/page.rb?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/lib/docter/page.rb (added)
+++ incubator/buildr/docter/trunk/lib/docter/page.rb Tue Nov 13 15:49:20 2007
@@ -0,0 +1,288 @@
+module Docter
+
+  # A single documentation page. Has title, content and ToC.
+  #
+  # The content is HTML without the H1 header or HEAD element, ripe for including inside the template.
+  # The title is HTML-encoded text, the ToC is created from H2/H3 elements.
+  #
+  # The content is transformed in three stages:
+  # # Transform from the original format (e.g. Textile, plain text) to HTML.
+  # # Parse the HTML to extract the body, title and ToC. The content comes from the body, less any
+  #   H1 element used for the title.
+  # # Apply filters each time the content is retrieved (form #content).
+  #
+  # Supported input formats include:
+  # * :plain -- Plain text, rendered as pre-formatted (pre).
+  # * :html -- The HTML body is extracted as the content, see below for ERB, title and ToC.
+  # * :textile -- Converted to HTML using RedCloth. See below for ERB, code blocks, title and ToC.
+  # * :markdown -- Converted to HTML using RedCloth. See below for ERB, code blocks, title and ToC.
+  #
+  # *EBR* To support dynamic content some formats are run through ERB first. You can use ERB to construct
+  # HTML, Textile, Markdown or content in any format the page is using. This happens before the content
+  # is converted to HTML.
+  #
+  # *Code blocks* Textile and Markdown support code blocks with syntax highlighting. To create a code block:
+  #   {{{!lang
+  #     ...
+  #   }}}
+  # You can use !lang to specify a language for syntax highlighting, e.g. !ruby, !sql, !sh. See Syntax
+  # for more information. The language is optional, code blocks without it are treated as plain text.
+  # You can also use syntax highlighting from HTML by specifying the class attribute on the pre element.
+  #
+  # *Title* The recommended way to specify the page title is using an H1 header. Only one H1 header is allowed,
+  # and that element is removed from the content. Alternatively, you can also use the TITLE element, if both
+  # TITLE and H1 are used, they must match.
+  #
+  # If none of these options are available (e.g. for :plain) the title comes from the filename, treating
+  # underscore as space and capitalizing first letter, e.g. change_log.txt becomes "Change Log".
+  #
+  # *ToC* The table of contents is constructed from H2 and H3 headers. H2 headers provide the top-level sections,
+  # and H3 headers are nested inside H2 headers.
+  #
+  # The ToC links to each section based on the ID attribute of the header. If the header lacks an ID attribute,
+  # one is created using the header title, for example:
+  #   h2. Getting Started
+  # becomes:
+  #   <h2 id="getting_started">Getting Started</h2>
+  # You can rely on these IDs to link inside the page and across pages.
+  #
+  # *Filters* Runs the default chain of filters, or those specified by the :filters option. See Filter
+  # for more information. Filters are typically used to do post-processing on the HTML, e.g. syntax highlighting,
+  # URL rewriting.
+  class Page < Resource::Base
+
+    # ToC entry for a page. Obtains title and URL from the page, includes entries from the page
+    # ToC and can return the page itself.
+    class ToCEntryForPage < ToCEntry #:nodoc:
+
+      def initialize(page)
+        @page = page
+      end
+
+      def title()
+        @page.title
+      end
+
+      def url()
+        @page.path
+      end
+
+      def entries()
+        @page.toc.entries
+      end
+
+    end
+
+    # :call-seq:
+    #   title() => string
+    #
+    # Returns the page title.
+    def title()
+      load
+      @title
+    end
+
+    def title=(title)
+      @title = title
+    end
+
+    # :call-seq:
+    #   content() => string
+    #
+    # Returns the page content (HTML).
+    def content()
+      load
+      Filter.process(@content)
+    end
+
+    # :call-seq:
+    #   toc() => ToC
+    #
+    # Returns the table of contents.
+    def toc()
+      load
+      @toc
+    end
+
+    # :call-seq:
+    #   path() => filename
+    #
+    # Returns the path for this page. You can use this to link to the page from any other page.
+    #
+    # For example, if the page name is "intro.textile" the path will be "intro.html".
+    def path()
+      @path ||= File.basename(@filename).downcase.ext(".html")
+    end
+
+    # :call-seq;
+    #   id() => string
+    #
+    # Returns fragment identifier for this page.
+    def id()
+      @id ||= title.gsub(/\s+/, "_").downcase
+    end
+
+    def entries() #:nodoc:
+      toc.entries
+    end
+
+    # :call-seq:
+    #   toc_entry() => ToCEntry
+    #
+    # Returns a ToC entry for this page. Uses the +one_page+ argument to determine whether to return
+    # a link to #path of the fragment #id.
+    def toc_entry()
+      @toc_entry ||= ToCEntryForPage.new(self)
+    end
+
+  protected
+
+    def create_from_html(html, options)
+      parse(erb_this(html), options)
+    end
+
+    def create_from_plain(text, options)
+      parse(%{<pre class="text">#{CGI.escapeHTML(text)}</pre>}, options)
+    end
+
+    def create_from_textile(textile, options)
+      parse(use_redcloth(:textile, textile, options), options)
+    end
+
+    def create_from_markdown(markdown, options)
+      parse(use_redcloth(:markdown, markdown, options), options)
+    end
+
+  private
+
+    if defined?(::RedCloth)
+      # :call-seq:
+      #   use_redcloth(format, text, options)
+      #
+      # Format may be :textile or :markdown. Runs erb_this on the text first to apply ERB code,
+      # processes code sections ({{{ ... }}}), and converts the Textile/Markdown text to HTML.
+      def use_redcloth(format, text, options)
+        text = erb_this(text)
+        # Process {{{ ... }}} code sections into pre tags.
+        text = text.gsub(/^\{\{\{([^\n]*)\n(.*?)\n\}\}\}/m) do
+          code, spec = $2, $1.scan(/^!(.*?)$/).to_s.strip
+          %{<notextile><pre class="#{spec.split(",").join(" ")}">#{CGI.escapeHTML(code)}</pre></notextile>}
+        end
+        # Create the HTML.
+        RedCloth.new(text, [:no_span_caps]).to_html(format)
+      end
+    else
+      def use_redcloth(format, text, options)
+        fail "You need to install RedCloth first:\n  gem install RedCloth"
+      end
+    end
+
+    # :call-seq:
+    #   parse(html, options)
+    #
+    # Parses HTML into the content, title and ToC. This method can take an HTML document and will extract
+    # its body. It can deduct the title from the H1 element, TITLE element or :title option, or filename.
+    def parse(html, options)
+      # Get the body (in most cases it's just the page). Make sure when we wreck havoc on the HTML,
+      # we're not changing any content passed to us.
+      body = html[regexp_element("body")] ? $2 : html.clone
+
+      # The correct structure is to use H1 for the document title (but TITLE element will also work).
+      # If both are used, they must both match. Two or more H1 is a sign you're using H1 instead of H2.
+      title = html.scan(regexp_element("title|h1")).map{ |parts| inner_text_from(parts.last) }.uniq
+      raise ArgumentError, "A page can only have one title, you can use the H1 element (preferred) or TITLE element, or both if they're the same. If you want to title sections, please use the H2 element" if title.size > 1
+      # Lacking that, we need to derive the title somehow.
+      title = title.first || options[:title] || (filename && filename.pathmap("%n").gsub("_", " ").capitalize) || "Untitled"
+      # Get rid of H1 header.
+      body.gsub!(regexp_element("h1"), "")
+
+      # Catalog all the major sections, based on the H2/H3 headers.
+      toc = ToC.new
+      body.gsub!(regexp_element("h[23]")) do |header|
+        tag, attributes, text = $1.downcase, $2.to_s, inner_text_from($3)
+        # Make sure all H2/H3 headers have a usable ID, create once if necessary.
+        id = CGI.unescape($3) if attributes[regexp_attribute("id")]
+        if id.blank?
+          id = CGI.unescapeHTML(text.downcase.gsub(" ", "_"))
+          header = %{<#{tag} #{attributes} id="#{id}">#{text}</#{tag}>}
+        end
+        if tag == "h2"
+          toc.add "##{id}", text
+        else
+          fail ArgumentError, "H3 section found without any H2 section." unless toc.last 
+          toc.last.add "##{id}", text
+        end
+        header
+      end
+      @content, @title, @toc = body, title, toc
+    end
+
+  end
+
+
+  # Filters are used to process HTML before rendering, e.g to apply syntax highlighting, URL rewriting.
+  # To add a new filter:
+  #   filter_for(:upcase) { |html| html.upcase }
+  module Filter
+
+    class << self
+
+      # :call-seq:
+      #   list() => names
+      #
+      # Return the names of all defined filters.
+      def list()
+        @filters.keys
+      end
+
+      # :call-seq:
+      #   filter_for(name) { |html| ... }
+      #
+      # Defines a filter for +name+ using a block that will transform the HTML. 
+      def filter_for(name, &block)
+        @filters[name.to_sym] = block
+        self
+      end
+
+      # :call-seq:
+      #   process(html) => html
+      #   process(html, *name) => html
+      #
+      # Process the HTML using the available filters and returns the resulting HTML.
+      # The second form uses only the selected filters.
+      def process(html, *using)
+        using = using.flatten.compact
+        (using.empty? ? @filters.values : @filters.values_at(*using)).
+          inject(html) { |html, filter| filter.call(html) }
+      end
+
+    end
+
+    @filters = {}
+
+  end
+
+
+  class << self
+
+    # :call-seq:
+    #   filter_for(name) { |html| ... }
+    #
+    # Defines a filter for +name+ using a block that will transform the HTML. 
+    def filter_for(name, &block)
+      Filter.filter_for(name, &block)
+    end
+
+    # :call-seq:
+    #   page(filename, options?)
+    #   page(format, content, options?)
+    #
+    # The first form loads the page from the specified filename. The second creates the page from
+    # the content string based on the specified format.
+    def page(*args)
+      Page.new(*args)
+    end
+
+  end
+
+end

Added: incubator/buildr/docter/trunk/lib/docter/rake.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/lib/docter/rake.rb?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/lib/docter/rake.rb (added)
+++ incubator/buildr/docter/trunk/lib/docter/rake.rb Tue Nov 13 15:49:20 2007
@@ -0,0 +1,25 @@
+module Docter
+  module Rake
+
+    class << self
+
+      def generate(target, collection, template, *args)
+        options = Hash === args.last ? args.pop.clone : {}
+        args.each { |arg| options[arg.to_sym] = true }
+        file target=>collection.dependencies + template.dependencies do |task|
+          collection.generate template, task.name, options[:one_page] ? :one_page : :all, options
+        end
+      end
+
+      def serve(task_name, collection, template, *args)
+        options = Hash === args.last ? args.pop.clone : {}
+        args.each { |arg| options[arg.to_sym] = true }
+        task task_name do
+          collection.serve template, options
+        end
+      end
+
+    end
+
+  end
+end

Added: incubator/buildr/docter/trunk/lib/docter/server.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/lib/docter/server.rb?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/lib/docter/server.rb (added)
+++ incubator/buildr/docter/trunk/lib/docter/server.rb Tue Nov 13 15:49:20 2007
@@ -0,0 +1,109 @@
+module Docter
+
+  class MongrelHandler < Mongrel::HttpHandler
+
+    attr_reader :collection, :template, :options
+
+    def initialize(collection, template, options = {})
+      @collection, @template, @options = collection, template, options || {}
+    end
+
+    def resources()
+      @resources ||= Resources.new
+    end
+
+    def process(request, response)
+      # Absolute path to relative path, default index.
+      path = request.params[Mongrel::Const::PATH_INFO].sub(/^\//, "")
+      path = "index.html" if path.empty?
+
+      begin
+        if file = template.find(path)
+          # Files served directly from disk (CSS, images, RDoc, etc).
+          since = request.params[Mongrel::Const::HTTP_IF_MODIFIED_SINCE]
+          if since && Time.parse(since) >= File.stat(file).mtime
+            response.status = 304
+            response.finished
+          else
+            puts "Serving #{path}" if verbose
+            response.start(200) do |head,out|
+              head[Mongrel::Const::LAST_MODIFIED] = CGI.rfc1123_date(File.stat(file).mtime)
+              out.write File.read(file)
+            end
+          end
+        elsif options[:one_page] && path == "index.html"
+          puts "Serving #{path}" if verbose
+          response.start(200) do |head,out|
+            head[Mongrel::Const::CONTENT_TYPE] = "text/html"
+            out.write collection.render(template, options)
+          end
+        elsif page = collection.page(path)
+          puts "Serving #{path}" if verbose
+          response.start(200) do |head,out|
+            head[Mongrel::Const::CONTENT_TYPE] = "text/html"
+            out.write collection.render(template, page, options)
+          end
+        else
+          response.start 404 do |head, out|
+            head[Mongrel::Const::CONTENT_TYPE] = "text/html"
+            out.write "<h1>Did you accidentally rm #{path}, or did you forget to :w it?</h1>"
+          end
+        end
+      rescue Exception=>error
+        response.start(500) do |head, out|
+          head["Content-Type"] = "text/plain"
+          error = ["#{error.class}: #{error}", error.backtrace.join("\n")]
+          out.puts *error
+          puts *error
+        end
+      end
+    end
+
+  end
+
+
+  class Server
+
+    PORT = 3000
+
+    attr_reader :collection, :template
+    attr_accessor :port, :options
+
+    def initialize(collection, template, *args)
+      @collection, @template = collection, template
+      @options = Hash === args.last ? args.pop.clone : {}
+      args.each { |arg| @options[arg.to_sym] = true }
+      @port = @options[:port] || PORT
+    end
+
+    def start(wait = true)
+      puts "Starting Mongrel on port #{port}"
+      @mongrel = Mongrel::HttpServer.new("0.0.0.0", port, 4)
+      @mongrel.register("/", MongrelHandler.new(collection, template, options))
+      if wait
+        @mongrel.run.join rescue nil
+      else
+        @mongrel.run
+      end
+    end
+
+    def stop()
+      puts "Stopping Mongrel"
+      @mongrel.stop if @mongrel
+    end
+
+  end
+
+
+  class Collection
+
+    def serve(template, *args)
+      options = Hash === args.last ? args.pop.clone : {}
+      options[:port] = args.shift if Integer === args.first
+      args.each { |arg| options[arg.to_sym] = true }
+      Server.new(self, template, options).start
+    end
+
+  end
+
+end

Added: incubator/buildr/docter/trunk/lib/docter/template.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/lib/docter/template.rb?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/lib/docter/template.rb (added)
+++ incubator/buildr/docter/trunk/lib/docter/template.rb Tue Nov 13 15:49:20 2007
@@ -0,0 +1,199 @@
+module Docter
+
+  # A template for formatting pages. The template is parsed once and processed using each Page to
+  # produce an HTML document. Processing can rely on various attributes of the Scope.
+  #
+  # A template will require additional files like CSS stylesheets, images, JavaScript, etc.
+  # You can associate additional resources with the template using #include and #exclude.
+  # These resources are copied to the destination directory when generating output, and
+  # served from the integrated Web server.
+  class Template < Resource::Base
+
+    module ContextMethods
+
+      include HTML
+
+      def collect_links(content, mark = false)
+        @links ||= []
+        content.gsub(regexp_element("a")) do |link|
+          url = $3 if link =~ regexp_attribute("href")
+          if url =~ /^\w+:/
+            unless index = @links.index(url)
+              index = @links.size
+              @links << [url, inner_text_from(link)]
+            end
+            mark ? "#{link}<sup>[#{index + 1}]</sup>" : link
+          else
+            link
+          end
+        end
+      end
+
+      def list_links(cls = nil)
+        # Remove duplicate links (same URL), sort by text and convert into DT/DD pairs.
+        links = @links.inject({}) { |hash, link| hash[link.first] ||= link.last ; hash }.
+          sort { |a,b| a.last <=> b.last }.
+          map { |url, text| %{<dt>#{text.capitalize}</dt><dd><a href="#{url}">#{url}</a></dd>} }
+        %{<dl class="#{cls}">#{links.join}</dl>}
+      end
+
+      def renumber_footnotes(html)
+        @footnote ||= 0
+        html.gsub(/<a href="#fn(\d+)">\1<\/a>/) {
+          # Renumber footnote references starting from the last footnote number.
+          fn = $1.to_i + @footnote
+           %{<a href="#fn#{fn}">#{fn}</a>}
+        }.gsub(/<p id="fn(\d+)"(.*?)><sup>\1<\/sup>(.*?)<\/p>/m) {
+          # Renumber footnotes the same way, and update the last footnote number.
+          @footnote += 1
+          %{<p id="fn#{@footnote}"#{$2}><sup>#{@footnote}</sup>#{$3}</p>}
+        }
+      end
+
+    end
+
+    # Options passed when creating the template.
+    attr_reader :options
+
+    def initialize(*args)
+      super
+      @sources = FileList[]
+    end
+
+    # :call-seq:
+    #   render(context) => html
+    #
+    # Renders this template. The template is processed using a context that provides the template
+    # with access to various methods and variables, e.g. the page being rendered, or the ToC.
+    #
+    # There are two ways to supply a context:
+    # * Hash -- Each key becomes a method you can call on the hash to obtain it's value.
+    #   The hash will include a method called template that returns the template itself.
+    # * Object -- Creates a context object that delegates all method calls to this object,
+    #   and adds the method template that returns the template itself. 
+    def render(context)
+      load
+      if Hash === context
+        hash = context.merge(:template=>self)
+        struct = Struct.new(*hash.keys).new(*hash.values)
+        struct.class.send :include, ContextMethods
+        @process.call struct
+      else
+        delegate = Class.new
+        context.class.instance_methods.each { |method| delegate.send :define_method, method, &context.method(method) }
+        context.class.send :include, ContextMethods
+        delegate.send(:define_method, :template) { self }
+        @process.call delegate.new
+      end
+    end
+
+    # :call-seq:
+    #   include(*paths) => self
+    #
+    # Adds files and directories included in the generated output.
+    def include(*paths)
+      @sources.include *paths.flatten
+      self
+    end
+
+    alias :add :include
+
+    # :call-seq:
+    #   exclude(*paths) => self
+    #
+    # Excludes files or directories from the generated output.
+    def exclude(*paths)
+      @sources.exclude *paths.flatten
+      self
+    end
+
+    # :call-seq:
+    #   find(path) => file
+    #
+    # Returns the location of a file on disk based on the request path.
+    #
+    # For example:
+    #   template.include("html/index.html", "images", "css/*")
+    #   map.find("index.html") => "html/index.html"
+    #   map.find("images/logo.png") => "images/logo.png"
+    #   map.find("fancy.css") => "css/fancy.css"
+    def find(path)
+      @sources.inject(nil) do |found, file|
+        break found if found
+        if File.directory?(file)
+          base = File.dirname(file) + "/"
+          FileList["#{file}/**/*"].find { |file| file.sub(base, "") == path }
+        else
+          file if File.basename(file) == path
+        end 
+      end
+    end
+
+    # :call-seq:
+    #   copy_resources(to_dir)
+    #
+    # Copy resource files to the destination directory.
+    def copy_resources(to_dir)
+      mkpath to_dir
+      @sources.each do |file|
+        if File.directory?(file)
+          base = File.dirname(file) + "/"
+          FileList[File.join(file, "**/*")].each do |file|
+            target = File.join(to_dir, file.sub(base, ""))
+            mkpath File.dirname(target)
+            cp file, target
+          end
+        else
+          cp file, to_dir
+        end 
+      end
+      touch to_dir # For Rake dependency management.
+    end
+
+    # :call-seq:
+    #
+    # Returns a list of dependencies (resource files, the template file, etc). Useful when creating
+    # a Rake task based on this template.
+    def dependencies()
+      @sources.map { |path| File.directory?(path) ? FileList[path, File.join(path, "**/*")] : path }.flatten +
+        [@filename].compact
+    end
+
+  protected
+
+    if defined?(::Haml)
+      def create_from_haml(content, options)
+        @options = options
+        template = Haml::Engine.new(content, :filename=>@filename)
+        @process = lambda { |context| template.render(context) }
+      end
+    else
+      def create_from_haml(content, options)
+        fail "You need to install HAML first:\n  gem install haml"
+      end
+    end
+
+    def create_from_erb(content, options)
+      @options = options
+      template = ERB.new(content)
+      @process = lambda { |context| template.result(context.instance_eval { binding }) }
+    end
+
+  end
+
+
+  class << self
+
+    # :call-seq:
+    #   template(filename, options?)
+    #   template(format, content, options?)
+    #
+    # The first form loads the template from the specified filename. The second creates the template from
+    # the content string based on the specified format.
+    def template(*args)
+      Template.new(*args)
+    end
+
+  end
+
+end

Added: incubator/buildr/docter/trunk/lib/docter/ultraviolet.rb
URL: http://svn.apache.org/viewvc/incubator/buildr/docter/trunk/lib/docter/ultraviolet.rb?rev=594723&view=auto
==============================================================================
--- incubator/buildr/docter/trunk/lib/docter/ultraviolet.rb (added)
+++ incubator/buildr/docter/trunk/lib/docter/ultraviolet.rb Tue Nov 13 15:49:20 2007
@@ -0,0 +1,28 @@
+require "uv" # gem install ultraviolet
+
+module Docter
+
+  SYNTAX_THEME = "eiffel"
+  SYNTAX_STYLESHEET = "css/#{SYNTAX_THEME}.css"
+  SYNTAX_MAP = { "sh"=>"shell-unix-generic" }
+
+  filter_for :syntax do |html|
+    html.gsub(HTML.regexp_element("pre")) do |pre|
+      attributes, code = $2, $3
+      if attributes[HTML.regexp_attribute("class")]
+        classes = $3.split(/\s+/)
+        lang = classes.first
+      end
+      if lang == "text"
+        Uv.parse(CGI.unescapeHTML(code), "xhtml", "plain_text", false, SYNTAX_THEME).
+          gsub(URI.regexp) { |uri| uri =~ /^http(s?):\/\// ? %{<a href="#{uri}">#{uri}</a>} : uri }
+      elsif lang
+        syntax = SYNTAX_MAP[lang] || (Uv.syntaxes.include?(lang) ? lang : "plain_text")
+        Uv.parse(CGI.unescapeHTML(code), "xhtml", syntax || "plain_text", classes.include?("lines"), SYNTAX_THEME)
+      else
+        Uv.parse(CGI.unescapeHTML(code), "xhtml", "plain_text", false, SYNTAX_THEME)
+      end
+    end
+  end
+
+end