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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/README.md +125 -36
  4. data/Rakefile +36 -7
  5. data/benchmark.rb +32 -4
  6. data/docs/ARCHITECTURE.md +32 -30
  7. data/docs/DEVELOPMENT.md +11 -3
  8. data/docs/QUALITY_ASSURANCE.md +3 -4
  9. data/docs/SPEC_SOURCES.md +7 -1
  10. data/docs/visual/VisualGallery.md +157 -157
  11. data/docs/wasm/ruby.wasm +0 -0
  12. data/lib/ruby_lsp/xlsxrb/addon.rb +1 -1
  13. data/lib/ruby_lsp/xlsxrb/completion_listener.rb +11 -13
  14. data/lib/xlsxrb/elements/coordinate_access.rb +101 -0
  15. data/lib/xlsxrb/elements/row.rb +2 -2
  16. data/lib/xlsxrb/elements/workbook.rb +20 -2
  17. data/lib/xlsxrb/elements/worksheet.rb +94 -124
  18. data/lib/xlsxrb/elements.rb +1 -0
  19. data/lib/xlsxrb/ooxml/cfb.rb +490 -0
  20. data/lib/xlsxrb/ooxml/crypto/agile.rb +285 -0
  21. data/lib/xlsxrb/ooxml/crypto/standard.rb +152 -0
  22. data/lib/xlsxrb/ooxml/crypto.rb +60 -0
  23. data/lib/xlsxrb/ooxml/worksheet_parser.rb +21 -15
  24. data/lib/xlsxrb/stream_row.rb +181 -0
  25. data/lib/xlsxrb/version.rb +1 -1
  26. data/lib/xlsxrb.rb +271 -122
  27. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +70 -0
  28. data/sig/generated/xlsxrb/elements/worksheet.rbs +119 -14
  29. data/sig/generated/xlsxrb/ooxml/cfb.rbs +121 -0
  30. data/sig/generated/xlsxrb/ooxml/crypto/agile.rbs +46 -0
  31. data/sig/generated/xlsxrb/ooxml/crypto/standard.rbs +28 -0
  32. data/sig/generated/xlsxrb/ooxml/crypto.rbs +17 -0
  33. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +2 -0
  34. data/sig/generated/xlsxrb/stream_row.rbs +127 -0
  35. data/sig/generated/xlsxrb.rbs +135 -71
  36. data/vendor/sdk_runner/Program.cs +3 -1
  37. 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 into an in-memory Elements::Workbook.
368
+ # Reads an XLSX file (streaming / lazy-loaded by default) from a file path, IO stream, or binary String.
364
369
  #
365
- # @example Read from file path
366
- # workbook = Xlsxrb.read("data.xlsx")
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
- # @example Read from IO stream
371
- # workbook = File.open("data.xlsx", "rb") { |io| Xlsxrb.read(io) }
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
- # @param source [String, IO] File path or IO object.
374
- # @return [Elements::Workbook] The parsed workbook.
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) -> Elements::Workbook
377
- def self.read(source)
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
- build_worksheet(sheet_info[:name], sheet_xml, shared_strings, styles)
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 Elements::Workbook to an XLSX file or IO stream.
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
- # @example Write to file
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
- #: (untyped target, untyped workbook) -> void
409
- def self.write(target, workbook)
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 |ws|
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
- Ooxml::WorkbookWriter.write(
448
- target,
449
- sheets: sheet_data,
450
- shared_strings: sst,
451
- shared_strings_index: sst_index,
452
- styles: workbook.styles,
453
- defined_names: wb_facade[:defined_names],
454
- core_properties: wb_facade[:core_properties],
455
- app_properties: wb_facade[:app_properties],
456
- custom_properties: wb_facade[:custom_properties],
457
- workbook_protection: wb_facade[:workbook_protection],
458
- workbook_properties: wb_facade[:workbook_properties]
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.foreach("large_data.xlsx") do |sheet|
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
- # puts row.to_a.inspect
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
- #: (String name, String sheet_xml, Array[String] shared_strings) -> void
516
- def initialize(name, sheet_xml, shared_strings)
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
- #: | () -> Enumerator[Elements::Row, void]
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 in this streaming sheet.
734
+ # Iterate over all cells across rows continuously (O(1) memory).
543
735
  #
544
- # @yield [row]
545
- # @yieldparam row [Elements::Row]
736
+ # @yield [cell]
737
+ # @yieldparam cell [Elements::Cell]
546
738
  # @return [Enumerator, void]
547
739
  # @api public
548
- #: () { (Elements::Row) -> void } -> void
549
- #: | () -> Enumerator[Elements::Row, void]
550
- def each(&)
551
- each_row(&)
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
- # Streaming read: yields StreamSheet objects one at a time for each sheet in the workbook.
556
- # Keeps memory usage minimal even for multi-gigabyte XLSX files.
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
- # Streaming write: yields a StreamWriter context for high-speed, zero-allocation XLSX generation.
594
- #
595
- # @example Generate an Excel file with styles and multiple sheets
596
- # Xlsxrb.generate("sales.xlsx") do |stream_writer|
597
- # stream_writer.sheet("Q1") do |sheet|
598
- # sheet.row(["Product", "Revenue"], styles: :bold)
599
- # sheet.row(["Widget", 15000])
600
- # end
601
- # end
602
- #
603
- # @param target [String, IO] File path or IO stream (e.g. pipe, socket, Rails response buffer).
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
- attributes = target.is_a?(String) ? { "filepath" => target } : {}
615
- Xlsxrb.in_span("Xlsxrb.generate", attributes: attributes) do
616
- stream_writer = StreamWriter.new(target, strict_excel_mode: strict_excel_mode)
617
- begin
618
- yield stream_writer
619
- stream_writer.close
620
- ensure
621
- stream_writer.cleanup!
622
- end
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