redmine_api_helper 0.3.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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 +371 -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 +176 -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 +336 -0
  21. data/lib/odf_writer/nested.rb +156 -0
  22. data/lib/odf_writer/odf_helper.rb +57 -0
  23. data/lib/odf_writer/parser/default.rb +691 -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 +483 -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 +234 -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 +36 -0
  60. metadata +162 -0
@@ -0,0 +1,691 @@
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
+ module Parser
24
+
25
+ ######################################################################################
26
+ #
27
+ # Default: html parser and code translator
28
+ #
29
+ ######################################################################################
30
+ class Default
31
+
32
+ attr_accessor :paragraphs
33
+
34
+ ####################################################################################
35
+ #
36
+ # constants
37
+ #
38
+ ####################################################################################
39
+ INLINE = %w( a span strong b em i ins u del strike sub sup code)
40
+ TEXTINLINE = %w(text:a text:span text:strong text:b text:em text:i text:ins text:u text:del text:strike text:sub text:sup text:code)
41
+ #SAFETAGS = %w(h1 h2 h3 h4 h5 h6 p a br div span strong b em i ins u del strike sub sup code li ul ol th td tr tbody thead tfoot table )
42
+ UNSAFETAGS = %w(script style)
43
+
44
+ #
45
+ # table: blank (white) cells, no borders, bold face,
46
+ # thrifty blank (white) cells, no borders, normal face,
47
+ # tiny blank (white) cells, no borders, tiny face,
48
+ # listmedium blank (white) cells, header rows bottom borders, medium face , footer rows top-borders
49
+ #
50
+ # list: blank (white) cells, header rows bottom borders, normal face, , footer rows top-borders
51
+ # boxes: blank (white) cells, all borders, normal face
52
+ # invoice: blank (white) cells, header rows bottom borders, header rows bold face, footer rows top-borders
53
+ # caption: blank (white) cells, bold face
54
+ #
55
+ # tc: column width
56
+ # tcs: first column width and last column width
57
+ #
58
+ TABLECLASSES = %w(table thrifty tiny list listmedium boxes invoice caption)
59
+ TABLESTYLES = {:table => {:thead => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]} },
60
+ :tbody => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]} },
61
+ :tfoot => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]} },
62
+
63
+ :tcs => ["tc", "tc", "tc"]
64
+ },
65
+
66
+ :thrifty => {:thead => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]} },
67
+ :tbody => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]} },
68
+ :tfoot => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["p", "p", "p"]} },
69
+
70
+ :tcs => ["tc", "tc", "tc"]
71
+ },
72
+
73
+ :tiny => {:thead => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["small", "small", "small"], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["small", "small", "small"], :ps => ["p", "p", "p"]} },
74
+ :tbody => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["small", "small", "small"], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["small", "small", "small"], :ps => ["p", "p", "p"]} },
75
+ :tfoot => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["small", "small", "small"], :ps => ["p", "p", "p"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["small", "small", "small"], :ps => ["p", "p", "p"]} },
76
+
77
+ :tcs => ["tc", "tc", "tc"]
78
+ },
79
+
80
+ :list => {:thead => {:tr => "tr", :ths =>{:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["", "", ""], :ps => ["monoright", "mono", "mono"]}, :tds => {:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["", "", ""], :ps => ["right", "p", "p"]} },
81
+ :tbody => {:tr => "tr", :ths =>{:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["", "", ""], :ps => ["monoright", "mono", "mono"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["right", "p", "p"]} },
82
+ :tfoot => {:tr => "tr", :ths =>{:tds => ["tdfoot", "tdfoot", "tdfoot"], :fs => ["", "", ""], :ps => ["monoright", "mono", "mono"]}, :tds => {:tds => ["tdfoot", "tdfoot", "tdfoot"], :fs => ["", "", ""], :ps => ["right", "p", "p"]} },
83
+
84
+ :tcs => ["tcnarrow", "tcfixed", "tcfixed"]
85
+ },
86
+
87
+ :listmedium => {:thead => {:tr => "tr", :ths =>{:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["medium", "medium", "medium"], :ps => ["monoright", "mono", "mono"]}, :tds => {:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["medium", "medium", "medium"], :ps => ["right", "p", "p"]} },
88
+ :tbody => {:tr => "tr", :ths =>{:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["medium", "medium", "medium"], :ps => ["monoright", "mono", "mono"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["medium", "medium", "medium"], :ps => ["right", "p", "p"]} },
89
+ :tfoot => {:tr => "tr", :ths =>{:tds => ["tdfoot", "tdfoot", "tdfoot"], :fs => ["medium", "medium", "medium"], :ps => ["monoright", "mono", "mono"]}, :tds => {:tds => ["tdfoot", "tdfoot", "tdfoot"], :fs => ["medium", "medium", "medium"], :ps => ["right", "p", "p"]} },
90
+
91
+ :tcs => ["tcnarrow", "tcfixed", "tcfixed"]
92
+ },
93
+
94
+ :boxes => {:thead => {:tr => "tr", :ths =>{:tds => ["tdbox", "tdbox", "tdbox"], :fs => ["", "", ""], :ps => ["monocenter", "monocenter", "monocenter"]}, :tds => {:tds => ["tdbox", "tdbox", "tdbox"], :fs => ["", "", ""], :ps => ["center", "center", "center"]} },
95
+ :tbody => {:tr => "tr", :ths =>{:tds => ["tdbox", "tdbox", "tdbox"], :fs => ["", "", ""], :ps => ["monocenter", "monocenter", "monocenter"]}, :tds => {:tds => ["tdbox", "tdbox", "tdbox"], :fs => ["", "", ""], :ps => ["center", "center", "center"]} },
96
+ :tfoot => {:tr => "tr", :ths =>{:tds => ["tdbox", "tdbox", "tdbox"], :fs => ["", "", ""], :ps => ["monocenter", "monocenter", "monocenter"]}, :tds => {:tds => ["tdbox", "tdbox", "tdbox"], :fs => ["", "", ""], :ps => ["center", "center", "center"]} },
97
+
98
+ :tcs => ["tc", "tc", "tc"]
99
+ },
100
+
101
+ :invoice => {:thead => {:tr => "tr", :ths =>{:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["medium", "medium", "medium"], :ps => ["mono", "monoright", "monoright" ]}, :tds => {:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["medium", "medium", "medium"], :ps => ["p", "right", "right" ]} },
102
+ :tbody => {:tr => "tr", :ths =>{:tds => ["tdhead", "tdhead", "tdhead"], :fs => ["medium", "medium", "medium"], :ps => ["mono", "monoright", "monoright" ]}, :tds => {:tds => ["td", "td", "td"], :fs => ["medium", "medium", "medium"], :ps => ["p", "right", "right" ]} },
103
+ :tfoot => {:tr => "tr", :ths =>{:tds => ["tdfoot", "tdfoot", "tdfoot"], :fs => ["medium", "medium", "medium"], :ps => ["mono", "monoright", "monoright" ]}, :tds => {:tds => ["tdfoot", "tdfoot", "tdfoot"], :fs => ["medium", "medium", "medium"], :ps => ["p", "right", "right" ]} },
104
+
105
+ :tcs => ["tcwide", "tcnarrow", "tcnarrow"]
106
+ },
107
+
108
+ :caption => {:thead => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["mono", "mono", "mono"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["mono", "mono", "mono"]} },
109
+ :tbody => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["mono", "mono", "mono"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["mono", "mono", "mono"]} },
110
+ :tfoot => {:tr => "tr", :ths =>{:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["mono", "mono", "mono"]}, :tds => {:tds => ["td", "td", "td"], :fs => ["", "", ""], :ps => ["mono", "mono", "mono"]} },
111
+
112
+ :tcs => ["tcnarrow", "tcwide", "tcwide"]
113
+ }
114
+ }
115
+
116
+ ####################################################################################
117
+ #
118
+ # initialize
119
+ #
120
+ ####################################################################################
121
+ def initialize(text, node, opts={})
122
+ @text = text
123
+ @paragraphs = []
124
+ @template_node = node
125
+ @doc = opts[:doc]
126
+ @remove_classes = opts[:remove_classes]
127
+ @remove_class_prefix = opts[:remove_class_prefix]
128
+ @remove_class_suffix = opts[:remove_class_suffix]
129
+
130
+ @styles = opts[:styles]
131
+
132
+ parse
133
+ end #def
134
+
135
+ ####################################################################################
136
+ #
137
+ # parse
138
+ #
139
+ ####################################################################################
140
+ def parse
141
+
142
+ #xml = @template_node.parse("<html>#{@text}</html>")
143
+ xml = "<html>#{@text}</html>"
144
+ odf = parse_formatting(xml).css("html").inner_html
145
+ @paragraphs << odf
146
+ return
147
+ end #def
148
+
149
+
150
+ ####################################################################################
151
+ #
152
+ # private
153
+ #
154
+ ####################################################################################
155
+ private
156
+
157
+ def parse_formatting(tag, level=0)
158
+
159
+ #
160
+ # strip superfluous control characters
161
+ #
162
+ duptag = tag.dup.to_s.gsub(/\n|\r|\t/, " ")
163
+
164
+ #
165
+ # strip superfluous whitespace
166
+ #
167
+ duptag.gsub!(/ /, " ") while duptag.match(/ /)
168
+
169
+ html = Nokogiri::XML( tag.to_s )
170
+
171
+ #
172
+ # remove unsafe tags
173
+ #
174
+ UNSAFETAGS.each do |ust|
175
+ html.xpath("//*[self::#{ust}]").each do |node|
176
+ node.remove
177
+ end
178
+ end
179
+
180
+ #
181
+ # divisor - just unpack
182
+ #
183
+ html.xpath("//*[self::div]").reverse.each do |node|
184
+ node.replace( node.dup.children )
185
+ end
186
+ #html.xpath("//*[self::div]").each {|node| node.replace(text_node( "p", node)) }
187
+
188
+ #
189
+ # remove requested tags with class
190
+ #
191
+ if @remove_classes.present?
192
+ case @remove_classes
193
+ when Array
194
+ contains = @remove_classes.map{|r| "contains(., '#{r}')"}.join(" or ")
195
+ else
196
+ contains = "contains(., '#{@remove_classes}')"
197
+ end
198
+ #nodes = html.xpath(".//*[@class[contains(., '#{contains}')]]")
199
+ nodes = html.xpath(".//*[@class[#{contains}]]")
200
+ nodes.each { |node| node.remove }
201
+ end
202
+
203
+ #
204
+ # remove requested class prefixes
205
+ #
206
+ if @remove_class_prefix.present?
207
+ case @remove_class_prefix
208
+ when Array
209
+ contains = @remove_class_prefix.map{|r| "contains(., '#{r}')"}.join(" or ")
210
+ else
211
+ contains = "contains(., '#{@remove_class_prefix}')"
212
+ end
213
+ #nodes = html.xpath(".//*[@class[ contains(., '#{@remove_class_prefix}')]]")
214
+ nodes = html.xpath(".//*[@class[#{contains}]]")
215
+ nodes.each do |node|
216
+ css_classes = node.attr("class").split(" ").select{|c| c.present?}
217
+ case @remove_class_prefix
218
+ when Array
219
+ @remove_class_prefix.each do |rcp|
220
+ css_classes.map!{ |css_class| css_class.gsub!(/\A#{rcp}(.*)\z/) { $1 } }
221
+ end
222
+ else
223
+ css_classes.map!{ |css_class| css_class.gsub!(/\A#{@remove_class_prefix}(.*)\z/) { $1 } }
224
+ end
225
+ node.set_attribute("class", css_classes.join(" "))
226
+ end
227
+ end
228
+
229
+ #
230
+ # remove requested class suffixes
231
+ #
232
+ if @remove_class_suffix.present?
233
+ case @remove_class_prefix
234
+ when Array
235
+ contains = @remove_class_suffix.map{|r| "contains(., '#{r}')"}.join(" or ")
236
+ else
237
+ contains = "contains(., '#{@remove_class_suffix}')"
238
+ end
239
+ #nodes = html.xpath(".//*[@class[ contains(., '#{@remove_class_suffix}')]]")
240
+ nodes = html.xpath(".//*[@class[#{contains}]]")
241
+ nodes.each do |node|
242
+ css_classes = node.attr("class").split(" ").select{|c| c.present?}
243
+ case @remove_class_prefix
244
+ when Array
245
+ @remove_class_suffix.each do |rcs|
246
+ css_classes.map!{ |css_class| css_class.gsub!(/\A(.*)#{rcs}\z/) { $1 } }
247
+ end
248
+ else
249
+ css_classes.map!{ |css_class| css_class.gsub!(/\A}(.*)#{@remove_class_suffix}\z/) { $1 } }
250
+ end
251
+ node.set_attribute("class", css_classes.join(" "))
252
+ end
253
+ end
254
+
255
+ #
256
+ # --- html nestable elements -------------------------------------------------------
257
+ #
258
+
259
+ #
260
+ # nested list items
261
+ #
262
+ html.xpath("//*[self::li]").each do |node|
263
+
264
+ li = text_node("list-item")
265
+
266
+ node.xpath("./text()").each do |text|
267
+ text.replace( blank_node("p", "", text) ) if text.text.present?
268
+ text.remove unless text.text.present?
269
+ end
270
+
271
+ node.children.each do |child|
272
+ child = child.replace( text_node("p", "") << child.dup ) if INLINE.include?(child.name)
273
+ child = child.replace( text_node("p", "") << child.dup ) if TEXTINLINE.include?(child.name)
274
+ li << parse_formatting(child, level+1).root
275
+ end
276
+
277
+ node.replace( li )
278
+ end
279
+
280
+ #
281
+ # nested unordered lists
282
+ #
283
+ html.xpath("//*[self::ul]").each do |node|
284
+
285
+ # unpack lists, which should not be encapsulated in a <p> tag,
286
+ # to be compatible with odf. Move them below the paragraph
287
+ if ["text:p", "p"].include? node.parent.name
288
+ node = node.parent.add_next_sibling( node )
289
+ end
290
+
291
+ ul = text_node("list", "ul", node)
292
+ node.replace( parse_formatting(ul, level+1).root )
293
+ end
294
+
295
+ #
296
+ # nested unordered lists
297
+ #
298
+ html.xpath("//*[self::ol]").each do |node|
299
+
300
+ # unpack lists, which should not be encapsulated in a <p> tag,
301
+ # to be compatible with odf. Move them below the paragraph
302
+ if ["text:p", "p"].include? node.parent.name
303
+ node = node.parent.add_next_sibling( node )
304
+ end
305
+
306
+ ol = text_node("list", "ol", node)
307
+ node.replace( parse_formatting(ol, level+1).root )
308
+ end
309
+
310
+ #
311
+ # --- html tables -----------------------------------------------------------------
312
+ #
313
+
314
+ #
315
+ # tables
316
+ #
317
+ html.xpath("//*[self::table]").each do |node|
318
+
319
+ # unpack tables, which should not be encapsulated in a <p> tag,
320
+ # to be compatible with odf. Move them below the paragraph
321
+ if ["text:p", "p"].include? node.parent.name
322
+ node = node.parent.add_next_sibling( node )
323
+ end
324
+
325
+ #
326
+ # use last matching css class for matching a local style
327
+ #
328
+ cssclasses = node["class"].to_s.split(/\ /)
329
+ cssc = (cssclasses & TABLECLASSES).last.presence&.to_sym || :table
330
+
331
+ table = table_node("table", cssc.to_s, node)
332
+ table["table:template-name"]= cssc.to_s.camelcase
333
+ #new_table = node.replace( parse_formatting(table, level+1).root )
334
+ new_table = node.replace( table )
335
+
336
+ max_cols = node.
337
+ xpath(".//*[local-name()='tr']").
338
+ map{|tr| tr.xpath("*[local-name()='td']")}.
339
+ map{|a| a.length}.max
340
+
341
+ max_cols.to_i.times do |col_index|
342
+
343
+ if col_index == 0 # last
344
+ tccss = TABLESTYLES.dig(cssc, :tcs ).to_a[2]
345
+
346
+ elsif col_index == (max_cols - 1) # first
347
+ tccss = TABLESTYLES.dig(cssc, :tcs ).to_a[0]
348
+
349
+ else
350
+ tccss = TABLESTYLES.dig(cssc, :tcs ).to_a[1]
351
+ end
352
+
353
+ tc = table_node("table-column", tccss)
354
+ new_table.children.first&.before( tc ) # inserted in reverse order
355
+ end
356
+
357
+ #---------------------------------------------------------------------------------
358
+ # table row groups thead, tbody, tfoot
359
+ #
360
+
361
+ # if plain table without rowgroups, then add rowgroup
362
+ rowgroups_count = 0
363
+ %i(thead tbody tfoot).each do |rowgroup|
364
+ rowgroups_count += new_table.xpath(".//*[self::#{rowgroup}]").length
365
+ end
366
+ if rowgroups_count == 0
367
+ tbody = Nokogiri::XML::Node.new("tbody", @doc)
368
+ tbody << new_table.xpath(".//*[self::tr]")
369
+ new_table.xpath(".//*[self::tr]").unlink
370
+ new_table << tbody
371
+ end
372
+
373
+ # handle all rowgroups
374
+ %i(thead tbody tfoot).each do |rowgroup|
375
+
376
+ #
377
+ # traverse thead, tbody and tfoot
378
+ #
379
+ new_table.xpath(".//*[self::#{rowgroup}]").each do |row_group_node|
380
+ case rowgroup
381
+ when :thead
382
+ trg = table_node("table-header-rows", "", row_group_node)
383
+ else
384
+ trg = table_node("table-rows", "", row_group_node)
385
+ end
386
+ #new_row_group_node = row_group_node.replace( parse_formatting(trg, level+1).root )
387
+ new_row_group_node = row_group_node.replace( trg )
388
+
389
+ #-----------------------------------------------------------------------------
390
+ # table rows
391
+ #
392
+ new_row_group_node.xpath(".//*[self::tr]").each_with_index do |tr_node, tr_index|
393
+
394
+ trcss = TABLESTYLES.dig(cssc, rowgroup, :tr )
395
+
396
+ tr = table_node("table-row", trcss, tr_node)
397
+ #new_tr_node = tr_node.replace( parse_formatting(tr).root )
398
+ new_tr_node = tr_node.replace( tr )
399
+
400
+ #---------------------------------------------------------------------------
401
+ # table body cells
402
+ #
403
+ new_tr_node.xpath(".//*[self::th or self::td]").each_with_index do |td_node, td_index|
404
+
405
+ if td_index == 0 #first column
406
+ tdcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :tds ).to_a[0]
407
+ pcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :ps ).to_a[0]
408
+ fcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :fs ).to_a[0]
409
+ elsif td_index == (max_cols - 1) #last column
410
+ tdcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :tds ).to_a[2]
411
+ pcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :ps ).to_a[2]
412
+ fcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :fs ).to_a[2]
413
+ else
414
+ tdcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :tds ).to_a[1]
415
+ pcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :ps ).to_a[1]
416
+ fcss = TABLESTYLES.dig(cssc, rowgroup, "#{td_node.name}s".to_sym, :fs ).to_a[1]
417
+ end
418
+
419
+ td = table_node("table-cell", tdcss)
420
+
421
+ # replace all free text in table cell by a p node
422
+ td_node.xpath("./text()").each do |text|
423
+ tx = blank_node( "span", fcss, text)
424
+ text.replace( text_node("p", pcss ) << tx ) if text.text.present?
425
+ text.remove unless text.text.present?
426
+ end
427
+
428
+ # encapsulate all free text in table children in spans
429
+ td_node.children.each do |child|
430
+ child.xpath("./text()").each do |text|
431
+ tx = blank_node( "span", fcss, text)
432
+ text.replace( tx ) if text.text.present? #&& child.name != "span"
433
+ text.remove unless text.text.present? #&& child.name != "span"
434
+ end
435
+ end
436
+
437
+ # replace all inline text in table cell
438
+ td_node.children.each do |child|
439
+ if (INLINE + TEXTINLINE).include?( child.name )
440
+ tx = blank_node( "span", fcss ) << child.dup
441
+ child = child.replace( text_node("p", pcss) << tx.dup )
442
+ end
443
+ td << parse_formatting(child).root
444
+ end
445
+
446
+ td["table:number-columns-spanned"]= td_node['colspan'] if td_node['colspan'].present?
447
+ td["table:number-rows-spanned"] = td_node['rowspan'] if td_node['rowspan'].present?
448
+
449
+ new_td_node = td_node.replace( td )
450
+
451
+ end
452
+ end
453
+ end
454
+ end
455
+ end
456
+
457
+ #
458
+ # --- html elements and entities --------------------------------------------------
459
+ #
460
+
461
+ #
462
+ # newline
463
+ #
464
+ html.xpath("//*[self::br]").each {|node| node.replace(blank_node( "line-break")) }
465
+
466
+ #
467
+ # horizontal ruler
468
+ #
469
+ html.xpath("//*[self::hr]").each {|node| node.replace(blank_node( "p")) }
470
+
471
+
472
+ #
473
+ # --- html block elements ---------------------------------------------------------
474
+ #
475
+
476
+ #
477
+ # headings
478
+ #
479
+ html.xpath("//*[self::h1]").each {|node| node.replace(text_node( "p", node)) }
480
+ html.xpath("//*[self::h2]").each {|node| node.replace(text_node( "p", node)) }
481
+ html.xpath("//*[self::h3]").each {|node| node.replace(text_node( "p", node)) }
482
+ html.xpath("//*[self::h4]").each {|node| node.replace(text_node( "p", node)) }
483
+ html.xpath("//*[self::h5]").each {|node| node.replace(text_node( "p", node)) }
484
+ html.xpath("//*[self::h6]").each {|node| node.replace(text_node( "p", node)) }
485
+
486
+ #
487
+ # paragraph
488
+ #
489
+ html.xpath("//*[self::p]").each {|node| node.replace(text_node( "p", node)) }
490
+
491
+ #
492
+ # pre
493
+ #
494
+ html.xpath("//*[self::pre]").each {|node| node.replace(text_node( "p", node)) }
495
+
496
+ #
497
+ # --- html inline elements ---------------------------------------------------------
498
+ #
499
+
500
+ #
501
+ # bold
502
+ #
503
+ html.xpath("//*[self::strong or self::b]").each {|node| node.replace(text_node( "span", "bold", node)) }
504
+
505
+ #
506
+ # italic
507
+ #
508
+ html.xpath("//*[self::em or self::i]").each {|node| node.replace(text_node( "span", "italic", node)) }
509
+
510
+ #
511
+ # underline
512
+ #
513
+ html.xpath("//*[self::ins or self::u]").each {|node| node.replace(text_node( "span", "underline", node)) }
514
+
515
+ #
516
+ # strikethrough
517
+ #
518
+ html.xpath("//*[self::del or self::strike]").each {|node| node.replace(text_node( "span", "strikethrough", node)) }
519
+
520
+ #
521
+ # superscript and subscript
522
+ #
523
+ html.xpath("//*[self::sup]").each {|node| node.replace(text_node( "span", "sup", node)) }
524
+ html.xpath("//*[self::sub]").each {|node| node.replace(text_node( "span", "sub", node)) }
525
+
526
+ #
527
+ # code
528
+ #
529
+ html.xpath("//*[self::code]").each {|node| node.replace(text_node( "span", "code", node)) }
530
+
531
+ #
532
+ # hyperlink or anchor or anchor with content
533
+ #
534
+ html.xpath("//*[self::a]").each do |node|
535
+
536
+ #
537
+ # self closing a-tag: bookmark
538
+ #
539
+ if node['href'].present?
540
+ cont = text_node("span", "a", node)
541
+ #a = office_node("a") << cont
542
+ a = text_node("a") << cont
543
+ a["xlink:href"]= node['href']
544
+ a["office:target-frame-name"]="_top"
545
+ a["xlink:show"]="replace"
546
+ else
547
+ a = text_node("bookmark")
548
+ a["text:name"]=node['name']
549
+ end
550
+ node.replace(a)
551
+ end
552
+
553
+ html
554
+ end #def
555
+
556
+
557
+ def blank_node( name, node_or_style=nil, node=nil )
558
+ p = text_node( name, node_or_style )
559
+ p.content = node.text if node
560
+ p
561
+ end #def
562
+
563
+ def office_node( name, node_or_style=nil, node=nil )
564
+
565
+ p = Nokogiri::XML::Node.new("office:#{name}", @doc)
566
+
567
+ if node_or_style.nil?
568
+ #nothing
569
+ elsif node_or_style.blank?
570
+ p << node.dup.children if node
571
+ elsif node_or_style.is_a?(String)
572
+ p['text:style-name']=node_or_style
573
+ p << node.dup.children if node
574
+ else
575
+ p['text:style-name']=check_style( node_or_style )
576
+ p << node_or_style.dup.children
577
+ end
578
+ p
579
+
580
+ end #def
581
+
582
+ def text_node( name, node_or_style=nil, node=nil )
583
+
584
+ p = Nokogiri::XML::Node.new("text:#{name}", @doc)
585
+
586
+ if node_or_style.nil?
587
+ #nothing
588
+ elsif node_or_style.blank?
589
+ p << node.dup.children if node
590
+ elsif node_or_style.is_a?(String)
591
+ p['text:style-name']=node_or_style
592
+ p << node.dup.children if node
593
+ else
594
+ p['text:style-name']=check_style( node_or_style )
595
+ p << node_or_style.dup.children
596
+ end
597
+ p
598
+ end #def
599
+
600
+ def table_node( name, node_or_style=nil, node=nil )
601
+
602
+ p = Nokogiri::XML::Node.new("table:#{name}", @doc)
603
+
604
+ if node_or_style.nil?
605
+ #nothing
606
+ elsif node_or_style.blank?
607
+ p << node.dup.children if node
608
+ elsif node_or_style.is_a?(String)
609
+ p['table:style-name']=node_or_style
610
+ p << node.dup.children if node
611
+ else
612
+ p['table:style-name']=check_style( node_or_style )
613
+ p << node_or_style.dup.children
614
+ end
615
+ p
616
+ end #def
617
+
618
+ def check_style(node)
619
+
620
+ style = ""
621
+
622
+ #
623
+ # header or
624
+ #
625
+ if node.name =~ /h(\d)/i
626
+ style = node.name.downcase
627
+
628
+ #
629
+ # quote or
630
+ #
631
+ elsif node.name == "p" && node.parent && node.parent.name == "blockquote"
632
+ style = "quote"
633
+
634
+ #
635
+ # pre
636
+ #
637
+ elsif node.name == "pre"
638
+ style = "pre"
639
+
640
+ #
641
+ # paragraph
642
+ #
643
+ elsif node.name == "p"
644
+ style = "paragraph"
645
+
646
+ end
647
+
648
+ #
649
+ # class overrides header / quote
650
+ #
651
+ if node["class"].present?
652
+
653
+ style = node["class"]
654
+ style = remove_prefixes( @remove_class_prefix, style ) if @remove_class_prefix.present?
655
+ style = remove_suffixes( @remove_class_suffix, style ) if @remove_class_suffix.present?
656
+ end
657
+
658
+ #
659
+ # style overrides class
660
+ #
661
+ case node["style"]
662
+ when /text-align:(\s*)center/
663
+ style = "center"
664
+ when /text-align:(\s*)left/
665
+ style = "left"
666
+ when /text-align:(\s*)right/
667
+ style = "right"
668
+ when /text-align:(\s*)justify/
669
+ style = "justify"
670
+ end
671
+
672
+ style ||= node.name
673
+
674
+ style
675
+ end #def
676
+
677
+ def remove_prefixes( prefix_array, classes_string)
678
+ css_classes = classes_string.split(/\s+/)
679
+ regex_raw = prefix_array.map{ |p| "\\A#{p}(.*?)\\z" }.join("|")
680
+ css_classes.map{ |css_class| (v = css_class.match(/#{regex_raw}/) { $1.to_s + $2.to_s + $3.to_s }; v.present? ? v : css_class) }.join(" ")
681
+ end #def
682
+
683
+ def remove_suffixes( prefix_array, classes_string)
684
+ css_classes = classes_string.split(/\s+/)
685
+ regex_raw = prefix_array.map{ |p| "\\A(.*?)#{p}\\z" }.join("|")
686
+ css_classes.map{ |css_class| (v = css_class.match(/#{regex_raw}/) { $1.to_s + $2.to_s + $3.to_s }; v.present? ? v : css_class) }.join(" ")
687
+ end #def
688
+
689
+ end #class
690
+ end #module
691
+ end #module