xlsxrb 0.1.14 → 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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -0
  3. data/README.md +37 -17
  4. data/benchmark/micro_ips.rb +70 -0
  5. data/lib/xlsxrb/elements/styles.rb +427 -15
  6. data/lib/xlsxrb/elements/workbook.rb +67 -4
  7. data/lib/xlsxrb/elements/worksheet.rb +117 -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 +14 -2
  11. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +12 -7
  12. data/lib/xlsxrb/ooxml/reader.rb +167 -139
  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 +63 -4
  17. data/lib/xlsxrb/ooxml/workbook_writer.rb +232 -59
  18. data/lib/xlsxrb/ooxml/worksheet_parser.rb +192 -123
  19. data/lib/xlsxrb/ooxml/worksheet_writer.rb +307 -125
  20. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +16 -12
  21. data/lib/xlsxrb/ooxml/writer/features_xml.rb +1 -1
  22. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +64 -15
  23. data/lib/xlsxrb/ooxml/writer.rb +31 -28
  24. data/lib/xlsxrb/ooxml/xml_builder.rb +22 -2
  25. data/lib/xlsxrb/ooxml/xml_parser.rb +45 -0
  26. data/lib/xlsxrb/ooxml/zip_reader.rb +83 -32
  27. data/lib/xlsxrb/stream_row.rb +4 -3
  28. data/lib/xlsxrb/stream_sheet.rb +259 -7
  29. data/lib/xlsxrb/stream_writer.rb +197 -31
  30. data/lib/xlsxrb/utils.rb +68 -0
  31. data/lib/xlsxrb/version.rb +1 -1
  32. data/lib/xlsxrb/workbook_builder.rb +110 -19
  33. data/lib/xlsxrb/worksheet_builder.rb +526 -15
  34. data/lib/xlsxrb.rb +889 -90
  35. data/sig/generated/xlsxrb/elements/styles.rbs +271 -5
  36. data/sig/generated/xlsxrb/elements/workbook.rbs +54 -3
  37. data/sig/generated/xlsxrb/elements/worksheet.rbs +63 -2
  38. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +2 -0
  39. data/sig/generated/xlsxrb/ooxml/reader.rbs +12 -0
  40. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +3 -1
  41. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +2 -0
  42. data/sig/generated/xlsxrb/ooxml/utils.rbs +22 -0
  43. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +7 -0
  44. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +13 -1
  45. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +18 -2
  46. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +13 -3
  47. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +5 -2
  48. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +6 -1
  49. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +9 -6
  50. data/sig/generated/xlsxrb/stream_row.rbs +3 -2
  51. data/sig/generated/xlsxrb/stream_sheet.rbs +132 -6
  52. data/sig/generated/xlsxrb/stream_writer.rbs +118 -27
  53. data/sig/generated/xlsxrb/utils.rbs +45 -0
  54. data/sig/generated/xlsxrb/worksheet_builder.rbs +252 -9
  55. data/sig/generated/xlsxrb.rbs +84 -10
  56. 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
@@ -47,7 +50,7 @@ module Xlsxrb
47
50
  # split_pane: { x_split:, y_split:, top_left_cell: }
48
51
  # selection: { active_cell:, sqref:, pane: }
49
52
  # sheet_view: Hash of sheet view properties
50
- def start(columns: [], sheet_properties: nil, freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil)
53
+ def start(columns: [], sheet_properties: nil, freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil, sheet_format: nil)
51
54
  return if @started
52
55
 
53
56
  @started = true
@@ -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
 
@@ -75,6 +78,7 @@ module Xlsxrb
75
78
 
76
79
  write_sheet_properties(sheet_properties) if sheet_properties && !sheet_properties.empty?
77
80
  write_sheet_views(freeze_pane: freeze_pane, split_pane: split_pane, selection: selection, sheet_view: sheet_view) if freeze_pane || split_pane || selection || (sheet_view && !sheet_view.empty?)
81
+ write_sheet_format_pr(sheet_format) if sheet_format && !sheet_format.empty?
78
82
  write_columns(columns) unless columns.empty?
79
83
 
80
84
  @builder.open_tag("sheetData")
@@ -88,7 +92,7 @@ module Xlsxrb
88
92
  row_num_str = row_num.to_s
89
93
  buf = @row_buffer ||= String.new(capacity: 65_536)
90
94
  buf << '<row r="' << row_num_str << '"'
91
- buf << ' s="' << attrs[:style_index].to_s << '" customFormat="1"' if attrs[:style_index]
95
+ buf << ' s="' << attrs[:style_index].to_s << '" customFormat="1"' if attrs[:style_index].is_a?(Integer)
92
96
  if attrs[:height]
93
97
  buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"'
94
98
  elsif attrs[:custom_height]
@@ -102,7 +106,7 @@ module Xlsxrb
102
106
  cells.each do |cell|
103
107
  if cell.is_a?(Elements::Cell)
104
108
  value = cell.value
105
- style_id = cell.style_index
109
+ cell_style_id = cell.style_index
106
110
  col_ref = cell.ref || "#{column_letter(cell.column_index)}#{row_num_str}"
107
111
  col_idx = cell.column_index
108
112
  formula = cell.formula
@@ -110,7 +114,7 @@ module Xlsxrb
110
114
  cell_type_val = nil
111
115
  elsif cell.is_a?(Hash)
112
116
  value = cell[:value]
113
- style_id = cell[:style_index]
117
+ cell_style_id = cell[:style_index]
114
118
  col_idx = cell[:column_index]
115
119
  col_ref = cell[:ref] || (col_idx ? "#{column_letter(col_idx)}#{row_num_str}" : nil)
116
120
  formula = cell[:formula]
@@ -118,7 +122,7 @@ module Xlsxrb
118
122
  cell_type_val = cell[:type]
119
123
  else
120
124
  value = cell
121
- style_id = nil
125
+ cell_style_id = nil
122
126
  col_idx = nil
123
127
  col_ref = nil
124
128
  formula = nil
@@ -127,7 +131,8 @@ module Xlsxrb
127
131
  end
128
132
 
129
133
  col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
130
- style_id ||= attrs[:style_index] || col_style
134
+ style_id = cell_style_id || attrs[:style_index] || col_style
135
+ style_id = nil unless style_id.is_a?(Integer)
131
136
 
132
137
  # Fast path for common unstyled cells
133
138
  if !formula && !style_id && !cell_type_val
@@ -136,7 +141,7 @@ module Xlsxrb
136
141
  buf << '<c r="' << col_ref << '"><v>' << value.to_s << "</v></c>"
137
142
  next
138
143
  when String
139
- if !value.start_with?("=") && sst_index && (idx = sst_index[value])
144
+ if sst_index && (idx = sst_index[value])
140
145
  buf << '<c r="' << col_ref << '" t="s"><v>' << idx.to_s << "</v></c>"
141
146
  next
142
147
  end
@@ -157,17 +162,14 @@ module Xlsxrb
157
162
  end
158
163
 
159
164
  if value.nil? && formula.nil?
160
- buf << '<c r="' << col_ref << '"'
161
- buf << ' s="' << style_id.to_s << '"' if style_id
162
- 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
163
170
  next
164
171
  end
165
172
 
166
- if value.is_a?(String) && value.start_with?("=") && !formula
167
- formula = value
168
- value = nil
169
- end
170
-
171
173
  xml_val = value
172
174
  type = cell_type_val
173
175
  formula_type = nil
@@ -181,22 +183,40 @@ module Xlsxrb
181
183
  formula_type = formula.type
182
184
  formula_ref = formula.ref
183
185
  formula_si = formula.shared_index
184
- xml_val = formula.cached_value.nil? ? (value || nil) : formula.cached_value
186
+ xml_val = formula.cached_value.nil? ? value : formula.cached_value
185
187
  elsif formula.is_a?(Hash)
186
188
  formula_expr = formula[:formula] || formula[:expression]
187
189
  formula_ca = formula[:calculate_always]
188
190
  formula_type = formula[:type]
189
191
  formula_ref = formula[:ref]
190
192
  formula_si = formula[:si] || formula[:shared_index]
191
- 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
192
200
  else
193
201
  formula_expr = formula
194
- xml_val = value || nil
202
+ xml_val = value
195
203
  end
196
- 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?("=")
197
205
  end
198
206
 
199
- 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
200
220
  case value
201
221
  when String
202
222
  if sst_index && (idx = sst_index[value])
@@ -288,13 +308,18 @@ module Xlsxrb
288
308
  end
289
309
  f_attrs << ' ca="1"' if formula_ca
290
310
  buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
291
- if xml_val
292
- buf << "<v>" << xml_val.to_s << "</v></c>"
293
- else
311
+ if xml_val.nil?
294
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>"
295
318
  end
296
319
  else
297
- 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>"
298
323
  end
299
324
  end
300
325
 
@@ -316,7 +341,7 @@ module Xlsxrb
316
341
  buf = @row_buffer ||= String.new(capacity: 65_536)
317
342
 
318
343
  if styles.nil? && attrs.nil? && @column_styles.nil?
319
- buf << "<row r=\"#{row_num_str}\">"
344
+ buf << '<row r="' << row_num_str << '">'
320
345
  col_index = 0
321
346
  max_len = values.length
322
347
  while col_index < max_len
@@ -329,27 +354,26 @@ module Xlsxrb
329
354
  case value
330
355
  when Integer
331
356
  val_str = value >= 0 && value < 65_536 ? INTEGER_STRINGS[value] : value.to_s
332
- 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>"
333
358
  when Float
334
- 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>"
335
360
  when String
336
- unless value.start_with?("=")
337
- 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
338
362
 
363
+ if sst_index
339
364
  idx = (sst_index[value] ||= begin
340
365
  sst << value
341
366
  sst.size - 1
342
367
  end)
343
368
  idx_str = idx < 65_536 ? INTEGER_STRINGS[idx] : idx.to_s
344
- buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"s\"><v>#{idx_str}</v></c>"
345
- 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>"
346
372
  end
347
- formula_expr = value[1..]
348
- buf << "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f><v>0</v></c>"
349
373
  when true
350
- 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>'
351
375
  when false
352
- 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>'
353
377
  when Date
354
378
  date_style_id = style_map ? style_map["__xlsxrb_date"] : nil
355
379
  buf << if date_style_id
@@ -380,8 +404,23 @@ module Xlsxrb
380
404
  end
381
405
  end
382
406
  f_attrs << ' ca="1"' if value.calculate_always
383
- v_val = value.cached_value.nil? ? 0 : value.cached_value
384
- 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>"
385
424
  when Hash
386
425
  if value.key?(:formula)
387
426
  formula_expr = value[:formula] || value[:expression]
@@ -399,8 +438,27 @@ module Xlsxrb
399
438
  end
400
439
  end
401
440
  f_attrs << ' ca="1"' if value[:calculate_always]
402
- xml_val = value[:value] || value[:cached_value] || 0
403
- 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>"
404
462
  end
405
463
  when Xlsxrb::Elements::CellError
406
464
  buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"e\"><v>#{value.code}</v></c>"
@@ -425,12 +483,19 @@ module Xlsxrb
425
483
  end
426
484
 
427
485
  is_styles_collection = styles && (styles.is_a?(Array) || styles.is_a?(Hash))
428
- single_style_id = nil
429
- single_style_id = style_map[styles] if styles && style_map && !is_styles_collection
486
+ single_style_id = resolve_style_id(styles, style_map) unless is_styles_collection
487
+
488
+ row_style = attrs ? attrs[:style_index] : nil
489
+ row_style_id = resolve_style_id(row_style, style_map)
430
490
 
431
491
  buf << '<row r="' << row_num_str << '"'
492
+ buf << ' s="' << row_style_id.to_s << '" customFormat="1"' if row_style_id.is_a?(Integer)
432
493
  if attrs
433
- 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
434
499
  buf << ' hidden="1"' if attrs[:hidden]
435
500
  buf << ' collapsed="1"' if attrs[:collapsed]
436
501
  buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level]
@@ -443,40 +508,31 @@ module Xlsxrb
443
508
  max_len = [max_len, styles_len].max
444
509
  end
445
510
 
446
- row_style = attrs ? attrs[:style_index] : nil
447
- row_style_id = row_style && style_map ? (style_map[row_style] || style_map[row_style.to_s] || row_style) : row_style
448
-
449
511
  col_index = 0
450
512
  while col_index < max_len
451
513
  value = col_index < values.length ? values[col_index] : nil
452
- style_id = single_style_id
514
+ cell_style_id = single_style_id
453
515
  if is_styles_collection
454
516
  style_name = if styles.is_a?(Array)
455
517
  col_index < styles.length ? styles[col_index] : nil
456
518
  else
457
519
  styles[col_index]
458
520
  end
459
- style_id = if style_map && style_name
460
- style_map[style_name] || style_map[style_name.to_s] || style_name
461
- else
462
- style_name
463
- end
521
+ cell_style_id = resolve_style_id(style_name, style_map)
464
522
  end
465
523
 
466
- if style_id.nil?
524
+ if cell_style_id.nil?
467
525
  col_style = @column_styles ? @column_styles[col_index] : nil
468
- col_style_id = if col_style && style_map
469
- style_map[col_style] || style_map[col_style.to_s] || col_style
470
- else
471
- col_style
472
- end
526
+ col_style_id = resolve_style_id(col_style, style_map)
473
527
  style_id = row_style_id || col_style_id
528
+ else
529
+ style_id = cell_style_id
474
530
  end
475
531
 
476
532
  col_ref = COLUMN_LETTERS[col_index] || column_letter(col_index)
477
533
 
478
534
  if value.nil?
479
- 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
480
536
  col_index += 1
481
537
  next
482
538
  end
@@ -494,7 +550,7 @@ module Xlsxrb
494
550
  col_index += 1
495
551
  next
496
552
  when String
497
- unless value.start_with?("=")
553
+ if sst_index
498
554
  idx = sst_index[value]
499
555
  unless idx
500
556
  sst << value
@@ -503,9 +559,11 @@ module Xlsxrb
503
559
  end
504
560
  idx_str = INTEGER_STRINGS[idx] || idx.to_s
505
561
  buf << '<c r="' << col_ref << row_num_str << '" t="s"><v>' << idx_str << "</v></c>"
506
- col_index += 1
507
- next
562
+ else
563
+ buf << '<c r="' << col_ref << row_num_str << '" t="inlineStr"><is><t>' << escape_xml(value) << "</t></is></c>"
508
564
  end
565
+ col_index += 1
566
+ next
509
567
  when true
510
568
  buf << '<c r="' << col_ref << row_num_str << '" t="b"><v>1</v></c>'
511
569
  col_index += 1
@@ -551,6 +609,9 @@ module Xlsxrb
551
609
  when false
552
610
  type = "b"
553
611
  xml_val = "0"
612
+ when Xlsxrb::Elements::CellError
613
+ type = "e"
614
+ xml_val = value.cached_value.code
554
615
  end
555
616
  when Hash
556
617
  if value.key?(:formula)
@@ -559,7 +620,11 @@ module Xlsxrb
559
620
  formula_type = value[:type]
560
621
  formula_ref = value[:ref]
561
622
  formula_si = value[:si] || value[:shared_index]
562
- 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
563
628
  case xml_val
564
629
  when String
565
630
  type = "str"
@@ -569,26 +634,12 @@ module Xlsxrb
569
634
  when false
570
635
  type = "b"
571
636
  xml_val = "0"
637
+ when Xlsxrb::Elements::CellError
638
+ type = "e"
639
+ xml_val = xml_val.code
572
640
  end
573
641
  end
574
- when String
575
- if value.start_with?("=") && value.length > 1
576
- formula_expr = value
577
- xml_val = nil
578
- elsif sst_index
579
- idx = sst_index[value]
580
- unless idx
581
- sst << value
582
- idx = sst.size - 1
583
- sst_index[value] = idx
584
- end
585
- xml_val = idx
586
- type = "s"
587
- else
588
- xml_val = value
589
- type = "inlineStr"
590
- end
591
- when Xlsxrb::Elements::RichText
642
+ when String, Xlsxrb::Elements::RichText
592
643
  if sst_index
593
644
  idx = sst_index[value]
594
645
  unless idx
@@ -638,11 +689,13 @@ module Xlsxrb
638
689
  end
639
690
  end
640
691
  f_attrs << ' ca="1"' if formula_ca
641
- buf << "<f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
642
- if xml_val
643
- buf << "<v>" << xml_val.to_s << "</v></c>"
644
- else
692
+ buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
693
+ if xml_val.nil?
645
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>"
646
699
  end
647
700
  elsif type == "inlineStr"
648
701
  buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
@@ -662,12 +715,12 @@ module Xlsxrb
662
715
 
663
716
  # Write the worksheet footer. Call once after all rows.
664
717
  # Options for post-sheetData elements (in OOXML order):
665
- def finish(drawing_rid: nil, sheet_protection: nil, auto_filter: nil,
718
+ def finish(drawing_rid: nil, sheet_protection: nil, protected_ranges: nil, auto_filter: nil,
666
719
  filter_columns: nil, sort_state: nil, merge_cells: nil,
667
720
  conditional_formats: nil, data_validations: nil,
668
721
  hyperlinks: nil, print_options: nil, page_margins: nil,
669
722
  page_setup: nil, header_footer: nil, row_breaks: nil,
670
- col_breaks: nil, tables: nil, table_start_rid: nil,
723
+ col_breaks: nil, cell_watches: nil, tables: nil, table_start_rid: nil,
671
724
  legacy_drawing_rid: nil, sparkline_groups: nil)
672
725
  return if @finished
673
726
 
@@ -681,6 +734,7 @@ module Xlsxrb
681
734
 
682
735
  # Elements must appear in OOXML specification order after sheetData
683
736
  write_sheet_protection(sheet_protection) if sheet_protection
737
+ write_protected_ranges(protected_ranges) if protected_ranges && !protected_ranges.empty?
684
738
  write_auto_filter(auto_filter, filter_columns, sort_state) if auto_filter
685
739
  write_merge_cells(merge_cells) if merge_cells && !merge_cells.empty?
686
740
  write_conditional_formatting(conditional_formats) if conditional_formats && !conditional_formats.empty?
@@ -692,6 +746,7 @@ module Xlsxrb
692
746
  write_header_footer(header_footer) if header_footer && !header_footer.empty?
693
747
  write_row_breaks(row_breaks) if row_breaks && !row_breaks.empty?
694
748
  write_col_breaks(col_breaks) if col_breaks && !col_breaks.empty?
749
+ write_cell_watches(cell_watches) if cell_watches && !cell_watches.empty?
695
750
  @builder.empty_tag("drawing", { "r:id": drawing_rid }) if drawing_rid
696
751
  @builder.empty_tag("legacyDrawing", { "r:id": legacy_drawing_rid }) if legacy_drawing_rid
697
752
  write_table_parts(tables, table_start_rid) if tables && !tables.empty?
@@ -701,6 +756,17 @@ module Xlsxrb
701
756
 
702
757
  private
703
758
 
759
+ def resolve_style_id(style, style_map)
760
+ return nil if style.nil?
761
+
762
+ if style_map
763
+ mapped = style_map[style] || style_map[style.to_s]
764
+ return mapped if mapped
765
+ end
766
+
767
+ style.is_a?(Integer) ? style : nil
768
+ end
769
+
704
770
  # --- Pre-sheetData elements ---
705
771
 
706
772
  def write_sheet_properties(props)
@@ -727,12 +793,26 @@ module Xlsxrb
727
793
 
728
794
  def write_sheet_views(freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil)
729
795
  @builder.open_tag("sheetViews")
730
- sv_attrs = { tabSelected: "1", workbookViewId: "0" }
796
+ sv_attrs = { workbookViewId: "0" }
731
797
  if sheet_view
732
- sv_attrs[:showGridLines] = "0" if sheet_view[:show_grid_lines] == false
733
- sv_attrs[:showRowColHeaders] = "0" if sheet_view[:show_row_col_headers] == false
734
- 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]
735
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]
736
816
  end
737
817
  @builder.open_tag("sheetView", sv_attrs)
738
818
  if freeze_pane
@@ -741,35 +821,59 @@ module Xlsxrb
741
821
  pane_attrs[:ySplit] = freeze_pane[:row].to_s if freeze_pane[:row]&.positive?
742
822
  top_left_col = column_letter(freeze_pane[:col] || 0)
743
823
  top_left_row = (freeze_pane[:row] || 0) + 1
744
- pane_attrs[:topLeftCell] = "#{top_left_col}#{top_left_row}"
824
+ pane_attrs[:topLeftCell] = freeze_pane[:top_left_cell] || "#{top_left_col}#{top_left_row}"
745
825
  pane_attrs[:state] = "frozen"
746
826
  # Determine active pane
747
- pane_attrs[:activePane] = if (freeze_pane[:col] || 0).positive? && (freeze_pane[:row] || 0).positive?
748
- "bottomRight"
749
- elsif (freeze_pane[:col] || 0).positive?
750
- "topRight"
751
- else
752
- "bottomLeft"
753
- 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
754
834
  @builder.empty_tag("pane", pane_attrs)
755
835
  elsif split_pane
756
836
  pane_attrs = {}
757
837
  pane_attrs[:xSplit] = split_pane[:x_split].to_s if split_pane[:x_split]&.positive?
758
838
  pane_attrs[:ySplit] = split_pane[:y_split].to_s if split_pane[:y_split]&.positive?
759
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]
760
842
  @builder.empty_tag("pane", pane_attrs)
761
843
  end
762
- if selection
763
- sel_attrs = {}
764
- sel_attrs[:activeCell] = selection[:active_cell] if selection[:active_cell]
765
- sel_attrs[:sqref] = selection[:sqref] || selection[:active_cell] if selection[:active_cell]
766
- sel_attrs[:pane] = selection[:pane] if selection[:pane]
767
- @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
768
856
  end
769
857
  @builder.close_tag("sheetView")
770
858
  @builder.close_tag("sheetViews")
771
859
  end
772
860
 
861
+ def write_sheet_format_pr(fmt)
862
+ return if fmt.nil? || fmt.empty?
863
+
864
+ attrs = {}
865
+ attrs[:defaultRowHeight] = fmt[:default_row_height].to_s if fmt[:default_row_height]
866
+ attrs[:defaultColWidth] = fmt[:default_col_width].to_s if fmt[:default_col_width]
867
+ attrs[:baseColWidth] = fmt[:base_col_width].to_s if fmt[:base_col_width]
868
+ attrs[:outlineLevelRow] = fmt[:outline_level_row].to_s if fmt[:outline_level_row]
869
+ attrs[:outlineLevelCol] = fmt[:outline_level_col].to_s if fmt[:outline_level_col]
870
+ attrs[:customHeight] = "1" if fmt[:custom_height]
871
+ attrs[:zeroHeight] = "1" if fmt[:zero_height]
872
+ attrs[:thickTop] = "1" if fmt[:thick_top]
873
+ attrs[:thickBottom] = "1" if fmt[:thick_bottom]
874
+ @builder.empty_tag("sheetFormatPr", attrs) unless attrs.empty?
875
+ end
876
+
773
877
  # --- Post-sheetData elements (in OOXML order) ---
774
878
 
775
879
  def write_sheet_protection(opts)
@@ -798,6 +902,28 @@ module Xlsxrb
798
902
  @builder.empty_tag("sheetProtection", attrs)
799
903
  end
800
904
 
905
+ def write_protected_ranges(ranges)
906
+ @builder.open_tag("protectedRanges")
907
+ ranges.each do |pr|
908
+ attrs = { sqref: pr[:sqref].to_s, name: pr[:name].to_s }
909
+ attrs[:password] = pr[:password].to_s if pr[:password]
910
+ attrs[:algorithmName] = pr[:algorithm_name].to_s if pr[:algorithm_name]
911
+ attrs[:hashValue] = pr[:hash_value].to_s if pr[:hash_value]
912
+ attrs[:saltValue] = pr[:salt_value].to_s if pr[:salt_value]
913
+ attrs[:spinCount] = pr[:spin_count].to_s if pr[:spin_count]
914
+ attrs[:securityDescriptor] = pr[:security_descriptor].to_s if pr[:security_descriptor]
915
+ sds = pr[:security_descriptors] || []
916
+ if sds.empty?
917
+ @builder.empty_tag("protectedRange", attrs)
918
+ else
919
+ @builder.open_tag("protectedRange", attrs)
920
+ sds.each { |sd| @builder.tag("securityDescriptor") { @builder.text(sd.to_s) } }
921
+ @builder.close_tag("protectedRange")
922
+ end
923
+ end
924
+ @builder.close_tag("protectedRanges")
925
+ end
926
+
801
927
  def write_auto_filter(range, filter_columns, sort_state)
802
928
  if (filter_columns && !filter_columns.empty?) || sort_state
803
929
  @builder.open_tag("autoFilter", { ref: range })
@@ -1026,6 +1152,9 @@ module Xlsxrb
1026
1152
  end
1027
1153
 
1028
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
+
1029
1158
  @builder.open_tag("dataValidations", { count: validations.size.to_s })
1030
1159
  validations.each do |dv|
1031
1160
  if dv[:in].is_a?(Array)
@@ -1045,6 +1174,8 @@ module Xlsxrb
1045
1174
  dv_attrs[:error] = dv[:error] if dv[:error]
1046
1175
  dv_attrs[:promptTitle] = dv[:prompt_title] if dv[:prompt_title]
1047
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]
1048
1179
 
1049
1180
  has_formulas = dv[:formula1] || dv[:formula2]
1050
1181
  if has_formulas
@@ -1101,7 +1232,7 @@ module Xlsxrb
1101
1232
  attrs[:scale] = opts[:scale].to_s if opts[:scale]
1102
1233
  attrs[:fitToWidth] = opts[:fit_to_width].to_s if opts[:fit_to_width]
1103
1234
  attrs[:fitToHeight] = opts[:fit_to_height].to_s if opts[:fit_to_height]
1104
- 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
1105
1236
  attrs[:pageOrder] = opts[:page_order].to_s if opts[:page_order]
1106
1237
  attrs[:blackAndWhite] = "1" if opts[:black_and_white]
1107
1238
  attrs[:draft] = "1" if opts[:draft]
@@ -1120,7 +1251,14 @@ module Xlsxrb
1120
1251
  def write_row_breaks(breaks)
1121
1252
  @builder.open_tag("rowBreaks", { count: breaks.size.to_s, manualBreakCount: breaks.size.to_s })
1122
1253
  breaks.each do |brk|
1123
- @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)
1124
1262
  end
1125
1263
  @builder.close_tag("rowBreaks")
1126
1264
  end
@@ -1128,11 +1266,26 @@ module Xlsxrb
1128
1266
  def write_col_breaks(breaks)
1129
1267
  @builder.open_tag("colBreaks", { count: breaks.size.to_s, manualBreakCount: breaks.size.to_s })
1130
1268
  breaks.each do |brk|
1131
- @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)
1132
1277
  end
1133
1278
  @builder.close_tag("colBreaks")
1134
1279
  end
1135
1280
 
1281
+ def write_cell_watches(watches)
1282
+ @builder.open_tag("cellWatches")
1283
+ watches.each do |w|
1284
+ @builder.empty_tag("cellWatch", { r: w.to_s })
1285
+ end
1286
+ @builder.close_tag("cellWatches")
1287
+ end
1288
+
1136
1289
  def write_table_parts(tables, start_rid)
1137
1290
  @builder.open_tag("tableParts", { count: tables.size.to_s })
1138
1291
  tables.each_with_index do |_tbl, idx|
@@ -1216,16 +1369,32 @@ module Xlsxrb
1216
1369
  def write_columns(columns)
1217
1370
  @builder.open_tag("cols")
1218
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
1219
1389
  attrs = {
1220
- min: ((col[:index] || col[:min] || 0) + 1).to_s,
1221
- max: ((col[:index] || col[:max] || col[:min] || 0) + 1).to_s
1390
+ min: min.to_s,
1391
+ max: max.to_s
1222
1392
  }
1223
- attrs[:width] = col[:width].to_s if col[:width]
1224
- attrs[:hidden] = "1" if col[:hidden]
1225
- attrs[:customWidth] = "1" if col[:custom_width] || col[:width]
1226
- attrs[:outlineLevel] = col[:outline_level].to_s if col[:outline_level]
1227
- style_id = col[:style_index] || col[:style]
1228
- 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)
1229
1398
  @builder.empty_tag("col", attrs)
1230
1399
  end
1231
1400
  @builder.close_tag("cols")
@@ -1237,15 +1406,27 @@ module Xlsxrb
1237
1406
 
1238
1407
  value = cell[:value]
1239
1408
  formula = cell[:formula]
1240
- # For formula cells with cached values, don't infer type from value
1241
- 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
1242
1417
  attrs[:t] = type if type
1243
1418
  col_idx = cell[:column_index]
1244
1419
  col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
1245
- style_id = cell[:style_index] || col_style
1246
- attrs[:s] = style_id.to_s if style_id
1420
+ cell_style_id = cell[:style_index]
1421
+ style_id = cell_style_id || col_style
1422
+ attrs[:s] = style_id.to_s if style_id.is_a?(Integer)
1247
1423
 
1248
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
1249
1430
  @builder.empty_tag("c", attrs)
1250
1431
  return
1251
1432
  end
@@ -1317,6 +1498,7 @@ module Xlsxrb
1317
1498
 
1318
1499
  def escape_xml(value)
1319
1500
  str = value.to_s
1501
+ str = str.dup.force_encoding(Encoding::UTF_8) if str.encoding == Encoding::BINARY
1320
1502
  str = str.gsub(INVALID_XML_CHARS_RE, "") if str.match?(INVALID_XML_CHARS_RE)
1321
1503
  str.match?(ESCAPE_RE) ? str.gsub(ESCAPE_RE, ESCAPE_MAP) : str
1322
1504
  end