home, index, New :: syntax-highlighting-demoEdit | Delete

Syntax Highlighting

Working through some of the outstanding issues for vanilla-rb, I've implemented some simple syntax highlighting (Ticket 16):

class Ruby
  def coloured(with='syntax')
    if with =~ /joy/
      puts "Hello, You Coloured World You"
    end
  end
end

It's pretty simple behind the scenes, although there are a few conceptual choices which I may revisit. Here's the scoop.

Blocks of content

I recently fixed code syntax highlighting on the rails-engines site, which is built using the reasonably-excellent radiant-cms system. In Radiant, you markup blocks of content, typically like this:

<r:code lang="ruby">
class Blah
  def something
  end
end
</r:code>

Radiant lets you define tags that wrap around content within the page, making this pretty simple.

However, vanilla doesn't work like that, or at least it doesn't at the moment. The building block of a page is the snip, not a chunk of text wrapped in a tag. There's no tag processing going on here at all, beyond the single, magically snip inclusion that makes it all work. This presents a problem when we want to treat a certain piece of text differently to the rest of the body of a snip.

The Vanilla Way

Since the building block is the snip, the natural thing to do is to move the code snippet into its own snip, and include that via a code dynasnip (see below for the self-syntax-highlighted source!). And so this call

{code ruby,test-code-highlighting}

works nicely for us. After the call to the code dynasnip, the first parameter is the language, and the second is the snip name to include:

class Included
  def from_another_snip
    return "syntax highlighted!"
  end
end

... but it's a pain to have to move every code sample out into its own snip (although that's certainly useful for larger chunks of code.

The solution to this, is to allow rendering of individual parts of snips via syntax highlighting. By adding the snip part to the parameter list

{code ruby,syntax-highlighting-demo,rubycodesample}

we get

class Test
  def initialize(name=nil)
    puts "Hello, World"
  end
end

The code dynasnip is rendering the rubycodesample part of this very blog post!

Next steps

While this certainly works, it's a pain to have to reference the current snip in order to get to the snip part. I have to do this with the comments dynasnip too, in order to find all the snips that are related to it. We could solve this if:

  1. Each renderer knew which snip it was rendering - currently true :)
  2. Each renderer knew which renderer called it - not currently possible :(

Ideally, the code dynasnip would be able to ask it's renderer (the Ruby renderer) to ask the renderer that is invoking it (in the case of this post, the Markdown renderer) which snip is doing the including. With me?

Code dyna 
    --> rendered by Ruby Renderer 
        --> invoked by Markdown Render while rendering this blog post

Yeah, it's a bit complicated, but it's probably worth it; it could help avoid circular rendering problems at the same time.

Anyway - syntax highlighting. Woot!

Super Bonus Appendix

Here's the code from the code dynasnip, highlighting itself, like an eternal self-consuming snake. Don't say I didn't warn you!

require 'syntax/convertors/html'
class CodeHighlighter < Dynasnip
  def handle(language, snip_to_render, part_to_render='content')
    snip = Vanilla.snip(snip_to_render)
    text = snip.__send__(part_to_render.to_sym)
    convertor = Syntax::Convertors::HTML.for_syntax(language)
    code = convertor.convert(text, false)
    %(<pre class="code ) + language + %("><code>) + code + %(</code></pre>)
  end

  self
end

Comments

  1. peter (2008-07-18 19:45:41)

    I assume you're James Adam, the main guy behind Rails Engines.

    If not, sorry.

    But if so, I've been developing on Rails for about a year and half. What Engines seeks to do seems like a painfully obvious need for anybody who has developed > 1 websites. That Rails 2.0 didn't even touch the issue behind Engines strikes me as hubris.

    But the thing that troubles me about Engines is that there doesn't seem to be a site providing examples of plugins developed on Engines (a la your old discarded login_engine). That would suggest that Engines has failed as a broad based solution, and has lived on only in hidden dev-shop caves and obscure dev group mail lists. Bummer if so.

    But it just seems that if Engines was such a bad idea, I could Google for a persuasive argument against it. But I can't. Or, alternatively if Engines was as useful as it seems, I could Google for a thriving open community of Engines developers. But I can't.

    There seems to be a strange anomaly in the universe here. My guess: banal human failings. (Sorry for digressing on your quiet blog.)

  2. Anonymous (2008-09-19 03:17:28)

    how about making snips get a body like radiant example?
    [Error rendering 'code' - "wrong number of arguments (1 for 2)"]
    ./lib/vanilla/renderers/ruby.rb:31:in `handle'
    ./lib/vanilla/renderers/ruby.rb:31:in `process_text'
    ./lib/vanilla/renderers/base.rb:62:in `render_without_including_snips'
    ./lib/vanilla/renderers/base.rb:51:in `render'
    ./lib/vanilla/app.rb:46:in `render'
    ./lib/vanilla/app.rb:68:in `rendering'
    ./lib/vanilla/app.rb:45:in `render'
    ./lib/vanilla/renderers/base.rb:41:in `include_snips'
    ./lib/vanilla/renderers/base.rb:31:in `gsub'
    ./lib/vanilla/renderers/base.rb:31:in `include_snips'
    ./lib/vanilla/renderers/base.rb:52:in `render'
    ./lib/vanilla/app.rb:46:in `render'
    ./lib/vanilla/app.rb:68:in `rendering'
    ./lib/vanilla/app.rb:45:in `render'
    ./lib/vanilla/dynasnips/comments.rb:54:in `render_comments'
    ./lib/vanilla/dynasnips/comments.rb:53:in `map'
    ./lib/vanilla/dynasnips/comments.rb:53:in `render_comments'
    ./lib/vanilla/dynasnips/comments.rb:21:in `get'
    ./lib/vanilla/renderers/ruby.rb:29:in `send'
    ./lib/vanilla/renderers/ruby.rb:29:in `process_text'
    ./lib/vanilla/renderers/base.rb:62:in `render_without_including_snips'
    ./lib/vanilla/renderers/base.rb:51:in `render'
    ./lib/vanilla/app.rb:46:in `render'
    ./lib/vanilla/app.rb:68:in `rendering'
    ./lib/vanilla/app.rb:45:in `render'
    ./lib/vanilla/renderers/base.rb:41:in `include_snips'
    ./lib/vanilla/renderers/base.rb:31:in `gsub'
    ./lib/vanilla/renderers/base.rb:31:in `include_snips'
    ./lib/vanilla/renderers/base.rb:52:in `render'
    ./lib/vanilla/app.rb:46:in `render'
    ./lib/vanilla/app.rb:68:in `rendering'
    ./lib/vanilla/app.rb:45:in `render'
    ./lib/vanilla/dynasnips/current_snip.rb:25:in `handle'
    ./lib/vanilla/renderers/ruby.rb:31:in `process_text'
    ./lib/vanilla/renderers/base.rb:62:in `render_without_including_snips'
    ./lib/vanilla/renderers/base.rb:51:in `render'
    ./lib/vanilla/app.rb:46:in `render'
    ./lib/vanilla/app.rb:68:in `rendering'
    ./lib/vanilla/app.rb:45:in `render'
    ./lib/vanilla/renderers/base.rb:41:in `include_snips'
    ./lib/vanilla/renderers/base.rb:31:in `gsub'
    ./lib/vanilla/renderers/base.rb:31:in `include_snips'
    ./lib/vanilla/renderers/base.rb:52:in `render'
    ./lib/vanilla/app.rb:26:in `present'
    ./lib/vanilla/rack_app.rb:24:in `call'
    /usr/lib/ruby/gems/1.8/gems/rack-0.4.0/lib/rack/static.rb:33:in `call'
    /usr/lib/ruby/gems/1.8/gems/rack-0.4.0/lib/rack/session/cookie.rb:30:in `call'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/connection.rb:59:in `pre_process'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/connection.rb:50:in `process'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/connection.rb:35:in `receive_data'
    /usr/lib/ruby/gems/1.8/gems/eventmachine-0.12.0/lib/eventmachine.rb:224:in `run_machine'
    /usr/lib/ruby/gems/1.8/gems/eventmachine-0.12.0/lib/eventmachine.rb:224:in `run'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/backends/base.rb:45:in `start'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/server.rb:146:in `start'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/controllers/controller.rb:79:in `start'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/runner.rb:166:in `send'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/runner.rb:166:in `run_command'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/lib/thin/runner.rb:136:in `run!'
    /usr/lib/ruby/gems/1.8/gems/thin-0.8.1/bin/thin:6
    /usr/bin/thin:19:in `load'
    /usr/bin/thin:19
    class Test def initialize(name=nil) puts "Hello, World" end end {/code}