xhtml_report_generator 3.1.1 → 4.0.3

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