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.
- Extracts the options from the args
- Uses the configured formatting engine to convert the content
- 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.