comparison lib/redmine/wiki_formatting/macros.rb @ 1115:433d4f72a19b redmine-2.2

Update to Redmine SVN revision 11137 on 2.2-stable branch
author Chris Cannam
date Mon, 07 Jan 2013 12:01:42 +0000
parents cbb26bc654de
children 3e4c3460b6ca
comparison
equal deleted inserted replaced
929:5f33065ddc4b 1115:433d4f72a19b
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 # 3 #
4 # This program is free software; you can redistribute it and/or 4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License 5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2 6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version. 7 # of the License, or (at your option) any later version.
17 17
18 module Redmine 18 module Redmine
19 module WikiFormatting 19 module WikiFormatting
20 module Macros 20 module Macros
21 module Definitions 21 module Definitions
22 def exec_macro(name, obj, args) 22 # Returns true if +name+ is the name of an existing macro
23 def macro_exists?(name)
24 Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym)
25 end
26
27 def exec_macro(name, obj, args, text)
28 macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
29 return unless macro_options
30
23 method_name = "macro_#{name}" 31 method_name = "macro_#{name}"
24 send(method_name, obj, args) if respond_to?(method_name) 32 unless macro_options[:parse_args] == false
33 args = args.split(',').map(&:strip)
34 end
35
36 begin
37 if self.class.instance_method(method_name).arity == 3
38 send(method_name, obj, args, text)
39 elsif text
40 raise "This macro does not accept a block of text"
41 else
42 send(method_name, obj, args)
43 end
44 rescue => e
45 "<div class=\"flash error\">Error executing the <strong>#{h name}</strong> macro (#{h e.to_s})</div>".html_safe
46 end
25 end 47 end
26 48
27 def extract_macro_options(args, *keys) 49 def extract_macro_options(args, *keys)
28 options = {} 50 options = {}
29 while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym) 51 while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym)
30 options[$1.downcase.to_sym] = $2 52 options[$1.downcase.to_sym] = $2
31 args.pop 53 args.pop
32 end 54 end
33 return [args, options] 55 return [args, options]
34 end 56 end
35 end 57 end
36 58
37 @@available_macros = {} 59 @@available_macros = {}
60 mattr_accessor :available_macros
38 61
39 class << self 62 class << self
40 # Called with a block to define additional macros.
41 # Macro blocks accept 2 arguments:
42 # * obj: the object that is rendered
43 # * args: macro arguments
44 #
45 # Plugins can use this method to define new macros: 63 # Plugins can use this method to define new macros:
46 # 64 #
47 # Redmine::WikiFormatting::Macros.register do 65 # Redmine::WikiFormatting::Macros.register do
48 # desc "This is my macro" 66 # desc "This is my macro"
49 # macro :my_macro do |obj, args| 67 # macro :my_macro do |obj, args|
50 # "My macro output" 68 # "My macro output"
51 # end 69 # end
70 #
71 # desc "This is my macro that accepts a block of text"
72 # macro :my_macro do |obj, args, text|
73 # "My macro output"
74 # end
52 # end 75 # end
53 def register(&block) 76 def register(&block)
54 class_eval(&block) if block_given? 77 class_eval(&block) if block_given?
55 end 78 end
56 79
57 private 80 # Defines a new macro with the given name, options and block.
58 # Defines a new macro with the given name and block. 81 #
59 def macro(name, &block) 82 # Options:
83 # * :desc - A description of the macro
84 # * :parse_args => false - Disables arguments parsing (the whole arguments
85 # string is passed to the macro)
86 #
87 # Macro blocks accept 2 or 3 arguments:
88 # * obj: the object that is rendered (eg. an Issue, a WikiContent...)
89 # * args: macro arguments
90 # * text: the block of text given to the macro (should be present only if the
91 # macro accepts a block of text). text is a String or nil if the macro is
92 # invoked without a block of text.
93 #
94 # Examples:
95 # By default, when the macro is invoked, the coma separated list of arguments
96 # is split and passed to the macro block as an array. If no argument is given
97 # the macro will be invoked with an empty array:
98 #
99 # macro :my_macro do |obj, args|
100 # # args is an array
101 # # and this macro do not accept a block of text
102 # end
103 #
104 # You can disable arguments spliting with the :parse_args => false option. In
105 # this case, the full string of arguments is passed to the macro:
106 #
107 # macro :my_macro, :parse_args => false do |obj, args|
108 # # args is a string
109 # end
110 #
111 # Macro can optionally accept a block of text:
112 #
113 # macro :my_macro do |obj, args, text|
114 # # this macro accepts a block of text
115 # end
116 #
117 # Macros are invoked in formatted text using double curly brackets. Arguments
118 # must be enclosed in parenthesis if any. A new line after the macro name or the
119 # arguments starts the block of text that will be passe to the macro (invoking
120 # a macro that do not accept a block of text with some text will fail).
121 # Examples:
122 #
123 # No arguments:
124 # {{my_macro}}
125 #
126 # With arguments:
127 # {{my_macro(arg1, arg2)}}
128 #
129 # With a block of text:
130 # {{my_macro
131 # multiple lines
132 # of text
133 # }}
134 #
135 # With arguments and a block of text
136 # {{my_macro(arg1, arg2)
137 # multiple lines
138 # of text
139 # }}
140 #
141 # If a block of text is given, the closing tag }} must be at the start of a new line.
142 def macro(name, options={}, &block)
143 options.assert_valid_keys(:desc, :parse_args)
144 unless name.to_s.match(/\A\w+\z/)
145 raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
146 end
147 unless block_given?
148 raise "Can not create a macro without a block!"
149 end
60 name = name.to_sym if name.is_a?(String) 150 name = name.to_sym if name.is_a?(String)
61 @@available_macros[name] = @@desc || '' 151 available_macros[name] = {:desc => @@desc || ''}.merge(options)
62 @@desc = nil 152 @@desc = nil
63 raise "Can not create a macro without a block!" unless block_given?
64 Definitions.send :define_method, "macro_#{name}".downcase, &block 153 Definitions.send :define_method, "macro_#{name}".downcase, &block
65 end 154 end
66 155
67 # Sets description for the next macro to be defined 156 # Sets description for the next macro to be defined
68 def desc(txt) 157 def desc(txt)
70 end 159 end
71 end 160 end
72 161
73 # Builtin macros 162 # Builtin macros
74 desc "Sample macro." 163 desc "Sample macro."
75 macro :hello_world do |obj, args| 164 macro :hello_world do |obj, args, text|
76 "Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}") 165 h("Hello world! Object: #{obj.class.name}, " +
166 (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
167 " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
168 )
77 end 169 end
78 170
79 desc "Displays a list of all available macros, including description if available." 171 desc "Displays a list of all available macros, including description if available."
80 macro :macro_list do |obj, args| 172 macro :macro_list do |obj, args|
81 out = '' 173 out = ''.html_safe
82 @@available_macros.keys.collect(&:to_s).sort.each do |macro| 174 @@available_macros.each do |macro, options|
83 out << content_tag('dt', content_tag('code', macro)) 175 out << content_tag('dt', content_tag('code', macro.to_s))
84 out << content_tag('dd', textilizable(@@available_macros[macro.to_sym])) 176 out << content_tag('dd', textilizable(options[:desc]))
85 end 177 end
86 content_tag('dl', out) 178 content_tag('dl', out)
87 end 179 end
88 180
89 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" + 181 desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
90 " !{{child_pages}} -- can be used from a wiki page only\n" + 182 " !{{child_pages}} -- can be used from a wiki page only\n" +
183 " !{{child_pages(depth=2)}} -- display 2 levels nesting only\n"
91 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" + 184 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
92 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo" 185 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
93 macro :child_pages do |obj, args| 186 macro :child_pages do |obj, args|
94 args, options = extract_macro_options(args, :parent) 187 args, options = extract_macro_options(args, :parent, :depth)
188 options[:depth] = options[:depth].to_i if options[:depth].present?
189
95 page = nil 190 page = nil
96 if args.size > 0 191 if args.size > 0
97 page = Wiki.find_page(args.first.to_s, :project => @project) 192 page = Wiki.find_page(args.first.to_s, :project => @project)
98 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version) 193 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
99 page = obj.page 194 page = obj.page
100 else 195 else
101 raise 'With no argument, this macro can be called from wiki pages only.' 196 raise 'With no argument, this macro can be called from wiki pages only.'
102 end 197 end
103 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project) 198 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
104 pages = ([page] + page.descendants).group_by(&:parent_id) 199 pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id)
105 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id) 200 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
106 end 201 end
107 202
108 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}" 203 desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
109 macro :include do |obj, args| 204 macro :include do |obj, args|
114 @included_wiki_pages << page.title 209 @included_wiki_pages << page.title
115 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false) 210 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
116 @included_wiki_pages.pop 211 @included_wiki_pages.pop
117 out 212 out
118 end 213 end
214
215 desc "Inserts of collapsed block of text. Example:\n\n {{collapse(View details...)\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}"
216 macro :collapse do |obj, args, text|
217 html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
218 show_label = args[0] || l(:button_show)
219 hide_label = args[1] || args[0] || l(:button_hide)
220 js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
221 out = ''.html_safe
222 out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
223 out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
224 out << content_tag('div', textilizable(text, :object => obj), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
225 out
226 end
227
228 desc "Displays a clickable thumbnail of an attached image. Examples:\n\n<pre>{{thumbnail(image.png)}}\n{{thumbnail(image.png, size=300, title=Thumbnail)}}</pre>"
229 macro :thumbnail do |obj, args|
230 args, options = extract_macro_options(args, :size, :title)
231 filename = args.first
232 raise 'Filename required' unless filename.present?
233 size = options[:size]
234 raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
235 size = size.to_i
236 size = nil unless size > 0
237 if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
238 title = options[:title] || attachment.title
239 img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename)
240 link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title)
241 else
242 raise "Attachment #{filename} not found"
243 end
244 end
119 end 245 end
120 end 246 end
121 end 247 end