xlsxrb 0.1.1 → 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.
data/Rakefile CHANGED
@@ -184,6 +184,7 @@ task :wasm do
184
184
  original_wasm_cache = File.expand_path("tmp/test_env/ruby.wasm", __dir__)
185
185
  packed_wasm_path = File.expand_path("docs/wasm/ruby.wasm", __dir__)
186
186
  wasm_bundle_dir = File.expand_path("tmp/wasm_bundle", __dir__)
187
+ bundle_assets_dir = File.expand_path("docs/wasm/bundle_assets", __dir__)
187
188
 
188
189
  # 1. Download original base Wasm if not cached locally
189
190
  unless File.exist?(original_wasm_cache)
@@ -199,11 +200,89 @@ task :wasm do
199
200
  puts "Original ruby.wasm cached successfully."
200
201
  end
201
202
 
202
- # 2. Synchronize latest lib/xlsxrb.rb to staging bundle directory before packing
203
- FileUtils.mkdir_p(File.dirname(packed_wasm_path))
203
+ # 2. Recreate and populate the staging bundle directories
204
+ FileUtils.rm_rf(wasm_bundle_dir)
205
+ FileUtils.rm_rf(bundle_assets_dir)
206
+ FileUtils.mkdir_p(wasm_bundle_dir)
207
+ FileUtils.mkdir_p(bundle_assets_dir)
208
+
209
+ # A. Write custom static stubs and gateways dynamically
210
+ File.write(File.join(bundle_assets_dir, "openssl.rb"), "# frozen_string_literal: true\n")
211
+
212
+ File.write(File.join(bundle_assets_dir, "opentelemetry.rb"), <<~RUBY)
213
+ # frozen_string_literal: true
214
+ module OpenTelemetry
215
+ def self.tracer_provider
216
+ @tracer_provider ||= Class.new {
217
+ def tracer(*args)
218
+ Class.new {
219
+ def in_span(*args)
220
+ yield Class.new { def record_exception(*args); end; def status=(*args); end }.new
221
+ end
222
+ }.new
223
+ end
224
+ }.new
225
+ end
226
+ end
227
+ RUBY
228
+
229
+ # Gateway for REXML
230
+ File.write(File.join(bundle_assets_dir, "rexml.rb"), "# frozen_string_literal: true\nrequire \"rexml/rexml\"\n")
231
+
232
+ # Resolve and copy host's rexml files
233
+ rexml_spec_path = $LOAD_PATH.find { |p| File.exist?(File.join(p, "rexml/rexml.rb")) }
234
+ if rexml_spec_path
235
+ FileUtils.cp_r(File.join(rexml_spec_path, "rexml"), bundle_assets_dir)
236
+ end
237
+
238
+ # Gateway for strscan
239
+ File.write(File.join(bundle_assets_dir, "strscan.rb"), <<~RUBY)
240
+ # frozen_string_literal: true
241
+ begin
242
+ require "strscan.so"
243
+ rescue LoadError
244
+ end
245
+ require "strscan/strscan"
246
+ RUBY
247
+
248
+ # Resolve and copy host's strscan files
249
+ strscan_spec_path = $LOAD_PATH.find { |p| File.exist?(File.join(p, "strscan/strscan.rb")) }
250
+ if strscan_spec_path
251
+ FileUtils.mkdir_p(File.join(bundle_assets_dir, "strscan"))
252
+ FileUtils.cp(File.join(strscan_spec_path, "strscan/strscan.rb"), File.join(bundle_assets_dir, "strscan/strscan.rb"))
253
+ end
254
+
255
+ # B. Copy necessary standard libraries dynamically from host's $LOAD_PATH
256
+ stdlib_files = [
257
+ "date.rb", "delegate.rb", "forwardable.rb", "securerandom.rb",
258
+ "random/formatter.rb", "set.rb", "tempfile.rb", "tmpdir.rb", "fileutils.rb"
259
+ ]
260
+ stdlib_files.each do |name|
261
+ path = $LOAD_PATH.find { |p| File.exist?(File.join(p, name)) }
262
+ if path
263
+ dest_path = File.join(bundle_assets_dir, name)
264
+ FileUtils.mkdir_p(File.dirname(dest_path))
265
+ FileUtils.cp(File.join(path, name), dest_path)
266
+ end
267
+ end
268
+
269
+ # C. Patch tmpdir.rb to automatically create /tmp in Wasm virtual filesystem (since Wasm has no writable /tmp by default)
270
+ tmpdir_path = File.join(bundle_assets_dir, "tmpdir.rb")
271
+ if File.exist?(tmpdir_path)
272
+ File.open(tmpdir_path, "a") do |f|
273
+ f.puts "\nclass Dir\n def self.tmpdir\n Dir.mkdir(\"/tmp\") rescue nil unless File.directory?(\"/tmp\")\n \"/tmp\"\n end\nend\n"
274
+ end
275
+ end
276
+
277
+ # D. Copy everything from bundle_assets_dir to wasm_bundle_dir
278
+ FileUtils.cp_r(File.join(bundle_assets_dir, "."), wasm_bundle_dir)
279
+
280
+ # E. Copy latest xlsxrb implementation files from lib/
204
281
  FileUtils.cp(File.expand_path("lib/xlsxrb.rb", __dir__), File.join(wasm_bundle_dir, "xlsxrb.rb"))
282
+ FileUtils.cp_r(File.expand_path("lib/xlsxrb", __dir__), wasm_bundle_dir)
205
283
 
206
284
  # 3. Compile custom ruby.wasm packaging staging libs
285
+ FileUtils.mkdir_p(File.dirname(packed_wasm_path))
207
286
  puts "Building packed ruby.wasm from staging bundle..."
208
287
  cmd = "bundle exec rbwasm pack #{original_wasm_cache} --dir #{wasm_bundle_dir}::/usr/local/lib/ruby/site_ruby -o #{packed_wasm_path}"
209
288
  puts "Executing: #{cmd}"
@@ -212,15 +291,119 @@ task :wasm do
212
291
  end
213
292
  end
214
293
 
294
+ desc "Fetch required external assets for offline usage"
295
+ task :fetch_assets do
296
+ require 'open-uri'
297
+ require 'fileutils'
298
+
299
+ def download_file(url, dest)
300
+ # Check if the file exists and is not a tiny placeholder/error document
301
+ return if File.exist?(dest) && File.size(dest) > 1024
302
+
303
+ puts "Downloading #{url} to #{dest}..."
304
+ FileUtils.mkdir_p(File.dirname(dest))
305
+
306
+ require 'net/http'
307
+ uri = URI.parse(url)
308
+ temp_dest = dest + ".tmp"
309
+
310
+ begin
311
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 15, read_timeout: 90) do |http|
312
+ request = Net::HTTP::Get.new(uri)
313
+ # Specify User-Agent to bypass scraping prevention on CDNs and act as a normal browser
314
+ request['User-Agent'] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
315
+ # Force raw (identity) encoding to prevent receiving Brotli-compressed (.br) data,
316
+ # which Ruby's Net::HTTP cannot decode automatically, leading to corrupted Wasm files.
317
+ request['Accept-Encoding'] = 'identity'
318
+
319
+ http.request(request) do |response|
320
+ if response.code.to_i != 200
321
+ raise "HTTP error #{response.code}: #{response.message}"
322
+ end
323
+
324
+ File.open(temp_dest, "wb") do |output|
325
+ response.read_body do |chunk|
326
+ output.write(chunk)
327
+ end
328
+ end
329
+ end
330
+ end
331
+ # Atomic rename to prevent leaving incomplete files on failure
332
+ File.rename(temp_dest, dest)
333
+ puts "Downloaded successfully."
334
+ rescue => e
335
+ puts "Failed to download #{url}: #{e.message}"
336
+ File.delete(temp_dest) if File.exist?(temp_dest)
337
+ File.delete(dest) if File.exist?(dest)
338
+ raise "Required asset download failed. Build aborted."
339
+ end
340
+ end
341
+
342
+ def fetch_google_fonts
343
+ css_dest = "docs/fonts/fonts.css"
344
+ return if File.exist?(css_dest)
345
+
346
+ puts "Fetching and localizing Google Fonts..."
347
+ font_url = "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
348
+
349
+ css_content = nil
350
+ begin
351
+ # Specify Chrome User-Agent to ensure Google Fonts returns modern and lightweight .woff2 formats
352
+ # instead of legacy formats (like .ttf or .eot) designed for older browsers
353
+ URI.open(font_url, "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") do |f|
354
+ css_content = f.read
355
+ end
356
+ rescue => e
357
+ puts "Failed to fetch Google Fonts CSS: #{e.message}"
358
+ return
359
+ end
360
+
361
+ urls = css_content.scan(/url\((https:\/\/fonts\.gstatic\.com\/[^\)]+)\)/).flatten
362
+
363
+ urls.uniq.each do |url|
364
+ filename = url.split("/").last
365
+ local_path = "docs/fonts/#{filename}"
366
+ download_file(url, local_path)
367
+ css_content.gsub!(url, filename)
368
+ end
369
+
370
+ FileUtils.mkdir_p("docs/fonts")
371
+ File.write(css_dest, css_content)
372
+ puts "Google Fonts localized successfully."
373
+ end
374
+
375
+ # Fetch Ruby WASI JS
376
+ download_file(
377
+ "https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.9.3-2.9.4/dist/browser.umd.js",
378
+ "docs/wasm/browser.umd.js"
379
+ )
380
+
381
+ # Fetch ZetaOffice Wasm
382
+ base_zeta_url = "https://cdn.zetaoffice.net/zetaoffice_latest/"
383
+ %w[soffice.js soffice.wasm soffice.data soffice.data.js.metadata qtloader.js].each do |file|
384
+ download_file(base_zeta_url + file, "docs/zetaoffice/#{file}")
385
+ end
386
+
387
+ # Fetch and localize Google Fonts
388
+ fetch_google_fonts
389
+ end
390
+
215
391
  desc "Generate RDoc documentation including Visual Gallery"
216
- task doc: :wasm do
392
+ task doc: [:wasm, :fetch_assets] do
217
393
  FileUtils.rm_rf("doc")
218
- sh "bundle exec rdoc --title 'xlsxrb Documentation' --main README.md README.md \"docs/visual/VisualGallery.md\" lib/"
394
+
395
+ # RDoc コマンドを実行 (--exclude を指定して docs 配下のプレビュー用アセットの誤パースを回避)
396
+ sh "bundle exec rdoc --op doc" \
397
+ " --exclude 'docs/coi-serviceworker\\.js'" \
398
+ " --exclude 'docs/zeta\\.js'" \
399
+ " --exclude 'docs/office_thread\\.js'" \
400
+ " --exclude 'docs/preview\\.html'" \
401
+ " --title 'xlsxrb Documentation' --main README.md README.md \"docs/visual/VisualGallery.md\" lib/"
219
402
 
220
403
  # Copy visual gallery images and files so they are available in RDoc output
221
404
  FileUtils.mkdir_p("doc/test/visual/baselines")
222
- FileUtils.cp_r("test/visual/baselines", "doc/test/visual")
223
405
  FileUtils.mkdir_p("doc/test/visual/support/illustrations")
406
+ FileUtils.cp_r(Dir.glob("test/visual/baselines/*"), "doc/test/visual/baselines")
224
407
  FileUtils.cp_r(Dir.glob("test/visual/support/illustrations/*.png"), "doc/test/visual/support/illustrations")
225
408
 
226
409
  FileUtils.mkdir_p("doc/docs/visual/files")
@@ -233,21 +416,35 @@ task doc: :wasm do
233
416
  FileUtils.mkdir_p("doc/css")
234
417
  FileUtils.mkdir_p("doc/js")
235
418
  FileUtils.mkdir_p("doc/wasm")
419
+ FileUtils.mkdir_p("doc/zetaoffice")
420
+ FileUtils.mkdir_p("doc/fonts")
421
+
236
422
  FileUtils.cp("docs/wasm/wasm_doc_helper.js", "doc/js/wasm_doc_helper.js")
237
423
  FileUtils.cp("docs/wasm/wasm_doc_helper.css", "doc/css/wasm_doc_helper.css")
238
424
  FileUtils.cp("docs/wasm/ruby.wasm", "doc/wasm/ruby.wasm")
425
+ FileUtils.cp("docs/wasm/browser.umd.js", "doc/wasm/browser.umd.js")
426
+ FileUtils.cp_r(Dir.glob("docs/zetaoffice/*"), "doc/zetaoffice")
427
+ FileUtils.cp_r(Dir.glob("docs/fonts/*"), "doc/fonts")
428
+
429
+ # Copy LibreOffice Wasm Preview assets to doc directory
430
+ FileUtils.cp("docs/preview.html", "doc/preview.html")
431
+ FileUtils.cp("docs/coi-serviceworker.js", "doc/coi-serviceworker.js")
432
+ FileUtils.cp("docs/zeta.js", "doc/zeta.js")
433
+ FileUtils.cp("docs/office_thread.js", "doc/office_thread.js")
239
434
 
240
435
  # Inject stylesheet and javascript loading tags to all generated HTML docs
241
436
  Dir.glob("doc/**/*.html").each do |html_path|
437
+ next if File.basename(html_path) == "preview.html"
242
438
  html_content = File.read(html_path)
243
439
  depth = html_path.sub(%r{\Adoc/}, "").count("/")
244
440
  rel_prefix = "../" * depth
245
441
 
442
+ coi_tag = %Q{<script src="#{rel_prefix}coi-serviceworker.js"></script>}
246
443
  js_tag = %Q{<script src="#{rel_prefix}js/wasm_doc_helper.js" defer></script>}
247
444
  css_tag = %Q{<link href="#{rel_prefix}css/wasm_doc_helper.css" rel="stylesheet">}
248
445
 
249
446
  if html_content.include?("<body")
250
- modified = html_content.sub("<body", "#{js_tag}\n#{css_tag}\n<body")
447
+ modified = html_content.sub("<body", "#{coi_tag}\n#{js_tag}\n#{css_tag}\n<body")
251
448
  File.write(html_path, modified)
252
449
  end
253
450
  end
@@ -259,15 +456,29 @@ namespace :doc do
259
456
  desc "Build and preview RDoc documentation locally"
260
457
  task preview: :doc do
261
458
  require "webrick"
262
-
459
+
263
460
  port = ENV.fetch("PORT", "8000").to_i
264
-
461
+
265
462
  # Configure WEBrick to serve 'doc' directory
266
463
  server = WEBrick::HTTPServer.new(
267
464
  Port: port,
268
465
  DocumentRoot: File.expand_path("doc", __dir__),
269
466
  Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
270
- AccessLog: []
467
+ AccessLog: [],
468
+ # If the file is a Brotli-compressed Wasm/Data asset (checked via magic bytes),
469
+ # dynamically inject 'Content-Encoding: br' header so the browser decompresses it natively.
470
+ RequestCallback: ->(req, res) {
471
+ if req.path.end_with?(".wasm") || req.path.end_with?(".data")
472
+ # Resolve physical file path from req.path manually since res.filename is nil at this stage
473
+ local_path = File.join(File.expand_path("doc", __dir__), req.path)
474
+ if File.exist?(local_path)
475
+ first_4 = File.binread(local_path, 4) rescue nil
476
+ if first_4 != "\x00asm"
477
+ res['Content-Encoding'] = 'br'
478
+ end
479
+ end
480
+ end
481
+ }
271
482
  )
272
483
 
273
484
  puts "=================================================="
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
- avg_time = results.compact.map { |r| r[:time] }.compact.sum / results.compact.size.to_f
204
- avg_cpu = results.compact.map { |r| r[:cpu] }.compact.sum / results.compact.size.to_f
205
- avg_mem = results.compact.map { |r| r[:memory] }.compact.sum / results.compact.size.to_f
206
- avg_gc = results.compact.map { |r| r[:gc_count] }.compact.sum / results.compact.size.to_f
207
- avg_alloc = results.compact.map { |r| r[:alloc_objects] }.compact.sum / results.compact.size.to_f
208
- avg_wchar = results.compact.map { |r| r[:wchar] }.compact.sum / results.compact.size.to_f
209
- avg_rchar = results.compact.map { |r| r[:rchar] }.compact.sum / results.compact.size.to_f
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: avg_time,
214
- cpu: avg_cpu,
215
- memory: avg_mem,
216
- gc_count: avg_gc,
217
- alloc_m: avg_alloc / 1_000_000.0,
218
- wchar_mb: avg_wchar / 1_048_576.0,
219
- rchar_mb: avg_rchar / 1_048_576.0
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
 
@@ -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: */