spreadshoot 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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/examples/basic.rb +86 -0
- data/lib/spreadshoot/version.rb +3 -0
- data/lib/spreadshoot.rb +528 -0
- data/spreadshoot.gemspec +19 -0
- metadata +65 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Mladen Jablanović
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Spreadshoot
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'spreadshoot'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install spreadshoot
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/examples/basic.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spreadshoot'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
spreadsheet = Spreadshoot.new do |s|
|
5
|
+
s.worksheet('Simple') do |w|
|
6
|
+
w.row do |r|
|
7
|
+
r.cell Date.today
|
8
|
+
r.cell 'foo'
|
9
|
+
@foo = r.cell 2
|
10
|
+
end
|
11
|
+
w.row do |r|
|
12
|
+
r.cell Date.today + 1
|
13
|
+
r.cell 'bar', :font => 'Times New Roman'
|
14
|
+
@bar = r.cell 3
|
15
|
+
end
|
16
|
+
w.row # empty one
|
17
|
+
w.row(:border => :top, :bold => true) do |r|
|
18
|
+
r.cell('total', :align => :center)
|
19
|
+
r.cell # empty cell
|
20
|
+
r.cell :formula => "#{@foo} + #{@bar}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
s.worksheet('Tables') do |w|
|
25
|
+
w.table(:direction => :horizontal) do |t|
|
26
|
+
t.row do |r|
|
27
|
+
r.cell 'foo'
|
28
|
+
@foo = r.cell 2
|
29
|
+
end
|
30
|
+
t.row do |r|
|
31
|
+
r.cell 'bar'
|
32
|
+
@bar = r.cell 3
|
33
|
+
end
|
34
|
+
t.row # empty one
|
35
|
+
t.row(:line => :above, :bold => true) do |r|
|
36
|
+
r.cell 'total'
|
37
|
+
r.cell # empty cell
|
38
|
+
@total1 = r.cell :formula => "#{@foo} + #{@bar}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
w.row # empty row
|
42
|
+
w.table do |t| # another table
|
43
|
+
t.row do |r|
|
44
|
+
r.cell 'foo'
|
45
|
+
@foo2 = r.cell 6
|
46
|
+
end
|
47
|
+
t.row do |r|
|
48
|
+
r.cell 'bar'
|
49
|
+
@bar2 = r.cell 7
|
50
|
+
end
|
51
|
+
t.row # empty one
|
52
|
+
t.row(:line => :above, :bold => true) do |r|
|
53
|
+
r.cell 'total'
|
54
|
+
r.cell # empty cell
|
55
|
+
@total2 = r.cell :formula => "#{@foo2} + #{@bar2}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
w.row do |r|
|
59
|
+
r.cell 'Grand total:'
|
60
|
+
r.cell
|
61
|
+
r.cell :formula => "#{@total1} + #{@total2}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
s.worksheet('Inverted Tables') do |w|
|
66
|
+
w.table(:direction => :horizontal) do |t|
|
67
|
+
t.row do |r|
|
68
|
+
r.cell 'foo'
|
69
|
+
@foo = r.cell 2
|
70
|
+
end
|
71
|
+
t.row do |r|
|
72
|
+
r.cell 'bar'
|
73
|
+
@bar = r.cell 3
|
74
|
+
end
|
75
|
+
t.row # empty one
|
76
|
+
t.row(:line => :above, :bold => true) do |r|
|
77
|
+
r.cell 'total'
|
78
|
+
r.cell # empty cell
|
79
|
+
r.cell :formula => "#{@foo} + #{@bar}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
spreadsheet.dump
|
86
|
+
spreadsheet.save(ARGV[0])
|
data/lib/spreadshoot.rb
ADDED
@@ -0,0 +1,528 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'date'
|
3
|
+
require 'builder'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
class Spreadshoot
|
7
|
+
|
8
|
+
# Create a new sheet, with given default formatting options
|
9
|
+
def initialize options = {}, &block
|
10
|
+
@worksheets = []
|
11
|
+
@ss = {}
|
12
|
+
@borders = {}
|
13
|
+
@fonts = {}
|
14
|
+
@styles = {}
|
15
|
+
@components = Set.new
|
16
|
+
yield(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Gets the shared index of a given string
|
20
|
+
def ss_index string
|
21
|
+
@components << :ss
|
22
|
+
unless i = @ss[string]
|
23
|
+
i = @ss.length
|
24
|
+
@ss[string] = i
|
25
|
+
end
|
26
|
+
i
|
27
|
+
end
|
28
|
+
|
29
|
+
# gets the shared index of a border set (one or more of :top, :bottom, :left, :right)
|
30
|
+
def border borders
|
31
|
+
borders = [borders] unless borders.is_a?(Array)
|
32
|
+
borders.sort!
|
33
|
+
unless i = @borders[borders]
|
34
|
+
i = @borders.length
|
35
|
+
@borders[borders] = i
|
36
|
+
end
|
37
|
+
i
|
38
|
+
end
|
39
|
+
|
40
|
+
def font options
|
41
|
+
unless i = @fonts[options]
|
42
|
+
i = @fonts.length
|
43
|
+
@fonts[options] = i
|
44
|
+
end
|
45
|
+
i
|
46
|
+
end
|
47
|
+
|
48
|
+
# gets the shared index of a cell style (given by options hash)
|
49
|
+
def style options = {}
|
50
|
+
font = {}
|
51
|
+
style = options.each_with_object({}) do |(option, value), acc|
|
52
|
+
case option
|
53
|
+
when :border
|
54
|
+
acc[:border] = self.border(value)
|
55
|
+
when :align
|
56
|
+
acc[:align] = value
|
57
|
+
when :bold, :italic, :font
|
58
|
+
font[option] = value
|
59
|
+
when :format
|
60
|
+
acc[:format] = case value
|
61
|
+
when :date
|
62
|
+
14
|
63
|
+
when :percent
|
64
|
+
10
|
65
|
+
else
|
66
|
+
value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
style[:font] = self.font(font) unless font.empty?
|
71
|
+
return nil if style.empty?
|
72
|
+
|
73
|
+
unless i = @styles[style]
|
74
|
+
i = @styles.length
|
75
|
+
@styles[style] = i
|
76
|
+
end
|
77
|
+
i
|
78
|
+
end
|
79
|
+
|
80
|
+
# Create a new worksheet within a spreadsheet
|
81
|
+
def worksheet title, options = {}, &block
|
82
|
+
ws = Worksheet.new(self, title, options, &block)
|
83
|
+
@worksheets << ws
|
84
|
+
end
|
85
|
+
|
86
|
+
# Saves the spreadsheet to an XLSX file with a given name
|
87
|
+
def save filename
|
88
|
+
dir = '/tmp/spreadshoot/'
|
89
|
+
FileUtils.rm_rf(dir)
|
90
|
+
FileUtils.mkdir_p(dir)
|
91
|
+
FileUtils.mkdir_p(File.join(dir, '_rels'))
|
92
|
+
FileUtils.mkdir_p(File.join(dir, 'xl', 'worksheets'))
|
93
|
+
FileUtils.mkdir_p(File.join(dir, 'xl', '_rels'))
|
94
|
+
File.open(File.join(dir, '[Content_Types].xml'), 'w') do |f|
|
95
|
+
f.write content_types
|
96
|
+
end
|
97
|
+
File.open(File.join(dir, '_rels', '.rels'), 'w') do |f|
|
98
|
+
f.write rels
|
99
|
+
end
|
100
|
+
File.open(File.join(dir, 'xl', 'workbook.xml'), 'w') do |f|
|
101
|
+
f.write workbook
|
102
|
+
end
|
103
|
+
@worksheets.each_with_index do |ws, i|
|
104
|
+
File.open(File.join(dir, 'xl', 'worksheets', "sheet#{i+1}.xml"), 'w') do |f|
|
105
|
+
f.write(ws)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
File.open(File.join(dir, 'xl', 'sharedStrings.xml'), 'w') do |f|
|
109
|
+
f.write shared_strings
|
110
|
+
end if @components.member?(:ss)
|
111
|
+
File.open(File.join(dir, 'xl', 'styles.xml'), 'w') do |f|
|
112
|
+
f.write styles
|
113
|
+
end
|
114
|
+
File.open(File.join(dir, 'xl', '_rels', 'workbook.xml.rels'), 'w') do |f|
|
115
|
+
f.write xl_rels
|
116
|
+
end
|
117
|
+
|
118
|
+
filename = File.absolute_path(filename)
|
119
|
+
FileUtils.chdir(dir)
|
120
|
+
File.delete(filename) if File.exists?(filename)
|
121
|
+
# zip the result
|
122
|
+
puts `zip -r #{filename} ./`
|
123
|
+
# FileUtils.rmdir_rf(dir)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Dumps main XMLs to the stdout (for debugging purposes)
|
127
|
+
def dump
|
128
|
+
puts @xml
|
129
|
+
|
130
|
+
@worksheets.each do |ws|
|
131
|
+
puts '==='
|
132
|
+
puts ws
|
133
|
+
end
|
134
|
+
|
135
|
+
puts '==='
|
136
|
+
puts shared_strings
|
137
|
+
|
138
|
+
puts '==='
|
139
|
+
puts styles
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Outputs final workbook XML
|
145
|
+
# <workbook
|
146
|
+
# xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
147
|
+
# xmlns:r="http:// schemas.openxmlformats.org /officeDocument/2006/relationships">
|
148
|
+
# <sheets>
|
149
|
+
# <sheet name="Sheet1" sheetId="1" r:id="rId1" />
|
150
|
+
# </sheets>
|
151
|
+
# </workbook>
|
152
|
+
def workbook
|
153
|
+
Builder::XmlMarkup.new.workbook(:xmlns => "http://schemas.openxmlformats.org/spreadsheetml/2006/main", :"xmlns:r" => "http://schemas.openxmlformats.org/officeDocument/2006/relationships") do |wb|
|
154
|
+
wb.sheets do |sheets|
|
155
|
+
@worksheets.each_with_index do |ws, i|
|
156
|
+
sheets.sheet(:name => ws.title, :sheetId => i+1, :"r:id" => "rId#{i+1}")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
163
|
+
# <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
164
|
+
# <Default Extension="bin" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings"/>
|
165
|
+
# <Override PartName="/customXml/itemProps2.xml" ContentType="application/vnd.openxmlformats-officedocument.customXmlProperties+xml"/>
|
166
|
+
# <Override PartName="/customXml/itemProps3.xml" ContentType="application/vnd.openxmlformats-officedocument.customXmlProperties+xml"/>
|
167
|
+
# <Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
|
168
|
+
# <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
169
|
+
# <Override PartName="/customXml/itemProps1.xml" ContentType="application/vnd.openxmlformats-officedocument.customXmlProperties+xml"/>
|
170
|
+
# <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
171
|
+
# <Default Extension="xml" ContentType="application/xml"/>
|
172
|
+
# <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
173
|
+
# <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
|
174
|
+
# <Override PartName="/xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
175
|
+
# <Override PartName="/xl/worksheets/sheet3.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
176
|
+
# <Override PartName="/xl/drawings/drawing1.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml"/>
|
177
|
+
# <Override PartName="/xl/comments2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>
|
178
|
+
# <Override PartName="/docProps/custom.xml" ContentType="application/vnd.openxmlformats-officedocument.custom-properties+xml"/>
|
179
|
+
# <Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
180
|
+
# <Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>
|
181
|
+
# <Override PartName="/xl/comments1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>
|
182
|
+
# <Override PartName="/xl/calcChain.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"/>
|
183
|
+
# <Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>
|
184
|
+
# <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
|
185
|
+
# </Types>
|
186
|
+
def content_types
|
187
|
+
Builder::XmlMarkup.new.Types(:xmlns => "http://schemas.openxmlformats.org/package/2006/content-types") do |xt|
|
188
|
+
xt.Default(:Extension => "bin", :ContentType => "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings")
|
189
|
+
xt.Default(:Extension => "rels", :ContentType => "application/vnd.openxmlformats-package.relationships+xml")
|
190
|
+
xt.Default(:Extension => "xml", :ContentType => "application/xml")
|
191
|
+
xt.Default(:Extension => "vml", :ContentType => "application/vnd.openxmlformats-officedocument.vmlDrawing")
|
192
|
+
xt.Override :PartName => "/xl/workbook.xml", :ContentType => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
|
193
|
+
xt.Override :PartName => "/xl/sharedStrings.xml", :ContentType => "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
|
194
|
+
xt.Override :PartName => "/xl/styles.xml", :ContentType => "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
|
195
|
+
@worksheets.count.times do |i|
|
196
|
+
xt.Override :PartName => "/xl/worksheets/sheet#{i+1}.xml", :ContentType => "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
202
|
+
# <si>
|
203
|
+
# <t>Region</t>
|
204
|
+
# </si>
|
205
|
+
# <si>
|
206
|
+
# <t>Sales Person</t>
|
207
|
+
# </si>
|
208
|
+
# <si>
|
209
|
+
# <t>Sales Quota</t>
|
210
|
+
# </si>
|
211
|
+
# ...12 more items ...
|
212
|
+
# </sst>
|
213
|
+
def shared_strings
|
214
|
+
Builder::XmlMarkup.new.sst(:xmlns => "http://schemas.openxmlformats.org/spreadsheetml/2006/main") do |xsst|
|
215
|
+
@ss.keys.each do |str|
|
216
|
+
xsst.si do |xsi|
|
217
|
+
xsi.t(str)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
224
|
+
# <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
225
|
+
# </Relationships>
|
226
|
+
def rels
|
227
|
+
Builder::XmlMarkup.new.Relationships(:xmlns => "http://schemas.openxmlformats.org/package/2006/relationships") do |rs|
|
228
|
+
rs.Relationship :Id => "rId1", :Type => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument", :Target => "xl/workbook.xml"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
233
|
+
# <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
234
|
+
# <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
|
235
|
+
# <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
236
|
+
# <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
|
237
|
+
# <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" Target="calcChain.xml"/>
|
238
|
+
# </Relationships>
|
239
|
+
def xl_rels
|
240
|
+
Builder::XmlMarkup.new.Relationships(:xmlns => "http://schemas.openxmlformats.org/package/2006/relationships") do |rs|
|
241
|
+
count = @worksheets.count
|
242
|
+
count.times do |i|
|
243
|
+
rs.Relationship :Id => "rId#{i+1}", :Type => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet", :Target => "worksheets/sheet#{i+1}.xml"
|
244
|
+
end
|
245
|
+
rs.Relationship :Id => "rId#{count+1}", :Type => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings", :Target => "sharedStrings.xml"
|
246
|
+
rs.Relationship(:Id => "rId#{count+2}", :Type => "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles", :Target => "styles.xml")
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def styles
|
251
|
+
Builder::XmlMarkup.new.styleSheet(:xmlns => "http://schemas.openxmlformats.org/spreadsheetml/2006/main") do |xs|
|
252
|
+
xs.fonts do |xf|
|
253
|
+
xf.font
|
254
|
+
@fonts.keys.each do |font|
|
255
|
+
xf.font do |x|
|
256
|
+
x.b(:val => 1) if font[:bold]
|
257
|
+
x.i(:val => 1) if font[:italic]
|
258
|
+
x.name(:val => font[:font]) if font[:font]
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
xs.fills do |xf|
|
264
|
+
xf.fill do |x|
|
265
|
+
x.patternFill(:patternType => 'none')
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
xs.borders do |xbs|
|
270
|
+
xbs.border do |xb|
|
271
|
+
[:left, :right, :top, :bottom, :diagonal].each do |kind|
|
272
|
+
xb.tag!(kind)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
@borders.keys.each do |border_set|
|
276
|
+
xbs.border do |xb|
|
277
|
+
[:left, :right, :top, :bottom, :diagonal].each do |kind|
|
278
|
+
if border_set.member?(kind)
|
279
|
+
xb.tag!(kind, :style => 'thin') do |x|
|
280
|
+
x.color(:rgb => 'FF000000')
|
281
|
+
end
|
282
|
+
else
|
283
|
+
xb.tag!(kind)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
xs.cellStyleXfs do |xcsx|
|
291
|
+
xcsx.xf(:fillId => 0, :borderId => 0, :numFmtId => 0)
|
292
|
+
end
|
293
|
+
|
294
|
+
xs.cellXfs do |xcx|
|
295
|
+
xcx.xf(:fillId => 0, :numFmtId => 0, :borderId => 0, :xfId => 0)
|
296
|
+
@styles.keys.each do |style|
|
297
|
+
align = style[:align]
|
298
|
+
border = style[:border]
|
299
|
+
font = style[:font]
|
300
|
+
options = {:fillId => 0, :xfId => 0} # default
|
301
|
+
options[:applyAlignment] = align ? 1 : 0
|
302
|
+
options[:applyBorder] = border ? 1 : 0
|
303
|
+
options[:borderId] = border ? border + 1 : 0
|
304
|
+
options[:fontId] = font + 1 if font
|
305
|
+
options[:applyFont] = font ? 1 : 0
|
306
|
+
options[:numFmtId] = style[:format] ? style[:format] : 0
|
307
|
+
options[:applyNumberFormat] = style[:format] ? 1 : 0
|
308
|
+
xcx.xf(options) do |xf|
|
309
|
+
if align = style[:align]
|
310
|
+
xf.alignment(:horizontal => align)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
# xs.cellStyles do |xcs|
|
317
|
+
# xcs.cellStyle(:name => "Normal", :xfId => "0", :builtinId => "0")
|
318
|
+
# end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
class Worksheet
|
323
|
+
# <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" >
|
324
|
+
# <sheetData>
|
325
|
+
# <row>
|
326
|
+
# <c>
|
327
|
+
# <v>1234</v>
|
328
|
+
# </c>
|
329
|
+
# </row>
|
330
|
+
# </sheetData>
|
331
|
+
# </worksheet>
|
332
|
+
|
333
|
+
attr_reader :title, :xml, :spreadsheet, :row_index, :col_index, :cells
|
334
|
+
def initialize spreadsheet, title, options = {}, &block
|
335
|
+
@cells = {}
|
336
|
+
@spreadsheet = spreadsheet
|
337
|
+
@title = title
|
338
|
+
@options = options
|
339
|
+
@row_index = 0
|
340
|
+
@col_index = 0
|
341
|
+
@column_widths = {}
|
342
|
+
# default table, if none defined
|
343
|
+
@current_table = Table.new(self, options)
|
344
|
+
|
345
|
+
yield self
|
346
|
+
end
|
347
|
+
|
348
|
+
# outputs the worksheet as OOXML
|
349
|
+
def to_s
|
350
|
+
@xml ||= Builder::XmlMarkup.new.worksheet(:xmlns => "http://schemas.openxmlformats.org/spreadsheetml/2006/main") do |ws|
|
351
|
+
unless @column_widths.empty?
|
352
|
+
ws.cols do |xcols|
|
353
|
+
@column_widths.keys.sort.each do |i|
|
354
|
+
width = @column_widths[i]
|
355
|
+
params = {:min => i+1, :max => i+1, :bestFit => 1}
|
356
|
+
params.merge!({:customWidth => 1, :width => width}) if width
|
357
|
+
xcols.col(params)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
ws.sheetData do |sd|
|
362
|
+
@cells.keys.sort.each do |row|
|
363
|
+
sd.row(:r => row+1) do |xr|
|
364
|
+
|
365
|
+
@cells[row].keys.sort.each do |col|
|
366
|
+
cell = @cells[row][col]
|
367
|
+
cell.output(xr)
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def row options = {}, &block
|
376
|
+
row = @current_table.row options, &block
|
377
|
+
@row_index += 1
|
378
|
+
row
|
379
|
+
end
|
380
|
+
|
381
|
+
def table options = {}
|
382
|
+
@current_table = table = Table.new(self, options)
|
383
|
+
yield table
|
384
|
+
@row_index += table.row_max
|
385
|
+
@col_index = 0
|
386
|
+
@current_table = Table.new(self, @options) # preparing one in case row directly called next
|
387
|
+
table
|
388
|
+
end
|
389
|
+
|
390
|
+
def set_col_width col, width
|
391
|
+
@column_widths[col] = width
|
392
|
+
end
|
393
|
+
end # Worksheet
|
394
|
+
|
395
|
+
|
396
|
+
# Allows you to group cells to a logical table within a worksheet. Makes putting several tables
|
397
|
+
# to the same worksheet easier.
|
398
|
+
class Table
|
399
|
+
attr_reader :worksheet, :direction, :col_max, :row_max, :col_index, :row_index
|
400
|
+
|
401
|
+
def initialize worksheet, options = {}
|
402
|
+
@worksheet = worksheet
|
403
|
+
@options = options
|
404
|
+
@direction = options[:direction] || :vertical
|
405
|
+
@row_index = 0
|
406
|
+
@col_index = 0
|
407
|
+
@row_max = 0
|
408
|
+
@col_max = 0
|
409
|
+
@row_topleft = options[:row_topleft] || @worksheet.row_index
|
410
|
+
@col_topleft = options[:col_topleft] || @worksheet.col_index
|
411
|
+
end
|
412
|
+
|
413
|
+
def col_index= val
|
414
|
+
@col_max = val if val > @col_max
|
415
|
+
@col_index = val
|
416
|
+
end
|
417
|
+
|
418
|
+
def row_index= val
|
419
|
+
@row_max = val if val > @row_max
|
420
|
+
@row_index = val
|
421
|
+
end
|
422
|
+
|
423
|
+
def row options = {}
|
424
|
+
row = Row.new(self, options)
|
425
|
+
yield(row) if block_given?
|
426
|
+
|
427
|
+
if @direction == :vertical
|
428
|
+
self.row_index += 1
|
429
|
+
self.col_index = 0
|
430
|
+
else
|
431
|
+
self.col_index += 1
|
432
|
+
self.row_index = 0
|
433
|
+
end
|
434
|
+
row
|
435
|
+
end
|
436
|
+
|
437
|
+
def current_row
|
438
|
+
@row_topleft + @row_index
|
439
|
+
end
|
440
|
+
|
441
|
+
def current_col
|
442
|
+
@col_topleft + @col_index
|
443
|
+
end
|
444
|
+
|
445
|
+
# alphanumeric representation of coordinates
|
446
|
+
def coords
|
447
|
+
"#{Cell.alpha_index(current_col)}#{current_row+1}"
|
448
|
+
end
|
449
|
+
|
450
|
+
end
|
451
|
+
|
452
|
+
# A row of a table. The table could be horizontal oriented, or vertical oriented.
|
453
|
+
class Row
|
454
|
+
def initialize table, options = {}
|
455
|
+
@table = table
|
456
|
+
@options = options
|
457
|
+
end
|
458
|
+
|
459
|
+
def cell value = nil, options = {}
|
460
|
+
cell = Cell.new(@table, value, @options.merge(options))
|
461
|
+
@table.worksheet.cells[@table.current_row] ||= {}
|
462
|
+
@table.worksheet.cells[@table.current_row][@table.current_col] = cell
|
463
|
+
if @table.direction == :vertical
|
464
|
+
@table.col_index += 1
|
465
|
+
else
|
466
|
+
@table.row_index += 1
|
467
|
+
end
|
468
|
+
cell
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
# A cell within a row.
|
473
|
+
class Cell
|
474
|
+
# maps numeric column indices to letter based:
|
475
|
+
# 0 -> 'A', 1 -> 'B', 26 -> 'AA' and so on
|
476
|
+
def self.alpha_index i
|
477
|
+
@alpha_indices ||= ('A'..'ZZ').to_a
|
478
|
+
@alpha_indices[i]
|
479
|
+
end
|
480
|
+
|
481
|
+
def initialize table, value, options = {}
|
482
|
+
@table = table
|
483
|
+
@value = value
|
484
|
+
@options = options
|
485
|
+
@coords = @table.coords
|
486
|
+
@table.worksheet.set_col_width(@table.current_col, @options[:width]) if @options.has_key?(:width)
|
487
|
+
@options[:format] ||= :date if @value.is_a?(Date) || @value.is_a?(Time)
|
488
|
+
end
|
489
|
+
|
490
|
+
def current_col
|
491
|
+
@table.current_col
|
492
|
+
end
|
493
|
+
|
494
|
+
def current_row
|
495
|
+
@table.current_row
|
496
|
+
end
|
497
|
+
|
498
|
+
# outputs the cell into the resulting xml
|
499
|
+
def output xn_parent
|
500
|
+
r = {:r => @coords}
|
501
|
+
if style = @table.worksheet.spreadsheet.style(@options)
|
502
|
+
r.merge!(:s => style + 1)
|
503
|
+
end
|
504
|
+
case @value
|
505
|
+
when String
|
506
|
+
i = @table.worksheet.spreadsheet.ss_index(@value)
|
507
|
+
xn_parent.c(r.merge(:t => 's')){ |xc| xc.v(i) }
|
508
|
+
when Hash # no @value, formula in options
|
509
|
+
@options = @value
|
510
|
+
xn_parent.c(r) do |xc|
|
511
|
+
xc.f(@options[:formula])
|
512
|
+
end
|
513
|
+
when Date, Time
|
514
|
+
xn_parent.c(r){|xc| xc.v((@value - Date.new(1899,12,30)).to_i)}
|
515
|
+
when nil
|
516
|
+
xn_parent.c(r)
|
517
|
+
else
|
518
|
+
xn_parent.c(r){ |xc| xc.v(@value) }
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
def to_s
|
523
|
+
@coords
|
524
|
+
end
|
525
|
+
|
526
|
+
end
|
527
|
+
|
528
|
+
end
|
data/spreadshoot.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/spreadshoot/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Mladen Jablanovic"]
|
6
|
+
gem.email = ["jablan@radioni.ca"]
|
7
|
+
gem.description = %q{Create XLSX files from scratch using Ruby}
|
8
|
+
gem.summary = %q{Ruby DSL for creating Excel xlsx spreadsheets}
|
9
|
+
gem.homepage = "https://github.com/jablan/spreadshoot"
|
10
|
+
|
11
|
+
gem.add_dependency "builder"
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.name = "spreadshoot"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = Spreadshoot::VERSION
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spreadshoot
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mladen Jablanovic
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-27 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: builder
|
16
|
+
requirement: &79609200 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *79609200
|
25
|
+
description: Create XLSX files from scratch using Ruby
|
26
|
+
email:
|
27
|
+
- jablan@radioni.ca
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- .gitignore
|
33
|
+
- Gemfile
|
34
|
+
- LICENSE
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- examples/basic.rb
|
38
|
+
- lib/spreadshoot.rb
|
39
|
+
- lib/spreadshoot/version.rb
|
40
|
+
- spreadshoot.gemspec
|
41
|
+
homepage: https://github.com/jablan/spreadshoot
|
42
|
+
licenses: []
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.8.11
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Ruby DSL for creating Excel xlsx spreadsheets
|
65
|
+
test_files: []
|