xlsxrb 0.1.5 → 0.1.7
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/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
- data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
- data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
- data/CHANGELOG.md +28 -0
- data/README.md +66 -44
- data/Rakefile +2 -1
- data/Steepfile +1 -2
- data/benchmark.rb +376 -0
- data/docs/ARCHITECTURE.md +12 -5
- data/docs/visual/VisualGallery.md +8 -0
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/ruby_lsp/xlsxrb/addon.rb +43 -0
- data/lib/ruby_lsp/xlsxrb/completion_listener.rb +769 -0
- data/lib/xlsxrb/elements/cell.rb +137 -11
- data/lib/xlsxrb/elements/column.rb +22 -0
- data/lib/xlsxrb/elements/row.rb +98 -5
- data/lib/xlsxrb/elements/types.rb +40 -5
- data/lib/xlsxrb/elements/workbook.rb +73 -5
- data/lib/xlsxrb/elements/worksheet.rb +110 -1
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +104 -40
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +144 -23
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +160 -145
- data/lib/xlsxrb/ooxml/xml_builder.rb +16 -10
- data/lib/xlsxrb/ooxml/zip_reader.rb +104 -18
- data/lib/xlsxrb/ooxml/zip_writer.rb +63 -25
- data/lib/xlsxrb/style_builder.rb +158 -30
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb.rb +892 -336
- data/rbs_collection.lock.yaml +28 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +39 -0
- data/sig/generated/xlsxrb/elements/column.rbs +35 -0
- data/sig/generated/xlsxrb/elements/row.rbs +39 -0
- data/sig/generated/xlsxrb/elements/types.rbs +82 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +32 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +34 -0
- data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
- data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
- data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
- data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
- data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
- data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
- data/sig/generated/xlsxrb/style_builder.rbs +212 -50
- data/sig/generated/xlsxrb.rbs +1646 -0
- data/sig/rexml.rbs +4 -0
- metadata +78 -1
|
@@ -5,15 +5,33 @@
|
|
|
5
5
|
module Xlsxrb
|
|
6
6
|
module Elements
|
|
7
7
|
# Represents a single worksheet in a workbook.
|
|
8
|
+
#
|
|
9
|
+
# @example Access cells and rows
|
|
10
|
+
# sheet = workbook.sheet(0)
|
|
11
|
+
# cell = sheet["A1"]
|
|
12
|
+
# sheet.each_row { |row| puts row.to_a.inspect }
|
|
13
|
+
#
|
|
14
|
+
# @api public
|
|
8
15
|
Worksheet = Data.define(:name, :rows, :columns, :charts, :unmapped_data, :errors) do
|
|
9
16
|
include Enumerable
|
|
10
17
|
|
|
18
|
+
# @param name [String] The worksheet name (max 31 characters).
|
|
19
|
+
# @param rows [Array<Elements::Row>] Rows in the sheet.
|
|
20
|
+
# @param columns [Array<Elements::Column>] Column definitions.
|
|
21
|
+
# @param charts [Array<Hash>] Charts in the sheet.
|
|
22
|
+
# @param unmapped_data [Hash] Additional metadata for round-tripping.
|
|
23
|
+
# @param errors [Array<String>, nil] Validation errors.
|
|
24
|
+
#: (name: String, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
|
|
11
25
|
def initialize(name:, rows: [], columns: [], charts: [], unmapped_data: {}, errors: nil)
|
|
12
26
|
computed_errors = errors || self.class.validate(name, rows)
|
|
13
27
|
super(name: name, rows: rows.freeze, columns: columns.freeze, charts: charts.freeze,
|
|
14
28
|
unmapped_data: unmapped_data, errors: computed_errors.freeze)
|
|
15
29
|
end
|
|
16
30
|
|
|
31
|
+
# Returns a Hash mapping Excel cell references (e.g. "A1") to Cell objects.
|
|
32
|
+
#
|
|
33
|
+
# @return [Hash<String, Elements::Cell>]
|
|
34
|
+
#: () -> Hash[String, Elements::Cell]
|
|
17
35
|
def cells_hash
|
|
18
36
|
h = {}
|
|
19
37
|
rows.each do |r|
|
|
@@ -25,51 +43,131 @@ module Xlsxrb
|
|
|
25
43
|
h
|
|
26
44
|
end
|
|
27
45
|
|
|
46
|
+
# Returns all cells ordered by row and column index.
|
|
47
|
+
#
|
|
48
|
+
# @return [Array<Elements::Cell>]
|
|
49
|
+
# @api public
|
|
50
|
+
#: () -> Array[Elements::Cell]
|
|
28
51
|
def cells
|
|
29
52
|
# Ensure ordered traversal
|
|
30
53
|
cells_hash.values.sort_by { |c| [c.row_index, c.column_index] }
|
|
31
54
|
end
|
|
32
55
|
|
|
56
|
+
# Access a cell by its Excel-style reference (e.g. "A1").
|
|
57
|
+
#
|
|
58
|
+
# @example
|
|
59
|
+
# sheet["A1"] #=> #<Elements::Cell value="Hello">
|
|
60
|
+
#
|
|
61
|
+
# @param ref [String, Symbol] Cell reference (e.g. "A1" or :A1).
|
|
62
|
+
# @return [Elements::Cell, nil]
|
|
63
|
+
# @api public
|
|
64
|
+
#: (String | Symbol ref) -> Elements::Cell?
|
|
33
65
|
def [](ref)
|
|
34
66
|
cells_hash[ref.to_s.upcase]
|
|
35
67
|
end
|
|
36
68
|
|
|
69
|
+
# Iterate over cells in the worksheet.
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# sheet.each do |cell|
|
|
73
|
+
# puts cell.value
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# @yield [cell]
|
|
77
|
+
# @yieldparam cell [Elements::Cell]
|
|
78
|
+
# @return [Enumerator, void]
|
|
79
|
+
# @api public
|
|
80
|
+
#: () { (Elements::Cell) -> void } -> void
|
|
81
|
+
#: | () -> Enumerator[Elements::Cell, void]
|
|
37
82
|
def each(&)
|
|
38
83
|
return to_enum(:each) unless block_given?
|
|
39
84
|
|
|
40
85
|
cells.each(&)
|
|
41
86
|
end
|
|
42
87
|
|
|
88
|
+
# Iterate over cells in the worksheet.
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# sheet.each_cell do |cell|
|
|
92
|
+
# puts "#{cell.ref}: #{cell.value}"
|
|
93
|
+
# end
|
|
94
|
+
#
|
|
95
|
+
# @yield [cell]
|
|
96
|
+
# @yieldparam cell [Elements::Cell]
|
|
97
|
+
# @return [Enumerator, void]
|
|
98
|
+
# @api public
|
|
99
|
+
#: () { (Elements::Cell) -> void } -> void
|
|
100
|
+
#: | () -> Enumerator[Elements::Cell, void]
|
|
43
101
|
def each_cell(&)
|
|
44
102
|
return to_enum(:each_cell) unless block_given?
|
|
45
103
|
|
|
46
104
|
cells.each(&)
|
|
47
105
|
end
|
|
48
106
|
|
|
107
|
+
# Iterate over rows in the worksheet.
|
|
108
|
+
#
|
|
109
|
+
# @example
|
|
110
|
+
# sheet.each_row do |row|
|
|
111
|
+
# puts "Row #{row.index}: #{row.to_a.inspect}"
|
|
112
|
+
# end
|
|
113
|
+
#
|
|
114
|
+
# @yield [row]
|
|
115
|
+
# @yieldparam row [Elements::Row]
|
|
116
|
+
# @return [Enumerator, void]
|
|
117
|
+
# @api public
|
|
118
|
+
#: () { (Elements::Row) -> void } -> void
|
|
119
|
+
#: | () -> Enumerator[Elements::Row, void]
|
|
49
120
|
def each_row(&)
|
|
50
121
|
return to_enum(:each_row) unless block_given?
|
|
51
122
|
|
|
52
123
|
rows.each(&)
|
|
53
124
|
end
|
|
54
125
|
|
|
126
|
+
# Returns whether the worksheet is valid according to OOXML specifications.
|
|
127
|
+
#
|
|
128
|
+
# @return [Boolean]
|
|
129
|
+
#: () -> bool
|
|
55
130
|
def valid?
|
|
56
131
|
errors.empty?
|
|
57
132
|
end
|
|
58
133
|
|
|
59
134
|
# Returns the row at the given 0-based index, or nil.
|
|
135
|
+
#
|
|
136
|
+
# @param index [Integer] 0-based row index.
|
|
137
|
+
# @return [Elements::Row, nil]
|
|
138
|
+
# @api public
|
|
139
|
+
#: (Integer index) -> Elements::Row?
|
|
60
140
|
def row_at(index)
|
|
61
141
|
rows.find { |r| r.index == index }
|
|
62
142
|
end
|
|
63
143
|
|
|
144
|
+
# Returns the first row in the sheet, or nil.
|
|
145
|
+
#
|
|
146
|
+
# @return [Elements::Row, nil]
|
|
147
|
+
# @api public
|
|
148
|
+
#: () -> Elements::Row?
|
|
64
149
|
def first_row
|
|
65
150
|
rows.min_by(&:index)
|
|
66
151
|
end
|
|
67
152
|
|
|
153
|
+
# Returns the last row in the sheet, or nil.
|
|
154
|
+
#
|
|
155
|
+
# @return [Elements::Row, nil]
|
|
156
|
+
# @api public
|
|
157
|
+
#: () -> Elements::Row?
|
|
68
158
|
def last_row
|
|
69
159
|
rows.max_by(&:index)
|
|
70
160
|
end
|
|
71
161
|
|
|
72
|
-
# Returns cell value at Excel-style reference (e.g. "A1").
|
|
162
|
+
# Returns the raw cell value at the given Excel-style reference (e.g. "A1").
|
|
163
|
+
#
|
|
164
|
+
# @example
|
|
165
|
+
# sheet.cell_value("A1") #=> "Sales Report"
|
|
166
|
+
#
|
|
167
|
+
# @param ref [String] Cell reference (e.g. "A1").
|
|
168
|
+
# @return [Object, nil]
|
|
169
|
+
# @api public
|
|
170
|
+
#: (String ref) -> untyped
|
|
73
171
|
def cell_value(ref)
|
|
74
172
|
parsed = Cell.parse_ref(ref)
|
|
75
173
|
return nil unless parsed
|
|
@@ -84,11 +182,16 @@ module Xlsxrb
|
|
|
84
182
|
|
|
85
183
|
# Returns a new Worksheet with the specified cell updated.
|
|
86
184
|
#
|
|
185
|
+
# @example
|
|
186
|
+
# new_sheet = sheet.update_cell("B1", value: "Updated")
|
|
187
|
+
#
|
|
87
188
|
# @param ref [String] The cell reference (e.g. "B1").
|
|
88
189
|
# @param value [Object] The new cell value.
|
|
89
190
|
# @param style_index [Integer, String, nil] Optional new style index.
|
|
90
191
|
# @param formula [Elements::Formula, nil] Optional new formula.
|
|
91
192
|
# @return [Worksheet] A new Worksheet instance.
|
|
193
|
+
# @api public
|
|
194
|
+
#: (String ref, ?value: untyped, ?style_index: Integer | String | nil, ?formula: Elements::Formula?) -> Elements::Worksheet
|
|
92
195
|
def update_cell(ref, value: nil, style_index: nil, formula: nil)
|
|
93
196
|
parsed = Cell.parse_ref(ref)
|
|
94
197
|
raise ArgumentError, "invalid cell reference: #{ref}" unless parsed
|
|
@@ -124,6 +227,12 @@ module Xlsxrb
|
|
|
124
227
|
with(rows: new_rows)
|
|
125
228
|
end
|
|
126
229
|
|
|
230
|
+
# Validates worksheet name and rows against OOXML limits.
|
|
231
|
+
#
|
|
232
|
+
# @param name [String]
|
|
233
|
+
# @param rows [Array<Elements::Row>]
|
|
234
|
+
# @return [Array<String>] List of errors.
|
|
235
|
+
#: (untyped name, untyped rows) -> Array[String]
|
|
127
236
|
def self.validate(name, rows)
|
|
128
237
|
errs = []
|
|
129
238
|
if name.nil? || !name.is_a?(String) || name.empty?
|
|
@@ -9,13 +9,20 @@ module Xlsxrb
|
|
|
9
9
|
# SAX-based parser for xl/sharedStrings.xml.
|
|
10
10
|
# Returns an Array of strings (index = SST index).
|
|
11
11
|
class SharedStringsParser
|
|
12
|
+
XML_ENTITIES = { "&" => "&", "<" => "<", ">" => ">", """ => '"', "'" => "'" }.freeze
|
|
13
|
+
|
|
12
14
|
# Parses all shared strings and returns an Array of strings.
|
|
13
|
-
def self.parse(xml_string,
|
|
15
|
+
def self.parse(xml_string, _part_name: "xl/sharedStrings.xml")
|
|
14
16
|
return [] if xml_string.nil? || xml_string.empty?
|
|
15
17
|
|
|
18
|
+
if xml_string.include?("<!DOCTYPE") || xml_string.include?("<!ENTITY")
|
|
19
|
+
listener = Class.new { include REXML::SAX2Listener }.new
|
|
20
|
+
XmlParser.parse(xml_string, listener)
|
|
21
|
+
end
|
|
22
|
+
|
|
16
23
|
strings = []
|
|
17
|
-
|
|
18
|
-
strings <<
|
|
24
|
+
fast_scan(xml_string) do |str|
|
|
25
|
+
strings << str
|
|
19
26
|
end
|
|
20
27
|
strings
|
|
21
28
|
end
|
|
@@ -25,53 +32,110 @@ module Xlsxrb
|
|
|
25
32
|
return enum_for(:each_event, xml_string, part_name: part_name) unless block
|
|
26
33
|
return if xml_string.nil? || xml_string.empty?
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
index = 0
|
|
36
|
+
fast_scan(xml_string) do |str|
|
|
37
|
+
block.call(Event.new(
|
|
38
|
+
type: :sst_item,
|
|
39
|
+
args: [str],
|
|
40
|
+
source: { part: part_name, index: index }
|
|
41
|
+
))
|
|
42
|
+
index += 1
|
|
43
|
+
end
|
|
30
44
|
end
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def initialize(part_name, &block)
|
|
37
|
-
@part_name = part_name
|
|
38
|
-
@block = block
|
|
39
|
-
@in_si = false
|
|
40
|
-
@in_t = false
|
|
41
|
-
@current_text = +""
|
|
42
|
-
@index = 0
|
|
43
|
-
end
|
|
46
|
+
def self.fast_scan(xml_src)
|
|
47
|
+
xml = xml_src.b
|
|
48
|
+
sst_start = xml.index("<sst")
|
|
49
|
+
return unless sst_start
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
sst_open_end = xml.index(">", sst_start)
|
|
52
|
+
return unless sst_open_end
|
|
53
|
+
return if xml.getbyte(sst_open_end - 1) == 47 # self-closing <sst/>
|
|
54
|
+
|
|
55
|
+
sst_end = xml.index("</sst>", sst_open_end)
|
|
56
|
+
sst_end ||= xml.bytesize
|
|
57
|
+
|
|
58
|
+
pos = sst_open_end + 1
|
|
59
|
+
while pos < sst_end
|
|
60
|
+
si_start = xml.index("<si", pos)
|
|
61
|
+
break unless si_start && si_start < sst_end
|
|
62
|
+
|
|
63
|
+
si_open_end = xml.index(">", si_start + 3)
|
|
64
|
+
break unless si_open_end
|
|
65
|
+
|
|
66
|
+
# Self-closing <si/>
|
|
67
|
+
if xml.getbyte(si_open_end - 1) == 47
|
|
68
|
+
yield ""
|
|
69
|
+
pos = si_open_end + 1
|
|
70
|
+
next
|
|
52
71
|
end
|
|
53
|
-
end
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
si_end = xml.index("</si>", si_open_end + 1)
|
|
74
|
+
break unless si_end
|
|
75
|
+
|
|
76
|
+
# Fast path: check for simple single <t>...</t> inside <si>
|
|
77
|
+
first_t = xml.index("<t", si_open_end + 1)
|
|
78
|
+
if first_t && first_t < si_end
|
|
79
|
+
t_open_end = xml.index(">", first_t + 2)
|
|
80
|
+
if t_open_end && t_open_end < si_end
|
|
81
|
+
if xml.getbyte(t_open_end - 1) == 47 # <t/>
|
|
82
|
+
str = ""
|
|
83
|
+
else
|
|
84
|
+
t_end = xml.index("</t>", t_open_end + 1)
|
|
85
|
+
if t_end && t_end <= si_end
|
|
86
|
+
next_t = xml.index("<t", t_end + 4)
|
|
87
|
+
if next_t && next_t < si_end
|
|
88
|
+
# Multiple <t> tags (rich text runs)
|
|
89
|
+
str = extract_multi_t(xml, si_open_end + 1, si_end)
|
|
90
|
+
else
|
|
91
|
+
raw_str = xml.byteslice(t_open_end + 1, t_end - t_open_end - 1).force_encoding("UTF-8")
|
|
92
|
+
str = raw_str.include?("&") ? raw_str.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES) : raw_str
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
str = ""
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
str = ""
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
str = ""
|
|
68
103
|
end
|
|
104
|
+
|
|
105
|
+
yield str.freeze
|
|
106
|
+
pos = si_end + 5
|
|
69
107
|
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private_class_method :fast_scan
|
|
111
|
+
|
|
112
|
+
def self.extract_multi_t(xml, from, to)
|
|
113
|
+
buf = +""
|
|
114
|
+
pos = from
|
|
115
|
+
while pos < to
|
|
116
|
+
t_start = xml.index("<t", pos)
|
|
117
|
+
break unless t_start && t_start < to
|
|
70
118
|
|
|
71
|
-
|
|
72
|
-
|
|
119
|
+
t_open_end = xml.index(">", t_start + 2)
|
|
120
|
+
break unless t_open_end
|
|
121
|
+
|
|
122
|
+
if xml.getbyte(t_open_end - 1) == 47
|
|
123
|
+
pos = t_open_end + 1
|
|
124
|
+
next
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
t_end = xml.index("</t>", t_open_end + 1)
|
|
128
|
+
break unless t_end && t_end <= to
|
|
129
|
+
|
|
130
|
+
raw_chunk = xml.byteslice(t_open_end + 1, t_end - t_open_end - 1).force_encoding("UTF-8")
|
|
131
|
+
raw_chunk = raw_chunk.gsub(/&(?:amp|lt|gt|quot|apos);/, XML_ENTITIES) if raw_chunk.include?("&")
|
|
132
|
+
buf << raw_chunk
|
|
133
|
+
pos = t_end + 4
|
|
73
134
|
end
|
|
135
|
+
buf
|
|
74
136
|
end
|
|
137
|
+
|
|
138
|
+
private_class_method :extract_multi_t
|
|
75
139
|
end
|
|
76
140
|
end
|
|
77
141
|
end
|
|
@@ -393,8 +393,7 @@ module Xlsxrb
|
|
|
393
393
|
if raw.include?(".")
|
|
394
394
|
raw.to_f
|
|
395
395
|
else
|
|
396
|
-
|
|
397
|
-
int_val.to_s == raw ? int_val : raw.to_f
|
|
396
|
+
raw.to_i
|
|
398
397
|
end
|
|
399
398
|
end
|
|
400
399
|
end
|
|
@@ -482,19 +481,53 @@ module Xlsxrb
|
|
|
482
481
|
next
|
|
483
482
|
end
|
|
484
483
|
|
|
485
|
-
row_tag = xml.byteslice(row_start, tag_end - row_start)
|
|
486
484
|
row_index = 0
|
|
487
|
-
|
|
488
|
-
|
|
485
|
+
has_custom_attrs = false
|
|
486
|
+
ri = row_start + 4
|
|
487
|
+
while ri < tag_end
|
|
488
|
+
rb = xml.getbyte(ri)
|
|
489
|
+
if rb == 114 && xml.getbyte(ri + 1) == 61 && xml.getbyte(ri + 2) == 34 # r="
|
|
490
|
+
ri += 3
|
|
491
|
+
while ri < tag_end
|
|
492
|
+
cb = xml.getbyte(ri)
|
|
493
|
+
break unless cb.between?(48, 57)
|
|
494
|
+
|
|
495
|
+
row_index = (row_index * 10) + (cb - 48)
|
|
496
|
+
ri += 1
|
|
497
|
+
|
|
498
|
+
end
|
|
499
|
+
row_index -= 1
|
|
500
|
+
elsif [104, 99, 111].include?(rb) # 'h', 'c', 'o' for ht, customHeight, hidden, outlineLevel
|
|
501
|
+
has_custom_attrs = true
|
|
502
|
+
ri += 1
|
|
503
|
+
else
|
|
504
|
+
ri += 1
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
attrs = if has_custom_attrs
|
|
509
|
+
row_tag = xml.byteslice(row_start, tag_end - row_start)
|
|
510
|
+
extract_row_attrs(row_tag)
|
|
511
|
+
else
|
|
512
|
+
EMPTY_HASH
|
|
513
|
+
end
|
|
489
514
|
|
|
490
|
-
attrs = extract_row_attrs(row_tag)
|
|
491
515
|
row_end = xml.index("</row>", tag_end + 1)
|
|
492
516
|
break unless row_end
|
|
493
517
|
|
|
494
518
|
row_source = { part: part_name, row: row_index }
|
|
495
519
|
cells = fast_parse_cells_direct(xml, tag_end + 1, row_end, shared_strings, row_source)
|
|
496
520
|
|
|
497
|
-
|
|
521
|
+
row_obj = Elements::Row.new(
|
|
522
|
+
index: row_index,
|
|
523
|
+
cells: cells,
|
|
524
|
+
height: attrs[:height],
|
|
525
|
+
hidden: attrs[:hidden] || false,
|
|
526
|
+
custom_height: attrs[:custom_height] || false,
|
|
527
|
+
outline_level: attrs[:outline_level],
|
|
528
|
+
errors: Elements::EMPTY_ERRORS
|
|
529
|
+
)
|
|
530
|
+
block.call(row_obj)
|
|
498
531
|
|
|
499
532
|
pos = row_end + 6
|
|
500
533
|
end
|
|
@@ -519,18 +552,90 @@ module Xlsxrb
|
|
|
519
552
|
c_tag_end = xml.index(">", c_start + 2)
|
|
520
553
|
break unless c_tag_end
|
|
521
554
|
|
|
522
|
-
#
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
555
|
+
# Zero-allocation attribute scan directly within the tag
|
|
556
|
+
type = nil
|
|
557
|
+
style_index = nil
|
|
558
|
+
col_idx = nil
|
|
559
|
+
row_idx = nil
|
|
560
|
+
|
|
561
|
+
ai = c_start + 2
|
|
562
|
+
while ai < c_tag_end
|
|
563
|
+
b = xml.getbyte(ai)
|
|
564
|
+
if [32, 9, 10, 13].include?(b)
|
|
565
|
+
ai += 1
|
|
566
|
+
next
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
if b == 114 && xml.getbyte(ai + 1) == 61 && xml.getbyte(ai + 2) == 34 # r="
|
|
570
|
+
ai += 3
|
|
571
|
+
col_idx = 0
|
|
572
|
+
while ai < c_tag_end
|
|
573
|
+
cb = xml.getbyte(ai)
|
|
574
|
+
if cb.between?(65, 90)
|
|
575
|
+
col_idx = (col_idx * 26) + (cb - 64)
|
|
576
|
+
ai += 1
|
|
577
|
+
elsif cb.between?(97, 122)
|
|
578
|
+
col_idx = (col_idx * 26) + (cb - 96)
|
|
579
|
+
ai += 1
|
|
580
|
+
else
|
|
581
|
+
break
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
col_idx -= 1
|
|
585
|
+
row_idx = 0
|
|
586
|
+
while ai < c_tag_end
|
|
587
|
+
cb = xml.getbyte(ai)
|
|
588
|
+
break unless cb.between?(48, 57)
|
|
589
|
+
|
|
590
|
+
row_idx = (row_idx * 10) + (cb - 48)
|
|
591
|
+
ai += 1
|
|
592
|
+
|
|
593
|
+
end
|
|
594
|
+
row_idx -= 1
|
|
595
|
+
ai += 1 if xml.getbyte(ai) == 34
|
|
596
|
+
elsif b == 116 && xml.getbyte(ai + 1) == 61 && xml.getbyte(ai + 2) == 34 # t="
|
|
597
|
+
ai += 3
|
|
598
|
+
type_b = xml.getbyte(ai)
|
|
599
|
+
if type_b == 115 && xml.getbyte(ai + 1) == 34 # "s"
|
|
600
|
+
type = "s"
|
|
601
|
+
ai += 2
|
|
602
|
+
elsif type_b == 98 && xml.getbyte(ai + 1) == 34 # "b"
|
|
603
|
+
type = "b"
|
|
604
|
+
ai += 2
|
|
605
|
+
elsif type_b == 101 && xml.getbyte(ai + 1) == 34 # "e"
|
|
606
|
+
type = "e"
|
|
607
|
+
ai += 2
|
|
608
|
+
else
|
|
609
|
+
t_end = xml.index('"', ai)
|
|
610
|
+
type = xml.byteslice(ai, t_end - ai) if t_end
|
|
611
|
+
ai = t_end ? t_end + 1 : c_tag_end
|
|
612
|
+
end
|
|
613
|
+
elsif b == 115 && xml.getbyte(ai + 1) == 61 && xml.getbyte(ai + 2) == 34 # s="
|
|
614
|
+
ai += 3
|
|
615
|
+
style_index = 0
|
|
616
|
+
while ai < c_tag_end
|
|
617
|
+
cb = xml.getbyte(ai)
|
|
618
|
+
break unless cb.between?(48, 57)
|
|
619
|
+
|
|
620
|
+
style_index = (style_index * 10) + (cb - 48)
|
|
621
|
+
ai += 1
|
|
622
|
+
|
|
623
|
+
end
|
|
624
|
+
ai += 1 if xml.getbyte(ai) == 34
|
|
625
|
+
else
|
|
626
|
+
ai += 1
|
|
627
|
+
end
|
|
628
|
+
end
|
|
528
629
|
|
|
529
630
|
# Self-closing <c ... />
|
|
530
631
|
if xml.getbyte(c_tag_end - 1) == 47
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
632
|
+
cells << Elements::Cell.new(
|
|
633
|
+
row_index: row_idx || row_source[:row],
|
|
634
|
+
column_index: col_idx || cells.size,
|
|
635
|
+
value: nil,
|
|
636
|
+
style_index: style_index,
|
|
637
|
+
errors: Elements::EMPTY_ERRORS
|
|
638
|
+
)
|
|
534
639
|
pos = c_tag_end + 1
|
|
535
640
|
next
|
|
536
641
|
end
|
|
@@ -554,8 +659,21 @@ module Xlsxrb
|
|
|
554
659
|
v_val_start = tag_pos + 3
|
|
555
660
|
v_end = xml.index("</v>", v_val_start)
|
|
556
661
|
if v_end
|
|
557
|
-
|
|
558
|
-
|
|
662
|
+
if type == "s"
|
|
663
|
+
# Zero-allocation integer parse for SST index
|
|
664
|
+
s_idx = 0
|
|
665
|
+
v_i = v_val_start
|
|
666
|
+
while v_i < v_end
|
|
667
|
+
s_idx = (s_idx * 10) + (xml.getbyte(v_i) - 48)
|
|
668
|
+
v_i += 1
|
|
669
|
+
end
|
|
670
|
+
value = shared_strings[s_idx] || ""
|
|
671
|
+
elsif type == "b"
|
|
672
|
+
value = xml.getbyte(v_val_start) == 49 # '1'
|
|
673
|
+
else
|
|
674
|
+
raw_value = xml.byteslice(v_val_start, v_end - v_val_start)
|
|
675
|
+
value = resolve_fast_value(raw_value, type, shared_strings)
|
|
676
|
+
end
|
|
559
677
|
cpos = v_end + 4
|
|
560
678
|
else
|
|
561
679
|
cpos = tag_pos + 3
|
|
@@ -603,11 +721,14 @@ module Xlsxrb
|
|
|
603
721
|
end
|
|
604
722
|
|
|
605
723
|
val_to_use = inline_str || value
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
724
|
+
cells << Elements::Cell.new(
|
|
725
|
+
row_index: row_idx || row_source[:row],
|
|
726
|
+
column_index: col_idx || cells.size,
|
|
727
|
+
value: val_to_use,
|
|
728
|
+
formula: formula,
|
|
729
|
+
style_index: style_index,
|
|
730
|
+
errors: Elements::EMPTY_ERRORS
|
|
731
|
+
)
|
|
611
732
|
|
|
612
733
|
pos = c_end + 4
|
|
613
734
|
end
|