xlsxrb 0.1.2 → 0.1.3

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.
@@ -25,27 +25,9 @@ module Xlsxrb
25
25
  # Streaming parse: yields one raw row hash at a time.
26
26
  def self.each_row(xml_string, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block)
27
27
  return enum_for(:each_row, xml_string, shared_strings: shared_strings, part_name: part_name) unless block
28
+ return if xml_string.nil? || xml_string.empty?
28
29
 
29
- current_row = nil
30
- each_event(xml_string, shared_strings: shared_strings, part_name: part_name) do |event|
31
- case event.type
32
- when :row_start
33
- index, attrs = event.args
34
- current_row = { index: index, cells: [], attrs: attrs, unmapped: EMPTY_ARRAY, source: event.source }
35
- when :cell
36
- ref, type, style_index, value, formula = event.args
37
- if current_row
38
- cell = { ref: ref, type: type, style_index: style_index, value: value, source: event.source }
39
- cell[:formula] = formula if formula
40
- current_row[:cells] << cell
41
- end
42
- when :row_end
43
- if current_row
44
- block.call(current_row)
45
- current_row = nil
46
- end
47
- end
48
- end
30
+ fast_scan_rows_direct(xml_string, shared_strings, part_name, &block)
49
31
  end
50
32
 
51
33
  # Parses column definitions (<cols>) from a worksheet.
@@ -187,7 +169,7 @@ module Xlsxrb
187
169
  source: row_source
188
170
  ))
189
171
 
190
- fast_scan_cells_events(xml, tag_end + 1, row_end, shared_strings, part_name, row_index, &block)
172
+ fast_scan_cells_events(xml, tag_end + 1, row_end, shared_strings, row_source, &block)
191
173
 
192
174
  block.call(Event.new(
193
175
  type: :row_end,
@@ -245,7 +227,7 @@ module Xlsxrb
245
227
 
246
228
  private_class_method :tag_attr
247
229
 
248
- def self.fast_scan_cells_events(xml, from, to, shared_strings, part_name, row_index, &block)
230
+ def self.fast_scan_cells_events(xml, from, to, shared_strings, row_source, &block)
249
231
  pos = from
250
232
 
251
233
  while pos < to
@@ -270,10 +252,12 @@ module Xlsxrb
270
252
 
271
253
  # Self-closing <c ... />
272
254
  if xml.getbyte(c_tag_end - 1) == 47
255
+ cell_source = row_source.dup
256
+ cell_source[:cell] = ref
273
257
  block.call(Event.new(
274
258
  type: :cell,
275
259
  args: [ref, type, style_index, nil, nil],
276
- source: { part: part_name, row: row_index, cell: ref }
260
+ source: cell_source
277
261
  ))
278
262
  pos = c_tag_end + 1
279
263
  next
@@ -347,10 +331,12 @@ module Xlsxrb
347
331
  end
348
332
 
349
333
  val_to_use = inline_str || value
334
+ cell_source = row_source.dup
335
+ cell_source[:cell] = ref
350
336
  block.call(Event.new(
351
337
  type: :cell,
352
338
  args: [ref, type, style_index, val_to_use, formula],
353
- source: { part: part_name, row: row_index, cell: ref }
339
+ source: cell_source
354
340
  ))
355
341
 
356
342
  pos = c_end + 4
@@ -450,6 +436,174 @@ module Xlsxrb
450
436
 
451
437
  def characters(_text); end
452
438
  end
439
+
440
+ def self.fast_scan_rows_direct(xml_src, shared_strings, part_name, &block)
441
+ xml = xml_src.b # force ASCII-8BIT for O(1) byte indexing
442
+
443
+ sd_start = xml.index("<sheetData")
444
+ return unless sd_start
445
+
446
+ sd_open_end = xml.index(">", sd_start)
447
+ return unless sd_open_end
448
+ return if xml.getbyte(sd_open_end - 1) == 47 # self-closing <sheetData/>
449
+
450
+ sd_end = xml.index("</sheetData>", sd_open_end)
451
+ return unless sd_end
452
+
453
+ pos = sd_open_end + 1
454
+
455
+ while pos < sd_end
456
+ row_start = xml.index("<row", pos)
457
+ break unless row_start && row_start < sd_end
458
+
459
+ nb = xml.getbyte(row_start + 4)
460
+ unless [32, 62, 9, 10, 13, 47].include?(nb)
461
+ pos = row_start + 4
462
+ next
463
+ end
464
+
465
+ tag_end = xml.index(">", row_start + 4)
466
+ break unless tag_end
467
+
468
+ if xml.getbyte(tag_end - 1) == 47
469
+ pos = tag_end + 1
470
+ next
471
+ end
472
+
473
+ row_tag = xml.byteslice(row_start, tag_end - row_start)
474
+ row_index = 0
475
+ r_val = tag_attr(row_tag, ' r="')
476
+ row_index = r_val.to_i - 1 if r_val
477
+
478
+ attrs = extract_row_attrs(row_tag)
479
+ row_end = xml.index("</row>", tag_end + 1)
480
+ break unless row_end
481
+
482
+ row_source = { part: part_name, row: row_index }
483
+ cells = fast_parse_cells_direct(xml, tag_end + 1, row_end, shared_strings, row_source)
484
+
485
+ block.call({ index: row_index, cells: cells, attrs: attrs, unmapped: EMPTY_ARRAY, source: row_source })
486
+
487
+ pos = row_end + 6
488
+ end
489
+ end
490
+
491
+ private_class_method :fast_scan_rows_direct
492
+
493
+ def self.fast_parse_cells_direct(xml, from, to, shared_strings, row_source)
494
+ cells = []
495
+ pos = from
496
+
497
+ while pos < to
498
+ c_start = xml.index("<c", pos)
499
+ break unless c_start && c_start < to
500
+
501
+ nb = xml.getbyte(c_start + 2)
502
+ unless [32, 62, 9, 10, 13, 47].include?(nb)
503
+ pos = c_start + 2
504
+ next
505
+ end
506
+
507
+ c_tag_end = xml.index(">", c_start + 2)
508
+ break unless c_tag_end
509
+
510
+ # Extract tag substring for bounded attribute search
511
+ c_tag = xml.byteslice(c_start, c_tag_end - c_start)
512
+ ref = tag_attr(c_tag, ' r="')
513
+ type = tag_attr(c_tag, ' t="')
514
+ style_str = tag_attr(c_tag, ' s="')
515
+ style_index = style_str&.to_i
516
+
517
+ # Self-closing <c ... />
518
+ if xml.getbyte(c_tag_end - 1) == 47
519
+ cell_source = row_source.dup
520
+ cell_source[:cell] = ref
521
+ cells << { ref: ref, type: type, style_index: style_index, value: nil, source: cell_source }
522
+ pos = c_tag_end + 1
523
+ next
524
+ end
525
+
526
+ c_end = xml.index("</c>", c_tag_end + 1)
527
+ break unless c_end
528
+
529
+ # Parse cell content sequentially (bounded to c_end - avoid unbounded scans)
530
+ value = nil
531
+ formula = nil
532
+ inline_str = nil
533
+ cpos = c_tag_end + 1
534
+ while cpos < c_end
535
+ tag_pos = xml.index("<", cpos)
536
+ break unless tag_pos && tag_pos < c_end
537
+
538
+ tag_char = xml.getbyte(tag_pos + 1)
539
+ case tag_char
540
+ when 118 # 'v'
541
+ if xml.getbyte(tag_pos + 2) == 62 # <v>
542
+ v_val_start = tag_pos + 3
543
+ v_end = xml.index("</v>", v_val_start)
544
+ if v_end
545
+ raw_value = xml.byteslice(v_val_start, v_end - v_val_start)
546
+ value = resolve_fast_value(raw_value, type, shared_strings)
547
+ cpos = v_end + 4
548
+ else
549
+ cpos = tag_pos + 3
550
+ end
551
+ elsif xml.getbyte(tag_pos + 2) == 47 && xml.getbyte(tag_pos + 3) == 62 # <v/>
552
+ cpos = tag_pos + 4
553
+ else
554
+ cpos = tag_pos + 2
555
+ end
556
+ when 102 # 'f'
557
+ f_tag_end = xml.index(">", tag_pos + 2)
558
+ if f_tag_end && f_tag_end < c_end
559
+ if xml.getbyte(f_tag_end - 1) == 47 # self-closing <f ... />
560
+ cpos = f_tag_end + 1
561
+ else
562
+ f_end = xml.index("</f>", f_tag_end + 1)
563
+ if f_end && f_end <= c_end
564
+ formula = xml.byteslice(f_tag_end + 1, f_end - f_tag_end - 1).force_encoding("UTF-8")
565
+ formula = decode_xml_entities(formula) if formula.include?("&")
566
+ cpos = f_end + 4
567
+ else
568
+ cpos = f_tag_end + 1
569
+ end
570
+ end
571
+ else
572
+ cpos = tag_pos + 2
573
+ end
574
+ when 105 # 'i' - <is>
575
+ if xml.byteslice(tag_pos, 4) == "<is>"
576
+ is_end = xml.index("</is>", tag_pos + 4)
577
+ if is_end && is_end <= c_end
578
+ inline_str = extract_inline_text(xml, tag_pos + 4, is_end)
579
+ cpos = is_end + 5
580
+ else
581
+ cpos = tag_pos + 4
582
+ end
583
+ else
584
+ cpos = tag_pos + 2
585
+ end
586
+ else
587
+ # Skip unknown tag
588
+ close = xml.index(">", tag_pos + 1)
589
+ cpos = close ? close + 1 : c_end
590
+ end
591
+ end
592
+
593
+ val_to_use = inline_str || value
594
+ cell_source = row_source.dup
595
+ cell_source[:cell] = ref
596
+ cell = { ref: ref, type: type, style_index: style_index, value: val_to_use, source: cell_source }
597
+ cell[:formula] = formula if formula
598
+ cells << cell
599
+
600
+ pos = c_end + 4
601
+ end
602
+
603
+ cells
604
+ end
605
+
606
+ private_class_method :fast_parse_cells_direct
453
607
  end
454
608
  end
455
609
  end
data/lib/xlsxrb/ooxml.rb CHANGED
@@ -17,6 +17,6 @@ module Xlsxrb
17
17
  # Handles ZIP extraction, SAX XML parsing, and XML generation
18
18
  # in strict accordance with ECMA-376.
19
19
  module Ooxml
20
- Event = Data.define(:type, :args, :source)
20
+ Event = Struct.new(:type, :args, :source)
21
21
  end
22
22
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xlsxrb
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xlsxrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - niku
@@ -82,10 +82,15 @@ files:
82
82
  - Rakefile
83
83
  - benchmark.rb
84
84
  - docs/ARCHITECTURE.md
85
+ - docs/DEVELOPMENT.md
85
86
  - docs/SPEC_SOURCES.md
87
+ - docs/coi-serviceworker.js
88
+ - docs/office_thread.js
89
+ - docs/preview.html
86
90
  - docs/visual/VisualGallery.md
87
91
  - docs/wasm/wasm_doc_helper.css
88
92
  - docs/wasm/wasm_doc_helper.js
93
+ - docs/zeta.js
89
94
  - lib/xlsxrb.rb
90
95
  - lib/xlsxrb/elements.rb
91
96
  - lib/xlsxrb/elements/cell.rb