redmine_api_helper 0.3.24

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

Potentially problematic release.


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

Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +11 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +339 -0
  7. data/README.md +30 -0
  8. data/Rakefile +2 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/lib/date_helper/date.rb +311 -0
  12. data/lib/odf_writer/bookmark.rb +110 -0
  13. data/lib/odf_writer/bookmark_reader.rb +77 -0
  14. data/lib/odf_writer/document.rb +372 -0
  15. data/lib/odf_writer/field.rb +174 -0
  16. data/lib/odf_writer/field_reader.rb +78 -0
  17. data/lib/odf_writer/image.rb +158 -0
  18. data/lib/odf_writer/image_reader.rb +76 -0
  19. data/lib/odf_writer/images.rb +89 -0
  20. data/lib/odf_writer/list_style.rb +331 -0
  21. data/lib/odf_writer/nested.rb +156 -0
  22. data/lib/odf_writer/odf_helper.rb +56 -0
  23. data/lib/odf_writer/parser/default.rb +685 -0
  24. data/lib/odf_writer/path_finder.rb +114 -0
  25. data/lib/odf_writer/section.rb +120 -0
  26. data/lib/odf_writer/section_reader.rb +61 -0
  27. data/lib/odf_writer/style.rb +417 -0
  28. data/lib/odf_writer/table.rb +135 -0
  29. data/lib/odf_writer/table_reader.rb +61 -0
  30. data/lib/odf_writer/template.rb +222 -0
  31. data/lib/odf_writer/text.rb +97 -0
  32. data/lib/odf_writer/text_reader.rb +77 -0
  33. data/lib/odf_writer/version.rb +29 -0
  34. data/lib/redmine_api_helper/api_helper.rb +333 -0
  35. data/lib/redmine_api_helper/args_helper.rb +106 -0
  36. data/lib/redmine_api_helper/attachments_api_helper.rb +52 -0
  37. data/lib/redmine_api_helper/define_api_helpers.rb +78 -0
  38. data/lib/redmine_api_helper/document_categories_api_helper.rb +38 -0
  39. data/lib/redmine_api_helper/groups_api_helper.rb +80 -0
  40. data/lib/redmine_api_helper/helpers.rb +50 -0
  41. data/lib/redmine_api_helper/issue_priorities_api_helper.rb +38 -0
  42. data/lib/redmine_api_helper/issue_relations_api_helper.rb +66 -0
  43. data/lib/redmine_api_helper/issue_statuses_api_helper.rb +36 -0
  44. data/lib/redmine_api_helper/issues_api_helper.rb +124 -0
  45. data/lib/redmine_api_helper/my_account_api_helper.rb +45 -0
  46. data/lib/redmine_api_helper/news_api_helper.rb +73 -0
  47. data/lib/redmine_api_helper/project_memberships_api_helper.rb +77 -0
  48. data/lib/redmine_api_helper/projects_api_helper.rb +73 -0
  49. data/lib/redmine_api_helper/roles_api_helper.rb +52 -0
  50. data/lib/redmine_api_helper/scripts_api_helper.rb +87 -0
  51. data/lib/redmine_api_helper/search_api_helper.rb +38 -0
  52. data/lib/redmine_api_helper/time_entries_api_helper.rb +73 -0
  53. data/lib/redmine_api_helper/time_entry_activities_api_helper.rb +38 -0
  54. data/lib/redmine_api_helper/trackers_api_helper.rb +38 -0
  55. data/lib/redmine_api_helper/users_api_helper.rb +73 -0
  56. data/lib/redmine_api_helper/version.rb +24 -0
  57. data/lib/redmine_api_helper/wiki_pages_api_helper.rb +66 -0
  58. data/lib/redmine_api_helper.rb +88 -0
  59. data/redmine_api_helper.gemspec +35 -0
  60. metadata +148 -0
@@ -0,0 +1,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