Daily Code Reading #31 – Redmine textilizable

This week I will be reading Redmine‘s text formatting code. I’ve worked on Redmine for a few years now but it’s text formatting is still a complex mystery to me. The text formatting code is used whenever a rich text area is used; that lets you enter bold, underline, internal Redmine links, etc.

The Code

To display any of this content, Redmine uses a #textilizable method. It uses Textile by default but formatters for markdown, Ruby Doc, reStructedText, Wiki Creole, and plain text.

module ApplicationHelper
  # Formats text according to system settings.
  # 2 ways to call this method:
  # * with a String: textilizable(text, options)
  # * with an object and one of its attribute: textilizable(issue, :description, options)
  def textilizable(*args)
    options = args.last.is_a?(Hash) ? args.pop : {}
    case args.size
    when 1
      obj = options[:object]
      text = args.shift
    when 2
      obj = args.shift
      attr = args.shift
      text = obj.send(attr).to_s
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
    return '' if text.blank?
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true
 
    text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
 
    parse_non_pre_blocks(text) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
        send method_name, text, project, obj, attr, only_path, options
      end
    end
  end
end

Review

#textilizable runs three steps in order to process the content.

  1. Extracts the options from the args
  2. Uses the configured formatting engine to convert the content
  3. Runs the result through some additional methods to convert the Redmine specific formats

Extracts the options from the args

    options = args.last.is_a?(Hash) ? args.pop : {}
    case args.size
    when 1
      obj = options[:object]
      text = args.shift
    when 2
      obj = args.shift
      attr = args.shift
      text = obj.send(attr).to_s
    else
      raise ArgumentError, 'invalid arguments to textilizable'
    end
    return '' if text.blank?
    project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
    only_path = options.delete(:only_path) == false ? false : true

Since #textilizable can be called several different ways, it needs to check how many arguments were passed in. First it removes the final argument so it can use it as an (optional) options hash. This changes the method parameters like:

  • textilizable(text, options) => textilizable(text)
  • textilizable(issue, :description, options) => textilizable(issue, :description)

Next the case statement is used to set the text content that needs to be converted. This is done directly in the base case or by calling the method on the object (e.g. textilizable(issue, :description) = > textilizable(issue.description)).

Finally, textilizable sets the project and only_path options from the parameters and object. These options will be used when links are created.

Convert the content using the formatting engine

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

This single line of code is doing a lot of work to process Redmine’s text formatting. The Redmine::WikiFormatting#to_html helper is being used to call the configured formatting, which is configured in Setting#text_formatting. It also looks like #to_html takes a block, which textilizable is using to execute any Redmine wiki macros.

Convert the Redmine specific formats

    parse_non_pre_blocks(text) do |text|
      [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
        send method_name, text, project, obj, attr, only_path, options
      end
    end

Finally, #textilizable runs the converted text through a few methods to finish converting Redmine specific markup. Each of these four methods (parse_non_pre_blocks, parse_inline_attachments, parse_wiki_links, parse_redmine_links) are all complex methods so I’ll be reading them in more depth this week.

After the #textilizable method runs, it returns an HTML formatting string which can be embedded directly into a view. Tomorrow I’ll take a look at Redmine::WikiFormatting#to_html to see how Redmine allows switching the formatting engine.