xlsxrb 0.1.11 → 0.1.13

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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -34
  3. data/README.md +26 -21
  4. data/lib/xlsxrb/chart_builder.rb +18 -18
  5. data/lib/xlsxrb/dsl_helpers.rb +175 -0
  6. data/lib/xlsxrb/elements/cell.rb +56 -22
  7. data/lib/xlsxrb/elements/column.rb +27 -3
  8. data/lib/xlsxrb/elements/row.rb +27 -8
  9. data/lib/xlsxrb/elements/workbook.rb +27 -14
  10. data/lib/xlsxrb/elements/worksheet.rb +17 -1
  11. data/lib/xlsxrb/ooxml/cfb.rb +9 -6
  12. data/lib/xlsxrb/ooxml/crypto/agile.rb +5 -5
  13. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +784 -0
  14. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +3478 -0
  15. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +1796 -0
  16. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +428 -0
  17. data/lib/xlsxrb/ooxml/reader/listeners.rb +4 -6446
  18. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +3 -5
  19. data/lib/xlsxrb/ooxml/utils.rb +5 -6
  20. data/lib/xlsxrb/ooxml/workbook_writer.rb +5 -7
  21. data/lib/xlsxrb/ooxml/worksheet_parser.rb +172 -9
  22. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +1602 -0
  23. data/lib/xlsxrb/ooxml/writer/features_xml.rb +731 -0
  24. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +348 -0
  25. data/lib/xlsxrb/ooxml/writer.rb +9 -2646
  26. data/lib/xlsxrb/ooxml/xml_builder.rb +31 -2
  27. data/lib/xlsxrb/ooxml/zip_generator.rb +26 -5
  28. data/lib/xlsxrb/ooxml/zip_reader.rb +364 -78
  29. data/lib/xlsxrb/ooxml/zip_writer.rb +32 -5
  30. data/lib/xlsxrb/stream_sheet.rb +94 -6
  31. data/lib/xlsxrb/stream_writer.rb +48 -115
  32. data/lib/xlsxrb/style_builder.rb +10 -1
  33. data/lib/xlsxrb/version.rb +1 -1
  34. data/lib/xlsxrb/workbook_builder.rb +1 -1
  35. data/lib/xlsxrb/worksheet_builder.rb +19 -81
  36. data/lib/xlsxrb.rb +449 -171
  37. data/sig/generated/xlsxrb/chart_builder.rbs +36 -36
  38. data/sig/generated/xlsxrb/dsl_helpers.rbs +92 -0
  39. data/sig/generated/xlsxrb/elements/cell.rbs +29 -0
  40. data/sig/generated/xlsxrb/elements/column.rbs +32 -21
  41. data/sig/generated/xlsxrb/elements/row.rbs +94 -14
  42. data/sig/generated/xlsxrb/elements/workbook.rbs +102 -10
  43. data/sig/generated/xlsxrb/elements/worksheet.rbs +9 -0
  44. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +173 -0
  45. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +400 -0
  46. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +442 -0
  47. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +80 -0
  48. data/sig/generated/xlsxrb/ooxml/reader/listeners.rbs +0 -1072
  49. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +0 -2
  50. data/sig/generated/xlsxrb/ooxml/utils.rbs +2 -0
  51. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +5 -4
  52. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +79 -0
  53. data/sig/generated/xlsxrb/ooxml/writer/features_xml.rbs +83 -0
  54. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +53 -0
  55. data/sig/generated/xlsxrb/ooxml/writer.rbs +6 -185
  56. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +18 -0
  57. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +15 -0
  58. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +96 -8
  59. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +5 -1
  60. data/sig/generated/xlsxrb/stream_sheet.rbs +43 -3
  61. data/sig/generated/xlsxrb/style_builder.rbs +7 -0
  62. data/sig/generated/xlsxrb.rbs +18 -11
  63. metadata +22 -144
  64. data/.devcontainer/Dockerfile +0 -65
  65. data/.devcontainer/devcontainer.json +0 -17
  66. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +0 -9
  67. data/.gem_rbs_collection/ast/2.4/ast.rbs +0 -73
  68. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +0 -9
  69. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +0 -4
  70. data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +0 -16
  71. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +0 -96
  72. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +0 -4
  73. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +0 -68
  74. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +0 -249
  75. data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +0 -4
  76. data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +0 -47
  77. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +0 -5
  78. data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +0 -9
  79. data/.gem_rbs_collection/csv/3.3/csv.rbs +0 -3871
  80. data/.gem_rbs_collection/csv/3.3/manifest.yaml +0 -3
  81. data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +0 -9
  82. data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +0 -48
  83. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +0 -9
  84. data/.gem_rbs_collection/listen/3.9/listen.rbs +0 -25
  85. data/.gem_rbs_collection/listen/3.9/listener.rbs +0 -24
  86. data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +0 -9
  87. data/.gem_rbs_collection/logger/1.7/formatter.rbs +0 -45
  88. data/.gem_rbs_collection/logger/1.7/log_device.rbs +0 -100
  89. data/.gem_rbs_collection/logger/1.7/logger.rbs +0 -796
  90. data/.gem_rbs_collection/logger/1.7/manifest.yaml +0 -2
  91. data/.gem_rbs_collection/logger/1.7/period.rbs +0 -17
  92. data/.gem_rbs_collection/logger/1.7/severity.rbs +0 -34
  93. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +0 -9
  94. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +0 -2332
  95. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +0 -4
  96. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +0 -9
  97. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +0 -86
  98. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +0 -9
  99. data/.gem_rbs_collection/parser/3.2/manifest.yaml +0 -7
  100. data/.gem_rbs_collection/parser/3.2/parser.rbs +0 -194
  101. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +0 -4
  102. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +0 -9
  103. data/.gem_rbs_collection/rainbow/3.0/global.rbs +0 -7
  104. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +0 -209
  105. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +0 -5
  106. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +0 -9
  107. data/.gem_rbs_collection/rake/13.0/manifest.yaml +0 -2
  108. data/.gem_rbs_collection/rake/13.0/rake.rbs +0 -39
  109. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +0 -9
  110. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +0 -17
  111. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +0 -9
  112. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +0 -208
  113. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +0 -9
  114. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +0 -903
  115. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +0 -9
  116. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +0 -8
  117. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +0 -42
  118. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +0 -5
  119. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +0 -47
  120. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +0 -30
  121. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +0 -9
  122. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +0 -7
  123. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +0 -19
  124. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +0 -31
  125. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +0 -18
  126. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +0 -12
  127. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +0 -11
  128. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +0 -13
  129. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +0 -95
  130. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +0 -31
  131. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +0 -58
  132. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +0 -24
  133. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +0 -17
  134. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +0 -23
  135. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +0 -22
  136. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +0 -30
  137. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +0 -20
  138. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +0 -15
  139. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +0 -26
  140. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +0 -21
  141. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +0 -131
  142. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +0 -14
  143. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +0 -33
  144. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +0 -21
  145. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +0 -63
  146. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +0 -55
  147. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +0 -35
  148. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +0 -7
  149. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +0 -10
  150. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +0 -22
  151. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +0 -29
  152. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +0 -17
  153. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +0 -13
  154. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +0 -10
  155. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +0 -8
  156. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +0 -6
  157. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +0 -30
  158. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +0 -10
  159. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +0 -10
  160. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +0 -5
  161. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +0 -15
  162. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +0 -3
  163. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +0 -40
  164. data/Rakefile +0 -567
  165. data/Steepfile +0 -18
  166. data/benchmark.rb +0 -406
  167. data/docs/ARCHITECTURE.md +0 -514
  168. data/docs/DEVELOPMENT.md +0 -133
  169. data/docs/PEER_LIBRARIES.md +0 -121
  170. data/docs/QUALITY_ASSURANCE.md +0 -25
  171. data/docs/SPEC_SOURCES.md +0 -48
  172. data/docs/assets/benchmark_results.svg +0 -144
  173. data/docs/assets/lsp_autocompletion.png +0 -0
  174. data/docs/assets/playground_preview.png +0 -0
  175. data/docs/coi-serviceworker.js +0 -82
  176. data/docs/office_thread.js +0 -77
  177. data/docs/preview.html +0 -719
  178. data/docs/visual/VisualGallery.md +0 -4672
  179. data/docs/wasm/ruby.wasm +0 -0
  180. data/docs/wasm/wasm_doc_helper.css +0 -351
  181. data/docs/wasm/wasm_doc_helper.js +0 -439
  182. data/docs/zeta.js +0 -1107
  183. data/rbs_collection.lock.yaml +0 -252
  184. data/rbs_collection.yaml +0 -19
  185. data/vendor/sdk_runner/Program.cs +0 -93
  186. data/vendor/sdk_runner/sdk_runner.csproj +0 -13
@@ -0,0 +1,731 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Xlsxrb
6
+ module Ooxml
7
+ class Writer
8
+ # Mixin containing Table, PivotTable, Document Properties, and External Link XML generation logic.
9
+ module FeaturesXml
10
+ # : (untyped pivot_table, untyped cache_id) -> untyped
11
+ def generate_pivot_table_xml(pivot_table, cache_id)
12
+ data_caption = pivot_table[:data_caption] || (pivot_table[:data_fields].first ? pivot_table[:data_fields].first[:name] : "Values")
13
+ pt_attrs = %( xmlns="#{SSML_NS}" name="#{xml_escape(pivot_table[:name])}" cacheId="#{cache_id}" dataCaption="#{xml_escape(data_caption)}")
14
+ pt_attrs << %( grandTotalCaption="#{xml_escape(pivot_table[:grand_total_caption])}") if pivot_table[:grand_total_caption]
15
+ pt_attrs << %( errorCaption="#{xml_escape(pivot_table[:error_caption])}") if pivot_table[:error_caption]
16
+ pt_attrs << ' showError="1"' if pivot_table[:show_error]
17
+ pt_attrs << %( missingCaption="#{xml_escape(pivot_table[:missing_caption])}") if pivot_table[:missing_caption]
18
+ pt_attrs << ' showMissing="0"' if pivot_table[:show_missing] == false
19
+ pt_attrs << %( tag="#{xml_escape(pivot_table[:tag])}") if pivot_table[:tag]
20
+ pt_attrs << %( dataOnRows="1") if pivot_table[:data_on_rows]
21
+ pt_attrs << %( dataOnRows="0") unless pivot_table[:data_on_rows]
22
+ pt_attrs << %( rowGrandTotals="0") if pivot_table[:row_grand_totals] == false
23
+ pt_attrs << %( colGrandTotals="0") if pivot_table[:col_grand_totals] == false
24
+ pt_attrs << %( compact="0") if pivot_table[:compact] == false
25
+ pt_attrs << %( outline="0") if pivot_table[:outline] == false
26
+ pt_attrs << %( outlineData="1") if pivot_table[:outline_data]
27
+ pt_attrs << %( compactData="0") if pivot_table[:compact_data] == false
28
+ pt_attrs << %( showHeaders="0") if pivot_table[:show_headers] == false
29
+ pt_attrs << %( showMultipleLabel="0") if pivot_table[:show_multiple_label] == false
30
+ pt_attrs << %( showDataDropDown="0") if pivot_table[:show_data_drop_down] == false
31
+ pt_attrs << %( indent="#{pivot_table[:indent]}") if pivot_table[:indent]
32
+ pt_attrs << ' published="1"' if pivot_table[:published]
33
+ pt_attrs << ' editData="1"' if pivot_table[:edit_data]
34
+ pt_attrs << ' disableFieldList="1"' if pivot_table[:disable_field_list]
35
+ pt_attrs << ' visualTotals="0"' if pivot_table[:visual_totals] == false
36
+ pt_attrs << ' printDrill="1"' if pivot_table[:print_drill]
37
+ pt_attrs << %( createdVersion="#{pivot_table[:created_version]}") if pivot_table[:created_version]
38
+ pt_attrs << %( updatedVersion="#{pivot_table[:updated_version]}") if pivot_table[:updated_version]
39
+ pt_attrs << %( minRefreshableVersion="#{pivot_table[:min_refreshable_version]}") if pivot_table[:min_refreshable_version]
40
+ anf = pivot_table.fetch(:apply_number_formats, false)
41
+ abf = pivot_table.fetch(:apply_border_formats, false)
42
+ aff = pivot_table.fetch(:apply_font_formats, false)
43
+ apf = pivot_table.fetch(:apply_pattern_formats, false)
44
+ aaf = pivot_table.fetch(:apply_alignment_formats, false)
45
+ awf = pivot_table.fetch(:apply_width_height_formats, true)
46
+ pt_attrs << %( applyNumberFormats="#{anf ? 1 : 0}")
47
+ pt_attrs << %( applyBorderFormats="#{abf ? 1 : 0}")
48
+ pt_attrs << %( applyFontFormats="#{aff ? 1 : 0}")
49
+ pt_attrs << %( applyPatternFormats="#{apf ? 1 : 0}")
50
+ pt_attrs << %( applyAlignmentFormats="#{aaf ? 1 : 0}")
51
+ pt_attrs << %( applyWidthHeightFormats="#{awf ? 1 : 0}")
52
+ pt_attrs << ' multipleFieldFilters="0"' if pivot_table[:multiple_field_filters] == false
53
+ pt_attrs << ' showDrill="0"' if pivot_table[:show_drill] == false
54
+ pt_attrs << ' showDataTips="0"' if pivot_table[:show_data_tips] == false
55
+ pt_attrs << ' enableDrill="0"' if pivot_table[:enable_drill] == false
56
+ pt_attrs << ' showMemberPropertyTips="0"' if pivot_table[:show_member_property_tips] == false
57
+ pt_attrs << ' itemPrintTitles="1"' if pivot_table[:item_print_titles]
58
+ pt_attrs << ' fieldPrintTitles="1"' if pivot_table[:field_print_titles]
59
+ pt_attrs << ' preserveFormatting="0"' if pivot_table[:preserve_formatting] == false
60
+ pt_attrs << ' pageOverThenDown="1"' if pivot_table[:page_over_then_down]
61
+ pt_attrs << %( pageWrap="#{pivot_table[:page_wrap]}") if pivot_table[:page_wrap]
62
+ parts = [
63
+ XML_HEADER,
64
+ "<pivotTableDefinition#{pt_attrs}>"
65
+ ]
66
+
67
+ # Compute field count from source range or explicit field_names.
68
+ field_count = if pivot_table[:field_names]
69
+ pivot_table[:field_names].size
70
+ else
71
+ (pivot_table[:row_fields].size + pivot_table[:col_fields].size + pivot_table[:data_fields].size).clamp(1, 100)
72
+ end
73
+ loc_attrs = %(<location ref="#{pivot_table[:dest_ref]}" firstHeaderRow="1" firstDataRow="1" firstDataCol="1")
74
+ loc_attrs << %( rowPageCount="#{pivot_table[:row_page_count]}") if pivot_table[:row_page_count]
75
+ loc_attrs << %( colPageCount="#{pivot_table[:col_page_count]}") if pivot_table[:col_page_count]
76
+ loc_attrs << "/>"
77
+ parts << loc_attrs
78
+ parts << %(<pivotFields count="#{field_count}">)
79
+ field_count.times do |fi|
80
+ attrs = +""
81
+ if pivot_table[:row_fields].include?(fi)
82
+ attrs << ' axis="axisRow"'
83
+ elsif pivot_table[:col_fields].include?(fi)
84
+ attrs << ' axis="axisCol"'
85
+ end
86
+ attrs << ' dataField="1"' if pivot_table[:data_fields].any? { |df| df[:fld] == fi }
87
+ fa = pivot_table[:field_attrs] && pivot_table[:field_attrs][fi]
88
+ attrs << %( compact="#{fa[:compact] ? "1" : "0"}") if fa && !fa[:compact].nil?
89
+ attrs << %( outline="#{fa[:outline] ? "1" : "0"}") if fa && !fa[:outline].nil?
90
+ attrs << %( subtotalTop="#{fa[:subtotal_top] ? "1" : "0"}") if fa && !fa[:subtotal_top].nil?
91
+ attrs << %( showAll="#{fa && fa[:show_all] == true ? "1" : "0"}")
92
+ attrs << %( numFmtId="#{fa[:num_fmt_id]}") if fa && fa[:num_fmt_id]
93
+ attrs << %( sortType="#{xml_escape(fa[:sort_type])}") if fa && fa[:sort_type]
94
+ attrs << ' defaultSubtotal="0"' if fa && fa[:default_subtotal] == false
95
+ attrs << ' insertBlankRow="1"' if fa && fa[:insert_blank_row]
96
+ attrs << ' insertPageBreak="1"' if fa && fa[:insert_page_break]
97
+ attrs << ' includeNewItemsInFilter="1"' if fa && fa[:include_new_items_in_filter]
98
+
99
+ field_items = pivot_table[:items] && pivot_table[:items][fi]
100
+ if field_items
101
+ parts << "<pivotField#{attrs}>"
102
+ parts << %(<items count="#{field_items.size + 1}">)
103
+ field_items.size.times { |ix| parts << %(<item x="#{ix}"/>) }
104
+ parts << '<item t="default"/>'
105
+ parts << "</items>"
106
+ parts << "</pivotField>"
107
+ else
108
+ parts << "<pivotField#{attrs}/>"
109
+ end
110
+ end
111
+ parts << "</pivotFields>"
112
+
113
+ unless pivot_table[:row_fields].empty?
114
+ parts << %(<rowFields count="#{pivot_table[:row_fields].size}">)
115
+ pivot_table[:row_fields].each { |f| parts << %(<field x="#{f}"/>) }
116
+ parts << "</rowFields>"
117
+ end
118
+
119
+ unless pivot_table[:col_fields].empty?
120
+ parts << %(<colFields count="#{pivot_table[:col_fields].size}">)
121
+ pivot_table[:col_fields].each { |f| parts << %(<field x="#{f}"/>) }
122
+ parts << "</colFields>"
123
+ end
124
+
125
+ unless pivot_table[:data_fields].empty?
126
+ parts << %(<dataFields count="#{pivot_table[:data_fields].size}">)
127
+ pivot_table[:data_fields].each do |df|
128
+ df_attrs = %( name="#{xml_escape(df[:name])}" fld="#{df[:fld]}" subtotal="#{df[:subtotal] || "sum"}")
129
+ df_attrs << %( showDataAs="#{xml_escape(df[:show_data_as])}") if df[:show_data_as]
130
+ df_attrs << %( baseField="#{df[:base_field]}") if df[:base_field]
131
+ df_attrs << %( baseItem="#{df[:base_item]}") if df[:base_item]
132
+ df_attrs << %( numFmtId="#{df[:num_fmt_id]}") if df[:num_fmt_id]
133
+ parts << "<dataField#{df_attrs}/>"
134
+ end
135
+ parts << "</dataFields>"
136
+ end
137
+
138
+ if pivot_table[:pivot_table_style]
139
+ psi = pivot_table[:pivot_table_style]
140
+ psi_attrs = +""
141
+ psi_attrs << %( name="#{xml_escape(psi[:name])}") if psi[:name]
142
+ psi_attrs << %( showRowHeaders="#{psi[:show_row_headers] ? "1" : "0"}") unless psi[:show_row_headers].nil?
143
+ psi_attrs << %( showColHeaders="#{psi[:show_col_headers] ? "1" : "0"}") unless psi[:show_col_headers].nil?
144
+ psi_attrs << %( showRowStripes="#{psi[:show_row_stripes] ? "1" : "0"}") unless psi[:show_row_stripes].nil?
145
+ psi_attrs << %( showColStripes="#{psi[:show_col_stripes] ? "1" : "0"}") unless psi[:show_col_stripes].nil?
146
+ psi_attrs << %( showLastColumn="#{psi[:show_last_column] ? "1" : "0"}") unless psi[:show_last_column].nil?
147
+ parts << "<pivotTableStyleInfo#{psi_attrs}/>"
148
+ end
149
+
150
+ parts << "</pivotTableDefinition>"
151
+ parts.join
152
+ end
153
+
154
+ # : (untyped pivot_table, untyped _cache_id) -> untyped
155
+ def generate_pivot_cache_definition_xml(pivot_table, _cache_id)
156
+ pcd_attrs = %( xmlns="#{SSML_NS}" xmlns:r="#{DOC_REL_NS}" r:id="rId1" refreshOnLoad="1")
157
+ pcd_attrs << ' saveData="0"' if pivot_table[:cache_save_data] == false
158
+ pcd_attrs << ' enableRefresh="0"' if pivot_table[:cache_enable_refresh] == false
159
+ pcd_attrs << %( refreshedBy="#{xml_escape(pivot_table[:cache_refreshed_by])}") if pivot_table[:cache_refreshed_by]
160
+ pcd_attrs << %( refreshedVersion="#{pivot_table[:cache_refreshed_version]}") if pivot_table[:cache_refreshed_version]
161
+ pcd_attrs << %( createdVersion="#{pivot_table[:cache_created_version]}") if pivot_table[:cache_created_version]
162
+ pcd_attrs << %( recordCount="#{pivot_table[:cache_record_count]}") if pivot_table[:cache_record_count]
163
+ pcd_attrs << ' optimizeMemory="1"' if pivot_table[:cache_optimize_memory]
164
+ parts = [
165
+ XML_HEADER,
166
+ "<pivotCacheDefinition#{pcd_attrs}>"
167
+ ]
168
+
169
+ # Parse source ref: "Sheet1!A1:C4" => sheet name + range.
170
+ source = pivot_table[:source_ref]
171
+ ws_name_attr = pivot_table[:source_name] ? %( name="#{xml_escape(pivot_table[:source_name])}") : ""
172
+ if source.include?("!")
173
+ sname, srange = source.split("!", 2)
174
+ sname = sname.delete("'")
175
+ parts << %(<cacheSource type="worksheet"><worksheetSource ref="#{srange}" sheet="#{xml_escape(sname)}"#{ws_name_attr}/></cacheSource>)
176
+ else
177
+ parts << %(<cacheSource type="worksheet"><worksheetSource ref="#{source}"#{ws_name_attr}/></cacheSource>)
178
+ end
179
+
180
+ field_count = pivot_table[:field_names] ? pivot_table[:field_names].size : (pivot_table[:row_fields].size + pivot_table[:col_fields].size + pivot_table[:data_fields].size)
181
+ parts << %(<cacheFields count="#{field_count}">)
182
+ field_count.times do |fi|
183
+ fname = if pivot_table[:field_names] && pivot_table[:field_names][fi]
184
+ pivot_table[:field_names][fi]
185
+ else
186
+ df = pivot_table[:data_fields].find { |d| d[:fld] == fi }
187
+ df ? df[:name] : "Field#{fi + 1}"
188
+ end
189
+ fa = pivot_table[:field_attrs] && pivot_table[:field_attrs][fi]
190
+ cf_num_fmt = (fa && fa[:cache_num_fmt_id]) || 0
191
+ cf_attrs = %( name="#{xml_escape(fname)}" numFmtId="#{cf_num_fmt}")
192
+ cf_attrs << %( caption="#{xml_escape(fa[:cache_caption])}") if fa && fa[:cache_caption]
193
+ cf_attrs << %( formula="#{xml_escape(fa[:cache_formula])}") if fa && fa[:cache_formula]
194
+ field_items = pivot_table[:items] && pivot_table[:items][fi]
195
+ if field_items
196
+ parts << "<cacheField#{cf_attrs}>"
197
+ parts << %(<sharedItems count="#{field_items.size}">)
198
+ field_items.each { |v| parts << %(<s v="#{xml_escape(v.to_s)}"/>) }
199
+ parts << "</sharedItems>"
200
+ parts << "</cacheField>"
201
+ else
202
+ parts << "<cacheField#{cf_attrs}><sharedItems/></cacheField>"
203
+ end
204
+ end
205
+ parts << "</cacheFields>"
206
+ parts << "</pivotCacheDefinition>"
207
+ parts.join
208
+ end
209
+
210
+ # : (untyped pivot_table) -> untyped
211
+ def generate_pivot_cache_records_xml(pivot_table)
212
+ items = pivot_table[:items]
213
+ if items&.values&.any? { |v| v && !v.empty? }
214
+ max_len = items.values.map { |v| v ? v.size : 0 }.max
215
+ parts = [XML_HEADER, %(<pivotCacheRecords xmlns="#{SSML_NS}" count="#{max_len}">)]
216
+ max_len.times do |ri|
217
+ parts << "<r>"
218
+ field_count = pivot_table[:field_names] ? pivot_table[:field_names].size : (pivot_table[:row_fields].size + pivot_table[:col_fields].size + pivot_table[:data_fields].size)
219
+ field_count.times do |fi|
220
+ field_items = items[fi]
221
+ parts << if field_items
222
+ %(<x v="#{ri < field_items.size ? ri : 0}"/>)
223
+ else
224
+ %(<n v="0"/>)
225
+ end
226
+ end
227
+ parts << "</r>"
228
+ end
229
+ parts << "</pivotCacheRecords>"
230
+ parts.join
231
+ else
232
+ [XML_HEADER, %(<pivotCacheRecords xmlns="#{SSML_NS}" count="0"/>)].join
233
+ end
234
+ end
235
+
236
+ # : (untyped cache_id) -> untyped
237
+ def generate_pivot_cache_rels(cache_id)
238
+ [
239
+ XML_HEADER,
240
+ %(<Relationships xmlns="#{REL_NS}">),
241
+ %(<Relationship Id="rId1" Type="#{DOC_REL_NS}/pivotCacheRecords" Target="pivotCacheRecords#{cache_id}.xml"/>),
242
+ "</Relationships>"
243
+ ].join
244
+ end
245
+
246
+ # : (untyped cache_id) -> untyped
247
+ def generate_pivot_table_rels(cache_id)
248
+ [
249
+ XML_HEADER,
250
+ %(<Relationships xmlns="#{REL_NS}">),
251
+ %(<Relationship Id="rId1" Type="#{DOC_REL_NS}/pivotCacheDefinition" Target="../pivotCache/pivotCacheDefinition#{cache_id}.xml"/>),
252
+ "</Relationships>"
253
+ ].join
254
+ end
255
+
256
+ # : (untyped ext_link) -> untyped
257
+ def generate_external_link_xml(ext_link)
258
+ parts = [
259
+ XML_HEADER,
260
+ %(<externalLink xmlns="#{SSML_NS}" xmlns:r="#{DOC_REL_NS}">),
261
+ '<externalBook r:id="rId1">'
262
+ ]
263
+ unless ext_link[:sheet_names].empty?
264
+ parts << "<sheetNames>"
265
+ ext_link[:sheet_names].each { |sn| parts << %(<sheetName val="#{xml_escape(sn)}"/>) }
266
+ parts << "</sheetNames>"
267
+ end
268
+ parts << "</externalBook>"
269
+ parts << "</externalLink>"
270
+ parts.join
271
+ end
272
+
273
+ # : (untyped ext_link) -> untyped
274
+ def generate_external_link_rels(ext_link)
275
+ [
276
+ XML_HEADER,
277
+ %(<Relationships xmlns="#{REL_NS}">),
278
+ %(<Relationship Id="rId1" Type="#{DOC_REL_NS}/externalLinkPath" Target="#{xml_escape(ext_link[:target])}" TargetMode="External"/>),
279
+ "</Relationships>"
280
+ ].join
281
+ end
282
+
283
+ # : (untyped ct_xml) -> untyped
284
+ def parse_extra_content_types(ct_xml)
285
+ ct_xml.scan(/<Default\s+Extension="([^"]+)"\s+ContentType="([^"]+)"/).each do |ext, ct|
286
+ @extra_ct_defaults[ext] ||= ct
287
+ end
288
+ ct_xml.scan(/<Override\s+PartName="([^"]+)"\s+ContentType="([^"]+)"/).each do |pn, ct|
289
+ @extra_ct_overrides[pn] ||= ct
290
+ end
291
+ end
292
+
293
+ # : (untyped element, untyped opts) -> ("" | ::String)
294
+ def build_line_end_xml(element, opts)
295
+ return "" unless opts
296
+
297
+ attrs = +""
298
+ attrs << %( type="#{xml_escape(opts[:type])}") if opts[:type]
299
+ attrs << %( w="#{xml_escape(opts[:w])}") if opts[:w]
300
+ attrs << %( len="#{xml_escape(opts[:len])}") if opts[:len]
301
+ "<a:#{element}#{attrs}/>"
302
+ end
303
+
304
+ # : (untyped value) -> untyped
305
+ def xml_escape(value)
306
+ value.to_s
307
+ .gsub(/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/, "")
308
+ .gsub("&", "&amp;")
309
+ .gsub("<", "&lt;")
310
+ .gsub(">", "&gt;")
311
+ .gsub('"', "&quot;")
312
+ .gsub("'", "&apos;")
313
+ end
314
+
315
+ # : (untyped rich_text) -> untyped
316
+ def rich_text_xml(rich_text)
317
+ rich_text.runs.map do |run|
318
+ font = run[:font]
319
+ if font && !font.empty?
320
+ rpr = +""
321
+ rpr << "<b/>" if font[:bold]
322
+ rpr << "<i/>" if font[:italic]
323
+ rpr << "<strike/>" if font[:strike]
324
+ if font[:underline]
325
+ rpr << if font[:underline] == true
326
+ "<u/>"
327
+ else
328
+ %(<u val="#{font[:underline]}"/>)
329
+ end
330
+ end
331
+ rpr << %(<vertAlign val="#{font[:vert_align]}"/>) if font[:vert_align]
332
+ rpr << %(<sz val="#{font[:sz]}"/>) if font[:sz]
333
+ rpr << emit_color_xml(font)
334
+ rpr << %(<rFont val="#{xml_escape(font[:name])}"/>) if font[:name]
335
+ rpr << %(<family val="#{font[:family]}"/>) if font[:family]
336
+ rpr << %(<scheme val="#{font[:scheme]}"/>) if font[:scheme]
337
+ "<r><rPr>#{rpr}</rPr><t>#{xml_escape(run[:text])}</t></r>"
338
+ else
339
+ "<r><t>#{xml_escape(run[:text])}</t></r>"
340
+ end
341
+ end.join
342
+ end
343
+
344
+ # : (untyped cell_ref, untyped value, untyped style_idx, ?untyped? sst, ?ph: untyped?) -> untyped
345
+ def cell_xml(cell_ref, value, style_idx, sst = nil, ph: nil) # rubocop:disable Naming/MethodParameterName
346
+ s_attr = style_idx ? %( s="#{style_idx}") : ""
347
+ ph_attr = ph ? ' ph="1"' : ""
348
+ case value
349
+ when Xlsxrb::Elements::CellError
350
+ %(<c r="#{cell_ref}" t="e"#{s_attr}#{ph_attr}><v>#{xml_escape(value.code)}</v></c>)
351
+ when Xlsxrb::Elements::Formula
352
+ f_attrs = +""
353
+ case value.type
354
+ when :shared
355
+ f_attrs << %( t="shared" si="#{value.shared_index}")
356
+ f_attrs << %( ref="#{value.ref}") if value.ref
357
+ when :array
358
+ f_attrs << %( t="array" ref="#{value.ref}") if value.ref
359
+ when :data_table
360
+ f_attrs << ' t="dataTable"'
361
+ f_attrs << ' dt2D="1"' if value.dt2d
362
+ f_attrs << ' dtr="1"' if value.dtr
363
+ f_attrs << %( r1="#{value.r1}") if value.r1
364
+ f_attrs << %( r2="#{value.r2}") if value.r2
365
+ end
366
+ f_attrs << ' ca="1"' if value.calculate_always
367
+ f_attrs << ' aca="1"' if value.aca
368
+ f_attrs << ' bx="1"' if value.bx
369
+ t_attr = ""
370
+ cached_val_str = value.cached_value.to_s
371
+ case value.cached_value
372
+ when String
373
+ t_attr = ' t="str"'
374
+ when true
375
+ t_attr = ' t="b"'
376
+ cached_val_str = "1"
377
+ when false
378
+ t_attr = ' t="b"'
379
+ cached_val_str = "0"
380
+ end
381
+ parts = %(<c r="#{cell_ref}"#{t_attr}#{s_attr}#{ph_attr}><f#{f_attrs}>#{xml_escape(value.expression)}</f>)
382
+ parts << "<v>#{xml_escape(cached_val_str)}</v>" unless value.cached_value.nil?
383
+ parts << "</c>"
384
+ parts
385
+ when Xlsxrb::Elements::RichText
386
+ if sst
387
+ rt_sst, = sst
388
+ idx = rt_sst[value]
389
+ %(<c r="#{cell_ref}" t="s"#{s_attr}#{ph_attr}><v>#{idx}</v></c>)
390
+ else
391
+ %(<c r="#{cell_ref}" t="inlineStr"#{s_attr}#{ph_attr}><is>#{rich_text_xml(value)}</is></c>)
392
+ end
393
+ when true, false
394
+ %(<c r="#{cell_ref}" t="b"#{s_attr}#{ph_attr}><v>#{value ? 1 : 0}</v></c>)
395
+ when Time
396
+ serial = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
397
+ dt_attr = s_attr.empty? && (dt_style = resolve_style_index(datetime_num_fmt_id)) ? %( s="#{dt_style}") : s_attr
398
+ %(<c r="#{cell_ref}"#{dt_attr}#{ph_attr}><v>#{serial}</v></c>)
399
+ when Date
400
+ serial = Xlsxrb::Ooxml::Utils.date_to_serial(value)
401
+ ds_attr = s_attr.empty? && (date_style = resolve_style_index(date_num_fmt_id)) ? %( s="#{date_style}") : s_attr
402
+ %(<c r="#{cell_ref}"#{ds_attr}#{ph_attr}><v>#{serial}</v></c>)
403
+ when Numeric
404
+ %(<c r="#{cell_ref}"#{s_attr}#{ph_attr}><v>#{value}</v></c>)
405
+ else
406
+ if sst
407
+ _, str_sst = sst
408
+ idx = str_sst[value.to_s]
409
+ %(<c r="#{cell_ref}" t="s"#{s_attr}#{ph_attr}><v>#{idx}</v></c>)
410
+ else
411
+ %(<c r="#{cell_ref}" t="inlineStr"#{s_attr}#{ph_attr}><is><t>#{xml_escape(value)}</t></is></c>)
412
+ end
413
+ end
414
+ end
415
+
416
+ # Returns the numFmtId for dates, registering it on first use.
417
+ # : () -> untyped
418
+ def date_num_fmt_id
419
+ @date_num_fmt_id ||= add_number_format(Xlsxrb::Ooxml::Utils::DEFAULT_DATE_FORMAT)
420
+ end
421
+
422
+ # Returns the numFmtId for datetime, registering it on first use.
423
+ # : () -> untyped
424
+ def datetime_num_fmt_id
425
+ @datetime_num_fmt_id ||= add_number_format(Xlsxrb::Ooxml::Utils::DEFAULT_DATETIME_FORMAT)
426
+ end
427
+
428
+ # Maps a numFmtId to a cellXfs index. Index 0 is the default (no format).
429
+ # : (untyped style_value) -> (nil | untyped)
430
+ def resolve_style_index(style_value)
431
+ return nil if style_value.nil?
432
+
433
+ # New-style: { xf_index: N } from set_cell_style.
434
+ return style_value[:xf_index] if style_value.is_a?(Hash) && style_value.key?(:xf_index)
435
+
436
+ # Legacy: raw num_fmt_id from set_cell_format — find or create matching xf entry.
437
+ num_fmt_id = style_value
438
+ @xf_index_map ||= begin
439
+ map = {}
440
+ @num_fmts.each_with_index do |nf, _i|
441
+ entry = { num_fmt_id: nf[:num_fmt_id], font_id: 0, fill_id: 0, border_id: 0 }
442
+ idx = @xf_entries.index(entry)
443
+ unless idx
444
+ @xf_entries << entry
445
+ idx = @xf_entries.size - 1
446
+ end
447
+ map[nf[:num_fmt_id]] = idx
448
+ end
449
+ map
450
+ end
451
+ @xf_index_map[num_fmt_id]
452
+ end
453
+
454
+ # : (untyped filter) -> untyped
455
+ def emit_filter_xml(filter)
456
+ case filter[:type]
457
+ when :filters
458
+ attrs = filter[:blank] ? +' blank="1"' : +""
459
+ attrs << %( calendarType="#{filter[:calendar_type]}") if filter[:calendar_type]
460
+ has_values = filter[:values]&.any?
461
+ has_date_groups = filter[:date_group_items]&.any?
462
+ if has_values || has_date_groups
463
+ parts = ["<filters#{attrs}>"]
464
+ filter[:values]&.each { |v| parts << %(<filter val="#{xml_escape(v)}"/>) }
465
+ filter[:date_group_items]&.each do |dg|
466
+ dg_attrs = %(dateTimeGrouping="#{dg[:date_time_grouping]}")
467
+ dg_attrs << %( year="#{dg[:year]}") if dg[:year]
468
+ dg_attrs << %( month="#{dg[:month]}") if dg[:month]
469
+ dg_attrs << %( day="#{dg[:day]}") if dg[:day]
470
+ dg_attrs << %( hour="#{dg[:hour]}") if dg[:hour]
471
+ dg_attrs << %( minute="#{dg[:minute]}") if dg[:minute]
472
+ dg_attrs << %( second="#{dg[:second]}") if dg[:second]
473
+ parts << "<dateGroupItem #{dg_attrs}/>"
474
+ end
475
+ parts << "</filters>"
476
+ parts.join
477
+ else
478
+ "<filters#{attrs}/>"
479
+ end
480
+ when :custom
481
+ if filter[:filters]
482
+ and_attr = filter[:and] ? ' and="1"' : ""
483
+ parts = ["<customFilters#{and_attr}>"]
484
+ filter[:filters].each do |cf|
485
+ parts << %(<customFilter operator="#{cf[:operator]}" val="#{xml_escape(cf[:val])}"/>)
486
+ end
487
+ parts << "</customFilters>"
488
+ parts.join
489
+ else
490
+ %(<customFilters><customFilter operator="#{filter[:operator]}" val="#{xml_escape(filter[:val])}"/></customFilters>)
491
+ end
492
+ when :dynamic
493
+ dyn_attrs = %( type="#{filter[:dynamic_type]}")
494
+ dyn_attrs << %( val="#{filter[:val]}") if filter[:val]
495
+ dyn_attrs << %( valIso="#{filter[:val_iso]}") if filter[:val_iso]
496
+ dyn_attrs << %( maxVal="#{filter[:max_val]}") if filter[:max_val]
497
+ dyn_attrs << %( maxValIso="#{filter[:max_val_iso]}") if filter[:max_val_iso]
498
+ "<dynamicFilter#{dyn_attrs}/>"
499
+ when :top10
500
+ top_attr = filter[:top] ? ' top="1"' : ""
501
+ pct_attr = filter[:percent] ? ' percent="1"' : ""
502
+ fv_attr = filter[:filter_val] ? %( filterVal="#{filter[:filter_val]}") : ""
503
+ %(<top10#{top_attr}#{pct_attr} val="#{filter[:val]}"#{fv_attr}/>)
504
+ when :color_filter
505
+ cf_attrs = %(dxfId="#{filter[:dxf_id]}")
506
+ cf_attrs << ' cellColor="0"' if filter[:cell_color] == false
507
+ %(<colorFilter #{cf_attrs}/>)
508
+ when :icon_filter
509
+ if_attrs = %(iconSet="#{filter[:icon_set]}")
510
+ if_attrs << %( iconId="#{filter[:icon_id]}") if filter[:icon_id]
511
+ %(<iconFilter #{if_attrs}/>)
512
+ else
513
+ ""
514
+ end
515
+ end
516
+
517
+ # : (untyped parts, untyped rule) -> untyped
518
+ def emit_cf_rule(parts, rule)
519
+ type = rule[:type]
520
+ if type.is_a?(String) || type.is_a?(Symbol)
521
+ t_str = type.to_s
522
+ snake = t_str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
523
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
524
+ .downcase.to_sym
525
+ type = snake if CF_TYPE_MAP.key?(snake)
526
+ end
527
+ rule_type = CF_TYPE_MAP[type] || type.to_s
528
+ rule_attrs = %(type="#{rule_type}")
529
+ rule_attrs << %( priority="#{rule[:priority]}") if rule[:priority]
530
+ rule_attrs << %( operator="#{rule[:operator]}") if rule[:operator]
531
+ rule_attrs << %( dxfId="#{rule[:format_id]}") if rule[:format_id]
532
+ rule_attrs << %( stopIfTrue="1") if rule[:stop_if_true]
533
+ rule_attrs << %( aboveAverage="0") if rule[:above_average] == false
534
+ rule_attrs << %( equalAverage="1") if rule[:equal_average]
535
+ rule_attrs << %( rank="#{rule[:rank]}") if rule[:rank]
536
+ rule_attrs << %( percent="1") if rule[:percent]
537
+ rule_attrs << %( bottom="1") if rule[:bottom]
538
+ rule_attrs << %( text="#{xml_escape(rule[:text])}") if rule[:text]
539
+ rule_attrs << %( timePeriod="#{rule[:time_period]}") if rule[:time_period]
540
+ rule_attrs << %( stdDev="#{rule[:std_dev]}") if rule[:std_dev]
541
+
542
+ case type
543
+ when :cell_is, :expression, :above_average, :top10, :duplicate_values, :unique_values,
544
+ :contains_text, :not_contains_text, :begins_with, :ends_with,
545
+ :contains_blanks, :not_contains_blanks, :time_period
546
+ formulas = rule[:formulas] || [rule[:formula]].compact
547
+ if formulas.empty?
548
+ parts << "<cfRule #{rule_attrs}/>"
549
+ else
550
+ parts << "<cfRule #{rule_attrs}>"
551
+ formulas.each { |f| parts << "<formula>#{xml_escape(f)}</formula>" }
552
+ parts << "</cfRule>"
553
+ end
554
+ when :color_scale
555
+ cs = rule[:color_scale]
556
+ parts << "<cfRule #{rule_attrs}>"
557
+ parts << "<colorScale>"
558
+ cs[:cfvo]&.each do |cfvo|
559
+ cfvo_attrs = %(type="#{cfvo[:type]}")
560
+ cfvo_attrs << %( val="#{cfvo[:val]}") if cfvo[:val]
561
+ cfvo_attrs << ' gte="0"' if cfvo[:gte] == false
562
+ parts << "<cfvo #{cfvo_attrs}/>"
563
+ end
564
+ cs[:colors]&.each { |c| parts << emit_cf_color_xml(c) }
565
+ parts << "</colorScale>"
566
+ parts << "</cfRule>"
567
+ when :data_bar
568
+ db = rule[:data_bar]
569
+ parts << "<cfRule #{rule_attrs}>"
570
+ db_attrs = +""
571
+ db_attrs << %( minLength="#{db[:min_length]}") if db[:min_length]
572
+ db_attrs << %( maxLength="#{db[:max_length]}") if db[:max_length]
573
+ db_attrs << %( showValue="#{db[:show_value] ? 1 : 0}") unless db[:show_value].nil?
574
+ parts << "<dataBar#{db_attrs}>"
575
+ db[:cfvo]&.each do |cfvo|
576
+ cfvo_attrs = %(type="#{cfvo[:type]}")
577
+ cfvo_attrs << %( val="#{cfvo[:val]}") if cfvo[:val]
578
+ cfvo_attrs << ' gte="0"' if cfvo[:gte] == false
579
+ parts << "<cfvo #{cfvo_attrs}/>"
580
+ end
581
+ parts << emit_cf_color_xml(db[:color]) if db[:color]
582
+ parts << "</dataBar>"
583
+ parts << "</cfRule>"
584
+ when :icon_set
585
+ is = rule[:icon_set]
586
+ parts << "<cfRule #{rule_attrs}>"
587
+ is_attrs = +""
588
+ is_attrs << %( iconSet="#{is[:icon_set]}") if is[:icon_set]
589
+ is_attrs << %( reverse="#{is[:reverse] ? 1 : 0}") unless is[:reverse].nil?
590
+ is_attrs << %( percent="#{is[:percent] ? 1 : 0}") unless is[:percent].nil?
591
+ is_attrs << %( showValue="#{is[:show_value] ? 1 : 0}") unless is[:show_value].nil?
592
+ parts << "<iconSet#{is_attrs}>"
593
+ is[:cfvo]&.each do |cfvo|
594
+ cfvo_attrs = %(type="#{cfvo[:type]}")
595
+ cfvo_attrs << %( val="#{cfvo[:val]}") if cfvo[:val]
596
+ cfvo_attrs << ' gte="0"' if cfvo[:gte] == false
597
+ parts << "<cfvo #{cfvo_attrs}/>"
598
+ end
599
+ parts << "</iconSet>"
600
+ parts << "</cfRule>"
601
+ else
602
+ parts << "<cfRule #{rule_attrs}/>"
603
+ end
604
+ end
605
+
606
+ # Emits a <color> element for CF rules, accepting either a plain RGB string or a hash.
607
+ # : (untyped color) -> (untyped | ::String)
608
+ def emit_cf_color_xml(color)
609
+ if color.is_a?(Hash)
610
+ emit_color_xml(color)
611
+ else
612
+ %(<color rgb="#{color}"/>)
613
+ end
614
+ end
615
+
616
+ # : (untyped sheet_cells) -> ("A1" | ::String)
617
+ def compute_dimension(sheet_cells)
618
+ return "A1" if sheet_cells.empty?
619
+
620
+ min_col = Float::INFINITY
621
+ max_col = 0
622
+ min_row = Float::INFINITY
623
+ max_row = 0
624
+ sheet_cells.each_key do |addr|
625
+ col_letter = extract_column_letter(addr)
626
+ row_num = extract_row_number(addr)
627
+ col_idx = column_letter_to_index(col_letter)
628
+ min_col = col_idx if col_idx < min_col
629
+ max_col = col_idx if col_idx > max_col
630
+ min_row = row_num if row_num < min_row
631
+ max_row = row_num if row_num > max_row
632
+ end
633
+ start_col = index_to_column_letter(min_col)
634
+ end_col = index_to_column_letter(max_col)
635
+ "#{start_col}#{min_row}:#{end_col}#{max_row}"
636
+ end
637
+
638
+ # : (untyped index) -> untyped
639
+ def index_to_column_letter(index)
640
+ result = +""
641
+ while index.positive?
642
+ index -= 1
643
+ result.prepend(("A".ord + (index % 26)).chr)
644
+ index /= 26
645
+ end
646
+ result
647
+ end
648
+
649
+ # : () -> untyped
650
+ def generate_core_properties_xml
651
+ parts = [
652
+ XML_HEADER,
653
+ %(<cp:coreProperties xmlns:cp="#{CP_NS}" xmlns:dc="#{DC_NS}" xmlns:dcterms="#{DCTERMS_NS}" xmlns:xsi="#{XSI_NS}">)
654
+ ]
655
+ parts << "<dc:title>#{xml_escape(@core_properties[:title])}</dc:title>" if @core_properties[:title]
656
+ parts << "<dc:subject>#{xml_escape(@core_properties[:subject])}</dc:subject>" if @core_properties[:subject]
657
+ parts << "<dc:creator>#{xml_escape(@core_properties[:creator])}</dc:creator>" if @core_properties[:creator]
658
+ parts << "<cp:keywords>#{xml_escape(@core_properties[:keywords])}</cp:keywords>" if @core_properties[:keywords]
659
+ parts << "<dc:description>#{xml_escape(@core_properties[:description])}</dc:description>" if @core_properties[:description]
660
+ parts << "<cp:lastModifiedBy>#{xml_escape(@core_properties[:last_modified_by])}</cp:lastModifiedBy>" if @core_properties[:last_modified_by]
661
+ parts << "<cp:revision>#{xml_escape(@core_properties[:revision])}</cp:revision>" if @core_properties[:revision]
662
+ parts << %(<dcterms:created xsi:type="dcterms:W3CDTF">#{xml_escape(@core_properties[:created])}</dcterms:created>) if @core_properties[:created]
663
+ parts << %(<dcterms:modified xsi:type="dcterms:W3CDTF">#{xml_escape(@core_properties[:modified])}</dcterms:modified>) if @core_properties[:modified]
664
+ parts << "<cp:category>#{xml_escape(@core_properties[:category])}</cp:category>" if @core_properties[:category]
665
+ parts << "<cp:contentStatus>#{xml_escape(@core_properties[:content_status])}</cp:contentStatus>" if @core_properties[:content_status]
666
+ parts << "<dc:language>#{xml_escape(@core_properties[:language])}</dc:language>" if @core_properties[:language]
667
+ parts << "</cp:coreProperties>"
668
+ parts.join
669
+ end
670
+
671
+ # : () -> untyped
672
+ def generate_app_properties_xml
673
+ parts = [
674
+ XML_HEADER,
675
+ %(<Properties xmlns="#{APP_NS}" xmlns:vt="#{VT_NS}">)
676
+ ]
677
+ parts << "<Application>#{xml_escape(@app_properties[:application])}</Application>" if @app_properties[:application]
678
+ parts << "<AppVersion>#{xml_escape(@app_properties[:app_version])}</AppVersion>" if @app_properties[:app_version]
679
+ if @app_properties[:heading_pairs] && @app_properties[:titles_of_parts]
680
+ hp = @app_properties[:heading_pairs]
681
+ tp = @app_properties[:titles_of_parts]
682
+ parts << "<HeadingPairs>"
683
+ parts << %(<vt:vector size="#{hp.size * 2}" baseType="variant">)
684
+ hp.each do |label, count|
685
+ parts << "<vt:variant><vt:lpstr>#{xml_escape(label)}</vt:lpstr></vt:variant>"
686
+ parts << "<vt:variant><vt:i4>#{count}</vt:i4></vt:variant>"
687
+ end
688
+ parts << "</vt:vector>"
689
+ parts << "</HeadingPairs>"
690
+ parts << "<TitlesOfParts>"
691
+ parts << %(<vt:vector size="#{tp.size}" baseType="lpstr">)
692
+ tp.each { |t| parts << "<vt:lpstr>#{xml_escape(t)}</vt:lpstr>" }
693
+ parts << "</vt:vector>"
694
+ parts << "</TitlesOfParts>"
695
+ end
696
+ parts << "</Properties>"
697
+ parts.join
698
+ end
699
+
700
+ # : () -> untyped
701
+ def generate_custom_properties_xml
702
+ custom_ns = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
703
+ parts = [
704
+ XML_HEADER,
705
+ %(<Properties xmlns="#{custom_ns}" xmlns:vt="#{VT_NS}">)
706
+ ]
707
+ @custom_properties.each_with_index do |prop, idx|
708
+ fmtid = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
709
+ pid = idx + 2 # pids start at 2
710
+ parts << %(<property fmtid="#{fmtid}" pid="#{pid}" name="#{xml_escape(prop[:name])}">)
711
+ parts << case prop[:type]
712
+ when :number, :int, :i4
713
+ "<vt:i4>#{prop[:value]}</vt:i4>"
714
+ when :float, :r8
715
+ "<vt:r8>#{prop[:value]}</vt:r8>"
716
+ when :bool
717
+ "<vt:bool>#{prop[:value] ? "true" : "false"}</vt:bool>"
718
+ when :date, :filetime
719
+ "<vt:filetime>#{prop[:value]}</vt:filetime>"
720
+ else
721
+ "<vt:lpwstr>#{xml_escape(prop[:value].to_s)}</vt:lpwstr>"
722
+ end
723
+ parts << "</property>"
724
+ end
725
+ parts << "</Properties>"
726
+ parts.join
727
+ end
728
+ end
729
+ end
730
+ end
731
+ end