fl-rocco 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.