Class: Xlsxrb::Ooxml::WorksheetParser

Inherits:
Object
  • Object
show all
Defined in:
lib/xlsxrb/ooxml/worksheet_parser.rb,
sig/xlsxrb.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 =
[].freeze
EMPTY_HASH =
{}.freeze
XML_ENTITIES =
{ "&amp;" => "&", "&lt;" => "<", "&gt;" => ">", "&quot;" => '"', "&apos;" => "'" }.freeze

Class Method Summary collapse

Class Method Details

.each_event(arg0, shared_strings:, part_name:) ⇒ void .each_event(arg0, shared_strings:, part_name:) ⇒ Enumerator[Event, void]

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

Overloads:

  • .each_event(arg0, shared_strings:, part_name:) ⇒ void

    This method returns an undefined value.

    Parameters:

    • arg0 (String)
    • shared_strings: (Array[String])
    • part_name: (String)
  • .each_event(arg0, shared_strings:, part_name:) ⇒ Enumerator[Event, void]

    Parameters:

    • arg0 (String)
    • shared_strings: (Array[String])
    • part_name: (String)

    Returns:

    • (Enumerator[Event, void])


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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/xlsxrb/ooxml/worksheet_parser.rb', line 73

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) ⇒ Object

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



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/xlsxrb/ooxml/worksheet_parser.rb', line 26

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

  current_row = nil
  each_event(xml_string, shared_strings: shared_strings, part_name: part_name) do |event|
    case event.type
    when :row_start
      index, attrs = event.args
      current_row = { index: index, cells: [], attrs: attrs, unmapped: EMPTY_ARRAY, source: event.source }
    when :cell
      ref, type, style_index, value, formula = event.args
      if current_row
        cell = { ref: ref, type: type, style_index: style_index, value: value, source: event.source }
        cell[:formula] = formula if formula
        current_row[:cells] << cell
      end
    when :row_end
      if current_row
        block.call(current_row)
        current_row = nil
      end
    end
  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: }


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

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.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/xlsxrb/ooxml/worksheet_parser.rb', line 52

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