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