Class: Rocco

Inherits:
Object
  • Object
show all
Defined in:
lib/rocco.rb,
lib/rocco/tasks.rb

Overview

Reopen the Rocco class and add a ‘make` class method. This is a simple bit of sugar over `Rocco::Task.new`. If you want your Rake task to be named something other than `:rocco`, you can use `Rocco::Task` directly.

Defined Under Namespace

Classes: Layout, Task

Constant Summary collapse

VERSION =
'0.5'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename, sources = [], options = {}, &block) ⇒ Rocco

Returns a new instance of Rocco.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/rocco.rb', line 79

def initialize(filename, sources=[], options={}, &block)
  @file       = filename
  @sources    = sources

  # When `block` is given, it must read the contents of the file using
  # whatever means necessary and return it as a string. With no `block`,
  # the file is read to retrieve data.
  @data =
    if block_given?
      yield
    else
      File.read(filename)
    end
  defaults = {
    :language      => 'ruby',
    :comment_chars => '#',
    :template_file => nil
  }
  @options = defaults.merge(options)
  @template_file = @options[:template_file]

  # If we detect a language
  if detect_language() != "text"
      # then assign the detected language to `:language`
      @options[:language] = detect_language()
      # and look for some comment characters
      @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()
  end
    
  if(@options[:comment_chars][:regex]) 
    @comment_pattern = Regexp.new(@options[:comment_chars][:regex])
  else @comment_pattern = Regexp.new("^\\s*#{@options[:comment_chars]}\s?")
  end
  puts @comment_pattern.source
  @sections = highlight(split(parse(@data)))
end

Instance Attribute Details

#fileObject (readonly)

The filename as given to ‘Rocco.new`.



196
197
198
# File 'lib/rocco.rb', line 196

def file
  @file
end

#optionsObject (readonly)

The merged options array



199
200
201
# File 'lib/rocco.rb', line 199

def options
  @options
end

#sectionsObject (readonly)

A list of two-tuples representing each section of the source file. Each item in the list has the form: ‘[docs_html, code_html]`, where both elements are strings containing the documentation and source code HTML, respectively.



205
206
207
# File 'lib/rocco.rb', line 205

def sections
  @sections
end

#sourcesObject (readonly)

A list of all source filenames included in the documentation set. Useful for building an index of other files.



209
210
211
# File 'lib/rocco.rb', line 209

def sources
  @sources
end

#template_fileObject (readonly)

An absolute path to a file that ought be used as a template for the HTML-rendered documentation.



213
214
215
# File 'lib/rocco.rb', line 213

def template_file
  @template_file
end

Class Method Details

.make(dest = 'docs/', source_files = 'lib/**/*.rb', options = {}) ⇒ Object



54
55
56
# File 'lib/rocco/tasks.rb', line 54

def self.make(dest='docs/', source_files='lib/**/*.rb', options={})
  Task.new(:rocco, dest, source_files, options)
end

Instance Method Details

#detect_languageObject

If ‘pygmentize` is available, we can use it to autodetect a file’s language based on its filename. Filenames without extensions, or with extensions that ‘pygmentize` doesn’t understand will return ‘text`. We’ll also return ‘text` if `pygmentize` isn’t available.

We’ll memoize the result, as we’ll call this a few times.



132
133
134
135
136
137
138
139
140
# File 'lib/rocco.rb', line 132

def detect_language
  @_language ||= begin
      if pygmentize?
          lang = %x[pygmentize -N #{@file}].strip!
      else
          "text"
      end
  end
end

#generate_comment_charsObject

Given a file’s language, we should be able to autopopulate the ‘comment_chars` variables for single-line comments. If we don’t have comment characters on record for a given language, we’ll use the user-provided ‘:comment_char` option (which defaults to `#`).

Comment characters are listed as:

{ :single => "//", :multi_start => "/**", :multi_middle => "*", :multi_end => "*/" }

‘:single` denotes the leading character of a single-line comment. `:multi_start` denotes the string that should appear alone on a line of code to begin a block of documentation. `:multi_middle` denotes the leading character of block comment content, and `:multi_end` is the string that ought appear alone on a line to close a block of documentation. That is:

/**                 [:multi][:start]
 *                  [:multi][:middle]
 *                  [:multi][:middle]
 *                  [:multi][:middle]
 */                 [:multi][:end]

If a language only has one type of comment, the missing type should be assigned ‘nil`.

At the moment, we’re only returning ‘:single`. Consider this groundwork for block comment parsing.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/rocco.rb', line 170

def generate_comment_chars
  @_commentchar ||= begin
      language        = @options[:language]
      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 => "*/" } },
          "java"          =>  { :single => "//",  :multi => { :start => "/**", :middle => "*", :end => "*/" } },
          "js"            =>  { :single => "//",  :multi => { :start => "/**", :middle => "*", :end => "*/" } },
          "php"           =>  { :single => "//",  :multi => { :start => "/**", :middle => "*", :end => "*/"}, :regex => "//(.*)|(?:\\*+\\n)(?m:(.*?))\\*+/" },
          "lua"           =>  { :single => "--",  :multi => nil },
          "python"        =>  { :single => "#",   :multi => { :start => '"""', :middle => nil, :end => '"""' } },
          "ruby"          =>  { :single => "#",   :multi => nil },
          "scheme"        =>  { :single => ";;",  :multi => nil },
      }

      if comment_styles[language]
          comment_styles[language]
      else
          @options[:comment_chars]
      end
  end
end

#highlight(blocks) ⇒ Object

Take the result of ‘split` and apply Markdown formatting to comments and syntax highlighting to source code.



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/rocco.rb', line 265

def highlight(blocks)
  docs_blocks, code_blocks = blocks

  # Combine all docs blocks into a single big markdown document with section
  # dividers and run through the Markdown processor. Then split it back out
  # into separate sections.
  markdown = docs_blocks.join("\n\n##### DIVIDER\n\n")
  docs_html = Markdown.new(markdown, :smart).
    to_html.
    split(/\n*<h5>DIVIDER<\/h5>\n*/m)

  # Combine all code blocks into a single big stream and run through either
  # `pygmentize(1)` or <http://pygments.appspot.com>
  code_stream = code_blocks.join("\n\n#{@options[:comment_chars][:single]} DIVIDER\n\n")

  if pygmentize? 
    code_html = highlight_pygmentize(code_stream)
  else 
    code_html = highlight_webservice(code_stream)
  end

  # Do some post-processing on the pygments output to split things back
  # into sections and remove partial `<pre>` blocks.
  code_html = code_html.
    split(/\n*<span class="c.?">#{@options[:comment_chars][:single]} 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)
end

#highlight_pygmentize(code) ⇒ Object

We ‘popen` a read/write pygmentize process in the parent and then fork off a child process to write the input.



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/rocco.rb', line 299

def highlight_pygmentize(code)
  code_html = nil
  open("|pygmentize -l #{@options[:language]} -O encoding=utf-8 -f html", 'r+') do |fd|
    pid =
      fork {
        fd.close_read
        fd.write code
        fd.close_write
        exit!
      }
    fd.close_write
    code_html = fd.read
    fd.close_read
    Process.wait(pid)
  end

  code_html
end

#highlight_webservice(code) ⇒ Object

Pygments is not one of those things that’s trivial for a ruby user to install, so we’ll fall back on a webservice to highlight the code if it isn’t available.



320
321
322
323
324
325
# File 'lib/rocco.rb', line 320

def highlight_webservice(code)
  Net::HTTP.post_form(
    URI.parse('http://pygments.appspot.com/'),
    {'lang' => @options[:language], 'code' => code}
  ).body
end

#parse(data) ⇒ Object

Parse the raw file data into a list of two-tuples. Each tuple has the form ‘[docs, code]` where both elements are arrays containing the raw lines parsed from the input file. The first line is ignored if it is a shebang line.



227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/rocco.rb', line 227

def parse(data)
  sections = []
  docs, code = [], []
  lines = data.split("\n")
  lines.shift if lines[0] =~ /^\#\!/
  lines.each do |line|
    case line
    when @comment_pattern
      if code.any?
        sections << [docs, code]
        docs, code = [], []
      end
      docs << line
    else
      code << line
    end
  end
  sections << [docs, code] if docs.any? || code.any?
  sections
end

#pygmentize?Boolean

Returns ‘true` if `pygmentize` is available locally, `false` otherwise.

Returns:

  • (Boolean)


121
122
123
124
# File 'lib/rocco.rb', line 121

def pygmentize?
  # Memoize the result, we'll call this a few times
  @_pygmentize ||= ENV['PATH'].split(':').any? { |dir| executable?("#{dir}/pygmentize") }
end

#split(sections) ⇒ Object

Take the list of paired sections two-tuples and split into two separate lists: one holding the comments with leaders removed and one with the code blocks.



251
252
253
254
255
256
257
258
259
260
261
# File 'lib/rocco.rb', line 251

def split(sections)
  docs_blocks, code_blocks = [], []
  sections.each do |docs,code|
    docs_blocks << docs.map { |line| line.sub(@comment_pattern, '') }.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

#to_htmlObject



217
218
219
# File 'lib/rocco.rb', line 217

def to_html
  Rocco::Layout.new(self, @template_file).render
end