rocco-obb 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +18 -0
- data/README +23 -0
- data/Rakefile +115 -0
- data/bin/rocco +97 -0
- data/lib/rocco/layout.mustache +46 -0
- data/lib/rocco/layout.rb +47 -0
- data/lib/rocco/tasks.rb +120 -0
- data/lib/rocco.rb +324 -0
- data/rocco.gemspec +39 -0
- metadata +101 -0
data/COPYING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2010 Ryan Tomayko <http://tomayko.com/about>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
___ ___ ___ ___ ___
|
4
|
+
/\ \ /\ \ /\ \ /\ \ /\ \
|
5
|
+
/::\ \ /::\ \ /::\ \ /::\ \ /::\ \
|
6
|
+
/::\:\__\ /:/\:\__\ /:/\:\__\ /:/\:\__\ /:/\:\__\
|
7
|
+
\;:::/ / \:\/:/ / \:\ \/__/ \:\ \/__/ \:\/:/ /
|
8
|
+
|:\/__/ \::/ / \:\__\ \:\__\ \::/ /
|
9
|
+
\|__| \/__/ \/__/ \/__/ \/__/
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
Rocco is a quick-and-dirty, literate-programming-style documentation
|
14
|
+
generator for Ruby. See the Rocco generated docs for more information:
|
15
|
+
|
16
|
+
<http://rtomayko.github.com/rocco/>
|
17
|
+
|
18
|
+
|
19
|
+
Rocco is a port of, and borrows heavily from, Docco -- the original
|
20
|
+
quick-and-dirty, hundred-line-long, literate-programming-style
|
21
|
+
documentation generator in CoffeeScript:
|
22
|
+
|
23
|
+
<http://jashkenas.github.com/docco/>
|
data/Rakefile
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
$LOAD_PATH.unshift 'lib'
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/clean'
|
5
|
+
|
6
|
+
task :default => [:sup, :docs, :test]
|
7
|
+
|
8
|
+
desc 'Holla'
|
9
|
+
task :sup do
|
10
|
+
verbose do
|
11
|
+
lines = File.read('README').split("\n")[0,12]
|
12
|
+
lines.map! { |line| line[15..-1] }
|
13
|
+
puts lines.join("\n")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'Run tests (default)'
|
18
|
+
Rake::TestTask.new(:test) do |t|
|
19
|
+
t.test_files = FileList['test/*_test.rb']
|
20
|
+
t.ruby_opts = ['-rubygems'] if defined? Gem
|
21
|
+
end
|
22
|
+
|
23
|
+
# Bring in Rocco tasks
|
24
|
+
require 'rocco/tasks'
|
25
|
+
Rocco::make 'docs/'
|
26
|
+
|
27
|
+
desc 'Build rocco docs'
|
28
|
+
task :docs => :rocco
|
29
|
+
directory 'docs/'
|
30
|
+
|
31
|
+
desc 'Build docs and open in browser for the reading'
|
32
|
+
task :read => :docs do
|
33
|
+
sh 'open docs/rocco.html'
|
34
|
+
end
|
35
|
+
|
36
|
+
# Make index.html a copy of rocco.html
|
37
|
+
file 'docs/index.html' => 'docs/rocco.html' do |f|
|
38
|
+
cp 'docs/rocco.html', 'docs/index.html', :preserve => true
|
39
|
+
end
|
40
|
+
task :docs => 'docs/index.html'
|
41
|
+
CLEAN.include 'docs/index.html'
|
42
|
+
|
43
|
+
# Alias for docs task
|
44
|
+
task :doc => :docs
|
45
|
+
|
46
|
+
# GITHUB PAGES ===============================================================
|
47
|
+
|
48
|
+
desc 'Update gh-pages branch'
|
49
|
+
task :pages => ['docs/.git', :docs] do
|
50
|
+
rev = `git rev-parse --short HEAD`.strip
|
51
|
+
Dir.chdir 'docs' do
|
52
|
+
sh "git add *.html"
|
53
|
+
sh "git commit -m 'rebuild pages from #{rev}'" do |ok,res|
|
54
|
+
if ok
|
55
|
+
verbose { puts "gh-pages updated" }
|
56
|
+
sh "git push -q o HEAD:gh-pages"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Update the pages/ directory clone
|
63
|
+
file 'docs/.git' => ['docs/', '.git/refs/heads/gh-pages'] do |f|
|
64
|
+
sh "cd docs && git init -q && git remote add o ../.git" if !File.exist?(f.name)
|
65
|
+
sh "cd docs && git fetch -q o && git reset -q --hard o/gh-pages && touch ."
|
66
|
+
end
|
67
|
+
CLOBBER.include 'docs/.git'
|
68
|
+
|
69
|
+
# PACKAGING =================================================================
|
70
|
+
|
71
|
+
if defined?(Gem)
|
72
|
+
SPEC = eval(File.read('rocco.gemspec'))
|
73
|
+
|
74
|
+
def package(ext='')
|
75
|
+
"pkg/rocco-#{SPEC.version}" + ext
|
76
|
+
end
|
77
|
+
|
78
|
+
desc 'Build packages'
|
79
|
+
task :package => %w[.gem .tar.gz].map {|e| package(e)}
|
80
|
+
|
81
|
+
desc 'Build and install as local gem'
|
82
|
+
task :install => package('.gem') do
|
83
|
+
sh "gem install #{package('.gem')}"
|
84
|
+
end
|
85
|
+
|
86
|
+
directory 'pkg/'
|
87
|
+
|
88
|
+
file package('.gem') => %w[pkg/ rocco.gemspec] + SPEC.files do |f|
|
89
|
+
sh "gem build rocco.gemspec"
|
90
|
+
mv File.basename(f.name), f.name
|
91
|
+
end
|
92
|
+
|
93
|
+
file package('.tar.gz') => %w[pkg/] + SPEC.files do |f|
|
94
|
+
sh "git archive --format=tar HEAD | gzip > #{f.name}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# GEMSPEC ===================================================================
|
99
|
+
|
100
|
+
file 'rocco.gemspec' => FileList['{lib,test,bin}/**','Rakefile'] do |f|
|
101
|
+
version = File.read('lib/rocco.rb')[/VERSION = '(.*)'/] && $1
|
102
|
+
date = Time.now.strftime("%Y-%m-%d")
|
103
|
+
spec = File.
|
104
|
+
read(f.name).
|
105
|
+
sub(/s\.version\s*=\s*'.*'/, "s.version = '#{version}'")
|
106
|
+
parts = spec.split(" # = MANIFEST =\n")
|
107
|
+
files = `git ls-files`.
|
108
|
+
split("\n").sort.reject{ |file| file =~ /^\./ }.
|
109
|
+
map{ |file| " #{file}" }.join("\n")
|
110
|
+
parts[1] = " s.files = %w[\n#{files}\n ]\n"
|
111
|
+
spec = parts.join(" # = MANIFEST =\n")
|
112
|
+
spec.sub!(/s.date = '.*'/, "s.date = '#{date}'")
|
113
|
+
File.open(f.name, 'w') { |io| io.write(spec) }
|
114
|
+
puts "#{f.name} #{version} (#{date})"
|
115
|
+
end
|
data/bin/rocco
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#/ Usage: rocco [-l <lang>] [-c <chars>] [-o <dir>] <file>...
|
3
|
+
#/ Generate literate-programming-style documentation for Ruby source <file>s.
|
4
|
+
#/
|
5
|
+
#/ Options:
|
6
|
+
#/ -l, --language=<lang> The Pygments lexer to use to highlight code
|
7
|
+
#/ -c, --comment-chars=<chars>
|
8
|
+
#/ The string to recognize as a comment marker
|
9
|
+
#/ -o, --output=<dir> Directory where generated HTML files are written
|
10
|
+
#/ -t, --template=<path> The file to use as template when rendering HTML
|
11
|
+
#/ --help Show this help message
|
12
|
+
|
13
|
+
require 'optparse'
|
14
|
+
require 'fileutils'
|
15
|
+
|
16
|
+
# Write usage message to stdout and exit.
|
17
|
+
def usage(stream=$stderr, status=1)
|
18
|
+
stream.puts File.readlines(__FILE__).
|
19
|
+
grep(/^#\//).
|
20
|
+
map { |line| line.sub(/^#. ?/, '') }.
|
21
|
+
join
|
22
|
+
exit status
|
23
|
+
end
|
24
|
+
|
25
|
+
# Like `Kernel#abort` but writes a note encouraging the user to consult
|
26
|
+
# `rocco --help` for more information.
|
27
|
+
def abort_with_note(message=nil)
|
28
|
+
$stderr.puts message if message
|
29
|
+
abort "See `rocco --help' for usage information."
|
30
|
+
end
|
31
|
+
|
32
|
+
# Parse command line options, aborting if anything goes wrong.
|
33
|
+
output_dir = '.'
|
34
|
+
sources = []
|
35
|
+
options = {}
|
36
|
+
ARGV.options { |o|
|
37
|
+
o.program_name = File.basename($0)
|
38
|
+
o.on("-o", "--output=DIR") { |dir| output_dir = dir }
|
39
|
+
o.on("-l", "--language=LANG") { |lang| options[:language] = lang }
|
40
|
+
o.on("-c", "--comment-chars=CHARS") { |chars| options[:comment_chars] = Regexp.escape(chars) }
|
41
|
+
o.on("-t", "--template=TEMPLATE") { |template| options[:template_file] = template }
|
42
|
+
o.on_tail("-h", "--help") { usage($stdout, 0) }
|
43
|
+
o.parse!
|
44
|
+
} or abort_with_note
|
45
|
+
|
46
|
+
# Use http://pygments.appspot.com in case `pygmentize(1)` isn't available.
|
47
|
+
if ! ENV['PATH'].split(':').any? { |dir| File.exist?("#{dir}/pygmentize") }
|
48
|
+
unless options[:webservice]
|
49
|
+
$stderr.puts "pygmentize not in PATH; using pygments.appspot.com instead"
|
50
|
+
options[:webservice] = true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Eat sources from ARGV.
|
55
|
+
sources << ARGV.shift while ARGV.any?
|
56
|
+
|
57
|
+
# Make sure we have some files to work with.
|
58
|
+
if sources.empty?
|
59
|
+
abort_with_note "#{File.basename($0)}: no input <file>s given"
|
60
|
+
end
|
61
|
+
|
62
|
+
# What a fucking mess. Most of this is duplicated in rocco.rb too.
|
63
|
+
libdir = File.expand_path('../../lib', __FILE__).sub(/^#{Dir.pwd}\//, '')
|
64
|
+
begin
|
65
|
+
require 'rdiscount'
|
66
|
+
require 'rocco'
|
67
|
+
rescue LoadError
|
68
|
+
case $!.to_s
|
69
|
+
when /rdiscount/
|
70
|
+
if !defined?(Gem)
|
71
|
+
warn "warn: #$!. trying again with rubygems"
|
72
|
+
require 'rubygems'
|
73
|
+
retry
|
74
|
+
else
|
75
|
+
require 'bluecloth'
|
76
|
+
Markdown = BlueCloth
|
77
|
+
$LOADED_FEATURES << 'rdiscount.rb'
|
78
|
+
retry
|
79
|
+
end
|
80
|
+
when /rocco/
|
81
|
+
if !$:.include?(libdir)
|
82
|
+
warn "warn: #$!. trying again with #{libdir} on load path"
|
83
|
+
$:.unshift(libdir)
|
84
|
+
retry
|
85
|
+
end
|
86
|
+
end
|
87
|
+
raise
|
88
|
+
end
|
89
|
+
|
90
|
+
# Run each file through Rocco and write output.
|
91
|
+
sources.each do |filename|
|
92
|
+
rocco = Rocco.new(filename, sources, options)
|
93
|
+
dest = File.join(output_dir, (filename.split('.')[0..-2].empty? ? filename : filename.split('.')[0..-2].join('.')) + '.html')
|
94
|
+
puts "rocco: #{filename} -> #{dest}"
|
95
|
+
FileUtils.mkdir_p File.dirname(dest)
|
96
|
+
File.open(dest, 'wb') { |fd| fd.write(rocco.to_html) }
|
97
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
5
|
+
<title>{{ title }}</title>
|
6
|
+
<link rel="stylesheet" href="http://jashkenas.github.com/docco/resources/docco.css">
|
7
|
+
</head>
|
8
|
+
<body>
|
9
|
+
<div id='container'>
|
10
|
+
<div id="background"></div>
|
11
|
+
{{#sources?}}
|
12
|
+
<div id="jump_to">
|
13
|
+
Jump To …
|
14
|
+
<div id="jump_wrapper">
|
15
|
+
<div id="jump_page">
|
16
|
+
{{#sources}}
|
17
|
+
<a class="source" href="{{ url }}">{{ basename }}</a>
|
18
|
+
{{/sources}}
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
{{/sources?}}
|
23
|
+
<table cellspacing=0 cellpadding=0>
|
24
|
+
<thead>
|
25
|
+
<tr>
|
26
|
+
<th class=docs><h1>{{ title }}</h1></th>
|
27
|
+
<th class=code></th>
|
28
|
+
</tr>
|
29
|
+
</thead>
|
30
|
+
<tbody>
|
31
|
+
{{#sections}}
|
32
|
+
<tr id='section-{{ num }}'>
|
33
|
+
<td class=docs>
|
34
|
+
<div class="pilwrap">
|
35
|
+
<a class="pilcrow" href="#section-{{ num }}">¶</a>
|
36
|
+
</div>
|
37
|
+
{{{ docs }}}
|
38
|
+
</td>
|
39
|
+
<td class=code>
|
40
|
+
<div class='highlight'><pre>{{{ code }}}</pre></div>
|
41
|
+
</td>
|
42
|
+
</tr>
|
43
|
+
{{/sections}}
|
44
|
+
</table>
|
45
|
+
</div>
|
46
|
+
</body>
|
data/lib/rocco/layout.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'mustache'
|
2
|
+
|
3
|
+
class Rocco::Layout < Mustache
|
4
|
+
self.template_path = File.dirname(__FILE__)
|
5
|
+
|
6
|
+
def initialize(doc, file=nil)
|
7
|
+
@doc = doc
|
8
|
+
if not file.nil?
|
9
|
+
Rocco::Layout.template_file = file
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def title
|
14
|
+
File.basename(@doc.file)
|
15
|
+
end
|
16
|
+
|
17
|
+
def sections
|
18
|
+
num = 0
|
19
|
+
@doc.sections.map do |docs,code|
|
20
|
+
{
|
21
|
+
:docs => docs,
|
22
|
+
:docs? => !docs.empty?,
|
23
|
+
:header? => /^<h.>.+<\/h.>$/.match( docs ),
|
24
|
+
|
25
|
+
:code => code,
|
26
|
+
:code? => !code.empty?,
|
27
|
+
|
28
|
+
:empty? => ( code.empty? && docs.empty? ),
|
29
|
+
:num => (num += 1)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def sources?
|
35
|
+
@doc.sources.length > 1
|
36
|
+
end
|
37
|
+
|
38
|
+
def sources
|
39
|
+
@doc.sources.sort.map do |source|
|
40
|
+
{
|
41
|
+
:path => source,
|
42
|
+
:basename => File.basename(source),
|
43
|
+
:url => File.basename(source).split('.')[0..-2].join('.') + '.html'
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/rocco/tasks.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
#### Rocco Rake Tasks
|
2
|
+
#
|
3
|
+
# To use the Rocco Rake tasks, require `rocco/tasks` in your `Rakefile`
|
4
|
+
# and define a Rake task with `rocco_task`. In its simplest form, `rocco_task`
|
5
|
+
# takes the path to a destination directory where HTML docs should be built:
|
6
|
+
#
|
7
|
+
# require 'rocco/tasks'
|
8
|
+
#
|
9
|
+
# desc "Build Rocco Docs"
|
10
|
+
# Rocco::make 'docs/'
|
11
|
+
#
|
12
|
+
# This creates a `:rocco` rake task, which can then be run with:
|
13
|
+
#
|
14
|
+
# rake rocco
|
15
|
+
#
|
16
|
+
# It's a good idea to guard against Rocco not being available, since your
|
17
|
+
# Rakefile will fail to load otherwise. Consider doing something like this,
|
18
|
+
# so that your Rakefile will still work
|
19
|
+
#
|
20
|
+
# begin
|
21
|
+
# require 'rocco/tasks'
|
22
|
+
# Rocco::make 'docs/'
|
23
|
+
# rescue LoadError
|
24
|
+
# warn "#$! -- rocco tasks not loaded."
|
25
|
+
# task :rocco
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# It's also possible to pass a glob pattern:
|
29
|
+
#
|
30
|
+
# Rocco::make 'html/', 'lib/thing/**/*.rb'
|
31
|
+
#
|
32
|
+
# Or a list of glob patterns:
|
33
|
+
#
|
34
|
+
# Rocco::make 'html/', ['lib/thing.rb', 'lib/thing/*.rb']
|
35
|
+
#
|
36
|
+
# Finally, it is also possible to specify which Pygments language you would
|
37
|
+
# like to use to highlight the code, as well as the comment characters for the
|
38
|
+
# language in the `options` hash:
|
39
|
+
#
|
40
|
+
# Rocco::make 'html/', 'lib/thing/**/*.rb', {
|
41
|
+
# :language => 'io',
|
42
|
+
# :comment_chars => '#'
|
43
|
+
# }
|
44
|
+
#
|
45
|
+
|
46
|
+
# Might be nice to defer this until we actually need to build docs but this
|
47
|
+
# will have to do for now.
|
48
|
+
require 'rocco'
|
49
|
+
|
50
|
+
# Reopen the Rocco class and add a `make` class method. This is a simple bit
|
51
|
+
# of sugar over `Rocco::Task.new`. If you want your Rake task to be named
|
52
|
+
# something other than `:rocco`, you can use `Rocco::Task` directly.
|
53
|
+
class Rocco
|
54
|
+
def self.make(dest='docs/', source_files='lib/**/*.rb', options={})
|
55
|
+
Task.new(:rocco, dest, source_files, options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# `Rocco::Task.new` takes a task name, the destination directory docs
|
59
|
+
# should be built under, and a source file pattern or file list.
|
60
|
+
class Task
|
61
|
+
def initialize(task_name, dest='docs/', sources='lib/**/*.rb', options={})
|
62
|
+
@name = task_name
|
63
|
+
@dest = dest[-1] == ?/ ? dest : "#{dest}/"
|
64
|
+
@sources = FileList[sources]
|
65
|
+
@options = options
|
66
|
+
|
67
|
+
# Make sure there's a `directory` task defined for our destination.
|
68
|
+
define_directory_task @dest
|
69
|
+
|
70
|
+
# Run over the source file list, constructing destination filenames
|
71
|
+
# and defining file tasks.
|
72
|
+
@sources.each do |source_file|
|
73
|
+
dest_file = File.basename(source_file).split('.')[0..-2].join('.') + '.html'
|
74
|
+
define_file_task source_file, "#{@dest}#{dest_file}"
|
75
|
+
|
76
|
+
# If `rake/clean` was required, add the generated files to the list.
|
77
|
+
# That way all Rocco generated are removed when running `rake clean`.
|
78
|
+
CLEAN.include "#{@dest}#{dest_file}" if defined? CLEAN
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Define the destination directory task and make the `:rocco` task depend
|
83
|
+
# on it. This causes the destination directory to be created if it doesn't
|
84
|
+
# already exist.
|
85
|
+
def define_directory_task(path)
|
86
|
+
directory path
|
87
|
+
task @name => path
|
88
|
+
end
|
89
|
+
|
90
|
+
# Setup a `file` task for a single Rocco output file (`dest_file`). It
|
91
|
+
# depends on the source file, the destination directory, and all of Rocco's
|
92
|
+
# internal source code, so that the destination file is rebuilt when any of
|
93
|
+
# those changes.
|
94
|
+
#
|
95
|
+
# You can run these tasks directly with Rake:
|
96
|
+
#
|
97
|
+
# rake docs/foo.html docs/bar.html
|
98
|
+
#
|
99
|
+
# ... would generate the `foo.html` and `bar.html` files but only if they
|
100
|
+
# don't already exist or one of their dependencies was changed.
|
101
|
+
def define_file_task(source_file, dest_file)
|
102
|
+
prerequisites = [@dest, source_file] + rocco_source_files
|
103
|
+
file dest_file => prerequisites do |f|
|
104
|
+
verbose { puts "rocco: #{source_file} -> #{dest_file}" }
|
105
|
+
rocco = Rocco.new(source_file, @sources.to_a, @options)
|
106
|
+
File.open(dest_file, 'wb') { |fd| fd.write(rocco.to_html) }
|
107
|
+
end
|
108
|
+
task @name => dest_file
|
109
|
+
end
|
110
|
+
|
111
|
+
# Return a `FileList` that includes all of Roccos source files. This causes
|
112
|
+
# output files to be regenerated properly when someone upgrades the Rocco
|
113
|
+
# library.
|
114
|
+
def rocco_source_files
|
115
|
+
libdir = File.expand_path('../..', __FILE__)
|
116
|
+
FileList["#{libdir}/rocco.rb", "#{libdir}/rocco/**"]
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
data/lib/rocco.rb
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
# **Rocco** is a Ruby port of [Docco][do], the quick-and-dirty,
|
2
|
+
# hundred-line-long, literate-programming-style documentation generator.
|
3
|
+
#
|
4
|
+
# Rocco reads Ruby source files and produces annotated source documentation
|
5
|
+
# in HTML format. Comments are formatted with [Markdown][md] and presented
|
6
|
+
# alongside syntax highlighted code so as to give an annotation effect.
|
7
|
+
# This page is the result of running Rocco against [its own source file][so].
|
8
|
+
#
|
9
|
+
# Most of this was written while waiting for [node.js][no] to build (so I
|
10
|
+
# could use Docco!). Docco's gorgeous HTML and CSS are taken verbatim.
|
11
|
+
# The main difference is that Rocco is written in Ruby instead of
|
12
|
+
# [CoffeeScript][co] and may be a bit easier to obtain and install in
|
13
|
+
# existing Ruby environments or where node doesn't run yet.
|
14
|
+
#
|
15
|
+
# Install Rocco with Rubygems:
|
16
|
+
#
|
17
|
+
# gem install rocco
|
18
|
+
#
|
19
|
+
# Once installed, the `rocco` command can be used to generate documentation
|
20
|
+
# for a set of Ruby source files:
|
21
|
+
#
|
22
|
+
# rocco lib/*.rb
|
23
|
+
#
|
24
|
+
# The HTML files are written to the current working directory.
|
25
|
+
#
|
26
|
+
# [no]: http://nodejs.org/
|
27
|
+
# [do]: http://jashkenas.github.com/docco/
|
28
|
+
# [co]: http://coffeescript.org/
|
29
|
+
# [md]: http://daringfireball.net/projects/markdown/
|
30
|
+
# [so]: http://github.com/rtomayko/rocco/blob/master/lib/rocco.rb#commit
|
31
|
+
|
32
|
+
#### Prerequisites
|
33
|
+
|
34
|
+
# We'll need a Markdown library. [RDiscount][rd], if we're lucky. Otherwise,
|
35
|
+
# issue a warning and fall back on using BlueCloth.
|
36
|
+
#
|
37
|
+
# [rd]: http://github.com/rtomayko/rdiscount
|
38
|
+
begin
|
39
|
+
require 'rdiscount'
|
40
|
+
rescue LoadError => boom
|
41
|
+
warn "WARNING: #{boom}. Trying bluecloth."
|
42
|
+
require 'bluecloth'
|
43
|
+
Markdown = BlueCloth
|
44
|
+
end
|
45
|
+
|
46
|
+
# We use [{{ mustache }}](http://defunkt.github.com/mustache/) for
|
47
|
+
# HTML templating.
|
48
|
+
require 'mustache'
|
49
|
+
|
50
|
+
# We use `Net::HTTP` to highlight code via <http://pygments.appspot.com>
|
51
|
+
require 'net/http'
|
52
|
+
|
53
|
+
# Code is run through [Pygments](http://pygments.org/) for syntax
|
54
|
+
# highlighting. If it's not installed, locally, use a webservice.
|
55
|
+
include FileTest
|
56
|
+
if !ENV['PATH'].split(':').any? { |dir| executable?("#{dir}/pygmentize") }
|
57
|
+
warn "WARNING: Pygments not found. Using webservice."
|
58
|
+
end
|
59
|
+
|
60
|
+
#### Public Interface
|
61
|
+
|
62
|
+
# `Rocco.new` takes a source `filename`, an optional list of source filenames
|
63
|
+
# for other documentation sources, an `options` hash, and an optional `block`.
|
64
|
+
# The `options` hash respects three members:
|
65
|
+
#
|
66
|
+
# * `:language`: specifies which Pygments lexer to use if one can't be
|
67
|
+
# auto-detected from the filename. _Defaults to `ruby`_.
|
68
|
+
#
|
69
|
+
# * `:comment_chars`, which specifies the comment characters of the
|
70
|
+
# target language. _Defaults to `#`_.
|
71
|
+
#
|
72
|
+
# * `:template_file`, which specifies a external template file to use
|
73
|
+
# when rendering the final, highlighted file via Mustache. _Defaults
|
74
|
+
# to `nil` (that is, Mustache will use `./lib/rocco/layout.mustache`)_.
|
75
|
+
#
|
76
|
+
class Rocco
|
77
|
+
VERSION = '0.5'
|
78
|
+
|
79
|
+
def initialize(filename, sources=[], options={}, &block)
|
80
|
+
@file = filename
|
81
|
+
@sources = sources
|
82
|
+
|
83
|
+
# When `block` is given, it must read the contents of the file using
|
84
|
+
# whatever means necessary and return it as a string. With no `block`,
|
85
|
+
# the file is read to retrieve data.
|
86
|
+
@data =
|
87
|
+
if block_given?
|
88
|
+
yield
|
89
|
+
else
|
90
|
+
File.read(filename)
|
91
|
+
end
|
92
|
+
defaults = {
|
93
|
+
:language => 'ruby',
|
94
|
+
:comment_chars => '#',
|
95
|
+
:template_file => nil
|
96
|
+
}
|
97
|
+
@options = defaults.merge(options)
|
98
|
+
@template_file = @options[:template_file]
|
99
|
+
|
100
|
+
# If we detect a language
|
101
|
+
if detect_language() != "text"
|
102
|
+
# then assign the detected language to `:language`
|
103
|
+
@options[:language] = detect_language()
|
104
|
+
# and look for some comment characters
|
105
|
+
@options[:comment_chars] = generate_comment_chars()
|
106
|
+
# If we didn't detect a language, but the user provided one, use it
|
107
|
+
# to look around for comment characters to override the default.
|
108
|
+
elsif @options[:language] != defaults[:language]
|
109
|
+
@options[:comment_chars] = generate_comment_chars()
|
110
|
+
end
|
111
|
+
@comment_pattern = Regexp.new("^\\s*#{@options[:comment_chars]}\s?")
|
112
|
+
|
113
|
+
@sections = highlight(split(parse(@data)))
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns `true` if `pygmentize` is available locally, `false` otherwise.
|
117
|
+
def pygmentize?
|
118
|
+
# Memoize the result, we'll call this a few times
|
119
|
+
@_pygmentize ||= ENV['PATH'].split(':').any? { |dir| executable?("#{dir}/pygmentize") }
|
120
|
+
end
|
121
|
+
|
122
|
+
# If `pygmentize` is available, we can use it to autodetect a file's
|
123
|
+
# language based on its filename. Filenames without extensions, or with
|
124
|
+
# extensions that `pygmentize` doesn't understand will return `text`.
|
125
|
+
# We'll also return `text` if `pygmentize` isn't available.
|
126
|
+
#
|
127
|
+
# We'll memoize the result, as we'll call this a few times.
|
128
|
+
def detect_language
|
129
|
+
@_language ||= begin
|
130
|
+
if pygmentize?
|
131
|
+
lang = %x[pygmentize -N #{@file}].strip!
|
132
|
+
else
|
133
|
+
"text"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Given a file's language, we should be able to autopopulate the
|
139
|
+
# `comment_chars` variables for single-line comments. If we don't
|
140
|
+
# have comment characters on record for a given language, we'll
|
141
|
+
# use the user-provided `:comment_char` option (which defaults to
|
142
|
+
# `#`).
|
143
|
+
#
|
144
|
+
# Comment characters are listed as:
|
145
|
+
#
|
146
|
+
# { :single => "//", :multi_start => "/**", :multi_middle => "*", :multi_end => "*/" }
|
147
|
+
#
|
148
|
+
# `:single` denotes the leading character of a single-line comment.
|
149
|
+
# `:multi_start` denotes the string that should appear alone on a
|
150
|
+
# line of code to begin a block of documentation. `:multi_middle`
|
151
|
+
# denotes the leading character of block comment content, and
|
152
|
+
# `:multi_end` is the string that ought appear alone on a line to
|
153
|
+
# close a block of documentation. That is:
|
154
|
+
#
|
155
|
+
# /** [:multi][:start]
|
156
|
+
# * [:multi][:middle]
|
157
|
+
# * [:multi][:middle]
|
158
|
+
# * [:multi][:middle]
|
159
|
+
# */ [:multi][:end]
|
160
|
+
#
|
161
|
+
# If a language only has one type of comment, the missing type
|
162
|
+
# should be assigned `nil`.
|
163
|
+
#
|
164
|
+
# At the moment, we're only returning `:single`. Consider this
|
165
|
+
# groundwork for block comment parsing.
|
166
|
+
def generate_comment_chars
|
167
|
+
@_commentchar ||= begin
|
168
|
+
language = @options[:language]
|
169
|
+
comment_styles = {
|
170
|
+
"bash" => { :single => "#", :multi => nil },
|
171
|
+
"c" => { :single => "//", :multi => { :start => "/**", :middle => "*", :end => "*/" } },
|
172
|
+
"coffee-script" => { :single => "#", :multi => { :start => "###", :middle => nil, :end => "###" } },
|
173
|
+
"cpp" => { :single => "//", :multi => { :start => "/**", :middle => "*", :end => "*/" } },
|
174
|
+
"java" => { :single => "//", :multi => { :start => "/**", :middle => "*", :end => "*/" } },
|
175
|
+
"js" => { :single => "//", :multi => { :start => "/**", :middle => "*", :end => "*/" } },
|
176
|
+
"php" => { :single => "//", :multi => { :start => "/**", :middle => "*", :end => "*/" } },
|
177
|
+
"lua" => { :single => "--", :multi => nil },
|
178
|
+
"python" => { :single => "#", :multi => { :start => '"""', :middle => nil, :end => '"""' } },
|
179
|
+
"ruby" => { :single => "#", :multi => nil },
|
180
|
+
"scheme" => { :single => ";;", :multi => nil },
|
181
|
+
}
|
182
|
+
|
183
|
+
if comment_styles[language]
|
184
|
+
comment_styles[language][:single]
|
185
|
+
else
|
186
|
+
@options[:comment_chars]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# The filename as given to `Rocco.new`.
|
192
|
+
attr_reader :file
|
193
|
+
|
194
|
+
# The merged options array
|
195
|
+
attr_reader :options
|
196
|
+
|
197
|
+
# A list of two-tuples representing each *section* of the source file. Each
|
198
|
+
# item in the list has the form: `[docs_html, code_html]`, where both
|
199
|
+
# elements are strings containing the documentation and source code HTML,
|
200
|
+
# respectively.
|
201
|
+
attr_reader :sections
|
202
|
+
|
203
|
+
# A list of all source filenames included in the documentation set. Useful
|
204
|
+
# for building an index of other files.
|
205
|
+
attr_reader :sources
|
206
|
+
|
207
|
+
# An absolute path to a file that ought be used as a template for the
|
208
|
+
# HTML-rendered documentation.
|
209
|
+
attr_reader :template_file
|
210
|
+
|
211
|
+
# Generate HTML output for the entire document.
|
212
|
+
require 'rocco/layout'
|
213
|
+
def to_html
|
214
|
+
Rocco::Layout.new(self, @template_file).render
|
215
|
+
end
|
216
|
+
|
217
|
+
#### Internal Parsing and Highlighting
|
218
|
+
|
219
|
+
# Parse the raw file data into a list of two-tuples. Each tuple has the
|
220
|
+
# form `[docs, code]` where both elements are arrays containing the
|
221
|
+
# raw lines parsed from the input file. The first line is ignored if it
|
222
|
+
# is a shebang line.
|
223
|
+
def parse(data)
|
224
|
+
sections = []
|
225
|
+
docs, code = [], []
|
226
|
+
lines = data.split("\n")
|
227
|
+
lines.shift if lines[0] =~ /^\#\!/
|
228
|
+
lines.each do |line|
|
229
|
+
case line
|
230
|
+
when @comment_pattern
|
231
|
+
if code.any?
|
232
|
+
sections << [docs, code]
|
233
|
+
docs, code = [], []
|
234
|
+
end
|
235
|
+
docs << line
|
236
|
+
else
|
237
|
+
code << line
|
238
|
+
end
|
239
|
+
end
|
240
|
+
sections << [docs, code] if docs.any? || code.any?
|
241
|
+
sections
|
242
|
+
end
|
243
|
+
|
244
|
+
# Take the list of paired *sections* two-tuples and split into two
|
245
|
+
# separate lists: one holding the comments with leaders removed and
|
246
|
+
# one with the code blocks.
|
247
|
+
def split(sections)
|
248
|
+
docs_blocks, code_blocks = [], []
|
249
|
+
sections.each do |docs,code|
|
250
|
+
docs_blocks << docs.map { |line| line.sub(@comment_pattern, '') }.join("\n")
|
251
|
+
code_blocks << code.map do |line|
|
252
|
+
tabs = line.match(/^(\t+)/)
|
253
|
+
tabs ? line.sub(/^\t+/, ' ' * tabs.captures[0].length) : line
|
254
|
+
end.join("\n")
|
255
|
+
end
|
256
|
+
[docs_blocks, code_blocks]
|
257
|
+
end
|
258
|
+
|
259
|
+
# Take the result of `split` and apply Markdown formatting to comments and
|
260
|
+
# syntax highlighting to source code.
|
261
|
+
def highlight(blocks)
|
262
|
+
docs_blocks, code_blocks = blocks
|
263
|
+
|
264
|
+
# Combine all docs blocks into a single big markdown document with section
|
265
|
+
# dividers and run through the Markdown processor. Then split it back out
|
266
|
+
# into separate sections.
|
267
|
+
markdown = docs_blocks.join("\n\n##### DIVIDER\n\n")
|
268
|
+
docs_html = Markdown.new(markdown, :smart).
|
269
|
+
to_html.
|
270
|
+
split(/\n*<h5>DIVIDER<\/h5>\n*/m)
|
271
|
+
|
272
|
+
# Combine all code blocks into a single big stream and run through either
|
273
|
+
# `pygmentize(1)` or <http://pygments.appspot.com>
|
274
|
+
code_stream = code_blocks.join("\n\n#{@options[:comment_chars]} DIVIDER\n\n")
|
275
|
+
|
276
|
+
if pygmentize?
|
277
|
+
code_html = highlight_pygmentize(code_stream)
|
278
|
+
else
|
279
|
+
code_html = highlight_webservice(code_stream)
|
280
|
+
end
|
281
|
+
|
282
|
+
# Do some post-processing on the pygments output to split things back
|
283
|
+
# into sections and remove partial `<pre>` blocks.
|
284
|
+
code_html = code_html.
|
285
|
+
split(/\n*<span class="c.?">#{@options[:comment_chars]} DIVIDER<\/span>\n*/m).
|
286
|
+
map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
|
287
|
+
map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') }
|
288
|
+
|
289
|
+
# Lastly, combine the docs and code lists back into a list of two-tuples.
|
290
|
+
docs_html.zip(code_html)
|
291
|
+
end
|
292
|
+
|
293
|
+
# We `popen` a read/write pygmentize process in the parent and
|
294
|
+
# then fork off a child process to write the input.
|
295
|
+
def highlight_pygmentize(code)
|
296
|
+
code_html = nil
|
297
|
+
open("|pygmentize -l #{@options[:language]} -O encoding=utf-8 -f html", 'r+') do |fd|
|
298
|
+
pid =
|
299
|
+
fork {
|
300
|
+
fd.close_read
|
301
|
+
fd.write code
|
302
|
+
fd.close_write
|
303
|
+
exit!
|
304
|
+
}
|
305
|
+
fd.close_write
|
306
|
+
code_html = fd.read
|
307
|
+
fd.close_read
|
308
|
+
Process.wait(pid)
|
309
|
+
end
|
310
|
+
|
311
|
+
code_html
|
312
|
+
end
|
313
|
+
|
314
|
+
# Pygments is not one of those things that's trivial for a ruby user to install,
|
315
|
+
# so we'll fall back on a webservice to highlight the code if it isn't available.
|
316
|
+
def highlight_webservice(code)
|
317
|
+
Net::HTTP.post_form(
|
318
|
+
URI.parse('http://pygments.appspot.com/'),
|
319
|
+
{'lang' => @options[:language], 'code' => code}
|
320
|
+
).body
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# And that's it.
|
data/rocco.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
3
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
4
|
+
|
5
|
+
s.name = 'rocco-obb'
|
6
|
+
s.version = '0.5'
|
7
|
+
s.date = '2010-09-10'
|
8
|
+
|
9
|
+
s.description = "Docco in Ruby"
|
10
|
+
s.summary = s.description
|
11
|
+
|
12
|
+
s.authors = ["Ryan Tomayko"]
|
13
|
+
s.email = "r@tomayko.com"
|
14
|
+
|
15
|
+
# = MANIFEST =
|
16
|
+
s.files = %w[
|
17
|
+
COPYING
|
18
|
+
README
|
19
|
+
Rakefile
|
20
|
+
bin/rocco
|
21
|
+
lib/rocco.rb
|
22
|
+
lib/rocco/layout.mustache
|
23
|
+
lib/rocco/layout.rb
|
24
|
+
lib/rocco/tasks.rb
|
25
|
+
rocco.gemspec
|
26
|
+
]
|
27
|
+
# = MANIFEST =
|
28
|
+
|
29
|
+
s.executables = ["rocco"]
|
30
|
+
|
31
|
+
s.test_files = s.files.select {|path| path =~ /^test\/.*_test.rb/}
|
32
|
+
s.add_dependency 'rdiscount'
|
33
|
+
s.add_dependency 'mustache'
|
34
|
+
|
35
|
+
s.has_rdoc = false
|
36
|
+
s.homepage = "http://github.com/oneblackbear/rocco/"
|
37
|
+
s.require_paths = %w[lib]
|
38
|
+
s.rubygems_version = '1.1.1'
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rocco-obb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 1
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 5
|
9
|
+
version: "0.5"
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Ryan Tomayko
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-09-10 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rdiscount
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: mustache
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
description: Docco in Ruby
|
49
|
+
email: r@tomayko.com
|
50
|
+
executables:
|
51
|
+
- rocco
|
52
|
+
extensions: []
|
53
|
+
|
54
|
+
extra_rdoc_files: []
|
55
|
+
|
56
|
+
files:
|
57
|
+
- COPYING
|
58
|
+
- README
|
59
|
+
- Rakefile
|
60
|
+
- bin/rocco
|
61
|
+
- lib/rocco.rb
|
62
|
+
- lib/rocco/layout.mustache
|
63
|
+
- lib/rocco/layout.rb
|
64
|
+
- lib/rocco/tasks.rb
|
65
|
+
- rocco.gemspec
|
66
|
+
has_rdoc: true
|
67
|
+
homepage: http://github.com/oneblackbear/rocco/
|
68
|
+
licenses: []
|
69
|
+
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
hash: 3
|
81
|
+
segments:
|
82
|
+
- 0
|
83
|
+
version: "0"
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
hash: 3
|
90
|
+
segments:
|
91
|
+
- 0
|
92
|
+
version: "0"
|
93
|
+
requirements: []
|
94
|
+
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 1.3.7
|
97
|
+
signing_key:
|
98
|
+
specification_version: 2
|
99
|
+
summary: Docco in Ruby
|
100
|
+
test_files: []
|
101
|
+
|