rbxl 1.2.0 → 1.4.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 +4 -4
- data/CHANGELOG.md +55 -1
- data/README.md +126 -12
- data/Rakefile +19 -0
- data/lib/rbxl/editable_cell.rb +176 -0
- data/lib/rbxl/editable_workbook.rb +315 -0
- data/lib/rbxl/editable_worksheet.rb +216 -0
- data/lib/rbxl/errors.rb +12 -0
- data/lib/rbxl/read_only_workbook.rb +78 -88
- data/lib/rbxl/shared_strings_loader.rb +100 -0
- data/lib/rbxl/version.rb +1 -1
- data/lib/rbxl.rb +61 -10
- data/sig/rbxl.rbs +70 -2
- metadata +6 -2
|
@@ -30,6 +30,16 @@ module Rbxl
|
|
|
30
30
|
# Namespace used by the OPC package relationships layer.
|
|
31
31
|
PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
|
|
32
32
|
|
|
33
|
+
# First 8 bytes of the OLE Compound File Binary format (legacy .xls,
|
|
34
|
+
# .doc, .ppt). Sniffed to short-circuit into a typed error before
|
|
35
|
+
# rubyzip bubbles up an opaque "end of central directory" failure.
|
|
36
|
+
OLE_CFB_MAGIC = "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1".b.freeze
|
|
37
|
+
private_constant :OLE_CFB_MAGIC
|
|
38
|
+
|
|
39
|
+
# ZIP local file header signature — the first bytes of every .xlsx.
|
|
40
|
+
ZIP_LOCAL_MAGIC = "PK\x03\x04".b.freeze
|
|
41
|
+
private_constant :ZIP_LOCAL_MAGIC
|
|
42
|
+
|
|
33
43
|
# @return [String] filesystem path the workbook was opened from
|
|
34
44
|
attr_reader :path
|
|
35
45
|
|
|
@@ -39,14 +49,28 @@ module Rbxl
|
|
|
39
49
|
# Convenience constructor equivalent to
|
|
40
50
|
# <tt>new(path, streaming:, date_conversion:)</tt>.
|
|
41
51
|
#
|
|
52
|
+
# When a block is given, the workbook is yielded to the block and
|
|
53
|
+
# {#close} is called automatically when the block returns (or raises).
|
|
54
|
+
# The block's return value is returned to the caller, matching the
|
|
55
|
+
# +File.open+ / +Zip::File.open+ idiom.
|
|
56
|
+
#
|
|
42
57
|
# @param path [String, #to_path] path to the <tt>.xlsx</tt> file
|
|
43
58
|
# @param streaming [Boolean] feed worksheet XML to the native parser in
|
|
44
59
|
# chunks (see {Rbxl.open})
|
|
45
60
|
# @param date_conversion [Boolean] convert numeric cells backed by a
|
|
46
61
|
# date/time +numFmt+ to Ruby date/time objects (see {Rbxl.open})
|
|
47
|
-
# @
|
|
62
|
+
# @yieldparam book [Rbxl::ReadOnlyWorkbook] the opened workbook
|
|
63
|
+
# @return [Rbxl::ReadOnlyWorkbook, Object] the workbook when no block is
|
|
64
|
+
# given, otherwise the block's return value
|
|
48
65
|
def self.open(path, streaming: false, date_conversion: false)
|
|
49
|
-
new(path, streaming: streaming, date_conversion: date_conversion)
|
|
66
|
+
book = new(path, streaming: streaming, date_conversion: date_conversion)
|
|
67
|
+
return book unless block_given?
|
|
68
|
+
|
|
69
|
+
begin
|
|
70
|
+
yield book
|
|
71
|
+
ensure
|
|
72
|
+
book.close
|
|
73
|
+
end
|
|
50
74
|
end
|
|
51
75
|
|
|
52
76
|
# Opens the ZIP archive, pre-loads shared strings, and indexes the
|
|
@@ -58,10 +82,11 @@ module Rbxl
|
|
|
58
82
|
# date-style lookup table to produced worksheets
|
|
59
83
|
def initialize(path, streaming: false, date_conversion: false)
|
|
60
84
|
@path = path
|
|
85
|
+
ensure_xlsx_format!(path)
|
|
61
86
|
@zip = Zip::File.open(path)
|
|
62
87
|
@streaming = streaming
|
|
63
88
|
@date_conversion = date_conversion
|
|
64
|
-
@shared_strings =
|
|
89
|
+
@shared_strings = SharedStringsLoader.load(@zip)
|
|
65
90
|
@sheet_entries = load_sheet_entries
|
|
66
91
|
@sheet_names = @sheet_entries.keys.freeze
|
|
67
92
|
@date_styles = nil
|
|
@@ -69,18 +94,23 @@ module Rbxl
|
|
|
69
94
|
@closed = false
|
|
70
95
|
end
|
|
71
96
|
|
|
72
|
-
# Returns a row-by-row worksheet by visible sheet name
|
|
97
|
+
# Returns a row-by-row worksheet by visible sheet name or by 0-based
|
|
98
|
+
# index into {#sheet_names}. Negative indexes count from the end, so
|
|
99
|
+
# <tt>sheet(-1)</tt> returns the last sheet.
|
|
73
100
|
#
|
|
74
101
|
# The returned object shares the workbook's ZIP handle. Closing the
|
|
75
102
|
# workbook invalidates any worksheets produced by prior calls.
|
|
76
103
|
#
|
|
77
|
-
# @param
|
|
104
|
+
# @param name_or_index [String, Integer] visible sheet name as listed in
|
|
105
|
+
# {#sheet_names}, or an integer index into that list
|
|
78
106
|
# @return [Rbxl::ReadOnlyWorksheet]
|
|
79
|
-
# @raise [Rbxl::SheetNotFoundError] if +
|
|
107
|
+
# @raise [Rbxl::SheetNotFoundError] if +name_or_index+ does not resolve
|
|
108
|
+
# to a sheet
|
|
80
109
|
# @raise [Rbxl::ClosedWorkbookError] if the workbook has been closed
|
|
81
|
-
def sheet(
|
|
110
|
+
def sheet(name_or_index)
|
|
82
111
|
ensure_open!
|
|
83
112
|
|
|
113
|
+
name = resolve_sheet_name(name_or_index)
|
|
84
114
|
entry_path = @sheet_entries.fetch(name) do
|
|
85
115
|
raise SheetNotFoundError, "sheet not found: #{name}"
|
|
86
116
|
end
|
|
@@ -97,6 +127,23 @@ module Rbxl
|
|
|
97
127
|
)
|
|
98
128
|
end
|
|
99
129
|
|
|
130
|
+
# Iterates the workbook's sheets in workbook order. Each worksheet is
|
|
131
|
+
# constructed on demand, so <tt>sheets.first</tt> allocates only the
|
|
132
|
+
# first sheet and <tt>sheets.lazy.find { ... }</tt> stops as soon as a
|
|
133
|
+
# match is found. Returned objects share the same ZIP handle and
|
|
134
|
+
# cached shared-strings / date-style tables as {#sheet}.
|
|
135
|
+
#
|
|
136
|
+
# @yieldparam worksheet [Rbxl::ReadOnlyWorksheet]
|
|
137
|
+
# @return [Enumerator<Rbxl::ReadOnlyWorksheet>] when no block is given
|
|
138
|
+
# @return [void] when a block is given
|
|
139
|
+
# @raise [Rbxl::ClosedWorkbookError] if the workbook has been closed
|
|
140
|
+
def sheets
|
|
141
|
+
ensure_open!
|
|
142
|
+
return enum_for(:sheets) unless block_given?
|
|
143
|
+
|
|
144
|
+
@sheet_names.each { |name| yield sheet(name) }
|
|
145
|
+
end
|
|
146
|
+
|
|
100
147
|
# Releases the underlying ZIP file handle. Idempotent; subsequent calls
|
|
101
148
|
# are no-ops.
|
|
102
149
|
#
|
|
@@ -119,6 +166,30 @@ module Rbxl
|
|
|
119
166
|
raise ClosedWorkbookError, "workbook has been closed" if closed?
|
|
120
167
|
end
|
|
121
168
|
|
|
169
|
+
def resolve_sheet_name(key)
|
|
170
|
+
return key unless key.is_a?(Integer)
|
|
171
|
+
|
|
172
|
+
name = @sheet_names[key]
|
|
173
|
+
return name if name
|
|
174
|
+
|
|
175
|
+
raise SheetNotFoundError, "sheet index out of range: #{key} (#{@sheet_names.length} sheet(s))"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def ensure_xlsx_format!(path)
|
|
179
|
+
header = File.binread(path, 8)
|
|
180
|
+
return if header.start_with?(ZIP_LOCAL_MAGIC)
|
|
181
|
+
|
|
182
|
+
if header.start_with?(OLE_CFB_MAGIC)
|
|
183
|
+
raise UnsupportedFormatError,
|
|
184
|
+
"#{path} looks like a legacy .xls (BIFF/CFB). " \
|
|
185
|
+
"rbxl supports .xlsx (OOXML) only; convert first, e.g. " \
|
|
186
|
+
"`libreoffice --headless --convert-to xlsx #{File.basename(path.to_s)}`."
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
raise UnsupportedFormatError,
|
|
190
|
+
"#{path} is not a valid .xlsx (no ZIP signature at offset 0)."
|
|
191
|
+
end
|
|
192
|
+
|
|
122
193
|
# Built-in numFmtId values that Excel resolves to date/time formats.
|
|
123
194
|
# Ids outside this set are dates only when the workbook provides a
|
|
124
195
|
# matching custom +<numFmt>+ entry whose format code contains date
|
|
@@ -185,87 +256,6 @@ module Rbxl
|
|
|
185
256
|
stripped.match?(/[ymdhs]/i)
|
|
186
257
|
end
|
|
187
258
|
|
|
188
|
-
def load_shared_strings
|
|
189
|
-
entry = @zip.find_entry("xl/sharedStrings.xml")
|
|
190
|
-
return [] unless entry
|
|
191
|
-
|
|
192
|
-
max_count = Rbxl.max_shared_strings
|
|
193
|
-
max_bytes = Rbxl.max_shared_string_bytes
|
|
194
|
-
|
|
195
|
-
# Reject zip-bomb style entries up front using the ZIP directory's
|
|
196
|
-
# declared uncompressed size, before allocating any decompression buffer.
|
|
197
|
-
if max_bytes && entry.size && entry.size > max_bytes
|
|
198
|
-
raise SharedStringsTooLargeError,
|
|
199
|
-
"shared strings uncompressed size #{entry.size} exceeds limit #{max_bytes}"
|
|
200
|
-
end
|
|
201
|
-
|
|
202
|
-
strings = []
|
|
203
|
-
total_bytes = 0
|
|
204
|
-
io = entry.get_input_stream
|
|
205
|
-
reader = Nokogiri::XML::Reader(io)
|
|
206
|
-
|
|
207
|
-
in_si = false
|
|
208
|
-
in_run = false
|
|
209
|
-
in_phonetic = false
|
|
210
|
-
collecting_text = false
|
|
211
|
-
buffer = +""
|
|
212
|
-
current_fragments = []
|
|
213
|
-
|
|
214
|
-
reader.each do |node|
|
|
215
|
-
case node.node_type
|
|
216
|
-
when Nokogiri::XML::Reader::TYPE_ELEMENT
|
|
217
|
-
case node.local_name
|
|
218
|
-
when "si"
|
|
219
|
-
in_si = true
|
|
220
|
-
current_fragments = []
|
|
221
|
-
when "r"
|
|
222
|
-
in_run = true if in_si
|
|
223
|
-
when "rPh"
|
|
224
|
-
in_phonetic = true if in_si
|
|
225
|
-
when "t"
|
|
226
|
-
next unless in_si && !in_phonetic
|
|
227
|
-
|
|
228
|
-
collecting_text = !in_run || node.depth.positive?
|
|
229
|
-
buffer.clear if collecting_text
|
|
230
|
-
end
|
|
231
|
-
when Nokogiri::XML::Reader::TYPE_TEXT, Nokogiri::XML::Reader::TYPE_CDATA
|
|
232
|
-
buffer << node.value if collecting_text
|
|
233
|
-
when Nokogiri::XML::Reader::TYPE_END_ELEMENT
|
|
234
|
-
case node.local_name
|
|
235
|
-
when "t"
|
|
236
|
-
if collecting_text
|
|
237
|
-
current_fragments << buffer.dup
|
|
238
|
-
collecting_text = false
|
|
239
|
-
end
|
|
240
|
-
when "r"
|
|
241
|
-
in_run = false
|
|
242
|
-
when "rPh"
|
|
243
|
-
in_phonetic = false
|
|
244
|
-
when "si"
|
|
245
|
-
value = current_fragments.join.freeze
|
|
246
|
-
total_bytes += value.bytesize
|
|
247
|
-
if max_bytes && total_bytes > max_bytes
|
|
248
|
-
raise SharedStringsTooLargeError,
|
|
249
|
-
"shared strings total size exceeds limit #{max_bytes}"
|
|
250
|
-
end
|
|
251
|
-
strings << value
|
|
252
|
-
if max_count && strings.size > max_count
|
|
253
|
-
raise SharedStringsTooLargeError,
|
|
254
|
-
"shared strings count exceeds limit #{max_count}"
|
|
255
|
-
end
|
|
256
|
-
in_si = false
|
|
257
|
-
in_run = false
|
|
258
|
-
in_phonetic = false
|
|
259
|
-
collecting_text = false
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
strings
|
|
265
|
-
ensure
|
|
266
|
-
io&.close
|
|
267
|
-
end
|
|
268
|
-
|
|
269
259
|
def load_sheet_entries
|
|
270
260
|
relationships = load_relationship_targets("xl/_rels/workbook.xml.rels")
|
|
271
261
|
sheets = {}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
module Rbxl
|
|
2
|
+
# Streams +xl/sharedStrings.xml+ out of an opened +.xlsx+ ZIP and decodes
|
|
3
|
+
# the table to an immutable +Array<String>+.
|
|
4
|
+
#
|
|
5
|
+
# Both the read-only and edit modes need this same view of the SST. The
|
|
6
|
+
# logic is identical — phonetic guides are skipped, +<r>+/+<t>+ runs inside
|
|
7
|
+
# an +<si>+ are concatenated, the count and byte caps configured on
|
|
8
|
+
# {Rbxl} are enforced — so it lives here as a single source of truth
|
|
9
|
+
# rather than being inlined twice.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module SharedStringsLoader
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param zip [Zip::File] the open package
|
|
16
|
+
# @return [Array<String>] frozen, index-aligned shared strings table
|
|
17
|
+
# @raise [Rbxl::SharedStringsTooLargeError] if the table exceeds the
|
|
18
|
+
# configured count or byte limits
|
|
19
|
+
def load(zip)
|
|
20
|
+
entry = zip.find_entry("xl/sharedStrings.xml")
|
|
21
|
+
return [].freeze unless entry
|
|
22
|
+
|
|
23
|
+
max_count = Rbxl.max_shared_strings
|
|
24
|
+
max_bytes = Rbxl.max_shared_string_bytes
|
|
25
|
+
|
|
26
|
+
# Reject zip-bomb style entries up front using the ZIP directory's
|
|
27
|
+
# declared uncompressed size, before allocating any decompression buffer.
|
|
28
|
+
if max_bytes && entry.size && entry.size > max_bytes
|
|
29
|
+
raise SharedStringsTooLargeError,
|
|
30
|
+
"shared strings uncompressed size #{entry.size} exceeds limit #{max_bytes}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
strings = []
|
|
34
|
+
total_bytes = 0
|
|
35
|
+
io = entry.get_input_stream
|
|
36
|
+
reader = Nokogiri::XML::Reader(io)
|
|
37
|
+
|
|
38
|
+
in_si = false
|
|
39
|
+
in_run = false
|
|
40
|
+
in_phonetic = false
|
|
41
|
+
collecting_text = false
|
|
42
|
+
buffer = +""
|
|
43
|
+
current_fragments = []
|
|
44
|
+
|
|
45
|
+
reader.each do |node|
|
|
46
|
+
case node.node_type
|
|
47
|
+
when Nokogiri::XML::Reader::TYPE_ELEMENT
|
|
48
|
+
case node.local_name
|
|
49
|
+
when "si"
|
|
50
|
+
in_si = true
|
|
51
|
+
current_fragments = []
|
|
52
|
+
when "r"
|
|
53
|
+
in_run = true if in_si
|
|
54
|
+
when "rPh"
|
|
55
|
+
in_phonetic = true if in_si
|
|
56
|
+
when "t"
|
|
57
|
+
next unless in_si && !in_phonetic
|
|
58
|
+
|
|
59
|
+
collecting_text = !in_run || node.depth.positive?
|
|
60
|
+
buffer.clear if collecting_text
|
|
61
|
+
end
|
|
62
|
+
when Nokogiri::XML::Reader::TYPE_TEXT, Nokogiri::XML::Reader::TYPE_CDATA
|
|
63
|
+
buffer << node.value if collecting_text
|
|
64
|
+
when Nokogiri::XML::Reader::TYPE_END_ELEMENT
|
|
65
|
+
case node.local_name
|
|
66
|
+
when "t"
|
|
67
|
+
if collecting_text
|
|
68
|
+
current_fragments << buffer.dup
|
|
69
|
+
collecting_text = false
|
|
70
|
+
end
|
|
71
|
+
when "r"
|
|
72
|
+
in_run = false
|
|
73
|
+
when "rPh"
|
|
74
|
+
in_phonetic = false
|
|
75
|
+
when "si"
|
|
76
|
+
value = current_fragments.join.freeze
|
|
77
|
+
total_bytes += value.bytesize
|
|
78
|
+
if max_bytes && total_bytes > max_bytes
|
|
79
|
+
raise SharedStringsTooLargeError,
|
|
80
|
+
"shared strings total size exceeds limit #{max_bytes}"
|
|
81
|
+
end
|
|
82
|
+
strings << value
|
|
83
|
+
if max_count && strings.size > max_count
|
|
84
|
+
raise SharedStringsTooLargeError,
|
|
85
|
+
"shared strings count exceeds limit #{max_count}"
|
|
86
|
+
end
|
|
87
|
+
in_si = false
|
|
88
|
+
in_run = false
|
|
89
|
+
in_phonetic = false
|
|
90
|
+
collecting_text = false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
strings.freeze
|
|
96
|
+
ensure
|
|
97
|
+
io&.close
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/lib/rbxl/version.rb
CHANGED
data/lib/rbxl.rb
CHANGED
|
@@ -8,9 +8,13 @@ require "zip"
|
|
|
8
8
|
require_relative "rbxl/cell"
|
|
9
9
|
require_relative "rbxl/empty_cell"
|
|
10
10
|
require_relative "rbxl/errors"
|
|
11
|
+
require_relative "rbxl/shared_strings_loader"
|
|
11
12
|
require_relative "rbxl/read_only_cell"
|
|
12
13
|
require_relative "rbxl/read_only_workbook"
|
|
13
14
|
require_relative "rbxl/read_only_worksheet"
|
|
15
|
+
require_relative "rbxl/editable_cell"
|
|
16
|
+
require_relative "rbxl/editable_worksheet"
|
|
17
|
+
require_relative "rbxl/editable_workbook"
|
|
14
18
|
require_relative "rbxl/row"
|
|
15
19
|
require_relative "rbxl/version"
|
|
16
20
|
require_relative "rbxl/write_only_cell"
|
|
@@ -19,9 +23,13 @@ require_relative "rbxl/write_only_worksheet"
|
|
|
19
23
|
|
|
20
24
|
# Minimal, memory-friendly XLSX reader/writer inspired by +openpyxl+.
|
|
21
25
|
#
|
|
22
|
-
# Rbxl exposes
|
|
26
|
+
# Rbxl exposes three explicit, non-overlapping modes, each picked up by
|
|
27
|
+
# {Rbxl.open} / {Rbxl.new}:
|
|
23
28
|
#
|
|
24
29
|
# * {Rbxl.open} returns a {Rbxl::ReadOnlyWorkbook} for row-by-row reads
|
|
30
|
+
# * {Rbxl.open} with <tt>edit: true</tt> returns a {Rbxl::EditableWorkbook}
|
|
31
|
+
# for surgical read-modify-save passes that round-trip every untouched
|
|
32
|
+
# part byte-for-byte
|
|
25
33
|
# * {Rbxl.new} returns a {Rbxl::WriteOnlyWorkbook} for one-shot writes
|
|
26
34
|
#
|
|
27
35
|
# The API is intentionally narrow so that memory usage stays predictable
|
|
@@ -86,11 +94,32 @@ module Rbxl
|
|
|
86
94
|
# @return [Integer, nil] per-worksheet streaming byte cap
|
|
87
95
|
attr_accessor :max_worksheet_bytes
|
|
88
96
|
|
|
89
|
-
# Opens an existing workbook
|
|
97
|
+
# Opens an existing workbook.
|
|
98
|
+
#
|
|
99
|
+
# By default opens in read-only row-by-row mode and returns a
|
|
100
|
+
# {Rbxl::ReadOnlyWorkbook}. Pass <tt>edit: true</tt> to open in
|
|
101
|
+
# read-modify-save mode and receive a {Rbxl::EditableWorkbook} instead.
|
|
102
|
+
# The two modes are wired up here at the module level so call sites pick
|
|
103
|
+
# a mode by keyword without juggling backend classes directly.
|
|
90
104
|
#
|
|
91
105
|
# The +read_only+ keyword defaults to +true+ and exists to mark the
|
|
92
|
-
# intent explicitly at the call site. Passing +read_only: false+
|
|
93
|
-
# {NotImplementedError}
|
|
106
|
+
# intent explicitly at the call site. Passing +read_only: false+ without
|
|
107
|
+
# also passing +edit: true+ raises {NotImplementedError} — there is no
|
|
108
|
+
# promiscuous read/write mode that mixes streaming reads with surgical
|
|
109
|
+
# writes.
|
|
110
|
+
#
|
|
111
|
+
# When a block is given, the workbook is yielded and automatically
|
|
112
|
+
# closed when the block returns (or raises), mirroring the +File.open+
|
|
113
|
+
# and +Zip::File.open+ idiom:
|
|
114
|
+
#
|
|
115
|
+
# Rbxl.open("report.xlsx") do |book|
|
|
116
|
+
# book.sheet("Report").each_row(values_only: true) { |row| p row }
|
|
117
|
+
# end
|
|
118
|
+
#
|
|
119
|
+
# Rbxl.open("template.xlsx", edit: true) do |book|
|
|
120
|
+
# book.sheet("Sheet1")["B5"].value = "Acme Inc."
|
|
121
|
+
# book.save
|
|
122
|
+
# end
|
|
94
123
|
#
|
|
95
124
|
# With <tt>streaming: true</tt>, the native backend (when loaded) feeds
|
|
96
125
|
# worksheet XML to the parser in chunks pulled from the ZIP input stream
|
|
@@ -110,19 +139,41 @@ module Rbxl
|
|
|
110
139
|
# disables the native fast path and routes reads through the Ruby
|
|
111
140
|
# worksheet parser.
|
|
112
141
|
#
|
|
142
|
+
# +streaming:+ and +date_conversion:+ are read-mode options and are
|
|
143
|
+
# rejected when paired with +edit: true+, since the editable backend
|
|
144
|
+
# does not run worksheets through the streaming parser.
|
|
145
|
+
#
|
|
113
146
|
# @param path [String, #to_path] filesystem path to an <tt>.xlsx</tt> file
|
|
114
|
-
# @param read_only [Boolean] retained for call-site clarity; must be
|
|
147
|
+
# @param read_only [Boolean] retained for call-site clarity; must be
|
|
148
|
+
# +true+ unless +edit: true+ is also passed
|
|
149
|
+
# @param edit [Boolean] open in read-modify-save mode; returns an
|
|
150
|
+
# {Rbxl::EditableWorkbook}
|
|
115
151
|
# @param streaming [Boolean] feed worksheet XML to the native parser in
|
|
116
152
|
# chunks instead of fully inflating the entry in advance. Ignored when
|
|
117
153
|
# the native extension is not loaded.
|
|
118
154
|
# @param date_conversion [Boolean] convert numeric cells backed by a
|
|
119
155
|
# date/time +numFmt+ to +Date+ / +Time+ / +DateTime+
|
|
120
|
-
# @
|
|
121
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
156
|
+
# @yieldparam book [Rbxl::ReadOnlyWorkbook, Rbxl::EditableWorkbook]
|
|
157
|
+
# opened workbook; auto-closed when the block returns
|
|
158
|
+
# @return [Rbxl::ReadOnlyWorkbook, Rbxl::EditableWorkbook, Object] the
|
|
159
|
+
# workbook when no block is given, otherwise the block's return value
|
|
160
|
+
# @raise [NotImplementedError] if +read_only+ is +false+ without
|
|
161
|
+
# +edit: true+
|
|
162
|
+
# @raise [ArgumentError] if +edit: true+ is paired with read-only options
|
|
163
|
+
def open(path, read_only: true, edit: false, streaming: false, date_conversion: false, &block)
|
|
164
|
+
if edit
|
|
165
|
+
if streaming || date_conversion
|
|
166
|
+
raise ArgumentError,
|
|
167
|
+
"edit: true is incompatible with streaming:/date_conversion:; " \
|
|
168
|
+
"those options apply to the read-only mode"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
return EditableWorkbook.open(path, &block)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
raise NotImplementedError, "read/write mode is not supported; pass read_only: true or edit: true" unless read_only
|
|
124
175
|
|
|
125
|
-
ReadOnlyWorkbook.open(path, streaming: streaming, date_conversion: date_conversion)
|
|
176
|
+
ReadOnlyWorkbook.open(path, streaming: streaming, date_conversion: date_conversion, &block)
|
|
126
177
|
end
|
|
127
178
|
|
|
128
179
|
# Creates a new workbook in write-only mode.
|
data/sig/rbxl.rbs
CHANGED
|
@@ -9,7 +9,10 @@ module Rbxl
|
|
|
9
9
|
type row_cells = Array[row_cell]
|
|
10
10
|
type dimensions = { ref: String, max_col: Integer, max_row: Integer }
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
type editable_cell_value = String | Integer | Float | bool | nil
|
|
13
|
+
|
|
14
|
+
def self.open: (pathish path, ?read_only: bool, ?edit: bool, ?streaming: bool, ?date_conversion: bool) -> (ReadOnlyWorkbook | EditableWorkbook)
|
|
15
|
+
| [T] (pathish path, ?read_only: bool, ?edit: bool, ?streaming: bool, ?date_conversion: bool) { (ReadOnlyWorkbook | EditableWorkbook) -> T } -> T
|
|
13
16
|
def self.new: (?write_only: bool) -> WriteOnlyWorkbook
|
|
14
17
|
|
|
15
18
|
attr_accessor self.max_shared_strings: Integer?
|
|
@@ -37,6 +40,21 @@ module Rbxl
|
|
|
37
40
|
class WorksheetTooLargeError < Error
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
class UnsupportedFormatError < Error
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class WorkbookFormatError < Error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class WorksheetFormatError < Error
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class CellValueError < WorksheetFormatError
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class EditableCellTypeError < Error
|
|
56
|
+
end
|
|
57
|
+
|
|
40
58
|
class Cell
|
|
41
59
|
attr_accessor value: cell_value
|
|
42
60
|
attr_accessor coordinate: String?
|
|
@@ -84,8 +102,11 @@ module Rbxl
|
|
|
84
102
|
attr_reader sheet_names: Array[String]
|
|
85
103
|
|
|
86
104
|
def self.open: (pathish path, ?streaming: bool, ?date_conversion: bool) -> ReadOnlyWorkbook
|
|
105
|
+
| [T] (pathish path, ?streaming: bool, ?date_conversion: bool) { (ReadOnlyWorkbook) -> T } -> T
|
|
87
106
|
def initialize: (pathish path, ?streaming: bool, ?date_conversion: bool) -> void
|
|
88
|
-
def sheet: (String
|
|
107
|
+
def sheet: (String | Integer name_or_index) -> ReadOnlyWorksheet
|
|
108
|
+
def sheets: () { (ReadOnlyWorksheet) -> void } -> void
|
|
109
|
+
| () -> Enumerator[ReadOnlyWorksheet, void]
|
|
89
110
|
def close: () -> void
|
|
90
111
|
def closed?: () -> bool
|
|
91
112
|
end
|
|
@@ -125,4 +146,51 @@ module Rbxl
|
|
|
125
146
|
def append: (row_input values) -> WriteOnlyWorksheet
|
|
126
147
|
def to_xml: () -> String
|
|
127
148
|
end
|
|
149
|
+
|
|
150
|
+
class EditableWorkbook
|
|
151
|
+
MAIN_NS: String
|
|
152
|
+
REL_NS: String
|
|
153
|
+
PACKAGE_REL_NS: String
|
|
154
|
+
OFFICE_DOC_REL_TYPE: String
|
|
155
|
+
|
|
156
|
+
attr_reader path: String
|
|
157
|
+
attr_reader sheet_names: Array[String]
|
|
158
|
+
|
|
159
|
+
def self.open: (pathish path) -> EditableWorkbook
|
|
160
|
+
| [T] (pathish path) { (EditableWorkbook) -> T } -> T
|
|
161
|
+
def initialize: (pathish path) -> void
|
|
162
|
+
def sheet: (String | Integer name_or_index) -> EditableWorksheet
|
|
163
|
+
def sheets: () { (EditableWorksheet) -> void } -> void
|
|
164
|
+
| () -> Enumerator[EditableWorksheet, void]
|
|
165
|
+
def save: (?pathish? path) -> String
|
|
166
|
+
def close: () -> bool
|
|
167
|
+
def closed?: () -> bool
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
class EditableWorksheet
|
|
171
|
+
MAIN_NS: String
|
|
172
|
+
|
|
173
|
+
attr_reader name: String
|
|
174
|
+
attr_reader entry_path: String
|
|
175
|
+
|
|
176
|
+
def initialize: (zip: untyped, entry_path: String, workbook_path: String, shared_strings: Array[String], name: String) -> void
|
|
177
|
+
def cell: (String coordinate) -> EditableCell
|
|
178
|
+
def []: (String coordinate) -> EditableCell
|
|
179
|
+
def dirty?: () -> bool
|
|
180
|
+
def to_xml: () -> String
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class EditableCell
|
|
184
|
+
MAIN_NS: String
|
|
185
|
+
|
|
186
|
+
attr_reader coordinate: String
|
|
187
|
+
|
|
188
|
+
def initialize: (worksheet: EditableWorksheet, coordinate: String) -> void
|
|
189
|
+
def value: () -> editable_cell_value
|
|
190
|
+
def value=: (editable_cell_value new_value) -> editable_cell_value
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
module SharedStringsLoader
|
|
194
|
+
def self.load: (untyped zip) -> Array[String]
|
|
195
|
+
end
|
|
128
196
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rbxl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Taro KOBAYASHI
|
|
@@ -60,6 +60,9 @@ files:
|
|
|
60
60
|
- ext/rbxl_native/native.c
|
|
61
61
|
- lib/rbxl.rb
|
|
62
62
|
- lib/rbxl/cell.rb
|
|
63
|
+
- lib/rbxl/editable_cell.rb
|
|
64
|
+
- lib/rbxl/editable_workbook.rb
|
|
65
|
+
- lib/rbxl/editable_worksheet.rb
|
|
63
66
|
- lib/rbxl/empty_cell.rb
|
|
64
67
|
- lib/rbxl/errors.rb
|
|
65
68
|
- lib/rbxl/native.rb
|
|
@@ -67,6 +70,7 @@ files:
|
|
|
67
70
|
- lib/rbxl/read_only_workbook.rb
|
|
68
71
|
- lib/rbxl/read_only_worksheet.rb
|
|
69
72
|
- lib/rbxl/row.rb
|
|
73
|
+
- lib/rbxl/shared_strings_loader.rb
|
|
70
74
|
- lib/rbxl/version.rb
|
|
71
75
|
- lib/rbxl/write_only_cell.rb
|
|
72
76
|
- lib/rbxl/write_only_workbook.rb
|
|
@@ -87,7 +91,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
87
91
|
requirements:
|
|
88
92
|
- - ">="
|
|
89
93
|
- !ruby/object:Gem::Version
|
|
90
|
-
version: '3.
|
|
94
|
+
version: '3.2'
|
|
91
95
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
96
|
requirements:
|
|
93
97
|
- - ">="
|