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.
data/docs/preview.html ADDED
@@ -0,0 +1,719 @@
1
+ <!DOCTYPE html>
2
+ <!-- SPDX-License-Identifier: MIT -->
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <!-- Enable Cross-Origin Isolation in client-side using service worker -->
8
+ <script src="coi-serviceworker.js"></script>
9
+ <title>xlsxrb Wasm LibreOffice Preview</title>
10
+ <!-- Localized Fonts -->
11
+ <link href="fonts/fonts.css" rel="stylesheet">
12
+
13
+ <style>
14
+ :root {
15
+ --bg-color: #0b0f19;
16
+ --panel-bg: #111827;
17
+ --accent-green: #10b981;
18
+ --accent-blue: #3b82f6;
19
+ --border-color: #1f2937;
20
+ --text-color: #f3f4f6;
21
+ --text-muted: #9ca3af;
22
+ }
23
+
24
+ * {
25
+ box-sizing: border-box;
26
+ margin: 0;
27
+ padding: 0;
28
+ }
29
+
30
+ body {
31
+ font-family: 'Inter', sans-serif;
32
+ background-color: var(--bg-color);
33
+ color: var(--text-color);
34
+ height: 100vh;
35
+ display: flex;
36
+ flex-direction: column;
37
+ overflow: hidden;
38
+ }
39
+
40
+ header {
41
+ background-color: var(--panel-bg);
42
+ border-bottom: 1px solid var(--border-color);
43
+ padding: 12px 24px;
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ z-index: 10;
48
+ }
49
+
50
+ .logo-area {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 12px;
54
+ }
55
+
56
+ .logo-title {
57
+ font-size: 1.1rem;
58
+ font-weight: 700;
59
+ letter-spacing: 0.5px;
60
+ background: linear-gradient(135deg, #60a5fa, #34d399);
61
+ -webkit-background-clip: text;
62
+ -webkit-text-fill-color: transparent;
63
+ }
64
+
65
+ .badge {
66
+ background-color: rgba(59, 130, 246, 0.15);
67
+ color: #60a5fa;
68
+ font-size: 0.75rem;
69
+ padding: 3px 8px;
70
+ border-radius: 9999px;
71
+ border: 1px solid rgba(59, 130, 246, 0.3);
72
+ }
73
+
74
+ .main-workspace {
75
+ flex: 1;
76
+ display: flex;
77
+ overflow: hidden;
78
+ position: relative;
79
+ }
80
+
81
+ /* Split Layout */
82
+ .editor-pane {
83
+ width: 40%;
84
+ min-width: 300px;
85
+ background-color: #0f172a;
86
+ border-right: 1px solid var(--border-color);
87
+ display: flex;
88
+ flex-direction: column;
89
+ height: 100%;
90
+ }
91
+
92
+ .preview-pane {
93
+ flex: 1;
94
+ background-color: var(--bg-color);
95
+ position: relative;
96
+ display: flex;
97
+ flex-direction: column;
98
+ height: 100%;
99
+ }
100
+
101
+ .pane-header {
102
+ background-color: var(--panel-bg);
103
+ border-bottom: 1px solid var(--border-color);
104
+ padding: 10px 16px;
105
+ display: flex;
106
+ justify-content: space-between;
107
+ align-items: center;
108
+ font-size: 0.85rem;
109
+ font-weight: 600;
110
+ color: var(--text-muted);
111
+ }
112
+
113
+ .editor-container {
114
+ flex: 1;
115
+ position: relative;
116
+ padding: 12px;
117
+ }
118
+
119
+ textarea.code-editor {
120
+ width: 100%;
121
+ height: 100%;
122
+ background-color: #090d16;
123
+ color: #38bdf8;
124
+ border: 1px solid var(--border-color);
125
+ border-radius: 8px;
126
+ padding: 16px;
127
+ font-family: 'JetBrains Mono', monospace;
128
+ font-size: 0.9rem;
129
+ line-height: 1.5;
130
+ resize: none;
131
+ outline: none;
132
+ transition: border-color 0.2s;
133
+ }
134
+
135
+ textarea.code-editor:focus {
136
+ border-color: var(--accent-blue);
137
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
138
+ }
139
+
140
+ .btn-run {
141
+ background: linear-gradient(135deg, #3b82f6, #10b981);
142
+ color: white;
143
+ border: none;
144
+ border-radius: 6px;
145
+ padding: 8px 16px;
146
+ font-size: 0.85rem;
147
+ font-weight: 600;
148
+ cursor: pointer;
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 8px;
152
+ transition: transform 0.1s, opacity 0.2s;
153
+ }
154
+
155
+ .btn-run:hover {
156
+ opacity: 0.9;
157
+ transform: translateY(-1px);
158
+ }
159
+
160
+ .btn-run:active {
161
+ transform: translateY(0);
162
+ }
163
+
164
+ .btn-run:disabled {
165
+ background: #374151;
166
+ color: #9ca3af;
167
+ cursor: not-allowed;
168
+ transform: none;
169
+ }
170
+
171
+ /* Preview Canvas */
172
+ .canvas-container {
173
+ flex: 1;
174
+ position: relative;
175
+ display: flex;
176
+ justify-content: center;
177
+ align-items: center;
178
+ overflow: hidden;
179
+ }
180
+
181
+ #qtcanvas {
182
+ width: 100%;
183
+ height: 100%;
184
+ border: none;
185
+ outline: none;
186
+ background-color: #ffffff; /* White background for spreadsheets */
187
+ display: block;
188
+ }
189
+
190
+ /* Console / Output logs */
191
+ .console-log {
192
+ height: 160px;
193
+ background-color: #020617;
194
+ border-top: 1px solid var(--border-color);
195
+ padding: 12px;
196
+ font-family: 'JetBrains Mono', monospace;
197
+ font-size: 0.8rem;
198
+ color: #e2e8f0;
199
+ overflow-y: auto;
200
+ white-space: pre-wrap;
201
+ }
202
+
203
+ /* Loading Overlays */
204
+ .overlay {
205
+ position: absolute;
206
+ top: 0;
207
+ left: 0;
208
+ right: 0;
209
+ bottom: 0;
210
+ background-color: rgba(11, 15, 25, 0.85);
211
+ display: flex;
212
+ flex-direction: column;
213
+ justify-content: center;
214
+ align-items: center;
215
+ z-index: 100;
216
+ backdrop-filter: blur(4px);
217
+ transition: opacity 0.3s ease;
218
+ }
219
+
220
+ .spinner {
221
+ width: 50px;
222
+ height: 50px;
223
+ border: 4px solid rgba(59, 130, 246, 0.1);
224
+ border-top: 4px solid var(--accent-blue);
225
+ border-radius: 50%;
226
+ animation: spin 1s linear infinite;
227
+ margin-bottom: 16px;
228
+ }
229
+
230
+ @keyframes spin {
231
+ 0% { transform: rotate(0deg); }
232
+ 100% { transform: rotate(360deg); }
233
+ }
234
+
235
+ .overlay-text {
236
+ font-size: 1rem;
237
+ font-weight: 600;
238
+ color: var(--text-color);
239
+ margin-bottom: 8px;
240
+ }
241
+
242
+ .overlay-subtext {
243
+ font-size: 0.8rem;
244
+ color: var(--text-muted);
245
+ }
246
+
247
+ /* Drawer / iframe Integration Styles */
248
+ body.iframe-mode header {
249
+ display: none !important;
250
+ }
251
+ body.iframe-mode .editor-pane {
252
+ display: none !important;
253
+ }
254
+ body.iframe-mode .preview-pane {
255
+ width: 100% !important;
256
+ flex: 1 !important;
257
+ }
258
+ body.iframe-mode .main-workspace {
259
+ height: 100vh !important;
260
+ border: none !important;
261
+ }
262
+ body.iframe-mode {
263
+ background-color: #ffffff !important;
264
+ color: #1f2937 !important;
265
+ }
266
+ body.iframe-mode .pane-header {
267
+ background-color: #f8fafc !important;
268
+ border-bottom: 1px solid #e2e8f0 !important;
269
+ color: #475569 !important;
270
+ font-family: inherit;
271
+ font-size: 0.85rem;
272
+ padding: 12px 20px;
273
+ }
274
+ body.iframe-mode .console-log {
275
+ background-color: #0f172a !important;
276
+ border-top: 1px solid #e2e8f0 !important;
277
+ font-size: 0.75rem;
278
+ }
279
+ </style>
280
+ </head>
281
+ <body>
282
+ <header>
283
+ <div class="logo-area">
284
+ <h1 class="logo-title">xlsxrb Live Preview</h1>
285
+ <span class="badge">ZetaOffice WebAssembly Demo</span>
286
+ </div>
287
+ <div>
288
+ <button class="btn-run" id="btnPreview" onclick="generateAndPreview()" disabled>
289
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
290
+ <polygon points="5 3 19 12 5 21 5 3"></polygon>
291
+ </svg>Run & Preview
292
+ </button>
293
+ </div>
294
+ </header>
295
+
296
+ <div class="main-workspace">
297
+ <!-- Left: Editor & Console -->
298
+ <div class="editor-pane">
299
+ <div class="pane-header">Ruby Code Editor</div>
300
+ <div class="editor-container">
301
+ <textarea class="code-editor" id="codeEditor" spellcheck="false">require "xlsxrb"
302
+
303
+ Xlsxrb.generate("preview.xlsx") do |w|
304
+ # 1. Define professional styles using fluent DSL
305
+ w.add_style("title") do |s|
306
+ s.bold
307
+ s.size 16
308
+ s.font_color "FFFFFF"
309
+ s.fill_color "1F497D" # Navy blue header
310
+ s.align_horizontal "center"
311
+ s.align_vertical "center"
312
+ end
313
+
314
+ w.add_style("subtitle") do |s|
315
+ s.italic
316
+ s.size 10
317
+ s.font_color "595959"
318
+ s.align_horizontal "left"
319
+ end
320
+
321
+ w.add_style("header") do |s|
322
+ s.bold
323
+ s.size 11
324
+ s.font_color "FFFFFF"
325
+ s.fill_color "2F5597" # Slate blue header
326
+ s.align_horizontal "center"
327
+ s.border_all style: "thin", color: "D9D9D9"
328
+ end
329
+
330
+ w.add_style("data_left") do |s|
331
+ s.size 10
332
+ s.align_horizontal "left"
333
+ s.border_all style: "thin", color: "D9D9D9"
334
+ end
335
+
336
+ w.add_style("data_center") do |s|
337
+ s.size 10
338
+ s.align_horizontal "center"
339
+ s.border_all style: "thin", color: "D9D9D9"
340
+ end
341
+
342
+ w.add_style("data_right") do |s|
343
+ s.size 10
344
+ s.align_horizontal "right"
345
+ s.border_all style: "thin", color: "D9D9D9"
346
+ end
347
+
348
+ w.add_style("currency") do |s|
349
+ s.size 10
350
+ s.number_format "$#,##0.00"
351
+ s.align_horizontal "right"
352
+ s.border_all style: "thin", color: "D9D9D9"
353
+ end
354
+
355
+ w.add_style("total") do |s|
356
+ s.bold
357
+ s.size 11
358
+ s.fill_color "D9E1F2" # Soft light blue fill
359
+ s.number_format "$#,##0.00"
360
+ s.align_horizontal "right"
361
+ s.border_bottom style: "double", color: "2F5597" # Double bottom border for accountant total
362
+ s.border_top style: "thin", color: "D9D9D9"
363
+ s.border_left style: "thin", color: "D9D9D9"
364
+ s.border_right style: "thin", color: "D9D9D9"
365
+ end
366
+
367
+ w.add_style("total_label") do |s|
368
+ s.bold
369
+ s.size 11
370
+ s.fill_color "D9E1F2"
371
+ s.align_horizontal "right"
372
+ s.border_bottom style: "double", color: "2F5597"
373
+ s.border_top style: "thin", color: "D9D9D9"
374
+ s.border_left style: "thin", color: "D9D9D9"
375
+ s.border_right style: "thin", color: "D9D9D9"
376
+ end
377
+
378
+ # 2. Start worksheet
379
+ w.add_sheet("Sales Report") do |sheet|
380
+ # Set column widths
381
+ sheet.set_column(0, width: 22) # Product Name
382
+ sheet.set_column(1, width: 12) # Quantity
383
+ sheet.set_column(2, width: 15) # Unit Price
384
+ sheet.set_column(3, width: 18) # Total Amount
385
+
386
+ # Title Row (A1:D1 merged)
387
+ sheet.add_row(["Premium Product Sales Report", "", "", ""], styles: "title", height: 35)
388
+ sheet.merge_cells("A1:D1")
389
+
390
+ # Metadata Row
391
+ sheet.add_row(["Report Generated at: #{Time.now.strftime('%Y-%m-%d %H:%M')}", "", "", ""], styles: "subtitle", height: 20)
392
+ sheet.merge_cells("A2:D2")
393
+ sheet.add_row([]) # Blank separator row
394
+
395
+ # Table Header Row
396
+ header_styles = ["header", "header", "header", "header"]
397
+ sheet.add_row(["Product Item", "Quantity", "Unit Price", "Total Value"], styles: header_styles, height: 24)
398
+
399
+ # Data Rows
400
+ row_styles = ["data_left", "data_center", "currency", "currency"]
401
+ sheet.add_row(["Wireless Headphones", 120, 89.99, "=B5*C5"], styles: row_styles, height: 20)
402
+ sheet.add_row(["Smart Watch", 85, 199.50, "=B6*C6"], styles: row_styles, height: 20)
403
+ sheet.add_row(["Bluetooth Speaker", 150, 45.00, "=B7*C7"], styles: row_styles, height: 20)
404
+ sheet.add_row(["Mechanical Keyboard", 60, 129.00, "=B8*C8"], styles: row_styles, height: 20)
405
+ sheet.add_row(["Ergonomic Mouse", 210, 59.90, "=B9*C9"], styles: row_styles, height: 20)
406
+
407
+ # Total Row (Using Accountants style double underline bottom border)
408
+ total_styles = ["total_label", "total_label", "total_label", "total"]
409
+ sheet.add_row(["Total Portfolio Value", "", "", "=SUM(D5:D9)"], styles: total_styles, height: 24)
410
+ end
411
+ end
412
+
413
+ puts "Excel generation completed!"</textarea>
414
+ </div>
415
+ <div class="console-log" id="consoleLog">Console ready. Loading engines...</div>
416
+ </div>
417
+
418
+ <!-- Right: LibreOffice Canvas -->
419
+ <div class="preview-pane">
420
+ <div class="pane-header">LibreOffice Spreadsheet Preview</div>
421
+ <div class="canvas-container" onselectstart="event.preventDefault()">
422
+ <!-- Loading Overlay -->
423
+ <div id="loadingOverlay" class="overlay">
424
+ <div class="spinner"></div>
425
+ <div class="overlay-text" id="loadingText">Initializing Engines...</div>
426
+ <div class="overlay-subtext" id="loadingSubtext">Downloading Ruby and LibreOffice Wasm binaries...</div>
427
+ </div>
428
+
429
+ <canvas
430
+ id="qtcanvas" contenteditable="true"
431
+ oncontextmenu="event.preventDefault()" onkeydown="event.preventDefault()"
432
+ style="width: 100%; height: 100%; visibility: hidden;">
433
+ <!-- Canvas target for LibreOffice rendering -->
434
+ </canvas>
435
+ </div>
436
+ </div>
437
+ </div>
438
+
439
+ <!-- Scripts Integration -->
440
+ <script type="text/javascript">
441
+ 'use strict';
442
+
443
+ // Detect if running inside an iframe (e.g. RDoc Side Drawer preview)
444
+ if (window.self !== window.top) {
445
+ document.body.classList.add('iframe-mode');
446
+ }
447
+
448
+ // Resolve the absolute URL dynamically to prevent path resolution failures inside Web Workers (Blob origin)
449
+ const current_href = window.location.href.split('#')[0].split('?')[0];
450
+ const base_dir = current_href.substring(0, current_href.lastIndexOf('/') + 1);
451
+ const soffice_base_url = base_dir + 'zetaoffice/';
452
+
453
+ const canvas = document.getElementById('qtcanvas');
454
+ const loadingOverlay = document.getElementById('loadingOverlay');
455
+ const loadingText = document.getElementById('loadingText');
456
+ const loadingSubtext = document.getElementById('loadingSubtext');
457
+ const btnPreview = document.getElementById('btnPreview');
458
+ const consoleLog = document.getElementById('consoleLog');
459
+
460
+ let rubyVM = null;
461
+ let zetaPort = null;
462
+ let isZetaLoaded = false;
463
+ let isRubyLoaded = false;
464
+
465
+ // Listen for incoming code from parent RDoc playground (Side Drawer integration)
466
+ let pendingCodeRun = null;
467
+ window.addEventListener("message", (event) => {
468
+ if (event.data && event.data.action === "load_code") {
469
+ const code = event.data.code;
470
+ const editor = document.getElementById('codeEditor');
471
+ if (editor) {
472
+ editor.value = code;
473
+ if (isZetaLoaded && isRubyLoaded && rubyVM) {
474
+ // Trigger preview run automatically if engine is already ready
475
+ generateAndPreview();
476
+ } else {
477
+ // Store code to run automatically once loading finishes
478
+ pendingCodeRun = code;
479
+ }
480
+ }
481
+ }
482
+ });
483
+
484
+ // 1. Setup Module settings for Emscripten / ZetaJS
485
+ var Module = {
486
+ canvas,
487
+ uno_scripts: ['./zeta.js', './office_thread.js'],
488
+ locateFile: function(path, prefix) { return (prefix || soffice_base_url) + path; },
489
+ mainScriptUrlOrBlob: new Blob(
490
+ ["importScripts('" + soffice_base_url + "soffice.js');"], {type: 'text/javascript'}
491
+ )
492
+ };
493
+
494
+ // Custom logger for textarea console
495
+ function logToConsole(message, type = 'info') {
496
+ const timestamp = new Date().toLocaleTimeString();
497
+ const prefix = `[${timestamp}] `;
498
+ consoleLog.textContent += `\n${prefix}${message}`;
499
+ consoleLog.scrollTop = consoleLog.scrollHeight;
500
+ console.log(`${prefix}${message}`);
501
+ }
502
+
503
+ // 2. Initialize Ruby.wasm (with xlsxrb bundled)
504
+ async function initRubyWasm() {
505
+ logToConsole("Fetching Ruby WebAssembly binary...");
506
+ try {
507
+ // Localized import of browser.umd.js for Ruby Wasm
508
+ await loadScript("wasm/browser.umd.js");
509
+
510
+ const response = await fetch("wasm/ruby.wasm?cb=" + Date.now());
511
+ const buffer = await response.arrayBuffer();
512
+ const module = await WebAssembly.compile(buffer);
513
+
514
+ const rubyWasm = window.rubyWasm || window["ruby-wasm-wasi"];
515
+ const DefaultRubyVM = rubyWasm.DefaultRubyVM;
516
+
517
+ let consoleBuffer = "";
518
+ const options = {
519
+ stdout: (str) => { consoleBuffer += str; if (str.trim()) logToConsole(`[Ruby stdout] ${str}`); },
520
+ stderr: (str) => { consoleBuffer += str; if (str.trim()) logToConsole(`[Ruby stderr] ${str}`, 'error'); }
521
+ };
522
+
523
+ let vmInit;
524
+ if (typeof DefaultRubyVM.initialize === "function") {
525
+ vmInit = DefaultRubyVM.initialize(module, options);
526
+ } else if (typeof DefaultRubyVM.initializeVM === "function") {
527
+ vmInit = DefaultRubyVM.initializeVM(module, options);
528
+ } else if (typeof DefaultRubyVM === "function") {
529
+ try {
530
+ vmInit = DefaultRubyVM(module, options);
531
+ } catch (e) {
532
+ const instance = new DefaultRubyVM();
533
+ const initFn = instance.initialize || instance.initializeVM;
534
+ if (typeof initFn === "function") {
535
+ vmInit = initFn.call(instance, module, options);
536
+ } else {
537
+ throw new Error("No initialization method found on DefaultRubyVM instance");
538
+ }
539
+ }
540
+ } else {
541
+ throw new Error("DefaultRubyVM is not available");
542
+ }
543
+
544
+ const { vm } = await vmInit;
545
+ rubyVM = vm;
546
+ isRubyLoaded = true;
547
+ logToConsole("Ruby WebAssembly Engine initialized successfully!");
548
+ checkEnginesReady();
549
+ } catch (e) {
550
+ logToConsole(`Error initializing Ruby Wasm: ${e.message}`, 'error');
551
+ console.error(e);
552
+ }
553
+ }
554
+
555
+ // Check if both engines are loaded
556
+ function checkEnginesReady() {
557
+ if (isRubyLoaded && isZetaLoaded) {
558
+ loadingOverlay.style.display = 'none';
559
+ btnPreview.disabled = false;
560
+ logToConsole("All engines are ready. Click 'Run & Preview' to view your sheet.");
561
+ if (pendingCodeRun) {
562
+ logToConsole("Auto-running incoming playground code...");
563
+ generateAndPreview();
564
+ pendingCodeRun = null;
565
+ }
566
+ } else if (isRubyLoaded) {
567
+ loadingText.textContent = "Loading LibreOffice Wasm...";
568
+ loadingSubtext.textContent = "ZetaOffice engine is downloading/compiling (~80MB, please wait)...";
569
+ }
570
+ }
571
+
572
+ // Helper to dynamically load external scripts
573
+ function loadScript(src) {
574
+ return new Promise((resolve, reject) => {
575
+ const script = document.createElement("script");
576
+ script.src = src;
577
+ script.crossOrigin = "anonymous";
578
+ script.onload = resolve;
579
+ script.onerror = reject;
580
+ document.head.appendChild(script);
581
+ });
582
+ }
583
+
584
+ // 3. Initialize LibreOffice (ZetaOffice) Wasm
585
+ function initZetaOffice() {
586
+ logToConsole("Fetching ZetaOffice WebAssembly binaries...");
587
+ const soffice_js = document.createElement("script");
588
+ soffice_js.src = soffice_base_url + "soffice.js";
589
+ soffice_js.crossOrigin = "anonymous";
590
+
591
+ soffice_js.onload = function() {
592
+ Module.uno_main.then(function(pPort) {
593
+ zetaPort = pPort;
594
+ zetaPort.onmessage = function(e) {
595
+ switch (e.data.cmd) {
596
+ case 'thr_running':
597
+ logToConsole("ZetaOffice background engine started.");
598
+ isZetaLoaded = true;
599
+ checkEnginesReady();
600
+ break;
601
+ case 'ui_ready':
602
+ logToConsole("LibreOffice UI successfully rendered!");
603
+ // Force resizing window to map canvas area correctly
604
+ window.dispatchEvent(new Event('resize'));
605
+ setTimeout(() => {
606
+ loadingOverlay.style.display = 'none';
607
+ canvas.style.visibility = 'visible';
608
+ btnPreview.disabled = false;
609
+ }, 1000);
610
+ break;
611
+ default:
612
+ logToConsole(`ZetaOffice Worker: ${e.data.cmd}`);
613
+ }
614
+ };
615
+ });
616
+ };
617
+ soffice_js.onerror = function(err) {
618
+ logToConsole("Failed to load soffice.js binary.", 'error');
619
+ };
620
+ document.body.appendChild(soffice_js);
621
+ }
622
+
623
+ // 4. Run & Preview Action
624
+ async function generateAndPreview() {
625
+ if (!rubyVM || !zetaPort) return;
626
+
627
+ btnPreview.disabled = true;
628
+ loadingOverlay.style.display = 'flex';
629
+ loadingText.textContent = "Generating spreadsheet...";
630
+ loadingSubtext.textContent = "Running xlsxrb in Ruby Wasm VM...";
631
+ canvas.style.visibility = 'hidden';
632
+
633
+ logToConsole("Running Ruby script to generate XLSX...");
634
+ let code = document.getElementById('codeEditor').value;
635
+ if (!code.includes('require "xlsxrb"') && !code.includes("require 'xlsxrb'")) {
636
+ code = 'require "xlsxrb"\n' + code;
637
+ }
638
+
639
+ // Clean old files in Ruby VFS to avoid collisions
640
+ try {
641
+ rubyVM.eval('Dir.glob("*.xlsx").each { |f| File.delete(f) rescue nil }');
642
+ } catch(e) {}
643
+
644
+ try {
645
+ rubyVM.eval(code);
646
+
647
+ // Dynamically detect the generated output file name in the VFS root
648
+ const generatedFile = rubyVM.eval('Dir.glob("*.xlsx").first').toString();
649
+ if (!generatedFile || generatedFile === "nil" || generatedFile.length === 0) {
650
+ throw new Error("No .xlsx file was generated by the script.");
651
+ }
652
+
653
+ // Read generated binary from Ruby VFS using hex bridge
654
+ const hexData = rubyVM.eval(`File.binread("${generatedFile}").unpack1("H*")`).toString();
655
+ const len = hexData.length;
656
+ const fileData = new Uint8Array(len / 2);
657
+ for (let i = 0; i < len; i += 2) {
658
+ fileData[i / 2] = parseInt(hexData.substr(i, 2), 16);
659
+ }
660
+ logToConsole(`Generated ${generatedFile} successfully. Size: ${fileData.length} bytes.`);
661
+
662
+ loadingText.textContent = "Loading into LibreOffice...";
663
+ loadingSubtext.textContent = "Writing file to ZetaOffice memory VFS...";
664
+
665
+ // Write file into ZetaOffice Emscripten VFS (through main FS scope safely)
666
+ const fsObject = window.FS || Module.FS;
667
+ if (!fsObject) {
668
+ throw new Error("LibreOffice WebAssembly FileSystem (FS) is not initialized yet.");
669
+ }
670
+ try { fsObject.mkdir('/tmp/office'); } catch(e) {}
671
+ fsObject.writeFile('/tmp/office/preview.xlsx', fileData);
672
+
673
+ logToConsole("Triggering LibreOffice renderer...");
674
+ // Instruct ZetaOffice thread worker to load the XLSX file
675
+ zetaPort.postMessage({ cmd: 'upload', filename: 'preview.xlsx' });
676
+ } catch (e) {
677
+ logToConsole(`Error executing Ruby script: ${e.message}`, 'error');
678
+ loadingOverlay.style.display = 'none';
679
+ btnPreview.disabled = false;
680
+ }
681
+ }
682
+
683
+ // Start loading engines on page load safely
684
+ window.addEventListener("load", () => {
685
+ if (!window.isSecureContext) {
686
+ const warnMsg = "Secure Context Required: Please access via 'localhost' or 'HTTPS' to enable preview.";
687
+ logToConsole(warnMsg, "error");
688
+ loadingText.textContent = "Secure Context Required";
689
+ loadingSubtext.textContent = "Service Workers (SharedArrayBuffer) require a secure origin. Please open this page via a local web server (e.g., http://localhost) or an HTTPS origin instead of file:// or a non-secure IP address.";
690
+ return;
691
+ }
692
+
693
+ if (navigator.serviceWorker && !navigator.serviceWorker.controller) {
694
+ logToConsole("Waiting for Service Worker activation (SharedArrayBuffer)...");
695
+ loadingText.textContent = "Preparing Environment...";
696
+ loadingSubtext.textContent = "Activating Cross-Origin Isolation Service Worker...";
697
+
698
+ navigator.serviceWorker.addEventListener("controllerchange", () => {
699
+ logToConsole("Service Worker activated! Starting engines...");
700
+ initRubyWasm();
701
+ initZetaOffice();
702
+ });
703
+ } else {
704
+ initRubyWasm();
705
+ initZetaOffice();
706
+ }
707
+ });
708
+
709
+ // Handle window resize for Qt engine mapping
710
+ window.addEventListener('resize', () => {
711
+ if (isZetaLoaded) {
712
+ setTimeout(() => {
713
+ window.dispatchEvent(new Event('resize'));
714
+ }, 150);
715
+ }
716
+ });
717
+ </script>
718
+ </body>
719
+ </html>