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. Rocco can be installed 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.1'
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 |