xlsxrb 0.1.2 → 0.1.3
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/README.md +57 -264
- data/Rakefile +137 -5
- data/benchmark.rb +23 -14
- data/docs/DEVELOPMENT.md +67 -0
- data/docs/coi-serviceworker.js +82 -0
- data/docs/office_thread.js +77 -0
- data/docs/preview.html +719 -0
- data/docs/visual/VisualGallery.md +0 -158
- data/docs/wasm/wasm_doc_helper.css +256 -94
- data/docs/wasm/wasm_doc_helper.js +261 -142
- data/docs/zeta.js +1107 -0
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +178 -24
- data/lib/xlsxrb/ooxml.rb +1 -1
- data/lib/xlsxrb/version.rb +1 -1
- metadata +6 -1
data/benchmark.rb
CHANGED
|
@@ -192,6 +192,13 @@ def run_in_subprocess(_name, &block)
|
|
|
192
192
|
end
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
+
def median(array)
|
|
196
|
+
return 0.0 if array.empty?
|
|
197
|
+
sorted = array.sort
|
|
198
|
+
len = sorted.length
|
|
199
|
+
(sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
|
|
200
|
+
end
|
|
201
|
+
|
|
195
202
|
def run_benchmark(name, snippet)
|
|
196
203
|
print format("%-25s", name)
|
|
197
204
|
results = ITERATIONS.times.map do
|
|
@@ -200,23 +207,25 @@ def run_benchmark(name, snippet)
|
|
|
200
207
|
end
|
|
201
208
|
puts
|
|
202
209
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
+
valid_results = results.compact
|
|
211
|
+
|
|
212
|
+
median_time = median(valid_results.map { |r| r[:time] }.compact)
|
|
213
|
+
median_cpu = median(valid_results.map { |r| r[:cpu] }.compact)
|
|
214
|
+
median_mem = median(valid_results.map { |r| r[:memory] }.compact)
|
|
215
|
+
median_gc = median(valid_results.map { |r| r[:gc_count] }.compact)
|
|
216
|
+
median_alloc = median(valid_results.map { |r| r[:alloc_objects] }.compact)
|
|
217
|
+
median_wchar = median(valid_results.map { |r| r[:wchar] }.compact)
|
|
218
|
+
median_rchar = median(valid_results.map { |r| r[:rchar] }.compact)
|
|
210
219
|
|
|
211
220
|
{
|
|
212
221
|
name: name,
|
|
213
|
-
time:
|
|
214
|
-
cpu:
|
|
215
|
-
memory:
|
|
216
|
-
gc_count:
|
|
217
|
-
alloc_m:
|
|
218
|
-
wchar_mb:
|
|
219
|
-
rchar_mb:
|
|
222
|
+
time: median_time,
|
|
223
|
+
cpu: median_cpu,
|
|
224
|
+
memory: median_mem,
|
|
225
|
+
gc_count: median_gc,
|
|
226
|
+
alloc_m: median_alloc / 1_000_000.0,
|
|
227
|
+
wchar_mb: median_wchar / 1_048_576.0,
|
|
228
|
+
rchar_mb: median_rchar / 1_048_576.0
|
|
220
229
|
}
|
|
221
230
|
end
|
|
222
231
|
|
data/docs/DEVELOPMENT.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Development & Contribution Guide
|
|
2
|
+
|
|
3
|
+
This document outlines the internal development workflow, test commands, and E2E testing policies for contributors working on `xlsxrb`.
|
|
4
|
+
|
|
5
|
+
## Test Commands
|
|
6
|
+
|
|
7
|
+
To run the different tiers of our testing strategy:
|
|
8
|
+
|
|
9
|
+
1. **Unit Tests:**
|
|
10
|
+
```bash
|
|
11
|
+
bundle exec rake test:unit
|
|
12
|
+
```
|
|
13
|
+
2. **Contract Tests:**
|
|
14
|
+
```bash
|
|
15
|
+
bundle exec rake test:contract
|
|
16
|
+
```
|
|
17
|
+
3. **Interoperability (E2E) Tests:**
|
|
18
|
+
Requires .NET SDK to be installed (pre-configured in Dev Container).
|
|
19
|
+
```bash
|
|
20
|
+
bundle exec rake test:e2e
|
|
21
|
+
```
|
|
22
|
+
4. **Visual Regression Tests (VRT):**
|
|
23
|
+
Requires LibreOffice, ImageMagick, and `poppler-utils`.
|
|
24
|
+
```bash
|
|
25
|
+
bundle exec rake test:visual
|
|
26
|
+
```
|
|
27
|
+
5. **Run All Tests:**
|
|
28
|
+
```bash
|
|
29
|
+
bundle exec rake test
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Development Workflow
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
To ensure systematic progress, reliable round-trip compatibility, and strict adherence to the ECMA-376 specification, we follow this iterative development cycle for each new feature:
|
|
39
|
+
|
|
40
|
+
1. **Select a Feature:** Choose a specific element or behavior from the specification to implement.
|
|
41
|
+
2. **Writer Unit Tests:** Write unit tests for the Writer component targeting this feature.
|
|
42
|
+
3. **Writer Implementation:** Implement the Writer functionality.
|
|
43
|
+
4. **Run Writer Tests:** Execute the Writer unit tests. If they fail, return to step 3.
|
|
44
|
+
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.
|
|
45
|
+
6. **Reader Unit Tests:** Write unit tests for the Reader component. Crucially, include round-trip tests to ensure the Reader can accurately parse the output of your Writer.
|
|
46
|
+
7. **Reader Implementation:** Implement the Reader functionality.
|
|
47
|
+
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.
|
|
48
|
+
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.
|
|
49
|
+
10. **Full Test Suite:** Run the entire test suite (`rake test`). If any tests fail, trace back to the appropriate step.
|
|
50
|
+
11. **Commit:** Commit the changes. The commit message must clearly describe the specific feature implemented in this cycle.
|
|
51
|
+
12. **Next Feature:** Proceed to the next feature and return to step 1.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## E2E Policy
|
|
56
|
+
|
|
57
|
+
E2E tests are required for every new feature. Omitting them is the exception, not the rule, and requires explicit justification.
|
|
58
|
+
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
Omission is only acceptable when **all** of the following hold:
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
2. No new XML element or branch is introduced.
|
|
65
|
+
3. Unit tests and round-trip tests fully cover the new behaviour.
|
|
66
|
+
4. `rake test` passes with Open XML SDK validation included.
|
|
67
|
+
5. The commit message explicitly names the existing E2E scenario that provides coverage and states why a new scenario adds no value.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
|
|
2
|
+
// Source: https://github.com/gzguidoti/coi-serviceworker
|
|
3
|
+
// Purpose: Allows running WebAssembly with SharedArrayBuffer on browsers by setting COOP and COEP headers via Service Worker.
|
|
4
|
+
let coepCredentialless = false;
|
|
5
|
+
if (typeof window === 'undefined') {
|
|
6
|
+
self.addEventListener("install", () => self.skipWaiting());
|
|
7
|
+
self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
|
|
8
|
+
|
|
9
|
+
self.addEventListener("message", (ev) => {
|
|
10
|
+
if (!ev.data) {
|
|
11
|
+
return;
|
|
12
|
+
} else if (ev.data.type === "deregister") {
|
|
13
|
+
self.registration
|
|
14
|
+
.unregister()
|
|
15
|
+
.then(() => {
|
|
16
|
+
return self.clients.matchAll();
|
|
17
|
+
})
|
|
18
|
+
.then(clients => {
|
|
19
|
+
clients.forEach((client) => client.navigate(client.url));
|
|
20
|
+
});
|
|
21
|
+
} else if (ev.data.type === "coepCredentialless") {
|
|
22
|
+
coepCredentialless = ev.data.value;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
self.addEventListener("fetch", function (event) {
|
|
27
|
+
const r = event.request;
|
|
28
|
+
if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const request = (coepCredentialless && r.mode === "no-cors")
|
|
33
|
+
? new Request(r, { credentials: "omit" })
|
|
34
|
+
: r;
|
|
35
|
+
event.respondWith(
|
|
36
|
+
fetch(request)
|
|
37
|
+
.then((response) => {
|
|
38
|
+
if (response.status === 0) {
|
|
39
|
+
return response;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const newHeaders = new Headers(response.headers);
|
|
43
|
+
newHeaders.set("Cross-Origin-Embedder-Policy",
|
|
44
|
+
coepCredentialless ? "credentialless" : "require-corp"
|
|
45
|
+
);
|
|
46
|
+
if (!coepCredentialless) {
|
|
47
|
+
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
|
|
48
|
+
}
|
|
49
|
+
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
|
50
|
+
|
|
51
|
+
return new Response(response.body, {
|
|
52
|
+
status: response.status,
|
|
53
|
+
statusText: response.statusText,
|
|
54
|
+
headers: newHeaders,
|
|
55
|
+
});
|
|
56
|
+
})
|
|
57
|
+
.catch((e) => console.error(e))
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
(() => {
|
|
62
|
+
const reloaded = sessionStorage.getItem("coiReloaded");
|
|
63
|
+
const isSecureContext = window.isSecureContext;
|
|
64
|
+
if (!isSecureContext) return;
|
|
65
|
+
|
|
66
|
+
if (navigator.serviceWorker) {
|
|
67
|
+
navigator.serviceWorker.register(window.document.currentScript.src).then(
|
|
68
|
+
(registration) => {
|
|
69
|
+
registration.addEventListener("updatefound", () => {
|
|
70
|
+
sessionStorage.setItem("coiReloaded", "true");
|
|
71
|
+
window.location.reload();
|
|
72
|
+
});
|
|
73
|
+
if (registration.active && !navigator.serviceWorker.controller) {
|
|
74
|
+
sessionStorage.setItem("coiReloaded", "true");
|
|
75
|
+
window.location.reload();
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
(err) => console.error("COI registration failed: ", err)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* -*- Mode: JS; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2; fill-column: 100 -*- */
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Source: Derived from ZetaOffice WebAssembly example (https://github.com/zetaoffice/zeta-wasm-examples)
|
|
4
|
+
// Purpose: Runs WebAssembly-based LibreOffice (Calc) in a separate Web Worker thread.
|
|
5
|
+
|
|
6
|
+
// Debugging note:
|
|
7
|
+
// Switch the web worker in the browsers debug tab to debug this code.
|
|
8
|
+
// It's the "em-pthread" web worker with the most memory usage, where "zetajs" is defined.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// global variables - zetajs environment:
|
|
14
|
+
let zetajs, css;
|
|
15
|
+
|
|
16
|
+
// = global variables (some are global for easier debugging) =
|
|
17
|
+
// common variables:
|
|
18
|
+
let context, desktop, xModel, ctrl;
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
function demo() {
|
|
22
|
+
context = zetajs.getUnoComponentContext();
|
|
23
|
+
const bean_overwrite = new css.beans.PropertyValue({Name: 'Overwrite', Value: true});
|
|
24
|
+
const bean_odt_export = new css.beans.PropertyValue({Name: 'FilterName', Value: 'writer8'});
|
|
25
|
+
desktop = css.frame.Desktop.create(context);
|
|
26
|
+
|
|
27
|
+
zetajs.mainPort.onmessage = function (e) {
|
|
28
|
+
switch (e.data.cmd) {
|
|
29
|
+
case 'upload':
|
|
30
|
+
loadFile(e.data.filename);
|
|
31
|
+
break;
|
|
32
|
+
case 'download':
|
|
33
|
+
xModel.store();
|
|
34
|
+
zetajs.mainPort.postMessage({cmd: 'download', id: e.data.id});
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
throw Error('Unknown message command: ' + e.data.cmd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
zetajs.mainPort.postMessage({cmd: 'thr_running'});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function loadFile(filename) {
|
|
44
|
+
if (xModel) {
|
|
45
|
+
try {
|
|
46
|
+
const xCloseable = css.util.XCloseable.query(xModel);
|
|
47
|
+
if (xCloseable) {
|
|
48
|
+
xCloseable.close(true);
|
|
49
|
+
} else {
|
|
50
|
+
xModel.dispose();
|
|
51
|
+
}
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.warn("xCloseable.close failed, trying dispose:", e);
|
|
54
|
+
try {
|
|
55
|
+
xModel.dispose();
|
|
56
|
+
} catch (e2) {
|
|
57
|
+
console.error("Failed to dispose old xModel:", e2);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
xModel = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const in_path = 'file:///tmp/office/' + filename;
|
|
64
|
+
xModel = desktop.loadComponentFromURL(in_path, '_default', 0, []);
|
|
65
|
+
ctrl = xModel.getCurrentController();
|
|
66
|
+
ctrl.getFrame().getContainerWindow().FullScreen = true;
|
|
67
|
+
zetajs.mainPort.postMessage({cmd: 'ui_ready'});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Module.zetajs.then(function(pZetajs) {
|
|
71
|
+
// initializing zetajs environment:
|
|
72
|
+
zetajs = pZetajs;
|
|
73
|
+
css = zetajs.uno.com.sun.star;
|
|
74
|
+
demo(); // launching demo
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/* vim:set shiftwidth=2 softtabstop=2 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
|