After reading the NullFormatter
yesterday, I wanted to take a short look at how Redmine’s Textile formatter works. I’m going to try to avoid much of the parsing code as possible, it’s very complex and could use an entire week of code reading itself.
The Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
require 'redcloth3' module Redmine module WikiFormatting module Textile class Formatter < RedCloth3 include ActionView::Helpers::TagHelper # auto_link rule after textile rules so that it doesn't break !image_url! tags RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc] def initialize(*args) super self.hard_breaks=true self.no_span_caps=true self.filter_styles=true end def to_html(*rules) @toc = [] super(*RULES).to_s end private # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a> def hard_break( text ) # ... snip ... end # Patch to add code highlighting support to RedCloth def smooth_offtags( text ) # ... snip ... end # Patch to add 'table of content' support to RedCloth def textile_p_withtoc(tag, atts, cite, content) # ... snip ... end alias :textile_h1 :textile_p_withtoc alias :textile_h2 :textile_p_withtoc alias :textile_h3 :textile_p_withtoc def inline_toc(text) # ... snip ... end # Turns all urls into clickable links (code from Rails). def inline_auto_link(text) # ... snip ... end # Turns all email addresses into clickable links (code from Rails). def inline_auto_mailto(text) # ... snip ... end end end end end |
Review
The first thing to notice is that Textile::Formatter
is a subclass of RedCloth3
. This will let the formatting reuse many of the methods from RedCloth directly. Since the formatter is used by calling #new
and then #to_html
, I’ll look at those methods first.
initialize
1 2 3 4 5 6 |
def initialize(*args) super self.hard_breaks=true self.no_span_caps=true self.filter_styles=true end |
RedCloth’s #initialize
takes two arguments: string
and restrictions
. Redmine only uses the first one, passing in the text content that needs to be formatted. After RedCloth has initialized the object, Formatter
sets three attributes to control the rendering:
- hard_breaks – converts single newlines to HTML break tags.
- no_span_caps – turns off the behavior where capitalized words automatically have span tags placed around them.
- filter_styles – turns off textile styles. I know this is off because there have been many security bugs found when styles were enabled
Now with a fresh object and the text content, #to_html
is used to… well, convert the text into HTML.
to_html
1 2 3 4 |
def to_html(*rules) @toc = [] super(*RULES).to_s end |
Redmine’s textile supports creating a table of contents from a wiki “macro” (1). The @toc
variable will be used later in the #inline_toc
method to render the table of contents.
Other than that, Formatter
just calls super with the formatting rules to apply:
- textile – provided by RedCloth, this runs the actual Textile conversion.
- block_markdown_rule – provided by RedCloth, this is a single rule from RedCloth’s markdown support. It converts Markdown’s horizontal rules into an
<hr>
element. - inline_auto_link – provided by Redmine, this converts all urls into clickable links.
- inline_auto_mailto – provided by Redmine, this converts all mailto urls into clickable links.
- inline_toc – provided by Redmine, this creates the automatic table of contents from the HTML header tags (
h1
,h2
,h3
).
Finally after all of the formatting is complete, Formatter
converts the content to a string and returns if back up the stack.
So these two methods are all that’s needed to hook up Redmine to RedCloth. I did some work on the Redmine markdown plugin and it also had to implement these methods to connect to a markdown library.
(1) Technically toc
isn’t a macro though there will be a big push to port it to Redmine’s macro system.