ooxml_excel 0.0.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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/ooxml_excel.rb +10 -0
- data/lib/ooxml_excel/comments.rb +26 -0
- data/lib/ooxml_excel/excel.rb +63 -0
- data/lib/ooxml_excel/helper/list.rb +67 -0
- data/lib/ooxml_excel/sheet.rb +249 -0
- data/lib/ooxml_excel/styles.rb +127 -0
- data/lib/ooxml_excel/version.rb +5 -0
- data/lib/ooxml_excel/workbook.rb +35 -0
- data/ooxml_excel.gemspec +34 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f763147398129f0ac41335c8ddf647bb2540917e
|
4
|
+
data.tar.gz: e9f93a0c9ee5ec07cdcd8269f77b49b9480e7980
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d3a06ce3169938a146da26eb347ffbc812b7b15627b04d237407fb25ed7138a2b31f298293a12e231a54e9a0756ab18092b9b24ac393e580b2bfb8c16727cbe0
|
7
|
+
data.tar.gz: cb765ec3f0bb940938f7a4ba968b7f8aa54549f0f7040eb6c4e60dd1165804ec72466c91649d24558903c7914e188ec7ecee93dd2cecac33399ed624b51eb424
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# OOXML Excel
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/ooxml_excel`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'ooxml_excel'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install ooxml_excel
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/ooxml_excel.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "ooxml_excel"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/ooxml_excel.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'active_support/all'
|
3
|
+
require 'zip'
|
4
|
+
require "ooxml_excel/version"
|
5
|
+
require "ooxml_excel/helper/list"
|
6
|
+
require "ooxml_excel/excel"
|
7
|
+
require "ooxml_excel/styles"
|
8
|
+
require "ooxml_excel/comments"
|
9
|
+
require "ooxml_excel/workbook"
|
10
|
+
require "ooxml_excel/sheet"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module OOXML
|
2
|
+
class Excel
|
3
|
+
class Comments
|
4
|
+
attr_reader :comments
|
5
|
+
|
6
|
+
def initialize(comments)
|
7
|
+
@comments = comments
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](id)
|
11
|
+
@comments[id]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load_from_stream(comment_xml)
|
15
|
+
comment_xml =Nokogiri.XML(comment_xml).remove_namespaces!
|
16
|
+
|
17
|
+
comments = comment_xml.xpath("//comments/commentList/comment").map do |comment_node|
|
18
|
+
value = (comment_node.xpath('./text/r/t').last || comment_node.at_xpath('./text/r/t') || comment_node.at_xpath('./text/t')).text
|
19
|
+
id = comment_node.attributes["ref"].to_s
|
20
|
+
[id, value]
|
21
|
+
end.to_h
|
22
|
+
new(comments)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module OOXML
|
2
|
+
class Excel
|
3
|
+
include OOXML::Helper::List
|
4
|
+
attr_reader :comments
|
5
|
+
def initialize(spreadsheet, load_only_sheet:nil)
|
6
|
+
@spreadsheet = spreadsheet
|
7
|
+
@sheets = {}
|
8
|
+
@comments = {}
|
9
|
+
# @themes = []
|
10
|
+
@workbook = nil
|
11
|
+
@load_only_sheet = nil
|
12
|
+
@styles = nil
|
13
|
+
load_xml_contents
|
14
|
+
end
|
15
|
+
|
16
|
+
def sheets
|
17
|
+
@workbook.sheets.map { |sheet| sheet[:name]}
|
18
|
+
end
|
19
|
+
|
20
|
+
def sheet(sheet_name)
|
21
|
+
sheet_from_workbook = @workbook.sheets.find { |sheet| sheet[:name] == sheet_name}
|
22
|
+
raise "No #{sheet_name} in workbook." if sheet_from_workbook.blank?
|
23
|
+
sheet = @sheets.fetch(sheet_from_workbook[:relationship_id])
|
24
|
+
|
25
|
+
# shared variables
|
26
|
+
sheet.name = sheet_name
|
27
|
+
sheet.comments = @comments[sheet_from_workbook[:relationship_id]]
|
28
|
+
sheet.styles = @styles
|
29
|
+
sheet.defined_names = @workbook.defined_names
|
30
|
+
sheet
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Currently supports DataValidation (comment), columns (check if hidden)
|
36
|
+
# TODO: list values, Font styles (if bold, colored in red etc..), background color
|
37
|
+
def load_xml_contents
|
38
|
+
shared_strings = []
|
39
|
+
Zip::File.open(@spreadsheet) do |spreadsheet_zip|
|
40
|
+
spreadsheet_zip.each do |entry|
|
41
|
+
stream_xml = entry.get_input_stream.read
|
42
|
+
if entry.name[/xl\/worksheets\/sheet(\d+)?\.xml/]
|
43
|
+
sheet_id = entry.name.scan(/xl\/worksheets\/sheet(\d+)?\.xml/).flatten.first
|
44
|
+
@sheets[sheet_id] = Excel::Sheet.load_from_stream(stream_xml, shared_strings)
|
45
|
+
elsif entry.name[/xl\/styles\.xml/]
|
46
|
+
@styles = Excel::Styles.load_from_stream(stream_xml)
|
47
|
+
# elsif entry.name[/xl\/theme(\d+)?\.xml/]
|
48
|
+
# @themes << Excel::Theme.load_from_stream(stream_xml)
|
49
|
+
elsif entry.name[/xl\/comments(\d+)?\.xml/]
|
50
|
+
comment_id = entry.name.scan(/xl\/comments(\d+)\.xml/).flatten.first
|
51
|
+
@comments[comment_id] = Excel::Comments.load_from_stream(stream_xml)
|
52
|
+
elsif entry.name == "xl/sharedStrings.xml"
|
53
|
+
Nokogiri.XML(stream_xml).remove_namespaces!.xpath('sst/si').each do |shared_string_node|
|
54
|
+
shared_strings << shared_string_node.at('t').text
|
55
|
+
end
|
56
|
+
elsif entry.name == "xl/workbook.xml"
|
57
|
+
@workbook = Workbook.load_from_stream(stream_xml)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module OOXML
|
2
|
+
module Helper
|
3
|
+
module List
|
4
|
+
|
5
|
+
# excel.rb
|
6
|
+
# fetch dropdown values based on given data validation formula
|
7
|
+
def list_values(formula)
|
8
|
+
# "Lists!$J$2:$J$4"
|
9
|
+
# transform into useful info
|
10
|
+
|
11
|
+
# for list values explicitly stated
|
12
|
+
if formula.include?(',')
|
13
|
+
formula.gsub('"', '').split(',')
|
14
|
+
# invalid format
|
15
|
+
elsif !formula.include?('!') && formula[/$/]
|
16
|
+
puts "Warning: This formula is not yet supported: #{formula} in your Data Validation's formula."
|
17
|
+
[]
|
18
|
+
else
|
19
|
+
# # required for fetching values
|
20
|
+
sheet_name = formula.gsub(/[\$\']/, '').scan(/^[^!]*/).first
|
21
|
+
cell_range_formula = formula.gsub(/\$/, '').scan(/(?<=!).+/).first
|
22
|
+
|
23
|
+
# fetch the sheet of the cell reference
|
24
|
+
working_sheet = sheet(sheet_name)
|
25
|
+
|
26
|
+
# gather values
|
27
|
+
list_values = working_sheet.list_values_from_formula(cell_range_formula)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Used in sheet.rb
|
32
|
+
def list_value_formula(cell_ref)
|
33
|
+
data_validation = data_validations.find { |data_validation| data_validation.sqref_range.include?(cell_ref)}
|
34
|
+
if data_validation.respond_to?(:type) && data_validation.type == "list"
|
35
|
+
if data_validation.formula[/[\s\$\,\:]/]
|
36
|
+
(data_validation.formula[/\$/].present?) ? "#{name}!#{data_validation.formula}" : data_validation.formula
|
37
|
+
else
|
38
|
+
@defined_names.fetch(data_validation.formula)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def list_values_from_formula(formula)
|
44
|
+
return [] if formula.blank?
|
45
|
+
|
46
|
+
# Formula values separated by comma
|
47
|
+
if formula.include?(":")
|
48
|
+
cell_letters = formula.gsub(/[\d]/, '').split(':')
|
49
|
+
start_index, end_index = formula.gsub(/[^\d:]/, '').split(':').map(&:to_i)
|
50
|
+
|
51
|
+
cell_letter = cell_letters.uniq.first
|
52
|
+
(start_index..end_index).to_a.map do |row_index|
|
53
|
+
row = rows[row_index-1]
|
54
|
+
next if row.blank?
|
55
|
+
row["#{cell_letter}#{row_index}"].value
|
56
|
+
end
|
57
|
+
else
|
58
|
+
# when only one value: B2
|
59
|
+
row_index = formula.gsub(/[^\d:]/, '').split(':').map(&:to_i).first
|
60
|
+
row = rows[row_index-1]
|
61
|
+
return if row.blank?
|
62
|
+
[row[formula].value]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
module OOXML
|
2
|
+
class Excel
|
3
|
+
class Sheet
|
4
|
+
include OOXML::Helper::List
|
5
|
+
attr_reader :columns, :data_validations, :shared_strings
|
6
|
+
attr_accessor :comments, :styles, :defined_names, :name
|
7
|
+
|
8
|
+
def initialize(xml, shared_strings)
|
9
|
+
@xml = xml
|
10
|
+
@shared_strings = shared_strings
|
11
|
+
@comments = {}
|
12
|
+
@defined_names = {}
|
13
|
+
@styles = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def code_name
|
17
|
+
@code_name ||= @xml.xpath('//sheetPr').attribute('codeName').try(:value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def data_validation_for_cell(cell_ref)
|
21
|
+
data_validations.find { |data_validation| data_validation.sqref_range.include?(cell_ref)}
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def column(id)
|
26
|
+
columns.select { |column| column.id_range.include?(id)}
|
27
|
+
end
|
28
|
+
|
29
|
+
def columns
|
30
|
+
@columns ||= begin
|
31
|
+
@xml.xpath('//cols/col').map do |column_node|
|
32
|
+
Excel::Sheet::Column.load_from_node(column_node)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](id)
|
38
|
+
if id.is_a?(String)
|
39
|
+
rows.find { |row| row.id == id}
|
40
|
+
else
|
41
|
+
rows[id]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def rows
|
46
|
+
@rows ||= begin
|
47
|
+
# TODO: get the value of merged cells
|
48
|
+
# merged_cells = @xml.xpath('//mergeCells/mergeCell').map { |merged_cell| merged_cell.attributes["ref"].try(:value) }
|
49
|
+
@xml.xpath('//sheetData/row').map do |row_node|
|
50
|
+
Excel::Sheet::Row.load_from_node(row_node, shared_strings)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def font(cell_reference)
|
56
|
+
style_id = fetch_style_style_id(cell_reference)
|
57
|
+
if style_id.present?
|
58
|
+
style = @styles.by_id(style_id.to_i)
|
59
|
+
|
60
|
+
(style.present?) ? style[:font] : nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def fill(cell_reference)
|
65
|
+
style_id = fetch_style_style_id(cell_reference)
|
66
|
+
if style_id.present?
|
67
|
+
style = @styles.by_id(style_id.to_i)
|
68
|
+
(style.present?) ? style[:fill] : nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def each_row
|
73
|
+
rows.each_with_index do |row, row_index|
|
74
|
+
yield row.cells.map(&:value), row_index
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def each_row_as_object
|
79
|
+
0.upto(rows.size).each do |row_index|
|
80
|
+
yield rows[row_index]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def data_validations
|
85
|
+
@data_validations ||= begin
|
86
|
+
@xml.xpath('//dataValidations/dataValidation').map do |data_validation_node|
|
87
|
+
Excel::Sheet::DataValidation.load_from_node(data_validation_node)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.load_from_stream(xml_stream, shared_strings)
|
93
|
+
self.new(Nokogiri.XML(xml_stream).remove_namespaces!, shared_strings)
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def fetch_style_style_id(cell_reference)
|
98
|
+
raise 'Invalid Cell Reference!' if cell_reference[/[A-Z]{1,}\d+/].blank?
|
99
|
+
row_index = cell_reference.scan(/[A-Z{1,}](\d+)/).flatten.first.to_i - 1
|
100
|
+
return if rows[row_index].blank? || rows[row_index][cell_reference].blank?
|
101
|
+
rows[row_index][cell_reference].s
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
module OOXML
|
109
|
+
class Excel
|
110
|
+
class Sheet
|
111
|
+
class Column
|
112
|
+
attr_accessor :id, :width, :custom_width, :id_range, :hidden
|
113
|
+
alias_method :hidden?, :hidden
|
114
|
+
def initialize(**attrs)
|
115
|
+
attrs.each { |property, value| send("#{property}=", value)}
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.load_from_node(column_node)
|
119
|
+
hidden_attr = column_node.attributes["hidden"]
|
120
|
+
new(id: column_node.attributes["min"].try(:value),
|
121
|
+
width: column_node.attributes["width"].try(:value),
|
122
|
+
custom_width: column_node.attributes["custom_width"].try(:value),
|
123
|
+
id_range: (column_node.attributes["min"].value.to_i..column_node.attributes["max"].value.to_i).to_a,
|
124
|
+
hidden: (hidden_attr.present?) ? hidden_attr.value == "1" : false)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
module OOXML
|
132
|
+
class Excel
|
133
|
+
class Sheet
|
134
|
+
class Row
|
135
|
+
attr_accessor :id, :spans, :cells
|
136
|
+
|
137
|
+
def initialize(**attrs)
|
138
|
+
attrs.each { |property, value| send("#{property}=", value)}
|
139
|
+
end
|
140
|
+
|
141
|
+
def [](id)
|
142
|
+
if id.is_a?(String)
|
143
|
+
cells.find { |row| row.id == id}
|
144
|
+
else
|
145
|
+
cells[id]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.load_from_node(row_node, shared_strings)
|
150
|
+
new(id: row_node.attributes["r"].try(:value),
|
151
|
+
spans: row_node.attributes["spans"].try(:value),
|
152
|
+
cells: row_node.xpath('c').map { |cell_node| Row::Cell.load_from_node(cell_node, shared_strings) } )
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
module OOXML
|
160
|
+
class Excel
|
161
|
+
class Sheet
|
162
|
+
class Row
|
163
|
+
class Cell
|
164
|
+
attr_accessor :id, :t, :s, :v, :shared_strings
|
165
|
+
# t = type
|
166
|
+
# v = value
|
167
|
+
# s = ??
|
168
|
+
def initialize(**attrs)
|
169
|
+
attrs.each { |property, value| send("#{property}=", value)}
|
170
|
+
end
|
171
|
+
|
172
|
+
def value
|
173
|
+
(v.present?) ? shared_strings[v.to_i] : nil
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.load_from_node(cell_node, shared_strings)
|
177
|
+
new(id: cell_node.attributes["r"].try(:value),
|
178
|
+
t: cell_node.attributes["t"].try(:value),
|
179
|
+
s: cell_node.attributes["s"].try(:value),
|
180
|
+
v: cell_node.at('v').try(:text),
|
181
|
+
shared_strings: shared_strings )
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
module OOXML
|
191
|
+
class Excel
|
192
|
+
class Sheet
|
193
|
+
class DataValidation
|
194
|
+
attr_accessor :allow_blank, :prompt, :type, :sqref, :formula
|
195
|
+
|
196
|
+
def sqref_range
|
197
|
+
@sqref_range ||= begin
|
198
|
+
# "BH5:BH271 BI5:BI271"
|
199
|
+
sqref.split( ' ').map do |splitted_by_space_sqref|
|
200
|
+
# ["BH5:BH271, "BI5:BI271"]
|
201
|
+
if splitted_by_space_sqref.is_a?(Array)
|
202
|
+
splitted_by_space_sqref.map do |sqref|
|
203
|
+
split_sqref(sqref)
|
204
|
+
end
|
205
|
+
else
|
206
|
+
# "BH5:BH271"
|
207
|
+
split_sqref(splitted_by_space_sqref)
|
208
|
+
end
|
209
|
+
end.flatten.uniq
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.load_from_node(data_validation_node)
|
214
|
+
allow_blank = data_validation_node.attribute('allowBlank').try(:value)
|
215
|
+
prompt = data_validation_node.attribute('prompt').try(:value)
|
216
|
+
type = data_validation_node.attribute('type').try(:value)
|
217
|
+
sqref = data_validation_node.attribute('sqref').try(:value)
|
218
|
+
formula = data_validation_node.at('formula1').try(:content)
|
219
|
+
|
220
|
+
self.new(allow_blank: allow_blank,
|
221
|
+
prompt: prompt,
|
222
|
+
type: type,
|
223
|
+
sqref: sqref,
|
224
|
+
formula: formula)
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
def initialize(**attrs)
|
229
|
+
attrs.each { |property, value| send("#{property}=", value)}
|
230
|
+
end
|
231
|
+
|
232
|
+
def split_sqref(sqref)
|
233
|
+
# Example: "BH5:BH271"
|
234
|
+
# starting_reference: BH5
|
235
|
+
# ending_reference: BH271
|
236
|
+
starting_reference, ending_reference = sqref.split(":")
|
237
|
+
|
238
|
+
# if the starting_reference column letters are the same with ending_reference
|
239
|
+
# use the first one otherwise use both
|
240
|
+
if ending_reference.blank? || starting_reference[/A-Z{1,}/] == ending_reference[/A-Z{1,}/]
|
241
|
+
starting_reference
|
242
|
+
else
|
243
|
+
[starting_reference, ending_reference]
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module OOXML
|
2
|
+
class Excel
|
3
|
+
class Styles
|
4
|
+
attr_accessor :fonts, :fills, :cell_style_xfs
|
5
|
+
def initialize(**attrs)
|
6
|
+
attrs.each { |property, value| send("#{property}=", value)}
|
7
|
+
end
|
8
|
+
|
9
|
+
def by_id(id)
|
10
|
+
cell_style = cell_style_xfs.fetch(id)
|
11
|
+
{
|
12
|
+
font: fonts_by_index(cell_style.font_id),
|
13
|
+
fill: fills_by_index(cell_style.fill_id)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def fonts_by_index(font_index)
|
18
|
+
@fonts[font_index]
|
19
|
+
end
|
20
|
+
|
21
|
+
def fills_by_index(fill_index)
|
22
|
+
@fills[fill_index]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.load_from_stream(xml_stream)
|
26
|
+
style_doc = Nokogiri.XML(xml_stream).remove_namespaces!
|
27
|
+
fonts = style_doc.xpath('//fonts/font')
|
28
|
+
fills = style_doc.xpath('//fills/fill')
|
29
|
+
|
30
|
+
# This element contains the master formatting records (xf) which
|
31
|
+
# define the formatting applied to cells in this workbook.
|
32
|
+
# link: https://msdn.microsoft.com/en-us/library/documentformat.openxml.spreadsheet.cellformats(v=office.14).aspx
|
33
|
+
cell_style_xfs = style_doc.xpath('//cellXfs/xf')
|
34
|
+
|
35
|
+
self.new(
|
36
|
+
fonts: fonts.map { |font_node| Excel::Styles::Font.load_from_node(font_node)},
|
37
|
+
fills: fills.map { |fill_node| Excel::Styles::Fill.load_from_node(fill_node)},
|
38
|
+
cell_style_xfs: cell_style_xfs.map { |cell_style_xfs_node| Excel::Styles::CellStyleXfs.load_from_node(cell_style_xfs_node)}
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module OOXML
|
46
|
+
class Excel
|
47
|
+
class Styles
|
48
|
+
class Font
|
49
|
+
attr_accessor :size, :name, :rgb_color, :theme, :bold
|
50
|
+
alias_method :bold?, :bold
|
51
|
+
def initialize(**attrs)
|
52
|
+
attrs.each { |property, value| send("#{property}=", value)}
|
53
|
+
end
|
54
|
+
def self.load_from_node(font_node)
|
55
|
+
font_size_node = font_node.at('sz')
|
56
|
+
font_color_node = font_node.at('color')
|
57
|
+
font_name_node = font_node.at('name')
|
58
|
+
font_bold_node = font_node.at('b')
|
59
|
+
self.new(
|
60
|
+
size: font_size_node.attributes["val"].value,
|
61
|
+
name: font_name_node.attributes["val"].value,
|
62
|
+
rgb_color: (font_color_node.present?) ? font_color_node.attributes["rgb"].try(:value) : nil,
|
63
|
+
theme: (font_color_node.present?) ? font_color_node.attributes["theme"].try(:value) : nil,
|
64
|
+
bold: font_bold_node.present?
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# <xf numFmtId="0" borderId="0" fillId="0" fontId="0" applyAlignment="1" applyFont="1" xfId="0"/>
|
73
|
+
module OOXML
|
74
|
+
class Excel
|
75
|
+
class Styles
|
76
|
+
class CellStyleXfs
|
77
|
+
attr_accessor :id, :number_formatting_id, :fill_id, :font_id
|
78
|
+
def initialize(**attrs)
|
79
|
+
attrs.each { |property, value| send("#{property}=", value)}
|
80
|
+
end
|
81
|
+
def self.load_from_node(cell_style_xfs_node)
|
82
|
+
attributes = cell_style_xfs_node.attributes
|
83
|
+
|
84
|
+
|
85
|
+
self.new(
|
86
|
+
id: attributes["xfId"].value.to_i,
|
87
|
+
number_formatting_id: attributes["numFmtId"].value.to_i,
|
88
|
+
fill_id: attributes["fillId"].value.to_i,
|
89
|
+
font_id: attributes["fontId"].value.to_i
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
module OOXML
|
99
|
+
class Excel
|
100
|
+
class Styles
|
101
|
+
class Fill
|
102
|
+
attr_accessor :pattern_type, :fg_color, :fg_color_theme, :fg_color_tint, :bg_color_index, :bg_color
|
103
|
+
|
104
|
+
def initialize(**attrs)
|
105
|
+
attrs.each { |property, value| send("#{property}=", value)}
|
106
|
+
end
|
107
|
+
def self.load_from_node(fill_node)
|
108
|
+
pattern_fill = fill_node.at('patternFill')
|
109
|
+
|
110
|
+
pattern_type = pattern_fill.attributes["patternType"].value
|
111
|
+
if pattern_type == "solid"
|
112
|
+
fg_color = pattern_fill.at('fgColor')
|
113
|
+
bg_color = pattern_fill.at('bgColor')
|
114
|
+
self.new(pattern_type: pattern_type,
|
115
|
+
fg_color: (fg_color.present?) ? fg_color.attributes["rgb"].try(:value) : nil,
|
116
|
+
fg_color_theme: (fg_color.present?) ? fg_color.attributes["theme"].try(:value) : nil,
|
117
|
+
fg_color_tint: (fg_color.present?) ? fg_color.attributes["tint"].try(:value) : nil,
|
118
|
+
bg_color: (bg_color.present?) ? bg_color.attributes["rgb"].try(:value) : nil,
|
119
|
+
bg_color_index: (bg_color.present?) ? bg_color.attributes["index"].try(:value) : nil)
|
120
|
+
else
|
121
|
+
self.new(pattern_type: pattern_type)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module OOXML
|
2
|
+
class Excel
|
3
|
+
class Workbook
|
4
|
+
def initialize(xml)
|
5
|
+
@xml = xml
|
6
|
+
end
|
7
|
+
|
8
|
+
def sheets
|
9
|
+
@sheets ||= begin
|
10
|
+
@xml.xpath('//sheets/sheet').map do |sheet_node|
|
11
|
+
name = sheet_node.attribute('name').value
|
12
|
+
rel_id = sheet_node.attribute('id').value.gsub(/[^\d+]/, '')
|
13
|
+
sheet_id = sheet_node.attribute('sheetId').value
|
14
|
+
{ name: name, sheet_id: sheet_id, relationship_id: rel_id}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def defined_names
|
20
|
+
@defined_names ||= begin
|
21
|
+
@xml.xpath('//definedNames/definedName').map do |defined_names_node|
|
22
|
+
name = defined_names_node.attribute('name').value
|
23
|
+
reference = defined_names_node.text
|
24
|
+
[name, reference]
|
25
|
+
end.to_h
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def self.load_from_stream(xml_stream)
|
31
|
+
self.new (Nokogiri.XML(xml_stream).remove_namespaces!)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/ooxml_excel.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ooxml_excel/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ooxml_excel"
|
8
|
+
spec.version = OOXML::Excel::VERSION
|
9
|
+
spec.authors = ["James Mones"]
|
10
|
+
spec.email = ["bajong009@gmail.com"]
|
11
|
+
spec.summary = %q{OOXML Excel - Parse Excel Spreadsheets (xlsx, xlsm).}
|
12
|
+
spec.description = %q{A Ruby spreadsheet parser for Excel (xlsx, xlsm).}
|
13
|
+
spec.homepage = "https://github.com/halcjames/ooxml_excel"
|
14
|
+
|
15
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
16
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
17
|
+
# if spec.respond_to?(:metadata)
|
18
|
+
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
19
|
+
# else
|
20
|
+
# raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
21
|
+
# end
|
22
|
+
|
23
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
spec.bindir = "exe"
|
25
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
26
|
+
spec.require_paths = ["lib"]
|
27
|
+
spec.add_dependency 'activesupport'
|
28
|
+
spec.add_dependency 'nokogiri', '~> 1'
|
29
|
+
spec.add_dependency 'rubyzip', '~> 1.1', '< 2.0.0'
|
30
|
+
|
31
|
+
spec.add_development_dependency "bundler", "~> 1.12"
|
32
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ooxml_excel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Mones
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-08-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rubyzip
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.1'
|
48
|
+
- - "<"
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 2.0.0
|
51
|
+
type: :runtime
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '1.1'
|
58
|
+
- - "<"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 2.0.0
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: bundler
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.12'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.12'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rake
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '10.0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '10.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rspec
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '3.0'
|
103
|
+
description: A Ruby spreadsheet parser for Excel (xlsx, xlsm).
|
104
|
+
email:
|
105
|
+
- bajong009@gmail.com
|
106
|
+
executables: []
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files: []
|
109
|
+
files:
|
110
|
+
- ".gitignore"
|
111
|
+
- ".rspec"
|
112
|
+
- ".travis.yml"
|
113
|
+
- Gemfile
|
114
|
+
- README.md
|
115
|
+
- Rakefile
|
116
|
+
- bin/console
|
117
|
+
- bin/setup
|
118
|
+
- lib/ooxml_excel.rb
|
119
|
+
- lib/ooxml_excel/comments.rb
|
120
|
+
- lib/ooxml_excel/excel.rb
|
121
|
+
- lib/ooxml_excel/helper/list.rb
|
122
|
+
- lib/ooxml_excel/sheet.rb
|
123
|
+
- lib/ooxml_excel/styles.rb
|
124
|
+
- lib/ooxml_excel/version.rb
|
125
|
+
- lib/ooxml_excel/workbook.rb
|
126
|
+
- ooxml_excel.gemspec
|
127
|
+
homepage: https://github.com/halcjames/ooxml_excel
|
128
|
+
licenses: []
|
129
|
+
metadata: {}
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 2.6.4
|
147
|
+
signing_key:
|
148
|
+
specification_version: 4
|
149
|
+
summary: OOXML Excel - Parse Excel Spreadsheets (xlsx, xlsm).
|
150
|
+
test_files: []
|