energon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +7 -0
- data/README +31 -0
- data/lib/energon/document.rb +105 -0
- data/lib/energon/excel_document.rb +138 -0
- data/lib/energon/od_document.rb +90 -0
- data/lib/energon/open_document_helper.rb +65 -0
- data/lib/energon/open_xml_helper.rb +210 -0
- data/lib/energon/parser.rb +262 -0
- data/lib/energon/word_document.rb +94 -0
- data/lib/energon.rb +24 -0
- data/rakefile.rb +59 -0
- data/test/test_document.rb +150 -0
- data/test/test_excel_document.rb +36 -0
- data/test/test_oo_document.rb +49 -0
- data/test/test_open_document_helper.rb +55 -0
- data/test/test_open_xml_helper.rb +95 -0
- data/test/test_word_document.rb +36 -0
- metadata +86 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2006 Woa! Kft <energon@woa.hu>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
=Energon
|
2
|
+
|
3
|
+
|
4
|
+
Energon is a report engine written in ruby. It allows you to
|
5
|
+
create reliable and advanced reports from your templates.
|
6
|
+
Both templates and output documents are based on OpenXML and OpenDocument formats.
|
7
|
+
|
8
|
+
Energon supports currently Text, Word and Excel documents.
|
9
|
+
|
10
|
+
1. Create a template of your choice (Text, Word, Excel).
|
11
|
+
2. Add placeholders into your document, those placeholders will be replaced later by some data. Note that you can add some formatting instructions on your placeholders.
|
12
|
+
3. Merge your data and your template. Data source is a simple ruby Hash, and thus can be created for instance from some ActiveRecord data.
|
13
|
+
4. Save your output, it's all that simple!
|
14
|
+
|
15
|
+
===Installation
|
16
|
+
To install the lastest Energon, just type:
|
17
|
+
gem install energon
|
18
|
+
|
19
|
+
===Changes
|
20
|
+
0.0.1: First release
|
21
|
+
|
22
|
+
===License
|
23
|
+
Energon is released under the MIT License
|
24
|
+
|
25
|
+
===Woa! Kft
|
26
|
+
|
27
|
+
Woa! Kft is a hungarian based company, and provides top-notch Rails development at affordable prices for individuals and small to medium size businesses.
|
28
|
+
|
29
|
+
===Support
|
30
|
+
Any questions, enhancement proposals, bug notifications or corrections can be sent to mailto:energon@woa.hu.
|
31
|
+
You can visit our website: http://www.woa.hu.
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'energon/parser'
|
2
|
+
|
3
|
+
module Woa; module Energon
|
4
|
+
class EnergonError < StandardError #:nodoc:
|
5
|
+
end
|
6
|
+
class NoPlaceholderFound < StandardError #:nodoc:
|
7
|
+
end
|
8
|
+
|
9
|
+
# This class is the fronthead class for Energon. It's the main class
|
10
|
+
# and it executes the hight level stuffs. It deals with text files. Subclasses deal with
|
11
|
+
# other kind of templates:
|
12
|
+
# * ExcelDocument for Excel templates
|
13
|
+
# * WordDocument for Word templates
|
14
|
+
# * OdDocument for OpenDocument (OpenOffice write and spreadsheet)
|
15
|
+
#
|
16
|
+
# :include: rdoc-header
|
17
|
+
#
|
18
|
+
# == Examples
|
19
|
+
#
|
20
|
+
# require 'energon'
|
21
|
+
# include Woa::Energon
|
22
|
+
# document = Document.new("@author@, @date@, @country@")
|
23
|
+
# document.add_values({:author => "Woa! Kft", :date => Time.now})
|
24
|
+
# document.add_value(:country => "Hungary")
|
25
|
+
# puts document.write #"Woa! Kft, Wed Nov 29 11:31:52 CET 2006, Hungary"
|
26
|
+
class Document
|
27
|
+
|
28
|
+
DefaultDelimiter = '@'
|
29
|
+
DefaultNewLine = "\n"
|
30
|
+
|
31
|
+
attr_accessor :delimiter
|
32
|
+
attr_accessor :new_line
|
33
|
+
|
34
|
+
# Just pass the template and specify if the delimiter is not the one by default ("@")
|
35
|
+
def initialize(template, delimiter=DefaultDelimiter)
|
36
|
+
raise EnergonError if delimiter.to_s.empty?
|
37
|
+
@delimiter = delimiter.to_s
|
38
|
+
@new_line = DefaultNewLine
|
39
|
+
@datas = {}
|
40
|
+
@template = template
|
41
|
+
@placeholders = []
|
42
|
+
extract_placeholders
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add a data
|
46
|
+
# * key is the name of the data
|
47
|
+
# * value is the data
|
48
|
+
def add_value(key, value)
|
49
|
+
@datas[key] = value
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add several datas from a Hash.
|
53
|
+
# * the key is the name of the data
|
54
|
+
# * the value is the data
|
55
|
+
def add_values(hash)
|
56
|
+
hash.each {|key, value| self.add_value(key, value)}
|
57
|
+
end
|
58
|
+
|
59
|
+
# Is the template valid ? It's valid when
|
60
|
+
# * there is at least one data
|
61
|
+
# * there is at least one placeholder in the template
|
62
|
+
# * there is at least one data for each placeholder
|
63
|
+
#
|
64
|
+
# It will return true or false.
|
65
|
+
def valid?
|
66
|
+
raise NoPlaceholderFound if @placeholders.empty?
|
67
|
+
@placeholders.each do |placeholder|
|
68
|
+
begin
|
69
|
+
Parser.merge(placeholder, @datas)
|
70
|
+
rescue NoDataFound
|
71
|
+
return(false)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Generate the final document.
|
78
|
+
# Raise an exception if an error occurs
|
79
|
+
#
|
80
|
+
# It's possible to specify the ouput to write directly in a specific stream.
|
81
|
+
def write(output=nil)
|
82
|
+
raise NoPlaceholderFound if @placeholders.empty?
|
83
|
+
@placeholders.each do |placeholder|
|
84
|
+
data = Parser.merge(placeholder, @datas)
|
85
|
+
data = data.join(@new_line) if data.is_a?(Array)
|
86
|
+
@template.gsub!("#{@delimiter}#{placeholder}#{@delimiter}", data)
|
87
|
+
end
|
88
|
+
return(@template) if output.nil?
|
89
|
+
output.puts @template
|
90
|
+
end
|
91
|
+
|
92
|
+
alias :close :write
|
93
|
+
|
94
|
+
#########
|
95
|
+
private #
|
96
|
+
#########
|
97
|
+
|
98
|
+
def extract_placeholders
|
99
|
+
# the regexp is : /@((\@|[^@])*)@/
|
100
|
+
@template.scan(Regexp.new("#{@delimiter}((#{Regexp.escape('\\' + @delimiter)}|[^#{@delimiter}])*)#{@delimiter}")) do |match, non_used|
|
101
|
+
@placeholders << match unless @placeholders.include?(match)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end; end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'energon/document'
|
2
|
+
require 'energon/open_xml_helper'
|
3
|
+
require 'rexml/document'
|
4
|
+
include REXML
|
5
|
+
|
6
|
+
module Woa; module Energon;
|
7
|
+
# This is a subclass of Document but it deals with Excel templates
|
8
|
+
# instead of Text templates.
|
9
|
+
#
|
10
|
+
# Everything is the same as Document (Except the input and the output).
|
11
|
+
# See Document for more details.
|
12
|
+
#
|
13
|
+
# :include: rdoc-header
|
14
|
+
class ExcelDocument < Document
|
15
|
+
|
16
|
+
def write()
|
17
|
+
raise NoPlaceholderFound if @placeholders.empty?
|
18
|
+
rows = {}
|
19
|
+
@placeholders.each do |placeholder_hash|
|
20
|
+
placeholder = placeholder_hash[:placeholder]
|
21
|
+
data = Parser.merge(placeholder, @datas)
|
22
|
+
if data.is_a?(Array)
|
23
|
+
placeholder_hash[:cells].each do |cell|
|
24
|
+
parent = cell
|
25
|
+
begin
|
26
|
+
next unless parent.name == "row"
|
27
|
+
rows[parent] = [] if rows[parent].nil?
|
28
|
+
rows[parent] << {:cell => cell, :data => data}
|
29
|
+
break
|
30
|
+
end while (parent = parent.parent)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
element = placeholder_hash[:shared_string_element]
|
34
|
+
element.text = data
|
35
|
+
end
|
36
|
+
end
|
37
|
+
inserted_rows = -1
|
38
|
+
rows.each do |row, cells|
|
39
|
+
last_row = row
|
40
|
+
index = row.attributes.get_attribute('r').value.to_i
|
41
|
+
continue = true
|
42
|
+
while continue
|
43
|
+
cells.each do |element|
|
44
|
+
datas = element[:data]
|
45
|
+
cell = element[:cell]
|
46
|
+
data = datas.pop
|
47
|
+
if data.nil?
|
48
|
+
continue = false
|
49
|
+
break
|
50
|
+
end
|
51
|
+
cell.text = add_shared_string(data.to_s)
|
52
|
+
end
|
53
|
+
if continue
|
54
|
+
new_row = row.deep_clone
|
55
|
+
new_row.add_attribute('r', index.to_s)
|
56
|
+
XPath.each(new_row, '//c[@r]') do |element|
|
57
|
+
value = element.attributes.get_attribute('r').value.to_s.gsub(/\d+/, index.to_s)
|
58
|
+
element.add_attribute('r', value)
|
59
|
+
end
|
60
|
+
index = index.next
|
61
|
+
row.parent.insert_after(last_row, new_row)
|
62
|
+
last_row = new_row
|
63
|
+
inserted_rows = inserted_rows.next
|
64
|
+
end
|
65
|
+
end
|
66
|
+
root = row.parent
|
67
|
+
initial_index = row.attributes.get_attribute('r').value.to_i
|
68
|
+
row.parent.delete(row)
|
69
|
+
index = last_row.parent.index(last_row)
|
70
|
+
XPath.each(root, 'row') do |element|
|
71
|
+
next if element.parent.index(element) <= index
|
72
|
+
value = element.attributes.get_attribute('r').value.to_i + inserted_rows
|
73
|
+
element.add_attribute('r', value)
|
74
|
+
XPath.each(element, 'c[@r]') do |element|
|
75
|
+
new_value = element.attributes.get_attribute('r').value.to_s.gsub(/\d+/, value.to_s)
|
76
|
+
element.add_attribute('r', new_value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
XPath.each(root, '//mergeCell[@ref]') do |element|
|
80
|
+
value = element.attributes.get_attribute('ref').value.to_s
|
81
|
+
if /([A-Z]+)(\d+):([A-Z]+)(\d+)/ =~ value
|
82
|
+
v1, v2, v3, v4 = Regexp.last_match[1..4]
|
83
|
+
v2 = v2.to_i + inserted_rows if v2.to_i > initial_index
|
84
|
+
v4 = v4.to_i + inserted_rows if v4.to_i > initial_index
|
85
|
+
element.add_attribute('ref', "#{v1}#{v2}:#{v3}#{v4}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
@documents.each {|document| @openxml.save(document) }
|
90
|
+
@openxml.write
|
91
|
+
end
|
92
|
+
alias :close :write
|
93
|
+
|
94
|
+
#############################
|
95
|
+
private
|
96
|
+
#############################
|
97
|
+
def extract_placeholders
|
98
|
+
@openxml = OpenXmlHelper.new_excel(@template)
|
99
|
+
@documents = @openxml.documents
|
100
|
+
|
101
|
+
# cells
|
102
|
+
cells = {}
|
103
|
+
@openxml.worksheets.each do |worksheet|
|
104
|
+
XPath.each(worksheet, '//c[@t="s"]/v') do |cell|
|
105
|
+
index = cell.text
|
106
|
+
cells[index] = [] if cells[index].nil?
|
107
|
+
cells[index] << cell
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# shared strings
|
112
|
+
@shared_strings_xml = @openxml.shared_strings
|
113
|
+
raise NoPlaceholderFound if @shared_strings_xml.nil?
|
114
|
+
@shared_strings = []
|
115
|
+
XPath.each(@shared_strings_xml, "//si/t") do |element|
|
116
|
+
index = element.parent.parent.index(element.parent)
|
117
|
+
@shared_strings << element
|
118
|
+
element.text.to_s.scan(Regexp.new("#{@delimiter}((#{Regexp.escape('\\' + @delimiter)}|[^#{@delimiter}])*)#{@delimiter}")) do |match, non_used|
|
119
|
+
@placeholders << {:placeholder => match, :shared_string_element => element, :index => index, :cells => cells[index.to_s].nil? ? [] : cells[index.to_s]}
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def add_shared_string(str)
|
125
|
+
root = @shared_strings_xml.root
|
126
|
+
t = Element.new('t')
|
127
|
+
t.text = str
|
128
|
+
si = Element.new('si')
|
129
|
+
si << t
|
130
|
+
root << si
|
131
|
+
count = root.attributes.get_attribute('count').value.to_i.next
|
132
|
+
unique_count = root.attributes.get_attribute('uniqueCount').value.to_i.next
|
133
|
+
root.add_attribute('count', count.to_s)
|
134
|
+
root.add_attribute('uniqueCount', unique_count.to_s)
|
135
|
+
count - 1
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end; end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'energon/document'
|
2
|
+
require 'energon/open_document_helper'
|
3
|
+
require 'rexml/document'
|
4
|
+
include REXML
|
5
|
+
|
6
|
+
module Woa; module Energon;
|
7
|
+
# This is a subclass of Document but it deals with OpenDocument
|
8
|
+
# (OpenOffice) templates instead of Text templates. It works
|
9
|
+
# with the writer and the spreadsheet files.
|
10
|
+
#
|
11
|
+
# Everything is the same as Document (Except the input and the output).
|
12
|
+
# See Document for more details.
|
13
|
+
#
|
14
|
+
# :include: rdoc-header
|
15
|
+
class OdDocument < Document
|
16
|
+
def write()
|
17
|
+
raise NoPlaceholderFound if @placeholders.empty?
|
18
|
+
rows = {}
|
19
|
+
|
20
|
+
|
21
|
+
@placeholders.each do |placeholder_hash|
|
22
|
+
placeholder = placeholder_hash[:placeholder]
|
23
|
+
data = Parser.merge(placeholder, @datas)
|
24
|
+
if data.is_a?(Array)
|
25
|
+
element = placeholder_hash[:element]
|
26
|
+
|
27
|
+
parent = XPath.first(element, "ancestor::table:table-row")
|
28
|
+
|
29
|
+
# if not in a table
|
30
|
+
if parent.nil?
|
31
|
+
element.text = element.text.to_s.sub("#{delimiter}#{placeholder}#{delimiter}", data.join)
|
32
|
+
else
|
33
|
+
rows[parent] ||= []
|
34
|
+
rows[parent] << {:element => element, :data => data, :placeholder => placeholder}
|
35
|
+
end
|
36
|
+
else
|
37
|
+
element = placeholder_hash[:element]
|
38
|
+
element.text = element.text.to_s.sub("#{delimiter}#{placeholder}#{delimiter}", data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
originals = {}
|
44
|
+
rows.each do |row, elements|
|
45
|
+
|
46
|
+
continue = true
|
47
|
+
while continue
|
48
|
+
|
49
|
+
originals.each do |element, text|
|
50
|
+
element.text = text
|
51
|
+
originals.delete(element)
|
52
|
+
end
|
53
|
+
|
54
|
+
elements.each do |hash|
|
55
|
+
element = hash[:element]
|
56
|
+
data = hash[:data]
|
57
|
+
placeholder = hash[:placeholder]
|
58
|
+
|
59
|
+
if data.empty?
|
60
|
+
continue = false
|
61
|
+
break
|
62
|
+
end
|
63
|
+
|
64
|
+
originals[element] = element.text.to_s.clone
|
65
|
+
txt = element.text = element.text.to_s.sub("#{delimiter}#{placeholder}#{delimiter}", data.pop)
|
66
|
+
end
|
67
|
+
row.parent.insert_after(row, row.deep_clone) if continue
|
68
|
+
end
|
69
|
+
row.parent.delete_element(row)
|
70
|
+
end
|
71
|
+
|
72
|
+
@odt.save(@content)
|
73
|
+
@odt.write
|
74
|
+
end
|
75
|
+
alias :close :write
|
76
|
+
|
77
|
+
#############################
|
78
|
+
private
|
79
|
+
#############################
|
80
|
+
def extract_placeholders
|
81
|
+
@odt = OpenDocumentHelper.new(@template)
|
82
|
+
@content = @odt.content
|
83
|
+
XPath.each(@content, "//text:p") do |element|
|
84
|
+
element.text.to_s.scan(Regexp.new("#{@delimiter}((#{Regexp.escape('\\' + @delimiter)}|[^#{@delimiter}])*)#{@delimiter}")) do |match, non_used|
|
85
|
+
@placeholders << {:placeholder => match, :element => element}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end; end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'iconv'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'zip/zipfilesystem'
|
6
|
+
include Zip
|
7
|
+
|
8
|
+
module Woa; module Energon
|
9
|
+
class InvalidODFDocument < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
# This class is an interface with OpenDocument files.
|
13
|
+
# It manages the content of the file so that caller classes
|
14
|
+
# just manage xml Objects (ReXml). It's not possible at this
|
15
|
+
# time to work with streams beacause of the zip library
|
16
|
+
# (rubyzip), it has to be done via files.
|
17
|
+
#
|
18
|
+
# :include: rdoc-header
|
19
|
+
class OpenDocumentHelper
|
20
|
+
|
21
|
+
attr_reader :type, :content
|
22
|
+
|
23
|
+
# * template: the name of the file
|
24
|
+
def initialize(template)
|
25
|
+
@zip_file = ZipFile.open(template)
|
26
|
+
@content = xml(read("content.xml")) rescue nil
|
27
|
+
raise InvalidODFDocument if @content.nil?
|
28
|
+
end
|
29
|
+
|
30
|
+
# Save the file content.xml
|
31
|
+
# * content: the xml Object (ReXml)
|
32
|
+
def save(content)
|
33
|
+
@content = content
|
34
|
+
save_xml(@content, "content.xml");
|
35
|
+
end
|
36
|
+
|
37
|
+
# Write the final document (the zipfile) and close the file
|
38
|
+
def write
|
39
|
+
@zip_file.close
|
40
|
+
end
|
41
|
+
|
42
|
+
alias :close :write
|
43
|
+
|
44
|
+
#########################################
|
45
|
+
private
|
46
|
+
#########################################
|
47
|
+
|
48
|
+
# Save a xml file
|
49
|
+
def save_xml(xml, file)
|
50
|
+
@zip_file.file.open(file, 'w') do |f|
|
51
|
+
f.write(xml.to_s.unpack('C*').pack('U*'))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# return the REXML::Document from a file
|
56
|
+
def xml(file)
|
57
|
+
REXML::Document.new(file)
|
58
|
+
end
|
59
|
+
|
60
|
+
# return a File from a file included in the main ZIP file
|
61
|
+
def read(file)
|
62
|
+
@zip_file.file.open(file, 'r') {|f| f.read }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end; end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'iconv'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'zip/zipfilesystem'
|
6
|
+
include Zip
|
7
|
+
|
8
|
+
module Woa; module Energon
|
9
|
+
class InvalidOpenXmlDocument < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
# This class is an interface with OpenXML files. It manages
|
13
|
+
# the content of the file so that caller classes just manage
|
14
|
+
# xml Object (ReXml). It's not possible at this time to work
|
15
|
+
# with streams beacause of the zip library (rubyzip), it has
|
16
|
+
# to be done via files.
|
17
|
+
#
|
18
|
+
# The support is not fully functional because of rubyzip which
|
19
|
+
# is buggy and generates wrong zip file not recognized by Office.
|
20
|
+
#
|
21
|
+
# :include: rdoc-header
|
22
|
+
class OpenXmlHelper
|
23
|
+
|
24
|
+
TypeWord = 0
|
25
|
+
TypeExcel = TypeWord.next
|
26
|
+
|
27
|
+
attr_reader :type, :shared_strings
|
28
|
+
attr_reader :workbook
|
29
|
+
attr_reader :worksheets
|
30
|
+
|
31
|
+
|
32
|
+
############# tempdir ##############
|
33
|
+
def OpenXmlHelper.finalize(tmpdir)
|
34
|
+
lambda do
|
35
|
+
FileUtils.remove_dir(tmpdir, true) if File.exist?(tmpdir)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
############# tempdir ##############
|
39
|
+
|
40
|
+
|
41
|
+
# * ftemplate: the name of the file
|
42
|
+
# * type: the type of the file (TypeWord or TypeExcel)
|
43
|
+
def initialize(template, type)
|
44
|
+
raise InvalidOpenXmlDocument, "Wrong Type" unless type == TypeWord || type == TypeExcel
|
45
|
+
@type = type
|
46
|
+
@template = template
|
47
|
+
|
48
|
+
# @zip_file = ZipFile.open(template)
|
49
|
+
############# tempdir ##############
|
50
|
+
n = 0
|
51
|
+
begin
|
52
|
+
@tmpdir = "tempdir.#{$$}.#{n}.dir"
|
53
|
+
n = n.next
|
54
|
+
end while File.exist?(@tmpdir)
|
55
|
+
|
56
|
+
ObjectSpace.define_finalizer(self, OpenXmlHelper.finalize(@tmpdir))
|
57
|
+
FileUtils.mkdir(@tmpdir)
|
58
|
+
|
59
|
+
ZipFile.foreach(template) do |entry|
|
60
|
+
next if entry.directory?
|
61
|
+
file = "#{@tmpdir}/#{entry.name}"
|
62
|
+
dir = File.dirname(file)
|
63
|
+
FileUtils.mkdir_p(dir) unless File.exist?(dir)
|
64
|
+
File.open(file, 'wb') do |file|
|
65
|
+
entry.get_input_stream {|stream| file.write(stream.read) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
############# tempdir ##############
|
69
|
+
|
70
|
+
rels = xml(read("_rels/.rels")) rescue nil
|
71
|
+
raise InvalidOpenXmlDocument, "No _rels/.rels file" if rels.nil?
|
72
|
+
|
73
|
+
elements = REXML::XPath.match(rels, "/Relationships/Relationship[@Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument']")
|
74
|
+
|
75
|
+
raise InvalidOpenXmlDocument, "This version of OpenXML Helper can't handle OpenXml files with more than one main document" unless elements.size == 1
|
76
|
+
|
77
|
+
target = elements.first.attribute('Target').to_s
|
78
|
+
|
79
|
+
@documents = {}
|
80
|
+
@shared_strings = nil
|
81
|
+
@workbook = nil
|
82
|
+
@worksheets = nil
|
83
|
+
|
84
|
+
case type
|
85
|
+
when TypeWord
|
86
|
+
add_document(target) if target =~ /^word\/document.xml$/
|
87
|
+
when TypeExcel
|
88
|
+
if target =~ /^xl\/workbook.xml$/
|
89
|
+
workbook = xml(read(target))
|
90
|
+
add_document(target, workbook)
|
91
|
+
@workbook = workbook
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
raise InvalidOpenXmlDocument if @documents.empty?
|
96
|
+
|
97
|
+
case type
|
98
|
+
when TypeWord
|
99
|
+
add_word_documents
|
100
|
+
when TypeExcel
|
101
|
+
add_excel_documents
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# create directly a new Excel file
|
106
|
+
# * template: the name of the file
|
107
|
+
def OpenXmlHelper.new_excel(template)
|
108
|
+
OpenXmlHelper.new(template, TypeExcel)
|
109
|
+
end
|
110
|
+
|
111
|
+
# create directly a new Word file
|
112
|
+
# * template: the name of the file
|
113
|
+
def OpenXmlHelper.new_word(template)
|
114
|
+
OpenXmlHelper.new(template, TypeWord)
|
115
|
+
end
|
116
|
+
|
117
|
+
# it returns all the files which could contain some data (Text, Numbers, Dates, ...).
|
118
|
+
# All the files who deal with the format, the style, ... are not included
|
119
|
+
#
|
120
|
+
# Each member of the Array is a REXML::Document
|
121
|
+
def documents
|
122
|
+
@documents.keys
|
123
|
+
end
|
124
|
+
|
125
|
+
# Save the modified content of the file represented by a REXML::Document
|
126
|
+
def save(xml)
|
127
|
+
save_xml(xml, @documents[xml]) if @documents.include?(xml)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Write the final document (the zipfile) and close the file
|
131
|
+
def write
|
132
|
+
save_shared_strings
|
133
|
+
# @zip_file.close
|
134
|
+
############# tempdir ##############
|
135
|
+
FileUtils.rm(@template) if File.exist?(@template)
|
136
|
+
FileUtils.cd(@tmpdir) do |dir|
|
137
|
+
system("zip -q -r ../#{@template} .")
|
138
|
+
end
|
139
|
+
############# tempdir ##############
|
140
|
+
end
|
141
|
+
|
142
|
+
alias :close :write
|
143
|
+
|
144
|
+
#########################################
|
145
|
+
private
|
146
|
+
#########################################
|
147
|
+
def save_shared_strings
|
148
|
+
save_xml(@shared_strings, @shared_strings_file) unless @shared_strings.nil?
|
149
|
+
end
|
150
|
+
|
151
|
+
def save_xml(xml, file)
|
152
|
+
# @zip_file.file.open(file, 'w') do |file|
|
153
|
+
############# tempdir ##############
|
154
|
+
File.open("#{@tmpdir}/#{file}", 'wb') do |f|
|
155
|
+
############# tempdir ##############
|
156
|
+
# xml.write(f)
|
157
|
+
f.write(xml.to_s.unpack('C*').pack('U*'))
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# add all dependent word files in @documents
|
162
|
+
def add_word_documents
|
163
|
+
REXML::XPath.match(xml(read('word/_rels/document.xml.rels')), "/Relationships/Relationship").each do |element|
|
164
|
+
next unless element.attribute('Type').to_s =~ /\/(footer|header|endnotes|footnotes|glossaryDocument)$/
|
165
|
+
add_document("word/#{element.attribute('Target')}")
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# add all dependent excel files in @documents
|
170
|
+
def add_excel_documents
|
171
|
+
@worksheets = []
|
172
|
+
REXML::XPath.match(xml(read('xl/_rels/workbook.xml.rels')), "/Relationships/Relationship").each do |element|
|
173
|
+
type = element.attribute('Type').to_s
|
174
|
+
target = "xl/#{element.attribute('Target')}"
|
175
|
+
if type =~ /\/(worksheet)$/
|
176
|
+
xml = xml(read(target))
|
177
|
+
add_document(target, xml)
|
178
|
+
@worksheets << xml
|
179
|
+
else
|
180
|
+
if type =~ /\/(sharedStrings)$/
|
181
|
+
raise InvalidOpenXmlDocument, "This version of OpenXML Helper allows only one SharedString file" unless @shared_strings.nil?
|
182
|
+
@shared_strings = xml(read(target))
|
183
|
+
@shared_strings_file = target
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
raise InvalidOpenXmlDocument if @shared_strings.nil?
|
188
|
+
end
|
189
|
+
|
190
|
+
# update @documents from a file
|
191
|
+
# it creates the xml
|
192
|
+
def add_document(target, xml=nil)
|
193
|
+
xml = xml(read(target)) if xml.nil?
|
194
|
+
@documents[xml] = target
|
195
|
+
end
|
196
|
+
|
197
|
+
# return the REXML::Document from a file
|
198
|
+
def xml(file)
|
199
|
+
REXML::Document.new(file)
|
200
|
+
end
|
201
|
+
|
202
|
+
# return a File from a file included in the main ZIP file
|
203
|
+
def read(file)
|
204
|
+
# @zip_file.file.open(file, 'r') {|f| f.read }
|
205
|
+
############# tempdir ##############
|
206
|
+
File.open("#{@tmpdir}/#{file}", 'rb') {|f| f.read}
|
207
|
+
############# tempdir ##############
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end; end
|