xlsxrb 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +57 -264
- data/Rakefile +137 -5
- data/benchmark.rb +23 -14
- data/docs/DEVELOPMENT.md +67 -0
- data/docs/coi-serviceworker.js +82 -0
- data/docs/office_thread.js +77 -0
- data/docs/preview.html +719 -0
- data/docs/visual/VisualGallery.md +0 -158
- data/docs/wasm/wasm_doc_helper.css +256 -94
- data/docs/wasm/wasm_doc_helper.js +261 -142
- data/docs/zeta.js +1107 -0
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +178 -24
- data/lib/xlsxrb/ooxml.rb +1 -1
- data/lib/xlsxrb/version.rb +1 -1
- metadata +6 -1
data/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>
|