Class: Xlsxrb::Ooxml::WorksheetParser

Inherits:
Object
  • Object
show all
Defined in:
lib/xlsxrb/ooxml/worksheet_parser.rb,
sig/generated/xlsxrb/ooxml/worksheet_parser.rbs

Overview

Streaming parser for xl/worksheets/sheetN.xml. Uses a fast string-scanning approach for row/cell extraction, falling back to REXML SAX only for column definitions and unmapped data.

Defined Under Namespace

Classes: ColumnsListener

Constant Summary collapse

EMPTY_ARRAY =

Returns:

  • (Object)
[].freeze
EMPTY_HASH =

Returns:

  • (Object)
{}.freeze
XML_ENTITIES =

Returns:

  • (Object)
{ "&amp;" => "&", "&lt;" => "<", "&gt;" => ">", "&quot;" => '"', "&apos;" => "'" }.freeze
COL_LETTERS =

Returns:

  • (Object)
(0...16_384).map do |index|
  result = +""
  i = index
  loop do
    result.prepend(("A".ord + (i % 26)).chr)
    i = (i / 26) - 1
    break if i.negative?
  end
  result.freeze
end.freeze
COL_MAP =

Returns:

  • (Object)
(0...16_384).to_h do |idx|
  [COL_LETTERS[idx], idx]
end.freeze
CELL_FAST_RE =

Returns:

  • (::Regexp)
%r{<(?:[a-zA-Z0-9_]+:)?c\s+r="([A-Za-z0-9]+)"(?:[^>]*?s="(\d+)")?(?:[^>]*?t="([a-zA-Z]+)")?[^>]*>(?:<(?:[a-zA-Z0-9_]+:)?f[^>]*>([^<]*)</(?:[a-zA-Z0-9_]+:)?f>)?(?:<(?:[a-zA-Z0-9_]+:)?v>([^<]*)</(?:[a-zA-Z0-9_]+:)?v>)?(?:<(?:[a-zA-Z0-9_]+:)?is>(.*?)</(?:[a-zA-Z0-9_]+:)?is>)?</(?:[a-zA-Z0-9_]+:)?c>|<(?:[a-zA-Z0-9_]+:)?c\s+r="([A-Za-z0-9]+)"(?:[^>]*?s="(\d+)")?[^>]*/>}
CELL_GENERIC_RE =

Returns:

  • (::Regexp)
%r{<(?:[a-zA-Z0-9_]+:)?c\b([^>]*?)(?:>(?:<(?:[a-zA-Z0-9_]+:)?f[^>]*>([^<]*)</(?:[a-zA-Z0-9_]+:)?f>)?(?:<(?:[a-zA-Z0-9_]+:)?v>([^<]*)</(?:[a-zA-Z0-9_]+:)?v>)?(?:<(?:[a-zA-Z0-9_]+:)?is>(.*?)</(?:[a-zA-Z0-9_]+:)?is>)?</(?:[a-zA-Z0-9_]+:)?c>|/>)}m
ATTR_R =

Returns:

  • (::Regexp)
/r="([A-Za-z0-9]+)"/
ATTR_T =

Returns:

  • (::Regexp)
/t="([a-zA-Z]+)"/
ATTR_S =

Returns:

  • (::Regexp)
/s="(\d+)"/

Class Method Summary collapse

Class Method Details

.each_event(xml_string, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block) ⇒ void

This method returns an undefined value.

Yields Event objects for columns, rows, cells, and hyperlinks.

Parameters:

  • xml_string (Object)
  • shared_strings: (Object) (defaults to: [])
  • part_name: (Object) (defaults to: "xl/worksheets/sheet1.xml")


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/xlsxrb/ooxml/worksheet_parser.rb', line 57

def self.each_event(xml_string, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block)
  return enum_for(:each_event, xml_string, shared_strings: shared_strings, part_name: part_name) unless block
  return if xml_string.nil? || xml_string.empty?

  # 1. Parse and yield column events
  if xml_string.include?("<cols")
    listener = ColumnsListener.new
    XmlParser.parse(xml_string, listener)
    listener.columns.each do |col|
      block.call(Event.new(
                   type: :column,
                   args: [col[:min], col[:max], col[:width], col[:hidden], col[:custom_width], col[:outline_level]],
                   source: { part: part_name }
                 ))
    end
  end

  # 2. Parse and yield row/cell events
  fast_scan_events(xml_string, shared_strings, part_name, &block)

  # 3. Parse and yield hyperlink events
  return unless xml_string.include?("hyperlink")

  xml = xml_string.b
  hpos_term = xml.index("hyperlinks")
  return unless hpos_term

  hpos = xml.rindex("<", hpos_term)
  return unless hpos

  h_end_term = xml.index("/hyperlinks", hpos)
  h_end = h_end_term ? xml.index(">", h_end_term) : xml.size
  h_end ||= xml.size

  pos = hpos
  while pos < h_end
    hl_start_term = xml.index("hyperlink", pos)
    break unless hl_start_term && hl_start_term < h_end

    hl_start = xml.rindex("<", hl_start_term)
    break unless hl_start && hl_start < h_end

    hl_tag_end = xml.index(">", hl_start + 1)
    break unless hl_tag_end

    hl_tag = xml.byteslice(hl_start, hl_tag_end - hl_start)
    ref = tag_attr(hl_tag, ' ref="')
    rid = tag_attr(hl_tag, ' r:id="') || tag_attr(hl_tag, ' id="')
    display = tag_attr(hl_tag, ' display="')
    tooltip = tag_attr(hl_tag, ' tooltip="')
    location = tag_attr(hl_tag, ' location="')

    if ref
      block.call(Event.new(
                   type: :hyperlink,
                   args: [ref, rid, display, tooltip, location],
                   source: { part: part_name, cell: ref }
                 ))
    end
    pos = hl_tag_end + 1
  end
end

.each_row(xml_string, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block) ⇒ void

This method returns an undefined value.

Streaming parse: yields one raw row hash at a time.

Parameters:

  • xml_string (Object)
  • shared_strings: (Object) (defaults to: [])
  • part_name: (Object) (defaults to: "xl/worksheets/sheet1.xml")


28
29
30
31
32
33
# File 'lib/xlsxrb/ooxml/worksheet_parser.rb', line 28

def self.each_row(xml_string, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block)
  return enum_for(:each_row, xml_string, shared_strings: shared_strings, part_name: part_name) unless block
  return if xml_string.nil? || xml_string.empty?

  fast_scan_rows_direct(xml_string, shared_strings, part_name, &block)
end

.fast_scan_cells_direct(xml, from, to, shared_strings, row_source, _prefix = "", &block) ⇒ void

This method returns an undefined value.

Parameters:

  • xml (Object)
  • from (Object)
  • to (Object)
  • shared_strings (Object)
  • row_source (Object)
  • _prefix (Object) (defaults to: "")


604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
# File 'lib/xlsxrb/ooxml/worksheet_parser.rb', line 604

def self.fast_scan_cells_direct(xml, from, to, shared_strings, row_source, _prefix = "", &block)
  chunk = xml.byteslice(from, to - from)
  row_idx = row_source.is_a?(Hash) ? row_source[:row] : row_source
  col_idx = 0
  matched_any = false

  chunk.scan(CELL_FAST_RE) do |r, s, t, f, v, is, self_r, self_s|
    matched_any = true
    ref = r || self_r
    s_val = s || self_s
    style_idx = s_val&.to_i
    c_idx = if ref
              expected_letter = COL_LETTERS[col_idx]
              if expected_letter && ref.start_with?(expected_letter)
                col_idx
              else
                c_len = 0
                c_len += 1 while (b = ref.getbyte(c_len)) && (b.between?(65, 90) || b.between?(97, 122))
                col_letters = ref.byteslice(0, c_len).upcase
                COL_MAP[col_letters] || col_idx
              end
            else
              col_idx
            end
    col_idx = c_idx + 1

    val = if t == "s"
            shared_strings[v.to_i] || ""
          elsif t == "b"
            v == "1"
          elsif %w[inlineStr str e].include?(t)
            if is
              extract_inline_text(is, 0, is.bytesize)
            elsif v
              v.include?("&") ? decode_xml_entities(v) : v
            else
              ""
            end
          elsif v
            v.include?(".") ? v.to_f : v.to_i
          end

    formula_expr = f&.include?("&") ? decode_xml_entities(f) : f
    cell = Elements::Cell.fast_create(row_idx, c_idx, val, style_idx, formula_expr)
    block.call(cell)
  end

  return if matched_any || (!chunk.include?("<c") && !chunk.include?(":c"))

  # Fallback for non-standard attribute ordering
  chunk.scan(CELL_GENERIC_RE) do |attrs, f, v, is|
    r = attrs[ATTR_R, 1]
    t = attrs[ATTR_T, 1]
    s = attrs[ATTR_S, 1]

    c_idx = if r
              expected_letter = COL_LETTERS[col_idx]
              if expected_letter && r.start_with?(expected_letter)
                col_idx
              else
                c_len = 0
                c_len += 1 while (b = r.getbyte(c_len)) && (b.between?(65, 90) || b.between?(97, 122))
                col_letters = r.byteslice(0, c_len).upcase
                COL_MAP[col_letters] || col_idx
              end
            else
              col_idx
            end
    col_idx = c_idx + 1

    val = if t == "s"
            shared_strings[v.to_i] || ""
          elsif t == "b"
            v == "1"
          elsif %w[inlineStr str e].include?(t)
            if is
              extract_inline_text(is, 0, is.bytesize)
            elsif v
              v.include?("&") ? decode_xml_entities(v) : v
            else
              ""
            end
          elsif v
            v.include?(".") ? v.to_f : v.to_i
          end

    style_idx = s&.to_i
    formula_expr = f&.include?("&") ? decode_xml_entities(f) : f
    cell = Elements::Cell.fast_create(row_idx, c_idx, val, style_idx, formula_expr)
    block.call(cell)
  end
end

.parse(xml_string, shared_strings: []) ⇒ Object

Parses all rows from a worksheet XML string. Returns an Array of raw row hashes:

{ index:, cells: [{ ref:, type:, style_index:, value:, formula: }], attrs:, unmapped: }

Parameters:

  • xml_string (Object)
  • shared_strings: (Object) (defaults to: [])

Returns:

  • (Object)


19
20
21
22
23
24
25
# File 'lib/xlsxrb/ooxml/worksheet_parser.rb', line 19

def self.parse(xml_string, shared_strings: [])
  return [] if xml_string.nil? || xml_string.empty?

  rows = []
  each_row(xml_string, shared_strings: shared_strings) { |row| rows << row }
  rows
end

.parse_columns(xml_string, part_name: "xl/worksheets/sheet1.xml") ⇒ Object

Parses column definitions () from a worksheet.

Parameters:

  • xml_string (Object)
  • part_name: (Object) (defaults to: "xl/worksheets/sheet1.xml")

Returns:

  • (Object)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/xlsxrb/ooxml/worksheet_parser.rb', line 36

def self.parse_columns(xml_string, part_name: "xl/worksheets/sheet1.xml")
  return [] if xml_string.nil? || xml_string.empty?

  columns = []
  each_event(xml_string, part_name: part_name) do |event|
    next unless event.type == :column

    min, max, width, hidden, custom_width, outline_level = event.args
    columns << {
      min: min,
      max: max,
      width: width,
      hidden: hidden,
      custom_width: custom_width,
      outline_level: outline_level
    }
  end
  columns
end