xlsxrb 0.1.11 → 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.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -34
  3. data/README.md +26 -21
  4. data/lib/xlsxrb/chart_builder.rb +18 -18
  5. data/lib/xlsxrb/dsl_helpers.rb +175 -0
  6. data/lib/xlsxrb/elements/cell.rb +56 -22
  7. data/lib/xlsxrb/elements/column.rb +27 -3
  8. data/lib/xlsxrb/elements/row.rb +27 -8
  9. data/lib/xlsxrb/elements/workbook.rb +27 -14
  10. data/lib/xlsxrb/elements/worksheet.rb +17 -1
  11. data/lib/xlsxrb/ooxml/cfb.rb +9 -6
  12. data/lib/xlsxrb/ooxml/crypto/agile.rb +5 -5
  13. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +784 -0
  14. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +3478 -0
  15. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +1796 -0
  16. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +428 -0
  17. data/lib/xlsxrb/ooxml/reader/listeners.rb +4 -6446
  18. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +3 -5
  19. data/lib/xlsxrb/ooxml/utils.rb +5 -6
  20. data/lib/xlsxrb/ooxml/workbook_writer.rb +5 -7
  21. data/lib/xlsxrb/ooxml/worksheet_parser.rb +172 -9
  22. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +1602 -0
  23. data/lib/xlsxrb/ooxml/writer/features_xml.rb +731 -0
  24. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +348 -0
  25. data/lib/xlsxrb/ooxml/writer.rb +9 -2646
  26. data/lib/xlsxrb/ooxml/xml_builder.rb +31 -2
  27. data/lib/xlsxrb/ooxml/zip_generator.rb +26 -5
  28. data/lib/xlsxrb/ooxml/zip_reader.rb +364 -78
  29. data/lib/xlsxrb/ooxml/zip_writer.rb +32 -5
  30. data/lib/xlsxrb/stream_sheet.rb +94 -6
  31. data/lib/xlsxrb/stream_writer.rb +48 -115
  32. data/lib/xlsxrb/style_builder.rb +10 -1
  33. data/lib/xlsxrb/version.rb +1 -1
  34. data/lib/xlsxrb/workbook_builder.rb +1 -1
  35. data/lib/xlsxrb/worksheet_builder.rb +19 -81
  36. data/lib/xlsxrb.rb +449 -171
  37. data/sig/generated/xlsxrb/chart_builder.rbs +36 -36
  38. data/sig/generated/xlsxrb/dsl_helpers.rbs +92 -0
  39. data/sig/generated/xlsxrb/elements/cell.rbs +29 -0
  40. data/sig/generated/xlsxrb/elements/column.rbs +32 -21
  41. data/sig/generated/xlsxrb/elements/row.rbs +94 -14
  42. data/sig/generated/xlsxrb/elements/workbook.rbs +102 -10
  43. data/sig/generated/xlsxrb/elements/worksheet.rbs +9 -0
  44. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +173 -0
  45. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +400 -0
  46. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +442 -0
  47. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +80 -0
  48. data/sig/generated/xlsxrb/ooxml/reader/listeners.rbs +0 -1072
  49. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +0 -2
  50. data/sig/generated/xlsxrb/ooxml/utils.rbs +2 -0
  51. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +5 -4
  52. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +79 -0
  53. data/sig/generated/xlsxrb/ooxml/writer/features_xml.rbs +83 -0
  54. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +53 -0
  55. data/sig/generated/xlsxrb/ooxml/writer.rbs +6 -185
  56. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +18 -0
  57. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +15 -0
  58. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +96 -8
  59. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +5 -1
  60. data/sig/generated/xlsxrb/stream_sheet.rbs +43 -3
  61. data/sig/generated/xlsxrb/style_builder.rbs +7 -0
  62. data/sig/generated/xlsxrb.rbs +18 -11
  63. metadata +22 -144
  64. data/.devcontainer/Dockerfile +0 -65
  65. data/.devcontainer/devcontainer.json +0 -17
  66. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +0 -9
  67. data/.gem_rbs_collection/ast/2.4/ast.rbs +0 -73
  68. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +0 -9
  69. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +0 -4
  70. data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +0 -16
  71. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +0 -96
  72. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +0 -4
  73. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +0 -68
  74. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +0 -249
  75. data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +0 -4
  76. data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +0 -47
  77. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +0 -5
  78. data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +0 -9
  79. data/.gem_rbs_collection/csv/3.3/csv.rbs +0 -3871
  80. data/.gem_rbs_collection/csv/3.3/manifest.yaml +0 -3
  81. data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +0 -9
  82. data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +0 -48
  83. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +0 -9
  84. data/.gem_rbs_collection/listen/3.9/listen.rbs +0 -25
  85. data/.gem_rbs_collection/listen/3.9/listener.rbs +0 -24
  86. data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +0 -9
  87. data/.gem_rbs_collection/logger/1.7/formatter.rbs +0 -45
  88. data/.gem_rbs_collection/logger/1.7/log_device.rbs +0 -100
  89. data/.gem_rbs_collection/logger/1.7/logger.rbs +0 -796
  90. data/.gem_rbs_collection/logger/1.7/manifest.yaml +0 -2
  91. data/.gem_rbs_collection/logger/1.7/period.rbs +0 -17
  92. data/.gem_rbs_collection/logger/1.7/severity.rbs +0 -34
  93. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +0 -9
  94. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +0 -2332
  95. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +0 -4
  96. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +0 -9
  97. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +0 -86
  98. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +0 -9
  99. data/.gem_rbs_collection/parser/3.2/manifest.yaml +0 -7
  100. data/.gem_rbs_collection/parser/3.2/parser.rbs +0 -194
  101. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +0 -4
  102. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +0 -9
  103. data/.gem_rbs_collection/rainbow/3.0/global.rbs +0 -7
  104. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +0 -209
  105. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +0 -5
  106. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +0 -9
  107. data/.gem_rbs_collection/rake/13.0/manifest.yaml +0 -2
  108. data/.gem_rbs_collection/rake/13.0/rake.rbs +0 -39
  109. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +0 -9
  110. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +0 -17
  111. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +0 -9
  112. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +0 -208
  113. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +0 -9
  114. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +0 -903
  115. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +0 -9
  116. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +0 -8
  117. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +0 -42
  118. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +0 -5
  119. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +0 -47
  120. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +0 -30
  121. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +0 -9
  122. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +0 -7
  123. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +0 -19
  124. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +0 -31
  125. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +0 -18
  126. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +0 -12
  127. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +0 -11
  128. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +0 -13
  129. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +0 -95
  130. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +0 -31
  131. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +0 -58
  132. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +0 -24
  133. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +0 -17
  134. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +0 -23
  135. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +0 -22
  136. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +0 -30
  137. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +0 -20
  138. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +0 -15
  139. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +0 -26
  140. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +0 -21
  141. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +0 -131
  142. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +0 -14
  143. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +0 -33
  144. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +0 -21
  145. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +0 -63
  146. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +0 -55
  147. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +0 -35
  148. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +0 -7
  149. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +0 -10
  150. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +0 -22
  151. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +0 -29
  152. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +0 -17
  153. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +0 -13
  154. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +0 -10
  155. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +0 -8
  156. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +0 -6
  157. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +0 -30
  158. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +0 -10
  159. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +0 -10
  160. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +0 -5
  161. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +0 -15
  162. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +0 -3
  163. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +0 -40
  164. data/Rakefile +0 -567
  165. data/Steepfile +0 -18
  166. data/benchmark.rb +0 -406
  167. data/docs/ARCHITECTURE.md +0 -514
  168. data/docs/DEVELOPMENT.md +0 -133
  169. data/docs/PEER_LIBRARIES.md +0 -121
  170. data/docs/QUALITY_ASSURANCE.md +0 -25
  171. data/docs/SPEC_SOURCES.md +0 -48
  172. data/docs/assets/benchmark_results.svg +0 -144
  173. data/docs/assets/lsp_autocompletion.png +0 -0
  174. data/docs/assets/playground_preview.png +0 -0
  175. data/docs/coi-serviceworker.js +0 -82
  176. data/docs/office_thread.js +0 -77
  177. data/docs/preview.html +0 -719
  178. data/docs/visual/VisualGallery.md +0 -4672
  179. data/docs/wasm/ruby.wasm +0 -0
  180. data/docs/wasm/wasm_doc_helper.css +0 -351
  181. data/docs/wasm/wasm_doc_helper.js +0 -439
  182. data/docs/zeta.js +0 -1107
  183. data/rbs_collection.lock.yaml +0 -252
  184. data/rbs_collection.yaml +0 -19
  185. data/vendor/sdk_runner/Program.cs +0 -93
  186. data/vendor/sdk_runner/sdk_runner.csproj +0 -13
@@ -3,14 +3,13 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  require_relative "xml_parser"
6
+ require_relative "xml_builder"
6
7
 
7
8
  module Xlsxrb
8
9
  module Ooxml
9
10
  # SAX-based parser for xl/sharedStrings.xml.
10
11
  # Returns an Array of strings (index = SST index).
11
12
  class SharedStringsParser
12
- XML_ENTITIES = { "&amp;" => "&", "&lt;" => "<", "&gt;" => ">", "&quot;" => '"', "&apos;" => "'" }.freeze
13
-
14
13
  # Parses all shared strings and returns an Array of strings.
15
14
  def self.parse(xml_string, _part_name: "xl/sharedStrings.xml")
16
15
  return [] if xml_string.nil? || xml_string.empty?
@@ -89,7 +88,7 @@ module Xlsxrb
89
88
  str = extract_multi_t(xml, si_open_end + 1, si_end)
90
89
  else
91
90
  raw_str = xml.byteslice(t_open_end + 1, t_end - t_open_end - 1).force_encoding("UTF-8")
92
- str = raw_str.include?("&") ? raw_str.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES) : raw_str
91
+ str = XmlBuilder.unescape(raw_str)
93
92
  end
94
93
  else
95
94
  str = ""
@@ -128,8 +127,7 @@ module Xlsxrb
128
127
  break unless t_end && t_end <= to
129
128
 
130
129
  raw_chunk = xml.byteslice(t_open_end + 1, t_end - t_open_end - 1).force_encoding("UTF-8")
131
- raw_chunk = raw_chunk.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES) if raw_chunk.include?("&")
132
- buf << raw_chunk
130
+ buf << XmlBuilder.unescape(raw_chunk)
133
131
  pos = t_end + 4
134
132
  end
135
133
  buf
@@ -12,6 +12,7 @@ module Xlsxrb
12
12
  module Utils
13
13
  # Excel 1900 date system epoch.
14
14
  EPOCH_1900 = Date.new(1899, 12, 31) # serial 1 = Jan 1, 1900
15
+ EPOCH_1900_JD = EPOCH_1900.jd
15
16
 
16
17
  # Built-in number format codes defined by SpreadsheetML.
17
18
  BUILTIN_NUM_FMT_CODES = {
@@ -57,7 +58,7 @@ module Xlsxrb
57
58
  class << self
58
59
  # Converts a Date to an Excel serial number (1900 system).
59
60
  def date_to_serial(date)
60
- serial = (date - EPOCH_1900).to_i
61
+ serial = date.jd - EPOCH_1900_JD
61
62
  # Lotus 1-2-3 bug: serial 60 = Feb 29, 1900 (doesn't exist).
62
63
  # Dates on or after Mar 1, 1900 (raw serial >= 60) need +1.
63
64
  serial += 1 if serial >= 60
@@ -82,13 +83,11 @@ module Xlsxrb
82
83
 
83
84
  # Converts a fractional Excel serial number to a Time (1900 system, UTC).
84
85
  def serial_to_datetime(serial)
85
- int_part = serial.to_i
86
- frac = serial - int_part
86
+ int_part, frac = serial.divmod(1)
87
87
  date = serial_to_date(int_part)
88
88
  total_seconds = (frac * 86_400).round
89
- hours = total_seconds / 3600
90
- minutes = (total_seconds % 3600) / 60
91
- seconds = total_seconds % 60
89
+ hours, remaining = total_seconds.divmod(3600)
90
+ minutes, seconds = remaining.divmod(60)
92
91
  Time.utc(date.year, date.month, date.day, hours, minutes, seconds)
93
92
  end
94
93
 
@@ -33,13 +33,11 @@ module Xlsxrb
33
33
  def self.write(target, sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
34
34
  defined_names: nil, core_properties: nil, app_properties: nil,
35
35
  custom_properties: nil, workbook_protection: nil, workbook_properties: nil)
36
- Xlsxrb.in_span("Ooxml::WorkbookWriter.write") do
37
- writer = new(sheets: sheets, shared_strings: shared_strings, shared_strings_index: shared_strings_index, styles: styles,
38
- defined_names: defined_names, core_properties: core_properties,
39
- app_properties: app_properties, custom_properties: custom_properties,
40
- workbook_protection: workbook_protection, workbook_properties: workbook_properties)
41
- writer.write_to(target)
42
- end
36
+ writer = new(sheets: sheets, shared_strings: shared_strings, shared_strings_index: shared_strings_index, styles: styles,
37
+ defined_names: defined_names, core_properties: core_properties,
38
+ app_properties: app_properties, custom_properties: custom_properties,
39
+ workbook_protection: workbook_protection, workbook_properties: workbook_properties)
40
+ writer.write_to(target)
43
41
  end
44
42
 
45
43
  def initialize(sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
@@ -3,6 +3,7 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  require_relative "xml_parser"
6
+ require_relative "xml_builder"
6
7
 
7
8
  module Xlsxrb
8
9
  module Ooxml
@@ -25,11 +26,15 @@ module Xlsxrb
25
26
  end
26
27
 
27
28
  # Streaming parse: yields one raw row hash at a time.
28
- def self.each_row(xml_string, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block)
29
- return enum_for(:each_row, xml_string, shared_strings: shared_strings, part_name: part_name) unless block
30
- return if xml_string.nil? || xml_string.empty?
29
+ def self.each_row(xml_source, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block)
30
+ return enum_for(:each_row, xml_source, shared_strings: shared_strings, part_name: part_name) unless block
31
+ return if xml_source.nil? || (xml_source.respond_to?(:empty?) && xml_source.empty?)
31
32
 
32
- fast_scan_rows_direct(xml_string, shared_strings, part_name, &block)
33
+ if xml_source.is_a?(String)
34
+ fast_scan_rows_direct(xml_source, shared_strings, part_name, &block)
35
+ else
36
+ scan_rows_stream(xml_source, shared_strings, part_name, &block)
37
+ end
33
38
  end
34
39
 
35
40
  # Parses column definitions (<cols>) from a worksheet.
@@ -428,17 +433,14 @@ module Xlsxrb
428
433
 
429
434
  private_class_method :resolve_fast_value
430
435
 
431
- XML_ENTITIES = { "&amp;" => "&", "&lt;" => "<", "&gt;" => ">", "&quot;" => '"', "&apos;" => "'" }.freeze
432
-
433
436
  # SECURITY NOTE: Decodes only standard predefined XML entities in a single pass.
434
437
  # This completely prevents "Billion Laughs" attacks (exponential entity expansion)
435
438
  # because it avoids recursive expansion and ignores custom entities entirely.
439
+ #: (String str) -> String
436
440
  def self.decode_xml_entities(str)
437
- str.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES)
441
+ XmlBuilder.unescape(str)
438
442
  end
439
443
 
440
- private_class_method :decode_xml_entities
441
-
442
444
  # Parses <cols> section for column definitions.
443
445
  class ColumnsListener
444
446
  include REXML::SAX2Listener
@@ -574,6 +576,167 @@ module Xlsxrb
574
576
 
575
577
  private_class_method :fast_scan_rows_direct
576
578
 
579
+ def self.scan_rows_stream(source, shared_strings, _part_name, &block)
580
+ buffer = +""
581
+ buffer.force_encoding(Encoding::BINARY)
582
+ in_sheet_data = false
583
+ prefix = ""
584
+ row_start_pattern = "<row"
585
+ row_start_len = 4
586
+ row_end_tag = "</row>"
587
+ row_end_len = 6
588
+ sd_end_tag = "</sheetData>"
589
+
590
+ push_chunk = lambda do |chunk|
591
+ return if in_sheet_data == :closed
592
+ return if chunk.nil? || chunk.empty?
593
+
594
+ buffer << chunk.b
595
+
596
+ unless in_sheet_data
597
+ sd_term = buffer.index("sheetData")
598
+ if sd_term.nil?
599
+ buffer.slice!(0...(buffer.bytesize - 32)) if buffer.bytesize > 64
600
+ return
601
+ end
602
+
603
+ sd_start = buffer.rindex("<", sd_term)
604
+ if sd_start.nil?
605
+ buffer.slice!(0...sd_term)
606
+ return
607
+ end
608
+
609
+ sd_open_end = buffer.index(">", sd_term)
610
+ return if sd_open_end.nil?
611
+
612
+ if buffer.getbyte(sd_open_end - 1) == 47
613
+ in_sheet_data = :closed
614
+ buffer.clear
615
+ return
616
+ end
617
+
618
+ prefix = buffer.byteslice(sd_start + 1, sd_term - (sd_start + 1))
619
+ row_start_pattern = "<#{prefix}row"
620
+ row_start_len = row_start_pattern.bytesize
621
+ row_end_tag = "</#{prefix}row>"
622
+ row_end_len = row_end_tag.bytesize
623
+ sd_end_tag = "</#{prefix}sheetData>"
624
+
625
+ buffer.slice!(0..sd_open_end)
626
+ in_sheet_data = true
627
+ end
628
+
629
+ pos = 0
630
+ last_consumed_pos = 0
631
+
632
+ loop do
633
+ sd_end_pos = buffer.index(sd_end_tag, pos)
634
+ row_start = buffer.index(row_start_pattern, pos)
635
+
636
+ if sd_end_pos && (row_start.nil? || sd_end_pos < row_start)
637
+ in_sheet_data = :closed
638
+ buffer.clear
639
+ return
640
+ end
641
+
642
+ break if row_start.nil?
643
+
644
+ next_pos = row_start + row_start_len
645
+ break if next_pos >= buffer.bytesize
646
+
647
+ nb = buffer.getbyte(next_pos)
648
+ unless [32, 62, 9, 10, 13, 47].include?(nb)
649
+ pos = next_pos
650
+ next
651
+ end
652
+
653
+ tag_end = buffer.index(">", next_pos)
654
+ break if tag_end.nil?
655
+
656
+ if buffer.getbyte(tag_end - 1) == 47
657
+ pos = tag_end + 1
658
+ last_consumed_pos = pos
659
+ next
660
+ end
661
+
662
+ row_end = buffer.index(row_end_tag, tag_end + 1)
663
+ if row_end.nil?
664
+ raise ArgumentError, "Malformed XML: row size exceeds maximum buffer limit" if buffer.bytesize > 50 * 1024 * 1024
665
+
666
+ break
667
+ end
668
+
669
+ row_index = 0
670
+ has_custom_attrs = false
671
+ ri = row_start + row_start_len
672
+ while ri < tag_end
673
+ rb = buffer.getbyte(ri)
674
+ if rb == 114 && buffer.getbyte(ri + 1) == 61 && buffer.getbyte(ri + 2) == 34 # r="
675
+ ri += 3
676
+ while ri < tag_end
677
+ cb = buffer.getbyte(ri)
678
+ break unless cb.between?(48, 57)
679
+
680
+ row_index = (row_index * 10) + (cb - 48)
681
+ ri += 1
682
+ end
683
+ row_index -= 1
684
+ elsif [104, 99, 111].include?(rb)
685
+ has_custom_attrs = true
686
+ ri += 1
687
+ else
688
+ ri += 1
689
+ end
690
+ end
691
+
692
+ attrs = if has_custom_attrs
693
+ row_tag = buffer.byteslice(row_start, tag_end - row_start)
694
+ extract_row_attrs(row_tag)
695
+ else
696
+ EMPTY_HASH
697
+ end
698
+
699
+ cells_xml = buffer.byteslice(tag_end + 1, row_end - (tag_end + 1))
700
+ row_obj = StreamRow.fast_create(
701
+ row_index,
702
+ cells_xml,
703
+ 0,
704
+ cells_xml.bytesize,
705
+ shared_strings,
706
+ prefix,
707
+ attrs[:height],
708
+ attrs[:hidden] || false,
709
+ attrs[:custom_height] || false,
710
+ attrs[:outline_level]
711
+ )
712
+ block.call(row_obj)
713
+
714
+ pos = row_end + row_end_len
715
+ last_consumed_pos = pos
716
+ end
717
+
718
+ buffer.slice!(0...last_consumed_pos) if last_consumed_pos.positive?
719
+ end
720
+
721
+ if source.respond_to?(:each_chunk)
722
+ source.each_chunk(65_536, &push_chunk)
723
+ elsif source.respond_to?(:read)
724
+ while (chunk = source.read(65_536))
725
+ break if chunk.empty?
726
+
727
+ push_chunk.call(chunk)
728
+ end
729
+ elsif source.respond_to?(:call)
730
+ source.call(&push_chunk)
731
+ elsif source.respond_to?(:each)
732
+ source.each(&push_chunk)
733
+ else
734
+ raise ArgumentError, "Unsupported stream source: #{source.class}"
735
+ end
736
+ end
737
+
738
+ private_class_method :scan_rows_stream
739
+
577
740
  COL_LETTERS = (0...16_384).map do |index|
578
741
  result = +""
579
742
  i = index