redmine_api_helper 0.3.35

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 +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