xlsxrb 0.1.8 → 0.1.10

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/README.md +77 -222
  4. data/Rakefile +22 -3
  5. data/docs/DEVELOPMENT.md +11 -3
  6. data/docs/PEER_LIBRARIES.md +121 -0
  7. data/docs/QUALITY_ASSURANCE.md +3 -4
  8. data/docs/SPEC_SOURCES.md +7 -1
  9. data/docs/assets/benchmark_results.svg +144 -0
  10. data/docs/assets/lsp_autocompletion.png +0 -0
  11. data/docs/assets/playground_preview.png +0 -0
  12. data/docs/visual/VisualGallery.md +183 -0
  13. data/docs/wasm/ruby.wasm +0 -0
  14. data/lib/xlsxrb/chart_builder.rb +333 -0
  15. data/lib/xlsxrb/elements/row.rb +2 -2
  16. data/lib/xlsxrb/elements/workbook.rb +3 -3
  17. data/lib/xlsxrb/elements/worksheet.rb +4 -4
  18. data/lib/xlsxrb/ooxml/cfb.rb +490 -0
  19. data/lib/xlsxrb/ooxml/crypto/agile.rb +285 -0
  20. data/lib/xlsxrb/ooxml/crypto/standard.rb +152 -0
  21. data/lib/xlsxrb/ooxml/crypto.rb +60 -0
  22. data/lib/xlsxrb/ooxml/reader/listeners.rb +6450 -0
  23. data/lib/xlsxrb/ooxml/reader.rb +2 -6438
  24. data/lib/xlsxrb/ooxml/workbook_writer.rb +10 -10
  25. data/lib/xlsxrb/ooxml/worksheet_parser.rb +161 -116
  26. data/lib/xlsxrb/ooxml/worksheet_writer.rb +5 -1
  27. data/lib/xlsxrb/stream_row.rb +7 -5
  28. data/lib/xlsxrb/stream_sheet.rb +125 -0
  29. data/lib/xlsxrb/stream_writer.rb +1646 -0
  30. data/lib/xlsxrb/version.rb +1 -1
  31. data/lib/xlsxrb/workbook_builder.rb +309 -0
  32. data/lib/xlsxrb/worksheet_builder.rb +691 -0
  33. data/lib/xlsxrb.rb +350 -2989
  34. data/sig/generated/xlsxrb/chart_builder.rbs +269 -0
  35. data/sig/generated/xlsxrb/elements/worksheet.rbs +8 -5
  36. data/sig/generated/xlsxrb/ooxml/cfb.rbs +121 -0
  37. data/sig/generated/xlsxrb/ooxml/crypto/agile.rbs +46 -0
  38. data/sig/generated/xlsxrb/ooxml/crypto/standard.rbs +28 -0
  39. data/sig/generated/xlsxrb/ooxml/crypto.rbs +17 -0
  40. data/sig/generated/xlsxrb/ooxml/reader/listeners.rbs +1074 -0
  41. data/sig/generated/xlsxrb/ooxml/reader.rbs +0 -1067
  42. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +3 -3
  43. data/sig/generated/xlsxrb/stream_row.rbs +7 -4
  44. data/sig/generated/xlsxrb/stream_sheet.rbs +100 -0
  45. data/sig/generated/xlsxrb/stream_writer.rbs +954 -0
  46. data/sig/generated/xlsxrb/workbook_builder.rbs +146 -0
  47. data/sig/generated/xlsxrb/worksheet_builder.rbs +349 -0
  48. data/sig/generated/xlsxrb.rbs +131 -1569
  49. data/vendor/sdk_runner/Program.cs +3 -1
  50. metadata +25 -1
@@ -543,6 +543,16 @@ module Xlsxrb
543
543
  b.declaration
544
544
  b.open_tag("styleSheet", { xmlns: SSML_NS })
545
545
 
546
+ # Number formats
547
+ num_fmts = @styles&.dig(:num_fmts) || []
548
+ unless num_fmts.empty?
549
+ b.tag("numFmts", { count: num_fmts.size.to_s }) do |_|
550
+ num_fmts.each do |nf|
551
+ b.empty_tag("numFmt", { numFmtId: nf[:num_fmt_id].to_s, formatCode: nf[:format_code] }) if nf.is_a?(Hash)
552
+ end
553
+ end
554
+ end
555
+
546
556
  # Fonts
547
557
  fonts = @styles&.dig(:fonts) || []
548
558
  fonts = [{}] if fonts.empty? # At least one default font
@@ -614,16 +624,6 @@ module Xlsxrb
614
624
  b.empty_tag("xf", { numFmtId: "0", fontId: "0", fillId: "0", borderId: "0" })
615
625
  end
616
626
 
617
- # Number formats
618
- num_fmts = @styles&.dig(:num_fmts) || []
619
- unless num_fmts.empty?
620
- b.tag("numFmts", { count: num_fmts.size.to_s }) do |_|
621
- num_fmts.each do |nf|
622
- b.empty_tag("numFmt", { numFmtId: nf[:num_fmt_id].to_s, formatCode: nf[:format_code] }) if nf.is_a?(Hash)
623
- end
624
- end
625
- end
626
-
627
627
  # Cell XFs (formatting)
628
628
  xf_entries = @styles&.dig(:xf_entries) || []
629
629
  xf_count = [xf_entries.size, 1].max
@@ -129,30 +129,41 @@ module Xlsxrb
129
129
  def self.fast_scan_events(xml_src, shared_strings, part_name, &block)
130
130
  xml = xml_src.b # force ASCII-8BIT for O(1) byte indexing
131
131
 
132
- sd_start = xml.index("<sheetData")
132
+ sd_term = xml.index("sheetData")
133
+ return unless sd_term
134
+
135
+ sd_start = xml.rindex("<", sd_term)
133
136
  return unless sd_start
134
137
 
138
+ prefix = xml.byteslice(sd_start + 1, sd_term - (sd_start + 1))
139
+
135
140
  sd_open_end = xml.index(">", sd_start)
136
141
  return unless sd_open_end
137
142
 
138
143
  return if xml.getbyte(sd_open_end - 1) == 47 # self-closing <sheetData/>
139
144
 
140
- sd_end = xml.index("</sheetData>", sd_open_end)
145
+ sd_end_tag = "</#{prefix}sheetData>"
146
+ sd_end = xml.index(sd_end_tag, sd_open_end)
141
147
  return unless sd_end
142
148
 
149
+ row_start_pattern = "<#{prefix}row"
150
+ row_start_len = row_start_pattern.bytesize
151
+ row_end_tag = "</#{prefix}row>"
152
+ row_end_len = row_end_tag.bytesize
153
+
143
154
  pos = sd_open_end + 1
144
155
 
145
156
  while pos < sd_end
146
- row_start = xml.index("<row", pos)
157
+ row_start = xml.index(row_start_pattern, pos)
147
158
  break unless row_start && row_start < sd_end
148
159
 
149
- nb = xml.getbyte(row_start + 4)
160
+ nb = xml.getbyte(row_start + row_start_len)
150
161
  unless [32, 62, 9, 10, 13, 47].include?(nb)
151
- pos = row_start + 4
162
+ pos = row_start + row_start_len
152
163
  next
153
164
  end
154
165
 
155
- tag_end = xml.index(">", row_start + 4)
166
+ tag_end = xml.index(">", row_start + row_start_len)
156
167
  break unless tag_end
157
168
 
158
169
  if xml.getbyte(tag_end - 1) == 47
@@ -168,7 +179,7 @@ module Xlsxrb
168
179
 
169
180
  attrs = extract_row_attrs(row_tag)
170
181
 
171
- row_end = xml.index("</row>", tag_end + 1)
182
+ row_end = xml.index(row_end_tag, tag_end + 1)
172
183
  break unless row_end
173
184
 
174
185
  row_source = { part: part_name, row: row_index }
@@ -178,7 +189,7 @@ module Xlsxrb
178
189
  source: row_source
179
190
  ))
180
191
 
181
- fast_scan_cells_events(xml, tag_end + 1, row_end, shared_strings, row_source, &block)
192
+ fast_scan_cells_events(xml, tag_end + 1, row_end, shared_strings, row_source, prefix, &block)
182
193
 
183
194
  block.call(Event.new(
184
195
  type: :row_end,
@@ -186,7 +197,7 @@ module Xlsxrb
186
197
  source: row_source
187
198
  ))
188
199
 
189
- pos = row_end + 6
200
+ pos = row_end + row_end_len
190
201
  end
191
202
  end
192
203
 
@@ -236,20 +247,38 @@ module Xlsxrb
236
247
 
237
248
  private_class_method :tag_attr
238
249
 
239
- def self.fast_scan_cells_events(xml, from, to, shared_strings, row_source, &block)
250
+ def self.fast_scan_cells_events(xml, from, to, shared_strings, row_source, prefix = "", &block)
240
251
  pos = from
252
+ c_start_pattern = "<#{prefix}c"
253
+ c_start_len = c_start_pattern.bytesize
254
+ c_end_tag = "</#{prefix}c>"
255
+ c_end_len = c_end_tag.bytesize
256
+
257
+ v_tag_start_str = "<#{prefix}v>"
258
+ v_tag_start_len = v_tag_start_str.bytesize
259
+ v_tag_end_str = "</#{prefix}v>"
260
+ v_tag_end_len = v_tag_end_str.bytesize
261
+
262
+ is_tag_start_str = "<#{prefix}is>"
263
+ is_tag_start_len = is_tag_start_str.bytesize
264
+ is_tag_end_str = "</#{prefix}is>"
265
+ is_tag_end_len = is_tag_end_str.bytesize
266
+
267
+ f_tag_start_str = "<#{prefix}f"
268
+ f_tag_end_str = "</#{prefix}f>"
269
+ f_tag_end_len = f_tag_end_str.bytesize
241
270
 
242
271
  while pos < to
243
- c_start = xml.index("<c", pos)
272
+ c_start = xml.index(c_start_pattern, pos)
244
273
  break unless c_start && c_start < to
245
274
 
246
- nb = xml.getbyte(c_start + 2)
275
+ nb = xml.getbyte(c_start + c_start_len)
247
276
  unless [32, 62, 9, 10, 13, 47].include?(nb)
248
- pos = c_start + 2
277
+ pos = c_start + c_start_len
249
278
  next
250
279
  end
251
280
 
252
- c_tag_end = xml.index(">", c_start + 2)
281
+ c_tag_end = xml.index(">", c_start + c_start_len)
253
282
  break unless c_tag_end
254
283
 
255
284
  # Extract tag substring for bounded attribute search
@@ -272,7 +301,7 @@ module Xlsxrb
272
301
  next
273
302
  end
274
303
 
275
- c_end = xml.index("</c>", c_tag_end + 1)
304
+ c_end = xml.index(c_end_tag, c_tag_end + 1)
276
305
  break unless c_end
277
306
 
278
307
  # Parse cell content sequentially (bounded to c_end - avoid unbounded scans)
@@ -284,35 +313,27 @@ module Xlsxrb
284
313
  tag_pos = xml.index("<", cpos)
285
314
  break unless tag_pos && tag_pos < c_end
286
315
 
287
- tag_char = xml.getbyte(tag_pos + 1)
288
- case tag_char
289
- when 118 # 'v'
290
- if xml.getbyte(tag_pos + 2) == 62 # <v>
291
- v_val_start = tag_pos + 3
292
- v_end = xml.index("</v>", v_val_start)
293
- if v_end
294
- raw_value = xml.byteslice(v_val_start, v_end - v_val_start)
295
- value = resolve_fast_value(raw_value, type, shared_strings)
296
- cpos = v_end + 4
297
- else
298
- cpos = tag_pos + 3
299
- end
300
- elsif xml.getbyte(tag_pos + 2) == 47 && xml.getbyte(tag_pos + 3) == 62 # <v/>
301
- cpos = tag_pos + 4
316
+ if xml.byteslice(tag_pos, v_tag_start_len) == v_tag_start_str
317
+ v_val_start = tag_pos + v_tag_start_len
318
+ v_end = xml.index(v_tag_end_str, v_val_start)
319
+ if v_end
320
+ raw_value = xml.byteslice(v_val_start, v_end - v_val_start)
321
+ value = resolve_fast_value(raw_value, type, shared_strings)
322
+ cpos = v_end + v_tag_end_len
302
323
  else
303
- cpos = tag_pos + 2
324
+ cpos = tag_pos + v_tag_start_len
304
325
  end
305
- when 102 # 'f'
306
- f_tag_end = xml.index(">", tag_pos + 2)
326
+ elsif xml.byteslice(tag_pos, f_tag_start_str.bytesize) == f_tag_start_str
327
+ f_tag_end = xml.index(">", tag_pos + f_tag_start_str.bytesize)
307
328
  if f_tag_end && f_tag_end < c_end
308
329
  if xml.getbyte(f_tag_end - 1) == 47 # self-closing <f ... />
309
330
  cpos = f_tag_end + 1
310
331
  else
311
- f_end = xml.index("</f>", f_tag_end + 1)
332
+ f_end = xml.index(f_tag_end_str, f_tag_end + 1)
312
333
  if f_end && f_end <= c_end
313
334
  formula = xml.byteslice(f_tag_end + 1, f_end - f_tag_end - 1).force_encoding("UTF-8")
314
335
  formula = decode_xml_entities(formula) if formula.include?("&")
315
- cpos = f_end + 4
336
+ cpos = f_end + f_tag_end_len
316
337
  else
317
338
  cpos = f_tag_end + 1
318
339
  end
@@ -320,17 +341,13 @@ module Xlsxrb
320
341
  else
321
342
  cpos = tag_pos + 2
322
343
  end
323
- when 105 # 'i' - <is>
324
- if xml.byteslice(tag_pos, 4) == "<is>"
325
- is_end = xml.index("</is>", tag_pos + 4)
326
- if is_end && is_end <= c_end
327
- inline_str = extract_inline_text(xml, tag_pos + 4, is_end)
328
- cpos = is_end + 5
329
- else
330
- cpos = tag_pos + 4
331
- end
344
+ elsif xml.byteslice(tag_pos, is_tag_start_len) == is_tag_start_str
345
+ is_end = xml.index(is_tag_end_str, tag_pos + is_tag_start_len)
346
+ if is_end && is_end <= c_end
347
+ inline_str = extract_inline_text(xml, tag_pos + is_tag_start_len, is_end)
348
+ cpos = is_end + is_tag_end_len
332
349
  else
333
- cpos = tag_pos + 2
350
+ cpos = tag_pos + is_tag_start_len
334
351
  end
335
352
  else
336
353
  # Skip unknown tag
@@ -348,7 +365,7 @@ module Xlsxrb
348
365
  source: cell_source
349
366
  ))
350
367
 
351
- pos = c_end + 4
368
+ pos = c_end + c_end_len
352
369
  end
353
370
  end
354
371
 
@@ -358,21 +375,32 @@ module Xlsxrb
358
375
  result = +""
359
376
  pos = from
360
377
  while pos < to
361
- t_start = xml.index("<t", pos)
378
+ t_start = xml.index("<", pos)
362
379
  break unless t_start && t_start < to
363
380
 
364
- t_tag_end = xml.index(">", t_start)
365
- break unless t_tag_end
366
- next (pos = t_start + 2) if xml.getbyte(t_tag_end - 1) == 47
381
+ t_term = xml.index("t", t_start)
382
+ break unless t_term && t_term < to
383
+
384
+ t_tag_end = xml.index(">", t_term)
385
+ break unless t_tag_end && t_tag_end < to
386
+
387
+ if xml.getbyte(t_tag_end - 1) == 47 # <t ... />
388
+ pos = t_tag_end + 1
389
+ next
390
+ end
391
+
392
+ t_close = xml.index("</", t_tag_end + 1)
393
+ break unless t_close && t_close <= to
367
394
 
368
- t_end = xml.index("</t>", t_tag_end + 1)
395
+ t_end = xml.index(">", t_close + 2)
369
396
  break unless t_end && t_end <= to
370
397
 
371
- result << xml.byteslice(t_tag_end + 1, t_end - t_tag_end - 1)
372
- pos = t_end + 4
398
+ text_segment = xml.byteslice(t_tag_end + 1, t_close - t_tag_end - 1).force_encoding("UTF-8")
399
+ text_segment = decode_xml_entities(text_segment) if text_segment.include?("&")
400
+ result << text_segment
401
+
402
+ pos = t_end + 1
373
403
  end
374
- result.force_encoding("UTF-8")
375
- result = decode_xml_entities(result) if result.include?("&")
376
404
  result
377
405
  end
378
406
 
@@ -451,29 +479,40 @@ module Xlsxrb
451
479
  def self.fast_scan_rows_direct(xml_src, shared_strings, part_name, &block)
452
480
  xml = xml_src.b # force ASCII-8BIT for O(1) byte indexing
453
481
 
454
- sd_start = xml.index("<sheetData")
482
+ sd_term = xml.index("sheetData")
483
+ return unless sd_term
484
+
485
+ sd_start = xml.rindex("<", sd_term)
455
486
  return unless sd_start
456
487
 
488
+ prefix = xml.byteslice(sd_start + 1, sd_term - (sd_start + 1))
489
+
457
490
  sd_open_end = xml.index(">", sd_start)
458
491
  return unless sd_open_end
459
492
  return if xml.getbyte(sd_open_end - 1) == 47 # self-closing <sheetData/>
460
493
 
461
- sd_end = xml.index("</sheetData>", sd_open_end)
494
+ sd_end_tag = "</#{prefix}sheetData>"
495
+ sd_end = xml.index(sd_end_tag, sd_open_end)
462
496
  return unless sd_end
463
497
 
498
+ row_start_pattern = "<#{prefix}row"
499
+ row_start_len = row_start_pattern.bytesize
500
+ row_end_tag = "</#{prefix}row>"
501
+ row_end_len = row_end_tag.bytesize
502
+
464
503
  pos = sd_open_end + 1
465
504
 
466
505
  while pos < sd_end
467
- row_start = xml.index("<row", pos)
506
+ row_start = xml.index(row_start_pattern, pos)
468
507
  break unless row_start && row_start < sd_end
469
508
 
470
- nb = xml.getbyte(row_start + 4)
509
+ nb = xml.getbyte(row_start + row_start_len)
471
510
  unless [32, 62, 9, 10, 13, 47].include?(nb)
472
- pos = row_start + 4
511
+ pos = row_start + row_start_len
473
512
  next
474
513
  end
475
514
 
476
- tag_end = xml.index(">", row_start + 4)
515
+ tag_end = xml.index(">", row_start + row_start_len)
477
516
  break unless tag_end
478
517
 
479
518
  if xml.getbyte(tag_end - 1) == 47
@@ -483,7 +522,7 @@ module Xlsxrb
483
522
 
484
523
  row_index = 0
485
524
  has_custom_attrs = false
486
- ri = row_start + 4
525
+ ri = row_start + row_start_len
487
526
  while ri < tag_end
488
527
  rb = xml.getbyte(ri)
489
528
  if rb == 114 && xml.getbyte(ri + 1) == 61 && xml.getbyte(ri + 2) == 34 # r="
@@ -512,7 +551,7 @@ module Xlsxrb
512
551
  EMPTY_HASH
513
552
  end
514
553
 
515
- row_end = xml.index("</row>", tag_end + 1)
554
+ row_end = xml.index(row_end_tag, tag_end + 1)
516
555
  break unless row_end
517
556
 
518
557
  { part: part_name, row: row_index }
@@ -522,6 +561,7 @@ module Xlsxrb
522
561
  from: tag_end + 1,
523
562
  to: row_end,
524
563
  shared_strings: shared_strings,
564
+ prefix: prefix,
525
565
  height: attrs[:height],
526
566
  hidden: attrs[:hidden] || false,
527
567
  custom_height: attrs[:custom_height] || false,
@@ -529,33 +569,51 @@ module Xlsxrb
529
569
  )
530
570
  block.call(row_obj)
531
571
 
532
- pos = row_end + 6
572
+ pos = row_end + row_end_len
533
573
  end
534
574
  end
535
575
 
536
576
  private_class_method :fast_scan_rows_direct
537
577
 
538
- def self.fast_parse_cells_direct(xml, from, to, shared_strings, row_source)
578
+ def self.fast_parse_cells_direct(xml, from, to, shared_strings, row_source, prefix = "")
539
579
  cells = []
540
- fast_scan_cells_direct(xml, from, to, shared_strings, row_source) { |c| cells << c }
580
+ fast_scan_cells_direct(xml, from, to, shared_strings, row_source, prefix) { |c| cells << c }
541
581
  cells
542
582
  end
543
583
 
544
- def self.fast_scan_cells_direct(xml, from, to, shared_strings, row_source, &block)
584
+ def self.fast_scan_cells_direct(xml, from, to, shared_strings, row_source, prefix = "", &block)
545
585
  pos = from
546
586
  cell_count = 0
587
+ c_start_pattern = "<#{prefix}c"
588
+ c_start_len = c_start_pattern.bytesize
589
+ c_end_tag = "</#{prefix}c>"
590
+ c_end_len = c_end_tag.bytesize
591
+
592
+ v_tag_start_str = "<#{prefix}v>"
593
+ v_tag_start_len = v_tag_start_str.bytesize
594
+ v_tag_end_str = "</#{prefix}v>"
595
+ v_tag_end_len = v_tag_end_str.bytesize
596
+
597
+ is_tag_start_str = "<#{prefix}is>"
598
+ is_tag_start_len = is_tag_start_str.bytesize
599
+ is_tag_end_str = "</#{prefix}is>"
600
+ is_tag_end_len = is_tag_end_str.bytesize
601
+
602
+ f_tag_start_str = "<#{prefix}f"
603
+ f_tag_end_str = "</#{prefix}f>"
604
+ f_tag_end_len = f_tag_end_str.bytesize
547
605
 
548
606
  while pos < to
549
- c_start = xml.index("<c", pos)
607
+ c_start = xml.index(c_start_pattern, pos)
550
608
  break unless c_start && c_start < to
551
609
 
552
- nb = xml.getbyte(c_start + 2)
610
+ nb = xml.getbyte(c_start + c_start_len)
553
611
  unless [32, 62, 9, 10, 13, 47].include?(nb)
554
- pos = c_start + 2
612
+ pos = c_start + c_start_len
555
613
  next
556
614
  end
557
615
 
558
- c_tag_end = xml.index(">", c_start + 2)
616
+ c_tag_end = xml.index(">", c_start + c_start_len)
559
617
  break unless c_tag_end
560
618
 
561
619
  # Zero-allocation attribute scan directly within the tag
@@ -564,7 +622,7 @@ module Xlsxrb
564
622
  col_idx = nil
565
623
  row_idx = nil
566
624
 
567
- ai = c_start + 2
625
+ ai = c_start + c_start_len
568
626
  while ai < c_tag_end
569
627
  b = xml.getbyte(ai)
570
628
  if [32, 9, 10, 13].include?(b)
@@ -646,7 +704,7 @@ module Xlsxrb
646
704
  next
647
705
  end
648
706
 
649
- c_end = xml.index("</c>", c_tag_end + 1)
707
+ c_end = xml.index(c_end_tag, c_tag_end + 1)
650
708
  break unless c_end
651
709
 
652
710
  # Parse cell content sequentially (bounded to c_end - avoid unbounded scans)
@@ -658,48 +716,40 @@ module Xlsxrb
658
716
  tag_pos = xml.index("<", cpos)
659
717
  break unless tag_pos && tag_pos < c_end
660
718
 
661
- tag_char = xml.getbyte(tag_pos + 1)
662
- case tag_char
663
- when 118 # 'v'
664
- if xml.getbyte(tag_pos + 2) == 62 # <v>
665
- v_val_start = tag_pos + 3
666
- v_end = xml.index("</v>", v_val_start)
667
- if v_end
668
- if type == "s"
669
- # Zero-allocation integer parse for SST index
670
- s_idx = 0
671
- v_i = v_val_start
672
- while v_i < v_end
673
- s_idx = (s_idx * 10) + (xml.getbyte(v_i) - 48)
674
- v_i += 1
675
- end
676
- value = shared_strings[s_idx] || ""
677
- elsif type == "b"
678
- value = xml.getbyte(v_val_start) == 49 # '1'
679
- else
680
- raw_value = xml.byteslice(v_val_start, v_end - v_val_start)
681
- value = resolve_fast_value(raw_value, type, shared_strings)
719
+ if xml.byteslice(tag_pos, v_tag_start_len) == v_tag_start_str
720
+ v_val_start = tag_pos + v_tag_start_len
721
+ v_end = xml.index(v_tag_end_str, v_val_start)
722
+ if v_end
723
+ if type == "s"
724
+ # Zero-allocation integer parse for SST index
725
+ s_idx = 0
726
+ v_i = v_val_start
727
+ while v_i < v_end
728
+ s_idx = (s_idx * 10) + (xml.getbyte(v_i) - 48)
729
+ v_i += 1
682
730
  end
683
- cpos = v_end + 4
731
+ value = shared_strings[s_idx] || ""
732
+ elsif type == "b"
733
+ value = xml.getbyte(v_val_start) == 49 # '1'
684
734
  else
685
- cpos = tag_pos + 3
735
+ raw_value = xml.byteslice(v_val_start, v_end - v_val_start)
736
+ value = resolve_fast_value(raw_value, type, shared_strings)
686
737
  end
687
- elsif xml.getbyte(tag_pos + 2) == 47 && xml.getbyte(tag_pos + 3) == 62 # <v/>
688
- cpos = tag_pos + 4
738
+ cpos = v_end + v_tag_end_len
689
739
  else
690
- cpos = tag_pos + 2
740
+ cpos = tag_pos + v_tag_start_len
691
741
  end
692
- when 102 # 'f'
693
- f_tag_end = xml.index(">", tag_pos + 2)
742
+ elsif xml.byteslice(tag_pos, f_tag_start_str.bytesize) == f_tag_start_str
743
+ f_tag_end = xml.index(">", tag_pos + f_tag_start_str.bytesize)
694
744
  if f_tag_end && f_tag_end < c_end
695
745
  if xml.getbyte(f_tag_end - 1) == 47 # self-closing <f ... />
696
746
  cpos = f_tag_end + 1
697
747
  else
698
- f_end = xml.index("</f>", f_tag_end + 1)
748
+ f_end = xml.index(f_tag_end_str, f_tag_end + 1)
699
749
  if f_end && f_end <= c_end
700
750
  formula = xml.byteslice(f_tag_end + 1, f_end - f_tag_end - 1).force_encoding("UTF-8")
701
751
  formula = decode_xml_entities(formula) if formula.include?("&")
702
- cpos = f_end + 4
752
+ cpos = f_end + f_tag_end_len
703
753
  else
704
754
  cpos = f_tag_end + 1
705
755
  end
@@ -707,17 +757,13 @@ module Xlsxrb
707
757
  else
708
758
  cpos = tag_pos + 2
709
759
  end
710
- when 105 # 'i' - <is>
711
- if xml.byteslice(tag_pos, 4) == "<is>"
712
- is_end = xml.index("</is>", tag_pos + 4)
713
- if is_end && is_end <= c_end
714
- inline_str = extract_inline_text(xml, tag_pos + 4, is_end)
715
- cpos = is_end + 5
716
- else
717
- cpos = tag_pos + 4
718
- end
760
+ elsif xml.byteslice(tag_pos, is_tag_start_len) == is_tag_start_str
761
+ is_end = xml.index(is_tag_end_str, tag_pos + is_tag_start_len)
762
+ if is_end && is_end <= c_end
763
+ inline_str = extract_inline_text(xml, tag_pos + is_tag_start_len, is_end)
764
+ cpos = is_end + is_tag_end_len
719
765
  else
720
- cpos = tag_pos + 2
766
+ cpos = tag_pos + is_tag_start_len
721
767
  end
722
768
  else
723
769
  # Skip unknown tag
@@ -726,11 +772,10 @@ module Xlsxrb
726
772
  end
727
773
  end
728
774
 
729
- val_to_use = inline_str || value
730
775
  cell_obj = Elements::Cell.new(
731
776
  row_index: row_idx || row_source[:row],
732
777
  column_index: col_idx || cell_count,
733
- value: val_to_use,
778
+ value: inline_str || value,
734
779
  formula: formula,
735
780
  style_index: style_index,
736
781
  errors: Elements::EMPTY_ERRORS
@@ -738,7 +783,7 @@ module Xlsxrb
738
783
  cell_count += 1
739
784
  block.call(cell_obj)
740
785
 
741
- pos = c_end + 4
786
+ pos = c_end + c_end_len
742
787
  end
743
788
  end
744
789
 
@@ -131,9 +131,10 @@ module Xlsxrb
131
131
  if formula.is_a?(Xlsxrb::Elements::Formula)
132
132
  formula_expr = formula.expression
133
133
  formula_ca = formula.calculate_always
134
- xml_val = formula.cached_value || nil
134
+ xml_val = formula.cached_value || value || nil
135
135
  else
136
136
  formula_expr = formula
137
+ xml_val = value || nil
137
138
  end
138
139
  formula_expr = formula_expr[1..] if formula_expr.start_with?("=")
139
140
  end
@@ -167,6 +168,9 @@ module Xlsxrb
167
168
  xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
168
169
  when BigDecimal
169
170
  xml_val = value.to_s("F")
171
+ when Xlsxrb::Elements::CellError
172
+ xml_val = value.code
173
+ type = "e"
170
174
  end
171
175
  end
172
176
 
@@ -26,18 +26,20 @@ module Xlsxrb
26
26
  # @param from [Integer] Byte offset where cells start.
27
27
  # @param to [Integer] Byte offset where cells end.
28
28
  # @param shared_strings [Array<String>] Shared strings table.
29
+ # @param prefix [String] XML namespace prefix (e.g. "x:" or "").
29
30
  # @param height [Float, Integer, nil] Row height in points.
30
31
  # @param hidden [Boolean] Whether the row is hidden.
31
32
  # @param custom_height [Boolean] Whether custom height is set.
32
33
  # @param outline_level [Integer, nil] Grouping/outline level.
33
- #: (index: Integer, xml_bytes: String, from: Integer, to: Integer, shared_strings: Array[String], ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
34
- def initialize(index:, xml_bytes:, from:, to:, shared_strings:, height: nil, hidden: false,
34
+ #: (index: Integer, xml_bytes: String, from: Integer, to: Integer, shared_strings: Array[String], ?prefix: String, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
35
+ def initialize(index:, xml_bytes:, from:, to:, shared_strings:, prefix: "", height: nil, hidden: false,
35
36
  custom_height: false, outline_level: nil)
36
37
  @index = index
37
38
  @xml = xml_bytes
38
39
  @from = from
39
40
  @to = to
40
41
  @shared_strings = shared_strings
42
+ @prefix = prefix
41
43
  @height = height
42
44
  @hidden = hidden
43
45
  @custom_height = custom_height
@@ -52,14 +54,14 @@ module Xlsxrb
52
54
  # @return [Enumerator, void]
53
55
  # @api public
54
56
  #: () { (Elements::Cell) -> void } -> void
55
- #: | () -> Enumerator[Elements::Cell, void]
57
+ #: () -> Enumerator[Elements::Cell, void]
56
58
  def each_cell(&)
57
59
  return enum_for(:each_cell) unless block_given?
58
60
 
59
61
  if @cells
60
62
  @cells.each(&)
61
63
  else
62
- Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, { row: @index }, &)
64
+ Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, { row: @index }, @prefix, &)
63
65
  end
64
66
  end
65
67
 
@@ -70,7 +72,7 @@ module Xlsxrb
70
72
  # @return [Enumerator, void]
71
73
  # @api public
72
74
  #: () { (Elements::Cell) -> void } -> void
73
- #: | () -> Enumerator[Elements::Cell, void]
75
+ #: () -> Enumerator[Elements::Cell, void]
74
76
  def each(&)
75
77
  each_cell(&)
76
78
  end