Manage your markup with has_markup

I recently decided to extract some plugins, in an effort to clean up the codebase for my blog.

The first thing I tackled was generation of HTML from my Post model.

In the beginning...

Here's the starting point for my model:

class Post < ActiveRecord::Base
  # --- SNIP ---

  # TODO move into own file... and plugin?
  def self.validates_markdown(*attrs)
    validates_each(*attrs) do |record, attr, value|
      begin
        BlueCloth.new(value).to_html unless value.nil?
      rescue BlueCloth::FormatError => e
        errors.add attr, "has #{e}"
      end
    end
  end
  validates_presence_of :content
  validates_markdown :content
  before_save :cache_content

  # Use BlueCloth to generate HTML ahead of time.
  def cache_content
    html = BlueCloth.new(self.content).to_html
    self.cached_content = html
  end

  # --- SNIP ---
end
  • validates_markdown really doesn't belong here
  • Nothing particularly interesting is happening, but the lines of code start to add up

Let's look at the partial for Post.

%div{ :id => dom_id(post), :class => dom_class(post) }
  %h2.title= link_to(post.title, post)
  .content~ post.cached_content
  • Eh, nothing particuarly interesting

Goals

  • One-liner for specifying markup, similar to has_many or acts_as_taggable
  • Optionally require the markup
  • Optionally cache the markup
  • Allow for different syntaxes

A whole new look on things

I was able to extract the logic out of my model and hit all of these goals.

The refactored model:

class Post < ActiveRecord::Base
  # --- SNIP ---

  has_markup :content, :required => true, :cache_html => true

  # --- SNIP ---
end
And the view is more or less the same:
%div{ :id => dom_id(post), :class => dom_class(post) }
  %h2.title= link_to(post.title, post)
  .content~ post.cached_content_html

Thoughts

  • Plugins are really easy to make
  • Plugins can pull a good amount of code out of your model, which results in it being more readable
  • I think I finally understand the difference between include and extend

Get it

If you're interested in trying this plugin, it is hosted on GitHub. You can install it by with:

script/plugin install git://github.com/technicalpickles/has_markup.git

Simple caching of markdown markdown in your model

As I've written about it before, I'm using markdown for this blog. Originally, I would do the translation from markdown to html every time a page is rendered. Seems pretty inefficient, right? Yeah, not so much.

As it turns out, it is really simple to cache this information.

For a little background, here's the schema for my posts:

create_table "posts", :force => true do |t|
  t.string   "title"
  t.text     "content"
end

And the model:

class Post < ActiveRecord::Base
  validates_presence_of :title
  validates_presence_of :content
end

What I want to do is, before a post is saved, to generate html from the markdown and save it to the database. Then, instead of generating it at page render, use the saved html.

ActiveRecord provides several callbacks into its lifecycle. There are a few ways you can add a callback as the RDoc demonstrates. The lifecycle we're really interested is before_save. This is for whenever the post is saved, regardless of it is creating a new one, or updating an existing one.

Before we implement the callback, let's update our schema with a migration.

$ script/generate migration AddCachedContent
class AddCachedContent < ActiveRecord::Migration
  def self.up
    add_column :posts, :cached_content, :text
  end

  def self.down
    remove_column :posts, :cached_content
  end
end

Hmm... one problem: we probably want to cache the content of all existing posts. So, let's add a method that will cache the content.


class Post < ActiveRecord::Base
  validates_presence_of :title
  validates_presence_of :content

  def cache_content
    self.cached_content = BlueCloth.new(self.content).to_html
  end
end

Now we have the means to cache our markdown, we can update our migration to cache the content of each post and save it:

class AddCachedContent < ActiveRecord::Migration
  def self.up
    add_column :posts, :cached_content, :text

    Post.find(:all).each do |post|
      post.cache_content
      post.save!
    end
  end

  def self.down
    remove_column :posts, :cached_content
  end
end

We have everything in place to go ahead and actually add the callback:

class Post < ActiveRecord::Base
  validates_presence_of :title
  validates_presence_of :content

  before_save :cache_content
  def cache_content
    self.cached_content = BlueCloth.new(self.content).to_html
  end
end

We're just about done. All that remains is to update places in the view that generate html from markdown to just use the cached_content instead.

Using Markdown in vim

My posts are written in Markdown, by using the wonderful BlueCloth library, as I discussed in this earlier post.

Considering this is my first Rails project, it definitely lacks a certain... sophistication. For example, posts are either published and live, or don't exist yet. And also, I don't have any autosave mechanism.

These two areas of want combined to make me a very sad panda one day. After writing up the post of the day, I may have restarted my computer, forgetting that I had a lovely work of writing in progress. Whoops. Got a little upset by that.

Until I have time to implement those two features, I found a suitable stop-gap: vim! So I've taken to simply writing stuff up in vim, and when it's ready, copy, paste, and publish. Easy peasy.

But where would vim be without some syntax highlighting? NOWHERE! Well, it's still pretty awesome regardless, but I don't think many would disagree that syntax highlighting is a bad thing.

While there isn't support out of box, some clever fellow wrote something up to do the trick. While I'm sure you could easily browse there to see how to do this, I'll reproduce it here as well.

Download this, and place it in ~/.vim/syntax/ (of course, creating it if it doesn't exist).

Now create, or add to ~/.vim/filetype.vim:

" markdown filetype file
if exists("did_load_filetypes")
 finish
endif
augroup markdown
 au! BufRead,BufNewFile *.mkd   setfiletype mkd
augroup END

And then to ~/.vimrc and/or ~/.gvimrc

 augroup mkd
  autocmd BufRead *.mkd  set ai formatoptions=tcroqn2 comments=n:>
 augroup END

I kind of feel like a plugin, or something, could take care of this for you, but I don't know enough vim voodoo to sure.

Now you have vim markdown goodness... Enjoy!