xlsxrb 0.1.2 → 0.1.4
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/.devcontainer/Dockerfile +1 -0
- data/README.md +57 -264
- data/Rakefile +164 -17
- data/benchmark.rb +24 -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 -29
data/Rakefile
CHANGED
|
@@ -192,11 +192,13 @@ task :wasm do
|
|
|
192
192
|
require "open-uri"
|
|
193
193
|
wasm_url = "https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.9.3-2.9.4/dist/ruby.wasm"
|
|
194
194
|
FileUtils.mkdir_p(File.dirname(original_wasm_cache))
|
|
195
|
+
# rubocop:disable Security/Open
|
|
195
196
|
URI.open(wasm_url) do |stream|
|
|
196
197
|
File.open(original_wasm_cache, "wb") do |file|
|
|
197
198
|
IO.copy_stream(stream, file)
|
|
198
199
|
end
|
|
199
200
|
end
|
|
201
|
+
# rubocop:enable Security/Open
|
|
200
202
|
puts "Original ruby.wasm cached successfully."
|
|
201
203
|
end
|
|
202
204
|
|
|
@@ -231,9 +233,7 @@ task :wasm do
|
|
|
231
233
|
|
|
232
234
|
# Resolve and copy host's rexml files
|
|
233
235
|
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
|
|
236
|
+
FileUtils.cp_r(File.join(rexml_spec_path, "rexml"), bundle_assets_dir) if rexml_spec_path
|
|
237
237
|
|
|
238
238
|
# Gateway for strscan
|
|
239
239
|
File.write(File.join(bundle_assets_dir, "strscan.rb"), <<~RUBY)
|
|
@@ -259,11 +259,11 @@ task :wasm do
|
|
|
259
259
|
]
|
|
260
260
|
stdlib_files.each do |name|
|
|
261
261
|
path = $LOAD_PATH.find { |p| File.exist?(File.join(p, name)) }
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
next unless path
|
|
263
|
+
|
|
264
|
+
dest_path = File.join(bundle_assets_dir, name)
|
|
265
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
266
|
+
FileUtils.cp(File.join(path, name), dest_path)
|
|
267
267
|
end
|
|
268
268
|
|
|
269
269
|
# C. Patch tmpdir.rb to automatically create /tmp in Wasm virtual filesystem (since Wasm has no writable /tmp by default)
|
|
@@ -286,20 +286,136 @@ task :wasm do
|
|
|
286
286
|
puts "Building packed ruby.wasm from staging bundle..."
|
|
287
287
|
cmd = "bundle exec rbwasm pack #{original_wasm_cache} --dir #{wasm_bundle_dir}::/usr/local/lib/ruby/site_ruby -o #{packed_wasm_path}"
|
|
288
288
|
puts "Executing: #{cmd}"
|
|
289
|
-
unless system(cmd)
|
|
290
|
-
|
|
289
|
+
raise "Failed to build packed ruby.wasm using rbwasm pack!" unless system(cmd)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def download_file(url, dest)
|
|
293
|
+
# Check if the file exists and is not a tiny placeholder/error document
|
|
294
|
+
return if File.exist?(dest) && File.size(dest) > 1024
|
|
295
|
+
|
|
296
|
+
puts "Downloading #{url} to #{dest}..."
|
|
297
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
298
|
+
|
|
299
|
+
require "net/http"
|
|
300
|
+
uri = URI.parse(url)
|
|
301
|
+
temp_dest = "#{dest}.tmp"
|
|
302
|
+
|
|
303
|
+
begin
|
|
304
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 15, read_timeout: 90) do |http|
|
|
305
|
+
request = Net::HTTP::Get.new(uri)
|
|
306
|
+
# Specify User-Agent to bypass scraping prevention on CDNs and act as a normal browser
|
|
307
|
+
request["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
308
|
+
# Force raw (identity) encoding to prevent receiving Brotli-compressed (.br) data,
|
|
309
|
+
# which Ruby's Net::HTTP cannot decode automatically, leading to corrupted Wasm files.
|
|
310
|
+
request["Accept-Encoding"] = "identity"
|
|
311
|
+
|
|
312
|
+
http.request(request) do |response|
|
|
313
|
+
raise "HTTP error #{response.code}: #{response.message}" if response.code.to_i != 200
|
|
314
|
+
|
|
315
|
+
File.open(temp_dest, "wb") do |output|
|
|
316
|
+
response.read_body do |chunk|
|
|
317
|
+
output.write(chunk)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
# Atomic rename to prevent leaving incomplete files on failure
|
|
323
|
+
File.rename(temp_dest, dest)
|
|
324
|
+
puts "Downloaded successfully."
|
|
325
|
+
|
|
326
|
+
# Dynamic Brotli Decompression if the server ignored identity encoding and sent Brotli (.br) data
|
|
327
|
+
if File.exist?(dest) && File.binread(dest, 4)&.bytes == [0xCF, 0xFF, 0xFF, 0x7F]
|
|
328
|
+
puts "Detected Brotli compression on #{dest}. Decompressing..."
|
|
329
|
+
|
|
330
|
+
unpacked = "#{dest}.unpacked"
|
|
331
|
+
if system("brotli -d -f -o #{unpacked} #{dest}")
|
|
332
|
+
File.rename(unpacked, dest)
|
|
333
|
+
puts "Decompressed #{dest} successfully."
|
|
334
|
+
else
|
|
335
|
+
FileUtils.rm_f(unpacked)
|
|
336
|
+
raise "Failed to decompress Brotli file: #{dest}"
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
rescue StandardError => e
|
|
340
|
+
puts "Failed to download #{url}: #{e.message}"
|
|
341
|
+
FileUtils.rm_f(temp_dest)
|
|
342
|
+
FileUtils.rm_f(dest)
|
|
343
|
+
raise "Required asset download failed. Build aborted."
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def fetch_google_fonts
|
|
348
|
+
css_dest = "docs/fonts/fonts.css"
|
|
349
|
+
return if File.exist?(css_dest)
|
|
350
|
+
|
|
351
|
+
puts "Fetching and localizing Google Fonts..."
|
|
352
|
+
font_url = "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
353
|
+
|
|
354
|
+
css_content = nil
|
|
355
|
+
begin
|
|
356
|
+
# Specify Chrome User-Agent to ensure Google Fonts returns modern and lightweight .woff2 formats
|
|
357
|
+
# instead of legacy formats (like .ttf or .eot) designed for older browsers
|
|
358
|
+
# rubocop:disable Security/Open
|
|
359
|
+
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|
|
|
360
|
+
css_content = f.read
|
|
361
|
+
end
|
|
362
|
+
# rubocop:enable Security/Open
|
|
363
|
+
rescue StandardError => e
|
|
364
|
+
puts "Failed to fetch Google Fonts CSS: #{e.message}"
|
|
365
|
+
return
|
|
291
366
|
end
|
|
367
|
+
|
|
368
|
+
urls = css_content.scan(%r{url\((https://fonts\.gstatic\.com/[^)]+)\)}).flatten
|
|
369
|
+
|
|
370
|
+
urls.uniq.each do |url|
|
|
371
|
+
filename = url.split("/").last
|
|
372
|
+
local_path = "docs/fonts/#{filename}"
|
|
373
|
+
download_file(url, local_path)
|
|
374
|
+
css_content.gsub!(url, filename)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
FileUtils.mkdir_p("docs/fonts")
|
|
378
|
+
File.write(css_dest, css_content)
|
|
379
|
+
puts "Google Fonts localized successfully."
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
desc "Fetch required external assets for offline usage"
|
|
383
|
+
task :fetch_assets do
|
|
384
|
+
require "open-uri"
|
|
385
|
+
require "fileutils"
|
|
386
|
+
|
|
387
|
+
# Fetch Ruby WASI JS
|
|
388
|
+
download_file(
|
|
389
|
+
"https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.9.3-2.9.4/dist/browser.umd.js",
|
|
390
|
+
"docs/wasm/browser.umd.js"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Fetch ZetaOffice Wasm
|
|
394
|
+
base_zeta_url = "https://cdn.zetaoffice.net/zetaoffice_latest/"
|
|
395
|
+
%w[soffice.js soffice.wasm soffice.data soffice.data.js.metadata qtloader.js].each do |file|
|
|
396
|
+
download_file(base_zeta_url + file, "docs/zetaoffice/#{file}")
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Fetch and localize Google Fonts
|
|
400
|
+
fetch_google_fonts
|
|
292
401
|
end
|
|
293
402
|
|
|
294
403
|
desc "Generate RDoc documentation including Visual Gallery"
|
|
295
|
-
task doc:
|
|
404
|
+
task doc: %i[wasm fetch_assets] do
|
|
296
405
|
FileUtils.rm_rf("doc")
|
|
297
|
-
|
|
406
|
+
|
|
407
|
+
# RDoc コマンドを実行 (--exclude を指定して docs 配下のプレビュー用アセットの誤パースを回避)
|
|
408
|
+
sh "bundle exec rdoc --op doc " \
|
|
409
|
+
"--exclude 'docs/coi-serviceworker\\.js' " \
|
|
410
|
+
"--exclude 'docs/zeta\\.js' " \
|
|
411
|
+
"--exclude 'docs/office_thread\\.js' " \
|
|
412
|
+
"--exclude 'docs/preview\\.html' " \
|
|
413
|
+
"--title 'xlsxrb Documentation' --main README.md README.md \"docs/visual/VisualGallery.md\" lib/"
|
|
298
414
|
|
|
299
415
|
# Copy visual gallery images and files so they are available in RDoc output
|
|
300
416
|
FileUtils.mkdir_p("doc/test/visual/baselines")
|
|
301
|
-
FileUtils.cp_r("test/visual/baselines", "doc/test/visual")
|
|
302
417
|
FileUtils.mkdir_p("doc/test/visual/support/illustrations")
|
|
418
|
+
FileUtils.cp_r(Dir.glob("test/visual/baselines/*"), "doc/test/visual/baselines")
|
|
303
419
|
FileUtils.cp_r(Dir.glob("test/visual/support/illustrations/*.png"), "doc/test/visual/support/illustrations")
|
|
304
420
|
|
|
305
421
|
FileUtils.mkdir_p("doc/docs/visual/files")
|
|
@@ -312,21 +428,36 @@ task doc: :wasm do
|
|
|
312
428
|
FileUtils.mkdir_p("doc/css")
|
|
313
429
|
FileUtils.mkdir_p("doc/js")
|
|
314
430
|
FileUtils.mkdir_p("doc/wasm")
|
|
431
|
+
FileUtils.mkdir_p("doc/zetaoffice")
|
|
432
|
+
FileUtils.mkdir_p("doc/fonts")
|
|
433
|
+
|
|
315
434
|
FileUtils.cp("docs/wasm/wasm_doc_helper.js", "doc/js/wasm_doc_helper.js")
|
|
316
435
|
FileUtils.cp("docs/wasm/wasm_doc_helper.css", "doc/css/wasm_doc_helper.css")
|
|
317
436
|
FileUtils.cp("docs/wasm/ruby.wasm", "doc/wasm/ruby.wasm")
|
|
437
|
+
FileUtils.cp("docs/wasm/browser.umd.js", "doc/wasm/browser.umd.js")
|
|
438
|
+
FileUtils.cp_r(Dir.glob("docs/zetaoffice/*"), "doc/zetaoffice")
|
|
439
|
+
FileUtils.cp_r(Dir.glob("docs/fonts/*"), "doc/fonts")
|
|
440
|
+
|
|
441
|
+
# Copy LibreOffice Wasm Preview assets to doc directory
|
|
442
|
+
FileUtils.cp("docs/preview.html", "doc/preview.html")
|
|
443
|
+
FileUtils.cp("docs/coi-serviceworker.js", "doc/coi-serviceworker.js")
|
|
444
|
+
FileUtils.cp("docs/zeta.js", "doc/zeta.js")
|
|
445
|
+
FileUtils.cp("docs/office_thread.js", "doc/office_thread.js")
|
|
318
446
|
|
|
319
447
|
# Inject stylesheet and javascript loading tags to all generated HTML docs
|
|
320
448
|
Dir.glob("doc/**/*.html").each do |html_path|
|
|
449
|
+
next if File.basename(html_path) == "preview.html"
|
|
450
|
+
|
|
321
451
|
html_content = File.read(html_path)
|
|
322
452
|
depth = html_path.sub(%r{\Adoc/}, "").count("/")
|
|
323
453
|
rel_prefix = "../" * depth
|
|
324
454
|
|
|
325
|
-
|
|
326
|
-
|
|
455
|
+
coi_tag = %(<script src="#{rel_prefix}coi-serviceworker.js"></script>)
|
|
456
|
+
js_tag = %(<script src="#{rel_prefix}js/wasm_doc_helper.js" defer></script>)
|
|
457
|
+
css_tag = %(<link href="#{rel_prefix}css/wasm_doc_helper.css" rel="stylesheet">)
|
|
327
458
|
|
|
328
459
|
if html_content.include?("<body")
|
|
329
|
-
modified = html_content.sub("<body", "#{js_tag}\n#{css_tag}\n<body")
|
|
460
|
+
modified = html_content.sub("<body", "#{coi_tag}\n#{js_tag}\n#{css_tag}\n<body")
|
|
330
461
|
File.write(html_path, modified)
|
|
331
462
|
end
|
|
332
463
|
end
|
|
@@ -346,7 +477,23 @@ namespace :doc do
|
|
|
346
477
|
Port: port,
|
|
347
478
|
DocumentRoot: File.expand_path("doc", __dir__),
|
|
348
479
|
Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
|
|
349
|
-
AccessLog: []
|
|
480
|
+
AccessLog: [],
|
|
481
|
+
# If the file is a Brotli-compressed Wasm/Data asset (checked via magic bytes),
|
|
482
|
+
# dynamically inject 'Content-Encoding: br' header so the browser decompresses it natively.
|
|
483
|
+
RequestCallback: lambda { |req, res|
|
|
484
|
+
if req.path.end_with?(".wasm") || req.path.end_with?(".data")
|
|
485
|
+
# Resolve physical file path from req.path manually since res.filename is nil at this stage
|
|
486
|
+
local_path = File.join(File.expand_path("doc", __dir__), req.path)
|
|
487
|
+
if File.exist?(local_path)
|
|
488
|
+
first_bytes = begin
|
|
489
|
+
File.binread(local_path, 4)
|
|
490
|
+
rescue StandardError
|
|
491
|
+
nil
|
|
492
|
+
end
|
|
493
|
+
res["Content-Encoding"] = "br" if first_bytes != "\x00asm"
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
}
|
|
350
497
|
)
|
|
351
498
|
|
|
352
499
|
puts "=================================================="
|
data/benchmark.rb
CHANGED
|
@@ -192,6 +192,14 @@ 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
|
+
|
|
198
|
+
sorted = array.sort
|
|
199
|
+
len = sorted.length
|
|
200
|
+
(sorted[(len - 1) / 2] + sorted[len / 2]) / 2.0
|
|
201
|
+
end
|
|
202
|
+
|
|
195
203
|
def run_benchmark(name, snippet)
|
|
196
204
|
print format("%-25s", name)
|
|
197
205
|
results = ITERATIONS.times.map do
|
|
@@ -200,23 +208,25 @@ def run_benchmark(name, snippet)
|
|
|
200
208
|
end
|
|
201
209
|
puts
|
|
202
210
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
valid_results = results.compact
|
|
212
|
+
|
|
213
|
+
median_time = median(valid_results.map { |r| r[:time] }.compact)
|
|
214
|
+
median_cpu = median(valid_results.map { |r| r[:cpu] }.compact)
|
|
215
|
+
median_mem = median(valid_results.map { |r| r[:memory] }.compact)
|
|
216
|
+
median_gc = median(valid_results.map { |r| r[:gc_count] }.compact)
|
|
217
|
+
median_alloc = median(valid_results.map { |r| r[:alloc_objects] }.compact)
|
|
218
|
+
median_wchar = median(valid_results.map { |r| r[:wchar] }.compact)
|
|
219
|
+
median_rchar = median(valid_results.map { |r| r[:rchar] }.compact)
|
|
210
220
|
|
|
211
221
|
{
|
|
212
222
|
name: name,
|
|
213
|
-
time:
|
|
214
|
-
cpu:
|
|
215
|
-
memory:
|
|
216
|
-
gc_count:
|
|
217
|
-
alloc_m:
|
|
218
|
-
wchar_mb:
|
|
219
|
-
rchar_mb:
|
|
223
|
+
time: median_time,
|
|
224
|
+
cpu: median_cpu,
|
|
225
|
+
memory: median_mem,
|
|
226
|
+
gc_count: median_gc,
|
|
227
|
+
alloc_m: median_alloc / 1_000_000.0,
|
|
228
|
+
wchar_mb: median_wchar / 1_048_576.0,
|
|
229
|
+
rchar_mb: median_rchar / 1_048_576.0
|
|
220
230
|
}
|
|
221
231
|
end
|
|
222
232
|
|
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: */
|