Code Syntax Highlighting in Mephisto with CodeRay 1 comment

Posted by robon July 20, 2006

The other day I switched this blog over from Typo to Mephisto, a lightweight blog/cms system written by Rick Olson. Over all, I’m glad I took the leap, but one thing I missed was a slick syntax highlighting system. I browsed through the source to make sure that there wasn’t some hidden feature supporting it that I had missed. Based on good reviews I had heard, I looked to CodeRay to get code syntax coloring working within Mephisto.

Googling with “rails+coderay” turned up this snippet from over at Rails Weenie. I tried it out by first installing CodeRay as a gem. After noticing some strange behavior I asked for help in #caboose. Rick pointed out that the Subversion version of CodeRay was miles ahead of the gem and that it would likely clear up the problems I was having. I ended up doing something like this to get the latest CodeRay into my Mephisto project:


$ cd ./vendor
$ svn export svn://rubyforge.org//var/svn/coderay/trunk/coderay/trunk/lib/
$ mv lib/* .
$ rmdir lib

(I chose not to set up externals for CodeRay, but you could go that route if you always want their edge version.)

I ended up getting Rick’s CodeRay snippet working in Mephisto as a Liquid filter, and then turned the whole thing into a Plugin called mephisto_code_colorizer. Here’s the meaningful files from the plugin, in pretty colors (paths relative to your Rails project root):

First, the init.rb, to load everything up:

./init.rb:
require 'coderay'
require 'snippet_parser'
require 'colorizer'
Liquid::Template.register_filter(Mephisto::Liquid::Colorizer)

Then, Rick’s SnippetParser class definition:

./bin/snippet_parser.rb:
<samp>

class SnippetParser < String
  class << self
    # SnippetParser.parse text do |tag, code, i|
    #   # return processed code
    # end   
    def parse(text, &block) 
      build_snippets text, &block
    end

    private 
      def method_missing(method, *args, &block) 
        new(args.first).send(method, *args[1..-1], &block) 
      end     
  end

  # returns snippets in an array
  def snippets
    build_snippets if @snippets.nil?
    @snippets
  end

  # wraps snippets in &lt;pre>&lt;code>
  def pre_format
    build_snippets do |tag, code, i|
      %(&lt;pre>&lt;code>#{code}&lt;/code>&lt;/pre>)
    end
  end

  protected
    def build_snippets(&block)
      @snippets = []
      contents  = []
      tag       = nil
      returning [] do |output|
        tokenizer = HTML::Tokenizer.new(self.strip)

        while token = tokenizer.next
          node = HTML::Node.parse(nil, 0, 0, token, false)
          if node.tag? && node.name == 'samp'
            if contents.blank? # open tag
              tag = node.dup
            else # closing tag
              output << close_snippet(tag, contents.join, &block)
              tag = nil
            end
            contents.clear
          else # inside a code tag
            (tag.nil? ? output : contents) << node.to_s
          end
        end

        # get any unfinished code blocks
        output << close_snippet(nil, contents.join, &block) unless contents.empty?
      end.join
    end

    def close_snippet(tag, contents, &block)
      @snippets << contents
      block ? block.call(tag, contents, @snippets.length) : %(&lt;samp>#{contents}&lt;/samp>)
    end
end
</samp>

Finally, I created a Liquid filter named “syntax” and baked it in with the other Liquid filters, with:

./bin/colorizer.rb:
<samp>

module Mephisto
  module Liquid
    module Colorizer
      include ActionView::Helpers::TagHelper
      require 'cgi'

      def syntax(html)
        # tag is the HTML::Node instance
        # code is the text inside the code tags
        # i is the counter for this current snippet.
        SnippetParser.parse CGI.unescapeHTML(html) do |tag, code, i|
          code.gsub! /^\s+\n/, '' # gets rid of an extra linebreak at the top.
          %(<div class='samp'>) + 
            CodeRay.scan(code,
            (tag.attributes['lang'].to_sym rescue nil) || 
            :ruby).div(:line_numbers => :list, :css => :style) +
          "</div>" 
        end
      end     

    end
  end
end
</samp>

The filter colorizes code marked up in samp tags. To filter the body of an article, use something like the following in your liquid templates:

./themes/site-1/templates/home.liquid:

...
{{ article.body | syntax }}
...

Wait! Almost forgot the css styles. Of course you’ll need them to have full control over all those nice colors. Check out my css file from this site for the colors I’m using.

I tossed this solution together pretty quickly because I have a lot of other things that I should be doing (like writing a book), but I would love to hear some feedback about how I might make this better. Please leave suggestions in the comments.

Thank you,
Rob


Comments

Leave a response

  1. rickAugust 08, 2006 @ 06:47 PM

    Great job! I’d like to package this up with Kevin’s coderay macro into a mephisto plugin.