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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -34
- data/README.md +26 -21
- data/lib/xlsxrb/chart_builder.rb +18 -18
- data/lib/xlsxrb/dsl_helpers.rb +175 -0
- data/lib/xlsxrb/elements/cell.rb +56 -22
- data/lib/xlsxrb/elements/column.rb +27 -3
- data/lib/xlsxrb/elements/row.rb +27 -8
- data/lib/xlsxrb/elements/workbook.rb +27 -14
- data/lib/xlsxrb/elements/worksheet.rb +17 -1
- data/lib/xlsxrb/ooxml/cfb.rb +9 -6
- data/lib/xlsxrb/ooxml/crypto/agile.rb +5 -5
- data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +784 -0
- data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +3478 -0
- data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +1796 -0
- data/lib/xlsxrb/ooxml/reader/listeners/style_listeners.rb +428 -0
- data/lib/xlsxrb/ooxml/reader/listeners.rb +4 -6446
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +3 -5
- data/lib/xlsxrb/ooxml/utils.rb +5 -6
- data/lib/xlsxrb/ooxml/workbook_writer.rb +5 -7
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +172 -9
- data/lib/xlsxrb/ooxml/writer/drawing_xml.rb +1602 -0
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +731 -0
- data/lib/xlsxrb/ooxml/writer/styles_xml.rb +348 -0
- data/lib/xlsxrb/ooxml/writer.rb +9 -2646
- data/lib/xlsxrb/ooxml/xml_builder.rb +31 -2
- data/lib/xlsxrb/ooxml/zip_generator.rb +26 -5
- data/lib/xlsxrb/ooxml/zip_reader.rb +364 -78
- data/lib/xlsxrb/ooxml/zip_writer.rb +32 -5
- data/lib/xlsxrb/stream_sheet.rb +94 -6
- data/lib/xlsxrb/stream_writer.rb +48 -115
- data/lib/xlsxrb/style_builder.rb +10 -1
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +1 -1
- data/lib/xlsxrb/worksheet_builder.rb +19 -81
- data/lib/xlsxrb.rb +449 -171
- data/sig/generated/xlsxrb/chart_builder.rbs +36 -36
- data/sig/generated/xlsxrb/dsl_helpers.rbs +92 -0
- data/sig/generated/xlsxrb/elements/cell.rbs +29 -0
- data/sig/generated/xlsxrb/elements/column.rbs +32 -21
- data/sig/generated/xlsxrb/elements/row.rbs +94 -14
- data/sig/generated/xlsxrb/elements/workbook.rbs +102 -10
- data/sig/generated/xlsxrb/elements/worksheet.rbs +9 -0
- data/sig/generated/xlsxrb/ooxml/reader/listeners/core_listeners.rbs +173 -0
- data/sig/generated/xlsxrb/ooxml/reader/listeners/drawing_listeners.rbs +400 -0
- data/sig/generated/xlsxrb/ooxml/reader/listeners/feature_listeners.rbs +442 -0
- data/sig/generated/xlsxrb/ooxml/reader/listeners/style_listeners.rbs +80 -0
- data/sig/generated/xlsxrb/ooxml/reader/listeners.rbs +0 -1072
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +0 -2
- data/sig/generated/xlsxrb/ooxml/utils.rbs +2 -0
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +5 -4
- data/sig/generated/xlsxrb/ooxml/writer/drawing_xml.rbs +79 -0
- data/sig/generated/xlsxrb/ooxml/writer/features_xml.rbs +83 -0
- data/sig/generated/xlsxrb/ooxml/writer/styles_xml.rbs +53 -0
- data/sig/generated/xlsxrb/ooxml/writer.rbs +6 -185
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +18 -0
- data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +15 -0
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +96 -8
- data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +5 -1
- data/sig/generated/xlsxrb/stream_sheet.rbs +43 -3
- data/sig/generated/xlsxrb/style_builder.rbs +7 -0
- data/sig/generated/xlsxrb.rbs +18 -11
- metadata +22 -144
- data/.devcontainer/Dockerfile +0 -65
- data/.devcontainer/devcontainer.json +0 -17
- data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/ast/2.4/ast.rbs +0 -73
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +0 -4
- data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +0 -16
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +0 -96
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +0 -4
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +0 -68
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +0 -249
- data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +0 -4
- data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +0 -47
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +0 -5
- data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/csv/3.3/csv.rbs +0 -3871
- data/.gem_rbs_collection/csv/3.3/manifest.yaml +0 -3
- data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +0 -48
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/listen/3.9/listen.rbs +0 -25
- data/.gem_rbs_collection/listen/3.9/listener.rbs +0 -24
- data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/logger/1.7/formatter.rbs +0 -45
- data/.gem_rbs_collection/logger/1.7/log_device.rbs +0 -100
- data/.gem_rbs_collection/logger/1.7/logger.rbs +0 -796
- data/.gem_rbs_collection/logger/1.7/manifest.yaml +0 -2
- data/.gem_rbs_collection/logger/1.7/period.rbs +0 -17
- data/.gem_rbs_collection/logger/1.7/severity.rbs +0 -34
- data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +0 -2332
- data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +0 -4
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +0 -86
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +0 -7
- data/.gem_rbs_collection/parser/3.2/parser.rbs +0 -194
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +0 -4
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +0 -7
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +0 -209
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +0 -5
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +0 -2
- data/.gem_rbs_collection/rake/13.0/rake.rbs +0 -39
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +0 -17
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +0 -208
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +0 -903
- data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +0 -9
- data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +0 -8
- data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +0 -42
- data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +0 -5
- data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +0 -47
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +0 -30
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +0 -9
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +0 -7
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +0 -19
- data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +0 -31
- data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +0 -18
- data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +0 -12
- data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +0 -11
- data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +0 -13
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +0 -95
- data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +0 -31
- data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +0 -58
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +0 -24
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +0 -17
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +0 -23
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +0 -22
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +0 -30
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +0 -20
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +0 -15
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +0 -26
- data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +0 -21
- data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +0 -131
- data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +0 -14
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +0 -33
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +0 -21
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +0 -63
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +0 -55
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +0 -35
- data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +0 -7
- data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +0 -10
- data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +0 -22
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +0 -29
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +0 -17
- data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +0 -13
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +0 -10
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +0 -8
- data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +0 -6
- data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +0 -30
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +0 -10
- data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +0 -10
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +0 -5
- data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +0 -15
- data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +0 -3
- data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +0 -40
- data/Rakefile +0 -567
- data/Steepfile +0 -18
- data/benchmark.rb +0 -406
- data/docs/ARCHITECTURE.md +0 -514
- data/docs/DEVELOPMENT.md +0 -133
- data/docs/PEER_LIBRARIES.md +0 -121
- data/docs/QUALITY_ASSURANCE.md +0 -25
- data/docs/SPEC_SOURCES.md +0 -48
- data/docs/assets/benchmark_results.svg +0 -144
- data/docs/assets/lsp_autocompletion.png +0 -0
- data/docs/assets/playground_preview.png +0 -0
- data/docs/coi-serviceworker.js +0 -82
- data/docs/office_thread.js +0 -77
- data/docs/preview.html +0 -719
- data/docs/visual/VisualGallery.md +0 -4672
- data/docs/wasm/ruby.wasm +0 -0
- data/docs/wasm/wasm_doc_helper.css +0 -351
- data/docs/wasm/wasm_doc_helper.js +0 -439
- data/docs/zeta.js +0 -1107
- data/rbs_collection.lock.yaml +0 -252
- data/rbs_collection.yaml +0 -19
- data/vendor/sdk_runner/Program.cs +0 -93
- data/vendor/sdk_runner/sdk_runner.csproj +0 -13
data/lib/xlsxrb/elements/cell.rb
CHANGED
|
@@ -201,8 +201,12 @@ module Xlsxrb
|
|
|
201
201
|
end
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
-
#
|
|
205
|
-
|
|
204
|
+
# Pure calculation: converts a 0-based column index to an Excel letter.
|
|
205
|
+
#
|
|
206
|
+
# @param index [Integer] 0-based column index.
|
|
207
|
+
# @return [String] Excel column letter.
|
|
208
|
+
#: (Integer index) -> String
|
|
209
|
+
def self.calculate_column_letter(index)
|
|
206
210
|
result = +""
|
|
207
211
|
i = index
|
|
208
212
|
loop do
|
|
@@ -210,8 +214,11 @@ module Xlsxrb
|
|
|
210
214
|
i = (i / 26) - 1
|
|
211
215
|
break if i.negative?
|
|
212
216
|
end
|
|
213
|
-
result
|
|
214
|
-
end
|
|
217
|
+
result
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Cache column letters up to Excel's limit (16,384)
|
|
221
|
+
@column_letters = (0...16_384).map { |index| calculate_column_letter(index).freeze }.freeze
|
|
215
222
|
|
|
216
223
|
# Converts a 0-based column index to an Excel letter (0 -> "A", 25 -> "Z", 26 -> "AA").
|
|
217
224
|
#
|
|
@@ -226,16 +233,16 @@ module Xlsxrb
|
|
|
226
233
|
def self.column_letter(index)
|
|
227
234
|
raise ArgumentError, "Column index must be a non-negative Integer, got #{index.inspect}" unless index.is_a?(Integer) && index >= 0
|
|
228
235
|
|
|
229
|
-
@column_letters[index] ||
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
@column_letters[index] || calculate_column_letter(index)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Pure calculation: converts an uppercase column letter string to a 0-based index.
|
|
240
|
+
#
|
|
241
|
+
# @param letter [String] Uppercase column letter string (e.g. "A", "AA").
|
|
242
|
+
# @return [Integer] 0-based column index.
|
|
243
|
+
#: (String letter) -> Integer
|
|
244
|
+
def self.calculate_column_index(letter)
|
|
245
|
+
letter.bytes.reduce(0) { |acc, b| (acc * 26) + (b - 64) } - 1
|
|
239
246
|
end
|
|
240
247
|
|
|
241
248
|
# Converts a column letter (e.g. "A", :AA) to a 0-based column index.
|
|
@@ -265,7 +272,7 @@ module Xlsxrb
|
|
|
265
272
|
|
|
266
273
|
raise ArgumentError, "Invalid column letter: #{letter.inspect}" unless str.match?(/\A[a-zA-Z]+\z/)
|
|
267
274
|
|
|
268
|
-
str.upcase
|
|
275
|
+
calculate_column_index(str.upcase)
|
|
269
276
|
end
|
|
270
277
|
|
|
271
278
|
# Parses an Excel-style reference to [row_index, col_index] (both 0-based).
|
|
@@ -299,10 +306,40 @@ module Xlsxrb
|
|
|
299
306
|
end
|
|
300
307
|
return nil if i.zero? || i == len
|
|
301
308
|
|
|
302
|
-
|
|
309
|
+
row_part = bytes.byteslice(i, len - i)
|
|
310
|
+
return nil unless row_part.match?(/\A[1-9]\d*\z/)
|
|
311
|
+
|
|
312
|
+
row = row_part.to_i - 1
|
|
303
313
|
[row, col - 1]
|
|
304
314
|
end
|
|
305
315
|
|
|
316
|
+
# Returns whether the coordinates are within OOXML limits (row: 0..1,048,575, col: 0..16,383).
|
|
317
|
+
#
|
|
318
|
+
# @param row_index [Integer] 0-based row index.
|
|
319
|
+
# @param column_index [Integer] 0-based column index.
|
|
320
|
+
# @return [Boolean]
|
|
321
|
+
#: (untyped row_index, untyped column_index) -> bool
|
|
322
|
+
def self.valid_coordinates?(row_index, column_index)
|
|
323
|
+
row_index.instance_of?(Integer) && row_index >= 0 && row_index < 1_048_576 &&
|
|
324
|
+
column_index.instance_of?(Integer) && column_index >= 0 && column_index < 16_384
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Returns whether the value type is supported by OOXML specifications.
|
|
328
|
+
#
|
|
329
|
+
# @param value [Object]
|
|
330
|
+
# @return [Boolean]
|
|
331
|
+
#: (untyped value) -> bool
|
|
332
|
+
def self.valid_value?(value)
|
|
333
|
+
case value
|
|
334
|
+
when nil, String, Numeric, true, false, Date, Time, Formula, RichText, CellError
|
|
335
|
+
true
|
|
336
|
+
when Hash
|
|
337
|
+
value.key?(:formula)
|
|
338
|
+
else
|
|
339
|
+
false
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
306
343
|
# Validates cell coordinates and value type against OOXML specifications.
|
|
307
344
|
#
|
|
308
345
|
# @param row_index [Integer]
|
|
@@ -311,18 +348,15 @@ module Xlsxrb
|
|
|
311
348
|
# @return [Array<String>] List of errors.
|
|
312
349
|
#: (untyped row_index, untyped column_index, untyped value) -> Array[String]
|
|
313
350
|
def self.validate(row_index, column_index, value)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
(value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false || value.is_a?(Date) || value.is_a?(Time) || value.is_a?(Formula) || (value.is_a?(Hash) && value.key?(:formula)) || value.is_a?(RichText) || value.is_a?(CellError))
|
|
317
|
-
return EMPTY_ERRORS
|
|
318
|
-
end
|
|
351
|
+
valid_val = valid_value?(value)
|
|
352
|
+
return EMPTY_ERRORS if valid_coordinates?(row_index, column_index) && valid_val
|
|
319
353
|
|
|
320
354
|
errs = []
|
|
321
355
|
errs << "row_index must be a non-negative Integer (got #{row_index.inspect})" if !row_index.is_a?(Integer) || row_index.negative?
|
|
322
356
|
errs << "column_index must be a non-negative Integer (got #{column_index.inspect})" if !column_index.is_a?(Integer) || column_index.negative?
|
|
323
357
|
errs << "row_index must be < 1048576 (got #{row_index}, max row is 1048575)" if row_index.is_a?(Integer) && row_index >= 1_048_576
|
|
324
358
|
errs << "column_index must be < 16384 (got #{column_index}, max column is XFD=16383)" if column_index.is_a?(Integer) && column_index >= 16_384
|
|
325
|
-
errs << "unsupported value type: #{value.class} (#{value.inspect}) — supported types: String, Numeric, true/false, Date, Time, or nil" unless
|
|
359
|
+
errs << "unsupported value type: #{value.class} (#{value.inspect}) — supported types: String, Numeric, true/false, Date, Time, Formula, RichText, CellError, or nil" unless valid_val
|
|
326
360
|
errs
|
|
327
361
|
end
|
|
328
362
|
end
|
|
@@ -11,7 +11,9 @@ module Xlsxrb
|
|
|
11
11
|
# col = Elements::Column.new(index: 0, width: 25.0)
|
|
12
12
|
#
|
|
13
13
|
# @api public
|
|
14
|
-
|
|
14
|
+
# rubocop:disable Style/DataInheritance -- Required as class syntax for mutant subject matcher
|
|
15
|
+
class Column < Data.define(:index, :width, :hidden, :custom_width, :outline_level, :unmapped_data, :errors)
|
|
16
|
+
# rubocop:enable Style/DataInheritance
|
|
15
17
|
# @param index [Integer] 0-based column index.
|
|
16
18
|
# @param width [Float, Integer, nil] Column width in characters.
|
|
17
19
|
# @param hidden [Boolean] Whether the column is hidden.
|
|
@@ -36,6 +38,20 @@ module Xlsxrb
|
|
|
36
38
|
errors.empty?
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
# Returns whether the column index is within valid OOXML range (0..16383).
|
|
42
|
+
#
|
|
43
|
+
# @param index [Object]
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
#: (untyped index) -> bool
|
|
46
|
+
def self.valid_index?(index)
|
|
47
|
+
case index
|
|
48
|
+
when Integer
|
|
49
|
+
index >= 0 && index < 16_384
|
|
50
|
+
else
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
39
55
|
# Validates column index against OOXML limits.
|
|
40
56
|
#
|
|
41
57
|
# @param index [Integer]
|
|
@@ -43,8 +59,16 @@ module Xlsxrb
|
|
|
43
59
|
#: (untyped index) -> Array[String]
|
|
44
60
|
def self.validate(index)
|
|
45
61
|
errs = []
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
case index
|
|
63
|
+
when Integer
|
|
64
|
+
if index.negative?
|
|
65
|
+
errs << "index must be a non-negative Integer (got #{index})"
|
|
66
|
+
elsif index >= 16_384
|
|
67
|
+
errs << "index must be < 16384 (got #{index}, max column is XFD=16383)"
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
errs << "index must be a non-negative Integer (got #{index.inspect})"
|
|
71
|
+
end
|
|
48
72
|
errs
|
|
49
73
|
end
|
|
50
74
|
end
|
data/lib/xlsxrb/elements/row.rb
CHANGED
|
@@ -13,8 +13,10 @@ module Xlsxrb
|
|
|
13
13
|
# row.to_a # array of cell values
|
|
14
14
|
#
|
|
15
15
|
# @api public
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# rubocop:disable Style/DataInheritance -- Required as class syntax for mutant subject matcher
|
|
17
|
+
class Row < Data.define(:index, :cells, :height, :hidden, :custom_height, :outline_level, :unmapped_data, :errors)
|
|
18
|
+
# rubocop:enable Style/DataInheritance
|
|
19
|
+
[Enumerable].each { |m| include m }
|
|
18
20
|
|
|
19
21
|
# @param index [Integer] 0-based row index.
|
|
20
22
|
# @param cells [Array<Elements::Cell>] Cells in this row.
|
|
@@ -152,6 +154,20 @@ module Xlsxrb
|
|
|
152
154
|
result
|
|
153
155
|
end
|
|
154
156
|
|
|
157
|
+
# Returns whether the row index is within valid OOXML range (0..1048575).
|
|
158
|
+
#
|
|
159
|
+
# @param index [Object]
|
|
160
|
+
# @return [Boolean]
|
|
161
|
+
#: (untyped index) -> bool
|
|
162
|
+
def self.valid_index?(index)
|
|
163
|
+
case index
|
|
164
|
+
when Integer
|
|
165
|
+
index >= 0 && index < 1_048_576
|
|
166
|
+
else
|
|
167
|
+
false
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
155
171
|
# Validates row index and cells against OOXML limits.
|
|
156
172
|
#
|
|
157
173
|
# @param index [Integer]
|
|
@@ -159,16 +175,19 @@ module Xlsxrb
|
|
|
159
175
|
# @return [Array<String>] List of errors.
|
|
160
176
|
#: (untyped index, untyped cells) -> Array[String]
|
|
161
177
|
def self.validate(index, cells)
|
|
162
|
-
return EMPTY_ERRORS if index.is_a?(Integer) && index >= 0 && index < 1_048_576 && cells.is_a?(Array)
|
|
163
|
-
|
|
164
178
|
errs = []
|
|
165
|
-
|
|
179
|
+
case index
|
|
180
|
+
when Integer
|
|
181
|
+
if index.negative?
|
|
182
|
+
errs << "index must be a non-negative Integer (got #{index})"
|
|
183
|
+
elsif index >= 1_048_576
|
|
184
|
+
errs << "index must be < 1048576 (got #{index}, max row is 1048575)"
|
|
185
|
+
end
|
|
186
|
+
else
|
|
166
187
|
errs << "index must be a non-negative Integer (got #{index.inspect})"
|
|
167
|
-
elsif index >= 1_048_576
|
|
168
|
-
errs << "index must be < 1048576 (got #{index}, max row is 1048576)"
|
|
169
188
|
end
|
|
170
189
|
errs << "cells must be an Array (got #{cells.class})" unless cells.is_a?(Array)
|
|
171
|
-
errs
|
|
190
|
+
errs.empty? ? EMPTY_ERRORS : errs
|
|
172
191
|
end
|
|
173
192
|
end
|
|
174
193
|
end
|
|
@@ -12,15 +12,17 @@ module Xlsxrb
|
|
|
12
12
|
# workbook.each { |s| puts s.name }
|
|
13
13
|
#
|
|
14
14
|
# @api public
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
# rubocop:disable Style/DataInheritance -- Required as class syntax for mutant subject matcher
|
|
16
|
+
class Workbook < Data.define(:sheets, :shared_strings, :styles, :unmapped_data, :errors)
|
|
17
|
+
# rubocop:enable Style/DataInheritance
|
|
18
|
+
[Enumerable].each { |m| include m }
|
|
17
19
|
|
|
18
|
-
# @param sheets [Array<Elements::Worksheet>] Worksheets in the workbook.
|
|
20
|
+
# @param sheets [Array<Elements::Worksheet, StreamSheet>] Worksheets in the workbook.
|
|
19
21
|
# @param shared_strings [Array<String>] Shared strings table.
|
|
20
22
|
# @param styles [Hash] Styles definition.
|
|
21
23
|
# @param unmapped_data [Hash] Additional metadata for round-tripping.
|
|
22
24
|
# @param errors [Array<String>, nil] Validation errors.
|
|
23
|
-
#: (?sheets: Array[Elements::Worksheet], ?shared_strings: Array[String], ?styles: Hash[untyped, untyped], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
|
|
25
|
+
#: (?sheets: Array[Elements::Worksheet | StreamSheet], ?shared_strings: Array[String], ?styles: Hash[untyped, untyped], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
|
|
24
26
|
def initialize(sheets: [], shared_strings: [], styles: {}, unmapped_data: {}, errors: nil)
|
|
25
27
|
computed_errors = errors || self.class.validate(sheets)
|
|
26
28
|
super(sheets: sheets.freeze, shared_strings: shared_strings.freeze, styles: styles,
|
|
@@ -35,14 +37,14 @@ module Xlsxrb
|
|
|
35
37
|
# end
|
|
36
38
|
#
|
|
37
39
|
# @yield [sheet]
|
|
38
|
-
# @yieldparam sheet [Elements::Worksheet]
|
|
40
|
+
# @yieldparam sheet [Elements::Worksheet, StreamSheet]
|
|
39
41
|
# @return [Enumerator, void]
|
|
40
|
-
#: () { (Elements::Worksheet) -> void } -> void
|
|
41
|
-
#: () -> Enumerator[Elements::Worksheet, void]
|
|
42
|
+
#: () { (Elements::Worksheet | StreamSheet) -> void } -> void
|
|
43
|
+
#: () -> Enumerator[Elements::Worksheet | StreamSheet, void]
|
|
42
44
|
def each(&)
|
|
43
45
|
sheets.each(&)
|
|
44
46
|
end
|
|
45
|
-
|
|
47
|
+
alias each_sheet each
|
|
46
48
|
|
|
47
49
|
# Returns whether the workbook is valid according to ECMA-376 rules.
|
|
48
50
|
#
|
|
@@ -58,10 +60,10 @@ module Xlsxrb
|
|
|
58
60
|
# wb.sheet(0)
|
|
59
61
|
# wb.sheet("Sales")
|
|
60
62
|
#
|
|
61
|
-
# @param identifier [Integer, String] 0-based index or sheet name.
|
|
62
|
-
# @return [Elements::Worksheet, nil]
|
|
63
|
+
# @param identifier [Integer, String, untyped] 0-based index or sheet name.
|
|
64
|
+
# @return [Elements::Worksheet, StreamSheet, nil]
|
|
63
65
|
# @api public
|
|
64
|
-
#: (?Integer | String identifier) -> Elements::Worksheet?
|
|
66
|
+
#: (?Integer | String | untyped identifier) -> (Elements::Worksheet | StreamSheet)?
|
|
65
67
|
def sheet(identifier = 0)
|
|
66
68
|
case identifier
|
|
67
69
|
when Integer
|
|
@@ -70,7 +72,7 @@ module Xlsxrb
|
|
|
70
72
|
sheets.find { |s| s.name == identifier }
|
|
71
73
|
end
|
|
72
74
|
end
|
|
73
|
-
|
|
75
|
+
alias [] sheet
|
|
74
76
|
|
|
75
77
|
# Loads all sheets into memory, returning an Elements::Workbook where every
|
|
76
78
|
# worksheet is a fully-parsed Elements::Worksheet supporting coordinate random access.
|
|
@@ -84,9 +86,20 @@ module Xlsxrb
|
|
|
84
86
|
#: () -> Elements::Workbook
|
|
85
87
|
def load
|
|
86
88
|
loaded_sheets = sheets.map { |s| s.respond_to?(:load) ? s.load : s }
|
|
89
|
+
close
|
|
87
90
|
with(sheets: loaded_sheets)
|
|
88
91
|
end
|
|
89
|
-
|
|
92
|
+
alias to_workbook load
|
|
93
|
+
|
|
94
|
+
# Closes any streaming resources associated with worksheets.
|
|
95
|
+
#
|
|
96
|
+
# @return [void]
|
|
97
|
+
# @api public
|
|
98
|
+
#: () -> void
|
|
99
|
+
def close
|
|
100
|
+
sheets.each { |s| s.close if s.respond_to?(:close) }
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
90
103
|
|
|
91
104
|
# Returns a new Workbook with the specified sheet updated.
|
|
92
105
|
# Yields the matched worksheet to the block, which must return a new Worksheet.
|
|
@@ -102,7 +115,7 @@ module Xlsxrb
|
|
|
102
115
|
# @yieldreturn [Elements::Worksheet] The modified worksheet.
|
|
103
116
|
# @return [Elements::Workbook] A new Workbook instance.
|
|
104
117
|
# @api public
|
|
105
|
-
#: (Integer | String identifier) { (Elements::Worksheet) -> Elements::Worksheet } -> Elements::Workbook
|
|
118
|
+
#: (Integer | String identifier) ?{ (Elements::Worksheet) -> Elements::Worksheet } -> Elements::Workbook
|
|
106
119
|
def update_sheet(identifier)
|
|
107
120
|
raise ArgumentError, "block is required" unless block_given?
|
|
108
121
|
|
|
@@ -197,6 +197,22 @@ module Xlsxrb
|
|
|
197
197
|
end
|
|
198
198
|
alias to_worksheet load
|
|
199
199
|
|
|
200
|
+
FORBIDDEN_NAME_CHARS = %r{[\\/?*\[\]]}
|
|
201
|
+
|
|
202
|
+
# Returns whether the worksheet name is valid according to OOXML specifications (1..31 chars, no forbidden chars).
|
|
203
|
+
#
|
|
204
|
+
# @param name [Object]
|
|
205
|
+
# @return [Boolean]
|
|
206
|
+
#: (untyped name) -> bool
|
|
207
|
+
def self.valid_name?(name)
|
|
208
|
+
case name
|
|
209
|
+
when String
|
|
210
|
+
!name.empty? && name.size <= 31 && !name.match?(FORBIDDEN_NAME_CHARS)
|
|
211
|
+
else
|
|
212
|
+
false
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
200
216
|
# Validates worksheet name and rows against OOXML limits.
|
|
201
217
|
#
|
|
202
218
|
# @param name [String]
|
|
@@ -209,7 +225,7 @@ module Xlsxrb
|
|
|
209
225
|
errs << "worksheet name must be a non-empty String (got #{name.inspect})"
|
|
210
226
|
else
|
|
211
227
|
errs << "worksheet name cannot exceed 31 characters (got #{name.size})" if name.size > 31
|
|
212
|
-
errs << "worksheet name cannot contain \\, /, ?, *, [, or ]" if name.match?(
|
|
228
|
+
errs << "worksheet name cannot contain \\, /, ?, *, [, or ]" if name.match?(FORBIDDEN_NAME_CHARS)
|
|
213
229
|
end
|
|
214
230
|
errs << "rows must be an Array (got #{rows.class})" unless rows.is_a?(Array)
|
|
215
231
|
if rows.is_a?(Array)
|
data/lib/xlsxrb/ooxml/cfb.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Xlsxrb
|
|
|
6
6
|
module Ooxml
|
|
7
7
|
# Pure-Ruby Compound File Binary (CFB / OLE Structured Storage) implementation for [MS-CFB] / [MS-OFFCRYPTO].
|
|
8
8
|
module Cfb
|
|
9
|
-
MAGIC = "
|
|
9
|
+
MAGIC = [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1].pack("C*").freeze
|
|
10
10
|
|
|
11
11
|
FREESECT = 0xFFFFFFFF
|
|
12
12
|
ENDOFCHAIN = 0xFFFFFFFE
|
|
@@ -61,9 +61,12 @@ module Xlsxrb
|
|
|
61
61
|
attr_reader :entries
|
|
62
62
|
|
|
63
63
|
def self.cfb?(data)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
case data
|
|
65
|
+
when String
|
|
66
|
+
data.start_with?(MAGIC)
|
|
67
|
+
else
|
|
68
|
+
false
|
|
69
|
+
end
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
def initialize(data)
|
|
@@ -372,7 +375,7 @@ module Xlsxrb
|
|
|
372
375
|
num_minifat_sec = (minifat_bytes.bytesize + sector_size - 1) / sector_size
|
|
373
376
|
num_minifat_sec.times do |s_idx|
|
|
374
377
|
chunk = minifat_bytes[s_idx * sector_size, sector_size] || "".b
|
|
375
|
-
chunk = chunk.ljust(sector_size,
|
|
378
|
+
chunk = chunk.ljust(sector_size, 255.chr(Encoding::BINARY)) if chunk.bytesize < sector_size
|
|
376
379
|
allocated_sectors << chunk
|
|
377
380
|
end
|
|
378
381
|
sector_chains << [first_minifat_sec, num_minifat_sec]
|
|
@@ -400,7 +403,7 @@ module Xlsxrb
|
|
|
400
403
|
fat[dir_sector_id] = ENDOFCHAIN
|
|
401
404
|
fat[fat_sector_id] = FATSECT
|
|
402
405
|
|
|
403
|
-
fat_bytes = fat.pack("V*").ljust(sector_size,
|
|
406
|
+
fat_bytes = fat.pack("V*").ljust(sector_size, 255.chr(Encoding::BINARY))
|
|
404
407
|
allocated_sectors << fat_bytes
|
|
405
408
|
|
|
406
409
|
# Build Header (512 bytes)
|
|
@@ -11,11 +11,11 @@ module Xlsxrb
|
|
|
11
11
|
module Crypto
|
|
12
12
|
# Implements Microsoft Office Agile Encryption specified in [MS-OFFCRYPTO] Section 2.3.4.
|
|
13
13
|
class Agile
|
|
14
|
-
BLOCK_KEY_VERIFIER_INPUT = "
|
|
15
|
-
BLOCK_KEY_VERIFIER_VALUE = "
|
|
16
|
-
BLOCK_KEY_KEY = "
|
|
17
|
-
BLOCK_KEY_INTEGRITY_KEY = "
|
|
18
|
-
BLOCK_KEY_INTEGRITY_VALUE = "
|
|
14
|
+
BLOCK_KEY_VERIFIER_INPUT = [0xfe, 0xa7, 0xd2, 0x76, 0x3b, 0x4b, 0x9e, 0x79].pack("C*").freeze
|
|
15
|
+
BLOCK_KEY_VERIFIER_VALUE = [0xd7, 0xaa, 0x0f, 0x6d, 0x30, 0x61, 0x34, 0x4e].pack("C*").freeze
|
|
16
|
+
BLOCK_KEY_KEY = [0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6].pack("C*").freeze
|
|
17
|
+
BLOCK_KEY_INTEGRITY_KEY = [0x5f, 0xb2, 0xad, 0x01, 0x0c, 0xb9, 0xe1, 0xf6].pack("C*").freeze
|
|
18
|
+
BLOCK_KEY_INTEGRITY_VALUE = [0xa0, 0x67, 0x7f, 0x02, 0xb2, 0x2c, 0x84, 0x33].pack("C*").freeze
|
|
19
19
|
|
|
20
20
|
SEGMENT_SIZE = 4096
|
|
21
21
|
|