xlsxrb 0.1.4 → 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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +1 -1
  3. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
  4. data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
  5. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
  6. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
  7. data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +16 -0
  8. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +96 -0
  9. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
  10. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +68 -0
  11. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
  12. data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +4 -0
  13. data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +47 -0
  14. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
  15. data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +9 -0
  16. data/.gem_rbs_collection/csv/3.3/csv.rbs +3871 -0
  17. data/.gem_rbs_collection/csv/3.3/manifest.yaml +3 -0
  18. data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +9 -0
  19. data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +48 -0
  20. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
  21. data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
  22. data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
  23. data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +9 -0
  24. data/.gem_rbs_collection/logger/1.7/formatter.rbs +45 -0
  25. data/.gem_rbs_collection/logger/1.7/log_device.rbs +100 -0
  26. data/.gem_rbs_collection/logger/1.7/logger.rbs +796 -0
  27. data/.gem_rbs_collection/logger/1.7/manifest.yaml +2 -0
  28. data/.gem_rbs_collection/logger/1.7/period.rbs +17 -0
  29. data/.gem_rbs_collection/logger/1.7/severity.rbs +34 -0
  30. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
  31. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
  32. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
  33. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
  34. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
  35. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
  36. data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
  37. data/.gem_rbs_collection/parser/3.2/parser.rbs +194 -0
  38. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
  39. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
  40. data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
  41. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
  42. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
  43. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  44. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  45. data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
  46. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
  47. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
  48. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
  49. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +208 -0
  50. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
  51. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +903 -0
  52. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
  53. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
  54. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
  55. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
  56. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
  57. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
  58. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
  59. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
  60. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
  61. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
  62. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
  63. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
  64. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
  65. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
  66. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
  67. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
  68. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
  69. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
  70. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
  71. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
  72. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
  73. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
  74. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
  75. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
  76. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
  77. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
  78. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
  79. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
  80. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
  81. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
  82. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
  83. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
  84. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
  85. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
  86. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
  87. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
  88. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
  89. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
  90. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
  91. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
  92. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
  93. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
  94. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
  95. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
  96. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
  97. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
  98. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
  99. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
  100. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
  101. data/CHANGELOG.md +42 -3
  102. data/README.md +130 -49
  103. data/Rakefile +23 -2
  104. data/Steepfile +17 -0
  105. data/benchmark.rb +324 -348
  106. data/docs/ARCHITECTURE.md +24 -0
  107. data/docs/DEVELOPMENT.md +58 -0
  108. data/docs/QUALITY_ASSURANCE.md +26 -0
  109. data/docs/visual/VisualGallery.md +893 -938
  110. data/docs/wasm/ruby.wasm +0 -0
  111. data/lib/xlsxrb/elements/cell.rb +113 -8
  112. data/lib/xlsxrb/elements/column.rb +2 -0
  113. data/lib/xlsxrb/elements/row.rb +52 -4
  114. data/lib/xlsxrb/elements/types.rb +6 -0
  115. data/lib/xlsxrb/elements/workbook.rb +29 -0
  116. data/lib/xlsxrb/elements/worksheet.rb +98 -1
  117. data/lib/xlsxrb/elements.rb +2 -0
  118. data/lib/xlsxrb/ooxml/reader.rb +8 -6
  119. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +106 -40
  120. data/lib/xlsxrb/ooxml/styles_parser.rb +2 -0
  121. data/lib/xlsxrb/ooxml/utils.rb +2 -0
  122. data/lib/xlsxrb/ooxml/workbook_parser.rb +6 -1
  123. data/lib/xlsxrb/ooxml/workbook_writer.rb +15 -4
  124. data/lib/xlsxrb/ooxml/worksheet_parser.rb +156 -23
  125. data/lib/xlsxrb/ooxml/worksheet_writer.rb +207 -152
  126. data/lib/xlsxrb/ooxml/writer.rb +217 -5
  127. data/lib/xlsxrb/ooxml/xml_builder.rb +18 -10
  128. data/lib/xlsxrb/ooxml/xml_parser.rb +2 -0
  129. data/lib/xlsxrb/ooxml/zip_generator.rb +5 -8
  130. data/lib/xlsxrb/ooxml/zip_reader.rb +179 -57
  131. data/lib/xlsxrb/ooxml/zip_writer.rb +65 -25
  132. data/lib/xlsxrb/ooxml.rb +3 -1
  133. data/lib/xlsxrb/style_builder.rb +284 -36
  134. data/lib/xlsxrb/version.rb +3 -1
  135. data/lib/xlsxrb.rb +1761 -275
  136. data/rbs_collection.lock.yaml +252 -0
  137. data/rbs_collection.yaml +19 -0
  138. data/sig/generated/xlsxrb/elements/cell.rbs +30 -0
  139. data/sig/generated/xlsxrb/elements/column.rbs +30 -0
  140. data/sig/generated/xlsxrb/elements/row.rbs +32 -0
  141. data/sig/generated/xlsxrb/elements/types.rbs +74 -0
  142. data/sig/generated/xlsxrb/elements/workbook.rbs +25 -0
  143. data/sig/generated/xlsxrb/elements/worksheet.rbs +27 -0
  144. data/sig/generated/xlsxrb/elements.rbs +8 -0
  145. data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
  146. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
  147. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
  148. data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
  149. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
  150. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
  151. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
  152. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
  153. data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
  154. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
  155. data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
  156. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
  157. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
  158. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
  159. data/sig/generated/xlsxrb/ooxml.rbs +23 -0
  160. data/sig/generated/xlsxrb/style_builder.rbs +250 -0
  161. data/sig/generated/xlsxrb/version.rbs +5 -0
  162. data/sig/generated/xlsxrb.rbs +1276 -0
  163. data/sig/rexml.rbs +4 -0
  164. metadata +145 -4
  165. data/measure_memory.rb +0 -42
  166. data/sig/xlsxrb.rbs +0 -23
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rbs_inline: enabled
4
+
3
5
  require "stringio"
4
6
  require_relative "xml_builder"
5
7
 
@@ -14,6 +16,7 @@ module Xlsxrb
14
16
  def initialize(io)
15
17
  @io = io
16
18
  @builder = XmlBuilder.new(@io)
19
+ @row_buffer = String.new(capacity: 65_536)
17
20
  @started = false
18
21
  @finished = false
19
22
  end
@@ -45,27 +48,27 @@ module Xlsxrb
45
48
 
46
49
  row_num = row_index + 1
47
50
  row_num_str = row_num.to_s
48
- io = @io
49
- io.write("<row r=\"")
50
- io.write(row_num_str)
51
- io.write('"')
51
+ buf = @row_buffer
52
+ buf.clear
53
+ buf << '<row r="' << row_num_str << '"'
52
54
  if attrs[:height]
53
- io.write(' ht="')
54
- io.write(attrs[:height].to_s)
55
- io.write('" customHeight="1"')
55
+ buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"'
56
56
  elsif attrs[:custom_height]
57
- io.write(' customHeight="1"')
58
- end
59
- io.write(' hidden="1"') if attrs[:hidden]
60
- if attrs[:outline_level]
61
- io.write(' outlineLevel="')
62
- io.write(attrs[:outline_level].to_s)
63
- io.write('"')
57
+ buf << ' customHeight="1"'
64
58
  end
65
- io.write(">")
59
+ buf << ' hidden="1"' if attrs[:hidden]
60
+ buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level]
61
+ buf << ">"
66
62
 
67
63
  cells.each do |cell|
68
- 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)
69
72
  value = cell[:value]
70
73
  style_id = cell[:style_index]
71
74
  col_ref = cell[:ref] || "#{column_letter(cell[:column_index])}#{row_num_str}"
@@ -73,24 +76,45 @@ module Xlsxrb
73
76
  formula_ca = cell[:formula_ca]
74
77
  cell_type_val = cell[:type]
75
78
  else
76
- value = cell.value
77
- style_id = cell.style_index
78
- col_ref = cell.ref || "#{column_letter(cell.column_index)}#{row_num_str}"
79
- formula = cell.formula
79
+ value = cell
80
+ style_id = nil
81
+ col_ref = nil
82
+ formula = nil
80
83
  formula_ca = false
81
84
  cell_type_val = nil
82
85
  end
83
86
 
84
- if value.nil? && formula.nil?
85
- io.write('<c r="')
86
- io.write(col_ref)
87
- io.write('"')
88
- if style_id
89
- io.write(' s="')
90
- io.write(style_id.to_s)
91
- 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
92
111
  end
93
- 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 << "/>"
94
118
  next
95
119
  end
96
120
 
@@ -141,89 +165,75 @@ module Xlsxrb
141
165
  xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value)
142
166
  when Time
143
167
  xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
168
+ when BigDecimal
169
+ xml_val = value.to_s("F")
144
170
  end
145
171
  end
146
172
 
147
- io.write('<c r="')
148
- io.write(col_ref)
149
- io.write('"')
150
- if style_id
151
- io.write(' s="')
152
- io.write(style_id.to_s)
153
- io.write('"')
154
- end
155
- if type
156
- io.write(' t="')
157
- io.write(type)
158
- io.write('"')
159
- end
173
+ buf << '<c r="' << col_ref << '"'
174
+ buf << ' s="' << style_id.to_s << '"' if style_id
175
+ buf << ' t="' << type << '"' if type
160
176
 
161
177
  if type == "inlineStr"
162
178
  if xml_val.is_a?(Xlsxrb::Elements::RichText)
163
- io.write("><is>")
179
+ buf << "><is>"
164
180
  xml_val.runs.each do |run|
165
181
  font = run[:font]
166
182
  if font && !font.empty?
167
- io.write("<r><rPr>")
168
- io.write("<b/>") if font[:bold]
169
- io.write("<i/>") if font[:italic]
170
- 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]
171
187
  if font[:underline]
172
188
  if font[:underline] == true
173
- io.write("<u/>")
189
+ buf << "<u/>"
174
190
  else
175
- io.write("<u val=\"#{font[:underline]}\"/>")
191
+ buf << '<u val="' << font[:underline].to_s << '"/>'
176
192
  end
177
193
  end
178
- io.write("<vertAlign val=\"#{font[:vert_align]}\"/>") if font[:vert_align]
179
- 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]
180
196
  if font[:color]
181
- io.write("<color rgb=\"#{font[:color]}\"/>")
197
+ buf << '<color rgb="' << font[:color].to_s << '"/>'
182
198
  elsif font[:theme]
183
199
  tint_attr = font[:tint] ? " tint=\"#{font[:tint]}\"" : ""
184
- io.write("<color theme=\"#{font[:theme]}\"#{tint_attr}/>")
200
+ buf << '<color theme="' << font[:theme].to_s << '"' << tint_attr << "/>"
185
201
  end
186
- io.write("<rFont val=\"#{escape_xml(font[:name])}\"/>") if font[:name]
187
- io.write("<family val=\"#{font[:family]}\"/>") if font[:family]
188
- io.write("<scheme val=\"#{font[:scheme]}\"/>") if font[:scheme]
189
- 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>"
190
206
  else
191
- io.write("<r><t>")
207
+ buf << "<r><t>"
192
208
  end
193
- io.write(escape_xml(run[:text]))
194
- io.write("</t></r>")
209
+ buf << escape_xml(run[:text]) << "</t></r>"
195
210
  end
196
- io.write("</is></c>")
211
+ buf << "</is></c>"
197
212
  else
198
- io.write("><is><t>")
199
- io.write(escape_xml(xml_val.to_s))
200
- io.write("</t></is></c>")
213
+ buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
201
214
  end
202
215
  elsif formula_expr
203
- if formula_ca
204
- io.write('><f ca="1">')
205
- else
206
- io.write("><f>")
207
- end
208
- io.write(escape_xml(formula_expr))
209
- 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>"
210
222
  if xml_val
211
- io.write("<v>")
212
- io.write(xml_val.to_s)
213
- io.write("</v></c>")
223
+ buf << "<v>" << xml_val.to_s << "</v></c>"
214
224
  else
215
- io.write("</c>")
225
+ buf << "</c>"
216
226
  end
217
227
  else
218
- io.write("><v>")
219
- io.write(xml_val.to_s)
220
- io.write("</v></c>")
228
+ buf << "><v>" << xml_val.to_s << "</v></c>"
221
229
  end
222
230
  end
223
231
 
224
- unmapped.each { |node| @builder.write_unmapped(node) }
232
+ buf << "</row>"
233
+ @io.write(buf)
234
+ buf.clear
225
235
 
226
- io.write("</row>")
236
+ unmapped.each { |node| @builder.write_unmapped(node) }
227
237
  end
228
238
 
229
239
  # Highly optimized row writing for StreamWriter that avoids allocating intermediate Hashes.
@@ -232,61 +242,89 @@ module Xlsxrb
232
242
 
233
243
  row_num = row_index + 1
234
244
  row_num_str = row_num.to_s
235
- style_lookup_enabled = styles && style_map && (styles.is_a?(Array) || styles.is_a?(Hash))
236
- io = @io
237
- io.write("<row r=\"")
238
- io.write(row_num_str)
239
- io.write('"')
245
+ is_styles_collection = styles && (styles.is_a?(Array) || styles.is_a?(Hash))
246
+ single_style_id = nil
247
+ single_style_id = style_map[styles] if styles && style_map && !is_styles_collection
248
+
249
+ buf = @row_buffer ||= String.new(capacity: 65_536)
250
+ buf << '<row r="' << row_num_str << '"'
240
251
  if attrs
241
- if attrs[:height]
242
- io.write(' ht="')
243
- io.write(attrs[:height].to_s)
244
- io.write('" customHeight="1"')
245
- end
246
- io.write(' hidden="1"') if attrs[:hidden]
247
- if attrs[:outline_level]
248
- io.write(' outlineLevel="')
249
- io.write(attrs[:outline_level].to_s)
250
- io.write('"')
251
- 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]
255
+ end
256
+ buf << ">"
257
+
258
+ max_len = values.length
259
+ if is_styles_collection
260
+ styles_len = styles.is_a?(Array) ? styles.length : (styles.keys.max || -1) + 1
261
+ max_len = [max_len, styles_len].max
252
262
  end
253
- io.write(">")
254
263
 
255
264
  col_index = 0
256
- while col_index < values.length
257
- value = values[col_index]
258
- style_id = nil
259
- if style_lookup_enabled
260
- style_name = styles[col_index]
265
+ while col_index < max_len
266
+ value = col_index < values.length ? values[col_index] : nil
267
+ style_id = single_style_id
268
+ if is_styles_collection && style_map
269
+ style_name = if styles.is_a?(Array)
270
+ col_index < styles.length ? styles[col_index] : nil
271
+ else
272
+ styles[col_index]
273
+ end
261
274
  style_id = style_map[style_name] if style_name
262
275
  end
263
276
 
264
277
  col_ref = column_letter(col_index)
265
278
 
266
279
  if value.nil?
267
- if style_id
268
- io.write('<c r="')
269
- io.write(col_ref)
270
- io.write(row_num_str)
271
- io.write('" s="')
272
- io.write(style_id.to_s)
273
- io.write('"/>')
274
- end
280
+ buf << '<c r="' << col_ref << row_num_str << '" s="' << style_id.to_s << '"/>' if style_id
275
281
  col_index += 1
276
282
  next
277
283
  end
278
284
 
279
- # Auto-convert string starting with '=' to formula
280
- if value.is_a?(String) && value.start_with?("=")
281
- formula_expr = value
282
- xml_val = nil
283
- else
284
- formula_expr = nil
285
- 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
286
321
  end
287
322
 
288
- type = nil
323
+ # General path: styled, formula, rich text, or complex cell
324
+ formula_expr = nil
289
325
  formula_ca = false
326
+ xml_val = value
327
+ type = nil
290
328
 
291
329
  case value
292
330
  when Xlsxrb::Elements::Formula
@@ -303,9 +341,26 @@ module Xlsxrb
303
341
  type = "b"
304
342
  xml_val = "0"
305
343
  end
344
+ when Hash
345
+ if value.key?(:formula)
346
+ formula_expr = value[:formula]
347
+ formula_ca = value[:calculate_always] || false
348
+ xml_val = value[:value]
349
+ case xml_val
350
+ when String
351
+ type = "str"
352
+ when true
353
+ type = "b"
354
+ xml_val = "1"
355
+ when false
356
+ type = "b"
357
+ xml_val = "0"
358
+ end
359
+ end
306
360
  when String
307
- if value.start_with?("=")
308
- # already set formula_expr
361
+ if value.start_with?("=") && value.length > 1
362
+ formula_expr = value
363
+ xml_val = nil
309
364
  else
310
365
  idx = sst_index[value]
311
366
  unless idx
@@ -335,6 +390,8 @@ module Xlsxrb
335
390
  xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value)
336
391
  when Time
337
392
  xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
393
+ when BigDecimal
394
+ xml_val = value.to_s("F")
338
395
  when Xlsxrb::Elements::CellError
339
396
  xml_val = value.code
340
397
  type = "e"
@@ -342,45 +399,31 @@ module Xlsxrb
342
399
 
343
400
  formula_expr = formula_expr[1..] if formula_expr&.start_with?("=")
344
401
 
345
- io.write('<c r="')
346
- io.write(col_ref)
347
- io.write(row_num_str)
348
- io.write('"')
349
- if style_id
350
- io.write(' s="')
351
- io.write(style_id.to_s)
352
- io.write('"')
353
- end
354
- if type
355
- io.write(' t="')
356
- io.write(type)
357
- io.write('"')
358
- 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
359
405
  if formula_expr
360
- if formula_ca
361
- io.write('><f ca="1">')
362
- else
363
- io.write("><f>")
364
- end
365
- io.write(escape_xml(formula_expr))
366
- 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>"
367
412
  if xml_val
368
- io.write("<v>")
369
- io.write(xml_val.to_s)
370
- io.write("</v></c>")
413
+ buf << "<v>" << xml_val.to_s << "</v></c>"
371
414
  else
372
- io.write("</c>")
415
+ buf << "</c>"
373
416
  end
374
417
  else
375
- io.write("><v>")
376
- io.write(xml_val.to_s)
377
- io.write("</v></c>")
418
+ buf << "><v>" << xml_val.to_s << "</v></c>"
378
419
  end
379
420
 
380
421
  col_index += 1
381
422
  end
382
423
 
383
- io.write("</row>")
424
+ buf << "</row>"
425
+ @io.write(buf)
426
+ buf.clear
384
427
  end
385
428
 
386
429
  # Write the worksheet footer. Call once after all rows.
@@ -396,6 +439,10 @@ module Xlsxrb
396
439
 
397
440
  start unless @started
398
441
  @finished = true
442
+ if @row_buffer && !@row_buffer.empty?
443
+ @io.write(@row_buffer)
444
+ @row_buffer.clear
445
+ end
399
446
  @builder.close_tag("sheetData")
400
447
 
401
448
  # Elements must appear in OOXML specification order after sheetData
@@ -747,6 +794,11 @@ module Xlsxrb
747
794
  def write_data_validations(validations)
748
795
  @builder.open_tag("dataValidations", { count: validations.size.to_s })
749
796
  validations.each do |dv|
797
+ if dv[:in].is_a?(Array)
798
+ dv = dv.dup
799
+ dv[:type] = "list"
800
+ dv[:formula1] = %("#{dv[:in].join(",")}")
801
+ end
750
802
  dv_attrs = { sqref: dv[:sqref] }
751
803
  dv_type = dv[:type]
752
804
  dv_attrs[:type] = dv_type.to_s if dv_type
@@ -913,8 +965,9 @@ module Xlsxrb
913
965
 
914
966
  def write_sparkline_color(tag, color)
915
967
  attrs = {}
916
- if color.is_a?(String)
917
- attrs[:rgb] = color
968
+ if color.is_a?(String) || color.is_a?(Symbol)
969
+ resolved = Xlsxrb::StyleBuilder.new.resolve_color(color)
970
+ attrs[:rgb] = resolved
918
971
  else
919
972
  attrs[:rgb] = color[:rgb] if color[:rgb]
920
973
  attrs[:theme] = color[:theme].to_s if color[:theme]
@@ -995,12 +1048,14 @@ module Xlsxrb
995
1048
  Xlsxrb::Elements::Cell.column_letter(index)
996
1049
  end
997
1050
 
1051
+ INVALID_XML_CHARS_RE = /[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/
998
1052
  ESCAPE_RE = /[&<>"']/
999
1053
  ESCAPE_MAP = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;", "'" => "&apos;" }.freeze
1000
- private_constant :ESCAPE_RE, :ESCAPE_MAP
1054
+ private_constant :INVALID_XML_CHARS_RE, :ESCAPE_RE, :ESCAPE_MAP
1001
1055
 
1002
1056
  def escape_xml(value)
1003
1057
  str = value.to_s
1058
+ str = str.gsub(INVALID_XML_CHARS_RE, "") if str.match?(INVALID_XML_CHARS_RE)
1004
1059
  str.match?(ESCAPE_RE) ? str.gsub(ESCAPE_RE, ESCAPE_MAP) : str
1005
1060
  end
1006
1061
  end