fl-rocco 1.0.0

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