xlsxrb 0.1.14 → 0.1.15
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 +47 -0
- data/README.md +15 -14
- data/lib/xlsxrb/elements/workbook.rb +36 -0
- data/lib/xlsxrb/elements/worksheet.rb +102 -7
- data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
- data/lib/xlsxrb/ooxml/reader.rb +2 -3
- data/lib/xlsxrb/ooxml/workbook_parser.rb +21 -1
- data/lib/xlsxrb/ooxml/workbook_writer.rb +17 -9
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +71 -19
- data/lib/xlsxrb/ooxml/writer/features_xml.rb +1 -1
- data/lib/xlsxrb/ooxml/xml_builder.rb +14 -2
- data/lib/xlsxrb/ooxml/zip_reader.rb +35 -3
- data/lib/xlsxrb/stream_sheet.rb +234 -2
- data/lib/xlsxrb/stream_writer.rb +162 -17
- data/lib/xlsxrb/utils.rb +53 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb/workbook_builder.rb +1 -10
- data/lib/xlsxrb/worksheet_builder.rb +512 -9
- data/lib/xlsxrb.rb +500 -9
- data/sig/generated/xlsxrb/elements/workbook.rbs +28 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +53 -2
- data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +1 -1
- data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +10 -2
- data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +3 -1
- data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +3 -0
- data/sig/generated/xlsxrb/stream_sheet.rbs +111 -1
- data/sig/generated/xlsxrb/stream_writer.rbs +100 -16
- data/sig/generated/xlsxrb/utils.rbs +32 -0
- data/sig/generated/xlsxrb/worksheet_builder.rbs +249 -7
- data/sig/generated/xlsxrb.rbs +39 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ec522a5f1d603d001d74555842823cfd56bf1fc96239e3e04daabadd345b5e9
|
|
4
|
+
data.tar.gz: b8cb5562079da225ceae806972851c9f0a1462083a1d8a05753f6896d511b6b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3344f027a164d86a5546b45be5cac8213588ef99aa27d5a3b466b0c0504f5fe8607cb34d6b95e6bc42685108b1403eb8a0e5a28fa268a1707911c65fcc42e2d
|
|
7
|
+
data.tar.gz: 47f50e238ff4f5209421d99f34ddd58fb26c6428020f0ff9731b8b34374e9b757621fe833f57763978346adc5243064d2bb8e16ceca27f05a0627402b5bf0b3d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.15] - 2026-10-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Block form for table and data validation configuration: Support optional configuration block in `table` and `validate_data` methods on `WorksheetBuilder`, `StreamWriter`, and `WorksheetProxy` using `TableBuilder` and `DataValidationBuilder` contexts.
|
|
7
|
+
- Protected range and cell watch configuration DSL and properties (`protected_range` and `cell_watch`): Add `protected_range` (and alias `add_protected_range`) and `cell_watch` (and alias `add_cell_watch`) methods to `WorksheetBuilder`, `StreamWriter`, and `WorksheetProxy`, normalize passwords into ECMA-376 compliant hashes or modern SHA-512 protection attributes, serialize `<protectedRanges>` and `<cellWatches>` according to OOXML schema sequence rules in `WorksheetWriter`, and expose `protected_ranges` and `cell_watches` readers on `Elements::Worksheet` and `StreamSheet`.
|
|
8
|
+
- Range coordinate conversion utilities (`Xlsxrb::Utils.parse_range` and `range_to_ref`): Parse cell range references (`"A1:C10"`, `"A1"`) to 0-based `[[top_row, left_col], [bottom_row, right_col]]` coordinate pairs, and format bounding coordinate tuples into A1-style range references (`"A1:C10"`) with `Xlsxrb.parse_range` and `Xlsxrb.range_to_ref` facade methods.
|
|
9
|
+
- Sheet format configuration DSL and properties (`sheet_format`): Add `sheet_format` method to `WorksheetBuilder`, `StreamWriter`, and `WorksheetProxy` supporting `default_row_height:`, `default_col_width:`, and extended properties (`outline_level_row`, `outline_level_col`, `zero_height`, `custom_height`, `thick_top`, `thick_bottom`), serialize `<sheetFormatPr>` schema-compliantly in `WorksheetWriter` and `WorkbookWriter`, and expose `sheet_format` reader on `Elements::Worksheet` and `StreamSheet`.
|
|
10
|
+
- Numeric and hexadecimal XML entity decoding in `XmlBuilder.unescape`: Support decoding decimal (`&#...;`) and hexadecimal (`&#x...;`) XML numeric character references with UTF-8 encoding and `RangeError` fallback.
|
|
11
|
+
- ZIP64 extra field support in `ZipReader`: Support reading ZIP64 extra fields (`0x0001`) in both central directory headers and local file headers for 64-bit uncompressed sizes, compressed sizes, and local header offsets.
|
|
12
|
+
- Expose `merged_cells` and `auto_filter` on `Elements::Worksheet`: Expose parsed sheet merge ranges and auto filter definitions directly on in-memory worksheets when loaded via `Xlsxrb.read` and `StreamSheet#load`.
|
|
13
|
+
- Expose document properties and protection settings on `Elements::Workbook`: Expose `core_properties`, `app_properties`, `custom_properties`, and `workbook_protection` readers on in-memory workbooks, populating them during `Xlsxrb.read` from `docProps/core.xml`, `docProps/app.xml`, `docProps/custom.xml`, and `<workbookProtection>`.
|
|
14
|
+
- Support updating cell hyperlinks and comments in `Xlsxrb.modify`: Synchronize modified cell hyperlinks and comments during template modification, updating or creating `_rels/sheetN.xml.rels`, `commentsN.xml`, `vmlDrawingN.vml`, and schema-compliant `<hyperlinks>` and `<legacyDrawing>` tags in worksheet XML.
|
|
15
|
+
- Expose sheet structure, print, and view metadata on `Elements::Worksheet` and `StreamSheet`: Expose `tables`, `shapes`, `freeze_pane`, `page_setup`, `page_margins`, `header_footer`, `print_options`, `sheet_protection`, `sheet_view`, `sheet_properties`, `row_breaks`, and `col_breaks` on both streaming and in-memory worksheets.
|
|
16
|
+
- WebAssembly Live Playground Monaco Editor integration: Replaced `<textarea>` with Monaco Editor in `docs/preview.html` featuring `vs-dark` theme, Ruby syntax highlighting, 2-space auto-indentation, auto-closing brackets and quotes, minimap, line numbers, and xlsxrb DSL snippet completion candidates (`Xlsxrb.write`, `wb.sheet`, `wb.style`, `s.row`, `s.column`, `s.chart`, `s.table`, `s.merge`, etc.). Implemented Blob URL worker initialization for Cross-Origin Isolation (COOP/COEP) compatibility and isolated AMD loader state during UMD binary bootstrap.
|
|
17
|
+
- Automated documentation and playground verification (`rake doc:check`): Validate generated documentation assets (file existence, minimum sizes, and magic bytes), verify HTTP server responses (status 200/206, MIME types, COOP/COEP headers, and absence of invalid compression headers) via byte-range requests, and automatically parse and execute all Live Playground presets to guarantee error-free generation prior to preview and deployment.
|
|
18
|
+
- WebAssembly Live Playground: Revamped `docs/preview.html` with current `Xlsxrb.write` DSL, added visually striking Executive Dashboard first view featuring an embedded Ruby gem logo image, KPI summary cards, zebra-striped table, currency formats, `=SUM` formulas, and a clustered bar chart in ~55 lines, added interactive preset selector with 11 spreadsheet scenarios (executive dashboard, sales reports, bar/line/pie charts, tables, borders, number formats, conditional formatting, hyperlinks, and Japanese text), direct URL deeplinking (`?example=...`), and seamless link integration throughout `VisualGallery.md` and `README.md`.
|
|
19
|
+
- New visual examples and VRT baselines for missing formatting and print features: `hyperlinks.rb` (external and internal cell hyperlinks), `column_styles.rb` (column-level default number formats and styling), `page_repeat_rows.rb` (repeating header rows for print setup), and `sheet_protection.rb` (password protection with granular permissions).
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Standardized top navigation header across all documentation pages and Live Playground: Unified navbar with `Documentation`, `Visual Gallery`, `Live Playground`, and `GitHub` navigation links, active page highlighting, search input, and SVG theme toggle.
|
|
23
|
+
- Reorganized documentation sidebar navigation: Promoted `Visual Gallery` and `Live Playground` to prominent action items at the top of the sidebar, renamed the collapsible `docs` folder to an open `Guides` section, and formatted screaming uppercase filenames into clean Title Case labels (`Architecture`, `Development Guide`, etc.) to eliminate visual noise.
|
|
24
|
+
- Enhanced search index discoverability: Pre-indexed all documentation guides, pages, and the Live Playground in `search_data.js` so search queries for "Visual Gallery", "Playground", and "Architecture" yield instant results.
|
|
25
|
+
- Universal rendering without font tofu: Replaced theme toggle emojis with SVG sun and moon icons, and replaced README comparison table emoji checkmarks with accessible text badges (`Yes` / `-`) to eliminate tofu glyphs (`□`) across Linux and headless environments.
|
|
26
|
+
- Visual Gallery layout polish: Constrained gallery overview thumbnails to 160px width with neat borders, styled `Live Playground` actions as interactive buttons, and constrained full baseline preview images to a maximum height of 480px to avoid unbounded scrolling whitespace.
|
|
27
|
+
- Documentation callout banners: Transformed GitHub-style markdown alert blockquotes (`[!TIP]`, `[!NOTE]`, `[!WARNING]`) into styled callout banners with SVGs.
|
|
28
|
+
- Cleaned up duplicated inline doc tags: Stripped repeated `rbs_inline: enabled` and `@api public` tags from generated module documentation.
|
|
29
|
+
- Align WebAssembly Live Playground design with documentation: Unify `docs/preview.html` visual styling with the RDoc Aliki documentation design system, adding light and dark theme support with local storage persistence, synchronized Monaco Editor theme switching (`vs` and `vs-dark`), top navigation bar matching documentation layout with links to documentation and visual gallery, solid brand coral red Run button matching documentation accent tokens, and dynamic status dot indicator (`Initializing...`, `Ready`, `Rendering...`, `Error`).
|
|
30
|
+
- Cleaned up visual examples and gallery generator: Stripped internal read validation debug dumps across all visual examples to present concise DSL code snippets, refreshed formula and merge syntax to use modern public APIs (`Xlsxrb.formula`, `A1:C1` ranges), and reorganized `VisualGallery.md` into 9 structured capability categories with dedicated preview tables and live playground links.
|
|
31
|
+
- Removed non-visual and redundant visual examples: Removed `invoice_statement.rb` and `streaming_append.rb` and their baseline directories from the visual regression test suite and gallery.
|
|
32
|
+
- Use `nil` for unstyled cells in `embedded_images.rb` example instead of undefined `"left"`.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- XML namespace prefix tolerance in template modification (`replace_sheet_data`): Locate `<sheetData>` and matching closing tags dynamically regardless of XML namespace prefixes (such as `<x:sheetData>` or `<d:sheetData>`) or self-closing syntax during `Xlsxrb.modify`.
|
|
36
|
+
- Resilience against duplicate XML declarations and malformed `.rels` files in `RelationshipsParser`: Sanitize duplicate or multiple `<?xml ...?>` declarations and provide regex fallback scanning if SAX parsing fails on malformed relationship XML.
|
|
37
|
+
- Fix false positive date detection for color tags like `[Red]` in `Ooxml::Reader#date_pattern?`: Unify custom number format date evaluation with `NumberFormatter.date_format?`, ignoring bracketed formatting codes (such as `[Red]` or `[Green]`) so negative numbers with colored styles are not misidentified as dates.
|
|
38
|
+
- Fix layout overlap between Playground Run & Preview button and theme toggle: Override absolute positioning of `.theme-toggle` from RDoc Aliki styles with static inline-flex flow in `docs/preview.html` to eliminate button collision and preserve clean spacing across light and dark modes.
|
|
39
|
+
- Prevent WebAssembly out-of-memory error during ZetaOffice instantiation on Firefox: Provide custom `Module.instantiateWasm` handler in `docs/preview.html` with streaming-first compilation (`WebAssembly.instantiateStreaming` / `WebAssembly.compileStreaming`) to bypass Emscripten's default `instantiateArrayBuffer` fallback that allocates an extra 160MB ArrayBuffer on memory pressure, and sequence Ruby Wasm and LibreOffice Wasm engine initialization to eliminate concurrent memory peaks.
|
|
40
|
+
- Resolve WebAssembly Live Playground (`docs/preview.html`) console errors and warnings across Chromium and Firefox:
|
|
41
|
+
- Compile `ruby.wasm` via streaming (`WebAssembly.compileStreaming`) with automatic fallback to avoid large intermediate ArrayBuffer heap allocations, eliminating `InternalError: out of memory` on memory-constrained browser environments.
|
|
42
|
+
- Bypass service worker registration and reloading in `coi-serviceworker.js` when native Cross-Origin Isolation headers (`COOP`/`COEP`) are already present, preventing in-flight network request abortions (`NS_BINDING_ABORTED`).
|
|
43
|
+
- Filter benign C/C++ Emscripten/Qt runtime stderr diagnostics in `Module.printErr` and route diagnostic output to `console.log` rather than `console.warn`.
|
|
44
|
+
- Guard UNO document model close/dispose handling in `office_thread.js` to cleanly close previous models without emitting `console.warn` on preset switching or re-runs.
|
|
45
|
+
- Prevent unhandled clipboard permission errors in Live Playground on Firefox: Intercept `navigator.permissions.query` in `docs/preview.html` and worker thread bootstrap to resolve dummy `PermissionStatus` objects for `clipboard-read` and `clipboard-write`, preventing uncaught `TypeError` exceptions from Embind in browsers that do not support clipboard descriptors in `PermissionDescriptor`.
|
|
46
|
+
- Fix missing closing angle bracket on styled formula cells in `WorksheetWriter`: Add missing `>` before `<f>` element when emitting styled formula cells in `WorksheetWriter#write_row_values`, resolving XML syntax errors (`<c r="..." s="..."<f>`) that caused LibreOffice and spreadsheet engines to reject sheet formatting.
|
|
47
|
+
- Prevent serialization of unresolved non-integer style names as `s` attribute in `WorksheetWriter`: Guard style resolution in `WorksheetWriter` to only emit valid integer style indices, ignoring unresolved strings or symbols to maintain ECMA-376 XML Schema conformance (`xs:unsignedInt`).
|
|
48
|
+
- Prevent invalid `Content-Encoding: br` header in local `doc:preview` server: Remove erroneous Brotli encoding header injected on uncompressed Emscripten `.data` packages (such as `soffice.data`), and supply native Cross-Origin Isolation headers (`COOP`, `COEP`, `CORP`) to resolve browser decompression failures (`NS_ERROR_INVALID_CONTENT_ENCODING`).
|
|
49
|
+
|
|
3
50
|
## [0.1.14] - 2026-10-10
|
|
4
51
|
|
|
5
52
|
### Added
|
data/README.md
CHANGED
|
@@ -10,16 +10,17 @@ The Ruby ecosystem has several XLSX libraries designed for specific tradeoffs:
|
|
|
10
10
|
|
|
11
11
|
| Library | Read | Write | Streaming | In-Memory |
|
|
12
12
|
| :--- | :---: | :---: | :---: | :---: |
|
|
13
|
-
| [roo](https://rubygems.org/gems/roo) |
|
|
14
|
-
| [creek](https://rubygems.org/gems/creek) |
|
|
15
|
-
| [xsv](https://rubygems.org/gems/xsv) |
|
|
16
|
-
| [simple_xlsx_reader](https://rubygems.org/gems/simple_xlsx_reader) |
|
|
17
|
-
| [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) |
|
|
18
|
-
| [write_xlsx](https://rubygems.org/gems/write_xlsx) |
|
|
19
|
-
| [xlsxtream](https://rubygems.org/gems/xlsxtream) |
|
|
20
|
-
| [fast_excel](https://rubygems.org/gems/fast_excel) |
|
|
21
|
-
| [rubyXL](https://rubygems.org/gems/rubyXL) |
|
|
22
|
-
| [xlsxrb](https://github.com/niku/xlsxrb) |
|
|
13
|
+
| [roo](https://rubygems.org/gems/roo) | Yes | - | - | Yes |
|
|
14
|
+
| [creek](https://rubygems.org/gems/creek) | Yes | - | Yes | - |
|
|
15
|
+
| [xsv](https://rubygems.org/gems/xsv) | Yes | - | Yes | - |
|
|
16
|
+
| [simple_xlsx_reader](https://rubygems.org/gems/simple_xlsx_reader) | Yes | - | Yes | - |
|
|
17
|
+
| [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) | - | Yes | - | Yes |
|
|
18
|
+
| [write_xlsx](https://rubygems.org/gems/write_xlsx) | - | Yes | - | Yes |
|
|
19
|
+
| [xlsxtream](https://rubygems.org/gems/xlsxtream) | - | Yes | Yes | - |
|
|
20
|
+
| [fast_excel](https://rubygems.org/gems/fast_excel) | - | Yes | Yes | - |
|
|
21
|
+
| [rubyXL](https://rubygems.org/gems/rubyXL) | Yes | Yes | - | Yes |
|
|
22
|
+
| [xlsxrb](https://github.com/niku/xlsxrb) | Yes | Yes | Yes | Yes |
|
|
23
|
+
|
|
23
24
|
|
|
24
25
|
These libraries make different architectural tradeoffs:
|
|
25
26
|
* Streaming Model: Writes or reads rows sequentially on-the-fly to maintain a constant $O(1)$, low-memory footprint regardless of dataset size.
|
|
@@ -39,7 +40,7 @@ Maintaining a gem that supports both reading and writing across streaming and in
|
|
|
39
40
|
|
|
40
41
|
### Autonomous Quality Assurance
|
|
41
42
|
|
|
42
|
-
To maintain its multi-layered QA suite (Steep static typing, 100% mutation kill rate across
|
|
43
|
+
To maintain its multi-layered QA suite (Steep static typing, 100% mutation kill rate across 43 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)).
|
|
43
44
|
|
|
44
45
|
## Installation
|
|
45
46
|
|
|
@@ -52,13 +53,13 @@ bundle add xlsxrb
|
|
|
52
53
|
|
|
53
54
|
Try `xlsxrb` directly in your browser without installing anything!
|
|
54
55
|
|
|
55
|
-
[
|
|
56
|
+
[Try the Live Demo / Interactive Playground](https://niku.github.io/xlsxrb/preview.html)
|
|
56
57
|
|
|
57
58
|
<p align="center">
|
|
58
59
|
<img src="docs/assets/playground_preview.png" width="100%" alt="Interactive WebAssembly Playground with Live LibreOffice Preview"/>
|
|
59
60
|
</p>
|
|
60
61
|
|
|
61
|
-
You can also browse
|
|
62
|
+
You can also browse 80+ rendered visual examples across all features in the [Visual Examples Gallery](docs/visual/VisualGallery.md).
|
|
62
63
|
|
|
63
64
|
## Usage
|
|
64
65
|
|
|
@@ -172,7 +173,7 @@ Verified by a multi-layered QA architecture:
|
|
|
172
173
|
* Microsoft Open XML SDK Validation: Validates generated OOXML structures against Microsoft's official SDK.
|
|
173
174
|
* Visual Regression Testing (VRT): Headless LibreOffice Calc pixel-by-pixel rendering checks.
|
|
174
175
|
* Contract & Round-Trip Tests: Verifies parity between Streaming and In-Memory APIs and round-trip read/write accuracy.
|
|
175
|
-
* Mutation Testing (Mutant): 100% mutant kill rate (1,
|
|
176
|
+
* Mutation Testing (Mutant): 100% mutant kill rate (1,528/1,528 mutations across 43 subjects) on pure algorithms, predicates, and coordinates ([docs/MUTATION_TESTING.md](docs/MUTATION_TESTING.md)).
|
|
176
177
|
* Security & DoS Protection: Formula injection mitigation and ZIP bomb protection.
|
|
177
178
|
|
|
178
179
|
For full architectural details, see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/QUALITY_ASSURANCE.md](docs/QUALITY_ASSURANCE.md).
|
|
@@ -72,6 +72,42 @@ module Xlsxrb
|
|
|
72
72
|
props&.[](:date1904) ? true : false
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Returns core document properties (e.g. title, creator, created), or an empty hash.
|
|
76
|
+
#
|
|
77
|
+
# @return [Hash<Symbol, untyped>]
|
|
78
|
+
# @api public
|
|
79
|
+
#: () -> Hash[Symbol, untyped]
|
|
80
|
+
def core_properties
|
|
81
|
+
unmapped_data[:core_properties] || unmapped_data.dig(:facade, :core_properties) || {}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns application properties (e.g. app_name, doc_security), or an empty hash.
|
|
85
|
+
#
|
|
86
|
+
# @return [Hash<Symbol, untyped>]
|
|
87
|
+
# @api public
|
|
88
|
+
#: () -> Hash[Symbol, untyped]
|
|
89
|
+
def app_properties
|
|
90
|
+
unmapped_data[:app_properties] || unmapped_data.dig(:facade, :app_properties) || {}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns custom document properties as an array of property hashes, or an empty array.
|
|
94
|
+
#
|
|
95
|
+
# @return [Array<Hash[Symbol, untyped]>]
|
|
96
|
+
# @api public
|
|
97
|
+
#: () -> Array[Hash[Symbol, untyped]]
|
|
98
|
+
def custom_properties
|
|
99
|
+
unmapped_data[:custom_properties] || unmapped_data.dig(:facade, :custom_properties) || []
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns workbook protection settings as a hash, or nil if unprotected.
|
|
103
|
+
#
|
|
104
|
+
# @return [Hash[Symbol, untyped], nil]
|
|
105
|
+
# @api public
|
|
106
|
+
#: () -> Hash[Symbol, untyped]?
|
|
107
|
+
def workbook_protection
|
|
108
|
+
unmapped_data[:workbook_protection] || unmapped_data.dig(:facade, :workbook_protection)
|
|
109
|
+
end
|
|
110
|
+
|
|
75
111
|
# Returns the defined name matching name (and optionally sheet), or nil.
|
|
76
112
|
#
|
|
77
113
|
# @example
|
|
@@ -17,7 +17,7 @@ module Xlsxrb
|
|
|
17
17
|
class Worksheet
|
|
18
18
|
[Enumerable, CoordinateAccess].each { |m| include m }
|
|
19
19
|
|
|
20
|
-
attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups
|
|
20
|
+
attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups, :merged_cells, :auto_filter, :tables, :shapes, :freeze_pane, :page_setup, :page_margins, :header_footer, :print_options, :sheet_protection, :sheet_view, :sheet_properties, :sheet_format, :row_breaks, :col_breaks, :protected_ranges, :cell_watches
|
|
21
21
|
attr_writer :dimension
|
|
22
22
|
alias conditional_formats conditional_formatting
|
|
23
23
|
|
|
@@ -42,8 +42,25 @@ module Xlsxrb
|
|
|
42
42
|
# @param print_area [String, nil] Sheet print area range (e.g. "A1:H50").
|
|
43
43
|
# @param print_titles [Hash, nil] Repeating print title rows/cols (e.g. { rows: "1:2", cols: "A:B" }).
|
|
44
44
|
# @param sparkline_groups [Array<Hash>] Sparkline groups in the sheet.
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
# @param merged_cells [Array<String>] Merged cell ranges (e.g. ["A1:B2"]).
|
|
46
|
+
# @param auto_filter [String, nil] AutoFilter range (e.g. "A1:E10").
|
|
47
|
+
# @param tables [Array<Hash>] Table definitions in the sheet.
|
|
48
|
+
# @param shapes [Array<Hash>] Shape definitions in the sheet.
|
|
49
|
+
# @param freeze_pane [Hash, nil] Freeze pane settings (e.g. { row: 1, col: 0 }).
|
|
50
|
+
# @param page_setup [Hash] Page setup properties (e.g. orientation, paper size).
|
|
51
|
+
# @param page_margins [Hash, nil] Page margins settings.
|
|
52
|
+
# @param header_footer [Hash] Header and footer configurations.
|
|
53
|
+
# @param print_options [Hash] Print options (e.g. gridlines, headings).
|
|
54
|
+
# @param sheet_protection [Hash, nil] Sheet protection settings.
|
|
55
|
+
# @param sheet_view [Hash] Sheet view configurations (e.g. zoom, gridlines).
|
|
56
|
+
# @param sheet_properties [Hash] Sheet properties (e.g. tabColor, outlinePr).
|
|
57
|
+
# @param sheet_format [Hash] Sheet format properties (e.g. defaultRowHeight, defaultColWidth).
|
|
58
|
+
# @param row_breaks [Array<Integer | Hash>] Explicit page row breaks.
|
|
59
|
+
# @param col_breaks [Array<Integer | Hash>] Explicit page column breaks.
|
|
60
|
+
# @param protected_ranges [Array<Hash>] Protected range definitions.
|
|
61
|
+
# @param cell_watches [Array<String>] Watched cell references.
|
|
62
|
+
#: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?conditional_formatting: Array[Hash[Symbol, untyped]]?, ?data_validations: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?conditional_formats: Array[Hash[Symbol, untyped]]?, ?state: Symbol, ?hyperlinks: Hash[String, untyped], ?comments: Array[Hash[Symbol, untyped]], ?images: Array[Elements::Image], ?styles: Hash[untyped, untyped]?, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?print_area: String?, ?print_titles: Hash[Symbol, untyped]?, ?sparkline_groups: Array[Hash[Symbol, untyped]], ?merged_cells: Array[String], ?auto_filter: String?, ?tables: Array[Hash[Symbol, untyped]], ?shapes: Array[Hash[Symbol, untyped]], ?freeze_pane: Hash[Symbol, untyped]?, ?page_setup: Hash[Symbol, untyped], ?page_margins: Hash[Symbol, untyped]?, ?header_footer: Hash[Symbol, untyped], ?print_options: Hash[Symbol, untyped], ?sheet_protection: Hash[Symbol, untyped]?, ?sheet_view: Hash[Symbol, untyped], ?sheet_properties: Hash[Symbol, untyped], ?sheet_format: Hash[Symbol, untyped], ?row_breaks: Array[untyped], ?col_breaks: Array[untyped], ?protected_ranges: Array[Hash[Symbol, untyped]], ?cell_watches: Array[String]) -> void
|
|
63
|
+
def initialize(name:, rows: [], columns: [], charts: [], conditional_formatting: nil, data_validations: [], unmapped_data: {}, errors: nil, conditional_formats: nil, state: :visible, hyperlinks: {}, comments: [], images: [], styles: nil, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, print_area: nil, print_titles: nil, sparkline_groups: [], merged_cells: [], auto_filter: nil, tables: [], shapes: [], freeze_pane: nil, page_setup: {}, page_margins: nil, header_footer: {}, print_options: {}, sheet_protection: nil, sheet_view: {}, sheet_properties: {}, sheet_format: {}, row_breaks: [], col_breaks: [], protected_ranges: [], cell_watches: [])
|
|
47
64
|
@name = name
|
|
48
65
|
@rows = (rows || []).freeze
|
|
49
66
|
@columns = (columns || []).freeze
|
|
@@ -67,6 +84,23 @@ module Xlsxrb
|
|
|
67
84
|
@trim_empty_rows = trim_empty_rows ? true : false
|
|
68
85
|
@pad_empty_rows = pad_empty_rows ? true : false
|
|
69
86
|
@pad_empty_cells = pad_empty_cells ? true : false
|
|
87
|
+
@merged_cells = (merged_cells || []).freeze
|
|
88
|
+
@auto_filter = auto_filter
|
|
89
|
+
@tables = (tables || []).freeze
|
|
90
|
+
@shapes = (shapes || []).freeze
|
|
91
|
+
@freeze_pane = freeze_pane&.freeze
|
|
92
|
+
@page_setup = (page_setup || {}).freeze
|
|
93
|
+
@page_margins = page_margins&.freeze
|
|
94
|
+
@header_footer = (header_footer || {}).freeze
|
|
95
|
+
@print_options = (print_options || {}).freeze
|
|
96
|
+
@sheet_protection = sheet_protection&.freeze
|
|
97
|
+
@sheet_view = (sheet_view || {}).freeze
|
|
98
|
+
@sheet_properties = (sheet_properties || {}).freeze
|
|
99
|
+
@sheet_format = (sheet_format || unmapped_data[:sheet_format] || unmapped_data.dig(:facade, :sheet_format) || {}).freeze
|
|
100
|
+
@row_breaks = (row_breaks || []).freeze
|
|
101
|
+
@col_breaks = (col_breaks || []).freeze
|
|
102
|
+
@protected_ranges = (protected_ranges || unmapped_data[:protected_ranges] || unmapped_data.dig(:facade, :protected_ranges) || []).freeze
|
|
103
|
+
@cell_watches = (cell_watches || unmapped_data[:cell_watches] || unmapped_data.dig(:facade, :cell_watches) || []).freeze
|
|
70
104
|
end
|
|
71
105
|
|
|
72
106
|
# Returns whether trailing empty rows are omitted during iteration.
|
|
@@ -488,6 +522,23 @@ module Xlsxrb
|
|
|
488
522
|
new_print_titles = changes.key?(:print_titles) ? changes[:print_titles] : print_titles
|
|
489
523
|
new_sparklines = changes.key?(:sparkline_groups) ? changes[:sparkline_groups] : sparkline_groups
|
|
490
524
|
new_trim = changes.key?(:trim_empty_rows) ? changes[:trim_empty_rows] : @trim_empty_rows
|
|
525
|
+
new_merged = changes.key?(:merged_cells) ? changes[:merged_cells] : merged_cells
|
|
526
|
+
new_auto_filter = changes.key?(:auto_filter) ? changes[:auto_filter] : auto_filter
|
|
527
|
+
new_tables = changes.key?(:tables) ? changes[:tables] : tables
|
|
528
|
+
new_shapes = changes.key?(:shapes) ? changes[:shapes] : shapes
|
|
529
|
+
new_freeze_pane = changes.key?(:freeze_pane) ? changes[:freeze_pane] : freeze_pane
|
|
530
|
+
new_page_setup = changes.key?(:page_setup) ? changes[:page_setup] : page_setup
|
|
531
|
+
new_page_margins = changes.key?(:page_margins) ? changes[:page_margins] : page_margins
|
|
532
|
+
new_header_footer = changes.key?(:header_footer) ? changes[:header_footer] : header_footer
|
|
533
|
+
new_print_options = changes.key?(:print_options) ? changes[:print_options] : print_options
|
|
534
|
+
new_sheet_protection = changes.key?(:sheet_protection) ? changes[:sheet_protection] : sheet_protection
|
|
535
|
+
new_sheet_view = changes.key?(:sheet_view) ? changes[:sheet_view] : sheet_view
|
|
536
|
+
new_sheet_properties = changes.key?(:sheet_properties) ? changes[:sheet_properties] : sheet_properties
|
|
537
|
+
new_sheet_format = changes.key?(:sheet_format) ? changes[:sheet_format] : sheet_format
|
|
538
|
+
new_row_breaks = changes.key?(:row_breaks) ? changes[:row_breaks] : row_breaks
|
|
539
|
+
new_col_breaks = changes.key?(:col_breaks) ? changes[:col_breaks] : col_breaks
|
|
540
|
+
new_protected_ranges = changes.key?(:protected_ranges) ? changes[:protected_ranges] : protected_ranges
|
|
541
|
+
new_cell_watches = changes.key?(:cell_watches) ? changes[:cell_watches] : cell_watches
|
|
491
542
|
|
|
492
543
|
self.class.new(
|
|
493
544
|
name: new_name,
|
|
@@ -507,7 +558,24 @@ module Xlsxrb
|
|
|
507
558
|
print_area: new_print_area,
|
|
508
559
|
print_titles: new_print_titles,
|
|
509
560
|
sparkline_groups: new_sparklines,
|
|
510
|
-
trim_empty_rows: new_trim
|
|
561
|
+
trim_empty_rows: new_trim,
|
|
562
|
+
merged_cells: new_merged,
|
|
563
|
+
auto_filter: new_auto_filter,
|
|
564
|
+
tables: new_tables,
|
|
565
|
+
shapes: new_shapes,
|
|
566
|
+
freeze_pane: new_freeze_pane,
|
|
567
|
+
page_setup: new_page_setup,
|
|
568
|
+
page_margins: new_page_margins,
|
|
569
|
+
header_footer: new_header_footer,
|
|
570
|
+
print_options: new_print_options,
|
|
571
|
+
sheet_protection: new_sheet_protection,
|
|
572
|
+
sheet_view: new_sheet_view,
|
|
573
|
+
sheet_properties: new_sheet_properties,
|
|
574
|
+
sheet_format: new_sheet_format,
|
|
575
|
+
row_breaks: new_row_breaks,
|
|
576
|
+
col_breaks: new_col_breaks,
|
|
577
|
+
protected_ranges: new_protected_ranges,
|
|
578
|
+
cell_watches: new_cell_watches
|
|
511
579
|
)
|
|
512
580
|
end
|
|
513
581
|
|
|
@@ -533,7 +601,22 @@ module Xlsxrb
|
|
|
533
601
|
print_area: print_area,
|
|
534
602
|
print_titles: print_titles,
|
|
535
603
|
sparkline_groups: sparkline_groups,
|
|
536
|
-
trim_empty_rows: @trim_empty_rows
|
|
604
|
+
trim_empty_rows: @trim_empty_rows,
|
|
605
|
+
merged_cells: merged_cells,
|
|
606
|
+
auto_filter: auto_filter,
|
|
607
|
+
tables: tables,
|
|
608
|
+
shapes: shapes,
|
|
609
|
+
freeze_pane: freeze_pane,
|
|
610
|
+
page_setup: page_setup,
|
|
611
|
+
page_margins: page_margins,
|
|
612
|
+
header_footer: header_footer,
|
|
613
|
+
print_options: print_options,
|
|
614
|
+
sheet_protection: sheet_protection,
|
|
615
|
+
sheet_view: sheet_view,
|
|
616
|
+
sheet_properties: sheet_properties,
|
|
617
|
+
sheet_format: sheet_format,
|
|
618
|
+
row_breaks: row_breaks,
|
|
619
|
+
col_breaks: col_breaks
|
|
537
620
|
}
|
|
538
621
|
end
|
|
539
622
|
|
|
@@ -544,13 +627,25 @@ module Xlsxrb
|
|
|
544
627
|
|
|
545
628
|
name == other.name && rows == other.rows && columns == other.columns && charts == other.charts &&
|
|
546
629
|
conditional_formatting == other.conditional_formatting && data_validations == other.data_validations &&
|
|
547
|
-
state == other.state && hyperlinks == other.hyperlinks && comments == other.comments
|
|
630
|
+
state == other.state && hyperlinks == other.hyperlinks && comments == other.comments &&
|
|
631
|
+
merged_cells == other.merged_cells && auto_filter == other.auto_filter &&
|
|
632
|
+
tables == other.tables && shapes == other.shapes && freeze_pane == other.freeze_pane &&
|
|
633
|
+
page_setup == other.page_setup && page_margins == other.page_margins &&
|
|
634
|
+
header_footer == other.header_footer && print_options == other.print_options &&
|
|
635
|
+
sheet_protection == other.sheet_protection && sheet_view == other.sheet_view &&
|
|
636
|
+
sheet_properties == other.sheet_properties && sheet_format == other.sheet_format &&
|
|
637
|
+
row_breaks == other.row_breaks && col_breaks == other.col_breaks
|
|
548
638
|
end
|
|
549
639
|
alias eql? ==
|
|
550
640
|
|
|
551
641
|
#: () -> Integer
|
|
552
642
|
def hash
|
|
553
|
-
[
|
|
643
|
+
[
|
|
644
|
+
self.class, name, rows, columns, charts, conditional_formatting, data_validations,
|
|
645
|
+
state, hyperlinks, comments, merged_cells, auto_filter, tables, shapes, freeze_pane,
|
|
646
|
+
page_setup, page_margins, header_footer, print_options, sheet_protection, sheet_view,
|
|
647
|
+
sheet_properties, sheet_format, row_breaks, col_breaks
|
|
648
|
+
].hash
|
|
554
649
|
end
|
|
555
650
|
|
|
556
651
|
# Returns self when load is called on an already in-memory Worksheet.
|
|
@@ -204,7 +204,7 @@ module Xlsxrb
|
|
|
204
204
|
def start_element(_uri, local_name, qname, _attributes)
|
|
205
205
|
name = element_name(local_name, qname)
|
|
206
206
|
case name
|
|
207
|
-
when "Application", "AppVersion"
|
|
207
|
+
when "Application", "AppVersion", "Company", "Manager"
|
|
208
208
|
@current_field = name
|
|
209
209
|
@text_buffer = +""
|
|
210
210
|
when "HeadingPairs"
|
|
@@ -232,6 +232,12 @@ module Xlsxrb
|
|
|
232
232
|
when "AppVersion"
|
|
233
233
|
@properties[:app_version] = @text_buffer.dup
|
|
234
234
|
@current_field = nil
|
|
235
|
+
when "Company"
|
|
236
|
+
@properties[:company] = @text_buffer.dup
|
|
237
|
+
@current_field = nil
|
|
238
|
+
when "Manager"
|
|
239
|
+
@properties[:manager] = @text_buffer.dup
|
|
240
|
+
@current_field = nil
|
|
235
241
|
when "lpstr"
|
|
236
242
|
if @inside_titles_of_parts
|
|
237
243
|
@titles_of_parts << @text_buffer.dup
|
data/lib/xlsxrb/ooxml/reader.rb
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
require "zlib"
|
|
6
6
|
require "rexml/parsers/sax2parser"
|
|
7
7
|
require "rexml/sax2listener"
|
|
8
|
+
require_relative "../number_formatter"
|
|
8
9
|
|
|
9
10
|
module Xlsxrb
|
|
10
11
|
module Ooxml
|
|
@@ -1280,9 +1281,7 @@ module Xlsxrb
|
|
|
1280
1281
|
end
|
|
1281
1282
|
|
|
1282
1283
|
def date_pattern?(code)
|
|
1283
|
-
|
|
1284
|
-
stripped = code.gsub(/"[^"]*"/, "").gsub(/\\[.]/, "")
|
|
1285
|
-
stripped.match?(/[ymdhsYMDHS]/)
|
|
1284
|
+
NumberFormatter.date_format?(code)
|
|
1286
1285
|
end
|
|
1287
1286
|
|
|
1288
1287
|
def extract_zip_entry(entry_name)
|
|
@@ -89,8 +89,28 @@ module Xlsxrb
|
|
|
89
89
|
def self.parse(xml_string)
|
|
90
90
|
return {} if xml_string.nil? || xml_string.empty?
|
|
91
91
|
|
|
92
|
+
sanitized = xml_string.lstrip
|
|
93
|
+
if sanitized.start_with?("<?xml")
|
|
94
|
+
first_decl_end = sanitized.index("?>")
|
|
95
|
+
if first_decl_end
|
|
96
|
+
decl = sanitized[0..(first_decl_end + 1)]
|
|
97
|
+
rest = sanitized[(first_decl_end + 2)..].gsub(/<\?xml\b[^>]*\?>/i, "")
|
|
98
|
+
sanitized = "#{decl}#{rest}".lstrip
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
92
102
|
listener = Listener.new
|
|
93
|
-
|
|
103
|
+
begin
|
|
104
|
+
XmlParser.parse(sanitized, listener)
|
|
105
|
+
rescue StandardError
|
|
106
|
+
# Fallback to regex scanning if SAX parser fails on malformed XML
|
|
107
|
+
sanitized.scan(%r{<(?:[a-zA-Z0-9_]+:)?Relationship\b([^>]*)/?>}i) do |match|
|
|
108
|
+
attrs_str = match[0]
|
|
109
|
+
id = attrs_str[/\bId=["']([^"']+)["']/i, 1]
|
|
110
|
+
target = attrs_str[/\bTarget=["']([^"']+)["']/i, 1]
|
|
111
|
+
listener.relationships[id] = target if id && target
|
|
112
|
+
end
|
|
113
|
+
end
|
|
94
114
|
listener.relationships
|
|
95
115
|
end
|
|
96
116
|
|
|
@@ -93,6 +93,9 @@ module Xlsxrb
|
|
|
93
93
|
selection: sheet[:selection],
|
|
94
94
|
sheet_view: sheet[:sheet_view],
|
|
95
95
|
sheet_properties: sheet[:sheet_properties],
|
|
96
|
+
sheet_format: sheet[:sheet_format],
|
|
97
|
+
protected_ranges: sheet[:protected_ranges],
|
|
98
|
+
cell_watches: sheet[:cell_watches],
|
|
96
99
|
tables: sheet_tables,
|
|
97
100
|
table_start_rid: info[:table_start_rid],
|
|
98
101
|
legacy_drawing_rid: info[:vml_rid],
|
|
@@ -917,14 +920,14 @@ module Xlsxrb
|
|
|
917
920
|
end
|
|
918
921
|
|
|
919
922
|
def write_worksheet_xml(io, sheet, drawing_rid: nil, sheet_protection: nil,
|
|
920
|
-
|
|
921
|
-
merge_cells: nil, conditional_formats: nil,
|
|
923
|
+
protected_ranges: nil, auto_filter: nil, filter_columns: nil,
|
|
924
|
+
sort_state: nil, merge_cells: nil, conditional_formats: nil,
|
|
922
925
|
data_validations: nil, hyperlinks: nil,
|
|
923
926
|
print_options: nil, page_margins: nil, page_setup: nil,
|
|
924
927
|
header_footer: nil, row_breaks: nil, col_breaks: nil,
|
|
925
|
-
|
|
926
|
-
sheet_view: nil, sheet_properties: nil,
|
|
927
|
-
tables: nil, table_start_rid: nil,
|
|
928
|
+
cell_watches: nil, freeze_pane: nil, split_pane: nil,
|
|
929
|
+
selection: nil, sheet_view: nil, sheet_properties: nil,
|
|
930
|
+
sheet_format: nil, tables: nil, table_start_rid: nil,
|
|
928
931
|
legacy_drawing_rid: nil, sparkline_groups: nil)
|
|
929
932
|
date1904 = @workbook_properties&.[](:date1904) ? true : false
|
|
930
933
|
ws = WorksheetWriter.new(io, date1904: date1904)
|
|
@@ -934,7 +937,8 @@ module Xlsxrb
|
|
|
934
937
|
freeze_pane: freeze_pane,
|
|
935
938
|
split_pane: split_pane,
|
|
936
939
|
selection: selection,
|
|
937
|
-
sheet_view: sheet_view
|
|
940
|
+
sheet_view: sheet_view,
|
|
941
|
+
sheet_format: sheet_format
|
|
938
942
|
)
|
|
939
943
|
if sheet[:rows_tmp_path]
|
|
940
944
|
File.open(sheet[:rows_tmp_path], "rb") do |f|
|
|
@@ -961,6 +965,7 @@ module Xlsxrb
|
|
|
961
965
|
ws.finish(
|
|
962
966
|
drawing_rid: drawing_rid,
|
|
963
967
|
sheet_protection: sheet_protection,
|
|
968
|
+
protected_ranges: protected_ranges,
|
|
964
969
|
auto_filter: auto_filter,
|
|
965
970
|
filter_columns: filter_columns,
|
|
966
971
|
sort_state: sort_state,
|
|
@@ -974,6 +979,7 @@ module Xlsxrb
|
|
|
974
979
|
header_footer: header_footer,
|
|
975
980
|
row_breaks: row_breaks,
|
|
976
981
|
col_breaks: col_breaks,
|
|
982
|
+
cell_watches: cell_watches,
|
|
977
983
|
tables: tables,
|
|
978
984
|
table_start_rid: table_start_rid,
|
|
979
985
|
legacy_drawing_rid: legacy_drawing_rid,
|
|
@@ -1102,11 +1108,13 @@ module Xlsxrb
|
|
|
1102
1108
|
@custom_properties.each_with_index do |prop, idx|
|
|
1103
1109
|
b.open_tag("property", { fmtid: "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}", pid: (idx + 2).to_s, name: prop[:name] })
|
|
1104
1110
|
case prop[:type]
|
|
1105
|
-
when :number
|
|
1111
|
+
when :number, :int, :i4
|
|
1106
1112
|
b.tag("vt:i4") { |_| b.text(prop[:value].to_s) }
|
|
1107
|
-
when :
|
|
1113
|
+
when :float, :r8
|
|
1114
|
+
b.tag("vt:r8") { |_| b.text(prop[:value].to_s) }
|
|
1115
|
+
when :bool, :boolean
|
|
1108
1116
|
b.tag("vt:bool") { |_| b.text(prop[:value] ? "true" : "false") }
|
|
1109
|
-
when :date
|
|
1117
|
+
when :date, :filetime
|
|
1110
1118
|
b.tag("vt:filetime") { |_| b.text(prop[:value].to_s) }
|
|
1111
1119
|
else
|
|
1112
1120
|
b.tag("vt:lpwstr") { |_| b.text(prop[:value].to_s) }
|