In this weeks code readings, I’ve taken a deep dive tour into how Redmine formats it’s “wiki” text. Today I’m going to wrap it up a final look at the wiki macro execution, #exec_macro
.
Review
Remember, here is the example macro I’m using:
This is a page that will include Design {{include(Design)}}
textilizable
1 |
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) } |
Starting inside of #textilizable
‘s #to_html
block:
-
macro
gets set to ‘include’ - ‘obj’ is the object passed into
#textilizable
, like a wiki page - and args is an array
['Design']
exec_macro
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module Redmine module WikiFormatting module Macros module Definitions def exec_macro(name, obj, args) method_name = "macro_#{name}" send(method_name, obj, args) if respond_to?(method_name) end end end end end |
#exec_macro
is really simple. It creates a method name by prefixing “macro_” to the name (“macro_include”) and then uses send
to call that method with the object and macro arguments. (send('macro_include', some_wiki_page, ['Design'])
).
In order to see how the macro_include
method was defined, we need to look at the rest of the macro API (it’s small).
macro
1 2 3 4 5 6 7 8 |
# Defines a new macro with the given name and block. def macro(name, &block) name = name.to_sym if name.is_a?(String) @@available_macros[name] = @@desc || '' @@desc = nil raise "Can not create a macro without a block!" unless block_given? Definitions.send :define_method, "macro_#{name}".downcase, &block end |
The #macro
method is used to define a new macro in Redmine. It’s given a name, a block, and is added to the @@available_macros
data structure. The key to this method is the :define_method
on the last line:
1 |
Definitions.send :define_method, "macro_#{name}".downcase, &block |
This will dynamically define a method based on the macro name that calls the macro’s block as the method body. Remember, Definitions
is the module above with #exec_macro
.
To wrap out this tour of the macros, lets take a look at the actual include
macro now.
include macro
1 2 3 4 5 6 7 8 9 |
macro :include do |obj, args| page = Wiki.find_page(args.first.to_s, :project => @project) raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) @included_wiki_pages ||= [] raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) @included_wiki_pages < page.attachments) @included_wiki_pages.pop out end |
The first thing this macro does is uses the args
to find the page name (‘Design’). Wiki#find_page
supports prefixing pages with a project to do cross project links.
1 2 3 |
@included_wiki_pages ||= [] raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title) @included_wiki_pages << page.title |
@included_wiki_pages
is used to keep track of all of the pages included. This will make sure that one page doesn’t include another page, which includes the first page, which includes the second page, which includes the first… well, you get the point. It’s circular.
1 2 3 |
out = textilizable(page.content, :text, :attachments => page.attachments) @included_wiki_pages.pop out |
Then the macro runs #textilizable
on the page content and returns the output. This lets the include
macro act recursively, each page that gets included can include other pages until the including stops or a circular inclusion occurs.
That wraps up this week’s code reading on Redmine’s formatting. I covered how content is sent to the formatting system, how Redmine decides which formatter to use, the details of the Redmine Textile formatter, how macros are detected, and finally how a macro is defined and executed. I didn’t get a chance to go through Redmine’s custom syntax but maybe I’ll tackle that with a later series.