xlsxrb 0.1.12 → 0.1.13
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 +59 -42
- data/README.md +26 -22
- data/lib/xlsxrb/dsl_helpers.rb +59 -0
- data/lib/xlsxrb/elements/workbook.rb +11 -0
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +3 -5
- data/lib/xlsxrb/ooxml/workbook_writer.rb +5 -7
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +172 -9
- data/lib/xlsxrb/ooxml/writer.rb +2 -1
- data/lib/xlsxrb/ooxml/xml_builder.rb +20 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +364 -78
- data/lib/xlsxrb/ooxml/zip_writer.rb +32 -5
- data/lib/xlsxrb/stream_sheet.rb +94 -6
- data/lib/xlsxrb/stream_writer.rb +33 -66
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +1 -1
- data/lib/xlsxrb/worksheet_builder.rb +3 -31
- data/lib/xlsxrb.rb +448 -171
- data/sig/generated/xlsxrb/dsl_helpers.rbs +32 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +7 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +0 -2
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +5 -4
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +11 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +96 -8
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +5 -1
- data/sig/generated/xlsxrb/stream_sheet.rbs +43 -3
- data/sig/generated/xlsxrb.rbs +18 -11
- metadata +6 -146
- data/.devcontainer/Dockerfile +0 -65
- data/.devcontainer/devcontainer.json +0 -17
- data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/ast/2.4/ast.rbs +0 -73
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +0 -4
- data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +0 -16
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +0 -96
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +0 -4
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +0 -68
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +0 -249
- data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +0 -4
- data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +0 -47
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +0 -5
- data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/csv/3.3/csv.rbs +0 -3871
- data/.gem_rbs_collection/csv/3.3/manifest.yaml +0 -3
- data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +0 -48
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/listen/3.9/listen.rbs +0 -25
- data/.gem_rbs_collection/listen/3.9/listener.rbs +0 -24
- data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/logger/1.7/formatter.rbs +0 -45
- data/.gem_rbs_collection/logger/1.7/log_device.rbs +0 -100
- data/.gem_rbs_collection/logger/1.7/logger.rbs +0 -796
- data/.gem_rbs_collection/logger/1.7/manifest.yaml +0 -2
- data/.gem_rbs_collection/logger/1.7/period.rbs +0 -17
- data/.gem_rbs_collection/logger/1.7/severity.rbs +0 -34
- data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +0 -2332
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +0 -4
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +0 -86
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +0 -7
- data/.gem_rbs_collection/parser/3.2/parser.rbs +0 -194
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +0 -4
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +0 -7
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +0 -209
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +0 -5
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +0 -2
- data/.gem_rbs_collection/rake/13.0/rake.rbs +0 -39
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +0 -17
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +0 -208
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +0 -903
- data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +0 -8
- data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +0 -42
- data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +0 -5
- data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +0 -47
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +0 -30
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +0 -9
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +0 -7
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +0 -19
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +0 -31
- data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +0 -18
- data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +0 -12
- data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +0 -11
- data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +0 -13
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +0 -95
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +0 -31
- data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +0 -58
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +0 -24
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +0 -17
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +0 -23
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +0 -22
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +0 -30
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +0 -20
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +0 -15
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +0 -26
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +0 -21
- data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +0 -131
- data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +0 -14
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +0 -33
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +0 -21
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +0 -63
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +0 -55
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +0 -35
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +0 -7
- data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +0 -10
- data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +0 -22
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +0 -29
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +0 -17
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +0 -13
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +0 -10
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +0 -8
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +0 -6
- data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +0 -30
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +0 -10
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +0 -10
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +0 -5
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +0 -15
- data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +0 -3
- data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +0 -40
- data/.mutant.yml +0 -8
- data/Rakefile +0 -633
- data/Steepfile +0 -18
- data/benchmark.rb +0 -406
- data/docs/ARCHITECTURE.md +0 -514
- data/docs/DEVELOPMENT.md +0 -137
- data/docs/MUTATION_TESTING.md +0 -52
- data/docs/PEER_LIBRARIES.md +0 -121
- data/docs/QUALITY_ASSURANCE.md +0 -26
- data/docs/SPEC_SOURCES.md +0 -48
- data/docs/assets/benchmark_results.svg +0 -144
- data/docs/assets/lsp_autocompletion.png +0 -0
- data/docs/assets/playground_preview.png +0 -0
- data/docs/coi-serviceworker.js +0 -82
- data/docs/office_thread.js +0 -77
- data/docs/preview.html +0 -719
- data/docs/visual/VisualGallery.md +0 -4683
- data/docs/wasm/ruby.wasm +0 -0
- data/docs/wasm/wasm_doc_helper.css +0 -351
- data/docs/wasm/wasm_doc_helper.js +0 -439
- data/docs/zeta.js +0 -1107
- data/rbs_collection.lock.yaml +0 -252
- data/rbs_collection.yaml +0 -19
- data/vendor/sdk_runner/Program.cs +0 -93
- data/vendor/sdk_runner/sdk_runner.csproj +0 -13
data/lib/xlsxrb.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "time"
|
|
|
7
7
|
require "openssl"
|
|
8
8
|
require "securerandom"
|
|
9
9
|
require "tempfile"
|
|
10
|
+
require "fileutils"
|
|
10
11
|
begin
|
|
11
12
|
require "bigdecimal"
|
|
12
13
|
rescue LoadError
|
|
@@ -17,7 +18,6 @@ rescue LoadError
|
|
|
17
18
|
Object.const_set(:BigDecimal, Class.new)
|
|
18
19
|
# simplecov:enable
|
|
19
20
|
end
|
|
20
|
-
require "opentelemetry"
|
|
21
21
|
require_relative "xlsxrb/version"
|
|
22
22
|
require_relative "xlsxrb/ooxml/zip_generator"
|
|
23
23
|
require_relative "xlsxrb/ooxml/writer"
|
|
@@ -71,28 +71,6 @@ module Xlsxrb
|
|
|
71
71
|
# Raised when encrypted package stream decryption fails.
|
|
72
72
|
class DecryptionError < EncryptedFileError; end
|
|
73
73
|
|
|
74
|
-
TRACER = OpenTelemetry.tracer_provider.tracer("xlsxrb", Xlsxrb::VERSION)
|
|
75
|
-
|
|
76
|
-
# Executes the block within an OpenTelemetry tracer span if tracing is configured.
|
|
77
|
-
#
|
|
78
|
-
# @param name [String] The span name.
|
|
79
|
-
# @param attributes [Hash, nil] Optional telemetry attributes.
|
|
80
|
-
# @yield Block to execute inside the tracing span.
|
|
81
|
-
# @return [Object] The result of the block.
|
|
82
|
-
#: (String name, ?attributes: Hash[String, untyped]?) { (*untyped) -> untyped } -> untyped
|
|
83
|
-
def self.in_span(name, attributes: nil, &)
|
|
84
|
-
if defined?(Ractor) && Ractor.current != Ractor.main
|
|
85
|
-
# simplecov:disable
|
|
86
|
-
# Test suite runs in the main Ractor. This branch is for multi-threaded usage via Ractors.
|
|
87
|
-
yield
|
|
88
|
-
# simplecov:enable
|
|
89
|
-
elsif attributes
|
|
90
|
-
TRACER.in_span(name, attributes: attributes, &)
|
|
91
|
-
else
|
|
92
|
-
TRACER.in_span(name, &)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
74
|
# Helper to construct {Elements::RichText} objects with formatted text runs.
|
|
97
75
|
#
|
|
98
76
|
# @example Create multi-run rich text
|
|
@@ -178,62 +156,37 @@ module Xlsxrb
|
|
|
178
156
|
#: (untyped source, ?password: String?) { (StreamSheet) -> void } -> void
|
|
179
157
|
#: (untyped source, ?password: String?) -> Elements::Workbook
|
|
180
158
|
def self.read(source, password: nil, &)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
rescue StandardError
|
|
193
|
-
nil
|
|
194
|
-
end
|
|
195
|
-
if Ooxml::Cfb::Reader.cfb?(first_bytes)
|
|
196
|
-
encrypted_data = File.binread(source)
|
|
197
|
-
decrypted_zip = Ooxml::Crypto.decrypt(encrypted_data, password)
|
|
198
|
-
source = StringIO.new(decrypted_zip)
|
|
199
|
-
end
|
|
200
|
-
end
|
|
201
|
-
elsif source.respond_to?(:read) && source.respond_to?(:pos) && source.respond_to?(:seek)
|
|
202
|
-
begin
|
|
203
|
-
cur_pos = source.pos
|
|
204
|
-
first_bytes = source.read(8)
|
|
205
|
-
source.seek(cur_pos)
|
|
206
|
-
if Ooxml::Cfb::Reader.cfb?(first_bytes)
|
|
207
|
-
full_data = source.read
|
|
208
|
-
decrypted_zip = Ooxml::Crypto.decrypt(full_data, password)
|
|
209
|
-
source = StringIO.new(decrypted_zip)
|
|
210
|
-
end
|
|
211
|
-
rescue StandardError
|
|
212
|
-
# Fall through to standard reader if seeking fails
|
|
213
|
-
end
|
|
214
|
-
end
|
|
159
|
+
prepared_source = prepare_source_io(source, password)
|
|
160
|
+
zip_reader = Ooxml::ZipReader.open(prepared_source)
|
|
161
|
+
begin
|
|
162
|
+
shared_strings_xml = zip_reader.read_entry("xl/sharedStrings.xml")
|
|
163
|
+
shared_strings = Ooxml::SharedStringsParser.parse(shared_strings_xml)
|
|
164
|
+
|
|
165
|
+
workbook_xml = zip_reader.read_entry("xl/workbook.xml")
|
|
166
|
+
workbook_sheets = Ooxml::WorkbookParser.parse(workbook_xml)
|
|
167
|
+
|
|
168
|
+
rels_xml = zip_reader.read_entry("xl/_rels/workbook.xml.rels")
|
|
169
|
+
rels = Ooxml::RelationshipsParser.parse(rels_xml)
|
|
215
170
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
entries = Ooxml::ZipReader.open(source, &:read_all)
|
|
219
|
-
shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
|
|
220
|
-
workbook_sheets = Ooxml::WorkbookParser.parse(entries["xl/workbook.xml"])
|
|
221
|
-
rels = Ooxml::RelationshipsParser.parse(entries["xl/_rels/workbook.xml.rels"])
|
|
222
|
-
styles = Ooxml::StylesParser.parse(entries["xl/styles.xml"])
|
|
171
|
+
styles_xml = zip_reader.read_entry("xl/styles.xml")
|
|
172
|
+
styles = Ooxml::StylesParser.parse(styles_xml)
|
|
223
173
|
|
|
224
174
|
sheets = workbook_sheets.map do |sheet_info|
|
|
225
175
|
target = rels[sheet_info[:r_id]]
|
|
226
176
|
next nil unless target
|
|
227
177
|
|
|
228
178
|
sheet_path = target.start_with?("/") ? target.delete_prefix("/") : "xl/#{target}"
|
|
229
|
-
|
|
230
|
-
|
|
179
|
+
next nil unless zip_reader.entry?(sheet_path)
|
|
180
|
+
|
|
181
|
+
chunk_supplier = ->(&blk) { zip_reader.each_entry_chunk(sheet_path, &blk) }
|
|
231
182
|
|
|
232
183
|
StreamSheet.new(
|
|
233
184
|
sheet_info[:name],
|
|
234
|
-
|
|
185
|
+
chunk_supplier,
|
|
235
186
|
shared_strings,
|
|
236
|
-
styles
|
|
187
|
+
styles,
|
|
188
|
+
zip_reader: zip_reader,
|
|
189
|
+
entry_name: sheet_path
|
|
237
190
|
)
|
|
238
191
|
end.compact
|
|
239
192
|
|
|
@@ -245,6 +198,8 @@ module Xlsxrb
|
|
|
245
198
|
else
|
|
246
199
|
wb
|
|
247
200
|
end
|
|
201
|
+
ensure
|
|
202
|
+
zip_reader.close if block_given?
|
|
248
203
|
end
|
|
249
204
|
end
|
|
250
205
|
|
|
@@ -297,35 +252,33 @@ module Xlsxrb
|
|
|
297
252
|
target = target_or_workbook
|
|
298
253
|
raise Error, "target is required" if target.nil?
|
|
299
254
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
stream_writer
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
elsif target.respond_to?(:write)
|
|
314
|
-
target.write(encrypted_bytes)
|
|
315
|
-
end
|
|
316
|
-
ensure
|
|
317
|
-
stream_writer.cleanup!
|
|
318
|
-
end
|
|
319
|
-
else
|
|
320
|
-
stream_writer = StreamWriter.new(target, strict_excel_mode: strict_excel_mode)
|
|
321
|
-
begin
|
|
322
|
-
yield stream_writer
|
|
323
|
-
stream_writer.close
|
|
324
|
-
ensure
|
|
325
|
-
stream_writer.cleanup!
|
|
255
|
+
if password && !password.empty?
|
|
256
|
+
buf = StringIO.new
|
|
257
|
+
buf.binmode
|
|
258
|
+
stream_writer = StreamWriter.new(buf, strict_excel_mode: strict_excel_mode)
|
|
259
|
+
begin
|
|
260
|
+
yield stream_writer
|
|
261
|
+
stream_writer.close
|
|
262
|
+
plain_bytes = buf.string.b
|
|
263
|
+
encrypted_bytes = Ooxml::Crypto.encrypt(plain_bytes, password, mode: encryption_mode)
|
|
264
|
+
if target.is_a?(String)
|
|
265
|
+
File.binwrite(target, encrypted_bytes)
|
|
266
|
+
elsif target.respond_to?(:write)
|
|
267
|
+
target.write(encrypted_bytes)
|
|
326
268
|
end
|
|
269
|
+
ensure
|
|
270
|
+
stream_writer.cleanup!
|
|
271
|
+
end
|
|
272
|
+
else
|
|
273
|
+
stream_writer = StreamWriter.new(target, strict_excel_mode: strict_excel_mode)
|
|
274
|
+
begin
|
|
275
|
+
yield stream_writer
|
|
276
|
+
stream_writer.close
|
|
277
|
+
ensure
|
|
278
|
+
stream_writer.cleanup!
|
|
327
279
|
end
|
|
328
280
|
end
|
|
281
|
+
return
|
|
329
282
|
end
|
|
330
283
|
|
|
331
284
|
if workbook_or_nil.nil?
|
|
@@ -343,79 +296,76 @@ module Xlsxrb
|
|
|
343
296
|
raise Error, "target is required" if target.nil?
|
|
344
297
|
raise Error, "workbook must be an Elements::Workbook" unless workbook.is_a?(Elements::Workbook)
|
|
345
298
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
sst << val
|
|
359
|
-
sst_index[val] = sst.size - 1
|
|
360
|
-
end
|
|
299
|
+
sst = []
|
|
300
|
+
sst_index = {}
|
|
301
|
+
|
|
302
|
+
# Collect shared strings and build index without allocating new Hashes
|
|
303
|
+
sheet_data = workbook.sheets.map do |raw_ws|
|
|
304
|
+
ws = raw_ws.respond_to?(:load) ? raw_ws.load : raw_ws
|
|
305
|
+
ws.rows.each do |row|
|
|
306
|
+
row.cells.each do |cell|
|
|
307
|
+
val = cell.value
|
|
308
|
+
if (val.is_a?(String) || val.is_a?(Elements::RichText)) && !sst_index.key?(val)
|
|
309
|
+
sst << val
|
|
310
|
+
sst_index[val] = sst.size - 1
|
|
361
311
|
end
|
|
362
312
|
end
|
|
363
|
-
columns = ws.columns.map do |col|
|
|
364
|
-
# simplecov:disable
|
|
365
|
-
# Edge case / untested delegation block
|
|
366
|
-
{ index: col.index, width: col.width, hidden: col.hidden, custom_width: col.custom_width, outline_level: col.outline_level }
|
|
367
|
-
# simplecov:enable
|
|
368
|
-
end
|
|
369
|
-
sd = { name: ws.name, rows: ws.rows, columns: columns }
|
|
370
|
-
sd[:charts] = ws.charts unless ws.charts.empty?
|
|
371
|
-
|
|
372
|
-
# Extract facade metadata from unmapped_data
|
|
373
|
-
facade = ws.unmapped_data[:facade]
|
|
374
|
-
facade&.each { |key, val| sd[key] = val }
|
|
375
|
-
|
|
376
|
-
sd
|
|
377
313
|
end
|
|
314
|
+
columns = ws.columns.map do |col|
|
|
315
|
+
# simplecov:disable
|
|
316
|
+
# Edge case / untested delegation block
|
|
317
|
+
{ index: col.index, width: col.width, hidden: col.hidden, custom_width: col.custom_width, outline_level: col.outline_level }
|
|
318
|
+
# simplecov:enable
|
|
319
|
+
end
|
|
320
|
+
sd = { name: ws.name, rows: ws.rows, columns: columns }
|
|
321
|
+
sd[:charts] = ws.charts unless ws.charts.empty?
|
|
378
322
|
|
|
379
|
-
# Extract
|
|
380
|
-
|
|
323
|
+
# Extract facade metadata from unmapped_data
|
|
324
|
+
facade = ws.unmapped_data[:facade]
|
|
325
|
+
facade&.each { |key, val| sd[key] = val }
|
|
381
326
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
shared_strings_index: sst_index,
|
|
410
|
-
styles: workbook.styles,
|
|
411
|
-
defined_names: wb_facade[:defined_names],
|
|
412
|
-
core_properties: wb_facade[:core_properties],
|
|
413
|
-
app_properties: wb_facade[:app_properties],
|
|
414
|
-
custom_properties: wb_facade[:custom_properties],
|
|
415
|
-
workbook_protection: wb_facade[:workbook_protection],
|
|
416
|
-
workbook_properties: wb_facade[:workbook_properties]
|
|
417
|
-
)
|
|
327
|
+
sd
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Extract workbook-level facade metadata
|
|
331
|
+
wb_facade = workbook.unmapped_data[:facade] || {}
|
|
332
|
+
|
|
333
|
+
if password && !password.empty?
|
|
334
|
+
buf = StringIO.new
|
|
335
|
+
buf.binmode
|
|
336
|
+
Ooxml::WorkbookWriter.write(
|
|
337
|
+
buf,
|
|
338
|
+
sheets: sheet_data,
|
|
339
|
+
shared_strings: sst,
|
|
340
|
+
shared_strings_index: sst_index,
|
|
341
|
+
styles: workbook.styles,
|
|
342
|
+
defined_names: wb_facade[:defined_names],
|
|
343
|
+
core_properties: wb_facade[:core_properties],
|
|
344
|
+
app_properties: wb_facade[:app_properties],
|
|
345
|
+
custom_properties: wb_facade[:custom_properties],
|
|
346
|
+
workbook_protection: wb_facade[:workbook_protection],
|
|
347
|
+
workbook_properties: wb_facade[:workbook_properties]
|
|
348
|
+
)
|
|
349
|
+
encrypted_bytes = Ooxml::Crypto.encrypt(buf.string.b, password, mode: encryption_mode)
|
|
350
|
+
if target.is_a?(String)
|
|
351
|
+
File.binwrite(target, encrypted_bytes)
|
|
352
|
+
elsif target.respond_to?(:write)
|
|
353
|
+
target.write(encrypted_bytes)
|
|
418
354
|
end
|
|
355
|
+
else
|
|
356
|
+
Ooxml::WorkbookWriter.write(
|
|
357
|
+
target,
|
|
358
|
+
sheets: sheet_data,
|
|
359
|
+
shared_strings: sst,
|
|
360
|
+
shared_strings_index: sst_index,
|
|
361
|
+
styles: workbook.styles,
|
|
362
|
+
defined_names: wb_facade[:defined_names],
|
|
363
|
+
core_properties: wb_facade[:core_properties],
|
|
364
|
+
app_properties: wb_facade[:app_properties],
|
|
365
|
+
custom_properties: wb_facade[:custom_properties],
|
|
366
|
+
workbook_protection: wb_facade[:workbook_protection],
|
|
367
|
+
workbook_properties: wb_facade[:workbook_properties]
|
|
368
|
+
)
|
|
419
369
|
end
|
|
420
370
|
end
|
|
421
371
|
|
|
@@ -445,12 +395,128 @@ module Xlsxrb
|
|
|
445
395
|
raise Error, "source is required" if source.nil?
|
|
446
396
|
raise Error, "block is required" unless block_given?
|
|
447
397
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
398
|
+
prepared_source = prepare_source_io(source, password)
|
|
399
|
+
in_place = target.nil? || target == source || (source.is_a?(String) && target.is_a?(String) && File.file?(target) && File.identical?(source, target))
|
|
400
|
+
temp_target_path = nil
|
|
401
|
+
|
|
402
|
+
begin
|
|
403
|
+
write_dest = if in_place && source.is_a?(String)
|
|
404
|
+
temp_target = Tempfile.new(["xlsxrb_mod", ".xlsx"], File.dirname(source))
|
|
405
|
+
temp_target_path = temp_target.path
|
|
406
|
+
temp_target.close
|
|
407
|
+
temp_target_path
|
|
408
|
+
elsif in_place && source.is_a?(StringIO)
|
|
409
|
+
StringIO.new
|
|
410
|
+
else
|
|
411
|
+
target || source
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
replaced_entries = {}
|
|
415
|
+
structural_change = false
|
|
416
|
+
|
|
417
|
+
Ooxml::ZipReader.open(prepared_source) do |zip_reader|
|
|
418
|
+
shared_strings_xml = zip_reader.read_entry("xl/sharedStrings.xml")
|
|
419
|
+
shared_strings = shared_strings_xml ? Ooxml::SharedStringsParser.parse(shared_strings_xml) : []
|
|
420
|
+
sst_index = {}
|
|
421
|
+
shared_strings.each_with_index { |s, idx| sst_index[s] ||= idx }
|
|
422
|
+
initial_sst_size = shared_strings.size
|
|
423
|
+
|
|
424
|
+
workbook_xml = zip_reader.read_entry("xl/workbook.xml")
|
|
425
|
+
workbook_sheets = workbook_xml ? Ooxml::WorkbookParser.parse(workbook_xml) : []
|
|
426
|
+
|
|
427
|
+
rels_xml = zip_reader.read_entry("xl/_rels/workbook.xml.rels")
|
|
428
|
+
rels = rels_xml ? Ooxml::RelationshipsParser.parse(rels_xml) : {}
|
|
429
|
+
|
|
430
|
+
styles_xml = zip_reader.read_entry("xl/styles.xml")
|
|
431
|
+
styles = styles_xml ? Ooxml::StylesParser.parse(styles_xml) : {}
|
|
432
|
+
|
|
433
|
+
sheet_entry_map = {}
|
|
434
|
+
loaded_sheets = workbook_sheets.map do |sheet_info|
|
|
435
|
+
target_path = rels[sheet_info[:r_id]]
|
|
436
|
+
next nil unless target_path
|
|
437
|
+
|
|
438
|
+
sheet_path = target_path.start_with?("/") ? target_path.delete_prefix("/") : "xl/#{target_path}"
|
|
439
|
+
next nil unless zip_reader.entry?(sheet_path)
|
|
440
|
+
|
|
441
|
+
sheet_entry_map[sheet_info[:name]] = sheet_path
|
|
442
|
+
sheet_xml = zip_reader.read_entry(sheet_path) || ""
|
|
443
|
+
build_worksheet(sheet_info[:name], sheet_xml, shared_strings, styles)
|
|
444
|
+
end.compact
|
|
445
|
+
|
|
446
|
+
original_workbook = Elements::Workbook.new(
|
|
447
|
+
sheets: loaded_sheets,
|
|
448
|
+
shared_strings: shared_strings,
|
|
449
|
+
styles: styles
|
|
450
|
+
)
|
|
451
|
+
mutable_shared_strings = shared_strings.dup
|
|
452
|
+
|
|
453
|
+
result_workbook = yield original_workbook
|
|
454
|
+
result_workbook = original_workbook unless result_workbook.is_a?(Elements::Workbook)
|
|
455
|
+
|
|
456
|
+
structural_change = result_workbook.sheets.size != original_workbook.sheets.size ||
|
|
457
|
+
result_workbook.sheets.map(&:name) != original_workbook.sheets.map(&:name)
|
|
458
|
+
|
|
459
|
+
if structural_change
|
|
460
|
+
write_target = target || source
|
|
461
|
+
write(write_target, result_workbook, password: password)
|
|
462
|
+
else
|
|
463
|
+
result_workbook.sheets.each_with_index do |new_sheet, idx|
|
|
464
|
+
orig_sheet = original_workbook.sheets[idx]
|
|
465
|
+
next if new_sheet.equal?(orig_sheet) || new_sheet == orig_sheet
|
|
466
|
+
|
|
467
|
+
entry_name = sheet_entry_map[new_sheet.name] || "xl/worksheets/sheet#{idx + 1}.xml"
|
|
468
|
+
orig_sheet_xml = zip_reader.read_entry(entry_name) || ""
|
|
469
|
+
|
|
470
|
+
new_sheet_data = serialize_sheet_data_xml(
|
|
471
|
+
new_sheet.rows,
|
|
472
|
+
mutable_shared_strings,
|
|
473
|
+
sst_index,
|
|
474
|
+
use_sst: !shared_strings_xml.nil?
|
|
475
|
+
)
|
|
476
|
+
updated_sheet_xml = replace_sheet_data(orig_sheet_xml, new_sheet_data)
|
|
477
|
+
updated_sheet_xml = update_dimension(updated_sheet_xml, new_sheet.rows)
|
|
478
|
+
replaced_entries[entry_name] = updated_sheet_xml
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
if mutable_shared_strings.size > initial_sst_size && shared_strings_xml
|
|
482
|
+
new_strings = mutable_shared_strings[initial_sst_size..]
|
|
483
|
+
replaced_entries["xl/sharedStrings.xml"] = append_to_shared_strings_xml(
|
|
484
|
+
shared_strings_xml,
|
|
485
|
+
new_strings
|
|
486
|
+
)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
perform_modify_write(
|
|
490
|
+
write_dest,
|
|
491
|
+
zip_reader,
|
|
492
|
+
replaced_entries,
|
|
493
|
+
password: password
|
|
494
|
+
)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
451
497
|
|
|
452
|
-
|
|
453
|
-
|
|
498
|
+
if in_place && !structural_change
|
|
499
|
+
if source.is_a?(String) && temp_target_path
|
|
500
|
+
FileUtils.mv(temp_target_path, source)
|
|
501
|
+
temp_target_path = nil
|
|
502
|
+
elsif source.is_a?(StringIO) && write_dest.is_a?(StringIO)
|
|
503
|
+
source.string = write_dest.string
|
|
504
|
+
elsif source.respond_to?(:write) && source.respond_to?(:seek) && write_dest.is_a?(StringIO)
|
|
505
|
+
source.seek(0)
|
|
506
|
+
source.write(write_dest.string)
|
|
507
|
+
source.truncate(source.pos) if source.respond_to?(:truncate)
|
|
508
|
+
source.rewind
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
ensure
|
|
512
|
+
if temp_target_path && File.exist?(temp_target_path)
|
|
513
|
+
begin
|
|
514
|
+
File.unlink(temp_target_path)
|
|
515
|
+
rescue SystemCallError
|
|
516
|
+
nil
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
454
520
|
end
|
|
455
521
|
|
|
456
522
|
# Builds an in-memory {Elements::Workbook} using a declarative DSL.
|
|
@@ -472,11 +538,9 @@ module Xlsxrb
|
|
|
472
538
|
def self.build(strict_excel_mode: true)
|
|
473
539
|
raise Error, "block is required" unless block_given?
|
|
474
540
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
builder.build
|
|
479
|
-
end
|
|
541
|
+
builder = WorkbookBuilder.new(strict_excel_mode: strict_excel_mode)
|
|
542
|
+
yield builder
|
|
543
|
+
builder.build
|
|
480
544
|
end
|
|
481
545
|
|
|
482
546
|
class << self
|
|
@@ -627,6 +691,219 @@ module Xlsxrb
|
|
|
627
691
|
attrs
|
|
628
692
|
# simplecov:enable
|
|
629
693
|
end
|
|
694
|
+
|
|
695
|
+
#: (untyped source, String? password) -> untyped
|
|
696
|
+
def prepare_source_io(source, password)
|
|
697
|
+
if source.is_a?(String)
|
|
698
|
+
if source.start_with?("PK\x03\x04") || source.include?("\x00") || Ooxml::Cfb::Reader.cfb?(source)
|
|
699
|
+
if Ooxml::Cfb::Reader.cfb?(source)
|
|
700
|
+
decrypted_zip = Ooxml::Crypto.decrypt(source, password)
|
|
701
|
+
StringIO.new(decrypted_zip)
|
|
702
|
+
else
|
|
703
|
+
StringIO.new(source)
|
|
704
|
+
end
|
|
705
|
+
elsif File.file?(source)
|
|
706
|
+
first_bytes = begin
|
|
707
|
+
File.binread(source, 8)
|
|
708
|
+
rescue StandardError
|
|
709
|
+
nil
|
|
710
|
+
end
|
|
711
|
+
if Ooxml::Cfb::Reader.cfb?(first_bytes)
|
|
712
|
+
encrypted_data = File.binread(source)
|
|
713
|
+
decrypted_zip = Ooxml::Crypto.decrypt(encrypted_data, password)
|
|
714
|
+
StringIO.new(decrypted_zip)
|
|
715
|
+
else
|
|
716
|
+
source
|
|
717
|
+
end
|
|
718
|
+
else
|
|
719
|
+
source
|
|
720
|
+
end
|
|
721
|
+
elsif source.respond_to?(:read) && source.respond_to?(:pos) && source.respond_to?(:seek)
|
|
722
|
+
begin
|
|
723
|
+
cur_pos = source.pos
|
|
724
|
+
first_bytes = source.read(8)
|
|
725
|
+
source.seek(cur_pos)
|
|
726
|
+
if Ooxml::Cfb::Reader.cfb?(first_bytes)
|
|
727
|
+
full_data = source.read
|
|
728
|
+
decrypted_zip = Ooxml::Crypto.decrypt(full_data, password)
|
|
729
|
+
StringIO.new(decrypted_zip)
|
|
730
|
+
else
|
|
731
|
+
source
|
|
732
|
+
end
|
|
733
|
+
rescue StandardError
|
|
734
|
+
source
|
|
735
|
+
end
|
|
736
|
+
else
|
|
737
|
+
source
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
#: (untyped dest, Ooxml::ZipReader zip_reader, Hash[String, String] replaced_entries, ?password: String?) -> void
|
|
742
|
+
def perform_modify_write(dest, zip_reader, replaced_entries, password: nil)
|
|
743
|
+
write_zip = lambda do |zip_out|
|
|
744
|
+
Ooxml::ZipWriter.open(zip_out) do |zip_writer|
|
|
745
|
+
zip_reader.entry_names.each do |name|
|
|
746
|
+
if replaced_entries.key?(name)
|
|
747
|
+
zip_writer.add_entry(name, replaced_entries[name])
|
|
748
|
+
else
|
|
749
|
+
zip_reader.copy_to_writer(name, zip_writer)
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
replaced_entries.each do |name, content|
|
|
753
|
+
next if zip_reader.entry?(name)
|
|
754
|
+
|
|
755
|
+
zip_writer.add_entry(name, content)
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
if password && !password.empty?
|
|
761
|
+
buf = StringIO.new
|
|
762
|
+
buf.binmode
|
|
763
|
+
write_zip.call(buf)
|
|
764
|
+
encrypted_bytes = Ooxml::Crypto.encrypt(buf.string.b, password)
|
|
765
|
+
if dest.is_a?(String)
|
|
766
|
+
File.binwrite(dest, encrypted_bytes)
|
|
767
|
+
elsif dest.respond_to?(:write)
|
|
768
|
+
dest.write(encrypted_bytes)
|
|
769
|
+
end
|
|
770
|
+
else
|
|
771
|
+
write_zip.call(dest)
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
#: (String sst_xml, Array[untyped] new_strings) -> String
|
|
776
|
+
def append_to_shared_strings_xml(sst_xml, new_strings)
|
|
777
|
+
return sst_xml if new_strings.nil? || new_strings.empty?
|
|
778
|
+
|
|
779
|
+
new_si_tags = +""
|
|
780
|
+
new_strings.each do |str|
|
|
781
|
+
s = str.to_s
|
|
782
|
+
needs_space = s.start_with?(" ", "\t", "\n") || s.end_with?(" ", "\t", "\n")
|
|
783
|
+
space_attr = needs_space ? ' xml:space="preserve"' : ""
|
|
784
|
+
new_si_tags << "<si><t#{space_attr}>#{Ooxml::XmlBuilder.escape(s)}</t></si>"
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
sst_close = sst_xml.rindex("</sst>")
|
|
788
|
+
updated_xml = if sst_close
|
|
789
|
+
prefix = sst_xml[0...sst_close]
|
|
790
|
+
suffix = sst_xml[sst_close..]
|
|
791
|
+
"#{prefix}#{new_si_tags}#{suffix}"
|
|
792
|
+
else
|
|
793
|
+
sst_start = sst_xml.index("<sst")
|
|
794
|
+
return sst_xml unless sst_start
|
|
795
|
+
|
|
796
|
+
sst_open_end = sst_xml.index("/>", sst_start)
|
|
797
|
+
return sst_xml unless sst_open_end
|
|
798
|
+
|
|
799
|
+
prefix = sst_xml[0...sst_open_end]
|
|
800
|
+
suffix = sst_xml[(sst_open_end + 2)..]
|
|
801
|
+
"#{prefix}>#{new_si_tags}</sst#{suffix}"
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
updated_xml.sub(/<sst\b([^>]*)>/) do
|
|
805
|
+
attrs = Regexp.last_match(1)
|
|
806
|
+
if attrs =~ /\bcount="(\d+)"/
|
|
807
|
+
old_count = Regexp.last_match(1).to_i
|
|
808
|
+
new_count = old_count + new_strings.size
|
|
809
|
+
attrs = attrs.sub(/\bcount="\d+"/, "count=\"#{new_count}\"")
|
|
810
|
+
else
|
|
811
|
+
attrs = "#{attrs} count=\"#{new_strings.size}\""
|
|
812
|
+
end
|
|
813
|
+
if attrs =~ /\buniqueCount="(\d+)"/
|
|
814
|
+
old_ucount = Regexp.last_match(1).to_i
|
|
815
|
+
new_ucount = old_ucount + new_strings.size
|
|
816
|
+
attrs = attrs.sub(/\buniqueCount="\d+"/, "uniqueCount=\"#{new_ucount}\"")
|
|
817
|
+
else
|
|
818
|
+
attrs = "#{attrs} uniqueCount=\"#{new_strings.size}\""
|
|
819
|
+
end
|
|
820
|
+
"<sst#{attrs}>"
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
#: (String sheet_xml, String new_sheet_data_xml) -> String
|
|
825
|
+
def replace_sheet_data(sheet_xml, new_sheet_data_xml)
|
|
826
|
+
sheet_data_start = sheet_xml.index("<sheetData")
|
|
827
|
+
return sheet_xml unless sheet_data_start
|
|
828
|
+
|
|
829
|
+
open_tag_end = sheet_xml.index(">", sheet_data_start)
|
|
830
|
+
return sheet_xml unless open_tag_end
|
|
831
|
+
|
|
832
|
+
prefix = sheet_xml[0...sheet_data_start]
|
|
833
|
+
suffix = if sheet_xml.getbyte(open_tag_end - 1) == 47
|
|
834
|
+
sheet_xml[(open_tag_end + 1)..]
|
|
835
|
+
else
|
|
836
|
+
close_tag_start = sheet_xml.index("</sheetData>", open_tag_end)
|
|
837
|
+
return sheet_xml unless close_tag_start
|
|
838
|
+
|
|
839
|
+
sheet_xml[(close_tag_start + 12)..]
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
"#{prefix}#{new_sheet_data_xml}#{suffix}"
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
#: (String sheet_xml, Array[Elements::Row] rows) -> String
|
|
846
|
+
def update_dimension(sheet_xml, rows)
|
|
847
|
+
return sheet_xml if rows.empty?
|
|
848
|
+
|
|
849
|
+
min_row = rows.first.index
|
|
850
|
+
max_row = rows.last.index
|
|
851
|
+
min_col = rows.map { |r| r.cells.first&.column_index }.compact.min || 0
|
|
852
|
+
max_col = rows.map { |r| r.cells.last&.column_index }.compact.max || 0
|
|
853
|
+
min_col_letter = Elements::Cell.column_letter(min_col)
|
|
854
|
+
max_col_letter = Elements::Cell.column_letter(max_col)
|
|
855
|
+
dim_ref = if min_row == max_row && min_col == max_col
|
|
856
|
+
"#{min_col_letter}#{min_row + 1}"
|
|
857
|
+
else
|
|
858
|
+
"#{min_col_letter}#{min_row + 1}:#{max_col_letter}#{max_row + 1}"
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
sheet_xml.sub(/<dimension\b([^>]*)\bref="[^"]*"/, "<dimension\\1ref=\"#{dim_ref}\"")
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
#: (Array[Elements::Row] rows, Array[untyped] shared_strings, Hash[untyped, Integer] sst_index, ?use_sst: bool) -> String
|
|
865
|
+
def serialize_sheet_data_xml(rows, shared_strings, sst_index, use_sst: true)
|
|
866
|
+
io = StringIO.new
|
|
867
|
+
io.write("<sheetData>")
|
|
868
|
+
ws = Ooxml::WorksheetWriter.new(io)
|
|
869
|
+
ws.instance_variable_set(:@started, true)
|
|
870
|
+
|
|
871
|
+
rows.each do |row|
|
|
872
|
+
attrs = {}
|
|
873
|
+
attrs[:height] = row.height if row.height
|
|
874
|
+
attrs[:hidden] = true if row.hidden
|
|
875
|
+
attrs[:custom_height] = true if row.custom_height
|
|
876
|
+
attrs[:outline_level] = row.outline_level if row.outline_level
|
|
877
|
+
|
|
878
|
+
if use_sst
|
|
879
|
+
row.cells.each do |cell|
|
|
880
|
+
val = cell.value
|
|
881
|
+
next unless val.is_a?(String) || val.is_a?(Elements::RichText)
|
|
882
|
+
next if val.is_a?(String) && val.start_with?("=")
|
|
883
|
+
next if sst_index.key?(val)
|
|
884
|
+
|
|
885
|
+
shared_strings << val
|
|
886
|
+
sst_index[val] = shared_strings.size - 1
|
|
887
|
+
end
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
ws.write_row(
|
|
891
|
+
row.index,
|
|
892
|
+
row.cells,
|
|
893
|
+
attrs: attrs,
|
|
894
|
+
unmapped: row.unmapped_data || [],
|
|
895
|
+
sst_index: use_sst ? sst_index : nil
|
|
896
|
+
)
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
buf = ws.instance_variable_get(:@row_buffer)
|
|
900
|
+
if buf && !buf.empty?
|
|
901
|
+
io.write(buf)
|
|
902
|
+
buf.clear
|
|
903
|
+
end
|
|
904
|
+
io.write("</sheetData>")
|
|
905
|
+
io.string
|
|
906
|
+
end
|
|
630
907
|
end
|
|
631
908
|
|
|
632
909
|
# Builds a raw cell hash from a value for streaming writes.
|