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
@@ -3,11 +3,19 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  require_relative "zip_generator"
6
+ require_relative "writer/drawing_xml"
7
+ require_relative "writer/features_xml"
8
+ require_relative "writer/styles_xml"
9
+ require_relative "../dsl_helpers"
6
10
 
7
11
  module Xlsxrb
8
12
  module Ooxml
9
13
  # Writes spreadsheet data into an XLSX file.
10
14
  class Writer
15
+ include DrawingXml
16
+ include FeaturesXml
17
+ include StylesXml
18
+
11
19
  XML_HEADER = %(<?xml version="1.0" encoding="UTF-8"?>)
12
20
  CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types"
13
21
  REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
@@ -2992,2651 +3000,6 @@ module Xlsxrb
2992
3000
  parts.join
2993
3001
  end
2994
3002
 
2995
- # : (untyped drawing_parts) -> untyped
2996
- def generate_drawing_xml(drawing_parts)
2997
- parts = [
2998
- XML_HEADER,
2999
- %(<xdr:wsDr xmlns:xdr="#{XDR_NS}" xmlns:a="#{A_NS}" xmlns:r="#{DOC_REL_NS}">)
3000
- ]
3001
-
3002
- drawing_parts.each do |dp|
3003
- case dp[:kind]
3004
- when :pic
3005
- img = dp[:img]
3006
- rid = "rId#{dp[:rid_index]}"
3007
- ea = img[:edit_as] || "oneCell"
3008
- img_pub_attr = img[:published] ? ' fPublished="1"' : ""
3009
- parts << %(<xdr:twoCellAnchor editAs="#{xml_escape(ea)}"#{img_pub_attr}>)
3010
- parts << anchor_xml("from", img[:from_col], img[:from_row], col_off: img[:from_col_off] || 0, row_off: img[:from_row_off] || 0)
3011
- parts << anchor_xml("to", img[:to_col], img[:to_row], col_off: img[:to_col_off] || 0, row_off: img[:to_row_off] || 0)
3012
- macro_attr = img[:macro] ? %( macro="#{xml_escape(img[:macro])}") : ""
3013
- parts << "<xdr:pic#{macro_attr}>"
3014
- descr_attr = img[:description] ? %( descr="#{xml_escape(img[:description])}") : ""
3015
- title_attr = img[:title] ? %( title="#{xml_escape(img[:title])}") : ""
3016
- hidden_attr = img[:hidden] ? ' hidden="1"' : ""
3017
- pic_lock_attrs = +""
3018
- pic_lock_attrs << ' noChangeAspect="1"' if img[:no_change_aspect]
3019
- pic_lock_attrs << ' noCrop="1"' if img[:no_crop]
3020
- pic_locks = pic_lock_attrs.empty? ? "<a:picLocks/>" : "<a:picLocks#{pic_lock_attrs}/>"
3021
- parts << %(<xdr:nvPicPr><xdr:cNvPr id="#{dp[:rid_index] + 1}" name="#{xml_escape(img[:name])}"#{descr_attr}#{title_attr}#{hidden_attr}/><xdr:cNvPicPr>#{pic_locks}</xdr:cNvPicPr></xdr:nvPicPr>)
3022
- src_rect_xml = if img[:src_rect]
3023
- sr = img[:src_rect]
3024
- sr_attrs = +""
3025
- sr_attrs << %( t="#{sr[:top]}") if sr[:top]
3026
- sr_attrs << %( b="#{sr[:bottom]}") if sr[:bottom]
3027
- sr_attrs << %( l="#{sr[:left]}") if sr[:left]
3028
- sr_attrs << %( r="#{sr[:right]}") if sr[:right]
3029
- "<a:srcRect#{sr_attrs}/>"
3030
- else
3031
- ""
3032
- end
3033
- blip_xml = if img[:alpha_mod_fix]
3034
- %(<a:blip r:embed="#{rid}"><a:alphaModFix amt="#{img[:alpha_mod_fix]}"/></a:blip>)
3035
- else
3036
- %(<a:blip r:embed="#{rid}"/>)
3037
- end
3038
- parts << %(<xdr:blipFill>#{blip_xml}#{src_rect_xml}<a:stretch><a:fillRect/></a:stretch></xdr:blipFill>)
3039
- img_line_xml = if img[:line_color]
3040
- ln_w_attr = img[:line_width] ? %( w="#{img[:line_width].to_i}") : ""
3041
- %(<a:ln#{ln_w_attr}><a:solidFill>#{color_xml(img[:line_color])}</a:solidFill></a:ln>)
3042
- else
3043
- ""
3044
- end
3045
- img_xfrm_xml = img[:rotation] ? %(<a:xfrm rot="#{img[:rotation].to_i}"><a:off x="0" y="0"/><a:ext cx="0" cy="0"/></a:xfrm>) : ""
3046
- parts << %(<xdr:spPr>#{img_xfrm_xml}<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>#{img_line_xml}</xdr:spPr>)
3047
- parts << "</xdr:pic>"
3048
- parts << client_data_xml(img)
3049
- parts << "</xdr:twoCellAnchor>"
3050
- when :chart
3051
- chart = dp[:chart]
3052
- rid = "rId#{dp[:rid_index]}"
3053
- chart_ea_attr = chart[:edit_as] ? %( editAs="#{xml_escape(chart[:edit_as])}") : ""
3054
- chart_pub_attr = chart[:published] ? ' fPublished="1"' : ""
3055
- parts << "<xdr:twoCellAnchor#{chart_ea_attr}#{chart_pub_attr}>"
3056
- parts << anchor_xml("from", chart[:from_col], chart[:from_row], col_off: chart[:from_col_off] || 0, row_off: chart[:from_row_off] || 0)
3057
- parts << anchor_xml("to", chart[:to_col], chart[:to_row], col_off: chart[:to_col_off] || 0, row_off: chart[:to_row_off] || 0)
3058
- gf_macro = chart[:frame_macro] ? xml_escape(chart[:frame_macro]) : ""
3059
- parts << %(<xdr:graphicFrame macro="#{gf_macro}">)
3060
- chart_frame_name = chart[:name] || chart[:title] || "Chart"
3061
- chart_descr_attr = chart[:description] ? %( descr="#{xml_escape(chart[:description])}") : ""
3062
- chart_title_attr = chart[:frame_title] ? %( title="#{xml_escape(chart[:frame_title])}") : ""
3063
- chart_hidden_attr = chart[:frame_hidden] ? ' hidden="1"' : ""
3064
- cnv_gf_pr = if chart[:frame_no_grp]
3065
- '<xdr:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></xdr:cNvGraphicFramePr>'
3066
- else
3067
- "<xdr:cNvGraphicFramePr/>"
3068
- end
3069
- parts << %(<xdr:nvGraphicFramePr><xdr:cNvPr id="#{dp[:rid_index] + 1}" name="#{xml_escape(chart_frame_name)}"#{chart_descr_attr}#{chart_title_attr}#{chart_hidden_attr}/>#{cnv_gf_pr}</xdr:nvGraphicFramePr>)
3070
- parts << '<xdr:xfrm><a:off x="0" y="0"/><a:ext cx="5000000" cy="3000000"/></xdr:xfrm>'
3071
- parts << %(<a:graphic><a:graphicData uri="#{C_NS}"><c:chart xmlns:c="#{C_NS}" xmlns:r="#{DOC_REL_NS}" r:id="#{rid}"/></a:graphicData></a:graphic>)
3072
- parts << "</xdr:graphicFrame>"
3073
- parts << client_data_xml(chart)
3074
- parts << "</xdr:twoCellAnchor>"
3075
- when :sp
3076
- shape = dp[:shape]
3077
- shape_ea_attr = shape[:edit_as] ? %( editAs="#{xml_escape(shape[:edit_as])}") : ""
3078
- shape_pub_attr = shape[:published] ? ' fPublished="1"' : ""
3079
- parts << "<xdr:twoCellAnchor#{shape_ea_attr}#{shape_pub_attr}>"
3080
- parts << anchor_xml("from", shape[:from_col], shape[:from_row], col_off: shape[:from_col_off] || 0, row_off: shape[:from_row_off] || 0)
3081
- parts << anchor_xml("to", shape[:to_col], shape[:to_row], col_off: shape[:to_col_off] || 0, row_off: shape[:to_row_off] || 0)
3082
- sp_macro_attr = shape[:macro] ? %( macro="#{xml_escape(shape[:macro])}") : ""
3083
- sp_textlink_attr = shape[:textlink] ? %( textlink="#{xml_escape(shape[:textlink])}") : ""
3084
- parts << "<xdr:sp#{sp_macro_attr}#{sp_textlink_attr}>"
3085
- shape_descr_attr = shape[:description] ? %( descr="#{xml_escape(shape[:description])}") : ""
3086
- shape_title_attr = shape[:title] ? %( title="#{xml_escape(shape[:title])}") : ""
3087
- shape_hidden_attr = shape[:hidden] ? ' hidden="1"' : ""
3088
- sp_lock_attrs = +""
3089
- sp_lock_attrs << ' noGrp="1"' if shape[:no_grp]
3090
- sp_lock_attrs << ' noRot="1"' if shape[:no_rot]
3091
- sp_lock_attrs << ' fLocksText="1"' if shape[:f_locks_text]
3092
- cnv_sp_pr = if sp_lock_attrs.empty?
3093
- "<xdr:cNvSpPr/>"
3094
- else
3095
- "<xdr:cNvSpPr><a:spLocks#{sp_lock_attrs}/></xdr:cNvSpPr>"
3096
- end
3097
- parts << %(<xdr:nvSpPr><xdr:cNvPr id="#{dp[:id]}" name="#{xml_escape(shape[:name])}"#{shape_descr_attr}#{shape_title_attr}#{shape_hidden_attr}/>#{cnv_sp_pr}</xdr:nvSpPr>)
3098
- shape_fill_xml = if shape[:no_fill]
3099
- "<a:noFill/>"
3100
- elsif shape[:fill_color]
3101
- "<a:solidFill>#{srgb_clr_xml(shape[:fill_color], alpha: shape[:fill_alpha], transforms: shape[:fill_color_transforms])}</a:solidFill>"
3102
- elsif shape[:gradient_fill]
3103
- gf = shape[:gradient_fill]
3104
- gf_attrs = +""
3105
- gf_attrs << %( rotWithShape="#{gf[:rot_with_shape] ? 1 : 0}") unless gf[:rot_with_shape].nil?
3106
- gf_attrs << %( flip="#{xml_escape(gf[:flip])}") if gf[:flip]
3107
- gs_xml = (gf[:stops] || []).map { |gs| %(<a:gs pos="#{gs[:pos]}">#{color_xml(gs[:color])}</a:gs>) }.join
3108
- type_xml = if gf[:path]
3109
- %(<a:path path="#{xml_escape(gf[:path])}"/>)
3110
- elsif gf[:angle]
3111
- scaled_attr = gf[:scaled] ? %( scaled="1") : %( scaled="0")
3112
- %(<a:lin ang="#{gf[:angle]}"#{scaled_attr}/>)
3113
- else
3114
- ""
3115
- end
3116
- tile_xml = gf[:tile_rect] ? %(<a:tileRect#{gf[:tile_rect].map { |k, v| %( #{k}="#{v}") }.join}/>) : ""
3117
- "<a:gradFill#{gf_attrs}><a:gsLst>#{gs_xml}</a:gsLst>#{type_xml}#{tile_xml}</a:gradFill>"
3118
- elsif shape[:pattern_fill]
3119
- pf = shape[:pattern_fill]
3120
- pf_children = +""
3121
- pf_children << %(<a:fgClr>#{color_xml(pf[:fg_color])}</a:fgClr>) if pf[:fg_color]
3122
- pf_children << %(<a:bgClr>#{color_xml(pf[:bg_color])}</a:bgClr>) if pf[:bg_color]
3123
- %(<a:pattFill prst="#{xml_escape(pf[:preset])}">#{pf_children}</a:pattFill>)
3124
- else
3125
- ""
3126
- end
3127
- shape_line_xml = if shape[:no_line]
3128
- "<a:ln><a:noFill/></a:ln>"
3129
- elsif shape[:line_color] || shape[:line_dash] || shape[:line_custom_dash] || shape[:head_end] || shape[:tail_end] || shape[:line_cap] || shape[:line_align] || shape[:line_compound] || shape[:line_join]
3130
- ln_attrs = +(shape[:line_width] ? %( w="#{shape[:line_width].to_i}") : "")
3131
- ln_attrs << %( cap="#{xml_escape(shape[:line_cap])}") if shape[:line_cap]
3132
- ln_attrs << %( algn="#{xml_escape(shape[:line_align])}") if shape[:line_align]
3133
- ln_attrs << %( cmpd="#{xml_escape(shape[:line_compound])}") if shape[:line_compound]
3134
- fill_part = if shape[:line_color]
3135
- "<a:solidFill>#{srgb_clr_xml(shape[:line_color], alpha: shape[:line_alpha], transforms: shape[:line_color_transforms])}</a:solidFill>"
3136
- else
3137
- ""
3138
- end
3139
- dash_part = if shape[:line_custom_dash]
3140
- ds_xml = shape[:line_custom_dash].map { |ds| %(<a:ds d="#{ds[:d]}" sp="#{ds[:sp]}"/>) }.join
3141
- "<a:custDash>#{ds_xml}</a:custDash>"
3142
- elsif shape[:line_dash]
3143
- %(<a:prstDash val="#{xml_escape(shape[:line_dash])}"/>)
3144
- else
3145
- ""
3146
- end
3147
- head_part = build_line_end_xml("headEnd", shape[:head_end])
3148
- tail_part = build_line_end_xml("tailEnd", shape[:tail_end])
3149
- join_part = case shape[:line_join]
3150
- when "round" then "<a:round/>"
3151
- when "bevel" then "<a:bevel/>"
3152
- when "miter"
3153
- shape[:line_miter_limit] ? "<a:miter lim=\"#{shape[:line_miter_limit].to_i}\"/>" : "<a:miter/>"
3154
- else ""
3155
- end
3156
- %(<a:ln#{ln_attrs}>#{fill_part}#{dash_part}#{head_part}#{tail_part}#{join_part}</a:ln>)
3157
- else
3158
- ""
3159
- end
3160
- shape_xfrm_xml = shape[:rotation] ? %(<a:xfrm rot="#{shape[:rotation].to_i}"><a:off x="0" y="0"/><a:ext cx="0" cy="0"/></a:xfrm>) : ""
3161
- av_lst_xml = if shape[:adjust_values]&.any?
3162
- gds = shape[:adjust_values].map { |gd| %(<a:gd name="#{xml_escape(gd[:name])}" fmla="#{xml_escape(gd[:fmla])}"/>) }
3163
- "<a:avLst>#{gds.join}</a:avLst>"
3164
- else
3165
- "<a:avLst/>"
3166
- end
3167
- effect_lst_xml = if shape[:outer_shadow] || shape[:inner_shadow] || shape[:glow] || shape[:soft_edge] || shape[:reflection] || shape[:blur]
3168
- effect_children = +""
3169
- if shape[:outer_shadow]
3170
- os = shape[:outer_shadow]
3171
- os_attrs = +""
3172
- os_attrs << %( blurRad="#{os[:blur_rad]}") if os[:blur_rad]
3173
- os_attrs << %( dist="#{os[:dist]}") if os[:dist]
3174
- os_attrs << %( dir="#{os[:dir]}") if os[:dir]
3175
- os_attrs << %( algn="#{xml_escape(os[:algn])}") if os[:algn]
3176
- os_attrs << %( rotWithShape="#{os[:rot_with_shape] ? 1 : 0}") unless os[:rot_with_shape].nil?
3177
- os_color = os[:color] ? srgb_clr_xml(os[:color], alpha: os[:alpha]) : ""
3178
- effect_children << "<a:outerShdw#{os_attrs}>#{os_color}</a:outerShdw>"
3179
- end
3180
- if shape[:glow]
3181
- gl = shape[:glow]
3182
- gl_attrs = +""
3183
- gl_attrs << %( rad="#{gl[:rad]}") if gl[:rad]
3184
- gl_color = gl[:color] ? srgb_clr_xml(gl[:color], alpha: gl[:alpha]) : ""
3185
- effect_children << "<a:glow#{gl_attrs}>#{gl_color}</a:glow>"
3186
- end
3187
- if shape[:inner_shadow]
3188
- is = shape[:inner_shadow]
3189
- is_attrs = +""
3190
- is_attrs << %( blurRad="#{is[:blur_rad]}") if is[:blur_rad]
3191
- is_attrs << %( dist="#{is[:dist]}") if is[:dist]
3192
- is_attrs << %( dir="#{is[:dir]}") if is[:dir]
3193
- is_color = is[:color] ? srgb_clr_xml(is[:color], alpha: is[:alpha]) : ""
3194
- effect_children << "<a:innerShdw#{is_attrs}>#{is_color}</a:innerShdw>"
3195
- end
3196
- if shape[:reflection]
3197
- rf = shape[:reflection]
3198
- rf_attrs = +""
3199
- rf_attrs << %( blurRad="#{rf[:blur_rad]}") if rf[:blur_rad]
3200
- rf_attrs << %( stA="#{rf[:st_a]}") if rf[:st_a]
3201
- rf_attrs << %( endA="#{rf[:end_a]}") if rf[:end_a]
3202
- rf_attrs << %( dist="#{rf[:dist]}") unless rf[:dist].nil?
3203
- rf_attrs << %( dir="#{rf[:dir]}") if rf[:dir]
3204
- rf_attrs << %( fadeDir="#{rf[:fade_dir]}") if rf[:fade_dir]
3205
- rf_attrs << %( sx="#{rf[:sx]}") if rf[:sx]
3206
- rf_attrs << %( sy="#{rf[:sy]}") if rf[:sy]
3207
- rf_attrs << %( kx="#{rf[:kx]}") if rf[:kx]
3208
- rf_attrs << %( ky="#{rf[:ky]}") if rf[:ky]
3209
- rf_attrs << %( algn="#{xml_escape(rf[:algn])}") if rf[:algn]
3210
- rf_attrs << %( rotWithShape="#{rf[:rot_with_shape] ? 1 : 0}") unless rf[:rot_with_shape].nil?
3211
- effect_children << "<a:reflection#{rf_attrs}/>"
3212
- end
3213
- if shape[:soft_edge]
3214
- se = shape[:soft_edge]
3215
- effect_children << %(<a:softEdge rad="#{se[:rad]}"/>)
3216
- end
3217
- if shape[:blur]
3218
- bl = shape[:blur]
3219
- bl_attrs = +""
3220
- bl_attrs << %( rad="#{bl[:rad]}") if bl[:rad]
3221
- bl_attrs << %( grow="#{bl[:grow] ? 1 : 0}") unless bl[:grow].nil?
3222
- effect_children << "<a:blur#{bl_attrs}/>"
3223
- end
3224
- "<a:effectLst>#{effect_children}</a:effectLst>"
3225
- else
3226
- ""
3227
- end
3228
- parts << %(<xdr:spPr>#{shape_xfrm_xml}<a:prstGeom prst="#{xml_escape(shape[:preset])}">#{av_lst_xml}</a:prstGeom>#{shape_fill_xml}#{shape_line_xml}#{effect_lst_xml}</xdr:spPr>)
3229
- if shape[:text] || shape[:text_paragraphs]
3230
- body_pr_attrs = +""
3231
- body_pr_attrs << %( rot="#{shape[:text_rot]}") if shape[:text_rot]
3232
- body_pr_attrs << %( spcFirstLastPara="1") if shape[:text_spc_first_last_para]
3233
- body_pr_attrs << %( wrap="#{xml_escape(shape[:text_wrap])}") if shape[:text_wrap]
3234
- body_pr_attrs << %( anchor="#{xml_escape(shape[:text_anchor])}") if shape[:text_anchor]
3235
- body_pr_attrs << %( anchorCtr="1") if shape[:text_anchor_ctr]
3236
- body_pr_attrs << %( vertOverflow="#{xml_escape(shape[:text_vert_overflow])}") if shape[:text_vert_overflow]
3237
- body_pr_attrs << %( horzOverflow="#{xml_escape(shape[:text_horz_overflow])}") if shape[:text_horz_overflow]
3238
- body_pr_attrs << %( numCol="#{shape[:text_num_col]}") if shape[:text_num_col]
3239
- body_pr_attrs << %( spcCol="#{shape[:text_spc_col]}") if shape[:text_spc_col]
3240
- body_pr_attrs << %( rtlCol="1") if shape[:text_rtl_col]
3241
- body_pr_attrs << %( fromWordArt="1") if shape[:text_from_word_art]
3242
- body_pr_attrs << %( upright="1") if shape[:text_upright]
3243
- body_pr_attrs << %( compatLnSpc="1") if shape[:text_compat_ln_spc]
3244
- body_pr_attrs << %( forceAA="1") if shape[:text_force_aa]
3245
- body_pr_attrs << %( vert="#{xml_escape(shape[:text_vertical])}") if shape[:text_vertical]
3246
- if shape[:text_insets]
3247
- ins = shape[:text_insets]
3248
- body_pr_attrs << %( lIns="#{ins[:left]}") if ins[:left]
3249
- body_pr_attrs << %( tIns="#{ins[:top]}") if ins[:top]
3250
- body_pr_attrs << %( rIns="#{ins[:right]}") if ins[:right]
3251
- body_pr_attrs << %( bIns="#{ins[:bottom]}") if ins[:bottom]
3252
- end
3253
- body_pr_children = +""
3254
- if shape[:text_warp]
3255
- tw = shape[:text_warp]
3256
- body_pr_children << %(<a:prstTxWarp prst="#{xml_escape(tw[:preset])}"><a:avLst/></a:prstTxWarp>)
3257
- end
3258
- autofit_xml = case shape[:autofit]
3259
- when "none" then "<a:noAutofit/>"
3260
- when "shape" then "<a:spAutoFit/>"
3261
- when "normal" then "<a:normAutofit/>"
3262
- when Hash
3263
- af = shape[:autofit]
3264
- case af[:type]
3265
- when "normal"
3266
- na_attrs = +""
3267
- na_attrs << %( fontScale="#{af[:font_scale]}") if af[:font_scale]
3268
- na_attrs << %( lnSpcReduction="#{af[:ln_spc_reduction]}") if af[:ln_spc_reduction]
3269
- "<a:normAutofit#{na_attrs}/>"
3270
- when "none" then "<a:noAutofit/>"
3271
- when "shape" then "<a:spAutoFit/>"
3272
- else ""
3273
- end
3274
- else ""
3275
- end
3276
- body_pr_children << autofit_xml
3277
- body_pr_xml = if body_pr_children.empty?
3278
- "<a:bodyPr#{body_pr_attrs}/>"
3279
- else
3280
- "<a:bodyPr#{body_pr_attrs}>#{body_pr_children}</a:bodyPr>"
3281
- end
3282
- parts << "<xdr:txBody>#{body_pr_xml}<a:lstStyle/>"
3283
- if shape[:text_paragraphs]
3284
- shape[:text_paragraphs].each { |para| parts << paragraph_xml(para) }
3285
- elsif shape[:text]
3286
- parts << paragraph_xml(
3287
- text: shape[:text], font: shape[:text_font],
3288
- align: shape[:text_align], font_align: shape[:text_font_align],
3289
- def_tab_sz: shape[:text_def_tab_sz], rtl: shape[:text_rtl],
3290
- ea_ln_brk: shape[:text_ea_ln_brk], latin_ln_brk: shape[:text_latin_ln_brk],
3291
- hanging_punct: shape[:text_hanging_punct], level: shape[:text_level],
3292
- indent: shape[:text_indent], spacing: shape[:text_spacing],
3293
- tab_stops: shape[:text_tab_stops], bullet: shape[:text_bullet],
3294
- def_rpr: shape[:text_def_rpr], end_para_rpr: shape[:text_end_para_rpr]
3295
- )
3296
- end
3297
- parts << "</xdr:txBody>"
3298
- end
3299
- parts << "</xdr:sp>"
3300
- parts << client_data_xml(shape)
3301
- parts << "</xdr:twoCellAnchor>"
3302
- end
3303
- end
3304
-
3305
- parts << "</xdr:wsDr>"
3306
- parts.join
3307
- end
3308
-
3309
- # : (untyped para) -> ::String
3310
- def paragraph_xml(para)
3311
- ppr_attrs = +""
3312
- ppr_attrs << %( algn="#{xml_escape(para[:align])}") if para[:align]
3313
- ppr_attrs << %( fontAlgn="#{xml_escape(para[:font_align])}") if para[:font_align]
3314
- ppr_attrs << %( defTabSz="#{para[:def_tab_sz]}") if para[:def_tab_sz]
3315
- ppr_attrs << %( rtl="1") if para[:rtl]
3316
- ppr_attrs << %( eaLnBrk="1") if para[:ea_ln_brk]
3317
- ppr_attrs << %( latinLnBrk="1") if para[:latin_ln_brk]
3318
- ppr_attrs << %( hangingPunct="1") if para[:hanging_punct]
3319
- ppr_attrs << %( lvl="#{para[:level]}") if para[:level]
3320
- if para[:indent]
3321
- ti = para[:indent]
3322
- ppr_attrs << %( marL="#{ti[:left]}") if ti[:left]
3323
- ppr_attrs << %( marR="#{ti[:right]}") if ti[:right]
3324
- ppr_attrs << %( indent="#{ti[:indent]}") if ti[:indent]
3325
- end
3326
- ppr_children = +""
3327
- if para[:spacing]
3328
- ts = para[:spacing]
3329
- if ts[:line]
3330
- ppr_children << %(<a:lnSpc><a:spcPts val="#{ts[:line]}"/></a:lnSpc>)
3331
- elsif ts[:line_pct]
3332
- ppr_children << %(<a:lnSpc><a:spcPct val="#{ts[:line_pct]}"/></a:lnSpc>)
3333
- end
3334
- if ts[:before]
3335
- ppr_children << %(<a:spcBef><a:spcPts val="#{ts[:before]}"/></a:spcBef>)
3336
- elsif ts[:before_pct]
3337
- ppr_children << %(<a:spcBef><a:spcPct val="#{ts[:before_pct]}"/></a:spcBef>)
3338
- end
3339
- if ts[:after]
3340
- ppr_children << %(<a:spcAft><a:spcPts val="#{ts[:after]}"/></a:spcAft>)
3341
- elsif ts[:after_pct]
3342
- ppr_children << %(<a:spcAft><a:spcPct val="#{ts[:after_pct]}"/></a:spcAft>)
3343
- end
3344
- end
3345
- if para[:tab_stops]&.any?
3346
- tabs = para[:tab_stops].map do |tab|
3347
- tab_attrs = %( pos="#{tab[:pos]}")
3348
- tab_attrs << %( algn="#{xml_escape(tab[:align])}") if tab[:align]
3349
- "<a:tab#{tab_attrs}/>"
3350
- end
3351
- ppr_children << "<a:tabLst>#{tabs.join}</a:tabLst>"
3352
- end
3353
- if para[:bullet]
3354
- bu = para[:bullet]
3355
- ppr_children << %(<a:buClr>#{color_xml(bu[:color])}</a:buClr>) if bu[:color]
3356
- ppr_children << %(<a:buSzPts val="#{bu[:size_pts]}"/>) if bu[:size_pts]
3357
- ppr_children << %(<a:buSzPct val="#{bu[:size_pct]}"/>) if bu[:size_pct]
3358
- ppr_children << %(<a:buFont typeface="#{xml_escape(bu[:font])}"/>) if bu[:font]
3359
- case bu[:type]
3360
- when "none"
3361
- ppr_children << "<a:buNone/>"
3362
- when "char"
3363
- ppr_children << %(<a:buChar char="#{xml_escape(bu[:char])}"/>)
3364
- when "auto"
3365
- auto_attrs = %( type="#{xml_escape(bu[:auto_type])}")
3366
- auto_attrs << %( startAt="#{bu[:start_at]}") if bu[:start_at]
3367
- ppr_children << "<a:buAutoNum#{auto_attrs}/>"
3368
- end
3369
- end
3370
- ppr_children << text_char_props_xml("a:defRPr", para[:def_rpr]) if para[:def_rpr]
3371
- ppr_xml = if ppr_attrs.empty? && ppr_children.empty?
3372
- ""
3373
- elsif ppr_children.empty?
3374
- "<a:pPr#{ppr_attrs}/>"
3375
- else
3376
- "<a:pPr#{ppr_attrs}>#{ppr_children}</a:pPr>"
3377
- end
3378
- end_para_rpr_xml = para[:end_para_rpr] ? text_char_props_xml("a:endParaRPr", para[:end_para_rpr]) : ""
3379
- runs_xml = if para[:runs]
3380
- para[:runs].map do |run|
3381
- run_rpr = run[:font] ? text_char_props_xml("a:rPr", run[:font]) : ""
3382
- "<a:r>#{run_rpr}<a:t>#{xml_escape(run[:text] || "")}</a:t></a:r>"
3383
- end.join
3384
- else
3385
- rpr_xml = para[:font] ? text_char_props_xml("a:rPr", para[:font]) : ""
3386
- "<a:r>#{rpr_xml}<a:t>#{xml_escape(para[:text] || "")}</a:t></a:r>"
3387
- end
3388
- "<a:p>#{ppr_xml}#{runs_xml}#{end_para_rpr_xml}</a:p>"
3389
- end
3390
-
3391
- # : (untyped tag, untyped font) -> untyped
3392
- def text_char_props_xml(tag, font)
3393
- attrs = +""
3394
- attrs << %( b="1") if font[:bold]
3395
- attrs << %( i="1") if font[:italic]
3396
- attrs << %( noProof="1") if font[:no_proof]
3397
- attrs << %( normalizeH="1") if font[:normalize_h]
3398
- attrs << %( kumimoji="1") if font[:kumimoji]
3399
- attrs << %( sz="#{font[:size]}") if font[:size]
3400
- attrs << %( strike="#{xml_escape(font[:strike])}") if font[:strike]
3401
- attrs << %( u="#{xml_escape(font[:underline])}") if font[:underline]
3402
- attrs << %( baseline="#{font[:baseline]}") if font[:baseline]
3403
- attrs << %( spc="#{font[:spacing]}") if font[:spacing]
3404
- attrs << %( kern="#{font[:kern]}") if font[:kern]
3405
- attrs << %( cap="#{xml_escape(font[:cap])}") if font[:cap]
3406
- attrs << %( lang="#{xml_escape(font[:lang])}") if font[:lang]
3407
- attrs << %( altLang="#{xml_escape(font[:alt_lang])}") if font[:alt_lang]
3408
- attrs << %( dirty="1") if font[:dirty]
3409
- attrs << %( smtClean="1") if font[:smt_clean]
3410
- attrs << %( err="1") if font[:err]
3411
- attrs << %( bmk="#{xml_escape(font[:bmk])}") if font[:bmk]
3412
- children = +""
3413
- children << %(<a:solidFill>#{color_xml(font[:color])}</a:solidFill>) if font[:color]
3414
- children << %(<a:highlight>#{color_xml(font[:highlight])}</a:highlight>) if font[:highlight]
3415
- children << %(<a:latin typeface="#{xml_escape(font[:name])}"/>) if font[:name]
3416
- children << %(<a:ea typeface="#{xml_escape(font[:ea_font])}"/>) if font[:ea_font]
3417
- children << %(<a:cs typeface="#{xml_escape(font[:cs_font])}"/>) if font[:cs_font]
3418
- children << %(<a:sym typeface="#{xml_escape(font[:sym_font])}"/>) if font[:sym_font]
3419
- children << "<a:uFillTx/>" if font[:u_fill_tx]
3420
- children << "<a:uLnTx/>" if font[:u_ln_tx]
3421
- if font[:line_color] || font[:line_width] || font[:line_dash] || font[:line_cap] || font[:line_join]
3422
- ln_attrs = +(font[:line_width] ? %( w="#{font[:line_width].to_i}") : "")
3423
- ln_attrs << %( cap="#{xml_escape(font[:line_cap])}") if font[:line_cap]
3424
- ln_fill = font[:line_color] ? %(<a:solidFill>#{color_xml(font[:line_color])}</a:solidFill>) : ""
3425
- ln_dash = font[:line_dash] ? %(<a:prstDash val="#{xml_escape(font[:line_dash])}"/>) : ""
3426
- ln_join = case font[:line_join]
3427
- when "round" then "<a:round/>"
3428
- when "bevel" then "<a:bevel/>"
3429
- when "miter"
3430
- font[:line_miter_limit] ? "<a:miter lim=\"#{font[:line_miter_limit].to_i}\"/>" : "<a:miter/>"
3431
- else ""
3432
- end
3433
- children << "<a:ln#{ln_attrs}>#{ln_fill}#{ln_dash}#{ln_join}</a:ln>"
3434
- end
3435
- if font[:outer_shadow] || font[:inner_shadow] || font[:glow] || font[:soft_edge] || font[:reflection] || font[:blur]
3436
- eff = +""
3437
- if font[:outer_shadow]
3438
- os = font[:outer_shadow]
3439
- oa = +""
3440
- oa << %( blurRad="#{os[:blur_rad]}") if os[:blur_rad]
3441
- oa << %( dist="#{os[:dist]}") if os[:dist]
3442
- oa << %( dir="#{os[:dir]}") if os[:dir]
3443
- oa << %( algn="#{xml_escape(os[:algn])}") if os[:algn]
3444
- oa << %( rotWithShape="#{os[:rot_with_shape] ? 1 : 0}") unless os[:rot_with_shape].nil?
3445
- oc = os[:color] ? srgb_clr_xml(os[:color], alpha: os[:alpha]) : ""
3446
- eff << "<a:outerShdw#{oa}>#{oc}</a:outerShdw>"
3447
- end
3448
- if font[:glow]
3449
- gl = font[:glow]
3450
- ga = +(gl[:rad] ? %( rad="#{gl[:rad]}") : "")
3451
- gc = gl[:color] ? srgb_clr_xml(gl[:color], alpha: gl[:alpha]) : ""
3452
- eff << "<a:glow#{ga}>#{gc}</a:glow>"
3453
- end
3454
- if font[:inner_shadow]
3455
- is = font[:inner_shadow]
3456
- ia = +""
3457
- ia << %( blurRad="#{is[:blur_rad]}") if is[:blur_rad]
3458
- ia << %( dist="#{is[:dist]}") if is[:dist]
3459
- ia << %( dir="#{is[:dir]}") if is[:dir]
3460
- ic = is[:color] ? srgb_clr_xml(is[:color], alpha: is[:alpha]) : ""
3461
- eff << "<a:innerShdw#{ia}>#{ic}</a:innerShdw>"
3462
- end
3463
- if font[:reflection]
3464
- rf = font[:reflection]
3465
- ra = +""
3466
- ra << %( blurRad="#{rf[:blur_rad]}") if rf[:blur_rad]
3467
- ra << %( stA="#{rf[:st_a]}") if rf[:st_a]
3468
- ra << %( endA="#{rf[:end_a]}") if rf[:end_a]
3469
- ra << %( dist="#{rf[:dist]}") unless rf[:dist].nil?
3470
- ra << %( dir="#{rf[:dir]}") if rf[:dir]
3471
- ra << %( fadeDir="#{rf[:fade_dir]}") if rf[:fade_dir]
3472
- ra << %( sx="#{rf[:sx]}") if rf[:sx]
3473
- ra << %( sy="#{rf[:sy]}") if rf[:sy]
3474
- ra << %( kx="#{rf[:kx]}") if rf[:kx]
3475
- ra << %( ky="#{rf[:ky]}") if rf[:ky]
3476
- ra << %( algn="#{xml_escape(rf[:algn])}") if rf[:algn]
3477
- ra << %( rotWithShape="#{rf[:rot_with_shape] ? 1 : 0}") unless rf[:rot_with_shape].nil?
3478
- eff << "<a:reflection#{ra}/>"
3479
- end
3480
- if font[:soft_edge]
3481
- se = font[:soft_edge]
3482
- eff << %(<a:softEdge rad="#{se[:rad]}"/>)
3483
- end
3484
- if font[:blur]
3485
- bl = font[:blur]
3486
- ba = +(bl[:rad] ? %( rad="#{bl[:rad]}") : "")
3487
- ba << %( grow="#{bl[:grow] ? 1 : 0}") unless bl[:grow].nil?
3488
- eff << "<a:blur#{ba}/>"
3489
- end
3490
- children << "<a:effectLst>#{eff}</a:effectLst>"
3491
- end
3492
- if children.empty?
3493
- "<#{tag}#{attrs}/>"
3494
- else
3495
- "<#{tag}#{attrs}>#{children}</#{tag}>"
3496
- end
3497
- end
3498
-
3499
- # : (untyped tag, untyped col, untyped row, ?col_off: ::Integer, ?row_off: ::Integer) -> ::String
3500
- def anchor_xml(tag, col, row, col_off: 0, row_off: 0)
3501
- "<xdr:#{tag}><xdr:col>#{col}</xdr:col><xdr:colOff>#{col_off}</xdr:colOff><xdr:row>#{row}</xdr:row><xdr:rowOff>#{row_off}</xdr:rowOff></xdr:#{tag}>"
3502
- end
3503
-
3504
- # : (untyped obj) -> ::String
3505
- def client_data_xml(obj)
3506
- cd_attrs = +""
3507
- cd_attrs << ' fLocksWithSheet="0"' if obj[:locks_with_sheet] == false
3508
- cd_attrs << ' fLocksWithSheet="1"' if obj[:locks_with_sheet] == true
3509
- cd_attrs << ' fPrintsWithSheet="0"' if obj[:prints_with_sheet] == false
3510
- cd_attrs << ' fPrintsWithSheet="1"' if obj[:prints_with_sheet] == true
3511
- "<xdr:clientData#{cd_attrs}/>"
3512
- end
3513
-
3514
- # : (untyped rels_data) -> untyped
3515
- def generate_drawing_rels(rels_data)
3516
- parts = [XML_HEADER, %(<Relationships xmlns="#{REL_NS}">)]
3517
- rels_data.each_with_index do |rel, i|
3518
- rel_type = case rel[:type]
3519
- when :image then "#{DOC_REL_NS}/image"
3520
- when :chart then "#{DOC_REL_NS}/chart"
3521
- end
3522
- parts << %(<Relationship Id="rId#{i + 1}" Type="#{rel_type}" Target="#{rel[:target]}"/>)
3523
- end
3524
- parts << "</Relationships>"
3525
- parts.join
3526
- end
3527
-
3528
- # : (untyped title_spec, ?overlay: bool) -> untyped
3529
- def build_chart_title_xml(title_spec, overlay: false)
3530
- overlay_val = overlay ? 1 : 0
3531
- if title_spec.is_a?(Hash)
3532
- text = xml_escape(title_spec[:text].to_s)
3533
- rotation = title_spec[:rotation]
3534
- body_pr = rotation ? %(<a:bodyPr rot="#{rotation}"/>) : "<a:bodyPr/>"
3535
- font = title_spec[:font] || {}
3536
- rpr_attrs = +""
3537
- rpr_attrs << %( b="1") if font[:bold]
3538
- rpr_attrs << %( i="1") if font[:italic]
3539
- rpr_attrs << %( sz="#{font[:size]}") if font[:size]
3540
- rpr_children = +""
3541
- rpr_children << %(<a:solidFill>#{color_xml(font[:color])}</a:solidFill>) if font[:color]
3542
- rpr_children << %(<a:latin typeface="#{xml_escape(font[:name])}"/>) if font[:name]
3543
- rpr_xml = if rpr_children.empty?
3544
- "<a:rPr#{rpr_attrs}/>"
3545
- else
3546
- "<a:rPr#{rpr_attrs}>#{rpr_children}</a:rPr>"
3547
- end
3548
- layout_xml = build_title_layout_xml(title_spec[:layout])
3549
- sp_xml = build_title_sp_pr(title_spec)
3550
- "<c:title><c:tx><c:rich>#{body_pr}<a:lstStyle/><a:p><a:r>#{rpr_xml}<a:t>#{text}</a:t></a:r></a:p></c:rich></c:tx>#{layout_xml}<c:overlay val=\"#{overlay_val}\"/>#{sp_xml}</c:title>"
3551
- else
3552
- "<c:title><c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>#{xml_escape(title_spec)}</a:t></a:r></a:p></c:rich></c:tx><c:overlay val=\"#{overlay_val}\"/></c:title>"
3553
- end
3554
- end
3555
-
3556
- # : (untyped title_spec, untyped chart, untyped prefix) -> untyped
3557
- def merge_flat_title_styling(title_spec, chart, prefix)
3558
- font_key = :"#{prefix}_font"
3559
- fill_key = :"#{prefix}_fill"
3560
- no_fill_key = :"#{prefix}_no_fill"
3561
- lc_key = :"#{prefix}_line_color"
3562
- lw_key = :"#{prefix}_line_width"
3563
- ld_key = :"#{prefix}_line_dash"
3564
- rot_key = :"#{prefix}_rotation"
3565
- return title_spec unless chart[font_key] || chart[fill_key] || chart[no_fill_key] ||
3566
- chart[lc_key] || chart[lw_key] || chart[ld_key] || chart[rot_key]
3567
-
3568
- spec = title_spec.is_a?(Hash) ? title_spec : { text: title_spec }
3569
- spec[:font] ||= chart[font_key] if chart[font_key]
3570
- spec[:fill_color] ||= chart[fill_key] if chart[fill_key]
3571
- spec[:no_fill] = chart[no_fill_key] if spec[:no_fill].nil? && !chart[no_fill_key].nil?
3572
- spec[:line_color] ||= chart[lc_key] if chart[lc_key]
3573
- spec[:line_width] ||= chart[lw_key] if chart[lw_key]
3574
- spec[:line_dash] ||= chart[ld_key] if chart[ld_key]
3575
- spec[:rotation] ||= chart[rot_key] if chart[rot_key]
3576
- spec
3577
- end
3578
-
3579
- # : (untyped spec) -> untyped
3580
- def build_title_sp_pr(spec)
3581
- children = +""
3582
- children << %(<a:solidFill>#{color_xml(spec[:fill_color])}</a:solidFill>) if spec[:fill_color]
3583
- children << "<a:noFill/>" if spec[:no_fill]
3584
- if spec[:line_color] || spec[:line_width] || spec[:line_dash]
3585
- lw = spec[:line_width] ? %( w="#{(spec[:line_width] * 12_700).to_i}") : ""
3586
- lf = spec[:line_color] ? %(<a:solidFill>#{color_xml(spec[:line_color])}</a:solidFill>) : ""
3587
- ld = spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(spec[:line_dash])}"/>) : ""
3588
- children << "<a:ln#{lw}>#{lf}#{ld}</a:ln>"
3589
- end
3590
- children.empty? ? "" : "<c:spPr>#{children}</c:spPr>"
3591
- end
3592
-
3593
- # : (untyped layout) -> ("" | untyped)
3594
- def build_title_layout_xml(layout)
3595
- return "" unless layout.is_a?(Hash)
3596
-
3597
- ml = +""
3598
- ml << %(<c:x val="#{layout[:x]}"/>) if layout[:x]
3599
- ml << %(<c:y val="#{layout[:y]}"/>) if layout[:y]
3600
- ml << %(<c:w val="#{layout[:w]}"/>) if layout[:w]
3601
- ml << %(<c:h val="#{layout[:h]}"/>) if layout[:h]
3602
- ml.empty? ? "" : "<c:layout><c:manualLayout>#{ml}</c:manualLayout></c:layout>"
3603
- end
3604
-
3605
- # : (untyped tag, untyped spec) -> ("" | untyped)
3606
- def gridlines_xml(tag, spec)
3607
- return "" unless spec
3608
-
3609
- if spec.is_a?(Hash)
3610
- sp_children = +""
3611
- if spec[:line_color] || spec[:line_width] || spec[:line_dash]
3612
- lw = spec[:line_width] ? %( w="#{(spec[:line_width] * 12_700).to_i}") : ""
3613
- lf = spec[:line_color] ? %(<a:solidFill>#{color_xml(spec[:line_color])}</a:solidFill>) : ""
3614
- ld = spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(spec[:line_dash])}"/>) : ""
3615
- sp_children << "<a:ln#{lw}>#{lf}#{ld}</a:ln>"
3616
- end
3617
- sp_children.empty? ? "<c:#{tag}/>" : "<c:#{tag}><c:spPr>#{sp_children}</c:spPr></c:#{tag}>"
3618
- else
3619
- "<c:#{tag}/>"
3620
- end
3621
- end
3622
-
3623
- # Resolves a cell reference like "Sheet1!$A$1:$A$5" to an array of values from @sheets.
3624
- # : (untyped ref) -> (nil | untyped)
3625
- def resolve_sheet_ref(ref)
3626
- return nil unless ref
3627
-
3628
- m = if ref.start_with?("'")
3629
- ref.match(/\A'([^']+)'!(.+)\z/)
3630
-
3631
- else
3632
- ref.match(/\A([^!]+)!(.+)\z/)
3633
-
3634
- end
3635
- return nil unless m
3636
-
3637
- sheet_name = m[1]
3638
- range_part = m[2]
3639
- return nil unless @sheets.key?(sheet_name)
3640
-
3641
- range_part = range_part.delete("$")
3642
- cells = if range_part.include?(":")
3643
- start_cell, end_cell = range_part.split(":", 2)
3644
- enumerate_cell_range(start_cell, end_cell)
3645
- else
3646
- [range_part]
3647
- end
3648
- cells.map { |addr| @sheets[sheet_name][addr] }
3649
- end
3650
-
3651
- # : (untyped start_cell, untyped end_cell) -> (::Array[untyped] | untyped)
3652
- def enumerate_cell_range(start_cell, end_cell)
3653
- sc = start_cell.match(/\A([A-Z]+)(\d+)\z/)
3654
- ec = end_cell.match(/\A([A-Z]+)(\d+)\z/)
3655
- return [start_cell] unless sc && ec
3656
-
3657
- start_col = column_letter_to_index(sc[1])
3658
- start_row = sc[2].to_i
3659
- end_col = column_letter_to_index(ec[1])
3660
- end_row = ec[2].to_i
3661
- cells = []
3662
- (start_row..end_row).each do |row|
3663
- (start_col..end_col).each do |col|
3664
- cells << "#{index_to_column_letter(col)}#{row}"
3665
- end
3666
- end
3667
- cells
3668
- end
3669
-
3670
- # : (untyped ref) -> ("" | ::String)
3671
- def num_cache_xml(ref)
3672
- values = resolve_sheet_ref(ref)
3673
- return "" unless values
3674
-
3675
- pts = values.each_with_index.filter_map do |v, i|
3676
- next unless v
3677
-
3678
- %(<c:pt idx="#{i}"><c:v>#{xml_escape(v.to_s)}</c:v></c:pt>)
3679
- end
3680
- %(<c:numCache><c:formatCode>General</c:formatCode><c:ptCount val="#{values.size}"/>#{pts.join}</c:numCache>)
3681
- end
3682
-
3683
- # : (untyped ref) -> ("" | ::String)
3684
- def str_cache_xml(ref)
3685
- values = resolve_sheet_ref(ref)
3686
- return "" unless values
3687
-
3688
- pts = values.each_with_index.filter_map do |v, i|
3689
- next unless v
3690
-
3691
- %(<c:pt idx="#{i}"><c:v>#{xml_escape(v.to_s)}</c:v></c:pt>)
3692
- end
3693
- %(<c:strCache><c:ptCount val="#{values.size}"/>#{pts.join}</c:strCache>)
3694
- end
3695
-
3696
- # : (untyped color, ?alpha: untyped?, ?transforms: untyped?) -> untyped
3697
- def color_xml(color, alpha: nil, transforms: nil)
3698
- if color.is_a?(Hash) && color[:scheme]
3699
- tag = "schemeClr"
3700
- val = xml_escape(color[:scheme])
3701
- transforms = color[:transforms] || transforms
3702
- else
3703
- tag = "srgbClr"
3704
- val = xml_escape(normalize_drawing_rgb(color))
3705
- end
3706
- children = +""
3707
- children << %(<a:alpha val="#{alpha}"/>) if alpha
3708
- (transforms || []).each do |t|
3709
- children << %(<a:#{xml_escape(t[:type])} val="#{t[:val]}"/>)
3710
- end
3711
- if children.empty?
3712
- %(<a:#{tag} val="#{val}"/>)
3713
- else
3714
- %(<a:#{tag} val="#{val}">#{children}</a:#{tag}>)
3715
- end
3716
- end
3717
-
3718
- # : (untyped color) -> untyped
3719
- def normalize_drawing_rgb(color)
3720
- str = color.to_s
3721
- hex = str.delete_prefix("#")
3722
-
3723
- # Common shorthand (e.g. #f00 -> FF0000)
3724
- return hex.chars.map { |c| c * 2 }.join.upcase if hex.match?(/\A[0-9A-Fa-f]{3}\z/)
3725
-
3726
- # ARGB -> RGB for DrawingML srgbClr (expects RRGGBB)
3727
- return hex[-6, 6].upcase if hex.match?(/\A[0-9A-Fa-f]{8}\z/)
3728
-
3729
- return hex.upcase if hex.match?(/\A[0-9A-Fa-f]{6}\z/)
3730
-
3731
- str
3732
- end
3733
-
3734
- alias srgb_clr_xml color_xml
3735
-
3736
- # : (untyped rotation, untyped font) -> ("" | ::String)
3737
- def build_axis_txpr(rotation, font)
3738
- return "" unless rotation || font
3739
-
3740
- body_attrs = rotation ? %( rot="#{rotation}") : ""
3741
- rpr_attrs = +""
3742
- rpr_children = +""
3743
- if font
3744
- rpr_attrs << %( sz="#{(font[:size] * 100).to_i}") if font[:size]
3745
- rpr_attrs << %( b="1") if font[:bold]
3746
- rpr_attrs << %( i="1") if font[:italic]
3747
- rpr_children << %(<a:solidFill>#{color_xml(font[:color])}</a:solidFill>) if font[:color]
3748
- rpr_children << %(<a:latin typeface="#{xml_escape(font[:name])}"/>) if font[:name]
3749
- end
3750
- def_rpr = if rpr_children.empty?
3751
- "<a:defRPr#{rpr_attrs}/>"
3752
- else
3753
- "<a:defRPr#{rpr_attrs}>#{rpr_children}</a:defRPr>"
3754
- end
3755
- %(<c:txPr><a:bodyPr#{body_attrs}/><a:lstStyle/><a:p><a:pPr>#{def_rpr}</a:pPr><a:endParaRPr/></a:p></c:txPr>)
3756
- end
3757
-
3758
- # : (untyped chart) -> untyped
3759
- def generate_chart_xml(chart)
3760
- chart_type = CHART_TYPE_MAP[chart[:type]] || "barChart"
3761
- no_axes = NO_AXIS_CHARTS.include?(chart_type)
3762
- parts = [
3763
- XML_HEADER,
3764
- %(<c:chartSpace xmlns:c="#{C_NS}" xmlns:a="#{A_NS}" xmlns:r="#{DOC_REL_NS}">)
3765
- ]
3766
- rc = chart[:rounded_corners]
3767
- parts << %(<c:roundedCorners val="#{rc ? 1 : 0}"/>) unless rc.nil?
3768
- parts << %(<c:style val="#{chart[:style]}"/>) if chart[:style]
3769
- if (prot = chart[:protection])
3770
- parts << "<c:protection>"
3771
- parts << %(<c:chartObject val="#{prot[:chart_object] ? 1 : 0}"/>) unless prot[:chart_object].nil?
3772
- parts << %(<c:data val="#{prot[:data] ? 1 : 0}"/>) unless prot[:data].nil?
3773
- parts << %(<c:formatting val="#{prot[:formatting] ? 1 : 0}"/>) unless prot[:formatting].nil?
3774
- parts << %(<c:selection val="#{prot[:selection] ? 1 : 0}"/>) unless prot[:selection].nil?
3775
- parts << %(<c:userInterface val="#{prot[:user_interface] ? 1 : 0}"/>) unless prot[:user_interface].nil?
3776
- parts << "</c:protection>"
3777
- end
3778
- parts << "<c:chart>"
3779
-
3780
- if chart[:title]
3781
- title_spec = chart[:title]
3782
- if chart[:title_font] || chart[:title_fill_color] || chart[:title_no_fill] ||
3783
- chart[:title_line_color] || chart[:title_line_width] || chart[:title_line_dash] ||
3784
- chart[:title_rotation]
3785
- title_spec = { text: title_spec } unless title_spec.is_a?(Hash)
3786
- title_spec[:font] ||= chart[:title_font] if chart[:title_font]
3787
- title_spec[:fill_color] ||= chart[:title_fill_color] if chart[:title_fill_color]
3788
- title_spec[:no_fill] = chart[:title_no_fill] if title_spec[:no_fill].nil? && !chart[:title_no_fill].nil?
3789
- title_spec[:line_color] ||= chart[:title_line_color] if chart[:title_line_color]
3790
- title_spec[:line_width] ||= chart[:title_line_width] if chart[:title_line_width]
3791
- title_spec[:line_dash] ||= chart[:title_line_dash] if chart[:title_line_dash]
3792
- title_spec[:rotation] ||= chart[:title_rotation] if chart[:title_rotation]
3793
- end
3794
- parts << build_chart_title_xml(title_spec, overlay: chart[:title_overlay])
3795
- end
3796
- atd = chart[:auto_title_deleted]
3797
- parts << %(<c:autoTitleDeleted val="#{atd ? 1 : 0}"/>) unless atd.nil?
3798
-
3799
- if (v3d = chart[:view_3d])
3800
- parts << "<c:view3D>"
3801
- parts << %(<c:rotX val="#{v3d[:rot_x]}"/>) if v3d[:rot_x]
3802
- parts << %(<c:hPercent val="#{v3d[:h_percent]}"/>) if v3d[:h_percent]
3803
- parts << %(<c:rotY val="#{v3d[:rot_y]}"/>) if v3d[:rot_y]
3804
- parts << %(<c:depthPercent val="#{v3d[:depth_percent]}"/>) if v3d[:depth_percent]
3805
- r_ang = v3d[:r_ang_ax]
3806
- parts << %(<c:rAngAx val="#{r_ang ? 1 : 0}"/>) unless r_ang.nil?
3807
- parts << %(<c:perspective val="#{v3d[:perspective]}"/>) if v3d[:perspective]
3808
- parts << "</c:view3D>"
3809
- end
3810
-
3811
- %i[floor side_wall back_wall].each do |wall_key|
3812
- next unless (wall = chart[wall_key])
3813
-
3814
- tag = { floor: "floor", side_wall: "sideWall", back_wall: "backWall" }[wall_key]
3815
- parts << "<c:#{tag}><c:spPr>"
3816
- parts << "<a:solidFill>#{srgb_clr_xml(wall[:fill_color])}</a:solidFill>" if wall[:fill_color]
3817
- parts << "<a:noFill/>" if wall[:no_fill]
3818
- if wall[:line_color] || wall[:line_dash]
3819
- w_attr = wall[:line_width] ? %( w="#{wall[:line_width]}") : ""
3820
- w_fill = wall[:line_color] ? %(<a:solidFill>#{srgb_clr_xml(wall[:line_color])}</a:solidFill>) : ""
3821
- w_dash = wall[:line_dash] ? %(<a:prstDash val="#{xml_escape(wall[:line_dash])}"/>) : ""
3822
- parts << "<a:ln#{w_attr}>#{w_fill}#{w_dash}</a:ln>"
3823
- end
3824
- parts << "</c:spPr></c:#{tag}>"
3825
- end
3826
-
3827
- parts << "<c:plotArea>"
3828
- pa_layout = chart[:plot_area_layout]
3829
- if pa_layout.is_a?(Hash)
3830
- ml_parts = +""
3831
- ml_parts << %(<c:layoutTarget val="#{xml_escape(pa_layout[:target])}"/>) if pa_layout[:target]
3832
- ml_parts << %(<c:xMode val="#{xml_escape(pa_layout[:x_mode])}"/>) if pa_layout[:x_mode]
3833
- ml_parts << %(<c:yMode val="#{xml_escape(pa_layout[:y_mode])}"/>) if pa_layout[:y_mode]
3834
- ml_parts << %(<c:wMode val="#{xml_escape(pa_layout[:w_mode])}"/>) if pa_layout[:w_mode]
3835
- ml_parts << %(<c:hMode val="#{xml_escape(pa_layout[:h_mode])}"/>) if pa_layout[:h_mode]
3836
- ml_parts << %(<c:x val="#{pa_layout[:x]}"/>) if pa_layout[:x]
3837
- ml_parts << %(<c:y val="#{pa_layout[:y]}"/>) if pa_layout[:y]
3838
- ml_parts << %(<c:w val="#{pa_layout[:w]}"/>) if pa_layout[:w]
3839
- ml_parts << %(<c:h val="#{pa_layout[:h]}"/>) if pa_layout[:h]
3840
- parts << if ml_parts.empty?
3841
- "<c:layout/>"
3842
- else
3843
- "<c:layout><c:manualLayout>#{ml_parts}</c:manualLayout></c:layout>"
3844
- end
3845
- else
3846
- parts << "<c:layout/>"
3847
- end
3848
- parts << "<c:#{chart_type}>"
3849
- parts << %(<c:ofPieType val="#{xml_escape(chart[:of_pie_type] || "pie")}"/>) if chart_type == "ofPieChart"
3850
- if %w[barChart bar3DChart].include?(chart_type)
3851
- bd = chart[:bar_dir] || "col"
3852
- gr = chart[:grouping] || "clustered"
3853
- parts << %(<c:barDir val="#{bd}"/><c:grouping val="#{gr}"/>)
3854
- elsif GROUPING_CHARTS.include?(chart_type)
3855
- parts << %(<c:grouping val="#{chart[:grouping] || "standard"}"/>)
3856
- end
3857
- parts << %(<c:scatterStyle val="#{chart[:scatter_style] || "lineMarker"}"/>) if chart_type == "scatterChart"
3858
- parts << %(<c:radarStyle val="#{chart[:radar_style] || "standard"}"/>) if chart_type == "radarChart"
3859
- vc = chart[:vary_colors]
3860
- parts << %(<c:varyColors val="#{vc ? 1 : 0}"/>) unless vc.nil?
3861
- wf = chart[:wireframe]
3862
- parts << %(<c:wireframe val="#{wf ? 1 : 0}"/>) unless wf.nil?
3863
-
3864
- all_series = chart[:series] || []
3865
- all_series.each_with_index do |ser, idx|
3866
- ser_order = ser[:order] || idx
3867
- parts << "<c:ser><c:idx val=\"#{idx}\"/><c:order val=\"#{ser_order}\"/>"
3868
- parts << "<c:tx><c:strRef><c:f>#{xml_escape(ser[:name])}</c:f>#{str_cache_xml(ser[:name])}</c:strRef></c:tx>" if ser[:name]
3869
- iin = ser[:invert_if_negative]
3870
- parts << %(<c:invertIfNegative val="#{iin ? 1 : 0}"/>) unless iin.nil?
3871
- parts << %(<c:explosion val="#{ser[:explosion]}"/>) if ser[:explosion]
3872
- ser[:data_points]&.each do |dp|
3873
- parts << "<c:dPt><c:idx val=\"#{dp[:idx]}\"/>"
3874
- dp_iin = dp[:invert_if_negative]
3875
- parts << %(<c:invertIfNegative val="#{dp_iin ? 1 : 0}"/>) unless dp_iin.nil?
3876
- if dp[:marker_symbol] || dp[:marker_size] || dp[:marker_fill] || dp[:marker_no_fill] || dp[:marker_line_color] || dp[:marker_line_dash] || dp[:marker_no_line]
3877
- parts << "<c:marker>"
3878
- parts << %(<c:symbol val="#{xml_escape(dp[:marker_symbol])}"/>) if dp[:marker_symbol]
3879
- parts << %(<c:size val="#{dp[:marker_size]}"/>) if dp[:marker_size]
3880
- if dp[:marker_fill] || dp[:marker_no_fill] || dp[:marker_line_color] || dp[:marker_line_dash] || dp[:marker_no_line]
3881
- mk_sp = +""
3882
- mk_sp << "<a:noFill/>" if dp[:marker_no_fill]
3883
- mk_sp << %(<a:solidFill>#{color_xml(dp[:marker_fill])}</a:solidFill>) if dp[:marker_fill]
3884
- if dp[:marker_no_line]
3885
- mk_sp << "<a:ln><a:noFill/></a:ln>"
3886
- elsif dp[:marker_line_color] || dp[:marker_line_dash]
3887
- mk_ln_w = dp[:marker_line_width] ? %( w="#{(dp[:marker_line_width] * 12_700).to_i}") : ""
3888
- mk_ln_f = dp[:marker_line_color] ? %(<a:solidFill>#{color_xml(dp[:marker_line_color])}</a:solidFill>) : ""
3889
- mk_ln_d = dp[:marker_line_dash] ? %(<a:prstDash val="#{xml_escape(dp[:marker_line_dash])}"/>) : ""
3890
- mk_sp << "<a:ln#{mk_ln_w}>#{mk_ln_f}#{mk_ln_d}</a:ln>"
3891
- end
3892
- parts << "<c:spPr>#{mk_sp}</c:spPr>"
3893
- end
3894
- parts << "</c:marker>"
3895
- end
3896
- dp_b3d = dp[:bubble_3d]
3897
- parts << %(<c:bubble3D val="#{dp_b3d ? 1 : 0}"/>) unless dp_b3d.nil?
3898
- parts << %(<c:explosion val="#{dp[:explosion]}"/>) if dp[:explosion]
3899
- dp_sp_children = +""
3900
- dp_sp_children << %(<a:solidFill>#{color_xml(dp[:fill_color])}</a:solidFill>) if dp[:fill_color]
3901
- dp_sp_children << "<a:noFill/>" if dp[:no_fill]
3902
- if dp[:no_line]
3903
- dp_sp_children << "<a:ln><a:noFill/></a:ln>"
3904
- elsif dp[:line_color] || dp[:line_width] || dp[:line_dash]
3905
- dp_ln_attrs = dp[:line_width] ? %( w="#{(dp[:line_width] * 12_700).to_i}") : ""
3906
- dp_ln_fill = dp[:line_color] ? %(<a:solidFill>#{color_xml(dp[:line_color])}</a:solidFill>) : ""
3907
- dp_ln_dash = dp[:line_dash] ? %(<a:prstDash val="#{xml_escape(dp[:line_dash])}"/>) : ""
3908
- dp_sp_children << "<a:ln#{dp_ln_attrs}>#{dp_ln_fill}#{dp_ln_dash}</a:ln>"
3909
- end
3910
- parts << "<c:spPr>#{dp_sp_children}</c:spPr>" unless dp_sp_children.empty?
3911
- parts << "</c:dPt>"
3912
- end
3913
- if ser[:fill_color] || ser[:no_fill] || ser[:line_color] || ser[:no_line] || ser[:line_width] || ser[:line_cap] || ser[:line_join] || ser[:line_dash]
3914
- parts << "<c:spPr>"
3915
- parts << %(<a:solidFill>#{color_xml(ser[:fill_color])}</a:solidFill>) if ser[:fill_color]
3916
- parts << "<a:noFill/>" if ser[:no_fill]
3917
- if ser[:line_color] || ser[:no_line] || ser[:line_width] || ser[:line_cap] || ser[:line_join] || ser[:line_dash]
3918
- lw = ser[:line_width] ? %( w="#{(ser[:line_width] * 12_700).to_i}") : ""
3919
- lc = ser[:line_cap] ? %( cap="#{xml_escape(ser[:line_cap])}") : ""
3920
- parts << "<a:ln#{lw}#{lc}>"
3921
- parts << "<a:noFill/>" if ser[:no_line]
3922
- parts << %(<a:solidFill>#{color_xml(ser[:line_color])}</a:solidFill>) if ser[:line_color]
3923
- parts << %(<a:prstDash val="#{xml_escape(ser[:line_dash])}"/>) if ser[:line_dash]
3924
- case ser[:line_join]
3925
- when "round" then parts << "<a:round/>"
3926
- when "bevel" then parts << "<a:bevel/>"
3927
- when "miter"
3928
- parts << (ser[:line_miter_limit] ? "<a:miter lim=\"#{ser[:line_miter_limit].to_i}\"/>" : "<a:miter/>")
3929
- end
3930
- parts << "</a:ln>"
3931
- end
3932
- parts << "</c:spPr>"
3933
- end
3934
- if ser[:marker_symbol] || ser[:marker_size] || ser[:marker_fill] || ser[:marker_no_fill] || ser[:marker_line_color] || ser[:marker_line_dash] || ser[:marker_no_line]
3935
- parts << "<c:marker>"
3936
- parts << %(<c:symbol val="#{xml_escape(ser[:marker_symbol])}"/>) if ser[:marker_symbol]
3937
- parts << %(<c:size val="#{ser[:marker_size]}"/>) if ser[:marker_size]
3938
- if ser[:marker_fill] || ser[:marker_no_fill] || ser[:marker_line_color] || ser[:marker_line_dash] || ser[:marker_no_line]
3939
- parts << "<c:spPr>"
3940
- parts << "<a:noFill/>" if ser[:marker_no_fill]
3941
- parts << %(<a:solidFill>#{color_xml(ser[:marker_fill])}</a:solidFill>) if ser[:marker_fill]
3942
- if ser[:marker_no_line]
3943
- parts << "<a:ln><a:noFill/></a:ln>"
3944
- elsif ser[:marker_line_color] || ser[:marker_line_dash]
3945
- mk_ln_w = ser[:marker_line_width] ? %( w="#{(ser[:marker_line_width] * 12_700).to_i}") : ""
3946
- mk_ln_f = ser[:marker_line_color] ? %(<a:solidFill>#{color_xml(ser[:marker_line_color])}</a:solidFill>) : ""
3947
- mk_ln_d = ser[:marker_line_dash] ? %(<a:prstDash val="#{xml_escape(ser[:marker_line_dash])}"/>) : ""
3948
- parts << "<a:ln#{mk_ln_w}>#{mk_ln_f}#{mk_ln_d}</a:ln>"
3949
- end
3950
- parts << "</c:spPr>"
3951
- end
3952
- parts << "</c:marker>"
3953
- end
3954
- if (dl = ser[:data_labels] || chart[:data_labels])
3955
- parts << "<c:dLbls>"
3956
- dl[:labels]&.each do |lbl|
3957
- parts << "<c:dLbl>"
3958
- parts << %(<c:idx val="#{lbl[:idx]}"/>)
3959
- if lbl[:delete]
3960
- parts << '<c:delete val="1"/>'
3961
- else
3962
- if (lbl_layout = lbl[:layout])
3963
- ml = +""
3964
- ml << %(<c:x val="#{lbl_layout[:x]}"/>) if lbl_layout[:x]
3965
- ml << %(<c:y val="#{lbl_layout[:y]}"/>) if lbl_layout[:y]
3966
- parts << (ml.empty? ? "<c:layout/>" : "<c:layout><c:manualLayout>#{ml}</c:manualLayout></c:layout>")
3967
- end
3968
- parts << "<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>#{xml_escape(lbl[:text])}</a:t></a:r></a:p></c:rich></c:tx>" if lbl[:text]
3969
- if lbl[:num_fmt]
3970
- lnf = lbl[:num_fmt]
3971
- lnf_src = lnf[:source_linked] ? ' sourceLinked="1"' : ""
3972
- parts << %(<c:numFmt formatCode="#{xml_escape(lnf[:format_code])}"#{lnf_src}/>)
3973
- end
3974
- lbl_sp = +""
3975
- lbl_sp << %(<a:solidFill>#{color_xml(lbl[:fill_color])}</a:solidFill>) if lbl[:fill_color]
3976
- lbl_sp << "<a:noFill/>" if lbl[:no_fill]
3977
- if lbl[:line_color] || lbl[:line_width] || lbl[:line_dash]
3978
- lbl_lw = lbl[:line_width] ? %( w="#{(lbl[:line_width] * 12_700).to_i}") : ""
3979
- lbl_lf = lbl[:line_color] ? %(<a:solidFill>#{color_xml(lbl[:line_color])}</a:solidFill>) : ""
3980
- lbl_ld = lbl[:line_dash] ? %(<a:prstDash val="#{xml_escape(lbl[:line_dash])}"/>) : ""
3981
- lbl_sp << "<a:ln#{lbl_lw}>#{lbl_lf}#{lbl_ld}</a:ln>"
3982
- end
3983
- parts << "<c:spPr>#{lbl_sp}</c:spPr>" unless lbl_sp.empty?
3984
- parts << build_axis_txpr(nil, lbl[:font]) if lbl[:font]
3985
- parts << %(<c:dLblPos val="#{lbl[:position]}"/>) if lbl[:position]
3986
- parts << "<c:showLegendKey val=\"#{lbl[:show_legend_key] ? 1 : 0}\"/>" unless lbl[:show_legend_key].nil?
3987
- parts << "<c:showVal val=\"#{lbl[:show_val] ? 1 : 0}\"/>" unless lbl[:show_val].nil?
3988
- parts << "<c:showCatName val=\"#{lbl[:show_cat_name] ? 1 : 0}\"/>" unless lbl[:show_cat_name].nil?
3989
- parts << "<c:showSerName val=\"#{lbl[:show_ser_name] ? 1 : 0}\"/>" unless lbl[:show_ser_name].nil?
3990
- parts << "<c:showPercent val=\"#{lbl[:show_percent] ? 1 : 0}\"/>" unless lbl[:show_percent].nil?
3991
- parts << "<c:showBubbleSize val=\"#{lbl[:show_bubble_size] ? 1 : 0}\"/>" unless lbl[:show_bubble_size].nil?
3992
- parts << "<c:separator>#{xml_escape(lbl[:separator])}</c:separator>" if lbl[:separator]
3993
- end
3994
- parts << "</c:dLbl>"
3995
- end
3996
- if dl[:num_fmt]
3997
- nf = dl[:num_fmt]
3998
- nf_src = nf[:source_linked] ? ' sourceLinked="1"' : ""
3999
- parts << %(<c:numFmt formatCode="#{xml_escape(nf[:format_code])}"#{nf_src}/>)
4000
- end
4001
- dl_sp_children = +""
4002
- dl_sp_children << %(<a:solidFill>#{color_xml(dl[:fill_color])}</a:solidFill>) if dl[:fill_color]
4003
- dl_sp_children << "<a:noFill/>" if dl[:no_fill]
4004
- if dl[:line_color] || dl[:line_width] || dl[:line_dash]
4005
- dl_lw = dl[:line_width] ? %( w="#{(dl[:line_width] * 12_700).to_i}") : ""
4006
- dl_lf = dl[:line_color] ? %(<a:solidFill>#{color_xml(dl[:line_color])}</a:solidFill>) : ""
4007
- dl_ld = dl[:line_dash] ? %(<a:prstDash val="#{xml_escape(dl[:line_dash])}"/>) : ""
4008
- dl_sp_children << "<a:ln#{dl_lw}>#{dl_lf}#{dl_ld}</a:ln>"
4009
- end
4010
- parts << "<c:spPr>#{dl_sp_children}</c:spPr>" unless dl_sp_children.empty?
4011
- parts << build_axis_txpr(nil, dl[:font]) if dl[:font]
4012
- parts << %(<c:dLblPos val="#{dl[:position]}"/>) if dl[:position]
4013
- parts << "<c:showLegendKey val=\"#{dl[:show_legend_key] ? 1 : 0}\"/>" unless dl[:show_legend_key].nil?
4014
- parts << "<c:showVal val=\"#{dl[:show_val] ? 1 : 0}\"/>" unless dl[:show_val].nil?
4015
- parts << "<c:showCatName val=\"#{dl[:show_cat_name] ? 1 : 0}\"/>" unless dl[:show_cat_name].nil?
4016
- parts << "<c:showSerName val=\"#{dl[:show_ser_name] ? 1 : 0}\"/>" unless dl[:show_ser_name].nil?
4017
- parts << "<c:showPercent val=\"#{dl[:show_percent] ? 1 : 0}\"/>" unless dl[:show_percent].nil?
4018
- parts << "<c:showBubbleSize val=\"#{dl[:show_bubble_size] ? 1 : 0}\"/>" unless dl[:show_bubble_size].nil?
4019
- parts << "<c:separator>#{xml_escape(dl[:separator])}</c:separator>" if dl[:separator]
4020
- sl = dl[:show_leader_lines]
4021
- parts << "<c:showLeaderLines val=\"#{sl ? 1 : 0}\"/>" unless sl.nil?
4022
- if dl[:leader_lines]
4023
- ll_spec = dl[:leader_lines]
4024
- if ll_spec.is_a?(Hash)
4025
- ll_sp = +""
4026
- if ll_spec[:line_color] || ll_spec[:line_width] || ll_spec[:line_dash]
4027
- ll_lw = ll_spec[:line_width] ? %( w="#{(ll_spec[:line_width] * 12_700).to_i}") : ""
4028
- ll_lf = ll_spec[:line_color] ? %(<a:solidFill>#{color_xml(ll_spec[:line_color])}</a:solidFill>) : ""
4029
- ll_ld = ll_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(ll_spec[:line_dash])}"/>) : ""
4030
- ll_sp << "<a:ln#{ll_lw}>#{ll_lf}#{ll_ld}</a:ln>"
4031
- end
4032
- parts << (ll_sp.empty? ? "<c:leaderLines/>" : "<c:leaderLines><c:spPr>#{ll_sp}</c:spPr></c:leaderLines>")
4033
- else
4034
- parts << "<c:leaderLines/>"
4035
- end
4036
- end
4037
- parts << "</c:dLbls>"
4038
- end
4039
- trendline_list = ser[:trendlines] || (ser[:trendline] ? [ser[:trendline]] : [])
4040
- trendline_list.each do |tl|
4041
- parts << "<c:trendline>"
4042
- parts << "<c:name>#{xml_escape(tl[:name])}</c:name>" if tl[:name]
4043
- tl_sp_children = +""
4044
- if tl[:line_color] || tl[:line_width] || tl[:line_dash]
4045
- tl_ln_w = tl[:line_width] ? %( w="#{(tl[:line_width] * 12_700).to_i}") : ""
4046
- tl_ln_f = tl[:line_color] ? %(<a:solidFill>#{color_xml(tl[:line_color])}</a:solidFill>) : ""
4047
- tl_ln_d = tl[:line_dash] ? %(<a:prstDash val="#{xml_escape(tl[:line_dash])}"/>) : ""
4048
- tl_sp_children << "<a:ln#{tl_ln_w}>#{tl_ln_f}#{tl_ln_d}</a:ln>"
4049
- end
4050
- parts << "<c:spPr>#{tl_sp_children}</c:spPr>" unless tl_sp_children.empty?
4051
- parts << %(<c:trendlineType val="#{xml_escape(tl[:type] || "linear")}"/>)
4052
- parts << %(<c:order val="#{tl[:order]}"/>) if tl[:order]
4053
- parts << %(<c:period val="#{tl[:period]}"/>) if tl[:period]
4054
- parts << %(<c:forward val="#{tl[:forward]}"/>) if tl[:forward]
4055
- parts << %(<c:backward val="#{tl[:backward]}"/>) if tl[:backward]
4056
- parts << %(<c:intercept val="#{tl[:intercept]}"/>) if tl[:intercept]
4057
- parts << %(<c:dispRSqr val="#{tl[:disp_r_sqr] ? 1 : 0}"/>) unless tl[:disp_r_sqr].nil?
4058
- parts << %(<c:dispEq val="#{tl[:disp_eq] ? 1 : 0}"/>) unless tl[:disp_eq].nil?
4059
- if tl[:label]
4060
- lbl = tl[:label]
4061
- parts << "<c:trendlineLbl>"
4062
- parts << build_title_layout_xml(lbl[:layout])
4063
- parts << "<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>#{xml_escape(lbl[:text])}</a:t></a:r></a:p></c:rich></c:tx>" if lbl[:text]
4064
- if lbl[:num_fmt]
4065
- nf = lbl[:num_fmt]
4066
- nf_src = nf.is_a?(Hash) ? nf : { format_code: nf }
4067
- nf_linked = if nf_src.key?(:source_linked)
4068
- nf_src[:source_linked] ? 1 : 0
4069
- else
4070
- 0
4071
- end
4072
- parts << %(<c:numFmt formatCode="#{xml_escape(nf_src[:format_code])}" sourceLinked="#{nf_linked}"/>)
4073
- end
4074
- tll_sp = +""
4075
- tll_sp << %(<a:solidFill>#{color_xml(lbl[:fill_color])}</a:solidFill>) if lbl[:fill_color]
4076
- tll_sp << "<a:noFill/>" if lbl[:no_fill]
4077
- if lbl[:line_color] || lbl[:line_width] || lbl[:line_dash]
4078
- tll_lw = lbl[:line_width] ? %( w="#{(lbl[:line_width] * 12_700).to_i}") : ""
4079
- tll_lf = lbl[:line_color] ? %(<a:solidFill>#{color_xml(lbl[:line_color])}</a:solidFill>) : ""
4080
- tll_ld = lbl[:line_dash] ? %(<a:prstDash val="#{xml_escape(lbl[:line_dash])}"/>) : ""
4081
- tll_sp << "<a:ln#{tll_lw}>#{tll_lf}#{tll_ld}</a:ln>"
4082
- end
4083
- parts << "<c:spPr>#{tll_sp}</c:spPr>" unless tll_sp.empty?
4084
- parts << build_axis_txpr(nil, lbl[:font]) if lbl[:font]
4085
- parts << "</c:trendlineLbl>"
4086
- end
4087
- parts << "</c:trendline>"
4088
- end
4089
- err_bars_list = ser[:error_bars_list] || (ser[:error_bars] ? [ser[:error_bars]] : [])
4090
- err_bars_list.each do |eb|
4091
- parts << "<c:errBars>"
4092
- parts << %(<c:errDir val="#{xml_escape(eb[:direction])}"/>) if eb[:direction]
4093
- parts << %(<c:errBarType val="#{xml_escape(eb[:bar_type] || "both")}"/>)
4094
- parts << %(<c:errValType val="#{xml_escape(eb[:val_type] || "fixedVal")}"/>)
4095
- parts << %(<c:noEndCap val="#{eb[:no_end_cap] ? 1 : 0}"/>) unless eb[:no_end_cap].nil?
4096
- parts << "<c:plus><c:numRef><c:f>#{xml_escape(eb[:plus])}</c:f></c:numRef></c:plus>" if eb[:plus]
4097
- parts << "<c:minus><c:numRef><c:f>#{xml_escape(eb[:minus])}</c:f></c:numRef></c:minus>" if eb[:minus]
4098
- parts << %(<c:val val="#{eb[:val]}"/>) if eb[:val]
4099
- eb_sp_children = +""
4100
- eb_sp_children << %(<a:solidFill>#{color_xml(eb[:fill_color])}</a:solidFill>) if eb[:fill_color]
4101
- eb_sp_children << "<a:noFill/>" if eb[:no_fill]
4102
- if eb[:line_color] || eb[:line_width] || eb[:line_dash]
4103
- eb_ln_w = eb[:line_width] ? %( w="#{(eb[:line_width] * 12_700).to_i}") : ""
4104
- eb_ln_f = eb[:line_color] ? %(<a:solidFill>#{color_xml(eb[:line_color])}</a:solidFill>) : ""
4105
- eb_ln_d = eb[:line_dash] ? %(<a:prstDash val="#{xml_escape(eb[:line_dash])}"/>) : ""
4106
- eb_sp_children << "<a:ln#{eb_ln_w}>#{eb_ln_f}#{eb_ln_d}</a:ln>"
4107
- end
4108
- parts << "<c:spPr>#{eb_sp_children}</c:spPr>" unless eb_sp_children.empty?
4109
- parts << "</c:errBars>"
4110
- end
4111
- uses_xy = %w[scatterChart bubbleChart].include?(chart_type)
4112
- cat_tag = uses_xy ? "xVal" : "cat"
4113
- val_tag = uses_xy ? "yVal" : "val"
4114
- if ser[:cat_ref]
4115
- parts << if uses_xy || ser[:cat_ref_type] == :num
4116
- "<c:#{cat_tag}><c:numRef><c:f>#{xml_escape(ser[:cat_ref])}</c:f>#{num_cache_xml(ser[:cat_ref])}</c:numRef></c:#{cat_tag}>"
4117
- else
4118
- "<c:#{cat_tag}><c:strRef><c:f>#{xml_escape(ser[:cat_ref])}</c:f>#{str_cache_xml(ser[:cat_ref])}</c:strRef></c:#{cat_tag}>"
4119
- end
4120
- end
4121
- parts << "<c:#{val_tag}><c:numRef><c:f>#{xml_escape(ser[:val_ref])}</c:f>#{num_cache_xml(ser[:val_ref])}</c:numRef></c:#{val_tag}>" if ser[:val_ref]
4122
- parts << "<c:bubbleSize><c:numRef><c:f>#{xml_escape(ser[:bubble_size_ref])}</c:f>#{num_cache_xml(ser[:bubble_size_ref])}</c:numRef></c:bubbleSize>" if ser[:bubble_size_ref]
4123
- parts << %(<c:smooth val="#{ser[:smooth] ? 1 : 0}"/>) unless ser[:smooth].nil?
4124
- parts << %(<c:shape val="#{xml_escape(ser[:shape])}"/>) if ser[:shape]
4125
- parts << "</c:ser>"
4126
- end
4127
-
4128
- if chart[:band_fmts]
4129
- parts << "<c:bandFmts>"
4130
- chart[:band_fmts].each do |bf|
4131
- parts << "<c:bandFmt><c:idx val=\"#{bf[:idx]}\"/>"
4132
- bf_sp = +""
4133
- bf_sp << %(<a:solidFill>#{color_xml(bf[:fill_color])}</a:solidFill>) if bf[:fill_color]
4134
- bf_sp << "<a:noFill/>" if bf[:no_fill]
4135
- if bf[:line_color] || bf[:line_width] || bf[:line_dash]
4136
- bf_lw = bf[:line_width] ? %( w="#{(bf[:line_width] * 12_700).to_i}") : ""
4137
- bf_lf = bf[:line_color] ? %(<a:solidFill>#{color_xml(bf[:line_color])}</a:solidFill>) : ""
4138
- bf_ld = bf[:line_dash] ? %(<a:prstDash val="#{xml_escape(bf[:line_dash])}"/>) : ""
4139
- bf_sp << "<a:ln#{bf_lw}>#{bf_lf}#{bf_ld}</a:ln>"
4140
- end
4141
- parts << "<c:spPr>#{bf_sp}</c:spPr>" unless bf_sp.empty?
4142
- parts << "</c:bandFmt>"
4143
- end
4144
- parts << "</c:bandFmts>"
4145
- end
4146
-
4147
- parts << %(<c:gapWidth val="#{chart[:gap_width]}"/>) if chart[:gap_width]
4148
- parts << %(<c:splitType val="#{xml_escape(chart[:split_type])}"/>) if chart[:split_type]
4149
- parts << %(<c:splitPos val="#{chart[:split_pos]}"/>) if chart[:split_pos]
4150
- if chart[:cust_split]&.any?
4151
- parts << "<c:custSplit>"
4152
- chart[:cust_split].each { |idx| parts << %(<c:secondPiePt val="#{idx}"/>) }
4153
- parts << "</c:custSplit>"
4154
- end
4155
- parts << %(<c:secondPieSize val="#{chart[:second_pie_size]}"/>) if chart[:second_pie_size]
4156
- parts << %(<c:gapDepth val="#{chart[:gap_depth]}"/>) if chart[:gap_depth]
4157
- parts << %(<c:overlap val="#{chart[:overlap]}"/>) if chart[:overlap]
4158
- parts << %(<c:shape val="#{chart[:bar_shape]}"/>) if chart[:bar_shape]
4159
- b3d = chart[:bubble_3d]
4160
- parts << %(<c:bubble3D val="#{b3d ? 1 : 0}"/>) unless b3d.nil?
4161
- parts << %(<c:bubbleScale val="#{chart[:bubble_scale]}"/>) if chart[:bubble_scale]
4162
- snb = chart[:show_neg_bubbles]
4163
- parts << %(<c:showNegBubbles val="#{snb ? 1 : 0}"/>) unless snb.nil?
4164
- parts << %(<c:sizeRepresents val="#{chart[:size_represents]}"/>) if chart[:size_represents]
4165
- parts << %(<c:firstSliceAng val="#{chart[:first_slice_ang]}"/>) if chart[:first_slice_ang]
4166
- parts << %(<c:holeSize val="#{chart[:hole_size]}"/>) if chart[:hole_size]
4167
- if chart[:ser_lines]
4168
- sl_spec = chart[:ser_lines]
4169
- if sl_spec.is_a?(Hash)
4170
- sl_sp = +""
4171
- if sl_spec[:line_color] || sl_spec[:line_width] || sl_spec[:line_dash]
4172
- sl_lw = sl_spec[:line_width] ? %( w="#{(sl_spec[:line_width] * 12_700).to_i}") : ""
4173
- sl_lf = sl_spec[:line_color] ? %(<a:solidFill>#{color_xml(sl_spec[:line_color])}</a:solidFill>) : ""
4174
- sl_ld = sl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(sl_spec[:line_dash])}"/>) : ""
4175
- sl_sp << "<a:ln#{sl_lw}>#{sl_lf}#{sl_ld}</a:ln>"
4176
- end
4177
- parts << (sl_sp.empty? ? "<c:serLines/>" : "<c:serLines><c:spPr>#{sl_sp}</c:spPr></c:serLines>")
4178
- else
4179
- parts << "<c:serLines/>"
4180
- end
4181
- end
4182
- if chart[:drop_lines]
4183
- dl_spec = chart[:drop_lines]
4184
- if dl_spec.is_a?(Hash)
4185
- dl_sp = +""
4186
- if dl_spec[:line_color] || dl_spec[:line_width] || dl_spec[:line_dash]
4187
- dl_lw = dl_spec[:line_width] ? %( w="#{(dl_spec[:line_width] * 12_700).to_i}") : ""
4188
- dl_lf = dl_spec[:line_color] ? %(<a:solidFill>#{color_xml(dl_spec[:line_color])}</a:solidFill>) : ""
4189
- dl_ld = dl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(dl_spec[:line_dash])}"/>) : ""
4190
- dl_sp << "<a:ln#{dl_lw}>#{dl_lf}#{dl_ld}</a:ln>"
4191
- end
4192
- parts << (dl_sp.empty? ? "<c:dropLines/>" : "<c:dropLines><c:spPr>#{dl_sp}</c:spPr></c:dropLines>")
4193
- else
4194
- parts << "<c:dropLines/>"
4195
- end
4196
- end
4197
- if chart[:hi_low_lines]
4198
- hl_spec = chart[:hi_low_lines]
4199
- if hl_spec.is_a?(Hash)
4200
- hl_sp = +""
4201
- if hl_spec[:line_color] || hl_spec[:line_width] || hl_spec[:line_dash]
4202
- hl_lw = hl_spec[:line_width] ? %( w="#{(hl_spec[:line_width] * 12_700).to_i}") : ""
4203
- hl_lf = hl_spec[:line_color] ? %(<a:solidFill>#{color_xml(hl_spec[:line_color])}</a:solidFill>) : ""
4204
- hl_ld = hl_spec[:line_dash] ? %(<a:prstDash val="#{xml_escape(hl_spec[:line_dash])}"/>) : ""
4205
- hl_sp << "<a:ln#{hl_lw}>#{hl_lf}#{hl_ld}</a:ln>"
4206
- end
4207
- parts << (hl_sp.empty? ? "<c:hiLowLines/>" : "<c:hiLowLines><c:spPr>#{hl_sp}</c:spPr></c:hiLowLines>")
4208
- else
4209
- parts << "<c:hiLowLines/>"
4210
- end
4211
- end
4212
- if chart[:up_down_bars]
4213
- udb = chart[:up_down_bars]
4214
- parts << "<c:upDownBars>"
4215
- parts << %(<c:gapWidth val="#{udb[:gap_width]}"/>) if udb.is_a?(Hash) && udb[:gap_width]
4216
- %i[up_bars down_bars].each do |bar_key|
4217
- tag = bar_key == :up_bars ? "upBars" : "downBars"
4218
- bar = udb.is_a?(Hash) ? udb[bar_key] : nil
4219
- if bar
4220
- bar_sp = +""
4221
- bar_sp << %(<a:solidFill>#{color_xml(bar[:fill_color])}</a:solidFill>) if bar[:fill_color]
4222
- bar_sp << "<a:noFill/>" if bar[:no_fill]
4223
- if bar[:line_color] || bar[:line_width] || bar[:line_dash]
4224
- b_lw = bar[:line_width] ? %( w="#{(bar[:line_width] * 12_700).to_i}") : ""
4225
- b_lf = bar[:line_color] ? %(<a:solidFill>#{color_xml(bar[:line_color])}</a:solidFill>) : ""
4226
- b_ld = bar[:line_dash] ? %(<a:prstDash val="#{xml_escape(bar[:line_dash])}"/>) : ""
4227
- bar_sp << "<a:ln#{b_lw}>#{b_lf}#{b_ld}</a:ln>"
4228
- end
4229
- parts << (bar_sp.empty? ? "<c:#{tag}/>" : "<c:#{tag}><c:spPr>#{bar_sp}</c:spPr></c:#{tag}>")
4230
- else
4231
- parts << "<c:#{tag}/>"
4232
- end
4233
- end
4234
- parts << "</c:upDownBars>"
4235
- end
4236
- mk = chart[:marker]
4237
- parts << %(<c:marker val="#{mk ? 1 : 0}"/>) unless mk.nil?
4238
- sm = chart[:smooth]
4239
- parts << %(<c:smooth val="#{sm ? 1 : 0}"/>) unless sm.nil?
4240
- three_axes = THREE_AXIS_CHARTS.include?(chart_type)
4241
- unless no_axes
4242
- parts << '<c:axId val="1"/><c:axId val="2"/>'
4243
- parts << '<c:axId val="3"/>' if three_axes
4244
- end
4245
- parts << "</c:#{chart_type}>"
4246
-
4247
- unless no_axes
4248
- cat_del = chart[:cat_axis_delete] ? 1 : 0
4249
- cat_orient = chart[:cat_axis_orientation] || "minMax"
4250
- cat_ax_tag = chart[:cat_axis_type] == :date ? "dateAx" : "catAx"
4251
- parts << %(<c:#{cat_ax_tag}><c:axId val="1"/><c:scaling>)
4252
- parts << %(<c:logBase val="#{chart[:cat_axis_log_base]}"/>) if chart[:cat_axis_log_base]
4253
- parts << %(<c:orientation val="#{cat_orient}"/>)
4254
- parts << %(<c:max val="#{chart[:cat_axis_scaling_max]}"/>) if chart[:cat_axis_scaling_max]
4255
- parts << %(<c:min val="#{chart[:cat_axis_scaling_min]}"/>) if chart[:cat_axis_scaling_min]
4256
- parts << %(</c:scaling><c:delete val="#{cat_del}"/><c:axPos val="#{chart[:cat_axis_pos] || "b"}"/>)
4257
- parts << gridlines_xml("majorGridlines", chart[:cat_axis_major_gridlines])
4258
- parts << gridlines_xml("minorGridlines", chart[:cat_axis_minor_gridlines])
4259
- if chart[:cat_axis_title]
4260
- cat_title_spec = merge_flat_title_styling(chart[:cat_axis_title], chart, :cat_axis_title)
4261
- parts << build_chart_title_xml(cat_title_spec)
4262
- end
4263
- if (cnf = chart[:cat_axis_num_fmt])
4264
- sl = cnf[:source_linked] ? 1 : 0
4265
- parts << %(<c:numFmt formatCode="#{xml_escape(cnf[:format_code])}" sourceLinked="#{sl}"/>)
4266
- end
4267
- parts << %(<c:majorTickMark val="#{chart[:cat_axis_major_tick_mark]}"/>) if chart[:cat_axis_major_tick_mark]
4268
- parts << %(<c:minorTickMark val="#{chart[:cat_axis_minor_tick_mark]}"/>) if chart[:cat_axis_minor_tick_mark]
4269
- parts << %(<c:tickLblPos val="#{chart[:cat_axis_tick_lbl_pos]}"/>) if chart[:cat_axis_tick_lbl_pos]
4270
- if chart[:cat_axis_line_color] || chart[:cat_axis_fill] || chart[:cat_axis_no_fill] || chart[:cat_axis_line_dash]
4271
- parts << "<c:spPr>"
4272
- parts << "<a:noFill/>" if chart[:cat_axis_no_fill]
4273
- parts << %(<a:solidFill>#{color_xml(chart[:cat_axis_fill])}</a:solidFill>) if chart[:cat_axis_fill]
4274
- if chart[:cat_axis_line_color] || chart[:cat_axis_line_dash]
4275
- w_attr = chart[:cat_axis_line_width] ? %( w="#{chart[:cat_axis_line_width]}") : ""
4276
- ca_ln_f = chart[:cat_axis_line_color] ? %(<a:solidFill>#{srgb_clr_xml(chart[:cat_axis_line_color])}</a:solidFill>) : ""
4277
- ca_ln_d = chart[:cat_axis_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:cat_axis_line_dash])}"/>) : ""
4278
- parts << "<a:ln#{w_attr}>#{ca_ln_f}#{ca_ln_d}</a:ln>"
4279
- end
4280
- parts << "</c:spPr>"
4281
- end
4282
- parts << build_axis_txpr(chart[:cat_axis_label_rotation], chart[:cat_axis_font])
4283
- parts << '<c:crossAx val="2"/>'
4284
- parts << %(<c:crosses val="#{chart[:cat_axis_crosses]}"/>) if chart[:cat_axis_crosses]
4285
- parts << %(<c:crossesAt val="#{chart[:cat_axis_crosses_at]}"/>) if !chart[:cat_axis_crosses] && chart[:cat_axis_crosses_at]
4286
- ca_auto = chart[:cat_axis_auto]
4287
- parts << %(<c:auto val="#{ca_auto ? 1 : 0}"/>) unless ca_auto.nil?
4288
- parts << %(<c:lblAlgn val="#{xml_escape(chart[:cat_axis_lbl_algn])}"/>) if chart[:cat_axis_lbl_algn]
4289
- parts << %(<c:lblOffset val="#{chart[:cat_axis_lbl_offset]}"/>) if chart[:cat_axis_lbl_offset]
4290
- parts << %(<c:tickLblSkip val="#{chart[:cat_axis_tick_lbl_skip]}"/>) if chart[:cat_axis_tick_lbl_skip]
4291
- parts << %(<c:tickMarkSkip val="#{chart[:cat_axis_tick_mark_skip]}"/>) if chart[:cat_axis_tick_mark_skip]
4292
- nml = chart[:cat_axis_no_multi_lvl_lbl]
4293
- parts << %(<c:noMultiLvlLbl val="#{nml ? 1 : 0}"/>) unless nml.nil?
4294
- if cat_ax_tag == "dateAx"
4295
- parts << %(<c:baseTimeUnit val="#{chart[:cat_axis_base_time_unit]}"/>) if chart[:cat_axis_base_time_unit]
4296
- parts << %(<c:majorUnit val="#{chart[:cat_axis_major_unit]}"/>) if chart[:cat_axis_major_unit]
4297
- parts << %(<c:majorTimeUnit val="#{chart[:cat_axis_major_time_unit]}"/>) if chart[:cat_axis_major_time_unit]
4298
- parts << %(<c:minorUnit val="#{chart[:cat_axis_minor_unit]}"/>) if chart[:cat_axis_minor_unit]
4299
- parts << %(<c:minorTimeUnit val="#{chart[:cat_axis_minor_time_unit]}"/>) if chart[:cat_axis_minor_time_unit]
4300
- end
4301
- parts << "</c:#{cat_ax_tag}>"
4302
- val_del = chart[:val_axis_delete] ? 1 : 0
4303
- val_orient = chart[:val_axis_orientation] || "minMax"
4304
- parts << %(<c:valAx><c:axId val="2"/><c:scaling>)
4305
- parts << %(<c:logBase val="#{chart[:val_axis_log_base]}"/>) if chart[:val_axis_log_base]
4306
- parts << %(<c:orientation val="#{val_orient}"/>)
4307
- parts << %(<c:max val="#{chart[:val_axis_scaling_max]}"/>) if chart[:val_axis_scaling_max]
4308
- parts << %(<c:min val="#{chart[:val_axis_scaling_min]}"/>) if chart[:val_axis_scaling_min]
4309
- parts << %(</c:scaling><c:delete val="#{val_del}"/><c:axPos val="#{chart[:val_axis_pos] || "l"}"/>)
4310
- parts << gridlines_xml("majorGridlines", chart[:val_axis_major_gridlines])
4311
- parts << gridlines_xml("minorGridlines", chart[:val_axis_minor_gridlines])
4312
- if chart[:val_axis_title]
4313
- val_title_spec = merge_flat_title_styling(chart[:val_axis_title], chart, :val_axis_title)
4314
- parts << build_chart_title_xml(val_title_spec)
4315
- end
4316
- if (vnf = chart[:val_axis_num_fmt])
4317
- sl = vnf[:source_linked] ? 1 : 0
4318
- parts << %(<c:numFmt formatCode="#{xml_escape(vnf[:format_code])}" sourceLinked="#{sl}"/>)
4319
- end
4320
- parts << %(<c:majorTickMark val="#{chart[:val_axis_major_tick_mark]}"/>) if chart[:val_axis_major_tick_mark]
4321
- parts << %(<c:minorTickMark val="#{chart[:val_axis_minor_tick_mark]}"/>) if chart[:val_axis_minor_tick_mark]
4322
- parts << %(<c:tickLblPos val="#{chart[:val_axis_tick_lbl_pos]}"/>) if chart[:val_axis_tick_lbl_pos]
4323
- if chart[:val_axis_line_color] || chart[:val_axis_fill] || chart[:val_axis_no_fill] || chart[:val_axis_line_dash]
4324
- parts << "<c:spPr>"
4325
- parts << "<a:noFill/>" if chart[:val_axis_no_fill]
4326
- parts << %(<a:solidFill>#{color_xml(chart[:val_axis_fill])}</a:solidFill>) if chart[:val_axis_fill]
4327
- if chart[:val_axis_line_color] || chart[:val_axis_line_dash]
4328
- w_attr = chart[:val_axis_line_width] ? %( w="#{chart[:val_axis_line_width]}") : ""
4329
- va_ln_f = chart[:val_axis_line_color] ? %(<a:solidFill>#{srgb_clr_xml(chart[:val_axis_line_color])}</a:solidFill>) : ""
4330
- va_ln_d = chart[:val_axis_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:val_axis_line_dash])}"/>) : ""
4331
- parts << "<a:ln#{w_attr}>#{va_ln_f}#{va_ln_d}</a:ln>"
4332
- end
4333
- parts << "</c:spPr>"
4334
- end
4335
- parts << build_axis_txpr(chart[:val_axis_label_rotation], chart[:val_axis_font])
4336
- parts << '<c:crossAx val="1"/>'
4337
- parts << %(<c:crosses val="#{chart[:val_axis_crosses]}"/>) if chart[:val_axis_crosses]
4338
- parts << %(<c:crossesAt val="#{chart[:val_axis_crosses_at]}"/>) if !chart[:val_axis_crosses] && chart[:val_axis_crosses_at]
4339
- parts << %(<c:crossBetween val="#{chart[:val_axis_cross_between]}"/>) if chart[:val_axis_cross_between]
4340
- parts << %(<c:majorUnit val="#{chart[:val_axis_major_unit]}"/>) if chart[:val_axis_major_unit]
4341
- parts << %(<c:minorUnit val="#{chart[:val_axis_minor_unit]}"/>) if chart[:val_axis_minor_unit]
4342
- if chart[:val_axis_disp_units]
4343
- du = chart[:val_axis_disp_units]
4344
- if du.is_a?(Hash)
4345
- parts << "<c:dispUnits>"
4346
- parts << %(<c:builtInUnit val="#{xml_escape(du[:built_in_unit])}"/>) if du[:built_in_unit]
4347
- parts << %(<c:custUnit val="#{du[:cust_unit]}"/>) if du[:cust_unit]
4348
- if du[:label]
4349
- dul = du[:label]
4350
- parts << "<c:dispUnitsLbl>"
4351
- if dul[:num_fmt]
4352
- nf = dul[:num_fmt]
4353
- nf_src = nf.is_a?(Hash) ? nf : { format_code: nf }
4354
- nf_linked = if nf_src.key?(:source_linked)
4355
- nf_src[:source_linked] ? 1 : 0
4356
- else
4357
- 0
4358
- end
4359
- parts << %(<c:numFmt formatCode="#{xml_escape(nf_src[:format_code])}" sourceLinked="#{nf_linked}"/>)
4360
- end
4361
- dul_sp = +""
4362
- dul_sp << %(<a:solidFill>#{color_xml(dul[:fill_color])}</a:solidFill>) if dul[:fill_color]
4363
- dul_sp << "<a:noFill/>" if dul[:no_fill]
4364
- if dul[:line_color] || dul[:line_width] || dul[:line_dash]
4365
- dul_lw = dul[:line_width] ? %( w="#{(dul[:line_width] * 12_700).to_i}") : ""
4366
- dul_lf = dul[:line_color] ? %(<a:solidFill>#{color_xml(dul[:line_color])}</a:solidFill>) : ""
4367
- dul_ld = dul[:line_dash] ? %(<a:prstDash val="#{xml_escape(dul[:line_dash])}"/>) : ""
4368
- dul_sp << "<a:ln#{dul_lw}>#{dul_lf}#{dul_ld}</a:ln>"
4369
- end
4370
- parts << "<c:spPr>#{dul_sp}</c:spPr>" unless dul_sp.empty?
4371
- parts << build_axis_txpr(nil, dul[:font]) if dul[:font]
4372
- parts << "</c:dispUnitsLbl>"
4373
- end
4374
- parts << "</c:dispUnits>"
4375
- else
4376
- parts << %(<c:dispUnits><c:builtInUnit val="#{xml_escape(du.to_s)}"/></c:dispUnits>)
4377
- end
4378
- end
4379
- parts << "</c:valAx>"
4380
-
4381
- if three_axes
4382
- parts << '<c:serAx><c:axId val="3"/><c:scaling><c:orientation val="minMax"/></c:scaling>'
4383
- parts << '<c:delete val="0"/><c:axPos val="b"/><c:crossAx val="2"/></c:serAx>'
4384
- end
4385
- end
4386
-
4387
- if chart[:data_table]
4388
- dt = chart[:data_table]
4389
- parts << "<c:dTable>"
4390
- parts << %(<c:showHorzBorder val="#{dt[:show_horz_border] ? 1 : 0}"/>) unless dt[:show_horz_border].nil?
4391
- parts << %(<c:showVertBorder val="#{dt[:show_vert_border] ? 1 : 0}"/>) unless dt[:show_vert_border].nil?
4392
- parts << %(<c:showOutline val="#{dt[:show_outline] ? 1 : 0}"/>) unless dt[:show_outline].nil?
4393
- parts << %(<c:showKeys val="#{dt[:show_keys] ? 1 : 0}"/>) unless dt[:show_keys].nil?
4394
- dt_sp_children = +""
4395
- dt_sp_children << %(<a:solidFill>#{color_xml(dt[:fill_color])}</a:solidFill>) if dt[:fill_color]
4396
- dt_sp_children << "<a:noFill/>" if dt[:no_fill]
4397
- if dt[:line_color] || dt[:line_width] || dt[:line_dash]
4398
- dt_ln_w = dt[:line_width] ? %( w="#{(dt[:line_width] * 12_700).to_i}") : ""
4399
- dt_ln_f = dt[:line_color] ? %(<a:solidFill>#{color_xml(dt[:line_color])}</a:solidFill>) : ""
4400
- dt_ln_d = dt[:line_dash] ? %(<a:prstDash val="#{xml_escape(dt[:line_dash])}"/>) : ""
4401
- dt_sp_children << "<a:ln#{dt_ln_w}>#{dt_ln_f}#{dt_ln_d}</a:ln>"
4402
- end
4403
- parts << "<c:spPr>#{dt_sp_children}</c:spPr>" unless dt_sp_children.empty?
4404
- parts << build_axis_txpr(nil, dt[:font]) if dt[:font]
4405
- parts << "</c:dTable>"
4406
- end
4407
-
4408
- if chart[:plot_area_fill] || chart[:plot_area_no_fill] || chart[:plot_area_line_color] || chart[:plot_area_line_dash]
4409
- pa_sp = +""
4410
- pa_sp << %(<a:solidFill>#{color_xml(chart[:plot_area_fill])}</a:solidFill>) if chart[:plot_area_fill]
4411
- pa_sp << "<a:noFill/>" if chart[:plot_area_no_fill]
4412
- if chart[:plot_area_line_color] || chart[:plot_area_line_dash]
4413
- pa_ln_w = chart[:plot_area_line_width] ? %( w="#{chart[:plot_area_line_width].to_i}") : ""
4414
- pa_ln_f = chart[:plot_area_line_color] ? %(<a:solidFill>#{color_xml(chart[:plot_area_line_color])}</a:solidFill>) : ""
4415
- pa_ln_d = chart[:plot_area_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:plot_area_line_dash])}"/>) : ""
4416
- pa_sp << "<a:ln#{pa_ln_w}>#{pa_ln_f}#{pa_ln_d}</a:ln>"
4417
- end
4418
- parts << "<c:spPr>#{pa_sp}</c:spPr>"
4419
- end
4420
-
4421
- parts << "</c:plotArea>"
4422
- legend_pos = chart.dig(:legend, :position) || "r"
4423
- legend_overlay = chart.dig(:legend, :overlay)
4424
- parts << %(<c:legend><c:legendPos val="#{legend_pos}"/>)
4425
- legend_entries = chart.dig(:legend, :entries)
4426
- legend_entries&.each do |entry|
4427
- parts << %(<c:legendEntry><c:idx val="#{entry[:idx]}"/>)
4428
- if !entry[:delete].nil?
4429
- parts << %(<c:delete val="#{entry[:delete] ? 1 : 0}"/>)
4430
- elsif entry[:font]
4431
- parts << build_axis_txpr(nil, entry[:font])
4432
- end
4433
- parts << "</c:legendEntry>"
4434
- end
4435
- legend_layout = chart.dig(:legend, :layout)
4436
- if legend_layout.is_a?(Hash)
4437
- ml_parts = +""
4438
- ml_parts << %(<c:layoutTarget val="#{xml_escape(legend_layout[:target])}"/>) if legend_layout[:target]
4439
- ml_parts << %(<c:xMode val="#{xml_escape(legend_layout[:x_mode])}"/>) if legend_layout[:x_mode]
4440
- ml_parts << %(<c:yMode val="#{xml_escape(legend_layout[:y_mode])}"/>) if legend_layout[:y_mode]
4441
- ml_parts << %(<c:wMode val="#{xml_escape(legend_layout[:w_mode])}"/>) if legend_layout[:w_mode]
4442
- ml_parts << %(<c:hMode val="#{xml_escape(legend_layout[:h_mode])}"/>) if legend_layout[:h_mode]
4443
- ml_parts << %(<c:x val="#{legend_layout[:x]}"/>) if legend_layout[:x]
4444
- ml_parts << %(<c:y val="#{legend_layout[:y]}"/>) if legend_layout[:y]
4445
- ml_parts << %(<c:w val="#{legend_layout[:w]}"/>) if legend_layout[:w]
4446
- ml_parts << %(<c:h val="#{legend_layout[:h]}"/>) if legend_layout[:h]
4447
- parts << "<c:layout><c:manualLayout>#{ml_parts}</c:manualLayout></c:layout>" unless ml_parts.empty?
4448
- end
4449
- parts << %(<c:overlay val="#{legend_overlay ? 1 : 0}"/>) unless legend_overlay.nil?
4450
- leg_sp_children = +""
4451
- leg_fill = chart.dig(:legend, :fill_color)
4452
- leg_sp_children << %(<a:solidFill>#{color_xml(leg_fill)}</a:solidFill>) if leg_fill
4453
- leg_no_fill = chart.dig(:legend, :no_fill)
4454
- leg_sp_children << "<a:noFill/>" if leg_no_fill
4455
- leg_lc = chart.dig(:legend, :line_color)
4456
- leg_lw = chart.dig(:legend, :line_width)
4457
- leg_ld = chart.dig(:legend, :line_dash)
4458
- if leg_lc || leg_lw || leg_ld
4459
- lw_attr = leg_lw ? %( w="#{(leg_lw * 12_700).to_i}") : ""
4460
- lf = leg_lc ? %(<a:solidFill>#{color_xml(leg_lc)}</a:solidFill>) : ""
4461
- ld = leg_ld ? %(<a:prstDash val="#{xml_escape(leg_ld)}"/>) : ""
4462
- leg_sp_children << "<a:ln#{lw_attr}>#{lf}#{ld}</a:ln>"
4463
- end
4464
- parts << "<c:spPr>#{leg_sp_children}</c:spPr>" unless leg_sp_children.empty?
4465
- if (lfont = chart.dig(:legend, :font))
4466
- parts << build_axis_txpr(nil, lfont)
4467
- end
4468
- parts << %(</c:legend>)
4469
- pvo = chart[:plot_vis_only]
4470
- parts << %(<c:plotVisOnly val="#{pvo ? 1 : 0}"/>) unless pvo.nil?
4471
- parts << %(<c:dispBlanksAs val="#{chart[:disp_blanks_as]}"/>) if chart[:disp_blanks_as]
4472
- sdom = chart[:show_d_lbls_over_max]
4473
- parts << %(<c:showDLblsOverMax val="#{sdom ? 1 : 0}"/>) unless sdom.nil?
4474
- parts << "</c:chart>"
4475
- if chart[:chart_fill] || chart[:chart_no_fill] || chart[:chart_line_color] || chart[:chart_line_dash]
4476
- cs_sp = +""
4477
- cs_sp << %(<a:solidFill>#{color_xml(chart[:chart_fill])}</a:solidFill>) if chart[:chart_fill]
4478
- cs_sp << "<a:noFill/>" if chart[:chart_no_fill]
4479
- if chart[:chart_line_color] || chart[:chart_line_dash]
4480
- cs_lw = chart[:chart_line_width] ? %( w="#{(chart[:chart_line_width] * 12_700).to_i}") : ""
4481
- cs_ln_f = chart[:chart_line_color] ? %(<a:solidFill>#{color_xml(chart[:chart_line_color])}</a:solidFill>) : ""
4482
- cs_ln_d = chart[:chart_line_dash] ? %(<a:prstDash val="#{xml_escape(chart[:chart_line_dash])}"/>) : ""
4483
- cs_sp << "<a:ln#{cs_lw}>#{cs_ln_f}#{cs_ln_d}</a:ln>"
4484
- end
4485
- parts << "<c:spPr>#{cs_sp}</c:spPr>"
4486
- end
4487
- parts << build_axis_txpr(nil, chart[:chart_font]) if chart[:chart_font]
4488
- if (ps = chart[:print_settings])
4489
- parts << "<c:printSettings>"
4490
- if (hf = ps[:header_footer])
4491
- parts << "<c:headerFooter>"
4492
- parts << "<c:oddHeader>#{xml_escape(hf[:odd_header])}</c:oddHeader>" if hf[:odd_header]
4493
- parts << "<c:oddFooter>#{xml_escape(hf[:odd_footer])}</c:oddFooter>" if hf[:odd_footer]
4494
- parts << "<c:evenHeader>#{xml_escape(hf[:even_header])}</c:evenHeader>" if hf[:even_header]
4495
- parts << "<c:evenFooter>#{xml_escape(hf[:even_footer])}</c:evenFooter>" if hf[:even_footer]
4496
- parts << "<c:firstHeader>#{xml_escape(hf[:first_header])}</c:firstHeader>" if hf[:first_header]
4497
- parts << "<c:firstFooter>#{xml_escape(hf[:first_footer])}</c:firstFooter>" if hf[:first_footer]
4498
- parts << "</c:headerFooter>"
4499
- end
4500
- if (pm = ps[:page_margins])
4501
- pm_attrs = %w[b l r t header footer].filter_map { |a| pm[a.to_sym] ? %( #{a}="#{pm[a.to_sym]}") : nil }.join
4502
- parts << "<c:pageMargins#{pm_attrs}/>"
4503
- end
4504
- if (psu = ps[:page_setup])
4505
- psu_parts = +""
4506
- psu_parts << %( paperSize="#{psu[:paper_size]}") if psu[:paper_size]
4507
- psu_parts << %( firstPageNumber="#{psu[:first_page_number]}") if psu[:first_page_number]
4508
- psu_parts << %( orientation="#{xml_escape(psu[:orientation])}") if psu[:orientation]
4509
- psu_parts << %( horizontalDpi="#{psu[:horizontal_dpi]}") if psu[:horizontal_dpi]
4510
- psu_parts << %( verticalDpi="#{psu[:vertical_dpi]}") if psu[:vertical_dpi]
4511
- psu_parts << %( copies="#{psu[:copies]}") if psu[:copies]
4512
- parts << "<c:pageSetup#{psu_parts}/>"
4513
- end
4514
- parts << "</c:printSettings>"
4515
- end
4516
- parts << "</c:chartSpace>"
4517
- parts.join
4518
- end
4519
-
4520
- # : (untyped sheet_comments) -> untyped
4521
- def generate_comments_xml(sheet_comments)
4522
- authors = sheet_comments.map { |c| c[:author] }.uniq
4523
- parts = [
4524
- XML_HEADER,
4525
- %(<comments xmlns="#{SSML_NS}">),
4526
- "<authors>"
4527
- ]
4528
- authors.each { |a| parts << "<author>#{xml_escape(a)}</author>" }
4529
- parts << "</authors><commentList>"
4530
- sheet_comments.each do |c|
4531
- aid = authors.index(c[:author]) || 0
4532
- text_xml = if c[:text].is_a?(Xlsxrb::Elements::RichText)
4533
- rich_text_xml(c[:text])
4534
- else
4535
- "<r><t>#{xml_escape(c[:text])}</t></r>"
4536
- end
4537
- comment_attrs = %(ref="#{c[:ref]}" authorId="#{aid}")
4538
- comment_attrs << %( guid="#{c[:guid]}") if c[:guid]
4539
- comment_attrs << %( shapeId="#{c[:shape_id]}") if c[:shape_id]
4540
- parts << "<comment #{comment_attrs}><text>#{text_xml}</text></comment>"
4541
- end
4542
- parts << "</commentList></comments>"
4543
- parts.join
4544
- end
4545
-
4546
- # : (untyped sheet_comments) -> untyped
4547
- def generate_vml_drawing_xml(sheet_comments)
4548
- parts = [
4549
- '<xml xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">',
4550
- '<o:shapelayout v:ext="edit"><o:idmap v:ext="edit" data="1"/></o:shapelayout>',
4551
- '<v:shapetype id="_x0000_t202" coordsize="21600,21600" o:spt="202" path="m,l,21600r21600,l21600,xe">',
4552
- '<v:stroke joinstyle="miter"/><v:path gradientshapeok="t" o:connecttype="rect"/>',
4553
- "</v:shapetype>"
4554
- ]
4555
- sheet_comments.each_with_index do |c, idx|
4556
- col, row = cell_to_col_row(c[:ref])
4557
- shape_id = 1025 + idx
4558
- parts << %(<v:shape id="_x0000_s#{shape_id}" type="#_x0000_t202" style="position:absolute;margin-left:59.25pt;margin-top:1.5pt;width:108pt;height:59.25pt;z-index:#{idx + 1};visibility:hidden" fillcolor="#ffffe1" o:insetmode="auto">)
4559
- parts << '<v:fill color2="#ffffe1"/>'
4560
- parts << '<v:shadow on="t" color="black" obscured="t"/>'
4561
- parts << '<v:path o:connecttype="none"/>'
4562
- parts << '<v:textbox style="mso-direction-alt:auto"><div style="text-align:left"></div></v:textbox>'
4563
- parts << '<x:ClientData ObjectType="Note">'
4564
- parts << "<x:MoveWithCells/><x:SizeWithCells/>"
4565
- parts << "<x:Anchor>#{col + 1}, 15, #{row}, 10, #{col + 3}, 15, #{row + 4}, 4</x:Anchor>"
4566
- parts << "<x:AutoFill>False</x:AutoFill>"
4567
- parts << "<x:Row>#{row}</x:Row>"
4568
- parts << "<x:Column>#{col}</x:Column>"
4569
- parts << "</x:ClientData></v:shape>"
4570
- end
4571
- parts << "</xml>"
4572
- parts.join
4573
- end
4574
-
4575
- # : (untyped cell_ref) -> (::Array[0] | ::Array[untyped])
4576
- def cell_to_col_row(cell_ref)
4577
- m = cell_ref.match(/\A([A-Z]+)(\d+)\z/)
4578
- return [0, 0] unless m
4579
-
4580
- col = column_letter_to_index(m[1]) - 1
4581
- row = m[2].to_i - 1
4582
- [col, row]
4583
- end
4584
-
4585
- # : (untyped pivot_table, untyped cache_id) -> untyped
4586
- def generate_pivot_table_xml(pivot_table, cache_id)
4587
- data_caption = pivot_table[:data_caption] || (pivot_table[:data_fields].first ? pivot_table[:data_fields].first[:name] : "Values")
4588
- pt_attrs = %( xmlns="#{SSML_NS}" name="#{xml_escape(pivot_table[:name])}" cacheId="#{cache_id}" dataCaption="#{xml_escape(data_caption)}")
4589
- pt_attrs << %( grandTotalCaption="#{xml_escape(pivot_table[:grand_total_caption])}") if pivot_table[:grand_total_caption]
4590
- pt_attrs << %( errorCaption="#{xml_escape(pivot_table[:error_caption])}") if pivot_table[:error_caption]
4591
- pt_attrs << ' showError="1"' if pivot_table[:show_error]
4592
- pt_attrs << %( missingCaption="#{xml_escape(pivot_table[:missing_caption])}") if pivot_table[:missing_caption]
4593
- pt_attrs << ' showMissing="0"' if pivot_table[:show_missing] == false
4594
- pt_attrs << %( tag="#{xml_escape(pivot_table[:tag])}") if pivot_table[:tag]
4595
- pt_attrs << %( dataOnRows="1") if pivot_table[:data_on_rows]
4596
- pt_attrs << %( dataOnRows="0") unless pivot_table[:data_on_rows]
4597
- pt_attrs << %( rowGrandTotals="0") if pivot_table[:row_grand_totals] == false
4598
- pt_attrs << %( colGrandTotals="0") if pivot_table[:col_grand_totals] == false
4599
- pt_attrs << %( compact="0") if pivot_table[:compact] == false
4600
- pt_attrs << %( outline="0") if pivot_table[:outline] == false
4601
- pt_attrs << %( outlineData="1") if pivot_table[:outline_data]
4602
- pt_attrs << %( compactData="0") if pivot_table[:compact_data] == false
4603
- pt_attrs << %( showHeaders="0") if pivot_table[:show_headers] == false
4604
- pt_attrs << %( showMultipleLabel="0") if pivot_table[:show_multiple_label] == false
4605
- pt_attrs << %( showDataDropDown="0") if pivot_table[:show_data_drop_down] == false
4606
- pt_attrs << %( indent="#{pivot_table[:indent]}") if pivot_table[:indent]
4607
- pt_attrs << ' published="1"' if pivot_table[:published]
4608
- pt_attrs << ' editData="1"' if pivot_table[:edit_data]
4609
- pt_attrs << ' disableFieldList="1"' if pivot_table[:disable_field_list]
4610
- pt_attrs << ' visualTotals="0"' if pivot_table[:visual_totals] == false
4611
- pt_attrs << ' printDrill="1"' if pivot_table[:print_drill]
4612
- pt_attrs << %( createdVersion="#{pivot_table[:created_version]}") if pivot_table[:created_version]
4613
- pt_attrs << %( updatedVersion="#{pivot_table[:updated_version]}") if pivot_table[:updated_version]
4614
- pt_attrs << %( minRefreshableVersion="#{pivot_table[:min_refreshable_version]}") if pivot_table[:min_refreshable_version]
4615
- anf = pivot_table.fetch(:apply_number_formats, false)
4616
- abf = pivot_table.fetch(:apply_border_formats, false)
4617
- aff = pivot_table.fetch(:apply_font_formats, false)
4618
- apf = pivot_table.fetch(:apply_pattern_formats, false)
4619
- aaf = pivot_table.fetch(:apply_alignment_formats, false)
4620
- awf = pivot_table.fetch(:apply_width_height_formats, true)
4621
- pt_attrs << %( applyNumberFormats="#{anf ? 1 : 0}")
4622
- pt_attrs << %( applyBorderFormats="#{abf ? 1 : 0}")
4623
- pt_attrs << %( applyFontFormats="#{aff ? 1 : 0}")
4624
- pt_attrs << %( applyPatternFormats="#{apf ? 1 : 0}")
4625
- pt_attrs << %( applyAlignmentFormats="#{aaf ? 1 : 0}")
4626
- pt_attrs << %( applyWidthHeightFormats="#{awf ? 1 : 0}")
4627
- pt_attrs << ' multipleFieldFilters="0"' if pivot_table[:multiple_field_filters] == false
4628
- pt_attrs << ' showDrill="0"' if pivot_table[:show_drill] == false
4629
- pt_attrs << ' showDataTips="0"' if pivot_table[:show_data_tips] == false
4630
- pt_attrs << ' enableDrill="0"' if pivot_table[:enable_drill] == false
4631
- pt_attrs << ' showMemberPropertyTips="0"' if pivot_table[:show_member_property_tips] == false
4632
- pt_attrs << ' itemPrintTitles="1"' if pivot_table[:item_print_titles]
4633
- pt_attrs << ' fieldPrintTitles="1"' if pivot_table[:field_print_titles]
4634
- pt_attrs << ' preserveFormatting="0"' if pivot_table[:preserve_formatting] == false
4635
- pt_attrs << ' pageOverThenDown="1"' if pivot_table[:page_over_then_down]
4636
- pt_attrs << %( pageWrap="#{pivot_table[:page_wrap]}") if pivot_table[:page_wrap]
4637
- parts = [
4638
- XML_HEADER,
4639
- "<pivotTableDefinition#{pt_attrs}>"
4640
- ]
4641
-
4642
- # Compute field count from source range or explicit field_names.
4643
- field_count = if pivot_table[:field_names]
4644
- pivot_table[:field_names].size
4645
- else
4646
- (pivot_table[:row_fields].size + pivot_table[:col_fields].size + pivot_table[:data_fields].size).clamp(1, 100)
4647
- end
4648
- loc_attrs = %(<location ref="#{pivot_table[:dest_ref]}" firstHeaderRow="1" firstDataRow="1" firstDataCol="1")
4649
- loc_attrs << %( rowPageCount="#{pivot_table[:row_page_count]}") if pivot_table[:row_page_count]
4650
- loc_attrs << %( colPageCount="#{pivot_table[:col_page_count]}") if pivot_table[:col_page_count]
4651
- loc_attrs << "/>"
4652
- parts << loc_attrs
4653
- parts << %(<pivotFields count="#{field_count}">)
4654
- field_count.times do |fi|
4655
- attrs = +""
4656
- if pivot_table[:row_fields].include?(fi)
4657
- attrs << ' axis="axisRow"'
4658
- elsif pivot_table[:col_fields].include?(fi)
4659
- attrs << ' axis="axisCol"'
4660
- end
4661
- attrs << ' dataField="1"' if pivot_table[:data_fields].any? { |df| df[:fld] == fi }
4662
- fa = pivot_table[:field_attrs] && pivot_table[:field_attrs][fi]
4663
- attrs << %( compact="#{fa[:compact] ? "1" : "0"}") if fa && !fa[:compact].nil?
4664
- attrs << %( outline="#{fa[:outline] ? "1" : "0"}") if fa && !fa[:outline].nil?
4665
- attrs << %( subtotalTop="#{fa[:subtotal_top] ? "1" : "0"}") if fa && !fa[:subtotal_top].nil?
4666
- attrs << %( showAll="#{fa && fa[:show_all] == true ? "1" : "0"}")
4667
- attrs << %( numFmtId="#{fa[:num_fmt_id]}") if fa && fa[:num_fmt_id]
4668
- attrs << %( sortType="#{xml_escape(fa[:sort_type])}") if fa && fa[:sort_type]
4669
- attrs << ' defaultSubtotal="0"' if fa && fa[:default_subtotal] == false
4670
- attrs << ' insertBlankRow="1"' if fa && fa[:insert_blank_row]
4671
- attrs << ' insertPageBreak="1"' if fa && fa[:insert_page_break]
4672
- attrs << ' includeNewItemsInFilter="1"' if fa && fa[:include_new_items_in_filter]
4673
-
4674
- field_items = pivot_table[:items] && pivot_table[:items][fi]
4675
- if field_items
4676
- parts << "<pivotField#{attrs}>"
4677
- parts << %(<items count="#{field_items.size + 1}">)
4678
- field_items.size.times { |ix| parts << %(<item x="#{ix}"/>) }
4679
- parts << '<item t="default"/>'
4680
- parts << "</items>"
4681
- parts << "</pivotField>"
4682
- else
4683
- parts << "<pivotField#{attrs}/>"
4684
- end
4685
- end
4686
- parts << "</pivotFields>"
4687
-
4688
- unless pivot_table[:row_fields].empty?
4689
- parts << %(<rowFields count="#{pivot_table[:row_fields].size}">)
4690
- pivot_table[:row_fields].each { |f| parts << %(<field x="#{f}"/>) }
4691
- parts << "</rowFields>"
4692
- end
4693
-
4694
- unless pivot_table[:col_fields].empty?
4695
- parts << %(<colFields count="#{pivot_table[:col_fields].size}">)
4696
- pivot_table[:col_fields].each { |f| parts << %(<field x="#{f}"/>) }
4697
- parts << "</colFields>"
4698
- end
4699
-
4700
- unless pivot_table[:data_fields].empty?
4701
- parts << %(<dataFields count="#{pivot_table[:data_fields].size}">)
4702
- pivot_table[:data_fields].each do |df|
4703
- df_attrs = %( name="#{xml_escape(df[:name])}" fld="#{df[:fld]}" subtotal="#{df[:subtotal] || "sum"}")
4704
- df_attrs << %( showDataAs="#{xml_escape(df[:show_data_as])}") if df[:show_data_as]
4705
- df_attrs << %( baseField="#{df[:base_field]}") if df[:base_field]
4706
- df_attrs << %( baseItem="#{df[:base_item]}") if df[:base_item]
4707
- df_attrs << %( numFmtId="#{df[:num_fmt_id]}") if df[:num_fmt_id]
4708
- parts << "<dataField#{df_attrs}/>"
4709
- end
4710
- parts << "</dataFields>"
4711
- end
4712
-
4713
- if pivot_table[:pivot_table_style]
4714
- psi = pivot_table[:pivot_table_style]
4715
- psi_attrs = +""
4716
- psi_attrs << %( name="#{xml_escape(psi[:name])}") if psi[:name]
4717
- psi_attrs << %( showRowHeaders="#{psi[:show_row_headers] ? "1" : "0"}") unless psi[:show_row_headers].nil?
4718
- psi_attrs << %( showColHeaders="#{psi[:show_col_headers] ? "1" : "0"}") unless psi[:show_col_headers].nil?
4719
- psi_attrs << %( showRowStripes="#{psi[:show_row_stripes] ? "1" : "0"}") unless psi[:show_row_stripes].nil?
4720
- psi_attrs << %( showColStripes="#{psi[:show_col_stripes] ? "1" : "0"}") unless psi[:show_col_stripes].nil?
4721
- psi_attrs << %( showLastColumn="#{psi[:show_last_column] ? "1" : "0"}") unless psi[:show_last_column].nil?
4722
- parts << "<pivotTableStyleInfo#{psi_attrs}/>"
4723
- end
4724
-
4725
- parts << "</pivotTableDefinition>"
4726
- parts.join
4727
- end
4728
-
4729
- # : (untyped pivot_table, untyped _cache_id) -> untyped
4730
- def generate_pivot_cache_definition_xml(pivot_table, _cache_id)
4731
- pcd_attrs = %( xmlns="#{SSML_NS}" xmlns:r="#{DOC_REL_NS}" r:id="rId1" refreshOnLoad="1")
4732
- pcd_attrs << ' saveData="0"' if pivot_table[:cache_save_data] == false
4733
- pcd_attrs << ' enableRefresh="0"' if pivot_table[:cache_enable_refresh] == false
4734
- pcd_attrs << %( refreshedBy="#{xml_escape(pivot_table[:cache_refreshed_by])}") if pivot_table[:cache_refreshed_by]
4735
- pcd_attrs << %( refreshedVersion="#{pivot_table[:cache_refreshed_version]}") if pivot_table[:cache_refreshed_version]
4736
- pcd_attrs << %( createdVersion="#{pivot_table[:cache_created_version]}") if pivot_table[:cache_created_version]
4737
- pcd_attrs << %( recordCount="#{pivot_table[:cache_record_count]}") if pivot_table[:cache_record_count]
4738
- pcd_attrs << ' optimizeMemory="1"' if pivot_table[:cache_optimize_memory]
4739
- parts = [
4740
- XML_HEADER,
4741
- "<pivotCacheDefinition#{pcd_attrs}>"
4742
- ]
4743
-
4744
- # Parse source ref: "Sheet1!A1:C4" => sheet name + range.
4745
- source = pivot_table[:source_ref]
4746
- ws_name_attr = pivot_table[:source_name] ? %( name="#{xml_escape(pivot_table[:source_name])}") : ""
4747
- if source.include?("!")
4748
- sname, srange = source.split("!", 2)
4749
- sname = sname.delete("'")
4750
- parts << %(<cacheSource type="worksheet"><worksheetSource ref="#{srange}" sheet="#{xml_escape(sname)}"#{ws_name_attr}/></cacheSource>)
4751
- else
4752
- parts << %(<cacheSource type="worksheet"><worksheetSource ref="#{source}"#{ws_name_attr}/></cacheSource>)
4753
- end
4754
-
4755
- 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)
4756
- parts << %(<cacheFields count="#{field_count}">)
4757
- field_count.times do |fi|
4758
- fname = if pivot_table[:field_names] && pivot_table[:field_names][fi]
4759
- pivot_table[:field_names][fi]
4760
- else
4761
- df = pivot_table[:data_fields].find { |d| d[:fld] == fi }
4762
- df ? df[:name] : "Field#{fi + 1}"
4763
- end
4764
- fa = pivot_table[:field_attrs] && pivot_table[:field_attrs][fi]
4765
- cf_num_fmt = (fa && fa[:cache_num_fmt_id]) || 0
4766
- cf_attrs = %( name="#{xml_escape(fname)}" numFmtId="#{cf_num_fmt}")
4767
- cf_attrs << %( caption="#{xml_escape(fa[:cache_caption])}") if fa && fa[:cache_caption]
4768
- cf_attrs << %( formula="#{xml_escape(fa[:cache_formula])}") if fa && fa[:cache_formula]
4769
- field_items = pivot_table[:items] && pivot_table[:items][fi]
4770
- if field_items
4771
- parts << "<cacheField#{cf_attrs}>"
4772
- parts << %(<sharedItems count="#{field_items.size}">)
4773
- field_items.each { |v| parts << %(<s v="#{xml_escape(v.to_s)}"/>) }
4774
- parts << "</sharedItems>"
4775
- parts << "</cacheField>"
4776
- else
4777
- parts << "<cacheField#{cf_attrs}><sharedItems/></cacheField>"
4778
- end
4779
- end
4780
- parts << "</cacheFields>"
4781
- parts << "</pivotCacheDefinition>"
4782
- parts.join
4783
- end
4784
-
4785
- # : (untyped pivot_table) -> untyped
4786
- def generate_pivot_cache_records_xml(pivot_table)
4787
- items = pivot_table[:items]
4788
- if items&.values&.any? { |v| v && !v.empty? }
4789
- max_len = items.values.map { |v| v ? v.size : 0 }.max
4790
- parts = [XML_HEADER, %(<pivotCacheRecords xmlns="#{SSML_NS}" count="#{max_len}">)]
4791
- max_len.times do |ri|
4792
- parts << "<r>"
4793
- 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)
4794
- field_count.times do |fi|
4795
- field_items = items[fi]
4796
- parts << if field_items
4797
- %(<x v="#{ri < field_items.size ? ri : 0}"/>)
4798
- else
4799
- %(<n v="0"/>)
4800
- end
4801
- end
4802
- parts << "</r>"
4803
- end
4804
- parts << "</pivotCacheRecords>"
4805
- parts.join
4806
- else
4807
- [XML_HEADER, %(<pivotCacheRecords xmlns="#{SSML_NS}" count="0"/>)].join
4808
- end
4809
- end
4810
-
4811
- # : (untyped cache_id) -> untyped
4812
- def generate_pivot_cache_rels(cache_id)
4813
- [
4814
- XML_HEADER,
4815
- %(<Relationships xmlns="#{REL_NS}">),
4816
- %(<Relationship Id="rId1" Type="#{DOC_REL_NS}/pivotCacheRecords" Target="pivotCacheRecords#{cache_id}.xml"/>),
4817
- "</Relationships>"
4818
- ].join
4819
- end
4820
-
4821
- # : (untyped cache_id) -> untyped
4822
- def generate_pivot_table_rels(cache_id)
4823
- [
4824
- XML_HEADER,
4825
- %(<Relationships xmlns="#{REL_NS}">),
4826
- %(<Relationship Id="rId1" Type="#{DOC_REL_NS}/pivotCacheDefinition" Target="../pivotCache/pivotCacheDefinition#{cache_id}.xml"/>),
4827
- "</Relationships>"
4828
- ].join
4829
- end
4830
-
4831
- # : (untyped ext_link) -> untyped
4832
- def generate_external_link_xml(ext_link)
4833
- parts = [
4834
- XML_HEADER,
4835
- %(<externalLink xmlns="#{SSML_NS}" xmlns:r="#{DOC_REL_NS}">),
4836
- '<externalBook r:id="rId1">'
4837
- ]
4838
- unless ext_link[:sheet_names].empty?
4839
- parts << "<sheetNames>"
4840
- ext_link[:sheet_names].each { |sn| parts << %(<sheetName val="#{xml_escape(sn)}"/>) }
4841
- parts << "</sheetNames>"
4842
- end
4843
- parts << "</externalBook>"
4844
- parts << "</externalLink>"
4845
- parts.join
4846
- end
4847
-
4848
- # : (untyped ext_link) -> untyped
4849
- def generate_external_link_rels(ext_link)
4850
- [
4851
- XML_HEADER,
4852
- %(<Relationships xmlns="#{REL_NS}">),
4853
- %(<Relationship Id="rId1" Type="#{DOC_REL_NS}/externalLinkPath" Target="#{xml_escape(ext_link[:target])}" TargetMode="External"/>),
4854
- "</Relationships>"
4855
- ].join
4856
- end
4857
-
4858
- # : (untyped ct_xml) -> untyped
4859
- def parse_extra_content_types(ct_xml)
4860
- ct_xml.scan(/<Default\s+Extension="([^"]+)"\s+ContentType="([^"]+)"/).each do |ext, ct|
4861
- @extra_ct_defaults[ext] ||= ct
4862
- end
4863
- ct_xml.scan(/<Override\s+PartName="([^"]+)"\s+ContentType="([^"]+)"/).each do |pn, ct|
4864
- @extra_ct_overrides[pn] ||= ct
4865
- end
4866
- end
4867
-
4868
- # : (untyped element, untyped opts) -> ("" | ::String)
4869
- def build_line_end_xml(element, opts)
4870
- return "" unless opts
4871
-
4872
- attrs = +""
4873
- attrs << %( type="#{xml_escape(opts[:type])}") if opts[:type]
4874
- attrs << %( w="#{xml_escape(opts[:w])}") if opts[:w]
4875
- attrs << %( len="#{xml_escape(opts[:len])}") if opts[:len]
4876
- "<a:#{element}#{attrs}/>"
4877
- end
4878
-
4879
- # : (untyped value) -> untyped
4880
- def xml_escape(value)
4881
- value.to_s
4882
- .gsub(/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/, "")
4883
- .gsub("&", "&amp;")
4884
- .gsub("<", "&lt;")
4885
- .gsub(">", "&gt;")
4886
- .gsub('"', "&quot;")
4887
- .gsub("'", "&apos;")
4888
- end
4889
-
4890
- # : (untyped rich_text) -> untyped
4891
- def rich_text_xml(rich_text)
4892
- rich_text.runs.map do |run|
4893
- font = run[:font]
4894
- if font && !font.empty?
4895
- rpr = +""
4896
- rpr << "<b/>" if font[:bold]
4897
- rpr << "<i/>" if font[:italic]
4898
- rpr << "<strike/>" if font[:strike]
4899
- if font[:underline]
4900
- rpr << if font[:underline] == true
4901
- "<u/>"
4902
- else
4903
- %(<u val="#{font[:underline]}"/>)
4904
- end
4905
- end
4906
- rpr << %(<vertAlign val="#{font[:vert_align]}"/>) if font[:vert_align]
4907
- rpr << %(<sz val="#{font[:sz]}"/>) if font[:sz]
4908
- rpr << emit_color_xml(font)
4909
- rpr << %(<rFont val="#{xml_escape(font[:name])}"/>) if font[:name]
4910
- rpr << %(<family val="#{font[:family]}"/>) if font[:family]
4911
- rpr << %(<scheme val="#{font[:scheme]}"/>) if font[:scheme]
4912
- "<r><rPr>#{rpr}</rPr><t>#{xml_escape(run[:text])}</t></r>"
4913
- else
4914
- "<r><t>#{xml_escape(run[:text])}</t></r>"
4915
- end
4916
- end.join
4917
- end
4918
-
4919
- # : (untyped cell_ref, untyped value, untyped style_idx, ?untyped? sst, ?ph: untyped?) -> untyped
4920
- def cell_xml(cell_ref, value, style_idx, sst = nil, ph: nil) # rubocop:disable Naming/MethodParameterName
4921
- s_attr = style_idx ? %( s="#{style_idx}") : ""
4922
- ph_attr = ph ? ' ph="1"' : ""
4923
- case value
4924
- when Xlsxrb::Elements::CellError
4925
- %(<c r="#{cell_ref}" t="e"#{s_attr}#{ph_attr}><v>#{xml_escape(value.code)}</v></c>)
4926
- when Xlsxrb::Elements::Formula
4927
- f_attrs = +""
4928
- case value.type
4929
- when :shared
4930
- f_attrs << %( t="shared" si="#{value.shared_index}")
4931
- f_attrs << %( ref="#{value.ref}") if value.ref
4932
- when :array
4933
- f_attrs << %( t="array" ref="#{value.ref}") if value.ref
4934
- when :data_table
4935
- f_attrs << ' t="dataTable"'
4936
- f_attrs << ' dt2D="1"' if value.dt2d
4937
- f_attrs << ' dtr="1"' if value.dtr
4938
- f_attrs << %( r1="#{value.r1}") if value.r1
4939
- f_attrs << %( r2="#{value.r2}") if value.r2
4940
- end
4941
- f_attrs << ' ca="1"' if value.calculate_always
4942
- f_attrs << ' aca="1"' if value.aca
4943
- f_attrs << ' bx="1"' if value.bx
4944
- t_attr = ""
4945
- cached_val_str = value.cached_value.to_s
4946
- case value.cached_value
4947
- when String
4948
- t_attr = ' t="str"'
4949
- when true
4950
- t_attr = ' t="b"'
4951
- cached_val_str = "1"
4952
- when false
4953
- t_attr = ' t="b"'
4954
- cached_val_str = "0"
4955
- end
4956
- parts = %(<c r="#{cell_ref}"#{t_attr}#{s_attr}#{ph_attr}><f#{f_attrs}>#{xml_escape(value.expression)}</f>)
4957
- parts << "<v>#{xml_escape(cached_val_str)}</v>" unless value.cached_value.nil?
4958
- parts << "</c>"
4959
- parts
4960
- when Xlsxrb::Elements::RichText
4961
- if sst
4962
- rt_sst, = sst
4963
- idx = rt_sst[value]
4964
- %(<c r="#{cell_ref}" t="s"#{s_attr}#{ph_attr}><v>#{idx}</v></c>)
4965
- else
4966
- %(<c r="#{cell_ref}" t="inlineStr"#{s_attr}#{ph_attr}><is>#{rich_text_xml(value)}</is></c>)
4967
- end
4968
- when true, false
4969
- %(<c r="#{cell_ref}" t="b"#{s_attr}#{ph_attr}><v>#{value ? 1 : 0}</v></c>)
4970
- when Time
4971
- serial = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
4972
- dt_attr = s_attr.empty? && (dt_style = resolve_style_index(datetime_num_fmt_id)) ? %( s="#{dt_style}") : s_attr
4973
- %(<c r="#{cell_ref}"#{dt_attr}#{ph_attr}><v>#{serial}</v></c>)
4974
- when Date
4975
- serial = Xlsxrb::Ooxml::Utils.date_to_serial(value)
4976
- ds_attr = s_attr.empty? && (date_style = resolve_style_index(date_num_fmt_id)) ? %( s="#{date_style}") : s_attr
4977
- %(<c r="#{cell_ref}"#{ds_attr}#{ph_attr}><v>#{serial}</v></c>)
4978
- when Numeric
4979
- %(<c r="#{cell_ref}"#{s_attr}#{ph_attr}><v>#{value}</v></c>)
4980
- else
4981
- if sst
4982
- _, str_sst = sst
4983
- idx = str_sst[value.to_s]
4984
- %(<c r="#{cell_ref}" t="s"#{s_attr}#{ph_attr}><v>#{idx}</v></c>)
4985
- else
4986
- %(<c r="#{cell_ref}" t="inlineStr"#{s_attr}#{ph_attr}><is><t>#{xml_escape(value)}</t></is></c>)
4987
- end
4988
- end
4989
- end
4990
-
4991
- # Returns the numFmtId for dates, registering it on first use.
4992
- # : () -> untyped
4993
- def date_num_fmt_id
4994
- @date_num_fmt_id ||= add_number_format(Xlsxrb::Ooxml::Utils::DEFAULT_DATE_FORMAT)
4995
- end
4996
-
4997
- # Returns the numFmtId for datetime, registering it on first use.
4998
- # : () -> untyped
4999
- def datetime_num_fmt_id
5000
- @datetime_num_fmt_id ||= add_number_format(Xlsxrb::Ooxml::Utils::DEFAULT_DATETIME_FORMAT)
5001
- end
5002
-
5003
- # Maps a numFmtId to a cellXfs index. Index 0 is the default (no format).
5004
- # : (untyped style_value) -> (nil | untyped)
5005
- def resolve_style_index(style_value)
5006
- return nil if style_value.nil?
5007
-
5008
- # New-style: { xf_index: N } from set_cell_style.
5009
- return style_value[:xf_index] if style_value.is_a?(Hash) && style_value.key?(:xf_index)
5010
-
5011
- # Legacy: raw num_fmt_id from set_cell_format — find or create matching xf entry.
5012
- num_fmt_id = style_value
5013
- @xf_index_map ||= begin
5014
- map = {}
5015
- @num_fmts.each_with_index do |nf, _i|
5016
- entry = { num_fmt_id: nf[:num_fmt_id], font_id: 0, fill_id: 0, border_id: 0 }
5017
- idx = @xf_entries.index(entry)
5018
- unless idx
5019
- @xf_entries << entry
5020
- idx = @xf_entries.size - 1
5021
- end
5022
- map[nf[:num_fmt_id]] = idx
5023
- end
5024
- map
5025
- end
5026
- @xf_index_map[num_fmt_id]
5027
- end
5028
-
5029
- # : (untyped filter) -> untyped
5030
- def emit_filter_xml(filter)
5031
- case filter[:type]
5032
- when :filters
5033
- attrs = filter[:blank] ? +' blank="1"' : +""
5034
- attrs << %( calendarType="#{filter[:calendar_type]}") if filter[:calendar_type]
5035
- has_values = filter[:values]&.any?
5036
- has_date_groups = filter[:date_group_items]&.any?
5037
- if has_values || has_date_groups
5038
- parts = ["<filters#{attrs}>"]
5039
- filter[:values]&.each { |v| parts << %(<filter val="#{xml_escape(v)}"/>) }
5040
- filter[:date_group_items]&.each do |dg|
5041
- dg_attrs = %(dateTimeGrouping="#{dg[:date_time_grouping]}")
5042
- dg_attrs << %( year="#{dg[:year]}") if dg[:year]
5043
- dg_attrs << %( month="#{dg[:month]}") if dg[:month]
5044
- dg_attrs << %( day="#{dg[:day]}") if dg[:day]
5045
- dg_attrs << %( hour="#{dg[:hour]}") if dg[:hour]
5046
- dg_attrs << %( minute="#{dg[:minute]}") if dg[:minute]
5047
- dg_attrs << %( second="#{dg[:second]}") if dg[:second]
5048
- parts << "<dateGroupItem #{dg_attrs}/>"
5049
- end
5050
- parts << "</filters>"
5051
- parts.join
5052
- else
5053
- "<filters#{attrs}/>"
5054
- end
5055
- when :custom
5056
- if filter[:filters]
5057
- and_attr = filter[:and] ? ' and="1"' : ""
5058
- parts = ["<customFilters#{and_attr}>"]
5059
- filter[:filters].each do |cf|
5060
- parts << %(<customFilter operator="#{cf[:operator]}" val="#{xml_escape(cf[:val])}"/>)
5061
- end
5062
- parts << "</customFilters>"
5063
- parts.join
5064
- else
5065
- %(<customFilters><customFilter operator="#{filter[:operator]}" val="#{xml_escape(filter[:val])}"/></customFilters>)
5066
- end
5067
- when :dynamic
5068
- dyn_attrs = %( type="#{filter[:dynamic_type]}")
5069
- dyn_attrs << %( val="#{filter[:val]}") if filter[:val]
5070
- dyn_attrs << %( valIso="#{filter[:val_iso]}") if filter[:val_iso]
5071
- dyn_attrs << %( maxVal="#{filter[:max_val]}") if filter[:max_val]
5072
- dyn_attrs << %( maxValIso="#{filter[:max_val_iso]}") if filter[:max_val_iso]
5073
- "<dynamicFilter#{dyn_attrs}/>"
5074
- when :top10
5075
- top_attr = filter[:top] ? ' top="1"' : ""
5076
- pct_attr = filter[:percent] ? ' percent="1"' : ""
5077
- fv_attr = filter[:filter_val] ? %( filterVal="#{filter[:filter_val]}") : ""
5078
- %(<top10#{top_attr}#{pct_attr} val="#{filter[:val]}"#{fv_attr}/>)
5079
- when :color_filter
5080
- cf_attrs = %(dxfId="#{filter[:dxf_id]}")
5081
- cf_attrs << ' cellColor="0"' if filter[:cell_color] == false
5082
- %(<colorFilter #{cf_attrs}/>)
5083
- when :icon_filter
5084
- if_attrs = %(iconSet="#{filter[:icon_set]}")
5085
- if_attrs << %( iconId="#{filter[:icon_id]}") if filter[:icon_id]
5086
- %(<iconFilter #{if_attrs}/>)
5087
- else
5088
- ""
5089
- end
5090
- end
5091
-
5092
- # : (untyped parts, untyped rule) -> untyped
5093
- def emit_cf_rule(parts, rule)
5094
- type = rule[:type]
5095
- if type.is_a?(String) || type.is_a?(Symbol)
5096
- t_str = type.to_s
5097
- snake = t_str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
5098
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
5099
- .downcase.to_sym
5100
- type = snake if CF_TYPE_MAP.key?(snake)
5101
- end
5102
- rule_type = CF_TYPE_MAP[type] || type.to_s
5103
- rule_attrs = %(type="#{rule_type}")
5104
- rule_attrs << %( priority="#{rule[:priority]}") if rule[:priority]
5105
- rule_attrs << %( operator="#{rule[:operator]}") if rule[:operator]
5106
- rule_attrs << %( dxfId="#{rule[:format_id]}") if rule[:format_id]
5107
- rule_attrs << %( stopIfTrue="1") if rule[:stop_if_true]
5108
- rule_attrs << %( aboveAverage="0") if rule[:above_average] == false
5109
- rule_attrs << %( equalAverage="1") if rule[:equal_average]
5110
- rule_attrs << %( rank="#{rule[:rank]}") if rule[:rank]
5111
- rule_attrs << %( percent="1") if rule[:percent]
5112
- rule_attrs << %( bottom="1") if rule[:bottom]
5113
- rule_attrs << %( text="#{xml_escape(rule[:text])}") if rule[:text]
5114
- rule_attrs << %( timePeriod="#{rule[:time_period]}") if rule[:time_period]
5115
- rule_attrs << %( stdDev="#{rule[:std_dev]}") if rule[:std_dev]
5116
-
5117
- case type
5118
- when :cell_is, :expression, :above_average, :top10, :duplicate_values, :unique_values,
5119
- :contains_text, :not_contains_text, :begins_with, :ends_with,
5120
- :contains_blanks, :not_contains_blanks, :time_period
5121
- formulas = rule[:formulas] || [rule[:formula]].compact
5122
- if formulas.empty?
5123
- parts << "<cfRule #{rule_attrs}/>"
5124
- else
5125
- parts << "<cfRule #{rule_attrs}>"
5126
- formulas.each { |f| parts << "<formula>#{xml_escape(f)}</formula>" }
5127
- parts << "</cfRule>"
5128
- end
5129
- when :color_scale
5130
- cs = rule[:color_scale]
5131
- parts << "<cfRule #{rule_attrs}>"
5132
- parts << "<colorScale>"
5133
- cs[:cfvo]&.each do |cfvo|
5134
- cfvo_attrs = %(type="#{cfvo[:type]}")
5135
- cfvo_attrs << %( val="#{cfvo[:val]}") if cfvo[:val]
5136
- cfvo_attrs << ' gte="0"' if cfvo[:gte] == false
5137
- parts << "<cfvo #{cfvo_attrs}/>"
5138
- end
5139
- cs[:colors]&.each { |c| parts << emit_cf_color_xml(c) }
5140
- parts << "</colorScale>"
5141
- parts << "</cfRule>"
5142
- when :data_bar
5143
- db = rule[:data_bar]
5144
- parts << "<cfRule #{rule_attrs}>"
5145
- db_attrs = +""
5146
- db_attrs << %( minLength="#{db[:min_length]}") if db[:min_length]
5147
- db_attrs << %( maxLength="#{db[:max_length]}") if db[:max_length]
5148
- db_attrs << %( showValue="#{db[:show_value] ? 1 : 0}") unless db[:show_value].nil?
5149
- parts << "<dataBar#{db_attrs}>"
5150
- db[:cfvo]&.each do |cfvo|
5151
- cfvo_attrs = %(type="#{cfvo[:type]}")
5152
- cfvo_attrs << %( val="#{cfvo[:val]}") if cfvo[:val]
5153
- cfvo_attrs << ' gte="0"' if cfvo[:gte] == false
5154
- parts << "<cfvo #{cfvo_attrs}/>"
5155
- end
5156
- parts << emit_cf_color_xml(db[:color]) if db[:color]
5157
- parts << "</dataBar>"
5158
- parts << "</cfRule>"
5159
- when :icon_set
5160
- is = rule[:icon_set]
5161
- parts << "<cfRule #{rule_attrs}>"
5162
- is_attrs = +""
5163
- is_attrs << %( iconSet="#{is[:icon_set]}") if is[:icon_set]
5164
- is_attrs << %( reverse="#{is[:reverse] ? 1 : 0}") unless is[:reverse].nil?
5165
- is_attrs << %( percent="#{is[:percent] ? 1 : 0}") unless is[:percent].nil?
5166
- is_attrs << %( showValue="#{is[:show_value] ? 1 : 0}") unless is[:show_value].nil?
5167
- parts << "<iconSet#{is_attrs}>"
5168
- is[:cfvo]&.each do |cfvo|
5169
- cfvo_attrs = %(type="#{cfvo[:type]}")
5170
- cfvo_attrs << %( val="#{cfvo[:val]}") if cfvo[:val]
5171
- cfvo_attrs << ' gte="0"' if cfvo[:gte] == false
5172
- parts << "<cfvo #{cfvo_attrs}/>"
5173
- end
5174
- parts << "</iconSet>"
5175
- parts << "</cfRule>"
5176
- else
5177
- parts << "<cfRule #{rule_attrs}/>"
5178
- end
5179
- end
5180
-
5181
- # Emits a <color> element for CF rules, accepting either a plain RGB string or a hash.
5182
- # : (untyped color) -> (untyped | ::String)
5183
- def emit_cf_color_xml(color)
5184
- if color.is_a?(Hash)
5185
- emit_color_xml(color)
5186
- else
5187
- %(<color rgb="#{color}"/>)
5188
- end
5189
- end
5190
-
5191
- # : (untyped sheet_cells) -> ("A1" | ::String)
5192
- def compute_dimension(sheet_cells)
5193
- return "A1" if sheet_cells.empty?
5194
-
5195
- min_col = Float::INFINITY
5196
- max_col = 0
5197
- min_row = Float::INFINITY
5198
- max_row = 0
5199
- sheet_cells.each_key do |addr|
5200
- col_letter = extract_column_letter(addr)
5201
- row_num = extract_row_number(addr)
5202
- col_idx = column_letter_to_index(col_letter)
5203
- min_col = col_idx if col_idx < min_col
5204
- max_col = col_idx if col_idx > max_col
5205
- min_row = row_num if row_num < min_row
5206
- max_row = row_num if row_num > max_row
5207
- end
5208
- start_col = index_to_column_letter(min_col)
5209
- end_col = index_to_column_letter(max_col)
5210
- "#{start_col}#{min_row}:#{end_col}#{max_row}"
5211
- end
5212
-
5213
- # : (untyped index) -> untyped
5214
- def index_to_column_letter(index)
5215
- result = +""
5216
- while index.positive?
5217
- index -= 1
5218
- result.prepend(("A".ord + (index % 26)).chr)
5219
- index /= 26
5220
- end
5221
- result
5222
- end
5223
-
5224
- # : () -> untyped
5225
- def generate_core_properties_xml
5226
- parts = [
5227
- XML_HEADER,
5228
- %(<cp:coreProperties xmlns:cp="#{CP_NS}" xmlns:dc="#{DC_NS}" xmlns:dcterms="#{DCTERMS_NS}" xmlns:xsi="#{XSI_NS}">)
5229
- ]
5230
- parts << "<dc:title>#{xml_escape(@core_properties[:title])}</dc:title>" if @core_properties[:title]
5231
- parts << "<dc:subject>#{xml_escape(@core_properties[:subject])}</dc:subject>" if @core_properties[:subject]
5232
- parts << "<dc:creator>#{xml_escape(@core_properties[:creator])}</dc:creator>" if @core_properties[:creator]
5233
- parts << "<cp:keywords>#{xml_escape(@core_properties[:keywords])}</cp:keywords>" if @core_properties[:keywords]
5234
- parts << "<dc:description>#{xml_escape(@core_properties[:description])}</dc:description>" if @core_properties[:description]
5235
- parts << "<cp:lastModifiedBy>#{xml_escape(@core_properties[:last_modified_by])}</cp:lastModifiedBy>" if @core_properties[:last_modified_by]
5236
- parts << "<cp:revision>#{xml_escape(@core_properties[:revision])}</cp:revision>" if @core_properties[:revision]
5237
- parts << %(<dcterms:created xsi:type="dcterms:W3CDTF">#{xml_escape(@core_properties[:created])}</dcterms:created>) if @core_properties[:created]
5238
- parts << %(<dcterms:modified xsi:type="dcterms:W3CDTF">#{xml_escape(@core_properties[:modified])}</dcterms:modified>) if @core_properties[:modified]
5239
- parts << "<cp:category>#{xml_escape(@core_properties[:category])}</cp:category>" if @core_properties[:category]
5240
- parts << "<cp:contentStatus>#{xml_escape(@core_properties[:content_status])}</cp:contentStatus>" if @core_properties[:content_status]
5241
- parts << "<dc:language>#{xml_escape(@core_properties[:language])}</dc:language>" if @core_properties[:language]
5242
- parts << "</cp:coreProperties>"
5243
- parts.join
5244
- end
5245
-
5246
- # : () -> untyped
5247
- def generate_app_properties_xml
5248
- parts = [
5249
- XML_HEADER,
5250
- %(<Properties xmlns="#{APP_NS}" xmlns:vt="#{VT_NS}">)
5251
- ]
5252
- parts << "<Application>#{xml_escape(@app_properties[:application])}</Application>" if @app_properties[:application]
5253
- parts << "<AppVersion>#{xml_escape(@app_properties[:app_version])}</AppVersion>" if @app_properties[:app_version]
5254
- if @app_properties[:heading_pairs] && @app_properties[:titles_of_parts]
5255
- hp = @app_properties[:heading_pairs]
5256
- tp = @app_properties[:titles_of_parts]
5257
- parts << "<HeadingPairs>"
5258
- parts << %(<vt:vector size="#{hp.size * 2}" baseType="variant">)
5259
- hp.each do |label, count|
5260
- parts << "<vt:variant><vt:lpstr>#{xml_escape(label)}</vt:lpstr></vt:variant>"
5261
- parts << "<vt:variant><vt:i4>#{count}</vt:i4></vt:variant>"
5262
- end
5263
- parts << "</vt:vector>"
5264
- parts << "</HeadingPairs>"
5265
- parts << "<TitlesOfParts>"
5266
- parts << %(<vt:vector size="#{tp.size}" baseType="lpstr">)
5267
- tp.each { |t| parts << "<vt:lpstr>#{xml_escape(t)}</vt:lpstr>" }
5268
- parts << "</vt:vector>"
5269
- parts << "</TitlesOfParts>"
5270
- end
5271
- parts << "</Properties>"
5272
- parts.join
5273
- end
5274
-
5275
- # : () -> untyped
5276
- def generate_custom_properties_xml
5277
- custom_ns = "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
5278
- parts = [
5279
- XML_HEADER,
5280
- %(<Properties xmlns="#{custom_ns}" xmlns:vt="#{VT_NS}">)
5281
- ]
5282
- @custom_properties.each_with_index do |prop, idx|
5283
- fmtid = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}"
5284
- pid = idx + 2 # pids start at 2
5285
- parts << %(<property fmtid="#{fmtid}" pid="#{pid}" name="#{xml_escape(prop[:name])}">)
5286
- parts << case prop[:type]
5287
- when :number, :int, :i4
5288
- "<vt:i4>#{prop[:value]}</vt:i4>"
5289
- when :float, :r8
5290
- "<vt:r8>#{prop[:value]}</vt:r8>"
5291
- when :bool
5292
- "<vt:bool>#{prop[:value] ? "true" : "false"}</vt:bool>"
5293
- when :date, :filetime
5294
- "<vt:filetime>#{prop[:value]}</vt:filetime>"
5295
- else
5296
- "<vt:lpwstr>#{xml_escape(prop[:value].to_s)}</vt:lpwstr>"
5297
- end
5298
- parts << "</property>"
5299
- end
5300
- parts << "</Properties>"
5301
- parts.join
5302
- end
5303
-
5304
- # : () -> untyped
5305
- def generate_styles_xml
5306
- parts = [
5307
- XML_HEADER,
5308
- %(<styleSheet xmlns="#{SSML_NS}">)
5309
- ]
5310
-
5311
- # numFmts
5312
- unless @num_fmts.empty?
5313
- parts << %(<numFmts count="#{@num_fmts.size}">)
5314
- @num_fmts.each do |nf|
5315
- parts << %(<numFmt numFmtId="#{nf[:num_fmt_id]}" formatCode="#{xml_escape(nf[:format_code])}"/>)
5316
- end
5317
- parts << "</numFmts>"
5318
- end
5319
-
5320
- # fonts
5321
- parts << %(<fonts count="#{@fonts.size}">)
5322
- @fonts.each { |f| parts << emit_font_xml(f) }
5323
- parts << "</fonts>"
5324
-
5325
- # fills
5326
- parts << %(<fills count="#{@fills.size}">)
5327
- @fills.each { |f| parts << emit_fill_xml(f) }
5328
- parts << "</fills>"
5329
-
5330
- # borders
5331
- parts << %(<borders count="#{@borders.size}">)
5332
- @borders.each { |b| parts << emit_border_xml(b) }
5333
- parts << "</borders>"
5334
-
5335
- # cellStyleXfs
5336
- parts << %(<cellStyleXfs count="#{@cell_style_xfs.size}">)
5337
- @cell_style_xfs.each do |xf|
5338
- parts << %(<xf numFmtId="#{xf[:num_fmt_id]}" fontId="#{xf[:font_id]}" fillId="#{xf[:fill_id]}" borderId="#{xf[:border_id]}"/>)
5339
- end
5340
- parts << "</cellStyleXfs>"
5341
-
5342
- # cellXfs
5343
- parts << %(<cellXfs count="#{@xf_entries.size}">)
5344
- @xf_entries.each do |xf|
5345
- apply_attrs = []
5346
- apply_attrs << ' applyNumberFormat="1"' if xf[:num_fmt_id].positive?
5347
- apply_attrs << ' applyFont="1"' if xf[:font_id].positive?
5348
- apply_attrs << ' applyFill="1"' if xf[:fill_id].positive?
5349
- apply_attrs << ' applyBorder="1"' if xf[:border_id].positive?
5350
- apply_attrs << ' applyAlignment="1"' if xf[:alignment]
5351
- apply_attrs << ' applyProtection="1"' if xf[:protection]
5352
- apply_attrs << ' quotePrefix="1"' if xf[:quote_prefix]
5353
- apply_attrs << ' pivotButton="1"' if xf[:pivot_button]
5354
- children = []
5355
- children << emit_alignment_xml(xf[:alignment]) if xf[:alignment]
5356
- children << emit_protection_xml(xf[:protection]) if xf[:protection]
5357
- xf_id = xf[:xf_id] || 0
5358
- parts << if children.any?
5359
- %(<xf numFmtId="#{xf[:num_fmt_id]}" fontId="#{xf[:font_id]}" fillId="#{xf[:fill_id]}" borderId="#{xf[:border_id]}" xfId="#{xf_id}"#{apply_attrs.join}>#{children.join}</xf>)
5360
- else
5361
- %(<xf numFmtId="#{xf[:num_fmt_id]}" fontId="#{xf[:font_id]}" fillId="#{xf[:fill_id]}" borderId="#{xf[:border_id]}" xfId="#{xf_id}"#{apply_attrs.join}/>)
5362
- end
5363
- end
5364
- parts << "</cellXfs>"
5365
-
5366
- # cellStyles
5367
- parts << %(<cellStyles count="#{@cell_style_names.size}">)
5368
- @cell_style_names.each do |cs|
5369
- cs_attrs = %(name="#{xml_escape(cs[:name])}" xfId="#{cs[:xf_id]}")
5370
- cs_attrs << %( builtinId="#{cs[:builtin_id]}") if cs[:builtin_id]
5371
- cs_attrs << %( iLevel="#{cs[:i_level]}") if cs[:i_level]
5372
- cs_attrs << ' hidden="1"' if cs[:hidden]
5373
- cs_attrs << ' customBuiltin="1"' if cs[:custom_builtin]
5374
- parts << "<cellStyle #{cs_attrs}/>"
5375
- end
5376
- parts << "</cellStyles>"
5377
-
5378
- # dxfs
5379
- unless @dxfs.empty?
5380
- parts << %(<dxfs count="#{@dxfs.size}">)
5381
- @dxfs.each { |d| parts << emit_dxf_xml(d) }
5382
- parts << "</dxfs>"
5383
- end
5384
-
5385
- # tableStyles
5386
- ts_styles = @table_styles[:styles] || []
5387
- unless ts_styles.empty? && @table_styles[:default_table_style].nil? && @table_styles[:default_pivot_style].nil?
5388
- ts_attrs = [%(count="#{ts_styles.size}")]
5389
- ts_attrs << %(defaultTableStyle="#{xml_escape(@table_styles[:default_table_style])}") if @table_styles[:default_table_style]
5390
- ts_attrs << %(defaultPivotStyle="#{xml_escape(@table_styles[:default_pivot_style])}") if @table_styles[:default_pivot_style]
5391
- if ts_styles.empty?
5392
- parts << "<tableStyles #{ts_attrs.join(" ")}/>"
5393
- else
5394
- parts << "<tableStyles #{ts_attrs.join(" ")}>"
5395
- ts_styles.each do |ts|
5396
- s_attrs = [%(name="#{xml_escape(ts[:name])}")]
5397
- s_attrs << %(pivot="0") if ts[:pivot] == false
5398
- s_attrs << %(table="0") if ts[:table] == false
5399
- s_attrs << %(count="#{ts[:elements].size}") unless ts[:elements].empty?
5400
- if ts[:elements].empty?
5401
- parts << "<tableStyle #{s_attrs.join(" ")}/>"
5402
- else
5403
- parts << "<tableStyle #{s_attrs.join(" ")}>"
5404
- ts[:elements].each do |el|
5405
- el_attrs = [%(type="#{el[:type]}")]
5406
- el_attrs << %(size="#{el[:size]}") if el[:size] && el[:size] != 1
5407
- el_attrs << %(dxfId="#{el[:dxf_id]}") if el[:dxf_id]
5408
- parts << "<tableStyleElement #{el_attrs.join(" ")}/>"
5409
- end
5410
- parts << "</tableStyle>"
5411
- end
5412
- end
5413
- parts << "</tableStyles>"
5414
- end
5415
- end
5416
-
5417
- # colors
5418
- unless @indexed_colors.empty? && @mru_colors.empty?
5419
- parts << "<colors>"
5420
- unless @indexed_colors.empty?
5421
- parts << "<indexedColors>"
5422
- @indexed_colors.each { |c| parts << %(<rgbColor rgb="#{c}"/>) }
5423
- parts << "</indexedColors>"
5424
- end
5425
- unless @mru_colors.empty?
5426
- parts << "<mruColors>"
5427
- @mru_colors.each { |c| parts << emit_color_xml(c) }
5428
- parts << "</mruColors>"
5429
- end
5430
- parts << "</colors>"
5431
- end
5432
-
5433
- parts << "</styleSheet>"
5434
- parts.join
5435
- end
5436
-
5437
- # : (untyped alignment) -> ::String
5438
- def emit_alignment_xml(alignment)
5439
- attrs = []
5440
- attrs << %(horizontal="#{alignment[:horizontal]}") if alignment[:horizontal]
5441
- attrs << %(vertical="#{alignment[:vertical]}") if alignment[:vertical]
5442
- attrs << %(wrapText="1") if alignment[:wrap_text]
5443
- attrs << %(textRotation="#{alignment[:text_rotation]}") if alignment[:text_rotation]
5444
- attrs << %(indent="#{alignment[:indent]}") if alignment[:indent]
5445
- attrs << %(relativeIndent="#{alignment[:relative_indent]}") if alignment[:relative_indent]
5446
- attrs << %(shrinkToFit="1") if alignment[:shrink_to_fit]
5447
- attrs << %(readingOrder="#{alignment[:reading_order]}") if alignment[:reading_order]
5448
- attrs << %(justifyLastLine="1") if alignment[:justify_last_line]
5449
- "<alignment #{attrs.join(" ")}/>"
5450
- end
5451
-
5452
- # : (untyped protection) -> ::String
5453
- def emit_protection_xml(protection)
5454
- attrs = []
5455
- attrs << %(locked="#{protection[:locked] ? "1" : "0"}") unless protection[:locked].nil?
5456
- attrs << %(hidden="#{protection[:hidden] ? "1" : "0"}") unless protection[:hidden].nil?
5457
- "<protection #{attrs.join(" ")}/>"
5458
- end
5459
-
5460
- # : (untyped source, ?tag: ::String) -> (::String | ::String | ::String | ::String | "")
5461
- def emit_color_xml(source, tag: "color")
5462
- if source[:auto]
5463
- %(<#{tag} auto="1"/>)
5464
- elsif source[:color] || source[:rgb]
5465
- %(<#{tag} rgb="#{source[:color] || source[:rgb]}"/>)
5466
- elsif source[:theme]
5467
- attrs = [%(theme="#{source[:theme]}")]
5468
- attrs << %(tint="#{source[:tint]}") if source[:tint]
5469
- %(<#{tag} #{attrs.join(" ")}/>)
5470
- elsif source[:indexed]
5471
- %(<#{tag} indexed="#{source[:indexed]}"/>)
5472
- else
5473
- ""
5474
- end
5475
- end
5476
-
5477
- # : (untyped font) -> untyped
5478
- def emit_font_xml(font)
5479
- parts = ["<font>"]
5480
- parts << "<b/>" if font[:bold]
5481
- parts << "<i/>" if font[:italic]
5482
- parts << "<strike/>" if font[:strike]
5483
- parts << "<shadow/>" if font[:shadow]
5484
- parts << "<outline/>" if font[:outline]
5485
- parts << "<condense/>" if font[:condense]
5486
- parts << "<extend/>" if font[:extend]
5487
- if font[:underline]
5488
- parts << if font[:underline] == true
5489
- "<u/>"
5490
- else
5491
- %(<u val="#{font[:underline]}"/>)
5492
- end
5493
- end
5494
- parts << %(<vertAlign val="#{font[:vert_align]}"/>) if font[:vert_align]
5495
- parts << %(<sz val="#{font[:sz]}"/>) if font[:sz]
5496
- parts << emit_color_xml(font)
5497
- parts << %(<name val="#{xml_escape(font[:name])}"/>) if font[:name]
5498
- parts << %(<family val="#{font[:family]}"/>) if font[:family]
5499
- parts << %(<charset val="#{font[:charset]}"/>) if font[:charset]
5500
- parts << %(<scheme val="#{font[:scheme]}"/>) if font[:scheme]
5501
- parts << "</font>"
5502
- parts.join
5503
- end
5504
-
5505
- # : (untyped fill) -> (untyped | ::String)
5506
- def emit_fill_xml(fill)
5507
- return emit_gradient_fill_xml(fill[:gradient]) if fill[:gradient]
5508
-
5509
- has_fg = fill[:fg_color] || fill[:fg_color_theme] || fill[:fg_color_indexed] || fill[:fg_color_auto]
5510
- has_bg = fill[:bg_color] || fill[:bg_color_theme] || fill[:bg_color_indexed] || fill[:bg_color_auto]
5511
- return "<fill><patternFill patternType=\"#{fill[:pattern]}\"/></fill>" if fill[:pattern] && !has_fg && !has_bg
5512
-
5513
- parts = ["<fill>"]
5514
- pt = fill[:pattern] || "solid"
5515
- parts << %(<patternFill patternType="#{pt}">)
5516
- parts << emit_fill_color_xml("fgColor", fill, :fg)
5517
- parts << emit_fill_color_xml("bgColor", fill, :bg)
5518
- parts << "</patternFill>"
5519
- parts << "</fill>"
5520
- parts.join
5521
- end
5522
-
5523
- # : (untyped tag, untyped fill, untyped prefix) -> (::String | ::String | ::String | ::String | "")
5524
- def emit_fill_color_xml(tag, fill, prefix)
5525
- if fill[:"#{prefix}_color"]
5526
- %(<#{tag} rgb="#{fill[:"#{prefix}_color"]}"/>)
5527
- elsif fill[:"#{prefix}_color_theme"]
5528
- attrs = [%(theme="#{fill[:"#{prefix}_color_theme"]}")]
5529
- attrs << %(tint="#{fill[:"#{prefix}_color_tint"]}") if fill[:"#{prefix}_color_tint"]
5530
- %(<#{tag} #{attrs.join(" ")}/>)
5531
- elsif fill[:"#{prefix}_color_indexed"]
5532
- %(<#{tag} indexed="#{fill[:"#{prefix}_color_indexed"]}"/>)
5533
- elsif fill[:"#{prefix}_color_auto"]
5534
- %(<#{tag} auto="1"/>)
5535
- else
5536
- ""
5537
- end
5538
- end
5539
-
5540
- # : (untyped gradient) -> untyped
5541
- def emit_gradient_fill_xml(gradient)
5542
- attrs = []
5543
- attrs << %(type="#{gradient[:type]}") if gradient[:type]
5544
- attrs << %(degree="#{gradient[:degree]}") if gradient[:degree]
5545
- attrs << %(left="#{gradient[:left]}") if gradient[:left]
5546
- attrs << %(right="#{gradient[:right]}") if gradient[:right]
5547
- attrs << %(top="#{gradient[:top]}") if gradient[:top]
5548
- attrs << %(bottom="#{gradient[:bottom]}") if gradient[:bottom]
5549
- parts = ["<fill>"]
5550
- parts << "<gradientFill#{" #{attrs.join(" ")}" unless attrs.empty?}"
5551
- if gradient[:stops]&.any?
5552
- parts[-1] = "#{parts[-1]}>"
5553
- gradient[:stops].each do |stop|
5554
- parts << %(<stop position="#{stop[:position]}">#{emit_color_xml(stop)}</stop>)
5555
- end
5556
- parts << "</gradientFill>"
5557
- else
5558
- parts[-1] = "#{parts[-1]}/>"
5559
- end
5560
- parts << "</fill>"
5561
- parts.join
5562
- end
5563
-
5564
- # : (untyped brk, default_max: untyped) -> ::String
5565
- def emit_brk_xml(brk, default_max:)
5566
- if brk.is_a?(Hash)
5567
- attrs = %(id="#{brk[:id]}")
5568
- attrs << %( min="#{brk[:min]}") if brk[:min]
5569
- attrs << %( max="#{brk.fetch(:max, default_max)}")
5570
- attrs << ' man="1"' if brk.fetch(:man, true)
5571
- attrs << ' pt="1"' if brk[:pt]
5572
- "<brk #{attrs}/>"
5573
- else
5574
- %(<brk id="#{brk}" max="#{default_max}" man="1"/>)
5575
- end
5576
- end
5577
-
5578
- # : (untyped bdr) -> untyped
5579
- def emit_border_xml(bdr)
5580
- border_attrs = []
5581
- border_attrs << ' diagonalUp="1"' if bdr[:diagonal_up]
5582
- border_attrs << ' diagonalDown="1"' if bdr[:diagonal_down]
5583
- border_attrs << ' outline="0"' if bdr[:outline] == false
5584
- parts = ["<border#{border_attrs.join}>"]
5585
- %i[left right top bottom diagonal vertical horizontal].each do |side|
5586
- s = bdr[side]
5587
- if s.is_a?(Hash)
5588
- parts << %(<#{side} style="#{s[:style]}">)
5589
- parts << emit_color_xml(s)
5590
- parts << "</#{side}>"
5591
- else
5592
- parts << "<#{side}/>"
5593
- end
5594
- end
5595
- parts << "</border>"
5596
- parts.join
5597
- end
5598
-
5599
- # : (untyped dxf) -> untyped
5600
- def emit_dxf_xml(dxf)
5601
- parts = ["<dxf>"]
5602
- parts << emit_font_xml(dxf[:font]) if dxf[:font]
5603
- if dxf[:num_fmt]
5604
- nf = dxf[:num_fmt]
5605
- parts << %(<numFmt numFmtId="#{nf[:num_fmt_id]}" formatCode="#{xml_escape(nf[:format_code])}"/>)
5606
- end
5607
- parts << emit_fill_xml(dxf[:fill]) if dxf[:fill]
5608
- parts << emit_alignment_xml(dxf[:alignment]) if dxf[:alignment]
5609
- parts << emit_border_xml(dxf[:border]) if dxf[:border]
5610
- parts << emit_protection_xml(dxf[:protection]) if dxf[:protection]
5611
- parts << "</dxf>"
5612
- parts.join
5613
- end
5614
-
5615
- # : (untyped cell_address) -> untyped
5616
- def validate_cell_address!(cell_address)
5617
- raise ArgumentError, "cell address must be a String" unless cell_address.is_a?(String)
5618
-
5619
- match = cell_address.match(CELL_ADDRESS_PATTERN)
5620
- raise ArgumentError, "invalid cell address: #{cell_address.inspect}" unless match
5621
-
5622
- row_num = match[2].to_i
5623
- raise ArgumentError, "row out of range: #{row_num}" unless row_num.between?(1, MAX_ROW)
5624
-
5625
- col_index = column_letter_to_index(match[1])
5626
- raise ArgumentError, "column out of range: #{match[1]}" unless col_index.between?(1, MAX_COLUMN_INDEX)
5627
- end
5628
-
5629
- # : (untyped letters) -> untyped
5630
- def column_letter_to_index(letters)
5631
- letters.chars.reduce(0) { |sum, char| (sum * 26) + (char.ord - "A".ord + 1) }
5632
- end
5633
-
5634
- # Extracts the column letter(s) from a cell address, e.g. "A" from "A1".
5635
- # : (untyped cell_address) -> untyped
5636
- def extract_column_letter(cell_address)
5637
- cell_address.match(/^([A-Z]+)/)[1]
5638
- end
5639
-
5640
3003
  # Extracts the row number from a cell address, e.g. 1 from "A1".
5641
3004
  # : (untyped cell_address) -> untyped
5642
3005
  def extract_row_number(cell_address)
@@ -5646,7 +3009,7 @@ module Xlsxrb
5646
3009
  # Converts a range like "A1:D20" to absolute "$A$1:$D$20".
5647
3010
  # : (untyped range) -> untyped
5648
3011
  def absolute_range(range)
5649
- range.split(":").map { |part| absolute_cell(part) }.join(":")
3012
+ DslHelpers.absolute_range(range)
5650
3013
  end
5651
3014
 
5652
3015
  # Converts "A1" to "$A$1".