diff --git a/index.html b/index.html index ad58300..d28252b 100644 --- a/index.html +++ b/index.html @@ -3,11 +3,21 @@ rocco.rb - +
+
+ Jump To … + +
@@ -18,8 +28,8 @@ - + @@ -65,8 +75,8 @@ for a set of Ruby source files:

+ + + + @@ -119,36 +140,146 @@ program on PATH.

- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -156,10 +287,21 @@ file is read to retrieve data.

  attr_reader :file
- + + + + + - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - + - + - + + + + + + + + + + + + + - + - - - - - + -
-
- # +
+

Rocco is a Ruby port of Docco, the quick-and-dirty, hundred-line-long, literate-programming-style documentation generator.

@@ -52,10 +62,10 @@ for a set of Ruby source files:

-
- # +
+

Prerequisites

-
- # +
+

We’ll need a Markdown library. RDiscount, if we’re lucky. Otherwise, issue a warning and fall back on using BlueCloth.

@@ -75,7 +85,7 @@ issue a warning and fall back on using BlueCloth.

begin
   require 'rdiscount'
 rescue LoadError => boom
-  warn "warn: #{boom}. trying bluecloth"
+  warn "WARNING: #{boom}. Trying bluecloth."
   require 'bluecloth'
   Markdown = BlueCloth
 end
@@ -83,8 +93,8 @@ issue a warning and fall back on using BlueCloth.

-
- # +
+

We use {{ mustache }} for HTML templating.

@@ -95,23 +105,34 @@ HTML templating.

-
- # +
+
-

Code is run through Pygments for syntax -highlighting. Fail fast right here if we can’t find the pygmentize -program on PATH.

+

We use Net::HTTP to highlight code via http://pygments.appspot.com

-
if ! ENV['PATH'].split(':').any? { |dir| File.exist?("#{dir}/pygmentize") }
-  fail "Pygments is required for syntax highlighting"
-end
+
require 'net/http'
-
- # +
+ +
+

Code is run through Pygments for syntax +highlighting. If it’s not installed, locally, use a webservice.

+
+
include FileTest
+if !ENV['PATH'].split(':').any? { |dir| executable?("#{dir}/pygmentize") }
+  warn "WARNING: Pygments not found. Using webservice."
+end
+
+
+

Public Interface

-
- # +
+
-

Rocco.new takes a source filename and an optional block. -When block is given, it must read the contents of the file using -whatever means necessary and return it as a string. With no block, the -file is read to retrieve data.

+

Rocco.new takes a source filename, an optional list of source filenames +for other documentation sources, an options hash, and an optional block. +The options hash respects three members:

+ +
    +
  • :language: specifies which Pygments lexer to use if one can’t be +auto-detected from the filename. Defaults to ruby.

  • +
  • :comment_chars, which specifies the comment characters of the +target language. Defaults to #.

  • +
  • :template_file, which specifies a external template file to use +when rendering the final, highlighted file via Mustache. Defaults +to nil (that is, Mustache will use ./lib/rocco/layout.mustache).

  • +
class Rocco
-  VERSION = '0.2'
+  VERSION = '0.6'
 
-  def initialize(filename, &block)
-    @file = filename
-    @data =
+  def initialize(filename, sources=[], options={}, &block)
+    @file       = filename
+    @sources    = sources
+
+
+ +
+

When block is given, it must read the contents of the file using +whatever means necessary and return it as a string. With no block, +the file is read to retrieve data.

+
+
    @data =
       if block_given?
         yield
       else
         File.read(filename)
       end
-    @sections = highlight(split(parse(@data)))
+
+    defaults = {
+      :language      => 'ruby',
+      :comment_chars => '#',
+      :template_file => nil
+    }
+    @options = defaults.merge(options)
+
+
+ +
+

If we detect a language

+
+
    if detect_language() != "text"
+
+
+ +
+

then assign the detected language to :language, and look for +comment characters based on that language

+
+
      @options[:language] = detect_language()
+      @options[:comment_chars] = generate_comment_chars()
+
+
+ +
+

If we didn’t detect a language, but the user provided one, use it +to look around for comment characters to override the default.

+
+
    elsif @options[:language] != defaults[:language]
+      @options[:comment_chars] = generate_comment_chars()
+
+
+ +
+

If neither is true, then convert the default comment character string +into the comment_char syntax (we’ll discuss that syntax in detail when +we get to generate_comment_chars() in a moment.

+
+
    else
+      @options[:comment_chars] = {
+        :single => @options[:comment_chars],
+        :multi => nil
+      }
+    end
+
+
+ +
+

Turn :comment_chars into a regex matching a series of spaces, the +:comment_chars string, and the an optional space. We’ll use that +to detect single-line comments.

+
+
    @comment_pattern =
+      Regexp.new("^\\s*#{@options[:comment_chars][:single]}\s?")
+
+
+ +
+

parse() the file contents stored in @data. Run the result through +split() and that result through highlight() to generate the final +section list.

+
+
    @sections = highlight(split(parse(@data)))
   end
-
- # +
+

The filename as given to Rocco.new.

-
- # +
+ +
+

The merged options array

+
+
  attr_reader :options
+
+
+

A list of two-tuples representing each section of the source file. Each item in the list has the form: [docs_html, code_html], where both @@ -170,65 +312,329 @@ respectively.

  attr_reader :sections
-
- # +
+ +
+

A list of all source filenames included in the documentation set. Useful +for building an index of other files.

+
+
  attr_reader :sources
+
+
+

Generate HTML output for the entire document.

  require 'rocco/layout'
   def to_html
-    Rocco::Layout.new(self).render
+    Rocco::Layout.new(self, @options[:template_file]).render
   end
-
- # +
+
-

Internal Parsing and Highlighting

+

Helper Functions

-
- # +
+ +
+

Returns true if pygmentize is available locally, false otherwise.

+
+
  def pygmentize?
+    @_pygmentize ||= ENV['PATH'].split(':').
+      any? { |dir| executable?("#{dir}/pygmentize") }
+  end
+
+
+ +
+

If pygmentize is available, we can use it to autodetect a file’s +language based on its filename. Filenames without extensions, or with +extensions that pygmentize doesn’t understand will return text. +We’ll also return text if pygmentize isn’t available.

+ +

We’ll memoize the result, as we’ll call this a few times.

+
+
  def detect_language
+    @_language ||=
+      if pygmentize?
+        %x[pygmentize -N #{@file}].strip!
+      else
+        "text"
+      end
+  end
+
+
+ +
+

Given a file’s language, we should be able to autopopulate the +comment_chars variables for single-line comments. If we don’t +have comment characters on record for a given language, we’ll +use the user-provided :comment_char option (which defaults to +#).

+ +

Comment characters are listed as:

+ +
{ :single       => "//",
+  :multi_start  => "/**",
+  :multi_middle => "*",
+  :multi_end    => "*/" }
+
+ +

:single denotes the leading character of a single-line comment. +:multi_start denotes the string that should appear alone on a +line of code to begin a block of documentation. :multi_middle +denotes the leading character of block comment content, and +:multi_end is the string that ought appear alone on a line to +close a block of documentation. That is:

+ +
/**                 [:multi][:start]
+ *                  [:multi][:middle]
+ ...
+ *                  [:multi][:middle]
+ */                 [:multi][:end]
+
+ +

If a language only has one type of comment, the missing type +should be assigned nil.

+ +

At the moment, we’re only returning :single. Consider this +groundwork for block comment parsing.

+
+
  COMMENT_STYLES  = {
+    "bash"          =>  { :single => "#", :multi => nil },
+    "c"             =>  {
+      :single => "//",
+      :multi  => { :start => "/**", :middle => "*", :end => "*/" }
+    },
+    "coffee-script" =>  {
+      :single => "#",
+      :multi  => { :start => "###", :middle => nil, :end => "###" }
+    },
+    "cpp" =>  {
+      :single => "//",
+      :multi  => { :start => "/**", :middle => "*", :end => "*/" }
+    },
+    "css"           =>  {
+      :single => nil,
+      :multi  => { :start => "/**", :middle => "*", :end => "*/" }
+    },
+    "java"          =>  {
+      :single => "//",
+      :multi  => { :start => "/**", :middle => "*", :end => "*/" }
+    },
+    "js"            =>  {
+      :single => "//",
+      :multi  => { :start => "/**", :middle => "*", :end => "*/" }
+    },
+    "lua"           =>  {
+      :single => "--",
+      :multi => nil
+    },
+    "python"        =>  {
+      :single => "#",
+      :multi  => { :start => '"""', :middle => nil, :end => '"""' }
+    },
+    "rb"            =>  {
+      :single => "#",
+      :multi  => { :start => '=begin', :middle => nil, :end => '=end' }
+    },
+    "scheme"        =>  { :single => ";;",  :multi => nil },
+  }
+
+  def generate_comment_chars
+    @_commentchar ||=
+      if COMMENT_STYLES[@options[:language]]
+        COMMENT_STYLES[@options[:language]]
+      else
+        { :single => @options[:comment_chars], :multi => nil }
+      end
+  end
+
+
+ +
+

Internal Parsing and Highlighting

+
+
+
+
+

Parse the raw file data into a list of two-tuples. Each tuple has the form [docs, code] where both elements are arrays containing the -raw lines parsed from the input file.

+raw lines parsed from the input file, comment characters stripped.

  def parse(data)
     sections = []
     docs, code = [], []
-    data.split("\n").each do |line|
-      case line
-      when /^\s*#/
-        if code.any?
-          sections << [docs, code]
-          docs, code = [], []
+    lines = data.split("\n")
+
+
+ +
+

The first line is ignored if it is a shebang line. We also ignore the +PEP 263 encoding information in python sourcefiles, and the similar ruby +1.9 syntax.

+
+
    lines.shift if lines[0] =~ /^\#\!/
+    lines.shift if lines[0] =~ /coding[:=]\s*[-\w.]+/ &&
+                   [ "python", "rb" ].include?(@options[:language])
+
+
+ +
+

To detect both block comments and single-line comments, we’ll set +up a tiny state machine, and loop through each line of the file. +This requires an in_comment_block boolean, and a few regular +expressions for line tests.

+
+
    in_comment_block = false
+    single_line_comment, block_comment_start, block_comment_mid, block_comment_end =
+      nil, nil, nil, nil
+    if not @options[:comment_chars][:single].nil?
+      single_line_comment = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:single])}\\s?")
+    end
+    if not @options[:comment_chars][:multi].nil?
+      block_comment_start = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*$")
+      block_comment_end   = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
+      if @options[:comment_chars][:multi][:middle]
+        block_comment_mid = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:middle])}\\s?")
+      end
+    end
+    lines.each do |line|
+
+
+ +
+

If we’re currently in a comment block, check whether the line matches +the end of a comment block.

+
+
      if in_comment_block
+        if block_comment_end && line.match( block_comment_end )
+          in_comment_block = false
+        else
+          docs << line.sub( block_comment_mid || '', '' )
+        end
+
+
+ +
+

Otherwise, check whether the line matches the beginning of a block, or +a single-line comment all on it’s lonesome. In either case, if there’s +code, start a new section

+
+
      else
+        if block_comment_start && line.match( block_comment_start )
+          in_comment_block = true
+          if code.any?
+            sections << [docs, code]
+            docs, code = [], []
+          end
+        elsif single_line_comment && line.match( single_line_comment )
+          if code.any?
+            sections << [docs, code]
+            docs, code = [], []
+          end
+          docs << line.sub( single_line_comment || '', '' )
+        else
+          code << line
         end
-        docs << line
-      else
-        code << line
       end
     end
     sections << [docs, code] if docs.any? || code.any?
-    sections
+    normalize_leading_spaces( sections )
   end
-
- # +
+ +
+

Normalizes documentation whitespace by checking for leading whitespace, +removing it, and then removing the same amount of whitespace from each +succeeding line. That is:

+ +
def func():
+  """
+    Comment 1
+    Comment 2
+  """
+  print "omg!"
+
+ +

should yield a comment block of Comment 1\nComment 2 and code of +def func():\n print "omg!"

+
+
  def normalize_leading_spaces( sections )
+    sections.map do |section|
+      if section.any? && section[0].any?
+        leading_space = section[0][0].match( "^\s+" )
+        if leading_space
+          section[0] =
+            section[0].map{ |line| line.sub( /^#{leading_space.to_s}/, '' ) }
+        end
+      end
+      section
+    end
+  end
+
+
+

Take the list of paired sections two-tuples and split into two separate lists: one holding the comments with leaders removed and @@ -238,17 +644,20 @@ one with the code blocks.

  def split(sections)
     docs_blocks, code_blocks = [], []
     sections.each do |docs,code|
-      docs_blocks << docs.map { |line| line.sub(/^\s*#\s?/, '') }.join("\n")
-      code_blocks << code.join("\n")
+      docs_blocks << docs.join("\n")
+      code_blocks << code.map do |line|
+        tabs = line.match(/^(\t+)/)
+        tabs ? line.sub(/^\t+/, '  ' * tabs.captures[0].length) : line
+      end.join("\n")
     end
     [docs_blocks, code_blocks]
   end
-
- # +
+

Take the result of split and apply Markdown formatting to comments and syntax highlighting to source code.

@@ -258,10 +667,10 @@ syntax highlighting to source code.

docs_blocks, code_blocks = blocks
-
- # +
+

Combine all docs blocks into a single big markdown document with section dividers and run through the Markdown processor. Then split it back out @@ -274,22 +683,97 @@ into separate sections.

split(/\n*<h5>DIVIDER<\/h5>\n*/m)
-
- # +
+
-

Combine all code blocks into a single big stream and run through -Pygments. We popen a read/write pygmentize process in the parent and +

Combine all code blocks into a single big stream with section dividers and +run through either pygmentize(1) or http://pygments.appspot.com

+
+
    span, espan = '<span class="c.?">', '</span>'
+    if @options[:comment_chars][:single]
+      front = @options[:comment_chars][:single]
+      divider_input  = "\n\n#{front} DIVIDER\n\n"
+      divider_output = Regexp.new(
+        [ "\\n*",
+          span,
+          Regexp.escape(front),
+          ' DIVIDER',
+          espan,
+          "\\n*"
+        ].join, Regexp::MULTILINE
+      )
+    else
+      front = @options[:comment_chars][:multi][:start]
+      back  = @options[:comment_chars][:multi][:end]
+      divider_input  = "\n\n#{front}\nDIVIDER\n#{back}\n\n"
+      divider_output = Regexp.new(
+        [ "\\n*",
+          span, Regexp.escape(front), espan,
+          "\\n",
+          span, "DIVIDER", espan,
+          "\\n",
+          span, Regexp.escape(back), espan,
+          "\\n*"
+        ].join, Regexp::MULTILINE
+      )
+    end
+
+    code_stream = code_blocks.join( divider_input )
+
+    code_html =
+      if pygmentize?
+        highlight_pygmentize(code_stream)
+      else
+        highlight_webservice(code_stream)
+      end
+
+
+ +
+

Do some post-processing on the pygments output to split things back +into sections and remove partial <pre> blocks.

+
+
    code_html = code_html.
+      split(divider_output).
+      map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
+      map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') }
+
+
+ +
+

Lastly, combine the docs and code lists back into a list of two-tuples.

+
+
    docs_html.zip(code_html)
+  end
+
+
+ +
+

We popen a read/write pygmentize process in the parent and then fork off a child process to write the input.

-
    code_html = nil
-    open("|pygmentize -l ruby -f html", 'r+') do |fd|
+        
  def highlight_pygmentize(code)
+    code_html = nil
+    open("|pygmentize -l #{@options[:language]} -O encoding=utf-8 -f html", 'r+') do |fd|
       pid =
         fork {
           fd.close_read
-          fd.write code_blocks.join("\n\n# DIVIDER\n\n")
+          fd.write code
           fd.close_write
           exit!
         }
@@ -297,41 +781,34 @@ then fork off a child process to write the input.

code_html = fd.read fd.close_read Process.wait(pid) - end
+ end + + code_html + end
-
- # +
+
-

Do some post-processing on the pygments output to split things back -into sections and remove partial <pre> blocks.

+

Pygments is not one of those things that’s trivial for a ruby user to install, +so we’ll fall back on a webservice to highlight the code if it isn’t available.

-
    code_html = code_html.
-      split(/\n*<span class="c1"># DIVIDER<\/span>\n*/m).
-      map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
-      map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') }
-
-
- # -
-

Lastly, combine the docs and code lists back into a list of two-tuples.

-
-
    docs_html.zip(code_html)
+        
  def highlight_webservice(code)
+    Net::HTTP.post_form(
+      URI.parse('http://pygments.appspot.com/'),
+      {'lang' => @options[:language], 'code' => code}
+    ).body
   end
 end
-
- # +
+

And that’s it.

@@ -340,6 +817,6 @@ into sections and remove partial <pre> blocks.

+
diff --git a/layout.html b/layout.html index e2de612..a8e6d4c 100644 --- a/layout.html +++ b/layout.html @@ -8,6 +8,16 @@
+
+ Jump To … + +
@@ -18,8 +28,8 @@
-
- # +
+
@@ -30,8 +40,11 @@ class Rocco::Layout < Mustache self.template_path = File.dirname(__FILE__) - def initialize(doc) + def initialize(doc, file=nil) @doc = doc + if not file.nil? + Rocco::Layout.template_file = file + end end def title @@ -42,9 +55,29 @@ num = 0 @doc.sections.map do |docs,code| { - :docs => docs, - :code => code, - :num => (num += 1) + :docs => docs, + :docs? => !docs.empty?, + :header? => /^<h.>.+<\/h.>$/.match( docs ), + + :code => code, + :code? => !code.empty?, + + :empty? => ( code.empty? && docs.empty? ), + :num => (num += 1) + } + end + end + + def sources? + @doc.sources.length > 1 + end + + def sources + @doc.sources.sort.map do |source| + { + :path => source, + :basename => File.basename(source), + :url => File.basename(source).split('.')[0..-2].join('.') + '.html' } end end diff --git a/rocco.html b/rocco.html index ad58300..ef132f3 100644 --- a/rocco.html +++ b/rocco.html @@ -8,6 +8,16 @@
+
+ Jump To … + +
@@ -18,8 +28,8 @@ @@ -65,8 +75,8 @@ for a set of Ruby source files:

+ + + + @@ -119,21 +140,26 @@ program on PATH.

- + - + @@ -156,10 +191,10 @@ file is read to retrieve data.

  attr_reader :file
- + - + + + + + + + + + - + @@ -195,22 +254,25 @@ respectively.

- + - + - + - + - + + + + + + + + + + + + + - + - - - - - +
-
- # +
+

Rocco is a Ruby port of Docco, the quick-and-dirty, hundred-line-long, literate-programming-style documentation generator.

@@ -54,8 +64,8 @@ for a set of Ruby source files:

-
- # +
+

Prerequisites

-
- # +
+

We’ll need a Markdown library. RDiscount, if we’re lucky. Otherwise, issue a warning and fall back on using BlueCloth.

@@ -75,7 +85,7 @@ issue a warning and fall back on using BlueCloth.

begin
   require 'rdiscount'
 rescue LoadError => boom
-  warn "warn: #{boom}. trying bluecloth"
+  warn "WARNING: #{boom}. Trying bluecloth."
   require 'bluecloth'
   Markdown = BlueCloth
 end
@@ -83,8 +93,8 @@ issue a warning and fall back on using BlueCloth.

-
- # +
+

We use {{ mustache }} for HTML templating.

@@ -95,23 +105,34 @@ HTML templating.

-
- # +
+
-

Code is run through Pygments for syntax -highlighting. Fail fast right here if we can’t find the pygmentize -program on PATH.

+

We use Net::HTTP to highlight code via http://pygments.appspot.com

-
if ! ENV['PATH'].split(':').any? { |dir| File.exist?("#{dir}/pygmentize") }
-  fail "Pygments is required for syntax highlighting"
-end
+
require 'net/http'
-
- # +
+ +
+

Code is run through Pygments for syntax +highlighting. If it’s not installed, locally, use a webservice.

+
+
include FileTest
+if !ENV['PATH'].split(':').any? { |dir| executable?("#{dir}/pygmentize") }
+  warn "WARNING: Pygments not found. Using webservice."
+end
+
+
+

Public Interface

-
- # +
+
-

Rocco.new takes a source filename and an optional block. -When block is given, it must read the contents of the file using -whatever means necessary and return it as a string. With no block, the -file is read to retrieve data.

+

Rocco.new takes a source filename, an optional list of source filenames +for other documentation sources, an options hash, and an optional block. +The options hash respects two members: :language, which specifies which +Pygments lexer to use; and :comment_chars, which specifies the comment +characters of the target language. The options default to 'ruby' and '#', +respectively. +When block is given, it must read the contents of the file using whatever +means necessary and return it as a string. With no block, the file is read +to retrieve data.

class Rocco
-  VERSION = '0.2'
+  VERSION = '0.5'
 
-  def initialize(filename, &block)
+  def initialize(filename, sources=[], options={}, &block)
     @file = filename
     @data =
       if block_given?
@@ -141,14 +167,23 @@ file is read to retrieve data.

else File.read(filename) end + defaults = { + :language => 'ruby', + :comment_chars => '#', + :template_file => nil + } + @options = defaults.merge(options) + @sources = sources + @comment_pattern = Regexp.new("^\\s*#{@options[:comment_chars]}\s?") + @template_file = @options[:template_file] @sections = highlight(split(parse(@data))) end
-
- # +
+

The filename as given to Rocco.new.

-
- # +
+

A list of two-tuples representing each section of the source file. Each item in the list has the form: [docs_html, code_html], where both @@ -170,24 +205,48 @@ respectively.

  attr_reader :sections
-
- # +
+ +
+

A list of all source filenames included in the documentation set. Useful +for building an index of other files.

+
+
  attr_reader :sources
+
+
+ +
+

An absolute path to a file that ought be used as a template for the +HTML-rendered documentation.

+
+
  attr_reader :template_file
+
+
+

Generate HTML output for the entire document.

  require 'rocco/layout'
   def to_html
-    Rocco::Layout.new(self).render
+    Rocco::Layout.new(self, @template_file).render
   end
-
- # +
+

Internal Parsing and Highlighting

-
- # +
+

Parse the raw file data into a list of two-tuples. Each tuple has the form [docs, code] where both elements are arrays containing the -raw lines parsed from the input file.

+raw lines parsed from the input file. The first line is ignored if it +is a shebang line.

  def parse(data)
     sections = []
     docs, code = [], []
-    data.split("\n").each do |line|
+    lines = data.split("\n")
+    lines.shift if lines[0] =~ /^\#\!/
+    lines.each do |line|
       case line
-      when /^\s*#/
+      when @comment_pattern
         if code.any?
           sections << [docs, code]
           docs, code = [], []
@@ -225,10 +287,10 @@ raw lines parsed from the input file.

end
-
- # +
+

Take the list of paired sections two-tuples and split into two separate lists: one holding the comments with leaders removed and @@ -238,17 +300,20 @@ one with the code blocks.

  def split(sections)
     docs_blocks, code_blocks = [], []
     sections.each do |docs,code|
-      docs_blocks << docs.map { |line| line.sub(/^\s*#\s?/, '') }.join("\n")
-      code_blocks << code.join("\n")
+      docs_blocks << docs.map { |line| line.sub(@comment_pattern, '') }.join("\n")
+      code_blocks << code.map do |line|
+        tabs = line.match(/^(\t+)/)
+        tabs ? line.sub(/^\t+/, '  ' * tabs.captures[0].length) : line
+      end.join("\n")
     end
     [docs_blocks, code_blocks]
   end
-
- # +
+

Take the result of split and apply Markdown formatting to comments and syntax highlighting to source code.

@@ -258,10 +323,10 @@ syntax highlighting to source code.

docs_blocks, code_blocks = blocks
-
- # +
+

Combine all docs blocks into a single big markdown document with section dividers and run through the Markdown processor. Then split it back out @@ -274,22 +339,67 @@ into separate sections.

split(/\n*<h5>DIVIDER<\/h5>\n*/m)
-
- # +
+
-

Combine all code blocks into a single big stream and run through -Pygments. We popen a read/write pygmentize process in the parent and +

Combine all code blocks into a single big stream and run through either +pygmentize(1) or http://pygments.appspot.com

+
+
    code_stream = code_blocks.join("\n\n#{@options[:comment_chars]} DIVIDER\n\n")
+
+    if ENV['PATH'].split(':').any? { |dir| executable?("#{dir}/pygmentize") }
+      code_html = highlight_pygmentize(code_stream)
+    else 
+      code_html = highlight_webservice(code_stream)
+    end
+
+
+ +
+

Do some post-processing on the pygments output to split things back +into sections and remove partial <pre> blocks.

+
+
    code_html = code_html.
+      split(/\n*<span class="c.?">#{@options[:comment_chars]} DIVIDER<\/span>\n*/m).
+      map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
+      map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') }
+
+
+ +
+

Lastly, combine the docs and code lists back into a list of two-tuples.

+
+
    docs_html.zip(code_html)
+  end
+
+
+ +
+

We popen a read/write pygmentize process in the parent and then fork off a child process to write the input.

-
    code_html = nil
-    open("|pygmentize -l ruby -f html", 'r+') do |fd|
+        
  def highlight_pygmentize(code)
+    code_html = nil
+    open("|pygmentize -l #{@options[:language]} -O encoding=utf-8 -f html", 'r+') do |fd|
       pid =
         fork {
           fd.close_read
-          fd.write code_blocks.join("\n\n# DIVIDER\n\n")
+          fd.write code
           fd.close_write
           exit!
         }
@@ -297,41 +407,35 @@ then fork off a child process to write the input.

code_html = fd.read fd.close_read Process.wait(pid) - end
+ end + + code_html + end +
-
- # +
+
-

Do some post-processing on the pygments output to split things back -into sections and remove partial <pre> blocks.

+

Pygments is not one of those things that’s trivial for a ruby user to install, +so we’ll fall back on a webservice to highlight the code if it isn’t available.

-
    code_html = code_html.
-      split(/\n*<span class="c1"># DIVIDER<\/span>\n*/m).
-      map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
-      map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') }
-
-
- # -
-

Lastly, combine the docs and code lists back into a list of two-tuples.

-
-
    docs_html.zip(code_html)
+        
  def highlight_webservice(code)
+    Net::HTTP.post_form(
+      URI.parse('http://pygments.appspot.com/'),
+      {'lang' => @options['language'], 'code' => code}
+    ).body
   end
 end
-
- # +
+

And that’s it.

diff --git a/tasks.html b/tasks.html index 9165939..c412c73 100644 --- a/tasks.html +++ b/tasks.html @@ -8,6 +8,16 @@
+
+ Jump To … + +
@@ -18,8 +28,8 @@ @@ -122,22 +145,22 @@ should be built under, and a source file pattern or file list.

-
- # +
+

Rocco Rake Tasks

@@ -60,6 +70,18 @@ end
Rocco::make 'html/', ['lib/thing.rb', 'lib/thing/*.rb']
 
+ +

Finally, it is also possible to specify which Pygments language you would +like to use to highlight the code, as well as the comment characters for the +language in the options hash:

+ +

Rocco::make ‘html/’, ‘lib/thing/*/.rb’, {

+ +
 :language => 'io',
+ :comment_chars => '#'
+
+ +

}

@@ -67,8 +89,8 @@ end
-
- # +
+

Might be nice to defer this until we actually need to build docs but this will have to do for now.

@@ -79,8 +101,8 @@ will have to do for now.

-
- # +
+

Reopen the Rocco class and add a make class method. This is a simple bit of sugar over Rocco::Task.new. If you want your Rake task to be named @@ -88,31 +110,32 @@ something other than :rocco, you can use Rocco::Task d

class Rocco
-  def self.make(dest='docs/', source_files='lib/**/*.rb')
-    Task.new(:rocco, dest, source_files)
+  def self.make(dest='docs/', source_files='lib/**/*.rb', options={})
+    Task.new(:rocco, dest, source_files, options)
   end
-
- # +
+

Rocco::Task.new takes a task name, the destination directory docs should be built under, and a source file pattern or file list.

  class Task
-    def initialize(task_name, dest='docs/', sources='lib/**/*.rb')
+    def initialize(task_name, dest='docs/', sources='lib/**/*.rb', options={})
       @name = task_name
       @dest = dest[-1] == ?/ ? dest : "#{dest}/"
-      @sources = FileList[sources]
+ @sources = FileList[sources] + @options = options
-
- # +
+

Make sure there’s a directory task defined for our destination.

-
- # +
+

Run over the source file list, constructing destination filenames and defining file tasks.

      @sources.each do |source_file|
-        dest_file = File.basename(source_file, '.rb') + '.html'
+        dest_file = File.basename(source_file).split('.')[0..-2].join('.') + '.html'
         define_file_task source_file, "#{@dest}#{dest_file}"
-
- # +
+

If rake/clean was required, add the generated files to the list. That way all Rocco generated are removed when running rake clean.

@@ -150,8 +173,8 @@ That way all Rocco generated are removed when running rake clean.
-
- # +
+

Define the destination directory task and make the :rocco task depend on it. This causes the destination directory to be created if it doesn’t @@ -166,8 +189,8 @@ already exist.

-
- # +
+

Setup a file task for a single Rocco output file (dest_file). It depends on the source file, the destination directory, and all of Rocco’s @@ -187,7 +210,7 @@ don’t already exist or one of their dependencies was changed.

prerequisites = [@dest, source_file] + rocco_source_files file dest_file => prerequisites do |f| verbose { puts "rocco: #{source_file} -> #{dest_file}" } - rocco = Rocco.new(source_file) + rocco = Rocco.new(source_file, @sources.to_a, @options) File.open(dest_file, 'wb') { |fd| fd.write(rocco.to_html) } end task @name => dest_file @@ -196,8 +219,8 @@ don’t already exist or one of their dependencies was changed.

-
- # +
+

Return a FileList that includes all of Roccos source files. This causes output files to be regenerated properly when someone upgrades the Rocco