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.
- checksums.yaml +4 -4
- data/README.md +57 -264
- data/Rakefile +220 -9
- 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/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.
|
|
203
|
-
FileUtils.
|
|
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
|
-
|
|
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
|
-
|
|
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: */
|