Introducing ActsAsMarkup: A Markdown, Textile, Wikitext, and RDoc Plugin for ActiveRecord

On a project I’ve been working on recently, I wanted to be able to enter in Markdown text to a field and convert it into HTML in a view. Having read about BlueCloth’s performance problems though, I didn’t want to use the built in markdown helper in ActionView.

So I decided to give RDiscount a try, but I wanted to have the value for the column stored as a RDiscount Markdown object in the model object. So I started by creating an acts_as_markdown class method that took a list of columns to be represented as markdown objects. Quickly, my fellow Vigeteers and I realized it would be useful for a bunch of other markup languages as well, along with other Markdown libraries.

Acts As Markup Internals

Let’s work with a simple example. Let’s say we have a Post model with a body column we want to put Markdown text into:

 class Post < ActiveRecord::Base acts_as_markdown :body end 

When a Post record is accessed, an instance variable holding the Markdown object will be created. If you use the . method syntax, you will get this markdown object; if you use the bracket [] syntax, you will get the original string returned from the database. The reason we store it in an instance variable instead of creating a Markdown object each time is to prevent the text from needing to be parsed each time and a new object being created, which can slow down the application.

All of the objects used by ActsAsMarkup either come with or have been modified to have to_s and to_html methods. The to_s method will return the original markdown string. This is so no wonky wizardry has to be done to get the field to work with ActionView form field helpers. The to_html method returns the processed HTML ready to be used in the view.

 @post["body"] # => "## Headline Text" @post.body # => #<RDiscount:…> @post.body.to_s # => "## Headline Text" @post.body.to_html # => "<h2> Headline Text</h2>" 

If you change the value of the Markdown text, the Markdown object will be recreated with the new text:

 @post.body.to_s # => "## Headline Text" @post.body = "### Another Headline" @post.body.to_s # => "### Another Headline" 

Options and Variations

By default, ActsAsMarkup will use the RDiscount library. It’s easy enough to change that -- just add a line in your environment.rb file:

 ActsAsMarkup.markdown_library = :bluecloth 

Currently, ActsAsMarkup supports BlueCloth, RDiscount, Ruby PEG, and Maruku Markdown libraries.

Of course, not everyone wants to use Markdown, so I’ve also built in support for Textile, RDoc, and Wikitext. These are available through convienence methods like acts_as_textile or by using the main acts_as_markup:

 acts_as_markup :language => :textile, :columns => :body 

However; what if you want to give your users a choice of different languages instead of locking them in? Well, that’s easy, too; there’s the variable language option. Instead of providing :markdown or :textile or some other language, you supply :variable to the language option. ActsAsMarkup will then use another column to determine which parser to use on the text column. By default, a column name of “markup_language” will be used, but you can change this by passing the column name via the :language_column option to acts_as_markup.

 acts_as_markup :language => :variable, :columns => :body, :language_column => :language_name 

When using the variable language option, the language column will accept case-insensitive names for the value (i.e. “Markdown” or “markdown”). Additionally, any value for the language column besides “markdown”, “textile”, “rdoc”, or “wikitext” will pass through as an ordinary string. This way, you can allow any value in this column you want, “XHTML”, “Text”, “Plain Text” or anything else, and process it (or not) elsewhere.

Give it to me!

ActsAsMarkup is being released as a gem and can be installed in the usual way:

sudo gem install acts_as_markup 

Since it’s meant to be used in a Rails app (although it can be used anywhere with ActiveRecord), you’ll probably want to put this line in your environment.rb file:

config.gem "acts_as_markup" 

The code can be found on GitHub, and the documentation can be found on RubyForge.

Update:

If you want to contribute something to ActsAsMarkup a good place is to add support for another Markdown Library or some other markup language.

Instructions for how to add a new Markdown Library:

  1. Add another item to the ActsAsMarkup::MARKDOWN_LIBS hash in the form of:
    :bluecloth => {:class_name => "BlueCloth", :lib_name => "bluecloth"}
    :lib_name should be the name needed to require the library, while :class_name should be the class that we are making an instance of.
  2. If you need to modify the object in anyway (e.g. to add a to_s or to_html method), add a file to the “lib/acts_as_markup/exts/” directory.
  3. Add appropriate tests (see current tests).

Instructions for how to add a new Markup Language:

  1. Add a “when” statement to the “case” statement in acts_as_markup. The “when” statement should match with a symbol that represents the language name in some way (e.g. “:markdown”).
  2. In the “when” block you need to set the “klass” local variable and require the library and the extension file if you need one (use the special require_extensions method to require extensions).
  3. Add the same lines you added to the previous “when” statement to the “:variable” “when” statement. But replace “klass” with “language_klass” (e.g. “markdown_klass”).
  4. Add a relevant “when” statement to the class_eval block for the “:variable” language option. This should look something like:
    when /markdown/i @#{col.to_s} = #{markdown_klass}.new(self['#{col.to_s}'].to_s)
  5. Add a convenience method (e.g. “acts_as_markdown”)
  6. Add an extension file to the “lib/acts_as_markup/exts/” directory if you need to modify the object in anyway.
  7. Add appropriate tests (see current tests).

Brian is a developer in our Boulder, CO, office. He loves making code readable and maintainable for clients such as Time Life and Shure.

More posts by Brian