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
@@ -93,6 +93,9 @@ module Xlsxrb
93
93
  selection: sheet[:selection],
94
94
  sheet_view: sheet[:sheet_view],
95
95
  sheet_properties: sheet[:sheet_properties],
96
+ sheet_format: sheet[:sheet_format],
97
+ protected_ranges: sheet[:protected_ranges],
98
+ cell_watches: sheet[:cell_watches],
96
99
  tables: sheet_tables,
97
100
  table_start_rid: info[:table_start_rid],
98
101
  legacy_drawing_rid: info[:vml_rid],
@@ -493,6 +496,14 @@ module Xlsxrb
493
496
  b.empty_tag("workbookProtection", wp_attrs) unless wp_attrs.empty?
494
497
  end
495
498
 
499
+ # Workbook view: which sheet tab is selected when the file is opened
500
+ active_tab = @workbook_properties&.[](:active_tab)
501
+ if active_tab
502
+ b.open_tag("bookViews")
503
+ b.empty_tag("workbookView", { activeTab: active_tab.to_s })
504
+ b.close_tag("bookViews")
505
+ end
506
+
496
507
  b.open_tag("sheets")
497
508
  @sheets.each_with_index do |sheet, idx|
498
509
  s_name = sheet.respond_to?(:name) ? sheet.name : sheet[:name]
@@ -528,6 +539,9 @@ module Xlsxrb
528
539
  b.close_tag("definedNames")
529
540
  end
530
541
 
542
+ # Calculation properties: ask Excel to recalculate every formula when the file is opened
543
+ b.empty_tag("calcPr", { fullCalcOnLoad: "1" }) if @workbook_properties&.[](:full_calc_on_load)
544
+
531
545
  # Pivot caches
532
546
  total_pivots = @sheets.sum { |s| (s[:pivot_tables] || []).size }
533
547
  if total_pivots.positive?
@@ -594,7 +608,11 @@ module Xlsxrb
594
608
  b.open_tag("styleSheet", { xmlns: SSML_NS })
595
609
 
596
610
  # Number formats
597
- raw_num_fmts = @styles&.dig(:num_fmts) || []
611
+ raw_num_fmts = if @styles.respond_to?(:num_fmts)
612
+ @styles.num_fmts
613
+ else
614
+ @styles&.dig(:num_fmts) || []
615
+ end
598
616
  num_fmts = if raw_num_fmts.is_a?(Hash)
599
617
  raw_num_fmts.map { |k, v| { num_fmt_id: k.to_i, format_code: v.to_s } }
600
618
  elsif raw_num_fmts.is_a?(Array)
@@ -619,7 +637,11 @@ module Xlsxrb
619
637
  end
620
638
 
621
639
  # Fonts
622
- fonts = @styles&.dig(:fonts) || []
640
+ fonts = if @styles.respond_to?(:fonts)
641
+ @styles.fonts
642
+ else
643
+ @styles&.dig(:fonts) || []
644
+ end
623
645
  fonts = [{}] if fonts.empty? # At least one default font
624
646
  b.tag("fonts", { count: fonts.size.to_s }) do |_|
625
647
  fonts.each do |font_props|
@@ -631,14 +653,19 @@ module Xlsxrb
631
653
  b.empty_tag("u", font_props[:underline] == "single" ? {} : { val: font_props[:underline] })
632
654
  end
633
655
  b.empty_tag("sz", { val: (font_props[:sz] || 11).to_s }) if font_props[:sz] || fonts == [{}]
634
- b.empty_tag("color", { rgb: font_props[:color] }) if font_props[:color]
656
+ color_attrs = color_tag_attrs(font_props[:color] || (font_props.key?(:rgb) || font_props.key?(:theme) || font_props.key?(:indexed) ? font_props : nil))
657
+ b.empty_tag("color", color_attrs) if color_attrs
635
658
  b.empty_tag("name", { val: font_props[:name] || "Calibri" })
636
659
  end
637
660
  end
638
661
  end
639
662
 
640
663
  # Fills
641
- fills = @styles&.dig(:fills) || []
664
+ fills = if @styles.respond_to?(:fills)
665
+ @styles.fills
666
+ else
667
+ @styles&.dig(:fills) || []
668
+ end
642
669
  fills = [{ pattern: "none" }, { pattern: "gray125" }] if fills.empty?
643
670
  b.tag("fills", { count: fills.size.to_s }) do |_|
644
671
  fills.each do |fill_props|
@@ -647,14 +674,18 @@ module Xlsxrb
647
674
  b.open_tag("gradientFill", { type: fill_props[:gradient][:type], degree: fill_props[:gradient][:degree]&.to_s }.compact)
648
675
  fill_props[:gradient][:stops].each do |stop|
649
676
  b.tag("stop", { position: stop[:position].to_s }) do |_|
650
- b.empty_tag("color", { rgb: stop[:color] })
677
+ c_attrs = color_tag_attrs(stop[:color] || stop)
678
+ b.empty_tag("color", c_attrs) if c_attrs
651
679
  end
652
680
  end
653
681
  b.close_tag("gradientFill")
654
682
  else
655
683
  b.tag("patternFill", { patternType: fill_props[:pattern] || "none" }) do |_|
656
- b.empty_tag("fgColor", { rgb: fill_props[:fg_color] }) if fill_props[:fg_color]
657
- b.empty_tag("bgColor", { rgb: fill_props[:bg_color] }) if fill_props[:bg_color]
684
+ fg_attrs = color_tag_attrs(fill_props[:fg_color] || (fill_props[:fg_color_theme] ? { theme: fill_props[:fg_color_theme], tint: fill_props[:fg_color_tint] } : nil) || (fill_props[:fg_color_indexed] ? { indexed: fill_props[:fg_color_indexed] } : nil))
685
+ b.empty_tag("fgColor", fg_attrs) if fg_attrs
686
+
687
+ bg_attrs = color_tag_attrs(fill_props[:bg_color] || (fill_props[:bg_color_theme] ? { theme: fill_props[:bg_color_theme], tint: fill_props[:bg_color_tint] } : nil) || (fill_props[:bg_color_indexed] ? { indexed: fill_props[:bg_color_indexed] } : nil))
688
+ b.empty_tag("bgColor", bg_attrs) if bg_attrs
658
689
  end
659
690
  end
660
691
  end
@@ -662,7 +693,11 @@ module Xlsxrb
662
693
  end
663
694
 
664
695
  # Borders
665
- borders = @styles&.dig(:borders) || []
696
+ borders = if @styles.respond_to?(:borders)
697
+ @styles.borders
698
+ else
699
+ @styles&.dig(:borders) || []
700
+ end
666
701
  borders = [{}] if borders.empty?
667
702
  b.tag("borders", { count: borders.size.to_s }) do |_|
668
703
  borders.each do |border_props|
@@ -674,16 +709,8 @@ module Xlsxrb
674
709
  side_data = border_props[side]
675
710
  if side_data
676
711
  b.tag(side.to_s, { style: side_data[:style] }) do |_|
677
- c = side_data[:color]
678
- if c.is_a?(Hash)
679
- c_attrs = {}
680
- c_attrs[:rgb] = c[:rgb] if c[:rgb]
681
- c_attrs[:theme] = c[:theme].to_s if c[:theme]
682
- c_attrs[:tint] = c[:tint].to_s if c[:tint]
683
- b.empty_tag("color", c_attrs) unless c_attrs.empty?
684
- elsif c
685
- b.empty_tag("color", { rgb: c })
686
- end
712
+ c_attrs = color_tag_attrs(side_data[:color])
713
+ b.empty_tag("color", c_attrs) if c_attrs
687
714
  end
688
715
  else
689
716
  b.empty_tag(side.to_s)
@@ -699,7 +726,11 @@ module Xlsxrb
699
726
  end
700
727
 
701
728
  # Cell XFs (formatting)
702
- xf_entries = @styles&.dig(:cell_xfs) || @styles&.dig(:xf_entries) || []
729
+ xf_entries = if @styles.respond_to?(:cell_xfs)
730
+ @styles.cell_xfs
731
+ else
732
+ @styles&.dig(:cell_xfs) || @styles&.dig(:xf_entries) || []
733
+ end
703
734
  xf_count = [xf_entries.size, 1].max
704
735
  b.tag("cellXfs", { count: xf_count.to_s }) do |_|
705
736
  xf_entries.each do |xf|
@@ -748,7 +779,11 @@ module Xlsxrb
748
779
  end
749
780
 
750
781
  # Differential formats (for conditional formatting)
751
- dxfs = @styles&.dig(:dxfs) || []
782
+ dxfs = if @styles.respond_to?(:dxfs)
783
+ @styles.dxfs
784
+ else
785
+ @styles&.dig(:dxfs) || []
786
+ end
752
787
  unless dxfs.empty?
753
788
  b.tag("dxfs", { count: dxfs.size.to_s }) do |_|
754
789
  dxfs.each do |dxf|
@@ -759,6 +794,9 @@ module Xlsxrb
759
794
  b.empty_tag("numFmt", { numFmtId: nf[:num_fmt_id].to_s, formatCode: nf[:format_code].to_s })
760
795
  end
761
796
  write_dxf_fill(b, dxf[:fill]) if dxf[:fill]
797
+ write_dxf_alignment(b, dxf[:alignment]) if dxf[:alignment]
798
+ write_dxf_border(b, dxf[:border]) if dxf[:border]
799
+ write_dxf_protection(b, dxf[:protection]) if dxf[:protection]
762
800
  end
763
801
  end
764
802
  end
@@ -769,8 +807,6 @@ module Xlsxrb
769
807
  end
770
808
 
771
809
  def preprocess_conditional_formats!
772
- @styles[:dxfs] ||= []
773
-
774
810
  @sheets.each do |sheet|
775
811
  rules = sheet[:conditional_formats]
776
812
  next if rules.nil? || rules.empty?
@@ -782,11 +818,15 @@ module Xlsxrb
782
818
  dxf = normalized.delete(:dxf) || extract_dxf_from_rule!(normalized)
783
819
  next normalized unless dxf
784
820
 
785
- dxf_id = @styles[:dxfs].index(dxf)
786
- unless dxf_id
787
- @styles[:dxfs] << dxf
788
- dxf_id = @styles[:dxfs].size - 1
789
- end
821
+ dxf_id = if @styles.respond_to?(:add_dxf)
822
+ @styles.dxfs.index(dxf) || @styles.add_dxf(dxf)
823
+ else
824
+ @styles[:dxfs] ||= []
825
+ @styles[:dxfs].index(dxf) || begin
826
+ @styles[:dxfs] << dxf
827
+ @styles[:dxfs].size - 1
828
+ end
829
+ end
790
830
  normalized[:format_id] = dxf_id
791
831
  normalized
792
832
  end
@@ -814,16 +854,24 @@ module Xlsxrb
814
854
 
815
855
  def write_dxf_font(builder, font)
816
856
  builder.tag("font") do |_|
817
- builder.empty_tag("b") if font[:bold]
818
- builder.empty_tag("i") if font[:italic]
857
+ if font.key?(:bold)
858
+ font[:bold] ? builder.empty_tag("b") : builder.empty_tag("b", { val: "0" })
859
+ end
860
+ if font.key?(:italic)
861
+ font[:italic] ? builder.empty_tag("i") : builder.empty_tag("i", { val: "0" })
862
+ end
863
+ if font.key?(:strike)
864
+ font[:strike] ? builder.empty_tag("strike") : builder.empty_tag("strike", { val: "0" })
865
+ end
819
866
  if font[:underline]
820
- if font[:underline] == true
867
+ if [true, "single"].include?(font[:underline])
821
868
  builder.empty_tag("u")
822
869
  else
823
870
  builder.empty_tag("u", { val: font[:underline] })
824
871
  end
825
872
  end
826
- builder.empty_tag("color", { rgb: font[:color] }) if font[:color]
873
+ color_attrs = color_tag_attrs(font[:color] || font)
874
+ builder.empty_tag("color", color_attrs) if color_attrs
827
875
  builder.empty_tag("name", { val: font[:name] }) if font[:name]
828
876
  builder.empty_tag("sz", { val: font[:sz].to_s }) if font[:sz]
829
877
  end
@@ -831,11 +879,120 @@ module Xlsxrb
831
879
 
832
880
  def write_dxf_fill(builder, fill)
833
881
  builder.tag("fill") do |_|
834
- pattern = fill[:pattern] || "solid"
835
- builder.tag("patternFill", { patternType: pattern }) do |_|
836
- builder.empty_tag("fgColor", { rgb: fill[:fg_color] }) if fill[:fg_color]
837
- builder.empty_tag("bgColor", { rgb: fill[:bg_color] }) if fill[:bg_color]
882
+ pattern_attrs = {}
883
+ pattern_attrs[:patternType] = fill[:pattern] if fill[:pattern]
884
+ builder.tag("patternFill", pattern_attrs) do |_|
885
+ fg_attrs = color_tag_attrs(fill[:fg_color])
886
+ builder.empty_tag("fgColor", fg_attrs) if fg_attrs
887
+ bg_attrs = color_tag_attrs(fill[:bg_color])
888
+ builder.empty_tag("bgColor", bg_attrs) if bg_attrs
889
+ end
890
+ end
891
+ end
892
+
893
+ def write_dxf_alignment(builder, alignment)
894
+ return if alignment.nil? || alignment.empty?
895
+
896
+ align_attrs = {}
897
+ align_attrs[:horizontal] = alignment[:horizontal] if alignment[:horizontal]
898
+ align_attrs[:vertical] = alignment[:vertical] if alignment[:vertical]
899
+ align_attrs[:wrapText] = "1" if alignment[:wrap_text]
900
+ align_attrs[:textRotation] = alignment[:text_rotation].to_s if alignment[:text_rotation]
901
+ align_attrs[:indent] = alignment[:indent].to_s if alignment[:indent]
902
+ align_attrs[:relativeIndent] = alignment[:relative_indent].to_s if alignment[:relative_indent]
903
+ align_attrs[:shrinkToFit] = "1" if alignment[:shrink_to_fit]
904
+ align_attrs[:readingOrder] = alignment[:reading_order].to_s if alignment[:reading_order]
905
+ align_attrs[:justifyLastLine] = "1" if alignment[:justify_last_line]
906
+ builder.empty_tag("alignment", align_attrs) unless align_attrs.empty?
907
+ end
908
+
909
+ def write_dxf_border(builder, border)
910
+ return if border.nil? || border.empty?
911
+
912
+ border_attrs = {}
913
+ border_attrs[:diagonalUp] = "1" if border[:diagonal_up]
914
+ border_attrs[:diagonalDown] = "1" if border[:diagonal_down]
915
+
916
+ builder.tag("border", border_attrs) do |_|
917
+ %i[left right top bottom diagonal].each do |side|
918
+ side_data = border[side]
919
+ next unless side_data
920
+
921
+ tag_attrs = {}
922
+ tag_attrs[:style] = side_data[:style] if side_data[:style]
923
+ c_attrs = color_tag_attrs(side_data[:color])
924
+ if c_attrs
925
+ builder.tag(side.to_s, tag_attrs) do |_|
926
+ builder.empty_tag("color", c_attrs)
927
+ end
928
+ elsif !tag_attrs.empty?
929
+ builder.empty_tag(side.to_s, tag_attrs)
930
+ else
931
+ builder.empty_tag(side.to_s)
932
+ end
933
+ end
934
+ end
935
+ end
936
+
937
+ def write_dxf_protection(builder, protection)
938
+ return if protection.nil? || protection.empty?
939
+
940
+ prot_attrs = {}
941
+ prot_attrs[:locked] = protection[:locked] ? "1" : "0" unless protection[:locked].nil?
942
+ prot_attrs[:hidden] = protection[:hidden] ? "1" : "0" unless protection[:hidden].nil?
943
+ builder.empty_tag("protection", prot_attrs) unless prot_attrs.empty?
944
+ end
945
+
946
+ # : (untyped rgb) -> ::String?
947
+ def format_rgb_hex(rgb)
948
+ return nil unless rgb
949
+
950
+ str = rgb.to_s.sub(/^#/, "").strip
951
+ return nil if str.empty?
952
+
953
+ case str
954
+ when /\A[0-9a-fA-F]{8}\z/
955
+ str.upcase
956
+ when /\A[0-9a-fA-F]{6}\z/
957
+ "FF#{str.upcase}"
958
+ when /\A[0-9a-fA-F]{3}\z/
959
+ r, g, b = str.chars
960
+ "FF#{r}#{r}#{g}#{g}#{b}#{b}".upcase
961
+ else
962
+ resolved = StyleBuilder.resolve_color(rgb)
963
+ case resolved
964
+ when /\A[0-9a-fA-F]{8}\z/
965
+ resolved.upcase
966
+ when /\A[0-9a-fA-F]{6}\z/
967
+ "FF#{resolved.upcase}"
968
+ end
969
+ end
970
+ end
971
+
972
+ # : (untyped spec) -> ::Hash[Symbol, ::String]?
973
+ def color_tag_attrs(spec)
974
+ return nil unless spec
975
+
976
+ spec = spec[:color] if spec.is_a?(Hash) && spec[:color].is_a?(Hash)
977
+ return nil unless spec
978
+
979
+ if spec.is_a?(Hash)
980
+ if spec[:auto]
981
+ { auto: "1" }
982
+ elsif spec[:rgb] || (spec[:color] && !spec[:color].is_a?(Hash))
983
+ raw_rgb = spec[:rgb] || spec[:color]
984
+ fmt = format_rgb_hex(raw_rgb)
985
+ fmt ? { rgb: fmt } : nil
986
+ elsif spec[:theme]
987
+ attrs = { theme: spec[:theme].to_s }
988
+ attrs[:tint] = spec[:tint].to_s if spec[:tint]
989
+ attrs
990
+ elsif spec[:indexed]
991
+ { indexed: spec[:indexed].to_s }
838
992
  end
993
+ elsif spec.is_a?(String) || spec.is_a?(Symbol)
994
+ fmt = format_rgb_hex(spec)
995
+ fmt ? { rgb: fmt } : nil
839
996
  end
840
997
  end
841
998
 
@@ -846,6 +1003,7 @@ module Xlsxrb
846
1003
 
847
1004
  def escape_xml(value)
848
1005
  str = value.to_s
1006
+ str = str.dup.force_encoding(Encoding::UTF_8) if str.encoding == Encoding::BINARY
849
1007
  str = str.gsub(INVALID_XML_CHARS_RE, "") if str.match?(INVALID_XML_CHARS_RE)
850
1008
  str.match?(ESCAPE_RE) ? str.gsub(ESCAPE_RE, ESCAPE_MAP) : str
851
1009
  end
@@ -878,11 +1036,10 @@ module Xlsxrb
878
1036
  end
879
1037
  buf << '<vertAlign val="' << font[:vert_align].to_s << '"/>' if font[:vert_align]
880
1038
  buf << '<sz val="' << font[:sz].to_s << '"/>' if font[:sz]
881
- if font[:color]
882
- buf << '<color rgb="' << font[:color].to_s << '"/>'
883
- elsif font[:theme]
884
- tint_attr = font[:tint] ? " tint=\"#{font[:tint]}\"" : ""
885
- buf << '<color theme="' << font[:theme].to_s << '"' << tint_attr << "/>"
1039
+ color_attrs = color_tag_attrs(font[:color] || font)
1040
+ if color_attrs
1041
+ attrs_str = color_attrs.map { |k, v| %(#{k}="#{v}") }.join(" ")
1042
+ buf << "<color #{attrs_str}/>"
886
1043
  end
887
1044
  buf << '<rFont val="' << escape_xml(font[:name]) << '"/>' if font[:name]
888
1045
  buf << '<family val="' << font[:family].to_s << '"/>' if font[:family]
@@ -891,17 +1048,11 @@ module Xlsxrb
891
1048
  else
892
1049
  buf << "<r><t>"
893
1050
  end
894
- buf << escape_xml(run[:text]) << "</t></r>"
1051
+ buf << escape_xml(run[:text].to_s) << "</t></r>"
895
1052
  end
896
1053
  buf << "</si>"
897
1054
  else
898
- s = str.to_s
899
- needs_space = s.start_with?(" ", "\t", "\n") || s.end_with?(" ", "\t", "\n")
900
- if needs_space
901
- buf << '<si><t xml:space="preserve">' << escape_xml(s) << "</t></si>"
902
- else
903
- buf << "<si><t>" << escape_xml(s) << "</t></si>"
904
- end
1055
+ buf << "<si><t>" << escape_xml(str.to_s) << "</t></si>"
905
1056
  end
906
1057
 
907
1058
  if buf.bytesize >= 32_768
@@ -917,14 +1068,14 @@ module Xlsxrb
917
1068
  end
918
1069
 
919
1070
  def write_worksheet_xml(io, sheet, drawing_rid: nil, sheet_protection: nil,
920
- auto_filter: nil, filter_columns: nil, sort_state: nil,
921
- merge_cells: nil, conditional_formats: nil,
1071
+ protected_ranges: nil, auto_filter: nil, filter_columns: nil,
1072
+ sort_state: nil, merge_cells: nil, conditional_formats: nil,
922
1073
  data_validations: nil, hyperlinks: nil,
923
1074
  print_options: nil, page_margins: nil, page_setup: nil,
924
1075
  header_footer: nil, row_breaks: nil, col_breaks: nil,
925
- freeze_pane: nil, split_pane: nil, selection: nil,
926
- sheet_view: nil, sheet_properties: nil,
927
- tables: nil, table_start_rid: nil,
1076
+ cell_watches: nil, freeze_pane: nil, split_pane: nil,
1077
+ selection: nil, sheet_view: nil, sheet_properties: nil,
1078
+ sheet_format: nil, tables: nil, table_start_rid: nil,
928
1079
  legacy_drawing_rid: nil, sparkline_groups: nil)
929
1080
  date1904 = @workbook_properties&.[](:date1904) ? true : false
930
1081
  ws = WorksheetWriter.new(io, date1904: date1904)
@@ -934,7 +1085,8 @@ module Xlsxrb
934
1085
  freeze_pane: freeze_pane,
935
1086
  split_pane: split_pane,
936
1087
  selection: selection,
937
- sheet_view: sheet_view
1088
+ sheet_view: sheet_view,
1089
+ sheet_format: sheet_format
938
1090
  )
939
1091
  if sheet[:rows_tmp_path]
940
1092
  File.open(sheet[:rows_tmp_path], "rb") do |f|
@@ -961,6 +1113,7 @@ module Xlsxrb
961
1113
  ws.finish(
962
1114
  drawing_rid: drawing_rid,
963
1115
  sheet_protection: sheet_protection,
1116
+ protected_ranges: protected_ranges,
964
1117
  auto_filter: auto_filter,
965
1118
  filter_columns: filter_columns,
966
1119
  sort_state: sort_state,
@@ -974,6 +1127,7 @@ module Xlsxrb
974
1127
  header_footer: header_footer,
975
1128
  row_breaks: row_breaks,
976
1129
  col_breaks: col_breaks,
1130
+ cell_watches: cell_watches,
977
1131
  tables: tables,
978
1132
  table_start_rid: table_start_rid,
979
1133
  legacy_drawing_rid: legacy_drawing_rid,
@@ -1002,8 +1156,25 @@ module Xlsxrb
1002
1156
  # Table columns
1003
1157
  columns = tbl[:columns] || []
1004
1158
  b.open_tag("tableColumns", { count: columns.size.to_s })
1005
- columns.each_with_index do |col_name, ci|
1006
- b.empty_tag("tableColumn", { id: (ci + 1).to_s, name: col_name })
1159
+ columns.each_with_index do |col, ci|
1160
+ col_name = col.is_a?(Hash) ? col[:name] : col
1161
+ col_attrs = { id: (ci + 1).to_s, name: col_name.to_s }
1162
+ if col.is_a?(Hash)
1163
+ col_attrs[:totalsRowFunction] = col[:totals_row_function].to_s if col[:totals_row_function]
1164
+ col_attrs[:totalsRowLabel] = col[:totals_row_label].to_s if col[:totals_row_label]
1165
+ col_attrs[:dataDxfId] = col[:data_dxf_id].to_s if col[:data_dxf_id]
1166
+ col_attrs[:totalsRowDxfId] = col[:totals_row_dxf_id].to_s if col[:totals_row_dxf_id]
1167
+ col_attrs[:headerRowDxfId] = col[:header_row_dxf_id].to_s if col[:header_row_dxf_id]
1168
+ col_attrs[:dataCellStyle] = col[:data_cell_style].to_s if col[:data_cell_style]
1169
+ end
1170
+ if col.is_a?(Hash) && (col[:calculated_column_formula] || col[:totals_row_formula])
1171
+ b.open_tag("tableColumn", col_attrs)
1172
+ b.tag("calculatedColumnFormula") { |cb| cb.text(col[:calculated_column_formula].to_s) } if col[:calculated_column_formula]
1173
+ b.tag("totalsRowFormula") { |tb| tb.text(col[:totals_row_formula].to_s) } if col[:totals_row_formula]
1174
+ b.close_tag("tableColumn")
1175
+ else
1176
+ b.empty_tag("tableColumn", col_attrs)
1177
+ end
1007
1178
  end
1008
1179
  b.close_tag("tableColumns")
1009
1180
 
@@ -1102,11 +1273,13 @@ module Xlsxrb
1102
1273
  @custom_properties.each_with_index do |prop, idx|
1103
1274
  b.open_tag("property", { fmtid: "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}", pid: (idx + 2).to_s, name: prop[:name] })
1104
1275
  case prop[:type]
1105
- when :number
1276
+ when :number, :int, :i4
1106
1277
  b.tag("vt:i4") { |_| b.text(prop[:value].to_s) }
1107
- when :bool
1278
+ when :float, :r8
1279
+ b.tag("vt:r8") { |_| b.text(prop[:value].to_s) }
1280
+ when :bool, :boolean
1108
1281
  b.tag("vt:bool") { |_| b.text(prop[:value] ? "true" : "false") }
1109
- when :date
1282
+ when :date, :filetime
1110
1283
  b.tag("vt:filetime") { |_| b.text(prop[:value].to_s) }
1111
1284
  else
1112
1285
  b.tag("vt:lpwstr") { |_| b.text(prop[:value].to_s) }