cure-odf-report 0.5.1b
Sign up to get free protection for your applications and to get access to all the features.
- 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
|