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,1796 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ require "rexml/parsers/sax2parser"
6
+ require "rexml/sax2listener"
7
+
8
+ module Xlsxrb
9
+ module Ooxml
10
+ class Reader
11
+ # SAX2 listener for parsing <autoFilter> element.
12
+ class AutoFilterListener
13
+ include REXML::SAX2Listener
14
+
15
+ attr_reader :ref, :filter_columns
16
+
17
+ def initialize
18
+ @ref = nil
19
+ @filter_columns = {}
20
+ @current_col_id = nil
21
+ @current_filter = nil
22
+ @inside_custom_filters = false
23
+ @custom_filters_list = []
24
+ @custom_filters_and = false
25
+ @filter_values = []
26
+ @filter_blank = false
27
+ end
28
+
29
+ def start_element(_uri, local_name, qname, attributes)
30
+ name = element_name(local_name, qname)
31
+ case name
32
+ when "autoFilter"
33
+ @ref = attributes["ref"]
34
+ when "filterColumn"
35
+ @current_col_id = attributes["colId"]&.to_i
36
+ @fc_hidden_button = attributes["hiddenButton"] == "1"
37
+ @fc_show_button = attributes["showButton"] == "0" ? false : nil
38
+ when "filters"
39
+ @filter_blank = attributes["blank"] == "1"
40
+ @filter_calendar_type = attributes["calendarType"]
41
+ @filter_values = []
42
+ @date_group_items = []
43
+ when "filter"
44
+ val = attributes["val"]
45
+ @filter_values << val if val
46
+ when "dateGroupItem"
47
+ dg = { date_time_grouping: attributes["dateTimeGrouping"] }
48
+ dg[:year] = attributes["year"].to_i if attributes["year"]
49
+ dg[:month] = attributes["month"].to_i if attributes["month"]
50
+ dg[:day] = attributes["day"].to_i if attributes["day"]
51
+ dg[:hour] = attributes["hour"].to_i if attributes["hour"]
52
+ dg[:minute] = attributes["minute"].to_i if attributes["minute"]
53
+ dg[:second] = attributes["second"].to_i if attributes["second"]
54
+ @date_group_items << dg
55
+ when "customFilters"
56
+ @inside_custom_filters = true
57
+ @custom_filters_and = attributes["and"] == "1"
58
+ @custom_filters_list = []
59
+ when "customFilter"
60
+ @custom_filters_list << { operator: attributes["operator"], val: attributes["val"] } if @inside_custom_filters
61
+ when "dynamicFilter"
62
+ df = { type: :dynamic, dynamic_type: attributes["type"] }
63
+ df[:val] = attributes["val"].to_f if attributes["val"]
64
+ df[:val_iso] = attributes["valIso"] if attributes["valIso"]
65
+ df[:max_val] = attributes["maxVal"].to_f if attributes["maxVal"]
66
+ df[:max_val_iso] = attributes["maxValIso"] if attributes["maxValIso"]
67
+ @current_filter = df
68
+ when "top10"
69
+ t10 = {
70
+ type: :top10,
71
+ top: attributes["top"] == "1",
72
+ percent: attributes["percent"] == "1",
73
+ val: attributes["val"]&.to_f&.to_i
74
+ }
75
+ t10[:filter_val] = attributes["filterVal"].to_f if attributes["filterVal"]
76
+ @current_filter = t10
77
+ when "colorFilter"
78
+ cf = { type: :color_filter, dxf_id: attributes["dxfId"]&.to_i }
79
+ cf[:cell_color] = false if attributes["cellColor"] == "0"
80
+ @current_filter = cf
81
+ when "iconFilter"
82
+ icf = { type: :icon_filter, icon_set: attributes["iconSet"] }
83
+ icf[:icon_id] = attributes["iconId"].to_i if attributes["iconId"]
84
+ @current_filter = icf
85
+ end
86
+ end
87
+
88
+ def end_element(_uri, local_name, qname)
89
+ name = element_name(local_name, qname)
90
+ case name
91
+ when "filters"
92
+ f = { type: :filters }
93
+ f[:blank] = true if @filter_blank
94
+ f[:calendar_type] = @filter_calendar_type if @filter_calendar_type
95
+ f[:values] = @filter_values unless @filter_values.empty?
96
+ f[:date_group_items] = @date_group_items unless @date_group_items.empty?
97
+ @current_filter = f
98
+ when "customFilters"
99
+ if @custom_filters_list.size == 1
100
+ cf = @custom_filters_list.first
101
+ @current_filter = { type: :custom, operator: cf[:operator], val: cf[:val] }
102
+ else
103
+ @current_filter = { type: :custom, filters: @custom_filters_list, and: @custom_filters_and }
104
+ end
105
+ @inside_custom_filters = false
106
+ when "filterColumn"
107
+ if @current_col_id && @current_filter
108
+ @current_filter[:hidden_button] = true if @fc_hidden_button
109
+ @current_filter[:show_button] = false if @fc_show_button == false
110
+ @filter_columns[@current_col_id] = @current_filter
111
+ end
112
+ @current_col_id = nil
113
+ @current_filter = nil
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def element_name(local_name, qname)
120
+ if local_name.nil? || local_name.empty?
121
+ qname.to_s.split(":").last
122
+ else
123
+ local_name
124
+ end
125
+ end
126
+ end
127
+
128
+ # SAX2 listener for parsing <sortState> element.
129
+ class SortStateListener
130
+ include REXML::SAX2Listener
131
+
132
+ attr_reader :sort_state
133
+
134
+ def initialize
135
+ @sort_state = nil
136
+ @inside_sort_state = false
137
+ @sort_conditions = []
138
+ end
139
+
140
+ def start_element(_uri, local_name, qname, attributes)
141
+ name = element_name(local_name, qname)
142
+ case name
143
+ when "sortState"
144
+ @inside_sort_state = true
145
+ ss = { ref: attributes["ref"], sort_conditions: [] }
146
+ ss[:column_sort] = true if attributes["columnSort"] == "1"
147
+ ss[:case_sensitive] = true if attributes["caseSensitive"] == "1"
148
+ ss[:sort_method] = attributes["sortMethod"] if attributes["sortMethod"]
149
+ @sort_state = ss
150
+ when "sortCondition"
151
+ return unless @inside_sort_state
152
+
153
+ sc = { ref: attributes["ref"] }
154
+ sc[:descending] = true if attributes["descending"] == "1"
155
+ sc[:sort_by] = attributes["sortBy"] if attributes["sortBy"]
156
+ sc[:custom_list] = attributes["customList"] if attributes["customList"]
157
+ dxf = attributes["dxfId"]
158
+ sc[:dxf_id] = dxf.to_i if dxf
159
+ sc[:icon_set] = attributes["iconSet"] if attributes["iconSet"]
160
+ iid = attributes["iconId"]
161
+ sc[:icon_id] = iid.to_i if iid
162
+ @sort_conditions << sc
163
+ end
164
+ end
165
+
166
+ def end_element(_uri, local_name, qname)
167
+ name = element_name(local_name, qname)
168
+ return unless name == "sortState" && @inside_sort_state
169
+
170
+ @sort_state[:sort_conditions] = @sort_conditions
171
+ @inside_sort_state = false
172
+ end
173
+
174
+ private
175
+
176
+ def element_name(local_name, qname)
177
+ if local_name.nil? || local_name.empty?
178
+ qname.to_s.split(":").last
179
+ else
180
+ local_name
181
+ end
182
+ end
183
+ end
184
+
185
+ # SAX2 listener for parsing docProps/app.xml.
186
+ class AppPropertiesListener
187
+ include REXML::SAX2Listener
188
+
189
+ attr_reader :properties
190
+
191
+ def initialize
192
+ @properties = {}
193
+ @current_field = nil
194
+ @text_buffer = +""
195
+ @inside_vector = false
196
+ @vector_items = []
197
+ @heading_pairs = []
198
+ @titles_of_parts = []
199
+ @inside_heading_pairs = false
200
+ @inside_titles_of_parts = false
201
+ @inside_variant = false
202
+ end
203
+
204
+ def start_element(_uri, local_name, qname, _attributes)
205
+ name = element_name(local_name, qname)
206
+ case name
207
+ when "Application", "AppVersion"
208
+ @current_field = name
209
+ @text_buffer = +""
210
+ when "HeadingPairs"
211
+ @inside_heading_pairs = true
212
+ when "TitlesOfParts"
213
+ @inside_titles_of_parts = true
214
+ when "variant"
215
+ @inside_variant = true
216
+ @text_buffer = +""
217
+ when "lpstr", "i4"
218
+ @text_buffer = +""
219
+ end
220
+ end
221
+
222
+ def characters(text)
223
+ @text_buffer << text
224
+ end
225
+
226
+ def end_element(_uri, local_name, qname)
227
+ name = element_name(local_name, qname)
228
+ case name
229
+ when "Application"
230
+ @properties[:application] = @text_buffer.dup
231
+ @current_field = nil
232
+ when "AppVersion"
233
+ @properties[:app_version] = @text_buffer.dup
234
+ @current_field = nil
235
+ when "lpstr"
236
+ if @inside_titles_of_parts
237
+ @titles_of_parts << @text_buffer.dup
238
+ elsif @inside_heading_pairs && @inside_variant
239
+ @vector_items << @text_buffer.dup
240
+ end
241
+ when "i4"
242
+ @vector_items << @text_buffer.to_i if @inside_heading_pairs && @inside_variant
243
+ when "variant"
244
+ @inside_variant = false
245
+ when "HeadingPairs"
246
+ # Convert flat array to pairs: [label, count, label, count, ...]
247
+ @heading_pairs = @vector_items.each_slice(2).to_a
248
+ @vector_items = []
249
+ @inside_heading_pairs = false
250
+ @properties[:heading_pairs] = @heading_pairs
251
+ when "TitlesOfParts"
252
+ @inside_titles_of_parts = false
253
+ @properties[:titles_of_parts] = @titles_of_parts
254
+ end
255
+ end
256
+
257
+ private
258
+
259
+ def element_name(local_name, qname)
260
+ if local_name.nil? || local_name.empty?
261
+ qname.to_s.split(":").last
262
+ else
263
+ local_name
264
+ end
265
+ end
266
+ end
267
+
268
+ # SAX2 listener for parsing docProps/core.xml.
269
+ class CorePropertiesListener
270
+ include REXML::SAX2Listener
271
+
272
+ attr_reader :properties
273
+
274
+ FIELD_MAP = {
275
+ "title" => :title,
276
+ "subject" => :subject,
277
+ "creator" => :creator,
278
+ "keywords" => :keywords,
279
+ "description" => :description,
280
+ "lastModifiedBy" => :last_modified_by,
281
+ "revision" => :revision,
282
+ "created" => :created,
283
+ "modified" => :modified,
284
+ "category" => :category,
285
+ "contentStatus" => :content_status,
286
+ "language" => :language
287
+ }.freeze
288
+
289
+ def initialize
290
+ @properties = {}
291
+ @current_field = nil
292
+ @text_buffer = +""
293
+ end
294
+
295
+ def start_element(_uri, local_name, qname, _attributes)
296
+ name = element_name(local_name, qname)
297
+ return unless FIELD_MAP.key?(name)
298
+
299
+ @current_field = FIELD_MAP[name]
300
+ @text_buffer = +""
301
+ end
302
+
303
+ def characters(text)
304
+ @text_buffer << text if @current_field
305
+ end
306
+
307
+ def end_element(_uri, local_name, qname)
308
+ name = element_name(local_name, qname)
309
+ return unless @current_field && FIELD_MAP.key?(name)
310
+
311
+ @properties[@current_field] = @text_buffer.dup unless @text_buffer.empty?
312
+ @current_field = nil
313
+ end
314
+
315
+ private
316
+
317
+ def element_name(local_name, qname)
318
+ if local_name.nil? || local_name.empty?
319
+ qname.to_s.split(":").last
320
+ else
321
+ local_name
322
+ end
323
+ end
324
+ end
325
+
326
+ # SAX2 listener for parsing custom properties (docProps/custom.xml).
327
+ class CustomPropertiesListener
328
+ include REXML::SAX2Listener
329
+
330
+ attr_reader :properties
331
+
332
+ def initialize
333
+ @properties = []
334
+ @current_name = nil
335
+ @current_type = nil
336
+ @text_buffer = +""
337
+ end
338
+
339
+ def start_element(_uri, local_name, qname, attributes)
340
+ name = element_name(local_name, qname)
341
+ case name
342
+ when "property"
343
+ @current_name = attributes["name"]
344
+ when "lpwstr"
345
+ @current_type = :string
346
+ @text_buffer = +""
347
+ when "i4"
348
+ @current_type = :number
349
+ @text_buffer = +""
350
+ when "r8"
351
+ @current_type = :float
352
+ @text_buffer = +""
353
+ when "bool"
354
+ @current_type = :bool
355
+ @text_buffer = +""
356
+ when "filetime"
357
+ @current_type = :date
358
+ @text_buffer = +""
359
+ end
360
+ end
361
+
362
+ def characters(text)
363
+ @text_buffer << text if @current_type
364
+ end
365
+
366
+ def end_element(_uri, local_name, qname)
367
+ name = element_name(local_name, qname)
368
+ case name
369
+ when "property"
370
+ @current_name = nil
371
+ when "lpwstr", "i4", "r8", "bool", "filetime"
372
+ if @current_name
373
+ value = case @current_type
374
+ when :number then @text_buffer.to_i
375
+ when :float then @text_buffer.to_f
376
+ when :bool then @text_buffer == "true"
377
+ else @text_buffer.dup
378
+ end
379
+ @properties << { name: @current_name, value: value, type: @current_type }
380
+ end
381
+ @current_type = nil
382
+ end
383
+ end
384
+
385
+ private
386
+
387
+ def element_name(local_name, qname)
388
+ if local_name.nil? || local_name.empty?
389
+ qname.to_s.split(":").last
390
+ else
391
+ local_name
392
+ end
393
+ end
394
+ end
395
+
396
+ # SAX2 listener for parsing <sheetPr> element (tabColor, outlinePr).
397
+ class SheetPropertiesListener
398
+ include REXML::SAX2Listener
399
+
400
+ attr_reader :properties
401
+
402
+ def initialize
403
+ @properties = {}
404
+ @inside_sheet_pr = false
405
+ end
406
+
407
+ def start_element(_uri, local_name, qname, attributes)
408
+ name = element_name(local_name, qname)
409
+ case name
410
+ when "sheetPr"
411
+ @inside_sheet_pr = true
412
+ sh = attributes["syncHorizontal"]
413
+ @properties[:sync_horizontal] = %w[1 true].include?(sh) unless sh.nil?
414
+ sv = attributes["syncVertical"]
415
+ @properties[:sync_vertical] = %w[1 true].include?(sv) unless sv.nil?
416
+ @properties[:sync_ref] = attributes["syncRef"] if attributes["syncRef"]
417
+ te = attributes["transitionEvaluation"]
418
+ @properties[:transition_evaluation] = %w[1 true].include?(te) unless te.nil?
419
+ tent = attributes["transitionEntry"]
420
+ @properties[:transition_entry] = %w[1 true].include?(tent) unless tent.nil?
421
+ @properties[:code_name] = attributes["codeName"] if attributes["codeName"]
422
+ fm = attributes["filterMode"]
423
+ @properties[:filter_mode] = %w[1 true].include?(fm) unless fm.nil?
424
+ pub = attributes["published"]
425
+ @properties[:published] = %w[1 true].include?(pub) unless pub.nil?
426
+ efcc = attributes["enableFormatConditionsCalculation"]
427
+ @properties[:enable_format_conditions_calculation] = %w[1 true].include?(efcc) unless efcc.nil?
428
+ when "tabColor"
429
+ if @inside_sheet_pr
430
+ @properties[:tab_color] = attributes["rgb"] if attributes["rgb"]
431
+ @properties[:tab_color_theme] = attributes["theme"].to_i if attributes["theme"]
432
+ @properties[:tab_color_tint] = attributes["tint"].to_f if attributes["tint"]
433
+ @properties[:tab_color_indexed] = attributes["indexed"].to_i if attributes["indexed"]
434
+ @properties[:tab_color_auto] = %w[1 true].include?(attributes["auto"]) if attributes["auto"]
435
+ end
436
+ when "outlinePr"
437
+ if @inside_sheet_pr
438
+ apply_s = attributes["applyStyles"]
439
+ @properties[:apply_styles] = %w[1 true].include?(apply_s) unless apply_s.nil?
440
+ sb = attributes["summaryBelow"]
441
+ @properties[:summary_below] = %w[1 true].include?(sb) unless sb.nil?
442
+ sr = attributes["summaryRight"]
443
+ @properties[:summary_right] = %w[1 true].include?(sr) unless sr.nil?
444
+ sos = attributes["showOutlineSymbols"]
445
+ @properties[:show_outline_symbols] = %w[1 true].include?(sos) unless sos.nil?
446
+ end
447
+ when "pageSetUpPr"
448
+ if @inside_sheet_pr
449
+ ftp = attributes["fitToPage"]
450
+ @properties[:fit_to_page] = %w[1 true].include?(ftp) unless ftp.nil?
451
+ apb = attributes["autoPageBreaks"]
452
+ @properties[:auto_page_breaks] = %w[1 true].include?(apb) unless apb.nil?
453
+ end
454
+ when "sheetCalcPr"
455
+ fcol = attributes["fullCalcOnLoad"]
456
+ @properties[:full_calc_on_load] = %w[1 true].include?(fcol) unless fcol.nil?
457
+ end
458
+ end
459
+
460
+ def end_element(_uri, local_name, qname)
461
+ name = element_name(local_name, qname)
462
+ @inside_sheet_pr = false if name == "sheetPr"
463
+ end
464
+
465
+ private
466
+
467
+ def element_name(local_name, qname)
468
+ if local_name.nil? || local_name.empty?
469
+ qname.to_s.split(":").last
470
+ else
471
+ local_name
472
+ end
473
+ end
474
+ end
475
+
476
+ # SAX2 listener for parsing <sheetProtection> element.
477
+ class SheetProtectionListener
478
+ include REXML::SAX2Listener
479
+
480
+ attr_reader :protection
481
+
482
+ def initialize
483
+ @protection = nil
484
+ end
485
+
486
+ BOOL_ATTRS = %i[sheet objects scenarios select_locked_cells select_unlocked_cells].freeze
487
+ FALSE_ATTRS = %i[format_cells format_columns format_rows insert_columns insert_rows
488
+ insert_hyperlinks delete_columns delete_rows sort auto_filter pivot_tables].freeze
489
+ ATTR_MAP = {
490
+ "sheet" => :sheet, "objects" => :objects, "scenarios" => :scenarios,
491
+ "formatCells" => :format_cells, "formatColumns" => :format_columns,
492
+ "formatRows" => :format_rows, "insertColumns" => :insert_columns,
493
+ "insertRows" => :insert_rows, "insertHyperlinks" => :insert_hyperlinks,
494
+ "deleteColumns" => :delete_columns, "deleteRows" => :delete_rows,
495
+ "selectLockedCells" => :select_locked_cells, "sort" => :sort,
496
+ "autoFilter" => :auto_filter, "pivotTables" => :pivot_tables,
497
+ "selectUnlockedCells" => :select_unlocked_cells,
498
+ "password" => :password, "algorithmName" => :algorithm_name,
499
+ "hashValue" => :hash_value, "saltValue" => :salt_value, "spinCount" => :spin_count
500
+ }.freeze
501
+
502
+ def start_element(_uri, local_name, qname, attributes)
503
+ name = element_name(local_name, qname)
504
+ return unless name == "sheetProtection"
505
+
506
+ prot = {}
507
+ ATTR_MAP.each do |xml_attr, sym|
508
+ val = attributes[xml_attr]
509
+ next if val.nil?
510
+
511
+ prot[sym] = if sym == :spin_count
512
+ val.to_i
513
+ elsif %i[password algorithm_name hash_value salt_value].include?(sym)
514
+ val
515
+ else
516
+ %w[1 true].include?(val)
517
+ end
518
+ end
519
+ @protection = prot unless prot.empty?
520
+ end
521
+
522
+ private
523
+
524
+ def element_name(local_name, qname)
525
+ if local_name.nil? || local_name.empty?
526
+ qname.to_s.split(":").last
527
+ else
528
+ local_name
529
+ end
530
+ end
531
+ end
532
+
533
+ # SAX2 listener for parsing <protectedRanges> element.
534
+ class ProtectedRangesListener
535
+ include REXML::SAX2Listener
536
+
537
+ attr_reader :ranges
538
+
539
+ def initialize
540
+ @ranges = []
541
+ @current = nil
542
+ end
543
+
544
+ def start_element(_uri, local_name, qname, attributes)
545
+ name = element_name(local_name, qname)
546
+ case name
547
+ when "protectedRange"
548
+ pr = {}
549
+ pr[:sqref] = attributes["sqref"] if attributes["sqref"]
550
+ pr[:name] = attributes["name"] if attributes["name"]
551
+ pr[:algorithm_name] = attributes["algorithmName"] if attributes["algorithmName"]
552
+ pr[:hash_value] = attributes["hashValue"] if attributes["hashValue"]
553
+ pr[:salt_value] = attributes["saltValue"] if attributes["saltValue"]
554
+ pr[:spin_count] = attributes["spinCount"].to_i if attributes["spinCount"]
555
+ @current = pr
556
+ when "securityDescriptor"
557
+ @in_sd = true
558
+ @sd_text = +""
559
+ end
560
+ end
561
+
562
+ def characters(text)
563
+ @sd_text << text if @in_sd
564
+ end
565
+
566
+ def end_element(_uri, local_name, qname)
567
+ name = element_name(local_name, qname)
568
+ case name
569
+ when "securityDescriptor"
570
+ (@current[:security_descriptors] ||= []) << @sd_text if @current
571
+ @in_sd = false
572
+ when "protectedRange"
573
+ @ranges << @current if @current
574
+ @current = nil
575
+ end
576
+ end
577
+
578
+ private
579
+
580
+ def element_name(local_name, qname)
581
+ if local_name.nil? || local_name.empty?
582
+ qname.to_s.split(":").last
583
+ else
584
+ local_name
585
+ end
586
+ end
587
+ end
588
+
589
+ # SAX2 listener for parsing <cellWatches> element.
590
+ class CellWatchesListener
591
+ include REXML::SAX2Listener
592
+
593
+ attr_reader :watches
594
+
595
+ def initialize
596
+ @watches = []
597
+ end
598
+
599
+ def start_element(_uri, local_name, qname, attributes)
600
+ name = element_name(local_name, qname)
601
+ @watches << attributes["r"] if name == "cellWatch" && attributes["r"]
602
+ end
603
+
604
+ private
605
+
606
+ def element_name(local_name, qname)
607
+ if local_name.nil? || local_name.empty?
608
+ qname.to_s.split(":").last
609
+ else
610
+ local_name
611
+ end
612
+ end
613
+ end
614
+
615
+ # SAX2 listener for parsing <ignoredErrors> element.
616
+ class IgnoredErrorsListener
617
+ include REXML::SAX2Listener
618
+
619
+ attr_reader :errors
620
+
621
+ def initialize
622
+ @errors = []
623
+ end
624
+
625
+ IGNORED_ERROR_BOOL_ATTRS = {
626
+ "evalError" => :eval_error, "twoDigitTextYear" => :two_digit_text_year,
627
+ "numberStoredAsText" => :number_stored_as_text, "formula" => :formula,
628
+ "formulaRange" => :formula_range, "unlockedFormula" => :unlocked_formula,
629
+ "emptyCellReference" => :empty_cell_reference, "listDataValidation" => :list_data_validation,
630
+ "calculatedColumn" => :calculated_column
631
+ }.freeze
632
+
633
+ def start_element(_uri, local_name, qname, attributes)
634
+ name = element_name(local_name, qname)
635
+ return unless name == "ignoredError" && attributes["sqref"]
636
+
637
+ ie = { sqref: attributes["sqref"] }
638
+ IGNORED_ERROR_BOOL_ATTRS.each do |xml_attr, sym|
639
+ ie[sym] = true if attributes[xml_attr] == "1"
640
+ end
641
+ @errors << ie
642
+ end
643
+
644
+ private
645
+
646
+ def element_name(local_name, qname)
647
+ if local_name.nil? || local_name.empty?
648
+ qname.to_s.split(":").last
649
+ else
650
+ local_name
651
+ end
652
+ end
653
+ end
654
+
655
+ # SAX2 listener for parsing <dataConsolidate> element.
656
+ class DataConsolidateListener
657
+ include REXML::SAX2Listener
658
+
659
+ attr_reader :result
660
+
661
+ def initialize
662
+ @result = nil
663
+ end
664
+
665
+ def start_element(_uri, local_name, qname, attributes)
666
+ name = element_name(local_name, qname)
667
+ case name
668
+ when "dataConsolidate"
669
+ @result = {}
670
+ @result[:function] = attributes["function"] if attributes["function"]
671
+ @result[:start_labels] = true if %w[1 true].include?(attributes["startLabels"])
672
+ @result[:left_labels] = true if %w[1 true].include?(attributes["leftLabels"])
673
+ @result[:top_labels] = true if %w[1 true].include?(attributes["topLabels"])
674
+ @result[:link] = true if %w[1 true].include?(attributes["link"])
675
+ @result[:data_refs] = []
676
+ when "dataRef"
677
+ if @result
678
+ ref = {}
679
+ ref[:ref] = attributes["ref"] if attributes["ref"]
680
+ ref[:name] = attributes["name"] if attributes["name"]
681
+ ref[:sheet] = attributes["sheet"] if attributes["sheet"]
682
+ @result[:data_refs] << ref
683
+ end
684
+ end
685
+ end
686
+
687
+ private
688
+
689
+ def element_name(local_name, qname)
690
+ if local_name.nil? || local_name.empty?
691
+ qname.to_s.split(":").last
692
+ else
693
+ local_name
694
+ end
695
+ end
696
+ end
697
+
698
+ # SAX2 listener for parsing <scenarios> element.
699
+ class ScenariosListener
700
+ include REXML::SAX2Listener
701
+
702
+ attr_reader :result
703
+
704
+ def initialize
705
+ @result = nil
706
+ @current_scenario = nil
707
+ end
708
+
709
+ def start_element(_uri, local_name, qname, attributes)
710
+ name = element_name(local_name, qname)
711
+ case name
712
+ when "scenarios"
713
+ @result = { scenarios: [] }
714
+ @result[:current] = attributes["current"].to_i if attributes["current"]
715
+ @result[:show] = attributes["show"].to_i if attributes["show"]
716
+ @result[:sqref] = attributes["sqref"] if attributes["sqref"]
717
+ when "scenario"
718
+ if @result
719
+ sc = { name: attributes["name"], input_cells: [] }
720
+ sc[:locked] = true if %w[1 true].include?(attributes["locked"])
721
+ sc[:hidden] = true if %w[1 true].include?(attributes["hidden"])
722
+ sc[:user] = attributes["user"] if attributes["user"]
723
+ sc[:comment] = attributes["comment"] if attributes["comment"]
724
+ @current_scenario = sc
725
+ end
726
+ when "inputCells"
727
+ if @current_scenario
728
+ ic = { r: attributes["r"], val: attributes["val"] }
729
+ ic[:deleted] = true if %w[1 true].include?(attributes["deleted"])
730
+ ic[:undone] = true if %w[1 true].include?(attributes["undone"])
731
+ ic[:num_fmt_id] = attributes["numFmtId"].to_i if attributes["numFmtId"]
732
+ @current_scenario[:input_cells] << ic
733
+ end
734
+ end
735
+ end
736
+
737
+ def end_element(_uri, local_name, qname)
738
+ name = element_name(local_name, qname)
739
+ return unless name == "scenario" && @current_scenario && @result
740
+
741
+ @result[:scenarios] << @current_scenario
742
+ @current_scenario = nil
743
+ end
744
+
745
+ private
746
+
747
+ def element_name(local_name, qname)
748
+ if local_name.nil? || local_name.empty?
749
+ qname.to_s.split(":").last
750
+ else
751
+ local_name
752
+ end
753
+ end
754
+ end
755
+
756
+ # SAX2 listener for parsing <sheetFormatPr> element.
757
+ class SheetFormatListener
758
+ include REXML::SAX2Listener
759
+
760
+ attr_reader :properties
761
+
762
+ def initialize
763
+ @properties = {}
764
+ end
765
+
766
+ def start_element(_uri, local_name, qname, attributes)
767
+ name = element_name(local_name, qname)
768
+ return unless name == "sheetFormatPr"
769
+
770
+ drh = attributes["defaultRowHeight"]
771
+ @properties[:default_row_height] = drh.to_f if drh
772
+ dcw = attributes["defaultColWidth"]
773
+ @properties[:default_col_width] = dcw.to_f if dcw
774
+ bcw = attributes["baseColWidth"]
775
+ @properties[:base_col_width] = bcw.to_i if bcw
776
+ olr = attributes["outlineLevelRow"]
777
+ @properties[:outline_level_row] = olr.to_i if olr
778
+ olc = attributes["outlineLevelCol"]
779
+ @properties[:outline_level_col] = olc.to_i if olc
780
+ ch = attributes["customHeight"]
781
+ @properties[:custom_height] = true if %w[1 true].include?(ch)
782
+ zh = attributes["zeroHeight"]
783
+ @properties[:zero_height] = true if %w[1 true].include?(zh)
784
+ tt = attributes["thickTop"]
785
+ @properties[:thick_top] = true if %w[1 true].include?(tt)
786
+ tb = attributes["thickBottom"]
787
+ @properties[:thick_bottom] = true if %w[1 true].include?(tb)
788
+ end
789
+
790
+ private
791
+
792
+ def element_name(local_name, qname)
793
+ if local_name.nil? || local_name.empty?
794
+ qname.to_s.split(":").last
795
+ else
796
+ local_name
797
+ end
798
+ end
799
+ end
800
+
801
+ # SAX2 listener for parsing <sheetViews><sheetView>, <pane>, and <selection>.
802
+ class SheetViewListener
803
+ include REXML::SAX2Listener
804
+
805
+ attr_reader :view, :pane, :selection
806
+
807
+ def initialize
808
+ @view = {}
809
+ @pane = nil
810
+ @selection = nil
811
+ @inside_sheet_views = false
812
+ end
813
+
814
+ def start_element(_uri, local_name, qname, attributes)
815
+ name = element_name(local_name, qname)
816
+ case name
817
+ when "sheetViews"
818
+ @inside_sheet_views = true
819
+ when "sheetView"
820
+ return unless @inside_sheet_views
821
+
822
+ wp = attributes["windowProtection"]
823
+ @view[:window_protection] = %w[1 true].include?(wp) unless wp.nil?
824
+ sf = attributes["showFormulas"]
825
+ @view[:show_formulas] = %w[1 true].include?(sf) unless sf.nil?
826
+ sgl = attributes["showGridLines"]
827
+ @view[:show_grid_lines] = %w[1 true].include?(sgl) unless sgl.nil?
828
+ srch = attributes["showRowColHeaders"]
829
+ @view[:show_row_col_headers] = %w[1 true].include?(srch) unless srch.nil?
830
+ szv = attributes["showZeros"]
831
+ @view[:show_zeros] = %w[1 true].include?(szv) unless szv.nil?
832
+ rtl = attributes["rightToLeft"]
833
+ @view[:right_to_left] = %w[1 true].include?(rtl) unless rtl.nil?
834
+ ts = attributes["tabSelected"]
835
+ @view[:tab_selected] = true if ts == "1"
836
+ srr = attributes["showRuler"]
837
+ @view[:show_ruler] = %w[1 true].include?(srr) unless srr.nil?
838
+ soss = attributes["showOutlineSymbols"]
839
+ @view[:show_outline_symbols] = %w[1 true].include?(soss) unless soss.nil?
840
+ dgc = attributes["defaultGridColor"]
841
+ @view[:default_grid_color] = %w[1 true].include?(dgc) unless dgc.nil?
842
+ sws = attributes["showWhiteSpace"]
843
+ @view[:show_white_space] = %w[1 true].include?(sws) unless sws.nil?
844
+ vm = attributes["view"]
845
+ @view[:view] = vm if vm
846
+ tlc = attributes["topLeftCell"]
847
+ @view[:top_left_cell] = tlc if tlc
848
+ cid = attributes["colorId"]
849
+ @view[:color_id] = cid.to_i if cid
850
+ zs = attributes["zoomScale"]
851
+ @view[:zoom_scale] = zs.to_i if zs
852
+ zsn = attributes["zoomScaleNormal"]
853
+ @view[:zoom_scale_normal] = zsn.to_i if zsn
854
+ zssl = attributes["zoomScaleSheetLayoutView"]
855
+ @view[:zoom_scale_sheet_layout_view] = zssl.to_i if zssl
856
+ zspl = attributes["zoomScalePageLayoutView"]
857
+ @view[:zoom_scale_page_layout_view] = zspl.to_i if zspl
858
+ when "pane"
859
+ return unless @inside_sheet_views
860
+
861
+ ys = attributes["ySplit"]
862
+ xs = attributes["xSplit"]
863
+ frozen = attributes["state"] == "frozen"
864
+ tlc = attributes["topLeftCell"]
865
+ ap = attributes["activePane"]
866
+ p = if frozen
867
+ {
868
+ row: ys ? ys.to_i : 0,
869
+ col: xs ? xs.to_i : 0,
870
+ state: :frozen
871
+ }
872
+ else
873
+ {
874
+ row: ys ? ys.to_i : 0,
875
+ col: xs ? xs.to_i : 0,
876
+ x_split: xs ? xs.to_i : 0,
877
+ y_split: ys ? ys.to_i : 0,
878
+ top_left_cell: tlc,
879
+ state: :split
880
+ }
881
+ end
882
+ p[:active_pane] = ap if ap
883
+ @pane = p
884
+ when "selection"
885
+ return unless @inside_sheet_views
886
+
887
+ ac = attributes["activeCell"]
888
+ sq = attributes["sqref"]
889
+ sel = { active_cell: ac, sqref: sq }
890
+ pn = attributes["pane"]
891
+ sel[:pane] = pn if pn
892
+ acid = attributes["activeCellId"]
893
+ sel[:active_cell_id] = acid.to_i if acid
894
+ @selection = sel if ac || sq || pn || acid
895
+ end
896
+ end
897
+
898
+ def end_element(_uri, local_name, qname)
899
+ name = element_name(local_name, qname)
900
+ @inside_sheet_views = false if name == "sheetViews"
901
+ end
902
+
903
+ private
904
+
905
+ def element_name(local_name, qname)
906
+ if local_name.nil? || local_name.empty?
907
+ qname.to_s.split(":").last
908
+ else
909
+ local_name
910
+ end
911
+ end
912
+ end
913
+
914
+ # SAX2 listener for parsing <dataValidations> elements.
915
+ class DataValidationsListener
916
+ include REXML::SAX2Listener
917
+
918
+ attr_reader :validations, :container_options
919
+
920
+ def initialize
921
+ @validations = []
922
+ @container_options = {}
923
+ @current_dv = nil
924
+ @inside_formula1 = false
925
+ @inside_formula2 = false
926
+ @text_buffer = +""
927
+ end
928
+
929
+ def start_element(_uri, local_name, qname, attributes)
930
+ name = element_name(local_name, qname)
931
+ case name
932
+ when "dataValidations"
933
+ dp = attributes["disablePrompts"]
934
+ @container_options[:disable_prompts] = true if %w[1 true].include?(dp)
935
+ xw = attributes["xWindow"]
936
+ @container_options[:x_window] = xw.to_i if xw
937
+ yw = attributes["yWindow"]
938
+ @container_options[:y_window] = yw.to_i if yw
939
+ when "dataValidation"
940
+ @current_dv = { sqref: attributes["sqref"] }
941
+ @current_dv[:type] = attributes["type"] if attributes["type"]
942
+ @current_dv[:operator] = attributes["operator"] if attributes["operator"]
943
+ @current_dv[:error_style] = attributes["errorStyle"] if attributes["errorStyle"]
944
+ @current_dv[:allow_blank] = true if attributes["allowBlank"] == "1"
945
+ @current_dv[:show_input_message] = true if attributes["showInputMessage"] == "1"
946
+ @current_dv[:show_error_message] = true if attributes["showErrorMessage"] == "1"
947
+ @current_dv[:error_title] = xml_unescape(attributes["errorTitle"]) if attributes["errorTitle"]
948
+ @current_dv[:error] = xml_unescape(attributes["error"]) if attributes["error"]
949
+ @current_dv[:prompt_title] = xml_unescape(attributes["promptTitle"]) if attributes["promptTitle"]
950
+ @current_dv[:prompt] = xml_unescape(attributes["prompt"]) if attributes["prompt"]
951
+ @current_dv[:show_drop_down] = true if attributes["showDropDown"] == "1"
952
+ @current_dv[:ime_mode] = attributes["imeMode"] if attributes["imeMode"]
953
+ when "formula1"
954
+ @inside_formula1 = true
955
+ @text_buffer = +""
956
+ when "formula2"
957
+ @inside_formula2 = true
958
+ @text_buffer = +""
959
+ end
960
+ end
961
+
962
+ def characters(text)
963
+ @text_buffer << text if @inside_formula1 || @inside_formula2
964
+ end
965
+
966
+ def end_element(_uri, local_name, qname)
967
+ name = element_name(local_name, qname)
968
+ case name
969
+ when "formula1"
970
+ @current_dv[:formula1] = @text_buffer.dup if @current_dv
971
+ @inside_formula1 = false
972
+ when "formula2"
973
+ @current_dv[:formula2] = @text_buffer.dup if @current_dv
974
+ @inside_formula2 = false
975
+ when "dataValidation"
976
+ @validations << @current_dv if @current_dv
977
+ @current_dv = nil
978
+ end
979
+ end
980
+
981
+ private
982
+
983
+ def element_name(local_name, qname)
984
+ if local_name.nil? || local_name.empty?
985
+ qname.to_s.split(":").last
986
+ else
987
+ local_name
988
+ end
989
+ end
990
+
991
+ def xml_unescape(str)
992
+ str.gsub("&amp;", "&").gsub("&lt;", "<").gsub("&gt;", ">").gsub("&quot;", '"').gsub("&apos;", "'")
993
+ end
994
+ end
995
+
996
+ # SAX2 listener for parsing conditionalFormatting elements.
997
+ class ConditionalFormattingListener
998
+ include REXML::SAX2Listener
999
+
1000
+ attr_reader :rules
1001
+
1002
+ def initialize
1003
+ @rules = []
1004
+ @current_sqref = nil
1005
+ @current_pivot = false
1006
+ @current_rule = nil
1007
+ @inside_formula = false
1008
+ @text_buffer = +""
1009
+ @cfvo_target = nil
1010
+ @color_target = nil
1011
+ end
1012
+
1013
+ def start_element(_uri, local_name, qname, attributes)
1014
+ name = element_name(local_name, qname)
1015
+ case name
1016
+ when "conditionalFormatting"
1017
+ @current_sqref = attributes["sqref"]
1018
+ @current_pivot = attributes["pivot"] == "1"
1019
+ when "cfRule"
1020
+ @current_rule = { sqref: @current_sqref, type: attributes["type"] }
1021
+ @current_rule[:pivot] = true if @current_pivot
1022
+ @current_rule[:priority] = attributes["priority"].to_i if attributes["priority"]
1023
+ @current_rule[:operator] = attributes["operator"] if attributes["operator"]
1024
+ @current_rule[:format_id] = attributes["dxfId"].to_i if attributes["dxfId"]
1025
+ @current_rule[:stop_if_true] = true if attributes["stopIfTrue"] == "1"
1026
+ @current_rule[:above_average] = false if attributes["aboveAverage"] == "0"
1027
+ @current_rule[:equal_average] = true if attributes["equalAverage"] == "1"
1028
+ @current_rule[:rank] = attributes["rank"].to_i if attributes["rank"]
1029
+ @current_rule[:percent] = true if attributes["percent"] == "1"
1030
+ @current_rule[:bottom] = true if attributes["bottom"] == "1"
1031
+ @current_rule[:text] = attributes["text"] if attributes["text"]
1032
+ @current_rule[:time_period] = attributes["timePeriod"] if attributes["timePeriod"]
1033
+ sd = attributes["stdDev"]
1034
+ @current_rule[:std_dev] = sd.to_i if sd
1035
+ when "formula"
1036
+ @inside_formula = true
1037
+ @text_buffer = +""
1038
+ when "colorScale"
1039
+ @current_rule[:color_scale] = { cfvo: [], colors: [] } if @current_rule
1040
+ @cfvo_target = :color_scale
1041
+ @color_target = :color_scale
1042
+ when "dataBar"
1043
+ if @current_rule
1044
+ db = { cfvo: [] }
1045
+ db[:min_length] = attributes["minLength"].to_i if attributes["minLength"]
1046
+ db[:max_length] = attributes["maxLength"].to_i if attributes["maxLength"]
1047
+ sv = attributes["showValue"]
1048
+ db[:show_value] = %w[1 true].include?(sv) unless sv.nil?
1049
+ @current_rule[:data_bar] = db
1050
+ end
1051
+ @cfvo_target = :data_bar
1052
+ @color_target = :data_bar
1053
+ when "iconSet"
1054
+ if @current_rule
1055
+ is = { cfvo: [] }
1056
+ is[:icon_set] = attributes["iconSet"] if attributes["iconSet"]
1057
+ rv = attributes["reverse"]
1058
+ is[:reverse] = %w[1 true].include?(rv) unless rv.nil?
1059
+ pct = attributes["percent"]
1060
+ is[:percent] = %w[1 true].include?(pct) unless pct.nil?
1061
+ sv = attributes["showValue"]
1062
+ is[:show_value] = %w[1 true].include?(sv) unless sv.nil?
1063
+ @current_rule[:icon_set] = is
1064
+ end
1065
+ @cfvo_target = :icon_set
1066
+ when "cfvo"
1067
+ cfvo = { type: attributes["type"] }
1068
+ cfvo[:val] = attributes["val"] if attributes["val"]
1069
+ gte = attributes["gte"]
1070
+ cfvo[:gte] = %w[1 true].include?(gte) unless gte.nil?
1071
+ append_cfvo(cfvo)
1072
+ when "color"
1073
+ if attributes["rgb"]
1074
+ append_cf_color({ rgb: attributes["rgb"] })
1075
+ elsif attributes["theme"]
1076
+ c = { theme: attributes["theme"].to_i }
1077
+ c[:tint] = attributes["tint"].to_f if attributes["tint"]
1078
+ append_cf_color(c)
1079
+ elsif attributes["indexed"]
1080
+ append_cf_color({ indexed: attributes["indexed"].to_i })
1081
+ end
1082
+ end
1083
+ end
1084
+
1085
+ def characters(text)
1086
+ @text_buffer << text if @inside_formula
1087
+ end
1088
+
1089
+ def end_element(_uri, local_name, qname)
1090
+ name = element_name(local_name, qname)
1091
+ case name
1092
+ when "formula"
1093
+ (@current_rule[:formulas] ||= []) << @text_buffer.dup if @current_rule
1094
+ @inside_formula = false
1095
+ when "cfRule"
1096
+ @rules << @current_rule if @current_rule
1097
+ @current_rule = nil
1098
+ when "conditionalFormatting"
1099
+ @current_sqref = nil
1100
+ when "colorScale", "dataBar", "iconSet"
1101
+ @cfvo_target = nil
1102
+ @color_target = nil
1103
+ end
1104
+ end
1105
+
1106
+ private
1107
+
1108
+ def append_cfvo(cfvo)
1109
+ return unless @current_rule && @cfvo_target
1110
+
1111
+ container = @current_rule[@cfvo_target]
1112
+ container[:cfvo] << cfvo if container
1113
+ end
1114
+
1115
+ def append_cf_color(color)
1116
+ return unless @current_rule && @color_target
1117
+
1118
+ container = @current_rule[@color_target]
1119
+ if container.is_a?(Hash) && container.key?(:colors)
1120
+ container[:colors] << color
1121
+ elsif container.is_a?(Hash)
1122
+ container[:color] = color
1123
+ end
1124
+ end
1125
+
1126
+ def element_name(local_name, qname)
1127
+ if local_name.nil? || local_name.empty?
1128
+ qname.to_s.split(":").last
1129
+ else
1130
+ local_name
1131
+ end
1132
+ end
1133
+ end
1134
+
1135
+ # SAX2 listener for parsing print/page elements from worksheet XML.
1136
+ class PrintPageListener
1137
+ include REXML::SAX2Listener
1138
+
1139
+ attr_reader :print_options, :page_margins, :page_setup, :header_footer, :row_breaks, :col_breaks
1140
+
1141
+ def initialize
1142
+ @print_options = {}
1143
+ @page_margins = nil
1144
+ @page_setup = {}
1145
+ @header_footer = {}
1146
+ @row_breaks = []
1147
+ @col_breaks = []
1148
+ @inside_header_footer = false
1149
+ @inside_row_breaks = false
1150
+ @inside_col_breaks = false
1151
+ @current_hf_field = nil
1152
+ @text_buffer = +""
1153
+ end
1154
+
1155
+ def start_element(_uri, local_name, qname, attributes)
1156
+ name = element_name(local_name, qname)
1157
+ case name
1158
+ when "printOptions"
1159
+ @print_options[:grid_lines] = true if attributes["gridLines"] == "1"
1160
+ @print_options[:headings] = true if attributes["headings"] == "1"
1161
+ @print_options[:horizontal_centered] = true if attributes["horizontalCentered"] == "1"
1162
+ @print_options[:vertical_centered] = true if attributes["verticalCentered"] == "1"
1163
+ gls = attributes["gridLinesSet"]
1164
+ @print_options[:grid_lines_set] = gls != "0" if gls
1165
+ when "pageMargins"
1166
+ m = {}
1167
+ %w[left right top bottom header footer].each do |k|
1168
+ v = attributes[k]
1169
+ m[k.to_sym] = v.to_f if v
1170
+ end
1171
+ @page_margins = m unless m.empty?
1172
+ when "pageSetup"
1173
+ o = attributes["orientation"]
1174
+ @page_setup[:orientation] = o if o
1175
+ ps = attributes["paperSize"]
1176
+ @page_setup[:paper_size] = ps.to_i if ps
1177
+ sc = attributes["scale"]
1178
+ @page_setup[:scale] = sc.to_i if sc
1179
+ ftw = attributes["fitToWidth"]
1180
+ @page_setup[:fit_to_width] = ftw.to_i if ftw
1181
+ fth = attributes["fitToHeight"]
1182
+ @page_setup[:fit_to_height] = fth.to_i if fth
1183
+ po = attributes["pageOrder"]
1184
+ @page_setup[:page_order] = po if po
1185
+ baw = attributes["blackAndWhite"]
1186
+ @page_setup[:black_and_white] = true if %w[1 true].include?(baw)
1187
+ dr = attributes["draft"]
1188
+ @page_setup[:draft] = true if %w[1 true].include?(dr)
1189
+ cc = attributes["cellComments"]
1190
+ @page_setup[:cell_comments] = cc if cc
1191
+ fpn = attributes["firstPageNumber"]
1192
+ @page_setup[:first_page_number] = fpn.to_i if fpn
1193
+ ufpn = attributes["useFirstPageNumber"]
1194
+ @page_setup[:use_first_page_number] = true if %w[1 true].include?(ufpn)
1195
+ hdpi = attributes["horizontalDpi"]
1196
+ @page_setup[:horizontal_dpi] = hdpi.to_i if hdpi
1197
+ vdpi = attributes["verticalDpi"]
1198
+ @page_setup[:vertical_dpi] = vdpi.to_i if vdpi
1199
+ cp = attributes["copies"]
1200
+ @page_setup[:copies] = cp.to_i if cp
1201
+ ph = attributes["paperHeight"]
1202
+ @page_setup[:paper_height] = ph if ph
1203
+ pw = attributes["paperWidth"]
1204
+ @page_setup[:paper_width] = pw if pw
1205
+ err = attributes["errors"]
1206
+ @page_setup[:errors] = err if err
1207
+ upd = attributes["usePrinterDefaults"]
1208
+ @page_setup[:use_printer_defaults] = %w[1 true].include?(upd) unless upd.nil?
1209
+ when "headerFooter"
1210
+ @inside_header_footer = true
1211
+ df = attributes["differentFirst"]
1212
+ @header_footer[:different_first] = true if %w[1 true].include?(df)
1213
+ doe = attributes["differentOddEven"]
1214
+ @header_footer[:different_odd_even] = true if %w[1 true].include?(doe)
1215
+ swd = attributes["scaleWithDoc"]
1216
+ @header_footer[:scale_with_doc] = swd != "0" if swd
1217
+ awm = attributes["alignWithMargins"]
1218
+ @header_footer[:align_with_margins] = awm != "0" if awm
1219
+ when "oddHeader", "oddFooter", "evenHeader", "evenFooter", "firstHeader", "firstFooter"
1220
+ if @inside_header_footer
1221
+ @current_hf_field = name
1222
+ @text_buffer = +""
1223
+ end
1224
+ when "rowBreaks"
1225
+ @inside_row_breaks = true
1226
+ when "colBreaks"
1227
+ @inside_col_breaks = true
1228
+ when "brk"
1229
+ id = attributes["id"]&.to_i
1230
+ if id
1231
+ brk = { id: id }
1232
+ mn = attributes["min"]
1233
+ brk[:min] = mn.to_i if mn
1234
+ mx = attributes["max"]
1235
+ brk[:max] = mx.to_i if mx
1236
+ brk[:man] = true if %w[1 true].include?(attributes["man"])
1237
+ brk[:pt] = true if %w[1 true].include?(attributes["pt"])
1238
+ @row_breaks << brk if @inside_row_breaks
1239
+ @col_breaks << brk if @inside_col_breaks
1240
+ end
1241
+ end
1242
+ end
1243
+
1244
+ def characters(text)
1245
+ @text_buffer << text if @current_hf_field
1246
+ end
1247
+
1248
+ def end_element(_uri, local_name, qname)
1249
+ name = element_name(local_name, qname)
1250
+ case name
1251
+ when "headerFooter"
1252
+ @inside_header_footer = false
1253
+ when "oddHeader"
1254
+ @header_footer[:odd_header] = @text_buffer.dup if @current_hf_field == "oddHeader"
1255
+ @current_hf_field = nil
1256
+ when "oddFooter"
1257
+ @header_footer[:odd_footer] = @text_buffer.dup if @current_hf_field == "oddFooter"
1258
+ @current_hf_field = nil
1259
+ when "evenHeader"
1260
+ @header_footer[:even_header] = @text_buffer.dup if @current_hf_field == "evenHeader"
1261
+ @current_hf_field = nil
1262
+ when "evenFooter"
1263
+ @header_footer[:even_footer] = @text_buffer.dup if @current_hf_field == "evenFooter"
1264
+ @current_hf_field = nil
1265
+ when "firstHeader"
1266
+ @header_footer[:first_header] = @text_buffer.dup if @current_hf_field == "firstHeader"
1267
+ @current_hf_field = nil
1268
+ when "firstFooter"
1269
+ @header_footer[:first_footer] = @text_buffer.dup if @current_hf_field == "firstFooter"
1270
+ @current_hf_field = nil
1271
+ when "rowBreaks"
1272
+ @inside_row_breaks = false
1273
+ when "colBreaks"
1274
+ @inside_col_breaks = false
1275
+ end
1276
+ end
1277
+
1278
+ private
1279
+
1280
+ def element_name(local_name, qname)
1281
+ if local_name.nil? || local_name.empty?
1282
+ qname.to_s.split(":").last
1283
+ else
1284
+ local_name
1285
+ end
1286
+ end
1287
+ end
1288
+
1289
+ # SAX2 listener for parsing table XML.
1290
+ class TableListener
1291
+ include REXML::SAX2Listener
1292
+
1293
+ attr_reader :table
1294
+
1295
+ def initialize
1296
+ @table = nil
1297
+ @columns = []
1298
+ @current_column = nil
1299
+ @inside_calc_formula = false
1300
+ @inside_totals_formula = false
1301
+ @text_buffer = +""
1302
+ end
1303
+
1304
+ def start_element(_uri, local_name, qname, attributes)
1305
+ name = element_name(local_name, qname)
1306
+ case name
1307
+ when "table"
1308
+ @table = {
1309
+ id: attributes["id"]&.to_i,
1310
+ name: attributes["name"],
1311
+ display_name: attributes["displayName"],
1312
+ ref: attributes["ref"]
1313
+ }
1314
+ trc = attributes["totalsRowCount"]
1315
+ @table[:totals_row_count] = trc.to_i if trc
1316
+ hrc = attributes["headerRowCount"]
1317
+ @table[:header_row_count] = hrc.to_i if hrc
1318
+ @table[:published] = true if attributes["published"] == "1"
1319
+ @table[:comment] = attributes["comment"] if attributes["comment"]
1320
+ @table[:insert_row] = true if attributes["insertRow"] == "1"
1321
+ @table[:insert_row_shift] = true if attributes["insertRowShift"] == "1"
1322
+ hrd = attributes["headerRowDxfId"]
1323
+ @table[:header_row_dxf_id] = hrd.to_i if hrd
1324
+ dd = attributes["dataDxfId"]
1325
+ @table[:data_dxf_id] = dd.to_i if dd
1326
+ trd = attributes["totalsRowDxfId"]
1327
+ @table[:totals_row_dxf_id] = trd.to_i if trd
1328
+ hrbd = attributes["headerRowBorderDxfId"]
1329
+ @table[:header_row_border_dxf_id] = hrbd.to_i if hrbd
1330
+ tbd = attributes["tableBorderDxfId"]
1331
+ @table[:table_border_dxf_id] = tbd.to_i if tbd
1332
+ trbd = attributes["totalsRowBorderDxfId"]
1333
+ @table[:totals_row_border_dxf_id] = trbd.to_i if trbd
1334
+ @table[:header_row_cell_style] = attributes["headerRowCellStyle"] if attributes["headerRowCellStyle"]
1335
+ @table[:totals_row_cell_style] = attributes["totalsRowCellStyle"] if attributes["totalsRowCellStyle"]
1336
+ cid = attributes["connectionId"]
1337
+ @table[:connection_id] = cid.to_i if cid
1338
+ @table[:table_type] = attributes["tableType"] if attributes["tableType"]
1339
+ when "tableColumn"
1340
+ col = { name: attributes["name"] }
1341
+ trf = attributes["totalsRowFunction"]
1342
+ col[:totals_row_function] = trf if trf
1343
+ trl = attributes["totalsRowLabel"]
1344
+ col[:totals_row_label] = trl if trl
1345
+ dd = attributes["dataDxfId"]
1346
+ col[:data_dxf_id] = dd.to_i if dd
1347
+ td = attributes["totalsRowDxfId"]
1348
+ col[:totals_row_dxf_id] = td.to_i if td
1349
+ hd = attributes["headerRowDxfId"]
1350
+ col[:header_row_dxf_id] = hd.to_i if hd
1351
+ dcs = attributes["dataCellStyle"]
1352
+ col[:data_cell_style] = dcs if dcs
1353
+ @current_column = col
1354
+ when "calculatedColumnFormula"
1355
+ @inside_calc_formula = true
1356
+ @text_buffer = +""
1357
+ when "totalsRowFormula"
1358
+ @inside_totals_formula = true
1359
+ @text_buffer = +""
1360
+ when "tableStyleInfo"
1361
+ if @table
1362
+ si = {}
1363
+ si[:name] = attributes["name"] if attributes["name"]
1364
+ sfc = attributes["showFirstColumn"]
1365
+ si[:show_first_column] = sfc == "1" unless sfc.nil?
1366
+ slc = attributes["showLastColumn"]
1367
+ si[:show_last_column] = slc == "1" unless slc.nil?
1368
+ srs = attributes["showRowStripes"]
1369
+ si[:show_row_stripes] = srs == "1" unless srs.nil?
1370
+ scs = attributes["showColumnStripes"]
1371
+ si[:show_column_stripes] = scs == "1" unless scs.nil?
1372
+ @table[:style] = si
1373
+ end
1374
+ end
1375
+ end
1376
+
1377
+ def characters(text)
1378
+ @text_buffer << text if @inside_calc_formula || @inside_totals_formula
1379
+ end
1380
+
1381
+ def end_element(_uri, local_name, qname)
1382
+ name = element_name(local_name, qname)
1383
+ case name
1384
+ when "calculatedColumnFormula"
1385
+ @current_column[:calculated_column_formula] = @text_buffer.dup if @current_column
1386
+ @inside_calc_formula = false
1387
+ when "totalsRowFormula"
1388
+ @current_column[:totals_row_formula] = @text_buffer.dup if @current_column
1389
+ @inside_totals_formula = false
1390
+ when "tableColumn"
1391
+ @columns << @current_column if @current_column
1392
+ @current_column = nil
1393
+ when "table"
1394
+ @table[:columns] = @columns if @table
1395
+ end
1396
+ end
1397
+
1398
+ private
1399
+
1400
+ def element_name(local_name, qname)
1401
+ if local_name.nil? || local_name.empty?
1402
+ qname.to_s.split(":").last
1403
+ else
1404
+ local_name
1405
+ end
1406
+ end
1407
+ end
1408
+
1409
+ # SAX2 listener for parsing calcChain.xml.
1410
+ class CalcChainListener
1411
+ include REXML::SAX2Listener
1412
+
1413
+ attr_reader :entries
1414
+
1415
+ def initialize
1416
+ @entries = []
1417
+ end
1418
+
1419
+ def start_element(_uri, local_name, qname, attributes)
1420
+ name = element_name(local_name, qname)
1421
+ return unless name == "c"
1422
+
1423
+ entry = {}
1424
+ entry[:ref] = attributes["r"] if attributes["r"]
1425
+ i = attributes["i"]
1426
+ entry[:sheet_id] = i.to_i if i
1427
+ @entries << entry
1428
+ end
1429
+
1430
+ private
1431
+
1432
+ def element_name(local_name, qname)
1433
+ if local_name.nil? || local_name.empty?
1434
+ qname.to_s.split(":").last
1435
+ else
1436
+ local_name
1437
+ end
1438
+ end
1439
+ end
1440
+
1441
+ # SAX2 listener for parsing pivotTable XML.
1442
+ class PivotTableListener
1443
+ include REXML::SAX2Listener
1444
+
1445
+ attr_reader :pivot_table
1446
+
1447
+ def initialize
1448
+ @pivot_table = nil
1449
+ @fields = []
1450
+ @row_fields = []
1451
+ @col_fields = []
1452
+ @data_fields = []
1453
+ @inside_row_fields = false
1454
+ @inside_col_fields = false
1455
+ @inside_data_fields = false
1456
+ @inside_pivot_field = false
1457
+ @inside_items = false
1458
+ @current_items = []
1459
+ end
1460
+
1461
+ def start_element(_uri, local_name, qname, attributes)
1462
+ name = element_name(local_name, qname)
1463
+ case name
1464
+ when "pivotTableDefinition"
1465
+ @pivot_table = {
1466
+ name: attributes["name"],
1467
+ cache_id: attributes["cacheId"]&.to_i
1468
+ }
1469
+ @pivot_table[:data_caption] = attributes["dataCaption"] if attributes["dataCaption"]
1470
+ @pivot_table[:data_on_rows] = attributes["dataOnRows"] == "1" if attributes["dataOnRows"]
1471
+ @pivot_table[:row_grand_totals] = attributes["rowGrandTotals"] != "0" if attributes["rowGrandTotals"]
1472
+ @pivot_table[:col_grand_totals] = attributes["colGrandTotals"] != "0" if attributes["colGrandTotals"]
1473
+ @pivot_table[:compact] = attributes["compact"] != "0" if attributes["compact"]
1474
+ @pivot_table[:outline] = attributes["outline"] != "0" if attributes["outline"]
1475
+ @pivot_table[:outline_data] = attributes["outlineData"] == "1" if attributes["outlineData"]
1476
+ @pivot_table[:compact_data] = attributes["compactData"] != "0" if attributes["compactData"]
1477
+ @pivot_table[:show_headers] = attributes["showHeaders"] != "0" if attributes["showHeaders"]
1478
+ @pivot_table[:show_multiple_label] = attributes["showMultipleLabel"] != "0" if attributes["showMultipleLabel"]
1479
+ @pivot_table[:show_data_drop_down] = attributes["showDataDropDown"] != "0" if attributes["showDataDropDown"]
1480
+ @pivot_table[:grand_total_caption] = attributes["grandTotalCaption"] if attributes["grandTotalCaption"]
1481
+ @pivot_table[:error_caption] = attributes["errorCaption"] if attributes["errorCaption"]
1482
+ @pivot_table[:show_error] = attributes["showError"] == "1" if attributes["showError"]
1483
+ @pivot_table[:missing_caption] = attributes["missingCaption"] if attributes["missingCaption"]
1484
+ @pivot_table[:show_missing] = attributes["showMissing"] != "0" if attributes["showMissing"]
1485
+ @pivot_table[:tag] = attributes["tag"] if attributes["tag"]
1486
+ @pivot_table[:indent] = attributes["indent"]&.to_i if attributes["indent"]
1487
+ @pivot_table[:published] = attributes["published"] == "1" if attributes["published"]
1488
+ @pivot_table[:edit_data] = attributes["editData"] == "1" if attributes["editData"]
1489
+ @pivot_table[:disable_field_list] = attributes["disableFieldList"] == "1" if attributes["disableFieldList"]
1490
+ @pivot_table[:visual_totals] = attributes["visualTotals"] != "0" if attributes["visualTotals"]
1491
+ @pivot_table[:print_drill] = attributes["printDrill"] == "1" if attributes["printDrill"]
1492
+ @pivot_table[:created_version] = attributes["createdVersion"]&.to_i if attributes["createdVersion"]
1493
+ @pivot_table[:updated_version] = attributes["updatedVersion"]&.to_i if attributes["updatedVersion"]
1494
+ @pivot_table[:min_refreshable_version] = attributes["minRefreshableVersion"]&.to_i if attributes["minRefreshableVersion"]
1495
+ %w[applyNumberFormats applyBorderFormats applyFontFormats
1496
+ applyPatternFormats applyAlignmentFormats applyWidthHeightFormats].each do |attr|
1497
+ next if attributes[attr].nil?
1498
+
1499
+ key = attr.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }.to_sym
1500
+ @pivot_table[key] = %w[1 true].include?(attributes[attr])
1501
+ end
1502
+ mff = attributes["multipleFieldFilters"]
1503
+ @pivot_table[:multiple_field_filters] = mff != "0" unless mff.nil?
1504
+ sdr = attributes["showDrill"]
1505
+ @pivot_table[:show_drill] = sdr != "0" unless sdr.nil?
1506
+ sdt = attributes["showDataTips"]
1507
+ @pivot_table[:show_data_tips] = sdt != "0" unless sdt.nil?
1508
+ edr = attributes["enableDrill"]
1509
+ @pivot_table[:enable_drill] = edr != "0" unless edr.nil?
1510
+ smpt = attributes["showMemberPropertyTips"]
1511
+ @pivot_table[:show_member_property_tips] = smpt != "0" unless smpt.nil?
1512
+ ipt = attributes["itemPrintTitles"]
1513
+ @pivot_table[:item_print_titles] = ipt == "1" unless ipt.nil?
1514
+ fpt = attributes["fieldPrintTitles"]
1515
+ @pivot_table[:field_print_titles] = fpt == "1" unless fpt.nil?
1516
+ pf = attributes["preserveFormatting"]
1517
+ @pivot_table[:preserve_formatting] = pf != "0" unless pf.nil?
1518
+ potd = attributes["pageOverThenDown"]
1519
+ @pivot_table[:page_over_then_down] = potd == "1" unless potd.nil?
1520
+ pw = attributes["pageWrap"]
1521
+ @pivot_table[:page_wrap] = pw.to_i if pw
1522
+ when "location"
1523
+ @pivot_table[:ref] = attributes["ref"] if @pivot_table
1524
+ @pivot_table[:row_page_count] = attributes["rowPageCount"]&.to_i if attributes["rowPageCount"]
1525
+ @pivot_table[:col_page_count] = attributes["colPageCount"]&.to_i if attributes["colPageCount"]
1526
+ when "pivotField"
1527
+ @inside_pivot_field = true
1528
+ @current_field = {}
1529
+ @current_field[:axis] = attributes["axis"] if attributes["axis"]
1530
+ @current_field[:data_field] = true if attributes["dataField"] == "1"
1531
+ @current_field[:name] = attributes["name"] if attributes["name"]
1532
+ @current_field[:show_all] = attributes["showAll"] != "0" if attributes["showAll"]
1533
+ @current_field[:compact] = attributes["compact"] != "0" if attributes["compact"]
1534
+ @current_field[:outline] = attributes["outline"] != "0" if attributes["outline"]
1535
+ @current_field[:subtotal_top] = attributes["subtotalTop"] != "0" if attributes["subtotalTop"]
1536
+ @current_field[:num_fmt_id] = attributes["numFmtId"]&.to_i if attributes["numFmtId"]
1537
+ @current_field[:sort_type] = attributes["sortType"] if attributes["sortType"]
1538
+ ds = attributes["defaultSubtotal"]
1539
+ @current_field[:default_subtotal] = ds != "0" unless ds.nil?
1540
+ @current_field[:insert_blank_row] = true if attributes["insertBlankRow"] == "1"
1541
+ @current_field[:insert_page_break] = true if attributes["insertPageBreak"] == "1"
1542
+ @current_field[:include_new_items_in_filter] = true if attributes["includeNewItemsInFilter"] == "1"
1543
+ @current_items = []
1544
+ when "items"
1545
+ @inside_items = true if @inside_pivot_field
1546
+ when "item"
1547
+ if @inside_items
1548
+ item_type = attributes["t"]
1549
+ item_x = attributes["x"]&.to_i
1550
+ @current_items << { x: item_x, t: item_type } if item_type || item_x
1551
+ end
1552
+ when "rowFields"
1553
+ @inside_row_fields = true
1554
+ when "colFields"
1555
+ @inside_col_fields = true
1556
+ when "dataFields"
1557
+ @inside_data_fields = true
1558
+ when "field"
1559
+ idx = attributes["x"]&.to_i
1560
+ @row_fields << idx if @inside_row_fields && idx
1561
+ @col_fields << idx if @inside_col_fields && idx
1562
+ when "dataField"
1563
+ if @inside_data_fields
1564
+ df = {
1565
+ name: attributes["name"],
1566
+ fld: attributes["fld"]&.to_i,
1567
+ subtotal: attributes["subtotal"] || "sum"
1568
+ }
1569
+ df[:show_data_as] = attributes["showDataAs"] if attributes["showDataAs"]
1570
+ df[:base_field] = attributes["baseField"]&.to_i if attributes["baseField"]
1571
+ df[:base_item] = attributes["baseItem"]&.to_i if attributes["baseItem"]
1572
+ df[:num_fmt_id] = attributes["numFmtId"]&.to_i if attributes["numFmtId"]
1573
+ @data_fields << df
1574
+ end
1575
+ when "pivotTableStyleInfo"
1576
+ if @pivot_table
1577
+ psi = {}
1578
+ psi[:name] = attributes["name"] if attributes["name"]
1579
+ srh = attributes["showRowHeaders"]
1580
+ psi[:show_row_headers] = srh == "1" unless srh.nil?
1581
+ sch = attributes["showColHeaders"]
1582
+ psi[:show_col_headers] = sch == "1" unless sch.nil?
1583
+ srs = attributes["showRowStripes"]
1584
+ psi[:show_row_stripes] = srs == "1" unless srs.nil?
1585
+ scs = attributes["showColStripes"]
1586
+ psi[:show_col_stripes] = scs == "1" unless scs.nil?
1587
+ slc = attributes["showLastColumn"]
1588
+ psi[:show_last_column] = slc == "1" unless slc.nil?
1589
+ @pivot_table[:pivot_table_style] = psi
1590
+ end
1591
+ end
1592
+ end
1593
+
1594
+ def end_element(_uri, local_name, qname)
1595
+ name = element_name(local_name, qname)
1596
+ case name
1597
+ when "pivotField"
1598
+ @current_field[:items] = @current_items unless @current_items.empty?
1599
+ @fields << @current_field
1600
+ @inside_pivot_field = false
1601
+ when "items"
1602
+ @inside_items = false
1603
+ when "pivotTableDefinition"
1604
+ if @pivot_table
1605
+ @pivot_table[:fields] = @fields
1606
+ @pivot_table[:row_fields] = @row_fields
1607
+ @pivot_table[:col_fields] = @col_fields
1608
+ @pivot_table[:data_fields] = @data_fields
1609
+ end
1610
+ when "rowFields"
1611
+ @inside_row_fields = false
1612
+ when "colFields"
1613
+ @inside_col_fields = false
1614
+ when "dataFields"
1615
+ @inside_data_fields = false
1616
+ end
1617
+ end
1618
+
1619
+ private
1620
+
1621
+ def element_name(local_name, qname)
1622
+ if local_name.nil? || local_name.empty?
1623
+ qname.to_s.split(":").last
1624
+ else
1625
+ local_name
1626
+ end
1627
+ end
1628
+ end
1629
+
1630
+ # SAX2 listener for parsing pivotCacheDefinition XML.
1631
+ class PivotCacheDefinitionListener
1632
+ include REXML::SAX2Listener
1633
+
1634
+ attr_reader :cache_definition
1635
+
1636
+ def initialize
1637
+ @cache_definition = {}
1638
+ @fields = []
1639
+ @current_field = nil
1640
+ @current_shared_items = nil
1641
+ end
1642
+
1643
+ def start_element(_uri, local_name, qname, attributes)
1644
+ name = element_name(local_name, qname)
1645
+ case name
1646
+ when "pivotCacheDefinition"
1647
+ sd = attributes["saveData"]
1648
+ @cache_definition[:save_data] = sd != "0" unless sd.nil?
1649
+ er = attributes["enableRefresh"]
1650
+ @cache_definition[:enable_refresh] = er != "0" unless er.nil?
1651
+ @cache_definition[:refreshed_by] = attributes["refreshedBy"] if attributes["refreshedBy"]
1652
+ @cache_definition[:refreshed_version] = attributes["refreshedVersion"]&.to_i if attributes["refreshedVersion"]
1653
+ @cache_definition[:created_version] = attributes["createdVersion"]&.to_i if attributes["createdVersion"]
1654
+ @cache_definition[:record_count] = attributes["recordCount"]&.to_i if attributes["recordCount"]
1655
+ om = attributes["optimizeMemory"]
1656
+ @cache_definition[:optimize_memory] = om == "1" unless om.nil?
1657
+ when "cacheSource"
1658
+ @cache_definition[:source_type] = attributes["type"] if attributes["type"]
1659
+ when "worksheetSource"
1660
+ @cache_definition[:source_ref] = attributes["ref"] if attributes["ref"]
1661
+ @cache_definition[:source_sheet] = attributes["sheet"] if attributes["sheet"]
1662
+ @cache_definition[:source_name] = attributes["name"] if attributes["name"]
1663
+ when "cacheField"
1664
+ @current_field = {}
1665
+ @current_field[:name] = attributes["name"] if attributes["name"]
1666
+ @current_field[:num_fmt_id] = attributes["numFmtId"]&.to_i if attributes["numFmtId"]
1667
+ @current_field[:caption] = attributes["caption"] if attributes["caption"]
1668
+ @current_field[:formula] = xml_unescape(attributes["formula"]) if attributes["formula"]
1669
+ when "sharedItems"
1670
+ @current_shared_items = [] if @current_field
1671
+ when "s", "d", "e"
1672
+ @current_shared_items << attributes["v"] if @current_shared_items && attributes["v"]
1673
+ when "n"
1674
+ @current_shared_items << attributes["v"]&.to_f if @current_shared_items && attributes["v"]
1675
+ when "b"
1676
+ @current_shared_items << (attributes["v"] == "1") if @current_shared_items
1677
+ when "m"
1678
+ @current_shared_items << nil if @current_shared_items
1679
+ end
1680
+ end
1681
+
1682
+ def end_element(_uri, local_name, qname)
1683
+ name = element_name(local_name, qname)
1684
+ case name
1685
+ when "sharedItems"
1686
+ @current_field[:shared_items] = @current_shared_items if @current_field && @current_shared_items && !@current_shared_items.empty?
1687
+ @current_shared_items = nil
1688
+ when "cacheField"
1689
+ if @current_field
1690
+ @fields << @current_field
1691
+ @current_field = nil
1692
+ end
1693
+ end
1694
+ end
1695
+
1696
+ def characters(_text); end
1697
+
1698
+ def end_document
1699
+ @cache_definition[:fields] = @fields unless @fields.empty?
1700
+ end
1701
+
1702
+ private
1703
+
1704
+ def element_name(local_name, qname)
1705
+ if local_name.nil? || local_name.empty?
1706
+ qname.to_s.split(":").last
1707
+ else
1708
+ local_name
1709
+ end
1710
+ end
1711
+
1712
+ def xml_unescape(str)
1713
+ str.gsub("&amp;", "&").gsub("&lt;", "<").gsub("&gt;", ">").gsub("&quot;", '"').gsub("&apos;", "'")
1714
+ end
1715
+ end
1716
+
1717
+ # SAX2 listener for parsing pivotCacheRecords XML.
1718
+ class PivotCacheRecordsListener
1719
+ include REXML::SAX2Listener
1720
+
1721
+ attr_reader :records
1722
+
1723
+ def initialize
1724
+ @records = []
1725
+ @current_record = nil
1726
+ end
1727
+
1728
+ def start_element(_uri, local_name, qname, attributes)
1729
+ name = element_name(local_name, qname)
1730
+ case name
1731
+ when "r"
1732
+ @current_record = []
1733
+ when "x"
1734
+ @current_record << { x: attributes["v"]&.to_i } if @current_record
1735
+ when "s", "d", "e"
1736
+ @current_record << attributes["v"] if @current_record && attributes["v"]
1737
+ when "n"
1738
+ @current_record << attributes["v"]&.to_f if @current_record && attributes["v"]
1739
+ when "b"
1740
+ @current_record << (attributes["v"] == "1") if @current_record
1741
+ when "m"
1742
+ @current_record << nil if @current_record
1743
+ end
1744
+ end
1745
+
1746
+ def end_element(_uri, local_name, qname)
1747
+ name = element_name(local_name, qname)
1748
+ return unless name == "r" && @current_record
1749
+
1750
+ @records << @current_record
1751
+ @current_record = nil
1752
+ end
1753
+
1754
+ private
1755
+
1756
+ def element_name(local_name, qname)
1757
+ if local_name.nil? || local_name.empty?
1758
+ qname.to_s.split(":").last
1759
+ else
1760
+ local_name
1761
+ end
1762
+ end
1763
+ end
1764
+
1765
+ # SAX2 listener for parsing externalLink XML.
1766
+ class ExternalLinkListener
1767
+ include REXML::SAX2Listener
1768
+
1769
+ attr_reader :sheet_names
1770
+
1771
+ def initialize
1772
+ @sheet_names = []
1773
+ end
1774
+
1775
+ def start_element(_uri, local_name, qname, attributes)
1776
+ name = element_name(local_name, qname)
1777
+ return unless name == "sheetName"
1778
+
1779
+ @sheet_names << attributes["val"] if attributes["val"]
1780
+ end
1781
+
1782
+ def end_element(_uri, _local_name, _qname); end
1783
+
1784
+ private
1785
+
1786
+ def element_name(local_name, qname)
1787
+ if local_name.nil? || local_name.empty?
1788
+ qname.to_s.split(":").last
1789
+ else
1790
+ local_name
1791
+ end
1792
+ end
1793
+ end
1794
+ end
1795
+ end
1796
+ end