xlsxrb 0.1.16 → 0.1.17

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/README.md +2 -2
  4. data/lib/xlsxrb/chart_builder.rb +41 -2
  5. data/lib/xlsxrb/dsl_helpers.rb +522 -6
  6. data/lib/xlsxrb/elements/cell.rb +34 -9
  7. data/lib/xlsxrb/elements/column.rb +2 -2
  8. data/lib/xlsxrb/elements/types.rb +1 -0
  9. data/lib/xlsxrb/elements/workbook.rb +48 -0
  10. data/lib/xlsxrb/elements/worksheet.rb +158 -11
  11. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +22 -1
  12. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +187 -24
  13. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +108 -1
  14. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +16 -11
  15. data/lib/xlsxrb/ooxml/reader.rb +88 -0
  16. data/lib/xlsxrb/ooxml/styles_parser.rb +19 -11
  17. data/lib/xlsxrb/ooxml/workbook_parser.rb +18 -3
  18. data/lib/xlsxrb/ooxml/workbook_writer.rb +266 -46
  19. data/lib/xlsxrb/ooxml/worksheet_parser.rb +18 -4
  20. data/lib/xlsxrb/ooxml/worksheet_writer.rb +289 -88
  21. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +579 -441
  22. data/lib/xlsxrb/ooxml/writer/features_xml.rb +26 -9
  23. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +23 -9
  24. data/lib/xlsxrb/ooxml/writer.rb +268 -31
  25. data/lib/xlsxrb/ooxml/zip_writer.rb +15 -5
  26. data/lib/xlsxrb/stream_sheet.rb +157 -3
  27. data/lib/xlsxrb/stream_writer.rb +946 -74
  28. data/lib/xlsxrb/style_builder.rb +59 -6
  29. data/lib/xlsxrb/utils.rb +65 -0
  30. data/lib/xlsxrb/version.rb +1 -1
  31. data/lib/xlsxrb/workbook_builder.rb +197 -14
  32. data/lib/xlsxrb/worksheet_builder.rb +1137 -44
  33. data/lib/xlsxrb.rb +302 -47
  34. data/sig/generated/xlsxrb/chart_builder.rbs +31 -3
  35. data/sig/generated/xlsxrb/dsl_helpers.rbs +119 -4
  36. data/sig/generated/xlsxrb/elements/cell.rbs +20 -3
  37. data/sig/generated/xlsxrb/elements/column.rbs +3 -3
  38. data/sig/generated/xlsxrb/elements/types.rbs +2 -0
  39. data/sig/generated/xlsxrb/elements/workbook.rbs +37 -0
  40. data/sig/generated/xlsxrb/elements/worksheet.rbs +82 -2
  41. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +2 -0
  42. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +74 -2
  43. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +36 -0
  44. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +3 -0
  45. data/sig/generated/xlsxrb/ooxml/reader.rbs +17 -0
  46. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +3 -0
  47. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +8 -0
  48. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +8 -4
  49. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +1 -1
  50. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +12 -1
  51. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +16 -0
  52. data/sig/generated/xlsxrb/ooxml/writer.rbs +42 -8
  53. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +8 -2
  54. data/sig/generated/xlsxrb/stream_sheet.rbs +101 -1
  55. data/sig/generated/xlsxrb/stream_writer.rbs +592 -23
  56. data/sig/generated/xlsxrb/style_builder.rbs +24 -6
  57. data/sig/generated/xlsxrb/utils.rbs +20 -0
  58. data/sig/generated/xlsxrb/workbook_builder.rbs +87 -5
  59. data/sig/generated/xlsxrb/worksheet_builder.rbs +554 -10
  60. data/sig/generated/xlsxrb.rbs +77 -16
  61. metadata +1 -1
@@ -54,6 +54,8 @@ module Xlsxrb
54
54
  return if @started
55
55
 
56
56
  @started = true
57
+ @sheet_properties = sheet_properties
58
+ @sheet_format = sheet_format
57
59
  @builder.declaration
58
60
  @builder.open_tag("worksheet", { xmlns: SSML_NS, "xmlns:r": DOC_REL_NS })
59
61
 
@@ -130,10 +132,15 @@ module Xlsxrb
130
132
  cell_type_val = nil
131
133
  end
132
134
 
135
+ cell_style_id = resolve_style_id(cell_style_id, nil)
133
136
  col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
134
- style_id = cell_style_id || attrs[:style_index] || col_style
137
+ col_style = resolve_style_id(col_style, nil)
138
+ row_style = resolve_style_id(attrs[:style_index], nil)
139
+ style_id = cell_style_id.nil? ? (row_style || col_style) : cell_style_id
135
140
  style_id = nil unless style_id.is_a?(Integer)
136
141
 
142
+ raise ArgumentError, "Non-finite Float #{value} is not supported in XLSX" if value.is_a?(Float) && !value.finite?
143
+
137
144
  # Fast path for common unstyled cells
138
145
  if !formula && !style_id && !cell_type_val
139
146
  case value
@@ -204,6 +211,8 @@ module Xlsxrb
204
211
  formula_expr = formula_expr[1..] if formula_expr.is_a?(String) && formula_expr.start_with?("=")
205
212
  end
206
213
 
214
+ raise ArgumentError, "Non-finite Float #{xml_val} is not supported in XLSX" if xml_val.is_a?(Float) && !xml_val.finite?
215
+
207
216
  if formula_expr
208
217
  case xml_val
209
218
  when true
@@ -257,43 +266,11 @@ module Xlsxrb
257
266
 
258
267
  if type == "inlineStr"
259
268
  if xml_val.is_a?(Xlsxrb::Elements::RichText)
260
- buf << "><is>"
261
- xml_val.runs.each do |run|
262
- font = run[:font]
263
- if font && !font.empty?
264
- buf << "<r><rPr>"
265
- buf << "<b/>" if font[:bold]
266
- buf << "<i/>" if font[:italic]
267
- buf << "<strike/>" if font[:strike]
268
- if font[:underline]
269
- if font[:underline] == true
270
- buf << "<u/>"
271
- else
272
- buf << '<u val="' << font[:underline].to_s << '"/>'
273
- end
274
- end
275
- buf << '<vertAlign val="' << font[:vert_align].to_s << '"/>' if font[:vert_align]
276
- buf << '<sz val="' << font[:sz].to_s << '"/>' if font[:sz]
277
- if font[:color]
278
- buf << '<color rgb="' << font[:color].to_s << '"/>'
279
- elsif font[:theme]
280
- tint_attr = font[:tint] ? " tint=\"#{font[:tint]}\"" : ""
281
- buf << '<color theme="' << font[:theme].to_s << '"' << tint_attr << "/>"
282
- end
283
- buf << '<rFont val="' << escape_xml(font[:name]) << '"/>' if font[:name]
284
- buf << '<family val="' << font[:family].to_s << '"/>' if font[:family]
285
- buf << '<scheme val="' << font[:scheme].to_s << '"/>' if font[:scheme]
286
- buf << "</rPr><t>"
287
- else
288
- buf << "<r><t>"
289
- end
290
- buf << escape_xml(run[:text]) << "</t></r>"
291
- end
292
- buf << "</is></c>"
269
+ write_inline_rich_text(buf, xml_val)
293
270
  else
294
271
  buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
295
272
  end
296
- elsif formula_expr
273
+ elsif formula_expr || (formula_type && formula_type.to_sym == :shared && !formula_si.nil?)
297
274
  f_attrs = +""
298
275
  if formula_type
299
276
  case formula_type.to_sym
@@ -307,7 +284,11 @@ module Xlsxrb
307
284
  end
308
285
  end
309
286
  f_attrs << ' ca="1"' if formula_ca
310
- buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
287
+ if formula_expr && !formula_expr.empty?
288
+ buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
289
+ else
290
+ buf << "><f#{f_attrs}/>"
291
+ end
311
292
  if xml_val.nil?
312
293
  buf << "<v>0</v></c>"
313
294
  else
@@ -356,6 +337,8 @@ module Xlsxrb
356
337
  val_str = value >= 0 && value < 65_536 ? INTEGER_STRINGS[value] : value.to_s
357
338
  buf << '<c r="' << col_ref << row_num_str << '"><v>' << val_str << "</v></c>"
358
339
  when Float
340
+ raise ArgumentError, "Non-finite Float #{value} is not supported in XLSX" unless value.finite?
341
+
359
342
  buf << '<c r="' << col_ref << row_num_str << '"><v>' << value.to_s << "</v></c>"
360
343
  when String
361
344
  raise ArgumentError, "Cell text length #{value.length} exceeds Excel limit of 32,767 characters" if @strict_excel_mode && value.length > 32_767
@@ -389,6 +372,8 @@ module Xlsxrb
389
372
  "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904)}</v></c>"
390
373
  end
391
374
  when Xlsxrb::Elements::Formula
375
+ raise ArgumentError, "Non-finite Float #{value.cached_value} is not supported in XLSX" if value.cached_value.is_a?(Float) && !value.cached_value.finite?
376
+
392
377
  formula_expr = value.expression
393
378
  formula_expr = formula_expr[1..] if formula_expr.start_with?("=")
394
379
  f_attrs = +""
@@ -420,20 +405,27 @@ module Xlsxrb
420
405
  else
421
406
  v_val = value.cached_value.is_a?(String) ? escape_xml(value.cached_value) : value.cached_value
422
407
  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>"
408
+ f_tag = if formula_expr && !formula_expr.empty?
409
+ "<f#{f_attrs}>#{escape_xml(formula_expr)}</f>"
410
+ else
411
+ "<f#{f_attrs}/>"
412
+ end
413
+ buf << "<c r=\"#{col_ref}#{row_num_str}\"#{c_type}>#{f_tag}<v>#{v_val}</v></c>"
424
414
  when Hash
425
- if value.key?(:formula)
415
+ if value.key?(:formula) || value.key?(:expression) || (value[:type]&.to_sym == :shared && (value.key?(:shared_index) || value.key?(:si)))
426
416
  formula_expr = value[:formula] || value[:expression]
427
- formula_expr = formula_expr[1..] if formula_expr.start_with?("=")
417
+ formula_expr = formula_expr[1..] if formula_expr.is_a?(String) && formula_expr.start_with?("=")
428
418
  f_attrs = +""
429
- if value[:type]
430
- case value[:type].to_sym
419
+ f_type = value[:type] || (:shared if value.key?(:shared_index) || value.key?(:si))
420
+ if f_type
421
+ case f_type.to_sym
431
422
  when :array
432
423
  f_attrs << ' t="array"'
433
424
  f_attrs << %( ref="#{value[:ref]}") if value[:ref]
434
425
  when :shared
435
426
  f_attrs << ' t="shared"'
436
- f_attrs << %( si="#{value[:si] || value[:shared_index]}") if value[:si] || value[:shared_index]
427
+ si = value[:si] || value[:shared_index]
428
+ f_attrs << %( si="#{si}") if si
437
429
  f_attrs << %( ref="#{value[:ref]}") if value[:ref]
438
430
  end
439
431
  end
@@ -445,6 +437,8 @@ module Xlsxrb
445
437
  else
446
438
  0
447
439
  end
440
+ raise ArgumentError, "Non-finite Float #{xml_val} is not supported in XLSX" if xml_val.is_a?(Float) && !xml_val.finite?
441
+
448
442
  c_type = ""
449
443
  case xml_val
450
444
  when true
@@ -458,19 +452,31 @@ module Xlsxrb
458
452
  xml_val = xml_val.code
459
453
  end
460
454
  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>"
455
+ f_tag = if formula_expr && !formula_expr.empty?
456
+ "<f#{f_attrs}>#{escape_xml(formula_expr)}</f>"
457
+ else
458
+ "<f#{f_attrs}/>"
459
+ end
460
+ buf << "<c r=\"#{col_ref}#{row_num_str}\"#{c_type}>#{f_tag}<v>#{xml_val}</v></c>"
462
461
  end
463
462
  when Xlsxrb::Elements::CellError
464
463
  buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"e\"><v>#{value.code}</v></c>"
465
464
  when BigDecimal
466
465
  buf << "<c r=\"#{col_ref}#{row_num_str}\"><v>#{value.to_s("F")}</v></c>"
467
466
  else
468
- idx = (sst_index[value] ||= begin
469
- sst << value
470
- sst.size - 1
471
- end)
472
- idx_str = idx < 65_536 ? INTEGER_STRINGS[idx] : idx.to_s
473
- buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"s\"><v>#{idx_str}</v></c>"
467
+ if sst_index
468
+ idx = (sst_index[value] ||= begin
469
+ sst << value
470
+ sst.size - 1
471
+ end)
472
+ idx_str = idx < 65_536 ? INTEGER_STRINGS[idx] : idx.to_s
473
+ buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"s\"><v>#{idx_str}</v></c>"
474
+ elsif value.is_a?(Xlsxrb::Elements::RichText)
475
+ buf << '<c r="' << col_ref << row_num_str << '" t="inlineStr"'
476
+ write_inline_rich_text(buf, value)
477
+ else
478
+ buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"inlineStr\"><is><t>#{escape_xml(value.to_s)}</t></is></c>"
479
+ end
474
480
  end
475
481
  end
476
482
 
@@ -537,6 +543,8 @@ module Xlsxrb
537
543
  next
538
544
  end
539
545
 
546
+ raise ArgumentError, "Non-finite Float #{value} is not supported in XLSX" if value.is_a?(Float) && !value.finite?
547
+
540
548
  # Fast path: unstyled numbers, booleans, and simple strings (majority of cells)
541
549
  if style_id.nil? && !value.is_a?(Xlsxrb::Elements::Formula) && !value.is_a?(Hash)
542
550
  case value
@@ -670,12 +678,14 @@ module Xlsxrb
670
678
  type = "e"
671
679
  end
672
680
 
681
+ raise ArgumentError, "Non-finite Float #{xml_val} is not supported in XLSX" if xml_val.is_a?(Float) && !xml_val.finite?
682
+
673
683
  formula_expr = formula_expr[1..] if formula_expr&.start_with?("=")
674
684
 
675
685
  buf << '<c r="' << col_ref << row_num_str << '"'
676
686
  buf << ' s="' << style_id.to_s << '"' if style_id
677
687
  buf << ' t="' << type << '"' if type
678
- if formula_expr
688
+ if formula_expr || (formula_type && formula_type.to_sym == :shared && !formula_si.nil?)
679
689
  f_attrs = +""
680
690
  if formula_type
681
691
  case formula_type.to_sym
@@ -689,7 +699,11 @@ module Xlsxrb
689
699
  end
690
700
  end
691
701
  f_attrs << ' ca="1"' if formula_ca
692
- buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
702
+ if formula_expr && !formula_expr.empty?
703
+ buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
704
+ else
705
+ buf << "><f#{f_attrs}/>"
706
+ end
693
707
  if xml_val.nil?
694
708
  buf << "<v>0</v></c>"
695
709
  else
@@ -698,7 +712,11 @@ module Xlsxrb
698
712
  buf << "<v>" << v_str << "</v></c>"
699
713
  end
700
714
  elsif type == "inlineStr"
701
- buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
715
+ if xml_val.is_a?(Xlsxrb::Elements::RichText)
716
+ write_inline_rich_text(buf, xml_val)
717
+ else
718
+ buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
719
+ end
702
720
  else
703
721
  buf << "><v>" << xml_val.to_s << "</v></c>"
704
722
  end
@@ -715,13 +733,13 @@ module Xlsxrb
715
733
 
716
734
  # Write the worksheet footer. Call once after all rows.
717
735
  # Options for post-sheetData elements (in OOXML order):
718
- def finish(drawing_rid: nil, sheet_protection: nil, protected_ranges: nil, auto_filter: nil,
736
+ def finish(drawing_rid: nil, picture_rid: nil, sheet_protection: nil, protected_ranges: nil, auto_filter: nil,
719
737
  filter_columns: nil, sort_state: nil, merge_cells: nil,
720
738
  conditional_formats: nil, data_validations: nil,
721
739
  hyperlinks: nil, print_options: nil, page_margins: nil,
722
740
  page_setup: nil, header_footer: nil, row_breaks: nil,
723
- col_breaks: nil, cell_watches: nil, tables: nil, table_start_rid: nil,
724
- legacy_drawing_rid: nil, sparkline_groups: nil)
741
+ col_breaks: nil, cell_watches: nil, ignored_errors: nil, tables: nil, table_start_rid: nil,
742
+ legacy_drawing_rid: nil, sparkline_groups: nil, phonetic_pr: nil, sheet_calc_pr: nil)
725
743
  return if @finished
726
744
 
727
745
  start unless @started
@@ -733,10 +751,13 @@ module Xlsxrb
733
751
  @builder.close_tag("sheetData")
734
752
 
735
753
  # Elements must appear in OOXML specification order after sheetData
754
+ calc_pr = sheet_calc_pr || (@sheet_properties if @sheet_properties && !@sheet_properties[:full_calc_on_load].nil?)
755
+ write_sheet_calc_pr(calc_pr) if calc_pr
736
756
  write_sheet_protection(sheet_protection) if sheet_protection
737
757
  write_protected_ranges(protected_ranges) if protected_ranges && !protected_ranges.empty?
738
758
  write_auto_filter(auto_filter, filter_columns, sort_state) if auto_filter
739
759
  write_merge_cells(merge_cells) if merge_cells && !merge_cells.empty?
760
+ write_phonetic_pr(phonetic_pr) if phonetic_pr
740
761
  write_conditional_formatting(conditional_formats) if conditional_formats && !conditional_formats.empty?
741
762
  write_data_validations(data_validations) if data_validations && !data_validations.empty?
742
763
  write_hyperlinks(hyperlinks) if hyperlinks && !hyperlinks.empty?
@@ -747,8 +768,10 @@ module Xlsxrb
747
768
  write_row_breaks(row_breaks) if row_breaks && !row_breaks.empty?
748
769
  write_col_breaks(col_breaks) if col_breaks && !col_breaks.empty?
749
770
  write_cell_watches(cell_watches) if cell_watches && !cell_watches.empty?
771
+ write_ignored_errors(ignored_errors) if ignored_errors && !ignored_errors.empty?
750
772
  @builder.empty_tag("drawing", { "r:id": drawing_rid }) if drawing_rid
751
773
  @builder.empty_tag("legacyDrawing", { "r:id": legacy_drawing_rid }) if legacy_drawing_rid
774
+ @builder.empty_tag("picture", { "r:id": picture_rid }) if picture_rid
752
775
  write_table_parts(tables, table_start_rid) if tables && !tables.empty?
753
776
  write_sparklines(sparkline_groups) if sparkline_groups && !sparkline_groups.empty?
754
777
  @builder.close_tag("worksheet")
@@ -764,6 +787,8 @@ module Xlsxrb
764
787
  return mapped if mapped
765
788
  end
766
789
 
790
+ return 0 if [:default, :none, "default", "none"].include?(style)
791
+
767
792
  style.is_a?(Integer) ? style : nil
768
793
  end
769
794
 
@@ -771,15 +796,29 @@ module Xlsxrb
771
796
 
772
797
  def write_sheet_properties(props)
773
798
  attrs = {}
774
- has_children = props[:tab_color] || !props[:fit_to_page].nil? || !props[:outline_below].nil? || !props[:outline_right].nil?
799
+ attrs[:codeName] = props[:code_name].to_s if props[:code_name]
800
+ tc_attrs = build_tab_color_attrs(props)
801
+
802
+ outline_props = props[:outline_properties] || props[:outline_pr] || props
803
+ apply_styles = props.fetch(:apply_styles, outline_props[:apply_styles])
804
+ summary_below = props.fetch(:summary_below, props.fetch(:outline_below, outline_props[:summary_below]))
805
+ summary_right = props.fetch(:summary_right, props.fetch(:outline_right, outline_props[:summary_right]))
806
+ show_outline_symbols = props.fetch(:show_outline_symbols, props.fetch(:show_symbols, outline_props[:show_outline_symbols]))
807
+ has_outline = !apply_styles.nil? || !summary_below.nil? || !summary_right.nil? || !show_outline_symbols.nil?
808
+
809
+ has_children = tc_attrs || !props[:fit_to_page].nil? || has_outline
775
810
  if has_children
776
811
  @builder.open_tag("sheetPr", attrs)
777
- @builder.empty_tag("tabColor", { rgb: props[:tab_color] }) if props[:tab_color]
778
-
779
- outline_attrs = {}
780
- outline_attrs[:summaryBelow] = props[:outline_below] ? "1" : "0" unless props[:outline_below].nil?
781
- outline_attrs[:summaryRight] = props[:outline_right] ? "1" : "0" unless props[:outline_right].nil?
782
- @builder.empty_tag("outlinePr", outline_attrs) unless outline_attrs.empty?
812
+ @builder.empty_tag("tabColor", tc_attrs) if tc_attrs
813
+
814
+ if has_outline
815
+ outline_attrs = {}
816
+ outline_attrs[:applyStyles] = apply_styles ? "1" : "0" unless apply_styles.nil?
817
+ outline_attrs[:summaryBelow] = summary_below ? "1" : "0" unless summary_below.nil?
818
+ outline_attrs[:summaryRight] = summary_right ? "1" : "0" unless summary_right.nil?
819
+ outline_attrs[:showOutlineSymbols] = show_outline_symbols ? "1" : "0" unless show_outline_symbols.nil?
820
+ @builder.empty_tag("outlinePr", outline_attrs) unless outline_attrs.empty?
821
+ end
783
822
 
784
823
  unless props[:fit_to_page].nil?
785
824
  @builder.empty_tag("pageSetUpPr", { fitToPage: props[:fit_to_page] ? "1" : "0" })
@@ -791,6 +830,29 @@ module Xlsxrb
791
830
  end
792
831
  end
793
832
 
833
+ def build_tab_color_attrs(props)
834
+ tc = props[:tab_color]
835
+ if tc.is_a?(Hash)
836
+ attrs = {}
837
+ attrs[:auto] = "1" if tc[:auto]
838
+ attrs[:indexed] = tc[:indexed].to_s if tc[:indexed]
839
+ attrs[:rgb] = tc[:rgb].to_s.delete_prefix("#") if tc[:rgb]
840
+ attrs[:theme] = tc[:theme].to_s if tc[:theme]
841
+ attrs[:tint] = tc[:tint].to_s if tc[:tint]
842
+ attrs.empty? ? nil : attrs
843
+ elsif tc.is_a?(String) || tc.is_a?(Symbol)
844
+ { rgb: tc.to_s.delete_prefix("#") }
845
+ elsif props[:tab_color_theme]
846
+ attrs = { theme: props[:tab_color_theme].to_s }
847
+ attrs[:tint] = props[:tab_color_tint].to_s if props[:tab_color_tint]
848
+ attrs
849
+ elsif props[:tab_color_indexed]
850
+ { indexed: props[:tab_color_indexed].to_s }
851
+ elsif props[:tab_color_auto]
852
+ { auto: "1" }
853
+ end
854
+ end
855
+
794
856
  def write_sheet_views(freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil)
795
857
  @builder.open_tag("sheetViews")
796
858
  sv_attrs = { workbookViewId: "0" }
@@ -1009,7 +1071,7 @@ module Xlsxrb
1009
1071
  snake = t_str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
1010
1072
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
1011
1073
  .downcase.to_sym
1012
- type = snake if %i[cell_is expression color_scale data_bar icon_set above_average top10 duplicate_values unique_values contains_text not_contains_text begins_with ends_with contains_blanks not_contains_blanks time_period].include?(snake)
1074
+ type = snake if %i[cell_is expression color_scale data_bar icon_set above_average top10 duplicate_values unique_values contains_text not_contains_text begins_with ends_with contains_blanks not_contains_blanks contains_errors not_contains_errors time_period].include?(snake)
1013
1075
  end
1014
1076
 
1015
1077
  cf_type = case type
@@ -1028,12 +1090,17 @@ module Xlsxrb
1028
1090
  when :ends_with then "endsWith"
1029
1091
  when :contains_blanks then "containsBlanks"
1030
1092
  when :not_contains_blanks then "notContainsBlanks"
1093
+ when :contains_errors then "containsErrors"
1094
+ when :not_contains_errors then "notContainsErrors"
1031
1095
  when :time_period then "timePeriod"
1032
1096
  else type.to_s
1033
1097
  end
1034
1098
 
1035
1099
  r_attrs = { type: cf_type, priority: (rule[:priority] || (idx + 1)).to_s }
1036
- r_attrs[:operator] = rule[:operator].to_s if rule[:operator]
1100
+ if rule[:operator]
1101
+ op = rule[:operator]
1102
+ r_attrs[:operator] = DslHelpers::CF_OPERATORS[op.to_sym] || op.to_s
1103
+ end
1037
1104
  r_attrs[:dxfId] = rule[:format_id].to_s if rule[:format_id]
1038
1105
  r_attrs[:stopIfTrue] = "1" if rule[:stop_if_true]
1039
1106
  r_attrs[:aboveAverage] = "0" if rule[:above_average] == false
@@ -1042,23 +1109,22 @@ module Xlsxrb
1042
1109
  r_attrs[:percent] = "1" if rule[:percent]
1043
1110
  r_attrs[:bottom] = "1" if rule[:bottom]
1044
1111
  r_attrs[:text] = rule[:text].to_s if rule[:text]
1045
- r_attrs[:timePeriod] = rule[:time_period].to_s if rule[:time_period]
1112
+ if rule[:time_period]
1113
+ tp = rule[:time_period]
1114
+ r_attrs[:timePeriod] = DslHelpers::CF_TIME_PERIODS[tp.to_sym] || tp.to_s
1115
+ end
1046
1116
  r_attrs[:stdDev] = rule[:std_dev].to_s if rule[:std_dev]
1047
1117
 
1048
1118
  case type
1049
1119
  when :color_scale
1050
1120
  @builder.open_tag("cfRule", r_attrs)
1051
1121
  cs = rule[:color_scale] || {}
1052
- cfvos = cs[:cfvo] || [
1053
- { type: "min" },
1054
- { type: "percent", val: "50" },
1055
- { type: "max" }
1056
- ]
1057
1122
  colors = cs[:colors] || [
1058
1123
  "FFF8696B", # Red
1059
1124
  "FFFFEB84", # Yellow
1060
1125
  "FF63BE7B" # Green
1061
1126
  ]
1127
+ cfvos = cs[:cfvo] || (colors.size == 2 ? [{ type: "min" }, { type: "max" }] : [{ type: "min" }, { type: "percent", val: "50" }, { type: "max" }])
1062
1128
  @builder.tag("colorScale") do |b|
1063
1129
  cfvos.each do |cfvo|
1064
1130
  cfvo_attrs = { type: cfvo[:type] }
@@ -1115,11 +1181,28 @@ module Xlsxrb
1115
1181
  @builder.open_tag("cfRule", r_attrs)
1116
1182
  is = rule[:icon_set] || {}
1117
1183
  icon_style = is[:icon_set] || rule[:icon_style] || "3Arrows"
1118
- cfvos = is[:cfvo] || [
1119
- { type: "percent", val: "0" },
1120
- { type: "percent", val: "33" },
1121
- { type: "percent", val: "67" }
1122
- ]
1184
+ cfvos = is[:cfvo] || if icon_style.to_s.start_with?("4")
1185
+ [
1186
+ { type: "percent", val: "0" },
1187
+ { type: "percent", val: "25" },
1188
+ { type: "percent", val: "50" },
1189
+ { type: "percent", val: "75" }
1190
+ ]
1191
+ elsif icon_style.to_s.start_with?("5")
1192
+ [
1193
+ { type: "percent", val: "0" },
1194
+ { type: "percent", val: "20" },
1195
+ { type: "percent", val: "40" },
1196
+ { type: "percent", val: "60" },
1197
+ { type: "percent", val: "80" }
1198
+ ]
1199
+ else
1200
+ [
1201
+ { type: "percent", val: "0" },
1202
+ { type: "percent", val: "33" },
1203
+ { type: "percent", val: "67" }
1204
+ ]
1205
+ end
1123
1206
  is_attrs = { iconSet: icon_style }
1124
1207
  is_attrs[:reverse] = is[:reverse] ? "1" : "0" unless is[:reverse].nil?
1125
1208
  is_attrs[:percent] = is[:percent] ? "1" : "0" unless is[:percent].nil?
@@ -1167,13 +1250,25 @@ module Xlsxrb
1167
1250
  dv_attrs[:type] = dv_type.to_s if dv_type
1168
1251
  dv_attrs[:operator] = dv[:operator].to_s if dv[:operator]
1169
1252
  dv_attrs[:allowBlank] = "1" if dv[:allow_blank]
1170
- dv_attrs[:showInputMessage] = "1" if dv[:show_input_message]
1171
- dv_attrs[:showErrorMessage] = "1" if dv[:show_error_message]
1253
+ show_input = if dv.key?(:show_input_message)
1254
+ dv[:show_input_message]
1255
+ else
1256
+ !dv[:prompt].nil? || !dv[:prompt_title].nil?
1257
+ end
1258
+ dv_attrs[:showInputMessage] = "1" if show_input
1259
+
1260
+ show_error = if dv.key?(:show_error_message)
1261
+ dv[:show_error_message]
1262
+ else
1263
+ !dv[:error].nil? || !dv[:error_title].nil?
1264
+ end
1265
+ dv_attrs[:showErrorMessage] = "1" if show_error
1266
+
1172
1267
  dv_attrs[:errorStyle] = dv[:error_style].to_s if dv[:error_style]
1173
- dv_attrs[:errorTitle] = dv[:error_title] if dv[:error_title]
1174
- dv_attrs[:error] = dv[:error] if dv[:error]
1175
- dv_attrs[:promptTitle] = dv[:prompt_title] if dv[:prompt_title]
1176
- dv_attrs[:prompt] = dv[:prompt] if dv[:prompt]
1268
+ dv_attrs[:errorTitle] = dv[:error_title].to_s.gsub(/\r\n|\r|\n/, "_x000a_") if dv[:error_title]
1269
+ dv_attrs[:error] = dv[:error].to_s.gsub(/\r\n|\r|\n/, "_x000a_") if dv[:error]
1270
+ dv_attrs[:promptTitle] = dv[:prompt_title].to_s.gsub(/\r\n|\r|\n/, "_x000a_") if dv[:prompt_title]
1271
+ dv_attrs[:prompt] = dv[:prompt].to_s.gsub(/\r\n|\r|\n/, "_x000a_") if dv[:prompt]
1177
1272
  dv_attrs[:showDropDown] = "1" if dv[:show_drop_down]
1178
1273
  dv_attrs[:imeMode] = dv[:ime_mode].to_s if dv[:ime_mode]
1179
1274
 
@@ -1286,6 +1381,41 @@ module Xlsxrb
1286
1381
  @builder.close_tag("cellWatches")
1287
1382
  end
1288
1383
 
1384
+ def write_ignored_errors(errors)
1385
+ @builder.open_tag("ignoredErrors")
1386
+ errors.each do |ie|
1387
+ attrs = { sqref: ie[:sqref].to_s }
1388
+ attrs[:evalError] = "1" if ie[:eval_error]
1389
+ attrs[:twoDigitTextYear] = "1" if ie[:two_digit_text_year]
1390
+ attrs[:numberStoredAsText] = "1" if ie[:number_stored_as_text]
1391
+ attrs[:formula] = "1" if ie[:formula]
1392
+ attrs[:formulaRange] = "1" if ie[:formula_range]
1393
+ attrs[:unlockedFormula] = "1" if ie[:unlocked_formula]
1394
+ attrs[:emptyCellReference] = "1" if ie[:empty_cell_reference]
1395
+ attrs[:listDataValidation] = "1" if ie[:list_data_validation]
1396
+ attrs[:calculatedColumn] = "1" if ie[:calculated_column]
1397
+ @builder.empty_tag("ignoredError", attrs)
1398
+ end
1399
+ @builder.close_tag("ignoredErrors")
1400
+ end
1401
+
1402
+ def write_phonetic_pr(props)
1403
+ attrs = { fontId: (props[:font_id] || props["font_id"] || 0).to_s }
1404
+ type = props[:type] || props["type"]
1405
+ attrs[:type] = type.to_s if type
1406
+ align = props[:alignment] || props["alignment"]
1407
+ attrs[:alignment] = align.to_s if align
1408
+ @builder.empty_tag("phoneticPr", attrs)
1409
+ end
1410
+
1411
+ def write_sheet_calc_pr(props)
1412
+ return unless props
1413
+
1414
+ attrs = {}
1415
+ attrs[:fullCalcOnLoad] = props[:full_calc_on_load] ? "1" : "0" unless props[:full_calc_on_load].nil?
1416
+ @builder.empty_tag("sheetCalcPr", attrs) unless attrs.empty?
1417
+ end
1418
+
1289
1419
  def write_table_parts(tables, start_rid)
1290
1420
  @builder.open_tag("tableParts", { count: tables.size.to_s })
1291
1421
  tables.each_with_index do |_tbl, idx|
@@ -1367,24 +1497,27 @@ module Xlsxrb
1367
1497
  # --- Columns ---
1368
1498
 
1369
1499
  def write_columns(columns)
1500
+ default_width = @sheet_format ? (@sheet_format[:default_col_width] || @sheet_format["default_col_width"]) : nil
1370
1501
  @builder.open_tag("cols")
1371
1502
  columns.each do |col|
1372
1503
  if col.is_a?(Elements::Column)
1373
1504
  min = col.unmapped_data[:min] || (col.index + 1)
1374
1505
  max = col.unmapped_data[:max] || (col.index + 1)
1375
- width = col.width
1506
+ style_id = col.style_index
1507
+ width = col.width || (style_id ? default_width : nil)
1376
1508
  hidden = col.hidden
1377
- custom_width = col.custom_width || col.width
1509
+ custom_width = col.custom_width || !width.nil?
1378
1510
  outline_level = col.outline_level
1379
- style_id = col.style_index
1511
+ collapsed = col.unmapped_data[:collapsed]
1380
1512
  else
1381
1513
  min = col[:min] || ((col[:index] || 0) + 1)
1382
1514
  max = col[:max] || ((col[:index] || 0) + 1)
1383
- width = col[:width]
1515
+ style_id = col[:style_index] || col[:style]
1516
+ width = col[:width] || (style_id ? default_width : nil)
1384
1517
  hidden = col[:hidden]
1385
- custom_width = col[:custom_width] || col[:width]
1518
+ custom_width = col[:custom_width] || !width.nil?
1386
1519
  outline_level = col[:outline_level]
1387
- style_id = col[:style_index] || col[:style]
1520
+ collapsed = col[:collapsed]
1388
1521
  end
1389
1522
  attrs = {
1390
1523
  min: min.to_s,
@@ -1392,8 +1525,9 @@ module Xlsxrb
1392
1525
  }
1393
1526
  attrs[:width] = width.to_s if width
1394
1527
  attrs[:hidden] = "1" if hidden
1395
- attrs[:customWidth] = "1" if custom_width
1528
+ attrs[:customWidth] = "1" if custom_width && width
1396
1529
  attrs[:outlineLevel] = outline_level.to_s if outline_level
1530
+ attrs[:collapsed] = "1" if collapsed
1397
1531
  attrs[:style] = style_id.to_s if style_id.is_a?(Integer)
1398
1532
  @builder.empty_tag("col", attrs)
1399
1533
  end
@@ -1491,6 +1625,73 @@ module Xlsxrb
1491
1625
  Xlsxrb::Elements::Cell.column_letter(index)
1492
1626
  end
1493
1627
 
1628
+ #: (String buf, Elements::RichText rich_text) -> void
1629
+ def write_inline_rich_text(buf, rich_text)
1630
+ buf << "><is>"
1631
+ rich_text.runs.each do |run|
1632
+ font = run[:font]
1633
+ if font && !font.empty?
1634
+ buf << "<r><rPr>"
1635
+ buf << "<b/>" if font[:bold]
1636
+ buf << "<i/>" if font[:italic]
1637
+ buf << "<strike/>" if font[:strike]
1638
+ if font[:underline]
1639
+ u_val = font[:underline]
1640
+ if u_val == true || u_val.to_s == "single"
1641
+ buf << "<u/>"
1642
+ else
1643
+ mapped_u = StyleBuilder::UNDERLINE_MAP[u_val.to_sym] || u_val
1644
+ buf << '<u val="' << mapped_u.to_s << '"/>'
1645
+ end
1646
+ end
1647
+ va_val = font[:vert_align] || font[:vertical_align]
1648
+ va_val ||= "superscript" if font[:superscript]
1649
+ va_val ||= "subscript" if font[:subscript]
1650
+ if va_val
1651
+ mapped_va = StyleBuilder::VERT_ALIGN_MAP[va_val.to_sym] || va_val
1652
+ buf << '<vertAlign val="' << mapped_va.to_s << '"/>'
1653
+ end
1654
+ font_sz = font[:sz] || font[:size]
1655
+ buf << '<sz val="' << font_sz.to_s << '"/>' if font_sz
1656
+ if font[:color]
1657
+ if font[:color].is_a?(Hash)
1658
+ c_hash = font[:color]
1659
+ if c_hash[:rgb]
1660
+ norm = StyleBuilder.normalize_color(c_hash[:rgb], alpha: true) || c_hash[:rgb].to_s
1661
+ buf << '<color rgb="' << norm << '"/>'
1662
+ elsif c_hash[:theme]
1663
+ tint_attr = c_hash[:tint] ? %( tint="#{c_hash[:tint]}") : ""
1664
+ buf << '<color theme="' << c_hash[:theme].to_s << '"' << tint_attr << "/>"
1665
+ elsif c_hash[:indexed]
1666
+ buf << '<color indexed="' << c_hash[:indexed].to_s << '"/>'
1667
+ elsif c_hash[:auto]
1668
+ buf << '<color auto="1"/>'
1669
+ end
1670
+ else
1671
+ raw_color = font[:color]
1672
+ norm_color = StyleBuilder.normalize_color(raw_color, alpha: true) || raw_color.to_s
1673
+ buf << '<color rgb="' << norm_color << '"/>'
1674
+ end
1675
+ elsif font[:theme]
1676
+ tint_attr = font[:tint] ? %( tint="#{font[:tint]}") : ""
1677
+ buf << '<color theme="' << font[:theme].to_s << '"' << tint_attr << "/>"
1678
+ elsif font[:indexed]
1679
+ buf << '<color indexed="' << font[:indexed].to_s << '"/>'
1680
+ elsif font[:auto]
1681
+ buf << '<color auto="1"/>'
1682
+ end
1683
+ buf << '<rFont val="' << escape_xml(font[:name]) << '"/>' if font[:name]
1684
+ buf << '<family val="' << font[:family].to_s << '"/>' if font[:family]
1685
+ buf << '<scheme val="' << font[:scheme].to_s << '"/>' if font[:scheme]
1686
+ buf << "</rPr><t>"
1687
+ else
1688
+ buf << "<r><t>"
1689
+ end
1690
+ buf << escape_xml(run[:text].to_s) << "</t></r>"
1691
+ end
1692
+ buf << "</is></c>"
1693
+ end
1694
+
1494
1695
  INVALID_XML_CHARS_RE = /[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/
1495
1696
  ESCAPE_RE = /[&<>"']/
1496
1697
  ESCAPE_MAP = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;", '"' => "&quot;", "'" => "&apos;" }.freeze