redmine_api_helper 0.3.24

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of redmine_api_helper might be problematic. Click here for more details.

Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +11 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +339 -0
  7. data/README.md +30 -0
  8. data/Rakefile +2 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/lib/date_helper/date.rb +311 -0
  12. data/lib/odf_writer/bookmark.rb +110 -0
  13. data/lib/odf_writer/bookmark_reader.rb +77 -0
  14. data/lib/odf_writer/document.rb +372 -0
  15. data/lib/odf_writer/field.rb +174 -0
  16. data/lib/odf_writer/field_reader.rb +78 -0
  17. data/lib/odf_writer/image.rb +158 -0
  18. data/lib/odf_writer/image_reader.rb +76 -0
  19. data/lib/odf_writer/images.rb +89 -0
  20. data/lib/odf_writer/list_style.rb +331 -0
  21. data/lib/odf_writer/nested.rb +156 -0
  22. data/lib/odf_writer/odf_helper.rb +56 -0
  23. data/lib/odf_writer/parser/default.rb +685 -0
  24. data/lib/odf_writer/path_finder.rb +114 -0
  25. data/lib/odf_writer/section.rb +120 -0
  26. data/lib/odf_writer/section_reader.rb +61 -0
  27. data/lib/odf_writer/style.rb +417 -0
  28. data/lib/odf_writer/table.rb +135 -0
  29. data/lib/odf_writer/table_reader.rb +61 -0
  30. data/lib/odf_writer/template.rb +222 -0
  31. data/lib/odf_writer/text.rb +97 -0
  32. data/lib/odf_writer/text_reader.rb +77 -0
  33. data/lib/odf_writer/version.rb +29 -0
  34. data/lib/redmine_api_helper/api_helper.rb +333 -0
  35. data/lib/redmine_api_helper/args_helper.rb +106 -0
  36. data/lib/redmine_api_helper/attachments_api_helper.rb +52 -0
  37. data/lib/redmine_api_helper/define_api_helpers.rb +78 -0
  38. data/lib/redmine_api_helper/document_categories_api_helper.rb +38 -0
  39. data/lib/redmine_api_helper/groups_api_helper.rb +80 -0
  40. data/lib/redmine_api_helper/helpers.rb +50 -0
  41. data/lib/redmine_api_helper/issue_priorities_api_helper.rb +38 -0
  42. data/lib/redmine_api_helper/issue_relations_api_helper.rb +66 -0
  43. data/lib/redmine_api_helper/issue_statuses_api_helper.rb +36 -0
  44. data/lib/redmine_api_helper/issues_api_helper.rb +124 -0
  45. data/lib/redmine_api_helper/my_account_api_helper.rb +45 -0
  46. data/lib/redmine_api_helper/news_api_helper.rb +73 -0
  47. data/lib/redmine_api_helper/project_memberships_api_helper.rb +77 -0
  48. data/lib/redmine_api_helper/projects_api_helper.rb +73 -0
  49. data/lib/redmine_api_helper/roles_api_helper.rb +52 -0
  50. data/lib/redmine_api_helper/scripts_api_helper.rb +87 -0
  51. data/lib/redmine_api_helper/search_api_helper.rb +38 -0
  52. data/lib/redmine_api_helper/time_entries_api_helper.rb +73 -0
  53. data/lib/redmine_api_helper/time_entry_activities_api_helper.rb +38 -0
  54. data/lib/redmine_api_helper/trackers_api_helper.rb +38 -0
  55. data/lib/redmine_api_helper/users_api_helper.rb +73 -0
  56. data/lib/redmine_api_helper/version.rb +24 -0
  57. data/lib/redmine_api_helper/wiki_pages_api_helper.rb +66 -0
  58. data/lib/redmine_api_helper.rb +88 -0
  59. data/redmine_api_helper.gemspec +35 -0
  60. metadata +148 -0
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Ruby Gem to create a self populating Open Document Format (.odf) text file.
4
+ #
5
+ # Copyright 2021 Stephan Wenzel <stephan.wenzel@drwpatent.de>
6
+ #
7
+ # This program is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU General Public License
9
+ # as published by the Free Software Foundation; either version 2
10
+ # of the License, or (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
+ #
21
+
22
+ module ODFWriter
23
+
24
+ ########################################################################################
25
+ #
26
+ # TextReader: find all texts and set name
27
+ #
28
+ ########################################################################################
29
+ class TextReader < FieldReader
30
+
31
+ attr_accessor :name
32
+
33
+ ######################################################################################
34
+ #
35
+ # initialize
36
+ #
37
+ ######################################################################################
38
+ def initialize(opts={})
39
+ @name = opts[:name]
40
+ end #def
41
+
42
+ ######################################################################################
43
+ #
44
+ # paths
45
+ #
46
+ ######################################################################################
47
+ def paths( file, doc)
48
+
49
+ # find nodes with matching field elements matching [FIELD] pattern
50
+ nodes = doc.xpath("//text()").select{|node| scan(node).present? }
51
+
52
+ # find path for each field
53
+ paths = nil
54
+ nodes.each do |node|
55
+ leaf = {:texts => scan(node)}
56
+ paths = PathFinder.trail(node, leaf, :root => file, :paths => paths)
57
+ end #each
58
+ paths.to_h
59
+
60
+ end #def
61
+
62
+ ######################################################################################
63
+ # private
64
+ ######################################################################################
65
+
66
+ private
67
+
68
+ def scan(node)
69
+ if name
70
+ node.text.scan(/(?<=#{Regexp.escape Text::DELIMITERS[0]})#{name.upcase}(?=#{Regexp.escape Text::DELIMITERS[1]})/)
71
+ else
72
+ node.text.scan(/(?<=#{Regexp.escape Text::DELIMITERS[0]})[A-Z0-9_]+?(?=#{Regexp.escape Text::DELIMITERS[1]})/)
73
+ end
74
+ end #def
75
+
76
+ end #class
77
+ end #module
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Ruby Gem to create a self populating Open Document Format (.odf) text file.
4
+ #
5
+ # Copyright 2021 Stephan Wenzel <stephan.wenzel@drwpatent.de>
6
+ #
7
+ # This program is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU General Public License
9
+ # as published by the Free Software Foundation; either version 2
10
+ # of the License, or (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
+ #
21
+
22
+ module ODFWriter
23
+ # 1.0.2
24
+ # - added arrify to document.rb
25
+ #
26
+ # 1.0.1 initial commit
27
+ #
28
+ VERSION = "1.0.2"
29
+ end #module
@@ -0,0 +1,333 @@
1
+ ##
2
+ # aids creating fiddles for redmine_scripting_engine
3
+ #
4
+ # Copyright 2021 Stephan Wenzel <stephan.wenzel@drwpatent.de>
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; either version 2
9
+ # of the License, or (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
+ #
20
+
21
+ require 'net/http'
22
+
23
+ module RedmineAPIHelper
24
+ module APIHelper
25
+
26
+ ########################################################################################
27
+ # api functions
28
+ ########################################################################################
29
+ class TooManyRedirects < StandardError; end
30
+ LIMIT_REDIRECTS = 10
31
+ USER_AGENT = ["Redmine API Helper", RedmineAPIHelper::VERSION].join(" ")
32
+
33
+ ########################################################################################
34
+ # returns error
35
+ ########################################################################################
36
+ def error(err)
37
+ OpenStruct.new(:error => err.message, :backtrace => err.backtrace)
38
+ end #def
39
+
40
+ ########################################################################################
41
+ # assembles url from fragments
42
+ ########################################################################################
43
+ def url_path(*fragments, **options)
44
+ [fragments.map do |fragment|
45
+ fragment.to_s.gsub(/\/\z/,"")
46
+ end.join("/"),
47
+ options.to_query.presence
48
+ ].compact.join("?")
49
+ end #def
50
+
51
+ ########################################################################################
52
+ # lists objects, corresponds to controller#index
53
+ ########################################################################################
54
+ def list_objects(objects, params={})
55
+ jget(:url => send("#{objects}_url"), :params => params ).send(objects)
56
+ rescue Exception => err
57
+ error(err)
58
+ end #def
59
+
60
+ def list_project_objects(project_id, objects, params={})
61
+ jget(:url => send("project_#{objects}_url", project_id), :params => params ).send(objects)
62
+ rescue Exception => err
63
+ error(err)
64
+ end #def
65
+
66
+ ########################################################################################
67
+ # reads object having id, corresponds to controller#show
68
+ ########################################################################################
69
+ def read_object(object, id, params={})
70
+ jget(:url => send("#{object}_url", id), :params => params ).send(object)
71
+ rescue Exception => err
72
+ error(err)
73
+ end #def
74
+
75
+ def read_project_object(project_id, object, id, params={})
76
+ jget(:url => send("project_#{object}_url", project_id, id), :params => params ).send(object)
77
+ rescue Exception => err
78
+ error(err)
79
+ end #def
80
+
81
+ ########################################################################################
82
+ # creates a new object with params, corresponds to controller#create
83
+ ########################################################################################
84
+ def create_object(object, params={})
85
+ jpost( {object => params}, :url => send("#{object.to_s.pluralize}_url") ).send(object)
86
+ rescue Exception => err
87
+ error(err)
88
+ end #def
89
+
90
+ def create_project_object(project_id, object, params={})
91
+ jpost( {object => params}, :url => send("project_#{object.to_s.pluralize}_url", project_id) ).send(object)
92
+ rescue Exception => err
93
+ error(err)
94
+ end #def
95
+
96
+ ########################################################################################
97
+ # updates an existing object with params, corresponds to controller#update
98
+ ########################################################################################
99
+ def update_object(object, id, params={})
100
+ jput( {object => params}, :url => send("#{object}_url", id) )
101
+ rescue Exception => err
102
+ error(err)
103
+ end #def
104
+
105
+ def update_project_object(project_id, object, id, params={})
106
+ jput( {object => params}, :url => send("project_#{object}_url", project_id, id) )
107
+ rescue Exception => err
108
+ error(err)
109
+ end #def
110
+
111
+ ########################################################################################
112
+ # deletes an existing object with params, corresponds to controller#destroy
113
+ ########################################################################################
114
+ def destroy_object(object, id, params={})
115
+ jdel(:url => send("#{object}_url", id), :params => params )
116
+ rescue Exception => err
117
+ error(err)
118
+ end #def
119
+
120
+ def destroy_project_object(project_id, object, id, params={})
121
+ jdel(:url => send("project_#{object}_url", project_id, id), :params => params )
122
+ rescue Exception => err
123
+ error(err)
124
+ end #def
125
+
126
+ ########################################################################################
127
+ # fetch(options), get request
128
+ ########################################################################################
129
+ def fetch(options={})
130
+
131
+ # create query parameters
132
+ params = options[:params].to_h.to_query
133
+ url = options[:url]
134
+
135
+ # create GET request
136
+ uri = URI.parse([url, params.presence].compact.join("?"))
137
+ req = Net::HTTP::Get.new(uri.request_uri)
138
+
139
+ # create HTTP handler
140
+ http = Net::HTTP.new(uri.host, uri.port)
141
+ http.use_ssl = uri.scheme.downcase == "https"
142
+
143
+ # get request
144
+ @http_response = http.request(req)
145
+
146
+ case @http_response
147
+
148
+ when Net::HTTPSuccess
149
+ @http_response.body.present? ? @http_response.body : serialize(:response => "OK")
150
+
151
+ when Net::HTTPRedirection
152
+ options[:redirects] = options[:redirects].to_i + 1
153
+ raise TooManyRedirects if options[:redirects] > LIMIT_REDIRECTS
154
+ fetch( options.merge(:url => response['location']) )
155
+
156
+ else
157
+ serialize(:response => @http_response.code)
158
+
159
+ end
160
+
161
+ end #def
162
+
163
+ ########################################################################################
164
+ # jget(options), get request
165
+ ########################################################################################
166
+ def jget(options={})
167
+
168
+ index = options[:index].to_i
169
+ json = options[:json].nil? || !!options[:json]
170
+ params = json ? options[:params].to_h.merge(:format => "json").to_query : options[:params].to_h.to_query
171
+ content_type = json ? "application/json" : options[:content_type]
172
+ api = options[:api_key].nil? ? true : !!options[:api_key]
173
+ url = options[:url].presence || args.objects[index].object_url
174
+
175
+ # create GET request
176
+ uri = URI.parse( [url, params.presence].compact.join("?"))
177
+ req = Net::HTTP::Get.new(uri.request_uri)
178
+ req["Content-Type"] = content_type
179
+ req['X-Redmine-API-Key'] = args.api_key if api
180
+ req["Referer"] = args.deep_try(:eparams, :url) || args.deep_try(:urls, :back)
181
+ req["User-Agent"] = USER_AGENT
182
+ req["Accept"] = "*/*"
183
+ req.basic_auth(args.site_user, args.site_password) if args.site_user.present? || args.site_password.present?
184
+
185
+ # create HTTP handler
186
+ http = Net::HTTP.new(uri.host, uri.port)
187
+ http.use_ssl = uri.scheme.downcase == "https"
188
+
189
+ # get request
190
+ @http_response = http.request(req)
191
+
192
+ # follow redirection or get result code
193
+ handle_response(options)
194
+
195
+ end #def
196
+
197
+ ########################################################################################
198
+ # jput(body, options), put request
199
+ ########################################################################################
200
+ def jput(body, options={})
201
+
202
+ index = options[:index].to_i
203
+ json = options[:json].nil? || !!options[:json]
204
+ params = json ? options[:params].to_h.merge(:format => "json").to_query : options[:params].to_h.to_query
205
+ content_type = json ? "application/json" : options[:content_type]
206
+ api = options[:api_key].nil? ? true : !!options[:api_key]
207
+ url = options[:url].presence || args.objects[index].object_url
208
+
209
+ # create PUT request
210
+ uri = URI.parse( [url, params.presence].compact.join("?"))
211
+ req = Net::HTTP::Put.new(uri.request_uri)
212
+ req["Content-Type"] = content_type
213
+ req['X-Redmine-API-Key'] = args.api_key if api
214
+ req["Referer"] = args.deep_try(:eparams, :url) || args.deep_try(:urls, :back)
215
+ req["User-Agent"] = USER_AGENT
216
+ req["Accept"] = "*/*"
217
+ req.basic_auth(args.site_user, args.site_password) if args.site_user.present? || args.site_password.present?
218
+
219
+ # create body
220
+ req.body = deserialize(body).to_json
221
+
222
+ # create HTTP handler
223
+ http = Net::HTTP.new(uri.host, uri.port)
224
+ http.use_ssl = uri.scheme.downcase == "https"
225
+
226
+ # get request
227
+ @http_response = http.request(req)
228
+
229
+ # follow redirection or get result code
230
+ handle_response(options)
231
+
232
+ end #def
233
+
234
+ ########################################################################################
235
+ # jpost(body, options), post request
236
+ ########################################################################################
237
+ def jpost(body, options={})
238
+
239
+ index = options[:index].to_i
240
+ json = options[:json].nil? || !!options[:json]
241
+ params = json ? options[:params].to_h.merge(:format => "json").to_query : options[:params].to_h.to_query
242
+ content_type = json ? "application/json" : options[:content_type]
243
+ api = options[:api_key].nil? ? true : !!options[:api_key]
244
+ url = options[:url].presence || args.objects[index].object_url
245
+
246
+ # create POST request
247
+ uri = URI.parse( [url, params.presence].compact.join("?"))
248
+ req = Net::HTTP::Post.new(uri.request_uri)
249
+ req["Content-Type"] = content_type
250
+ req['X-Redmine-API-Key'] = args.api_key if api
251
+ req["Referer"] = args.deep_try(:eparams, :url) || args.deep_try(:urls, :back)
252
+ req["User-Agent"] = USER_AGENT
253
+ req["Accept"] = "*/*"
254
+ req.basic_auth(args.site_user, args.site_password) if args.site_user.present? || args.site_password.present?
255
+
256
+ # create body
257
+ req.body = deserialize(body).to_json
258
+
259
+ # create HTTP handler
260
+ http = Net::HTTP.new(uri.host, uri.port)
261
+ http.use_ssl = uri.scheme.downcase == "https"
262
+
263
+ # get request
264
+ @http_response = http.request(req)
265
+
266
+ # follow redirection or get result code
267
+ handle_response(options)
268
+
269
+ end #def
270
+
271
+ ########################################################################################
272
+ # jdel(options), delete request
273
+ ########################################################################################
274
+ def jdel(options={})
275
+
276
+ index = options[:index].to_i
277
+ json = options[:json].nil? || !!options[:json]
278
+ params = json ? options[:params].to_h.merge(:format => "json").to_query : options[:params].to_h.to_query
279
+ content_type = json ? "application/json" : options[:content_type]
280
+ api = options[:api_key].nil? ? true : !!options[:api_key]
281
+ url = options[:url].presence || args.objects[index].object_url
282
+
283
+ # create DELETE request
284
+ uri = URI.parse( [url, params.presence].compact.join("?"))
285
+ req = Net::HTTP::Delete.new(uri.request_uri)
286
+ req["Content-Type"] = content_type
287
+ req['X-Redmine-API-Key'] = args.api_key if api
288
+ req["Referer"] = args.deep_try(:eparams, :url) || args.deep_try(:urls, :back)
289
+ req["User-Agent"] = USER_AGENT
290
+ req["Accept"] = "*/*"
291
+ req.basic_auth(args.site_user, args.site_password) if args.site_user.present? || args.site_password.present?
292
+
293
+ # create HTTP handler
294
+ http = Net::HTTP.new(uri.host, uri.port)
295
+ http.use_ssl = uri.scheme.downcase == "https"
296
+
297
+ # get request
298
+ @http_response = http.request(req)
299
+
300
+ # follow redirection or get result code
301
+ handle_response(options)
302
+
303
+ end #def
304
+
305
+ ########################################################################################
306
+ # private
307
+ ########################################################################################
308
+ private
309
+
310
+ ########################################################################################
311
+ # handle_response
312
+ ########################################################################################
313
+ def handle_response(options)
314
+
315
+ case @http_response
316
+
317
+ when Net::HTTPSuccess
318
+ @http_response.body.present? ? serialize(JSON.parse(@http_response.body)) : serialize(:result => @http_response.code)
319
+
320
+ when Net::HTTPRedirection
321
+ options[:redirects] = options[:redirects].to_i + 1
322
+ raise TooManyRedirects if options[:redirects] > LIMIT_REDIRECTS
323
+ function = caller_locations(1,1)[0].label.to_sym
324
+ send(function, options)
325
+
326
+ else
327
+ @http_response.body.present? ? serialize(JSON.parse(@http_response.body)) : serialize(:result => @http_response.code)
328
+
329
+ end
330
+ end #def
331
+
332
+ end #module
333
+ end #module
@@ -0,0 +1,106 @@
1
+ ##
2
+ # aids creating fiddles for redmine_scripting_engine
3
+ #
4
+ # Copyright x 2021 Stephan Wenzel <stephan.wenzel@drwpatent.de>
5
+ #
6
+ # This program is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; either version 2
9
+ # of the License, or (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, write to the Free Software
18
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
+ #
20
+
21
+ module RedmineAPIHelper
22
+ module ArgsHelper
23
+
24
+ ########################################################################################
25
+ # iterates over current object, set index for functions accessing current object
26
+ ########################################################################################
27
+ def iterate(&block)
28
+ args.objects.map do |object|
29
+ obj = yield object
30
+ @index += 1 unless @index + 1 >= args.objects.length
31
+ obj
32
+ end
33
+ end #def
34
+
35
+ ########################################################################################
36
+ # gets value of field in current object
37
+ ########################################################################################
38
+ def value( *fields )
39
+ args.objects[index].deep_try(*fields)
40
+ end #def
41
+
42
+ ########################################################################################
43
+ # serializes object to OpenStruct
44
+ ########################################################################################
45
+ def serialize(object, **options)
46
+ if object.is_a?(Hash)
47
+ OpenStruct.new(object.map{ |key, val| [ key, serialize(val, options) ] }.to_h)
48
+ elsif object.is_a?(Array)
49
+ object.map{ |obj| serialize(obj, options) }
50
+ else # assumed to be a primitive value
51
+ if options[:parse]
52
+ JSON.parse(object, object_class:OpenStruct) rescue object
53
+ else
54
+ object
55
+ end
56
+ end
57
+ end #def
58
+
59
+ ########################################################################################
60
+ # serializes JSON string to OpenStruct
61
+ ########################################################################################
62
+ def jparse(object)
63
+ serialize(object, :parse => true)
64
+ end #def
65
+
66
+ ########################################################################################
67
+ # deserializes object from OpenStruct
68
+ ########################################################################################
69
+ def deserialize(object)
70
+ if object.is_a?(OpenStruct)
71
+ return deserialize( object.to_h )
72
+ elsif object.is_a?(Hash)
73
+ return object.map{|key, obj| [key, deserialize(obj)]}.to_h
74
+ elsif object.is_a?(Array)
75
+ return object.map{|obj| deserialize(obj)}
76
+ else # assumed to be a primitive value
77
+ return object
78
+ end
79
+ end #def
80
+
81
+ ########################################################################################
82
+ # print pretty arguments passed to ruby script by plugin
83
+ ########################################################################################
84
+ def pretty(a=args)
85
+ JSON.pretty_generate(deserialize(a))
86
+ end #def
87
+
88
+ ########################################################################################
89
+ # print pretty response returned from http request
90
+ ########################################################################################
91
+ def pretty_response(hr=@http_response)
92
+ JSON.pretty_generate({
93
+ :code => hr.try(:code),
94
+ :body => JSON.parse(hr.try(:body).to_s)
95
+ })
96
+ end #def
97
+
98
+ ########################################################################################
99
+ # create html link
100
+ ########################################################################################
101
+ def link_to(body, url)
102
+ "<a href='#{url}'>#{body}</a>"
103
+ end #def
104
+
105
+ end #module
106
+ end #module