odf-report 0.5.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +5 -13
  2. data/.github/workflows/gem-push.yml +40 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +4 -0
  5. data/CHANGELOG.md +56 -0
  6. data/README.md +220 -0
  7. data/Rakefile +8 -0
  8. data/bin/odt-extract +10 -0
  9. data/bin/odt-viewer +18 -0
  10. data/lib/odf-report.rb +11 -9
  11. data/lib/odf-report/data_source.rb +65 -0
  12. data/lib/odf-report/field.rb +35 -36
  13. data/lib/odf-report/image.rb +57 -0
  14. data/lib/odf-report/nestable.rb +65 -0
  15. data/lib/odf-report/parser/default.rb +5 -4
  16. data/lib/odf-report/report.rb +29 -57
  17. data/lib/odf-report/section.rb +17 -80
  18. data/lib/odf-report/table.rb +52 -81
  19. data/lib/odf-report/template.rb +88 -0
  20. data/lib/odf-report/text.rb +2 -4
  21. data/lib/odf-report/version.rb +1 -1
  22. data/odf-report.gemspec +7 -4
  23. data/spec/fields_spec.rb +77 -0
  24. data/spec/images/image_1.jpg +0 -0
  25. data/spec/images/image_2.jpg +0 -0
  26. data/spec/images/image_3.jpg +0 -0
  27. data/{test → spec/images}/piriapolis.jpg +0 -0
  28. data/spec/images/placeholder.jpg +0 -0
  29. data/{test → spec/images}/rails.png +0 -0
  30. data/spec/images_spec.rb +159 -0
  31. data/spec/sections_spec.rb +51 -0
  32. data/spec/spec_helper.rb +43 -0
  33. data/spec/tables_spec.rb +39 -0
  34. data/spec/template_spec.rb +45 -0
  35. data/spec/templates/images.odt +0 -0
  36. data/spec/templates/specs.odt +0 -0
  37. data/test/fields_inside_text_test.rb +38 -0
  38. data/test/images_test.rb +32 -0
  39. data/test/nested_tables_test.rb +43 -0
  40. data/test/sections_test.rb +44 -0
  41. data/test/sub_sections_test.rb +58 -0
  42. data/test/table_headers_test.rb +41 -0
  43. data/test/tables_test.rb +67 -0
  44. data/test/templates/images/image_1.jpg +0 -0
  45. data/test/templates/images/image_2.jpg +0 -0
  46. data/test/templates/images/image_3.jpg +0 -0
  47. data/test/templates/images/placeholder.jpg +0 -0
  48. data/test/templates/images/placeholder.png +0 -0
  49. data/test/templates/piriapolis.jpg +0 -0
  50. data/test/templates/rails.png +0 -0
  51. data/test/templates/test_images.odt +0 -0
  52. data/test/templates/test_sub_sections.odt +0 -0
  53. data/test/templates/test_text.odt +0 -0
  54. data/test/test.rb +262 -0
  55. data/test/text_test.rb +56 -0
  56. metadata +151 -46
  57. data/README.textile +0 -225
  58. data/lib/odf-report/fields.rb +0 -40
  59. data/lib/odf-report/file.rb +0 -50
  60. data/lib/odf-report/images.rb +0 -44
  61. data/lib/odf-report/nested.rb +0 -34
  62. data/test/test_fields_inside_text.rb +0 -37
  63. data/test/test_nested_tables.rb +0 -39
  64. data/test/test_sections.rb +0 -39
  65. data/test/test_sub_sections.rb +0 -57
  66. data/test/test_table_headers.rb +0 -39
  67. data/test/test_tables.rb +0 -62
  68. data/test/test_text.rb +0 -48
@@ -1,56 +1,55 @@
1
1
  module ODFReport
2
+ class Field
2
3
 
3
- class Field
4
+ DELIMITERS = %w([ ])
4
5
 
5
- DELIMITERS = ['[', ']']
6
+ def initialize(opts, &block)
7
+ @name = opts[:name]
8
+ @data_source = DataSource.new(opts, &block)
9
+ end
6
10
 
7
- def initialize(opts, &block)
8
- @name = opts[:name]
9
- @data_field = opts[:data_field]
11
+ def set_source(record)
12
+ @data_source.set_source(record)
13
+ self
14
+ end
10
15
 
11
- unless @value = opts[:value]
16
+ def replace!(content, data_item = nil)
12
17
 
13
- if block_given?
14
- @block = block
18
+ txt = content.inner_html
15
19
 
16
- else
17
- @block = lambda { |item| self.extract_value(item) }
18
- end
20
+ txt.gsub!(to_placeholder, sanitize(@data_source.value))
19
21
 
20
- end
21
-
22
- end
23
-
24
- def get_value(data_item = nil)
25
- @value || @block.call(data_item) || ''
26
- end
22
+ content.inner_html = txt
27
23
 
28
- def to_placeholder
29
- if DELIMITERS.is_a?(Array)
30
- "#{DELIMITERS[0]}#{@name.to_s.upcase}#{DELIMITERS[1]}"
31
- else
32
- "#{DELIMITERS}#{@name.to_s.upcase}#{DELIMITERS}"
33
24
  end
34
- end
35
25
 
36
- def extract_value(data_item)
37
- return unless data_item
26
+ private
38
27
 
39
- key = @data_field || @name
28
+ def to_placeholder
29
+ if DELIMITERS.is_a?(Array)
30
+ "#{DELIMITERS[0]}#{@name.to_s.upcase}#{DELIMITERS[1]}"
31
+ else
32
+ "#{DELIMITERS}#{@name.to_s.upcase}#{DELIMITERS}"
33
+ end
34
+ end
40
35
 
41
- if data_item.is_a?(Hash)
42
- data_item[key] || data_item[key.to_s.downcase] || data_item[key.to_s.upcase] || data_item[key.to_s.downcase.to_sym]
36
+ def sanitize(txt)
37
+ txt = html_escape(txt)
38
+ txt = odf_linebreak(txt)
39
+ txt
40
+ end
43
41
 
44
- elsif data_item.respond_to?(key.to_s.downcase.to_sym)
45
- data_item.send(key.to_s.downcase.to_sym)
42
+ HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
46
43
 
47
- else
48
- raise "Can't find field [#{key}] in this #{data_item.class}"
44
+ def html_escape(s)
45
+ return "" unless s
46
+ s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
47
+ end
49
48
 
49
+ def odf_linebreak(s)
50
+ return "" unless s
51
+ s.to_s.gsub("\n", "<text:line-break/>")
50
52
  end
51
53
 
52
54
  end
53
-
54
55
  end
55
-
56
- end
@@ -0,0 +1,57 @@
1
+ module ODFReport
2
+ class Image < Field
3
+
4
+ IMAGE_DIR_NAME = "Pictures"
5
+
6
+ attr_reader :files
7
+
8
+ def initialize(opts, &block)
9
+ @files = []
10
+ super
11
+ end
12
+
13
+ def replace!(doc, data_item = nil)
14
+
15
+ frame = doc.xpath("//draw:frame[@draw:name='#{@name}']").first
16
+ image = doc.xpath("//draw:frame[@draw:name='#{@name}']/draw:image").first
17
+
18
+ return unless image
19
+
20
+ file = @data_source.value
21
+
22
+ if file
23
+ image.attribute('href').content = File.join(IMAGE_DIR_NAME, File.basename(file))
24
+ frame.attribute('name').content = SecureRandom.uuid
25
+
26
+ @files << file
27
+ else
28
+ frame.remove
29
+ end
30
+
31
+ end
32
+
33
+ def self.include_image_file(zip_file, image_file)
34
+ return unless image_file
35
+
36
+ href = File.join(IMAGE_DIR_NAME, File.basename(image_file))
37
+
38
+ zip_file.update_file(href, File.read(image_file))
39
+ end
40
+
41
+ def self.include_manifest_entry(content, image_file)
42
+ return unless image_file
43
+
44
+ return unless root_node = content.at("//manifest:manifest")
45
+
46
+ href = File.join(IMAGE_DIR_NAME, File.basename(image_file))
47
+
48
+ entry = content.create_element('manifest:file-entry')
49
+ entry['manifest:full-path'] = href
50
+ entry['manifest:media-type'] = MIME::Types.type_for(href)[0].content_type
51
+
52
+ root_node.add_child entry
53
+
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,65 @@
1
+ module ODFReport
2
+ class Nestable
3
+
4
+ def initialize(opts)
5
+ @name = opts[:name]
6
+
7
+ @data_source = DataSource.new(opts)
8
+
9
+ @fields = []
10
+ @texts = []
11
+ @tables = []
12
+ @sections = []
13
+ @images = []
14
+
15
+ end
16
+
17
+ def set_source(data_item)
18
+ @data_source.set_source(data_item)
19
+ self
20
+ end
21
+
22
+ def add_field(name, data_field=nil, &block)
23
+ opts = { name: name, data_field: data_field }
24
+ @fields << Field.new(opts, &block)
25
+ end
26
+ alias_method :add_column, :add_field
27
+
28
+ def add_text(name, data_field=nil, &block)
29
+ opts = {name: name, data_field: data_field}
30
+ @texts << Text.new(opts, &block)
31
+ end
32
+
33
+ def add_image(name, data_field=nil, &block)
34
+ opts = {name: name, data_field: data_field}
35
+ @images << Image.new(opts, &block)
36
+ end
37
+
38
+ def add_table(table_name, collection_field, opts={})
39
+ opts.merge!(name: table_name, collection_field: collection_field)
40
+ tab = Table.new(opts)
41
+ @tables << tab
42
+
43
+ yield(tab)
44
+ end
45
+
46
+ def add_section(section_name, collection_field, opts={})
47
+ opts.merge!(name: section_name, collection_field: collection_field)
48
+ sec = Section.new(opts)
49
+ @sections << sec
50
+
51
+ yield(sec)
52
+ end
53
+
54
+ def all_images
55
+ (@images.map(&:files) + @sections.map(&:all_images) + @tables.map(&:all_images)).flatten
56
+ end
57
+
58
+ def wrap_with_ns(node)
59
+ <<-XML
60
+ <root xmlns:draw="a" xmlns:xlink="b" xmlns:text="c" xmlns:table="d">#{node.to_xml}</root>
61
+ XML
62
+ end
63
+
64
+ end
65
+ end
@@ -34,6 +34,7 @@ module Parser
34
34
  end
35
35
 
36
36
  def parse
37
+
37
38
  xml = @template_node.parse(@text)
38
39
 
39
40
  xml.css("p", "h1", "h2").each do |p|
@@ -59,9 +60,9 @@ module Parser
59
60
 
60
61
  def parse_formatting(text)
61
62
  text.strip!
62
- text.gsub!(/<strong>(.+?)<\/strong>/) { "<text:span text:style-name=\"bold\">#{$1}<\/text:span>" }
63
- text.gsub!(/<em>(.+?)<\/em>/) { "<text:span text:style-name=\"italic\">#{$1}<\/text:span>" }
64
- text.gsub!(/<u>(.+?)<\/u>/) { "<text:span text:style-name=\"underline\">#{$1}<\/text:span>" }
63
+ text.gsub!(/<strong.*?>(.+?)<\/strong>/) { "<text:span text:style-name=\"bold\">#{$1}<\/text:span>" }
64
+ text.gsub!(/<em.*?>(.+?)<\/em>/) { "<text:span text:style-name=\"italic\">#{$1}<\/text:span>" }
65
+ text.gsub!(/<u.*?>(.+?)<\/u>/) { "<text:span text:style-name=\"underline\">#{$1}<\/text:span>" }
65
66
  text.gsub!("\n", "")
66
67
  text
67
68
  end
@@ -87,4 +88,4 @@ module Parser
87
88
 
88
89
  end
89
90
 
90
- end
91
+ end
@@ -1,38 +1,35 @@
1
1
  module ODFReport
2
2
 
3
3
  class Report
4
- include Fields, Images
5
4
 
6
- attr_accessor :fields, :tables, :images, :sections, :file, :texts
5
+ def initialize(template_name = nil, io: nil)
7
6
 
8
- def initialize(template_name, &block)
9
-
10
- @file = ODFReport::File.new(template_name)
7
+ @template = ODFReport::Template.new(template_name, io: io)
11
8
 
12
9
  @texts = []
13
10
  @fields = []
14
11
  @tables = []
15
- @images = {}
16
- @image_names_replacements = {}
17
12
  @sections = []
18
13
 
19
- yield(self)
14
+ @images = []
15
+
16
+ yield(self) if block_given?
20
17
 
21
18
  end
22
19
 
23
- def add_field(field_tag, value='', &block)
20
+ def add_field(field_tag, value='')
24
21
  opts = {:name => field_tag, :value => value}
25
- field = Field.new(opts, &block)
22
+ field = Field.new(opts)
26
23
  @fields << field
27
24
  end
28
25
 
29
- def add_text(field_tag, value='', &block)
26
+ def add_text(field_tag, value='')
30
27
  opts = {:name => field_tag, :value => value}
31
28
  text = Text.new(opts)
32
29
  @texts << text
33
30
  end
34
31
 
35
- def add_table(table_name, collection, opts={}, &block)
32
+ def add_table(table_name, collection, opts={})
36
33
  opts.merge!(:name => table_name, :collection => collection)
37
34
  tab = Table.new(opts)
38
35
  @tables << tab
@@ -40,7 +37,7 @@ class Report
40
37
  yield(tab)
41
38
  end
42
39
 
43
- def add_section(section_name, collection, opts={}, &block)
40
+ def add_section(section_name, collection, opts={})
44
41
  opts.merge!(:name => section_name, :collection => collection)
45
42
  sec = Section.new(opts)
46
43
  @sections << sec
@@ -48,71 +45,46 @@ class Report
48
45
  yield(sec)
49
46
  end
50
47
 
51
- def add_image(name, path)
52
- @images[name] = path
48
+ def add_image(image_name, value=nil)
49
+ opts = {:name => image_name, :value => value}
50
+ image = Image.new(opts)
51
+ @images << image
53
52
  end
54
53
 
55
54
  def generate(dest = nil)
56
55
 
57
- @file.update_content do |file|
56
+ @template.update_content do |file|
58
57
 
59
- file.update_files('content.xml', 'styles.xml') do |txt|
58
+ file.update_files do |doc|
60
59
 
61
- parse_document(txt) do |doc|
60
+ @sections.each { |c| c.replace!(doc) }
61
+ @tables.each { |c| c.replace!(doc) }
62
62
 
63
- replace_texts!(doc)
64
- replace_fields!(doc)
63
+ @texts.each { |c| c.replace!(doc) }
64
+ @fields.each { |c| c.replace!(doc) }
65
65
 
66
- replace_sections!(doc)
67
- replace_tables!(doc)
66
+ @images.each { |c| c.replace!(doc) }
68
67
 
69
- find_image_name_matches(doc)
70
- avoid_duplicate_image_names(doc)
68
+ end
71
69
 
72
- end
70
+ all_images.each { |i| Image.include_image_file(file, i) }
73
71
 
72
+ file.update_manifest do |content|
73
+ all_images.each { |i| Image.include_manifest_entry(content, i) }
74
74
  end
75
75
 
76
- replace_images(file)
77
-
78
76
  end
79
77
 
80
78
  if dest
81
- ::File.open(dest, "wb") {|f| f.write(@file.data) }
79
+ File.open(dest, "wb") { |f| f.write(@template.data) }
82
80
  else
83
- @file.data
84
- end
85
-
86
- end
87
-
88
- private
89
-
90
- def parse_document(txt)
91
- doc = Nokogiri::XML(txt)
92
- yield doc
93
- txt.replace(doc.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML))
94
- end
95
-
96
- def replace_fields!(content)
97
- field_replace!(content)
98
- end
99
-
100
- def replace_texts!(content)
101
- @texts.each do |text|
102
- text.replace!(content)
81
+ @template.data
103
82
  end
104
- end
105
83
 
106
- def replace_tables!(content)
107
- @tables.each do |table|
108
- table.replace!(content)
109
- end
110
84
  end
111
85
 
112
- def replace_sections!(content)
113
- @sections.each do |section|
114
- section.replace!(content)
115
- end
86
+ def all_images
87
+ @all_images ||= (@images.map(&:files) + @sections.map(&:all_images) + @tables.map(&:all_images)).flatten.uniq
116
88
  end
117
89
 
118
90
  end
@@ -1,102 +1,39 @@
1
1
  module ODFReport
2
+ class Section < Nestable
2
3
 
3
- class Section
4
- include Fields, Nested
4
+ def replace!(doc)
5
5
 
6
- attr_accessor :fields, :tables, :data, :name, :collection_field, :parent
6
+ return unless find_section_node(doc)
7
7
 
8
- def initialize(opts)
9
- @name = opts[:name]
10
- @collection_field = opts[:collection_field]
11
- @collection = opts[:collection]
12
- @parent = opts[:parent]
8
+ @data_source.each do |record|
13
9
 
14
- @fields = []
15
- @texts = []
10
+ new_section = deep_clone(@section_node)
16
11
 
17
- @tables = []
18
- @sections = []
19
- end
20
-
21
- def add_field(name, data_field=nil, &block)
22
- opts = {:name => name, :data_field => data_field}
23
- field = Field.new(opts, &block)
24
- @fields << field
25
-
26
- end
27
-
28
- def add_text(name, data_field=nil, &block)
29
- opts = {:name => name, :data_field => data_field}
30
- field = Text.new(opts, &block)
31
- @texts << field
32
-
33
- end
34
-
35
- def add_table(table_name, collection_field, opts={}, &block)
36
- opts.merge!(:name => table_name, :collection_field => collection_field, :parent => self)
37
- tab = Table.new(opts)
38
- @tables << tab
39
-
40
- yield(tab)
41
- end
42
-
43
- def add_section(section_name, collection_field, opts={}, &block)
44
- opts.merge!(:name => section_name, :collection_field => collection_field, :parent => self)
45
- sec = Section.new(opts)
46
- @sections << sec
47
-
48
- yield(sec)
49
- end
50
-
51
- def populate!(row)
52
- @collection = get_collection_from_item(row, @collection_field) if row
53
- end
12
+ @tables.each { |t| t.set_source(record).replace!(new_section) }
13
+ @sections.each { |s| s.set_source(record).replace!(new_section) }
14
+ @texts.each { |t| t.set_source(record).replace!(new_section) }
15
+ @fields.each { |f| f.set_source(record).replace!(new_section) }
16
+ @images.each { |i| i.set_source(record).replace!(new_section) }
54
17
 
55
- def replace!(doc, row = nil)
56
-
57
- return unless section = find_section_node(doc)
58
-
59
- template = section.dup
60
-
61
- populate!(row)
62
-
63
- @collection.each do |data_item|
64
- new_section = template.dup
65
-
66
- @texts.each do |t|
67
- t.replace!(new_section, data_item)
68
- end
69
-
70
- @tables.each do |t|
71
- t.replace!(new_section, data_item)
72
- end
73
-
74
- @sections.each do |s|
75
- s.replace!(new_section, data_item)
76
- end
77
-
78
- replace_fields!(new_section, data_item)
79
-
80
- section.before(new_section)
18
+ @section_node.before(new_section.to_xml)
81
19
 
82
20
  end
83
21
 
84
- section.remove
22
+ @section_node.remove
85
23
 
86
24
  end # replace_section
87
25
 
88
26
  private
89
27
 
90
28
  def find_section_node(doc)
29
+ @section_node = doc.at_css("text|section[@text|name='#{@name}']")
30
+ end
91
31
 
92
- sections = doc.xpath(".//text:section[@text:name='#{@name}']")
93
-
94
- sections.empty? ? nil : sections.first
32
+ def deep_clone(node)
33
+ Nokogiri::XML(wrap_with_ns(node)).at("text|section")
34
+ .tap { |n| n.attribute('name').content = SecureRandom.uuid }
95
35
 
96
36
  end
97
37
 
98
38
  end
99
-
100
39
  end
101
-
102
-