rubyXL-git-ref-6002046 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +63 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +197 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/lib/rubyXL.rb +21 -0
- data/lib/rubyXL/cell.rb +325 -0
- data/lib/rubyXL/generic_storage.rb +40 -0
- data/lib/rubyXL/objects/border.rb +53 -0
- data/lib/rubyXL/objects/cell_style.rb +73 -0
- data/lib/rubyXL/objects/color.rb +23 -0
- data/lib/rubyXL/objects/column_range.rb +88 -0
- data/lib/rubyXL/objects/data_validation.rb +31 -0
- data/lib/rubyXL/objects/defined_name.rb +27 -0
- data/lib/rubyXL/objects/fill.rb +42 -0
- data/lib/rubyXL/objects/font.rb +109 -0
- data/lib/rubyXL/objects/formula.rb +8 -0
- data/lib/rubyXL/objects/ooxml_object.rb +177 -0
- data/lib/rubyXL/objects/reference.rb +98 -0
- data/lib/rubyXL/objects/sheet_view.rb +62 -0
- data/lib/rubyXL/objects/worksheet.rb +11 -0
- data/lib/rubyXL/parser.rb +307 -0
- data/lib/rubyXL/private_class.rb +95 -0
- data/lib/rubyXL/shared_strings.rb +35 -0
- data/lib/rubyXL/workbook.rb +342 -0
- data/lib/rubyXL/worksheet.rb +1118 -0
- data/lib/rubyXL/writer/app_writer.rb +51 -0
- data/lib/rubyXL/writer/calc_chain_writer.rb +18 -0
- data/lib/rubyXL/writer/content_types_writer.rb +113 -0
- data/lib/rubyXL/writer/core_writer.rb +34 -0
- data/lib/rubyXL/writer/generic_writer.rb +33 -0
- data/lib/rubyXL/writer/root_rels_writer.rb +17 -0
- data/lib/rubyXL/writer/shared_strings_writer.rb +21 -0
- data/lib/rubyXL/writer/styles_writer.rb +64 -0
- data/lib/rubyXL/writer/theme_writer.rb +337 -0
- data/lib/rubyXL/writer/workbook_rels_writer.rb +43 -0
- data/lib/rubyXL/writer/workbook_writer.rb +73 -0
- data/lib/rubyXL/writer/worksheet_writer.rb +164 -0
- data/lib/rubyXL/zip.rb +20 -0
- data/rdoc/created.rid +36 -0
- data/rdoc/fonts.css +167 -0
- data/rdoc/fonts/Lato-Light.ttf +0 -0
- data/rdoc/fonts/Lato-LightItalic.ttf +0 -0
- data/rdoc/fonts/Lato-Regular.ttf +0 -0
- data/rdoc/fonts/Lato-RegularItalic.ttf +0 -0
- data/rdoc/fonts/SourceCodePro-Bold.ttf +0 -0
- data/rdoc/fonts/SourceCodePro-Regular.ttf +0 -0
- data/rdoc/images/add.png +0 -0
- data/rdoc/images/arrow_up.png +0 -0
- data/rdoc/images/brick.png +0 -0
- data/rdoc/images/brick_link.png +0 -0
- data/rdoc/images/bug.png +0 -0
- data/rdoc/images/bullet_black.png +0 -0
- data/rdoc/images/bullet_toggle_minus.png +0 -0
- data/rdoc/images/bullet_toggle_plus.png +0 -0
- data/rdoc/images/date.png +0 -0
- data/rdoc/images/delete.png +0 -0
- data/rdoc/images/find.png +0 -0
- data/rdoc/images/loadingAnimation.gif +0 -0
- data/rdoc/images/macFFBgHack.png +0 -0
- data/rdoc/images/package.png +0 -0
- data/rdoc/images/page_green.png +0 -0
- data/rdoc/images/page_white_text.png +0 -0
- data/rdoc/images/page_white_width.png +0 -0
- data/rdoc/images/plugin.png +0 -0
- data/rdoc/images/ruby.png +0 -0
- data/rdoc/images/tag_blue.png +0 -0
- data/rdoc/images/tag_green.png +0 -0
- data/rdoc/images/transparent.png +0 -0
- data/rdoc/images/wrench.png +0 -0
- data/rdoc/images/wrench_orange.png +0 -0
- data/rdoc/images/zoom.png +0 -0
- data/rdoc/js/darkfish.js +140 -0
- data/rdoc/js/jquery.js +18 -0
- data/rdoc/js/navigation.js +142 -0
- data/rdoc/js/search.js +109 -0
- data/rdoc/js/search_index.js +1 -0
- data/rdoc/js/searcher.js +228 -0
- data/rdoc/rdoc.css +580 -0
- data/rubyXL-git-ref-6002046.gemspec +143 -0
- data/spec/lib/cell_spec.rb +407 -0
- data/spec/lib/color_spec.rb +14 -0
- data/spec/lib/parser_spec.rb +80 -0
- data/spec/lib/workbook_spec.rb +73 -0
- data/spec/lib/worksheet_spec.rb +1789 -0
- metadata +231 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
require 'pp'
|
2
|
+
|
3
|
+
module RubyXL
|
4
|
+
class OOXMLObject
|
5
|
+
|
6
|
+
# Throughout this class, setting class variables through explicit method calls rather
|
7
|
+
# than by directly addressing the name of the variable because of context issues:
|
8
|
+
# addressing variable by name creates it in the context of defining class,
|
9
|
+
# while calling the setter/getter method addresses it in the context of descendant class,
|
10
|
+
# which is what we need.
|
11
|
+
def self.obtain_class_variable(var_name, default = {})
|
12
|
+
if class_variable_defined?(var_name) then
|
13
|
+
self.class_variable_get(var_name)
|
14
|
+
else
|
15
|
+
self.class_variable_set(var_name, default)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def obtain_class_variable(var_name, default = {})
|
20
|
+
self.class.obtain_class_variable(var_name, default)
|
21
|
+
end
|
22
|
+
private :obtain_class_variable
|
23
|
+
|
24
|
+
def self.define_attribute(attr_name, attr_type, extra_params = {})
|
25
|
+
attrs = obtain_class_variable(:@@ooxml_attributes)
|
26
|
+
|
27
|
+
accessor = extra_params[:accessor] || accessorize(attr_name)
|
28
|
+
|
29
|
+
attrs[accessor] = {
|
30
|
+
:attr_name => attr_name.to_s,
|
31
|
+
:attr_type => attr_type,
|
32
|
+
:optional => !extra_params[:required],
|
33
|
+
:default => extra_params[:default],
|
34
|
+
:validation => extra_params[:values]
|
35
|
+
}
|
36
|
+
|
37
|
+
self.send(:attr_accessor, accessor)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.define_child_node(klass, extra_params = {})
|
41
|
+
child_nodes = obtain_class_variable(:@@ooxml_child_nodes)
|
42
|
+
child_node_name = (extra_params[:node_name] || klass.class_variable_get(:@@ooxml_tag_name)).to_s
|
43
|
+
accessor = (extra_params[:accessor] || accessorize(child_node_name)).to_sym
|
44
|
+
|
45
|
+
child_nodes[child_node_name] = {
|
46
|
+
:class => klass,
|
47
|
+
:is_array => extra_params[:collection],
|
48
|
+
:accessor => accessor
|
49
|
+
}
|
50
|
+
|
51
|
+
self.send(:attr_accessor, accessor)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.define_element_name(v)
|
55
|
+
self.class_variable_set(:@@ooxml_tag_name, v)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.set_countable
|
59
|
+
self.class_variable_set(:@@ooxml_countable, true)
|
60
|
+
self.send(:attr_accessor, :count)
|
61
|
+
end
|
62
|
+
|
63
|
+
def write_xml(xml, node_name_override = nil)
|
64
|
+
before_write_xml if self.respond_to?(:before_write_xml)
|
65
|
+
attrs = prepare_attributes
|
66
|
+
element_text = attrs.delete('_')
|
67
|
+
elem = xml.create_element(node_name_override || obtain_class_variable(:@@ooxml_tag_name), attrs, element_text)
|
68
|
+
child_nodes = obtain_class_variable(:@@ooxml_child_nodes)
|
69
|
+
child_nodes.each_pair { |child_node_name, child_node_params|
|
70
|
+
obj = self.send(child_node_params[:accessor])
|
71
|
+
unless obj.nil?
|
72
|
+
if child_node_params[:is_array] then obj.each { |item| elem << item.write_xml(xml) }
|
73
|
+
else elem << obj.write_xml(xml, child_node_name)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
}
|
77
|
+
elem
|
78
|
+
end
|
79
|
+
|
80
|
+
def initialize(params = {})
|
81
|
+
obtain_class_variable(:@@ooxml_attributes).each_key { |k| instance_variable_set("@#{k}", params[k]) }
|
82
|
+
obtain_class_variable(:@@ooxml_child_nodes).each_pair { |k, v|
|
83
|
+
|
84
|
+
initial_value =
|
85
|
+
if params.has_key?(v[:accessor]) then params[v[:accessor]]
|
86
|
+
elsif v[:is_array] then []
|
87
|
+
else nil
|
88
|
+
end
|
89
|
+
|
90
|
+
instance_variable_set("@#{v[:accessor]}", initial_value)
|
91
|
+
}
|
92
|
+
|
93
|
+
instance_variable_set("@count", 0) if obtain_class_variable(:@@ooxml_countable, false)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.parse(node)
|
97
|
+
obj = self.new
|
98
|
+
|
99
|
+
obtain_class_variable(:@@ooxml_attributes).each_pair { |k, v|
|
100
|
+
|
101
|
+
raw_value = if v[:attr_name] == '_' then node.text
|
102
|
+
else
|
103
|
+
attr = node.attributes[v[:attr_name]]
|
104
|
+
attr && attr.value
|
105
|
+
end
|
106
|
+
|
107
|
+
val = raw_value &&
|
108
|
+
case v[:attr_type]
|
109
|
+
when :int then Integer(raw_value)
|
110
|
+
when :float then Float(raw_value)
|
111
|
+
when :string then raw_value
|
112
|
+
when :sqref then RubyXL::Sqref.new(raw_value)
|
113
|
+
when :ref then RubyXL::Reference.new(raw_value)
|
114
|
+
when :bool then ['1', 'true'].include?(raw_value)
|
115
|
+
end
|
116
|
+
|
117
|
+
obj.send("#{k}=", val)
|
118
|
+
}
|
119
|
+
|
120
|
+
known_child_nodes = obtain_class_variable(:@@ooxml_child_nodes)
|
121
|
+
|
122
|
+
unless known_child_nodes.empty?
|
123
|
+
node.element_children.each { |child_node|
|
124
|
+
child_node_name = child_node.name
|
125
|
+
child_node_params = known_child_nodes[child_node_name]
|
126
|
+
raise "Unknown child node: #{child_node_name}" if child_node_params.nil?
|
127
|
+
parsed_object = child_node_params[:class].parse(child_node)
|
128
|
+
if child_node_params[:is_array] then obj.send(child_node_params[:accessor]) << parsed_object
|
129
|
+
else obj.send("#{child_node_params[:accessor]}=", parsed_object)
|
130
|
+
end
|
131
|
+
}
|
132
|
+
end
|
133
|
+
|
134
|
+
obj
|
135
|
+
end
|
136
|
+
|
137
|
+
def dup
|
138
|
+
new_copy = super
|
139
|
+
new_copy.count = 0 if obtain_class_variable(:@@ooxml_countable, false)
|
140
|
+
new_copy
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
def self.accessorize(str)
|
145
|
+
acc = str.to_s.dup
|
146
|
+
acc.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
|
147
|
+
acc.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
148
|
+
acc.gsub!(':','_')
|
149
|
+
acc.downcase.to_sym
|
150
|
+
end
|
151
|
+
|
152
|
+
def prepare_attributes
|
153
|
+
xml_attrs = {}
|
154
|
+
|
155
|
+
obtain_class_variable(:@@ooxml_attributes).each_pair { |k, v|
|
156
|
+
val = self.send(k)
|
157
|
+
|
158
|
+
if val.nil? then
|
159
|
+
next if v[:optional]
|
160
|
+
val = v[:default]
|
161
|
+
end
|
162
|
+
|
163
|
+
val = val &&
|
164
|
+
case v[:attr_type]
|
165
|
+
when :bool then val ? '1' : '0'
|
166
|
+
when :float then val.to_s.gsub(/\.0*$/, '') # Trim trailing zeroes
|
167
|
+
else val
|
168
|
+
end
|
169
|
+
|
170
|
+
xml_attrs[v[:attr_name]] = val
|
171
|
+
}
|
172
|
+
|
173
|
+
xml_attrs
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module RubyXL
|
2
|
+
class Reference
|
3
|
+
ROW_MAX = 1024*1024
|
4
|
+
COL_MAX = 16393
|
5
|
+
|
6
|
+
attr_reader :row_range, :col_range
|
7
|
+
|
8
|
+
# RubyXL::Reference.new(row, col)
|
9
|
+
# RubyXL::Reference.new(row_from, row_to, col_from, col_to)
|
10
|
+
# RubyXL::Reference.new(reference_string)
|
11
|
+
def initialize(*params)
|
12
|
+
row_from = row_to = col_from = col_to = nil
|
13
|
+
|
14
|
+
case params.size
|
15
|
+
when 4 then row_from, row_to, col_from, col_to = params
|
16
|
+
when 2 then row_from, col_from = params
|
17
|
+
when 1 then
|
18
|
+
raise ArgumentError.new("invalid value for #{self.class}: #{params[0].inspect}") unless params[0].is_a?(String)
|
19
|
+
from, to = params[0].split(':')
|
20
|
+
row_from, col_from = self.class.ref2ind(from)
|
21
|
+
row_to, col_to = self.class.ref2ind(to) unless to.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
@row_range = Range.new(row_from || 0, row_to || row_from || ROW_MAX)
|
25
|
+
@col_range = Range.new(col_from || 0, col_to || col_from || COL_MAX)
|
26
|
+
end
|
27
|
+
|
28
|
+
def single_cell?
|
29
|
+
(@row_range.begin == @row_range.end) && (@col_range.begin == @col_range.end)
|
30
|
+
end
|
31
|
+
|
32
|
+
def first_row
|
33
|
+
@row_range.begin
|
34
|
+
end
|
35
|
+
|
36
|
+
def last_row
|
37
|
+
@row_range.end
|
38
|
+
end
|
39
|
+
|
40
|
+
def first_col
|
41
|
+
@col_range.begin
|
42
|
+
end
|
43
|
+
|
44
|
+
def last_col
|
45
|
+
@col_range.end
|
46
|
+
end
|
47
|
+
|
48
|
+
def ==(other)
|
49
|
+
!other.nil? && (@row_range == other.row_range) && (@col_range == other.col_range)
|
50
|
+
end
|
51
|
+
|
52
|
+
def cover?(other)
|
53
|
+
!other.nil? && (@row_range.cover?(other.row_range.begin) && @row_range.cover?(other.row_range.end) &&
|
54
|
+
@col_range.cover?(other.col_range.begin) && @col_range.cover?(other.col_range.end))
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_s
|
58
|
+
if single_cell? then
|
59
|
+
self.class.ind2ref(@row_range.begin, @col_range.begin)
|
60
|
+
else
|
61
|
+
self.class.ind2ref(@row_range.begin, @col_range.begin) + ':' + self.class.ind2ref(@row_range.end, @col_range.end)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def inspect
|
66
|
+
if single_cell? then
|
67
|
+
"#<#{self.class} @row=#{@row_range.begin} @col=#{@col_range.begin}>"
|
68
|
+
else
|
69
|
+
"#<#{self.class} @row_range=#{@row_range} @col_range=#{@col_range}>"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Converts +row+ and +col+ zero-based indices to Excel-style cell reference
|
74
|
+
# (0) A...Z, AA...AZ, BA... ...ZZ, AAA... ...AZZ, BAA... ...XFD (16383)
|
75
|
+
def self.ind2ref(row = 0, col = 0)
|
76
|
+
RubyXL::ColumnRange.ind2ref(col) + (row + 1).to_s
|
77
|
+
end
|
78
|
+
|
79
|
+
# Converts Excel-style cell reference to +row+ and +col+ zero-based indices.
|
80
|
+
def self.ref2ind(str)
|
81
|
+
return [ -1, -1 ] unless str =~ /^([A-Z]+)(\d+)$/
|
82
|
+
[ $2.to_i - 1, RubyXL::ColumnRange.ref2ind($1) ]
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
class Sqref < Array
|
88
|
+
|
89
|
+
def initialize(str)
|
90
|
+
str.split.each { |ref_str| self << RubyXL::Reference.new(ref_str) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s
|
94
|
+
self.collect{ |ref| ref.to_s }.join(' ')
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module RubyXL
|
2
|
+
# http://www.schemacentral.com/sc/ooxml/e-ssml_pane-1.html
|
3
|
+
class Pane < OOXMLObject
|
4
|
+
define_attribute(:xSplit, :int)
|
5
|
+
define_attribute(:ySplit, :int)
|
6
|
+
define_attribute(:topLeftCell, :string)
|
7
|
+
define_attribute(:activePane, :string, :default => 'topLeft',
|
8
|
+
:values => %w{ bottomRight topRight bottomLeft topLeft })
|
9
|
+
define_attribute(:state, :string, :default=> 'split',
|
10
|
+
:values => %w{ split frozen frozenSplit })
|
11
|
+
define_element_name 'pane'
|
12
|
+
end
|
13
|
+
|
14
|
+
# http://www.schemacentral.com/sc/ooxml/e-ssml_selection-1.html
|
15
|
+
class Selection < OOXMLObject
|
16
|
+
define_attribute(:pane, :string,
|
17
|
+
:values => %w{ bottomRight topRight bottomLeft topLeft })
|
18
|
+
define_attribute(:activeCell, :ref)
|
19
|
+
define_attribute(:activeCellId, :int) # 0-based index of @active_cell in @sqref
|
20
|
+
define_attribute(:sqref, :sqref) # Array of references to the selected cells.
|
21
|
+
define_element_name 'selection'
|
22
|
+
|
23
|
+
def before_write_xml
|
24
|
+
# Normally, rindex of activeCellId in sqref:
|
25
|
+
# <selection activeCell="E12" activeCellId="9" sqref="A4 B6 C8 D10 E12 A4 B6 C8 D10 E12"/>
|
26
|
+
if @active_cell_id.nil? && !@active_cell.nil? && @sqref.size > 1 then
|
27
|
+
# But, things can be more complex:
|
28
|
+
# <selection activeCell="E8" activeCellId="2" sqref="A4:B4 C6:D6 E8:F8"/>
|
29
|
+
# Not using .reverse.each here to avoid memory reallocation.
|
30
|
+
@sqref.each_with_index { |ref, ind| @active_cell_id = ind if ref.cover?(@active_cell) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# http://www.schemacentral.com/sc/ooxml/e-ssml_sheetView-1.html
|
36
|
+
class SheetView < OOXMLObject
|
37
|
+
define_attribute(:windowProtection, :bool, :default => false)
|
38
|
+
define_attribute(:showFormulas, :bool, :default => false)
|
39
|
+
define_attribute(:showGridLines, :bool, :default => true)
|
40
|
+
define_attribute(:showRowColHeaders, :bool, :default => true)
|
41
|
+
define_attribute(:showZeros, :bool, :default => true)
|
42
|
+
define_attribute(:rightToLeft, :bool, :default => false)
|
43
|
+
define_attribute(:tabSelected, :bool, :default => false)
|
44
|
+
define_attribute(:showRuler, :bool, :default => true)
|
45
|
+
define_attribute(:showOutlineSymbols, :bool, :default => true)
|
46
|
+
define_attribute(:defaultGridColor, :bool, :default => true)
|
47
|
+
define_attribute(:showWhiteSpace, :bool, :default => true)
|
48
|
+
define_attribute(:view, :string, :default => 'normal',
|
49
|
+
:values => %w{ normal pageBreakPreview pageLayout })
|
50
|
+
define_attribute(:topLeftCell, :ref)
|
51
|
+
define_attribute(:colorId, :int, :default => 64)
|
52
|
+
define_attribute(:zoomScale, :int, :default => 100)
|
53
|
+
define_attribute(:zoomScaleNormal, :bool, :default => 0)
|
54
|
+
define_attribute(:zoomScaleSheetLayoutView, :bool, :default => 0)
|
55
|
+
define_attribute(:zoomScalePageLayoutView, :bool, :default => 0)
|
56
|
+
define_attribute(:workbookViewId, :int, :required => true, :default => 0 )
|
57
|
+
define_child_node(RubyXL::Pane)
|
58
|
+
define_child_node(RubyXL::Selection, :collection => true, :accessor => :selections )
|
59
|
+
define_element_name 'sheetView'
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module RubyXL
|
2
|
+
|
3
|
+
# Eventually, the entire code for Worksheet will be moved here. One small step at a time!
|
4
|
+
|
5
|
+
# http://www.schemacentral.com/sc/ooxml/e-ssml_legacyDrawing-1.html
|
6
|
+
class LegacyDrawing < OOXMLObject
|
7
|
+
define_attribute(:'r:id', :string, :required => true)
|
8
|
+
define_element_name 'legacyDrawing'
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'zip'
|
4
|
+
require 'rubyXL/generic_storage'
|
5
|
+
|
6
|
+
module RubyXL
|
7
|
+
|
8
|
+
class Parser
|
9
|
+
def self.parse(file_path, opts = {})
|
10
|
+
self.new(opts).parse(file_path)
|
11
|
+
end
|
12
|
+
|
13
|
+
# +:data_only+ allows only the sheet data to be parsed, so as to speed up parsing
|
14
|
+
# However, using this option will result in date-formatted cells being interpreted as numbers
|
15
|
+
def initialize(opts = {})
|
16
|
+
@data_only = opts.is_a?(TrueClass) || opts[:data_only]
|
17
|
+
@skip_filename_check = opts[:skip_filename_check]
|
18
|
+
end
|
19
|
+
|
20
|
+
def data_only
|
21
|
+
@data_only = true
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse(file_path, opts = {})
|
26
|
+
|
27
|
+
# options handling
|
28
|
+
|
29
|
+
wb = Workbook.new([], file_path)
|
30
|
+
|
31
|
+
raise 'Not .xlsx or .xlsm excel file' unless @skip_filename_check ||
|
32
|
+
%w{.xlsx .xlsm}.include?(File.extname(file_path))
|
33
|
+
|
34
|
+
dir_path = File.join(File.dirname(file_path), Dir::Tmpname.make_tmpname(['rubyXL', '.tmp'], nil))
|
35
|
+
|
36
|
+
MyZip.new.unzip(file_path, dir_path)
|
37
|
+
|
38
|
+
files = {}
|
39
|
+
|
40
|
+
workbook_file = Nokogiri::XML.parse(File.open(File.join(dir_path,'xl','workbook.xml'),'r'))
|
41
|
+
rels_doc = Nokogiri::XML.parse(File.open(File.join(dir_path, 'xl', '_rels', 'workbook.xml.rels'), 'r'))
|
42
|
+
|
43
|
+
if(File.exist?(File.join(dir_path,'xl','sharedStrings.xml')))
|
44
|
+
shared_string_file = Nokogiri::XML.parse(File.open(File.join(dir_path,'xl','sharedStrings.xml'),'r'))
|
45
|
+
end
|
46
|
+
|
47
|
+
unless @data_only
|
48
|
+
wb.media = RubyXL::GenericStorage.new(File.join('xl', 'media')).binary.load_dir(dir_path)
|
49
|
+
wb.external_links = RubyXL::GenericStorage.new(File.join('xl', 'externalLinks')).load_dir(dir_path)
|
50
|
+
wb.external_links_rels = RubyXL::GenericStorage.new(File.join('xl', 'externalLinks', '_rels')).load_dir(dir_path)
|
51
|
+
wb.drawings = RubyXL::GenericStorage.new(File.join('xl', 'drawings')).load_dir(dir_path)
|
52
|
+
wb.drawings_rels = RubyXL::GenericStorage.new(File.join('xl', 'drawings', '_rels')).load_dir(dir_path)
|
53
|
+
wb.charts = RubyXL::GenericStorage.new(File.join('xl', 'charts')).load_dir(dir_path)
|
54
|
+
wb.chart_rels = RubyXL::GenericStorage.new(File.join('xl', 'charts', '_rels')).load_dir(dir_path)
|
55
|
+
wb.printer_settings = RubyXL::GenericStorage.new(File.join('xl', 'printerSettings')).binary.load_dir(dir_path)
|
56
|
+
wb.worksheet_rels = RubyXL::GenericStorage.new(File.join('xl', 'worksheets', '_rels')).load_dir(dir_path)
|
57
|
+
wb.macros = RubyXL::GenericStorage.new('xl').binary.load_file(dir_path, 'vbaProject.bin')
|
58
|
+
wb.theme = RubyXL::GenericStorage.new(File.join('xl', 'theme')).load_file(dir_path, 'theme1.xml')
|
59
|
+
|
60
|
+
core_file = Nokogiri::XML.parse(File.open(File.join(dir_path, 'docProps', 'core.xml'), 'r'))
|
61
|
+
wb.creator = core_file.css('dc|creator').children.to_s
|
62
|
+
wb.modifier = core_file.css('cp|last_modified_by').children.to_s
|
63
|
+
wb.created_at = core_file.css('dcterms|created').children.to_s
|
64
|
+
wb.modified_at = core_file.css('dcterms|modified').children.to_s
|
65
|
+
|
66
|
+
app_file = Nokogiri::XML.parse(File.open(File.join(dir_path, 'docProps', 'app.xml'), 'r'))
|
67
|
+
wb.company = app_file.css('Company').children.to_s
|
68
|
+
wb.application = app_file.css('Application').children.to_s
|
69
|
+
wb.appversion = app_file.css('AppVersion').children.to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
styles_xml = Nokogiri::XML.parse(File.open(File.join(dir_path, 'xl', 'styles.xml'), 'r'))
|
73
|
+
|
74
|
+
defined_names = workbook_file.css('definedNames definedName')
|
75
|
+
wb.defined_names = defined_names.collect { |node| RubyXL::DefinedName.parse(node) }
|
76
|
+
|
77
|
+
wb.date1904 = workbook_file.css('workbookPr').attribute('date1904').to_s == '1'
|
78
|
+
|
79
|
+
wb.shared_strings_XML = shared_string_file.to_s
|
80
|
+
|
81
|
+
unless shared_string_file.nil?
|
82
|
+
sst = shared_string_file.css('sst')
|
83
|
+
|
84
|
+
# According to http://msdn.microsoft.com/en-us/library/office/gg278314.aspx,
|
85
|
+
# these attributes may be either both missing, or both present. Need to validate.
|
86
|
+
wb.shared_strings.count_attr = sst.attribute('count').value.to_i
|
87
|
+
wb.shared_strings.unique_count_attr = sst.attribute('uniqueCount').value.to_i
|
88
|
+
|
89
|
+
# Note that the strings may contain text formatting, such as changing font color/properties
|
90
|
+
# in the middle of the string. We do not support that in this gem... at least yet!
|
91
|
+
# If you save the file, this formatting will be destoyed.
|
92
|
+
shared_string_file.css('si').each_with_index { |node, i|
|
93
|
+
wb.shared_strings.add(node.css('t').inject(''){ |s, c| s + c.text }, i)
|
94
|
+
}
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
fills = styles_xml.css('fills fill')
|
99
|
+
wb.fills = fills.collect { |node| RubyXL::Fill.parse(node) }
|
100
|
+
|
101
|
+
colors = styles_xml.css('colors').first
|
102
|
+
|
103
|
+
if colors then
|
104
|
+
colors.element_children.each { |color_type_node|
|
105
|
+
wb.colors[color_type_node.name] ||= []
|
106
|
+
color_type_node.element_children.each { |color_node|
|
107
|
+
wb.colors[color_type_node.name] << RubyXL::Color.parse(color_node)
|
108
|
+
}
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
borders = styles_xml.css('borders border')
|
113
|
+
wb.borders = borders.collect { |node| RubyXL::Border.parse(node) }
|
114
|
+
|
115
|
+
fonts = styles_xml.css('fonts font')
|
116
|
+
wb.fonts = fonts.collect { |node| RubyXL::Font.parse(node) }
|
117
|
+
|
118
|
+
cell_styles = styles_xml.css('cellStyles cellStyle')
|
119
|
+
wb.cell_styles = cell_styles.collect { |node| RubyXL::CellStyle.parse(node) }
|
120
|
+
|
121
|
+
num_fmts = styles_xml.css('numFmts numFmt')
|
122
|
+
wb.num_fmts = num_fmts.collect { |node| RubyXL::NumFmt.parse(node) }
|
123
|
+
|
124
|
+
csxfs = styles_xml.css('cellStyleXfs xf')
|
125
|
+
wb.cell_style_xfs = csxfs.collect { |node| RubyXL::XF.parse(node) }
|
126
|
+
|
127
|
+
cxfs = styles_xml.css('cellXfs xf')
|
128
|
+
wb.cell_xfs = cxfs.collect { |node| RubyXL::XF.parse(node) }
|
129
|
+
|
130
|
+
#fills out count information for each font, fill, and border
|
131
|
+
wb.cell_xfs.each { |style|
|
132
|
+
id = style.font_id
|
133
|
+
wb.fonts[id].count += 1 #unless id.nil?
|
134
|
+
|
135
|
+
id = style.fill_id
|
136
|
+
wb.fills[id].count += 1 #unless id.nil?
|
137
|
+
|
138
|
+
id = style.border_id
|
139
|
+
wb.borders[id].count += 1 #unless id.nil?
|
140
|
+
}
|
141
|
+
|
142
|
+
# Not sure why they were getting sheet names from god knows where.
|
143
|
+
# There *may* have been a good reason behind it, so not tossing this code out entirely yet.
|
144
|
+
# sheet_names = app_file.css('TitlesOfParts vt|vector vt|lpstr').children
|
145
|
+
|
146
|
+
workbook_file.css('sheets sheet').each_with_index { |sheet_node, i|
|
147
|
+
sheet_rid = sheet_node.attributes['id'].value
|
148
|
+
sheet_file_path = rels_doc.css("Relationships Relationship[Id=#{sheet_rid}]").first.attributes['Target']
|
149
|
+
worksheet_xml = Nokogiri::XML.parse(File.read(File.join(dir_path, 'xl', sheet_file_path)))
|
150
|
+
parse_worksheet(wb, i, worksheet_xml, sheet_node.attributes['name'].value,
|
151
|
+
sheet_node.attributes['sheetId'].value )
|
152
|
+
}
|
153
|
+
|
154
|
+
FileUtils.remove_entry_secure(dir_path)
|
155
|
+
|
156
|
+
return wb
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
# Parse the incoming +worksheet_xml+ into a new +Worksheet+ object
|
162
|
+
def parse_worksheet(wb, i, worksheet_xml, worksheet_name, sheet_id)
|
163
|
+
worksheet = Worksheet.new(wb, worksheet_name)
|
164
|
+
wb.worksheets[i] = worksheet # Due to "validate_workbook" issues. Should remove that validation eventually.
|
165
|
+
worksheet.sheet_id = sheet_id
|
166
|
+
|
167
|
+
dimensions_node = worksheet_xml.css('dimension').first
|
168
|
+
return nil if dimensions_node.nil? # Temporary plug for Issue #76
|
169
|
+
|
170
|
+
dimensions = RubyXL::Reference.new(dimensions_node.attribute('ref').value)
|
171
|
+
cols = dimensions.last_col
|
172
|
+
|
173
|
+
# Create empty arrays for workcells. Using +downto()+ here so memory for +sheet_data[]+ is
|
174
|
+
# allocated on the first iteration (in case of +upto()+, +sheet_data[]+ would end up being
|
175
|
+
# reallocated on every iteration).
|
176
|
+
dimensions.last_row.downto(0) { |i| worksheet.sheet_data[i] = Array.new(cols + 1) }
|
177
|
+
|
178
|
+
namespaces = worksheet_xml.root.namespaces
|
179
|
+
|
180
|
+
if @data_only
|
181
|
+
row_xpath = '/xmlns:worksheet/xmlns:sheetData/xmlns:row[xmlns:c[xmlns:v]]'
|
182
|
+
cell_xpath = './xmlns:c[xmlns:v[text()]]'
|
183
|
+
else
|
184
|
+
row_xpath = '/xmlns:worksheet/xmlns:sheetData/xmlns:row'
|
185
|
+
cell_xpath = './xmlns:c'
|
186
|
+
|
187
|
+
sheet_views_nodes = worksheet_xml.xpath('/xmlns:worksheet/xmlns:sheetViews/xmlns:sheetView', namespaces)
|
188
|
+
worksheet.sheet_views = sheet_views_nodes.collect { |node| RubyXL::SheetView.parse(node) }
|
189
|
+
|
190
|
+
col_node_set = worksheet_xml.xpath('/xmlns:worksheet/xmlns:cols/xmlns:col',namespaces)
|
191
|
+
worksheet.column_ranges = col_node_set.collect { |col_node| RubyXL::ColumnRange.parse(col_node) }
|
192
|
+
|
193
|
+
merged_cells_nodeset = worksheet_xml.xpath('/xmlns:worksheet/xmlns:mergeCells/xmlns:mergeCell', namespaces)
|
194
|
+
worksheet.merged_cells = merged_cells_nodeset.collect { |child| RubyXL::Reference.new(child.attributes['ref'].text) }
|
195
|
+
|
196
|
+
# worksheet.pane = worksheet.sheet_view[:pane]
|
197
|
+
|
198
|
+
data_validations = worksheet_xml.xpath('/xmlns:worksheet/xmlns:dataValidations/xmlns:dataValidation', namespaces)
|
199
|
+
worksheet.validations = data_validations.collect { |node| RubyXL::DataValidation.parse(node) }
|
200
|
+
|
201
|
+
# Currently not working #TODO#
|
202
|
+
# ext_list_node = worksheet_xml.xpath('/xmlns:worksheet/xmlns:extLst', namespaces)
|
203
|
+
|
204
|
+
legacy_drawing_nodes = worksheet_xml.xpath('/xmlns:worksheet/xmlns:legacyDrawing', namespaces)
|
205
|
+
worksheet.legacy_drawings = legacy_drawing_nodes.collect { |node| RubyXL::LegacyDrawing.parse(node) }
|
206
|
+
|
207
|
+
drawing_nodes = worksheet_xml.xpath('/xmlns:worksheet/xmlns:drawing', namespaces)
|
208
|
+
worksheet.drawings = drawing_nodes.collect { |n| n.attributes['id'] }
|
209
|
+
end
|
210
|
+
|
211
|
+
worksheet_xml.xpath(row_xpath, namespaces).each { |row|
|
212
|
+
unless @data_only
|
213
|
+
##row styles##
|
214
|
+
row_attributes = row.attributes
|
215
|
+
row_style = row_attributes['s'] && Integer(row_attributes['s'].value) || 0
|
216
|
+
row_num = Integer(row_attributes['r'].content)
|
217
|
+
|
218
|
+
worksheet.row_styles[row_num] = { :style => row_style }
|
219
|
+
|
220
|
+
if !row_attributes['ht'].nil? && (!row_attributes['ht'].content.nil? || row_attributes['ht'].content.strip != "" )
|
221
|
+
worksheet.change_row_height(row_num - 1, Float(row_attributes['ht'].value))
|
222
|
+
end
|
223
|
+
##end row styles##
|
224
|
+
end
|
225
|
+
|
226
|
+
row.search(cell_xpath).each { |value|
|
227
|
+
#attributes is from the excel cell(c) and is basically location information and style and type
|
228
|
+
value_attributes = value.attributes
|
229
|
+
# r attribute contains the location like A1
|
230
|
+
cell_index = RubyXL::Reference.ref2ind(value_attributes['r'].content)
|
231
|
+
style_index = 0
|
232
|
+
# t is optional and contains the type of the cell
|
233
|
+
data_type = value_attributes['t'].content if value_attributes['t']
|
234
|
+
element_hash ={}
|
235
|
+
|
236
|
+
value.children.each { |node| element_hash["#{node.name()}_element"] = node }
|
237
|
+
|
238
|
+
# v is the value element that is part of the cell
|
239
|
+
if element_hash["v_element"]
|
240
|
+
v_element_content = element_hash["v_element"].content
|
241
|
+
else
|
242
|
+
v_element_content=""
|
243
|
+
end
|
244
|
+
|
245
|
+
if v_element_content == "" # no data
|
246
|
+
cell_data = nil
|
247
|
+
elsif data_type == RubyXL::Cell::SHARED_STRING
|
248
|
+
str_index = Integer(v_element_content)
|
249
|
+
cell_data = wb.shared_strings[str_index].to_s
|
250
|
+
elsif data_type == RubyXL::Cell::RAW_STRING
|
251
|
+
cell_data = v_element_content
|
252
|
+
elsif data_type == RubyXL::Cell::ERROR
|
253
|
+
cell_data = v_element_content
|
254
|
+
else# (value.css('v').to_s != "") && (value.css('v').children.to_s != "") #is number
|
255
|
+
data_type = ''
|
256
|
+
if(v_element_content =~ /\./ or v_element_content =~ /\d+e\-?\d+/i) #is float
|
257
|
+
cell_data = Float(v_element_content)
|
258
|
+
else
|
259
|
+
cell_data = Integer(v_element_content)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# f is the formula element
|
264
|
+
cell_formula = nil
|
265
|
+
fmla_css = element_hash["f_element"]
|
266
|
+
if fmla_css && fmla_css.content
|
267
|
+
fmla_css_content= fmla_css.content
|
268
|
+
if(fmla_css_content != "")
|
269
|
+
cell_formula = fmla_css_content
|
270
|
+
cell_formula_attr = {}
|
271
|
+
fmla_css_attributes = fmla_css.attributes
|
272
|
+
cell_formula_attr['t'] = fmla_css_attributes['t'].content if fmla_css_attributes['t']
|
273
|
+
cell_formula_attr['ref'] = fmla_css_attributes['ref'].content if fmla_css_attributes['ref']
|
274
|
+
cell_formula_attr['si'] = fmla_css_attributes['si'].content if fmla_css_attributes['si']
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
style_index = value['s'].to_i #nil goes to 0 (default)
|
279
|
+
|
280
|
+
worksheet.sheet_data[cell_index[0]][cell_index[1]] =
|
281
|
+
Cell.new(worksheet,cell_index[0],cell_index[1],cell_data,cell_formula,
|
282
|
+
data_type,style_index,cell_formula_attr)
|
283
|
+
cell = worksheet.sheet_data[cell_index[0]][cell_index[1]]
|
284
|
+
}
|
285
|
+
}
|
286
|
+
|
287
|
+
worksheet
|
288
|
+
end
|
289
|
+
|
290
|
+
def self.attr_int(node, attr_name)
|
291
|
+
attr = node.attributes[attr_name]
|
292
|
+
attr && Integer(attr.value)
|
293
|
+
end
|
294
|
+
|
295
|
+
def self.attr_float(node, attr_name)
|
296
|
+
attr = node.attributes[attr_name]
|
297
|
+
attr && Float(attr.value)
|
298
|
+
end
|
299
|
+
|
300
|
+
def self.attr_string(node, attr_name)
|
301
|
+
attr = node.attributes[attr_name]
|
302
|
+
attr && attr.value
|
303
|
+
end
|
304
|
+
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|