xlsxrb 0.1.15 → 0.1.16

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +22 -3
  4. data/benchmark/micro_ips.rb +70 -0
  5. data/lib/xlsxrb/elements/styles.rb +427 -15
  6. data/lib/xlsxrb/elements/workbook.rb +31 -4
  7. data/lib/xlsxrb/elements/worksheet.rb +22 -7
  8. data/lib/xlsxrb/ooxml/cfb.rb +10 -0
  9. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +56 -38
  10. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
  11. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +12 -7
  12. data/lib/xlsxrb/ooxml/reader.rb +165 -136
  13. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +83 -28
  14. data/lib/xlsxrb/ooxml/styles_parser.rb +57 -14
  15. data/lib/xlsxrb/ooxml/utils.rb +59 -2
  16. data/lib/xlsxrb/ooxml/workbook_parser.rb +42 -3
  17. data/lib/xlsxrb/ooxml/workbook_writer.rb +215 -50
  18. data/lib/xlsxrb/ooxml/worksheet_parser.rb +192 -123
  19. data/lib/xlsxrb/ooxml/worksheet_writer.rb +238 -108
  20. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +16 -12
  21. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +64 -15
  22. data/lib/xlsxrb/ooxml/writer.rb +31 -28
  23. data/lib/xlsxrb/ooxml/xml_builder.rb +12 -4
  24. data/lib/xlsxrb/ooxml/xml_parser.rb +45 -0
  25. data/lib/xlsxrb/ooxml/zip_reader.rb +48 -29
  26. data/lib/xlsxrb/stream_row.rb +4 -3
  27. data/lib/xlsxrb/stream_sheet.rb +26 -6
  28. data/lib/xlsxrb/stream_writer.rb +35 -14
  29. data/lib/xlsxrb/utils.rb +15 -0
  30. data/lib/xlsxrb/version.rb +1 -1
  31. data/lib/xlsxrb/workbook_builder.rb +110 -10
  32. data/lib/xlsxrb/worksheet_builder.rb +15 -7
  33. data/lib/xlsxrb.rb +416 -108
  34. data/sig/generated/xlsxrb/elements/styles.rbs +271 -5
  35. data/sig/generated/xlsxrb/elements/workbook.rbs +26 -3
  36. data/sig/generated/xlsxrb/elements/worksheet.rbs +12 -2
  37. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +2 -0
  38. data/sig/generated/xlsxrb/ooxml/reader.rbs +12 -0
  39. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +3 -1
  40. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +2 -0
  41. data/sig/generated/xlsxrb/ooxml/utils.rbs +22 -0
  42. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +7 -0
  43. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +12 -0
  44. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +18 -2
  45. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
  46. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +5 -2
  47. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +4 -1
  48. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +6 -6
  49. data/sig/generated/xlsxrb/stream_row.rbs +3 -2
  50. data/sig/generated/xlsxrb/stream_sheet.rbs +21 -5
  51. data/sig/generated/xlsxrb/stream_writer.rbs +18 -11
  52. data/sig/generated/xlsxrb/utils.rbs +13 -0
  53. data/sig/generated/xlsxrb/worksheet_builder.rbs +3 -2
  54. data/sig/generated/xlsxrb.rbs +47 -10
  55. metadata +2 -1
@@ -24,11 +24,14 @@ module Xlsxrb
24
24
  result.freeze
25
25
  end.freeze
26
26
 
27
- INTEGER_STRINGS = (0..65_535).map(&:to_s).freeze
27
+ INTEGER_STRINGS = (0..65_535).map { |n| -n.to_s }.freeze
28
28
 
29
- def initialize(io, date1904: false)
29
+ attr_accessor :escape_formulas
30
+
31
+ def initialize(io, date1904: false, escape_formulas: false)
30
32
  @io = io
31
33
  @date1904 = date1904 ? true : false
34
+ @escape_formulas = escape_formulas ? true : false
32
35
  @builder = XmlBuilder.new(@io)
33
36
  @row_buffer = String.new(capacity: 65_536)
34
37
  @started = false
@@ -59,12 +62,12 @@ module Xlsxrb
59
62
  columns.each do |col|
60
63
  if col.is_a?(Elements::Column)
61
64
  s = col.style_index
62
- min = col.index
63
- max = col.index
65
+ min = col.unmapped_data[:min] ? col.unmapped_data[:min] - 1 : col.index
66
+ max = col.unmapped_data[:max] ? col.unmapped_data[:max] - 1 : col.index
64
67
  else
65
68
  s = col[:style_index] || col[:style]
66
- min = col[:index] || col[:min] || 0
67
- max = col[:index] || col[:max] || col[:min] || 0
69
+ min = col[:min] ? col[:min] - 1 : (col[:index] || 0)
70
+ max = col[:max] ? col[:max] - 1 : (col[:index] || min)
68
71
  end
69
72
  next unless s
70
73
 
@@ -103,7 +106,7 @@ module Xlsxrb
103
106
  cells.each do |cell|
104
107
  if cell.is_a?(Elements::Cell)
105
108
  value = cell.value
106
- style_id = cell.style_index
109
+ cell_style_id = cell.style_index
107
110
  col_ref = cell.ref || "#{column_letter(cell.column_index)}#{row_num_str}"
108
111
  col_idx = cell.column_index
109
112
  formula = cell.formula
@@ -111,7 +114,7 @@ module Xlsxrb
111
114
  cell_type_val = nil
112
115
  elsif cell.is_a?(Hash)
113
116
  value = cell[:value]
114
- style_id = cell[:style_index]
117
+ cell_style_id = cell[:style_index]
115
118
  col_idx = cell[:column_index]
116
119
  col_ref = cell[:ref] || (col_idx ? "#{column_letter(col_idx)}#{row_num_str}" : nil)
117
120
  formula = cell[:formula]
@@ -119,7 +122,7 @@ module Xlsxrb
119
122
  cell_type_val = cell[:type]
120
123
  else
121
124
  value = cell
122
- style_id = nil
125
+ cell_style_id = nil
123
126
  col_idx = nil
124
127
  col_ref = nil
125
128
  formula = nil
@@ -128,7 +131,7 @@ module Xlsxrb
128
131
  end
129
132
 
130
133
  col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
131
- style_id ||= attrs[:style_index] || col_style
134
+ style_id = cell_style_id || attrs[:style_index] || col_style
132
135
  style_id = nil unless style_id.is_a?(Integer)
133
136
 
134
137
  # Fast path for common unstyled cells
@@ -138,7 +141,7 @@ module Xlsxrb
138
141
  buf << '<c r="' << col_ref << '"><v>' << value.to_s << "</v></c>"
139
142
  next
140
143
  when String
141
- if !value.start_with?("=") && sst_index && (idx = sst_index[value])
144
+ if sst_index && (idx = sst_index[value])
142
145
  buf << '<c r="' << col_ref << '" t="s"><v>' << idx.to_s << "</v></c>"
143
146
  next
144
147
  end
@@ -159,17 +162,14 @@ module Xlsxrb
159
162
  end
160
163
 
161
164
  if value.nil? && formula.nil?
162
- buf << '<c r="' << col_ref << '"'
163
- buf << ' s="' << style_id.to_s << '"' if style_id
164
- buf << "/>"
165
+ if cell_style_id.is_a?(Integer)
166
+ buf << '<c r="' << col_ref << '" s="' << cell_style_id.to_s << '"/>'
167
+ else
168
+ buf << '<c r="' << col_ref << '"/>'
169
+ end
165
170
  next
166
171
  end
167
172
 
168
- if value.is_a?(String) && value.start_with?("=") && !formula
169
- formula = value
170
- value = nil
171
- end
172
-
173
173
  xml_val = value
174
174
  type = cell_type_val
175
175
  formula_type = nil
@@ -183,22 +183,40 @@ module Xlsxrb
183
183
  formula_type = formula.type
184
184
  formula_ref = formula.ref
185
185
  formula_si = formula.shared_index
186
- xml_val = formula.cached_value.nil? ? (value || nil) : formula.cached_value
186
+ xml_val = formula.cached_value.nil? ? value : formula.cached_value
187
187
  elsif formula.is_a?(Hash)
188
188
  formula_expr = formula[:formula] || formula[:expression]
189
189
  formula_ca = formula[:calculate_always]
190
190
  formula_type = formula[:type]
191
191
  formula_ref = formula[:ref]
192
192
  formula_si = formula[:si] || formula[:shared_index]
193
- xml_val = formula[:value] || formula[:cached_value] || value || nil
193
+ xml_val = if formula.key?(:value)
194
+ formula[:value]
195
+ elsif formula.key?(:cached_value)
196
+ formula[:cached_value]
197
+ else
198
+ value
199
+ end
194
200
  else
195
201
  formula_expr = formula
196
- xml_val = value || nil
202
+ xml_val = value
197
203
  end
198
- formula_expr = formula_expr[1..] if formula_expr.start_with?("=")
204
+ formula_expr = formula_expr[1..] if formula_expr.is_a?(String) && formula_expr.start_with?("=")
199
205
  end
200
206
 
201
- if !formula_expr && !type
207
+ if formula_expr
208
+ case xml_val
209
+ when true
210
+ type = "b"
211
+ xml_val = "1"
212
+ when false
213
+ type = "b"
214
+ xml_val = "0"
215
+ when Xlsxrb::Elements::CellError
216
+ type = "e"
217
+ xml_val = xml_val.code
218
+ end
219
+ elsif !type
202
220
  case value
203
221
  when String
204
222
  if sst_index && (idx = sst_index[value])
@@ -290,13 +308,18 @@ module Xlsxrb
290
308
  end
291
309
  f_attrs << ' ca="1"' if formula_ca
292
310
  buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
293
- if xml_val
294
- buf << "<v>" << xml_val.to_s << "</v></c>"
295
- else
311
+ if xml_val.nil?
296
312
  buf << "<v>0</v></c>"
313
+ else
314
+ v_str = xml_val.to_s
315
+ v_str = v_str.dup.force_encoding(Encoding::UTF_8) if v_str.encoding == Encoding::BINARY
316
+ v_str = escape_xml(v_str) if type == "str" || xml_val.is_a?(String)
317
+ buf << "<v>" << v_str << "</v></c>"
297
318
  end
298
319
  else
299
- buf << "><v>" << xml_val.to_s << "</v></c>"
320
+ v_str = xml_val.to_s
321
+ v_str = v_str.dup.force_encoding(Encoding::UTF_8) if v_str.encoding == Encoding::BINARY
322
+ buf << "><v>" << v_str << "</v></c>"
300
323
  end
301
324
  end
302
325
 
@@ -318,7 +341,7 @@ module Xlsxrb
318
341
  buf = @row_buffer ||= String.new(capacity: 65_536)
319
342
 
320
343
  if styles.nil? && attrs.nil? && @column_styles.nil?
321
- buf << "<row r=\"#{row_num_str}\">"
344
+ buf << '<row r="' << row_num_str << '">'
322
345
  col_index = 0
323
346
  max_len = values.length
324
347
  while col_index < max_len
@@ -331,27 +354,26 @@ module Xlsxrb
331
354
  case value
332
355
  when Integer
333
356
  val_str = value >= 0 && value < 65_536 ? INTEGER_STRINGS[value] : value.to_s
334
- buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{val_str}</v></c>"
357
+ buf << '<c r="' << col_ref << row_num_str << '"><v>' << val_str << "</v></c>"
335
358
  when Float
336
- buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{value}</v></c>"
359
+ buf << '<c r="' << col_ref << row_num_str << '"><v>' << value.to_s << "</v></c>"
337
360
  when String
338
- unless value.start_with?("=")
339
- raise ArgumentError, "Cell text length #{value.length} exceeds Excel limit of 32,767 characters" if @strict_excel_mode && value.length > 32_767
361
+ raise ArgumentError, "Cell text length #{value.length} exceeds Excel limit of 32,767 characters" if @strict_excel_mode && value.length > 32_767
340
362
 
363
+ if sst_index
341
364
  idx = (sst_index[value] ||= begin
342
365
  sst << value
343
366
  sst.size - 1
344
367
  end)
345
368
  idx_str = idx < 65_536 ? INTEGER_STRINGS[idx] : idx.to_s
346
- buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"s\"><v>#{idx_str}</v></c>"
347
- next
369
+ buf << '<c r="' << col_ref << row_num_str << '" t="s"><v>' << idx_str << "</v></c>"
370
+ else
371
+ buf << '<c r="' << col_ref << row_num_str << '" t="inlineStr"><is><t>' << escape_xml(value) << "</t></is></c>"
348
372
  end
349
- formula_expr = value[1..]
350
- buf << "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f><v>0</v></c>"
351
373
  when true
352
- buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"b\"><v>1</v></c>"
374
+ buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>1</v></c>'
353
375
  when false
354
- buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"b\"><v>0</v></c>"
376
+ buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>0</v></c>'
355
377
  when Date
356
378
  date_style_id = style_map ? style_map["__xlsxrb_date"] : nil
357
379
  buf << if date_style_id
@@ -382,8 +404,23 @@ module Xlsxrb
382
404
  end
383
405
  end
384
406
  f_attrs << ' ca="1"' if value.calculate_always
385
- v_val = value.cached_value.nil? ? 0 : value.cached_value
386
- buf << "<c r=\"#{col_ref}#{row_num_str}\"><f#{f_attrs}>#{escape_xml(formula_expr)}</f><v>#{v_val}</v></c>"
407
+ c_type = ""
408
+ case value.cached_value
409
+ when true
410
+ c_type = ' t="b"'
411
+ v_val = "1"
412
+ when false
413
+ c_type = ' t="b"'
414
+ v_val = "0"
415
+ when Xlsxrb::Elements::CellError
416
+ c_type = ' t="e"'
417
+ v_val = value.cached_value.code
418
+ when nil
419
+ v_val = 0
420
+ else
421
+ v_val = value.cached_value.is_a?(String) ? escape_xml(value.cached_value) : value.cached_value
422
+ end
423
+ buf << "<c r=\"#{col_ref}#{row_num_str}\"#{c_type}><f#{f_attrs}>#{escape_xml(formula_expr)}</f><v>#{v_val}</v></c>"
387
424
  when Hash
388
425
  if value.key?(:formula)
389
426
  formula_expr = value[:formula] || value[:expression]
@@ -401,8 +438,27 @@ module Xlsxrb
401
438
  end
402
439
  end
403
440
  f_attrs << ' ca="1"' if value[:calculate_always]
404
- xml_val = value[:value] || value[:cached_value] || 0
405
- buf << "<c r=\"#{col_ref}#{row_num_str}\"><f#{f_attrs}>#{escape_xml(formula_expr)}</f><v>#{xml_val}</v></c>"
441
+ xml_val = if value.key?(:value) && !value[:value].nil?
442
+ value[:value]
443
+ elsif value.key?(:cached_value) && !value[:cached_value].nil?
444
+ value[:cached_value]
445
+ else
446
+ 0
447
+ end
448
+ c_type = ""
449
+ case xml_val
450
+ when true
451
+ c_type = ' t="b"'
452
+ xml_val = "1"
453
+ when false
454
+ c_type = ' t="b"'
455
+ xml_val = "0"
456
+ when Xlsxrb::Elements::CellError
457
+ c_type = ' t="e"'
458
+ xml_val = xml_val.code
459
+ end
460
+ xml_val = escape_xml(xml_val) if xml_val.is_a?(String)
461
+ buf << "<c r=\"#{col_ref}#{row_num_str}\"#{c_type}><f#{f_attrs}>#{escape_xml(formula_expr)}</f><v>#{xml_val}</v></c>"
406
462
  end
407
463
  when Xlsxrb::Elements::CellError
408
464
  buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"e\"><v>#{value.code}</v></c>"
@@ -429,9 +485,17 @@ module Xlsxrb
429
485
  is_styles_collection = styles && (styles.is_a?(Array) || styles.is_a?(Hash))
430
486
  single_style_id = resolve_style_id(styles, style_map) unless is_styles_collection
431
487
 
488
+ row_style = attrs ? attrs[:style_index] : nil
489
+ row_style_id = resolve_style_id(row_style, style_map)
490
+
432
491
  buf << '<row r="' << row_num_str << '"'
492
+ buf << ' s="' << row_style_id.to_s << '" customFormat="1"' if row_style_id.is_a?(Integer)
433
493
  if attrs
434
- buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"' if attrs[:height]
494
+ if attrs[:height]
495
+ buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"'
496
+ elsif attrs[:custom_height]
497
+ buf << ' customHeight="1"'
498
+ end
435
499
  buf << ' hidden="1"' if attrs[:hidden]
436
500
  buf << ' collapsed="1"' if attrs[:collapsed]
437
501
  buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level]
@@ -444,32 +508,31 @@ module Xlsxrb
444
508
  max_len = [max_len, styles_len].max
445
509
  end
446
510
 
447
- row_style = attrs ? attrs[:style_index] : nil
448
- row_style_id = resolve_style_id(row_style, style_map)
449
-
450
511
  col_index = 0
451
512
  while col_index < max_len
452
513
  value = col_index < values.length ? values[col_index] : nil
453
- style_id = single_style_id
514
+ cell_style_id = single_style_id
454
515
  if is_styles_collection
455
516
  style_name = if styles.is_a?(Array)
456
517
  col_index < styles.length ? styles[col_index] : nil
457
518
  else
458
519
  styles[col_index]
459
520
  end
460
- style_id = resolve_style_id(style_name, style_map)
521
+ cell_style_id = resolve_style_id(style_name, style_map)
461
522
  end
462
523
 
463
- if style_id.nil?
524
+ if cell_style_id.nil?
464
525
  col_style = @column_styles ? @column_styles[col_index] : nil
465
526
  col_style_id = resolve_style_id(col_style, style_map)
466
527
  style_id = row_style_id || col_style_id
528
+ else
529
+ style_id = cell_style_id
467
530
  end
468
531
 
469
532
  col_ref = COLUMN_LETTERS[col_index] || column_letter(col_index)
470
533
 
471
534
  if value.nil?
472
- buf << '<c r="' << col_ref << row_num_str << '" s="' << style_id.to_s << '"/>' if style_id
535
+ buf << '<c r="' << col_ref << row_num_str << '" s="' << cell_style_id.to_s << '"/>' if cell_style_id
473
536
  col_index += 1
474
537
  next
475
538
  end
@@ -487,7 +550,7 @@ module Xlsxrb
487
550
  col_index += 1
488
551
  next
489
552
  when String
490
- unless value.start_with?("=")
553
+ if sst_index
491
554
  idx = sst_index[value]
492
555
  unless idx
493
556
  sst << value
@@ -496,9 +559,11 @@ module Xlsxrb
496
559
  end
497
560
  idx_str = INTEGER_STRINGS[idx] || idx.to_s
498
561
  buf << '<c r="' << col_ref << row_num_str << '" t="s"><v>' << idx_str << "</v></c>"
499
- col_index += 1
500
- next
562
+ else
563
+ buf << '<c r="' << col_ref << row_num_str << '" t="inlineStr"><is><t>' << escape_xml(value) << "</t></is></c>"
501
564
  end
565
+ col_index += 1
566
+ next
502
567
  when true
503
568
  buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>1</v></c>'
504
569
  col_index += 1
@@ -544,6 +609,9 @@ module Xlsxrb
544
609
  when false
545
610
  type = "b"
546
611
  xml_val = "0"
612
+ when Xlsxrb::Elements::CellError
613
+ type = "e"
614
+ xml_val = value.cached_value.code
547
615
  end
548
616
  when Hash
549
617
  if value.key?(:formula)
@@ -552,7 +620,11 @@ module Xlsxrb
552
620
  formula_type = value[:type]
553
621
  formula_ref = value[:ref]
554
622
  formula_si = value[:si] || value[:shared_index]
555
- xml_val = value[:value] || value[:cached_value]
623
+ xml_val = if value.key?(:value)
624
+ value[:value]
625
+ elsif value.key?(:cached_value)
626
+ value[:cached_value]
627
+ end
556
628
  case xml_val
557
629
  when String
558
630
  type = "str"
@@ -562,26 +634,12 @@ module Xlsxrb
562
634
  when false
563
635
  type = "b"
564
636
  xml_val = "0"
637
+ when Xlsxrb::Elements::CellError
638
+ type = "e"
639
+ xml_val = xml_val.code
565
640
  end
566
641
  end
567
- when String
568
- if value.start_with?("=") && value.length > 1
569
- formula_expr = value
570
- xml_val = nil
571
- elsif sst_index
572
- idx = sst_index[value]
573
- unless idx
574
- sst << value
575
- idx = sst.size - 1
576
- sst_index[value] = idx
577
- end
578
- xml_val = idx
579
- type = "s"
580
- else
581
- xml_val = value
582
- type = "inlineStr"
583
- end
584
- when Xlsxrb::Elements::RichText
642
+ when String, Xlsxrb::Elements::RichText
585
643
  if sst_index
586
644
  idx = sst_index[value]
587
645
  unless idx
@@ -632,10 +690,12 @@ module Xlsxrb
632
690
  end
633
691
  f_attrs << ' ca="1"' if formula_ca
634
692
  buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
635
- if xml_val
636
- buf << "<v>" << xml_val.to_s << "</v></c>"
637
- else
693
+ if xml_val.nil?
638
694
  buf << "<v>0</v></c>"
695
+ else
696
+ v_str = xml_val.to_s
697
+ v_str = escape_xml(v_str) if type == "str" || xml_val.is_a?(String)
698
+ buf << "<v>" << v_str << "</v></c>"
639
699
  end
640
700
  elsif type == "inlineStr"
641
701
  buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
@@ -733,12 +793,26 @@ module Xlsxrb
733
793
 
734
794
  def write_sheet_views(freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil)
735
795
  @builder.open_tag("sheetViews")
736
- sv_attrs = { tabSelected: "1", workbookViewId: "0" }
796
+ sv_attrs = { workbookViewId: "0" }
737
797
  if sheet_view
738
- sv_attrs[:showGridLines] = "0" if sheet_view[:show_grid_lines] == false
739
- sv_attrs[:showRowColHeaders] = "0" if sheet_view[:show_row_col_headers] == false
740
- sv_attrs[:rightToLeft] = "1" if sheet_view[:right_to_left]
798
+ sv_attrs[:windowProtection] = sheet_view[:window_protection] ? "1" : "0" unless sheet_view[:window_protection].nil?
799
+ sv_attrs[:showFormulas] = sheet_view[:show_formulas] ? "1" : "0" unless sheet_view[:show_formulas].nil?
800
+ sv_attrs[:showGridLines] = sheet_view[:show_grid_lines] ? "1" : "0" unless sheet_view[:show_grid_lines].nil?
801
+ sv_attrs[:showRowColHeaders] = sheet_view[:show_row_col_headers] ? "1" : "0" unless sheet_view[:show_row_col_headers].nil?
802
+ sv_attrs[:showZeros] = sheet_view[:show_zeros] ? "1" : "0" unless sheet_view[:show_zeros].nil?
803
+ sv_attrs[:rightToLeft] = sheet_view[:right_to_left] ? "1" : "0" unless sheet_view[:right_to_left].nil?
804
+ sv_attrs[:tabSelected] = "1" if sheet_view[:tab_selected]
805
+ sv_attrs[:showRuler] = sheet_view[:show_ruler] ? "1" : "0" unless sheet_view[:show_ruler].nil?
806
+ sv_attrs[:showOutlineSymbols] = sheet_view[:show_outline_symbols] ? "1" : "0" unless sheet_view[:show_outline_symbols].nil?
807
+ sv_attrs[:defaultGridColor] = sheet_view[:default_grid_color] ? "1" : "0" unless sheet_view[:default_grid_color].nil?
808
+ sv_attrs[:showWhiteSpace] = sheet_view[:show_white_space] ? "1" : "0" unless sheet_view[:show_white_space].nil?
809
+ sv_attrs[:view] = sheet_view[:view].to_s if sheet_view[:view]
810
+ sv_attrs[:topLeftCell] = sheet_view[:top_left_cell].to_s if sheet_view[:top_left_cell]
811
+ sv_attrs[:colorId] = sheet_view[:color_id].to_s if sheet_view[:color_id]
741
812
  sv_attrs[:zoomScale] = sheet_view[:zoom_scale].to_s if sheet_view[:zoom_scale]
813
+ sv_attrs[:zoomScaleNormal] = sheet_view[:zoom_scale_normal].to_s if sheet_view[:zoom_scale_normal]
814
+ sv_attrs[:zoomScaleSheetLayoutView] = sheet_view[:zoom_scale_sheet_layout_view].to_s if sheet_view[:zoom_scale_sheet_layout_view]
815
+ sv_attrs[:zoomScalePageLayoutView] = sheet_view[:zoom_scale_page_layout_view].to_s if sheet_view[:zoom_scale_page_layout_view]
742
816
  end
743
817
  @builder.open_tag("sheetView", sv_attrs)
744
818
  if freeze_pane
@@ -747,30 +821,38 @@ module Xlsxrb
747
821
  pane_attrs[:ySplit] = freeze_pane[:row].to_s if freeze_pane[:row]&.positive?
748
822
  top_left_col = column_letter(freeze_pane[:col] || 0)
749
823
  top_left_row = (freeze_pane[:row] || 0) + 1
750
- pane_attrs[:topLeftCell] = "#{top_left_col}#{top_left_row}"
824
+ pane_attrs[:topLeftCell] = freeze_pane[:top_left_cell] || "#{top_left_col}#{top_left_row}"
751
825
  pane_attrs[:state] = "frozen"
752
826
  # Determine active pane
753
- pane_attrs[:activePane] = if (freeze_pane[:col] || 0).positive? && (freeze_pane[:row] || 0).positive?
754
- "bottomRight"
755
- elsif (freeze_pane[:col] || 0).positive?
756
- "topRight"
757
- else
758
- "bottomLeft"
759
- end
827
+ pane_attrs[:activePane] = freeze_pane[:active_pane] || if (freeze_pane[:col] || 0).positive? && (freeze_pane[:row] || 0).positive?
828
+ "bottomRight"
829
+ elsif (freeze_pane[:col] || 0).positive?
830
+ "topRight"
831
+ else
832
+ "bottomLeft"
833
+ end
760
834
  @builder.empty_tag("pane", pane_attrs)
761
835
  elsif split_pane
762
836
  pane_attrs = {}
763
837
  pane_attrs[:xSplit] = split_pane[:x_split].to_s if split_pane[:x_split]&.positive?
764
838
  pane_attrs[:ySplit] = split_pane[:y_split].to_s if split_pane[:y_split]&.positive?
765
839
  pane_attrs[:topLeftCell] = split_pane[:top_left_cell] if split_pane[:top_left_cell]
840
+ pane_attrs[:activePane] = split_pane[:active_pane] if split_pane[:active_pane]
841
+ pane_attrs[:state] = split_pane[:state].to_s if split_pane[:state]
766
842
  @builder.empty_tag("pane", pane_attrs)
767
843
  end
768
- if selection
769
- sel_attrs = {}
770
- sel_attrs[:activeCell] = selection[:active_cell] if selection[:active_cell]
771
- sel_attrs[:sqref] = selection[:sqref] || selection[:active_cell] if selection[:active_cell]
772
- sel_attrs[:pane] = selection[:pane] if selection[:pane]
773
- @builder.empty_tag("selection", sel_attrs)
844
+ active_selection = selection || (sheet_view[:selection] if sheet_view)
845
+ if active_selection
846
+ selections = active_selection.is_a?(Array) ? active_selection : [active_selection]
847
+ selections.each do |sel|
848
+ sel_attrs = {}
849
+ sel_attrs[:pane] = sel[:pane].to_s if sel[:pane]
850
+ sel_attrs[:activeCell] = sel[:active_cell].to_s if sel[:active_cell]
851
+ sel_attrs[:activeCellId] = sel[:active_cell_id].to_s if sel[:active_cell_id]
852
+ sqref = sel[:sqref] || sel[:active_cell]
853
+ sel_attrs[:sqref] = sqref.to_s if sqref
854
+ @builder.empty_tag("selection", sel_attrs) unless sel_attrs.empty?
855
+ end
774
856
  end
775
857
  @builder.close_tag("sheetView")
776
858
  @builder.close_tag("sheetViews")
@@ -1070,6 +1152,9 @@ module Xlsxrb
1070
1152
  end
1071
1153
 
1072
1154
  def write_data_validations(validations)
1155
+ validations = validations.reject { |dv| dv[:sqref].nil? || dv[:sqref].to_s.empty? }
1156
+ return if validations.empty?
1157
+
1073
1158
  @builder.open_tag("dataValidations", { count: validations.size.to_s })
1074
1159
  validations.each do |dv|
1075
1160
  if dv[:in].is_a?(Array)
@@ -1089,6 +1174,8 @@ module Xlsxrb
1089
1174
  dv_attrs[:error] = dv[:error] if dv[:error]
1090
1175
  dv_attrs[:promptTitle] = dv[:prompt_title] if dv[:prompt_title]
1091
1176
  dv_attrs[:prompt] = dv[:prompt] if dv[:prompt]
1177
+ dv_attrs[:showDropDown] = "1" if dv[:show_drop_down]
1178
+ dv_attrs[:imeMode] = dv[:ime_mode].to_s if dv[:ime_mode]
1092
1179
 
1093
1180
  has_formulas = dv[:formula1] || dv[:formula2]
1094
1181
  if has_formulas
@@ -1145,7 +1232,7 @@ module Xlsxrb
1145
1232
  attrs[:scale] = opts[:scale].to_s if opts[:scale]
1146
1233
  attrs[:fitToWidth] = opts[:fit_to_width].to_s if opts[:fit_to_width]
1147
1234
  attrs[:fitToHeight] = opts[:fit_to_height].to_s if opts[:fit_to_height]
1148
- attrs[:firstPageNumber] = opts[:first_page_number].to_s if opts[:first_page_number]
1235
+ attrs[:firstPageNumber] = opts[:first_page_number].to_s if opts[:first_page_number] && opts[:first_page_number].to_i >= 0
1149
1236
  attrs[:pageOrder] = opts[:page_order].to_s if opts[:page_order]
1150
1237
  attrs[:blackAndWhite] = "1" if opts[:black_and_white]
1151
1238
  attrs[:draft] = "1" if opts[:draft]
@@ -1164,7 +1251,14 @@ module Xlsxrb
1164
1251
  def write_row_breaks(breaks)
1165
1252
  @builder.open_tag("rowBreaks", { count: breaks.size.to_s, manualBreakCount: breaks.size.to_s })
1166
1253
  breaks.each do |brk|
1167
- @builder.empty_tag("brk", { id: brk.to_s, max: "16383", man: "1" })
1254
+ attrs = if brk.is_a?(Hash)
1255
+ a = { id: (brk[:id] || brk[:row] || 0).to_s, max: (brk[:max] || 16_383).to_s, man: brk.fetch(:man, true) ? "1" : "0" }
1256
+ a[:min] = brk[:min].to_s if brk[:min]
1257
+ a
1258
+ else
1259
+ { id: brk.to_s, max: "16383", man: "1" }
1260
+ end
1261
+ @builder.empty_tag("brk", attrs)
1168
1262
  end
1169
1263
  @builder.close_tag("rowBreaks")
1170
1264
  end
@@ -1172,7 +1266,14 @@ module Xlsxrb
1172
1266
  def write_col_breaks(breaks)
1173
1267
  @builder.open_tag("colBreaks", { count: breaks.size.to_s, manualBreakCount: breaks.size.to_s })
1174
1268
  breaks.each do |brk|
1175
- @builder.empty_tag("brk", { id: brk.to_s, max: "1048575", man: "1" })
1269
+ attrs = if brk.is_a?(Hash)
1270
+ a = { id: (brk[:id] || brk[:col] || 0).to_s, max: (brk[:max] || 1_048_575).to_s, man: brk.fetch(:man, true) ? "1" : "0" }
1271
+ a[:min] = brk[:min].to_s if brk[:min]
1272
+ a
1273
+ else
1274
+ { id: brk.to_s, max: "1048575", man: "1" }
1275
+ end
1276
+ @builder.empty_tag("brk", attrs)
1176
1277
  end
1177
1278
  @builder.close_tag("colBreaks")
1178
1279
  end
@@ -1268,16 +1369,32 @@ module Xlsxrb
1268
1369
  def write_columns(columns)
1269
1370
  @builder.open_tag("cols")
1270
1371
  columns.each do |col|
1372
+ if col.is_a?(Elements::Column)
1373
+ min = col.unmapped_data[:min] || (col.index + 1)
1374
+ max = col.unmapped_data[:max] || (col.index + 1)
1375
+ width = col.width
1376
+ hidden = col.hidden
1377
+ custom_width = col.custom_width || col.width
1378
+ outline_level = col.outline_level
1379
+ style_id = col.style_index
1380
+ else
1381
+ min = col[:min] || ((col[:index] || 0) + 1)
1382
+ max = col[:max] || ((col[:index] || 0) + 1)
1383
+ width = col[:width]
1384
+ hidden = col[:hidden]
1385
+ custom_width = col[:custom_width] || col[:width]
1386
+ outline_level = col[:outline_level]
1387
+ style_id = col[:style_index] || col[:style]
1388
+ end
1271
1389
  attrs = {
1272
- min: ((col[:index] || col[:min] || 0) + 1).to_s,
1273
- max: ((col[:index] || col[:max] || col[:min] || 0) + 1).to_s
1390
+ min: min.to_s,
1391
+ max: max.to_s
1274
1392
  }
1275
- attrs[:width] = col[:width].to_s if col[:width]
1276
- attrs[:hidden] = "1" if col[:hidden]
1277
- attrs[:customWidth] = "1" if col[:custom_width] || col[:width]
1278
- attrs[:outlineLevel] = col[:outline_level].to_s if col[:outline_level]
1279
- style_id = col[:style_index] || col[:style]
1280
- attrs[:style] = style_id.to_s if style_id
1393
+ attrs[:width] = width.to_s if width
1394
+ attrs[:hidden] = "1" if hidden
1395
+ attrs[:customWidth] = "1" if custom_width
1396
+ attrs[:outlineLevel] = outline_level.to_s if outline_level
1397
+ attrs[:style] = style_id.to_s if style_id.is_a?(Integer)
1281
1398
  @builder.empty_tag("col", attrs)
1282
1399
  end
1283
1400
  @builder.close_tag("cols")
@@ -1289,15 +1406,27 @@ module Xlsxrb
1289
1406
 
1290
1407
  value = cell[:value]
1291
1408
  formula = cell[:formula]
1292
- # For formula cells with cached values, don't infer type from value
1293
- type = formula ? cell[:type] : (cell[:type] || cell_type(value))
1409
+ # For formula cells with cached values, infer type for booleans and strings
1410
+ type = cell[:type] || if formula
1411
+ case value
1412
+ when true, false then "b"
1413
+ end
1414
+ else
1415
+ cell_type(value)
1416
+ end
1294
1417
  attrs[:t] = type if type
1295
1418
  col_idx = cell[:column_index]
1296
1419
  col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
1297
- style_id = cell[:style_index] || col_style
1420
+ cell_style_id = cell[:style_index]
1421
+ style_id = cell_style_id || col_style
1298
1422
  attrs[:s] = style_id.to_s if style_id.is_a?(Integer)
1299
1423
 
1300
1424
  if value.nil? && formula.nil?
1425
+ if cell_style_id.is_a?(Integer)
1426
+ attrs[:s] = cell_style_id.to_s
1427
+ else
1428
+ attrs.delete(:s)
1429
+ end
1301
1430
  @builder.empty_tag("c", attrs)
1302
1431
  return
1303
1432
  end
@@ -1369,6 +1498,7 @@ module Xlsxrb
1369
1498
 
1370
1499
  def escape_xml(value)
1371
1500
  str = value.to_s
1501
+ str = str.dup.force_encoding(Encoding::UTF_8) if str.encoding == Encoding::BINARY
1372
1502
  str = str.gsub(INVALID_XML_CHARS_RE, "") if str.match?(INVALID_XML_CHARS_RE)
1373
1503
  str.match?(ESCAPE_RE) ? str.gsub(ESCAPE_RE, ESCAPE_MAP) : str
1374
1504
  end