Today I’m digging back into the WikiFormatting to read through #execute_macros
.
The Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
module Redmine module WikiFormatting class < e "<div class="flash error">Error executing the <strong>#{macro}</strong> macro (#{e})</div>" end || all else all end end end end end end |
Review
It will be best to use an example macro in order to trace the flow of this method. Redmine has a macro called include
which can be used to include another wiki page into the current one. Lets use the following text as our page content:
This is a page that will include Design {{include(Design)}}
Tracing the flow of this macro, the call stack looks like:
- ApplicationHelper#textilizable calls Redmine::WikiFormatting.to_html with a block
{ |macro, args| exec_macro(macro, obj, args) }
- Redmine::WikiFormatting.to_html uses the configured formatters to format the plain text
- Since Redmine::WikiFormatting.to_html was passed a block, it runs
execute_macros(text, block)
which passes the block from#textilizable
Now the program ends up in the method above with the converted text and the block from #textilizable
. At this point it runs the MACROS_RE
regular expression through gsub!
so it can replace all of the macros with their content.
MACROS_RE regular expression
1 2 3 4 5 6 7 8 9 |
MACROS_RE = / (!)? # escaping ( \{\{ # opening tag ([\w]+) # macro name (\(([^\}]*)\))? # optional arguments \}\} # closing tag ) /x unless const_defined?(:MACROS_RE) |
The first thing to notice is that the expression uses the /x
modifier. This tells it to ignore whitespace between each regular expression token and lets the expression be written on multiple lines with inline comments. Without it, the expression would be much harder to read:
1 |
/(!)(\{\{([\w]+)(\(([^\}]*)\))?\}\})/ |
I think the comments explain what parts match but here are the different match sets:
- the “!” prefix to escape
- the entire macro after the escape
- the macro name, without the
{}
- all of the arguments enclosed with
()
- all of the arguments
gsub
Having done a lot of regular expression work on my book, I’ve become very familiar with how regular expressions are used with #gsub!
and a block. First #gsub!
checks if the text matches and for every match it yields to the block, setting up the standard $1
, $2
, $n
variables from the regular expression result. Whatever is returned from #gsub!
is used as the replacement.
For example:
1 2 3 4 5 |
"Little Stream Software writes Rails code".gsub!(/(Rails)/) do "Redmine" end # returns => "Little Stream Software writes Redmine Code" |
So when the MACROS_RE
matches, the regular expression results are setup and passed into the block.
Macro Arguments
1 |
args = ($5 || '').split(',').each(&:strip) |
MACROS_RE
matches the arguments and passes them down as a comma separated string; ‘Design’ from the example. ||
is used in case the macro wasn’t passed any arguments. Then split
is used to convert the string to an array of arguments, separated by the commas. Finally, strip
is called on them to handle any extra whitespace.
Escaped macro
1 2 3 4 5 |
if esc.nil? # ... else all end |
It took me a minute to understand what esc
was doing but then I remembered that it’s only set when the macro is escaped so it can be printed to the page. So when the macro is escaped, the full macro content is rendered using the second match ($2
or all
).
Calling the macro
1 2 3 4 5 |
begin macros_runner.call(macro, args) rescue => e "<div class="flash error">Error executing the <strong>#{macro}</strong> macro (#{e})</div>" end || all |
At this point we have a macro (include
) and some args (['Design']
). So the block passed all the way down from textilizable
is run and the output is passed back up to #to_html
.
There is still one more level of code that I need to dig though before we get to the actual running macro. This is in the exec_macro
method that textilizable
uses.