xlsxrb 0.1.12 → 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 +59 -42
- data/README.md +26 -22
- data/lib/xlsxrb/dsl_helpers.rb +59 -0
- data/lib/xlsxrb/elements/workbook.rb +11 -0
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +3 -5
- data/lib/xlsxrb/ooxml/workbook_writer.rb +5 -7
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +172 -9
- data/lib/xlsxrb/ooxml/writer.rb +2 -1
- data/lib/xlsxrb/ooxml/xml_builder.rb +20 -0
- 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 +33 -66
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +1 -1
- data/lib/xlsxrb/worksheet_builder.rb +3 -31
- data/lib/xlsxrb.rb +448 -171
- data/sig/generated/xlsxrb/dsl_helpers.rbs +32 -0
- data/sig/generated/xlsxrb/elements/workbook.rbs +7 -0
- data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +0 -2
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +5 -4
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +11 -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.rbs +18 -11
- metadata +6 -146
- 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/.mutant.yml +0 -8
- data/Rakefile +0 -633
- data/Steepfile +0 -18
- data/benchmark.rb +0 -406
- data/docs/ARCHITECTURE.md +0 -514
- data/docs/DEVELOPMENT.md +0 -137
- data/docs/MUTATION_TESTING.md +0 -52
- data/docs/PEER_LIBRARIES.md +0 -121
- data/docs/QUALITY_ASSURANCE.md +0 -26
- 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 -4683
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6733f82b936fb790e6192de72890ecedd016f21dd4a5e43ee0f5123a2d35f5e
|
|
4
|
+
data.tar.gz: 3dd84ebc97a5c10a2777f6bd7557b06660e690f0723f8af6664a244a143adaa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ca0217ecd3a83a922a1098740d584c6e78fd0695f46acaa661286497a0e0de1c9214990eb7fef622a10ad4a1641f9aae73079ab61b0787570a22e434b446d0e
|
|
7
|
+
data.tar.gz: 7603890bc9f20d9143656ea83f0fd75410b4636cc66bbd152e79965b4b2bc07013de05f1489560fc896b9f5e1890c9fb6b421ec8dbc49b66ca9dec7912ab617c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
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
|
+
|
|
1
18
|
## [0.1.12] - 2026-09-20
|
|
2
19
|
|
|
3
20
|
### Added
|
|
4
|
-
-
|
|
21
|
+
- Mutation Testing Framework (`mutant`):
|
|
5
22
|
- Full-scale integration of mutant test suite targeting the Functional Core (10 classes and modules, 38 subjects).
|
|
6
|
-
- Achieved
|
|
23
|
+
- Achieved 100.00% Kill Rate (1,091 / 1,091 mutations killed, 0 alive) without exclusions.
|
|
7
24
|
- Added dedicated root configuration [`.mutant.yml`](.mutant.yml) for streamlined CLI execution (`bundle exec mutant run -- '...'`).
|
|
8
25
|
- Integrated `bundle exec rake mutant:pure` into GitHub Actions CI pipeline (`main.yml`), guaranteeing mutation-tested quality on every push and pull request.
|
|
9
|
-
- Added
|
|
10
|
-
-
|
|
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`.
|
|
11
28
|
|
|
12
29
|
### Changed
|
|
13
|
-
-
|
|
30
|
+
- Functional Core & Imperative Shell Architecture:
|
|
14
31
|
- Purified core business logic and domain entities into deterministic, side-effect-free class methods and predicates:
|
|
15
32
|
- Extracted coordinate and validation predicates: `Elements::Cell.valid_value?`, `Cell.valid_coordinates?`, `Cell.calculate_column_letter`, `Cell.calculate_column_index`.
|
|
16
33
|
- Extracted domain validation predicates: `Elements::Row.valid_index?`, `Elements::Column.valid_index?`, `Elements::Worksheet.valid_name?`.
|
|
17
|
-
- 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
|
|
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).
|
|
18
35
|
- Extracted binary packing utilities in `Ooxml::ZipGenerator` (`le16`, `le32`, and bitfield arithmetic for `dos_datetime`).
|
|
19
36
|
- Extracted pure XML character escaping logic in `Ooxml::XmlBuilder.escape`.
|
|
20
37
|
- Modularized `Ooxml::Writer` into domain-specific mixin modules (`DrawingXml`, `FeaturesXml`, `StylesXml`).
|
|
@@ -22,12 +39,12 @@
|
|
|
22
39
|
- Modularized reader SAX listeners into domain-specific parser components.
|
|
23
40
|
|
|
24
41
|
### Fixed
|
|
25
|
-
-
|
|
42
|
+
- Boundary Validation & Error Reporting:
|
|
26
43
|
- Strictly validate row number digits in `Elements::Cell.parse_ref` to reject malformed cell coordinate strings (e.g. `A0`, `A01`).
|
|
27
44
|
- Harmonized supported value type checks and nil/empty handling across `Elements::Cell.validate`.
|
|
28
45
|
- Corrected maximum row index boundary error messages in `Elements::Row.validate`.
|
|
29
46
|
- Hardened cryptographic stream boundary checks and corrupted header handling in Standard and Agile encryption modes.
|
|
30
|
-
-
|
|
47
|
+
- RBS Runtime Type Validation:
|
|
31
48
|
- Expanded `Elements::Workbook#initialize`, `#sheet`, and `[]` type annotations to accept `StreamSheet` instances used by streaming readers.
|
|
32
49
|
- Made update block parameter optional in `Elements::Workbook#update_sheet` to allow testing missing block validation.
|
|
33
50
|
- Supported `Numeric` types (Float and Integer) in `ChartBuilder` and `SeriesBuilder` kwargs (`width`, etc.).
|
|
@@ -36,15 +53,15 @@
|
|
|
36
53
|
## [0.1.11] - 2026-08-19
|
|
37
54
|
|
|
38
55
|
### Performance
|
|
39
|
-
-
|
|
40
|
-
- Achieved sub-second streaming write performance (
|
|
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.
|
|
41
58
|
- Implemented 1-Pass Direct ZIP streaming pipeline, eliminating all intermediate tempfiles, disk seek-backs, and extra deflate flush cycles.
|
|
42
59
|
- Added dedicated unstyled fast-path in `WorksheetWriter#write_row_values` with precomputed coordinate and integer lookup tables.
|
|
43
60
|
- Pre-registered default date/time formatting styles, completely eliminating redundant per-row scan loops.
|
|
44
61
|
- Streamlined `ZipWriter` instance variable lookups to eliminate per-chunk hash overhead.
|
|
45
62
|
- Added `alias << row` to `WorksheetProxy` and `StreamWriter` for idiomatic Ruby streaming.
|
|
46
|
-
-
|
|
47
|
-
- Reduced streaming read execution time from
|
|
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).
|
|
48
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.
|
|
49
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.
|
|
50
67
|
- Optimized `StreamRow#cells` to populate cell arrays via direct block traversal, eliminating Enumerator object allocations.
|
|
@@ -55,56 +72,56 @@
|
|
|
55
72
|
## [0.1.10] - 2026-08-19
|
|
56
73
|
|
|
57
74
|
### Added
|
|
58
|
-
-
|
|
59
|
-
-
|
|
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:
|
|
60
77
|
- Embedded interactive WebAssembly Playground live demo preview in `README.md`.
|
|
61
78
|
- Added real-world Ruby LSP autocompletion and RBS type hint preview in `README.md`.
|
|
62
79
|
- Created accurate, neutral linear-scale SVG benchmark performance chart.
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
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.
|
|
69
86
|
|
|
70
87
|
### Fixed
|
|
71
|
-
-
|
|
88
|
+
- WebAssembly (ruby.wasm) Compatibility: Bundled `pp` and `prettyprint` standard libraries in `ruby.wasm` package to resolve REXML LoadError during browser-based evaluation.
|
|
72
89
|
|
|
73
90
|
### Changed
|
|
74
|
-
-
|
|
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.
|
|
75
92
|
|
|
76
93
|
## [0.1.9] - 2026-08-18
|
|
77
94
|
|
|
78
95
|
### Added
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
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:
|
|
85
102
|
- Constant-time hash verification via `OpenSSL.secure_compare` to prevent timing attacks (CWE-208).
|
|
86
103
|
- CSPRNG-backed salt, IV, and session key generation via `SecureRandom` (CWE-330).
|
|
87
|
-
-
|
|
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).
|
|
88
105
|
- Strict exception hierarchy (`EncryptedFileError`, `InvalidPasswordError`, `DecryptionError`).
|
|
89
|
-
-
|
|
90
|
-
-
|
|
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.
|
|
91
108
|
|
|
92
109
|
## [0.1.8] - 2026-08-18
|
|
93
110
|
|
|
94
111
|
### Changed
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
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`.
|
|
99
116
|
|
|
100
117
|
### Added
|
|
101
|
-
-
|
|
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.
|
|
102
119
|
|
|
103
120
|
## [0.1.7] - 2026-08-16
|
|
104
121
|
|
|
105
122
|
### Added
|
|
106
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.`).
|
|
107
|
-
-
|
|
124
|
+
- YARD documentation (`@param`, `@return`, `@example`) across all public APIs, builders, proxies, and elements.
|
|
108
125
|
|
|
109
126
|
### Developer Experience
|
|
110
127
|
- Enhanced `rbs-inline` type signatures across all facade methods and builder objects with automated RBS generation.
|
|
@@ -119,10 +136,10 @@
|
|
|
119
136
|
- Hash-compatible symbol indexing in `Elements::Row#[]` (`:cells`, `:index`, `:height`, `:attrs`) and `Elements::Cell#[]` (`:value`, `:ref`, `:style_index`).
|
|
120
137
|
|
|
121
138
|
### Performance
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
-
|
|
125
|
-
-
|
|
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).
|
|
126
143
|
|
|
127
144
|
### Documentation
|
|
128
145
|
- Overhauled `benchmark.rb` with `bundler/inline` for deterministic, zero-setup benchmark reproduction pinning peer gem versions.
|
|
@@ -142,7 +159,7 @@
|
|
|
142
159
|
- Full `RBS::Test` runtime type validation enabled for the entire test suite.
|
|
143
160
|
- Extensive Excel limit warnings documented via YARD tags.
|
|
144
161
|
- Formal SemVer API contract with `@api public` tags for user-facing methods.
|
|
145
|
-
-
|
|
162
|
+
- Mutation testing (Mutant) and test coverage (SimpleCov) integrations.
|
|
146
163
|
|
|
147
164
|
### Changed
|
|
148
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
|
|
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
|
-
|
|
|
20
|
+
| [xlsxrb](https://github.com/niku/xlsxrb) | ✅ | ✅ | ✅ | ✅ |
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
|
|
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`
|
|
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
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
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
|
|
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
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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,12 +166,12 @@ To reproduce locally: `ruby benchmark.rb 100000 10`
|
|
|
162
166
|
|
|
163
167
|
## Quality Assurance & Testing
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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.
|
|
171
175
|
|
|
172
176
|
For full architectural details, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/QUALITY_ASSURANCE.md](docs/QUALITY_ASSURANCE.md).
|
|
173
177
|
|
data/lib/xlsxrb/dsl_helpers.rb
CHANGED
|
@@ -112,5 +112,64 @@ module Xlsxrb
|
|
|
112
112
|
tbl.merge!(opts)
|
|
113
113
|
tbl
|
|
114
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
|
|
115
174
|
end
|
|
116
175
|
end
|
|
@@ -86,10 +86,21 @@ module Xlsxrb
|
|
|
86
86
|
#: () -> Elements::Workbook
|
|
87
87
|
def load
|
|
88
88
|
loaded_sheets = sheets.map { |s| s.respond_to?(:load) ? s.load : s }
|
|
89
|
+
close
|
|
89
90
|
with(sheets: loaded_sheets)
|
|
90
91
|
end
|
|
91
92
|
alias to_workbook load
|
|
92
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
|
|
103
|
+
|
|
93
104
|
# Returns a new Workbook with the specified sheet updated.
|
|
94
105
|
# Yields the matched worksheet to the block, which must return a new Worksheet.
|
|
95
106
|
#
|
|
@@ -3,14 +3,13 @@
|
|
|
3
3
|
# rbs_inline: enabled
|
|
4
4
|
|
|
5
5
|
require_relative "xml_parser"
|
|
6
|
+
require_relative "xml_builder"
|
|
6
7
|
|
|
7
8
|
module Xlsxrb
|
|
8
9
|
module Ooxml
|
|
9
10
|
# SAX-based parser for xl/sharedStrings.xml.
|
|
10
11
|
# Returns an Array of strings (index = SST index).
|
|
11
12
|
class SharedStringsParser
|
|
12
|
-
XML_ENTITIES = { "&" => "&", "<" => "<", ">" => ">", """ => '"', "'" => "'" }.freeze
|
|
13
|
-
|
|
14
13
|
# Parses all shared strings and returns an Array of strings.
|
|
15
14
|
def self.parse(xml_string, _part_name: "xl/sharedStrings.xml")
|
|
16
15
|
return [] if xml_string.nil? || xml_string.empty?
|
|
@@ -89,7 +88,7 @@ module Xlsxrb
|
|
|
89
88
|
str = extract_multi_t(xml, si_open_end + 1, si_end)
|
|
90
89
|
else
|
|
91
90
|
raw_str = xml.byteslice(t_open_end + 1, t_end - t_open_end - 1).force_encoding("UTF-8")
|
|
92
|
-
str =
|
|
91
|
+
str = XmlBuilder.unescape(raw_str)
|
|
93
92
|
end
|
|
94
93
|
else
|
|
95
94
|
str = ""
|
|
@@ -128,8 +127,7 @@ module Xlsxrb
|
|
|
128
127
|
break unless t_end && t_end <= to
|
|
129
128
|
|
|
130
129
|
raw_chunk = xml.byteslice(t_open_end + 1, t_end - t_open_end - 1).force_encoding("UTF-8")
|
|
131
|
-
|
|
132
|
-
buf << raw_chunk
|
|
130
|
+
buf << XmlBuilder.unescape(raw_chunk)
|
|
133
131
|
pos = t_end + 4
|
|
134
132
|
end
|
|
135
133
|
buf
|
|
@@ -33,13 +33,11 @@ module Xlsxrb
|
|
|
33
33
|
def self.write(target, sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
|
|
34
34
|
defined_names: nil, core_properties: nil, app_properties: nil,
|
|
35
35
|
custom_properties: nil, workbook_protection: nil, workbook_properties: nil)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
writer.write_to(target)
|
|
42
|
-
end
|
|
36
|
+
writer = new(sheets: sheets, shared_strings: shared_strings, shared_strings_index: shared_strings_index, styles: styles,
|
|
37
|
+
defined_names: defined_names, core_properties: core_properties,
|
|
38
|
+
app_properties: app_properties, custom_properties: custom_properties,
|
|
39
|
+
workbook_protection: workbook_protection, workbook_properties: workbook_properties)
|
|
40
|
+
writer.write_to(target)
|
|
43
41
|
end
|
|
44
42
|
|
|
45
43
|
def initialize(sheets:, shared_strings: [], shared_strings_index: nil, styles: nil,
|