Daily Code Reading #35 – Redmine exec_macro

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.