You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

macros.rb 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2019 Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. module Redmine
  19. module WikiFormatting
  20. module Macros
  21. module Definitions
  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. def exec_macro(name, obj, args, text, options={})
  27. macro_options = Redmine::WikiFormatting::Macros.available_macros[name.to_sym]
  28. return unless macro_options
  29. if options[:inline_attachments] == false
  30. Redmine::WikiFormatting::Macros.inline_attachments = false
  31. else
  32. Redmine::WikiFormatting::Macros.inline_attachments = true
  33. end
  34. method_name = "macro_#{name}"
  35. unless macro_options[:parse_args] == false
  36. args = args.split(',').map(&:strip)
  37. end
  38. begin
  39. if self.class.instance_method(method_name).arity == 3
  40. send(method_name, obj, args, text)
  41. elsif text
  42. raise "This macro does not accept a block of text"
  43. else
  44. send(method_name, obj, args)
  45. end
  46. rescue => e
  47. "<div class=\"flash error\">Error executing the <strong>#{h name}</strong> macro (#{h e.to_s})</div>".html_safe
  48. end
  49. end
  50. def extract_macro_options(args, *keys)
  51. options = {}
  52. while args.last.to_s.strip =~ %r{^(.+?)\=(.+)$} && keys.include?($1.downcase.to_sym)
  53. options[$1.downcase.to_sym] = $2
  54. args.pop
  55. end
  56. return [args, options]
  57. end
  58. end
  59. @@available_macros = {}
  60. @@inline_attachments = true
  61. mattr_accessor :available_macros
  62. mattr_accessor :inline_attachments
  63. class << self
  64. # Plugins can use this method to define new macros:
  65. #
  66. # Redmine::WikiFormatting::Macros.register do
  67. # desc "This is my macro"
  68. # macro :my_macro do |obj, args|
  69. # "My macro output"
  70. # end
  71. #
  72. # desc "This is my macro that accepts a block of text"
  73. # macro :my_macro do |obj, args, text|
  74. # "My macro output"
  75. # end
  76. # end
  77. def register(&block)
  78. class_eval(&block) if block_given?
  79. end
  80. # Defines a new macro with the given name, options and block.
  81. #
  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 comma 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 /\A\w+\z/.match?(name.to_s)
  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
  150. name = name.to_s.downcase.to_sym
  151. available_macros[name] = {:desc => @@desc || ''}.merge(options)
  152. @@desc = nil
  153. Definitions.send :define_method, "macro_#{name}", &block
  154. end
  155. # Sets description for the next macro to be defined
  156. def desc(txt)
  157. @@desc = txt
  158. end
  159. end
  160. # Builtin macros
  161. desc "Sample macro."
  162. macro :hello_world do |obj, args, text|
  163. h("Hello world! Object: #{obj.class.name}, " +
  164. (args.empty? ? "Called with no argument" : "Arguments: #{args.join(', ')}") +
  165. " and " + (text.present? ? "a #{text.size} bytes long block of text." : "no block of text.")
  166. )
  167. end
  168. desc "Displays a list of all available macros, including description if available."
  169. macro :macro_list do |obj, args|
  170. out = ''.html_safe
  171. @@available_macros.each do |macro, options|
  172. out << content_tag('dt', content_tag('code', macro.to_s))
  173. out << content_tag('dd', content_tag('pre', options[:desc]))
  174. end
  175. content_tag('dl', out)
  176. end
  177. desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
  178. "{{child_pages}} -- can be used from a wiki page only\n" +
  179. "{{child_pages(depth=2)}} -- display 2 levels nesting only\n" +
  180. "{{child_pages(Foo)}} -- lists all children of page Foo\n" +
  181. "{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
  182. macro :child_pages do |obj, args|
  183. args, options = extract_macro_options(args, :parent, :depth)
  184. options[:depth] = options[:depth].to_i if options[:depth].present?
  185. page = nil
  186. if args.size > 0
  187. page = Wiki.find_page(args.first.to_s, :project => @project)
  188. elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
  189. page = obj.page
  190. else
  191. raise 'With no argument, this macro can be called from wiki pages only.'
  192. end
  193. raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
  194. pages = page.self_and_descendants(options[:depth]).group_by(&:parent_id)
  195. render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
  196. end
  197. desc "Includes a wiki page. Examples:\n\n" +
  198. "{{include(Foo)}}\n" +
  199. "{{include(projectname:Foo)}} -- to include a page of a specific project wiki"
  200. macro :include do |obj, args|
  201. page = Wiki.find_page(args.first.to_s, :project => @project)
  202. raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
  203. @included_wiki_pages ||= []
  204. raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.id)
  205. @included_wiki_pages << page.id
  206. out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false, :inline_attachments => @@inline_attachments)
  207. @included_wiki_pages.pop
  208. out
  209. end
  210. desc "Inserts of collapsed block of text. Examples:\n\n" +
  211. "{{collapse\nThis is a block of text that is collapsed by default.\nIt can be expanded by clicking a link.\n}}\n\n" +
  212. "{{collapse(View details...)\nWith custom link text.\n}}"
  213. macro :collapse do |obj, args, text|
  214. html_id = "collapse-#{Redmine::Utils.random_hex(4)}"
  215. show_label = args[0] || l(:button_show)
  216. hide_label = args[1] || args[0] || l(:button_hide)
  217. js = "$('##{html_id}-show, ##{html_id}-hide').toggle(); $('##{html_id}').fadeToggle(150);"
  218. out = ''.html_safe
  219. out << link_to_function(show_label, js, :id => "#{html_id}-show", :class => 'collapsible collapsed')
  220. out << link_to_function(hide_label, js, :id => "#{html_id}-hide", :class => 'collapsible', :style => 'display:none;')
  221. out << content_tag('div', textilizable(text, :object => obj, :headings => false, :inline_attachments => @@inline_attachments), :id => html_id, :class => 'collapsed-text', :style => 'display:none;')
  222. out
  223. end
  224. desc "Displays a clickable thumbnail of an attached image.\n" +
  225. "Default size is 200 pixels. Examples:\n\n" +
  226. "{{thumbnail(image.png)}}\n" +
  227. "{{thumbnail(image.png, size=300, title=Thumbnail)}} -- with custom title and size"
  228. macro :thumbnail do |obj, args|
  229. args, options = extract_macro_options(args, :size, :title)
  230. filename = args.first
  231. raise 'Filename required' unless filename.present?
  232. size = options[:size]
  233. raise 'Invalid size parameter' unless size.nil? || /^\d+$/.match?(size)
  234. size = size.to_i
  235. size = 200 unless size > 0
  236. if obj && obj.respond_to?(:attachments) && attachment = Attachment.latest_attach(obj.attachments, filename)
  237. title = options[:title] || attachment.title
  238. thumbnail_url = url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment, :size => size, :only_path => @only_path)
  239. image_url = url_for(:controller => 'attachments', :action => 'show', :id => attachment, :only_path => @only_path)
  240. img = image_tag(thumbnail_url, :alt => attachment.filename)
  241. link_to(img, image_url, :class => 'thumbnail', :title => title)
  242. else
  243. raise "Attachment #{filename} not found"
  244. end
  245. end
  246. desc "Displays an issue link including additional information. Examples:\n\n" +
  247. "{{issue(123)}} -- Issue #123: Enhance macro capabilities\n" +
  248. "{{issue(123, project=true)}} -- Andromeda - Issue #123: Enhance macro capabilities\n" +
  249. "{{issue(123, tracker=false)}} -- #123: Enhance macro capabilities\n" +
  250. "{{issue(123, subject=false, project=true)}} -- Andromeda - Issue #123\n"
  251. macro :issue do |obj, args|
  252. args, options = extract_macro_options(args, :project, :subject, :tracker)
  253. id = args.first
  254. issue = Issue.visible.find_by(id: id)
  255. if issue
  256. # remove invalid options
  257. options.delete_if { |k,v| v != 'true' && v != 'false' }
  258. # turn string values into boolean
  259. options.each do |k, v|
  260. options[k] = v == 'true'
  261. end
  262. link_to_issue(issue, options)
  263. else
  264. # Fall back to regular issue link format to indicate, that there
  265. # should have been something.
  266. "##{id}"
  267. end
  268. end
  269. end
  270. end
  271. end