xlsxrb 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -34
  3. data/README.md +26 -21
  4. data/lib/xlsxrb/chart_builder.rb +18 -18
  5. data/lib/xlsxrb/dsl_helpers.rb +175 -0
  6. data/lib/xlsxrb/elements/cell.rb +56 -22
  7. data/lib/xlsxrb/elements/column.rb +27 -3
  8. data/lib/xlsxrb/elements/row.rb +27 -8
  9. data/lib/xlsxrb/elements/workbook.rb +27 -14
  10. data/lib/xlsxrb/elements/worksheet.rb +17 -1
  11. data/lib/xlsxrb/ooxml/cfb.rb +9 -6
  12. data/lib/xlsxrb/ooxml/crypto/agile.rb +5 -5
  13. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +784 -0
  14. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +3478 -0
  15. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +1796 -0
  16. data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +428 -0
  17. data/lib/xlsxrb/ooxml/reader/listeners.rb +4 -6446
  18. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +3 -5
  19. data/lib/xlsxrb/ooxml/utils.rb +5 -6
  20. data/lib/xlsxrb/ooxml/workbook_writer.rb +5 -7
  21. data/lib/xlsxrb/ooxml/worksheet_parser.rb +172 -9
  22. data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +1602 -0
  23. data/lib/xlsxrb/ooxml/writer/features_xml.rb +731 -0
  24. data/lib/xlsxrb/ooxml/writer/styles_xml.rb +348 -0
  25. data/lib/xlsxrb/ooxml/writer.rb +9 -2646
  26. data/lib/xlsxrb/ooxml/xml_builder.rb +31 -2
  27. data/lib/xlsxrb/ooxml/zip_generator.rb +26 -5
  28. data/lib/xlsxrb/ooxml/zip_reader.rb +364 -78
  29. data/lib/xlsxrb/ooxml/zip_writer.rb +32 -5
  30. data/lib/xlsxrb/stream_sheet.rb +94 -6
  31. data/lib/xlsxrb/stream_writer.rb +48 -115
  32. data/lib/xlsxrb/style_builder.rb +10 -1
  33. data/lib/xlsxrb/version.rb +1 -1
  34. data/lib/xlsxrb/workbook_builder.rb +1 -1
  35. data/lib/xlsxrb/worksheet_builder.rb +19 -81
  36. data/lib/xlsxrb.rb +449 -171
  37. data/sig/generated/xlsxrb/chart_builder.rbs +36 -36
  38. data/sig/generated/xlsxrb/dsl_helpers.rbs +92 -0
  39. data/sig/generated/xlsxrb/elements/cell.rbs +29 -0
  40. data/sig/generated/xlsxrb/elements/column.rbs +32 -21
  41. data/sig/generated/xlsxrb/elements/row.rbs +94 -14
  42. data/sig/generated/xlsxrb/elements/workbook.rbs +102 -10
  43. data/sig/generated/xlsxrb/elements/worksheet.rbs +9 -0
  44. data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +173 -0
  45. data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +400 -0
  46. data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +442 -0
  47. data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +80 -0
  48. data/sig/generated/xlsxrb/ooxml/reader/listeners.rbs +0 -1072
  49. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +0 -2
  50. data/sig/generated/xlsxrb/ooxml/utils.rbs +2 -0
  51. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +5 -4
  52. data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +79 -0
  53. data/sig/generated/xlsxrb/ooxml/writer/features_xml.rbs +83 -0
  54. data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +53 -0
  55. data/sig/generated/xlsxrb/ooxml/writer.rbs +6 -185
  56. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +18 -0
  57. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +15 -0
  58. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +96 -8
  59. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +5 -1
  60. data/sig/generated/xlsxrb/stream_sheet.rbs +43 -3
  61. data/sig/generated/xlsxrb/style_builder.rbs +7 -0
  62. data/sig/generated/xlsxrb.rbs +18 -11
  63. metadata +22 -144
  64. data/.devcontainer/Dockerfile +0 -65
  65. data/.devcontainer/devcontainer.json +0 -17
  66. data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +0 -9
  67. data/.gem_rbs_collection/ast/2.4/ast.rbs +0 -73
  68. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +0 -9
  69. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +0 -4
  70. data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +0 -16
  71. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +0 -96
  72. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +0 -4
  73. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +0 -68
  74. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +0 -249
  75. data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +0 -4
  76. data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +0 -47
  77. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +0 -5
  78. data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +0 -9
  79. data/.gem_rbs_collection/csv/3.3/csv.rbs +0 -3871
  80. data/.gem_rbs_collection/csv/3.3/manifest.yaml +0 -3
  81. data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +0 -9
  82. data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +0 -48
  83. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +0 -9
  84. data/.gem_rbs_collection/listen/3.9/listen.rbs +0 -25
  85. data/.gem_rbs_collection/listen/3.9/listener.rbs +0 -24
  86. data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +0 -9
  87. data/.gem_rbs_collection/logger/1.7/formatter.rbs +0 -45
  88. data/.gem_rbs_collection/logger/1.7/log_device.rbs +0 -100
  89. data/.gem_rbs_collection/logger/1.7/logger.rbs +0 -796
  90. data/.gem_rbs_collection/logger/1.7/manifest.yaml +0 -2
  91. data/.gem_rbs_collection/logger/1.7/period.rbs +0 -17
  92. data/.gem_rbs_collection/logger/1.7/severity.rbs +0 -34
  93. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +0 -9
  94. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +0 -2332
  95. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +0 -4
  96. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +0 -9
  97. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +0 -86
  98. data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +0 -9
  99. data/.gem_rbs_collection/parser/3.2/manifest.yaml +0 -7
  100. data/.gem_rbs_collection/parser/3.2/parser.rbs +0 -194
  101. data/.gem_rbs_collection/parser/3.2/polyfill.rbs +0 -4
  102. data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +0 -9
  103. data/.gem_rbs_collection/rainbow/3.0/global.rbs +0 -7
  104. data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +0 -209
  105. data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +0 -5
  106. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +0 -9
  107. data/.gem_rbs_collection/rake/13.0/manifest.yaml +0 -2
  108. data/.gem_rbs_collection/rake/13.0/rake.rbs +0 -39
  109. data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +0 -9
  110. data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +0 -17
  111. data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +0 -9
  112. data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +0 -208
  113. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +0 -9
  114. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +0 -903
  115. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +0 -9
  116. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +0 -8
  117. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +0 -42
  118. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +0 -5
  119. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +0 -47
  120. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +0 -30
  121. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +0 -9
  122. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +0 -7
  123. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +0 -19
  124. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +0 -31
  125. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +0 -18
  126. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +0 -12
  127. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +0 -11
  128. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +0 -13
  129. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +0 -95
  130. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +0 -31
  131. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +0 -58
  132. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +0 -24
  133. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +0 -17
  134. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +0 -23
  135. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +0 -22
  136. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +0 -30
  137. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +0 -20
  138. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +0 -15
  139. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +0 -26
  140. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +0 -21
  141. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +0 -131
  142. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +0 -14
  143. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +0 -33
  144. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +0 -21
  145. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +0 -63
  146. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +0 -55
  147. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +0 -35
  148. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +0 -7
  149. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +0 -10
  150. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +0 -22
  151. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +0 -29
  152. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +0 -17
  153. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +0 -13
  154. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +0 -10
  155. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +0 -8
  156. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +0 -6
  157. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +0 -30
  158. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +0 -10
  159. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +0 -10
  160. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +0 -5
  161. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +0 -15
  162. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +0 -3
  163. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +0 -40
  164. data/Rakefile +0 -567
  165. data/Steepfile +0 -18
  166. data/benchmark.rb +0 -406
  167. data/docs/ARCHITECTURE.md +0 -514
  168. data/docs/DEVELOPMENT.md +0 -133
  169. data/docs/PEER_LIBRARIES.md +0 -121
  170. data/docs/QUALITY_ASSURANCE.md +0 -25
  171. data/docs/SPEC_SOURCES.md +0 -48
  172. data/docs/assets/benchmark_results.svg +0 -144
  173. data/docs/assets/lsp_autocompletion.png +0 -0
  174. data/docs/assets/playground_preview.png +0 -0
  175. data/docs/coi-serviceworker.js +0 -82
  176. data/docs/office_thread.js +0 -77
  177. data/docs/preview.html +0 -719
  178. data/docs/visual/VisualGallery.md +0 -4672
  179. data/docs/wasm/ruby.wasm +0 -0
  180. data/docs/wasm/wasm_doc_helper.css +0 -351
  181. data/docs/wasm/wasm_doc_helper.js +0 -439
  182. data/docs/zeta.js +0 -1107
  183. data/rbs_collection.lock.yaml +0 -252
  184. data/rbs_collection.yaml +0 -19
  185. data/vendor/sdk_runner/Program.cs +0 -93
  186. data/vendor/sdk_runner/sdk_runner.csproj +0 -13
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Xlsxrb
6
+ module Ooxml
7
+ class Writer
8
+ # Mixin containing Stylesheet XML generation logic.
9
+ module StylesXml
10
+ # : () -> untyped
11
+ def generate_styles_xml
12
+ parts = [
13
+ XML_HEADER,
14
+ %(<styleSheet xmlns="#{SSML_NS}">)
15
+ ]
16
+
17
+ # numFmts
18
+ unless @num_fmts.empty?
19
+ parts << %(<numFmts count="#{@num_fmts.size}">)
20
+ @num_fmts.each do |nf|
21
+ parts << %(<numFmt numFmtId="#{nf[:num_fmt_id]}" formatCode="#{xml_escape(nf[:format_code])}"/>)
22
+ end
23
+ parts << "</numFmts>"
24
+ end
25
+
26
+ # fonts
27
+ parts << %(<fonts count="#{@fonts.size}">)
28
+ @fonts.each { |f| parts << emit_font_xml(f) }
29
+ parts << "</fonts>"
30
+
31
+ # fills
32
+ parts << %(<fills count="#{@fills.size}">)
33
+ @fills.each { |f| parts << emit_fill_xml(f) }
34
+ parts << "</fills>"
35
+
36
+ # borders
37
+ parts << %(<borders count="#{@borders.size}">)
38
+ @borders.each { |b| parts << emit_border_xml(b) }
39
+ parts << "</borders>"
40
+
41
+ # cellStyleXfs
42
+ parts << %(<cellStyleXfs count="#{@cell_style_xfs.size}">)
43
+ @cell_style_xfs.each do |xf|
44
+ parts << %(<xf numFmtId="#{xf[:num_fmt_id]}" fontId="#{xf[:font_id]}" fillId="#{xf[:fill_id]}" borderId="#{xf[:border_id]}"/>)
45
+ end
46
+ parts << "</cellStyleXfs>"
47
+
48
+ # cellXfs
49
+ parts << %(<cellXfs count="#{@xf_entries.size}">)
50
+ @xf_entries.each do |xf|
51
+ apply_attrs = []
52
+ apply_attrs << ' applyNumberFormat="1"' if xf[:num_fmt_id].positive?
53
+ apply_attrs << ' applyFont="1"' if xf[:font_id].positive?
54
+ apply_attrs << ' applyFill="1"' if xf[:fill_id].positive?
55
+ apply_attrs << ' applyBorder="1"' if xf[:border_id].positive?
56
+ apply_attrs << ' applyAlignment="1"' if xf[:alignment]
57
+ apply_attrs << ' applyProtection="1"' if xf[:protection]
58
+ apply_attrs << ' quotePrefix="1"' if xf[:quote_prefix]
59
+ apply_attrs << ' pivotButton="1"' if xf[:pivot_button]
60
+ children = []
61
+ children << emit_alignment_xml(xf[:alignment]) if xf[:alignment]
62
+ children << emit_protection_xml(xf[:protection]) if xf[:protection]
63
+ xf_id = xf[:xf_id] || 0
64
+ parts << if children.any?
65
+ %(<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>)
66
+ else
67
+ %(<xf numFmtId="#{xf[:num_fmt_id]}" fontId="#{xf[:font_id]}" fillId="#{xf[:fill_id]}" borderId="#{xf[:border_id]}" xfId="#{xf_id}"#{apply_attrs.join}/>)
68
+ end
69
+ end
70
+ parts << "</cellXfs>"
71
+
72
+ # cellStyles
73
+ parts << %(<cellStyles count="#{@cell_style_names.size}">)
74
+ @cell_style_names.each do |cs|
75
+ cs_attrs = %(name="#{xml_escape(cs[:name])}" xfId="#{cs[:xf_id]}")
76
+ cs_attrs << %( builtinId="#{cs[:builtin_id]}") if cs[:builtin_id]
77
+ cs_attrs << %( iLevel="#{cs[:i_level]}") if cs[:i_level]
78
+ cs_attrs << ' hidden="1"' if cs[:hidden]
79
+ cs_attrs << ' customBuiltin="1"' if cs[:custom_builtin]
80
+ parts << "<cellStyle #{cs_attrs}/>"
81
+ end
82
+ parts << "</cellStyles>"
83
+
84
+ # dxfs
85
+ unless @dxfs.empty?
86
+ parts << %(<dxfs count="#{@dxfs.size}">)
87
+ @dxfs.each { |d| parts << emit_dxf_xml(d) }
88
+ parts << "</dxfs>"
89
+ end
90
+
91
+ # tableStyles
92
+ ts_styles = @table_styles[:styles] || []
93
+ unless ts_styles.empty? && @table_styles[:default_table_style].nil? && @table_styles[:default_pivot_style].nil?
94
+ ts_attrs = [%(count="#{ts_styles.size}")]
95
+ ts_attrs << %(defaultTableStyle="#{xml_escape(@table_styles[:default_table_style])}") if @table_styles[:default_table_style]
96
+ ts_attrs << %(defaultPivotStyle="#{xml_escape(@table_styles[:default_pivot_style])}") if @table_styles[:default_pivot_style]
97
+ if ts_styles.empty?
98
+ parts << "<tableStyles #{ts_attrs.join(" ")}/>"
99
+ else
100
+ parts << "<tableStyles #{ts_attrs.join(" ")}>"
101
+ ts_styles.each do |ts|
102
+ s_attrs = [%(name="#{xml_escape(ts[:name])}")]
103
+ s_attrs << %(pivot="0") if ts[:pivot] == false
104
+ s_attrs << %(table="0") if ts[:table] == false
105
+ s_attrs << %(count="#{ts[:elements].size}") unless ts[:elements].empty?
106
+ if ts[:elements].empty?
107
+ parts << "<tableStyle #{s_attrs.join(" ")}/>"
108
+ else
109
+ parts << "<tableStyle #{s_attrs.join(" ")}>"
110
+ ts[:elements].each do |el|
111
+ el_attrs = [%(type="#{el[:type]}")]
112
+ el_attrs << %(size="#{el[:size]}") if el[:size] && el[:size] != 1
113
+ el_attrs << %(dxfId="#{el[:dxf_id]}") if el[:dxf_id]
114
+ parts << "<tableStyleElement #{el_attrs.join(" ")}/>"
115
+ end
116
+ parts << "</tableStyle>"
117
+ end
118
+ end
119
+ parts << "</tableStyles>"
120
+ end
121
+ end
122
+
123
+ # colors
124
+ unless @indexed_colors.empty? && @mru_colors.empty?
125
+ parts << "<colors>"
126
+ unless @indexed_colors.empty?
127
+ parts << "<indexedColors>"
128
+ @indexed_colors.each { |c| parts << %(<rgbColor rgb="#{c}"/>) }
129
+ parts << "</indexedColors>"
130
+ end
131
+ unless @mru_colors.empty?
132
+ parts << "<mruColors>"
133
+ @mru_colors.each { |c| parts << emit_color_xml(c) }
134
+ parts << "</mruColors>"
135
+ end
136
+ parts << "</colors>"
137
+ end
138
+
139
+ parts << "</styleSheet>"
140
+ parts.join
141
+ end
142
+
143
+ # : (untyped alignment) -> ::String
144
+ def emit_alignment_xml(alignment)
145
+ attrs = []
146
+ attrs << %(horizontal="#{alignment[:horizontal]}") if alignment[:horizontal]
147
+ attrs << %(vertical="#{alignment[:vertical]}") if alignment[:vertical]
148
+ attrs << %(wrapText="1") if alignment[:wrap_text]
149
+ attrs << %(textRotation="#{alignment[:text_rotation]}") if alignment[:text_rotation]
150
+ attrs << %(indent="#{alignment[:indent]}") if alignment[:indent]
151
+ attrs << %(relativeIndent="#{alignment[:relative_indent]}") if alignment[:relative_indent]
152
+ attrs << %(shrinkToFit="1") if alignment[:shrink_to_fit]
153
+ attrs << %(readingOrder="#{alignment[:reading_order]}") if alignment[:reading_order]
154
+ attrs << %(justifyLastLine="1") if alignment[:justify_last_line]
155
+ "<alignment #{attrs.join(" ")}/>"
156
+ end
157
+
158
+ # : (untyped protection) -> ::String
159
+ def emit_protection_xml(protection)
160
+ attrs = []
161
+ attrs << %(locked="#{protection[:locked] ? "1" : "0"}") unless protection[:locked].nil?
162
+ attrs << %(hidden="#{protection[:hidden] ? "1" : "0"}") unless protection[:hidden].nil?
163
+ "<protection #{attrs.join(" ")}/>"
164
+ end
165
+
166
+ # : (untyped source, ?tag: ::String) -> (::String | ::String | ::String | ::String | "")
167
+ def emit_color_xml(source, tag: "color")
168
+ if source[:auto]
169
+ %(<#{tag} auto="1"/>)
170
+ elsif source[:color] || source[:rgb]
171
+ %(<#{tag} rgb="#{source[:color] || source[:rgb]}"/>)
172
+ elsif source[:theme]
173
+ attrs = [%(theme="#{source[:theme]}")]
174
+ attrs << %(tint="#{source[:tint]}") if source[:tint]
175
+ %(<#{tag} #{attrs.join(" ")}/>)
176
+ elsif source[:indexed]
177
+ %(<#{tag} indexed="#{source[:indexed]}"/>)
178
+ else
179
+ ""
180
+ end
181
+ end
182
+
183
+ # : (untyped font) -> untyped
184
+ def emit_font_xml(font)
185
+ parts = ["<font>"]
186
+ parts << "<b/>" if font[:bold]
187
+ parts << "<i/>" if font[:italic]
188
+ parts << "<strike/>" if font[:strike]
189
+ parts << "<shadow/>" if font[:shadow]
190
+ parts << "<outline/>" if font[:outline]
191
+ parts << "<condense/>" if font[:condense]
192
+ parts << "<extend/>" if font[:extend]
193
+ if font[:underline]
194
+ parts << if font[:underline] == true
195
+ "<u/>"
196
+ else
197
+ %(<u val="#{font[:underline]}"/>)
198
+ end
199
+ end
200
+ parts << %(<vertAlign val="#{font[:vert_align]}"/>) if font[:vert_align]
201
+ parts << %(<sz val="#{font[:sz]}"/>) if font[:sz]
202
+ parts << emit_color_xml(font)
203
+ parts << %(<name val="#{xml_escape(font[:name])}"/>) if font[:name]
204
+ parts << %(<family val="#{font[:family]}"/>) if font[:family]
205
+ parts << %(<charset val="#{font[:charset]}"/>) if font[:charset]
206
+ parts << %(<scheme val="#{font[:scheme]}"/>) if font[:scheme]
207
+ parts << "</font>"
208
+ parts.join
209
+ end
210
+
211
+ # : (untyped fill) -> (untyped | ::String)
212
+ def emit_fill_xml(fill)
213
+ return emit_gradient_fill_xml(fill[:gradient]) if fill[:gradient]
214
+
215
+ has_fg = fill[:fg_color] || fill[:fg_color_theme] || fill[:fg_color_indexed] || fill[:fg_color_auto]
216
+ has_bg = fill[:bg_color] || fill[:bg_color_theme] || fill[:bg_color_indexed] || fill[:bg_color_auto]
217
+ return "<fill><patternFill patternType=\"#{fill[:pattern]}\"/></fill>" if fill[:pattern] && !has_fg && !has_bg
218
+
219
+ parts = ["<fill>"]
220
+ pt = fill[:pattern] || "solid"
221
+ parts << %(<patternFill patternType="#{pt}">)
222
+ parts << emit_fill_color_xml("fgColor", fill, :fg)
223
+ parts << emit_fill_color_xml("bgColor", fill, :bg)
224
+ parts << "</patternFill>"
225
+ parts << "</fill>"
226
+ parts.join
227
+ end
228
+
229
+ # : (untyped tag, untyped fill, untyped prefix) -> (::String | ::String | ::String | ::String | "")
230
+ def emit_fill_color_xml(tag, fill, prefix)
231
+ if fill[:"#{prefix}_color"]
232
+ %(<#{tag} rgb="#{fill[:"#{prefix}_color"]}"/>)
233
+ elsif fill[:"#{prefix}_color_theme"]
234
+ attrs = [%(theme="#{fill[:"#{prefix}_color_theme"]}")]
235
+ attrs << %(tint="#{fill[:"#{prefix}_color_tint"]}") if fill[:"#{prefix}_color_tint"]
236
+ %(<#{tag} #{attrs.join(" ")}/>)
237
+ elsif fill[:"#{prefix}_color_indexed"]
238
+ %(<#{tag} indexed="#{fill[:"#{prefix}_color_indexed"]}"/>)
239
+ elsif fill[:"#{prefix}_color_auto"]
240
+ %(<#{tag} auto="1"/>)
241
+ else
242
+ ""
243
+ end
244
+ end
245
+
246
+ # : (untyped gradient) -> untyped
247
+ def emit_gradient_fill_xml(gradient)
248
+ attrs = []
249
+ attrs << %(type="#{gradient[:type]}") if gradient[:type]
250
+ attrs << %(degree="#{gradient[:degree]}") if gradient[:degree]
251
+ attrs << %(left="#{gradient[:left]}") if gradient[:left]
252
+ attrs << %(right="#{gradient[:right]}") if gradient[:right]
253
+ attrs << %(top="#{gradient[:top]}") if gradient[:top]
254
+ attrs << %(bottom="#{gradient[:bottom]}") if gradient[:bottom]
255
+ parts = ["<fill>"]
256
+ parts << "<gradientFill#{" #{attrs.join(" ")}" unless attrs.empty?}"
257
+ if gradient[:stops]&.any?
258
+ parts[-1] = "#{parts[-1]}>"
259
+ gradient[:stops].each do |stop|
260
+ parts << %(<stop position="#{stop[:position]}">#{emit_color_xml(stop)}</stop>)
261
+ end
262
+ parts << "</gradientFill>"
263
+ else
264
+ parts[-1] = "#{parts[-1]}/>"
265
+ end
266
+ parts << "</fill>"
267
+ parts.join
268
+ end
269
+
270
+ # : (untyped brk, default_max: untyped) -> ::String
271
+ def emit_brk_xml(brk, default_max:)
272
+ if brk.is_a?(Hash)
273
+ attrs = %(id="#{brk[:id]}")
274
+ attrs << %( min="#{brk[:min]}") if brk[:min]
275
+ attrs << %( max="#{brk.fetch(:max, default_max)}")
276
+ attrs << ' man="1"' if brk.fetch(:man, true)
277
+ attrs << ' pt="1"' if brk[:pt]
278
+ "<brk #{attrs}/>"
279
+ else
280
+ %(<brk id="#{brk}" max="#{default_max}" man="1"/>)
281
+ end
282
+ end
283
+
284
+ # : (untyped bdr) -> untyped
285
+ def emit_border_xml(bdr)
286
+ border_attrs = []
287
+ border_attrs << ' diagonalUp="1"' if bdr[:diagonal_up]
288
+ border_attrs << ' diagonalDown="1"' if bdr[:diagonal_down]
289
+ border_attrs << ' outline="0"' if bdr[:outline] == false
290
+ parts = ["<border#{border_attrs.join}>"]
291
+ %i[left right top bottom diagonal vertical horizontal].each do |side|
292
+ s = bdr[side]
293
+ if s.is_a?(Hash)
294
+ parts << %(<#{side} style="#{s[:style]}">)
295
+ parts << emit_color_xml(s)
296
+ parts << "</#{side}>"
297
+ else
298
+ parts << "<#{side}/>"
299
+ end
300
+ end
301
+ parts << "</border>"
302
+ parts.join
303
+ end
304
+
305
+ # : (untyped dxf) -> untyped
306
+ def emit_dxf_xml(dxf)
307
+ parts = ["<dxf>"]
308
+ parts << emit_font_xml(dxf[:font]) if dxf[:font]
309
+ if dxf[:num_fmt]
310
+ nf = dxf[:num_fmt]
311
+ parts << %(<numFmt numFmtId="#{nf[:num_fmt_id]}" formatCode="#{xml_escape(nf[:format_code])}"/>)
312
+ end
313
+ parts << emit_fill_xml(dxf[:fill]) if dxf[:fill]
314
+ parts << emit_alignment_xml(dxf[:alignment]) if dxf[:alignment]
315
+ parts << emit_border_xml(dxf[:border]) if dxf[:border]
316
+ parts << emit_protection_xml(dxf[:protection]) if dxf[:protection]
317
+ parts << "</dxf>"
318
+ parts.join
319
+ end
320
+
321
+ # : (untyped cell_address) -> untyped
322
+ def validate_cell_address!(cell_address)
323
+ raise ArgumentError, "cell address must be a String" unless cell_address.is_a?(String)
324
+
325
+ match = cell_address.match(CELL_ADDRESS_PATTERN)
326
+ raise ArgumentError, "invalid cell address: #{cell_address.inspect}" unless match
327
+
328
+ row_num = match[2].to_i
329
+ raise ArgumentError, "row out of range: #{row_num}" unless row_num.between?(1, MAX_ROW)
330
+
331
+ col_index = column_letter_to_index(match[1])
332
+ raise ArgumentError, "column out of range: #{match[1]}" unless col_index.between?(1, MAX_COLUMN_INDEX)
333
+ end
334
+
335
+ # : (untyped letters) -> untyped
336
+ def column_letter_to_index(letters)
337
+ letters.chars.reduce(0) { |sum, char| (sum * 26) + (char.ord - "A".ord + 1) }
338
+ end
339
+
340
+ # Extracts the column letter(s) from a cell address, e.g. "A" from "A1".
341
+ # : (untyped cell_address) -> untyped
342
+ def extract_column_letter(cell_address)
343
+ cell_address.match(/^([A-Z]+)/)[1]
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end