rbxl 1.0.0
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/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/Rakefile +5 -0
- data/ext/rbxl_native/extconf.rb +51 -0
- data/ext/rbxl_native/native.c +677 -0
- data/lib/rbxl/cell.rb +3 -0
- data/lib/rbxl/empty_cell.rb +13 -0
- data/lib/rbxl/errors.rb +7 -0
- data/lib/rbxl/native.rb +15 -0
- data/lib/rbxl/read_only_cell.rb +3 -0
- data/lib/rbxl/read_only_workbook.rb +153 -0
- data/lib/rbxl/read_only_worksheet.rb +501 -0
- data/lib/rbxl/row.rb +23 -0
- data/lib/rbxl/version.rb +3 -0
- data/lib/rbxl/write_only_cell.rb +10 -0
- data/lib/rbxl/write_only_workbook.rb +143 -0
- data/lib/rbxl/write_only_worksheet.rb +180 -0
- data/lib/rbxl.rb +33 -0
- metadata +97 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
module Rbxl
|
|
2
|
+
class WriteOnlyWorkbook
|
|
3
|
+
attr_reader :worksheets
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@worksheets = []
|
|
7
|
+
@closed = false
|
|
8
|
+
@saved = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_sheet(name)
|
|
12
|
+
ensure_writable!
|
|
13
|
+
|
|
14
|
+
sheet = WriteOnlyWorksheet.new(name: name)
|
|
15
|
+
@worksheets << sheet
|
|
16
|
+
sheet
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def save(path)
|
|
20
|
+
ensure_writable!
|
|
21
|
+
raise Error, "at least one worksheet is required" if worksheets.empty?
|
|
22
|
+
|
|
23
|
+
Zip::OutputStream.open(path) do |zip|
|
|
24
|
+
write_entry(zip, "[Content_Types].xml", content_types_xml)
|
|
25
|
+
write_entry(zip, "_rels/.rels", root_rels_xml)
|
|
26
|
+
write_entry(zip, "xl/workbook.xml", workbook_xml)
|
|
27
|
+
write_entry(zip, "xl/_rels/workbook.xml.rels", workbook_rels_xml)
|
|
28
|
+
write_entry(zip, "xl/styles.xml", styles_xml)
|
|
29
|
+
|
|
30
|
+
worksheets.each_with_index do |sheet, index|
|
|
31
|
+
write_entry(zip, "xl/worksheets/sheet#{index + 1}.xml", sheet.to_xml)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@saved = true
|
|
36
|
+
close
|
|
37
|
+
path
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def close
|
|
41
|
+
@closed = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def closed?
|
|
45
|
+
@closed
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def ensure_writable!
|
|
51
|
+
raise ClosedWorkbookError, "workbook has been closed" if closed?
|
|
52
|
+
raise WorkbookAlreadySavedError, "write-only workbook can only be saved once" if @saved
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def write_entry(zip, name, content)
|
|
56
|
+
zip.put_next_entry(name)
|
|
57
|
+
zip.write(content)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def content_types_xml
|
|
61
|
+
worksheet_overrides = worksheets.each_index.map do |index|
|
|
62
|
+
%(<Override PartName="/xl/worksheets/sheet#{index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>)
|
|
63
|
+
end.join
|
|
64
|
+
|
|
65
|
+
<<~XML.chomp
|
|
66
|
+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
67
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
68
|
+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
69
|
+
<Default Extension="xml" ContentType="application/xml"/>
|
|
70
|
+
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
|
71
|
+
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
|
72
|
+
#{worksheet_overrides}
|
|
73
|
+
</Types>
|
|
74
|
+
XML
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def root_rels_xml
|
|
78
|
+
<<~XML.chomp
|
|
79
|
+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
80
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
81
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
|
82
|
+
</Relationships>
|
|
83
|
+
XML
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def workbook_xml
|
|
87
|
+
sheet_nodes = worksheets.each_with_index.map do |sheet, index|
|
|
88
|
+
%(<sheet name="#{escape(sheet.name)}" sheetId="#{index + 1}" r:id="rId#{index + 1}"/>)
|
|
89
|
+
end.join
|
|
90
|
+
|
|
91
|
+
<<~XML.chomp
|
|
92
|
+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
93
|
+
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
94
|
+
<sheets>#{sheet_nodes}</sheets>
|
|
95
|
+
</workbook>
|
|
96
|
+
XML
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def workbook_rels_xml
|
|
100
|
+
relationship_nodes = worksheets.each_with_index.map do |_, index|
|
|
101
|
+
%(<Relationship Id="rId#{index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet#{index + 1}.xml"/>)
|
|
102
|
+
end
|
|
103
|
+
relationship_nodes << %(<Relationship Id="rId#{worksheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>)
|
|
104
|
+
|
|
105
|
+
<<~XML.chomp
|
|
106
|
+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
107
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
108
|
+
#{relationship_nodes.join}
|
|
109
|
+
</Relationships>
|
|
110
|
+
XML
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def styles_xml
|
|
114
|
+
<<~XML.chomp
|
|
115
|
+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
116
|
+
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
117
|
+
<fonts count="1">
|
|
118
|
+
<font><sz val="11"/><name val="Calibri"/></font>
|
|
119
|
+
</fonts>
|
|
120
|
+
<fills count="1">
|
|
121
|
+
<fill><patternFill patternType="none"/></fill>
|
|
122
|
+
</fills>
|
|
123
|
+
<borders count="1">
|
|
124
|
+
<border><left/><right/><top/><bottom/><diagonal/></border>
|
|
125
|
+
</borders>
|
|
126
|
+
<cellStyleXfs count="1">
|
|
127
|
+
<xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
|
|
128
|
+
</cellStyleXfs>
|
|
129
|
+
<cellXfs count="1">
|
|
130
|
+
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
|
|
131
|
+
</cellXfs>
|
|
132
|
+
<cellStyles count="1">
|
|
133
|
+
<cellStyle name="Normal" xfId="0" builtinId="0"/>
|
|
134
|
+
</cellStyles>
|
|
135
|
+
</styleSheet>
|
|
136
|
+
XML
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def escape(value)
|
|
140
|
+
CGI.escapeHTML(value.to_s)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
module Rbxl
|
|
2
|
+
class WriteOnlyWorksheet
|
|
3
|
+
attr_reader :name
|
|
4
|
+
|
|
5
|
+
def initialize(name:)
|
|
6
|
+
@name = name
|
|
7
|
+
@rows = []
|
|
8
|
+
@column_name_cache = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def <<(values)
|
|
12
|
+
append(values)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def append(values)
|
|
16
|
+
unless values.is_a?(Array) || values.is_a?(Enumerator)
|
|
17
|
+
raise TypeError, "row must be an Array or Enumerator, got #{values.class}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@rows << Array(values)
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_xml
|
|
25
|
+
if defined?(Rbxl::Native)
|
|
26
|
+
return Rbxl::Native.generate_sheet(@rows)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
dimension_ref = @rows.empty? ? "A1" : "A1:#{column_name(max_columns)}#{@rows.length}"
|
|
30
|
+
buf = +""
|
|
31
|
+
buf << '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
32
|
+
buf << "\n"
|
|
33
|
+
buf << '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
|
|
34
|
+
buf << "\n"
|
|
35
|
+
buf << ' <dimension ref="'
|
|
36
|
+
buf << dimension_ref
|
|
37
|
+
buf << '"/>'
|
|
38
|
+
buf << "\n"
|
|
39
|
+
buf << ' <sheetData>'
|
|
40
|
+
|
|
41
|
+
@rows.each_with_index do |row_values, row_index|
|
|
42
|
+
row_num_str = (row_index + 1).to_s
|
|
43
|
+
buf << '<row r="'
|
|
44
|
+
buf << row_num_str
|
|
45
|
+
buf << '">'
|
|
46
|
+
row_values.each_with_index do |value, col_index|
|
|
47
|
+
serialize_cell_to(buf, column_name(col_index + 1), row_num_str, value)
|
|
48
|
+
end
|
|
49
|
+
buf << '</row>'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
buf << "</sheetData>\n</worksheet>"
|
|
53
|
+
buf
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def serialize_cell_to(buf, col_name, row_num_str, value)
|
|
59
|
+
if value.is_a?(WriteOnlyCell)
|
|
60
|
+
serialize_write_only_cell_to(buf, col_name, row_num_str, value)
|
|
61
|
+
return
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
buf << '<c r="'
|
|
65
|
+
buf << col_name
|
|
66
|
+
buf << row_num_str
|
|
67
|
+
case value
|
|
68
|
+
when nil
|
|
69
|
+
buf << '"/>'
|
|
70
|
+
when true
|
|
71
|
+
buf << '" t="b"><v>1</v></c>'
|
|
72
|
+
when false
|
|
73
|
+
buf << '" t="b"><v>0</v></c>'
|
|
74
|
+
when Integer
|
|
75
|
+
buf << '"><v>'
|
|
76
|
+
buf << value.to_s
|
|
77
|
+
buf << '</v></c>'
|
|
78
|
+
when Numeric
|
|
79
|
+
buf << '"><v>'
|
|
80
|
+
buf << value.to_s
|
|
81
|
+
buf << '</v></c>'
|
|
82
|
+
when Date, DateTime, Time
|
|
83
|
+
buf << '" t="inlineStr"><is><t>'
|
|
84
|
+
escape_to(buf, value.iso8601)
|
|
85
|
+
buf << '</t></is></c>'
|
|
86
|
+
else
|
|
87
|
+
buf << '" t="inlineStr"><is><t>'
|
|
88
|
+
escape_to(buf, value.to_s)
|
|
89
|
+
buf << '</t></is></c>'
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def escape_to(buf, str)
|
|
94
|
+
i = 0
|
|
95
|
+
len = str.bytesize
|
|
96
|
+
start = 0
|
|
97
|
+
|
|
98
|
+
while i < len
|
|
99
|
+
byte = str.getbyte(i)
|
|
100
|
+
case byte
|
|
101
|
+
when 38 # &
|
|
102
|
+
buf << str.byteslice(start, i - start) if i > start
|
|
103
|
+
buf << '&'
|
|
104
|
+
start = i + 1
|
|
105
|
+
when 60 # <
|
|
106
|
+
buf << str.byteslice(start, i - start) if i > start
|
|
107
|
+
buf << '<'
|
|
108
|
+
start = i + 1
|
|
109
|
+
when 62 # >
|
|
110
|
+
buf << str.byteslice(start, i - start) if i > start
|
|
111
|
+
buf << '>'
|
|
112
|
+
start = i + 1
|
|
113
|
+
when 34 # "
|
|
114
|
+
buf << str.byteslice(start, i - start) if i > start
|
|
115
|
+
buf << '"'
|
|
116
|
+
start = i + 1
|
|
117
|
+
end
|
|
118
|
+
i += 1
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if start == 0
|
|
122
|
+
buf << str
|
|
123
|
+
elsif start < len
|
|
124
|
+
buf << str.byteslice(start, len - start)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def column_name(index)
|
|
129
|
+
@column_name_cache[index] ||= begin
|
|
130
|
+
name = +""
|
|
131
|
+
current = index
|
|
132
|
+
while current.positive?
|
|
133
|
+
current -= 1
|
|
134
|
+
name.prepend((65 + (current % 26)).chr)
|
|
135
|
+
current /= 26
|
|
136
|
+
end
|
|
137
|
+
name.freeze
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def max_columns
|
|
142
|
+
max = 0
|
|
143
|
+
@rows.each { |r| max = r.length if r.length > max }
|
|
144
|
+
max > 0 ? max : 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def serialize_write_only_cell_to(buf, col_name, row_num_str, cell)
|
|
148
|
+
buf << '<c r="'
|
|
149
|
+
buf << col_name
|
|
150
|
+
buf << row_num_str
|
|
151
|
+
buf << '"'
|
|
152
|
+
if cell.style_id
|
|
153
|
+
buf << ' s="'
|
|
154
|
+
buf << cell.style_id.to_s
|
|
155
|
+
buf << '"'
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
case cell.value
|
|
159
|
+
when nil
|
|
160
|
+
buf << '/>'
|
|
161
|
+
when true
|
|
162
|
+
buf << ' t="b"><v>1</v></c>'
|
|
163
|
+
when false
|
|
164
|
+
buf << ' t="b"><v>0</v></c>'
|
|
165
|
+
when Numeric
|
|
166
|
+
buf << '><v>'
|
|
167
|
+
buf << cell.value.to_s
|
|
168
|
+
buf << '</v></c>'
|
|
169
|
+
when Date, DateTime, Time
|
|
170
|
+
buf << ' t="inlineStr"><is><t>'
|
|
171
|
+
escape_to(buf, cell.value.iso8601)
|
|
172
|
+
buf << '</t></is></c>'
|
|
173
|
+
else
|
|
174
|
+
buf << ' t="inlineStr"><is><t>'
|
|
175
|
+
escape_to(buf, cell.value.to_s)
|
|
176
|
+
buf << '</t></is></c>'
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
data/lib/rbxl.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
require "date"
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
require "stringio"
|
|
5
|
+
require "zip"
|
|
6
|
+
|
|
7
|
+
require_relative "rbxl/cell"
|
|
8
|
+
require_relative "rbxl/empty_cell"
|
|
9
|
+
require_relative "rbxl/errors"
|
|
10
|
+
require_relative "rbxl/read_only_cell"
|
|
11
|
+
require_relative "rbxl/read_only_workbook"
|
|
12
|
+
require_relative "rbxl/read_only_worksheet"
|
|
13
|
+
require_relative "rbxl/row"
|
|
14
|
+
require_relative "rbxl/version"
|
|
15
|
+
require_relative "rbxl/write_only_cell"
|
|
16
|
+
require_relative "rbxl/write_only_workbook"
|
|
17
|
+
require_relative "rbxl/write_only_worksheet"
|
|
18
|
+
|
|
19
|
+
module Rbxl
|
|
20
|
+
class << self
|
|
21
|
+
def open(path, read_only: false)
|
|
22
|
+
raise ArgumentError, "read_only: true is required for this MVP" unless read_only
|
|
23
|
+
|
|
24
|
+
ReadOnlyWorkbook.open(path)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def new(write_only: false)
|
|
28
|
+
raise ArgumentError, "write_only: true is required for this MVP" unless write_only
|
|
29
|
+
|
|
30
|
+
WriteOnlyWorkbook.new
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rbxl
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Taro KOBAYASHI
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rubyzip
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: nokogiri
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.19'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '2.0'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '1.19'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.0'
|
|
46
|
+
description: A small Ruby gem for read-only and write-only xlsx workflows.
|
|
47
|
+
email:
|
|
48
|
+
- taro@matzlika.co.jp
|
|
49
|
+
executables: []
|
|
50
|
+
extensions:
|
|
51
|
+
- ext/rbxl_native/extconf.rb
|
|
52
|
+
extra_rdoc_files: []
|
|
53
|
+
files:
|
|
54
|
+
- LICENSE.txt
|
|
55
|
+
- README.md
|
|
56
|
+
- Rakefile
|
|
57
|
+
- ext/rbxl_native/extconf.rb
|
|
58
|
+
- ext/rbxl_native/native.c
|
|
59
|
+
- lib/rbxl.rb
|
|
60
|
+
- lib/rbxl/cell.rb
|
|
61
|
+
- lib/rbxl/empty_cell.rb
|
|
62
|
+
- lib/rbxl/errors.rb
|
|
63
|
+
- lib/rbxl/native.rb
|
|
64
|
+
- lib/rbxl/read_only_cell.rb
|
|
65
|
+
- lib/rbxl/read_only_workbook.rb
|
|
66
|
+
- lib/rbxl/read_only_worksheet.rb
|
|
67
|
+
- lib/rbxl/row.rb
|
|
68
|
+
- lib/rbxl/version.rb
|
|
69
|
+
- lib/rbxl/write_only_cell.rb
|
|
70
|
+
- lib/rbxl/write_only_workbook.rb
|
|
71
|
+
- lib/rbxl/write_only_worksheet.rb
|
|
72
|
+
homepage: https://github.com/matzlika/rbxl
|
|
73
|
+
licenses:
|
|
74
|
+
- MIT
|
|
75
|
+
metadata:
|
|
76
|
+
homepage_uri: https://github.com/matzlika/rbxl
|
|
77
|
+
source_code_uri: https://github.com/matzlika/rbxl
|
|
78
|
+
bug_tracker_uri: https://github.com/matzlika/rbxl/issues
|
|
79
|
+
changelog_uri: https://github.com/matzlika/rbxl/releases
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '3.1'
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubygems_version: 4.0.3
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: Streaming xlsx reader/writer inspired by openpyxl
|
|
97
|
+
test_files: []
|