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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 476e6f6e689eb65fecbbda4b81592c124b50f16fff0bbc9fec027a1f55ed4454
4
- data.tar.gz: 8364dc6bff9b5f9601dc4cb375ef27b0456b7f88e721b2b52d1db7ba342c7de5
3
+ metadata.gz: 0ec522a5f1d603d001d74555842823cfd56bf1fc96239e3e04daabadd345b5e9
4
+ data.tar.gz: b8cb5562079da225ceae806972851c9f0a1462083a1d8a05753f6896d511b6b5
5
5
  SHA512:
6
- metadata.gz: 31f365a17ad3f5976033ddc7a9220c5606c6d8f6acfca356ef1b3404cd1ed6d1f86d2c86d57a5e27daf15a46bea3d1c193f4fb60662b2eccbc7078fe41777bbb
7
- data.tar.gz: 67fe19a961f2bb204d19e5b9a49ade2c01e48de7d6a09ac1c87f7c591b570c0ba64494b43ce59d3ea492a70e18cf967d775af4fb55623287ab6b7b648bb0843c
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 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)).
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
- [👉 Try the Live Demo / Interactive Playground](https://niku.github.io/xlsxrb/docs/visual/VisualGallery_md.html)
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 50+ rendered visual examples across all features in the [Visual Examples Gallery](docs/visual/VisualGallery.md).
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,091/1,091 mutations across 38 subjects) on pure algorithms, predicates, and coordinates ([docs/MUTATION_TESTING.md](docs/MUTATION_TESTING.md)).
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
- #: (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, Hash[Symbol, 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]]) -> void
46
- 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: [])
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
- [self.class, name, rows, columns, charts, conditional_formatting, data_validations, state, hyperlinks, comments].hash
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
@@ -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
- # Strip quoted strings to avoid false matches.
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
- XmlParser.parse(xml_string, listener)
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
- auto_filter: nil, filter_columns: nil, sort_state: nil,
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
- freeze_pane: nil, split_pane: nil, selection: nil,
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 :bool
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) }