odf-report 0.5.2 → 0.7.2
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.
- checksums.yaml +5 -5
- data/.github/workflows/gem-push.yml +40 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +62 -0
- data/README.md +220 -0
- data/bin/odt-extract +10 -0
- data/bin/odt-viewer +18 -0
- data/lib/odf-report.rb +11 -8
- data/lib/odf-report/data_source.rb +65 -0
- data/lib/odf-report/field.rb +7 -40
- data/lib/odf-report/image.rb +57 -0
- data/lib/odf-report/nestable.rb +65 -0
- data/lib/odf-report/report.rb +24 -28
- data/lib/odf-report/section.rb +15 -42
- data/lib/odf-report/table.rb +54 -59
- data/lib/odf-report/template.rb +88 -0
- data/lib/odf-report/text.rb +1 -3
- data/lib/odf-report/version.rb +1 -1
- data/odf-report.gemspec +4 -4
- data/spec/fields_spec.rb +4 -4
- data/spec/images/image_1.jpg +0 -0
- data/spec/images/image_2.jpg +0 -0
- data/spec/images/image_3.jpg +0 -0
- data/spec/images/piriapolis.jpg +0 -0
- data/spec/images/placeholder.jpg +0 -0
- data/spec/images/rails.png +0 -0
- data/spec/images_spec.rb +159 -0
- data/spec/sections_spec.rb +51 -0
- data/spec/spec_helper.rb +2 -4
- data/spec/tables_spec.rb +1 -1
- data/spec/template_spec.rb +45 -0
- data/spec/templates/images.odt +0 -0
- data/spec/templates/specs.odt +0 -0
- data/test/images_test.rb +32 -0
- data/test/templates/images/image_1.jpg +0 -0
- data/test/templates/images/image_2.jpg +0 -0
- data/test/templates/images/image_3.jpg +0 -0
- data/test/templates/images/placeholder.jpg +0 -0
- data/test/templates/images/placeholder.png +0 -0
- data/test/templates/test_images.odt +0 -0
- data/test/test.rb +262 -0
- metadata +77 -23
- data/README.textile +0 -223
- data/lib/odf-report/file.rb +0 -50
- data/lib/odf-report/images.rb +0 -44
- data/lib/odf-report/nested.rb +0 -62
- data/spec/specs.odt +0 -0
data/lib/odf-report/field.rb
CHANGED
@@ -5,56 +5,25 @@ module ODFReport
|
|
5
5
|
|
6
6
|
def initialize(opts, &block)
|
7
7
|
@name = opts[:name]
|
8
|
-
@
|
9
|
-
|
10
|
-
unless @value = opts[:value]
|
11
|
-
|
12
|
-
if block_given?
|
13
|
-
@block = block
|
14
|
-
|
15
|
-
else
|
16
|
-
@block = lambda { |item| self.extract_value(item) }
|
17
|
-
end
|
18
|
-
|
19
|
-
end
|
8
|
+
@data_source = DataSource.new(opts, &block)
|
9
|
+
end
|
20
10
|
|
11
|
+
def set_source(record)
|
12
|
+
@data_source.set_source(record)
|
13
|
+
self
|
21
14
|
end
|
22
15
|
|
23
16
|
def replace!(content, data_item = nil)
|
24
17
|
|
25
18
|
txt = content.inner_html
|
26
19
|
|
27
|
-
|
28
|
-
|
29
|
-
txt.gsub!(to_placeholder, sanitize(val))
|
20
|
+
txt.gsub!(to_placeholder, sanitize(@data_source.value))
|
30
21
|
|
31
22
|
content.inner_html = txt
|
32
23
|
|
33
24
|
end
|
34
25
|
|
35
|
-
|
36
|
-
@value || @block.call(data_item) || ''
|
37
|
-
end
|
38
|
-
|
39
|
-
def extract_value(data_item)
|
40
|
-
return unless data_item
|
41
|
-
|
42
|
-
key = @data_field || @name
|
43
|
-
|
44
|
-
if data_item.is_a?(Hash)
|
45
|
-
data_item[key] || data_item[key.to_s.downcase] || data_item[key.to_s.upcase] || data_item[key.to_s.downcase.to_sym]
|
46
|
-
|
47
|
-
elsif data_item.respond_to?(key.to_s.downcase.to_sym)
|
48
|
-
data_item.send(key.to_s.downcase.to_sym)
|
49
|
-
|
50
|
-
else
|
51
|
-
raise "Can't find field [#{key}] in this #{data_item.class}"
|
52
|
-
|
53
|
-
end
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
private
|
26
|
+
private
|
58
27
|
|
59
28
|
def to_placeholder
|
60
29
|
if DELIMITERS.is_a?(Array)
|
@@ -82,7 +51,5 @@ module ODFReport
|
|
82
51
|
s.to_s.gsub("\n", "<text:line-break/>")
|
83
52
|
end
|
84
53
|
|
85
|
-
|
86
|
-
|
87
54
|
end
|
88
55
|
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
|
data/lib/odf-report/report.rb
CHANGED
@@ -1,20 +1,19 @@
|
|
1
1
|
module ODFReport
|
2
2
|
|
3
3
|
class Report
|
4
|
-
include Images
|
5
4
|
|
6
|
-
def initialize(template_name,
|
5
|
+
def initialize(template_name = nil, io: nil)
|
7
6
|
|
8
|
-
@
|
7
|
+
@template = ODFReport::Template.new(template_name, io: io)
|
9
8
|
|
10
9
|
@texts = []
|
11
10
|
@fields = []
|
12
11
|
@tables = []
|
13
|
-
@images = {}
|
14
|
-
@image_names_replacements = {}
|
15
12
|
@sections = []
|
16
13
|
|
17
|
-
|
14
|
+
@images = []
|
15
|
+
|
16
|
+
yield(self) if block_given?
|
18
17
|
|
19
18
|
end
|
20
19
|
|
@@ -46,49 +45,46 @@ class Report
|
|
46
45
|
yield(sec)
|
47
46
|
end
|
48
47
|
|
49
|
-
def add_image(
|
50
|
-
|
48
|
+
def add_image(image_name, value=nil)
|
49
|
+
opts = {:name => image_name, :value => value}
|
50
|
+
image = Image.new(opts)
|
51
|
+
@images << image
|
51
52
|
end
|
52
53
|
|
53
54
|
def generate(dest = nil)
|
54
55
|
|
55
|
-
@
|
56
|
+
@template.update_content do |file|
|
56
57
|
|
57
|
-
file.update_files
|
58
|
+
file.update_files do |doc|
|
58
59
|
|
59
|
-
|
60
|
+
@sections.each { |c| c.replace!(doc) }
|
61
|
+
@tables.each { |c| c.replace!(doc) }
|
60
62
|
|
61
|
-
|
62
|
-
|
63
|
+
@texts.each { |c| c.replace!(doc) }
|
64
|
+
@fields.each { |c| c.replace!(doc) }
|
63
65
|
|
64
|
-
|
65
|
-
@fields.each { |f| f.replace!(doc) }
|
66
|
+
@images.each { |c| c.replace!(doc) }
|
66
67
|
|
67
|
-
|
68
|
-
avoid_duplicate_image_names(doc)
|
68
|
+
end
|
69
69
|
|
70
|
-
|
70
|
+
all_images.each { |i| Image.include_image_file(file, i) }
|
71
71
|
|
72
|
+
file.update_manifest do |content|
|
73
|
+
all_images.each { |i| Image.include_manifest_entry(content, i) }
|
72
74
|
end
|
73
75
|
|
74
|
-
replace_images(file)
|
75
|
-
|
76
76
|
end
|
77
77
|
|
78
78
|
if dest
|
79
|
-
|
79
|
+
File.open(dest, "wb") { |f| f.write(@template.data) }
|
80
80
|
else
|
81
|
-
@
|
81
|
+
@template.data
|
82
82
|
end
|
83
83
|
|
84
84
|
end
|
85
85
|
|
86
|
-
|
87
|
-
|
88
|
-
def parse_document(txt)
|
89
|
-
doc = Nokogiri::XML(txt)
|
90
|
-
yield doc
|
91
|
-
txt.replace(doc.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML))
|
86
|
+
def all_images
|
87
|
+
@all_images ||= (@images.map(&:files) + @sections.map(&:all_images) + @tables.map(&:all_images)).flatten.uniq
|
92
88
|
end
|
93
89
|
|
94
90
|
end
|
data/lib/odf-report/section.rb
CHANGED
@@ -1,39 +1,21 @@
|
|
1
1
|
module ODFReport
|
2
|
+
class Section < Nestable
|
2
3
|
|
3
|
-
|
4
|
-
include Nested
|
4
|
+
def replace!(doc)
|
5
5
|
|
6
|
-
|
7
|
-
@name = opts[:name]
|
8
|
-
@collection_field = opts[:collection_field]
|
9
|
-
@collection = opts[:collection]
|
6
|
+
return unless find_section_node(doc)
|
10
7
|
|
11
|
-
@
|
12
|
-
@texts = []
|
13
|
-
@tables = []
|
14
|
-
@sections = []
|
8
|
+
@data_source.each do |record|
|
15
9
|
|
16
|
-
|
17
|
-
|
18
|
-
def replace!(doc, row = nil)
|
19
|
-
|
20
|
-
return unless @section_node = find_section_node(doc)
|
21
|
-
|
22
|
-
@collection = get_collection_from_item(row, @collection_field) if row
|
23
|
-
|
24
|
-
@collection.each do |data_item|
|
25
|
-
|
26
|
-
new_section = get_section_node
|
27
|
-
|
28
|
-
@tables.each { |t| t.replace!(new_section, data_item) }
|
10
|
+
new_section = deep_clone(@section_node)
|
29
11
|
|
30
|
-
@
|
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) }
|
31
17
|
|
32
|
-
@
|
33
|
-
|
34
|
-
@fields.each { |f| f.replace!(new_section, data_item) }
|
35
|
-
|
36
|
-
@section_node.before(new_section)
|
18
|
+
@section_node.before(new_section.to_xml)
|
37
19
|
|
38
20
|
end
|
39
21
|
|
@@ -44,23 +26,14 @@ module ODFReport
|
|
44
26
|
private
|
45
27
|
|
46
28
|
def find_section_node(doc)
|
47
|
-
|
48
|
-
sections = doc.xpath(".//text:section[@text:name='#{@name}']")
|
49
|
-
|
50
|
-
sections.empty? ? nil : sections.first
|
51
|
-
|
29
|
+
@section_node = doc.at_css("text|section[@text|name='#{@name}']")
|
52
30
|
end
|
53
31
|
|
54
|
-
def
|
55
|
-
node
|
56
|
-
|
57
|
-
name = node.get_attribute('text:name').to_s
|
58
|
-
@idx ||=0; @idx +=1
|
59
|
-
node.set_attribute('text:name', "#{name}_#{@idx}")
|
32
|
+
def deep_clone(node)
|
33
|
+
Nokogiri::XML(wrap_with_ns(node)).at("text|section")
|
34
|
+
.tap { |n| n.attribute('name').content = SecureRandom.uuid }
|
60
35
|
|
61
|
-
node
|
62
36
|
end
|
63
37
|
|
64
38
|
end
|
65
|
-
|
66
39
|
end
|
data/lib/odf-report/table.rb
CHANGED
@@ -1,88 +1,83 @@
|
|
1
1
|
module ODFReport
|
2
|
+
class Table < Nestable
|
2
3
|
|
3
|
-
|
4
|
-
|
4
|
+
def initialize(opts)
|
5
|
+
super(opts)
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
@template_rows = []
|
8
|
+
@header = opts[:header] || false
|
9
|
+
@skip_if_empty = opts[:skip_if_empty] || false
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
@tables = []
|
12
|
+
def replace!(doc)
|
13
|
+
return unless table = find_table_node(doc)
|
14
14
|
|
15
|
-
|
16
|
-
@header = opts[:header] || false
|
17
|
-
@skip_if_empty = opts[:skip_if_empty] || false
|
18
|
-
end
|
15
|
+
@template_rows = table.xpath("table:table-row")
|
19
16
|
|
20
|
-
|
17
|
+
@header = table.xpath("table:table-header-rows").empty? ? @header : false
|
21
18
|
|
22
|
-
|
19
|
+
if @skip_if_empty && @data_source.empty?
|
20
|
+
table.remove
|
21
|
+
return
|
22
|
+
end
|
23
23
|
|
24
|
-
|
24
|
+
@data_source.each do |record|
|
25
25
|
|
26
|
-
|
26
|
+
new_node = get_next_row
|
27
27
|
|
28
|
+
@tables.each { |n| n.set_source(record).replace!(new_node) }
|
29
|
+
@sections.each { |n| n.set_source(record).replace!(new_node) }
|
30
|
+
@texts.each { |n| n.set_source(record).replace!(new_node) }
|
31
|
+
@fields.each { |n| n.set_source(record).replace!(new_node) }
|
32
|
+
@images.each { |n| n.set_source(record).replace!(new_node) }
|
28
33
|
|
29
|
-
|
34
|
+
table.add_child(new_node.to_xml)
|
30
35
|
|
31
|
-
|
32
|
-
table.remove
|
33
|
-
return
|
34
|
-
end
|
36
|
+
end
|
35
37
|
|
36
|
-
|
38
|
+
@template_rows.each_with_index do |r, i|
|
39
|
+
r.remove if (get_start_node..template_length) === i
|
40
|
+
end
|
37
41
|
|
38
|
-
|
42
|
+
end # replace
|
39
43
|
|
40
|
-
|
44
|
+
private
|
41
45
|
|
42
|
-
|
46
|
+
def get_next_row
|
47
|
+
if @template_rows.size == 1
|
43
48
|
|
44
|
-
|
49
|
+
ret = @template_rows.first
|
45
50
|
|
46
|
-
|
51
|
+
else
|
52
|
+
@row_cursor = get_start_node unless defined?(@row_cursor)
|
47
53
|
|
48
|
-
|
54
|
+
ret = @template_rows[@row_cursor]
|
55
|
+
if @template_rows.size == @row_cursor + 1
|
56
|
+
@row_cursor = get_start_node
|
57
|
+
else
|
58
|
+
@row_cursor += 1
|
59
|
+
end
|
60
|
+
end
|
49
61
|
|
50
|
-
|
51
|
-
|
62
|
+
return deep_clone(ret)
|
63
|
+
# return ret.dup
|
52
64
|
end
|
53
65
|
|
54
|
-
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
def get_next_row
|
59
|
-
@row_cursor = get_start_node unless defined?(@row_cursor)
|
60
|
-
|
61
|
-
ret = @template_rows[@row_cursor]
|
62
|
-
if @template_rows.size == @row_cursor + 1
|
63
|
-
@row_cursor = get_start_node
|
64
|
-
else
|
65
|
-
@row_cursor += 1
|
66
|
+
def get_start_node
|
67
|
+
@header ? 1 : 0
|
66
68
|
end
|
67
|
-
return ret.dup
|
68
|
-
end
|
69
|
-
|
70
|
-
def get_start_node
|
71
|
-
@header ? 1 : 0
|
72
|
-
end
|
73
|
-
|
74
|
-
def template_length
|
75
|
-
@tl ||= @template_rows.size
|
76
|
-
end
|
77
69
|
|
78
|
-
|
70
|
+
def template_length
|
71
|
+
@tl ||= @template_rows.size
|
72
|
+
end
|
79
73
|
|
80
|
-
|
74
|
+
def find_table_node(doc)
|
75
|
+
doc.at_css("table|table[@table|name='#{@name}']")
|
76
|
+
end
|
81
77
|
|
82
|
-
|
78
|
+
def deep_clone(node)
|
79
|
+
Nokogiri::XML(wrap_with_ns(node)).at("table|table-row")
|
80
|
+
end
|
83
81
|
|
84
82
|
end
|
85
|
-
|
86
|
-
end
|
87
|
-
|
88
83
|
end
|