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
@@ -109,7 +109,33 @@ module Xlsxrb
109
109
  compressed_size: @entry_compressed_size,
110
110
  uncompressed_size: @entry_uncompressed_size,
111
111
  offset: @entry_offset,
112
- gp_flag: @entry_gp_flag
112
+ gp_flag: @entry_gp_flag,
113
+ method: 8
114
+ }
115
+ end
116
+
117
+ # Copies an entry from an existing archive IO without decompressing or recompressing.
118
+ #: (String path, untyped source_io, data_offset: Integer, compressed_size: Integer, uncompressed_size: Integer, crc32: Integer, ?method: Integer) -> void
119
+ def copy_raw_entry(path, source_io, data_offset:, compressed_size:, uncompressed_size:, crc32:, method: 8)
120
+ raise "ZipWriter is closed" if @closed
121
+
122
+ offset = @bytes_written
123
+ write_local_header(path, crc32, compressed_size, uncompressed_size, gp_flag: 0, method: method)
124
+
125
+ if compressed_size.positive?
126
+ source_io.seek(data_offset, IO::SEEK_SET)
127
+ IO.copy_stream(source_io, @io, compressed_size)
128
+ @bytes_written += compressed_size
129
+ end
130
+
131
+ @entries << {
132
+ path: path,
133
+ crc32: crc32,
134
+ compressed_size: compressed_size,
135
+ uncompressed_size: uncompressed_size,
136
+ offset: offset,
137
+ gp_flag: 0,
138
+ method: method
113
139
  }
114
140
  end
115
141
 
@@ -159,7 +185,8 @@ module Xlsxrb
159
185
  compressed_size: compressed.bytesize,
160
186
  uncompressed_size: content_bytes.bytesize,
161
187
  offset: offset,
162
- gp_flag: 0
188
+ gp_flag: 0,
189
+ method: 8
163
190
  }
164
191
  end
165
192
 
@@ -172,13 +199,13 @@ module Xlsxrb
172
199
  end
173
200
  end
174
201
 
175
- def write_local_header(path, crc, compressed_size, uncompressed_size, gp_flag: 0, count_bytes: true)
202
+ def write_local_header(path, crc, compressed_size, uncompressed_size, gp_flag: 0, count_bytes: true, method: 8)
176
203
  name_bytes = path.encode("UTF-8").b
177
204
  header = [
178
205
  0x04034B50, # local file header signature
179
206
  20, # version needed (2.0)
180
207
  gp_flag, # general purpose bit flag (0x0008 if data descriptor follows)
181
- 8, # compression method (deflate)
208
+ method, # compression method (0 = store, 8 = deflate)
182
209
  0, # last mod file time
183
210
  33, # last mod file date
184
211
  crc,
@@ -206,7 +233,7 @@ module Xlsxrb
206
233
  20, # version made by
207
234
  20, # version needed
208
235
  entry[:gp_flag] || 0, # general purpose bit flag
209
- 8, # compression method
236
+ entry[:method] || 8, # compression method
210
237
  0, # last mod file time
211
238
  33, # last mod file date
212
239
  entry[:crc32],
@@ -36,15 +36,20 @@ module Xlsxrb
36
36
  # Initializes a streaming worksheet context.
37
37
  #
38
38
  # @param name [String] The sheet name.
39
- # @param sheet_xml [String] Raw XML content of the worksheet.
39
+ # @param sheet_source [String, Proc, IO, nil] Raw XML content or chunk supplier/stream.
40
40
  # @param shared_strings [Array<String>] Shared strings table.
41
41
  # @param styles [Hash, nil] Optional parsed styles hash.
42
- #: (String name, String sheet_xml, Array[String] shared_strings, ?Hash[untyped, untyped]? styles) -> void
43
- def initialize(name, sheet_xml, shared_strings, styles = nil)
42
+ # @param zip_reader [Ooxml::ZipReader, nil] Optional ZipReader context.
43
+ # @param entry_name [String, nil] Archive entry name for this sheet.
44
+ #: (String name, untyped sheet_source, Array[String] shared_strings, ?Hash[untyped, untyped]? styles, ?zip_reader: Ooxml::ZipReader?, ?entry_name: String?) -> void
45
+ def initialize(name, sheet_source, shared_strings, styles = nil, zip_reader: nil, entry_name: nil)
44
46
  @name = name
45
- @sheet_xml = sheet_xml
47
+ @sheet_source = sheet_source
46
48
  @shared_strings = shared_strings
47
49
  @styles = styles
50
+ @zip_reader = zip_reader
51
+ @entry_name = entry_name
52
+ @sheet_xml = sheet_source if sheet_source.is_a?(String)
48
53
  end
49
54
 
50
55
  # Iterates over rows in this streaming worksheet with O(1) memory.
@@ -63,7 +68,13 @@ module Xlsxrb
63
68
  def each_row(&)
64
69
  return enum_for(:each_row) unless block_given?
65
70
 
66
- Ooxml::WorksheetParser.each_row(@sheet_xml, shared_strings: @shared_strings, &)
71
+ source = if @sheet_source
72
+ @sheet_source
73
+ elsif @zip_reader && @entry_name
74
+ ->(&blk) { @zip_reader.each_entry_chunk(@entry_name, &blk) }
75
+ end
76
+
77
+ Ooxml::WorksheetParser.each_row(source, shared_strings: @shared_strings, &)
67
78
  end
68
79
 
69
80
  # Iterates over all cells across all rows continuously with O(1) memory.
@@ -112,8 +123,85 @@ module Xlsxrb
112
123
  # @api public
113
124
  #: () -> Elements::Worksheet
114
125
  def load
115
- Xlsxrb.send(:build_worksheet, @name, @sheet_xml, @shared_strings, @styles)
126
+ Xlsxrb.send(:build_worksheet, @name, raw_sheet_xml, @shared_strings, @styles)
116
127
  end
117
128
  alias to_worksheet load
129
+
130
+ # Returns merged cell ranges (e.g. ["A1:B2"]) for this worksheet.
131
+ #
132
+ # @return [Array<String>]
133
+ # @api public
134
+ #: () -> Array[String]
135
+ def merged_cells
136
+ @merged_cells ||= begin
137
+ listener = Ooxml::Reader::MergeCellsListener.new
138
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
139
+ listener.ranges
140
+ end
141
+ end
142
+
143
+ # Returns the auto-filter range (e.g. "A1:E100") for this worksheet, or nil if none.
144
+ #
145
+ # @return [String, nil]
146
+ # @api public
147
+ #: () -> String?
148
+ def auto_filter
149
+ @auto_filter ||= begin
150
+ listener = Ooxml::Reader::AutoFilterListener.new
151
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
152
+ listener.ref
153
+ end
154
+ end
155
+
156
+ # Returns data validation rules configured for this worksheet.
157
+ #
158
+ # @return [Array<Hash[Symbol, untyped]>]
159
+ # @api public
160
+ #: () -> Array[Hash[Symbol, untyped]]
161
+ def data_validations
162
+ @data_validations ||= begin
163
+ listener = Ooxml::Reader::DataValidationsListener.new
164
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
165
+ listener.validations
166
+ end
167
+ end
168
+
169
+ # Returns conditional formatting rules configured for this worksheet.
170
+ #
171
+ # @return [Array<Hash[Symbol, untyped]>]
172
+ # @api public
173
+ #: () -> Array[Hash[Symbol, untyped]]
174
+ def conditional_formats
175
+ @conditional_formats ||= begin
176
+ listener = Ooxml::Reader::ConditionalFormattingListener.new
177
+ Ooxml::XmlParser.parse(raw_sheet_xml, listener)
178
+ listener.rules
179
+ end
180
+ end
181
+
182
+ # Closes any underlying reader resources.
183
+ #: () -> void
184
+ def close
185
+ @zip_reader&.close
186
+ nil
187
+ end
188
+
189
+ private
190
+
191
+ # Returns the full raw XML string for this worksheet, loaded on demand.
192
+ #: () -> String
193
+ def raw_sheet_xml
194
+ @raw_sheet_xml ||= if @sheet_source.is_a?(String)
195
+ @sheet_source
196
+ elsif @zip_reader && @entry_name
197
+ @zip_reader.read_entry(@entry_name) || ""
198
+ elsif @sheet_source.respond_to?(:call)
199
+ buf = +""
200
+ @sheet_source.call { |c| buf << c }
201
+ buf
202
+ else
203
+ ""
204
+ end
205
+ end
118
206
  end
119
207
  end
@@ -5,6 +5,7 @@
5
5
  require "tempfile"
6
6
  require_relative "style_builder"
7
7
  require_relative "chart_builder"
8
+ require_relative "dsl_helpers"
8
9
  require_relative "ooxml/writer"
9
10
  require_relative "ooxml/worksheet_writer"
10
11
  require_relative "ooxml/workbook_writer"
@@ -910,39 +911,11 @@ module Xlsxrb
910
911
  sheet if @current_sheet.nil?
911
912
 
912
913
  row_index = @current_row_index
913
- # See: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
914
- if @strict_excel_mode
915
- raise ArgumentError, "Row index #{row_index} exceeds Excel limit of 1,048,576 rows" if row_index >= 1_048_576
916
- raise ArgumentError, "Row height #{height} must be between 0 and 409 points (Excel limitation)" if height && (height.negative? || height > 409)
917
- end
914
+ DslHelpers.validate_row_bounds!(row_index, height, strict_excel_mode: @strict_excel_mode)
918
915
  @current_row_index += 1
919
916
 
920
- if values.is_a?(Hash)
921
- max_col = values.keys.map { |k| Elements::Cell.column_index(k) }.max || -1
922
- cells_array = Array.new(max_col + 1)
923
- values.each do |k, v|
924
- idx = Elements::Cell.column_index(k)
925
- cells_array[idx] = v
926
- end
927
- values = cells_array
928
- end
929
-
930
- if styles.is_a?(Hash)
931
- expanded_styles = {}
932
- styles.each do |k, v|
933
- if k.is_a?(Range) || k.is_a?(Array)
934
- k.each { |idx| expanded_styles[Elements::Cell.column_index(idx)] = v }
935
- else
936
- expanded_styles[Elements::Cell.column_index(k)] = v
937
- end
938
- end
939
- max_col_style = expanded_styles.keys.max || -1
940
- styles_array = Array.new(max_col_style + 1)
941
- expanded_styles.each do |idx, v|
942
- styles_array[idx] = v
943
- end
944
- styles = styles_array
945
- end
917
+ values = DslHelpers.normalize_row_values(values)
918
+ styles = DslHelpers.normalize_row_styles(styles)
946
919
 
947
920
  if @current_charts && !@current_charts.empty?
948
921
  @current_cells ||= {}
@@ -984,16 +957,9 @@ module Xlsxrb
984
957
  def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil)
985
958
  raise ArgumentError, "Column width #{width} must be between 0 and 255 characters (Excel limitation)" if @strict_excel_mode && width && (width.negative? || width > 255)
986
959
 
987
- indices = case index
988
- when Range, Array
989
- index.map { |i| Elements::Cell.column_index(i) }
990
- else
991
- [Elements::Cell.column_index(index)]
992
- end
993
-
994
960
  sheet if @current_sheet.nil?
995
961
 
996
- indices.each do |idx|
962
+ DslHelpers.normalize_column_indices(index).each do |idx|
997
963
  @current_columns << { index: idx, width: width, hidden: hidden, custom_width: custom_width || !width.nil?, outline_level: outline_level }
998
964
  end
999
965
  end
@@ -1109,14 +1075,9 @@ module Xlsxrb
1109
1075
  # @return [void]
1110
1076
  # @api public
1111
1077
  #: (untyped ref, columns: untyped, ?name: untyped, ?display_name: untyped, ?style: untyped, **untyped opts) -> void
1112
- def table(ref, columns:, name: nil, display_name: nil, style: nil, **opts)
1078
+ def table(ref, columns:, name: nil, display_name: nil, style: nil, **)
1113
1079
  sheet if @current_sheet.nil?
1114
- tbl = { ref: ref, columns: columns }
1115
- tbl[:name] = name if name
1116
- tbl[:display_name] = display_name if display_name
1117
- tbl[:style] = style if style
1118
- tbl.merge!(opts)
1119
- @current_tables << tbl
1080
+ @current_tables << DslHelpers.normalize_table_options(ref, columns: columns, name: name, display_name: display_name, style: style, **)
1120
1081
  end
1121
1082
 
1122
1083
  # Adds a Pivot Table.
@@ -1186,28 +1147,13 @@ module Xlsxrb
1186
1147
  #: (?(String | Hash[Symbol, Integer | String])? range, ?row: Integer?, ?col_start: (Integer | String)?, ?col_end: (Integer | String)?, ?row_start: Integer?, ?row_end: Integer?) -> void
1187
1148
  def merge(range = nil, row: nil, col_start: nil, col_end: nil, row_start: nil, row_end: nil)
1188
1149
  sheet if @current_sheet.nil?
1189
- if range.is_a?(Hash)
1190
- row = range[:row]
1191
- row_start = range[:row_start]
1192
- row_end = range[:row_end]
1193
- col_start = range[:col_start]
1194
- col_end = range[:col_end]
1195
- range = nil
1196
- end
1197
-
1198
- if range
1199
- raise ArgumentError, "Invalid merge range format: '#{range}'. Expected format like 'A1:B2'." if @strict_excel_mode && !range.match?(/^[A-Za-z]{1,3}\d+(:[A-Za-z]{1,3}\d+)?$/)
1200
-
1201
- @current_merge_cells << range
1202
- else
1203
- r_start = row || row_start || 0
1204
- r_end = row || row_end || 0
1205
- c_start = Elements::Cell.column_index(col_start || 0)
1206
- c_end = Elements::Cell.column_index(col_end || 0)
1207
- start_ref = "#{Xlsxrb::Elements::Cell.column_letter(c_start)}#{r_start + 1}"
1208
- end_ref = "#{Xlsxrb::Elements::Cell.column_letter(c_end)}#{r_end + 1}"
1209
- @current_merge_cells << "#{start_ref}:#{end_ref}"
1210
- end
1150
+ canonical = DslHelpers.normalize_merge_range(
1151
+ range,
1152
+ row: row, col_start: col_start, col_end: col_end,
1153
+ row_start: row_start, row_end: row_end,
1154
+ strict_excel_mode: @strict_excel_mode
1155
+ )
1156
+ @current_merge_cells << canonical unless @current_merge_cells.include?(canonical)
1211
1157
  end
1212
1158
 
1213
1159
  # Freezes window panes at the given row and column.
@@ -1263,7 +1209,9 @@ module Xlsxrb
1263
1209
  #: (?left: Float?, ?right: Float?, ?top: Float?, ?bottom: Float?, ?header: Float?, ?footer: Float?) -> void
1264
1210
  def page_margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
1265
1211
  sheet if @current_sheet.nil?
1266
- @current_page_margins = { left: left, right: right, top: top, bottom: bottom, header: header, footer: footer }.compact
1212
+ @current_page_margins = DslHelpers.normalize_page_margins(
1213
+ left: left, right: right, top: top, bottom: bottom, header: header, footer: footer
1214
+ )
1267
1215
  end
1268
1216
 
1269
1217
  # Sets page setup configuration.
@@ -1308,17 +1256,7 @@ module Xlsxrb
1308
1256
  #: (**untyped opts) -> void
1309
1257
  def protect_sheet(**opts)
1310
1258
  sheet if @current_sheet.nil?
1311
- normalized = opts.dup
1312
- plain_password = normalized[:password]
1313
- needs_hash = plain_password.is_a?(String) && !plain_password.empty? &&
1314
- normalized[:algorithm_name].nil? && normalized[:hash_value].nil? &&
1315
- normalized[:salt_value].nil? && normalized[:spin_count].nil? &&
1316
- !plain_password.match?(/\A[0-9A-Fa-f]{4}\z/)
1317
- if needs_hash
1318
- normalized.delete(:password)
1319
- normalized.merge!(Xlsxrb::Ooxml::Utils.hash_password(plain_password))
1320
- end
1321
- @current_sheet_protection = normalized
1259
+ @current_sheet_protection = DslHelpers.normalize_protection_options(opts)
1322
1260
  end
1323
1261
 
1324
1262
  # Inserts an embedded image into the active sheet.
@@ -1543,37 +1481,35 @@ module Xlsxrb
1543
1481
  def close
1544
1482
  raise ArgumentError, "Workbook must contain at least one sheet (Excel limitation)" if @strict_excel_mode && @sheets.empty? && @current_sheet.nil?
1545
1483
 
1546
- Xlsxrb.in_span("StreamWriter#close") do
1547
- flush_current_sheet
1548
-
1549
- styles_definition = {
1550
- fonts: @style_writer.fonts.dup,
1551
- fills: @style_writer.fills.dup,
1552
- borders: @style_writer.borders.dup,
1553
- xf_entries: @style_writer.xf_entries.dup,
1554
- num_fmts: @style_writer.num_fmts.dup,
1555
- dxfs: @dxfs || []
1556
- }
1557
-
1558
- resolved_names = resolve_defined_names(@defined_names, @sheets)
1559
-
1560
- wb_writer = Ooxml::WorkbookWriter.new(
1561
- sheets: @sheets,
1562
- shared_strings: @sst,
1563
- shared_strings_index: @sst_index,
1564
- styles: styles_definition,
1565
- defined_names: resolved_names.empty? ? nil : resolved_names,
1566
- core_properties: @core_properties.empty? ? nil : @core_properties,
1567
- app_properties: @app_properties.empty? ? nil : @app_properties,
1568
- custom_properties: @custom_properties.empty? ? nil : @custom_properties,
1569
- workbook_protection: @workbook_protection,
1570
- workbook_properties: @workbook_properties
1571
- )
1572
- wb_writer.write_package_parts(@zip)
1573
-
1574
- @zip.close
1575
- @io.close if @owns_io && !@io.closed?
1576
- end
1484
+ flush_current_sheet
1485
+
1486
+ styles_definition = {
1487
+ fonts: @style_writer.fonts.dup,
1488
+ fills: @style_writer.fills.dup,
1489
+ borders: @style_writer.borders.dup,
1490
+ xf_entries: @style_writer.xf_entries.dup,
1491
+ num_fmts: @style_writer.num_fmts.dup,
1492
+ dxfs: @dxfs || []
1493
+ }
1494
+
1495
+ resolved_names = resolve_defined_names(@defined_names, @sheets)
1496
+
1497
+ wb_writer = Ooxml::WorkbookWriter.new(
1498
+ sheets: @sheets,
1499
+ shared_strings: @sst,
1500
+ shared_strings_index: @sst_index,
1501
+ styles: styles_definition,
1502
+ defined_names: resolved_names.empty? ? nil : resolved_names,
1503
+ core_properties: @core_properties.empty? ? nil : @core_properties,
1504
+ app_properties: @app_properties.empty? ? nil : @app_properties,
1505
+ custom_properties: @custom_properties.empty? ? nil : @custom_properties,
1506
+ workbook_protection: @workbook_protection,
1507
+ workbook_properties: @workbook_properties
1508
+ )
1509
+ wb_writer.write_package_parts(@zip)
1510
+
1511
+ @zip.close
1512
+ @io.close if @owns_io && !@io.closed?
1577
1513
  ensure
1578
1514
  cleanup!
1579
1515
  end
@@ -1604,10 +1540,7 @@ module Xlsxrb
1604
1540
 
1605
1541
  #: (untyped range) -> untyped
1606
1542
  def absolute_range(range)
1607
- # simplecov:disable
1608
- # Edge case / untested delegation block
1609
- range.gsub(/([A-Z]+)(\d+)/, '$\1$\2')
1610
- # simplecov:enable
1543
+ DslHelpers.absolute_range(range)
1611
1544
  end
1612
1545
 
1613
1546
  #: (untyped names, untyped sheets) -> untyped
@@ -20,7 +20,12 @@ module Xlsxrb
20
20
  grey: "FF808080"
21
21
  }.freeze
22
22
 
23
- def resolve_color(color)
23
+ # Resolves a color symbol, string, or prefix to an aRGB hex string.
24
+ #
25
+ # @param color [Symbol, String, Object, nil]
26
+ # @return [String, nil]
27
+ #: (untyped color) -> String?
28
+ def self.resolve_color(color)
24
29
  return nil unless color
25
30
 
26
31
  if color.is_a?(Symbol) || (color.is_a?(String) && color.start_with?(":") && color.length > 1)
@@ -30,6 +35,10 @@ module Xlsxrb
30
35
  color.to_s
31
36
  end
32
37
 
38
+ def resolve_color(color)
39
+ self.class.resolve_color(color)
40
+ end
41
+
33
42
  #: (?String? name) -> void
34
43
  def initialize(name = nil)
35
44
  @name = name
@@ -3,5 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  module Xlsxrb
6
- VERSION = "0.1.11"
6
+ VERSION = "0.1.13"
7
7
  end
@@ -210,7 +210,7 @@ module Xlsxrb
210
210
 
211
211
  #: (untyped range) -> untyped
212
212
  def absolute_range(range)
213
- range.gsub(/([A-Z]+)(\d+)/, '$\1$\2')
213
+ DslHelpers.absolute_range(range)
214
214
  end
215
215
 
216
216
  #: (untyped names, untyped sheets) -> untyped
@@ -2,6 +2,8 @@
2
2
 
3
3
  # rbs_inline: enabled
4
4
 
5
+ require_relative "dsl_helpers"
6
+
5
7
  module Xlsxrb
6
8
  # DSL context for building a single in-memory worksheet in {Xlsxrb.build}.
7
9
  #
@@ -85,38 +87,10 @@ module Xlsxrb
85
87
  #: (Array[untyped] | Hash[untyped, untyped] values, ?styles: untyped, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
86
88
  def row(values, styles: nil, height: nil, hidden: false, custom_height: false, outline_level: nil)
87
89
  row_index = @rows.size
88
- # See: https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
89
- if @strict_excel_mode
90
- raise ArgumentError, "Row index #{row_index} exceeds Excel limit of 1,048,576 rows" if row_index >= 1_048_576
91
- raise ArgumentError, "Row height #{height} must be between 0 and 409 points (Excel limitation)" if height && (height.negative? || height > 409)
92
- end
90
+ DslHelpers.validate_row_bounds!(row_index, height, strict_excel_mode: @strict_excel_mode)
93
91
 
94
- if values.is_a?(Hash)
95
- max_col = values.keys.map { |k| Elements::Cell.column_index(k) }.max || -1
96
- cells_array = Array.new(max_col + 1)
97
- values.each do |k, v|
98
- idx = Elements::Cell.column_index(k)
99
- cells_array[idx] = v
100
- end
101
- values = cells_array
102
- end
103
-
104
- if styles.is_a?(Hash)
105
- expanded_styles = {}
106
- styles.each do |k, v|
107
- if k.is_a?(Range) || k.is_a?(Array)
108
- k.each { |idx| expanded_styles[Elements::Cell.column_index(idx)] = v }
109
- else
110
- expanded_styles[Elements::Cell.column_index(k)] = v
111
- end
112
- end
113
- max_col_style = expanded_styles.keys.max || -1
114
- styles_array = Array.new(max_col_style + 1)
115
- expanded_styles.each do |idx, v|
116
- styles_array[idx] = v
117
- end
118
- styles = styles_array
119
- end
92
+ values = DslHelpers.normalize_row_values(values)
93
+ styles = DslHelpers.normalize_row_styles(styles)
120
94
 
121
95
  # Auto-detect Date / Time for built-in styles
122
96
  values.each_with_index do |val, idx|
@@ -239,14 +213,7 @@ module Xlsxrb
239
213
  def column(index, width: nil, hidden: false, custom_width: false, outline_level: nil)
240
214
  raise ArgumentError, "Column width #{width} must be between 0 and 255 characters (Excel limitation)" if @strict_excel_mode && width && (width.negative? || width > 255)
241
215
 
242
- indices = case index
243
- when Range, Array
244
- index.map { |i| Elements::Cell.column_index(i) }
245
- else
246
- [Elements::Cell.column_index(index)]
247
- end
248
-
249
- indices.each do |idx|
216
+ DslHelpers.normalize_column_indices(index).each do |idx|
250
217
  @columns << Elements::Column.new(
251
218
  index: idx,
252
219
  width: width,
@@ -359,13 +326,8 @@ module Xlsxrb
359
326
  # @return [void]
360
327
  # @api public
361
328
  #: (String ref, columns: Array[String | Hash[Symbol, untyped]], ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) -> void
362
- def table(ref, columns:, name: nil, display_name: nil, style: nil, **opts)
363
- tbl = { ref: ref, columns: columns }
364
- tbl[:name] = name if name
365
- tbl[:display_name] = display_name if display_name
366
- tbl[:style] = style if style
367
- tbl.merge!(opts)
368
- @tables << tbl
329
+ def table(ref, columns:, name: nil, display_name: nil, style: nil, **)
330
+ @tables << DslHelpers.normalize_table_options(ref, columns: columns, name: name, display_name: display_name, style: style, **)
369
331
  end
370
332
 
371
333
  # Adds a Pivot Table to the sheet.
@@ -436,29 +398,13 @@ module Xlsxrb
436
398
  # @api public
437
399
  #: (?(String | Hash[Symbol, Integer | String])? range, ?row: Integer?, ?col_start: (Integer | String)?, ?col_end: (Integer | String)?, ?row_start: Integer?, ?row_end: Integer?) -> void
438
400
  def merge(range = nil, row: nil, col_start: nil, col_end: nil, row_start: nil, row_end: nil)
439
- if range.is_a?(Hash)
440
- row = range[:row]
441
- row_start = range[:row_start]
442
- row_end = range[:row_end]
443
- col_start = range[:col_start]
444
- col_end = range[:col_end]
445
- range = nil
446
- end
447
-
448
- if range
449
- raise ArgumentError, "Invalid merge range format: '#{range}'. Expected format like 'A1:B2'." if @strict_excel_mode && !range.match?(/^[A-Za-z]{1,3}\d+(:[A-Za-z]{1,3}\d+)?$/)
450
- return if @merge_cells_ranges.include?(range)
451
-
452
- @merge_cells_ranges << range
453
- else
454
- r_start = row || row_start || 0
455
- r_end = row || row_end || 0
456
- c_start = Elements::Cell.column_index(col_start || 0)
457
- c_end = Elements::Cell.column_index(col_end || 0)
458
- start_ref = "#{Xlsxrb::Elements::Cell.column_letter(c_start)}#{r_start + 1}"
459
- end_ref = "#{Xlsxrb::Elements::Cell.column_letter(c_end)}#{r_end + 1}"
460
- @merge_cells_ranges << "#{start_ref}:#{end_ref}"
461
- end
401
+ canonical = DslHelpers.normalize_merge_range(
402
+ range,
403
+ row: row, col_start: col_start, col_end: col_end,
404
+ row_start: row_start, row_end: row_end,
405
+ strict_excel_mode: @strict_excel_mode
406
+ )
407
+ @merge_cells_ranges << canonical unless @merge_cells_ranges.include?(canonical)
462
408
  end
463
409
 
464
410
  # Freezes window panes at the specified row and column.
@@ -510,7 +456,9 @@ module Xlsxrb
510
456
  # @api public
511
457
  #: (?left: Float?, ?right: Float?, ?top: Float?, ?bottom: Float?, ?header: Float?, ?footer: Float?) -> void
512
458
  def page_margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
513
- @page_margins = { left: left, right: right, top: top, bottom: bottom, header: header, footer: footer }.compact
459
+ @page_margins = DslHelpers.normalize_page_margins(
460
+ left: left, right: right, top: top, bottom: bottom, header: header, footer: footer
461
+ )
514
462
  end
515
463
 
516
464
  # Sets page setup configuration (orientation, paper size, fit to page, scaling).
@@ -551,17 +499,7 @@ module Xlsxrb
551
499
  # @api public
552
500
  #: (**untyped opts) -> void
553
501
  def protect_sheet(**opts)
554
- normalized = opts.dup
555
- plain_password = normalized[:password]
556
- needs_hash = plain_password.is_a?(String) && !plain_password.empty? &&
557
- normalized[:algorithm_name].nil? && normalized[:hash_value].nil? &&
558
- normalized[:salt_value].nil? && normalized[:spin_count].nil? &&
559
- !plain_password.match?(/\A[0-9A-Fa-f]{4}\z/)
560
- if needs_hash
561
- normalized.delete(:password)
562
- normalized.merge!(Xlsxrb::Ooxml::Utils.hash_password(plain_password))
563
- end
564
- @sheet_protection = normalized
502
+ @sheet_protection = DslHelpers.normalize_protection_options(opts)
565
503
  end
566
504
 
567
505
  # Inserts an embedded image from raw binary data.