xlsxrb 0.1.5 → 0.1.6

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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
  4. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
  5. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
  6. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
  7. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
  8. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
  9. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
  10. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
  11. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
  12. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
  13. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
  14. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
  15. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
  16. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
  17. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
  18. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
  19. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
  20. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
  21. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
  22. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
  23. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
  24. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
  25. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
  26. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
  27. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
  28. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
  29. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
  30. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
  31. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
  32. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
  33. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
  34. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
  35. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
  36. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
  37. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
  38. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
  39. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
  40. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
  41. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
  42. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
  43. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
  44. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
  45. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
  46. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
  47. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
  48. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
  49. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
  50. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
  51. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
  52. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
  53. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
  54. data/CHANGELOG.md +18 -0
  55. data/README.md +39 -32
  56. data/Rakefile +2 -1
  57. data/Steepfile +0 -2
  58. data/benchmark.rb +376 -0
  59. data/docs/ARCHITECTURE.md +12 -5
  60. data/docs/wasm/ruby.wasm +0 -0
  61. data/lib/xlsxrb/elements/cell.rb +39 -7
  62. data/lib/xlsxrb/elements/row.rb +17 -4
  63. data/lib/xlsxrb/elements/types.rb +4 -0
  64. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +104 -40
  65. data/lib/xlsxrb/ooxml/worksheet_parser.rb +144 -23
  66. data/lib/xlsxrb/ooxml/worksheet_writer.rb +160 -145
  67. data/lib/xlsxrb/ooxml/xml_builder.rb +16 -10
  68. data/lib/xlsxrb/ooxml/zip_reader.rb +104 -18
  69. data/lib/xlsxrb/ooxml/zip_writer.rb +63 -25
  70. data/lib/xlsxrb/style_builder.rb +158 -30
  71. data/lib/xlsxrb/version.rb +1 -1
  72. data/lib/xlsxrb.rb +443 -236
  73. data/rbs_collection.lock.yaml +28 -0
  74. data/sig/generated/xlsxrb/elements/cell.rbs +30 -0
  75. data/sig/generated/xlsxrb/elements/column.rbs +30 -0
  76. data/sig/generated/xlsxrb/elements/row.rbs +32 -0
  77. data/sig/generated/xlsxrb/elements/types.rbs +74 -0
  78. data/sig/generated/xlsxrb/elements/workbook.rbs +25 -0
  79. data/sig/generated/xlsxrb/elements/worksheet.rbs +27 -0
  80. data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
  81. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
  82. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
  83. data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
  84. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
  85. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
  86. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
  87. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
  88. data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
  89. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
  90. data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
  91. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
  92. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
  93. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
  94. data/sig/generated/xlsxrb/style_builder.rbs +212 -50
  95. data/sig/generated/xlsxrb.rbs +1276 -0
  96. data/sig/rexml.rbs +4 -0
  97. metadata +76 -1
@@ -16,6 +16,7 @@ module Xlsxrb
16
16
  def initialize(io)
17
17
  @io = io
18
18
  @builder = XmlBuilder.new(@io)
19
+ @row_buffer = String.new(capacity: 65_536)
19
20
  @started = false
20
21
  @finished = false
21
22
  end
@@ -47,27 +48,27 @@ module Xlsxrb
47
48
 
48
49
  row_num = row_index + 1
49
50
  row_num_str = row_num.to_s
50
- io = @io
51
- io.write("<row r=\"")
52
- io.write(row_num_str)
53
- io.write('"')
51
+ buf = @row_buffer
52
+ buf.clear
53
+ buf << '<row r="' << row_num_str << '"'
54
54
  if attrs[:height]
55
- io.write(' ht="')
56
- io.write(attrs[:height].to_s)
57
- io.write('" customHeight="1"')
55
+ buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"'
58
56
  elsif attrs[:custom_height]
59
- io.write(' customHeight="1"')
57
+ buf << ' customHeight="1"'
60
58
  end
61
- io.write(' hidden="1"') if attrs[:hidden]
62
- if attrs[:outline_level]
63
- io.write(' outlineLevel="')
64
- io.write(attrs[:outline_level].to_s)
65
- io.write('"')
66
- end
67
- io.write(">")
59
+ buf << ' hidden="1"' if attrs[:hidden]
60
+ buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level]
61
+ buf << ">"
68
62
 
69
63
  cells.each do |cell|
70
- if cell.is_a?(Hash)
64
+ if cell.is_a?(Elements::Cell)
65
+ value = cell.value
66
+ style_id = cell.style_index
67
+ col_ref = cell.ref || "#{column_letter(cell.column_index)}#{row_num_str}"
68
+ formula = cell.formula
69
+ formula_ca = false
70
+ cell_type_val = nil
71
+ elsif cell.is_a?(Hash)
71
72
  value = cell[:value]
72
73
  style_id = cell[:style_index]
73
74
  col_ref = cell[:ref] || "#{column_letter(cell[:column_index])}#{row_num_str}"
@@ -75,24 +76,45 @@ module Xlsxrb
75
76
  formula_ca = cell[:formula_ca]
76
77
  cell_type_val = cell[:type]
77
78
  else
78
- value = cell.value
79
- style_id = cell.style_index
80
- col_ref = cell.ref || "#{column_letter(cell.column_index)}#{row_num_str}"
81
- formula = cell.formula
79
+ value = cell
80
+ style_id = nil
81
+ col_ref = nil
82
+ formula = nil
82
83
  formula_ca = false
83
84
  cell_type_val = nil
84
85
  end
85
86
 
86
- if value.nil? && formula.nil?
87
- io.write('<c r="')
88
- io.write(col_ref)
89
- io.write('"')
90
- if style_id
91
- io.write(' s="')
92
- io.write(style_id.to_s)
93
- io.write('"')
87
+ # Fast path for common unstyled cells
88
+ if !formula && !style_id && !cell_type_val
89
+ case value
90
+ when Integer, Float
91
+ buf << '<c r="' << col_ref << '"><v>' << value.to_s << "</v></c>"
92
+ next
93
+ when String
94
+ if !value.start_with?("=") && sst_index && (idx = sst_index[value])
95
+ buf << '<c r="' << col_ref << '" t="s"><v>' << idx.to_s << "</v></c>"
96
+ next
97
+ end
98
+ when true
99
+ buf << '<c r="' << col_ref << '" t="b"><v>1</v></c>'
100
+ next
101
+ when false
102
+ buf << '<c r="' << col_ref << '" t="b"><v>0</v></c>'
103
+ next
104
+ when nil
105
+ buf << '<c r="' << col_ref << '"/>'
106
+ next
107
+ when Date
108
+ serial = Xlsxrb::Ooxml::Utils.date_to_serial(value)
109
+ buf << '<c r="' << col_ref << '"><v>' << serial.to_s << "</v></c>"
110
+ next
94
111
  end
95
- io.write("/>")
112
+ end
113
+
114
+ if value.nil? && formula.nil?
115
+ buf << '<c r="' << col_ref << '"'
116
+ buf << ' s="' << style_id.to_s << '"' if style_id
117
+ buf << "/>"
96
118
  next
97
119
  end
98
120
 
@@ -148,86 +170,70 @@ module Xlsxrb
148
170
  end
149
171
  end
150
172
 
151
- io.write('<c r="')
152
- io.write(col_ref)
153
- io.write('"')
154
- if style_id
155
- io.write(' s="')
156
- io.write(style_id.to_s)
157
- io.write('"')
158
- end
159
- if type
160
- io.write(' t="')
161
- io.write(type)
162
- io.write('"')
163
- end
173
+ buf << '<c r="' << col_ref << '"'
174
+ buf << ' s="' << style_id.to_s << '"' if style_id
175
+ buf << ' t="' << type << '"' if type
164
176
 
165
177
  if type == "inlineStr"
166
178
  if xml_val.is_a?(Xlsxrb::Elements::RichText)
167
- io.write("><is>")
179
+ buf << "><is>"
168
180
  xml_val.runs.each do |run|
169
181
  font = run[:font]
170
182
  if font && !font.empty?
171
- io.write("<r><rPr>")
172
- io.write("<b/>") if font[:bold]
173
- io.write("<i/>") if font[:italic]
174
- io.write("<strike/>") if font[:strike]
183
+ buf << "<r><rPr>"
184
+ buf << "<b/>" if font[:bold]
185
+ buf << "<i/>" if font[:italic]
186
+ buf << "<strike/>" if font[:strike]
175
187
  if font[:underline]
176
188
  if font[:underline] == true
177
- io.write("<u/>")
189
+ buf << "<u/>"
178
190
  else
179
- io.write("<u val=\"#{font[:underline]}\"/>")
191
+ buf << '<u val="' << font[:underline].to_s << '"/>'
180
192
  end
181
193
  end
182
- io.write("<vertAlign val=\"#{font[:vert_align]}\"/>") if font[:vert_align]
183
- io.write("<sz val=\"#{font[:sz]}\"/>") if font[:sz]
194
+ buf << '<vertAlign val="' << font[:vert_align].to_s << '"/>' if font[:vert_align]
195
+ buf << '<sz val="' << font[:sz].to_s << '"/>' if font[:sz]
184
196
  if font[:color]
185
- io.write("<color rgb=\"#{font[:color]}\"/>")
197
+ buf << '<color rgb="' << font[:color].to_s << '"/>'
186
198
  elsif font[:theme]
187
199
  tint_attr = font[:tint] ? " tint=\"#{font[:tint]}\"" : ""
188
- io.write("<color theme=\"#{font[:theme]}\"#{tint_attr}/>")
200
+ buf << '<color theme="' << font[:theme].to_s << '"' << tint_attr << "/>"
189
201
  end
190
- io.write("<rFont val=\"#{escape_xml(font[:name])}\"/>") if font[:name]
191
- io.write("<family val=\"#{font[:family]}\"/>") if font[:family]
192
- io.write("<scheme val=\"#{font[:scheme]}\"/>") if font[:scheme]
193
- io.write("</rPr><t>")
202
+ buf << '<rFont val="' << escape_xml(font[:name]) << '"/>' if font[:name]
203
+ buf << '<family val="' << font[:family].to_s << '"/>' if font[:family]
204
+ buf << '<scheme val="' << font[:scheme].to_s << '"/>' if font[:scheme]
205
+ buf << "</rPr><t>"
194
206
  else
195
- io.write("<r><t>")
207
+ buf << "<r><t>"
196
208
  end
197
- io.write(escape_xml(run[:text]))
198
- io.write("</t></r>")
209
+ buf << escape_xml(run[:text]) << "</t></r>"
199
210
  end
200
- io.write("</is></c>")
211
+ buf << "</is></c>"
201
212
  else
202
- io.write("><is><t>")
203
- io.write(escape_xml(xml_val.to_s))
204
- io.write("</t></is></c>")
213
+ buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
205
214
  end
206
215
  elsif formula_expr
207
- if formula_ca
208
- io.write('><f ca="1">')
209
- else
210
- io.write("><f>")
211
- end
212
- io.write(escape_xml(formula_expr))
213
- io.write("</f>")
216
+ buf << if formula_ca
217
+ '><f ca="1">'
218
+ else
219
+ "><f>"
220
+ end
221
+ buf << escape_xml(formula_expr) << "</f>"
214
222
  if xml_val
215
- io.write("<v>")
216
- io.write(xml_val.to_s)
217
- io.write("</v></c>")
223
+ buf << "<v>" << xml_val.to_s << "</v></c>"
218
224
  else
219
- io.write("</c>")
225
+ buf << "</c>"
220
226
  end
221
227
  else
222
- io.write("><v>")
223
- io.write(xml_val.to_s)
224
- io.write("</v></c>")
228
+ buf << "><v>" << xml_val.to_s << "</v></c>"
225
229
  end
226
230
  end
227
231
 
228
- unmapped.each { |node| @builder.write_unmapped(node) }
232
+ buf << "</row>"
233
+ @io.write(buf)
234
+ buf.clear
229
235
 
230
- io.write("</row>")
236
+ unmapped.each { |node| @builder.write_unmapped(node) }
231
237
  end
232
238
 
233
239
  # Highly optimized row writing for StreamWriter that avoids allocating intermediate Hashes.
@@ -239,24 +245,15 @@ module Xlsxrb
239
245
  is_styles_collection = styles && (styles.is_a?(Array) || styles.is_a?(Hash))
240
246
  single_style_id = nil
241
247
  single_style_id = style_map[styles] if styles && style_map && !is_styles_collection
242
- io = @io
243
- io.write("<row r=\"")
244
- io.write(row_num_str)
245
- io.write('"')
248
+
249
+ buf = @row_buffer ||= String.new(capacity: 65_536)
250
+ buf << '<row r="' << row_num_str << '"'
246
251
  if attrs
247
- if attrs[:height]
248
- io.write(' ht="')
249
- io.write(attrs[:height].to_s)
250
- io.write('" customHeight="1"')
251
- end
252
- io.write(' hidden="1"') if attrs[:hidden]
253
- if attrs[:outline_level]
254
- io.write(' outlineLevel="')
255
- io.write(attrs[:outline_level].to_s)
256
- io.write('"')
257
- end
252
+ buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"' if attrs[:height]
253
+ buf << ' hidden="1"' if attrs[:hidden]
254
+ buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level]
258
255
  end
259
- io.write(">")
256
+ buf << ">"
260
257
 
261
258
  max_len = values.length
262
259
  if is_styles_collection
@@ -280,29 +277,54 @@ module Xlsxrb
280
277
  col_ref = column_letter(col_index)
281
278
 
282
279
  if value.nil?
283
- if style_id
284
- io.write('<c r="')
285
- io.write(col_ref)
286
- io.write(row_num_str)
287
- io.write('" s="')
288
- io.write(style_id.to_s)
289
- io.write('"/>')
290
- end
280
+ buf << '<c r="' << col_ref << row_num_str << '" s="' << style_id.to_s << '"/>' if style_id
291
281
  col_index += 1
292
282
  next
293
283
  end
294
284
 
295
- # Auto-convert string starting with '=' to formula
296
- if value.is_a?(String) && value.start_with?("=")
297
- formula_expr = value
298
- xml_val = nil
299
- else
300
- formula_expr = nil
301
- xml_val = value
285
+ # Fast path: unstyled numbers, booleans, and simple strings (majority of cells)
286
+ if style_id.nil? && !value.is_a?(Xlsxrb::Elements::Formula) && !value.is_a?(Hash)
287
+ case value
288
+ when Integer, Float
289
+ buf << '<c r="' << col_ref << row_num_str << '"><v>' << value.to_s << "</v></c>"
290
+ col_index += 1
291
+ next
292
+ when String
293
+ unless value.start_with?("=")
294
+ idx = sst_index[value]
295
+ unless idx
296
+ sst << value
297
+ idx = sst.size - 1
298
+ sst_index[value] = idx
299
+ end
300
+ buf << '<c r="' << col_ref << row_num_str << '" t="s"><v>' << idx.to_s << "</v></c>"
301
+ col_index += 1
302
+ next
303
+ end
304
+ when true
305
+ buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>1</v></c>'
306
+ col_index += 1
307
+ next
308
+ when false
309
+ buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>0</v></c>'
310
+ col_index += 1
311
+ next
312
+ when Date
313
+ buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.date_to_serial(value).to_s << "</v></c>"
314
+ col_index += 1
315
+ next
316
+ when Time
317
+ buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.datetime_to_serial(value).to_s << "</v></c>"
318
+ col_index += 1
319
+ next
320
+ end
302
321
  end
303
322
 
304
- type = nil
323
+ # General path: styled, formula, rich text, or complex cell
324
+ formula_expr = nil
305
325
  formula_ca = false
326
+ xml_val = value
327
+ type = nil
306
328
 
307
329
  case value
308
330
  when Xlsxrb::Elements::Formula
@@ -336,8 +358,9 @@ module Xlsxrb
336
358
  end
337
359
  end
338
360
  when String
339
- if value.start_with?("=")
340
- # already set formula_expr
361
+ if value.start_with?("=") && value.length > 1
362
+ formula_expr = value
363
+ xml_val = nil
341
364
  else
342
365
  idx = sst_index[value]
343
366
  unless idx
@@ -376,45 +399,31 @@ module Xlsxrb
376
399
 
377
400
  formula_expr = formula_expr[1..] if formula_expr&.start_with?("=")
378
401
 
379
- io.write('<c r="')
380
- io.write(col_ref)
381
- io.write(row_num_str)
382
- io.write('"')
383
- if style_id
384
- io.write(' s="')
385
- io.write(style_id.to_s)
386
- io.write('"')
387
- end
388
- if type
389
- io.write(' t="')
390
- io.write(type)
391
- io.write('"')
392
- end
402
+ buf << '<c r="' << col_ref << row_num_str << '"'
403
+ buf << ' s="' << style_id.to_s << '"' if style_id
404
+ buf << ' t="' << type << '"' if type
393
405
  if formula_expr
394
- if formula_ca
395
- io.write('><f ca="1">')
396
- else
397
- io.write("><f>")
398
- end
399
- io.write(escape_xml(formula_expr))
400
- io.write("</f>")
406
+ buf << if formula_ca
407
+ '><f ca="1">'
408
+ else
409
+ "><f>"
410
+ end
411
+ buf << escape_xml(formula_expr) << "</f>"
401
412
  if xml_val
402
- io.write("<v>")
403
- io.write(xml_val.to_s)
404
- io.write("</v></c>")
413
+ buf << "<v>" << xml_val.to_s << "</v></c>"
405
414
  else
406
- io.write("</c>")
415
+ buf << "</c>"
407
416
  end
408
417
  else
409
- io.write("><v>")
410
- io.write(xml_val.to_s)
411
- io.write("</v></c>")
418
+ buf << "><v>" << xml_val.to_s << "</v></c>"
412
419
  end
413
420
 
414
421
  col_index += 1
415
422
  end
416
423
 
417
- io.write("</row>")
424
+ buf << "</row>"
425
+ @io.write(buf)
426
+ buf.clear
418
427
  end
419
428
 
420
429
  # Write the worksheet footer. Call once after all rows.
@@ -430,6 +439,10 @@ module Xlsxrb
430
439
 
431
440
  start unless @started
432
441
  @finished = true
442
+ if @row_buffer && !@row_buffer.empty?
443
+ @io.write(@row_buffer)
444
+ @row_buffer.clear
445
+ end
433
446
  @builder.close_tag("sheetData")
434
447
 
435
448
  # Elements must appear in OOXML specification order after sheetData
@@ -1035,12 +1048,14 @@ module Xlsxrb
1035
1048
  Xlsxrb::Elements::Cell.column_letter(index)
1036
1049
  end
1037
1050
 
1051
+ INVALID_XML_CHARS_RE = /[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/
1038
1052
  ESCAPE_RE = /[&<>"']/
1039
1053
  ESCAPE_MAP = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;", "'" => "&apos;" }.freeze
1040
- private_constant :ESCAPE_RE, :ESCAPE_MAP
1054
+ private_constant :INVALID_XML_CHARS_RE, :ESCAPE_RE, :ESCAPE_MAP
1041
1055
 
1042
1056
  def escape_xml(value)
1043
- str = value.to_s.gsub(/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/, "")
1057
+ str = value.to_s
1058
+ str = str.gsub(INVALID_XML_CHARS_RE, "") if str.match?(INVALID_XML_CHARS_RE)
1044
1059
  str.match?(ESCAPE_RE) ? str.gsub(ESCAPE_RE, ESCAPE_MAP) : str
1045
1060
  end
1046
1061
  end
@@ -27,8 +27,10 @@ module Xlsxrb
27
27
  self
28
28
  end
29
29
 
30
+ EMPTY_HASH = {}.freeze
31
+
30
32
  # Opens a tag, yields for children, then closes the tag.
31
- def tag(name, attrs = {}, &block)
33
+ def tag(name, attrs = EMPTY_HASH, &block)
32
34
  if block
33
35
  open_tag(name, attrs)
34
36
  yield self
@@ -39,22 +41,26 @@ module Xlsxrb
39
41
  self
40
42
  end
41
43
 
42
- def open_tag(name, attrs = {})
43
- @io << "<#{name}"
44
- write_attrs(attrs)
45
- @io << ">"
44
+ def open_tag(name, attrs = EMPTY_HASH)
45
+ @io.write("<")
46
+ @io.write(name)
47
+ write_attrs(attrs) unless attrs.empty?
48
+ @io.write(">")
46
49
  self
47
50
  end
48
51
 
49
52
  def close_tag(name)
50
- @io << "</#{name}>"
53
+ @io.write("</")
54
+ @io.write(name)
55
+ @io.write(">")
51
56
  self
52
57
  end
53
58
 
54
- def empty_tag(name, attrs = {})
55
- @io << "<#{name}"
56
- write_attrs(attrs)
57
- @io << "/>"
59
+ def empty_tag(name, attrs = EMPTY_HASH)
60
+ @io.write("<")
61
+ @io.write(name)
62
+ write_attrs(attrs) unless attrs.empty?
63
+ @io.write("/>")
58
64
  self
59
65
  end
60
66