diff --git a/index.html b/index.html new file mode 100644 index 0000000..ad58300 --- /dev/null +++ b/index.html @@ -0,0 +1,345 @@ + + +
+ +rocco.rb |
+ + |
---|---|
+
+ #
+
+ Rocco is a Ruby port of Docco, the quick-and-dirty, +hundred-line-long, literate-programming-style documentation generator. + +Rocco reads Ruby source files and produces annotated source documentation +in HTML format. Comments are formatted with Markdown and presented +alongside syntax highlighted code so as to give an annotation effect. +This page is the result of running Rocco against its own source file. + +Most of this was written while waiting for node.js to build (so I +could use Docco!). Docco’s gorgeous HTML and CSS are taken verbatim. +The main difference is that Rocco is written in Ruby instead of +CoffeeScript and may be a bit easier to obtain and install in +existing Ruby environments or where node doesn’t run yet. + +Install Rocco with Rubygems: + +
+
+Once installed, the
+
+The HTML files are written to the current working directory. + |
+
+
+ |
+
+
+ #
+
+ Prerequisites+ |
+
+
+ |
+
+
+ #
+
+ We’ll need a Markdown library. RDiscount, if we’re lucky. Otherwise, +issue a warning and fall back on using BlueCloth. + |
+
+ begin
+ require 'rdiscount'
+rescue LoadError => boom
+ warn "warn: #{boom}. trying bluecloth"
+ require 'bluecloth'
+ Markdown = BlueCloth
+end |
+
+
+ #
+
+ We use {{ mustache }} for +HTML templating. + |
+
+ require 'mustache' |
+
+
+ #
+
+ Code is run through Pygments for syntax
+highlighting. Fail fast right here if we can’t find the |
+
+ if ! ENV['PATH'].split(':').any? { |dir| File.exist?("#{dir}/pygmentize") }
+ fail "Pygments is required for syntax highlighting"
+end |
+
+
+ #
+
+ Public Interface+ |
+
+
+ |
+
+
+ #
+
+
|
+
+ class Rocco
+ VERSION = '0.2'
+
+ def initialize(filename, &block)
+ @file = filename
+ @data =
+ if block_given?
+ yield
+ else
+ File.read(filename)
+ end
+ @sections = highlight(split(parse(@data)))
+ end |
+
+
+ #
+
+ The filename as given to |
+
+ attr_reader :file |
+
+
+ #
+
+ A list of two-tuples representing each section of the source file. Each
+item in the list has the form: |
+
+ attr_reader :sections |
+
+
+ #
+
+ Generate HTML output for the entire document. + |
+
+ require 'rocco/layout'
+ def to_html
+ Rocco::Layout.new(self).render
+ end |
+
+
+ #
+
+ Internal Parsing and Highlighting+ |
+
+
+ |
+
+
+ #
+
+ Parse the raw file data into a list of two-tuples. Each tuple has the
+form |
+
+ def parse(data)
+ sections = []
+ docs, code = [], []
+ data.split("\n").each do |line|
+ case line
+ when /^\s*#/
+ if code.any?
+ sections << [docs, code]
+ docs, code = [], []
+ end
+ docs << line
+ else
+ code << line
+ end
+ end
+ sections << [docs, code] if docs.any? || code.any?
+ sections
+ end |
+
+
+ #
+
+ Take the list of paired sections two-tuples and split into two +separate lists: one holding the comments with leaders removed and +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")
+ end
+ [docs_blocks, code_blocks]
+ end |
+
+
+ #
+
+ Take the result of |
+
+ def highlight(blocks)
+ 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 +into separate sections. + |
+
+ markdown = docs_blocks.join("\n\n##### DIVIDER\n\n")
+ docs_html = Markdown.new(markdown, :smart).
+ to_html.
+ split(/\n*<h5>DIVIDER<\/h5>\n*/m) |
+
+
+ #
+
+ Combine all code blocks into a single big stream and run through
+Pygments. We |
+
+ code_html = nil
+ open("|pygmentize -l ruby -f html", 'r+') do |fd|
+ pid =
+ fork {
+ fd.close_read
+ fd.write code_blocks.join("\n\n# DIVIDER\n\n")
+ fd.close_write
+ exit!
+ }
+ fd.close_write
+ code_html = fd.read
+ fd.close_read
+ Process.wait(pid)
+ end |
+
+
+ #
+
+ Do some post-processing on the pygments output to split things back
+into sections and remove partial |
+
+ 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)
+ end
+end |
+
+
+ #
+
+ And that’s it. + + |
+
+
+ |
+
layout.rb |
+ + |
---|---|
+
+ #
+
+
+
+ |
+
+ require 'mustache'
+
+class Rocco::Layout < Mustache
+ self.template_path = File.dirname(__FILE__)
+
+ def initialize(doc)
+ @doc = doc
+ end
+
+ def title
+ File.basename(@doc.file)
+ end
+
+ def sections
+ num = 0
+ @doc.sections.map do |docs,code|
+ {
+ :docs => docs,
+ :code => code,
+ :num => (num += 1)
+ }
+ end
+ end
+end |
+
rocco.rb |
+ + |
---|---|
+
+ #
+
+ Rocco is a Ruby port of Docco, the quick-and-dirty, +hundred-line-long, literate-programming-style documentation generator. + +Rocco reads Ruby source files and produces annotated source documentation +in HTML format. Comments are formatted with Markdown and presented +alongside syntax highlighted code so as to give an annotation effect. +This page is the result of running Rocco against its own source file. + +Most of this was written while waiting for node.js to build (so I +could use Docco!). Docco’s gorgeous HTML and CSS are taken verbatim. +The main difference is that Rocco is written in Ruby instead of +CoffeeScript and may be a bit easier to obtain and install in +existing Ruby environments or where node doesn’t run yet. + +Install Rocco with Rubygems: + +
+
+Once installed, the
+
+The HTML files are written to the current working directory. + |
+
+
+ |
+
+
+ #
+
+ Prerequisites+ |
+
+
+ |
+
+
+ #
+
+ We’ll need a Markdown library. RDiscount, if we’re lucky. Otherwise, +issue a warning and fall back on using BlueCloth. + |
+
+ begin
+ require 'rdiscount'
+rescue LoadError => boom
+ warn "warn: #{boom}. trying bluecloth"
+ require 'bluecloth'
+ Markdown = BlueCloth
+end |
+
+
+ #
+
+ We use {{ mustache }} for +HTML templating. + |
+
+ require 'mustache' |
+
+
+ #
+
+ Code is run through Pygments for syntax
+highlighting. Fail fast right here if we can’t find the |
+
+ if ! ENV['PATH'].split(':').any? { |dir| File.exist?("#{dir}/pygmentize") }
+ fail "Pygments is required for syntax highlighting"
+end |
+
+
+ #
+
+ Public Interface+ |
+
+
+ |
+
+
+ #
+
+
|
+
+ class Rocco
+ VERSION = '0.2'
+
+ def initialize(filename, &block)
+ @file = filename
+ @data =
+ if block_given?
+ yield
+ else
+ File.read(filename)
+ end
+ @sections = highlight(split(parse(@data)))
+ end |
+
+
+ #
+
+ The filename as given to |
+
+ attr_reader :file |
+
+
+ #
+
+ A list of two-tuples representing each section of the source file. Each
+item in the list has the form: |
+
+ attr_reader :sections |
+
+
+ #
+
+ Generate HTML output for the entire document. + |
+
+ require 'rocco/layout'
+ def to_html
+ Rocco::Layout.new(self).render
+ end |
+
+
+ #
+
+ Internal Parsing and Highlighting+ |
+
+
+ |
+
+
+ #
+
+ Parse the raw file data into a list of two-tuples. Each tuple has the
+form |
+
+ def parse(data)
+ sections = []
+ docs, code = [], []
+ data.split("\n").each do |line|
+ case line
+ when /^\s*#/
+ if code.any?
+ sections << [docs, code]
+ docs, code = [], []
+ end
+ docs << line
+ else
+ code << line
+ end
+ end
+ sections << [docs, code] if docs.any? || code.any?
+ sections
+ end |
+
+
+ #
+
+ Take the list of paired sections two-tuples and split into two +separate lists: one holding the comments with leaders removed and +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")
+ end
+ [docs_blocks, code_blocks]
+ end |
+
+
+ #
+
+ Take the result of |
+
+ def highlight(blocks)
+ 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 +into separate sections. + |
+
+ markdown = docs_blocks.join("\n\n##### DIVIDER\n\n")
+ docs_html = Markdown.new(markdown, :smart).
+ to_html.
+ split(/\n*<h5>DIVIDER<\/h5>\n*/m) |
+
+
+ #
+
+ Combine all code blocks into a single big stream and run through
+Pygments. We |
+
+ code_html = nil
+ open("|pygmentize -l ruby -f html", 'r+') do |fd|
+ pid =
+ fork {
+ fd.close_read
+ fd.write code_blocks.join("\n\n# DIVIDER\n\n")
+ fd.close_write
+ exit!
+ }
+ fd.close_write
+ code_html = fd.read
+ fd.close_read
+ Process.wait(pid)
+ end |
+
+
+ #
+
+ Do some post-processing on the pygments output to split things back
+into sections and remove partial |
+
+ 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)
+ end
+end |
+
+
+ #
+
+ And that’s it. + + |
+
+
+ |
+
tasks.rb |
+ + |
---|---|
+
+ #
+
+ Rocco Rake Tasks+ +To use the Rocco Rake tasks, require
+
+This creates a
+
+It’s a good idea to guard against Rocco not being available, since your +Rakefile will fail to load otherwise. Consider doing something like this, +so that your Rakefile will still work + +
+
+It’s also possible to pass a glob pattern: + +
+
+Or a list of glob patterns: + +
+ |
+
+
+ |
+
+
+ #
+
+ Might be nice to defer this until we actually need to build docs but this +will have to do for now. + |
+
+ require 'rocco' |
+
+
+ #
+
+ Reopen the Rocco class and add a |
+
+ class Rocco
+ def self.make(dest='docs/', source_files='lib/**/*.rb')
+ Task.new(:rocco, dest, source_files)
+ end |
+
+
+ #
+
+
|
+
+ class Task
+ def initialize(task_name, dest='docs/', sources='lib/**/*.rb')
+ @name = task_name
+ @dest = dest[-1] == ?/ ? dest : "#{dest}/"
+ @sources = FileList[sources] |
+
+
+ #
+
+ Make sure there’s a |
+
+ define_directory_task @dest |
+
+
+ #
+
+ 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'
+ define_file_task source_file, "#{@dest}#{dest_file}" |
+
+
+ #
+
+ If |
+
+ CLEAN.include "#{@dest}#{dest_file}" if defined? CLEAN
+ end
+ end |
+
+
+ #
+
+ Define the destination directory task and make the |
+
+ def define_directory_task(path)
+ directory path
+ task @name => path
+ end |
+
+
+ #
+
+ Setup a You can run these tasks directly with Rake: + +
+
+… would generate the |
+
+ def define_file_task(source_file, dest_file)
+ 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)
+ File.open(dest_file, 'wb') { |fd| fd.write(rocco.to_html) }
+ end
+ task @name => dest_file
+ end |
+
+
+ #
+
+ Return a |
+
+ def rocco_source_files
+ libdir = File.expand_path('../..', __FILE__)
+ FileList["#{libdir}/rocco.rb", "#{libdir}/rocco/**"]
+ end
+
+ end
+end |
+