redmine_api_helper 0.3.24

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