Daily Code Reading #32 – Redmine WikiFormatting

Yesterday’s post showed how ApplicationHelper#textilizable used Redmine::WikiFormatting#to_html to convert the text content into HTML.

1
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }

Today I’m going to look into the #to_html method to see how the conversion is run.

The Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module Redmine
  module WikiFormatting
    class < 2.kilobyte && cache_store && cache_key = cache_key_for(format, options[:object], options[:attribute])
          # Text retrieved from the cache store may be frozen
          # We need to dup it so we can do in-place substitutions with gsub!
          cache_store.fetch cache_key do
            formatter_for(format).new(text).to_html
          end.dup
        else
          formatter_for(format).new(text).to_html
        end
        if block_given?
          execute_macros(text, block)
        end
        text
      end
    end
  end
end

Review

The first thing that #to_html does is to check if the formatted text has been cached in ActiveSupport::Cache. Then it uses #formatter_for which will pick the configured formatter to render the content. Finally, the Redmine macros are run with #execute_macros.

Caching

1
2
3
4
5
6
text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, options[:object], options[:attribute])
  # Text retrieved from the cache store may be frozen
  # We need to dup it so we can do in-place substitutions with gsub!
  cache_store.fetch cache_key do
    formatter_for(format).new(text).to_html
  end.dup

Caching the text formatting was a recent addition to Redmine, so it’s still very strict about when content is cached.

  • The Cache Formatted Text setting in the Administration panel needs to be enabled, and
  • The text size needs to be bigger than 2K, and
  • The cache check should miss (i.e. no preview content cached)

When all of these cases match, Redmine will run the block passed to cache_store.fetch and store the result into the cache for later use. Then Redmine uses the #formatter_for method to render the content.

Content rendering

1
2
3
4
def formatter_for(name)
  entry = @@formatters[name.to_s]
  (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
end

Redmine::WikiFormatting#to_html calls formatter_for(format).new(text).to_html both when caching the content and when no caching is enabled. formatter_for is just using the lookup table of valid formatters and returning their class to the caller. What’s nice about this process is that Redmine is able to return the NullFormatter if nothing is found, which gives a good plain text fallback. If I assume that the NullFormatter is used, then the call for formatter_for would be converted to this:

1
Redmine::WikiFormatting::NullFormatter::Formatter.new(text).to_html

NullFormatter#to_html

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
# Default formatter module
module NullFormatter
  class Formatter
    include ActionView::Helpers::TagHelper
    include ActionView::Helpers::TextHelper
    include ActionView::Helpers::UrlHelper
 
    def initialize(text)
      @text = text
    end
 
    def to_html(*args)
      simple_format(auto_link(CGI::escapeHTML(@text)))
    end
  end
 
  module Helper
    def wikitoolbar_for(field_id)
    end
 
    def heads_for_wiki_formatter
    end
 
    def initial_page_content(page)
      page.pretty_title.to_s
    end
  end
end

To get a better idea of what a Formatter does, here is the entire NullFormatter::Formatter class. It’s easy to see that there isn’t very much going on here. First a new object is initialized with the text that needs to be rendered. Then #to_html uses simple_format and auto_link to create a basic HTML section.

Macro execution

1
2
3
if block_given?
  execute_macros(text, block)
end

Finally Redmine::WikiFormatting#to_html runs #execute_macro when there is a block argument. I’ll save reviewing #execute_macros until later, it’s a complex method that uses a large regular expression to match the macros in the text.

Now that I have a better understanding of how Redmine’s Wiki Formatting works, I’m going to take a deeper look at how the textile formatter is structured. Since it’s using _why’s RedCloth3 I’m expecting this piece to be pretty complex.