Compare commits
139 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1ce551581a | ||
|
b1bf8add92 | ||
|
dde147a171 | ||
|
a175735f0c | ||
|
57be0b7695 | ||
|
4f81074ad4 | ||
|
4c4ef24222 | ||
|
97a971897f | ||
|
d149a9bbf4 | ||
|
c4da54408a | ||
|
d98793dc7e | ||
|
9b65c1cd99 | ||
|
7ba75f0503 | ||
|
c1a5017468 | ||
|
58e7e4f7d8 | ||
|
3e9a14e896 | ||
|
9e88a3925c | ||
|
ba393d1d8c | ||
|
61d84c85c2 | ||
|
e727446de7 | ||
|
d4f527f2d6 | ||
|
6736281302 | ||
|
b0582c9deb | ||
|
6193734ee1 | ||
|
3ebbb82f14 | ||
|
c1afb8a0d4 | ||
|
fe09cbe0e3 | ||
|
c5d0f3ba43 | ||
|
d0690e6269 | ||
|
6c96fcb681 | ||
|
c39dbd7816 | ||
|
8dd30853aa | ||
|
e5e0d53f18 | ||
|
d495074320 | ||
|
b993200fc1 | ||
|
66e8f9a319 | ||
|
f9dd29eafa | ||
|
3f1a8dfef3 | ||
|
19506b13db | ||
|
3224735671 | ||
|
a841026e52 | ||
|
f40e57baae | ||
|
a2d316b20d | ||
|
66723da96f | ||
|
b2a76fac29 | ||
|
6874288c8a | ||
|
5271a2ba31 | ||
|
12b26fe704 | ||
|
9d49139090 | ||
|
1fa99adbda | ||
|
cffe49a813 | ||
|
1262d50857 | ||
|
b11543d382 | ||
|
d0211ecc99 | ||
|
77dff765b6 | ||
|
f177a9d7e2 | ||
|
d067210faa | ||
|
7609e1a624 | ||
|
ba93d23634 | ||
|
515966dcc9 | ||
|
6aa2217706 | ||
|
e506c5172a | ||
|
a4d0e41413 | ||
|
b87f4a63e3 | ||
|
185da24fc3 | ||
|
020e8050bc | ||
|
0b392c1094 | ||
|
a43f0fc584 | ||
|
1b211bcc08 | ||
|
38683a8cc2 | ||
|
94b3fd4e51 | ||
|
f144e7e3b2 | ||
|
236fb2731c | ||
|
6266408828 | ||
|
6cf8de0a02 | ||
|
bb9b167b13 | ||
|
bb8fcb9ef0 | ||
|
b9b69d98fb | ||
|
198be61e7c | ||
|
939e7f0e8a | ||
|
e57b208570 | ||
|
6aa7bd6a33 | ||
|
889fcb286b | ||
|
eed5d48981 | ||
|
957e5cf197 | ||
|
feb22ad147 | ||
|
367437fec5 | ||
|
c1a4dd756a | ||
|
3dc4f87c12 | ||
|
6595d5f885 | ||
|
8c948bbb95 | ||
|
55700ff584 | ||
|
2f54f4c424 | ||
|
17eeb9e75f | ||
|
68b1529714 | ||
|
fb4b5404ae | ||
|
c1837853d6 | ||
|
4fa61ea14a | ||
|
bc8bdccb7d | ||
|
545fd53b88 | ||
|
2f8337f49c | ||
|
1978a5fe98 | ||
|
3af16f3afe | ||
|
3b48e38cba | ||
|
bda3a62e42 | ||
|
b6ece339b9 | ||
|
37aeba2247 | ||
|
bf401ef38b | ||
|
cf12978037 | ||
|
02dc9c6c4c | ||
|
832048d946 | ||
|
27b2ee03db | ||
|
73c433f484 | ||
|
459b492390 | ||
|
895bf7a759 | ||
|
a7282e4606 | ||
|
ef7dbdcda0 | ||
|
8559a79837 | ||
|
e467978b69 | ||
|
cc09e0ff1e | ||
|
399a7ebff0 | ||
|
2250694c32 | ||
|
94eaea6339 | ||
|
580d0ef7fc | ||
|
606087986e | ||
|
5a544ca5de | ||
|
84cff4945e | ||
|
39a4c00fc2 | ||
|
cf10e21b30 | ||
|
3234f7013f | ||
|
c3cfde7ebd | ||
|
8dba9306bc | ||
|
4a0dfe7421 | ||
|
50fb900e5e | ||
|
5eca6cdc9b | ||
|
7b0f7d31fd | ||
|
1b4389ef72 | ||
|
b69e02c802 | ||
|
9f3826d8e0 |
63
CHANGES.md
Normal file
63
CHANGES.md
Normal file
@ -0,0 +1,63 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
0.6 (2011-03-05)
|
||||
----------------
|
||||
|
||||
This release brought to you almost entirely
|
||||
by [mikewest](http://github.com/mikewest).
|
||||
|
||||
### Features
|
||||
|
||||
* Added `-t`/`--template` CLI option that allows you to specify a Mustache
|
||||
template that ought be used when rendering the final documentation.
|
||||
(Issue #16)
|
||||
|
||||
* More variables in templates:
|
||||
* `docs?`: True if `docs` contains text of any sort, False if it's empty.
|
||||
* `code?`: True if `code` contains text of any sort, False if it's empty.
|
||||
* `empty?`: True if both `code` and `docs` are empty. False otherwise.
|
||||
* `header?`: True if `docs` contains _only_ a HTML header. False otherwise.
|
||||
|
||||
* Test suite! (Run `rake test`)
|
||||
|
||||
* Autodetect file's language if Pygments is installed locally (Issue #19)
|
||||
|
||||
* Autopopulate comment characters for known languages (Issue #20)
|
||||
|
||||
* Correctly parse block comments (Issue #22)
|
||||
|
||||
* Stripping encoding definitions from Ruby and Python files in the same
|
||||
way we strip shebang lines (Issue #21)
|
||||
|
||||
* Adjusting section IDs to contain descriptive test from headers. A header
|
||||
section's ID might be `section-Header_text_goes_here` for friendlier URLs.
|
||||
Other section IDs will remain the same (`section-2` will stay
|
||||
`section-2`). (Issue #28)
|
||||
|
||||
### Bugs Fixed
|
||||
|
||||
* Docco's CSS changed: we updated Rocco's HTML accordingly, and pinned
|
||||
the CSS file to Docco's 0.3.0 tag. (Issues #12 and #23)
|
||||
|
||||
* Fixed code highlighting for shell scripts (among others) (Issue #13)
|
||||
|
||||
* Fixed buggy regex for comment char stripping (Issue #15)
|
||||
|
||||
* Specifying UTF-8 encoding for Pygments (Issue #10)
|
||||
|
||||
* Extensionless file support (thanks to [Vasily Polovnyov][vast] for the
|
||||
fix!) (Issue #24)
|
||||
|
||||
* Fixing language support for Pygments webservice (Issue #11)
|
||||
|
||||
* The source jumplist now generates correctly relative URLs (Issue #26)
|
||||
|
||||
* Fixed an issue with using mustache's `template_path=` incorrectly.
|
||||
|
||||
[vast]: https://github.com/vast
|
||||
|
||||
0.5
|
||||
---
|
||||
|
||||
Rocco 0.5 emerged from the hazy mists, complete and unfettered by history.
|
18
COPYING
Normal file
18
COPYING
Normal file
@ -0,0 +1,18 @@
|
||||
Copyright (c) 2010 Ryan Tomayko <http://tomayko.com/about>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to
|
||||
deal in the Software without restriction, including without limitation the
|
||||
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
README
Normal file
23
README
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
|
||||
___ ___ ___ ___ ___
|
||||
/\ \ /\ \ /\ \ /\ \ /\ \
|
||||
/::\ \ /::\ \ /::\ \ /::\ \ /::\ \
|
||||
/::\:\__\ /:/\:\__\ /:/\:\__\ /:/\:\__\ /:/\:\__\
|
||||
\;:::/ / \:\/:/ / \:\ \/__/ \:\ \/__/ \:\/:/ /
|
||||
|:\/__/ \::/ / \:\__\ \:\__\ \::/ /
|
||||
\|__| \/__/ \/__/ \/__/ \/__/
|
||||
|
||||
|
||||
|
||||
Rocco is a quick-and-dirty, literate-programming-style documentation
|
||||
generator for Ruby. See the Rocco generated docs for more information:
|
||||
|
||||
<http://rtomayko.github.com/rocco/>
|
||||
|
||||
|
||||
Rocco is a port of, and borrows heavily from, Docco -- the original
|
||||
quick-and-dirty, hundred-line-long, literate-programming-style
|
||||
documentation generator in CoffeeScript:
|
||||
|
||||
<http://jashkenas.github.com/docco/>
|
115
Rakefile
Normal file
115
Rakefile
Normal file
@ -0,0 +1,115 @@
|
||||
$LOAD_PATH.unshift 'lib'
|
||||
|
||||
require 'rake/testtask'
|
||||
require 'rake/clean'
|
||||
|
||||
task :default => [:sup, :docs, :test]
|
||||
|
||||
desc 'Holla'
|
||||
task :sup do
|
||||
verbose do
|
||||
lines = File.read('README').split("\n")[0,12]
|
||||
lines.map! { |line| line[15..-1] }
|
||||
puts lines.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Run tests (default)'
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.test_files = FileList['test/suite.rb']
|
||||
t.ruby_opts = ['-rubygems'] if defined? Gem
|
||||
end
|
||||
|
||||
# Bring in Rocco tasks
|
||||
require 'rocco/tasks'
|
||||
Rocco::make 'docs/'
|
||||
|
||||
desc 'Build rocco docs'
|
||||
task :docs => :rocco
|
||||
directory 'docs/'
|
||||
|
||||
desc 'Build docs and open in browser for the reading'
|
||||
task :read => :docs do
|
||||
sh 'open docs/lib/rocco.html'
|
||||
end
|
||||
|
||||
# Make index.html a copy of rocco.html
|
||||
file 'docs/index.html' => 'docs/lib/rocco.html' do |f|
|
||||
cp 'docs/lib/rocco.html', 'docs/index.html', :preserve => true
|
||||
end
|
||||
task :docs => 'docs/index.html'
|
||||
CLEAN.include 'docs/index.html'
|
||||
|
||||
# Alias for docs task
|
||||
task :doc => :docs
|
||||
|
||||
# GITHUB PAGES ===============================================================
|
||||
|
||||
desc 'Update gh-pages branch'
|
||||
task :pages => ['docs/.git', :docs] do
|
||||
rev = `git rev-parse --short HEAD`.strip
|
||||
Dir.chdir 'docs' do
|
||||
sh "git add *.html"
|
||||
sh "git commit -m 'rebuild pages from #{rev}'" do |ok,res|
|
||||
if ok
|
||||
verbose { puts "gh-pages updated" }
|
||||
sh "git push -q o HEAD:gh-pages"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Update the pages/ directory clone
|
||||
file 'docs/.git' => ['docs/', '.git/refs/heads/gh-pages'] do |f|
|
||||
sh "cd docs && git init -q && git remote add o ../.git" if !File.exist?(f.name)
|
||||
sh "cd docs && git fetch -q o && git reset -q --hard o/gh-pages && touch ."
|
||||
end
|
||||
CLOBBER.include 'docs/.git'
|
||||
|
||||
# PACKAGING =================================================================
|
||||
|
||||
if defined?(Gem)
|
||||
SPEC = eval(File.read('rocco.gemspec'))
|
||||
|
||||
def package(ext='')
|
||||
"pkg/rocco-#{SPEC.version}" + ext
|
||||
end
|
||||
|
||||
desc 'Build packages'
|
||||
task :package => %w[.gem .tar.gz].map {|e| package(e)}
|
||||
|
||||
desc 'Build and install as local gem'
|
||||
task :install => package('.gem') do
|
||||
sh "gem install #{package('.gem')}"
|
||||
end
|
||||
|
||||
directory 'pkg/'
|
||||
|
||||
file package('.gem') => %w[pkg/ rocco.gemspec] + SPEC.files do |f|
|
||||
sh "gem build rocco.gemspec"
|
||||
mv File.basename(f.name), f.name
|
||||
end
|
||||
|
||||
file package('.tar.gz') => %w[pkg/] + SPEC.files do |f|
|
||||
sh "git archive --format=tar HEAD | gzip > #{f.name}"
|
||||
end
|
||||
end
|
||||
|
||||
# GEMSPEC ===================================================================
|
||||
|
||||
file 'rocco.gemspec' => FileList['{lib,test,bin}/**','Rakefile'] do |f|
|
||||
version = File.read('lib/rocco.rb')[/VERSION = '(.*)'/] && $1
|
||||
date = Time.now.strftime("%Y-%m-%d")
|
||||
spec = File.
|
||||
read(f.name).
|
||||
sub(/s\.version\s*=\s*'.*'/, "s.version = '#{version}'")
|
||||
parts = spec.split(" # = MANIFEST =\n")
|
||||
files = `git ls-files`.
|
||||
split("\n").sort.reject{ |file| file =~ /^\./ }.
|
||||
map{ |file| " #{file}" }.join("\n")
|
||||
parts[1] = " s.files = %w[\n#{files}\n ]\n"
|
||||
spec = parts.join(" # = MANIFEST =\n")
|
||||
spec.sub!(/s.date = '.*'/, "s.date = '#{date}'")
|
||||
File.open(f.name, 'w') { |io| io.write(spec) }
|
||||
puts "#{f.name} #{version} (#{date})"
|
||||
end
|
100
bin/rocco
Executable file
100
bin/rocco
Executable file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env ruby
|
||||
#/ Usage: rocco [-l <lang>] [-c <chars>] [-o <dir>] <file>...
|
||||
#/ Generate literate-programming-style documentation for Ruby source <file>s.
|
||||
#/
|
||||
#/ Options:
|
||||
#/ -l, --language=<lang> The Pygments lexer to use to highlight code
|
||||
#/ -c, --comment-chars=<chars>
|
||||
#/ The string to recognize as a comment marker
|
||||
#/ -o, --output=<dir> Directory where generated HTML files are written
|
||||
#/ -t, --template=<path> The file to use as template when rendering HTML
|
||||
#/ -d, --docblocks Parse Docblock @annotations in comments
|
||||
#/ --help Show this help message
|
||||
|
||||
require 'optparse'
|
||||
require 'fileutils'
|
||||
|
||||
# Write usage message to stdout and exit.
|
||||
def usage(stream=$stderr, status=1)
|
||||
stream.puts File.readlines(__FILE__).
|
||||
grep(/^#\//).
|
||||
map { |line| line.sub(/^#. ?/, '') }.
|
||||
join
|
||||
exit status
|
||||
end
|
||||
|
||||
# Like `Kernel#abort` but writes a note encouraging the user to consult
|
||||
# `rocco --help` for more information.
|
||||
def abort_with_note(message=nil)
|
||||
$stderr.puts message if message
|
||||
abort "See `rocco --help' for usage information."
|
||||
end
|
||||
|
||||
# Parse command line options, aborting if anything goes wrong.
|
||||
output_dir = '.'
|
||||
sources = []
|
||||
options = {}
|
||||
ARGV.options { |o|
|
||||
o.program_name = File.basename($0)
|
||||
o.on("-o", "--output=DIR") { |dir| output_dir = dir }
|
||||
o.on("-l", "--language=LANG") { |lang| options[:language] = lang }
|
||||
o.on("-c", "--comment-chars=CHARS") { |chars| options[:comment_chars] = Regexp.escape(chars) }
|
||||
o.on("-t", "--template=TEMPLATE") { |template| options[:template_file] = template }
|
||||
o.on("-d", "--docblocks") { options[:docblocks] = true }
|
||||
o.on_tail("-h", "--help") { usage($stdout, 0) }
|
||||
o.parse!
|
||||
} or abort_with_note
|
||||
|
||||
# Use http://pygments.appspot.com in case `pygmentize(1)` isn't available.
|
||||
if ! ENV['PATH'].split(':').any? { |dir| File.exist?("#{dir}/pygmentize") }
|
||||
unless options[:webservice]
|
||||
$stderr.puts "pygmentize not in PATH; using pygments.appspot.com instead"
|
||||
options[:webservice] = true
|
||||
end
|
||||
end
|
||||
|
||||
# Eat sources from ARGV.
|
||||
sources << ARGV.shift while ARGV.any?
|
||||
|
||||
# Make sure we have some files to work with.
|
||||
if sources.empty?
|
||||
abort_with_note "#{File.basename($0)}: no input <file>s given"
|
||||
end
|
||||
|
||||
# What a fucking mess. Most of this is duplicated in rocco.rb too.
|
||||
libdir = File.expand_path('../../lib', __FILE__).sub(/^#{Dir.pwd}\//, '')
|
||||
begin
|
||||
require 'rdiscount'
|
||||
require 'rocco'
|
||||
rescue LoadError
|
||||
case $!.to_s
|
||||
when /rdiscount/
|
||||
if !defined?(Gem)
|
||||
warn "warn: #$!. trying again with rubygems"
|
||||
require 'rubygems'
|
||||
retry
|
||||
else
|
||||
require 'bluecloth'
|
||||
Markdown = BlueCloth
|
||||
$LOADED_FEATURES << 'rdiscount.rb'
|
||||
retry
|
||||
end
|
||||
when /rocco/
|
||||
if !$:.include?(libdir)
|
||||
warn "warn: #$!. trying again with #{libdir} on load path"
|
||||
$:.unshift(libdir)
|
||||
retry
|
||||
end
|
||||
end
|
||||
raise
|
||||
end
|
||||
|
||||
# Run each file through Rocco and write output.
|
||||
sources.each do |filename|
|
||||
rocco = Rocco.new(filename, sources, options)
|
||||
dest = filename.sub(Regexp.new("#{File.extname(filename)}$"),".html")
|
||||
dest = File.join(output_dir, dest) if output_dir != '.'
|
||||
puts "rocco: #{filename} -> #{dest}"
|
||||
FileUtils.mkdir_p File.dirname(dest)
|
||||
File.open(dest, 'wb') { |fd| fd.write(rocco.to_html) }
|
||||
end
|
531
lib/rocco.rb
Normal file
531
lib/rocco.rb
Normal file
@ -0,0 +1,531 @@
|
||||
# **Rocco** is a Ruby port of [Docco][do], the quick-and-dirty,
|
||||
# hundred-line-long, literate-programming-style documentation generator.
|
||||
#
|
||||
# Rocco reads Ruby source files and produces annotated source documentation
|
||||
# in HTML format. Comments are formatted with [Markdown][md] and presented
|
||||
# alongside syntax highlighted code so as to give an annotation effect.
|
||||
# This page is the result of running Rocco against [its own source file][so].
|
||||
#
|
||||
# Most of this was written while waiting for [node.js][no] to build (so I
|
||||
# could use Docco!). Docco's gorgeous HTML and CSS are taken verbatim.
|
||||
# The main difference is that Rocco is written in Ruby instead of
|
||||
# [CoffeeScript][co] and may be a bit easier to obtain and install in
|
||||
# existing Ruby environments or where node doesn't run yet.
|
||||
#
|
||||
# Install Rocco with Rubygems:
|
||||
#
|
||||
# gem install rocco
|
||||
#
|
||||
# Once installed, the `rocco` command can be used to generate documentation
|
||||
# for a set of Ruby source files:
|
||||
#
|
||||
# rocco lib/*.rb
|
||||
#
|
||||
# The HTML files are written to the current working directory.
|
||||
#
|
||||
# [no]: http://nodejs.org/
|
||||
# [do]: http://jashkenas.github.com/docco/
|
||||
# [co]: http://coffeescript.org/
|
||||
# [md]: http://daringfireball.net/projects/markdown/
|
||||
# [so]: http://github.com/rtomayko/rocco/blob/master/lib/rocco.rb#commit
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
# We'll need a Markdown library. [RDiscount][rd], if we're lucky. Otherwise,
|
||||
# issue a warning and fall back on using BlueCloth.
|
||||
#
|
||||
# [rd]: http://github.com/rtomayko/rdiscount
|
||||
begin
|
||||
require 'rdiscount'
|
||||
rescue LoadError => boom
|
||||
warn "WARNING: #{boom}. Trying bluecloth."
|
||||
require 'bluecloth'
|
||||
Markdown = BlueCloth
|
||||
end
|
||||
|
||||
# We use [{{ mustache }}](http://defunkt.github.com/mustache/) for
|
||||
# HTML templating.
|
||||
require 'mustache'
|
||||
|
||||
# We use `Net::HTTP` to highlight code via <http://pygments.appspot.com>
|
||||
require 'net/http'
|
||||
|
||||
# Code is run through [Pygments](http://pygments.org/) for syntax
|
||||
# highlighting. If it's not installed, locally, use a webservice.
|
||||
if !ENV['PATH'].split(':').any? { |dir| File.executable?("#{dir}/pygmentize") }
|
||||
warn "WARNING: Pygments not found. Using webservice."
|
||||
end
|
||||
|
||||
#### Public Interface
|
||||
|
||||
# `Rocco.new` takes a source `filename`, an optional list of source filenames
|
||||
# for other documentation sources, an `options` hash, and an optional `block`.
|
||||
# The `options` hash respects three members:
|
||||
#
|
||||
# * `:language`: specifies which Pygments lexer to use if one can't be
|
||||
# auto-detected from the filename. _Defaults to `ruby`_.
|
||||
#
|
||||
# * `:comment_chars`, which specifies the comment characters of the
|
||||
# target language. _Defaults to `#`_.
|
||||
#
|
||||
# * `:template_file`, which specifies a external template file to use
|
||||
# when rendering the final, highlighted file via Mustache. _Defaults
|
||||
# to `nil` (that is, Mustache will use `./lib/rocco/layout.mustache`)_.
|
||||
#
|
||||
class Rocco
|
||||
VERSION = '0.7'
|
||||
|
||||
def initialize(filename, sources=[], options={}, &block)
|
||||
@file = filename
|
||||
@sources = sources
|
||||
|
||||
# When `block` is given, it must read the contents of the file using
|
||||
# whatever means necessary and return it as a string. With no `block`,
|
||||
# the file is read to retrieve data.
|
||||
@data =
|
||||
if block_given?
|
||||
yield
|
||||
else
|
||||
File.read(filename)
|
||||
end
|
||||
|
||||
defaults = {
|
||||
:language => 'ruby',
|
||||
:comment_chars => '#',
|
||||
:template_file => nil
|
||||
}
|
||||
@options = defaults.merge(options)
|
||||
|
||||
# If we detect a language
|
||||
if detect_language() != "text"
|
||||
# then assign the detected language to `:language`, and look for
|
||||
# comment characters based on that language
|
||||
@options[:language] = detect_language()
|
||||
@options[:comment_chars] = generate_comment_chars()
|
||||
|
||||
# If we didn't detect a language, but the user provided one, use it
|
||||
# to look around for comment characters to override the default.
|
||||
elsif @options[:language] != defaults[:language]
|
||||
@options[:comment_chars] = generate_comment_chars()
|
||||
|
||||
# If neither is true, then convert the default comment character string
|
||||
# into the comment_char syntax (we'll discuss that syntax in detail when
|
||||
# we get to `generate_comment_chars()` in a moment.
|
||||
else
|
||||
@options[:comment_chars] = {
|
||||
:single => @options[:comment_chars],
|
||||
:multi => nil
|
||||
}
|
||||
end
|
||||
|
||||
# Turn `:comment_chars` into a regex matching a series of spaces, the
|
||||
# `:comment_chars` string, and the an optional space. We'll use that
|
||||
# to detect single-line comments.
|
||||
@comment_pattern =
|
||||
Regexp.new("^\\s*#{@options[:comment_chars][:single]}\s?")
|
||||
|
||||
# `parse()` the file contents stored in `@data`. Run the result through
|
||||
# `split()` and that result through `highlight()` to generate the final
|
||||
# section list.
|
||||
@sections = highlight(split(parse(@data)))
|
||||
end
|
||||
|
||||
# The filename as given to `Rocco.new`.
|
||||
attr_reader :file
|
||||
|
||||
# The merged options array
|
||||
attr_reader :options
|
||||
|
||||
# A list of two-tuples representing each *section* of the source file. Each
|
||||
# item in the list has the form: `[docs_html, code_html]`, where both
|
||||
# elements are strings containing the documentation and source code HTML,
|
||||
# respectively.
|
||||
attr_reader :sections
|
||||
|
||||
# A list of all source filenames included in the documentation set. Useful
|
||||
# for building an index of other files.
|
||||
attr_reader :sources
|
||||
|
||||
# Generate HTML output for the entire document.
|
||||
require 'rocco/layout'
|
||||
def to_html
|
||||
Rocco::Layout.new(self, @options[:template_file]).render
|
||||
end
|
||||
|
||||
# Helper Functions
|
||||
# ----------------
|
||||
|
||||
# Returns `true` if `pygmentize` is available locally, `false` otherwise.
|
||||
def pygmentize?
|
||||
@_pygmentize ||= ENV['PATH'].split(':').
|
||||
any? { |dir| File.executable?("#{dir}/pygmentize") }
|
||||
end
|
||||
|
||||
# If `pygmentize` is available, we can use it to autodetect a file's
|
||||
# language based on its filename. Filenames without extensions, or with
|
||||
# extensions that `pygmentize` doesn't understand will return `text`.
|
||||
# We'll also return `text` if `pygmentize` isn't available.
|
||||
#
|
||||
# We'll memoize the result, as we'll call this a few times.
|
||||
def detect_language
|
||||
@_language ||=
|
||||
if pygmentize?
|
||||
%x[pygmentize -N #{@file}].strip.split('+').first
|
||||
else
|
||||
"text"
|
||||
end
|
||||
end
|
||||
|
||||
# Given a file's language, we should be able to autopopulate the
|
||||
# `comment_chars` variables for single-line comments. If we don't
|
||||
# have comment characters on record for a given language, we'll
|
||||
# use the user-provided `:comment_char` option (which defaults to
|
||||
# `#`).
|
||||
#
|
||||
# Comment characters are listed as:
|
||||
#
|
||||
# { :single => "//",
|
||||
# :multi_start => "/**",
|
||||
# :multi_middle => "*",
|
||||
# :multi_end => "*/" }
|
||||
#
|
||||
# `:single` denotes the leading character of a single-line comment.
|
||||
# `:multi_start` denotes the string that should appear alone on a
|
||||
# line of code to begin a block of documentation. `:multi_middle`
|
||||
# denotes the leading character of block comment content, and
|
||||
# `:multi_end` is the string that ought appear alone on a line to
|
||||
# close a block of documentation. That is:
|
||||
#
|
||||
# /** [:multi][:start]
|
||||
# * [:multi][:middle]
|
||||
# ...
|
||||
# * [:multi][:middle]
|
||||
# */ [:multi][:end]
|
||||
#
|
||||
# If a language only has one type of comment, the missing type
|
||||
# should be assigned `nil`.
|
||||
#
|
||||
# At the moment, we're only returning `:single`. Consider this
|
||||
# groundwork for block comment parsing.
|
||||
C_STYLE_COMMENTS = {
|
||||
:single => "//",
|
||||
:multi => { :start => "/**", :middle => "*", :end => "*/" },
|
||||
:heredoc => nil
|
||||
}
|
||||
COMMENT_STYLES = {
|
||||
"bash" => { :single => "#", :multi => nil },
|
||||
"c" => C_STYLE_COMMENTS,
|
||||
"coffee-script" => {
|
||||
:single => "#",
|
||||
:multi => { :start => "###", :middle => nil, :end => "###" },
|
||||
:heredoc => nil
|
||||
},
|
||||
"cpp" => C_STYLE_COMMENTS,
|
||||
"csharp" => C_STYLE_COMMENTS,
|
||||
"css" => {
|
||||
:single => nil,
|
||||
:multi => { :start => "/**", :middle => "*", :end => "*/" },
|
||||
:heredoc => nil
|
||||
},
|
||||
"html" => {
|
||||
:single => nil,
|
||||
:multi => { :start => '<!--', :middle => nil, :end => '-->' },
|
||||
:heredoc => nil
|
||||
},
|
||||
"java" => C_STYLE_COMMENTS,
|
||||
"js" => C_STYLE_COMMENTS,
|
||||
"lua" => {
|
||||
:single => "--",
|
||||
:multi => nil,
|
||||
:heredoc => nil
|
||||
},
|
||||
"php" => C_STYLE_COMMENTS,
|
||||
"python" => {
|
||||
:single => "#",
|
||||
:multi => { :start => '"""', :middle => nil, :end => '"""' },
|
||||
:heredoc => nil
|
||||
},
|
||||
"rb" => {
|
||||
:single => "#",
|
||||
:multi => { :start => '=begin', :middle => nil, :end => '=end' },
|
||||
:heredoc => "<<-"
|
||||
},
|
||||
"scala" => C_STYLE_COMMENTS,
|
||||
"scheme" => { :single => ";;", :multi => nil, :heredoc => nil },
|
||||
"xml" => {
|
||||
:single => nil,
|
||||
:multi => { :start => '<!--', :middle => nil, :end => '-->' },
|
||||
:heredoc => nil
|
||||
},
|
||||
}
|
||||
|
||||
def generate_comment_chars
|
||||
@_commentchar ||=
|
||||
if COMMENT_STYLES[@options[:language]]
|
||||
COMMENT_STYLES[@options[:language]]
|
||||
else
|
||||
{ :single => @options[:comment_chars], :multi => nil, :heredoc => nil }
|
||||
end
|
||||
end
|
||||
|
||||
# Internal Parsing and Highlighting
|
||||
# ---------------------------------
|
||||
|
||||
# Parse the raw file data into a list of two-tuples. Each tuple has the
|
||||
# form `[docs, code]` where both elements are arrays containing the
|
||||
# raw lines parsed from the input file, comment characters stripped.
|
||||
def parse(data)
|
||||
sections = []
|
||||
docs, code = [], []
|
||||
lines = data.split("\n")
|
||||
|
||||
# The first line is ignored if it is a shebang line. We also ignore the
|
||||
# PEP 263 encoding information in python sourcefiles, and the similar ruby
|
||||
# 1.9 syntax.
|
||||
lines.shift if lines[0] =~ /^\#\!/
|
||||
lines.shift if lines[0] =~ /coding[:=]\s*[-\w.]+/ &&
|
||||
[ "python", "rb" ].include?(@options[:language])
|
||||
|
||||
# To detect both block comments and single-line comments, we'll set
|
||||
# up a tiny state machine, and loop through each line of the file.
|
||||
# This requires an `in_comment_block` boolean, and a few regular
|
||||
# expressions for line tests. We'll do the same for fake heredoc parsing.
|
||||
in_comment_block = false
|
||||
in_heredoc = false
|
||||
single_line_comment, block_comment_start, block_comment_mid, block_comment_end =
|
||||
nil, nil, nil, nil
|
||||
if not @options[:comment_chars][:single].nil?
|
||||
single_line_comment = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:single])}\\s?")
|
||||
end
|
||||
if not @options[:comment_chars][:multi].nil?
|
||||
block_comment_start = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*$")
|
||||
block_comment_end = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
|
||||
block_comment_one_liner = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*(.*?)\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
|
||||
block_comment_start_with = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*(.*?)$")
|
||||
block_comment_end_with = Regexp.new("\\s*(.*?)\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
|
||||
if @options[:comment_chars][:multi][:middle]
|
||||
block_comment_mid = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:middle])}\\s?")
|
||||
end
|
||||
end
|
||||
if not @options[:comment_chars][:heredoc].nil?
|
||||
heredoc_start = Regexp.new("#{Regexp.escape(@options[:comment_chars][:heredoc])}(\\S+)$")
|
||||
end
|
||||
lines.each do |line|
|
||||
# If we're currently in a comment block, check whether the line matches
|
||||
# the _end_ of a comment block or the _end_ of a comment block with a
|
||||
# comment.
|
||||
if in_comment_block
|
||||
if block_comment_end && line.match(block_comment_end)
|
||||
in_comment_block = false
|
||||
elsif block_comment_end_with && line.match(block_comment_end_with)
|
||||
in_comment_block = false
|
||||
docs << line.match(block_comment_end_with).captures.first.
|
||||
sub(block_comment_mid || '', '')
|
||||
else
|
||||
docs << line.sub(block_comment_mid || '', '')
|
||||
end
|
||||
# If we're currently in a heredoc, we're looking for the end of the
|
||||
# heredoc, and everything it contains is code.
|
||||
elsif in_heredoc
|
||||
if line.match(Regexp.new("^#{Regexp.escape(in_heredoc)}$"))
|
||||
in_heredoc = false
|
||||
end
|
||||
code << line
|
||||
# Otherwise, check whether the line starts a heredoc. If so, note the end
|
||||
# pattern, and the line is code. Otherwise check whether the line matches
|
||||
# the beginning of a block, or a single-line comment all on it's lonesome.
|
||||
# In either case, if there's code, start a new section.
|
||||
else
|
||||
if heredoc_start && line.match(heredoc_start)
|
||||
in_heredoc = $1
|
||||
code << line
|
||||
elsif block_comment_one_liner && line.match(block_comment_one_liner)
|
||||
if code.any?
|
||||
sections << [docs, code]
|
||||
docs, code = [], []
|
||||
end
|
||||
docs << line.match(block_comment_one_liner).captures.first
|
||||
elsif block_comment_start && line.match(block_comment_start)
|
||||
in_comment_block = true
|
||||
if code.any?
|
||||
sections << [docs, code]
|
||||
docs, code = [], []
|
||||
end
|
||||
elsif block_comment_start_with && line.match(block_comment_start_with)
|
||||
in_comment_block = true
|
||||
if code.any?
|
||||
sections << [docs, code]
|
||||
docs, code = [], []
|
||||
end
|
||||
docs << line.match(block_comment_start_with).captures.first
|
||||
elsif single_line_comment && line.match(single_line_comment)
|
||||
if code.any?
|
||||
sections << [docs, code]
|
||||
docs, code = [], []
|
||||
end
|
||||
docs << line.sub(single_line_comment || '', '')
|
||||
else
|
||||
code << line
|
||||
end
|
||||
end
|
||||
end
|
||||
sections << [docs, code] if docs.any? || code.any?
|
||||
normalize_leading_spaces(sections)
|
||||
end
|
||||
|
||||
# Normalizes documentation whitespace by checking for leading whitespace,
|
||||
# removing it, and then removing the same amount of whitespace from each
|
||||
# succeeding line. That is:
|
||||
#
|
||||
# def func():
|
||||
# """
|
||||
# Comment 1
|
||||
# Comment 2
|
||||
# """
|
||||
# print "omg!"
|
||||
#
|
||||
# should yield a comment block of `Comment 1\nComment 2` and code of
|
||||
# `def func():\n print "omg!"`
|
||||
def normalize_leading_spaces(sections)
|
||||
sections.map do |section|
|
||||
if section.any? && section[0].any?
|
||||
leading_space = section[0][0].match("^\s+")
|
||||
if leading_space
|
||||
section[0] =
|
||||
section[0].map{ |line| line.sub(/^#{leading_space.to_s}/, '') }
|
||||
end
|
||||
end
|
||||
section
|
||||
end
|
||||
end
|
||||
|
||||
# Take the list of paired *sections* two-tuples and split into two
|
||||
# separate lists: one holding the comments with leaders removed and
|
||||
# one with the code blocks.
|
||||
def split(sections)
|
||||
docs_blocks, code_blocks = [], []
|
||||
sections.each do |docs,code|
|
||||
docs_blocks << docs.join("\n")
|
||||
code_blocks << code.map do |line|
|
||||
tabs = line.match(/^(\t+)/)
|
||||
tabs ? line.sub(/^\t+/, ' ' * tabs.captures[0].length) : line
|
||||
end.join("\n")
|
||||
end
|
||||
[docs_blocks, code_blocks]
|
||||
end
|
||||
|
||||
# Take a list of block comments and convert Docblock @annotations to
|
||||
# Markdown syntax.
|
||||
def docblock(docs)
|
||||
docs.map do |doc|
|
||||
doc.split("\n").map do |line|
|
||||
line.match(/^@\w+/) ? line.sub(/^@(\w+)\s+/, '> **\1** ')+" " : line
|
||||
end.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
# Take the result of `split` and apply Markdown formatting to comments and
|
||||
# syntax highlighting to source code.
|
||||
def highlight(blocks)
|
||||
docs_blocks, code_blocks = blocks
|
||||
|
||||
# Pre-process Docblock @annotations.
|
||||
if @options[:docblocks]
|
||||
docs_blocks = docblock(docs_blocks)
|
||||
end
|
||||
|
||||
# Combine all docs blocks into a single big markdown document with section
|
||||
# dividers and run through the Markdown processor. Then split it back out
|
||||
# into separate sections.
|
||||
markdown = docs_blocks.join("\n\n##### DIVIDER\n\n")
|
||||
docs_html = process_markdown(markdown).
|
||||
split(/\n*<h5>DIVIDER<\/h5>\n*/m)
|
||||
|
||||
# Combine all code blocks into a single big stream with section dividers and
|
||||
# run through either `pygmentize(1)` or <http://pygments.appspot.com>
|
||||
span, espan = '<span class="c.?">', '</span>'
|
||||
if @options[:comment_chars][:single]
|
||||
front = @options[:comment_chars][:single]
|
||||
divider_input = "\n\n#{front} DIVIDER\n\n"
|
||||
divider_output = Regexp.new(
|
||||
[ "\\n*",
|
||||
span,
|
||||
Regexp.escape(CGI.escapeHTML(front)),
|
||||
' DIVIDER',
|
||||
espan,
|
||||
"\\n*"
|
||||
].join, Regexp::MULTILINE
|
||||
)
|
||||
else
|
||||
front = @options[:comment_chars][:multi][:start]
|
||||
back = @options[:comment_chars][:multi][:end]
|
||||
divider_input = "\n\n#{front}\nDIVIDER\n#{back}\n\n"
|
||||
divider_output = Regexp.new(
|
||||
[ "\\n*",
|
||||
span, Regexp.escape(CGI.escapeHTML(front)), espan,
|
||||
"\\n",
|
||||
span, "DIVIDER", espan,
|
||||
"\\n",
|
||||
span, Regexp.escape(CGI.escapeHTML(back)), espan,
|
||||
"\\n*"
|
||||
].join, Regexp::MULTILINE
|
||||
)
|
||||
end
|
||||
|
||||
code_stream = code_blocks.join(divider_input)
|
||||
|
||||
code_html =
|
||||
if pygmentize?
|
||||
highlight_pygmentize(code_stream)
|
||||
else
|
||||
highlight_webservice(code_stream)
|
||||
end
|
||||
|
||||
# Do some post-processing on the pygments output to split things back
|
||||
# into sections and remove partial `<pre>` blocks.
|
||||
code_html = code_html.
|
||||
split(divider_output).
|
||||
map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
|
||||
map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') }
|
||||
|
||||
# Lastly, combine the docs and code lists back into a list of two-tuples.
|
||||
docs_html.zip(code_html)
|
||||
end
|
||||
|
||||
# Convert Markdown to classy HTML.
|
||||
def process_markdown(text)
|
||||
Markdown.new(text, :smart).to_html
|
||||
end
|
||||
|
||||
# We `popen` a read/write pygmentize process in the parent and
|
||||
# then fork off a child process to write the input.
|
||||
def highlight_pygmentize(code)
|
||||
code_html = nil
|
||||
open("|pygmentize -l #{@options[:language]} -O encoding=utf-8 -f html", 'r+') do |fd|
|
||||
pid =
|
||||
fork {
|
||||
fd.close_read
|
||||
fd.write code
|
||||
fd.close_write
|
||||
exit!
|
||||
}
|
||||
fd.close_write
|
||||
code_html = fd.read
|
||||
fd.close_read
|
||||
Process.wait(pid)
|
||||
end
|
||||
|
||||
code_html
|
||||
end
|
||||
|
||||
# Pygments is not one of those things that's trivial for a ruby user to install,
|
||||
# so we'll fall back on a webservice to highlight the code if it isn't available.
|
||||
def highlight_webservice(code)
|
||||
Net::HTTP.post_form(
|
||||
URI.parse('http://pygments.appspot.com/'),
|
||||
{'lang' => @options[:language], 'code' => code}
|
||||
).body
|
||||
end
|
||||
end
|
||||
|
||||
# And that's it.
|
190
lib/rocco/docco.css
Normal file
190
lib/rocco/docco.css
Normal file
@ -0,0 +1,190 @@
|
||||
/*--------------------- Layout and Typography ----------------------------*/
|
||||
body {
|
||||
font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
color: #252519;
|
||||
margin: 0; padding: 0;
|
||||
}
|
||||
a {
|
||||
color: #261a3b;
|
||||
}
|
||||
a:visited {
|
||||
color: #261a3b;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 0px 0 15px 0;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 40px;
|
||||
}
|
||||
#container {
|
||||
position: relative;
|
||||
}
|
||||
#background {
|
||||
position: fixed;
|
||||
top: 0; left: 525px; right: 0; bottom: 0;
|
||||
background: #f5f5ff;
|
||||
border-left: 1px solid #e5e5ee;
|
||||
z-index: -1;
|
||||
}
|
||||
#jump_to, #jump_page {
|
||||
background: white;
|
||||
-webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777;
|
||||
-webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px;
|
||||
font: 10px Arial;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
}
|
||||
#jump_to, #jump_wrapper {
|
||||
position: fixed;
|
||||
top: 0; right: 0;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
#jump_wrapper {
|
||||
padding: 0;
|
||||
display: none;
|
||||
}
|
||||
#jump_to:hover #jump_wrapper {
|
||||
display: block;
|
||||
}
|
||||
#jump_page {
|
||||
padding: 5px 0 3px;
|
||||
margin: 0 0 25px 25px;
|
||||
overflow: hidden;
|
||||
zoom: 1;
|
||||
}
|
||||
#jump_page .source {
|
||||
float: left;
|
||||
display: inline;
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
#jump_page .source:hover {
|
||||
background: #f5f5ff;
|
||||
}
|
||||
#jump_page .source:first-child {
|
||||
}
|
||||
table td {
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
td.docs, th.docs {
|
||||
max-width: 450px;
|
||||
min-width: 450px;
|
||||
min-height: 5px;
|
||||
padding: 10px 25px 1px 50px;
|
||||
overflow-x: hidden;
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
.docs pre {
|
||||
margin: 15px 0 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.docs p tt, .docs p code {
|
||||
background: #f8f8ff;
|
||||
border: 1px solid #dedede;
|
||||
font-size: 12px;
|
||||
padding: 0 0.2em;
|
||||
}
|
||||
.pilwrap {
|
||||
position: relative;
|
||||
}
|
||||
.pilcrow {
|
||||
font: 12px Arial;
|
||||
text-decoration: none;
|
||||
color: #454545;
|
||||
position: absolute;
|
||||
top: 3px; left: -20px;
|
||||
padding: 1px 2px;
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
}
|
||||
td.docs:hover .pilcrow {
|
||||
opacity: 1;
|
||||
}
|
||||
td.code, th.code {
|
||||
padding: 14px 15px 16px 25px;
|
||||
width: 100%;
|
||||
vertical-align: top;
|
||||
background: #f5f5ff;
|
||||
border-left: 1px solid #e5e5ee;
|
||||
}
|
||||
pre, tt, code {
|
||||
font-size: 12px; line-height: 18px;
|
||||
font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace;
|
||||
margin: 0; padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/*---------------------- Syntax Highlighting -----------------------------*/
|
||||
td.linenos { background-color: #f0f0f0; padding-right: 10px; }
|
||||
span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }
|
||||
body .hll { background-color: #ffffcc }
|
||||
body .c { color: #408080; font-style: italic } /* Comment */
|
||||
body .err { border: 1px solid #FF0000 } /* Error */
|
||||
body .k { color: #954121 } /* Keyword */
|
||||
body .o { color: #666666 } /* Operator */
|
||||
body .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||
body .cp { color: #BC7A00 } /* Comment.Preproc */
|
||||
body .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||
body .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||
body .gd { color: #A00000 } /* Generic.Deleted */
|
||||
body .ge { font-style: italic } /* Generic.Emph */
|
||||
body .gr { color: #FF0000 } /* Generic.Error */
|
||||
body .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
body .gi { color: #00A000 } /* Generic.Inserted */
|
||||
body .go { color: #808080 } /* Generic.Output */
|
||||
body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
body .gs { font-weight: bold } /* Generic.Strong */
|
||||
body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
body .gt { color: #0040D0 } /* Generic.Traceback */
|
||||
body .kc { color: #954121 } /* Keyword.Constant */
|
||||
body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */
|
||||
body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */
|
||||
body .kp { color: #954121 } /* Keyword.Pseudo */
|
||||
body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */
|
||||
body .kt { color: #B00040 } /* Keyword.Type */
|
||||
body .m { color: #666666 } /* Literal.Number */
|
||||
body .s { color: #219161 } /* Literal.String */
|
||||
body .na { color: #7D9029 } /* Name.Attribute */
|
||||
body .nb { color: #954121 } /* Name.Builtin */
|
||||
body .nc { color: #0000FF; font-weight: bold } /* Name.Class */
|
||||
body .no { color: #880000 } /* Name.Constant */
|
||||
body .nd { color: #AA22FF } /* Name.Decorator */
|
||||
body .ni { color: #999999; font-weight: bold } /* Name.Entity */
|
||||
body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||
body .nf { color: #0000FF } /* Name.Function */
|
||||
body .nl { color: #A0A000 } /* Name.Label */
|
||||
body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
|
||||
body .nt { color: #954121; font-weight: bold } /* Name.Tag */
|
||||
body .nv { color: #19469D } /* Name.Variable */
|
||||
body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
|
||||
body .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
body .mf { color: #666666 } /* Literal.Number.Float */
|
||||
body .mh { color: #666666 } /* Literal.Number.Hex */
|
||||
body .mi { color: #666666 } /* Literal.Number.Integer */
|
||||
body .mo { color: #666666 } /* Literal.Number.Oct */
|
||||
body .sb { color: #219161 } /* Literal.String.Backtick */
|
||||
body .sc { color: #219161 } /* Literal.String.Char */
|
||||
body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */
|
||||
body .s2 { color: #219161 } /* Literal.String.Double */
|
||||
body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||
body .sh { color: #219161 } /* Literal.String.Heredoc */
|
||||
body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||
body .sx { color: #954121 } /* Literal.String.Other */
|
||||
body .sr { color: #BB6688 } /* Literal.String.Regex */
|
||||
body .s1 { color: #219161 } /* Literal.String.Single */
|
||||
body .ss { color: #19469D } /* Literal.String.Symbol */
|
||||
body .bp { color: #954121 } /* Name.Builtin.Pseudo */
|
||||
body .vc { color: #19469D } /* Name.Variable.Class */
|
||||
body .vg { color: #19469D } /* Name.Variable.Global */
|
||||
body .vi { color: #19469D } /* Name.Variable.Instance */
|
||||
body .il { color: #666666 } /* Literal.Number.Integer.Long */
|
46
lib/rocco/layout.mustache
Normal file
46
lib/rocco/layout.mustache
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<title>{{ title }}</title>
|
||||
<style type="text/css">{{{ styles }}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='container'>
|
||||
<div id="background"></div>
|
||||
{{#sources?}}
|
||||
<div id="jump_to">
|
||||
Jump To …
|
||||
<div id="jump_wrapper">
|
||||
<div id="jump_page">
|
||||
{{#sources}}
|
||||
<a class="source" href="{{ url }}">{{ basename }}</a>
|
||||
{{/sources}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/sources?}}
|
||||
<table cellspacing=0 cellpadding=0>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class=docs><h1>{{ title }}</h1></th>
|
||||
<th class=code></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#sections}}
|
||||
<tr id='section-{{ section_id }}'>
|
||||
<td class=docs>
|
||||
<div class="pilwrap">
|
||||
<a class="pilcrow" href="#section-{{ section_id }}">¶</a>
|
||||
</div>
|
||||
{{{ docs }}}
|
||||
</td>
|
||||
<td class=code>
|
||||
<div class='highlight'><pre>{{{ code }}}</pre></div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/sections}}
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
63
lib/rocco/layout.rb
Normal file
63
lib/rocco/layout.rb
Normal file
@ -0,0 +1,63 @@
|
||||
require 'mustache'
|
||||
require 'pathname'
|
||||
|
||||
class Rocco::Layout < Mustache
|
||||
self.template_path = "#{File.dirname(__FILE__)}/.."
|
||||
|
||||
def initialize(doc, file=nil)
|
||||
@doc = doc
|
||||
if not file.nil?
|
||||
Rocco::Layout.template_file = file
|
||||
end
|
||||
end
|
||||
|
||||
def title
|
||||
File.basename(@doc.file)
|
||||
end
|
||||
|
||||
def file
|
||||
@doc.file
|
||||
end
|
||||
|
||||
def styles
|
||||
File.read(File.expand_path('../docco.css', __FILE__))
|
||||
end
|
||||
|
||||
def sections
|
||||
num = 0
|
||||
@doc.sections.map do |docs,code|
|
||||
code ||= ''
|
||||
is_header = /^<h.>(.+)<\/h.>$/.match( docs )
|
||||
header_text = is_header && is_header[1].split.join("_")
|
||||
num += 1
|
||||
{
|
||||
:docs => docs,
|
||||
:docs? => !docs.empty?,
|
||||
:header? => is_header,
|
||||
|
||||
:code => code,
|
||||
:code? => !code.empty?,
|
||||
|
||||
:empty? => ( code.empty? && docs.empty? ),
|
||||
:section_id => is_header ? header_text : num
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def sources?
|
||||
@doc.sources.length > 1
|
||||
end
|
||||
|
||||
def sources
|
||||
currentpath = Pathname.new( File.dirname( @doc.file ) )
|
||||
@doc.sources.sort { |left, right| File.split(left).last <=> File.split(right).last }.map do |source|
|
||||
htmlpath = Pathname.new( source.sub( Regexp.new( "#{File.extname(source)}$"), ".html" ) )
|
||||
|
||||
{
|
||||
:path => source,
|
||||
:basename => File.basename(source),
|
||||
:url => htmlpath.relative_path_from( currentpath )
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
123
lib/rocco/tasks.rb
Normal file
123
lib/rocco/tasks.rb
Normal file
@ -0,0 +1,123 @@
|
||||
#### Rocco Rake Tasks
|
||||
#
|
||||
# To use the Rocco Rake tasks, require `rocco/tasks` in your `Rakefile`
|
||||
# and define a Rake task with `rocco_task`. In its simplest form, `rocco_task`
|
||||
# takes the path to a destination directory where HTML docs should be built:
|
||||
#
|
||||
# require 'rocco/tasks'
|
||||
#
|
||||
# desc "Build Rocco Docs"
|
||||
# Rocco::make 'docs/'
|
||||
#
|
||||
# This creates a `:rocco` rake task, which can then be run with:
|
||||
#
|
||||
# rake rocco
|
||||
#
|
||||
# It's a good idea to guard against Rocco not being available, since your
|
||||
# Rakefile will fail to load otherwise. Consider doing something like this,
|
||||
# so that your Rakefile will still work
|
||||
#
|
||||
# begin
|
||||
# require 'rocco/tasks'
|
||||
# Rocco::make 'docs/'
|
||||
# rescue LoadError
|
||||
# warn "#$! -- rocco tasks not loaded."
|
||||
# task :rocco
|
||||
# end
|
||||
#
|
||||
# It's also possible to pass a glob pattern:
|
||||
#
|
||||
# Rocco::make 'html/', 'lib/thing/**/*.rb'
|
||||
#
|
||||
# Or a list of glob patterns:
|
||||
#
|
||||
# Rocco::make 'html/', ['lib/thing.rb', 'lib/thing/*.rb']
|
||||
#
|
||||
# Finally, it is also possible to specify which Pygments language you would
|
||||
# like to use to highlight the code, as well as the comment characters for the
|
||||
# language in the `options` hash:
|
||||
#
|
||||
# Rocco::make 'html/', 'lib/thing/**/*.rb', {
|
||||
# :language => 'io',
|
||||
# :comment_chars => '#'
|
||||
# }
|
||||
#
|
||||
|
||||
# Might be nice to defer this until we actually need to build docs but this
|
||||
# will have to do for now.
|
||||
require 'rocco'
|
||||
|
||||
# Reopen the Rocco class and add a `make` class method. This is a simple bit
|
||||
# of sugar over `Rocco::Task.new`. If you want your Rake task to be named
|
||||
# something other than `:rocco`, you can use `Rocco::Task` directly.
|
||||
class Rocco
|
||||
def self.make(dest='docs/', source_files='lib/**/*.rb', options={})
|
||||
Task.new(:rocco, dest, source_files, options)
|
||||
end
|
||||
|
||||
# `Rocco::Task.new` takes a task name, the destination directory docs
|
||||
# should be built under, and a source file pattern or file list.
|
||||
class Task
|
||||
include Rake::DSL if defined?(Rake::DSL)
|
||||
|
||||
def initialize(task_name, dest='docs/', sources='lib/**/*.rb', options={})
|
||||
@name = task_name
|
||||
@dest = dest[-1] == ?/ ? dest : "#{dest}/"
|
||||
@sources = FileList[sources]
|
||||
@options = options
|
||||
|
||||
# Make sure there's a `directory` task defined for our destination.
|
||||
define_directory_task @dest
|
||||
|
||||
# Run over the source file list, constructing destination filenames
|
||||
# and defining file tasks.
|
||||
@sources.each do |source_file|
|
||||
dest_file = source_file.sub(Regexp.new("#{File.extname(source_file)}$"), ".html")
|
||||
define_file_task source_file, "#{@dest}#{dest_file}"
|
||||
|
||||
# If `rake/clean` was required, add the generated files to the list.
|
||||
# That way all Rocco generated are removed when running `rake clean`.
|
||||
CLEAN.include "#{@dest}#{dest_file}" if defined? CLEAN
|
||||
end
|
||||
end
|
||||
|
||||
# Define the destination directory task and make the `:rocco` task depend
|
||||
# on it. This causes the destination directory to be created if it doesn't
|
||||
# already exist.
|
||||
def define_directory_task(path)
|
||||
directory path
|
||||
task @name => path
|
||||
end
|
||||
|
||||
# Setup a `file` task for a single Rocco output file (`dest_file`). It
|
||||
# depends on the source file, the destination directory, and all of Rocco's
|
||||
# internal source code, so that the destination file is rebuilt when any of
|
||||
# those changes.
|
||||
#
|
||||
# You can run these tasks directly with Rake:
|
||||
#
|
||||
# rake docs/foo.html docs/bar.html
|
||||
#
|
||||
# ... would generate the `foo.html` and `bar.html` files but only if they
|
||||
# don't already exist or one of their dependencies was changed.
|
||||
def define_file_task(source_file, dest_file)
|
||||
prerequisites = [@dest, source_file] + rocco_source_files
|
||||
file dest_file => prerequisites do |f|
|
||||
verbose { puts "rocco: #{source_file} -> #{dest_file}" }
|
||||
rocco = Rocco.new(source_file, @sources.to_a, @options)
|
||||
FileUtils.mkdir_p(File.dirname(dest_file))
|
||||
File.open(dest_file, 'wb') { |fd| fd.write(rocco.to_html) }
|
||||
end
|
||||
task @name => dest_file
|
||||
end
|
||||
|
||||
# Return a `FileList` that includes all of Roccos source files. This causes
|
||||
# output files to be regenerated properly when someone upgrades the Rocco
|
||||
# library.
|
||||
def rocco_source_files
|
||||
libdir = File.expand_path('../..', __FILE__)
|
||||
FileList["#{libdir}/rocco.rb", "#{libdir}/rocco/**"]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
56
rocco.gemspec
Normal file
56
rocco.gemspec
Normal file
@ -0,0 +1,56 @@
|
||||
$LOAD_PATH.unshift 'lib'
|
||||
require "rocco"
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.specification_version = 2 if s.respond_to? :specification_version=
|
||||
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
||||
|
||||
s.name = 'rocco'
|
||||
s.version = Rocco::VERSION
|
||||
s.date = Time.now.strftime('%Y-%m-%d')
|
||||
|
||||
s.description = "Docco in Ruby"
|
||||
s.summary = s.description
|
||||
|
||||
s.authors = ["Ryan Tomayko", "Mike West"]
|
||||
s.email = ["r@tomayko.com", "<mike@mikewest.org>"]
|
||||
|
||||
# = MANIFEST =
|
||||
s.files = %w[
|
||||
CHANGES.md
|
||||
COPYING
|
||||
README
|
||||
Rakefile
|
||||
bin/rocco
|
||||
lib/rocco.rb
|
||||
lib/rocco/layout.mustache
|
||||
lib/rocco/layout.rb
|
||||
lib/rocco/tasks.rb
|
||||
rocco.gemspec
|
||||
test/fixtures/issue10.iso-8859-1.rb
|
||||
test/fixtures/issue10.utf-8.rb
|
||||
test/helper.rb
|
||||
test/suite.rb
|
||||
test/test_basics.rb
|
||||
test/test_block_comments.rb
|
||||
test/test_comment_normalization.rb
|
||||
test/test_commentchar_detection.rb
|
||||
test/test_descriptive_section_names.rb
|
||||
test/test_language_detection.rb
|
||||
test/test_reported_issues.rb
|
||||
test/test_skippable_lines.rb
|
||||
test/test_source_list.rb
|
||||
]
|
||||
# = MANIFEST =
|
||||
|
||||
s.executables = ["rocco"]
|
||||
|
||||
s.test_files = s.files.select {|path| path =~ /^test\/.*_test.rb/}
|
||||
s.add_dependency 'rdiscount'
|
||||
s.add_dependency 'mustache'
|
||||
|
||||
s.has_rdoc = false
|
||||
s.homepage = "http://rtomayko.github.com/rocco/"
|
||||
s.require_paths = %w[lib]
|
||||
s.rubygems_version = '1.1.1'
|
||||
end
|
1
test/fixtures/issue10.iso-8859-1.rb
vendored
Normal file
1
test/fixtures/issue10.iso-8859-1.rb
vendored
Normal file
@ -0,0 +1 @@
|
||||
# hello wörld
|
1
test/fixtures/issue10.utf-8.rb
vendored
Normal file
1
test/fixtures/issue10.utf-8.rb
vendored
Normal file
@ -0,0 +1 @@
|
||||
# hello ąćęłńóśźż
|
20
test/helper.rb
Normal file
20
test/helper.rb
Normal file
@ -0,0 +1,20 @@
|
||||
rootdir = File.expand_path('../../lib', __FILE__)
|
||||
$LOAD_PATH.unshift "#{rootdir}/lib"
|
||||
|
||||
require 'test/unit'
|
||||
begin; require 'turn'; rescue LoadError; end
|
||||
begin
|
||||
require 'rdiscount'
|
||||
rescue LoadError
|
||||
if !defined?(Gem)
|
||||
require 'rubygems'
|
||||
retry
|
||||
end
|
||||
end
|
||||
require 'rocco'
|
||||
|
||||
def roccoize( filename, contents, options = {} )
|
||||
Rocco.new( filename, [ filename ], options ) {
|
||||
contents
|
||||
}
|
||||
end
|
5
test/suite.rb
Normal file
5
test/suite.rb
Normal file
@ -0,0 +1,5 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
require 'test/unit'
|
||||
|
||||
Dir[File.expand_path('../test_*.rb', __FILE__)].
|
||||
each { |file| require file }
|
63
test/test_basics.rb
Normal file
63
test/test_basics.rb
Normal file
@ -0,0 +1,63 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoBasicTests < Test::Unit::TestCase
|
||||
def test_rocco_exists_and_is_instancable
|
||||
roccoize( "filename.rb", "# Comment 1\ndef codeblock\nend\n" )
|
||||
end
|
||||
|
||||
def test_filename
|
||||
r = roccoize( "filename.rb", "# Comment 1\ndef codeblock\nend\n" )
|
||||
assert_equal "filename.rb", r.file
|
||||
end
|
||||
|
||||
def test_sources
|
||||
r = roccoize( "filename.rb", "# Comment 1\ndef codeblock\nend\n" )
|
||||
assert_equal [ "filename.rb" ], r.sources
|
||||
end
|
||||
|
||||
def test_sections
|
||||
r = roccoize( "filename.rb", "# Comment 1\ndef codeblock\nend\n" )
|
||||
assert_equal 1, r.sections.length
|
||||
assert_equal 2, r.sections[ 0 ].length
|
||||
assert_equal "<p>Comment 1</p>\n", r.sections[ 0 ][ 0 ]
|
||||
assert_equal "<span class=\"k\">def</span> <span class=\"nf\">codeblock</span>\n<span class=\"k\">end</span>", r.sections[ 0 ][ 1 ]
|
||||
end
|
||||
|
||||
def test_parsing
|
||||
r = Rocco.new( 'test' ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "# Comment 1\ndef codeblock\nend\n" )
|
||||
)
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock" ] ],
|
||||
[ [ "Comment 2" ], [ "end" ] ]
|
||||
],
|
||||
r.parse( "# Comment 1\ndef codeblock\n# Comment 2\nend\n" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_splitting
|
||||
r = Rocco.new( 'test' ) { "" } # Generate throwaway instance so I can test `split`
|
||||
assert_equal(
|
||||
[
|
||||
[ "Comment 1" ],
|
||||
[ "def codeblock\nend" ]
|
||||
],
|
||||
r.split([ [ [ "Comment 1" ], [ "def codeblock", "end" ] ] ])
|
||||
)
|
||||
assert_equal(
|
||||
[
|
||||
[ "Comment 1", "Comment 2" ],
|
||||
[ "def codeblock", "end" ]
|
||||
],
|
||||
r.split( [
|
||||
[ [ "Comment 1" ], [ "def codeblock" ] ],
|
||||
[ [ "Comment 2" ], [ "end" ] ]
|
||||
] )
|
||||
)
|
||||
end
|
||||
end
|
64
test/test_block_comment_styles.rb
Normal file
64
test/test_block_comment_styles.rb
Normal file
@ -0,0 +1,64 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoBlockCommentTest < Test::Unit::TestCase
|
||||
def test_one_liner
|
||||
r = Rocco.new( 'test', '', { :language => "c" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "/** Comment 1 */\ndef codeblock\nend\n" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_block_start_with_comment
|
||||
r = Rocco.new( 'test', '', { :language => "c" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1a", "Comment 1b" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "/** Comment 1a\n * Comment 1b\n */\ndef codeblock\nend\n" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_block_end_with_comment
|
||||
r = Rocco.new( 'test', '', { :language => "c" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1a", "Comment 1b" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "/**\n * Comment 1a\n Comment 1b */\ndef codeblock\nend\n" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_block_end_with_comment_and_middle
|
||||
r = Rocco.new( 'test', '', { :language => "c" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1a", "Comment 1b" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "/**\n * Comment 1a\n * Comment 1b */\ndef codeblock\nend\n" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_block_start_with_comment_and_end_with_comment
|
||||
r = Rocco.new( 'test', '', { :language => "c" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1a", "Comment 1b" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "/** Comment 1a\n Comment 1b */\ndef codeblock\nend\n" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_block_start_with_comment_and_end_with_comment_and_middle
|
||||
r = Rocco.new( 'test', '', { :language => "c" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1a", "Comment 1b" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "/** Comment 1a\n * Comment 1b */\ndef codeblock\nend\n" )
|
||||
)
|
||||
end
|
||||
|
||||
end
|
101
test/test_block_comments.rb
Normal file
101
test/test_block_comments.rb
Normal file
@ -0,0 +1,101 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoBlockCommentTest < Test::Unit::TestCase
|
||||
def test_basics
|
||||
r = Rocco.new( 'test', '', { :language => "c" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "/**\n * Comment 1\n */\ndef codeblock\nend\n" )
|
||||
)
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1a", "Comment 1b" ], [ "def codeblock", "end" ] ]
|
||||
],
|
||||
r.parse( "/**\n * Comment 1a\n * Comment 1b\n */\ndef codeblock\nend\n" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_multiple_blocks
|
||||
r = Rocco.new( 'test', '', { :language => "c" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ],
|
||||
[ [ "Comment 2" ], [] ]
|
||||
],
|
||||
r.parse( "/**\n * Comment 1\n */\ndef codeblock\nend\n/**\n * Comment 2\n */\n" )
|
||||
)
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ],
|
||||
[ [ "Comment 2" ], [ "if false", "end" ] ]
|
||||
],
|
||||
r.parse( "/**\n * Comment 1\n */\ndef codeblock\nend\n/**\n * Comment 2\n */\nif false\nend" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_block_without_middle_character
|
||||
r = Rocco.new( 'test', '', { :language => "python" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ],
|
||||
[ [ "Comment 2" ], [] ]
|
||||
],
|
||||
r.parse( "\"\"\"\n Comment 1\n\"\"\"\ndef codeblock\nend\n\"\"\"\n Comment 2\n\"\"\"\n" )
|
||||
)
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ],
|
||||
[ [ "Comment 2" ], [ "if false", "end" ] ]
|
||||
],
|
||||
r.parse( "\"\"\"\n Comment 1\n\"\"\"\ndef codeblock\nend\n\"\"\"\n Comment 2\n\"\"\"\nif false\nend" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_language_without_single_line_comments_parse
|
||||
r = Rocco.new( 'test', '', { :language => "css" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ],
|
||||
[ [ "Comment 2" ], [ "if false", "end" ] ]
|
||||
],
|
||||
r.parse( "/**\n * Comment 1\n */\ndef codeblock\nend\n/**\n * Comment 2\n */\nif false\nend" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_language_without_single_line_comments_split
|
||||
r = Rocco.new( 'test', '', { :language => "css" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ "Comment 1", "Comment 2" ],
|
||||
[ "def codeblock\nend", "if false\nend" ]
|
||||
],
|
||||
r.split( [
|
||||
[ [ "Comment 1" ], [ "def codeblock", "end" ] ],
|
||||
[ [ "Comment 2" ], [ "if false", "end" ] ]
|
||||
] )
|
||||
)
|
||||
end
|
||||
|
||||
def test_language_without_single_line_comments_highlight
|
||||
r = Rocco.new( 'test', '', { :language => "css" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
|
||||
highlighted = r.highlight( r.split( r.parse( "/**\n * This is a comment!\n */\n.rule { goes: here; }\n/**\n * Comment 2\n */\n.rule2 { goes: here; }" ) ) )
|
||||
assert_equal(
|
||||
"<p>This is a comment!</p>",
|
||||
highlighted[0][0]
|
||||
)
|
||||
assert_equal(
|
||||
"<p>Comment 2</p>\n",
|
||||
highlighted[1][0]
|
||||
)
|
||||
assert(
|
||||
!highlighted[0][1].include?("DIVIDER") &&
|
||||
!highlighted[1][1].include?("DIVIDER"),
|
||||
"`DIVIDER` stripped successfully."
|
||||
)
|
||||
assert( true )
|
||||
end
|
||||
|
||||
end
|
25
test/test_comment_normalization.rb
Normal file
25
test/test_comment_normalization.rb
Normal file
@ -0,0 +1,25 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoCommentNormalization < Test::Unit::TestCase
|
||||
def test_normal_comments
|
||||
r = Rocco.new( 'test', '', { :language => "python" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1a", "Comment 1b" ], [ "def codeblock", "end" ] ],
|
||||
[ [ "Comment 2a", " Comment 2b" ], [] ]
|
||||
],
|
||||
r.parse( "\"\"\"\n Comment 1a\n Comment 1b\n\"\"\"\ndef codeblock\nend\n\"\"\"\n Comment 2a\n Comment 2b\n\"\"\"\n" )
|
||||
)
|
||||
end
|
||||
|
||||
def test_single_line_comments
|
||||
r = Rocco.new( 'test', '', { :language => "python" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1a", "Comment 1b" ], [ "def codeblock", "end" ] ],
|
||||
[ [ "Comment 2a", " Comment 2b" ], [] ]
|
||||
],
|
||||
r.parse( "# Comment 1a\n# Comment 1b\ndef codeblock\nend\n# Comment 2a\n# Comment 2b\n" )
|
||||
)
|
||||
end
|
||||
end
|
28
test/test_commentchar_detection.rb
Normal file
28
test/test_commentchar_detection.rb
Normal file
@ -0,0 +1,28 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoAutomaticCommentChars < Test::Unit::TestCase
|
||||
def test_basic_detection
|
||||
r = Rocco.new( 'filename.js' ) { "" }
|
||||
assert_equal "//", r.options[:comment_chars][:single]
|
||||
end
|
||||
|
||||
def test_fallback_language
|
||||
r = Rocco.new( 'filename.an_extension_with_no_meaning_whatsoever', '', { :language => "js" } ) { "" }
|
||||
assert_equal "//", r.options[:comment_chars][:single]
|
||||
end
|
||||
|
||||
def test_fallback_default
|
||||
r = Rocco.new( 'filename.an_extension_with_no_meaning_whatsoever' ) { "" }
|
||||
assert_equal "#", r.options[:comment_chars][:single], "`:comment_chars` should be `#` when falling back to defaults."
|
||||
end
|
||||
|
||||
def test_fallback_user
|
||||
r = Rocco.new( 'filename.an_extension_with_no_meaning_whatsoever', '', { :comment_chars => "user" } ) { "" }
|
||||
assert_equal "user", r.options[:comment_chars][:single], "`:comment_chars` should be the user's default when falling back to user-provided settings."
|
||||
end
|
||||
|
||||
def test_fallback_user_with_unknown_language
|
||||
r = Rocco.new( 'filename.an_extension_with_no_meaning_whatsoever', '', { :language => "not-a-language", :comment_chars => "user" } ) { "" }
|
||||
assert_equal "user", r.options[:comment_chars][:single], "`:comment_chars` should be the user's default when falling back to user-provided settings."
|
||||
end
|
||||
end
|
30
test/test_descriptive_section_names.rb
Normal file
30
test/test_descriptive_section_names.rb
Normal file
@ -0,0 +1,30 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoDescriptiveSectionNamesTests < Test::Unit::TestCase
|
||||
def test_section_name
|
||||
r = roccoize( "filename.rb", "# # Comment 1\ndef codeblock\nend\n" )
|
||||
html = r.to_html
|
||||
assert(
|
||||
html.include?( "<tr id='section-Comment_1'>" ),
|
||||
"The first section should be named"
|
||||
)
|
||||
assert(
|
||||
html.include?( '<a class="pilcrow" href="#section-Comment_1">' ),
|
||||
"The rendered HTML should link to a named section"
|
||||
)
|
||||
end
|
||||
|
||||
def test_section_numbering
|
||||
r = roccoize( "filename.rb", "# # Header 1\ndef codeblock\nend\n# Comment 1\ndef codeblock1\nend\n# # Header 2\ndef codeblock2\nend" )
|
||||
html = r.to_html
|
||||
assert(
|
||||
html.include?( '<a class="pilcrow" href="#section-Header_1">' ) &&
|
||||
html.include?( '<a class="pilcrow" href="#section-Header_2">' ),
|
||||
"First and second headers should be named sections"
|
||||
)
|
||||
assert(
|
||||
html.include?( '<a class="pilcrow" href="#section-2">' ),
|
||||
"Sections should continue numbering as though headers were counted."
|
||||
)
|
||||
end
|
||||
end
|
22
test/test_docblock_annotations.rb
Normal file
22
test/test_docblock_annotations.rb
Normal file
@ -0,0 +1,22 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoDocblockAnnotationsTest < Test::Unit::TestCase
|
||||
def test_basics
|
||||
r = Rocco.new( 'test', '', { :language => "c", :docblocks => true } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
"Comment\n\n> **param** mixed foo \n> **return** void "
|
||||
],
|
||||
r.docblock( ["Comment\n\n@param mixed foo\n@return void"] )
|
||||
)
|
||||
end
|
||||
def test_highlighted_in_blocks
|
||||
r = Rocco.new( 'test', '', { :language => "c", :docblocks => true } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
highlighted = r.highlight( r.split( r.parse( "/**\n * Comment\n * @param type name\n */\ndef codeblock\nend\n" ) ) )
|
||||
|
||||
assert_equal(
|
||||
"<p>Comment</p>\n\n<blockquote><p><strong>param</strong> type name</p></blockquote>\n",
|
||||
highlighted[0][0]
|
||||
)
|
||||
end
|
||||
end
|
13
test/test_heredoc.rb
Normal file
13
test/test_heredoc.rb
Normal file
@ -0,0 +1,13 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoHeredocTest < Test::Unit::TestCase
|
||||
def test_basics
|
||||
r = Rocco.new( 'test', '', { :language => "rb" } ) { "" } # Generate throwaway instance so I can test `parse`
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "heredoc <<-EOH", "#comment", "code", "EOH" ] ]
|
||||
],
|
||||
r.parse( "# Comment 1\nheredoc <<-EOH\n#comment\ncode\nEOH" )
|
||||
)
|
||||
end
|
||||
end
|
27
test/test_language_detection.rb
Normal file
27
test/test_language_detection.rb
Normal file
@ -0,0 +1,27 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoLanguageDetection < Test::Unit::TestCase
|
||||
def test_basic_detection
|
||||
r = Rocco.new( 'filename.py' ) { "" }
|
||||
if r.pygmentize?
|
||||
assert_equal "python", r.detect_language(), "`detect_language()` should return the correct language"
|
||||
assert_equal "python", r.options[:language], "`@options[:language]` should be set to the correct language"
|
||||
end
|
||||
end
|
||||
|
||||
def test_fallback_default
|
||||
r = Rocco.new( 'filename.an_extension_with_no_meaning_whatsoever' ) { "" }
|
||||
if r.pygmentize?
|
||||
assert_equal "text", r.detect_language(), "`detect_language()` should return `text` when nothing else is detected"
|
||||
assert_equal "ruby", r.options[:language], "`@options[:language]` should be set to `ruby` when nothing else is detected"
|
||||
end
|
||||
end
|
||||
|
||||
def test_fallback_user
|
||||
r = Rocco.new( 'filename.an_extension_with_no_meaning_whatsoever', '', { :language => "c" } ) { "" }
|
||||
if r.pygmentize?
|
||||
assert_equal "text", r.detect_language(), "`detect_language()` should return `text` nothing else is detected"
|
||||
assert_equal "c", r.options[:language], "`@options[:language]` should be set to the user's setting when nothing else is detected"
|
||||
end
|
||||
end
|
||||
end
|
86
test/test_reported_issues.rb
Normal file
86
test/test_reported_issues.rb
Normal file
@ -0,0 +1,86 @@
|
||||
# encoding: utf-8
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoIssueTests < Test::Unit::TestCase
|
||||
def test_issue07_incorrect_parsing_in_c_mode
|
||||
# Precursor to issue #13 below, Rocco incorrectly parsed C/C++
|
||||
# http://github.com/rtomayko/rocco/issues#issue/7
|
||||
r = Rocco.new( 'issue7.c', [], { :language => 'c' } ) {
|
||||
"// *stdio.h* declares *puts*\n#include <stdio.h>\n\n//### code hello world\n\n// every C program contains function *main*.\nint main (int argc, char *argv[]) {\n puts('hello world');\n return 0;\n}\n\n// that's it!"
|
||||
}
|
||||
r.sections.each do | section |
|
||||
if not section[1].nil?
|
||||
assert(
|
||||
!section[1].include?("<span class=\"c\"># DIVIDER</span>"),
|
||||
"`# DIVIDER` present in code text, which means the highligher screwed up somewhere."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_issue10_utf8_processing
|
||||
# Rocco has issues with strange UTF-8 characters: need to explicitly set the encoding for Pygments
|
||||
# http://github.com/rtomayko/rocco/issues#issue/10
|
||||
r = Rocco.new( File.dirname(__FILE__) + "/fixtures/issue10.utf-8.rb" )
|
||||
assert_equal(
|
||||
"<p>hello ąćęłńóśźż</p>\n",
|
||||
r.sections[0][0],
|
||||
"UTF-8 input files ought behave correctly."
|
||||
)
|
||||
# and, just for grins, ensure that iso-8859-1 works too.
|
||||
# @TODO: Is this really the correct behavior? Converting text
|
||||
# to UTF-8 on the way out is probably preferable.
|
||||
r = Rocco.new( File.dirname(__FILE__) + "/fixtures/issue10.iso-8859-1.rb" )
|
||||
assert_equal(
|
||||
"<p>hello w\366rld</p>\n",
|
||||
r.sections[0][0],
|
||||
"ISO-8859-1 input should probably also behave correctly."
|
||||
)
|
||||
end
|
||||
|
||||
def test_issue12_css_octothorpe_classname_change
|
||||
# Docco changed some CSS classes. Rocco needs to update its default template.
|
||||
# http://github.com/rtomayko/rocco/issues#issue/12
|
||||
r = Rocco.new( 'issue12.sh' ) {
|
||||
"# Comment 1\n# Comment 1\nprint 'omg!'"
|
||||
}
|
||||
html = r.to_html
|
||||
assert(
|
||||
!html.include?( "<div class=\"octowrap\">" ),
|
||||
"`octowrap` wrapper is present in rendered HTML. This ought be replaced with `pilwrap`."
|
||||
)
|
||||
assert(
|
||||
!html.include?( "<a class=\"octothorpe\" href=\"#section-1\">" ),
|
||||
"`octothorpe` link is present in rendered HTML. This ought be replaced with `pilcrow`."
|
||||
)
|
||||
end
|
||||
|
||||
def test_issue13_incorrect_code_divider_parsing
|
||||
# In `bash` mode (among others), the comment class is `c`, not `c1`.
|
||||
# http://github.com/rtomayko/rocco/issues#issue/13
|
||||
r = Rocco.new( 'issue13.sh', [], { :language => 'bash' } ) {
|
||||
"# Comment 1\necho 'code';\n# Comment 2\necho 'code';\n# Comment 3\necho 'code';\n"
|
||||
}
|
||||
r.sections.each do | section |
|
||||
if not section[1].nil?
|
||||
assert(
|
||||
!section[1].include?("<span class=\"c\"># DIVIDER</span>"),
|
||||
"`# DIVIDER` present in code text, which means the highligher screwed up somewhere."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_issue15_extra_space_after_comment_character_remains
|
||||
# After the comment character, a single space should be removed.
|
||||
# http://github.com/rtomayko/rocco/issues#issue/15
|
||||
r = Rocco.new( 'issue15.sh') {
|
||||
"# Comment 1\n# ---------\necho 'code';"
|
||||
}
|
||||
assert(
|
||||
!r.sections[0][0].include?( "<hr />" ),
|
||||
"`<hr />` present in rendered documentation text. It should be a header, not text followed by a horizontal rule."
|
||||
)
|
||||
assert_equal("<h2>Comment 1</h2>\n", r.sections[0][0])
|
||||
end
|
||||
end
|
64
test/test_skippable_lines.rb
Normal file
64
test/test_skippable_lines.rb
Normal file
@ -0,0 +1,64 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoSkippableLines < Test::Unit::TestCase
|
||||
def test_shebang_first_line
|
||||
r = Rocco.new( 'filename.sh' ) { "" }
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock" ] ],
|
||||
[ [ "Comment 2" ], [ "end" ] ]
|
||||
],
|
||||
r.parse( "#!/usr/bin/env bash\n# Comment 1\ndef codeblock\n# Comment 2\nend\n" ),
|
||||
"Shebang should be stripped when it appears as the first line."
|
||||
)
|
||||
end
|
||||
|
||||
def test_shebang_in_content
|
||||
r = Rocco.new( 'filename.sh' ) { "" }
|
||||
assert_equal(
|
||||
[
|
||||
# @TODO: `#!/` shouldn't be recognized as a comment.
|
||||
[ [ "Comment 1", "!/usr/bin/env bash" ], [ "def codeblock" ] ],
|
||||
[ [ "Comment 2" ], [ "end" ] ]
|
||||
],
|
||||
r.parse( "# Comment 1\n#!/usr/bin/env bash\ndef codeblock\n# Comment 2\nend\n" ),
|
||||
"Shebang shouldn't be stripped anywhere other than as the first line."
|
||||
)
|
||||
end
|
||||
|
||||
def test_encoding_in_ruby
|
||||
r = Rocco.new( 'filename.rb' ) { "" }
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock" ] ],
|
||||
[ [ "Comment 2" ], [ "end" ] ]
|
||||
],
|
||||
r.parse( "#!/usr/bin/env bash\n# encoding: utf-8\n# Comment 1\ndef codeblock\n# Comment 2\nend\n" ),
|
||||
"Strings matching the PEP 263 encoding definition regex should be stripped when they appear at the top of a python document."
|
||||
)
|
||||
end
|
||||
|
||||
def test_encoding_in_python
|
||||
r = Rocco.new( 'filename.py' ) { "" }
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "Comment 1" ], [ "def codeblock" ] ],
|
||||
[ [ "Comment 2" ], [ "end" ] ]
|
||||
],
|
||||
r.parse( "#!/usr/bin/env bash\n# encoding: utf-8\n# Comment 1\ndef codeblock\n# Comment 2\nend\n" ),
|
||||
"Strings matching the PEP 263 encoding definition regex should be stripped when they appear at the top of a python document."
|
||||
)
|
||||
end
|
||||
|
||||
def test_encoding_in_notpython
|
||||
r = Rocco.new( 'filename.sh' ) { "" }
|
||||
assert_equal(
|
||||
[
|
||||
[ [ "encoding: utf-8", "Comment 1" ], [ "def codeblock" ] ],
|
||||
[ [ "Comment 2" ], [ "end" ] ]
|
||||
],
|
||||
r.parse( "#!/usr/bin/env bash\n# encoding: utf-8\n# Comment 1\ndef codeblock\n# Comment 2\nend\n" ),
|
||||
"Strings matching the PEP 263 encoding definition regex should be stripped when they appear at the top of a python document."
|
||||
)
|
||||
end
|
||||
end
|
29
test/test_source_list.rb
Normal file
29
test/test_source_list.rb
Normal file
@ -0,0 +1,29 @@
|
||||
require File.expand_path('../helper', __FILE__)
|
||||
|
||||
class RoccoSourceListTests < Test::Unit::TestCase
|
||||
def test_flat_sourcelist
|
||||
r = Rocco.new( 'issue26.sh', [ 'issue26a.sh', 'issue26b.sh', 'issue26c.sh' ] ) {
|
||||
"# Comment 1\n# Comment 1\nprint 'omg!'"
|
||||
}
|
||||
html = r.to_html
|
||||
assert(
|
||||
html.include?( '<a class="source" href="issue26a.html">issue26a.sh</a>' ) &&
|
||||
html.include?( '<a class="source" href="issue26b.html">issue26b.sh</a>' ) &&
|
||||
html.include?( '<a class="source" href="issue26c.html">issue26c.sh</a>' ),
|
||||
"URLs correctly generated for files in a flat directory structure"
|
||||
)
|
||||
end
|
||||
|
||||
def test_heiarachical_sourcelist
|
||||
r = Rocco.new( 'a/issue26.sh', [ 'a/issue26a.sh', 'b/issue26b.sh', 'c/issue26c.sh' ] ) {
|
||||
"# Comment 1\n# Comment 1\nprint 'omg!'"
|
||||
}
|
||||
html = r.to_html
|
||||
assert(
|
||||
html.include?( '<a class="source" href="issue26a.html">issue26a.sh</a>' ) &&
|
||||
html.include?( '<a class="source" href="../b/issue26b.html">issue26b.sh</a>' ) &&
|
||||
html.include?( '<a class="source" href="../c/issue26c.html">issue26c.sh</a>' ),
|
||||
"URLs correctly generated for files in a flat directory structure"
|
||||
)
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user