##// END OF EJS Templates
Merged r10885 from trunk (#12451)....
Jean-Philippe Lang -
r10778:6062a0a89d50
parent child
Show More
@@ -1,231 +1,231
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 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.
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 coma separated list of arguments
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
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_sym if name.is_a?(String)
150 name = name.to_sym if name.is_a?(String)
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}".downcase, &block
153 Definitions.send :define_method, "macro_#{name}".downcase, &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', textilizable(options[:desc]))
176 out << content_tag('dd', textilizable(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(Foo)}} -- lists all children of page Foo\n" +
183 " !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
184 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
184 " !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
185 macro :child_pages do |obj, args|
185 macro :child_pages do |obj, args|
186 args, options = extract_macro_options(args, :parent)
186 args, options = extract_macro_options(args, :parent)
187 page = nil
187 page = nil
188 if args.size > 0
188 if args.size > 0
189 page = Wiki.find_page(args.first.to_s, :project => @project)
189 page = Wiki.find_page(args.first.to_s, :project => @project)
190 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
190 elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
191 page = obj.page
191 page = obj.page
192 else
192 else
193 raise 'With no argument, this macro can be called from wiki pages only.'
193 raise 'With no argument, this macro can be called from wiki pages only.'
194 end
194 end
195 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
195 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
196 pages = ([page] + page.descendants).group_by(&:parent_id)
196 pages = ([page] + page.descendants).group_by(&:parent_id)
197 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
197 render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
198 end
198 end
199
199
200 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)}}"
200 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)}}"
201 macro :include do |obj, args|
201 macro :include do |obj, args|
202 page = Wiki.find_page(args.first.to_s, :project => @project)
202 page = Wiki.find_page(args.first.to_s, :project => @project)
203 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
203 raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
204 @included_wiki_pages ||= []
204 @included_wiki_pages ||= []
205 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
205 raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
206 @included_wiki_pages << page.title
206 @included_wiki_pages << page.title
207 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
207 out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
208 @included_wiki_pages.pop
208 @included_wiki_pages.pop
209 out
209 out
210 end
210 end
211
211
212 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>"
212 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>"
213 macro :thumbnail do |obj, args|
213 macro :thumbnail do |obj, args|
214 args, options = extract_macro_options(args, :size, :title)
214 args, options = extract_macro_options(args, :size, :title)
215 filename = args.first
215 filename = args.first
216 raise 'Filename required' unless filename.present?
216 raise 'Filename required' unless filename.present?
217 size = options[:size]
217 size = options[:size]
218 raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
218 raise 'Invalid size parameter' unless size.nil? || size.match(/^\d+$/)
219 size = size.to_i
219 size = size.to_i
220 size = nil unless size > 0
220 size = nil unless size > 0
221 if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
221 if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
222 title = options[:title] || attachment.title
222 title = options[:title] || attachment.title
223 img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename)
223 img = image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size), :alt => attachment.filename)
224 link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title)
224 link_to(img, url_for(:controller => 'attachments', :action => 'show', :id => attachment), :class => 'thumbnail', :title => title)
225 else
225 else
226 raise "Attachment #{filename} not found"
226 raise "Attachment #{filename} not found"
227 end
227 end
228 end
228 end
229 end
229 end
230 end
230 end
231 end
231 end
@@ -1,295 +1,314
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 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.
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 require File.expand_path('../../../../../test_helper', __FILE__)
18 require File.expand_path('../../../../../test_helper', __FILE__)
19
19
20 class Redmine::WikiFormatting::MacrosTest < ActionView::TestCase
20 class Redmine::WikiFormatting::MacrosTest < ActionView::TestCase
21 include ApplicationHelper
21 include ApplicationHelper
22 include ActionView::Helpers::TextHelper
22 include ActionView::Helpers::TextHelper
23 include ActionView::Helpers::SanitizeHelper
23 include ActionView::Helpers::SanitizeHelper
24 include ERB::Util
24 include ERB::Util
25 extend ActionView::Helpers::SanitizeHelper::ClassMethods
25 extend ActionView::Helpers::SanitizeHelper::ClassMethods
26
26
27 fixtures :projects, :roles, :enabled_modules, :users,
27 fixtures :projects, :roles, :enabled_modules, :users,
28 :repositories, :changesets,
28 :repositories, :changesets,
29 :trackers, :issue_statuses, :issues,
29 :trackers, :issue_statuses, :issues,
30 :versions, :documents,
30 :versions, :documents,
31 :wikis, :wiki_pages, :wiki_contents,
31 :wikis, :wiki_pages, :wiki_contents,
32 :boards, :messages,
32 :boards, :messages,
33 :attachments
33 :attachments
34
34
35 def setup
35 def setup
36 super
36 super
37 @project = nil
37 @project = nil
38 end
38 end
39
39
40 def teardown
40 def teardown
41 end
41 end
42
42
43 def test_macro_registration
43 def test_macro_registration
44 Redmine::WikiFormatting::Macros.register do
44 Redmine::WikiFormatting::Macros.register do
45 macro :foo do |obj, args|
45 macro :foo do |obj, args|
46 "Foo: #{args.size} (#{args.join(',')}) (#{args.class.name})"
46 "Foo: #{args.size} (#{args.join(',')}) (#{args.class.name})"
47 end
47 end
48 end
48 end
49
49
50 assert_equal '<p>Foo: 0 () (Array)</p>', textilizable("{{foo}}")
50 assert_equal '<p>Foo: 0 () (Array)</p>', textilizable("{{foo}}")
51 assert_equal '<p>Foo: 0 () (Array)</p>', textilizable("{{foo()}}")
51 assert_equal '<p>Foo: 0 () (Array)</p>', textilizable("{{foo()}}")
52 assert_equal '<p>Foo: 1 (arg1) (Array)</p>', textilizable("{{foo(arg1)}}")
52 assert_equal '<p>Foo: 1 (arg1) (Array)</p>', textilizable("{{foo(arg1)}}")
53 assert_equal '<p>Foo: 2 (arg1,arg2) (Array)</p>', textilizable("{{foo(arg1, arg2)}}")
53 assert_equal '<p>Foo: 2 (arg1,arg2) (Array)</p>', textilizable("{{foo(arg1, arg2)}}")
54 end
54 end
55
55
56 def test_macro_registration_parse_args_set_to_false_should_disable_arguments_parsing
56 def test_macro_registration_parse_args_set_to_false_should_disable_arguments_parsing
57 Redmine::WikiFormatting::Macros.register do
57 Redmine::WikiFormatting::Macros.register do
58 macro :bar, :parse_args => false do |obj, args|
58 macro :bar, :parse_args => false do |obj, args|
59 "Bar: (#{args}) (#{args.class.name})"
59 "Bar: (#{args}) (#{args.class.name})"
60 end
60 end
61 end
61 end
62
62
63 assert_equal '<p>Bar: (args, more args) (String)</p>', textilizable("{{bar(args, more args)}}")
63 assert_equal '<p>Bar: (args, more args) (String)</p>', textilizable("{{bar(args, more args)}}")
64 assert_equal '<p>Bar: () (String)</p>', textilizable("{{bar}}")
64 assert_equal '<p>Bar: () (String)</p>', textilizable("{{bar}}")
65 assert_equal '<p>Bar: () (String)</p>', textilizable("{{bar()}}")
65 assert_equal '<p>Bar: () (String)</p>', textilizable("{{bar()}}")
66 end
66 end
67
67
68 def test_macro_registration_with_3_args_should_receive_text_argument
68 def test_macro_registration_with_3_args_should_receive_text_argument
69 Redmine::WikiFormatting::Macros.register do
69 Redmine::WikiFormatting::Macros.register do
70 macro :baz do |obj, args, text|
70 macro :baz do |obj, args, text|
71 "Baz: (#{args.join(',')}) (#{text.class.name}) (#{text})"
71 "Baz: (#{args.join(',')}) (#{text.class.name}) (#{text})"
72 end
72 end
73 end
73 end
74
74
75 assert_equal "<p>Baz: () (NilClass) ()</p>", textilizable("{{baz}}")
75 assert_equal "<p>Baz: () (NilClass) ()</p>", textilizable("{{baz}}")
76 assert_equal "<p>Baz: () (NilClass) ()</p>", textilizable("{{baz()}}")
76 assert_equal "<p>Baz: () (NilClass) ()</p>", textilizable("{{baz()}}")
77 assert_equal "<p>Baz: () (String) (line1\nline2)</p>", textilizable("{{baz()\nline1\nline2\n}}")
77 assert_equal "<p>Baz: () (String) (line1\nline2)</p>", textilizable("{{baz()\nline1\nline2\n}}")
78 assert_equal "<p>Baz: (arg1,arg2) (String) (line1\nline2)</p>", textilizable("{{baz(arg1, arg2)\nline1\nline2\n}}")
78 assert_equal "<p>Baz: (arg1,arg2) (String) (line1\nline2)</p>", textilizable("{{baz(arg1, arg2)\nline1\nline2\n}}")
79 end
79 end
80
80
81 def test_multiple_macros_on_the_same_line
81 def test_multiple_macros_on_the_same_line
82 Redmine::WikiFormatting::Macros.macro :foo do |obj, args|
82 Redmine::WikiFormatting::Macros.macro :foo do |obj, args|
83 args.any? ? "args: #{args.join(',')}" : "no args"
83 args.any? ? "args: #{args.join(',')}" : "no args"
84 end
84 end
85
85
86 assert_equal '<p>no args no args</p>', textilizable("{{foo}} {{foo}}")
86 assert_equal '<p>no args no args</p>', textilizable("{{foo}} {{foo}}")
87 assert_equal '<p>args: a,b no args</p>', textilizable("{{foo(a,b)}} {{foo}}")
87 assert_equal '<p>args: a,b no args</p>', textilizable("{{foo(a,b)}} {{foo}}")
88 assert_equal '<p>args: a,b args: c,d</p>', textilizable("{{foo(a,b)}} {{foo(c,d)}}")
88 assert_equal '<p>args: a,b args: c,d</p>', textilizable("{{foo(a,b)}} {{foo(c,d)}}")
89 assert_equal '<p>no args args: c,d</p>', textilizable("{{foo}} {{foo(c,d)}}")
89 assert_equal '<p>no args args: c,d</p>', textilizable("{{foo}} {{foo(c,d)}}")
90 end
90 end
91
91
92 def test_macro_should_receive_the_object_as_argument_when_with_object_and_attribute
92 def test_macro_should_receive_the_object_as_argument_when_with_object_and_attribute
93 issue = Issue.find(1)
93 issue = Issue.find(1)
94 issue.description = "{{hello_world}}"
94 issue.description = "{{hello_world}}"
95 assert_equal '<p>Hello world! Object: Issue, Called with no argument and no block of text.</p>', textilizable(issue, :description)
95 assert_equal '<p>Hello world! Object: Issue, Called with no argument and no block of text.</p>', textilizable(issue, :description)
96 end
96 end
97
97
98 def test_macro_should_receive_the_object_as_argument_when_called_with_object_option
98 def test_macro_should_receive_the_object_as_argument_when_called_with_object_option
99 text = "{{hello_world}}"
99 text = "{{hello_world}}"
100 assert_equal '<p>Hello world! Object: Issue, Called with no argument and no block of text.</p>', textilizable(text, :object => Issue.find(1))
100 assert_equal '<p>Hello world! Object: Issue, Called with no argument and no block of text.</p>', textilizable(text, :object => Issue.find(1))
101 end
101 end
102
102
103 def test_extract_macro_options_should_with_args
104 options = extract_macro_options(["arg1", "arg2"], :foo, :size)
105 assert_equal([["arg1", "arg2"], {}], options)
106 end
107
108 def test_extract_macro_options_should_with_options
109 options = extract_macro_options(["foo=bar", "size=2"], :foo, :size)
110 assert_equal([[], {:foo => "bar", :size => "2"}], options)
111 end
112
113 def test_extract_macro_options_should_with_args_and_options
114 options = extract_macro_options(["arg1", "arg2", "foo=bar", "size=2"], :foo, :size)
115 assert_equal([["arg1", "arg2"], {:foo => "bar", :size => "2"}], options)
116 end
117
118 def test_extract_macro_options_should_parse_options_lazily
119 options = extract_macro_options(["params=x=1&y=2"], :params)
120 assert_equal([[], {:params => "x=1&y=2"}], options)
121 end
103
122
104 def test_macro_exception_should_be_displayed
123 def test_macro_exception_should_be_displayed
105 Redmine::WikiFormatting::Macros.macro :exception do |obj, args|
124 Redmine::WikiFormatting::Macros.macro :exception do |obj, args|
106 raise "My message"
125 raise "My message"
107 end
126 end
108
127
109 text = "{{exception}}"
128 text = "{{exception}}"
110 assert_include '<div class="flash error">Error executing the <strong>exception</strong> macro (My message)</div>', textilizable(text)
129 assert_include '<div class="flash error">Error executing the <strong>exception</strong> macro (My message)</div>', textilizable(text)
111 end
130 end
112
131
113 def test_macro_arguments_should_not_be_parsed_by_formatters
132 def test_macro_arguments_should_not_be_parsed_by_formatters
114 text = '{{hello_world(http://www.redmine.org, #1)}}'
133 text = '{{hello_world(http://www.redmine.org, #1)}}'
115 assert_include 'Arguments: http://www.redmine.org, #1', textilizable(text)
134 assert_include 'Arguments: http://www.redmine.org, #1', textilizable(text)
116 end
135 end
117
136
118 def test_exclamation_mark_should_not_run_macros
137 def test_exclamation_mark_should_not_run_macros
119 text = "!{{hello_world}}"
138 text = "!{{hello_world}}"
120 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
139 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
121 end
140 end
122
141
123 def test_exclamation_mark_should_escape_macros
142 def test_exclamation_mark_should_escape_macros
124 text = "!{{hello_world(<tag>)}}"
143 text = "!{{hello_world(<tag>)}}"
125 assert_equal '<p>{{hello_world(&lt;tag&gt;)}}</p>', textilizable(text)
144 assert_equal '<p>{{hello_world(&lt;tag&gt;)}}</p>', textilizable(text)
126 end
145 end
127
146
128 def test_unknown_macros_should_not_be_replaced
147 def test_unknown_macros_should_not_be_replaced
129 text = "{{unknown}}"
148 text = "{{unknown}}"
130 assert_equal '<p>{{unknown}}</p>', textilizable(text)
149 assert_equal '<p>{{unknown}}</p>', textilizable(text)
131 end
150 end
132
151
133 def test_unknown_macros_should_parsed_as_text
152 def test_unknown_macros_should_parsed_as_text
134 text = "{{unknown(*test*)}}"
153 text = "{{unknown(*test*)}}"
135 assert_equal '<p>{{unknown(<strong>test</strong>)}}</p>', textilizable(text)
154 assert_equal '<p>{{unknown(<strong>test</strong>)}}</p>', textilizable(text)
136 end
155 end
137
156
138 def test_unknown_macros_should_be_escaped
157 def test_unknown_macros_should_be_escaped
139 text = "{{unknown(<tag>)}}"
158 text = "{{unknown(<tag>)}}"
140 assert_equal '<p>{{unknown(&lt;tag&gt;)}}</p>', textilizable(text)
159 assert_equal '<p>{{unknown(&lt;tag&gt;)}}</p>', textilizable(text)
141 end
160 end
142
161
143 def test_html_safe_macro_output_should_not_be_escaped
162 def test_html_safe_macro_output_should_not_be_escaped
144 Redmine::WikiFormatting::Macros.macro :safe_macro do |obj, args|
163 Redmine::WikiFormatting::Macros.macro :safe_macro do |obj, args|
145 "<tag>".html_safe
164 "<tag>".html_safe
146 end
165 end
147 assert_equal '<p><tag></p>', textilizable("{{safe_macro}}")
166 assert_equal '<p><tag></p>', textilizable("{{safe_macro}}")
148 end
167 end
149
168
150 def test_macro_hello_world
169 def test_macro_hello_world
151 text = "{{hello_world}}"
170 text = "{{hello_world}}"
152 assert textilizable(text).match(/Hello world!/)
171 assert textilizable(text).match(/Hello world!/)
153 end
172 end
154
173
155 def test_macro_hello_world_should_escape_arguments
174 def test_macro_hello_world_should_escape_arguments
156 text = "{{hello_world(<tag>)}}"
175 text = "{{hello_world(<tag>)}}"
157 assert_include 'Arguments: &lt;tag&gt;', textilizable(text)
176 assert_include 'Arguments: &lt;tag&gt;', textilizable(text)
158 end
177 end
159
178
160 def test_macro_macro_list
179 def test_macro_macro_list
161 text = "{{macro_list}}"
180 text = "{{macro_list}}"
162 assert_match %r{<code>hello_world</code>}, textilizable(text)
181 assert_match %r{<code>hello_world</code>}, textilizable(text)
163 end
182 end
164
183
165 def test_macro_include
184 def test_macro_include
166 @project = Project.find(1)
185 @project = Project.find(1)
167 # include a page of the current project wiki
186 # include a page of the current project wiki
168 text = "{{include(Another page)}}"
187 text = "{{include(Another page)}}"
169 assert_include 'This is a link to a ticket', textilizable(text)
188 assert_include 'This is a link to a ticket', textilizable(text)
170
189
171 @project = nil
190 @project = nil
172 # include a page of a specific project wiki
191 # include a page of a specific project wiki
173 text = "{{include(ecookbook:Another page)}}"
192 text = "{{include(ecookbook:Another page)}}"
174 assert_include 'This is a link to a ticket', textilizable(text)
193 assert_include 'This is a link to a ticket', textilizable(text)
175
194
176 text = "{{include(ecookbook:)}}"
195 text = "{{include(ecookbook:)}}"
177 assert_include 'CookBook documentation', textilizable(text)
196 assert_include 'CookBook documentation', textilizable(text)
178
197
179 text = "{{include(unknowidentifier:somepage)}}"
198 text = "{{include(unknowidentifier:somepage)}}"
180 assert_include 'Page not found', textilizable(text)
199 assert_include 'Page not found', textilizable(text)
181 end
200 end
182
201
183 def test_macro_child_pages
202 def test_macro_child_pages
184 expected = "<p><ul class=\"pages-hierarchy\">\n" +
203 expected = "<p><ul class=\"pages-hierarchy\">\n" +
185 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
204 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
186 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
205 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
187 "</ul>\n</p>"
206 "</ul>\n</p>"
188
207
189 @project = Project.find(1)
208 @project = Project.find(1)
190 # child pages of the current wiki page
209 # child pages of the current wiki page
191 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
210 assert_equal expected, textilizable("{{child_pages}}", :object => WikiPage.find(2).content)
192 # child pages of another page
211 # child pages of another page
193 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
212 assert_equal expected, textilizable("{{child_pages(Another_page)}}", :object => WikiPage.find(1).content)
194
213
195 @project = Project.find(2)
214 @project = Project.find(2)
196 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
215 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page)}}", :object => WikiPage.find(1).content)
197 end
216 end
198
217
199 def test_macro_child_pages_with_option
218 def test_macro_child_pages_with_option
200 expected = "<p><ul class=\"pages-hierarchy\">\n" +
219 expected = "<p><ul class=\"pages-hierarchy\">\n" +
201 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
220 "<li><a href=\"/projects/ecookbook/wiki/Another_page\">Another page</a>\n" +
202 "<ul class=\"pages-hierarchy\">\n" +
221 "<ul class=\"pages-hierarchy\">\n" +
203 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
222 "<li><a href=\"/projects/ecookbook/wiki/Child_1\">Child 1</a></li>\n" +
204 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
223 "<li><a href=\"/projects/ecookbook/wiki/Child_2\">Child 2</a></li>\n" +
205 "</ul>\n</li>\n</ul>\n</p>"
224 "</ul>\n</li>\n</ul>\n</p>"
206
225
207 @project = Project.find(1)
226 @project = Project.find(1)
208 # child pages of the current wiki page
227 # child pages of the current wiki page
209 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
228 assert_equal expected, textilizable("{{child_pages(parent=1)}}", :object => WikiPage.find(2).content)
210 # child pages of another page
229 # child pages of another page
211 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
230 assert_equal expected, textilizable("{{child_pages(Another_page, parent=1)}}", :object => WikiPage.find(1).content)
212
231
213 @project = Project.find(2)
232 @project = Project.find(2)
214 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
233 assert_equal expected, textilizable("{{child_pages(ecookbook:Another_page, parent=1)}}", :object => WikiPage.find(1).content)
215 end
234 end
216
235
217 def test_macro_child_pages_without_wiki_page_should_fail
236 def test_macro_child_pages_without_wiki_page_should_fail
218 assert_match /can be called from wiki pages only/, textilizable("{{child_pages}}")
237 assert_match /can be called from wiki pages only/, textilizable("{{child_pages}}")
219 end
238 end
220
239
221 def test_macro_thumbnail
240 def test_macro_thumbnail
222 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
241 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
223 textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14))
242 textilizable("{{thumbnail(testfile.png)}}", :object => Issue.find(14))
224 end
243 end
225
244
226 def test_macro_thumbnail_with_size
245 def test_macro_thumbnail_with_size
227 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17/200" /></a></p>',
246 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="testfile.PNG"><img alt="testfile.PNG" src="/attachments/thumbnail/17/200" /></a></p>',
228 textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14))
247 textilizable("{{thumbnail(testfile.png, size=200)}}", :object => Issue.find(14))
229 end
248 end
230
249
231 def test_macro_thumbnail_with_title
250 def test_macro_thumbnail_with_title
232 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="Cool image"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
251 assert_equal '<p><a href="/attachments/17" class="thumbnail" title="Cool image"><img alt="testfile.PNG" src="/attachments/thumbnail/17" /></a></p>',
233 textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14))
252 textilizable("{{thumbnail(testfile.png, title=Cool image)}}", :object => Issue.find(14))
234 end
253 end
235
254
236 def test_macro_thumbnail_with_invalid_filename_should_fail
255 def test_macro_thumbnail_with_invalid_filename_should_fail
237 assert_include 'test.png not found',
256 assert_include 'test.png not found',
238 textilizable("{{thumbnail(test.png)}}", :object => Issue.find(14))
257 textilizable("{{thumbnail(test.png)}}", :object => Issue.find(14))
239 end
258 end
240
259
241 def test_macros_should_not_be_executed_in_pre_tags
260 def test_macros_should_not_be_executed_in_pre_tags
242 text = <<-RAW
261 text = <<-RAW
243 {{hello_world(foo)}}
262 {{hello_world(foo)}}
244
263
245 <pre>
264 <pre>
246 {{hello_world(pre)}}
265 {{hello_world(pre)}}
247 !{{hello_world(pre)}}
266 !{{hello_world(pre)}}
248 </pre>
267 </pre>
249
268
250 {{hello_world(bar)}}
269 {{hello_world(bar)}}
251 RAW
270 RAW
252
271
253 expected = <<-EXPECTED
272 expected = <<-EXPECTED
254 <p>Hello world! Object: NilClass, Arguments: foo and no block of text.</p>
273 <p>Hello world! Object: NilClass, Arguments: foo and no block of text.</p>
255
274
256 <pre>
275 <pre>
257 {{hello_world(pre)}}
276 {{hello_world(pre)}}
258 !{{hello_world(pre)}}
277 !{{hello_world(pre)}}
259 </pre>
278 </pre>
260
279
261 <p>Hello world! Object: NilClass, Arguments: bar and no block of text.</p>
280 <p>Hello world! Object: NilClass, Arguments: bar and no block of text.</p>
262 EXPECTED
281 EXPECTED
263
282
264 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(text).gsub(%r{[\r\n\t]}, '')
283 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(text).gsub(%r{[\r\n\t]}, '')
265 end
284 end
266
285
267 def test_macros_should_be_escaped_in_pre_tags
286 def test_macros_should_be_escaped_in_pre_tags
268 text = "<pre>{{hello_world(<tag>)}}</pre>"
287 text = "<pre>{{hello_world(<tag>)}}</pre>"
269 assert_equal "<pre>{{hello_world(&lt;tag&gt;)}}</pre>", textilizable(text)
288 assert_equal "<pre>{{hello_world(&lt;tag&gt;)}}</pre>", textilizable(text)
270 end
289 end
271
290
272 def test_macros_should_not_mangle_next_macros_outputs
291 def test_macros_should_not_mangle_next_macros_outputs
273 text = '{{macro(2)}} !{{macro(2)}} {{hello_world(foo)}}'
292 text = '{{macro(2)}} !{{macro(2)}} {{hello_world(foo)}}'
274 assert_equal '<p>{{macro(2)}} {{macro(2)}} Hello world! Object: NilClass, Arguments: foo and no block of text.</p>', textilizable(text)
293 assert_equal '<p>{{macro(2)}} {{macro(2)}} Hello world! Object: NilClass, Arguments: foo and no block of text.</p>', textilizable(text)
275 end
294 end
276
295
277 def test_macros_with_text_should_not_mangle_following_macros
296 def test_macros_with_text_should_not_mangle_following_macros
278 text = <<-RAW
297 text = <<-RAW
279 {{hello_world
298 {{hello_world
280 Line of text
299 Line of text
281 }}
300 }}
282
301
283 {{hello_world
302 {{hello_world
284 Another line of text
303 Another line of text
285 }}
304 }}
286 RAW
305 RAW
287
306
288 expected = <<-EXPECTED
307 expected = <<-EXPECTED
289 <p>Hello world! Object: NilClass, Called with no argument and a 12 bytes long block of text.</p>
308 <p>Hello world! Object: NilClass, Called with no argument and a 12 bytes long block of text.</p>
290 <p>Hello world! Object: NilClass, Called with no argument and a 20 bytes long block of text.</p>
309 <p>Hello world! Object: NilClass, Called with no argument and a 20 bytes long block of text.</p>
291 EXPECTED
310 EXPECTED
292
311
293 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(text).gsub(%r{[\r\n\t]}, '')
312 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(text).gsub(%r{[\r\n\t]}, '')
294 end
313 end
295 end
314 end
General Comments 0
You need to be logged in to leave comments. Login now