xlsxrb 0.1.0
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.
- checksums.yaml +7 -0
- data/.devcontainer/Dockerfile +64 -0
- data/.devcontainer/devcontainer.json +17 -0
- data/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +384 -0
- data/Rakefile +290 -0
- data/benchmark.rb +390 -0
- data/docs/ARCHITECTURE.md +488 -0
- data/docs/SPEC_SOURCES.md +42 -0
- data/docs/visual/VisualGallery.md +4684 -0
- data/docs/wasm/wasm_doc_helper.css +189 -0
- data/docs/wasm/wasm_doc_helper.js +320 -0
- data/lib/xlsxrb/elements/cell.rb +77 -0
- data/lib/xlsxrb/elements/column.rb +28 -0
- data/lib/xlsxrb/elements/row.rb +47 -0
- data/lib/xlsxrb/elements/types.rb +36 -0
- data/lib/xlsxrb/elements/workbook.rb +47 -0
- data/lib/xlsxrb/elements/worksheet.rb +50 -0
- data/lib/xlsxrb/elements.rb +15 -0
- data/lib/xlsxrb/ooxml/reader.rb +8048 -0
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +75 -0
- data/lib/xlsxrb/ooxml/styles_parser.rb +194 -0
- data/lib/xlsxrb/ooxml/utils.rb +122 -0
- data/lib/xlsxrb/ooxml/workbook_parser.rb +78 -0
- data/lib/xlsxrb/ooxml/workbook_writer.rb +1000 -0
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +455 -0
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +1008 -0
- data/lib/xlsxrb/ooxml/writer.rb +5450 -0
- data/lib/xlsxrb/ooxml/xml_builder.rb +113 -0
- data/lib/xlsxrb/ooxml/xml_parser.rb +68 -0
- data/lib/xlsxrb/ooxml/zip_generator.rb +162 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +158 -0
- data/lib/xlsxrb/ooxml/zip_writer.rb +213 -0
- data/lib/xlsxrb/ooxml.rb +22 -0
- data/lib/xlsxrb/style_builder.rb +241 -0
- data/lib/xlsxrb/version.rb +5 -0
- data/lib/xlsxrb.rb +1427 -0
- data/measure_memory.rb +42 -0
- data/sig/xlsxrb.rbs +23 -0
- data/vendor/sdk_runner/Program.cs +91 -0
- data/vendor/sdk_runner/sdk_runner.csproj +13 -0
- metadata +144 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "xml_parser"
|
|
4
|
+
|
|
5
|
+
module Xlsxrb
|
|
6
|
+
module Ooxml
|
|
7
|
+
# Streaming parser for xl/worksheets/sheetN.xml.
|
|
8
|
+
# Uses a fast string-scanning approach for row/cell extraction,
|
|
9
|
+
# falling back to REXML SAX only for column definitions and unmapped data.
|
|
10
|
+
class WorksheetParser
|
|
11
|
+
EMPTY_ARRAY = [].freeze
|
|
12
|
+
EMPTY_HASH = {}.freeze
|
|
13
|
+
|
|
14
|
+
# Parses all rows from a worksheet XML string.
|
|
15
|
+
# Returns an Array of raw row hashes:
|
|
16
|
+
# { index:, cells: [{ ref:, type:, style_index:, value:, formula: }], attrs:, unmapped: }
|
|
17
|
+
def self.parse(xml_string, shared_strings: [])
|
|
18
|
+
return [] if xml_string.nil? || xml_string.empty?
|
|
19
|
+
|
|
20
|
+
rows = []
|
|
21
|
+
each_row(xml_string, shared_strings: shared_strings) { |row| rows << row }
|
|
22
|
+
rows
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Streaming parse: yields one raw row hash at a time.
|
|
26
|
+
def self.each_row(xml_string, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block)
|
|
27
|
+
return enum_for(:each_row, xml_string, shared_strings: shared_strings, part_name: part_name) unless block
|
|
28
|
+
|
|
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
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Parses column definitions (<cols>) from a worksheet.
|
|
52
|
+
def self.parse_columns(xml_string, part_name: "xl/worksheets/sheet1.xml")
|
|
53
|
+
return [] if xml_string.nil? || xml_string.empty?
|
|
54
|
+
|
|
55
|
+
columns = []
|
|
56
|
+
each_event(xml_string, part_name: part_name) do |event|
|
|
57
|
+
next unless event.type == :column
|
|
58
|
+
|
|
59
|
+
min, max, width, hidden, custom_width, outline_level = event.args
|
|
60
|
+
columns << {
|
|
61
|
+
min: min,
|
|
62
|
+
max: max,
|
|
63
|
+
width: width,
|
|
64
|
+
hidden: hidden,
|
|
65
|
+
custom_width: custom_width,
|
|
66
|
+
outline_level: outline_level
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
columns
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Yields Event objects for columns, rows, cells, and hyperlinks.
|
|
73
|
+
def self.each_event(xml_string, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block)
|
|
74
|
+
return enum_for(:each_event, xml_string, shared_strings: shared_strings, part_name: part_name) unless block
|
|
75
|
+
return if xml_string.nil? || xml_string.empty?
|
|
76
|
+
|
|
77
|
+
# 1. Parse and yield column events
|
|
78
|
+
if xml_string.include?("<cols")
|
|
79
|
+
listener = ColumnsListener.new
|
|
80
|
+
XmlParser.parse(xml_string, listener)
|
|
81
|
+
listener.columns.each do |col|
|
|
82
|
+
block.call(Event.new(
|
|
83
|
+
type: :column,
|
|
84
|
+
args: [col[:min], col[:max], col[:width], col[:hidden], col[:custom_width], col[:outline_level]],
|
|
85
|
+
source: { part: part_name }
|
|
86
|
+
))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 2. Parse and yield row/cell events
|
|
91
|
+
fast_scan_events(xml_string, shared_strings, part_name, &block)
|
|
92
|
+
|
|
93
|
+
# 3. Parse and yield hyperlink events
|
|
94
|
+
return unless xml_string.include?("hyperlink")
|
|
95
|
+
|
|
96
|
+
xml = xml_string.b
|
|
97
|
+
hpos_term = xml.index("hyperlinks")
|
|
98
|
+
return unless hpos_term
|
|
99
|
+
|
|
100
|
+
hpos = xml.rindex("<", hpos_term)
|
|
101
|
+
return unless hpos
|
|
102
|
+
|
|
103
|
+
h_end_term = xml.index("/hyperlinks", hpos)
|
|
104
|
+
h_end = h_end_term ? xml.index(">", h_end_term) : xml.size
|
|
105
|
+
h_end ||= xml.size
|
|
106
|
+
|
|
107
|
+
pos = hpos
|
|
108
|
+
while pos < h_end
|
|
109
|
+
hl_start_term = xml.index("hyperlink", pos)
|
|
110
|
+
break unless hl_start_term && hl_start_term < h_end
|
|
111
|
+
|
|
112
|
+
hl_start = xml.rindex("<", hl_start_term)
|
|
113
|
+
break unless hl_start && hl_start < h_end
|
|
114
|
+
|
|
115
|
+
hl_tag_end = xml.index(">", hl_start + 1)
|
|
116
|
+
break unless hl_tag_end
|
|
117
|
+
|
|
118
|
+
hl_tag = xml.byteslice(hl_start, hl_tag_end - hl_start)
|
|
119
|
+
ref = tag_attr(hl_tag, ' ref="')
|
|
120
|
+
rid = tag_attr(hl_tag, ' r:id="') || tag_attr(hl_tag, ' id="')
|
|
121
|
+
display = tag_attr(hl_tag, ' display="')
|
|
122
|
+
tooltip = tag_attr(hl_tag, ' tooltip="')
|
|
123
|
+
location = tag_attr(hl_tag, ' location="')
|
|
124
|
+
|
|
125
|
+
if ref
|
|
126
|
+
block.call(Event.new(
|
|
127
|
+
type: :hyperlink,
|
|
128
|
+
args: [ref, rid, display, tooltip, location],
|
|
129
|
+
source: { part: part_name, cell: ref }
|
|
130
|
+
))
|
|
131
|
+
end
|
|
132
|
+
pos = hl_tag_end + 1
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# ---- Fast string-scanning event parser (byte-level) ----
|
|
137
|
+
|
|
138
|
+
def self.fast_scan_events(xml_src, shared_strings, part_name, &block)
|
|
139
|
+
xml = xml_src.b # force ASCII-8BIT for O(1) byte indexing
|
|
140
|
+
|
|
141
|
+
sd_start = xml.index("<sheetData")
|
|
142
|
+
return unless sd_start
|
|
143
|
+
|
|
144
|
+
sd_open_end = xml.index(">", sd_start)
|
|
145
|
+
return unless sd_open_end
|
|
146
|
+
|
|
147
|
+
return if xml.getbyte(sd_open_end - 1) == 47 # self-closing <sheetData/>
|
|
148
|
+
|
|
149
|
+
sd_end = xml.index("</sheetData>", sd_open_end)
|
|
150
|
+
return unless sd_end
|
|
151
|
+
|
|
152
|
+
pos = sd_open_end + 1
|
|
153
|
+
|
|
154
|
+
while pos < sd_end
|
|
155
|
+
row_start = xml.index("<row", pos)
|
|
156
|
+
break unless row_start && row_start < sd_end
|
|
157
|
+
|
|
158
|
+
nb = xml.getbyte(row_start + 4)
|
|
159
|
+
unless [32, 62, 9, 10, 13, 47].include?(nb)
|
|
160
|
+
pos = row_start + 4
|
|
161
|
+
next
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
tag_end = xml.index(">", row_start + 4)
|
|
165
|
+
break unless tag_end
|
|
166
|
+
|
|
167
|
+
if xml.getbyte(tag_end - 1) == 47
|
|
168
|
+
pos = tag_end + 1
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Row index and attrs from tag substring (bounded search)
|
|
173
|
+
row_tag = xml.byteslice(row_start, tag_end - row_start)
|
|
174
|
+
row_index = 0
|
|
175
|
+
r_val = tag_attr(row_tag, ' r="')
|
|
176
|
+
row_index = r_val.to_i - 1 if r_val
|
|
177
|
+
|
|
178
|
+
attrs = extract_row_attrs(row_tag)
|
|
179
|
+
|
|
180
|
+
row_end = xml.index("</row>", tag_end + 1)
|
|
181
|
+
break unless row_end
|
|
182
|
+
|
|
183
|
+
row_source = { part: part_name, row: row_index }
|
|
184
|
+
block.call(Event.new(
|
|
185
|
+
type: :row_start,
|
|
186
|
+
args: [row_index, attrs],
|
|
187
|
+
source: row_source
|
|
188
|
+
))
|
|
189
|
+
|
|
190
|
+
fast_scan_cells_events(xml, tag_end + 1, row_end, shared_strings, part_name, row_index, &block)
|
|
191
|
+
|
|
192
|
+
block.call(Event.new(
|
|
193
|
+
type: :row_end,
|
|
194
|
+
args: [],
|
|
195
|
+
source: row_source
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
pos = row_end + 6
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private_class_method :fast_scan_events
|
|
203
|
+
|
|
204
|
+
def self.extract_row_attrs(row_tag)
|
|
205
|
+
attrs = EMPTY_HASH
|
|
206
|
+
|
|
207
|
+
ht_val = tag_attr(row_tag, ' ht="')
|
|
208
|
+
if ht_val
|
|
209
|
+
attrs = {}
|
|
210
|
+
attrs[:height] = ht_val.to_f
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if row_tag.include?('hidden="1"')
|
|
214
|
+
attrs = {} if attrs.equal?(EMPTY_HASH)
|
|
215
|
+
attrs[:hidden] = true
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if row_tag.include?('customHeight="1"')
|
|
219
|
+
attrs = {} if attrs.equal?(EMPTY_HASH)
|
|
220
|
+
attrs[:custom_height] = true
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
ol_val = tag_attr(row_tag, ' outlineLevel="')
|
|
224
|
+
if ol_val
|
|
225
|
+
attrs = {} if attrs.equal?(EMPTY_HASH)
|
|
226
|
+
attrs[:outline_level] = ol_val.to_i
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
attrs
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private_class_method :extract_row_attrs
|
|
233
|
+
|
|
234
|
+
# Extract an attribute value from a small tag substring (bounded search).
|
|
235
|
+
def self.tag_attr(tag, prefix)
|
|
236
|
+
a_pos = tag.index(prefix)
|
|
237
|
+
return nil unless a_pos
|
|
238
|
+
|
|
239
|
+
val_start = a_pos + prefix.bytesize
|
|
240
|
+
val_end = tag.index('"', val_start)
|
|
241
|
+
return nil unless val_end
|
|
242
|
+
|
|
243
|
+
tag.byteslice(val_start, val_end - val_start).force_encoding("UTF-8")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
private_class_method :tag_attr
|
|
247
|
+
|
|
248
|
+
def self.fast_scan_cells_events(xml, from, to, shared_strings, part_name, row_index, &block)
|
|
249
|
+
pos = from
|
|
250
|
+
|
|
251
|
+
while pos < to
|
|
252
|
+
c_start = xml.index("<c", pos)
|
|
253
|
+
break unless c_start && c_start < to
|
|
254
|
+
|
|
255
|
+
nb = xml.getbyte(c_start + 2)
|
|
256
|
+
unless [32, 62, 9, 10, 13, 47].include?(nb)
|
|
257
|
+
pos = c_start + 2
|
|
258
|
+
next
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
c_tag_end = xml.index(">", c_start + 2)
|
|
262
|
+
break unless c_tag_end
|
|
263
|
+
|
|
264
|
+
# Extract tag substring for bounded attribute search
|
|
265
|
+
c_tag = xml.byteslice(c_start, c_tag_end - c_start)
|
|
266
|
+
ref = tag_attr(c_tag, ' r="')
|
|
267
|
+
type = tag_attr(c_tag, ' t="')
|
|
268
|
+
style_str = tag_attr(c_tag, ' s="')
|
|
269
|
+
style_index = style_str&.to_i
|
|
270
|
+
|
|
271
|
+
# Self-closing <c ... />
|
|
272
|
+
if xml.getbyte(c_tag_end - 1) == 47
|
|
273
|
+
block.call(Event.new(
|
|
274
|
+
type: :cell,
|
|
275
|
+
args: [ref, type, style_index, nil, nil],
|
|
276
|
+
source: { part: part_name, row: row_index, cell: ref }
|
|
277
|
+
))
|
|
278
|
+
pos = c_tag_end + 1
|
|
279
|
+
next
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
c_end = xml.index("</c>", c_tag_end + 1)
|
|
283
|
+
break unless c_end
|
|
284
|
+
|
|
285
|
+
# Parse cell content sequentially (bounded to c_end - avoid unbounded scans)
|
|
286
|
+
value = nil
|
|
287
|
+
formula = nil
|
|
288
|
+
inline_str = nil
|
|
289
|
+
cpos = c_tag_end + 1
|
|
290
|
+
while cpos < c_end
|
|
291
|
+
tag_pos = xml.index("<", cpos)
|
|
292
|
+
break unless tag_pos && tag_pos < c_end
|
|
293
|
+
|
|
294
|
+
tag_char = xml.getbyte(tag_pos + 1)
|
|
295
|
+
case tag_char
|
|
296
|
+
when 118 # 'v'
|
|
297
|
+
if xml.getbyte(tag_pos + 2) == 62 # <v>
|
|
298
|
+
v_val_start = tag_pos + 3
|
|
299
|
+
v_end = xml.index("</v>", v_val_start)
|
|
300
|
+
if v_end
|
|
301
|
+
raw_value = xml.byteslice(v_val_start, v_end - v_val_start)
|
|
302
|
+
value = resolve_fast_value(raw_value, type, shared_strings)
|
|
303
|
+
cpos = v_end + 4
|
|
304
|
+
else
|
|
305
|
+
cpos = tag_pos + 3
|
|
306
|
+
end
|
|
307
|
+
elsif xml.getbyte(tag_pos + 2) == 47 && xml.getbyte(tag_pos + 3) == 62 # <v/>
|
|
308
|
+
cpos = tag_pos + 4
|
|
309
|
+
else
|
|
310
|
+
cpos = tag_pos + 2
|
|
311
|
+
end
|
|
312
|
+
when 102 # 'f'
|
|
313
|
+
f_tag_end = xml.index(">", tag_pos + 2)
|
|
314
|
+
if f_tag_end && f_tag_end < c_end
|
|
315
|
+
if xml.getbyte(f_tag_end - 1) == 47 # self-closing <f ... />
|
|
316
|
+
cpos = f_tag_end + 1
|
|
317
|
+
else
|
|
318
|
+
f_end = xml.index("</f>", f_tag_end + 1)
|
|
319
|
+
if f_end && f_end <= c_end
|
|
320
|
+
formula = xml.byteslice(f_tag_end + 1, f_end - f_tag_end - 1).force_encoding("UTF-8")
|
|
321
|
+
formula = decode_xml_entities(formula) if formula.include?("&")
|
|
322
|
+
cpos = f_end + 4
|
|
323
|
+
else
|
|
324
|
+
cpos = f_tag_end + 1
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
else
|
|
328
|
+
cpos = tag_pos + 2
|
|
329
|
+
end
|
|
330
|
+
when 105 # 'i' - <is>
|
|
331
|
+
if xml.byteslice(tag_pos, 4) == "<is>"
|
|
332
|
+
is_end = xml.index("</is>", tag_pos + 4)
|
|
333
|
+
if is_end && is_end <= c_end
|
|
334
|
+
inline_str = extract_inline_text(xml, tag_pos + 4, is_end)
|
|
335
|
+
cpos = is_end + 5
|
|
336
|
+
else
|
|
337
|
+
cpos = tag_pos + 4
|
|
338
|
+
end
|
|
339
|
+
else
|
|
340
|
+
cpos = tag_pos + 2
|
|
341
|
+
end
|
|
342
|
+
else
|
|
343
|
+
# Skip unknown tag
|
|
344
|
+
close = xml.index(">", tag_pos + 1)
|
|
345
|
+
cpos = close ? close + 1 : c_end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
val_to_use = inline_str || value
|
|
350
|
+
block.call(Event.new(
|
|
351
|
+
type: :cell,
|
|
352
|
+
args: [ref, type, style_index, val_to_use, formula],
|
|
353
|
+
source: { part: part_name, row: row_index, cell: ref }
|
|
354
|
+
))
|
|
355
|
+
|
|
356
|
+
pos = c_end + 4
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
private_class_method :fast_scan_cells_events
|
|
361
|
+
|
|
362
|
+
def self.extract_inline_text(xml, from, to)
|
|
363
|
+
result = +""
|
|
364
|
+
pos = from
|
|
365
|
+
while pos < to
|
|
366
|
+
t_start = xml.index("<t", pos)
|
|
367
|
+
break unless t_start && t_start < to
|
|
368
|
+
|
|
369
|
+
t_tag_end = xml.index(">", t_start)
|
|
370
|
+
break unless t_tag_end
|
|
371
|
+
next (pos = t_start + 2) if xml.getbyte(t_tag_end - 1) == 47
|
|
372
|
+
|
|
373
|
+
t_end = xml.index("</t>", t_tag_end + 1)
|
|
374
|
+
break unless t_end && t_end <= to
|
|
375
|
+
|
|
376
|
+
result << xml.byteslice(t_tag_end + 1, t_end - t_tag_end - 1)
|
|
377
|
+
pos = t_end + 4
|
|
378
|
+
end
|
|
379
|
+
result.force_encoding("UTF-8")
|
|
380
|
+
result = decode_xml_entities(result) if result.include?("&")
|
|
381
|
+
result
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
private_class_method :extract_inline_text
|
|
385
|
+
|
|
386
|
+
def self.resolve_fast_value(raw, type, shared_strings)
|
|
387
|
+
case type
|
|
388
|
+
when "s"
|
|
389
|
+
shared_strings[raw.to_i] || ""
|
|
390
|
+
when "b"
|
|
391
|
+
raw == "1"
|
|
392
|
+
when "e", "str", "inlineStr"
|
|
393
|
+
val = raw.force_encoding("UTF-8")
|
|
394
|
+
val.include?("&") ? decode_xml_entities(val) : val
|
|
395
|
+
else
|
|
396
|
+
return nil if raw.empty?
|
|
397
|
+
|
|
398
|
+
if raw.include?(".")
|
|
399
|
+
raw.to_f
|
|
400
|
+
else
|
|
401
|
+
int_val = raw.to_i
|
|
402
|
+
int_val.to_s == raw ? int_val : raw.to_f
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
private_class_method :resolve_fast_value
|
|
408
|
+
|
|
409
|
+
XML_ENTITIES = { "&" => "&", "<" => "<", ">" => ">", """ => '"', "'" => "'" }.freeze
|
|
410
|
+
|
|
411
|
+
def self.decode_xml_entities(str)
|
|
412
|
+
str.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
private_class_method :decode_xml_entities
|
|
416
|
+
|
|
417
|
+
# Parses <cols> section for column definitions.
|
|
418
|
+
class ColumnsListener
|
|
419
|
+
include REXML::SAX2Listener
|
|
420
|
+
|
|
421
|
+
attr_reader :columns
|
|
422
|
+
|
|
423
|
+
def initialize
|
|
424
|
+
@columns = []
|
|
425
|
+
@in_cols = false
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def start_element(_uri, localname, _qname, attrs)
|
|
429
|
+
case localname
|
|
430
|
+
when "cols"
|
|
431
|
+
@in_cols = true
|
|
432
|
+
when "col"
|
|
433
|
+
return unless @in_cols
|
|
434
|
+
|
|
435
|
+
col = {
|
|
436
|
+
min: attrs["min"]&.to_i,
|
|
437
|
+
max: attrs["max"]&.to_i,
|
|
438
|
+
width: attrs["width"]&.to_f,
|
|
439
|
+
hidden: attrs["hidden"] == "1",
|
|
440
|
+
custom_width: attrs["customWidth"] == "1",
|
|
441
|
+
outline_level: attrs["outlineLevel"]&.to_i
|
|
442
|
+
}
|
|
443
|
+
@columns << col
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def end_element(_uri, localname, _qname)
|
|
448
|
+
@in_cols = false if localname == "cols"
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def characters(_text); end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|