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
+ # BookmarkReader: find all bookmarks and set name
27
+ #
28
+ ########################################################################################
29
+ class BookmarkReader
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
+ # get_paths: limit to paths with ancestors 'text '(content.xml) and master-styles (styles.xml)
45
+ #
46
+ ######################################################################################
47
+ def paths( root, doc)
48
+
49
+ # find nodes with matching field elements matching [BOOKMARK] pattern
50
+ nodes = doc.xpath("//*[self::text:bookmark or self::text:bookmark-start]").select{|node| scan(node).present? }
51
+
52
+ # find path for each field
53
+ paths = nil
54
+ nodes.each do |node|
55
+ leaf = {:bookmarks => scan(node)}
56
+ paths = PathFinder.trail(node, leaf, :root => root, :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.attr("text:name") == name.upcase ? [node.attr("text:name")] : []
71
+ else
72
+ [node.attr("text:name")]
73
+ end
74
+ end #def
75
+
76
+ end #class
77
+ end #module
@@ -0,0 +1,372 @@
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
+ # Document: create document from template
27
+ #
28
+ ########################################################################################
29
+ class Document
30
+ include Images
31
+
32
+ ######################################################################################
33
+ #
34
+ # initialize
35
+ #
36
+ ######################################################################################
37
+ def initialize(path = nil, zip_stream: nil, &block)
38
+
39
+ @template = ODFWriter::Template.new(path, :zip_stream => zip_stream)
40
+
41
+ @fields = {}
42
+ @field_readers = {}
43
+
44
+ @texts = {}
45
+ @text_readers = {}
46
+
47
+ @bookmarks = {}
48
+ @bookmark_readers = {}
49
+
50
+ @images = {}
51
+ @image_readers = {}
52
+
53
+ @tables = {}
54
+ @table_readers = {}
55
+
56
+ @sections = {}
57
+ @section_readers = {}
58
+
59
+ @styles = {}
60
+ @list_styles = {}
61
+
62
+ if block_given?
63
+ instance_eval(&block)
64
+ end
65
+
66
+ end #def
67
+
68
+ ######################################################################################
69
+ #
70
+ # add_readers
71
+ #
72
+ ######################################################################################
73
+ def add_readers(files = ODFWriter::Template::CONTENT_FILES.keys)
74
+ files.each do |file|
75
+ add_bookmark_reader(file)
76
+ add_field_reader(file)
77
+ add_text_reader(file)
78
+ add_image_reader(file)
79
+ add_section_reader(file)
80
+ add_table_reader(file)
81
+ end
82
+ end #def
83
+
84
+ ######################################################################################
85
+ #
86
+ # add_field
87
+ #
88
+ ######################################################################################
89
+ def add_field(file, name, opts={}, &block)
90
+ opts.merge!(:name => name)
91
+ @fields[file] ||= []; @fields[file] << Field.new(opts, &block)
92
+ end #def
93
+
94
+ ######################################################################################
95
+ #
96
+ # add_field_reader
97
+ #
98
+ ######################################################################################
99
+ def add_field_reader(file, name=nil)
100
+ @field_readers[file] ||= []; @field_readers[file] << FieldReader.new(:name => name)
101
+ end #def
102
+
103
+ ######################################################################################
104
+ #
105
+ # add_text
106
+ #
107
+ ######################################################################################
108
+ def add_text(file, name, opts={}, &block)
109
+ opts.merge!(:name => name)
110
+ @texts[file] ||= []; @texts[file] << Text.new(opts, &block)
111
+ end #def
112
+
113
+ ######################################################################################
114
+ #
115
+ # add_text_reader
116
+ #
117
+ ######################################################################################
118
+ def add_text_reader(file, name=nil)
119
+ @text_readers[file] ||= []; @text_readers[file] << TextReader.new(:name => name)
120
+ end #def
121
+
122
+ ######################################################################################
123
+ #
124
+ # add_bookmark
125
+ #
126
+ ######################################################################################
127
+ def add_bookmark(file, name, opts={}, &block)
128
+ opts.merge!(:name => name)
129
+ @bookmarks[file] ||= []; @bookmarks[file] << Bookmark.new(opts, &block)
130
+ end #def
131
+
132
+ ######################################################################################
133
+ #
134
+ # add_bookmark_reader
135
+ #
136
+ ######################################################################################
137
+ def add_bookmark_reader(file, name=nil)
138
+ @bookmark_readers[file] ||= []; @bookmark_readers[file] << BookmarkReader.new(:name => name)
139
+ end #def
140
+
141
+ ######################################################################################
142
+ #
143
+ # add_image
144
+ #
145
+ ######################################################################################
146
+ def add_image(file, name, opts={}, &block)
147
+ opts.merge!(:name => name)
148
+ @images[file] ||= []; @images[file] << Image.new(opts, &block)
149
+ end #def
150
+
151
+ ######################################################################################
152
+ #
153
+ # add_image_reader
154
+ #
155
+ ######################################################################################
156
+ def add_image_reader(file, name=nil)
157
+ @image_readers[file] ||= []; @image_readers[file] << ImageReader.new(:name => name)
158
+ end #def
159
+
160
+ ######################################################################################
161
+ #
162
+ # add_table
163
+ #
164
+ ######################################################################################
165
+ def add_table(file, name, collection, opts={})
166
+ opts.merge!(:name => name, :collection => collection)
167
+ table = Table.new(opts)
168
+ @tables[file] ||= []; @tables[file] << table
169
+ yield(table)
170
+ end #def
171
+
172
+ ######################################################################################
173
+ #
174
+ # add_table_reader
175
+ #
176
+ ######################################################################################
177
+ def add_table_reader(file, name=nil)
178
+ @table_readers[file] ||= []; @table_readers[file] << TableReader.new(:name => name)
179
+ end #def
180
+
181
+ ######################################################################################
182
+ #
183
+ # add_section
184
+ #
185
+ ######################################################################################
186
+ def add_section(file, name, collection, opts={})
187
+ opts.merge!(:name => name, :collection => collection)
188
+ section = Section.new(opts)
189
+ @sections[file] ||= []; @sections[file] << section
190
+ yield(section)
191
+ end #def
192
+
193
+ ######################################################################################
194
+ #
195
+ # add_section_reader
196
+ #
197
+ ######################################################################################
198
+ def add_section_reader(file, name=nil)
199
+ @section_readers[file] ||= []; @section_readers[file] << SectionReader.new(:name => name)
200
+ end #def
201
+
202
+ ######################################################################################
203
+ #
204
+ # add_style
205
+ #
206
+ ######################################################################################
207
+ def add_style(root, *styles )
208
+ @styles[root] ||= []; @styles[root] << Style.new( *styles )
209
+ end #def
210
+
211
+ ######################################################################################
212
+ #
213
+ # add_list_style
214
+ #
215
+ ######################################################################################
216
+ def add_list_style(root, *list_styles )
217
+ list_styles.each do |list_style|
218
+ @list_styles[root] ||= []; @list_styles[root] << ListStyle.new( list_style )
219
+ end
220
+ end #def
221
+
222
+ ######################################################################################
223
+ #
224
+ # tree
225
+ #
226
+ ######################################################################################
227
+ def tree
228
+ results = {}
229
+ @template.content do |file, doc|
230
+ #results.deep_merge!( leafs( file, doc)) # requires Rails
231
+ results = deep_merge(results, leafs( file, doc))
232
+ end
233
+ results
234
+ end #def
235
+
236
+ ######################################################################################
237
+ #
238
+ # leafs
239
+ #
240
+ ######################################################################################
241
+ def leafs( file, doc)
242
+ results={}
243
+ # requires Rails
244
+ # results.deep_merge! @bookmark_readers[file].map { |r| r.paths(file, doc) }.inject{|m,n| m.deep_merge(n){|k, v1,v2| v1 + v2}} if @bookmark_readers[file]
245
+ # results.deep_merge! @field_readers[file].map { |r| r.paths(file, doc) }.inject{|m,n| m.deep_merge(n){|k, v1,v2| v1 + v2}} if @field_readers[file]
246
+ # results.deep_merge! @text_readers[file].map { |r| r.paths(file, doc) }.inject{|m,n| m.deep_merge(n){|k, v1,v2| v1 + v2}} if @text_readers[file]
247
+ # results.deep_merge! @image_readers[file].map { |r| r.paths(file, doc) }.inject{|m,n| m.deep_merge(n){|k, v1,v2| v1 + v2}} if @image_readers[file]
248
+
249
+ results = deep_merge( results, @bookmark_readers[file].map { |r| r.paths(file, doc) }.inject{|m,n| deep_merge(m, n)} ) if @bookmark_readers[file]
250
+ results = deep_merge( results, @field_readers[file].map { |r| r.paths(file, doc) }.inject{|m,n| deep_merge(m, n)} ) if @field_readers[file]
251
+ results = deep_merge( results, @text_readers[file].map { |r| r.paths(file, doc) }.inject{|m,n| deep_merge(m, n)} ) if @text_readers[file]
252
+ results = deep_merge( results, @image_readers[file].map { |r| r.paths(file, doc) }.inject{|m,n| deep_merge(m, n)} ) if @image_readers[file]
253
+ results
254
+ end #def
255
+
256
+ ######################################################################################
257
+ #
258
+ # populate
259
+ #
260
+ ######################################################################################
261
+ def populate(object, options={})
262
+
263
+ file = options[:file] || :content
264
+ coll = options[:coll] || []
265
+ tree = options[:tree] || self.tree
266
+ prok = options[:proc]
267
+ list = options[:list]; list = Array(list).compact
268
+
269
+ tree.each do |key, names|
270
+ case key
271
+ when :fields
272
+ names.each do |name|
273
+ add_field(file, name, :value => prok ? prok.call(object, name) : object.try(name.downcase.to_sym))
274
+ end #def
275
+ when :texts
276
+ names.each do |name|
277
+ add_text(file, name, :value => prok ? prok.call(object, name) : object.try(name.downcase.to_sym))
278
+ end #def
279
+ when :bookmarks
280
+ names.each do |name|
281
+ add_bookmark(file, name, :value => prok ? prok.call(object, name) : object.try(name.downcase.to_sym))
282
+ end #def
283
+ when :images
284
+ names.each do |name|
285
+ add_image(file, name, :value => (prok ? prok.call(object, name) : object.try(name.downcase.to_sym)))
286
+ end #def
287
+ when :tables
288
+ names.each do |name, table_tree|
289
+ if list.include?(name)
290
+ add_table(file, name, coll, options){|table| table.populate(table_tree, options)}
291
+ elsif object.respond_to?(name.underscore.to_sym)
292
+ add_table(file, name, arrify(object.send(name.underscore.to_sym)), options){|table| table.populate(table_tree, options)}
293
+ end
294
+ end #def
295
+ when :sections
296
+ names.each do |name, section_tree|
297
+ if list.include?(name)
298
+ add_section(file, name, coll, options){|section| section.populate(section_tree, options)} if list.include?(name)
299
+ elsif object.respond_to?(name.underscore.to_sym)
300
+ add_section(file, name, arrify(object.send(name.underscore.to_sym)), options){|section| section.populate(section_tree, options)}
301
+ end
302
+ end #def
303
+ when :files
304
+ names.each do |file, file_tree|
305
+ populate(object, options.merge(:file => file, :tree => file_tree))
306
+ end
307
+ end #case
308
+ end #each
309
+ end #def
310
+
311
+ ######################################################################################
312
+ #
313
+ # write
314
+ #
315
+ ######################################################################################
316
+ def write(dest = nil)
317
+
318
+ @template.update_content do |template|
319
+
320
+ template.update_files do |file, doc, manifest|
321
+
322
+ @styles[file].to_a.each { |s| s.add_style(doc) }
323
+ @list_styles[file].to_a.each { |s| s.add_list_style(doc) }
324
+
325
+ @sections[file].to_a.each { |s| s.replace!(doc, manifest, template) }
326
+ @tables[file].to_a.each { |t| t.replace!(doc, manifest, template) }
327
+
328
+ @texts[file].to_a.each { |t| t.replace!(doc) }
329
+ @fields[file].to_a.each { |f| f.replace!(doc) }
330
+
331
+ @bookmarks[file].to_a.each { |b| b.replace!(doc) }
332
+ @images[file].to_a.each { |i| i.replace!(doc, manifest, template) }
333
+
334
+ Image.unique_image_names( doc ) if @images.present?
335
+ end
336
+
337
+ end
338
+
339
+ if dest
340
+ ::File.open(dest, "wb") {|f| f.write(@template.data) }
341
+ else
342
+ @template.data
343
+ end
344
+
345
+ end #def
346
+
347
+ ######################################################################################
348
+ #
349
+ # private
350
+ #
351
+ ######################################################################################
352
+ private
353
+
354
+ # deep_merge without using Rails
355
+ def deep_merge(left, right)
356
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : [:undefined, nil, :nil].include?(v2) ? v1 : v1 + v2 }
357
+ left.merge(right, &merger)
358
+ end #def
359
+
360
+ def arrify(obj)
361
+ case obj
362
+ when Array
363
+ obj
364
+ when Hash
365
+ [obj]
366
+ else
367
+ obj.respond_to?(:to_a) ? obj.to_a : [obj]
368
+ end
369
+ end #def
370
+
371
+ end #class
372
+ end #module
@@ -0,0 +1,174 @@
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
+ # Field: replace fields
27
+ #
28
+ ########################################################################################
29
+ class Field
30
+
31
+ ######################################################################################
32
+ #
33
+ # constants
34
+ #
35
+ ######################################################################################
36
+ DELIMITERS = %w([ ])
37
+
38
+ ######################################################################################
39
+ #
40
+ # constants
41
+ #
42
+ ######################################################################################
43
+ attr_accessor :name
44
+
45
+ ######################################################################################
46
+ #
47
+ # initialize
48
+ #
49
+ ######################################################################################
50
+ def initialize(options, &block)
51
+
52
+ @name = options[:name]
53
+ @value = options[:value]
54
+ @field = options[:field]
55
+ @key = @field || @name
56
+ @proc = options[:proc]
57
+
58
+ @remove_classes = options[:remove_classes]
59
+ @remove_class_prefix = options[:remove_class_prefix]
60
+ @remove_class_suffix = options[:remove_class_suffix]
61
+
62
+ @value ||= @proc
63
+
64
+ unless @value
65
+ if block_given?
66
+ @value = block
67
+ else
68
+ @value = lambda { |item, key| field(item, key) }
69
+ end
70
+ end
71
+ end #def
72
+
73
+ ######################################################################################
74
+ #
75
+ # replace!
76
+ #
77
+ ######################################################################################
78
+ def replace!(content, item = nil)
79
+ txt = content.inner_html
80
+ txt.gsub!(placeholder, sanitize(value(item)))
81
+ content.inner_html = txt
82
+ end #def
83
+
84
+ ######################################################################################
85
+ #
86
+ # value
87
+ #
88
+ ######################################################################################
89
+ def value(item = nil)
90
+ @value.is_a?(Proc) ? @value.call(item, @key) : @value
91
+ end #def
92
+
93
+ ######################################################################################
94
+ #
95
+ # field
96
+ #
97
+ ######################################################################################
98
+ def field(item, key)
99
+ case item
100
+ when NilClass
101
+ key
102
+ when Hash
103
+ hash_value(item, key)
104
+ else
105
+ item_field(item, key)
106
+ end
107
+ end #def
108
+
109
+ ######################################################################################
110
+ #
111
+ # private
112
+ #
113
+ ######################################################################################
114
+ private
115
+
116
+ ######################################################################################
117
+ # hash_value
118
+ ######################################################################################
119
+ def hash_value(hash, key)
120
+ hash[key.to_s] || hash[key.to_sym] ||
121
+ hash[key.to_s.underscore] || hash[key.to_s.underscore.to_sym]
122
+ end #def
123
+
124
+ ######################################################################################
125
+ # item_field
126
+ ######################################################################################
127
+ def item_field(item, field)
128
+ item.try(field.to_s.to_sym) ||
129
+ item.try(field.to_s.underscore.to_sym)
130
+ end #def
131
+
132
+ ######################################################################################
133
+ # placeholder
134
+ ######################################################################################
135
+ def placeholder
136
+ "#{DELIMITERS[0]}#{@name.to_s.upcase}#{DELIMITERS[1]}"
137
+ end #def
138
+
139
+ ######################################################################################
140
+ # sanitize
141
+ ######################################################################################
142
+ def sanitize(text)
143
+ # if we get some object, which is not a string, Numeric or the like
144
+ # f.i. a Hash or an Arry or a CollectionProxy or an image then return @key to avoid
145
+ # uggly errors
146
+ return @key.to_s if text.respond_to?(:each)
147
+ text = html_escape(text)
148
+ text = odf_linebreak(text)
149
+ text
150
+ end #def
151
+
152
+ HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
153
+
154
+ def html_escape(s)
155
+ return "" unless s
156
+ s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
157
+ end #def
158
+
159
+ def odf_linebreak(s)
160
+ return "" unless s
161
+ s = s.encode(universal_newline: true)
162
+ s.to_s.gsub("\n", "<text:line-break/>").gsub("<br.*?>", "<text:line-break/>")
163
+ end #def
164
+
165
+ def deep_fields(fs)
166
+ fs.split(/\./)
167
+ end #def
168
+
169
+ def deep_try(item, f)
170
+ deep_fields(f).inject(item) {|obj,f| obj.try(f.to_s.underscore.to_sym)}
171
+ end #def
172
+
173
+ end #class
174
+ end #module