diff --git a/index.html b/index.html index ad58300..d28252b 100644 --- a/index.html +++ b/index.html @@ -3,11 +3,21 @@
- | |
- | @@ -65,8 +75,8 @@ for a set of Ruby source files:|
-
- #
+
+ ¶
We’ll need a Markdown library. RDiscount, if we’re lucky. Otherwise, issue a warning and fall back on using BlueCloth. @@ -75,7 +85,7 @@ issue a warning and fall back on using BlueCloth.begin
require 'rdiscount'
rescue LoadError => boom
- warn "warn: #{boom}. trying bluecloth"
+ warn "WARNING: #{boom}. Trying bluecloth."
require 'bluecloth'
Markdown = BlueCloth
end | |
- | |
-
- #
+
+ ¶
- Code is run through Pygments for syntax
-highlighting. Fail fast right here if we can’t find the We use |
- if ! ENV['PATH'].split(':').any? { |dir| File.exist?("#{dir}/pygmentize") }
- fail "Pygments is required for syntax highlighting"
-end require 'net/http' |
- | +
+ include FileTest
+if !ENV['PATH'].split(':').any? { |dir| executable?("#{dir}/pygmentize") }
+ warn "WARNING: Pygments not found. Using webservice."
+end |
+
+
+ ¶
Public Interface |
@@ -119,36 +140,146 @@ program on PATH.
|
-
- #
+
+ ¶
-
|
class Rocco
- VERSION = '0.2'
+ VERSION = '0.6'
- def initialize(filename, &block)
- @file = filename
- @data =
+ def initialize(filename, sources=[], options={}, &block)
+ @file = filename
+ @sources = sources |
+
+
+ ¶
+
+ When |
+
+ @data =
if block_given?
yield
else
File.read(filename)
end
- @sections = highlight(split(parse(@data)))
+
+ defaults = {
+ :language => 'ruby',
+ :comment_chars => '#',
+ :template_file => nil
+ }
+ @options = defaults.merge(options) |
+
+
+ ¶
+
+ If we detect a language + |
+
+ if detect_language() != "text" |
+
+
+ ¶
+
+ then assign the detected language to |
+
+ @options[:language] = detect_language()
+ @options[:comment_chars] = generate_comment_chars() |
+
+
+ ¶
+
+ If we didn’t detect a language, but the user provided one, use it +to look around for comment characters to override the default. + |
+
+ elsif @options[:language] != defaults[:language]
+ @options[:comment_chars] = generate_comment_chars() |
+
+
+ ¶
+
+ If neither is true, then convert the default comment character string
+into the comment_char syntax (we’ll discuss that syntax in detail when
+we get to |
+
+ else
+ @options[:comment_chars] = {
+ :single => @options[:comment_chars],
+ :multi => nil
+ }
+ end |
+
+
+ ¶
+
+ Turn |
+
+ @comment_pattern =
+ Regexp.new("^\\s*#{@options[:comment_chars][:single]}\s?") |
+
+
+ ¶
+
+
|
+
+ @sections = highlight(split(parse(@data)))
end |
- | @@ -156,10 +287,21 @@ file is read to retrieve data.|
- | +
+ attr_reader :options |
+
+
+ ¶
A list of two-tuples representing each section of the source file. Each
item in the list has the form: attr_reader :sections |
|
- | +
+ attr_reader :sources |
+
+
+ ¶
Generate HTML output for the entire document. |
require 'rocco/layout'
def to_html
- Rocco::Layout.new(self).render
+ Rocco::Layout.new(self, @options[:template_file]).render
end |
- |
|
- | +
+ def pygmentize?
+ @_pygmentize ||= ENV['PATH'].split(':').
+ any? { |dir| executable?("#{dir}/pygmentize") }
+ end |
+
+
+ ¶
+
+ If We’ll memoize the result, as we’ll call this a few times. + |
+
+ def detect_language
+ @_language ||=
+ if pygmentize?
+ %x[pygmentize -N #{@file}].strip!
+ else
+ "text"
+ end
+ end |
+
+
+ ¶
+
+ Given a file’s language, we should be able to autopopulate the
+ Comment characters are listed as: + +
+
+
+
+If a language only has one type of comment, the missing type
+should be assigned At the moment, we’re only returning |
+
+ COMMENT_STYLES = {
+ "bash" => { :single => "#", :multi => nil },
+ "c" => {
+ :single => "//",
+ :multi => { :start => "/**", :middle => "*", :end => "*/" }
+ },
+ "coffee-script" => {
+ :single => "#",
+ :multi => { :start => "###", :middle => nil, :end => "###" }
+ },
+ "cpp" => {
+ :single => "//",
+ :multi => { :start => "/**", :middle => "*", :end => "*/" }
+ },
+ "css" => {
+ :single => nil,
+ :multi => { :start => "/**", :middle => "*", :end => "*/" }
+ },
+ "java" => {
+ :single => "//",
+ :multi => { :start => "/**", :middle => "*", :end => "*/" }
+ },
+ "js" => {
+ :single => "//",
+ :multi => { :start => "/**", :middle => "*", :end => "*/" }
+ },
+ "lua" => {
+ :single => "--",
+ :multi => nil
+ },
+ "python" => {
+ :single => "#",
+ :multi => { :start => '"""', :middle => nil, :end => '"""' }
+ },
+ "rb" => {
+ :single => "#",
+ :multi => { :start => '=begin', :middle => nil, :end => '=end' }
+ },
+ "scheme" => { :single => ";;", :multi => nil },
+ }
+
+ def generate_comment_chars
+ @_commentchar ||=
+ if COMMENT_STYLES[@options[:language]]
+ COMMENT_STYLES[@options[:language]]
+ else
+ { :single => @options[:comment_chars], :multi => nil }
+ end
+ end |
+
+
+ ¶
+
+ Internal Parsing and Highlighting+ |
+
+
+ |
+
+
+ ¶
Parse the raw file data into a list of two-tuples. Each tuple has the
form |
def parse(data)
sections = []
docs, code = [], []
- data.split("\n").each do |line|
- case line
- when /^\s*#/
- if code.any?
- sections << [docs, code]
- docs, code = [], []
+ lines = data.split("\n") |
+
+
+ ¶
+
+ The first line is ignored if it is a shebang line. We also ignore the +PEP 263 encoding information in python sourcefiles, and the similar ruby +1.9 syntax. + |
+
+ lines.shift if lines[0] =~ /^\#\!/
+ lines.shift if lines[0] =~ /coding[:=]\s*[-\w.]+/ &&
+ [ "python", "rb" ].include?(@options[:language]) |
+
+
+ ¶
+
+ To detect both block comments and single-line comments, we’ll set
+up a tiny state machine, and loop through each line of the file.
+This requires an |
+
+ in_comment_block = false
+ single_line_comment, block_comment_start, block_comment_mid, block_comment_end =
+ nil, nil, nil, nil
+ if not @options[:comment_chars][:single].nil?
+ single_line_comment = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:single])}\\s?")
+ end
+ if not @options[:comment_chars][:multi].nil?
+ block_comment_start = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:start])}\\s*$")
+ block_comment_end = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:end])}\\s*$")
+ if @options[:comment_chars][:multi][:middle]
+ block_comment_mid = Regexp.new("^\\s*#{Regexp.escape(@options[:comment_chars][:multi][:middle])}\\s?")
+ end
+ end
+ lines.each do |line| |
+
+
+ ¶
+
+ If we’re currently in a comment block, check whether the line matches +the end of a comment block. + |
+
+ if in_comment_block
+ if block_comment_end && line.match( block_comment_end )
+ in_comment_block = false
+ else
+ docs << line.sub( block_comment_mid || '', '' )
+ end |
+
+
+ ¶
+
+ Otherwise, check whether the line matches the beginning of a block, or +a single-line comment all on it’s lonesome. In either case, if there’s +code, start a new section + |
+
+ else
+ if block_comment_start && line.match( block_comment_start )
+ in_comment_block = true
+ if code.any?
+ sections << [docs, code]
+ docs, code = [], []
+ end
+ elsif single_line_comment && line.match( single_line_comment )
+ if code.any?
+ sections << [docs, code]
+ docs, code = [], []
+ end
+ docs << line.sub( single_line_comment || '', '' )
+ else
+ code << line
end
- docs << line
- else
- code << line
end
end
sections << [docs, code] if docs.any? || code.any?
- sections
+ normalize_leading_spaces( sections )
end |
-
- #
+
+ ¶
+
+ Normalizes documentation whitespace by checking for leading whitespace, +removing it, and then removing the same amount of whitespace from each +succeeding line. That is: + +
+
+should yield a comment block of |
+
+ def normalize_leading_spaces( sections )
+ sections.map do |section|
+ if section.any? && section[0].any?
+ leading_space = section[0][0].match( "^\s+" )
+ if leading_space
+ section[0] =
+ section[0].map{ |line| line.sub( /^#{leading_space.to_s}/, '' ) }
+ end
+ end
+ section
+ end
+ end |
+
+
+ ¶
Take the list of paired sections two-tuples and split into two separate lists: one holding the comments with leaders removed and @@ -238,17 +644,20 @@ one with the code blocks. def split(sections)
docs_blocks, code_blocks = [], []
sections.each do |docs,code|
- docs_blocks << docs.map { |line| line.sub(/^\s*#\s?/, '') }.join("\n")
- code_blocks << code.join("\n")
+ docs_blocks << docs.join("\n")
+ code_blocks << code.map do |line|
+ tabs = line.match(/^(\t+)/)
+ tabs ? line.sub(/^\t+/, ' ' * tabs.captures[0].length) : line
+ end.join("\n")
end
[docs_blocks, code_blocks]
end |
|
- | |
- | |
-
- #
+
+ ¶
- Combine all code blocks into a single big stream and run through
-Pygments. We Combine all code blocks into a single big stream with section dividers and
+run through either |
+
+ span, espan = '<span class="c.?">', '</span>'
+ if @options[:comment_chars][:single]
+ front = @options[:comment_chars][:single]
+ divider_input = "\n\n#{front} DIVIDER\n\n"
+ divider_output = Regexp.new(
+ [ "\\n*",
+ span,
+ Regexp.escape(front),
+ ' DIVIDER',
+ espan,
+ "\\n*"
+ ].join, Regexp::MULTILINE
+ )
+ else
+ front = @options[:comment_chars][:multi][:start]
+ back = @options[:comment_chars][:multi][:end]
+ divider_input = "\n\n#{front}\nDIVIDER\n#{back}\n\n"
+ divider_output = Regexp.new(
+ [ "\\n*",
+ span, Regexp.escape(front), espan,
+ "\\n",
+ span, "DIVIDER", espan,
+ "\\n",
+ span, Regexp.escape(back), espan,
+ "\\n*"
+ ].join, Regexp::MULTILINE
+ )
+ end
+
+ code_stream = code_blocks.join( divider_input )
+
+ code_html =
+ if pygmentize?
+ highlight_pygmentize(code_stream)
+ else
+ highlight_webservice(code_stream)
+ end |
+
+
+ ¶
+
+ Do some post-processing on the pygments output to split things back
+into sections and remove partial |
+
+ code_html = code_html.
+ split(divider_output).
+ map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
+ map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') } |
+
+
+ ¶
+
+ Lastly, combine the docs and code lists back into a list of two-tuples. + |
+
+ docs_html.zip(code_html)
+ end |
+
+
+ ¶
+
+ We |
- code_html = nil
- open("|pygmentize -l ruby -f html", 'r+') do |fd|
+ |
- |
- code_html = code_html.
- split(/\n*<span class="c1"># DIVIDER<\/span>\n*/m).
- map { |code| code.sub(/\n?<div class="highlight"><pre>/m, '') }.
- map { |code| code.sub(/\n?<\/pre><\/div>\n/m, '') } |
-
-
- #
-
- Lastly, combine the docs and code lists back into a list of two-tuples. - |
-
- docs_html.zip(code_html)
+ |
- |
-
- #
+
+ ¶
@@ -30,8 +40,11 @@
class Rocco::Layout < Mustache
self.template_path = File.dirname(__FILE__)
- def initialize(doc)
+ def initialize(doc, file=nil)
@doc = doc
+ if not file.nil?
+ Rocco::Layout.template_file = file
+ end
end
def title
@@ -42,9 +55,29 @@
num = 0
@doc.sections.map do |docs,code|
{
- :docs => docs,
- :code => code,
- :num => (num += 1)
+ :docs => docs,
+ :docs? => !docs.empty?,
+ :header? => /^<h.>.+<\/h.>$/.match( docs ),
+
+ :code => code,
+ :code? => !code.empty?,
+
+ :empty? => ( code.empty? && docs.empty? ),
+ :num => (num += 1)
+ }
+ end
+ end
+
+ def sources?
+ @doc.sources.length > 1
+ end
+
+ def sources
+ @doc.sources.sort.map do |source|
+ {
+ :path => source,
+ :basename => File.basename(source),
+ :url => File.basename(source).split('.')[0..-2].join('.') + '.html'
}
end
end
diff --git a/rocco.html b/rocco.html
index ad58300..ef132f3 100644
--- a/rocco.html
+++ b/rocco.html
@@ -8,6 +8,16 @@
+
|