##// END OF EJS Templates
Merged r16227 (#24869)....
Jean-Philippe Lang -
r15909:c717698803d7
parent child
Show More
@@ -1,256 +1,256
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 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.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
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 # Returns true if +name+ is the name of an existing macro
22 # Returns true if +name+ is the name of an existing macro
23 def macro_exists?(name)
23 def macro_exists?(name)
24 Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym)
24 Redmine::WikiFormatting::Macros.available_macros.key?(name.to_sym)
25 end
25 end
26
26
27 def exec_macro(name, obj, args, text)
27 def exec_macro(name, obj, args, text)
28 macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
28 macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
29 return unless macro_options
29 return unless macro_options
30
30
31 method_name = "macro_#{name}"
31 method_name = "macro_#{name}"
32 unless macro_options[:parse_args] == false
32 unless macro_options[:parse_args] == false
33 args = args.split(',').map(&:strip)
33 args = args.split(',').map(&:strip)
34 end
34 end
35
35
36 begin
36 begin
37 if self.class.instance_method(method_name).arity == 3
37 if self.class.instance_method(method_name).arity == 3
38 send(method_name, obj, args, text)
38 send(method_name, obj, args, text)
39 elsif text
39 elsif text
40 raise "This macro does not accept a block of text"
40 raise "This macro does not accept a block of text"
41 else
41 else
42 send(method_name, obj, args)
42 send(method_name, obj, args)
43 end
43 end
44 rescue => e
44 rescue => e
45 "<div class=\"flash error\">Error executing the <strong>#{h name}</strong> macro (#{h e.to_s})</div>".html_safe
45 "<div class=\"flash error\">Error executing the <strong>#{h name}</strong> macro (#{h e.to_s})</div>".html_safe
46 end
46 end
47 end
47 end
48
48
49 def extract_macro_options(args, *keys)
49 def extract_macro_options(args, *keys)
50 options = {}
50 options = {}
51 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)
52 options[$1.downcase.to_sym] = $2
52 options[$1.downcase.to_sym] = $2
53 args.pop
53 args.pop
54 end
54 end
55 return [args, options]
55 return [args, options]
56 end
56 end
57 end
57 end
58
58
59 @@available_macros = {}
59 @@available_macros = {}
60 mattr_accessor :available_macros
60 mattr_accessor :available_macros
61
61
62 class << self
62 class << self
63 # Plugins can use this method to define new macros:
63 # Plugins can use this method to define new macros:
64 #
64 #
65 # Redmine::WikiFormatting::Macros.register do
65 # Redmine::WikiFormatting::Macros.register do
66 # desc "This is my macro"
66 # desc "This is my macro"
67 # macro :my_macro do |obj, args|
67 # macro :my_macro do |obj, args|
68 # "My macro output"
68 # "My macro output"
69 # end
69 # end
70 #
70 #
71 # desc "This is my macro that accepts a block of text"
71 # desc "This is my macro that accepts a block of text"
72 # macro :my_macro do |obj, args, text|
72 # macro :my_macro do |obj, args, text|
73 # "My macro output"
73 # "My macro output"
74 # end
74 # end
75 # end
75 # end
76 def register(&block)
76 def register(&block)
77 class_eval(&block) if block_given?
77 class_eval(&block) if block_given?
78 end
78 end
79
79
80 # Defines a new macro with the given name, options and block.
80 # Defines a new macro with the given name, options and block.
81 #
81 #
82 # Options:
82 # Options:
83 # * :desc - A description of the macro
83 # * :desc - A description of the macro
84 # * :parse_args => false - Disables arguments parsing (the whole arguments
84 # * :parse_args => false - Disables arguments parsing (the whole arguments
85 # string is passed to the macro)
85 # string is passed to the macro)
86 #
86 #
87 # Macro blocks accept 2 or 3 arguments:
87 # Macro blocks accept 2 or 3 arguments:
88 # * obj: the object that is rendered (eg. an Issue, a WikiContent...)
88 # * obj: the object that is rendered (eg. an Issue, a WikiContent...)
89 # * args: macro arguments
89 # * args: macro arguments
90 # * text: the block of text given to the macro (should be present only if the
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
91 # macro accepts a block of text). text is a String or nil if the macro is
92 # invoked without a block of text.
92 # invoked without a block of text.
93 #
93 #
94 # Examples:
94 # Examples:
95 # By default, when the macro is invoked, the comma separated list of arguments
95 # By default, when the macro is invoked, the comma separated list of arguments
96 # is split and passed to the macro block as an array. If no argument is given
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:
97 # the macro will be invoked with an empty array:
98 #
98 #
99 # macro :my_macro do |obj, args|
99 # macro :my_macro do |obj, args|
100 # # args is an array
100 # # args is an array
101 # # and this macro do not accept a block of text
101 # # and this macro do not accept a block of text
102 # end
102 # end
103 #
103 #
104 # You can disable arguments spliting with the :parse_args => false option. In
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:
105 # this case, the full string of arguments is passed to the macro:
106 #
106 #
107 # macro :my_macro, :parse_args => false do |obj, args|
107 # macro :my_macro, :parse_args => false do |obj, args|
108 # # args is a string
108 # # args is a string
109 # end
109 # end
110 #
110 #
111 # Macro can optionally accept a block of text:
111 # Macro can optionally accept a block of text:
112 #
112 #
113 # macro :my_macro do |obj, args, text|
113 # macro :my_macro do |obj, args, text|
114 # # this macro accepts a block of text
114 # # this macro accepts a block of text
115 # end
115 # end
116 #
116 #
117 # Macros are invoked in formatted text using double curly brackets. Arguments
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
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
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).
120 # a macro that do not accept a block of text with some text will fail).
121 # Examples:
121 # Examples:
122 #
122 #
123 # No arguments:
123 # No arguments:
124 # {{my_macro}}
124 # {{my_macro}}
125 #
125 #
126 # With arguments:
126 # With arguments:
127 # {{my_macro(arg1, arg2)}}
127 # {{my_macro(arg1, arg2)}}
128 #
128 #
129 # With a block of text:
129 # With a block of text:
130 # {{my_macro
130 # {{my_macro
131 # multiple lines
131 # multiple lines
132 # of text
132 # of text
133 # }}
133 # }}
134 #
134 #
135 # With arguments and a block of text
135 # With arguments and a block of text
136 # {{my_macro(arg1, arg2)
136 # {{my_macro(arg1, arg2)
137 # multiple lines
137 # multiple lines
138 # of text
138 # of text
139 # }}
139 # }}
140 #
140 #
141 # If a block of text is given, the closing tag }} must be at the start of a new line.
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)
142 def macro(name, options={}, &block)
143 options.assert_valid_keys(:desc, :parse_args)
143 options.assert_valid_keys(:desc, :parse_args)
144 unless name.to_s.match(/\A\w+\z/)
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)"
145 raise "Invalid macro name: #{name} (only 0-9, A-Z, a-z and _ characters are accepted)"
146 end
146 end
147 unless block_given?
147 unless block_given?
148 raise "Can not create a macro without a block!"
148 raise "Can not create a macro without a block!"
149 end
149 end
150 name = name.to_s.downcase.to_sym
150 name = name.to_s.downcase.to_sym
151 available_macros[name] = {:desc => @@desc || ''}.merge(options)
151 available_macros[name] = {:desc => @@desc || ''}.merge(options)
152 @@desc = nil
152 @@desc = nil
153 Definitions.send :define_method, "macro_#{name}", &block
153 Definitions.send :define_method, "macro_#{name}", &block
154 end
154 end
155
155
156 # Sets description for the next macro to be defined
156 # Sets description for the next macro to be defined
157 def desc(txt)
157 def desc(txt)
158 @@desc = txt
158 @@desc = txt
159 end
159 end
160 end
160 end
161
161
162 # Builtin macros
162 # Builtin macros
163 desc "Sample macro."
163 desc "Sample macro."
164 macro :hello_world do |obj, args, text|
164 macro :hello_world do |obj, args, text|
165 h("Hello world! Object: #{obj.class.name}, " +
165 h("Hello world! Object: #{obj.class.name}, " +
166 (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
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.")
167 " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
168 )
168 )
169 end
169 end
170
170
171 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."
172 macro :macro_list do |obj, args|
172 macro :macro_list do |obj, args|
173 out = ''.html_safe
173 out = ''.html_safe
174 @@available_macros.each do |macro, options|
174 @@available_macros.each do |macro, options|
175 out << content_tag('dt', content_tag('code', macro.to_s))
175 out << content_tag('dt', content_tag('code', macro.to_s))
176 out << content_tag('dd', content_tag('pre', options[:desc]))
176 out << content_tag('dd', content_tag('pre', options[:desc]))
177 end
177 end
178 content_tag('dl', out)
178 content_tag('dl', out)
179 end
179 end
180
180
181 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" +
182 "{{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" +
183 "{{child_pages(depth=2)}} -- display 2 levels nesting only\n" +
184 "{{child_pages(Foo)}} -- lists all children of page Foo\n" +
184 "{{child_pages(Foo)}} -- lists all children of page Foo\n" +
185 "{{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"
186 macro :child_pages do |obj, args|
186 macro :child_pages do |obj, args|
187 args, options = extract_macro_options(args, :parent, :depth)
187 args, options = extract_macro_options(args, :parent, :depth)
188 options[:depth] = options[:depth].to_i if options[:depth].present?
188 options[:depth] = options[:depth].to_i if options[:depth].present?
189
189
190 page = nil
190 page = nil
191 if args.size > 0
191 if args.size > 0
192 page = Wiki.find_page(args.first.to_s, :project => @project)
192 page = Wiki.find_page(args.first.to_s, :project => @project)
193 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
193 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
194 page = obj.page
194 page = obj.page
195 else
195 else
196 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.'
197 end
197 end
198 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)
199 pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id)
199 pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id)
200 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
200 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
201 end
201 end
202
202
203 desc "Includes a wiki page. Examples:\n\n" +
203 desc "Includes a wiki page. Examples:\n\n" +
204 "{{include(Foo)}}\n" +
204 "{{include(Foo)}}\n" +
205 "{{include(projectname:Foo)}} -- to include a page of a specific project wiki"
205 "{{include(projectname:Foo)}} -- to include a page of a specific project wiki"
206 macro :include do |obj, args|
206 macro :include do |obj, args|
207 page = Wiki.find_page(args.first.to_s, :project => @project)
207 page = Wiki.find_page(args.first.to_s, :project => @project)
208 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
208 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
209 @included_wiki_pages ||= []
209 @included_wiki_pages ||= []
210 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
210 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.id)
211 @included_wiki_pages << page.title
211 @included_wiki_pages << page.id
212 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
212 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
213 @included_wiki_pages.pop
213 @included_wiki_pages.pop
214 out
214 out
215 end
215 end
216
216
217 desc "Inserts of collapsed block of text. Examples:\n\n" +
217 desc "Inserts of collapsed block of text. Examples:\n\n" +
218 "{{collapse\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}\n\n" +
218 "{{collapse\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}\n\n" +
219 "{{collapse(View details...)\nWith custom link text.\n}}"
219 "{{collapse(View details...)\nWith custom link text.\n}}"
220 macro :collapse do |obj, args, text|
220 macro :collapse do |obj, args, text|
221 html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
221 html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
222 show_label = args[0] || l(:button_show)
222 show_label = args[0] || l(:button_show)
223 hide_label = args[1] || args[0] || l(:button_hide)
223 hide_label = args[1] || args[0] || l(:button_hide)
224 js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
224 js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
225 out = ''.html_safe
225 out = ''.html_safe
226 out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
226 out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
227 out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
227 out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
228 out << content_tag('div', textilizable(text, :object => obj, :headings => false), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
228 out << content_tag('div', textilizable(text, :object => obj, :headings => false), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
229 out
229 out
230 end
230 end
231
231
232 desc "Displays a clickable thumbnail of an attached image. Examples:\n\n" +
232 desc "Displays a clickable thumbnail of an attached image. Examples:\n\n" +
233 "{{thumbnail(image.png)}}\n" +
233 "{{thumbnail(image.png)}}\n" +
234 "{{thumbnail(image.png, size=300, title=Thumbnail)}} -- with custom title and size"
234 "{{thumbnail(image.png, size=300, title=Thumbnail)}} -- with custom title and size"
235 macro :thumbnail do |obj, args|
235 macro :thumbnail do |obj, args|
236 args, options = extract_macro_options(args, :size, :title)
236 args, options = extract_macro_options(args, :size, :title)
237 filename = args.first
237 filename = args.first
238 raise 'Filename required' unless filename.present?
238 raise 'Filename required' unless filename.present?
239 size = options[:size]
239 size = options[:size]
240 raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
240 raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
241 size = size.to_i
241 size = size.to_i
242 size = nil unless size > 0
242 size = nil unless size > 0
243 if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
243 if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
244 title = options[:title] || attachment.title
244 title = options[:title] || attachment.title
245 thumbnail_url = url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size, :only_path => @only_path)
245 thumbnail_url = url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size, :only_path => @only_path)
246 image_url = url_for(:controller => 'attachments', :action => 'show', :id => attachment, :only_path => @only_path)
246 image_url = url_for(:controller => 'attachments', :action => 'show', :id => attachment, :only_path => @only_path)
247
247
248 img = image_tag(thumbnail_url, :alt => attachment.filename)
248 img = image_tag(thumbnail_url, :alt => attachment.filename)
249 link_to(img, image_url, :class => 'thumbnail', :title => title)
249 link_to(img, image_url, :class => 'thumbnail', :title => title)
250 else
250 else
251 raise "Attachment #{filename} not found"
251 raise "Attachment #{filename} not found"
252 end
252 end
253 end
253 end
254 end
254 end
255 end
255 end
256 end
256 end
General Comments 0
You need to be logged in to leave comments. Login now