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,784 @@
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 shared string table (xl/sharedStrings.xml).
12
+ class SharedStringsListener
13
+ include REXML::SAX2Listener
14
+
15
+ attr_reader :strings
16
+
17
+ def initialize
18
+ @strings = []
19
+ @inside_si = false
20
+ @inside_r = false
21
+ @inside_rpr = false
22
+ @inside_t = false
23
+ @text_buffer = +""
24
+ @runs = []
25
+ @current_font = {}
26
+ @has_runs = false
27
+ end
28
+
29
+ def start_element(_uri, local_name, qname, attributes)
30
+ name = element_name(local_name, qname)
31
+
32
+ case name
33
+ when "si"
34
+ @inside_si = true
35
+ @text_buffer = +""
36
+ @runs = []
37
+ @has_runs = false
38
+ when "r"
39
+ @inside_r = true
40
+ @has_runs = true
41
+ @current_font = {}
42
+ when "rPr"
43
+ @inside_rpr = true if @inside_r
44
+ when "b"
45
+ @current_font[:bold] = true if @inside_rpr
46
+ when "i"
47
+ @current_font[:italic] = true if @inside_rpr
48
+ when "strike"
49
+ @current_font[:strike] = true if @inside_rpr
50
+ when "u"
51
+ if @inside_rpr
52
+ val = attributes["val"]
53
+ @current_font[:underline] = val || true
54
+ end
55
+ when "vertAlign"
56
+ @current_font[:vert_align] = attributes["val"] if @inside_rpr && attributes["val"]
57
+ when "sz"
58
+ @current_font[:sz] = attributes["val"]&.to_f if @inside_rpr
59
+ when "color"
60
+ if @inside_rpr
61
+ if attributes["rgb"]
62
+ @current_font[:color] = attributes["rgb"]
63
+ elsif attributes["theme"]
64
+ @current_font[:theme] = attributes["theme"].to_i
65
+ @current_font[:tint] = attributes["tint"].to_f if attributes["tint"]
66
+ elsif attributes["indexed"]
67
+ @current_font[:indexed] = attributes["indexed"].to_i
68
+ end
69
+ end
70
+ when "rFont"
71
+ @current_font[:name] = attributes["val"] if @inside_rpr
72
+ when "family"
73
+ @current_font[:family] = attributes["val"]&.to_i if @inside_rpr
74
+ when "scheme"
75
+ @current_font[:scheme] = attributes["val"] if @inside_rpr
76
+ when "t"
77
+ @inside_t = @inside_si
78
+ @text_buffer = +"" if @inside_r
79
+ end
80
+ end
81
+
82
+ def characters(text)
83
+ @text_buffer << text if @inside_t
84
+ end
85
+
86
+ def end_element(_uri, local_name, qname)
87
+ name = element_name(local_name, qname)
88
+
89
+ case name
90
+ when "t"
91
+ @inside_t = false
92
+ when "rPr"
93
+ @inside_rpr = false
94
+ when "r"
95
+ run = { text: @text_buffer.dup }
96
+ run[:font] = @current_font.dup unless @current_font.empty?
97
+ @runs << run
98
+ @inside_r = false
99
+ when "si"
100
+ @strings << if @has_runs
101
+ Xlsxrb::Elements::RichText.new(runs: @runs)
102
+ else
103
+ @text_buffer.dup
104
+ end
105
+ @inside_si = false
106
+ @text_buffer = +""
107
+ @runs = []
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def element_name(local_name, qname)
114
+ if local_name.nil? || local_name.empty?
115
+ qname.to_s.split(":").last
116
+ else
117
+ local_name
118
+ end
119
+ end
120
+ end
121
+
122
+ # SAX2 listener for parsing worksheet cells.
123
+ class WorksheetListener
124
+ include REXML::SAX2Listener
125
+
126
+ attr_reader :cells, :row_attributes, :cell_phonetic
127
+
128
+ def initialize(shared_strings = [])
129
+ @shared_strings = shared_strings
130
+ @cells = {}
131
+ @row_attributes = {}
132
+ @cell_phonetic = {}
133
+ @current_cell_ref = nil
134
+ @current_cell_type = nil
135
+ @inside_value = false
136
+ @inside_inline_text = false
137
+ @inside_formula = false
138
+ @inside_is = false
139
+ @inside_is_r = false
140
+ @inside_is_rpr = false
141
+ @value_buffer = +""
142
+ @inline_text_buffer = +""
143
+ @formula_buffer = +""
144
+ @is_runs = []
145
+ @is_has_runs = false
146
+ @is_current_font = {}
147
+ @is_run_text = +""
148
+ end
149
+
150
+ def start_element(_uri, local_name, qname, attributes)
151
+ name = element_name(local_name, qname)
152
+
153
+ case name
154
+ when "row"
155
+ parse_row_attributes(attributes)
156
+ when "c"
157
+ @current_cell_ref = attributes["r"]
158
+ @current_cell_type = attributes["t"]
159
+ @current_cell_ph = %w[1 true].include?(attributes["ph"])
160
+ @value_buffer = +""
161
+ @inline_text_buffer = +""
162
+ @formula_buffer = +""
163
+ @formula_type = nil
164
+ @formula_ref = nil
165
+ @formula_si = nil
166
+ @formula_ca = nil
167
+ @formula_aca = nil
168
+ @formula_bx = nil
169
+ @formula_dt2d = nil
170
+ @formula_dtr = nil
171
+ @formula_r1 = nil
172
+ @formula_r2 = nil
173
+ @is_runs = []
174
+ @is_has_runs = false
175
+ when "v"
176
+ @inside_value = true
177
+ when "f"
178
+ @inside_formula = true
179
+ @formula_type = attributes["t"]
180
+ @formula_ref = attributes["ref"]
181
+ si = attributes["si"]
182
+ @formula_si = si&.to_i
183
+ @formula_ca = true if %w[1 true].include?(attributes["ca"])
184
+ @formula_aca = true if %w[1 true].include?(attributes["aca"])
185
+ @formula_bx = true if %w[1 true].include?(attributes["bx"])
186
+ @formula_dt2d = true if %w[1 true].include?(attributes["dt2D"])
187
+ @formula_dtr = true if %w[1 true].include?(attributes["dtr"])
188
+ @formula_r1 = attributes["r1"] if attributes["r1"]
189
+ @formula_r2 = attributes["r2"] if attributes["r2"]
190
+ when "is"
191
+ @inside_is = true if @current_cell_type == "inlineStr"
192
+ when "r"
193
+ if @inside_is
194
+ @inside_is_r = true
195
+ @is_has_runs = true
196
+ @is_current_font = {}
197
+ @is_run_text = +""
198
+ end
199
+ when "rPr"
200
+ @inside_is_rpr = true if @inside_is_r
201
+ when "b"
202
+ @is_current_font[:bold] = true if @inside_is_rpr
203
+ when "i"
204
+ @is_current_font[:italic] = true if @inside_is_rpr
205
+ when "strike"
206
+ @is_current_font[:strike] = true if @inside_is_rpr
207
+ when "u"
208
+ if @inside_is_rpr
209
+ val = attributes["val"]
210
+ @is_current_font[:underline] = val || true
211
+ end
212
+ when "vertAlign"
213
+ @is_current_font[:vert_align] = attributes["val"] if @inside_is_rpr && attributes["val"]
214
+ when "sz"
215
+ @is_current_font[:sz] = attributes["val"]&.to_f if @inside_is_rpr
216
+ when "color"
217
+ if @inside_is_rpr
218
+ if attributes["rgb"]
219
+ @is_current_font[:color] = attributes["rgb"]
220
+ elsif attributes["theme"]
221
+ @is_current_font[:theme] = attributes["theme"].to_i
222
+ @is_current_font[:tint] = attributes["tint"].to_f if attributes["tint"]
223
+ elsif attributes["indexed"]
224
+ @is_current_font[:indexed] = attributes["indexed"].to_i
225
+ end
226
+ end
227
+ when "rFont"
228
+ @is_current_font[:name] = attributes["val"] if @inside_is_rpr
229
+ when "family"
230
+ @is_current_font[:family] = attributes["val"]&.to_i if @inside_is_rpr
231
+ when "scheme"
232
+ @is_current_font[:scheme] = attributes["val"] if @inside_is_rpr
233
+ when "t"
234
+ if @inside_is_r
235
+ @is_run_text = +""
236
+ @inside_inline_text = true
237
+ elsif @inside_is || (@current_cell_type == "inlineStr" && !@current_cell_ref.nil?)
238
+ @inside_inline_text = true
239
+ end
240
+ end
241
+ end
242
+
243
+ def characters(text)
244
+ @value_buffer << text if @inside_value
245
+ if @inside_inline_text
246
+ if @inside_is_r
247
+ @is_run_text << text
248
+ else
249
+ @inline_text_buffer << text
250
+ end
251
+ end
252
+ @formula_buffer << text if @inside_formula
253
+ end
254
+
255
+ def end_element(_uri, local_name, qname)
256
+ name = element_name(local_name, qname)
257
+
258
+ case name
259
+ when "v"
260
+ @inside_value = false
261
+ when "f"
262
+ @inside_formula = false
263
+ when "t"
264
+ @inside_inline_text = false
265
+ when "rPr"
266
+ @inside_is_rpr = false
267
+ when "r"
268
+ if @inside_is_r
269
+ run = { text: @is_run_text.dup }
270
+ run[:font] = @is_current_font.dup unless @is_current_font.empty?
271
+ @is_runs << run
272
+ @inside_is_r = false
273
+ end
274
+ when "is"
275
+ @inside_is = false
276
+ when "c"
277
+ store_cell_value
278
+ @cell_phonetic[@current_cell_ref] = true if @current_cell_ph
279
+ @current_cell_ref = nil
280
+ @current_cell_type = nil
281
+ @value_buffer = +""
282
+ @inline_text_buffer = +""
283
+ @formula_buffer = +""
284
+ end
285
+ end
286
+
287
+ private
288
+
289
+ def parse_row_attributes(attributes)
290
+ row_num = attributes["r"]&.to_i
291
+ return unless row_num
292
+
293
+ attrs = {}
294
+ ht = attributes["ht"]
295
+ attrs[:height] = ht.to_f if ht && attributes["customHeight"] == "1"
296
+ attrs[:hidden] = true if attributes["hidden"] == "1"
297
+ ol = attributes["outlineLevel"]
298
+ attrs[:outline_level] = ol.to_i if ol && ol != "0"
299
+ attrs[:collapsed] = true if attributes["collapsed"] == "1"
300
+ s = attributes["s"]
301
+ attrs[:style] = s.to_i if s
302
+ attrs[:thick_top] = true if attributes["thickTop"] == "1"
303
+ attrs[:thick_bot] = true if attributes["thickBot"] == "1"
304
+ attrs[:ph] = true if attributes["ph"] == "1"
305
+ @row_attributes[row_num] = attrs unless attrs.empty?
306
+ end
307
+
308
+ def store_cell_value
309
+ return if @current_cell_ref.nil?
310
+
311
+ unless @formula_buffer.empty? && @formula_type.nil?
312
+ cached = @value_buffer.empty? ? nil : @value_buffer.dup
313
+ f_type = { "shared" => :shared, "array" => :array, "dataTable" => :data_table }[@formula_type]
314
+ @cells[@current_cell_ref] = Xlsxrb::Elements::Formula.new(
315
+ expression: @formula_buffer.dup,
316
+ cached_value: cached,
317
+ type: f_type,
318
+ ref: @formula_ref,
319
+ shared_index: @formula_si,
320
+ calculate_always: @formula_ca,
321
+ aca: @formula_aca,
322
+ bx: @formula_bx,
323
+ dt2d: @formula_dt2d,
324
+ dtr: @formula_dtr,
325
+ r1: @formula_r1,
326
+ r2: @formula_r2
327
+ )
328
+ return
329
+ end
330
+
331
+ case @current_cell_type
332
+ when "inlineStr"
333
+ @cells[@current_cell_ref] = if @is_has_runs
334
+ Xlsxrb::Elements::RichText.new(runs: @is_runs.map(&:dup))
335
+ else
336
+ @inline_text_buffer.dup
337
+ end
338
+ when "s"
339
+ index = @value_buffer.to_i
340
+ @cells[@current_cell_ref] = @shared_strings[index] || ""
341
+ when "e"
342
+ code = @value_buffer.dup
343
+ @cells[@current_cell_ref] = if Xlsxrb::Elements::VALID_ERROR_CODES.include?(code)
344
+ Xlsxrb::Elements::CellError.new(code:)
345
+ else
346
+ code
347
+ end
348
+ when "b"
349
+ @cells[@current_cell_ref] = @value_buffer.strip == "1"
350
+ when nil, "", "n"
351
+ return if @value_buffer.empty?
352
+
353
+ raw = @value_buffer.dup
354
+ @cells[@current_cell_ref] = numeric_value(raw)
355
+ end
356
+ end
357
+
358
+ def numeric_value(raw)
359
+ if raw.include?(".")
360
+ raw.to_f
361
+ else
362
+ raw.to_i
363
+ end
364
+ end
365
+
366
+ def element_name(local_name, qname)
367
+ if local_name.nil? || local_name.empty?
368
+ qname.to_s.split(":").last
369
+ else
370
+ local_name
371
+ end
372
+ end
373
+ end
374
+
375
+ # SAX2 listener for parsing workbook.xml to discover sheet names, rIds, and workbook-level properties.
376
+ class WorkbookListener
377
+ include REXML::SAX2Listener
378
+
379
+ attr_reader :sheets, :workbook_properties, :workbook_views, :calc_properties, :defined_names,
380
+ :workbook_protection, :file_version, :file_sharing, :conformance, :file_recovery_properties
381
+
382
+ def initialize
383
+ @sheets = []
384
+ @workbook_properties = {}
385
+ @workbook_views = {}
386
+ @calc_properties = {}
387
+ @file_recovery_properties = {}
388
+ @defined_names = []
389
+ @workbook_protection = nil
390
+ @file_version = {}
391
+ @file_sharing = {}
392
+ @conformance = nil
393
+ @inside_defined_name = false
394
+ @current_dn_attrs = nil
395
+ @dn_text_buffer = +""
396
+ end
397
+
398
+ def start_element(_uri, local_name, qname, attributes)
399
+ name = element_name(local_name, qname)
400
+ case name
401
+ when "workbook"
402
+ @conformance = attributes["conformance"] if attributes["conformance"]
403
+ when "sheet"
404
+ @sheets << { name: attributes["name"], rid: attributes["r:id"], state: attributes["state"] }
405
+ when "fileVersion"
406
+ an = attributes["appName"]
407
+ @file_version[:app_name] = an if an
408
+ le = attributes["lastEdited"]
409
+ @file_version[:last_edited] = le if le
410
+ loe = attributes["lowestEdited"]
411
+ @file_version[:lowest_edited] = loe if loe
412
+ rb = attributes["rupBuild"]
413
+ @file_version[:rup_build] = rb if rb
414
+ cn = attributes["codeName"]
415
+ @file_version[:code_name] = cn if cn
416
+ when "fileSharing"
417
+ @file_sharing[:read_only_recommended] = true if %w[1 true].include?(attributes["readOnlyRecommended"])
418
+ un = attributes["userName"]
419
+ @file_sharing[:user_name] = un if un
420
+ an = attributes["algorithmName"]
421
+ @file_sharing[:algorithm_name] = an if an
422
+ hv = attributes["hashValue"]
423
+ @file_sharing[:hash_value] = hv if hv
424
+ sv = attributes["saltValue"]
425
+ @file_sharing[:salt_value] = sv if sv
426
+ sc = attributes["spinCount"]
427
+ @file_sharing[:spin_count] = sc.to_i if sc
428
+ when "workbookPr"
429
+ d1904 = attributes["date1904"]
430
+ @workbook_properties[:date1904] = %w[1 true].include?(d1904) unless d1904.nil?
431
+ dtv = attributes["defaultThemeVersion"]
432
+ @workbook_properties[:default_theme_version] = dtv.to_i if dtv
433
+ cn = attributes["codeName"]
434
+ @workbook_properties[:code_name] = cn if cn
435
+ fp = attributes["filterPrivacy"]
436
+ @workbook_properties[:filter_privacy] = %w[1 true].include?(fp) unless fp.nil?
437
+ acp = attributes["autoCompressPictures"]
438
+ @workbook_properties[:auto_compress_pictures] = %w[1 true].include?(acp) unless acp.nil?
439
+ bf = attributes["backupFile"]
440
+ @workbook_properties[:backup_file] = %w[1 true].include?(bf) unless bf.nil?
441
+ so = attributes["showObjects"]
442
+ @workbook_properties[:show_objects] = so if so
443
+ ul = attributes["updateLinks"]
444
+ @workbook_properties[:update_links] = ul if ul
445
+ rac = attributes["refreshAllConnections"]
446
+ @workbook_properties[:refresh_all_connections] = %w[1 true].include?(rac) unless rac.nil?
447
+ cc = attributes["checkCompatibility"]
448
+ @workbook_properties[:check_compatibility] = %w[1 true].include?(cc) unless cc.nil?
449
+ hpfl = attributes["hidePivotFieldList"]
450
+ @workbook_properties[:hide_pivot_field_list] = %w[1 true].include?(hpfl) unless hpfl.nil?
451
+ sbut = attributes["showBorderUnselectedTables"]
452
+ @workbook_properties[:show_border_unselected_tables] = %w[1 true].include?(sbut) unless sbut.nil?
453
+ ps = attributes["promptedSolutions"]
454
+ @workbook_properties[:prompted_solutions] = %w[1 true].include?(ps) unless ps.nil?
455
+ sia = attributes["showInkAnnotation"]
456
+ @workbook_properties[:show_ink_annotation] = %w[1 true].include?(sia) unless sia.nil?
457
+ selv = attributes["saveExternalLinkValues"]
458
+ @workbook_properties[:save_external_link_values] = %w[1 true].include?(selv) unless selv.nil?
459
+ spcf = attributes["showPivotChartFilter"]
460
+ @workbook_properties[:show_pivot_chart_filter] = %w[1 true].include?(spcf) unless spcf.nil?
461
+ arq = attributes["allowRefreshQuery"]
462
+ @workbook_properties[:allow_refresh_query] = %w[1 true].include?(arq) unless arq.nil?
463
+ pi = attributes["publishItems"]
464
+ @workbook_properties[:publish_items] = %w[1 true].include?(pi) unless pi.nil?
465
+ dcompat = attributes["dateCompatibility"]
466
+ @workbook_properties[:date_compatibility] = %w[1 true].include?(dcompat) unless dcompat.nil?
467
+ when "workbookView"
468
+ at = attributes["activeTab"]
469
+ @workbook_views[:active_tab] = at.to_i if at
470
+ fs = attributes["firstSheet"]
471
+ @workbook_views[:first_sheet] = fs.to_i if fs
472
+ vis = attributes["visibility"]
473
+ @workbook_views[:visibility] = vis if vis
474
+ min = attributes["minimized"]
475
+ @workbook_views[:minimized] = %w[1 true].include?(min) unless min.nil?
476
+ shs = attributes["showHorizontalScroll"]
477
+ @workbook_views[:show_horizontal_scroll] = %w[1 true].include?(shs) unless shs.nil?
478
+ svs = attributes["showVerticalScroll"]
479
+ @workbook_views[:show_vertical_scroll] = %w[1 true].include?(svs) unless svs.nil?
480
+ sst = attributes["showSheetTabs"]
481
+ @workbook_views[:show_sheet_tabs] = %w[1 true].include?(sst) unless sst.nil?
482
+ xw = attributes["xWindow"]
483
+ @workbook_views[:x_window] = xw.to_i if xw
484
+ yw = attributes["yWindow"]
485
+ @workbook_views[:y_window] = yw.to_i if yw
486
+ ww = attributes["windowWidth"]
487
+ @workbook_views[:window_width] = ww.to_i if ww
488
+ wh = attributes["windowHeight"]
489
+ @workbook_views[:window_height] = wh.to_i if wh
490
+ tr = attributes["tabRatio"]
491
+ @workbook_views[:tab_ratio] = tr.to_i if tr
492
+ afdg = attributes["autoFilterDateGrouping"]
493
+ @workbook_views[:auto_filter_date_grouping] = %w[1 true].include?(afdg) unless afdg.nil?
494
+ when "calcPr"
495
+ ci = attributes["calcId"]
496
+ @calc_properties[:calc_id] = ci.to_i if ci
497
+ cm = attributes["calcMode"]
498
+ @calc_properties[:calc_mode] = cm if cm
499
+ fcol = attributes["fullCalcOnLoad"]
500
+ @calc_properties[:full_calc_on_load] = %w[1 true].include?(fcol) unless fcol.nil?
501
+ iter = attributes["iterate"]
502
+ @calc_properties[:iterate] = %w[1 true].include?(iter) unless iter.nil?
503
+ ic = attributes["iterateCount"]
504
+ @calc_properties[:iterate_count] = ic.to_i if ic
505
+ id = attributes["iterateDelta"]
506
+ @calc_properties[:iterate_delta] = id.to_f if id
507
+ rm = attributes["refMode"]
508
+ @calc_properties[:ref_mode] = rm if rm
509
+ cc = attributes["calcCompleted"]
510
+ @calc_properties[:calc_completed] = %w[1 true].include?(cc) unless cc.nil?
511
+ cos = attributes["calcOnSave"]
512
+ @calc_properties[:calc_on_save] = %w[1 true].include?(cos) unless cos.nil?
513
+ fprec = attributes["fullPrecision"]
514
+ @calc_properties[:full_precision] = %w[1 true].include?(fprec) unless fprec.nil?
515
+ conc = attributes["concurrentCalc"]
516
+ @calc_properties[:concurrent_calc] = %w[1 true].include?(conc) unless conc.nil?
517
+ cmc = attributes["concurrentManualCount"]
518
+ @calc_properties[:concurrent_manual_count] = cmc.to_i if cmc
519
+ ffc = attributes["forceFullCalc"]
520
+ @calc_properties[:force_full_calc] = %w[1 true].include?(ffc) unless ffc.nil?
521
+ when "fileRecoveryPr"
522
+ ar = attributes["autoRecover"]
523
+ @file_recovery_properties[:auto_recover] = %w[1 true].include?(ar) unless ar.nil?
524
+ cs = attributes["crashSave"]
525
+ @file_recovery_properties[:crash_save] = %w[1 true].include?(cs) unless cs.nil?
526
+ del = attributes["dataExtractLoad"]
527
+ @file_recovery_properties[:data_extract_load] = %w[1 true].include?(del) unless del.nil?
528
+ rl = attributes["repairLoad"]
529
+ @file_recovery_properties[:repair_load] = %w[1 true].include?(rl) unless rl.nil?
530
+ when "workbookProtection"
531
+ prot = {}
532
+ ls = attributes["lockStructure"]
533
+ prot[:lock_structure] = %w[1 true].include?(ls) unless ls.nil?
534
+ lw = attributes["lockWindows"]
535
+ prot[:lock_windows] = %w[1 true].include?(lw) unless lw.nil?
536
+ lr = attributes["lockRevision"]
537
+ prot[:lock_revision] = %w[1 true].include?(lr) unless lr.nil?
538
+ wp = attributes["workbookPassword"]
539
+ prot[:password] = wp if wp
540
+ an = attributes["workbookAlgorithmName"]
541
+ if an
542
+ prot[:algorithm_name] = an
543
+ hv = attributes["workbookHashValue"]
544
+ prot[:hash_value] = hv if hv
545
+ sv = attributes["workbookSaltValue"]
546
+ prot[:salt_value] = sv if sv
547
+ sc = attributes["workbookSpinCount"]
548
+ prot[:spin_count] = sc.to_i if sc
549
+ end
550
+ ran = attributes["revisionsAlgorithmName"]
551
+ if ran
552
+ prot[:revisions_algorithm_name] = ran
553
+ rhv = attributes["revisionsHashValue"]
554
+ prot[:revisions_hash_value] = rhv if rhv
555
+ rsv = attributes["revisionsSaltValue"]
556
+ prot[:revisions_salt_value] = rsv if rsv
557
+ rsc = attributes["revisionsSpinCount"]
558
+ prot[:revisions_spin_count] = rsc.to_i if rsc
559
+ end
560
+ rp = attributes["revisionsPassword"]
561
+ prot[:revisions_password] = rp if rp
562
+ @workbook_protection = prot unless prot.empty?
563
+ when "definedName"
564
+ @inside_defined_name = true
565
+ @current_dn_attrs = {
566
+ name: attributes["name"],
567
+ hidden: %w[1 true].include?(attributes["hidden"])
568
+ }
569
+ lsi = attributes["localSheetId"]
570
+ @current_dn_attrs[:local_sheet_id] = lsi.to_i if lsi
571
+ @current_dn_attrs[:comment] = attributes["comment"] if attributes["comment"]
572
+ @current_dn_attrs[:description] = attributes["description"] if attributes["description"]
573
+ @current_dn_attrs[:function] = true if %w[1 true].include?(attributes["function"])
574
+ @current_dn_attrs[:vb_procedure] = true if %w[1 true].include?(attributes["vbProcedure"])
575
+ @current_dn_attrs[:xlm] = true if %w[1 true].include?(attributes["xlm"])
576
+ @current_dn_attrs[:shortcut_key] = attributes["shortcutKey"] if attributes["shortcutKey"]
577
+ @current_dn_attrs[:publish_to_server] = true if %w[1 true].include?(attributes["publishToServer"])
578
+ @current_dn_attrs[:workbook_parameter] = true if %w[1 true].include?(attributes["workbookParameter"])
579
+ fgi = attributes["functionGroupId"]
580
+ @current_dn_attrs[:function_group_id] = fgi.to_i if fgi
581
+ @current_dn_attrs[:custom_menu] = attributes["customMenu"] if attributes["customMenu"]
582
+ @current_dn_attrs[:help] = attributes["help"] if attributes["help"]
583
+ @current_dn_attrs[:status_bar] = attributes["statusBar"] if attributes["statusBar"]
584
+ @dn_text_buffer = +""
585
+ end
586
+ end
587
+
588
+ def characters(text)
589
+ @dn_text_buffer << text if @inside_defined_name
590
+ end
591
+
592
+ def end_element(_uri, local_name, qname)
593
+ name = element_name(local_name, qname)
594
+ return unless name == "definedName" && @inside_defined_name
595
+
596
+ @current_dn_attrs[:value] = @dn_text_buffer.dup
597
+ @defined_names << @current_dn_attrs
598
+ @inside_defined_name = false
599
+ @current_dn_attrs = nil
600
+ end
601
+
602
+ private
603
+
604
+ def element_name(local_name, qname)
605
+ if local_name.nil? || local_name.empty?
606
+ qname.to_s.split(":").last
607
+ else
608
+ local_name
609
+ end
610
+ end
611
+ end
612
+
613
+ # SAX2 listener for parsing .rels files to map rId to Target.
614
+ class RelsListener
615
+ include REXML::SAX2Listener
616
+
617
+ attr_reader :relationships
618
+
619
+ def initialize
620
+ @relationships = []
621
+ end
622
+
623
+ def start_element(_uri, local_name, qname, attributes)
624
+ name = element_name(local_name, qname)
625
+ return unless name == "Relationship"
626
+
627
+ @relationships << { id: attributes["Id"], target: attributes["Target"], type: attributes["Type"] }
628
+ end
629
+
630
+ private
631
+
632
+ def element_name(local_name, qname)
633
+ if local_name.nil? || local_name.empty?
634
+ qname.to_s.split(":").last
635
+ else
636
+ local_name
637
+ end
638
+ end
639
+ end
640
+
641
+ # SAX2 listener for parsing <cols><col> elements from worksheet XML.
642
+ class ColumnsListener
643
+ include REXML::SAX2Listener
644
+
645
+ # Returns { column_index => width } hash (1-based indices).
646
+ attr_reader :raw_columns, :raw_column_attrs
647
+
648
+ def initialize
649
+ @raw_columns = {}
650
+ @raw_column_attrs = {}
651
+ end
652
+
653
+ def columns
654
+ @raw_columns
655
+ end
656
+
657
+ def start_element(_uri, local_name, qname, attributes)
658
+ name = element_name(local_name, qname)
659
+ return unless name == "col"
660
+
661
+ min_val = attributes["min"]&.to_i
662
+ max_val = attributes["max"]&.to_i
663
+ width = attributes["width"]&.to_f
664
+ return unless min_val && max_val
665
+
666
+ (min_val..max_val).each do |i|
667
+ @raw_columns[i] = width if width
668
+ attrs = {}
669
+ attrs[:hidden] = true if attributes["hidden"] == "1"
670
+ attrs[:best_fit] = true if attributes["bestFit"] == "1"
671
+ ol = attributes["outlineLevel"]
672
+ attrs[:outline_level] = ol.to_i if ol && ol != "0"
673
+ attrs[:collapsed] = true if attributes["collapsed"] == "1"
674
+ s = attributes["style"]
675
+ attrs[:style] = s.to_i if s && s != "0"
676
+ attrs[:phonetic] = true if attributes["phonetic"] == "1"
677
+ @raw_column_attrs[i] = attrs unless attrs.empty?
678
+ end
679
+ end
680
+
681
+ private
682
+
683
+ def element_name(local_name, qname)
684
+ if local_name.nil? || local_name.empty?
685
+ qname.to_s.split(":").last
686
+ else
687
+ local_name
688
+ end
689
+ end
690
+ end
691
+
692
+ # SAX2 listener for parsing <mergeCells><mergeCell> elements.
693
+ class MergeCellsListener
694
+ include REXML::SAX2Listener
695
+
696
+ attr_reader :ranges
697
+
698
+ def initialize
699
+ @ranges = []
700
+ end
701
+
702
+ def start_element(_uri, local_name, qname, attributes)
703
+ name = element_name(local_name, qname)
704
+ return unless name == "mergeCell"
705
+
706
+ ref = attributes["ref"]
707
+ @ranges << ref if ref
708
+ end
709
+
710
+ private
711
+
712
+ def element_name(local_name, qname)
713
+ if local_name.nil? || local_name.empty?
714
+ qname.to_s.split(":").last
715
+ else
716
+ local_name
717
+ end
718
+ end
719
+ end
720
+
721
+ # SAX2 listener for parsing <hyperlinks><hyperlink> elements.
722
+ class HyperlinksListener
723
+ include REXML::SAX2Listener
724
+
725
+ attr_reader :links
726
+
727
+ def initialize
728
+ @links = []
729
+ end
730
+
731
+ def start_element(_uri, local_name, qname, attributes)
732
+ name = element_name(local_name, qname)
733
+ return unless name == "hyperlink"
734
+
735
+ ref = attributes["ref"]
736
+ return unless ref
737
+
738
+ link = { ref: ref }
739
+ link[:rid] = attributes["r:id"] if attributes["r:id"]
740
+ link[:display] = attributes["display"] if attributes["display"]
741
+ link[:tooltip] = attributes["tooltip"] if attributes["tooltip"]
742
+ link[:location] = attributes["location"] if attributes["location"]
743
+ @links << link
744
+ end
745
+
746
+ private
747
+
748
+ def element_name(local_name, qname)
749
+ if local_name.nil? || local_name.empty?
750
+ qname.to_s.split(":").last
751
+ else
752
+ local_name
753
+ end
754
+ end
755
+ end
756
+
757
+ # SAX2 listener for parsing <dimension> element.
758
+ class DimensionListener
759
+ include REXML::SAX2Listener
760
+
761
+ attr_reader :ref
762
+
763
+ def initialize
764
+ @ref = nil
765
+ end
766
+
767
+ def start_element(_uri, local_name, qname, attributes)
768
+ name = element_name(local_name, qname)
769
+ @ref = attributes["ref"] if name == "dimension"
770
+ end
771
+
772
+ private
773
+
774
+ def element_name(local_name, qname)
775
+ if local_name.nil? || local_name.empty?
776
+ qname.to_s.split(":").last
777
+ else
778
+ local_name
779
+ end
780
+ end
781
+ end
782
+ end
783
+ end
784
+ end