xlsxrb 0.1.7 → 0.1.9

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/README.md +125 -36
  4. data/Rakefile +36 -7
  5. data/benchmark.rb +32 -4
  6. data/docs/ARCHITECTURE.md +32 -30
  7. data/docs/DEVELOPMENT.md +11 -3
  8. data/docs/QUALITY_ASSURANCE.md +3 -4
  9. data/docs/SPEC_SOURCES.md +7 -1
  10. data/docs/visual/VisualGallery.md +157 -157
  11. data/docs/wasm/ruby.wasm +0 -0
  12. data/lib/ruby_lsp/xlsxrb/addon.rb +1 -1
  13. data/lib/ruby_lsp/xlsxrb/completion_listener.rb +11 -13
  14. data/lib/xlsxrb/elements/coordinate_access.rb +101 -0
  15. data/lib/xlsxrb/elements/row.rb +2 -2
  16. data/lib/xlsxrb/elements/workbook.rb +20 -2
  17. data/lib/xlsxrb/elements/worksheet.rb +94 -124
  18. data/lib/xlsxrb/elements.rb +1 -0
  19. data/lib/xlsxrb/ooxml/cfb.rb +490 -0
  20. data/lib/xlsxrb/ooxml/crypto/agile.rb +285 -0
  21. data/lib/xlsxrb/ooxml/crypto/standard.rb +152 -0
  22. data/lib/xlsxrb/ooxml/crypto.rb +60 -0
  23. data/lib/xlsxrb/ooxml/worksheet_parser.rb +21 -15
  24. data/lib/xlsxrb/stream_row.rb +181 -0
  25. data/lib/xlsxrb/version.rb +1 -1
  26. data/lib/xlsxrb.rb +271 -122
  27. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +70 -0
  28. data/sig/generated/xlsxrb/elements/worksheet.rbs +119 -14
  29. data/sig/generated/xlsxrb/ooxml/cfb.rbs +121 -0
  30. data/sig/generated/xlsxrb/ooxml/crypto/agile.rbs +46 -0
  31. data/sig/generated/xlsxrb/ooxml/crypto/standard.rbs +28 -0
  32. data/sig/generated/xlsxrb/ooxml/crypto.rbs +17 -0
  33. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +2 -0
  34. data/sig/generated/xlsxrb/stream_row.rbs +127 -0
  35. data/sig/generated/xlsxrb.rbs +135 -71
  36. data/vendor/sdk_runner/Program.cs +3 -1
  37. metadata +13 -1
@@ -27,6 +27,15 @@ module Xlsxrb
27
27
  class ZipError < Error
28
28
  end
29
29
 
30
+ class EncryptedFileError < Error
31
+ end
32
+
33
+ class InvalidPasswordError < EncryptedFileError
34
+ end
35
+
36
+ class DecryptionError < EncryptedFileError
37
+ end
38
+
30
39
  TRACER: untyped
31
40
 
32
41
  def self.in_span: (untyped name, ?attributes: untyped) ?{ (?) -> untyped } -> untyped
@@ -266,33 +275,87 @@ module Xlsxrb
266
275
  # : (String expression, ?cached_value: String | Numeric | bool | nil) -> Elements::Formula
267
276
  def self.formula: (String expression, ?cached_value: String | Numeric | bool | nil) -> Elements::Formula
268
277
 
269
- # Reads an XLSX file into an in-memory Elements::Workbook.
278
+ # Reads an XLSX file (streaming / lazy-loaded by default) from a file path, IO stream, or binary String.
270
279
  #
271
- # @example Read from file path
272
- # workbook = Xlsxrb.read("data.xlsx")
273
- # sheet = workbook["Sheet1"]
274
- # puts sheet["A1"].value
280
+ # Sheets and rows are streamed lazily with O(1) constant memory. If a block is given,
281
+ # yields each StreamSheet sequentially.
282
+ #
283
+ # Call #load on the returned Workbook or Sheet to convert to an in-memory representation
284
+ # for coordinate random access (e.g. sheet["A1"]).
285
+ #
286
+ # @example Streaming read across sheets and rows (O(1) memory)
287
+ # Xlsxrb.read("large.xlsx") do |sheet|
288
+ # puts "Sheet: #{sheet.name}"
289
+ # sheet.each_row do |row|
290
+ # row.each_cell { |cell| puts "#{cell.ref}: #{cell.value}" }
291
+ # end
292
+ # end
275
293
  #
276
- # @example Read from IO stream
277
- # workbook = File.open("data.xlsx", "rb") { |io| Xlsxrb.read(io) }
294
+ # @example Lazy workbook access and explicit in-memory loading
295
+ # wb = Xlsxrb.read("data.xlsx")
296
+ # sheet = wb.sheets.first
297
+ # sheet.each_row { |row| ... } # streams with O(1) memory
298
+ # doc_sheet = sheet.load # explicitly load into memory
299
+ # puts doc_sheet["A1"].value # coordinate random access
278
300
  #
279
- # @param source [String, IO] File path or IO object.
280
- # @return [Elements::Workbook] The parsed workbook.
301
+ # @param source [String, IO] File path, binary content string (starting with PK..), or IO object.
302
+ # @param password [String, nil] Optional password to decrypt password-protected XLSX files.
303
+ # @yield [sheet] Yields each streaming sheet.
304
+ # @yieldparam sheet [StreamSheet] The streaming worksheet object.
305
+ # @return [Elements::Workbook, void] Returns Elements::Workbook when no block is given.
281
306
  # @api public
282
- # : (untyped source) -> Elements::Workbook
283
- def self.read: (untyped source) -> Elements::Workbook
307
+ # : (untyped source, ?password: String?) { (StreamSheet) -> void } -> void
308
+ # : (untyped source, ?password: String?) -> Elements::Workbook
309
+ def self.read: (untyped source, ?password: String?) { (StreamSheet) -> void } -> void
310
+ | (untyped source, ?password: String?) -> Elements::Workbook
284
311
 
285
- # Writes an Elements::Workbook to an XLSX file or IO stream.
312
+ # Writes an XLSX file or IO stream (streaming or in-memory), or returns a binary string.
313
+ #
314
+ # @overload write(target, password: nil, strict_excel_mode: true, &block)
315
+ # Streaming write: yields a StreamWriter context for high-speed, zero-allocation XLSX generation.
316
+ # @param target [String, IO] Destination file path or writable IO object.
317
+ # @param password [String, nil] Optional password to encrypt the generated XLSX file.
318
+ # @param strict_excel_mode [Boolean] Whether to enforce Excel specifications.
319
+ # @yield [stream_writer]
320
+ # @yieldparam stream_writer [Xlsxrb::StreamWriter]
321
+ # @return [void]
322
+ #
323
+ # @overload write(workbook, password: nil)
324
+ # In-memory write: exports the workbook to an in-memory binary String.
325
+ # @param workbook [Elements::Workbook] The workbook to write.
326
+ # @param password [String, nil] Optional password to encrypt the binary string.
327
+ # @return [String] Binary data representing the XLSX file.
328
+ #
329
+ # @overload write(target, workbook, password: nil)
330
+ # In-memory write: writes the workbook to a file path or IO stream.
331
+ # @param target [String, IO] Destination file path or writable IO object.
332
+ # @param workbook [Elements::Workbook] The workbook to write.
333
+ # @param password [String, nil] Optional password to encrypt the output file.
334
+ # @return [void]
335
+ #
336
+ # @example Streaming write to file
337
+ # Xlsxrb.write("output.xlsx") do |writer|
338
+ # writer.sheet("Sheet1") { |s| s.row(["Hello", "World"]) }
339
+ # end
286
340
  #
287
- # @example Write to file
341
+ # @example Password-protected streaming write
342
+ # Xlsxrb.write("protected.xlsx", password: "secret_password") do |writer|
343
+ # writer.sheet("Confidential") { |s| s.row(["Private Data", 100]) }
344
+ # end
345
+ #
346
+ # @example In-memory export to binary string
347
+ # binary_data = Xlsxrb.write(workbook)
348
+ #
349
+ # @example In-memory write to file
288
350
  # Xlsxrb.write("output.xlsx", workbook)
289
351
  #
290
- # @param target [String, IO] File path or IO object.
291
- # @param workbook [Elements::Workbook] The workbook to write.
292
- # @return [void]
293
352
  # @api public
294
- # : (untyped target, untyped workbook) -> void
295
- def self.write: (untyped target, untyped workbook) -> void
353
+ # : (Elements::Workbook workbook, ?password: String?, ?encryption_mode: Symbol) -> String
354
+ # : (untyped target, Elements::Workbook | untyped workbook, ?password: String?, ?encryption_mode: Symbol) -> void
355
+ # : (untyped target_or_workbook, ?Elements::Workbook | untyped workbook_or_nil, ?password: String?, ?encryption_mode: Symbol, ?strict_excel_mode: bool) ?{ (StreamWriter) -> void } -> untyped
356
+ def self.write: (Elements::Workbook workbook, ?password: String?, ?encryption_mode: Symbol) -> String
357
+ | (untyped target, Elements::Workbook | untyped workbook, ?password: String?, ?encryption_mode: Symbol) -> void
358
+ | (untyped target_or_workbook, ?Elements::Workbook | untyped workbook_or_nil, ?password: String?, ?encryption_mode: Symbol, ?strict_excel_mode: bool) ?{ (StreamWriter) -> void } -> untyped
296
359
 
297
360
  # Modifies an existing XLSX file.
298
361
  # Reads the workbook, passes it to the block, and writes the result.
@@ -309,24 +372,36 @@ module Xlsxrb
309
372
  #
310
373
  # @param source [String, IO] The source file path or IO object.
311
374
  # @param target [String, IO, nil] The target file path or IO object. If nil, overwrites source.
375
+ # @param password [String, nil] Optional password for reading and writing protected files.
312
376
  # @yield [workbook] Yields the parsed workbook.
313
377
  # @yieldparam workbook [Elements::Workbook] The parsed workbook.
314
378
  # @yieldreturn [Elements::Workbook] The modified workbook.
315
379
  # @return [void]
316
380
  # @api public
317
- # : (untyped source, ?untyped target) ?{ (Elements::Workbook) -> untyped } -> void
318
- def self.modify: (untyped source, ?untyped target) ?{ (Elements::Workbook) -> untyped } -> void
381
+ # : (untyped source, ?untyped target, ?password: String?) ?{ (Elements::Workbook) -> untyped } -> void
382
+ def self.modify: (untyped source, ?untyped target, ?password: String?) ?{ (Elements::Workbook) -> untyped } -> void
319
383
 
320
384
  # Represents a sheet being streamed sequentially from an XLSX file.
385
+ # Provides O(1) constant-memory streaming over rows and cells.
386
+ #
387
+ # Call #load (or #to_worksheet) to convert this streaming sheet into an
388
+ # in-memory Elements::Worksheet supporting coordinate random access (sheet["A1"]).
321
389
  #
322
- # @example Iterate rows in streaming mode
323
- # Xlsxrb.foreach("large_data.xlsx") do |sheet|
390
+ # @example Iterate rows and cells in streaming mode (O(1) memory)
391
+ # Xlsxrb.read("large_data.xlsx") do |sheet|
324
392
  # puts "Processing sheet: #{sheet.name}"
325
393
  # sheet.each_row do |row|
326
- # puts row.to_a.inspect
394
+ # row.each_cell do |cell|
395
+ # puts "#{cell.ref}: #{cell.value}"
396
+ # end
327
397
  # end
328
398
  # end
329
399
  #
400
+ # @example Load into an in-memory Worksheet for coordinate random access
401
+ # wb = Xlsxrb.read("data.xlsx")
402
+ # doc_sheet = wb.sheet(0).load
403
+ # puts doc_sheet["A1"].value
404
+ #
330
405
  # @api public
331
406
  class StreamSheet
332
407
  attr_reader name: untyped
@@ -334,65 +409,54 @@ module Xlsxrb
334
409
  # @param name [String] The sheet name.
335
410
  # @param sheet_xml [String] Raw XML content of the sheet.
336
411
  # @param shared_strings [Array<String>] Shared strings table.
337
- # : (String name, String sheet_xml, Array[String] shared_strings) -> void
338
- def initialize: (String name, String sheet_xml, Array[String] shared_strings) -> void
412
+ # @param styles [Hash, nil] Styles table.
413
+ # : (String name, String sheet_xml, Array[String] shared_strings, ?Hash[untyped, untyped]? styles) -> void
414
+ def initialize: (String name, String sheet_xml, Array[String] shared_strings, ?Hash[untyped, untyped]? styles) -> void
339
415
 
340
- # Iterate over rows in this streaming sheet.
416
+ # Iterate over rows in this streaming sheet (O(1) memory).
341
417
  #
342
418
  # @yield [row]
343
- # @yieldparam row [Elements::Row]
419
+ # @yieldparam row [StreamRow, Elements::Row]
344
420
  # @return [Enumerator, void]
345
421
  # @api public
346
- # : () { (Elements::Row) -> void } -> void
347
- # : | () -> Enumerator[Elements::Row, void]
348
- def each_row: () { (Elements::Row) -> void } -> void
422
+ # : () { (StreamRow | Elements::Row) -> void } -> void
423
+ # : () -> Enumerator[StreamRow | Elements::Row, void]
424
+ def each_row: () { (StreamRow | Elements::Row) -> void } -> void
425
+ | () -> Enumerator[StreamRow | Elements::Row, void]
349
426
 
350
- # Iterate over rows in this streaming sheet.
427
+ # Iterate over all cells across rows continuously (O(1) memory).
351
428
  #
352
- # @yield [row]
353
- # @yieldparam row [Elements::Row]
429
+ # @yield [cell]
430
+ # @yieldparam cell [Elements::Cell]
354
431
  # @return [Enumerator, void]
355
432
  # @api public
356
- # : () { (Elements::Row) -> void } -> void
357
- # : | () -> Enumerator[Elements::Row, void]
358
- def each: () { (Elements::Row) -> void } -> void
359
- end
433
+ # : () { (Elements::Cell) -> void } -> void
434
+ # : () -> Enumerator[Elements::Cell, void]
435
+ def each_cell: () { (Elements::Cell) -> void } -> void
436
+ | () -> Enumerator[Elements::Cell, void]
360
437
 
361
- # Streaming read: yields StreamSheet objects one at a time for each sheet in the workbook.
362
- # Keeps memory usage minimal even for multi-gigabyte XLSX files.
363
- #
364
- # @example
365
- # Xlsxrb.foreach("large.xlsx") do |sheet|
366
- # puts "Sheet: #{sheet.name}"
367
- # sheet.each_row { |row| process(row) }
368
- # end
369
- #
370
- # @param source [String, IO] File path or IO object.
371
- # @yield [sheet] Yields each sheet.
372
- # @yieldparam sheet [StreamSheet] The streaming sheet object.
373
- # @return [Enumerator, void]
374
- # @api public
375
- # : (untyped source) ?{ (StreamSheet) -> void } -> untyped
376
- def self.foreach: (untyped source) ?{ (StreamSheet) -> void } -> untyped
438
+ # Default Enumerable iteration iterates rows in the streaming sheet.
439
+ #
440
+ # @yield [row]
441
+ # @yieldparam row [StreamRow, Elements::Row]
442
+ # @return [Enumerator, void]
443
+ # @api public
444
+ # : () { (StreamRow | Elements::Row) -> void } -> void
445
+ # : () -> Enumerator[StreamRow | Elements::Row, void]
446
+ def each: () { (StreamRow | Elements::Row) -> void } -> void
447
+ | () -> Enumerator[StreamRow | Elements::Row, void]
448
+
449
+ # Loads this sheet completely into an in-memory Elements::Worksheet,
450
+ # enabling coordinate random access (sheet["A1"]), row lookups (row_at),
451
+ # and immutable cell updates (update_cell).
452
+ #
453
+ # @return [Elements::Worksheet] The fully parsed in-memory worksheet.
454
+ # @api public
455
+ # : () -> Elements::Worksheet
456
+ def load: () -> Elements::Worksheet
377
457
 
378
- # Streaming write: yields a StreamWriter context for high-speed, zero-allocation XLSX generation.
379
- #
380
- # @example Generate an Excel file with styles and multiple sheets
381
- # Xlsxrb.generate("sales.xlsx") do |stream_writer|
382
- # stream_writer.sheet("Q1") do |sheet|
383
- # sheet.row(["Product", "Revenue"], styles: :bold)
384
- # sheet.row(["Widget", 15000])
385
- # end
386
- # end
387
- #
388
- # @param target [String, IO] File path or IO stream (e.g. pipe, socket, Rails response buffer).
389
- # @param strict_excel_mode [Boolean] Whether to enforce Excel specifications (max rows/cols/length).
390
- # @yield [stream_writer]
391
- # @yieldparam stream_writer [Xlsxrb::StreamWriter]
392
- # @return [void]
393
- # @api public
394
- # : (untyped target, ?strict_excel_mode: bool) ?{ (StreamWriter) -> void } -> void
395
- def self.generate: (untyped target, ?strict_excel_mode: bool) ?{ (StreamWriter) -> void } -> void
458
+ alias to_worksheet load
459
+ end
396
460
 
397
461
  # Builds an in-memory Elements::Workbook using a declarative DSL.
398
462
  #
@@ -46,13 +46,15 @@ async Task<ScenarioExecResult> RunScenario(string code, string xlsxPath)
46
46
  typeof(Enumerable).Assembly,
47
47
  typeof(Console).Assembly,
48
48
  typeof(SpreadsheetDocument).Assembly,
49
- typeof(OpenXmlValidator).Assembly
49
+ typeof(OpenXmlValidator).Assembly,
50
+ typeof(System.Security.Cryptography.Aes).Assembly
50
51
  )
51
52
  .AddImports(
52
53
  "System",
53
54
  "System.IO",
54
55
  "System.Linq",
55
56
  "System.Collections.Generic",
57
+ "System.Security.Cryptography",
56
58
  "DocumentFormat.OpenXml",
57
59
  "DocumentFormat.OpenXml.Packaging",
58
60
  "DocumentFormat.OpenXml.Spreadsheet",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xlsxrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - niku
@@ -184,11 +184,16 @@ files:
184
184
  - lib/xlsxrb/elements.rb
185
185
  - lib/xlsxrb/elements/cell.rb
186
186
  - lib/xlsxrb/elements/column.rb
187
+ - lib/xlsxrb/elements/coordinate_access.rb
187
188
  - lib/xlsxrb/elements/row.rb
188
189
  - lib/xlsxrb/elements/types.rb
189
190
  - lib/xlsxrb/elements/workbook.rb
190
191
  - lib/xlsxrb/elements/worksheet.rb
191
192
  - lib/xlsxrb/ooxml.rb
193
+ - lib/xlsxrb/ooxml/cfb.rb
194
+ - lib/xlsxrb/ooxml/crypto.rb
195
+ - lib/xlsxrb/ooxml/crypto/agile.rb
196
+ - lib/xlsxrb/ooxml/crypto/standard.rb
192
197
  - lib/xlsxrb/ooxml/reader.rb
193
198
  - lib/xlsxrb/ooxml/shared_strings_parser.rb
194
199
  - lib/xlsxrb/ooxml/styles_parser.rb
@@ -203,6 +208,7 @@ files:
203
208
  - lib/xlsxrb/ooxml/zip_generator.rb
204
209
  - lib/xlsxrb/ooxml/zip_reader.rb
205
210
  - lib/xlsxrb/ooxml/zip_writer.rb
211
+ - lib/xlsxrb/stream_row.rb
206
212
  - lib/xlsxrb/style_builder.rb
207
213
  - lib/xlsxrb/version.rb
208
214
  - rbs_collection.lock.yaml
@@ -211,11 +217,16 @@ files:
211
217
  - sig/generated/xlsxrb/elements.rbs
212
218
  - sig/generated/xlsxrb/elements/cell.rbs
213
219
  - sig/generated/xlsxrb/elements/column.rbs
220
+ - sig/generated/xlsxrb/elements/coordinate_access.rbs
214
221
  - sig/generated/xlsxrb/elements/row.rbs
215
222
  - sig/generated/xlsxrb/elements/types.rbs
216
223
  - sig/generated/xlsxrb/elements/workbook.rbs
217
224
  - sig/generated/xlsxrb/elements/worksheet.rbs
218
225
  - sig/generated/xlsxrb/ooxml.rbs
226
+ - sig/generated/xlsxrb/ooxml/cfb.rbs
227
+ - sig/generated/xlsxrb/ooxml/crypto.rbs
228
+ - sig/generated/xlsxrb/ooxml/crypto/agile.rbs
229
+ - sig/generated/xlsxrb/ooxml/crypto/standard.rbs
219
230
  - sig/generated/xlsxrb/ooxml/reader.rbs
220
231
  - sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs
221
232
  - sig/generated/xlsxrb/ooxml/styles_parser.rbs
@@ -230,6 +241,7 @@ files:
230
241
  - sig/generated/xlsxrb/ooxml/zip_generator.rbs
231
242
  - sig/generated/xlsxrb/ooxml/zip_reader.rbs
232
243
  - sig/generated/xlsxrb/ooxml/zip_writer.rbs
244
+ - sig/generated/xlsxrb/stream_row.rbs
233
245
  - sig/generated/xlsxrb/style_builder.rbs
234
246
  - sig/generated/xlsxrb/version.rbs
235
247
  - sig/rexml.rbs