cure-odf-report 0.5.1b
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 +7 -0
- data/.gitignore +6 -0
- data/.rspec +4 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/Manifest +12 -0
- data/README.textile +223 -0
- data/Rakefile +9 -0
- data/lib/odf-report.rb +16 -0
- data/lib/odf-report/actions/remove_section.rb +22 -0
- data/lib/odf-report/field.rb +88 -0
- data/lib/odf-report/file.rb +50 -0
- data/lib/odf-report/images.rb +44 -0
- data/lib/odf-report/nested.rb +62 -0
- data/lib/odf-report/parser/default.rb +91 -0
- data/lib/odf-report/report.rb +103 -0
- data/lib/odf-report/section.rb +64 -0
- data/lib/odf-report/table.rb +88 -0
- data/lib/odf-report/text.rb +43 -0
- data/lib/odf-report/version.rb +3 -0
- data/odf-report.gemspec +31 -0
- data/spec/fields_spec.rb +77 -0
- data/spec/result/specs.odt +0 -0
- data/spec/result/tables.rb +38 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/specs.odt +0 -0
- data/spec/tables_spec.rb +39 -0
- data/test/fields_inside_text_test.rb +38 -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/piriapolis.jpg +0 -0
- data/test/templates/rails.png +0 -0
- data/test/templates/test_fields_inside_text.odt +0 -0
- data/test/templates/test_nested_tables.odt +0 -0
- data/test/templates/test_sections.odt +0 -0
- data/test/templates/test_sub_sections.odt +0 -0
- data/test/templates/test_table_headers.odt +0 -0
- data/test/templates/test_tables.odt +0 -0
- data/test/templates/test_text.odt +0 -0
- data/test/text_test.rb +56 -0
- metadata +204 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
module ODFReport
|
2
|
+
|
3
|
+
module Images
|
4
|
+
|
5
|
+
IMAGE_DIR_NAME = "Pictures"
|
6
|
+
|
7
|
+
def find_image_name_matches(content)
|
8
|
+
|
9
|
+
@images.each_pair do |image_name, path|
|
10
|
+
if node = content.xpath("//draw:frame[@draw:name='#{image_name}']/draw:image").first
|
11
|
+
placeholder_path = node.attribute('href').value
|
12
|
+
@image_names_replacements[path] = ::File.join(IMAGE_DIR_NAME, ::File.basename(placeholder_path))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
def replace_images(file)
|
19
|
+
|
20
|
+
return if @images.empty?
|
21
|
+
|
22
|
+
@image_names_replacements.each_pair do |path, template_image|
|
23
|
+
|
24
|
+
file.output_stream.put_next_entry(template_image)
|
25
|
+
file.output_stream.write ::File.read(path)
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end # replace_images
|
30
|
+
|
31
|
+
# newer versions of LibreOffice can't open files with duplicates image names
|
32
|
+
def avoid_duplicate_image_names(content)
|
33
|
+
|
34
|
+
nodes = content.xpath("//draw:frame[@draw:name]")
|
35
|
+
|
36
|
+
nodes.each_with_index do |node, i|
|
37
|
+
node.attribute('name').value = "pic_#{i}"
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module ODFReport
|
2
|
+
|
3
|
+
module Nested
|
4
|
+
|
5
|
+
def add_field(name, data_field=nil, &block)
|
6
|
+
opts = {:name => name, :data_field => data_field}
|
7
|
+
field = Field.new(opts, &block)
|
8
|
+
@fields << field
|
9
|
+
|
10
|
+
end
|
11
|
+
alias_method :add_column, :add_field
|
12
|
+
|
13
|
+
def add_text(name, data_field=nil, &block)
|
14
|
+
opts = {:name => name, :data_field => data_field}
|
15
|
+
field = Text.new(opts, &block)
|
16
|
+
@texts << field
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
def add_table(table_name, collection_field, opts={})
|
21
|
+
opts.merge!(:name => table_name, :collection_field => collection_field)
|
22
|
+
tab = Table.new(opts)
|
23
|
+
@tables << tab
|
24
|
+
|
25
|
+
yield(tab)
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_section(section_name, collection_field, opts={})
|
29
|
+
opts.merge!(:name => section_name, :collection_field => collection_field)
|
30
|
+
sec = Section.new(opts)
|
31
|
+
@sections << sec
|
32
|
+
|
33
|
+
yield(sec)
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def get_collection_from_item(item, collection_field)
|
38
|
+
|
39
|
+
return item[collection_field] if item.is_a?(Hash)
|
40
|
+
|
41
|
+
if collection_field.is_a?(Array)
|
42
|
+
tmp = item.dup
|
43
|
+
collection_field.each do |f|
|
44
|
+
if f.is_a?(Hash)
|
45
|
+
tmp = tmp.send(f.keys[0], f.values[0])
|
46
|
+
else
|
47
|
+
tmp = tmp.send(f)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
collection = tmp
|
51
|
+
elsif collection_field.is_a?(Hash)
|
52
|
+
collection = item.send(collection_field.keys[0], collection_field.values[0])
|
53
|
+
else
|
54
|
+
collection = item.send(collection_field)
|
55
|
+
end
|
56
|
+
|
57
|
+
return collection
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module ODFReport
|
2
|
+
|
3
|
+
module Parser
|
4
|
+
|
5
|
+
|
6
|
+
# Default HTML parser
|
7
|
+
#
|
8
|
+
# sample HTML
|
9
|
+
#
|
10
|
+
# <p> first paragraph </p>
|
11
|
+
# <p> second <strong>paragraph</strong> </p>
|
12
|
+
# <blockquote>
|
13
|
+
# <p> first <em>quote paragraph</em> </p>
|
14
|
+
# <p> first quote paragraph </p>
|
15
|
+
# <p> first quote paragraph </p>
|
16
|
+
# </blockquote>
|
17
|
+
# <p> third <strong>paragraph</strong> </p>
|
18
|
+
#
|
19
|
+
# <p style="margin: 100px"> fourth <em>paragraph</em> </p>
|
20
|
+
# <p style="margin: 120px"> fifth paragraph </p>
|
21
|
+
# <p> sixth <strong>paragraph</strong> </p>
|
22
|
+
#
|
23
|
+
|
24
|
+
class Default
|
25
|
+
|
26
|
+
attr_accessor :paragraphs
|
27
|
+
|
28
|
+
def initialize(text, template_node)
|
29
|
+
@text = text
|
30
|
+
@paragraphs = []
|
31
|
+
@template_node = template_node
|
32
|
+
|
33
|
+
parse
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse
|
37
|
+
|
38
|
+
xml = @template_node.parse(@text)
|
39
|
+
|
40
|
+
xml.css("p", "h1", "h2").each do |p|
|
41
|
+
|
42
|
+
style = check_style(p)
|
43
|
+
text = parse_formatting(p.inner_html)
|
44
|
+
|
45
|
+
add_paragraph(text, style)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_paragraph(text, style)
|
50
|
+
|
51
|
+
node = @template_node.dup
|
52
|
+
|
53
|
+
node['text:style-name'] = style if style
|
54
|
+
node.children = text
|
55
|
+
|
56
|
+
@paragraphs << node
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def parse_formatting(text)
|
62
|
+
text.strip!
|
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>" }
|
66
|
+
text.gsub!("\n", "")
|
67
|
+
text
|
68
|
+
end
|
69
|
+
|
70
|
+
def check_style(node)
|
71
|
+
style = nil
|
72
|
+
|
73
|
+
if node.name =~ /h\d/i
|
74
|
+
style = "title"
|
75
|
+
|
76
|
+
elsif node.parent && node.parent.name == "blockquote"
|
77
|
+
style = "quote"
|
78
|
+
|
79
|
+
elsif node['style'] =~ /margin/
|
80
|
+
style = "quote"
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
style
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module ODFReport
|
2
|
+
|
3
|
+
class Report
|
4
|
+
include Images
|
5
|
+
|
6
|
+
def initialize(template_name, &block)
|
7
|
+
|
8
|
+
@file = ODFReport::File.new(template_name)
|
9
|
+
|
10
|
+
@texts = []
|
11
|
+
@fields = []
|
12
|
+
@tables = []
|
13
|
+
@images = {}
|
14
|
+
@image_names_replacements = {}
|
15
|
+
@sections = []
|
16
|
+
@actions = []
|
17
|
+
|
18
|
+
yield(self)
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_field(field_tag, value='')
|
23
|
+
opts = {:name => field_tag, :value => value}
|
24
|
+
field = Field.new(opts)
|
25
|
+
@fields << field
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_text(field_tag, value='')
|
29
|
+
opts = {:name => field_tag, :value => value}
|
30
|
+
text = Text.new(opts)
|
31
|
+
@texts << text
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_table(table_name, collection, opts={})
|
35
|
+
opts.merge!(:name => table_name, :collection => collection)
|
36
|
+
tab = Table.new(opts)
|
37
|
+
@tables << tab
|
38
|
+
|
39
|
+
yield(tab)
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_section(section_name, collection, opts={})
|
43
|
+
opts.merge!(:name => section_name, :collection => collection)
|
44
|
+
sec = Section.new(opts)
|
45
|
+
@sections << sec
|
46
|
+
|
47
|
+
yield(sec)
|
48
|
+
end
|
49
|
+
|
50
|
+
def remove_section(section_name)
|
51
|
+
@actions << ODFReport::Actions::RemoveSection.new(section_name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_image(name, path)
|
55
|
+
@images[name] = path
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate(dest = nil)
|
59
|
+
|
60
|
+
@file.update_content do |file|
|
61
|
+
|
62
|
+
file.update_files('content.xml', 'styles.xml') do |txt|
|
63
|
+
|
64
|
+
parse_document(txt) do |doc|
|
65
|
+
|
66
|
+
@sections.each { |s| s.replace!(doc) }
|
67
|
+
@tables.each { |t| t.replace!(doc) }
|
68
|
+
|
69
|
+
@texts.each { |t| t.replace!(doc) }
|
70
|
+
@fields.each { |f| f.replace!(doc) }
|
71
|
+
|
72
|
+
@actions.each { |action| action.process!(doc) }
|
73
|
+
|
74
|
+
find_image_name_matches(doc)
|
75
|
+
avoid_duplicate_image_names(doc)
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
replace_images(file)
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
if dest
|
86
|
+
::File.open(dest, "wb") {|f| f.write(@file.data) }
|
87
|
+
else
|
88
|
+
@file.data
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def parse_document(txt)
|
96
|
+
doc = Nokogiri::XML(txt)
|
97
|
+
yield doc
|
98
|
+
txt.replace(doc.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML))
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ODFReport
|
2
|
+
|
3
|
+
class Section
|
4
|
+
include Nested
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
@name = opts[:name].to_s.upcase
|
8
|
+
@collection_field = opts[:collection_field]
|
9
|
+
@collection = opts[:collection]
|
10
|
+
|
11
|
+
@fields = []
|
12
|
+
@texts = []
|
13
|
+
@tables = []
|
14
|
+
@sections = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def replace!(doc, row = nil)
|
18
|
+
return unless @section_node = find_section_node(doc)
|
19
|
+
|
20
|
+
@collection = get_collection_from_item(row, @collection_field) if row
|
21
|
+
|
22
|
+
@collection.each do |data_item|
|
23
|
+
|
24
|
+
new_section = get_section_node
|
25
|
+
|
26
|
+
@tables.each { |t| t.replace!(new_section, data_item) }
|
27
|
+
|
28
|
+
@sections.each { |s| s.replace!(new_section, data_item) }
|
29
|
+
|
30
|
+
@texts.each { |t| t.replace!(new_section, data_item) }
|
31
|
+
|
32
|
+
@fields.each { |f| f.replace!(new_section, data_item) }
|
33
|
+
|
34
|
+
@section_node.before(new_section)
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
@section_node.remove
|
39
|
+
|
40
|
+
end # replace_section
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def find_section_node(doc)
|
45
|
+
|
46
|
+
sections = doc.xpath(".//text:section[@text:name='#{@name}']")
|
47
|
+
|
48
|
+
sections.empty? ? nil : sections.first
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_section_node
|
53
|
+
node = @section_node.dup
|
54
|
+
|
55
|
+
name = node.get_attribute('text:name').to_s
|
56
|
+
@idx ||=0; @idx +=1
|
57
|
+
node.set_attribute('text:name', "#{name}_#{@idx}")
|
58
|
+
|
59
|
+
node
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module ODFReport
|
2
|
+
|
3
|
+
class Table
|
4
|
+
include Nested
|
5
|
+
|
6
|
+
def initialize(opts)
|
7
|
+
@name = opts[:name]
|
8
|
+
@collection_field = opts[:collection_field]
|
9
|
+
@collection = opts[:collection]
|
10
|
+
|
11
|
+
@fields = []
|
12
|
+
@texts = []
|
13
|
+
@tables = []
|
14
|
+
|
15
|
+
@template_rows = []
|
16
|
+
@header = opts[:header] || false
|
17
|
+
@skip_if_empty = opts[:skip_if_empty] || false
|
18
|
+
end
|
19
|
+
|
20
|
+
def replace!(doc, row = nil)
|
21
|
+
|
22
|
+
return unless table = find_table_node(doc)
|
23
|
+
|
24
|
+
@template_rows = table.xpath("table:table-row")
|
25
|
+
|
26
|
+
@header = table.xpath("table:table-header-rows").empty? ? @header : false
|
27
|
+
|
28
|
+
|
29
|
+
@collection = get_collection_from_item(row, @collection_field) if row
|
30
|
+
|
31
|
+
if @skip_if_empty && @collection.empty?
|
32
|
+
table.remove
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
@collection.each do |data_item|
|
37
|
+
|
38
|
+
new_node = get_next_row
|
39
|
+
|
40
|
+
@tables.each { |t| t.replace!(new_node, data_item) }
|
41
|
+
|
42
|
+
@texts.each { |t| t.replace!(new_node, data_item) }
|
43
|
+
|
44
|
+
@fields.each { |f| f.replace!(new_node, data_item) }
|
45
|
+
|
46
|
+
table.add_child(new_node)
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
@template_rows.each_with_index do |r, i|
|
51
|
+
r.remove if (get_start_node..template_length) === i
|
52
|
+
end
|
53
|
+
|
54
|
+
end # replace
|
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
|
+
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
|
+
|
78
|
+
def find_table_node(doc)
|
79
|
+
|
80
|
+
tables = doc.xpath(".//table:table[@table:name='#{@name}']")
|
81
|
+
|
82
|
+
tables.empty? ? nil : tables.first
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|