xlsxrb 0.1.7 → 0.1.9
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 +26 -2
- data/README.md +125 -36
- data/Rakefile +36 -7
- data/benchmark.rb +32 -4
- data/docs/ARCHITECTURE.md +32 -30
- data/docs/DEVELOPMENT.md +11 -3
- data/docs/QUALITY_ASSURANCE.md +3 -4
- data/docs/SPEC_SOURCES.md +7 -1
- data/docs/visual/VisualGallery.md +157 -157
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/ruby_lsp/xlsxrb/addon.rb +1 -1
- data/lib/ruby_lsp/xlsxrb/completion_listener.rb +11 -13
- data/lib/xlsxrb/elements/coordinate_access.rb +101 -0
- data/lib/xlsxrb/elements/row.rb +2 -2
- data/lib/xlsxrb/elements/workbook.rb +20 -2
- data/lib/xlsxrb/elements/worksheet.rb +94 -124
- data/lib/xlsxrb/elements.rb +1 -0
- data/lib/xlsxrb/ooxml/cfb.rb +490 -0
- data/lib/xlsxrb/ooxml/crypto/agile.rb +285 -0
- data/lib/xlsxrb/ooxml/crypto/standard.rb +152 -0
- data/lib/xlsxrb/ooxml/crypto.rb +60 -0
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +21 -15
- data/lib/xlsxrb/stream_row.rb +181 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb.rb +271 -122
- data/sig/generated/xlsxrb/elements/coordinate_access.rbs +70 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +119 -14
- data/sig/generated/xlsxrb/ooxml/cfb.rbs +121 -0
- data/sig/generated/xlsxrb/ooxml/crypto/agile.rbs +46 -0
- data/sig/generated/xlsxrb/ooxml/crypto/standard.rbs +28 -0
- data/sig/generated/xlsxrb/ooxml/crypto.rbs +17 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +2 -0
- data/sig/generated/xlsxrb/stream_row.rbs +127 -0
- data/sig/generated/xlsxrb.rbs +135 -71
- data/vendor/sdk_runner/Program.cs +3 -1
- metadata +13 -1
data/lib/xlsxrb.rb
CHANGED
|
@@ -22,8 +22,10 @@ require_relative "xlsxrb/version"
|
|
|
22
22
|
require_relative "xlsxrb/ooxml/zip_generator"
|
|
23
23
|
require_relative "xlsxrb/ooxml/writer"
|
|
24
24
|
require_relative "xlsxrb/ooxml/reader"
|
|
25
|
+
require_relative "xlsxrb/ooxml/crypto"
|
|
25
26
|
require_relative "xlsxrb/ooxml"
|
|
26
27
|
require_relative "xlsxrb/elements"
|
|
28
|
+
require_relative "xlsxrb/stream_row"
|
|
27
29
|
require_relative "xlsxrb/style_builder"
|
|
28
30
|
|
|
29
31
|
# Ruby XLSX read/write library.
|
|
@@ -41,6 +43,9 @@ module Xlsxrb
|
|
|
41
43
|
class ParseError < Error; end
|
|
42
44
|
class ValidationError < Error; end
|
|
43
45
|
class ZipError < Error; end
|
|
46
|
+
class EncryptedFileError < Error; end
|
|
47
|
+
class InvalidPasswordError < EncryptedFileError; end
|
|
48
|
+
class DecryptionError < EncryptedFileError; end
|
|
44
49
|
|
|
45
50
|
TRACER = OpenTelemetry.tracer_provider.tracer("xlsxrb", Xlsxrb::VERSION)
|
|
46
51
|
|
|
@@ -360,28 +365,80 @@ module Xlsxrb
|
|
|
360
365
|
)
|
|
361
366
|
end
|
|
362
367
|
|
|
363
|
-
# Reads an XLSX file
|
|
368
|
+
# Reads an XLSX file (streaming / lazy-loaded by default) from a file path, IO stream, or binary String.
|
|
364
369
|
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
# sheet = workbook["Sheet1"]
|
|
368
|
-
# puts sheet["A1"].value
|
|
370
|
+
# Sheets and rows are streamed lazily with O(1) constant memory. If a block is given,
|
|
371
|
+
# yields each StreamSheet sequentially.
|
|
369
372
|
#
|
|
370
|
-
#
|
|
371
|
-
#
|
|
373
|
+
# Call #load on the returned Workbook or Sheet to convert to an in-memory representation
|
|
374
|
+
# for coordinate random access (e.g. sheet["A1"]).
|
|
372
375
|
#
|
|
373
|
-
# @
|
|
374
|
-
#
|
|
376
|
+
# @example Streaming read across sheets and rows (O(1) memory)
|
|
377
|
+
# Xlsxrb.read("large.xlsx") do |sheet|
|
|
378
|
+
# puts "Sheet: #{sheet.name}"
|
|
379
|
+
# sheet.each_row do |row|
|
|
380
|
+
# row.each_cell { |cell| puts "#{cell.ref}: #{cell.value}" }
|
|
381
|
+
# end
|
|
382
|
+
# end
|
|
383
|
+
#
|
|
384
|
+
# @example Lazy workbook access and explicit in-memory loading
|
|
385
|
+
# wb = Xlsxrb.read("data.xlsx")
|
|
386
|
+
# sheet = wb.sheets.first
|
|
387
|
+
# sheet.each_row { |row| ... } # streams with O(1) memory
|
|
388
|
+
# doc_sheet = sheet.load # explicitly load into memory
|
|
389
|
+
# puts doc_sheet["A1"].value # coordinate random access
|
|
390
|
+
#
|
|
391
|
+
# @param source [String, IO] File path, binary content string (starting with PK..), or IO object.
|
|
392
|
+
# @param password [String, nil] Optional password to decrypt password-protected XLSX files.
|
|
393
|
+
# @yield [sheet] Yields each streaming sheet.
|
|
394
|
+
# @yieldparam sheet [StreamSheet] The streaming worksheet object.
|
|
395
|
+
# @return [Elements::Workbook, void] Returns Elements::Workbook when no block is given.
|
|
375
396
|
# @api public
|
|
376
|
-
#: (untyped source) ->
|
|
377
|
-
|
|
397
|
+
#: (untyped source, ?password: String?) { (StreamSheet) -> void } -> void
|
|
398
|
+
#: (untyped source, ?password: String?) -> Elements::Workbook
|
|
399
|
+
def self.read(source, password: nil, &)
|
|
400
|
+
if source.is_a?(String)
|
|
401
|
+
if source.start_with?("PK\x03\x04") || source.include?("\x00") || Ooxml::Cfb::Reader.cfb?(source)
|
|
402
|
+
if Ooxml::Cfb::Reader.cfb?(source)
|
|
403
|
+
decrypted_zip = Ooxml::Crypto.decrypt(source, password)
|
|
404
|
+
source = StringIO.new(decrypted_zip)
|
|
405
|
+
else
|
|
406
|
+
source = StringIO.new(source)
|
|
407
|
+
end
|
|
408
|
+
elsif File.file?(source)
|
|
409
|
+
first_bytes = begin
|
|
410
|
+
File.binread(source, 8)
|
|
411
|
+
rescue StandardError
|
|
412
|
+
nil
|
|
413
|
+
end
|
|
414
|
+
if Ooxml::Cfb::Reader.cfb?(first_bytes)
|
|
415
|
+
encrypted_data = File.binread(source)
|
|
416
|
+
decrypted_zip = Ooxml::Crypto.decrypt(encrypted_data, password)
|
|
417
|
+
source = StringIO.new(decrypted_zip)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
elsif source.respond_to?(:read) && source.respond_to?(:pos) && source.respond_to?(:seek)
|
|
421
|
+
begin
|
|
422
|
+
cur_pos = source.pos
|
|
423
|
+
first_bytes = source.read(8)
|
|
424
|
+
source.seek(cur_pos)
|
|
425
|
+
if Ooxml::Cfb::Reader.cfb?(first_bytes)
|
|
426
|
+
full_data = source.read
|
|
427
|
+
decrypted_zip = Ooxml::Crypto.decrypt(full_data, password)
|
|
428
|
+
source = StringIO.new(decrypted_zip)
|
|
429
|
+
end
|
|
430
|
+
rescue StandardError
|
|
431
|
+
# Fall through to standard reader if seeking fails
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
378
435
|
attributes = source.is_a?(String) ? { "filepath" => source } : {}
|
|
379
436
|
Xlsxrb.in_span("Xlsxrb.read", attributes: attributes) do
|
|
380
437
|
entries = Ooxml::ZipReader.open(source, &:read_all)
|
|
381
438
|
shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
|
|
382
|
-
styles = Ooxml::StylesParser.parse(entries["xl/styles.xml"])
|
|
383
439
|
workbook_sheets = Ooxml::WorkbookParser.parse(entries["xl/workbook.xml"])
|
|
384
440
|
rels = Ooxml::RelationshipsParser.parse(entries["xl/_rels/workbook.xml.rels"])
|
|
441
|
+
styles = Ooxml::StylesParser.parse(entries["xl/styles.xml"])
|
|
385
442
|
|
|
386
443
|
sheets = workbook_sheets.map do |sheet_info|
|
|
387
444
|
target = rels[sheet_info[:r_id]]
|
|
@@ -389,24 +446,119 @@ module Xlsxrb
|
|
|
389
446
|
|
|
390
447
|
sheet_path = target.start_with?("/") ? target.delete_prefix("/") : "xl/#{target}"
|
|
391
448
|
sheet_xml = entries[sheet_path]
|
|
392
|
-
|
|
449
|
+
next nil if sheet_xml.nil? || sheet_xml.empty?
|
|
450
|
+
|
|
451
|
+
StreamSheet.new(
|
|
452
|
+
sheet_info[:name],
|
|
453
|
+
sheet_xml,
|
|
454
|
+
shared_strings,
|
|
455
|
+
styles
|
|
456
|
+
)
|
|
393
457
|
end.compact
|
|
394
458
|
|
|
395
|
-
Elements::Workbook.new(sheets: sheets, shared_strings: shared_strings, styles: styles)
|
|
459
|
+
wb = Elements::Workbook.new(sheets: sheets, shared_strings: shared_strings, styles: styles)
|
|
460
|
+
|
|
461
|
+
if block_given?
|
|
462
|
+
sheets.each(&)
|
|
463
|
+
nil
|
|
464
|
+
else
|
|
465
|
+
wb
|
|
466
|
+
end
|
|
396
467
|
end
|
|
397
468
|
end
|
|
398
469
|
|
|
399
|
-
# Writes an
|
|
470
|
+
# Writes an XLSX file or IO stream (streaming or in-memory), or returns a binary string.
|
|
471
|
+
#
|
|
472
|
+
# @overload write(target, password: nil, strict_excel_mode: true, &block)
|
|
473
|
+
# Streaming write: yields a StreamWriter context for high-speed, zero-allocation XLSX generation.
|
|
474
|
+
# @param target [String, IO] Destination file path or writable IO object.
|
|
475
|
+
# @param password [String, nil] Optional password to encrypt the generated XLSX file.
|
|
476
|
+
# @param strict_excel_mode [Boolean] Whether to enforce Excel specifications.
|
|
477
|
+
# @yield [stream_writer]
|
|
478
|
+
# @yieldparam stream_writer [Xlsxrb::StreamWriter]
|
|
479
|
+
# @return [void]
|
|
400
480
|
#
|
|
401
|
-
# @
|
|
481
|
+
# @overload write(workbook, password: nil)
|
|
482
|
+
# In-memory write: exports the workbook to an in-memory binary String.
|
|
483
|
+
# @param workbook [Elements::Workbook] The workbook to write.
|
|
484
|
+
# @param password [String, nil] Optional password to encrypt the binary string.
|
|
485
|
+
# @return [String] Binary data representing the XLSX file.
|
|
486
|
+
#
|
|
487
|
+
# @overload write(target, workbook, password: nil)
|
|
488
|
+
# In-memory write: writes the workbook to a file path or IO stream.
|
|
489
|
+
# @param target [String, IO] Destination file path or writable IO object.
|
|
490
|
+
# @param workbook [Elements::Workbook] The workbook to write.
|
|
491
|
+
# @param password [String, nil] Optional password to encrypt the output file.
|
|
492
|
+
# @return [void]
|
|
493
|
+
#
|
|
494
|
+
# @example Streaming write to file
|
|
495
|
+
# Xlsxrb.write("output.xlsx") do |writer|
|
|
496
|
+
# writer.sheet("Sheet1") { |s| s.row(["Hello", "World"]) }
|
|
497
|
+
# end
|
|
498
|
+
#
|
|
499
|
+
# @example Password-protected streaming write
|
|
500
|
+
# Xlsxrb.write("protected.xlsx", password: "secret_password") do |writer|
|
|
501
|
+
# writer.sheet("Confidential") { |s| s.row(["Private Data", 100]) }
|
|
502
|
+
# end
|
|
503
|
+
#
|
|
504
|
+
# @example In-memory export to binary string
|
|
505
|
+
# binary_data = Xlsxrb.write(workbook)
|
|
506
|
+
#
|
|
507
|
+
# @example In-memory write to file
|
|
402
508
|
# Xlsxrb.write("output.xlsx", workbook)
|
|
403
509
|
#
|
|
404
|
-
# @param target [String, IO] File path or IO object.
|
|
405
|
-
# @param workbook [Elements::Workbook] The workbook to write.
|
|
406
|
-
# @return [void]
|
|
407
510
|
# @api public
|
|
408
|
-
#: (
|
|
409
|
-
|
|
511
|
+
#: (Elements::Workbook workbook, ?password: String?, ?encryption_mode: Symbol) -> String
|
|
512
|
+
#: (untyped target, Elements::Workbook | untyped workbook, ?password: String?, ?encryption_mode: Symbol) -> void
|
|
513
|
+
#: (untyped target_or_workbook, ?Elements::Workbook | untyped workbook_or_nil, ?password: String?, ?encryption_mode: Symbol, ?strict_excel_mode: bool) ?{ (StreamWriter) -> void } -> untyped
|
|
514
|
+
def self.write(target_or_workbook, workbook_or_nil = nil, password: nil, encryption_mode: :standard, strict_excel_mode: true, &block)
|
|
515
|
+
if block_given?
|
|
516
|
+
target = target_or_workbook
|
|
517
|
+
raise Error, "target is required" if target.nil?
|
|
518
|
+
|
|
519
|
+
attributes = target.is_a?(String) ? { "filepath" => target } : {}
|
|
520
|
+
return Xlsxrb.in_span("Xlsxrb.write", attributes: attributes) do
|
|
521
|
+
if password && !password.empty?
|
|
522
|
+
buf = StringIO.new
|
|
523
|
+
buf.binmode
|
|
524
|
+
stream_writer = StreamWriter.new(buf, strict_excel_mode: strict_excel_mode)
|
|
525
|
+
begin
|
|
526
|
+
yield stream_writer
|
|
527
|
+
stream_writer.close
|
|
528
|
+
plain_bytes = buf.string.b
|
|
529
|
+
encrypted_bytes = Ooxml::Crypto.encrypt(plain_bytes, password, mode: encryption_mode)
|
|
530
|
+
if target.is_a?(String)
|
|
531
|
+
File.binwrite(target, encrypted_bytes)
|
|
532
|
+
elsif target.respond_to?(:write)
|
|
533
|
+
target.write(encrypted_bytes)
|
|
534
|
+
end
|
|
535
|
+
ensure
|
|
536
|
+
stream_writer.cleanup!
|
|
537
|
+
end
|
|
538
|
+
else
|
|
539
|
+
stream_writer = StreamWriter.new(target, strict_excel_mode: strict_excel_mode)
|
|
540
|
+
begin
|
|
541
|
+
yield stream_writer
|
|
542
|
+
stream_writer.close
|
|
543
|
+
ensure
|
|
544
|
+
stream_writer.cleanup!
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
if workbook_or_nil.nil?
|
|
551
|
+
wb = target_or_workbook
|
|
552
|
+
raise Error, "workbook must be an Elements::Workbook" unless wb.is_a?(Elements::Workbook)
|
|
553
|
+
|
|
554
|
+
io = StringIO.new
|
|
555
|
+
io.binmode
|
|
556
|
+
write(io, wb, password: password, encryption_mode: encryption_mode)
|
|
557
|
+
return io.string.b
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
target = target_or_workbook
|
|
561
|
+
workbook = workbook_or_nil
|
|
410
562
|
raise Error, "target is required" if target.nil?
|
|
411
563
|
raise Error, "workbook must be an Elements::Workbook" unless workbook.is_a?(Elements::Workbook)
|
|
412
564
|
|
|
@@ -416,7 +568,8 @@ module Xlsxrb
|
|
|
416
568
|
sst_index = {}
|
|
417
569
|
|
|
418
570
|
# Collect shared strings and build index without allocating new Hashes
|
|
419
|
-
sheet_data = workbook.sheets.map do |
|
|
571
|
+
sheet_data = workbook.sheets.map do |raw_ws|
|
|
572
|
+
ws = raw_ws.respond_to?(:load) ? raw_ws.load : raw_ws
|
|
420
573
|
ws.rows.each do |row|
|
|
421
574
|
row.cells.each do |cell|
|
|
422
575
|
val = cell.value
|
|
@@ -444,19 +597,44 @@ module Xlsxrb
|
|
|
444
597
|
|
|
445
598
|
# Extract workbook-level facade metadata
|
|
446
599
|
wb_facade = workbook.unmapped_data[:facade] || {}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
600
|
+
|
|
601
|
+
if password && !password.empty?
|
|
602
|
+
buf = StringIO.new
|
|
603
|
+
buf.binmode
|
|
604
|
+
Ooxml::WorkbookWriter.write(
|
|
605
|
+
buf,
|
|
606
|
+
sheets: sheet_data,
|
|
607
|
+
shared_strings: sst,
|
|
608
|
+
shared_strings_index: sst_index,
|
|
609
|
+
styles: workbook.styles,
|
|
610
|
+
defined_names: wb_facade[:defined_names],
|
|
611
|
+
core_properties: wb_facade[:core_properties],
|
|
612
|
+
app_properties: wb_facade[:app_properties],
|
|
613
|
+
custom_properties: wb_facade[:custom_properties],
|
|
614
|
+
workbook_protection: wb_facade[:workbook_protection],
|
|
615
|
+
workbook_properties: wb_facade[:workbook_properties]
|
|
616
|
+
)
|
|
617
|
+
encrypted_bytes = Ooxml::Crypto.encrypt(buf.string.b, password, mode: encryption_mode)
|
|
618
|
+
if target.is_a?(String)
|
|
619
|
+
File.binwrite(target, encrypted_bytes)
|
|
620
|
+
elsif target.respond_to?(:write)
|
|
621
|
+
target.write(encrypted_bytes)
|
|
622
|
+
end
|
|
623
|
+
else
|
|
624
|
+
Ooxml::WorkbookWriter.write(
|
|
625
|
+
target,
|
|
626
|
+
sheets: sheet_data,
|
|
627
|
+
shared_strings: sst,
|
|
628
|
+
shared_strings_index: sst_index,
|
|
629
|
+
styles: workbook.styles,
|
|
630
|
+
defined_names: wb_facade[:defined_names],
|
|
631
|
+
core_properties: wb_facade[:core_properties],
|
|
632
|
+
app_properties: wb_facade[:app_properties],
|
|
633
|
+
custom_properties: wb_facade[:custom_properties],
|
|
634
|
+
workbook_protection: wb_facade[:workbook_protection],
|
|
635
|
+
workbook_properties: wb_facade[:workbook_properties]
|
|
636
|
+
)
|
|
637
|
+
end
|
|
460
638
|
end
|
|
461
639
|
end
|
|
462
640
|
|
|
@@ -475,34 +653,46 @@ module Xlsxrb
|
|
|
475
653
|
#
|
|
476
654
|
# @param source [String, IO] The source file path or IO object.
|
|
477
655
|
# @param target [String, IO, nil] The target file path or IO object. If nil, overwrites source.
|
|
656
|
+
# @param password [String, nil] Optional password for reading and writing protected files.
|
|
478
657
|
# @yield [workbook] Yields the parsed workbook.
|
|
479
658
|
# @yieldparam workbook [Elements::Workbook] The parsed workbook.
|
|
480
659
|
# @yieldreturn [Elements::Workbook] The modified workbook.
|
|
481
660
|
# @return [void]
|
|
482
661
|
# @api public
|
|
483
|
-
#: (untyped source, ?untyped target) ?{ (Elements::Workbook) -> untyped } -> void
|
|
484
|
-
def self.modify(source, target = nil)
|
|
662
|
+
#: (untyped source, ?untyped target, ?password: String?) ?{ (Elements::Workbook) -> untyped } -> void
|
|
663
|
+
def self.modify(source, target = nil, password: nil)
|
|
485
664
|
raise Error, "source is required" if source.nil?
|
|
486
665
|
raise Error, "block is required" unless block_given?
|
|
487
666
|
|
|
488
|
-
workbook = read(source)
|
|
667
|
+
workbook = read(source, password: password).load
|
|
489
668
|
result_workbook = yield workbook
|
|
490
669
|
result_workbook = workbook unless result_workbook.is_a?(Elements::Workbook)
|
|
491
670
|
|
|
492
671
|
write_target = target || source
|
|
493
|
-
write(write_target, result_workbook)
|
|
672
|
+
write(write_target, result_workbook, password: password)
|
|
494
673
|
end
|
|
495
674
|
|
|
496
675
|
# Represents a sheet being streamed sequentially from an XLSX file.
|
|
676
|
+
# Provides O(1) constant-memory streaming over rows and cells.
|
|
677
|
+
#
|
|
678
|
+
# Call #load (or #to_worksheet) to convert this streaming sheet into an
|
|
679
|
+
# in-memory Elements::Worksheet supporting coordinate random access (sheet["A1"]).
|
|
497
680
|
#
|
|
498
|
-
# @example Iterate rows in streaming mode
|
|
499
|
-
# Xlsxrb.
|
|
681
|
+
# @example Iterate rows and cells in streaming mode (O(1) memory)
|
|
682
|
+
# Xlsxrb.read("large_data.xlsx") do |sheet|
|
|
500
683
|
# puts "Processing sheet: #{sheet.name}"
|
|
501
684
|
# sheet.each_row do |row|
|
|
502
|
-
#
|
|
685
|
+
# row.each_cell do |cell|
|
|
686
|
+
# puts "#{cell.ref}: #{cell.value}"
|
|
687
|
+
# end
|
|
503
688
|
# end
|
|
504
689
|
# end
|
|
505
690
|
#
|
|
691
|
+
# @example Load into an in-memory Worksheet for coordinate random access
|
|
692
|
+
# wb = Xlsxrb.read("data.xlsx")
|
|
693
|
+
# doc_sheet = wb.sheet(0).load
|
|
694
|
+
# puts doc_sheet["A1"].value
|
|
695
|
+
#
|
|
506
696
|
# @api public
|
|
507
697
|
class StreamSheet
|
|
508
698
|
[Enumerable].each { |m| include m }
|
|
@@ -512,26 +702,28 @@ module Xlsxrb
|
|
|
512
702
|
# @param name [String] The sheet name.
|
|
513
703
|
# @param sheet_xml [String] Raw XML content of the sheet.
|
|
514
704
|
# @param shared_strings [Array<String>] Shared strings table.
|
|
515
|
-
|
|
516
|
-
|
|
705
|
+
# @param styles [Hash, nil] Styles table.
|
|
706
|
+
#: (String name, String sheet_xml, Array[String] shared_strings, ?Hash[untyped, untyped]? styles) -> void
|
|
707
|
+
def initialize(name, sheet_xml, shared_strings, styles = nil)
|
|
517
708
|
@name = name
|
|
518
709
|
@sheet_xml = sheet_xml
|
|
519
710
|
@shared_strings = shared_strings
|
|
711
|
+
@styles = styles
|
|
520
712
|
end
|
|
521
713
|
|
|
522
|
-
# Iterate over rows in this streaming sheet.
|
|
714
|
+
# Iterate over rows in this streaming sheet (O(1) memory).
|
|
523
715
|
#
|
|
524
716
|
# @yield [row]
|
|
525
|
-
# @yieldparam row [Elements::Row]
|
|
717
|
+
# @yieldparam row [StreamRow, Elements::Row]
|
|
526
718
|
# @return [Enumerator, void]
|
|
527
719
|
# @api public
|
|
528
|
-
#: () { (Elements::Row) -> void } -> void
|
|
529
|
-
#:
|
|
720
|
+
#: () { (StreamRow | Elements::Row) -> void } -> void
|
|
721
|
+
#: () -> Enumerator[StreamRow | Elements::Row, void]
|
|
530
722
|
def each_row
|
|
531
723
|
return enum_for(:each_row) unless block_given?
|
|
532
724
|
|
|
533
725
|
Ooxml::WorksheetParser.each_row(@sheet_xml, shared_strings: @shared_strings) do |row|
|
|
534
|
-
if row.is_a?(Elements::Row)
|
|
726
|
+
if row.is_a?(Elements::Row) || row.is_a?(StreamRow)
|
|
535
727
|
yield row
|
|
536
728
|
else
|
|
537
729
|
yield Xlsxrb.send(:build_row_from_raw, row)
|
|
@@ -539,88 +731,45 @@ module Xlsxrb
|
|
|
539
731
|
end
|
|
540
732
|
end
|
|
541
733
|
|
|
542
|
-
# Iterate over rows
|
|
734
|
+
# Iterate over all cells across rows continuously (O(1) memory).
|
|
543
735
|
#
|
|
544
|
-
# @yield [
|
|
545
|
-
# @yieldparam
|
|
736
|
+
# @yield [cell]
|
|
737
|
+
# @yieldparam cell [Elements::Cell]
|
|
546
738
|
# @return [Enumerator, void]
|
|
547
739
|
# @api public
|
|
548
|
-
#: () { (Elements::
|
|
549
|
-
#:
|
|
550
|
-
def
|
|
551
|
-
|
|
552
|
-
end
|
|
553
|
-
end
|
|
740
|
+
#: () { (Elements::Cell) -> void } -> void
|
|
741
|
+
#: () -> Enumerator[Elements::Cell, void]
|
|
742
|
+
def each_cell(&)
|
|
743
|
+
return enum_for(:each_cell) unless block_given?
|
|
554
744
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
#
|
|
558
|
-
# @example
|
|
559
|
-
# Xlsxrb.foreach("large.xlsx") do |sheet|
|
|
560
|
-
# puts "Sheet: #{sheet.name}"
|
|
561
|
-
# sheet.each_row { |row| process(row) }
|
|
562
|
-
# end
|
|
563
|
-
#
|
|
564
|
-
# @param source [String, IO] File path or IO object.
|
|
565
|
-
# @yield [sheet] Yields each sheet.
|
|
566
|
-
# @yieldparam sheet [StreamSheet] The streaming sheet object.
|
|
567
|
-
# @return [Enumerator, void]
|
|
568
|
-
# @api public
|
|
569
|
-
#: (untyped source) ?{ (StreamSheet) -> void } -> untyped
|
|
570
|
-
def self.foreach(source)
|
|
571
|
-
return enum_for(:foreach, source) unless block_given?
|
|
572
|
-
|
|
573
|
-
attributes = source.is_a?(String) ? { "filepath" => source } : {}
|
|
574
|
-
Xlsxrb.in_span("Xlsxrb.foreach", attributes: attributes) do
|
|
575
|
-
entries = Ooxml::ZipReader.open(source, &:read_all)
|
|
576
|
-
shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
|
|
577
|
-
workbook_sheets = Ooxml::WorkbookParser.parse(entries["xl/workbook.xml"])
|
|
578
|
-
rels = Ooxml::RelationshipsParser.parse(entries["xl/_rels/workbook.xml.rels"])
|
|
579
|
-
|
|
580
|
-
workbook_sheets.each do |sheet_info|
|
|
581
|
-
target = rels[sheet_info[:r_id]]
|
|
582
|
-
next unless target
|
|
583
|
-
|
|
584
|
-
sheet_path = target.start_with?("/") ? target.delete_prefix("/") : "xl/#{target}"
|
|
585
|
-
sheet_xml = entries[sheet_path]
|
|
586
|
-
next if sheet_xml.nil? || sheet_xml.empty?
|
|
587
|
-
|
|
588
|
-
yield StreamSheet.new(sheet_info[:name], sheet_xml, shared_strings)
|
|
745
|
+
each_row do |row|
|
|
746
|
+
row.each_cell(&)
|
|
589
747
|
end
|
|
590
748
|
end
|
|
591
|
-
end
|
|
592
749
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
# @param strict_excel_mode [Boolean] Whether to enforce Excel specifications (max rows/cols/length).
|
|
605
|
-
# @yield [stream_writer]
|
|
606
|
-
# @yieldparam stream_writer [Xlsxrb::StreamWriter]
|
|
607
|
-
# @return [void]
|
|
608
|
-
# @api public
|
|
609
|
-
#: (untyped target, ?strict_excel_mode: bool) ?{ (StreamWriter) -> void } -> void
|
|
610
|
-
def self.generate(target, strict_excel_mode: true)
|
|
611
|
-
raise Error, "target is required" if target.nil?
|
|
612
|
-
raise Error, "block is required" unless block_given?
|
|
750
|
+
# Default Enumerable iteration iterates rows in the streaming sheet.
|
|
751
|
+
#
|
|
752
|
+
# @yield [row]
|
|
753
|
+
# @yieldparam row [StreamRow, Elements::Row]
|
|
754
|
+
# @return [Enumerator, void]
|
|
755
|
+
# @api public
|
|
756
|
+
#: () { (StreamRow | Elements::Row) -> void } -> void
|
|
757
|
+
#: () -> Enumerator[StreamRow | Elements::Row, void]
|
|
758
|
+
def each(&)
|
|
759
|
+
each_row(&)
|
|
760
|
+
end
|
|
613
761
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
762
|
+
# Loads this sheet completely into an in-memory Elements::Worksheet,
|
|
763
|
+
# enabling coordinate random access (sheet["A1"]), row lookups (row_at),
|
|
764
|
+
# and immutable cell updates (update_cell).
|
|
765
|
+
#
|
|
766
|
+
# @return [Elements::Worksheet] The fully parsed in-memory worksheet.
|
|
767
|
+
# @api public
|
|
768
|
+
#: () -> Elements::Worksheet
|
|
769
|
+
def load
|
|
770
|
+
Xlsxrb.send(:build_worksheet, @name, @sheet_xml, @shared_strings, @styles)
|
|
623
771
|
end
|
|
772
|
+
alias to_worksheet load
|
|
624
773
|
end
|
|
625
774
|
|
|
626
775
|
# Builds an in-memory Elements::Workbook using a declarative DSL.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Generated from lib/xlsxrb/elements/coordinate_access.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Xlsxrb
|
|
4
|
+
module Elements
|
|
5
|
+
# Mixin providing coordinate-based and random-access cell/row lookups
|
|
6
|
+
# for in-memory worksheet structures.
|
|
7
|
+
#
|
|
8
|
+
# Expects the including class to provide a `#rows` method returning an `Array<Elements::Row>`.
|
|
9
|
+
#
|
|
10
|
+
# @api public
|
|
11
|
+
module CoordinateAccess
|
|
12
|
+
# Returns a Hash mapping Excel cell references (e.g. "A1") to Cell objects.
|
|
13
|
+
#
|
|
14
|
+
# @return [Hash<String, Elements::Cell>]
|
|
15
|
+
# : () -> Hash[String, Elements::Cell]
|
|
16
|
+
def cells_hash: () -> Hash[String, Elements::Cell]
|
|
17
|
+
|
|
18
|
+
# Returns all cells ordered by row and column index.
|
|
19
|
+
#
|
|
20
|
+
# @return [Array<Elements::Cell>]
|
|
21
|
+
# @api public
|
|
22
|
+
# : () -> Array[Elements::Cell]
|
|
23
|
+
def cells: () -> Array[Elements::Cell]
|
|
24
|
+
|
|
25
|
+
# Access a cell by its Excel-style reference (e.g. "A1").
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# sheet["A1"] #=> #<Elements::Cell value="Hello">
|
|
29
|
+
#
|
|
30
|
+
# @param ref [String, Symbol] Cell reference (e.g. "A1" or :A1).
|
|
31
|
+
# @return [Elements::Cell, nil]
|
|
32
|
+
# @api public
|
|
33
|
+
# : (String | Symbol ref) -> Elements::Cell?
|
|
34
|
+
def []: (String | Symbol ref) -> Elements::Cell?
|
|
35
|
+
|
|
36
|
+
# Returns the row at the given 0-based index, or nil.
|
|
37
|
+
#
|
|
38
|
+
# @param index [Integer] 0-based row index.
|
|
39
|
+
# @return [Elements::Row, nil]
|
|
40
|
+
# @api public
|
|
41
|
+
# : (Integer index) -> Elements::Row?
|
|
42
|
+
def row_at: (Integer index) -> Elements::Row?
|
|
43
|
+
|
|
44
|
+
# Returns the first row in the sheet, or nil.
|
|
45
|
+
#
|
|
46
|
+
# @return [Elements::Row, nil]
|
|
47
|
+
# @api public
|
|
48
|
+
# : () -> Elements::Row?
|
|
49
|
+
def first_row: () -> Elements::Row?
|
|
50
|
+
|
|
51
|
+
# Returns the last row in the sheet, or nil.
|
|
52
|
+
#
|
|
53
|
+
# @return [Elements::Row, nil]
|
|
54
|
+
# @api public
|
|
55
|
+
# : () -> Elements::Row?
|
|
56
|
+
def last_row: () -> Elements::Row?
|
|
57
|
+
|
|
58
|
+
# Returns the raw cell value at the given Excel-style reference (e.g. "A1").
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# sheet.cell_value("A1") #=> "Sales Report"
|
|
62
|
+
#
|
|
63
|
+
# @param ref [String] Cell reference (e.g. "A1").
|
|
64
|
+
# @return [Object, nil]
|
|
65
|
+
# @api public
|
|
66
|
+
# : (String ref) -> untyped
|
|
67
|
+
def cell_value: (String ref) -> untyped
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|