xhtml_report_generator 3.1.2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 71becd9cbcd9bbc0470db37fbbe6c57c78983eb6
4
- data.tar.gz: 2771bacf4522614f803f186280723810e2b9a224
3
+ metadata.gz: ca9e62e4f9ddf0bbc7c52be1956bffe5ccd46b62
4
+ data.tar.gz: ebe93934511f888827cf8a83a3c8ecb80449327b
5
5
  SHA512:
6
- metadata.gz: ae44b1c93a03773a5e76b3cceee8a6498a8ffb51ed26d6a64eff77d641cb312711fc80827e30d56c2764a4d643d9ca3c400ec268b37c9ed4c6c853d2474f3812
7
- data.tar.gz: f47caa45144a60e750d7cff960051f92f5f18f97e4234982ac8ea9c6a035e751f5ff0e38c9dec0776a180750fcf3881f2b4377dec0eee540fdb256b284194c09
6
+ metadata.gz: b8ee85b1bdc6677051f69288b8530d8c43d55dd93e41970ae7e3748f063efd647287952c5ece3387983a99aa88f472455cc6c98bce3f214ae0254ffbaf5f3f54
7
+ data.tar.gz: b2750549a9a8e5d44ca6e4cdceddfb979e28dcc71d5c0cd66bdcc7882741cabd263f8ea1209dc225841bf97763428e91e8d1010990d85c7b7bf7c91d310eb3d9
data/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  xhtml_report_generator
2
2
  ======================
3
3
 
4
- This project was written to provide an easy way to create valid xhtml or html documents.
5
- My main usecases is the automatic creation of (test-)reports that are human readable and include a table of contents.
4
+ This project was written to provide an easy way to create valid xhtml or html5 documents.
5
+ The main use cases is the automatic creation of (test-)reports that are human readable and include a table of contents.
6
6
  xhtml_report_generator can be used very similar like a ruby Logger, but there are some caveats.
7
7
  It is not a Logger replacement, since the complete document is always kept in memory and
8
8
  only written to disk on demand. Hence in case of crashes the data might be lost if it wasn't written before.
9
+ There is a "sync" option but it has a performance penalty if you need to generate a lot of content.
9
10
 
10
- All logic (js and css) is inlined which makes it very easy to send the report to someone else by mail and view it offline.
11
+ All logic (js and css) is inlined which makes it very easy to send the report by mail and view it offline.
11
12
  Also pdf export is easy by just printing the report. By default there is a special css with media print making the layout suitable for printing.
12
13
 
13
14
  Ruby version
14
15
  -----
15
- This gem was mainly tested with ruby version 2.2.3. Except of the test_encoding_issues unit tests, all other tests are
16
+ This gem was mainly tested with ruby versions >=2.2. Except of the test_encoding_issues unit tests, all other tests are
16
17
  also passing with 1.9.3.
17
18
 
18
19
 
@@ -43,14 +44,11 @@ gen1.write("myreport.xhtml")
43
44
 
44
45
  [Preview](https://cdn.rawgit.com/lumean/xhtml-report-generator/master/examples/basic_report.html)
45
46
 
47
+ More examples can be found in the [examples](../master/examples) or [test](../master/test) folders
46
48
 
47
- More examples can be found in the [examples](../master/examples) or
48
- [test](../master/test) folders
49
-
50
-
51
- By default "custom.rb" is loaded through instance eval, see
52
- [XhtmlReportGenerator/Custom](http://www.rubydoc.info/gems/xhtml_report_generator/Custom) and
53
- [XhtmlReportGenerator/Generator](http://www.rubydoc.info/gems/xhtml_report_generator/XhtmlReportGenerator/Generator)
49
+ Documentation
50
+ -----
51
+ See [XhtmlReportGenerator/Generator](http://www.rubydoc.info/gems/xhtml_report_generator/XhtmlReportGenerator/Generator)
54
52
  for the documentation of available methods.
55
53
 
56
54
  Advanced example1: custom tables including pictures or links
@@ -119,9 +117,10 @@ gen1.write("path/to/CustomTable.xhtml")
119
117
  [Preview](https://cdn.rawgit.com/lumean/xhtml-report-generator/master/test/CustomTableReference.xhtml)
120
118
 
121
119
 
122
- Advanced example2: including some graphs to your reports
120
+ Advanced example2: including some graphs/charts to your reports
123
121
  ----------------------------------
124
- Due to the xml nature it is also easy to insert SVG graphs / pictures. Check out the svg-graph gem
122
+ Due to the xml nature it is also easy to insert SVG graphs / pictures. Check out the svg-graph gem,
123
+ or you can even natively include a c3.js graph
125
124
 
126
125
  ```ruby
127
126
  require 'xhtml_report_generator'
@@ -164,10 +163,10 @@ gen1.write("graph.xhtml")
164
163
 
165
164
  Customizing the Report with CSS
166
165
  -------------------------------
167
- The styling of the report is done through css. This allowes you to customize most of the formatting as to your liking.
166
+ The styling of the report is done through css. This allows you to customize most of the formatting as to your liking.
168
167
  The split.js relevant section should only be changed if you know what you're doing, otherwise the layout might break.
169
168
 
170
- As a starting point begin with the [default css used by the report](../master/lib/xhtml_report_generator/style_template.css)
169
+ As a starting point begin with the [default css used by the report](../master/resource/css/style.css)
171
170
  ```ruby
172
171
  require 'xhtml_report_generator'
173
172
 
@@ -185,12 +184,20 @@ gen1.create_layout("Page Title")
185
184
  [Preview](https://cdn.rawgit.com/lumean/xhtml-report-generator/master/examples/custom_css.html)
186
185
 
187
186
  The project is built in a way that lets you supply your own methods for everything. By default the methods , js and css files provided
188
- with the gem are used, but you can override those by specifying your own. The primary usecase is to override the default css
189
- to customize the look and feel of the generated html files. But if you want you can event write your complete own generator.
187
+ with the gem are used, but you can override those by specifying your own. The primary use case is to override the default css
188
+ to customize the look and feel of the generated html files. But if you want you can even write your own generator.
189
+ Have a look at [custom_reporter.rb](../master/lib/test/custom_reporter.rb).
190
+
191
+ Changes from version 3.x to 4.x
192
+ -------------------------------
193
+ If you just use the default values for initialize (i.e. no options/using defaults) then the upgrade should be seamless.
190
194
 
191
- As a start you can copy the [custom.rb](../master/lib/xhtml_report_generator/custom.rb) file and rename the functions if you don't like the
192
- default naming.
195
+ The option :custom_rb was removed and behavior for the initialize method "XhtmlReportGenerator::Generator.new" changed.
196
+ You should extend your own subclass from XhtmlReportGenerator::Generator to do any customization.
197
+ The js, css and css_print files given for initialize are now included after the default files. Previously if you'd
198
+ specify any of those files, only your files would have been included in the head section.
193
199
 
200
+ For a complete list of changes see [changelog.txt](../master/changelog.txt)
194
201
 
195
202
  Changes from version 2.x to 3.x
196
203
  -------------------------------
@@ -1,81 +1,103 @@
1
1
  # encoding: utf-8
2
2
  require 'rexml/document'
3
3
  require 'rexml/formatters/transitive'
4
+ require 'base64'
4
5
 
5
6
  module XhtmlReportGenerator
6
-
7
+
8
+ VERSION = '4.0.0'
9
+
7
10
  # This is the main generator class. It can be instanced with custom javascript, css, and ruby files to allow
8
11
  # generation of arbitrary reports.
12
+ # @attr [REXML::Document] document This is the html document / actual report
13
+ # @attr [String] file path to the file where this report is saved to. Default: nil
14
+ # @attr [Boolean] sync if true, the report will be written to disk after every modificaiton. Default: false
9
15
  class Generator
10
- attr_accessor :document, :file
16
+ attr_accessor :document, :file, :sync
11
17
  # @param opts [Hash] See the example for an explanation of the valid symbols
12
- # @example Valid symbols for the opts Hash
13
- # :js if specified, array of javascript files which are inlined into the html header section
14
- # :css if specified, array of css files which are inlined into the html header section
18
+ # @example Valid keys for the opts Hash
19
+ # :title Title in the header section, defaults to "Title"
20
+ # :js if specified, array of javascript files which are inlined into the html header section, after
21
+ # the default included js files (check in sourcecode below).
22
+ # :css if specified, array of css files which are inlined into the html header section after
23
+ # the default included css files (check in sourcecode below).
15
24
  # :css_print if specified, array of css files which are inlined into the html header section with media=print
16
- # :custom_rb if specified, path to a custom Module containing all the logic to create content for the report
17
- # see (custom.rb) on how to write it. As a last statement you should extend your module name
18
- # outside of the module definition.
25
+ # after the default included print css files (check in sourcecode below).
19
26
  def initialize(opts = {})
20
27
  # define the default values
21
- path = File.expand_path("../xhtml_report_generator", __FILE__)
28
+ resources = File.expand_path("../../resource/", __FILE__)
22
29
  defaults = {
30
+ :title => "Title",
23
31
  :js => [
24
- File.expand_path("jquery.min.js",path),
25
- File.expand_path("split.min.js",path),
26
- File.expand_path("toc.min.js",path)
32
+ File.expand_path("js/jquery-3.2.1.min.js", resources),
33
+ File.expand_path("d3v3.5.17/d3.min.js", resources),
34
+ File.expand_path("c3v0.4.18/c3.min.js", resources),
35
+ File.expand_path("js/split.min.js", resources),
36
+ File.expand_path("js/layout_split.js", resources),
37
+ File.expand_path("js/table_of_contents.js", resources),
38
+ File.expand_path("js/toggle_linewrap.js", resources),
27
39
  ],
28
40
  :css => [
29
- File.expand_path("style_template.css",path)
41
+ File.expand_path("css/style.css", resources),
42
+ File.expand_path("c3v0.4.18/c3.min.css", resources),
30
43
  ],
31
44
  :css_print => [
32
- File.expand_path("print_template.css",path)
45
+ File.expand_path("css/print.css", resources)
33
46
  ],
34
- :custom_rb => File.expand_path("custom.rb",path)
47
+ #:custom_rb => File.expand_path("../custom.rb", __FILE__),
35
48
  }
36
-
37
- opts[:js] = defaults[:js] if !opts.has_key?(:js)
38
- opts[:css] = defaults[:css] if !opts.has_key?(:css)
39
- opts[:css_print] = defaults[:css_print] if !opts.has_key?(:css_print)
40
- opts[:custom_rb] = defaults[:custom_rb] if !opts.has_key?(:custom_rb)
41
-
42
- # load the custom module and extend it, use instance_eval otherwise the module will affect
43
- # all existing Generator classes
44
- instance_eval(File.read(opts[:custom_rb]), opts[:custom_rb])
45
-
46
- @document = Generator.create_xhtml_document("Title")
49
+ @sync = false
50
+
51
+ opts[:title] = defaults[:title] if !opts.key?(:title)
52
+
53
+ if opts.key?(:js)
54
+ opts[:js] = defaults[:js] + opts[:js]
55
+ else
56
+ opts[:js] = defaults[:js]
57
+ end
58
+ if opts.key?(:css)
59
+ opts[:css] = defaults[:css] + opts[:css]
60
+ else
61
+ opts[:css] = defaults[:css]
62
+ end
63
+ if opts.key?(:css_print)
64
+ opts[:css_print] = defaults[:css_print] + opts[:css_print]
65
+ else
66
+ opts[:css_print] = defaults[:css_print]
67
+ end
68
+
69
+ @document = Generator.create_xhtml_document(opts[:title])
47
70
  head = @document.elements["//head"]
48
-
71
+
49
72
  head.add_element("meta", {"charset" => "utf-8"})
50
-
73
+
51
74
  # insert css
52
75
  opts[:css].each do |css_path|
53
76
  style = head.add_element("style", {"type" => "text/css"})
54
77
  cdata(File.read(css_path), style)
55
78
  end
56
-
79
+
57
80
  # insert css for printing
58
81
  opts[:css_print].each do |css_path|
59
82
  style = head.add_element("style", {"type" => "text/css", "media"=>"print"})
60
83
  cdata(File.read(css_path), style)
61
84
  end
62
-
85
+
63
86
  # inster js files
64
87
  opts[:js].each do |js_path|
65
88
  script = head.add_element("script", {"type" => "text/javascript"})
66
89
  cdata(File.read(js_path), script)
67
90
  end
68
-
91
+ document_changed()
69
92
  end
70
-
71
- # Surrounds CData tag with c-style comments to remain compatible with normal html.
93
+
94
+ # Surrounds CData tag with c-style comments to remain compatible with normal html.
72
95
  # For plain xhtml documents this is not needed.
73
96
  # Example /*<![CDATA[*/\n ...content ... \n/*]]>*/
74
97
  # @param str [String] the string to be enclosed in cdata
75
98
  # @param parent_element [REXML::Element] the element to which cdata should be added
76
99
  # @return [String] CDATA enclosed in c-style comments /**/
77
100
  def cdata(str, parent_element)
78
- f = REXML::Formatters::Transitive.new(0) # use Transitive to preserve source formatting
79
101
  # somehow there is a problem with CDATA, any text added after will automatically go into the CDATA
80
102
  # so we have do add a dummy node after the CDATA and then add the text.
81
103
  parent_element.add_text("/*")
@@ -83,14 +105,15 @@ module XhtmlReportGenerator
83
105
  parent_element.add(REXML::Comment.new("dummy comment to make c-style comments for cdata work"))
84
106
  parent_element.add_text("*/")
85
107
  end
86
-
87
- # Check if the give string is a valid UTF-8 byte sequence. If it is not valid UTF-8, then
108
+
109
+ # Check if the give string is a valid UTF-8 byte sequence. If it is not valid UTF-8, then
88
110
  # all invalid bytes are replaced by "\u2e2e" (\xe2\xb8\xae) ('REVERSED QUESTION MARK') because the default
89
- # replacement character "\uFFFD" ('QUESTION MARK IN DIAMOND BOX') is two slots wide and might
111
+ # replacement character "\uFFFD" ('QUESTION MARK IN DIAMOND BOX') is two slots wide and might
90
112
  # destroy mono spaced formatting
91
113
  # @param str [String] of any encoding
92
114
  # @return [String] UTF-8 encoded valid string
93
115
  def encoding_fixer(str)
116
+ str = str.to_s # catch str = nil
94
117
  #if !str.force_encoding('UTF-8').valid_encoding?
95
118
  # str.encode!('UTF-8', 'ISO-8859-1', {:invalid => :replace, :undef => :replace, :xml => :text})
96
119
  #end
@@ -120,7 +143,7 @@ module XhtmlReportGenerator
120
143
  end
121
144
 
122
145
  # returns the string representation of the xml document
123
- # @param indent [Number] indent for child elements. defaults to 0.
146
+ # @param indent [Number] indent for child elements. defaults to 0.
124
147
  # Note: if you change the indet this might destroy formatting of <pre> sections
125
148
  # @return [String] formatted xml document
126
149
  def to_s(indent = 0)
@@ -130,26 +153,652 @@ module XhtmlReportGenerator
130
153
  # for compatibility with 1.9.3
131
154
  # @document.write({:output=>output, :indent=>indent, :transitive=>true})
132
155
  # change to Formatters since document.write is deprecated
133
- f = REXML::Formatters::Transitive.new(indent)
156
+ f = REXML::Formatters::Transitive.new(indent)
134
157
  f.write(@document, output)
135
158
  return output
136
159
  end
137
-
160
+
138
161
  # Saves the xml document to a file. If no file is given, the file which was used most recently for this Generator
139
162
  # object will be overwritten.
140
163
  # @param file [String] absolute or relative path to the file to which will be written. Default: last file used.
141
164
  # @param mode [String] defaults to 'w', one of the file open modes that allows writing ['r+','w','w+','a','a+']
142
165
  def write(file=@file, mode='w')
143
166
  # instance variables are nil if they were never initialized
144
- if file == nil
145
- raise "no valid file given"
167
+ if file.nil?
168
+ raise "no valid file given: '#{file}'"
146
169
  end
147
170
  @file = file
148
171
  File.open(file, "#{mode}:UTF-8") {|f| f.write(self.to_s.force_encoding(Encoding::UTF_8))}
149
172
  end
150
-
151
- end
152
- end
153
173
 
174
+ # This method should be called after every change to the document.
175
+ # Here we ensure the report is written to disk after each change
176
+ # if #sync is true. If #sync is false this method does nothing
177
+ def document_changed()
178
+ if @sync
179
+ if @file.nil?
180
+ raise "You must call #write at least once before you can enable synced mode"
181
+ end
182
+ write()
183
+ end
184
+ end
185
+
186
+ # creates the basic page layout and sets the current Element to the main content area (middle div)
187
+ # @example The middle div is matched by the following xPath
188
+ # //body/div[@id='middle']
189
+ # @param title [String] the title of the document
190
+ # @param layout [Fixnum] one of 0,1,2,3 where 0 means minimal layout without left and right table of contents,
191
+ # 1 means only left toc, 2 means only right toc, and 3 means full layout with left and right toc.
192
+ def create_layout(title, layout=3)
193
+ raise "invalid layout selector, choose from 0..3" if (layout < 0) || (layout > 3)
194
+
195
+ @body = @document.elements["//body"]
196
+ # only add the layout if it is not already there
197
+ if !@layout
198
+ head = @body.add_element("div", {"class" => "head", "id" => "head"})
199
+ head.add_element("button", {"id" => "pre_toggle_linewrap"}).add_text("Toggle Linewrap")
200
+
201
+ if (layout & 0x1) != 0
202
+ div = @body.add_element("div", {"class" => "lefttoc split split-horizontal", "id" => "ltoc"})
203
+ div.add_text("Table of Contents")
204
+ div.add_element("br")
205
+ end
206
+
207
+ @div_middle = @body.add_element("div", {"class" => "middle split split-horizontal", "id" => "middle"})
208
+
209
+ if (layout & 0x2) != 0
210
+ div = @body.add_element("div", {"class" => "righttoc split split-horizontal", "id" => "rtoc"})
211
+ div.add_text("Quick Links")
212
+ div.add_element("br");div.add_element("br")
213
+ end
214
+
215
+ @body.add_element("p", {"class" => "#{layout}", "id" => "layout"}).add_text("this text should be hidden")
216
+
217
+ @layout = true
218
+ end
219
+ @current = @document.elements["//body/div[@id='middle']"]
220
+ set_title(title)
221
+ document_changed()
222
+ end
223
+
224
+ # sets the title of the document in the <head> section as well as in the layout header div
225
+ # create_layout must be called before!
226
+ # @param title [String] the text which will be insertead
227
+ def set_title(title)
228
+ if !@layout
229
+ raise "call create_layout first"
230
+ end
231
+ pagetitle = @document.elements["//head/title"]
232
+ pagetitle.text = title
233
+ div = @document.elements["//body/div[@id='head']"]
234
+ div.text = title
235
+ document_changed()
236
+ end
237
+
238
+ # returns the title text of the report
239
+ # @return [String] The title of the report
240
+ def get_title()
241
+ pagetitle = @document.elements["//head/title"]
242
+ return pagetitle.text
243
+ end
244
+
245
+ # set the current element to the element or first element matched by the xpath expression.
246
+ # The current element is the one which can be modified through highlighting.
247
+ # @param xpath [REXML::Element|String] the element or an xpath string
248
+ def set_current!(xpath)
249
+ if xpath.is_a?(REXML::Element)
250
+ @current = xpath
251
+ elsif xpath.is_a?(String)
252
+ @current = @document.elements[xpath]
253
+ else
254
+ raise "xpath is neither a String nor a REXML::Element"
255
+ end
256
+ end
257
+
258
+ # returns the current xml element
259
+ # @return [REXML::Element] the xml element after which the following elements will be added
260
+ def get_current()
261
+ return @current
262
+ end
263
+
264
+ # returns the plain text without any xml tags of the specified element and all its children
265
+ # @param el [REXML::Element] The element from which to fetch the text children. Defaults to @current
266
+ # @param recursive [Boolean] whether or not to recurse into the children of the given "el"
267
+ # @return [String] text contents of xml node
268
+ def get_element_text(el = @current, recursive = true)
269
+ out = ""
270
+ el.to_a.each { |child|
271
+ if child.is_a?(REXML::Text)
272
+ out << child.value()
273
+ else
274
+ if recursive
275
+ out << get_element_text(child, true)
276
+ end
277
+ end
278
+ }
279
+ return out
280
+ end
281
+
282
+ # @param elem [REXML::Element]
283
+ # @return [String]
284
+ def element_to_string(elem)
285
+ f = REXML::Formatters::Transitive.new(0) # use Transitive to preserve source formatting (e.g. <pre> tags)
286
+ out = ""
287
+ f.write(elem, out)
288
+ return out
289
+ end
290
+
291
+ # @see #code
292
+ # Instead of adding content to the report, this method returns the produced html code as a string.
293
+ # This can be used to insert code into #custom_table (with the option data_is_xhtml: true)
294
+ # @return [String] the code including <pre> tags as a string
295
+ def get_code_html(attrs={}, &block)
296
+ temp = REXML::Element.new("pre")
297
+ temp.add_attributes(attrs)
298
+ raise "Block argument is mandatory" unless block_given?
299
+ text = encoding_fixer(block.call())
300
+ temp.add_text(text)
301
+ element_to_string(temp)
302
+ end
303
+
304
+ # Appends a <pre> node after the @current node
305
+ # @param attrs [Hash] attributes for the <pre> element. The following classes can be passed as attributes and are predefined with a different
306
+ # background for your convenience !{"class" => "code0"} (light-blue), !{"class" => "code1"} (red-brown),
307
+ # !{"class" => "code2"} (light-green), !{"class" => "code3"} (light-yellow). You may also specify your own background
308
+ # as follows: !{"style" => "background: #FF00FF;"}.
309
+ # @yieldreturn [String] the text to be added to the <pre> element
310
+ # @return [REXML::Element] the Element which was just added
311
+ def code(attrs={}, &block)
312
+ temp = REXML::Element.new("pre")
313
+ temp.add_attributes(attrs)
314
+ @div_middle.insert_after(@current, temp)
315
+ @current = temp
316
+ raise "Block argument is mandatory" unless block_given?
317
+ text = encoding_fixer(block.call())
318
+ @current.add_text(text)
319
+ document_changed()
320
+ return @current
321
+ end
322
+
323
+ # Appends a <script> node after the @current node
324
+ # @param attrs [Hash] attributes for the <script> element. The following attribute is added by default:
325
+ # type="text/javascript"
326
+ # @yieldreturn [String] the actual javascript code to be added to the <script> element
327
+ # @return [REXML::Element] the Element which was just added
328
+ def javascript(attrs={}, &block)
329
+ default_attrs = {"type" => "text/javascript"}
330
+ attrs = default_attrs.merge(attrs)
331
+ temp = REXML::Element.new("script")
332
+ temp.add_attributes(attrs)
333
+ @div_middle.insert_after(@current, temp)
334
+ @current = temp
335
+ raise "Block argument is mandatory" unless block_given?
336
+ script_content = encoding_fixer(block.call())
337
+ cdata(script_content, @current)
338
+ document_changed()
339
+ return @current
340
+ end
341
+
342
+ # @see #content
343
+ # Instead of adding content to the report, this method returns the produced html code as a string.
344
+ # This can be used to insert code into #custom_table (with the option data_is_xhtml: true)
345
+ # @return [String] the code including <pre> tags as a string
346
+ def get_content_html(attrs={}, &block)
347
+ temp = REXML::Element.new("p")
348
+ temp.add_attributes(attrs)
349
+ raise "Block argument is mandatory" unless block_given?
350
+ text = encoding_fixer(block.call())
351
+ temp.add_text(text)
352
+ element_to_string(temp)
353
+ end
354
+
355
+ # Appends a <p> node after the @current node
356
+ # @param attrs [Hash] attributes for the <p> element
357
+ # @yieldreturn [String] the text to be added to the <p> element
358
+ # @return [REXML::Element] the Element which was just added
359
+ def content(attrs={}, &block)
360
+ temp = REXML::Element.new("p")
361
+ temp.add_attributes(attrs)
362
+ @div_middle.insert_after(@current, temp)
363
+ @current = temp
364
+ raise "Block argument is mandatory" unless block_given?
365
+ text = encoding_fixer(block.call())
366
+ @current.add_text(text)
367
+ document_changed()
368
+ return @current
369
+ end
370
+
371
+ # insert arbitrary xml code after the @current element in the content pane (div middle)
372
+ # @param text [String] valid xhtml code which is included into the document
373
+ # @return [REXML::Element] the Element which was just added
374
+ def html(text)
375
+ # we need to create a new document with a pseudo root becaus having multiple nodes at top
376
+ # level is not valid xml
377
+ doc = REXML::Document.new("<root>"+text.to_s+"</root>")
378
+ # then we move all children of root to the actual div middle element and insert after current
379
+ for i in doc.root.to_a do
380
+ @div_middle.insert_after(@current, i)
381
+ @current = i
382
+ end
383
+ document_changed()
384
+ return @current
385
+ end
386
+
387
+ # @see #link
388
+ # Instead of adding content to the report, this method returns the produced html code as a string.
389
+ # This can be used to insert code into #custom_table (with the option data_is_xhtml: true)
390
+ # @return [String] the code including <a> tags as a string
391
+ def get_link_html(href, attrs={}, &block)
392
+ temp = REXML::Element.new("a")
393
+ attrs.merge!({"href" => href})
394
+ temp.add_attributes(attrs)
395
+ raise "Block argument is mandatory" unless block_given?
396
+ text = encoding_fixer(block.call())
397
+ temp.add_text(text)
398
+ element_to_string(temp)
399
+ end
400
+
401
+ # Appends a <a href = > node after the @current nodes
402
+ # @param href [String] this is the
403
+ # @param attrs [Hash] attributes for the <a> element
404
+ # @yieldreturn [String] the text to be added to the <a> element
405
+ # @return [REXML::Element] the Element which was just added
406
+ def link(href, attrs={}, &block)
407
+ temp = REXML::Element.new("a")
408
+ attrs.merge!({"href" => href})
409
+ temp.add_attributes(attrs)
410
+ @div_middle.insert_after(@current, temp)
411
+ @current = temp
412
+ raise "Block argument is mandatory" unless block_given?
413
+ text = encoding_fixer(block.call())
414
+ @current.add_text(text)
415
+ document_changed()
416
+ return @current
417
+ end
418
+
419
+ # @see #image
420
+ # Instead of adding content to the report, this method returns the produced html code as a string.
421
+ # This can be used to insert code into #custom_table (with the option data_is_xhtml: true)
422
+ # @return [String] the code including <pre> tags as a string
423
+ def get_image_html(path, attributes = {})
424
+ # read image as binary and do a base64 encoding
425
+ binary_data = Base64.strict_encode64(IO.binread(path))
426
+ type = File.extname(path).gsub('.', '')
427
+ # create the element
428
+ temp = REXML::Element.new("img")
429
+ # add the picture
430
+ temp.add_attribute("src","data:image/#{type};base64,#{binary_data}")
431
+ temp.add_attributes(attributes)
432
+ element_to_string(temp)
433
+ end
434
+
435
+ # @param path [String] absolute or relative path to the image that should be inserted into the report
436
+ # @param attributes [Hash] attributes for the <img> element, any valid html attributes can be specified
437
+ # you may specify attributes such "alt", "height", "width"
438
+ # @option attrs [String] "class" by default every heading is added to the left table of contents (toc)
439
+ def image(path, attributes = {})
440
+ # read image as binary and do a base64 encoding
441
+ binary_data = Base64.strict_encode64(IO.binread(path))
442
+ type = File.extname(path).gsub('.', '')
443
+ # create the element
444
+ temp = REXML::Element.new("img")
445
+ # add the picture
446
+ temp.add_attribute("src","data:image/#{type};base64,#{binary_data}")
447
+ temp.add_attributes(attributes)
448
+
449
+ @div_middle.insert_after(@current, temp)
450
+ @current = temp
451
+ document_changed()
452
+ return @current
453
+ end
454
+
455
+ # Scans all REXML::Text children of an REXML::Element for any occurrences of regex.
456
+ # The text will be matched as one, not line by line as you might think.
457
+ # If you want to write a regexp matching multiple lines keep in mind that the dot "." by default doesn't
458
+ # match newline characters. Consider using the "m" option (e.g. /regex/m ) which makes dot match newlines
459
+ # or match newlines explicitly.
460
+ # highlight_captures then puts a <span> </span> tag around all captures of the regex
461
+ # NOTE: nested captures are not supported and don't make sense in this context!!
462
+ # @param regex [Regexp] a regular expression that will be matched
463
+ # @param color [String] either one of "y", "r", "g", "b" (yellow, red, green, blue) or a valid html color code (e.g. "#80BFFF")
464
+ # @param el [REXML::Element] the Element (scope) which will be searched for pattern matches, by default the last inserted element will be scanned
465
+ # @return [Fixnum] the number of highlighted captures
466
+ def highlight_captures(regex, color="y", el = @current)
467
+ # get all children of the current node
468
+ arr = el.to_a()
469
+ num_matches = 0
470
+ # first we have to detach all children from parent, otherwise we can cause ordering issues
471
+ arr.each {|i| i.remove() }
472
+ # depth first recursion into grand-children
473
+ for i in arr do
474
+ if i.is_a?(REXML::Text)
475
+ # in general a text looks as follows:
476
+ # .*(matchstring|.*)*
477
+
478
+ # We get an array of [[start,length], [start,length], ...] for all our regex SUB-matches
479
+ positions = i.value().enum_for(:scan, regex).flat_map {
480
+ # Regexp.last_match is a MatchData object, the index 0 is the entire match, 1 to n are captures
481
+ array = Array.new
482
+ for k in 1..Regexp.last_match.length - 1 do
483
+ array.push([Regexp.last_match.begin(k),
484
+ Regexp.last_match.end(k)-Regexp.last_match.begin(k)])
485
+ end
486
+ # return the array for the flat_map
487
+ array
488
+ }
489
+ num_matches += positions.length
490
+ if ["y","r","g","b"].include?(color)
491
+ attr = {"class" => color}
492
+ elsif color.match(/^#[A-Fa-f0-9]{6}$/)
493
+ attr = {"style" => "background: #{color};"}
494
+ else
495
+ raise "invalid color: #{color}"
496
+ end
497
+ replace_text_with_elements(el, i, "span", attr, positions)
498
+ else
499
+ # for non-text nodes we recurse into it and finally reattach to our parent to preserve ordering
500
+ num_matches += highlight_captures(regex, color, i)
501
+ el.add(i)
502
+ end # if i.is_a?(REXML::Text)
503
+ end # for i in arr do
504
+ document_changed()
505
+ return num_matches
506
+ end
507
+
508
+ # Scans all REXML::Text children of an REXML::Element for any occurrences of regex.
509
+ # The text will be matched as one, not line by line as you might think.
510
+ # If you want to write a regexp matching multiple lines keep in mind that the dot "." by default doesn't
511
+ # match newline characters. Consider using the "m" option (e.g. /regex/m ) which makes dot match newlines
512
+ # or match newlines explicitly.
513
+ # highlight then puts a <span> </span> tag around all matches of regex
514
+ # @param regex [Regexp] a regular expression that will be matched
515
+ # @param color [String] either one of "y", "r", "g", "b" (yellow, red, green, blue) or a valid html color code (e.g. "#80BFFF")
516
+ # @param el [REXML::Element] the Element (scope) which will be searched for pattern matches
517
+ # @return [Fixnum] the number of highlighted captures
518
+ def highlight(regex, color="y", el = @current)
519
+ # get all children of the current node
520
+ arr = el.to_a()
521
+ num_matches = 0
522
+ #puts arr.inspect
523
+ # first we have to detach all children from parent, otherwise we can cause ordering issues
524
+ arr.each {|i| i.remove() }
525
+ # depth first recursion into grand-children
526
+ for i in arr do
527
+ #puts i.class.to_s()
528
+ if i.is_a?(REXML::Text)
529
+ # in general a text looks as follows:
530
+ # .*(matchstring|.*)*
531
+
532
+ # We get an array of [[start,length], [start,length], ...] for all our regex matches
533
+ positions = i.value().enum_for(:scan, regex).map {
534
+ [Regexp.last_match.begin(0),
535
+ Regexp.last_match.end(0)-Regexp.last_match.begin(0)]
536
+ }
537
+ num_matches += positions.length
538
+ if ["y","r","g","b"].include?(color)
539
+ attr = {"class" => color}
540
+ elsif color.match(/^#[A-Fa-f0-9]{6}$/)
541
+ attr = {"style" => "background: #{color};"}
542
+ else
543
+ raise "invalid color: #{color}"
544
+ end
545
+ replace_text_with_elements(el, i, "span", attr, positions)
546
+ else
547
+ # for non-text nodes we recurse into it and finally reattach to our parent to preserve ordering
548
+ # puts "recurse"
549
+ num_matches += highlight(regex, color, i)
550
+ el.add(i)
551
+ end # if i.is_a?(REXML::Text)
552
+ end # for i in arr do
553
+ document_changed()
554
+ return num_matches
555
+ end
556
+
557
+ # creates a html table from two dimensional array of the form Array [row] [col]
558
+ # @param table_data [Array<Array>] of the form Array [row] [col] containing all data, the '.to_s' method will be called on each element,
559
+ # @param headers [Number] either of 0, 1, 2, 3. Where 0 is no headers (<th>) at all, 1 is only the first row,
560
+ # 2 is only the first column and 3 is both, first row and first column as <th> elements. Every other number
561
+ # is equivalent to the bitwise AND of the two least significant bits with 1, 2 or 3
562
+ # @return [REXML::Element] the Element which was just added
563
+ def table(table_data, headers=0, table_attrs={}, tr_attrs={}, th_attrs={}, td_attrs={})
564
+ opts = {
565
+ headers: headers,
566
+ data_is_xhtml: false,
567
+ table_attrs: table_attrs,
568
+ th_attrs: th_attrs,
569
+ tr_attrs: tr_attrs,
570
+ td_attrs: td_attrs,
571
+ }
572
+ custom_table(table_data, opts)
573
+ end
574
+
575
+ # creates a html table from two dimensional array of the form Array [row] [col]
576
+ # @param table_data [Array<Array>] of the form Array [row] [col] containing all data, the '.to_s' method will be called on each element,
577
+ # @option opts [Number] :headers either of 0, 1, 2, 3. Where 0 is no headers (<th>) at all, 1 is only the first row,
578
+ # 2 is only the first column and 3 is both, first row and first column as <th> elements. Every other number
579
+ # is equivalent to the bitwise AND of the two least significant bits with 1, 2 or 3
580
+ # @option opts [Boolean] :data_is_xhtml defaults to false, if true table_data is inserted as xhtml without any sanitation or escaping.
581
+ # This way a table can be used for custom layouts.
582
+ # @option opts [Hash] :table_attrs html attributes for the <table> tag
583
+ # @option opts [Hash] :th_attrs html attributes for the <th> tag
584
+ # @option opts [Hash] :tr_attrs html attributes for the <tr> tag
585
+ # @option opts [Hash] :td_attrs html attributes for the <td> tag
586
+ # @option opts [Array<Hash>] :special Array of hashes for custom attributes on specific cells (<td> only) of the table
587
+ # @example Example of the :special attributes
588
+ # opts[:special] = [
589
+ # {
590
+ # col_title: 'rx_DroppedFrameCount', # string or regexp or nil # if neither title nor index are present, the condition is evaluated for all <td> cells
591
+ # col_index: 5..7, # Fixnum, Range or nil # index has precedence over title
592
+ # row_title: 'D_0_BE_iMix', # string or regexp or nil
593
+ # row_index: 6, # Fixnum, Range or nil
594
+ # condition: Proc.new { |e| Integer(e) != 0 }, # a proc
595
+ # attributes: {"style" => "background-color: #DB7093;"},
596
+ # },
597
+ # ]
598
+ # @return [REXML::Element] the Element which was just added
599
+ def custom_table(table_data, opts = {})
600
+ defaults = {
601
+ headers: 0,
602
+ data_is_xhtml: false,
603
+ table_attrs: {},
604
+ th_attrs: {},
605
+ tr_attrs: {},
606
+ td_attrs: {},
607
+ special: [],
608
+ }
609
+ o = defaults.merge(opts)
610
+
611
+ temp = REXML::Element.new("table")
612
+ temp.add_attributes(o[:table_attrs])
613
+ row_titles = table_data.collect{|row| row[0].to_s}
614
+ col_titles = table_data[0].collect{|title| title.to_s}
615
+
616
+ for i in 0..table_data.length-1 do # row
617
+ row = temp.add_element("tr", o[:tr_attrs])
618
+ for j in 0..table_data[i].length-1 do # column
619
+ if (i == 0 && (0x1 & o[:headers])==0x1)
620
+ col = row.add_element("th", o[:th_attrs])
621
+ elsif (j == 0 && (0x2 & o[:headers])==0x2)
622
+ col = row.add_element("th", o[:th_attrs])
623
+ elsif ((i == 0 || j ==0) && (0x3 & o[:headers])==0x3)
624
+ col = row.add_element("th", o[:th_attrs])
625
+ else
626
+ # we need to deepcopy the attributes
627
+ _td_attrs = Marshal.load(Marshal.dump(o[:td_attrs]))
628
+
629
+ # check all special criteria
630
+ o[:special].each do |h|
631
+ # check if the current cell is a candidate for special
632
+ if !h[:col_index].nil?
633
+ if h[:col_index].is_a?(Range)
634
+ next if (!h[:col_index].include?(j)) # skip if not in range
635
+ elsif h[:col_index].is_a?(Integer)
636
+ next if (h[:col_index] != j) # skip if not at index
637
+ end
638
+ elsif !h[:col_title].nil?
639
+ next if !col_titles[j].match(h[:col_title])
640
+ end
641
+ # check if the current cell is a candidate for special
642
+ if !h[:row_index].nil?
643
+ if h[:row_index].is_a?(Range)
644
+ next if (!h[:row_index].include?(i)) # skip if not in range
645
+ elsif h[:row_index].is_a?(Integer)
646
+ next if (h[:row_index] != i) # skip if not at index
647
+ end
648
+ elsif !h[:row_title].nil?
649
+ next if !row_titles[i].match(h[:row_title])
650
+ end
651
+
652
+ # here we are a candidate for special, so we check if we meet the condition
653
+ # puts h[:attributes].inspect
654
+ # puts "cell value row #{i} col #{j}: #{table_data[i][j]}"
655
+ # puts h[:condition].call(table_data[i][j]).inspect
656
+ if h[:condition].call(table_data[i][j])
657
+ h[:attributes].each { |attr, val|
658
+ # debug, verify deepcopy
659
+ # puts "objects are equal: #{_td_attrs[attr].equal?(o[:td_attrs][attr])}"
660
+ if !_td_attrs[attr].nil?
661
+ # assume the existing attribute is a string (other types don't make much sense for html)
662
+ _td_attrs[attr] << val
663
+ else
664
+ # create the attribute if it is not already there
665
+ _td_attrs[attr] = val
666
+ end
667
+ }
668
+ end
669
+ end
670
+
671
+ col = row.add_element("td", _td_attrs)
672
+ end
673
+ if o[:data_is_xhtml]
674
+ # we need to create a new document with a pseudo root because having multiple nodes at top
675
+ # level is not valid xml
676
+ doc = REXML::Document.new("<root>" + table_data[i][j].to_s + "</root>")
677
+ # then we move all children of root to the actual div middle element and insert after current
678
+ for elem in doc.root.to_a do
679
+ if elem.is_a?(REXML::Text)
680
+ # due to reasons unclear to me, text needs special treatment
681
+ col.add_text(elem)
682
+ else
683
+ col.add_element(elem) # add the td element
684
+ end
685
+ end
686
+ else
687
+ col.add_text(table_data[i][j].to_s)
688
+ end
689
+ end # for j in 0..table_data[i].length-1 do # column
690
+ end # for i in 0..table_data.length-1 do # row
691
+
692
+ @div_middle.insert_after(@current, temp)
693
+ @current = temp
694
+ document_changed()
695
+ return @current
696
+ end
697
+
698
+
699
+ # Appends a new heading element to body, and sets current to this new heading
700
+ # @param tag_type [String] specifiy "h1", "h2", "h3" for the heading, defaults to "h1"
701
+ # @param attrs [Hash] attributes for the <h#> element, any valid html attributes can be specified
702
+ # @option attrs [String] "class" by default every heading is added to the left table of contents (toc)
703
+ # use the class "onlyrtoc" or "bothtoc" to add a heading only to the right toc or to both tocs respectively
704
+ # @yieldreturn [String] the text to be added to the <h#> element
705
+ # @return [REXML::Element] the Element which was just added
706
+ def heading(tag_type="h1", attrs={}, &block)
707
+ temp = REXML::Element.new(tag_type)
708
+ temp.add_attributes(attrs)
709
+
710
+ @div_middle.insert_after(@current, temp)
711
+ @current = temp
712
+ raise "Block argument is mandatory" unless block_given?
713
+ text = encoding_fixer(block.call())
714
+ @current.text = text
715
+ document_changed()
716
+ return @current
717
+ end
718
+
719
+ # Inserts a new heading element at the very beginning of the middle div section, and points @current to this heading
720
+ # @param tag_type [String] specifiy "h1", "h2", "h3" for the heading, defaults to "h1"
721
+ # @param attrs [Hash] attributes for the <h#> element, any valid html attributes can be specified
722
+ # @option attrs [String] "class" by default every heading is added to the left table of contents (toc)
723
+ # use the class "onlyrtoc" or "bothtoc" to add a heading only to the right toc or to both tocs respectively
724
+ # @yieldreturn [String] the text to be added to the <h#> element
725
+ # @return [REXML::Element] the Element which was just added
726
+ def heading_top(tag_type="h1", attrs={}, &block)
727
+ temp = REXML::Element.new(tag_type)
728
+ temp.add_attributes(attrs)
729
+
730
+ # check if there are any child elements
731
+ if @div_middle.has_elements?()
732
+ # insert before the first child of div middle
733
+ @div_middle.insert_before("//div[@id='middle']/*[1]", temp)
734
+ else
735
+ # middle is empty, just insert the heading
736
+ @div_middle.insert_after(@current, temp)
737
+ end
738
+
739
+ @current = temp
740
+ raise "Block argument is mandatory" unless block_given?
741
+ text = encoding_fixer(block.call())
742
+ @current.text = text
743
+ document_changed()
744
+ return @current
745
+ end
746
+
747
+ # Helper Method for the highlight methods. it will introduce specific xhtml tags around parts of a text child of an xml element.
748
+ # @example
749
+ # we have the following xml part
750
+ # <test>
751
+ # some arbitrary
752
+ # text child content
753
+ # </test>
754
+ # now we call replace_text_with_elements to place <span> around the word "arbitrary"
755
+ # =>
756
+ # <test>
757
+ # some <span>arbitrary</span>
758
+ # text child content
759
+ # </test>
760
+ # @param parent [REXML::Element] the parent to which "element" should be attached after parsing, e.g. <test>
761
+ # @param element [REXML::Element] the Text element, into which tags will be added at the specified indices of @index_length_array, e.g. the REXML::Text children of <test> in the example
762
+ # @param tagname [String] the tag that will be introduced as <tagname> at the indices specified
763
+ # @param attribs [Hash] Attributes that will be added to the inserted tag e.g. <tagname attrib="test">
764
+ # @param index_length_array [Array] Array of the form [[index, lenght], [index, lenght], ...] that specifies
765
+ # the start position and length of the substring around which the tags will be introduced
766
+ def replace_text_with_elements(parent, element, tagname, attribs, index_length_array)
767
+ last_end = 0
768
+ index = 0
769
+ #puts index_length_array.inspect
770
+ #puts element.inspect
771
+ for j in index_length_array do
772
+ # reattach normal (unmatched) text
773
+ if j[0] > last_end
774
+ # text = REXML::Text.new(element.value()[ last_end, j[0] - last_end ])
775
+ # parent.add_text(text)
776
+ # add text without creating a textnode, textnode screws up formatting (e.g. all whitespace are condensed into one)
777
+ parent.add_text( element.value()[ last_end, j[0] - last_end ] )
778
+ end
779
+ #create the tag node with attributes and add the text to it
780
+ tag = parent.add_element(REXML::Element.new(tagname), attribs)
781
+ tag.add_text(element.value()[ j[0], j[1] ])
782
+ last_end = j[0]+j[1]
783
+
784
+ # in the last round check for any remaining text
785
+ if index == index_length_array.length - 1
786
+ if last_end < element.value().length
787
+ # text = REXML::Text.new(element.value()[ last_end, element.value().length - last_end ])
788
+ # parent.add(text)
789
+ # add text without creating a textnode, textnode screws up formatting (e.g. all whitespace are condensed into one)
790
+ parent.add_text( element.value()[ last_end, element.value().length - last_end ] )
791
+ end
792
+ end
793
+ index += 1
794
+ end # for j in positions do
795
+
796
+ # don't forget to reattach the textnode if there are no regex matches at all
797
+ if index == 0
798
+ parent.add(element)
799
+ end
154
800
 
801
+ end # replace_text_with_elements
155
802
 
803
+ end # Generator
804
+ end # XhtmlReportGenerator