Introducing ActsAsMarkup: A Markdown, Textile, Wikitext, and RDoc Plugin for ActiveRecord
Brian Landau, Former Developer
Article Category:
Posted on
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:
- 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. - If you need to modify the object in anyway (e.g. to add a
to_s
orto_html
method), add a file to the “lib/acts_as_markup/exts/” directory. - Add appropriate tests (see current tests).
Instructions for how to add a new Markup Language:
- Add a “
when
” statement to the “case
” statement inacts_as_markup
. The “when
” statement should match with a symbol that represents the language name in some way (e.g. “:markdown
”). - 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 specialrequire_extensions
method to require extensions). - 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
”). - Add a relevant “
when
” statement to theclass_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)
- Add a convenience method (e.g. “
acts_as_markdown
”) - Add an extension file to the “lib/acts_as_markup/exts/” directory if you need to modify the object in anyway.
- Add appropriate tests (see current tests).