xlsxrb 0.1.13 → 0.1.14
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 +4 -4
- data/CHANGELOG.md +57 -0
- data/README.md +2 -0
- data/lib/xlsxrb/colors.rb +118 -0
- data/lib/xlsxrb/elements/cell.rb +205 -16
- data/lib/xlsxrb/elements/column.rb +6 -4
- data/lib/xlsxrb/elements/coordinate_access.rb +32 -0
- data/lib/xlsxrb/elements/image.rb +128 -0
- data/lib/xlsxrb/elements/row.rb +206 -9
- data/lib/xlsxrb/elements/styles.rb +157 -0
- data/lib/xlsxrb/elements/types.rb +95 -1
- data/lib/xlsxrb/elements/workbook.rb +85 -4
- data/lib/xlsxrb/elements/worksheet.rb +424 -29
- data/lib/xlsxrb/elements.rb +2 -0
- data/lib/xlsxrb/number_formatter.rb +429 -0
- data/lib/xlsxrb/ooxml/cfb.rb +1 -1
- data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +7 -5
- data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +11 -4
- data/lib/xlsxrb/ooxml/reader.rb +12 -14
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
- data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
- data/lib/xlsxrb/ooxml/utils.rb +34 -18
- data/lib/xlsxrb/ooxml/workbook_parser.rb +53 -15
- data/lib/xlsxrb/ooxml/workbook_writer.rb +65 -14
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +206 -61
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +4 -2
- data/lib/xlsxrb/ooxml/writer.rb +14 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +109 -25
- data/lib/xlsxrb/stream_row.rb +243 -16
- data/lib/xlsxrb/stream_sheet.rb +414 -15
- data/lib/xlsxrb/stream_writer.rb +213 -17
- data/lib/xlsxrb/style_builder.rb +308 -22
- data/lib/xlsxrb/utils.rb +158 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +73 -3
- data/lib/xlsxrb/worksheet_builder.rb +227 -16
- data/lib/xlsxrb.rb +578 -79
- data/sig/generated/xlsxrb/colors.rbs +40 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
- data/sig/generated/xlsxrb/elements/column.rbs +3 -2
- data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
- data/sig/generated/xlsxrb/elements/image.rbs +82 -0
- data/sig/generated/xlsxrb/elements/row.rbs +83 -4
- data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
- data/sig/generated/xlsxrb/elements/types.rbs +43 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +51 -2
- data/sig/generated/xlsxrb/elements/worksheet.rbs +231 -18
- data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
- data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +10 -0
- data/sig/generated/xlsxrb/stream_row.rbs +100 -6
- data/sig/generated/xlsxrb/stream_sheet.rbs +247 -13
- data/sig/generated/xlsxrb/stream_writer.rbs +120 -14
- data/sig/generated/xlsxrb/style_builder.rbs +55 -18
- data/sig/generated/xlsxrb/utils.rbs +119 -0
- data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
- data/sig/generated/xlsxrb/worksheet_builder.rbs +107 -11
- data/sig/generated/xlsxrb.rbs +186 -27
- metadata +11 -1
|
@@ -13,49 +13,71 @@ module Xlsxrb
|
|
|
13
13
|
class WorksheetParser
|
|
14
14
|
EMPTY_ARRAY = [].freeze
|
|
15
15
|
EMPTY_HASH = {}.freeze
|
|
16
|
+
TAG_DELIM_BYTES = [32, 62, 9, 10, 13, 47].freeze
|
|
17
|
+
ROW_ATTR_START_BYTES = [104, 99, 111, 115].freeze
|
|
16
18
|
|
|
17
19
|
# Parses all rows from a worksheet XML string.
|
|
18
20
|
# Returns an Array of raw row hashes:
|
|
19
21
|
# { index:, cells: [{ ref:, type:, style_index:, value:, formula: }], attrs:, unmapped: }
|
|
20
|
-
def self.parse(xml_string, shared_strings: [])
|
|
22
|
+
def self.parse(xml_string, shared_strings: [], styles: nil)
|
|
21
23
|
return [] if xml_string.nil? || xml_string.empty?
|
|
22
24
|
|
|
23
25
|
rows = []
|
|
24
|
-
each_row(xml_string, shared_strings: shared_strings) { |row| rows << row }
|
|
26
|
+
each_row(xml_string, shared_strings: shared_strings, styles: styles) { |row| rows << row }
|
|
25
27
|
rows
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
# Streaming parse: yields one raw row hash at a time.
|
|
29
|
-
def self.each_row(xml_source, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", &block)
|
|
30
|
-
return enum_for(:each_row, xml_source, shared_strings: shared_strings, part_name: part_name) unless block
|
|
31
|
+
def self.each_row(xml_source, shared_strings: [], part_name: "xl/worksheets/sheet1.xml", styles: nil, date1904: false, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, &block)
|
|
32
|
+
return enum_for(:each_row, xml_source, shared_strings: shared_strings, part_name: part_name, styles: styles, date1904: date1904, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block
|
|
31
33
|
return if xml_source.nil? || (xml_source.respond_to?(:empty?) && xml_source.empty?)
|
|
32
34
|
|
|
35
|
+
row_consumer = if trim_empty_rows
|
|
36
|
+
pending_empty = []
|
|
37
|
+
lambda do |row|
|
|
38
|
+
if row.empty?
|
|
39
|
+
pending_empty << row
|
|
40
|
+
else
|
|
41
|
+
pending_empty.each(&block)
|
|
42
|
+
pending_empty.clear
|
|
43
|
+
block.call(row)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
block
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if pad_empty_rows
|
|
51
|
+
current_expected_idx = 0
|
|
52
|
+
inner_consumer = row_consumer
|
|
53
|
+
row_consumer = lambda do |row|
|
|
54
|
+
while current_expected_idx < row.index
|
|
55
|
+
empty_row = StreamRow.fast_create(
|
|
56
|
+
current_expected_idx, "".b, 0, 0, shared_strings, "", nil, false, false, nil, nil, styles, date1904, false, pad_empty_cells
|
|
57
|
+
)
|
|
58
|
+
inner_consumer.call(empty_row)
|
|
59
|
+
current_expected_idx += 1
|
|
60
|
+
end
|
|
61
|
+
inner_consumer.call(row)
|
|
62
|
+
current_expected_idx = row.index + 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
33
66
|
if xml_source.is_a?(String)
|
|
34
|
-
fast_scan_rows_direct(xml_source, shared_strings, part_name, &
|
|
67
|
+
fast_scan_rows_direct(xml_source, shared_strings, part_name, styles, date1904, pad_empty_cells, &row_consumer)
|
|
35
68
|
else
|
|
36
|
-
scan_rows_stream(xml_source, shared_strings, part_name, &
|
|
69
|
+
scan_rows_stream(xml_source, shared_strings, part_name, styles, date1904, pad_empty_cells, &row_consumer)
|
|
37
70
|
end
|
|
38
71
|
end
|
|
39
72
|
|
|
40
73
|
# Parses column definitions (<cols>) from a worksheet.
|
|
41
|
-
def self.parse_columns(xml_string,
|
|
74
|
+
def self.parse_columns(xml_string, _part_name = "xl/worksheets/sheet1.xml")
|
|
42
75
|
return [] if xml_string.nil? || xml_string.empty?
|
|
76
|
+
return [] unless xml_string.include?("<cols")
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
min, max, width, hidden, custom_width, outline_level = event.args
|
|
49
|
-
columns << {
|
|
50
|
-
min: min,
|
|
51
|
-
max: max,
|
|
52
|
-
width: width,
|
|
53
|
-
hidden: hidden,
|
|
54
|
-
custom_width: custom_width,
|
|
55
|
-
outline_level: outline_level
|
|
56
|
-
}
|
|
57
|
-
end
|
|
58
|
-
columns
|
|
78
|
+
listener = ColumnsListener.new
|
|
79
|
+
XmlParser.parse(xml_string, listener)
|
|
80
|
+
listener.columns
|
|
59
81
|
end
|
|
60
82
|
|
|
61
83
|
# Yields Event objects for columns, rows, cells, and hyperlinks.
|
|
@@ -80,19 +102,37 @@ module Xlsxrb
|
|
|
80
102
|
fast_scan_events(xml_string, shared_strings, part_name, &block)
|
|
81
103
|
|
|
82
104
|
# 3. Parse and yield hyperlink events
|
|
83
|
-
|
|
105
|
+
parse_hyperlinks(xml_string).each do |hl|
|
|
106
|
+
block.call(Event.new(
|
|
107
|
+
type: :hyperlink,
|
|
108
|
+
args: [hl[:ref], hl[:rid], hl[:display], hl[:tooltip], hl[:location]],
|
|
109
|
+
source: { part: part_name, cell: hl[:ref] }
|
|
110
|
+
))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Parses all hyperlink elements from worksheet XML.
|
|
115
|
+
#
|
|
116
|
+
# @param xml_string [String, nil]
|
|
117
|
+
# @return [Array<Hash[Symbol, untyped]>]
|
|
118
|
+
# @api public
|
|
119
|
+
#: (String?) -> Array[Hash[Symbol, untyped]]
|
|
120
|
+
def self.parse_hyperlinks(xml_string)
|
|
121
|
+
return [] if xml_string.nil? || xml_string.empty?
|
|
122
|
+
return [] unless xml_string.include?("hyperlink")
|
|
84
123
|
|
|
85
124
|
xml = xml_string.b
|
|
86
125
|
hpos_term = xml.index("hyperlinks")
|
|
87
|
-
return unless hpos_term
|
|
126
|
+
return [] unless hpos_term
|
|
88
127
|
|
|
89
128
|
hpos = xml.rindex("<", hpos_term)
|
|
90
|
-
return unless hpos
|
|
129
|
+
return [] unless hpos
|
|
91
130
|
|
|
92
131
|
h_end_term = xml.index("/hyperlinks", hpos)
|
|
93
132
|
h_end = h_end_term ? xml.index(">", h_end_term) : xml.size
|
|
94
133
|
h_end ||= xml.size
|
|
95
134
|
|
|
135
|
+
links = []
|
|
96
136
|
pos = hpos
|
|
97
137
|
while pos < h_end
|
|
98
138
|
hl_start_term = xml.index("hyperlink", pos)
|
|
@@ -106,20 +146,22 @@ module Xlsxrb
|
|
|
106
146
|
|
|
107
147
|
hl_tag = xml.byteslice(hl_start, hl_tag_end - hl_start)
|
|
108
148
|
ref = tag_attr(hl_tag, ' ref="')
|
|
109
|
-
rid = tag_attr(hl_tag, ' r:id="') || tag_attr(hl_tag, ' id="')
|
|
149
|
+
rid = tag_attr(hl_tag, ' r:id="') || tag_attr(hl_tag, ' id="') || hl_tag[/(?:[a-zA-Z0-9_]+:)?id="([^"]+)"/, 1]
|
|
110
150
|
display = tag_attr(hl_tag, ' display="')
|
|
111
151
|
tooltip = tag_attr(hl_tag, ' tooltip="')
|
|
112
152
|
location = tag_attr(hl_tag, ' location="')
|
|
113
153
|
|
|
114
154
|
if ref
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
155
|
+
entry = { ref: ref }
|
|
156
|
+
entry[:rid] = rid if rid
|
|
157
|
+
entry[:display] = display if display
|
|
158
|
+
entry[:tooltip] = tooltip if tooltip
|
|
159
|
+
entry[:location] = location if location
|
|
160
|
+
links << entry
|
|
120
161
|
end
|
|
121
162
|
pos = hl_tag_end + 1
|
|
122
163
|
end
|
|
164
|
+
links
|
|
123
165
|
end
|
|
124
166
|
|
|
125
167
|
# ---- Fast string-scanning event parser (byte-level) ----
|
|
@@ -157,13 +199,14 @@ module Xlsxrb
|
|
|
157
199
|
row_end_len = row_end_tag.bytesize
|
|
158
200
|
|
|
159
201
|
pos = sd_open_end + 1
|
|
202
|
+
last_row_index = -1
|
|
160
203
|
|
|
161
204
|
while pos < sd_end
|
|
162
205
|
row_start = xml.index(row_start_pattern, pos)
|
|
163
206
|
break unless row_start && row_start < sd_end
|
|
164
207
|
|
|
165
208
|
nb = xml.getbyte(row_start + row_start_len)
|
|
166
|
-
unless
|
|
209
|
+
unless TAG_DELIM_BYTES.include?(nb)
|
|
167
210
|
pos = row_start + row_start_len
|
|
168
211
|
next
|
|
169
212
|
end
|
|
@@ -178,9 +221,9 @@ module Xlsxrb
|
|
|
178
221
|
|
|
179
222
|
# Row index and attrs from tag substring (bounded search)
|
|
180
223
|
row_tag = xml.byteslice(row_start, tag_end - row_start)
|
|
181
|
-
row_index = 0
|
|
182
224
|
r_val = tag_attr(row_tag, ' r="')
|
|
183
|
-
row_index = r_val.to_i - 1
|
|
225
|
+
row_index = r_val ? r_val.to_i - 1 : last_row_index + 1
|
|
226
|
+
last_row_index = row_index
|
|
184
227
|
|
|
185
228
|
attrs = extract_row_attrs(row_tag)
|
|
186
229
|
|
|
@@ -217,22 +260,33 @@ module Xlsxrb
|
|
|
217
260
|
attrs[:height] = ht_val.to_f
|
|
218
261
|
end
|
|
219
262
|
|
|
220
|
-
if row_tag.include?('hidden="1"')
|
|
263
|
+
if row_tag.include?('hidden="1"') || row_tag.include?('hidden="true"')
|
|
221
264
|
attrs = {} if attrs.equal?(EMPTY_HASH)
|
|
222
265
|
attrs[:hidden] = true
|
|
223
266
|
end
|
|
224
267
|
|
|
225
|
-
if row_tag.include?('customHeight="1"')
|
|
268
|
+
if row_tag.include?('customHeight="1"') || row_tag.include?('customHeight="true"')
|
|
226
269
|
attrs = {} if attrs.equal?(EMPTY_HASH)
|
|
227
270
|
attrs[:custom_height] = true
|
|
228
271
|
end
|
|
229
272
|
|
|
273
|
+
if row_tag.include?('collapsed="1"') || row_tag.include?('collapsed="true"')
|
|
274
|
+
attrs = {} if attrs.equal?(EMPTY_HASH)
|
|
275
|
+
attrs[:collapsed] = true
|
|
276
|
+
end
|
|
277
|
+
|
|
230
278
|
ol_val = tag_attr(row_tag, ' outlineLevel="')
|
|
231
279
|
if ol_val
|
|
232
280
|
attrs = {} if attrs.equal?(EMPTY_HASH)
|
|
233
281
|
attrs[:outline_level] = ol_val.to_i
|
|
234
282
|
end
|
|
235
283
|
|
|
284
|
+
s_val = tag_attr(row_tag, ' s="')
|
|
285
|
+
if s_val
|
|
286
|
+
attrs = {} if attrs.equal?(EMPTY_HASH)
|
|
287
|
+
attrs[:style_index] = s_val.to_i
|
|
288
|
+
end
|
|
289
|
+
|
|
236
290
|
attrs
|
|
237
291
|
end
|
|
238
292
|
|
|
@@ -278,7 +332,7 @@ module Xlsxrb
|
|
|
278
332
|
break unless c_start && c_start < to
|
|
279
333
|
|
|
280
334
|
nb = xml.getbyte(c_start + c_start_len)
|
|
281
|
-
unless
|
|
335
|
+
unless TAG_DELIM_BYTES.include?(nb)
|
|
282
336
|
pos = c_start + c_start_len
|
|
283
337
|
next
|
|
284
338
|
end
|
|
@@ -401,8 +455,7 @@ module Xlsxrb
|
|
|
401
455
|
break unless t_end && t_end <= to
|
|
402
456
|
|
|
403
457
|
text_segment = xml.byteslice(t_tag_end + 1, t_close - t_tag_end - 1).force_encoding("UTF-8")
|
|
404
|
-
|
|
405
|
-
result << text_segment
|
|
458
|
+
result << normalize_text(text_segment)
|
|
406
459
|
|
|
407
460
|
pos = t_end + 1
|
|
408
461
|
end
|
|
@@ -418,8 +471,7 @@ module Xlsxrb
|
|
|
418
471
|
when "b"
|
|
419
472
|
raw == "1"
|
|
420
473
|
when "e", "str", "inlineStr"
|
|
421
|
-
|
|
422
|
-
val.include?("&") ? decode_xml_entities(val) : val
|
|
474
|
+
normalize_text(raw.force_encoding("UTF-8"))
|
|
423
475
|
else
|
|
424
476
|
return nil if raw.empty?
|
|
425
477
|
|
|
@@ -441,6 +493,12 @@ module Xlsxrb
|
|
|
441
493
|
XmlBuilder.unescape(str)
|
|
442
494
|
end
|
|
443
495
|
|
|
496
|
+
#: (String str) -> String
|
|
497
|
+
def self.normalize_text(str)
|
|
498
|
+
s = decode_xml_entities(str)
|
|
499
|
+
s.include?("\r") ? s.gsub("\r\n", "\n").tr("\r", "\n") : s
|
|
500
|
+
end
|
|
501
|
+
|
|
444
502
|
# Parses <cols> section for column definitions.
|
|
445
503
|
class ColumnsListener
|
|
446
504
|
include REXML::SAX2Listener
|
|
@@ -465,7 +523,8 @@ module Xlsxrb
|
|
|
465
523
|
width: attrs["width"]&.to_f,
|
|
466
524
|
hidden: attrs["hidden"] == "1",
|
|
467
525
|
custom_width: attrs["customWidth"] == "1",
|
|
468
|
-
outline_level: attrs["outlineLevel"]&.to_i
|
|
526
|
+
outline_level: attrs["outlineLevel"]&.to_i,
|
|
527
|
+
style_index: attrs["style"]&.to_i
|
|
469
528
|
}
|
|
470
529
|
@columns << col
|
|
471
530
|
end
|
|
@@ -478,7 +537,9 @@ module Xlsxrb
|
|
|
478
537
|
def characters(_text); end
|
|
479
538
|
end
|
|
480
539
|
|
|
481
|
-
|
|
540
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
|
541
|
+
def self.fast_scan_rows_direct(xml_src, shared_strings, _part_name, styles = nil, date1904 = false, pad_empty_cells = false, &block)
|
|
542
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
|
482
543
|
xml = xml_src.b # force ASCII-8BIT for O(1) byte indexing
|
|
483
544
|
|
|
484
545
|
sd_term = xml.index("sheetData")
|
|
@@ -503,13 +564,14 @@ module Xlsxrb
|
|
|
503
564
|
row_end_len = row_end_tag.bytesize
|
|
504
565
|
|
|
505
566
|
pos = sd_open_end + 1
|
|
567
|
+
last_row_index = -1
|
|
506
568
|
|
|
507
569
|
while pos < sd_end
|
|
508
570
|
row_start = xml.index(row_start_pattern, pos)
|
|
509
571
|
break unless row_start && row_start < sd_end
|
|
510
572
|
|
|
511
573
|
nb = xml.getbyte(row_start + row_start_len)
|
|
512
|
-
unless
|
|
574
|
+
unless TAG_DELIM_BYTES.include?(nb)
|
|
513
575
|
pos = row_start + row_start_len
|
|
514
576
|
next
|
|
515
577
|
end
|
|
@@ -522,23 +584,23 @@ module Xlsxrb
|
|
|
522
584
|
next
|
|
523
585
|
end
|
|
524
586
|
|
|
525
|
-
row_index =
|
|
587
|
+
row_index = nil
|
|
526
588
|
has_custom_attrs = false
|
|
527
589
|
ri = row_start + row_start_len
|
|
528
590
|
while ri < tag_end
|
|
529
591
|
rb = xml.getbyte(ri)
|
|
530
592
|
if rb == 114 && xml.getbyte(ri + 1) == 61 && xml.getbyte(ri + 2) == 34 # r="
|
|
531
593
|
ri += 3
|
|
594
|
+
parsed_r = 0
|
|
532
595
|
while ri < tag_end
|
|
533
596
|
cb = xml.getbyte(ri)
|
|
534
597
|
break unless cb.between?(48, 57)
|
|
535
598
|
|
|
536
|
-
|
|
599
|
+
parsed_r = (parsed_r * 10) + (cb - 48)
|
|
537
600
|
ri += 1
|
|
538
|
-
|
|
539
601
|
end
|
|
540
|
-
row_index
|
|
541
|
-
elsif
|
|
602
|
+
row_index = parsed_r - 1
|
|
603
|
+
elsif ROW_ATTR_START_BYTES.include?(rb) # 'h', 'c', 'o', 's' for ht, customHeight, hidden, outlineLevel, s
|
|
542
604
|
has_custom_attrs = true
|
|
543
605
|
ri += 1
|
|
544
606
|
else
|
|
@@ -546,6 +608,9 @@ module Xlsxrb
|
|
|
546
608
|
end
|
|
547
609
|
end
|
|
548
610
|
|
|
611
|
+
row_index ||= last_row_index + 1
|
|
612
|
+
last_row_index = row_index
|
|
613
|
+
|
|
549
614
|
attrs = if has_custom_attrs
|
|
550
615
|
row_tag = xml.byteslice(row_start, tag_end - row_start)
|
|
551
616
|
extract_row_attrs(row_tag)
|
|
@@ -566,7 +631,12 @@ module Xlsxrb
|
|
|
566
631
|
attrs[:height],
|
|
567
632
|
attrs[:hidden] || false,
|
|
568
633
|
attrs[:custom_height] || false,
|
|
569
|
-
attrs[:outline_level]
|
|
634
|
+
attrs[:outline_level],
|
|
635
|
+
attrs[:style_index],
|
|
636
|
+
styles,
|
|
637
|
+
date1904,
|
|
638
|
+
attrs[:collapsed] || false,
|
|
639
|
+
pad_empty_cells
|
|
570
640
|
)
|
|
571
641
|
block.call(row_obj)
|
|
572
642
|
|
|
@@ -576,7 +646,9 @@ module Xlsxrb
|
|
|
576
646
|
|
|
577
647
|
private_class_method :fast_scan_rows_direct
|
|
578
648
|
|
|
579
|
-
|
|
649
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
|
650
|
+
def self.scan_rows_stream(source, shared_strings, _part_name, styles = nil, date1904 = false, pad_empty_cells = false, &block)
|
|
651
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
|
580
652
|
buffer = +""
|
|
581
653
|
buffer.force_encoding(Encoding::BINARY)
|
|
582
654
|
in_sheet_data = false
|
|
@@ -586,6 +658,7 @@ module Xlsxrb
|
|
|
586
658
|
row_end_tag = "</row>"
|
|
587
659
|
row_end_len = 6
|
|
588
660
|
sd_end_tag = "</sheetData>"
|
|
661
|
+
last_row_index = -1
|
|
589
662
|
|
|
590
663
|
push_chunk = lambda do |chunk|
|
|
591
664
|
return if in_sheet_data == :closed
|
|
@@ -628,9 +701,9 @@ module Xlsxrb
|
|
|
628
701
|
|
|
629
702
|
pos = 0
|
|
630
703
|
last_consumed_pos = 0
|
|
704
|
+
sd_end_pos = buffer.index(sd_end_tag)
|
|
631
705
|
|
|
632
706
|
loop do
|
|
633
|
-
sd_end_pos = buffer.index(sd_end_tag, pos)
|
|
634
707
|
row_start = buffer.index(row_start_pattern, pos)
|
|
635
708
|
|
|
636
709
|
if sd_end_pos && (row_start.nil? || sd_end_pos < row_start)
|
|
@@ -645,7 +718,7 @@ module Xlsxrb
|
|
|
645
718
|
break if next_pos >= buffer.bytesize
|
|
646
719
|
|
|
647
720
|
nb = buffer.getbyte(next_pos)
|
|
648
|
-
unless
|
|
721
|
+
unless TAG_DELIM_BYTES.include?(nb)
|
|
649
722
|
pos = next_pos
|
|
650
723
|
next
|
|
651
724
|
end
|
|
@@ -666,22 +739,23 @@ module Xlsxrb
|
|
|
666
739
|
break
|
|
667
740
|
end
|
|
668
741
|
|
|
669
|
-
row_index =
|
|
742
|
+
row_index = nil
|
|
670
743
|
has_custom_attrs = false
|
|
671
744
|
ri = row_start + row_start_len
|
|
672
745
|
while ri < tag_end
|
|
673
746
|
rb = buffer.getbyte(ri)
|
|
674
747
|
if rb == 114 && buffer.getbyte(ri + 1) == 61 && buffer.getbyte(ri + 2) == 34 # r="
|
|
675
748
|
ri += 3
|
|
749
|
+
parsed_r = 0
|
|
676
750
|
while ri < tag_end
|
|
677
751
|
cb = buffer.getbyte(ri)
|
|
678
752
|
break unless cb.between?(48, 57)
|
|
679
753
|
|
|
680
|
-
|
|
754
|
+
parsed_r = (parsed_r * 10) + (cb - 48)
|
|
681
755
|
ri += 1
|
|
682
756
|
end
|
|
683
|
-
row_index
|
|
684
|
-
elsif
|
|
757
|
+
row_index = parsed_r - 1
|
|
758
|
+
elsif ROW_ATTR_START_BYTES.include?(rb)
|
|
685
759
|
has_custom_attrs = true
|
|
686
760
|
ri += 1
|
|
687
761
|
else
|
|
@@ -689,6 +763,9 @@ module Xlsxrb
|
|
|
689
763
|
end
|
|
690
764
|
end
|
|
691
765
|
|
|
766
|
+
row_index ||= last_row_index + 1
|
|
767
|
+
last_row_index = row_index
|
|
768
|
+
|
|
692
769
|
attrs = if has_custom_attrs
|
|
693
770
|
row_tag = buffer.byteslice(row_start, tag_end - row_start)
|
|
694
771
|
extract_row_attrs(row_tag)
|
|
@@ -707,7 +784,12 @@ module Xlsxrb
|
|
|
707
784
|
attrs[:height],
|
|
708
785
|
attrs[:hidden] || false,
|
|
709
786
|
attrs[:custom_height] || false,
|
|
710
|
-
attrs[:outline_level]
|
|
787
|
+
attrs[:outline_level],
|
|
788
|
+
attrs[:style_index],
|
|
789
|
+
styles,
|
|
790
|
+
date1904,
|
|
791
|
+
attrs[:collapsed] || false,
|
|
792
|
+
pad_empty_cells
|
|
711
793
|
)
|
|
712
794
|
block.call(row_obj)
|
|
713
795
|
|
|
@@ -718,20 +800,32 @@ module Xlsxrb
|
|
|
718
800
|
buffer.slice!(0...last_consumed_pos) if last_consumed_pos.positive?
|
|
719
801
|
end
|
|
720
802
|
|
|
721
|
-
|
|
722
|
-
source.each_chunk
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
803
|
+
catch(:done) do
|
|
804
|
+
if source.respond_to?(:each_chunk)
|
|
805
|
+
source.each_chunk(65_536) do |chunk|
|
|
806
|
+
push_chunk.call(chunk)
|
|
807
|
+
throw :done if in_sheet_data == :closed
|
|
808
|
+
end
|
|
809
|
+
elsif source.respond_to?(:read)
|
|
810
|
+
while (chunk = source.read(65_536))
|
|
811
|
+
break if chunk.empty?
|
|
726
812
|
|
|
727
|
-
|
|
813
|
+
push_chunk.call(chunk)
|
|
814
|
+
break if in_sheet_data == :closed
|
|
815
|
+
end
|
|
816
|
+
elsif source.respond_to?(:call)
|
|
817
|
+
source.call do |chunk|
|
|
818
|
+
push_chunk.call(chunk)
|
|
819
|
+
throw :done if in_sheet_data == :closed
|
|
820
|
+
end
|
|
821
|
+
elsif source.respond_to?(:each)
|
|
822
|
+
source.each do |chunk|
|
|
823
|
+
push_chunk.call(chunk)
|
|
824
|
+
break if in_sheet_data == :closed
|
|
825
|
+
end
|
|
826
|
+
else
|
|
827
|
+
raise ArgumentError, "Unsupported stream source: #{source.class}"
|
|
728
828
|
end
|
|
729
|
-
elsif source.respond_to?(:call)
|
|
730
|
-
source.call(&push_chunk)
|
|
731
|
-
elsif source.respond_to?(:each)
|
|
732
|
-
source.each(&push_chunk)
|
|
733
|
-
else
|
|
734
|
-
raise ArgumentError, "Unsupported stream source: #{source.class}"
|
|
735
829
|
end
|
|
736
830
|
end
|
|
737
831
|
|
|
@@ -752,8 +846,8 @@ module Xlsxrb
|
|
|
752
846
|
[COL_LETTERS[idx], idx]
|
|
753
847
|
end.freeze
|
|
754
848
|
|
|
755
|
-
CELL_FAST_RE = %r{<(?:[a-zA-Z0-9_]+:)?c\s+r="([A-Za-z0-9]+)"(?:[^>]*?s="(\d+)")?(?:[^>]*?t="([a-zA-Z]+)")?[^>]
|
|
756
|
-
CELL_GENERIC_RE = %r{<(?:[a-zA-Z0-9_]+:)?c\b([^>]*?)(
|
|
849
|
+
CELL_FAST_RE = %r{<(?:[a-zA-Z0-9_]+:)?c\s+r="([A-Za-z0-9]+)"(?:[^>]*?s="(\d+)")?(?:[^>]*?t="([a-zA-Z]+)")?[^>]*>\s*(?:<(?:[a-zA-Z0-9_]+:)?f\b[^>]*?(?:/>|>([^<]*)</(?:[a-zA-Z0-9_]+:)?f>)\s*)?(?:<(?:[a-zA-Z0-9_]+:)?v>([^<]*)</(?:[a-zA-Z0-9_]+:)?v>\s*)?(?:<(?:[a-zA-Z0-9_]+:)?is>(.*?)</(?:[a-zA-Z0-9_]+:)?is>\s*)?.*?</(?:[a-zA-Z0-9_]+:)?c>|<(?:[a-zA-Z0-9_]+:)?c\s+r="([A-Za-z0-9]+)"(?:[^>]*?s="(\d+)")?[^>]*/>}m
|
|
850
|
+
CELL_GENERIC_RE = %r{<(?:[a-zA-Z0-9_]+:)?c\b([^>]*?)(?:>\s*(?:<(?:[a-zA-Z0-9_]+:)?f\b[^>]*?(?:/>|>([^<]*)</(?:[a-zA-Z0-9_]+:)?f>)\s*)?(?:<(?:[a-zA-Z0-9_]+:)?v>([^<]*)</(?:[a-zA-Z0-9_]+:)?v>\s*)?(?:<(?:[a-zA-Z0-9_]+:)?is>(.*?)</(?:[a-zA-Z0-9_]+:)?is>\s*)?.*?</(?:[a-zA-Z0-9_]+:)?c>|/>)}m
|
|
757
851
|
ATTR_R = /r="([A-Za-z0-9]+)"/
|
|
758
852
|
ATTR_T = /t="([a-zA-Z]+)"/
|
|
759
853
|
ATTR_S = /s="(\d+)"/
|
|
@@ -764,7 +858,9 @@ module Xlsxrb
|
|
|
764
858
|
cells
|
|
765
859
|
end
|
|
766
860
|
|
|
767
|
-
|
|
861
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
|
862
|
+
def self.fast_scan_cells_direct(xml, from, to, shared_strings, row_source, _prefix = "", styles = nil, date1904 = false, &block)
|
|
863
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
|
768
864
|
chunk = xml.byteslice(from, to - from)
|
|
769
865
|
row_idx = row_source.is_a?(Hash) ? row_source[:row] : row_source
|
|
770
866
|
col_idx = 0
|
|
@@ -789,6 +885,8 @@ module Xlsxrb
|
|
|
789
885
|
col_idx
|
|
790
886
|
end
|
|
791
887
|
col_idx = c_idx + 1
|
|
888
|
+
v = v.strip if v
|
|
889
|
+
f = f.strip if f
|
|
792
890
|
|
|
793
891
|
val = if t == "s"
|
|
794
892
|
shared_strings[v.to_i] || ""
|
|
@@ -798,7 +896,7 @@ module Xlsxrb
|
|
|
798
896
|
if is
|
|
799
897
|
extract_inline_text(is, 0, is.bytesize)
|
|
800
898
|
elsif v
|
|
801
|
-
|
|
899
|
+
normalize_text(v)
|
|
802
900
|
else
|
|
803
901
|
""
|
|
804
902
|
end
|
|
@@ -806,8 +904,16 @@ module Xlsxrb
|
|
|
806
904
|
v.include?(".") ? v.to_f : v.to_i
|
|
807
905
|
end
|
|
808
906
|
|
|
809
|
-
|
|
810
|
-
|
|
907
|
+
raw_val = if is
|
|
908
|
+
extract_inline_text(is, 0, is.bytesize)
|
|
909
|
+
elsif v
|
|
910
|
+
normalize_text(v)
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
formula_expr = f ? normalize_text(f) : nil
|
|
914
|
+
formula_expr = nil if formula_expr && formula_expr.empty?
|
|
915
|
+
fmt_code = NumberFormatter.format_code_for(style_idx, styles) if styles && style_idx
|
|
916
|
+
cell = Elements::Cell.fast_create(row_idx, c_idx, val, style_idx, formula_expr, raw_val, fmt_code, date1904)
|
|
811
917
|
block.call(cell)
|
|
812
918
|
end
|
|
813
919
|
|
|
@@ -833,6 +939,8 @@ module Xlsxrb
|
|
|
833
939
|
col_idx
|
|
834
940
|
end
|
|
835
941
|
col_idx = c_idx + 1
|
|
942
|
+
v = v.strip if v
|
|
943
|
+
f = f.strip if f
|
|
836
944
|
|
|
837
945
|
val = if t == "s"
|
|
838
946
|
shared_strings[v.to_i] || ""
|
|
@@ -842,7 +950,7 @@ module Xlsxrb
|
|
|
842
950
|
if is
|
|
843
951
|
extract_inline_text(is, 0, is.bytesize)
|
|
844
952
|
elsif v
|
|
845
|
-
|
|
953
|
+
normalize_text(v)
|
|
846
954
|
else
|
|
847
955
|
""
|
|
848
956
|
end
|
|
@@ -850,13 +958,145 @@ module Xlsxrb
|
|
|
850
958
|
v.include?(".") ? v.to_f : v.to_i
|
|
851
959
|
end
|
|
852
960
|
|
|
961
|
+
raw_val = if is
|
|
962
|
+
extract_inline_text(is, 0, is.bytesize)
|
|
963
|
+
elsif v
|
|
964
|
+
normalize_text(v)
|
|
965
|
+
end
|
|
966
|
+
|
|
853
967
|
style_idx = s&.to_i
|
|
854
|
-
formula_expr = f
|
|
855
|
-
|
|
968
|
+
formula_expr = f ? normalize_text(f) : nil
|
|
969
|
+
formula_expr = nil if formula_expr && formula_expr.empty?
|
|
970
|
+
fmt_code = NumberFormatter.format_code_for(style_idx, styles) if styles && style_idx
|
|
971
|
+
cell = Elements::Cell.fast_create(row_idx, c_idx, val, style_idx, formula_expr, raw_val, fmt_code, date1904)
|
|
856
972
|
block.call(cell)
|
|
857
973
|
end
|
|
858
974
|
end
|
|
859
975
|
|
|
976
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
|
977
|
+
#: (String xml, Integer from, Integer to, Array[String] shared_strings, ?untyped styles, ?bool date1904, ?type_cast: bool) -> Array[untyped]
|
|
978
|
+
def self.fast_scan_row_values(xml, from, to, shared_strings, styles = nil, date1904 = false, type_cast: false)
|
|
979
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
|
980
|
+
chunk = xml.byteslice(from, to - from)
|
|
981
|
+
return [] if chunk.nil? || chunk.empty?
|
|
982
|
+
|
|
983
|
+
col_idx = 0
|
|
984
|
+
matched_any = false
|
|
985
|
+
row_values = []
|
|
986
|
+
|
|
987
|
+
chunk.scan(CELL_FAST_RE) do |r, s, t, _f, v, is, self_r, self_s|
|
|
988
|
+
matched_any = true
|
|
989
|
+
ref = r || self_r
|
|
990
|
+
s_val = s || self_s
|
|
991
|
+
c_idx = if ref
|
|
992
|
+
expected_letter = COL_LETTERS[col_idx]
|
|
993
|
+
if expected_letter && ref.start_with?(expected_letter)
|
|
994
|
+
col_idx
|
|
995
|
+
else
|
|
996
|
+
c_len = 0
|
|
997
|
+
c_len += 1 while (b = ref.getbyte(c_len)) && (b.between?(65, 90) || b.between?(97, 122))
|
|
998
|
+
col_letters = ref.byteslice(0, c_len).upcase
|
|
999
|
+
COL_MAP[col_letters] || col_idx
|
|
1000
|
+
end
|
|
1001
|
+
else
|
|
1002
|
+
col_idx
|
|
1003
|
+
end
|
|
1004
|
+
col_idx = c_idx + 1
|
|
1005
|
+
v = v.strip if v
|
|
1006
|
+
|
|
1007
|
+
val = if t == "s"
|
|
1008
|
+
shared_strings[v.to_i] || ""
|
|
1009
|
+
elsif t == "b"
|
|
1010
|
+
v == "1"
|
|
1011
|
+
elsif %w[inlineStr str e].include?(t)
|
|
1012
|
+
if is
|
|
1013
|
+
extract_inline_text(is, 0, is.bytesize)
|
|
1014
|
+
elsif v
|
|
1015
|
+
normalize_text(v)
|
|
1016
|
+
else
|
|
1017
|
+
""
|
|
1018
|
+
end
|
|
1019
|
+
elsif v
|
|
1020
|
+
v.include?(".") ? v.to_f : v.to_i
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
if type_cast && val.is_a?(Numeric) && s_val && styles
|
|
1024
|
+
fmt_type = NumberFormatter.format_type_for(s_val.to_i, styles)
|
|
1025
|
+
begin
|
|
1026
|
+
case fmt_type
|
|
1027
|
+
when :date
|
|
1028
|
+
val = Utils.serial_to_date(val, date1904: date1904)
|
|
1029
|
+
when :datetime, :time
|
|
1030
|
+
val = Utils.serial_to_datetime(val, date1904: date1904)
|
|
1031
|
+
end
|
|
1032
|
+
rescue StandardError
|
|
1033
|
+
# Keep numeric value on conversion failure
|
|
1034
|
+
end
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
row_values[c_idx] = val
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
return row_values if matched_any || (!chunk.include?("<c") && !chunk.include?(":c"))
|
|
1041
|
+
|
|
1042
|
+
# Fallback for non-standard attribute ordering
|
|
1043
|
+
chunk.scan(CELL_GENERIC_RE) do |attrs, _f, v, is|
|
|
1044
|
+
r = attrs[ATTR_R, 1]
|
|
1045
|
+
t = attrs[ATTR_T, 1]
|
|
1046
|
+
s = attrs[ATTR_S, 1]
|
|
1047
|
+
|
|
1048
|
+
c_idx = if r
|
|
1049
|
+
expected_letter = COL_LETTERS[col_idx]
|
|
1050
|
+
if expected_letter && r.start_with?(expected_letter)
|
|
1051
|
+
col_idx
|
|
1052
|
+
else
|
|
1053
|
+
c_len = 0
|
|
1054
|
+
c_len += 1 while (b = r.getbyte(c_len)) && (b.between?(65, 90) || b.between?(97, 122))
|
|
1055
|
+
col_letters = r.byteslice(0, c_len).upcase
|
|
1056
|
+
COL_MAP[col_letters] || col_idx
|
|
1057
|
+
end
|
|
1058
|
+
else
|
|
1059
|
+
col_idx
|
|
1060
|
+
end
|
|
1061
|
+
col_idx = c_idx + 1
|
|
1062
|
+
v = v.strip if v
|
|
1063
|
+
|
|
1064
|
+
val = if t == "s"
|
|
1065
|
+
shared_strings[v.to_i] || ""
|
|
1066
|
+
elsif t == "b"
|
|
1067
|
+
v == "1"
|
|
1068
|
+
elsif %w[inlineStr str e].include?(t)
|
|
1069
|
+
if is
|
|
1070
|
+
extract_inline_text(is, 0, is.bytesize)
|
|
1071
|
+
elsif v
|
|
1072
|
+
normalize_text(v)
|
|
1073
|
+
else
|
|
1074
|
+
""
|
|
1075
|
+
end
|
|
1076
|
+
elsif v
|
|
1077
|
+
v.include?(".") ? v.to_f : v.to_i
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
if type_cast && val.is_a?(Numeric) && s && styles
|
|
1081
|
+
fmt_type = NumberFormatter.format_type_for(s.to_i, styles)
|
|
1082
|
+
begin
|
|
1083
|
+
case fmt_type
|
|
1084
|
+
when :date
|
|
1085
|
+
val = Utils.serial_to_date(val, date1904: date1904)
|
|
1086
|
+
when :datetime, :time
|
|
1087
|
+
val = Utils.serial_to_datetime(val, date1904: date1904)
|
|
1088
|
+
end
|
|
1089
|
+
rescue StandardError
|
|
1090
|
+
# Keep numeric value on conversion failure
|
|
1091
|
+
end
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
row_values[c_idx] = val
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
row_values
|
|
1098
|
+
end
|
|
1099
|
+
|
|
860
1100
|
private_class_method :fast_parse_cells_direct
|
|
861
1101
|
end
|
|
862
1102
|
end
|