2010-03-09 01:09:18 +00:00
|
|
|
# **Rocco** is a Ruby port of [Docco][do], the quick-and-dirty,
|
|
|
|
# hundred-line-long, literate-programming-style documentation generator.
|
2010-03-08 17:56:18 +00:00
|
|
|
#
|
2010-03-09 01:09:18 +00:00
|
|
|
# Rocco reads Ruby source files and produces annotated source documentation
|
|
|
|
# in HTML format. Comments are formatted with [Markdown][md] and presented
|
|
|
|
# alongside syntax highlighted code so as to give an annotation effect.
|
2010-03-11 14:22:20 +00:00
|
|
|
# This page is the result of running Rocco against [its own source file][so].
|
2010-03-09 01:09:18 +00:00
|
|
|
#
|
|
|
|
# Most of this was written while waiting for [node.js][no] 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][co] and may be a bit easier to obtain and install in
|
|
|
|
# existing Ruby environments or where node doesn't run yet.
|
|
|
|
#
|
2010-03-11 14:22:20 +00:00
|
|
|
# Install Rocco with Rubygems:
|
2010-03-09 01:09:18 +00:00
|
|
|
#
|
|
|
|
# gem install rocco
|
|
|
|
#
|
|
|
|
# Once installed, the `rocco` command can be used to generate documentation
|
|
|
|
# for a set of Ruby source files:
|
|
|
|
#
|
|
|
|
# rocco lib/*.rb
|
|
|
|
#
|
|
|
|
# The HTML files are written to the current working directory.
|
|
|
|
#
|
|
|
|
# [no]: http://nodejs.org/
|
|
|
|
# [do]: http://jashkenas.github.com/docco/
|
|
|
|
# [co]: http://coffeescript.org/
|
|
|
|
# [md]: http://daringfireball.net/projects/markdown/
|
2010-03-11 14:22:20 +00:00
|
|
|
# [so]: http://github.com/rtomayko/rocco/blob/master/lib/rocco.rb#commit
|
2010-03-08 17:56:18 +00:00
|
|
|
|
2010-03-09 01:09:18 +00:00
|
|
|
#### Prerequisites
|
2010-03-08 17:56:18 +00:00
|
|
|
|
2010-03-09 16:56:01 +00:00
|
|
|
# We'll need a Markdown library. [RDiscount][rd], if we're lucky. Otherwise,
|
|
|
|
# issue a warning and fall back on using BlueCloth.
|
|
|
|
#
|
|
|
|
# [rd]: http://github.com/rtomayko/rdiscount
|
|
|
|
begin
|
|
|
|
require 'rdiscount'
|
|
|
|
rescue LoadError => boom
|
2010-03-30 19:07:30 +00:00
|
|
|
warn "WARNING: #{boom}. Trying bluecloth."
|
2010-03-09 16:56:01 +00:00
|
|
|
require 'bluecloth'
|
|
|
|
Markdown = BlueCloth
|
|
|
|
end
|
2010-03-08 17:56:18 +00:00
|
|
|
|
2010-03-09 01:30:40 +00:00
|
|
|
# We use [{{ mustache }}](http://defunkt.github.com/mustache/) for
|
2010-03-09 17:15:50 +00:00
|
|
|
# HTML templating.
|
2010-03-09 01:30:40 +00:00
|
|
|
require 'mustache'
|
|
|
|
|
2010-03-19 22:53:51 +00:00
|
|
|
# We use `Net::HTTP` to highlight code via <http://pygments.appspot.com>
|
|
|
|
require 'net/http'
|
2010-03-09 01:30:40 +00:00
|
|
|
|
2010-03-30 18:57:45 +00:00
|
|
|
# Code is run through [Pygments](http://pygments.org/) for syntax
|
|
|
|
# highlighting. If it's not installed, locally, use a webservice.
|
2010-04-02 22:59:52 +00:00
|
|
|
include FileTest
|
|
|
|
if !ENV['PATH'].split(':').any? { |dir| executable?("#{dir}/pygmentize") }
|
2010-03-30 19:07:30 +00:00
|
|
|
warn "WARNING: Pygments not found. Using webservice."
|
|
|
|
end
|
2010-03-30 18:57:45 +00:00
|
|
|
|
2010-03-09 01:09:18 +00:00
|
|
|
#### Public Interface
|
|
|
|
|
2010-03-11 16:01:46 +00:00
|
|
|
# `Rocco.new` takes a source `filename`, an optional list of source filenames
|
2010-03-16 09:17:17 +00:00
|
|
|
# for other documentation sources, an `options` hash, and an optional `block`.
|
2010-10-20 12:34:09 +00:00
|
|
|
# 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`)_.
|
|
|
|
#
|
2010-03-16 09:17:17 +00:00
|
|
|
# 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.
|
2010-03-08 17:56:18 +00:00
|
|
|
class Rocco
|
2010-09-10 17:32:31 +00:00
|
|
|
VERSION = '0.5'
|
2010-03-09 01:45:52 +00:00
|
|
|
|
2010-03-16 09:17:17 +00:00
|
|
|
def initialize(filename, sources=[], options={}, &block)
|
2010-03-08 17:56:18 +00:00
|
|
|
@file = filename
|
|
|
|
@data =
|
|
|
|
if block_given?
|
|
|
|
yield
|
|
|
|
else
|
|
|
|
File.read(filename)
|
|
|
|
end
|
2010-03-19 22:53:51 +00:00
|
|
|
defaults = {
|
|
|
|
:language => 'ruby',
|
|
|
|
:comment_chars => '#',
|
2010-10-17 10:25:35 +00:00
|
|
|
:template_file => nil
|
2010-03-19 22:53:51 +00:00
|
|
|
}
|
2010-03-16 09:17:17 +00:00
|
|
|
@options = defaults.merge(options)
|
2010-03-11 16:01:46 +00:00
|
|
|
@sources = sources
|
2010-10-14 16:57:35 +00:00
|
|
|
@comment_pattern = Regexp.new("^\\s*#{@options[:comment_chars]}\s?")
|
2010-10-17 10:25:35 +00:00
|
|
|
@template_file = @options[:template_file]
|
2010-10-20 12:34:09 +00:00
|
|
|
|
|
|
|
@options[:language] = detect_language()
|
2010-03-09 17:15:50 +00:00
|
|
|
@sections = highlight(split(parse(@data)))
|
2010-03-08 17:56:18 +00:00
|
|
|
end
|
|
|
|
|
2010-10-20 12:34:09 +00:00
|
|
|
# Returns `true` if `pygmentize` is available locally, `false` otherwise.
|
|
|
|
def pygmentize?
|
|
|
|
# Memoize the result, we'll call this a few times
|
|
|
|
@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`. In
|
|
|
|
# that case, we'll fallback to the user-provided `:language` option.
|
|
|
|
def detect_language
|
|
|
|
default = @options[:language]
|
|
|
|
if pygmentize?
|
|
|
|
lang = %x[pygmentize -N #{@file}].strip!
|
|
|
|
( !lang || lang == "text" ) ? default : lang
|
|
|
|
else
|
|
|
|
default
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2010-03-09 01:09:18 +00:00
|
|
|
# The filename as given to `Rocco.new`.
|
2010-03-08 23:21:28 +00:00
|
|
|
attr_reader :file
|
|
|
|
|
2010-10-20 12:34:09 +00:00
|
|
|
# The merged options array
|
|
|
|
attr_reader :options
|
|
|
|
|
2010-03-08 23:21:28 +00:00
|
|
|
# A list of two-tuples representing each *section* of the source file. Each
|
2010-03-09 01:09:18 +00:00
|
|
|
# item in the list has the form: `[docs_html, code_html]`, where both
|
|
|
|
# elements are strings containing the documentation and source code HTML,
|
|
|
|
# respectively.
|
2010-03-08 23:21:28 +00:00
|
|
|
attr_reader :sections
|
|
|
|
|
2010-03-11 16:01:46 +00:00
|
|
|
# A list of all source filenames included in the documentation set. Useful
|
|
|
|
# for building an index of other files.
|
|
|
|
attr_reader :sources
|
|
|
|
|
2010-10-17 10:25:35 +00:00
|
|
|
# An absolute path to a file that ought be used as a template for the
|
|
|
|
# HTML-rendered documentation.
|
|
|
|
attr_reader :template_file
|
|
|
|
|
2010-03-09 01:09:18 +00:00
|
|
|
# Generate HTML output for the entire document.
|
|
|
|
require 'rocco/layout'
|
|
|
|
def to_html
|
2010-10-17 10:25:35 +00:00
|
|
|
Rocco::Layout.new(self, @template_file).render
|
2010-03-09 01:09:18 +00:00
|
|
|
end
|
2010-03-08 23:21:28 +00:00
|
|
|
|
2010-03-09 01:09:18 +00:00
|
|
|
#### Internal Parsing and Highlighting
|
|
|
|
|
2010-03-09 17:15:50 +00:00
|
|
|
# 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
|
2010-03-16 09:17:17 +00:00
|
|
|
# raw lines parsed from the input file. The first line is ignored if it
|
|
|
|
# is a shebang line.
|
2010-03-09 01:09:18 +00:00
|
|
|
def parse(data)
|
|
|
|
sections = []
|
|
|
|
docs, code = [], []
|
2010-03-16 09:17:17 +00:00
|
|
|
lines = data.split("\n")
|
|
|
|
lines.shift if lines[0] =~ /^\#\!/
|
|
|
|
lines.each do |line|
|
2010-03-09 01:09:18 +00:00
|
|
|
case line
|
2010-03-16 09:17:17 +00:00
|
|
|
when @comment_pattern
|
2010-03-09 01:09:18 +00:00
|
|
|
if code.any?
|
|
|
|
sections << [docs, code]
|
|
|
|
docs, code = [], []
|
2010-03-08 17:56:18 +00:00
|
|
|
end
|
2010-03-09 01:09:18 +00:00
|
|
|
docs << line
|
|
|
|
else
|
|
|
|
code << line
|
2010-03-08 17:56:18 +00:00
|
|
|
end
|
|
|
|
end
|
2010-03-09 01:09:18 +00:00
|
|
|
sections << [docs, code] if docs.any? || code.any?
|
|
|
|
sections
|
|
|
|
end
|
2010-03-08 17:56:18 +00:00
|
|
|
|
2010-03-09 17:15:50 +00:00
|
|
|
# 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)
|
2010-03-09 01:09:18 +00:00
|
|
|
docs_blocks, code_blocks = [], []
|
|
|
|
sections.each do |docs,code|
|
2010-03-16 09:17:17 +00:00
|
|
|
docs_blocks << docs.map { |line| line.sub(@comment_pattern, '') }.join("\n")
|
2010-03-17 02:58:29 +00:00
|
|
|
code_blocks << code.map do |line|
|
|
|
|
tabs = line.match(/^(\t+)/)
|
2010-03-19 19:05:25 +00:00
|
|
|
tabs ? line.sub(/^\t+/, ' ' * tabs.captures[0].length) : line
|
2010-03-17 02:58:29 +00:00
|
|
|
end.join("\n")
|
2010-03-09 01:09:18 +00:00
|
|
|
end
|
2010-03-09 17:15:50 +00:00
|
|
|
[docs_blocks, code_blocks]
|
|
|
|
end
|
|
|
|
|
|
|
|
# Take the result of `split` and apply Markdown formatting to comments and
|
|
|
|
# syntax highlighting to source code.
|
|
|
|
def highlight(blocks)
|
|
|
|
docs_blocks, code_blocks = blocks
|
2010-03-08 17:56:18 +00:00
|
|
|
|
2010-03-09 17:15:50 +00:00
|
|
|
# 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.
|
2010-03-09 16:55:17 +00:00
|
|
|
markdown = docs_blocks.join("\n\n##### DIVIDER\n\n")
|
2010-03-09 01:09:18 +00:00
|
|
|
docs_html = Markdown.new(markdown, :smart).
|
|
|
|
to_html.
|
2010-03-09 16:55:17 +00:00
|
|
|
split(/\n*<h5>DIVIDER<\/h5>\n*/m)
|
2010-03-09 01:09:18 +00:00
|
|
|
|
2010-03-19 22:53:51 +00:00
|
|
|
# Combine all code blocks into a single big stream and run through either
|
|
|
|
# `pygmentize(1)` or <http://pygments.appspot.com>
|
2010-03-30 20:18:17 +00:00
|
|
|
code_stream = code_blocks.join("\n\n#{@options[:comment_chars]} DIVIDER\n\n")
|
2010-03-19 22:53:51 +00:00
|
|
|
|
2010-10-20 12:34:09 +00:00
|
|
|
if pygmentize?
|
2010-03-30 19:07:30 +00:00
|
|
|
code_html = highlight_pygmentize(code_stream)
|
2010-04-02 22:59:52 +00:00
|
|
|
else
|
|
|
|
code_html = highlight_webservice(code_stream)
|
2010-03-30 19:07:30 +00:00
|
|
|
end
|
2010-03-19 22:53:51 +00:00
|
|
|
|
|
|
|
# Do some post-processing on the pygments output to split things back
|
|
|
|
# into sections and remove partial `<pre>` blocks.
|
|
|
|
code_html = code_html.
|
2010-10-14 16:31:20 +00:00
|
|
|
split(/\n*<span class="c.?">#{@options[:comment_chars]} DIVIDER<\/span>\n*/m).
|
2010-03-19 22:53:51 +00:00
|
|
|
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.
|
|
|
|
def highlight_pygmentize(code)
|
2010-03-09 01:09:18 +00:00
|
|
|
code_html = nil
|
2010-10-19 11:32:03 +00:00
|
|
|
open("|pygmentize -l #{@options[:language]} -O encoding=utf-8 -f html", 'r+') do |fd|
|
2010-03-09 17:16:53 +00:00
|
|
|
pid =
|
|
|
|
fork {
|
|
|
|
fd.close_read
|
2010-03-19 22:53:51 +00:00
|
|
|
fd.write code
|
2010-03-09 17:16:53 +00:00
|
|
|
fd.close_write
|
|
|
|
exit!
|
|
|
|
}
|
2010-03-09 01:09:18 +00:00
|
|
|
fd.close_write
|
|
|
|
code_html = fd.read
|
|
|
|
fd.close_read
|
2010-03-09 17:16:53 +00:00
|
|
|
Process.wait(pid)
|
2010-03-08 23:21:28 +00:00
|
|
|
end
|
2010-03-08 17:56:18 +00:00
|
|
|
|
2010-03-19 22:53:51 +00:00
|
|
|
code_html
|
|
|
|
end
|
2010-03-30 18:46:24 +00:00
|
|
|
|
|
|
|
# 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.
|
2010-03-19 22:53:51 +00:00
|
|
|
def highlight_webservice(code)
|
|
|
|
Net::HTTP.post_form(
|
|
|
|
URI.parse('http://pygments.appspot.com/'),
|
2010-10-20 12:27:03 +00:00
|
|
|
{'lang' => @options[:language], 'code' => code}
|
2010-03-19 22:53:51 +00:00
|
|
|
).body
|
2010-03-08 17:56:18 +00:00
|
|
|
end
|
|
|
|
end
|
2010-03-10 12:07:21 +00:00
|
|
|
|
|
|
|
# And that's it.
|