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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf81256a8c23f02b4ef5a1f2efb91122850960da349cc1c00bce37d4055486ac
4
- data.tar.gz: c0fdb2a48e67beb7a59730fe947ca819c5ed72196c858b02a430cb5f85e953c5
3
+ metadata.gz: a6733f82b936fb790e6192de72890ecedd016f21dd4a5e43ee0f5123a2d35f5e
4
+ data.tar.gz: 3dd84ebc97a5c10a2777f6bd7557b06660e690f0723f8af6664a244a143adaa6
5
5
  SHA512:
6
- metadata.gz: a4e3b7098c50d3d9e5958333f49ad2b8c22399f383067c01b0481fb82e9d1eaa50481de15afe8b2b883d6f0ee1697144ae55d028771eeca159cc38f46626043f
7
- data.tar.gz: 25c3979864c017334256986e5697c793f40b22b3cdc7419d18e3ef090925f0297909ca8fafc9188cf37769344e656fa7e900cf8da8f3a310599e1cd9ab61ed8b
6
+ metadata.gz: 6ca0217ecd3a83a922a1098740d584c6e78fd0695f46acaa661286497a0e0de1c9214990eb7fef622a10ad4a1641f9aae73079ab61b0787570a22e434b446d0e
7
+ data.tar.gz: 7603890bc9f20d9143656ea83f0fd75410b4636cc66bbd152e79965b4b2bc07013de05f1489560fc896b9f5e1890c9fb6b421ec8dbc49b66ca9dec7912ab617c
data/CHANGELOG.md CHANGED
@@ -1,15 +1,67 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.13] - 2026-09-25
4
+
5
+ ### Added
6
+ - Non-destructive template modification: Support modifying existing XLSX templates while preserving unmapped parts, media, charts, drawings, and VBA macros via `Xlsxrb.modify`.
7
+ - StreamSheet structural metadata accessors: Expose `merged_cells`, `auto_filter`, `data_validations`, and `conditional_formats` on `StreamSheet`.
8
+ - Pre-Push Smart Change Detection: Optimized `bin/pre-push --all` to automatically skip runtime RBS validation and pure logic mutation testing when unrelated files are modified, adding `--last` and `--force` controls.
9
+
10
+ ### Changed
11
+ - True O(1) memory streaming read via chunked entry inflation in `ZipReader` and `WorksheetParser`, preventing unbounded memory retention on large worksheets.
12
+ - Extracted pure DSL normalizers (`Xlsxrb::DslHelpers`) and fast XML unescape utilities (`Ooxml::XmlBuilder.unescape`).
13
+ - Constrained `rexml` runtime dependency to `~> 3.0` and excluded documentation, build assets, and developer configurations from the gem package.
14
+
15
+ ### Removed
16
+ - OpenTelemetry runtime dependency and tracer spans.
17
+
18
+ ## [0.1.12] - 2026-09-20
19
+
20
+ ### Added
21
+ - Mutation Testing Framework (`mutant`):
22
+ - Full-scale integration of mutant test suite targeting the Functional Core (10 classes and modules, 38 subjects).
23
+ - Achieved 100.00% Kill Rate (1,091 / 1,091 mutations killed, 0 alive) without exclusions.
24
+ - Added dedicated root configuration [`.mutant.yml`](.mutant.yml) for streamlined CLI execution (`bundle exec mutant run -- '...'`).
25
+ - Integrated `bundle exec rake mutant:pure` into GitHub Actions CI pipeline (`main.yml`), guaranteeing mutation-tested quality on every push and pull request.
26
+ - Added architecture and testing guide in [`docs/MUTATION_TESTING.md`](docs/MUTATION_TESTING.md).
27
+ - Pre-Push Quality Verification Script (`bin/pre-push`): Added automated pre-push verification script running RuboCop, Steep type checking, RBS sync validation, Unit & Contract tests, RBS runtime type validation, and pure logic mutation testing, with automatic hook installation in `bin/setup`.
28
+
29
+ ### Changed
30
+ - Functional Core & Imperative Shell Architecture:
31
+ - Purified core business logic and domain entities into deterministic, side-effect-free class methods and predicates:
32
+ - Extracted coordinate and validation predicates: `Elements::Cell.valid_value?`, `Cell.valid_coordinates?`, `Cell.calculate_column_letter`, `Cell.calculate_column_index`.
33
+ - Extracted domain validation predicates: `Elements::Row.valid_index?`, `Elements::Column.valid_index?`, `Elements::Worksheet.valid_name?`.
34
+ - Replaced date/time serial calculations in `Ooxml::Utils` with pure mathematical conversions (Julian Day math for `date_to_serial`, `divmod` arithmetic for `serial_to_datetime`, and 1900 leap year bug handling).
35
+ - Extracted binary packing utilities in `Ooxml::ZipGenerator` (`le16`, `le32`, and bitfield arithmetic for `dos_datetime`).
36
+ - Extracted pure XML character escaping logic in `Ooxml::XmlBuilder.escape`.
37
+ - Modularized `Ooxml::Writer` into domain-specific mixin modules (`DrawingXml`, `FeaturesXml`, `StylesXml`).
38
+ - Extracted shared DSL parameter normalization and range expansion into `Xlsxrb::DslHelpers`.
39
+ - Modularized reader SAX listeners into domain-specific parser components.
40
+
41
+ ### Fixed
42
+ - Boundary Validation & Error Reporting:
43
+ - Strictly validate row number digits in `Elements::Cell.parse_ref` to reject malformed cell coordinate strings (e.g. `A0`, `A01`).
44
+ - Harmonized supported value type checks and nil/empty handling across `Elements::Cell.validate`.
45
+ - Corrected maximum row index boundary error messages in `Elements::Row.validate`.
46
+ - Hardened cryptographic stream boundary checks and corrupted header handling in Standard and Agile encryption modes.
47
+ - RBS Runtime Type Validation:
48
+ - Expanded `Elements::Workbook#initialize`, `#sheet`, and `[]` type annotations to accept `StreamSheet` instances used by streaming readers.
49
+ - Made update block parameter optional in `Elements::Workbook#update_sheet` to allow testing missing block validation.
50
+ - Supported `Numeric` types (Float and Integer) in `ChartBuilder` and `SeriesBuilder` kwargs (`width`, etc.).
51
+ - Handled `RBS::Test::Tester::TypeError` in unit tests verifying block return type validation.
52
+
1
53
  ## [0.1.11] - 2026-08-19
2
54
 
3
55
  ### Performance
4
- - **Sub-Second Streaming Write (< 1.0s / 1,000,000 cells)**:
5
- - Achieved sub-second streaming write performance (**0.97 s** median for 1M cells) while maintaining 100% full SST (Shared String Table) XML deduplication and ISO/IEC 29500 compatibility.
56
+ - Sub-Second Streaming Write (< 1.0s / 1,000,000 cells):
57
+ - Achieved sub-second streaming write performance (0.97 s median for 1M cells) while maintaining 100% full SST (Shared String Table) XML deduplication and ISO/IEC 29500 compatibility.
6
58
  - Implemented 1-Pass Direct ZIP streaming pipeline, eliminating all intermediate tempfiles, disk seek-backs, and extra deflate flush cycles.
7
59
  - Added dedicated unstyled fast-path in `WorksheetWriter#write_row_values` with precomputed coordinate and integer lookup tables.
8
60
  - Pre-registered default date/time formatting styles, completely eliminating redundant per-row scan loops.
9
61
  - Streamlined `ZipWriter` instance variable lookups to eliminate per-chunk hash overhead.
10
62
  - Added `alias << row` to `WorksheetProxy` and `StreamWriter` for idiomatic Ruby streaming.
11
- - **High-Speed Streaming Read (1.61s / 1,000,000 cells)**:
12
- - Reduced streaming read execution time from **3.40 s** down to **1.61 s** (over 2x speedup).
63
+ - High-Speed Streaming Read (1.61s / 1,000,000 cells):
64
+ - Reduced streaming read execution time from 3.40 s down to 1.61 s (over 2x speedup).
13
65
  - Refactored `Elements::Cell` into an optimized lightweight class with `Cell.fast_create`, reducing 1M cell instantiation overhead by 5.2x (0.92s → 0.17s) while maintaining full pattern matching (`deconstruct`, `deconstruct_keys`) and immutability.
14
66
  - Replaced per-byte Ruby scanning in `WorksheetParser.fast_scan_cells_direct` with an Onigmo C-level regular expression scanner (`CELL_FAST_RE`), eliminating 100,000 intermediate XML substring allocations and nested byte loops.
15
67
  - Optimized `StreamRow#cells` to populate cell arrays via direct block traversal, eliminating Enumerator object allocations.
@@ -20,56 +72,56 @@
20
72
  ## [0.1.10] - 2026-08-19
21
73
 
22
74
  ### Added
23
- - **Peer Libraries Ecosystem Guide ([docs/PEER_LIBRARIES.md](docs/PEER_LIBRARIES.md))**: Introduced a respectful overview of the Ruby XLSX ecosystem featuring official self-descriptions, architectural tradeoffs (SST vs. Inline Strings, Streaming vs. In-Memory), and reproducible benchmarks across 9 popular Ruby XLSX gems.
24
- - **Visual Assets & Screen Previews**:
75
+ - Peer Libraries Ecosystem Guide ([docs/PEER_LIBRARIES.md](docs/PEER_LIBRARIES.md)): Introduced a respectful overview of the Ruby XLSX ecosystem featuring official self-descriptions, architectural tradeoffs (SST vs. Inline Strings, Streaming vs. In-Memory), and reproducible benchmarks across 9 popular Ruby XLSX gems.
76
+ - Visual Assets & Screen Previews:
25
77
  - Embedded interactive WebAssembly Playground live demo preview in `README.md`.
26
78
  - Added real-world Ruby LSP autocompletion and RBS type hint preview in `README.md`.
27
79
  - Created accurate, neutral linear-scale SVG benchmark performance chart.
28
- - **Enterprise-Grade Test Suite Expansion**:
29
- - **ECMA-376 XSD Schema Validation**: Comprehensive XML schema validation suite ensuring strict element ordering and ISO/IEC 29500 compliance.
30
- - **Contract Testing Suite**: Comprehensive parity verification between Streaming (`Xlsxrb.write`) and In-Memory (`Xlsxrb.build`) APIs.
31
- - **Property-Based Testing (PBT)**: Expanded automated random generation tests for Row/Column invariants, styles, and edge cases.
32
- - **Visual Regression Testing (VRT)**: Added new visual baselines for table styles, drawing shapes, and pivot tables.
33
- - **E2E Interoperability Suite**: Added tests for namespace-prefixed XML streaming, conditional formatting, and table structures.
80
+ - Multi-Layered Test Suite Expansion:
81
+ - ECMA-376 XSD Schema Validation: XML schema validation suite ensuring strict element ordering and ISO/IEC 29500 compliance.
82
+ - Contract Testing Suite: Parity verification between Streaming (`Xlsxrb.write`) and In-Memory (`Xlsxrb.build`) APIs.
83
+ - Property-Based Testing (PBT): Expanded automated random generation tests for Row/Column invariants, styles, and edge cases.
84
+ - Visual Regression Testing (VRT): Added new visual baselines for table styles, drawing shapes, and pivot tables.
85
+ - E2E Interoperability Suite: Added tests for namespace-prefixed XML streaming, conditional formatting, and table structures.
34
86
 
35
87
  ### Fixed
36
- - **WebAssembly (ruby.wasm) Compatibility**: Bundled `pp` and `prettyprint` standard libraries in `ruby.wasm` package to resolve REXML LoadError during browser-based evaluation.
88
+ - WebAssembly (ruby.wasm) Compatibility: Bundled `pp` and `prettyprint` standard libraries in `ruby.wasm` package to resolve REXML LoadError during browser-based evaluation.
37
89
 
38
90
  ### Changed
39
- - **Streamlined README**: Refactored README from 363 to 175 lines, focusing on core motivation, clean 4-column feature matrix, concise usage examples, and direct links to specialized documentation.
91
+ - Streamlined README: Refactored README from 363 to 175 lines, focusing on core motivation, clean 4-column feature matrix, concise usage examples, and direct links to specialized documentation.
40
92
 
41
93
  ## [0.1.9] - 2026-08-18
42
94
 
43
95
  ### Added
44
- - **Password Protection & Document Encryption ([MS-OFFCRYPTO] / [MS-CFB])**: Full native Pure-Ruby support for reading, writing, and modifying password-protected Excel spreadsheets without any external C-extension dependencies.
45
- - **Standard Encryption**: AES-128-ECB and SHA-1 Key Derivation with CryptoAPI 50,000-spin hashing, fully interoperable across Microsoft Excel, LibreOffice, and Google Sheets.
46
- - **Agile Encryption**: Modern AES-256-CBC, PBKDF2/SHA-512, and HMAC-SHA512 data integrity verification.
47
- - **Compound File Binary (CFB) Engine**: Pure-Ruby reader and writer for OLE structured storage containers with Mini Stream, FAT/MiniFAT sectors, and Red-Black tree directory management.
48
- - **Transparent Public API Integration**: Added `password:` and `encryption_mode:` arguments to `Xlsxrb.read`, `Xlsxrb.write`, and `Xlsxrb.modify`.
49
- - **Security & Threat Model Hardening**:
96
+ - Password Protection & Document Encryption ([MS-OFFCRYPTO] / [MS-CFB]): Full native Pure-Ruby support for reading, writing, and modifying password-protected Excel spreadsheets without any external C-extension dependencies.
97
+ - Standard Encryption: AES-128-ECB and SHA-1 Key Derivation with CryptoAPI 50,000-spin hashing, fully interoperable across Microsoft Excel, LibreOffice, and Google Sheets.
98
+ - Agile Encryption: Modern AES-256-CBC, PBKDF2/SHA-512, and HMAC-SHA512 data integrity verification.
99
+ - Compound File Binary (CFB) Engine: Pure-Ruby reader and writer for OLE structured storage containers with Mini Stream, FAT/MiniFAT sectors, and Red-Black tree directory management.
100
+ - Transparent Public API Integration: Added `password:` and `encryption_mode:` arguments to `Xlsxrb.read`, `Xlsxrb.write`, and `Xlsxrb.modify`.
101
+ - Security & Threat Model Hardening:
50
102
  - Constant-time hash verification via `OpenSSL.secure_compare` to prevent timing attacks (CWE-208).
51
103
  - CSPRNG-backed salt, IV, and session key generation via `SecureRandom` (CWE-330).
52
- - Robust DoS defense: spinCount limit ($\le 10\text{M}$), CFB circular sector chain loop detection in directory/FAT parsing, and `total_size` bounds validation (CWE-400, CWE-835).
104
+ - DoS defense: spinCount limit ($\le 10\text{M}$), CFB circular sector chain loop detection in directory/FAT parsing, and `total_size` bounds validation (CWE-400, CWE-835).
53
105
  - Strict exception hierarchy (`EncryptedFileError`, `InvalidPasswordError`, `DecryptionError`).
54
- - **Cross-Platform & Interoperability Validation**: Bidirectional validation with Microsoft .NET OpenXML SDK and LibreOffice Calc.
55
- - **WebAssembly (ruby.wasm) Support**: Pre-packaged `docs/wasm/ruby.wasm` updated with document encryption support for browser playground.
106
+ - Cross-Platform & Interoperability Validation: Bidirectional validation with Microsoft .NET OpenXML SDK and LibreOffice Calc.
107
+ - WebAssembly (ruby.wasm) Support: Pre-packaged `docs/wasm/ruby.wasm` updated with document encryption support for browser playground.
56
108
 
57
109
  ## [0.1.8] - 2026-08-18
58
110
 
59
111
  ### Changed
60
- - **Unified Symmetric Entrypoints**: Consolidated reading into `Xlsxrb.read` (supporting file path, IO, and raw binary string) and writing into `Xlsxrb.write` (supporting streaming blocks or in-memory Workbooks). Removed legacy `open`, `foreach`, and `generate` methods.
61
- - **Streaming-First Defaults**: `Xlsxrb.read` yields and returns lightweight `StreamSheet` instances with $O(1)$ constant-memory consumption by default.
62
- - **Explicit In-Memory Materialization (`#load`)**: Stripped accidental random-access memory traps from `StreamSheet`; introduced explicit `StreamSheet#load` / `Workbook#load` (inspired by ActiveRecord Relations) to transition from lazy streaming to in-memory `Elements::Worksheet` / `Elements::Workbook`.
63
- - **`CoordinateAccess` Module**: Extracted coordinate lookup methods (`[]`, `cell_value`, `row_at`, `first_row`, `last_row`, `cells`, `cells_hash`) into a dedicated `Xlsxrb::Elements::CoordinateAccess` mixin module included in `Elements::Worksheet`.
112
+ - Unified Symmetric Entrypoints: Consolidated reading into `Xlsxrb.read` (supporting file path, IO, and raw binary string) and writing into `Xlsxrb.write` (supporting streaming blocks or in-memory Workbooks). Removed legacy `open`, `foreach`, and `generate` methods.
113
+ - Streaming-First Defaults: `Xlsxrb.read` yields and returns lightweight `StreamSheet` instances with $O(1)$ constant-memory consumption by default.
114
+ - Explicit In-Memory Materialization (`#load`): Stripped accidental random-access memory traps from `StreamSheet`; introduced explicit `StreamSheet#load` / `Workbook#load` (inspired by ActiveRecord Relations) to transition from lazy streaming to in-memory `Elements::Worksheet` / `Elements::Workbook`.
115
+ - `CoordinateAccess` Module: Extracted coordinate lookup methods (`[]`, `cell_value`, `row_at`, `first_row`, `last_row`, `cells`, `cells_hash`) into a dedicated `Xlsxrb::Elements::CoordinateAccess` mixin module included in `Elements::Worksheet`.
64
116
 
65
117
  ### Added
66
- - **Default Cell Streaming (`Xlsxrb::StreamRow`)**: Enabled streaming along both row and cell dimensions via `row.each_cell` and `sheet.each_cell`, parsing cells on-demand to handle sheets with thousands of columns in $O(1)$ constant memory.
118
+ - Default Cell Streaming (`Xlsxrb::StreamRow`): Enabled streaming along both row and cell dimensions via `row.each_cell` and `sheet.each_cell`, parsing cells on-demand to handle sheets with thousands of columns in $O(1)$ constant memory.
67
119
 
68
120
  ## [0.1.7] - 2026-08-16
69
121
 
70
122
  ### Added
71
123
  - Bundled native Ruby LSP Add-on (`RubyLsp::Xlsxrb::Addon`) for zero-configuration, context-aware method autocompletion and rich markdown documentation in VS Code and LSP-enabled editors for block arguments (`wb.`, `s.`, `sheet.`, `stream_writer.`, `stream_sheet.`).
72
- - Comprehensive YARD documentation (`@param`, `@return`, `@example`) across all public APIs, builders, proxies, and elements.
124
+ - YARD documentation (`@param`, `@return`, `@example`) across all public APIs, builders, proxies, and elements.
73
125
 
74
126
  ### Developer Experience
75
127
  - Enhanced `rbs-inline` type signatures across all facade methods and builder objects with automated RBS generation.
@@ -84,10 +136,10 @@
84
136
  - Hash-compatible symbol indexing in `Elements::Row#[]` (`:cells`, `:index`, `:height`, `:attrs`) and `Elements::Cell#[]` (`:value`, `:ref`, `:style_index`).
85
137
 
86
138
  ### Performance
87
- - **Streaming Write**: Replaced per-cell micro IO calls with row-level string buffering and fast-path serialization for unstyled cells (1,000,000 cells in 1.62s with standard SST).
88
- - **In-Memory Write**: Optimized DOM serialization (`WorksheetWriter#write_row`) with row-level buffer aggregation, boosting 1,000,000 cells write from 22.33s to 4.06s (5.5x faster) and reducing GC count by 83%.
89
- - **Streaming Read**: Implemented zero-allocation byte scanning for cell attributes (`r="..."`, `t="..."`, `s="..."`) and direct integer conversion for SST indexes, cutting GC count by ~70% (129 -> 40) and boosting 1,000,000 cells read to 3.38s.
90
- - **In-Memory Read**: Reduced peak memory footprint by 57% (582 MB -> 250 MB).
139
+ - Streaming Write: Replaced per-cell micro IO calls with row-level string buffering and fast-path serialization for unstyled cells (1,000,000 cells in 1.62s with standard SST).
140
+ - In-Memory Write: Optimized DOM serialization (`WorksheetWriter#write_row`) with row-level buffer aggregation, boosting 1,000,000 cells write from 22.33s to 4.06s (5.5x faster) and reducing GC count by 83%.
141
+ - Streaming Read: Implemented zero-allocation byte scanning for cell attributes (`r="..."`, `t="..."`, `s="..."`) and direct integer conversion for SST indexes, cutting GC count by ~70% (129 -> 40) and boosting 1,000,000 cells read to 3.38s.
142
+ - In-Memory Read: Reduced peak memory footprint by 57% (582 MB -> 250 MB).
91
143
 
92
144
  ### Documentation
93
145
  - Overhauled `benchmark.rb` with `bundler/inline` for deterministic, zero-setup benchmark reproduction pinning peer gem versions.
@@ -107,7 +159,7 @@
107
159
  - Full `RBS::Test` runtime type validation enabled for the entire test suite.
108
160
  - Extensive Excel limit warnings documented via YARD tags.
109
161
  - Formal SemVer API contract with `@api public` tags for user-facing methods.
110
- - Comprehensive mutation testing (Mutant) and test coverage (SimpleCov) integrations.
162
+ - Mutation testing (Mutant) and test coverage (SimpleCov) integrations.
111
163
 
112
164
  ### Changed
113
165
  - Replaced `method_missing` with statically defined, fully typed methods in `WorksheetProxy`, `ChartBuilder`, and `SeriesBuilder`.
data/README.md CHANGED
@@ -4,7 +4,7 @@ A Ruby library for reading and writing XLSX files with streaming support.
4
4
 
5
5
  ## Motivation
6
6
 
7
- The Ruby ecosystem already has great XLSX libraries, each designed for specific tradeoffs:
7
+ The Ruby ecosystem has several XLSX libraries designed for specific tradeoffs:
8
8
 
9
9
  | Library | Read | Write | Streaming | In-Memory |
10
10
  | :--- | :---: | :---: | :---: | :---: |
@@ -17,23 +17,27 @@ The Ruby ecosystem already has great XLSX libraries, each designed for specific
17
17
  | [xlsxtream](https://rubygems.org/gems/xlsxtream) | ❌ | ✅ | ✅ | ❌ |
18
18
  | [fast_excel](https://rubygems.org/gems/fast_excel) | ❌ | ✅ | ✅ | ❌ |
19
19
  | [rubyXL](https://rubygems.org/gems/rubyXL) | ✅ | ✅ | ❌ | ✅ |
20
- | **[xlsxrb](https://github.com/niku/xlsxrb)** | **✅** | **✅** | **✅** | **✅** |
20
+ | [xlsxrb](https://github.com/niku/xlsxrb) | ✅ | ✅ | ✅ | ✅ |
21
21
 
22
- Each of these libraries makes deliberate architectural choices:
23
- * **Streaming Model**: Writes or reads rows sequentially on-the-fly to maintain a constant $O(1)$, low-memory footprint regardless of dataset size.
24
- * **In-Memory Model**: Builds a complete document object model, offering flexible random access, cell updates, and document templates at the cost of memory usage on large spreadsheets.
22
+ These libraries make different architectural tradeoffs:
23
+ * Streaming Model: Writes or reads rows sequentially on-the-fly to maintain a constant $O(1)$, low-memory footprint regardless of dataset size.
24
+ * In-Memory Model: Builds a complete document object model, offering flexible random access, cell updates, and document templates at the cost of memory usage on large spreadsheets.
25
25
 
26
- Traditionally, maintaining an all-in-one gem that offers both reading and writing across both streaming and in-memory models, alongside rich OOXML features, high performance, and strict compatibility, presents an inherent open-source challenge: the cumulative maintenance overhead often exceeds the capacity of individual human maintainers.
26
+ Maintaining a gem that supports both reading and writing across streaming and in-memory models, alongside OOXML features and specification compatibility, involves significant ongoing maintenance overhead.
27
27
 
28
- `xlsxrb` is built on a modern premise: **Advanced Agentic AI (AI Coders) can sustainably handle this maintenance demand.** By utilizing AI agents to automate end-to-end testing, visual regression testing, specification compliance verification, and documentation updates, `xlsxrb` delivers a fast, specification-compliant, and fully-featured XLSX library built for long-term sustainability.
28
+ `xlsxrb` addresses this through automated workflows: AI coding agents handle routine maintenance tasks such as end-to-end testing, visual regression testing, schema compliance checks, and documentation synchronization, enabling sustainable maintenance of both streaming and in-memory architectures.
29
29
 
30
30
  ### Design Principles
31
31
 
32
- - **Minimal Dependencies**: Zero core logic dependencies. Built purely on the Ruby standard library and bundled gems (`zlib`, `rexml`, etc.). The only runtime dependency is `opentelemetry-api` (zero-overhead no-op when unconfigured).
33
- - **Streaming Support**: True $O(1)$ constant memory streaming for both reading and writing massive spreadsheets.
34
- - **Strict OpenXML Interoperability**: Fully compliant with ISO/IEC 29500 (ECMA-376) and validated continuously against the official Microsoft [Open XML SDK](https://github.com/dotnet/Open-XML-SDK).
35
- - **AI-Assisted Sustainability**: Leveraging AI coding agents for automated quality assurance, E2E validation, and continuous feature expansion.
36
- - **Modern Ruby**: Built for Ruby 4.0 or higher.
32
+ - Zero Runtime Dependencies: Built strictly on the Ruby standard library and bundled gems (`zlib`, `rexml`, etc.) with zero third-party runtime dependencies.
33
+ - Streaming Support: Constant $O(1)$ memory streaming for reading and writing spreadsheets.
34
+ - OpenXML Interoperability: Compliant with ISO/IEC 29500 (ECMA-376) and validated against the Microsoft [Open XML SDK](https://github.com/dotnet/Open-XML-SDK).
35
+ - AI-Assisted Maintenance: Uses AI coding agents for automated quality assurance and verification workflows. Operational guidelines are defined in [AGENTS.md](AGENTS.md).
36
+ - Ruby 4.0+: Requires Ruby 4.0 or higher.
37
+
38
+ ### Autonomous Quality Assurance
39
+
40
+ To maintain its multi-layered QA suite (Steep static typing, 100% mutation kill rate across 38 subjects, ECMA-376 XSD validation, and $O(1)$ streaming memory) without high manual maintenance overhead, xlsxrb defines remediation workflows for AI coding agents ([AGENTS.md](AGENTS.md), [docs/QA_AGENTS.md](docs/QA_AGENTS.md)).
37
41
 
38
42
  ## Installation
39
43
 
@@ -134,7 +138,7 @@ end
134
138
 
135
139
  ### IDE Autocompletion & Ruby LSP Support
136
140
 
137
- Includes a native **Ruby LSP Add-on** and full **RBS signatures** for zero-configuration method autocompletion and hover documentation in VS Code and other editors:
141
+ Includes a native Ruby LSP Add-on and full RBS signatures for zero-configuration method autocompletion and hover documentation in VS Code and other editors:
138
142
 
139
143
  <p align="center">
140
144
  <img src="docs/assets/lsp_autocompletion.png" width="100%" alt="Ruby LSP Autocompletion & Type Signature Hints in VS Code"/>
@@ -142,9 +146,9 @@ Includes a native **Ruby LSP Add-on** and full **RBS signatures** for zero-confi
142
146
 
143
147
  ## Feature Support & ECMA-376 Compliance
144
148
 
145
- `xlsxrb` supports nearly all major business spreadsheet features:
146
- * **Layout & Structure**: Formulas, Hyperlinks, Merge Cells, Freeze/Split Panes, Page Setup, Auto Filters, Data Validations, Sheet/Workbook Protection.
147
- * **Styling & Media**: Rich Text, Cell Styles & Fills, Conditional Formatting (color scales, data bars), Embedded Images, Charts (Line, Bar, Pie, Radar, Scatter).
149
+ `xlsxrb` supports standard spreadsheet features:
150
+ * Layout & Structure: Formulas, Hyperlinks, Merge Cells, Freeze/Split Panes, Page Setup, Auto Filters, Data Validations, Sheet/Workbook Protection.
151
+ * Styling & Media: Rich Text, Cell Styles & Fills, Conditional Formatting (color scales, data bars), Embedded Images, Charts (Line, Bar, Pie, Radar, Scatter).
148
152
 
149
153
  For full details, see [docs/SPEC_SOURCES.md](docs/SPEC_SOURCES.md).
150
154
 
@@ -162,11 +166,12 @@ To reproduce locally: `ruby benchmark.rb 100000 10`
162
166
 
163
167
  ## Quality Assurance & Testing
164
168
 
165
- Backed by an enterprise-grade QA architecture to guarantee absolute reliability:
166
- * **Official Microsoft Open XML SDK Validation**: Validates generated OOXML structures against Microsoft's official SDK.
167
- * **Visual Regression Testing (VRT)**: Headless LibreOffice Calc pixel-by-pixel rendering checks.
168
- * **Contract & Round-Trip Tests**: Verifies parity between Streaming and In-Memory APIs and round-trip read/write accuracy.
169
- * **Security & DoS Protection**: Formula injection mitigation and ZIP bomb protection.
169
+ Verified by a multi-layered QA architecture:
170
+ * Microsoft Open XML SDK Validation: Validates generated OOXML structures against Microsoft's official SDK.
171
+ * Visual Regression Testing (VRT): Headless LibreOffice Calc pixel-by-pixel rendering checks.
172
+ * Contract & Round-Trip Tests: Verifies parity between Streaming and In-Memory APIs and round-trip read/write accuracy.
173
+ * Mutation Testing (Mutant): 100% mutant kill rate (1,091/1,091 mutations across 38 subjects) on pure algorithms, predicates, and coordinates ([docs/MUTATION_TESTING.md](docs/MUTATION_TESTING.md)).
174
+ * Security & DoS Protection: Formula injection mitigation and ZIP bomb protection.
170
175
 
171
176
  For full architectural details, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/QUALITY_ASSURANCE.md](docs/QUALITY_ASSURANCE.md).
172
177
 
@@ -80,7 +80,7 @@ module Xlsxrb
80
80
  # @param kwargs [Hash] Keyword arguments for legend styling/layout.
81
81
  # @return [Object] The configured legend property.
82
82
  # @api public
83
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
83
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
84
84
  def legend(*args, **kwargs)
85
85
  @options[:legend] = kwargs.empty? ? args.first : kwargs
86
86
  end
@@ -91,7 +91,7 @@ module Xlsxrb
91
91
  # @param kwargs [Hash] Keyword arguments for plot area configuration.
92
92
  # @return [Object] The configured plot_area property.
93
93
  # @api public
94
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
94
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
95
95
  def plot_area(*args, **kwargs)
96
96
  @options[:plot_area] = kwargs.empty? ? args.first : kwargs
97
97
  end
@@ -102,7 +102,7 @@ module Xlsxrb
102
102
  # @param kwargs [Hash] Keyword arguments.
103
103
  # @return [Object] The configured chart_space property.
104
104
  # @api public
105
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
105
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
106
106
  def chart_space(*args, **kwargs)
107
107
  @options[:chart_space] = kwargs.empty? ? args.first : kwargs
108
108
  end
@@ -113,7 +113,7 @@ module Xlsxrb
113
113
  # @param kwargs [Hash] Keyword arguments.
114
114
  # @return [Object] The configured style property.
115
115
  # @api public
116
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String | Integer) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String | Integer)
116
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String | Integer) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String | Integer)
117
117
  def style(*args, **kwargs)
118
118
  @options[:style] = kwargs.empty? ? args.first : kwargs
119
119
  end
@@ -124,7 +124,7 @@ module Xlsxrb
124
124
  # @param kwargs [Hash] Keyword arguments.
125
125
  # @return [Object] The configured data_labels property.
126
126
  # @api public
127
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
127
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
128
128
  def data_labels(*args, **kwargs)
129
129
  @options[:data_labels] = kwargs.empty? ? args.first : kwargs
130
130
  end
@@ -135,7 +135,7 @@ module Xlsxrb
135
135
  # @param kwargs [Hash] Keyword arguments.
136
136
  # @return [Boolean, String]
137
137
  # @api public
138
- #: (*(bool | String) args, **String | Integer | bool | nil kwargs) -> (bool | String)
138
+ #: (*(bool | String) args, **String | Numeric | bool | nil kwargs) -> (bool | String)
139
139
  def plot_visible_only(*args, **kwargs)
140
140
  @options[:plot_visible_only] = kwargs.empty? ? args.first : kwargs
141
141
  end
@@ -146,7 +146,7 @@ module Xlsxrb
146
146
  # @param kwargs [Hash] Keyword arguments.
147
147
  # @return [String]
148
148
  # @api public
149
- #: (*(String) args, **String | Integer | bool | nil kwargs) -> String
149
+ #: (*(String) args, **String | Numeric | bool | nil kwargs) -> String
150
150
  def display_blanks_as(*args, **kwargs)
151
151
  @options[:display_blanks_as] = kwargs.empty? ? args.first : kwargs
152
152
  end
@@ -157,7 +157,7 @@ module Xlsxrb
157
157
  # @param kwargs [Hash] Keyword arguments.
158
158
  # @return [Object]
159
159
  # @api public
160
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
160
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
161
161
  def view3d(*args, **kwargs)
162
162
  @options[:view3d] = kwargs.empty? ? args.first : kwargs
163
163
  end
@@ -168,7 +168,7 @@ module Xlsxrb
168
168
  # @param kwargs [Hash] Keyword arguments.
169
169
  # @return [Object]
170
170
  # @api public
171
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
171
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
172
172
  def category_axis(*args, **kwargs)
173
173
  @options[:category_axis] = kwargs.empty? ? args.first : kwargs
174
174
  end
@@ -179,7 +179,7 @@ module Xlsxrb
179
179
  # @param kwargs [Hash] Keyword arguments.
180
180
  # @return [Object]
181
181
  # @api public
182
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
182
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
183
183
  def value_axis(*args, **kwargs)
184
184
  @options[:value_axis] = kwargs.empty? ? args.first : kwargs
185
185
  end
@@ -190,7 +190,7 @@ module Xlsxrb
190
190
  # @param kwargs [Hash] Keyword arguments.
191
191
  # @return [Boolean, String]
192
192
  # @api public
193
- #: (*(bool | String) args, **String | Integer | bool | nil kwargs) -> (bool | String)
193
+ #: (*(bool | String) args, **String | Numeric | bool | nil kwargs) -> (bool | String)
194
194
  def show_legend_key(*args, **kwargs)
195
195
  @options[:show_legend_key] = kwargs.empty? ? args.first : kwargs
196
196
  end
@@ -258,7 +258,7 @@ module Xlsxrb
258
258
  # @param kwargs [Hash] Keyword arguments.
259
259
  # @return [Object]
260
260
  # @api public
261
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
261
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
262
262
  def fill(*args, **kwargs)
263
263
  @options[:fill] = kwargs.empty? ? args.first : kwargs
264
264
  end
@@ -269,7 +269,7 @@ module Xlsxrb
269
269
  # @param kwargs [Hash] Keyword arguments.
270
270
  # @return [Object]
271
271
  # @api public
272
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
272
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
273
273
  def line(*args, **kwargs)
274
274
  @options[:line] = kwargs.empty? ? args.first : kwargs
275
275
  end
@@ -280,7 +280,7 @@ module Xlsxrb
280
280
  # @param kwargs [Hash] Keyword arguments.
281
281
  # @return [Object]
282
282
  # @api public
283
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
283
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
284
284
  def trendline(*args, **kwargs)
285
285
  @options[:trendline] = kwargs.empty? ? args.first : kwargs
286
286
  end
@@ -291,7 +291,7 @@ module Xlsxrb
291
291
  # @param kwargs [Hash] Keyword arguments.
292
292
  # @return [Object]
293
293
  # @api public
294
- #: (*(Hash[Symbol, String | Integer | bool | nil] | String) args, **String | Integer | bool | nil kwargs) -> (Hash[Symbol, String | Integer | bool | nil] | String)
294
+ #: (*(Hash[Symbol, String | Numeric | bool | nil] | String) args, **String | Numeric | bool | nil kwargs) -> (Hash[Symbol, String | Numeric | bool | nil] | String)
295
295
  def data_labels(*args, **kwargs)
296
296
  @options[:data_labels] = kwargs.empty? ? args.first : kwargs
297
297
  end
@@ -302,7 +302,7 @@ module Xlsxrb
302
302
  # @param kwargs [Hash] Keyword arguments.
303
303
  # @return [Boolean, String]
304
304
  # @api public
305
- #: (*(bool | String) args, **String | Integer | bool | nil kwargs) -> (bool | String)
305
+ #: (*(bool | String) args, **String | Numeric | bool | nil kwargs) -> (bool | String)
306
306
  def smooth(*args, **kwargs)
307
307
  @options[:smooth] = kwargs.empty? ? args.first : kwargs
308
308
  end
@@ -313,7 +313,7 @@ module Xlsxrb
313
313
  # @param kwargs [Hash] Keyword arguments.
314
314
  # @return [String]
315
315
  # @api public
316
- #: (*(String) args, **String | Integer | bool | nil kwargs) -> String
316
+ #: (*(String) args, **String | Numeric | bool | nil kwargs) -> String
317
317
  def shape(*args, **kwargs)
318
318
  @options[:shape] = kwargs.empty? ? args.first : kwargs
319
319
  end
@@ -324,7 +324,7 @@ module Xlsxrb
324
324
  # @param kwargs [Hash] Keyword arguments.
325
325
  # @return [String]
326
326
  # @api public
327
- #: (*(String) args, **String | Integer | bool | nil kwargs) -> String
327
+ #: (*(String) args, **String | Numeric | bool | nil kwargs) -> String
328
328
  def type(*args, **kwargs)
329
329
  @options[:type] = kwargs.empty? ? args.first : kwargs
330
330
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ require_relative "elements/cell"
6
+ require_relative "ooxml/utils"
7
+
8
+ module Xlsxrb
9
+ # Internal helper module for normalizing and validating DSL arguments
10
+ # shared between {WorksheetBuilder} (in-memory) and {StreamWriter} (streaming).
11
+ #
12
+ # @api private
13
+ module DslHelpers
14
+ # Normalizes merge cell range string or coordinate components into a canonical range reference (e.g. "A1:C3").
15
+ #
16
+ # @param range [String, Hash, nil]
17
+ # @param row [Integer, nil]
18
+ # @param col_start [Integer, String, nil]
19
+ # @param col_end [Integer, String, nil]
20
+ # @param row_start [Integer, nil]
21
+ # @param row_end [Integer, nil]
22
+ # @param strict_excel_mode [Boolean]
23
+ # @return [String]
24
+ #: (untyped range, ?row: Integer?, ?col_start: (Integer | String)?, ?col_end: (Integer | String)?, ?row_start: Integer?, ?row_end: Integer?, ?strict_excel_mode: bool) -> String
25
+ def self.normalize_merge_range(range = nil, row: nil, col_start: nil, col_end: nil, row_start: nil, row_end: nil, strict_excel_mode: true)
26
+ if range.is_a?(Hash)
27
+ row = range[:row]
28
+ row_start = range[:row_start]
29
+ row_end = range[:row_end]
30
+ col_start = range[:col_start]
31
+ col_end = range[:col_end]
32
+ range = nil
33
+ end
34
+
35
+ if range
36
+ raise ArgumentError, "Invalid merge range format: '#{range}'. Expected format like 'A1:B2'." if strict_excel_mode && !range.match?(/\A[A-Za-z]{1,3}\d+(:[A-Za-z]{1,3}\d+)?\z/)
37
+
38
+ range
39
+ else
40
+ r_start = row || row_start || 0
41
+ r_end = row || row_end || 0
42
+ c_start = Elements::Cell.column_index(col_start || 0)
43
+ c_end = Elements::Cell.column_index(col_end || 0)
44
+ start_ref = "#{Elements::Cell.column_letter(c_start)}#{r_start + 1}"
45
+ end_ref = "#{Elements::Cell.column_letter(c_end)}#{r_end + 1}"
46
+ "#{start_ref}:#{end_ref}"
47
+ end
48
+ end
49
+
50
+ # Normalizes sheet protection options and hashes plain-text password if supplied.
51
+ #
52
+ # @param opts [Hash]
53
+ # @return [Hash]
54
+ #: (Hash[Symbol, untyped] opts) -> Hash[Symbol, untyped]
55
+ def self.normalize_protection_options(opts)
56
+ normalized = opts.dup
57
+ plain_password = normalized[:password]
58
+ needs_hash = plain_password.is_a?(String) && !plain_password.empty? &&
59
+ normalized[:algorithm_name].nil? && normalized[:hash_value].nil? &&
60
+ normalized[:salt_value].nil? && normalized[:spin_count].nil? &&
61
+ !plain_password.match?(/\A[0-9A-Fa-f]{4}\z/)
62
+ if needs_hash
63
+ normalized.delete(:password)
64
+ normalized.merge!(Ooxml::Utils.hash_password(plain_password))
65
+ end
66
+ normalized
67
+ end
68
+
69
+ # Normalizes column index or letter or Range to an array of 0-based column integers.
70
+ #
71
+ # @param index [Integer, String, Symbol, Range, Array]
72
+ # @return [Array<Integer>]
73
+ #: (untyped index) -> Array[Integer]
74
+ def self.normalize_column_indices(index)
75
+ case index
76
+ when Range, Array
77
+ index.map { |i| Elements::Cell.column_index(i) }
78
+ else
79
+ [Elements::Cell.column_index(index)]
80
+ end
81
+ end
82
+
83
+ # Normalizes page margins into a compact hash.
84
+ #
85
+ # @param left [Float, nil]
86
+ # @param right [Float, nil]
87
+ # @param top [Float, nil]
88
+ # @param bottom [Float, nil]
89
+ # @param header [Float, nil]
90
+ # @param footer [Float, nil]
91
+ # @return [Hash{Symbol => Float}]
92
+ #: (?left: Float?, ?right: Float?, ?top: Float?, ?bottom: Float?, ?header: Float?, ?footer: Float?) -> Hash[Symbol, Float]
93
+ def self.normalize_page_margins(left: nil, right: nil, top: nil, bottom: nil, header: nil, footer: nil)
94
+ { left: left, right: right, top: top, bottom: bottom, header: header, footer: footer }.compact
95
+ end
96
+
97
+ # Normalizes table options hash.
98
+ #
99
+ # @param ref [String]
100
+ # @param columns [Array<String>, Array<Hash>]
101
+ # @param name [String, nil]
102
+ # @param display_name [String, nil]
103
+ # @param style [String, nil]
104
+ # @param opts [Hash]
105
+ # @return [Hash]
106
+ #: (String ref, columns: untyped, ?name: String?, ?display_name: String?, ?style: String?, **untyped opts) -> Hash[Symbol, untyped]
107
+ def self.normalize_table_options(ref, columns:, name: nil, display_name: nil, style: nil, **opts)
108
+ tbl = { ref: ref, columns: columns }
109
+ tbl[:name] = name if name
110
+ tbl[:display_name] = display_name if display_name
111
+ tbl[:style] = style if style
112
+ tbl.merge!(opts)
113
+ tbl
114
+ end
115
+
116
+ # Converts a cell range (e.g. "A1:B2" or "A1") to an absolute reference format (e.g. "$A$1:$B$2" or "$A$1").
117
+ #
118
+ # @param range [String]
119
+ # @return [String]
120
+ #: (String range) -> String
121
+ def self.absolute_range(range)
122
+ range.gsub(/(?:\$)?([A-Z]+)(?:\$)?(\d+)\b/, '$\1$\2')
123
+ end
124
+
125
+ # Normalizes row values: if given a column-keyed Hash, converts to a sparse Array.
126
+ # If already an Array or other non-Hash enumerable, returns as-is without allocations.
127
+ #
128
+ # @param values [Array<Object>, Hash{String, Integer, Symbol => Object}, nil]
129
+ # @return [Array<Object>, nil]
130
+ #: (untyped values) -> untyped
131
+ def self.normalize_row_values(values)
132
+ return values unless values.is_a?(Hash)
133
+
134
+ cells_array = []
135
+ values.each do |k, v|
136
+ cells_array[Elements::Cell.column_index(k)] = v
137
+ end
138
+ cells_array
139
+ end
140
+
141
+ # Normalizes row styles: if given a Hash of column-keyed styles (including Ranges/Arrays),
142
+ # expands to a sparse Array of styles. If already an Array or nil, returns as-is without allocations.
143
+ #
144
+ # @param styles [untyped]
145
+ # @return [untyped]
146
+ #: (untyped styles) -> untyped
147
+ def self.normalize_row_styles(styles)
148
+ return styles unless styles.is_a?(Hash)
149
+
150
+ styles_array = []
151
+ styles.each do |k, v|
152
+ if k.is_a?(Range) || k.is_a?(Array)
153
+ k.each { |idx| styles_array[Elements::Cell.column_index(idx)] = v }
154
+ else
155
+ styles_array[Elements::Cell.column_index(k)] = v
156
+ end
157
+ end
158
+ styles_array
159
+ end
160
+
161
+ # Validates row index and height against Excel limits in strict mode.
162
+ #
163
+ # @param row_index [Integer]
164
+ # @param height [Float, Integer, nil]
165
+ # @param strict_excel_mode [Boolean]
166
+ # @return [void]
167
+ #: (Integer row_index, Float | Integer | nil height, ?strict_excel_mode: bool) -> void
168
+ def self.validate_row_bounds!(row_index, height, strict_excel_mode: true)
169
+ return unless strict_excel_mode
170
+
171
+ raise ArgumentError, "Row index #{row_index} exceeds Excel limit of 1,048,576 rows" if row_index >= 1_048_576
172
+ raise ArgumentError, "Row height #{height} must be between 0 and 409 points (Excel limitation)" if height && (height.negative? || height > 409)
173
+ end
174
+ end
175
+ end