Rokko -- fat-free Rocco
Rokko is an else one Ruby port of Docco,
the quick-and-dirty, hundred-line-long, literate-programming-style documentation generator.
Rokko 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.
Why Rokko?
- Rokko supports only Ruby source files (consider using Rocco
if you need more languages).
- Rokko uses awesome highlight.js library
for syntax highlighting.
- Rokko can generate offline-ready documentation (all assets are bundled).
- Rokko can generate an index file with links to everything (like Table of Contents).
|
|
We'll definitely need Redcarpet (Markdown library)
and Mustache (templating engine)
|
require 'redcarpet'
require 'mustache'
require File.expand_path('../rokko/version', __FILE__)
|
Public interface
Rokko.new takes a filename, an optional list of source filenames
for other documentation sources, an options hash, and an optional block.
When block is given, it must return a string with file contents.
With no block , the file is read to retrieve data.
|
module Rokko
class Rokko
attr_reader :file
attr_reader :sections
attr_reader :sources
attr_reader :options
|
Comment patterns
|
@@comment_pattern = /^\s*#(?!\{)\s?/
@@block_comment_start = /^\s*=begin\s*$/
@@block_comment_end = /^\s*=end\s*$/
def initialize(filename, sources = [], options = {}, &block)
@file = filename
@sources = sources
@options = options
@data = if block_given?
yield
else
File.read(filename)
end
@sections = prettify(split(parse(@data)))
end
|
Markdown renderer shared between Rokko and IndexLayout classes.
fenced_code_blocks: true enables parsing of code blocks
delimeted with 3 or more ~ or backticks:
```ruby
puts 'ruby code'
```
|
def self.renderer
Redcarpet::Markdown.new(Redcarpet::Render::HTML, fenced_code_blocks: true)
end
|
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, comment characters stripped.
|
def parse(data)
sections = []
docs, code = [], []
lines = data.split("\n")
|
Skip shebang and encoding information
|
lines.shift if lines[0] =~ /^\#\!/
lines.shift if lines[0] =~ /coding[:=]\s*[-\w.]+/
in_comment_block = false
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 line.match(@@block_comment_end)
in_comment_block = false
else
docs << line
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 line.match(@@block_comment_start)
in_comment_block = true
if code.any?
sections << [docs, code]
docs, code = [], []
end
elsif line.match(@@comment_pattern)
if code.any?
sections << [docs, code]
docs, code = [], []
end
docs << line.sub(@@comment_pattern, '')
else
code << line
end
end
end
sections << [docs, code] if docs.any? || code.any?
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
|
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
one with the code blocks
|
def split(sections)
docs_blocks, code_blocks = [], []
sections.each do |docs,code|
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
|
def prettify(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
|
rendered_html = self.class.renderer.render(docs_blocks.join("\n\n##### DIVIDER\n\n"))
rendered_html = ' ' if rendered_html == '' # ''.split() won't return anything useful
docs_html = rendered_html.split(/\n*<h5>DIVIDER<\/h5>\n*/m)
docs_html.zip(code_blocks)
end
def to_html
require File.expand_path('../rokko/layout', __FILE__)
::Rokko::Layout.new(self).render
end
end
end
|