dkastner-rocco 0.8

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