dkastner-rocco 0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.md +63 -0
- data/COPYING +18 -0
- data/README +23 -0
- data/Rakefile +115 -0
- data/bin/rocco +100 -0
- data/lib/rocco.rb +526 -0
- data/lib/rocco/layout.mustache +46 -0
- data/lib/rocco/layout.rb +54 -0
- data/lib/rocco/tasks.rb +123 -0
- data/rocco.gemspec +54 -0
- data/test/fixtures/issue10.iso-8859-1.rb +1 -0
- data/test/fixtures/issue10.utf-8.rb +1 -0
- data/test/helper.rb +20 -0
- data/test/suite.rb +5 -0
- data/test/test_basics.rb +63 -0
- data/test/test_block_comments.rb +101 -0
- data/test/test_comment_normalization.rb +25 -0
- data/test/test_commentchar_detection.rb +28 -0
- data/test/test_descriptive_section_names.rb +30 -0
- data/test/test_language_detection.rb +27 -0
- data/test/test_reported_issues.rb +86 -0
- data/test/test_skippable_lines.rb +64 -0
- data/test/test_source_list.rb +29 -0
- metadata +111 -0
data/CHANGES.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
CHANGES
|
2
|
+
=======
|
3
|
+
|
4
|
+
0.6 (2011-03-05)
|
5
|
+
----------------
|
6
|
+
|
7
|
+
This release brought to you almost entirely
|
8
|
+
by [mikewest](http://github.com/mikewest).
|
9
|
+
|
10
|
+
### Features
|
11
|
+
|
12
|
+
* Added `-t`/`--template` CLI option that allows you to specify a Mustache
|
13
|
+
template that ought be used when rendering the final documentation.
|
14
|
+
(Issue #16)
|
15
|
+
|
16
|
+
* More variables in templates:
|
17
|
+
* `docs?`: True if `docs` contains text of any sort, False if it's empty.
|
18
|
+
* `code?`: True if `code` contains text of any sort, False if it's empty.
|
19
|
+
* `empty?`: True if both `code` and `docs` are empty. False otherwise.
|
20
|
+
* `header?`: True if `docs` contains _only_ a HTML header. False otherwise.
|
21
|
+
|
22
|
+
* Test suite! (Run `rake test`)
|
23
|
+
|
24
|
+
* Autodetect file's language if Pygments is installed locally (Issue #19)
|
25
|
+
|
26
|
+
* Autopopulate comment characters for known languages (Issue #20)
|
27
|
+
|
28
|
+
* Correctly parse block comments (Issue #22)
|
29
|
+
|
30
|
+
* Stripping encoding definitions from Ruby and Python files in the same
|
31
|
+
way we strip shebang lines (Issue #21)
|
32
|
+
|
33
|
+
* Adjusting section IDs to contain descriptive test from headers. A header
|
34
|
+
section's ID might be `section-Header_text_goes_here` for friendlier URLs.
|
35
|
+
Other section IDs will remain the same (`section-2` will stay
|
36
|
+
`section-2`). (Issue #28)
|
37
|
+
|
38
|
+
### Bugs Fixed
|
39
|
+
|
40
|
+
* Docco's CSS changed: we updated Rocco's HTML accordingly, and pinned
|
41
|
+
the CSS file to Docco's 0.3.0 tag. (Issues #12 and #23)
|
42
|
+
|
43
|
+
* Fixed code highlighting for shell scripts (among others) (Issue #13)
|
44
|
+
|
45
|
+
* Fixed buggy regex for comment char stripping (Issue #15)
|
46
|
+
|
47
|
+
* Specifying UTF-8 encoding for Pygments (Issue #10)
|
48
|
+
|
49
|
+
* Extensionless file support (thanks to [Vasily Polovnyov][vast] for the
|
50
|
+
fix!) (Issue #24)
|
51
|
+
|
52
|
+
* Fixing language support for Pygments webservice (Issue #11)
|
53
|
+
|
54
|
+
* The source jumplist now generates correctly relative URLs (Issue #26)
|
55
|
+
|
56
|
+
* Fixed an issue with using mustache's `template_path=` incorrectly.
|
57
|
+
|
58
|
+
[vast]: https://github.com/vast
|
59
|
+
|
60
|
+
0.5
|
61
|
+
---
|
62
|
+
|
63
|
+
Rocco 0.5 emerged from the hazy mists, complete and unfettered by history.
|
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/suite.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/lib/rocco.html'
|
34
|
+
end
|
35
|
+
|
36
|
+
# Make index.html a copy of rocco.html
|
37
|
+
file 'docs/index.html' => 'docs/lib/rocco.html' do |f|
|
38
|
+
cp 'docs/lib/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,100 @@
|
|
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
|
+
#/ -d, --docblocks Parse Docblock @annotations in comments
|
12
|
+
#/ --help Show this help message
|
13
|
+
|
14
|
+
require 'optparse'
|
15
|
+
require 'fileutils'
|
16
|
+
|
17
|
+
# Write usage message to stdout and exit.
|
18
|
+
def usage(stream=$stderr, status=1)
|
19
|
+
stream.puts File.readlines(__FILE__).
|
20
|
+
grep(/^#\//).
|
21
|
+
map { |line| line.sub(/^#. ?/, '') }.
|
22
|
+
join
|
23
|
+
exit status
|
24
|
+
end
|
25
|
+
|
26
|
+
# Like `Kernel#abort` but writes a note encouraging the user to consult
|
27
|
+
# `rocco --help` for more information.
|
28
|
+
def abort_with_note(message=nil)
|
29
|
+
$stderr.puts message if message
|
30
|
+
abort "See `rocco --help' for usage information."
|
31
|
+
end
|
32
|
+
|
33
|
+
# Parse command line options, aborting if anything goes wrong.
|
34
|
+
output_dir = '.'
|
35
|
+
sources = []
|
36
|
+
options = {}
|
37
|
+
ARGV.options { |o|
|
38
|
+
o.program_name = File.basename($0)
|
39
|
+
o.on("-o", "--output=DIR") { |dir| output_dir = dir }
|
40
|
+
o.on("-l", "--language=LANG") { |lang| options[:language] = lang }
|
41
|
+
o.on("-c", "--comment-chars=CHARS") { |chars| options[:comment_chars] = Regexp.escape(chars) }
|
42
|
+
o.on("-t", "--template=TEMPLATE") { |template| options[:template_file] = template }
|
43
|
+
o.on("-d", "--docblocks") { options[:docblocks] = true }
|
44
|
+
o.on_tail("-h", "--help") { usage($stdout, 0) }
|
45
|
+
o.parse!
|
46
|
+
} or abort_with_note
|
47
|
+
|
48
|
+
# Use http://pygments.appspot.com in case `pygmentize(1)` isn't available.
|
49
|
+
if ! ENV['PATH'].split(':').any? { |dir| File.exist?("#{dir}/pygmentize") }
|
50
|
+
unless options[:webservice]
|
51
|
+
$stderr.puts "pygmentize not in PATH; using pygments.appspot.com instead"
|
52
|
+
options[:webservice] = true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Eat sources from ARGV.
|
57
|
+
sources << ARGV.shift while ARGV.any?
|
58
|
+
|
59
|
+
# Make sure we have some files to work with.
|
60
|
+
if sources.empty?
|
61
|
+
abort_with_note "#{File.basename($0)}: no input <file>s given"
|
62
|
+
end
|
63
|
+
|
64
|
+
# What a fucking mess. Most of this is duplicated in rocco.rb too.
|
65
|
+
libdir = File.expand_path('../../lib', __FILE__).sub(/^#{Dir.pwd}\//, '')
|
66
|
+
begin
|
67
|
+
require 'rdiscount'
|
68
|
+
require 'rocco'
|
69
|
+
rescue LoadError
|
70
|
+
case $!.to_s
|
71
|
+
when /rdiscount/
|
72
|
+
if !defined?(Gem)
|
73
|
+
warn "warn: #$!. trying again with rubygems"
|
74
|
+
require 'rubygems'
|
75
|
+
retry
|
76
|
+
else
|
77
|
+
require 'bluecloth'
|
78
|
+
Markdown = BlueCloth
|
79
|
+
$LOADED_FEATURES << 'rdiscount.rb'
|
80
|
+
retry
|
81
|
+
end
|
82
|
+
when /rocco/
|
83
|
+
if !$:.include?(libdir)
|
84
|
+
warn "warn: #$!. trying again with #{libdir} on load path"
|
85
|
+
$:.unshift(libdir)
|
86
|
+
retry
|
87
|
+
end
|
88
|
+
end
|
89
|
+
raise
|
90
|
+
end
|
91
|
+
|
92
|
+
# Run each file through Rocco and write output.
|
93
|
+
sources.each do |filename|
|
94
|
+
rocco = Rocco.new(filename, sources, options)
|
95
|
+
dest = filename.sub(Regexp.new("#{File.extname(filename)}$"),".html")
|
96
|
+
dest = File.join(output_dir, dest) if output_dir != '.'
|
97
|
+
puts "rocco: #{filename} -> #{dest}"
|
98
|
+
FileUtils.mkdir_p File.dirname(dest)
|
99
|
+
File.open(dest, 'wb') { |fd| fd.write(rocco.to_html) }
|
100
|
+
end
|
data/lib/rocco.rb
ADDED
@@ -0,0 +1,526 @@
|
|
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
|
+
if !ENV['PATH'].split(':').any? { |dir| File.executable?("#{dir}/pygmentize") }
|
56
|
+
warn "WARNING: Pygments not found. Using webservice."
|
57
|
+
end
|
58
|
+
|
59
|
+
#### Public Interface
|
60
|
+
|
61
|
+
# `Rocco.new` takes a source `filename`, an optional list of source filenames
|
62
|
+
# for other documentation sources, an `options` hash, and an optional `block`.
|
63
|
+
# The `options` hash respects three members:
|
64
|
+
#
|
65
|
+
# * `:language`: specifies which Pygments lexer to use if one can't be
|
66
|
+
# auto-detected from the filename. _Defaults to `ruby`_.
|
67
|
+
#
|
68
|
+
# * `:comment_chars`, which specifies the comment characters of the
|
69
|
+
# target language. _Defaults to `#`_.
|
70
|
+
#
|
71
|
+
# * `:template_file`, which specifies a external template file to use
|
72
|
+
# when rendering the final, highlighted file via Mustache. _Defaults
|
73
|
+
# to `nil` (that is, Mustache will use `./lib/rocco/layout.mustache`)_.
|
74
|
+
#
|
75
|
+
class Rocco
|
76
|
+
VERSION = '0.6'
|
77
|
+
|
78
|
+
def initialize(filename, sources=[], options={}, &block)
|
79
|
+
@file = filename
|
80
|
+
@sources = sources
|
81
|
+
|
82
|
+
# When `block` is given, it must read the contents of the file using
|
83
|
+
# whatever means necessary and return it as a string. With no `block`,
|
84
|
+
# the file is read to retrieve data.
|
85
|
+
@data =
|
86
|
+
if block_given?
|
87
|
+
yield
|
88
|
+
else
|
89
|
+
File.read(filename)
|
90
|
+
end
|
91
|
+
|
92
|
+
defaults = {
|
93
|
+
:language => 'ruby',
|
94
|
+
:comment_chars => '#',
|
95
|
+
:template_file => nil
|
96
|
+
}
|
97
|
+
@options = defaults.merge(options)
|
98
|
+
|
99
|
+
# If we detect a language
|
100
|
+
if detect_language() != "text"
|
101
|
+
# then assign the detected language to `:language`, and look for
|
102
|
+
# comment characters based on that language
|
103
|
+
@options[:language] = detect_language()
|
104
|
+
@options[:comment_chars] = generate_comment_chars()
|
105
|
+
|
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
|
+
|
111
|
+
# If neither is true, then convert the default comment character string
|
112
|
+
# into the comment_char syntax (we'll discuss that syntax in detail when
|
113
|
+
# we get to `generate_comment_chars()` in a moment.
|
114
|
+
else
|
115
|
+
@options[:comment_chars] = {
|
116
|
+
:single => @options[:comment_chars],
|
117
|
+
:multi => nil
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
# Turn `:comment_chars` into a regex matching a series of spaces, the
|
122
|
+
# `:comment_chars` string, and the an optional space. We'll use that
|
123
|
+
# to detect single-line comments.
|
124
|
+
@comment_pattern =
|
125
|
+
Regexp.new("^\\s*#{@options[:comment_chars][:single]}\s?")
|
126
|
+
|
127
|
+
# `parse()` the file contents stored in `@data`. Run the result through
|
128
|
+
# `split()` and that result through `highlight()` to generate the final
|
129
|
+
# section list.
|
130
|
+
@sections = highlight(split(parse(@data)))
|
131
|
+
end
|
132
|
+
|
133
|
+
# The filename as given to `Rocco.new`.
|
134
|
+
attr_reader :file
|
135
|
+
|
136
|
+
# The merged options array
|
137
|
+
attr_reader :options
|
138
|
+
|
139
|
+
# A list of two-tuples representing each *section* of the source file. Each
|
140
|
+
# item in the list has the form: `[docs_html, code_html]`, where both
|
141
|
+
# elements are strings containing the documentation and source code HTML,
|
142
|
+
# respectively.
|
143
|
+
attr_reader :sections
|
144
|
+
|
145
|
+
# A list of all source filenames included in the documentation set. Useful
|
146
|
+
# for building an index of other files.
|
147
|
+
attr_reader :sources
|
148
|
+
|
149
|
+
# Generate HTML output for the entire document.
|
150
|
+
require 'rocco/layout'
|
151
|
+
def to_html
|
152
|
+
Rocco::Layout.new(self, @options[:template_file]).render
|
153
|
+
end
|
154
|
+
|
155
|
+
# Helper Functions
|
156
|
+
# ----------------
|
157
|
+
|
158
|
+
# Returns `true` if `pygmentize` is available locally, `false` otherwise.
|
159
|
+
def pygmentize?
|
160
|
+
@_pygmentize ||= ENV['PATH'].split(':').
|
161
|
+
any? { |dir| File.executable?("#{dir}/pygmentize") }
|
162
|
+
end
|
163
|
+
|
164
|
+
# If `pygmentize` is available, we can use it to autodetect a file's
|
165
|
+
# language based on its filename. Filenames without extensions, or with
|
166
|
+
# extensions that `pygmentize` doesn't understand will return `text`.
|
167
|
+
# We'll also return `text` if `pygmentize` isn't available.
|
168
|
+
#
|
169
|
+
# We'll memoize the result, as we'll call this a few times.
|
170
|
+
def detect_language
|
171
|
+
@_language ||=
|
172
|
+
if pygmentize?
|
173
|
+
%x[pygmentize -N #{@file}].strip.split('+').first
|
174
|
+
else
|
175
|
+
"text"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Given a file's language, we should be able to autopopulate the
|
180
|
+
# `comment_chars` variables for single-line comments. If we don't
|
181
|
+
# have comment characters on record for a given language, we'll
|
182
|
+
# use the user-provided `:comment_char` option (which defaults to
|
183
|
+
# `#`).
|
184
|
+
#
|
185
|
+
# Comment characters are listed as:
|
186
|
+
#
|
187
|
+
# { :single => "//",
|
188
|
+
# :multi_start => "/**",
|
189
|
+
# :multi_middle => "*",
|
190
|
+
# :multi_end => "*/" }
|
191
|
+
#
|
192
|
+
# `:single` denotes the leading character of a single-line comment.
|
193
|
+
# `:multi_start` denotes the string that should appear alone on a
|
194
|
+
# line of code to begin a block of documentation. `:multi_middle`
|
195
|
+
# denotes the leading character of block comment content, and
|
196
|
+
# `:multi_end` is the string that ought appear alone on a line to
|
197
|
+
# close a block of documentation. That is:
|
198
|
+
#
|
199
|
+
# /** [:multi][:start]
|
200
|
+
# * [:multi][:middle]
|
201
|
+
# ...
|
202
|
+
# * [:multi][:middle]
|
203
|
+
# */ [:multi][:end]
|
204
|
+
#
|
205
|
+
# If a language only has one type of comment, the missing type
|
206
|
+
# should be assigned `nil`.
|
207
|
+
#
|
208
|
+
# At the moment, we're only returning `:single`. Consider this
|
209
|
+
# groundwork for block comment parsing.
|
210
|
+
C_STYLE_COMMENTS = {
|
211
|
+
:single => "//",
|
212
|
+
:multi => { :start => "/**", :middle => "*", :end => "*/" },
|
213
|
+
:heredoc => nil
|
214
|
+
}
|
215
|
+
COMMENT_STYLES = {
|
216
|
+
"bash" => { :single => "#", :multi => nil },
|
217
|
+
"c" => C_STYLE_COMMENTS,
|
218
|
+
"coffee-script" => {
|
219
|
+
:single => "#",
|
220
|
+
:multi => { :start => "###", :middle => nil, :end => "###" },
|
221
|
+
:heredoc => nil
|
222
|
+
},
|
223
|
+
"cpp" => C_STYLE_COMMENTS,
|
224
|
+
"csharp" => C_STYLE_COMMENTS,
|
225
|
+
"css" => {
|
226
|
+
:single => nil,
|
227
|
+
:multi => { :start => "/**", :middle => "*", :end => "*/" },
|
228
|
+
:heredoc => nil
|
229
|
+
},
|
230
|
+
"html" => {
|
231
|
+
:single => nil,
|
232
|
+
:multi => { :start => '<!--', :middle => nil, :end => '-->' },
|
233
|
+
:heredoc => nil
|
234
|
+
},
|
235
|
+
"java" => C_STYLE_COMMENTS,
|
236
|
+
"js" => C_STYLE_COMMENTS,
|
237
|
+
"lua" => {
|
238
|
+
:single => "--",
|
239
|
+
:multi => nil,
|
240
|
+
:heredoc => nil
|
241
|
+
},
|
242
|
+
"php" => C_STYLE_COMMENTS,
|
243
|
+
"python" => {
|
244
|
+
:single => "#",
|
245
|
+
:multi => { :start => '"""', :middle => nil, :end => '"""' },
|
246
|
+
:heredoc => nil
|
247
|
+
},
|
248
|
+
"rb" => {
|
249
|
+
:single => "#",
|
250
|
+
:multi => { :start => '=begin', :middle => nil, :end => '=end' },
|
251
|
+
:heredoc => "<<-"
|
252
|
+
},
|
253
|
+
"scheme" => { :single => ";;", :multi => nil, :heredoc => nil },
|
254
|
+
"xml" => {
|
255
|
+
:single => nil,
|
256
|
+
:multi => { :start => '<!--', :middle => nil, :end => '-->' },
|
257
|
+
:heredoc => nil
|
258
|
+
},
|
259
|
+
}
|
260
|
+
|
261
|
+
def generate_comment_chars
|
262
|
+
@_commentchar ||=
|
263
|
+
if COMMENT_STYLES[@options[:language]]
|
264
|
+
COMMENT_STYLES[@options[:language]]
|
265
|
+
else
|
266
|
+
{ :single => @options[:comment_chars], :multi => nil, :heredoc => nil }
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Internal Parsing and Highlighting
|
271
|
+
# ---------------------------------
|
272
|
+
|
273
|
+
# Parse the raw file data into a list of two-tuples. Each tuple has the
|
274
|
+
# form `[docs, code]` where both elements are arrays containing the
|
275
|
+
# raw lines parsed from the input file, comment characters stripped.
|
276
|
+
def parse(data)
|
277
|
+
sections = []
|
278
|
+
docs, code = [], []
|
279
|
+
lines = data.split("\n")
|
280
|
+
|
281
|
+
# The first line is ignored if it is a shebang line. We also ignore the
|
282
|
+
# PEP 263 encoding information in python sourcefiles, and the similar ruby
|
283
|
+
# 1.9 syntax.
|
284
|
+
lines.shift if lines[0] =~ /^\#\!/
|
285
|
+
lines.shift if lines[0] =~ /coding[:=]\s*[-\w.]+/ &&
|
286
|
+
[ "python", "rb" ].include?(@options[:language])
|
287
|
+
|
288
|
+
# To detect both block comments and single-line comments, we'll set
|
289
|
+
# up a tiny state machine, and loop through each line of the file.
|
290
|
+
# This requires an `in_comment_block` boolean, and a few regular
|
291
|
+
# expressions for line tests. We'll do the same for fake heredoc parsing.
|
292
|
+
in_comment_block = false
|
293
|
+
in_heredoc = false
|
294
|
+
single_line_comment, block_comment_start, block_comment_mid, block_comment_end =
|
295
|
+
nil, nil, nil, nil
|
296
|
+
if not @options[:comment_chars][:single].nil?
|
297
|
+
single_line_comment = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:single])}\\s?")
|
298
|
+
end
|
299
|
+
if not @options[:comment_chars][:multi].nil?
|
300
|
+
block_comment_start = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*$")
|
301
|
+
block_comment_end = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
|
302
|
+
block_comment_one_liner = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*(.*?)\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
|
303
|
+
block_comment_start_with = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*(.*?)$")
|
304
|
+
block_comment_end_with = Regexp.new("\\s*(.*?)\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
|
305
|
+
if @options[:comment_chars][:multi][:middle]
|
306
|
+
block_comment_mid = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:middle])}\\s?")
|
307
|
+
end
|
308
|
+
end
|
309
|
+
if not @options[:comment_chars][:heredoc].nil?
|
310
|
+
heredoc_start = Regexp.new("#{Regexp.escape(@options[:comment_chars][:heredoc])}(\\S+)$")
|
311
|
+
end
|
312
|
+
lines.each do |line|
|
313
|
+
# If we're currently in a comment block, check whether the line matches
|
314
|
+
# the _end_ of a comment block or the _end_ of a comment block with a
|
315
|
+
# comment.
|
316
|
+
if in_comment_block
|
317
|
+
if block_comment_end && line.match(block_comment_end)
|
318
|
+
in_comment_block = false
|
319
|
+
elsif block_comment_end_with && line.match(block_comment_end_with)
|
320
|
+
in_comment_block = false
|
321
|
+
docs << line.match(block_comment_end_with).captures.first.
|
322
|
+
sub(block_comment_mid || '', '')
|
323
|
+
else
|
324
|
+
docs << line.sub(block_comment_mid || '', '')
|
325
|
+
end
|
326
|
+
# If we're currently in a heredoc, we're looking for the end of the
|
327
|
+
# heredoc, and everything it contains is code.
|
328
|
+
elsif in_heredoc
|
329
|
+
if line.match(Regexp.new("^#{Regexp.escape(in_heredoc)}$"))
|
330
|
+
in_heredoc = false
|
331
|
+
end
|
332
|
+
code << line
|
333
|
+
# Otherwise, check whether the line starts a heredoc. If so, note the end
|
334
|
+
# pattern, and the line is code. Otherwise check whether the line matches
|
335
|
+
# the beginning of a block, or a single-line comment all on it's lonesome.
|
336
|
+
# In either case, if there's code, start a new section.
|
337
|
+
else
|
338
|
+
if heredoc_start && line.match(heredoc_start)
|
339
|
+
in_heredoc = $1
|
340
|
+
code << line
|
341
|
+
elsif block_comment_one_liner && line.match(block_comment_one_liner)
|
342
|
+
if code.any?
|
343
|
+
sections << [docs, code]
|
344
|
+
docs, code = [], []
|
345
|
+
end
|
346
|
+
docs << line.match(block_comment_one_liner).captures.first
|
347
|
+
elsif block_comment_start && line.match(block_comment_start)
|
348
|
+
in_comment_block = true
|
349
|
+
if code.any?
|
350
|
+
sections << [docs, code]
|
351
|
+
docs, code = [], []
|
352
|
+
end
|
353
|
+
elsif block_comment_start_with && line.match(block_comment_start_with)
|
354
|
+
in_comment_block = true
|
355
|
+
if code.any?
|
356
|
+
sections << [docs, code]
|
357
|
+
docs, code = [], []
|
358
|
+
end
|
359
|
+
docs << line.match(block_comment_start_with).captures.first
|
360
|
+
elsif single_line_comment && line.match(single_line_comment)
|
361
|
+
if code.any?
|
362
|
+
sections << [docs, code]
|
363
|
+
docs, code = [], []
|
364
|
+
end
|
365
|
+
docs << line.sub(single_line_comment || '', '')
|
366
|
+
else
|
367
|
+
code << line
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
sections << [docs, code] if docs.any? || code.any?
|
372
|
+
normalize_leading_spaces(sections)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Normalizes documentation whitespace by checking for leading whitespace,
|
376
|
+
# removing it, and then removing the same amount of whitespace from each
|
377
|
+
# succeeding line. That is:
|
378
|
+
#
|
379
|
+
# def func():
|
380
|
+
# """
|
381
|
+
# Comment 1
|
382
|
+
# Comment 2
|
383
|
+
# """
|
384
|
+
# print "omg!"
|
385
|
+
#
|
386
|
+
# should yield a comment block of `Comment 1\nComment 2` and code of
|
387
|
+
# `def func():\n print "omg!"`
|
388
|
+
def normalize_leading_spaces(sections)
|
389
|
+
sections.map do |section|
|
390
|
+
if section.any? && section[0].any?
|
391
|
+
leading_space = section[0][0].match("^\s+")
|
392
|
+
if leading_space
|
393
|
+
section[0] =
|
394
|
+
section[0].map{ |line| line.sub(/^#{leading_space.to_s}/, '') }
|
395
|
+
end
|
396
|
+
end
|
397
|
+
section
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
# Take the list of paired *sections* two-tuples and split into two
|
402
|
+
# separate lists: one holding the comments with leaders removed and
|
403
|
+
# one with the code blocks.
|
404
|
+
def split(sections)
|
405
|
+
docs_blocks, code_blocks = [], []
|
406
|
+
sections.each do |docs,code|
|
407
|
+
docs_blocks << docs.join("\n")
|
408
|
+
code_blocks << code.map do |line|
|
409
|
+
tabs = line.match(/^(\t+)/)
|
410
|
+
tabs ? line.sub(/^\t+/, ' ' * tabs.captures[0].length) : line
|
411
|
+
end.join("\n")
|
412
|
+
end
|
413
|
+
[docs_blocks, code_blocks]
|
414
|
+
end
|
415
|
+
|
416
|
+
# Take a list of block comments and convert Docblock @annotations to
|
417
|
+
# Markdown syntax.
|
418
|
+
def docblock(docs)
|
419
|
+
docs.map do |doc|
|
420
|
+
doc.split("\n").map do |line|
|
421
|
+
line.match(/^@\w+/) ? line.sub(/^@(\w+)\s+/, '> **\1** ')+" " : line
|
422
|
+
end.join("\n")
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
# Take the result of `split` and apply Markdown formatting to comments and
|
427
|
+
# syntax highlighting to source code.
|
428
|
+
def highlight(blocks)
|
429
|
+
docs_blocks, code_blocks = blocks
|
430
|
+
|
431
|
+
# Pre-process Docblock @annotations.
|
432
|
+
if @options[:docblocks]
|
433
|
+
docs_blocks = docblock(docs_blocks)
|
434
|
+
end
|
435
|
+
|
436
|
+
# Combine all docs blocks into a single big markdown document with section
|
437
|
+
# dividers and run through the Markdown processor. Then split it back out
|
438
|
+
# into separate sections.
|
439
|
+
markdown = docs_blocks.join("\n\n##### DIVIDER\n\n")
|
440
|
+
docs_html = Markdown.new(markdown, :smart).
|
441
|
+
to_html.
|
442
|
+
split(/\n*<h5>DIVIDER<\/h5>\n*/m)
|
443
|
+
|
444
|
+
# Combine all code blocks into a single big stream with section dividers and
|
445
|
+
# run through either `pygmentize(1)` or <http://pygments.appspot.com>
|
446
|
+
span, espan = '<span class="c.?">', '</span>'
|
447
|
+
if @options[:comment_chars][:single]
|
448
|
+
front = @options[:comment_chars][:single]
|
449
|
+
divider_input = "\n\n#{front} DIVIDER\n\n"
|
450
|
+
divider_output = Regexp.new(
|
451
|
+
[ "\\n*",
|
452
|
+
span,
|
453
|
+
Regexp.escape(CGI.escapeHTML(front)),
|
454
|
+
' DIVIDER',
|
455
|
+
espan,
|
456
|
+
"\\n*"
|
457
|
+
].join, Regexp::MULTILINE
|
458
|
+
)
|
459
|
+
else
|
460
|
+
front = @options[:comment_chars][:multi][:start]
|
461
|
+
back = @options[:comment_chars][:multi][:end]
|
462
|
+
divider_input = "\n\n#{front}\nDIVIDER\n#{back}\n\n"
|
463
|
+
divider_output = Regexp.new(
|
464
|
+
[ "\\n*",
|
465
|
+
span, Regexp.escape(CGI.escapeHTML(front)), espan,
|
466
|
+
"\\n",
|
467
|
+
span, "DIVIDER", espan,
|
468
|
+
"\\n",
|
469
|
+
span, Regexp.escape(CGI.escapeHTML(back)), espan,
|
470
|
+
"\\n*"
|
471
|
+
].join, Regexp::MULTILINE
|
472
|
+
)
|
473
|
+
end
|
474
|
+
|
475
|
+
code_stream = code_blocks.join(divider_input)
|
476
|
+
|
477
|
+
code_html =
|
478
|
+
if pygmentize?
|
479
|
+
highlight_pygmentize(code_stream)
|
480
|
+
else
|
481
|
+
highlight_webservice(code_stream)
|
482
|
+
end
|
483
|
+
|
484
|
+
# Do some post-processing on the pygments output to split things back
|
485
|
+
# into sections and remove partial `<pre>` blocks.
|
486
|
+
code_html = code_html.
|
487
|
+
split(divider_output).
|
488
|
+
map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
|
489
|
+
map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') }
|
490
|
+
|
491
|
+
# Lastly, combine the docs and code lists back into a list of two-tuples.
|
492
|
+
docs_html.zip(code_html)
|
493
|
+
end
|
494
|
+
|
495
|
+
# We `popen` a read/write pygmentize process in the parent and
|
496
|
+
# then fork off a child process to write the input.
|
497
|
+
def highlight_pygmentize(code)
|
498
|
+
code_html = nil
|
499
|
+
open("|pygmentize -l #{@options[:language]} -O encoding=utf-8 -f html", 'r+') do |fd|
|
500
|
+
pid =
|
501
|
+
fork {
|
502
|
+
fd.close_read
|
503
|
+
fd.write code
|
504
|
+
fd.close_write
|
505
|
+
exit!
|
506
|
+
}
|
507
|
+
fd.close_write
|
508
|
+
code_html = fd.read
|
509
|
+
fd.close_read
|
510
|
+
Process.wait(pid)
|
511
|
+
end
|
512
|
+
|
513
|
+
code_html
|
514
|
+
end
|
515
|
+
|
516
|
+
# Pygments is not one of those things that's trivial for a ruby user to install,
|
517
|
+
# so we'll fall back on a webservice to highlight the code if it isn't available.
|
518
|
+
def highlight_webservice(code)
|
519
|
+
Net::HTTP.post_form(
|
520
|
+
URI.parse('http://pygments.appspot.com/'),
|
521
|
+
{'lang' => @options[:language], 'code' => code}
|
522
|
+
).body
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
# And that's it.
|