dkastner-rocco 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.