xlsxrb 0.1.0
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 +7 -0
- data/.devcontainer/Dockerfile +64 -0
- data/.devcontainer/devcontainer.json +17 -0
- data/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +384 -0
- data/Rakefile +290 -0
- data/benchmark.rb +390 -0
- data/docs/ARCHITECTURE.md +488 -0
- data/docs/SPEC_SOURCES.md +42 -0
- data/docs/visual/VisualGallery.md +4684 -0
- data/docs/wasm/wasm_doc_helper.css +189 -0
- data/docs/wasm/wasm_doc_helper.js +320 -0
- data/lib/xlsxrb/elements/cell.rb +77 -0
- data/lib/xlsxrb/elements/column.rb +28 -0
- data/lib/xlsxrb/elements/row.rb +47 -0
- data/lib/xlsxrb/elements/types.rb +36 -0
- data/lib/xlsxrb/elements/workbook.rb +47 -0
- data/lib/xlsxrb/elements/worksheet.rb +50 -0
- data/lib/xlsxrb/elements.rb +15 -0
- data/lib/xlsxrb/ooxml/reader.rb +8048 -0
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +75 -0
- data/lib/xlsxrb/ooxml/styles_parser.rb +194 -0
- data/lib/xlsxrb/ooxml/utils.rb +122 -0
- data/lib/xlsxrb/ooxml/workbook_parser.rb +78 -0
- data/lib/xlsxrb/ooxml/workbook_writer.rb +1000 -0
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +455 -0
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +1008 -0
- data/lib/xlsxrb/ooxml/writer.rb +5450 -0
- data/lib/xlsxrb/ooxml/xml_builder.rb +113 -0
- data/lib/xlsxrb/ooxml/xml_parser.rb +68 -0
- data/lib/xlsxrb/ooxml/zip_generator.rb +162 -0
- data/lib/xlsxrb/ooxml/zip_reader.rb +158 -0
- data/lib/xlsxrb/ooxml/zip_writer.rb +213 -0
- data/lib/xlsxrb/ooxml.rb +22 -0
- data/lib/xlsxrb/style_builder.rb +241 -0
- data/lib/xlsxrb/version.rb +5 -0
- data/lib/xlsxrb.rb +1427 -0
- data/measure_memory.rb +42 -0
- data/sig/xlsxrb.rbs +23 -0
- data/vendor/sdk_runner/Program.cs +91 -0
- data/vendor/sdk_runner/sdk_runner.csproj +13 -0
- metadata +144 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f2c0205aacdd92c016133af19ecd2cb6adfb9891e2e37fdbbdbc40eea1c7c6e1
|
|
4
|
+
data.tar.gz: eebd75e5893fb6d2e8435cfbeba2858098b3c99b3ed091ec73477c5e5482a884
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8dd84504dbe911d8a208e256648a2ff54209831e03cdc5dab185d1697c9283ad6e09dd0e144f9822c27eaba01445734bc76f86cda6f2f20cc3016a8060b769f2
|
|
7
|
+
data.tar.gz: 640e167013287ff3a27cfd75601d279bfe57d2fb212d3646ae5e50c21ff4b5e3e4f06bb09a3c583af08714f023f41cb5f1ec304f600a2b0cf37268e0f72f3e85
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
FROM ruby:4.0
|
|
2
|
+
|
|
3
|
+
ARG USERNAME=vscode
|
|
4
|
+
ARG USER_UID=1000
|
|
5
|
+
ARG USER_GID=$USER_UID
|
|
6
|
+
|
|
7
|
+
# Create a non-root user (vscode) with passwordless sudo privileges
|
|
8
|
+
# following Microsoft's Dev Container guidelines to prevent file permission conflicts.
|
|
9
|
+
RUN apt-get update && apt-get install -y sudo ca-certificates \
|
|
10
|
+
# Check if GID already exists, otherwise create it
|
|
11
|
+
&& if [ "$(getent group ${USER_GID})" ]; then \
|
|
12
|
+
echo "GID ${USER_GID} already exists."; \
|
|
13
|
+
else \
|
|
14
|
+
groupadd --gid ${USER_GID} ${USERNAME}; \
|
|
15
|
+
fi \
|
|
16
|
+
# Check if UID already exists, otherwise create user
|
|
17
|
+
&& if [ "$(getent passwd ${USER_UID})" ]; then \
|
|
18
|
+
echo "UID ${USER_UID} already exists."; \
|
|
19
|
+
else \
|
|
20
|
+
useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} -s /bin/bash; \
|
|
21
|
+
fi \
|
|
22
|
+
# Grant passwordless sudo permissions
|
|
23
|
+
&& echo "${USERNAME} ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/${USERNAME} \
|
|
24
|
+
&& chmod 0440 /etc/sudoers.d/${USERNAME} \
|
|
25
|
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
26
|
+
|
|
27
|
+
# Install .NET SDK for E2E testing with Open-XML-SDK.
|
|
28
|
+
# Use dotnet-install so the container works on both arm64 and amd64 hosts.
|
|
29
|
+
RUN apt-get update && apt-get install -y curl ca-certificates && \
|
|
30
|
+
curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh && \
|
|
31
|
+
bash /tmp/dotnet-install.sh --channel 8.0 --quality ga --install-dir /usr/share/dotnet && \
|
|
32
|
+
ln -s /usr/share/dotnet/dotnet /usr/bin/dotnet && \
|
|
33
|
+
rm -f /tmp/dotnet-install.sh && \
|
|
34
|
+
apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
35
|
+
|
|
36
|
+
# Install LibreOffice, poppler-utils, ImageMagick, and fonts for Visual Examples/VRT
|
|
37
|
+
RUN apt-get update && apt-get install -y \
|
|
38
|
+
libreoffice-calc \
|
|
39
|
+
poppler-utils \
|
|
40
|
+
imagemagick \
|
|
41
|
+
fonts-liberation \
|
|
42
|
+
fonts-noto-cjk \
|
|
43
|
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
44
|
+
|
|
45
|
+
# Pre-create the workspace folder and assign ownership to the vscode user
|
|
46
|
+
RUN mkdir -p /workspaces/xlsxrb && chown -R ${USERNAME}:${USERNAME} /workspaces
|
|
47
|
+
|
|
48
|
+
# Set up the workspace
|
|
49
|
+
USER ${USERNAME}
|
|
50
|
+
WORKDIR /workspaces/xlsxrb
|
|
51
|
+
|
|
52
|
+
# Pre-download DocumentFormat.OpenXml package to the NuGet cache
|
|
53
|
+
RUN mkdir /tmp/nuget_warmup && cd /tmp/nuget_warmup && \
|
|
54
|
+
dotnet new console && \
|
|
55
|
+
dotnet add package DocumentFormat.OpenXml --version 3.5.1 && \
|
|
56
|
+
cd / && rm -rf /tmp/nuget_warmup
|
|
57
|
+
|
|
58
|
+
# Install gems (Respect Gemfile.lock, maintain correct user ownership)
|
|
59
|
+
COPY --chown=vscode:vscode Gemfile xlsxrb.gemspec ./
|
|
60
|
+
COPY --chown=vscode:vscode lib/xlsxrb/version.rb ./lib/xlsxrb/
|
|
61
|
+
RUN bundle install
|
|
62
|
+
|
|
63
|
+
# Copy the remaining project application files
|
|
64
|
+
COPY --chown=vscode:vscode . .
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xlsxrb Development",
|
|
3
|
+
"build": {
|
|
4
|
+
"dockerfile": "Dockerfile",
|
|
5
|
+
"context": ".."
|
|
6
|
+
},
|
|
7
|
+
"workspaceFolder": "/workspaces/xlsxrb",
|
|
8
|
+
"customizations": {
|
|
9
|
+
"vscode": {
|
|
10
|
+
"extensions": [
|
|
11
|
+
"Shopify.ruby-lsp",
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"remoteUser": "vscode",
|
|
16
|
+
"postCreateCommand": "bundle install && bin/setup"
|
|
17
|
+
}
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
- Add specification reference policy and implemented specification mapping (`docs/SPEC_SOURCES.md`).
|
|
4
|
+
- Introduce unified event-based streaming and parsing architecture (`Ooxml::Event` and event streams for WorksheetParser/SharedStringsParser).
|
|
5
|
+
- Restructure test suite into four distinct tiers (Unit, Contract, E2E, Visual).
|
|
6
|
+
- Add visual examples gallery (`examples/visual/` and `docs/visual/README.md`).
|
|
7
|
+
- Implement visual regression testing (VRT) pipeline comparing generated sheets against reference baselines.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-25
|
|
11
|
+
|
|
12
|
+
- Initial release
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"xlsxrb" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["10890+niku@users.noreply.github.com"](mailto:"10890+niku@users.noreply.github.com").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 niku
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# Xlsxrb
|
|
2
|
+
|
|
3
|
+
A Ruby library for reading and writing XLSX files with streaming support.
|
|
4
|
+
|
|
5
|
+
## Motivation
|
|
6
|
+
|
|
7
|
+
The Ruby ecosystem already has great XLSX libraries. Each is well-designed for its purpose:
|
|
8
|
+
|
|
9
|
+
| Library | Read | Write | Streaming (low memory) |
|
|
10
|
+
| -------------------------------------------------- | ---- | ----- | ---------------------- |
|
|
11
|
+
| [roo](https://rubygems.org/gems/roo) | ✅ | ❌ | ✅ |
|
|
12
|
+
| [creek](https://rubygems.org/gems/creek) | ✅ | ❌ | ✅ |
|
|
13
|
+
| [xsv](https://rubygems.org/gems/xsv) | ✅ | ❌ | ✅ |
|
|
14
|
+
| [caxlsx / axlsx](https://rubygems.org/gems/caxlsx) | ❌ | ✅ | ❌ |
|
|
15
|
+
| [xlsxtream](https://rubygems.org/gems/xlsxtream) | ❌ | ✅ | ✅ |
|
|
16
|
+
| [rubyXL](https://rubygems.org/gems/rubyXL) | ✅ | ✅ | ❌ |
|
|
17
|
+
| [fast_excel](https://rubygems.org/gems/fast_excel) | ❌ | ✅ | ✅ |
|
|
18
|
+
|
|
19
|
+
Each of these libraries makes deliberate tradeoffs, and they do so thoughtfully. Some focus exclusively on highly efficient reading or writing by streaming data, while others provide a rich API for complex, in-memory document modifications.
|
|
20
|
+
|
|
21
|
+
`xlsxrb` is for cases where you need **both** reading and writing in a single library, while also keeping memory usage predictable for large files.
|
|
22
|
+
|
|
23
|
+
### Design Principles
|
|
24
|
+
|
|
25
|
+
- **Minimal Dependencies (Zero Core Logic Dependencies):** This library avoids heavy third-party XLSX/XML/ZIP gems, building all core parsing and writing features purely on the Ruby standard library and bundled gems (`zlib`, `rexml`, etc.). The only runtime dependency is `opentelemetry-api`, which provides zero-overhead observability. If you do not configure an OpenTelemetry SDK in your application, it acts as a lightweight no-op, keeping the runtime footprint extremely small.
|
|
26
|
+
- **Streaming Support:** Both reading and writing are designed to handle large files efficiently by streaming data, keeping memory usage low and predictable.
|
|
27
|
+
- **Memory-Efficient XML Parsing:** For reading operations, the library uses a custom byte-level streaming parser for worksheet rows (with targeted SAX parsing where appropriate) instead of DOM-based parsing, so entire XML documents are never loaded into memory. This enables true streaming capability for large spreadsheets.
|
|
28
|
+
- **Modern Ruby 4.0+:** Built for the future with Ruby 4.0 or higher.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle add xlsxrb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or without Bundler:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
gem install xlsxrb
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
On Ruby 4+, some components used by `xlsxrb` and its test suite are shipped as bundled gems rather than built-in default libraries. When using Bundler, those bundled gems are resolved and installed in the usual way.
|
|
43
|
+
|
|
44
|
+
## Interactive Playground (WebAssembly)
|
|
45
|
+
|
|
46
|
+
You can try `xlsxrb` directly in your browser without installing anything!
|
|
47
|
+
|
|
48
|
+
We have integrated an interactive WebAssembly-powered playground into our RDoc documentation. You can edit the code examples, run them in the browser sandbox, and download the generated `.xlsx` spreadsheets immediately.
|
|
49
|
+
|
|
50
|
+
To launch the playground locally:
|
|
51
|
+
1. Generate the WebAssembly bundle and interactive RDoc:
|
|
52
|
+
```bash
|
|
53
|
+
bundle exec rake doc
|
|
54
|
+
```
|
|
55
|
+
2. Start the local preview server:
|
|
56
|
+
```bash
|
|
57
|
+
bundle exec rake doc:preview
|
|
58
|
+
```
|
|
59
|
+
3. Open [http://localhost:8000](http://localhost:8000) in your browser and click the **"Try it in Browser"** button on any code example!
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
For visual demonstrations of various features and their generated output side-by-side with code examples, check the [Visual Examples Gallery](docs/visual/VisualGallery.md).
|
|
64
|
+
|
|
65
|
+
`xlsxrb` offers two different approaches to reading and writing XLSX files: **Streaming** and **In-Memory**.
|
|
66
|
+
|
|
67
|
+
In most cases, the **Streaming** approach is the best choice because it is highly memory efficient, avoiding loading entire files or structures into RAM. You should always try the Streaming approach first.
|
|
68
|
+
|
|
69
|
+
However, if your use case requires **Random Access** (e.g., reading a cell at `Z100`, then returning to `A1`) or you need to build or modify an entire document iteratively before writing, the **In-Memory** approach is required.
|
|
70
|
+
|
|
71
|
+
### 1. Streaming (Recommended)
|
|
72
|
+
|
|
73
|
+
#### Streaming Read
|
|
74
|
+
|
|
75
|
+
Read rows one at a time without loading the entire file into memory:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
require "xlsxrb"
|
|
79
|
+
|
|
80
|
+
# Yields Xlsxrb::Elements::Row objects for the first sheet
|
|
81
|
+
Xlsxrb.foreach("large_file.xlsx", sheet: 0) do |row|
|
|
82
|
+
puts "Row #{row.index}: #{row.cells.map(&:value).join(', ')}"
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Streaming Write
|
|
87
|
+
|
|
88
|
+
Generate large files efficiently by writing data directly to the file stream:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
require "xlsxrb"
|
|
92
|
+
|
|
93
|
+
Xlsxrb.generate("large_output.xlsx") do |writer|
|
|
94
|
+
writer.add_sheet("Sales Data") do
|
|
95
|
+
writer.add_row(["Date", "Amount", "Status"])
|
|
96
|
+
writer.add_row([Date.today, 100, true])
|
|
97
|
+
writer.set_column(0, width: 15.5)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
_For more advanced streaming features (such as adding charts, sparklines, or complex styling during stream generation), see the [Visual Examples Gallery](docs/visual/VisualGallery.md)._
|
|
103
|
+
|
|
104
|
+
### 2. In-Memory
|
|
105
|
+
|
|
106
|
+
#### Reading into memory
|
|
107
|
+
|
|
108
|
+
Parse the entire workbook for random access by cell reference:
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
require "xlsxrb"
|
|
112
|
+
|
|
113
|
+
workbook = Xlsxrb.read("example.xlsx")
|
|
114
|
+
sheet = workbook.sheets.first
|
|
115
|
+
|
|
116
|
+
# Random access by cell reference
|
|
117
|
+
puts "Value at C10: #{sheet.cell_value("C10")}"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Writing from memory
|
|
121
|
+
|
|
122
|
+
Construct a workbook hierarchy in memory using the DSL and write it:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
require "xlsxrb"
|
|
126
|
+
|
|
127
|
+
workbook = Xlsxrb.build do |w|
|
|
128
|
+
w.add_sheet("My Sheet") do |s|
|
|
129
|
+
s.add_row(["Hello", "World"])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
Xlsxrb.write("output.xlsx", workbook)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### Modifying an existing workbook
|
|
137
|
+
|
|
138
|
+
Read an existing file, make changes, and write it back:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
require "xlsxrb"
|
|
142
|
+
|
|
143
|
+
Xlsxrb.modify("template.xlsx", "output.xlsx") do |wb|
|
|
144
|
+
sheet = wb.sheet(0)
|
|
145
|
+
row0 = sheet.row_at(0)
|
|
146
|
+
|
|
147
|
+
new_cell = Xlsxrb::Elements::Cell.new(row_index: 0, column_index: 1, value: "Updated")
|
|
148
|
+
new_row = row0.with(cells: row0.cells.map { |c| c.column_index == 1 ? new_cell : c })
|
|
149
|
+
new_sheet = sheet.with(rows: sheet.rows.map { |r| r.index == 0 ? new_row : r })
|
|
150
|
+
|
|
151
|
+
wb.with(sheets: wb.sheets.map.with_index { |s, i| i == 0 ? new_sheet : s })
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
_For details on adding charts or shapes in-memory, see the [Visual Examples Gallery](docs/visual/VisualGallery.md)._
|
|
156
|
+
|
|
157
|
+
## Specification
|
|
158
|
+
|
|
159
|
+
## Sheet Features
|
|
160
|
+
|
|
161
|
+
All sheet-level features work identically in both the streaming (`Xlsxrb.generate`) and in-memory (`Xlsxrb.build`) APIs. Detailed code examples, runnable scripts, and side-by-side visual rendering results (screenshots) for all sheet-level features are available in the [Visual Gallery (docs/visual/VisualGallery.md)](docs/visual/VisualGallery.md).
|
|
162
|
+
|
|
163
|
+
- **[Formulas](docs/visual/VisualGallery.md#cell-formulas)** - Write standard or Excel formulas with automatic evaluation.
|
|
164
|
+
- **[Hyperlinks](docs/visual/VisualGallery.md#basic-data)** - Attach web URLs or jump to internal worksheet cell locations.
|
|
165
|
+
- **[Auto Filter](docs/visual/VisualGallery.md#interactive-autofilter)** - Add interactive filter drop-down menus to column ranges.
|
|
166
|
+
- **[Data Validation](docs/visual/VisualGallery.md#interactive-validation-list)** - Restrict allowed cell values (lists, range limits, dates, times, custom expressions) with custom warning popups.
|
|
167
|
+
- **[Conditional Formatting](docs/visual/VisualGallery.md#cf-begins-with)** - Highlight cells automatically using rule comparisons, color scales, data bars, icon sets, or formula expressions.
|
|
168
|
+
- **[Tables](docs/visual/VisualGallery.md#borders)** - Wrap cell ranges in structured tables with column headers and preset visual styles.
|
|
169
|
+
- **[Pivot Tables](docs/visual/VisualGallery.md#pivot-tables)** - Summarize data ranges dynamically with row/column grouping and subtotals.
|
|
170
|
+
- **[Comments](docs/visual/VisualGallery.md#interactive-comments)** - Attach hover-activated popup notes to specific cells.
|
|
171
|
+
- **[Merge Cells](docs/visual/VisualGallery.md#merge-freeze)** - Combine rectangular cell ranges into single styled cells.
|
|
172
|
+
- **[Freeze & Split Panes](docs/visual/VisualGallery.md#merge-freeze)** - Freeze top rows or left columns (or split viewport by pixel offsets) to keep headers visible.
|
|
173
|
+
- **[Page margins & Print Setup](docs/visual/VisualGallery.md#page-setup)** - Set portrait/landscape orientation, margins, paper sizes, scaling (fit-to-page), and odd/even headers & footers.
|
|
174
|
+
- **[Print Options](docs/visual/VisualGallery.md#page-grid-lines-print)** - Toggle gridlines, row/column headings visibility when printing, and centering.
|
|
175
|
+
- **[Sheet Protection](docs/visual/VisualGallery.md#sheet-protection)** - Lock sheet structures, formatting, or objects (optionally with SHA-512 hashed passwords).
|
|
176
|
+
- **[Page Breaks](docs/visual/VisualGallery.md#page-setup)** - Insert manual horizontal or vertical page breaks.
|
|
177
|
+
- **[Images](docs/visual/VisualGallery.md#embedded-images)** - Embed and anchor floating images (PNG, JPEG) with custom dimensions.
|
|
178
|
+
- **[Sparklines](docs/visual/VisualGallery.md#sparkline-column)** - Insert inline column or line trend charts into single cells.
|
|
179
|
+
- **[Shapes & Drawings](docs/visual/VisualGallery.md#shapes)** - Draw rectangles, ellipses, and other presets with solid fills, lines, and custom text.
|
|
180
|
+
- **[Sheet Properties and View Settings](docs/visual/VisualGallery.md#sheet-tab-colors)** - Customize per-sheet properties (tab colors, hide gridlines, default zoom levels).
|
|
181
|
+
|
|
182
|
+
## Workbook Features
|
|
183
|
+
|
|
184
|
+
Workbook-level methods are called directly on the writer/builder object (not inside `add_sheet`).
|
|
185
|
+
|
|
186
|
+
### Defined Names, Print Area, and Print Titles
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
Xlsxrb.generate("named.xlsx") do |w|
|
|
190
|
+
w.add_sheet("Data") do |s|
|
|
191
|
+
s.add_row(["Name", "Score"])
|
|
192
|
+
s.add_row(["Alice", 95])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# General named range
|
|
196
|
+
w.add_defined_name("TaxRate", "0.1", hidden: false)
|
|
197
|
+
|
|
198
|
+
# Print area for the sheet named "Data"
|
|
199
|
+
w.set_print_area("A1:B10", sheet: "Data")
|
|
200
|
+
|
|
201
|
+
# Repeat first row and first column when printing
|
|
202
|
+
w.set_print_titles(rows: "1:1", cols: "A:A", sheet: "Data")
|
|
203
|
+
end
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Workbook Protection
|
|
207
|
+
|
|
208
|
+
Lock the workbook structure (prevents adding/removing/renaming sheets).
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
Xlsxrb.generate("locked_workbook.xlsx") do |w|
|
|
212
|
+
w.add_sheet("Sheet1") { |s| s.add_row(["Data"]) }
|
|
213
|
+
|
|
214
|
+
w.set_workbook_protection(lock_structure: true, lock_windows: false)
|
|
215
|
+
end
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Document Properties (Core, App, and Custom)
|
|
219
|
+
|
|
220
|
+
Embed metadata into the XLSX file's document properties panels.
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
Xlsxrb.generate("with_props.xlsx") do |w|
|
|
224
|
+
w.add_sheet("Sheet1") { |s| s.add_row(["Hello"]) }
|
|
225
|
+
|
|
226
|
+
# Core properties (Dublin Core — visible in File > Info)
|
|
227
|
+
w.set_core_property(:title, "Quarterly Report")
|
|
228
|
+
w.set_core_property(:subject, "Sales data Q1 2026")
|
|
229
|
+
w.set_core_property(:creator, "Alice")
|
|
230
|
+
w.set_core_property(:keywords, "sales, report, 2026")
|
|
231
|
+
w.set_core_property(:description, "Auto-generated by xlsxrb")
|
|
232
|
+
|
|
233
|
+
# App properties (application-level metadata)
|
|
234
|
+
w.set_app_property(:application, "MyApp/2.0")
|
|
235
|
+
w.set_app_property(:company, "Acme Corp")
|
|
236
|
+
|
|
237
|
+
# Custom properties (arbitrary key/value pairs)
|
|
238
|
+
w.add_custom_property("ReportVersion", "42", type: :integer)
|
|
239
|
+
w.add_custom_property("ApprovedBy", "Bob", type: :string)
|
|
240
|
+
w.add_custom_property("Published", true, type: :bool)
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
This project aims to be compliant with [ECMA-376](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/) (Office Open XML file formats). Specifically, the library targets the **Transitional** version of the specification rather than the **Strict** version. The Transitional version (detailed in Part 4) is the format most commonly produced and consumed by existing spreadsheet applications, making it the practical choice for real-world interoperability.
|
|
245
|
+
|
|
246
|
+
For detailed specification references and policies, see [SPEC_SOURCES.md](docs/SPEC_SOURCES.md).
|
|
247
|
+
|
|
248
|
+
## Benchmarks
|
|
249
|
+
|
|
250
|
+
The following benchmarks measure the time and memory required to process both 100,000 cells (10,000 rows × 10 columns) and 1,000,000 cells (100,000 rows × 10 columns) XLSX files, averaged over 5 iterations on Ruby 3.4+.
|
|
251
|
+
|
|
252
|
+
### Write Performance (100,000 cells)
|
|
253
|
+
|
|
254
|
+
| Library | Time | Peak Memory | GC Count |
|
|
255
|
+
| ---------------------- | ------ | ----------- | -------- |
|
|
256
|
+
| xlsxtream (Streaming) | 0.15 s | 65.0 MB | 4.4 |
|
|
257
|
+
| fast_excel (Streaming) | 0.28 s | 64.4 MB | 2.0 |
|
|
258
|
+
| xlsxrb (Streaming) | 0.34 s | 64.4 MB | 19.0 |
|
|
259
|
+
| caxlsx (In-Memory) | 1.19 s | 73.8 MB | 5.0 |
|
|
260
|
+
| xlsxrb (In-Memory) | 3.06 s | 132.4 MB | 34.0 |
|
|
261
|
+
| rubyXL (In-Memory) | 5.45 s | 274.8 MB | 33.0 |
|
|
262
|
+
|
|
263
|
+
### Read Performance (100,000 cells)
|
|
264
|
+
|
|
265
|
+
| Library | Time | Peak Memory | GC Count |
|
|
266
|
+
| ------------------ | ------ | ----------- | -------- |
|
|
267
|
+
| xlsxrb (Streaming) | 1.02 s | 67.4 MB | 192.0 |
|
|
268
|
+
| creek (Streaming) | 1.32 s | 160.4 MB | 396.8 |
|
|
269
|
+
| roo (Streaming) | 1.33 s | 87.4 MB | 31.0 |
|
|
270
|
+
| xlsxrb (In-Memory) | 1.82 s | 153.9 MB | 20.0 |
|
|
271
|
+
| xsv (Streaming) | 3.51 s | 92.1 MB | 98.0 |
|
|
272
|
+
| rubyXL (In-Memory) | 4.95 s | 265.9 MB | 38.0 |
|
|
273
|
+
|
|
274
|
+
### Write Performance (1,000,000 cells)
|
|
275
|
+
|
|
276
|
+
| Library | Time | Peak Memory | GC Count |
|
|
277
|
+
| ---------------------- | ------- | ----------- | -------- |
|
|
278
|
+
| xlsxtream (Streaming) | 0.11 s | 65.1 MB | 4.2 |
|
|
279
|
+
| fast_excel (Streaming) | 2.26 s | 64.4 MB | 28.0 |
|
|
280
|
+
| xlsxrb (Streaming) | 3.82 s | 79.0 MB | 199.0 |
|
|
281
|
+
| caxlsx (In-Memory) | 4.52 s | 142.9 MB | 15.6 |
|
|
282
|
+
| xlsxrb (In-Memory) | 11.61 s | 454.5 MB | 61.4 |
|
|
283
|
+
| rubyXL (In-Memory) | 86.63 s | 2072.0 MB | 91.4 |
|
|
284
|
+
|
|
285
|
+
### Read Performance (1,000,000 cells)
|
|
286
|
+
|
|
287
|
+
| Library | Time | Peak Memory | GC Count |
|
|
288
|
+
| ------------------ | ------- | ----------- | -------- |
|
|
289
|
+
| xlsxrb (Streaming) | 11.05 s | 102.9 MB | 582.0 |
|
|
290
|
+
| roo (Streaming) | 13.20 s | 127.9 MB | 233.4 |
|
|
291
|
+
| xsv (Streaming) | 15.35 s | 94.3 MB | 984.4 |
|
|
292
|
+
| creek (Streaming) | 15.69 s | 706.5 MB | 3974.6 |
|
|
293
|
+
| xlsxrb (In-Memory) | 25.23 s | 991.7 MB | 41.4 |
|
|
294
|
+
| rubyXL (In-Memory) | 56.26 s | 1858.3 MB | 127.0 |
|
|
295
|
+
|
|
296
|
+
For reference, the following specification files from the Ecma International website are located in the `vendor/docs/` directory:
|
|
297
|
+
|
|
298
|
+
- `vendor/docs/ECMA-376-Part1/Ecma Office Open XML Part 1 - Fundamentals And Markup Language Reference.pdf`: Part 1 - Fundamentals And Markup Language Reference
|
|
299
|
+
- `vendor/docs/ECMA-376-Part2/ECMA-376-2_5th_edition_december_2021.pdf`: Part 2 - Open Packaging Conventions
|
|
300
|
+
- `vendor/docs/ECMA-376-Part3/ECMA-376-3_5th_edition_december_2015.pdf`: Part 3 - Markup Compatibility and Extensibility
|
|
301
|
+
- `vendor/docs/ECMA-376-Part4/Ecma Office Open XML Part 4 - Transitional Migration Features.pdf`: Part 4 - Transitional Migration Features
|
|
302
|
+
|
|
303
|
+
## Testing Strategy
|
|
304
|
+
|
|
305
|
+
To ensure high quality and strict compliance with the ECMA-376 specification while maintaining a fast development loop, we employ a 4-tier testing strategy:
|
|
306
|
+
|
|
307
|
+
1. **Unit Tests (Fast & No Dependencies):**
|
|
308
|
+
- Verify individual components (Writer, Reader, Packaging) using only the Ruby standard library and bundled gems.
|
|
309
|
+
- **Round-Trip Testing:** Ensure that files generated by the `xlsxrb` Writer can be seamlessly and accurately parsed back by the `xlsxrb` Reader.
|
|
310
|
+
- Run via: `bundle exec rake test:unit`
|
|
311
|
+
|
|
312
|
+
2. **Contract Tests (API Consistency):**
|
|
313
|
+
- Execute the same data scenario through both the Streaming (`Xlsxrb.generate`) and In-Memory (`Xlsxrb.build` + `Xlsxrb.write`) APIs.
|
|
314
|
+
- Verify that both APIs produce semantically identical OOXML output.
|
|
315
|
+
- Run via: `bundle exec rake test:contract`
|
|
316
|
+
|
|
317
|
+
3. **Interoperability Testing (E2E):**
|
|
318
|
+
- Utilize the official **[Open XML SDK](https://github.com/dotnet/Open-XML-SDK)** for robust, two-way verification:
|
|
319
|
+
- **Writer Validation:** Files generated by `xlsxrb` are read and validated using the Open XML SDK validator.
|
|
320
|
+
- **Reader Validation:** Complex, real-world XLSX files generated by the SDK are parsed and verified by the `xlsxrb` reader.
|
|
321
|
+
- Requires `.NET SDK`.
|
|
322
|
+
- Run via: `bundle exec rake test:e2e`
|
|
323
|
+
|
|
324
|
+
4. **Visual Examples & VRT (Visual Regression Testing):**
|
|
325
|
+
- **Visual Gallery:** Automatically compiles example DSL scripts under `examples/visual/` into the [Visual Examples Gallery](docs/visual/VisualGallery.md).
|
|
326
|
+
- **Visual Regression Testing:** Renders generated spreadsheets to PNGs using a headless LibreOffice Calc engine, and compares them against baselines to catch rendering errors or layout regressions.
|
|
327
|
+
- Requires `libreoffice-calc`, `poppler-utils` (`pdftoppm`), and `imagemagick`.
|
|
328
|
+
- Run via: `bundle exec rake test:visual`
|
|
329
|
+
|
|
330
|
+
**Note on Test Environments:**
|
|
331
|
+
All necessary dependencies (.NET SDK, LibreOffice, etc.) are pre-configured in the repository's **Dev Container** setup for a consistent local and CI experience.
|
|
332
|
+
|
|
333
|
+
## Development
|
|
334
|
+
|
|
335
|
+
This project is designed to be developed using [Dev Containers](https://containers.dev/). The provided `.devcontainer` configuration includes all necessary tools (Ruby 4.0, and the .NET SDK for E2E testing) to ensure a consistent development environment.
|
|
336
|
+
|
|
337
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
338
|
+
|
|
339
|
+
### Development Workflow
|
|
340
|
+
|
|
341
|
+
High-level API expansion follows the Facade rules documented in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). In short: if a low-level writer feature is stable, the default expectation is that it should eventually be exposed through the high-level DSL as well, with consistent naming, both streaming and in-memory coverage, backward-compatible options/block forms where practical, and matching Facade-level tests.
|
|
342
|
+
|
|
343
|
+
To ensure systematic progress, perfect round-trip compatibility, and strict adherence to the ECMA-376 specification, we follow this iterative development cycle for each new feature:
|
|
344
|
+
|
|
345
|
+
1. **Select a Feature:** Choose a specific element or behavior from the specification to implement.
|
|
346
|
+
2. **Writer Unit Tests:** Write unit tests for the Writer component targeting this feature.
|
|
347
|
+
3. **Writer Implementation:** Implement the Writer functionality.
|
|
348
|
+
4. **Run Writer Tests:** Execute the Writer unit tests. If they fail, return to step 3.
|
|
349
|
+
5. **Writer E2E & Validation:** Test the Writer's generated XLSX file using the Open XML SDK. This includes structural validation using `OpenXmlValidator`. If the test or validation fails, return to step 2.
|
|
350
|
+
6. **Reader Unit Tests:** Write unit tests for the Reader component. **Crucially, include round-trip tests** to ensure the Reader can perfectly parse the output of your Writer.
|
|
351
|
+
7. **Reader Implementation:** Implement the Reader functionality.
|
|
352
|
+
8. **Run Reader Tests:** Execute the Reader unit tests. If they fail, return to step 6 or 7. If the round-trip test reveals a structural flaw in the Writer's output, return all the way back to step 2.
|
|
353
|
+
9. **Reader E2E:** Verify that the Reader can successfully parse a valid XLSX file generated by the Open XML SDK that includes the new feature. If it fails, return to step 6 or 7.
|
|
354
|
+
10. **Full Test Suite:** Run the entire test suite (`rake test`). If any tests fail, trace back to the appropriate step.
|
|
355
|
+
11. **Commit:** Commit the changes. The commit message must clearly describe the specific feature implemented in this cycle.
|
|
356
|
+
12. **Next Feature:** Proceed to the next feature and return to step 1.
|
|
357
|
+
|
|
358
|
+
#### E2E Policy
|
|
359
|
+
|
|
360
|
+
E2E tests are required for every new feature. Omitting them is the exception, not the rule, and requires explicit justification.
|
|
361
|
+
|
|
362
|
+
A strong signal that E2E should not be omitted: if you are adding a new XML element, a new attribute on a top-level structure, or a new public API parameter, E2E is expected.
|
|
363
|
+
|
|
364
|
+
Omission is only acceptable when **all** of the following hold:
|
|
365
|
+
|
|
366
|
+
1. The change adds a minor attribute to an XML structure that is **already exercised end-to-end** by an existing E2E scenario for the same element.
|
|
367
|
+
2. No new XML element or branch is introduced.
|
|
368
|
+
3. Unit tests and round-trip tests fully cover the new behaviour.
|
|
369
|
+
4. `rake test` passes with Open XML SDK validation included.
|
|
370
|
+
5. The commit message explicitly names the existing E2E scenario that provides coverage and states why a new scenario adds no value.
|
|
371
|
+
|
|
372
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
373
|
+
|
|
374
|
+
## Contributing
|
|
375
|
+
|
|
376
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/niku/xlsxrb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/niku/xlsxrb/blob/main/CODE_OF_CONDUCT.md).
|
|
377
|
+
|
|
378
|
+
## License
|
|
379
|
+
|
|
380
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
381
|
+
|
|
382
|
+
## Code of Conduct
|
|
383
|
+
|
|
384
|
+
Everyone interacting in the Xlsxrb project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/niku/xlsxrb/blob/main/CODE_OF_CONDUCT.md).
|