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.
- checksums.yaml +5 -13
- data/.github/workflows/gem-push.yml +40 -0
- data/.gitignore +2 -0
- data/.rspec +4 -0
- data/CHANGELOG.md +56 -0
- data/README.md +220 -0
- data/Rakefile +8 -0
- data/bin/odt-extract +10 -0
- data/bin/odt-viewer +18 -0
- data/lib/odf-report.rb +11 -9
- data/lib/odf-report/data_source.rb +65 -0
- data/lib/odf-report/field.rb +35 -36
- data/lib/odf-report/image.rb +57 -0
- data/lib/odf-report/nestable.rb +65 -0
- data/lib/odf-report/parser/default.rb +5 -4
- data/lib/odf-report/report.rb +29 -57
- data/lib/odf-report/section.rb +17 -80
- data/lib/odf-report/table.rb +52 -81
- data/lib/odf-report/template.rb +88 -0
- data/lib/odf-report/text.rb +2 -4
- data/lib/odf-report/version.rb +1 -1
- data/odf-report.gemspec +7 -4
- data/spec/fields_spec.rb +77 -0
- 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/{test → spec/images}/piriapolis.jpg +0 -0
- data/spec/images/placeholder.jpg +0 -0
- data/{test → 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 +43 -0
- data/spec/tables_spec.rb +39 -0
- data/spec/template_spec.rb +45 -0
- data/spec/templates/images.odt +0 -0
- data/spec/templates/specs.odt +0 -0
- data/test/fields_inside_text_test.rb +38 -0
- data/test/images_test.rb +32 -0
- data/test/nested_tables_test.rb +43 -0
- data/test/sections_test.rb +44 -0
- data/test/sub_sections_test.rb +58 -0
- data/test/table_headers_test.rb +41 -0
- data/test/tables_test.rb +67 -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/piriapolis.jpg +0 -0
- data/test/templates/rails.png +0 -0
- data/test/templates/test_images.odt +0 -0
- data/test/templates/test_sub_sections.odt +0 -0
- data/test/templates/test_text.odt +0 -0
- data/test/test.rb +262 -0
- data/test/text_test.rb +56 -0
- metadata +151 -46
- data/README.textile +0 -225
- data/lib/odf-report/fields.rb +0 -40
- data/lib/odf-report/file.rb +0 -50
- data/lib/odf-report/images.rb +0 -44
- data/lib/odf-report/nested.rb +0 -34
- data/test/test_fields_inside_text.rb +0 -37
- data/test/test_nested_tables.rb +0 -39
- data/test/test_sections.rb +0 -39
- data/test/test_sub_sections.rb +0 -57
- data/test/test_table_headers.rb +0 -39
- data/test/test_tables.rb +0 -62
- data/test/test_text.rb +0 -48
data/lib/odf-report/field.rb
CHANGED
@@ -1,56 +1,55 @@
|
|
1
1
|
module ODFReport
|
2
|
+
class Field
|
2
3
|
|
3
|
-
|
4
|
+
DELIMITERS = %w([ ])
|
4
5
|
|
5
|
-
|
6
|
+
def initialize(opts, &block)
|
7
|
+
@name = opts[:name]
|
8
|
+
@data_source = DataSource.new(opts, &block)
|
9
|
+
end
|
6
10
|
|
7
|
-
|
8
|
-
|
9
|
-
|
11
|
+
def set_source(record)
|
12
|
+
@data_source.set_source(record)
|
13
|
+
self
|
14
|
+
end
|
10
15
|
|
11
|
-
|
16
|
+
def replace!(content, data_item = nil)
|
12
17
|
|
13
|
-
|
14
|
-
@block = block
|
18
|
+
txt = content.inner_html
|
15
19
|
|
16
|
-
|
17
|
-
@block = lambda { |item| self.extract_value(item) }
|
18
|
-
end
|
20
|
+
txt.gsub!(to_placeholder, sanitize(@data_source.value))
|
19
21
|
|
20
|
-
|
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
|
-
|
37
|
-
return unless data_item
|
26
|
+
private
|
38
27
|
|
39
|
-
|
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
|
-
|
42
|
-
|
36
|
+
def sanitize(txt)
|
37
|
+
txt = html_escape(txt)
|
38
|
+
txt = odf_linebreak(txt)
|
39
|
+
txt
|
40
|
+
end
|
43
41
|
|
44
|
-
|
45
|
-
data_item.send(key.to_s.downcase.to_sym)
|
42
|
+
HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"' }
|
46
43
|
|
47
|
-
|
48
|
-
|
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
|
63
|
-
text.gsub!(/<em
|
64
|
-
text.gsub!(/<u
|
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
|
data/lib/odf-report/report.rb
CHANGED
@@ -1,38 +1,35 @@
|
|
1
1
|
module ODFReport
|
2
2
|
|
3
3
|
class Report
|
4
|
-
include Fields, Images
|
5
4
|
|
6
|
-
|
5
|
+
def initialize(template_name = nil, io: nil)
|
7
6
|
|
8
|
-
|
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
|
-
|
14
|
+
@images = []
|
15
|
+
|
16
|
+
yield(self) if block_given?
|
20
17
|
|
21
18
|
end
|
22
19
|
|
23
|
-
def add_field(field_tag, value=''
|
20
|
+
def add_field(field_tag, value='')
|
24
21
|
opts = {:name => field_tag, :value => value}
|
25
|
-
field = Field.new(opts
|
22
|
+
field = Field.new(opts)
|
26
23
|
@fields << field
|
27
24
|
end
|
28
25
|
|
29
|
-
def add_text(field_tag, value=''
|
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={}
|
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={}
|
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(
|
52
|
-
|
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
|
-
@
|
56
|
+
@template.update_content do |file|
|
58
57
|
|
59
|
-
file.update_files
|
58
|
+
file.update_files do |doc|
|
60
59
|
|
61
|
-
|
60
|
+
@sections.each { |c| c.replace!(doc) }
|
61
|
+
@tables.each { |c| c.replace!(doc) }
|
62
62
|
|
63
|
-
|
64
|
-
|
63
|
+
@texts.each { |c| c.replace!(doc) }
|
64
|
+
@fields.each { |c| c.replace!(doc) }
|
65
65
|
|
66
|
-
|
67
|
-
replace_tables!(doc)
|
66
|
+
@images.each { |c| c.replace!(doc) }
|
68
67
|
|
69
|
-
|
70
|
-
avoid_duplicate_image_names(doc)
|
68
|
+
end
|
71
69
|
|
72
|
-
|
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
|
-
|
79
|
+
File.open(dest, "wb") { |f| f.write(@template.data) }
|
82
80
|
else
|
83
|
-
@
|
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
|
113
|
-
@sections.
|
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
|
data/lib/odf-report/section.rb
CHANGED
@@ -1,102 +1,39 @@
|
|
1
1
|
module ODFReport
|
2
|
+
class Section < Nestable
|
2
3
|
|
3
|
-
|
4
|
-
include Fields, Nested
|
4
|
+
def replace!(doc)
|
5
5
|
|
6
|
-
|
6
|
+
return unless find_section_node(doc)
|
7
7
|
|
8
|
-
|
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
|
-
|
15
|
-
@texts = []
|
10
|
+
new_section = deep_clone(@section_node)
|
16
11
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|