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.
@@ -4,7 +4,7 @@
4
4
  document.addEventListener("DOMContentLoaded", () => {
5
5
  const codeBlocks = document.querySelectorAll("pre.ruby");
6
6
  codeBlocks.forEach((block, index) => {
7
- setupPlayground(block, index);
7
+ setupLazyPlayground(block, index);
8
8
  });
9
9
  });
10
10
 
@@ -21,11 +21,11 @@
21
21
  }
22
22
  isInitializing = true;
23
23
 
24
- // Load browser.umd.js dynamically from CDN
25
- await loadScript("https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.9.3-2.9.4/dist/browser.umd.js");
24
+ // Load browser.umd.js locally using the relative path prefix
25
+ const prefix = window.rdoc_rel_prefix || "./";
26
+ await loadScript(`${prefix}wasm/browser.umd.js`);
26
27
 
27
28
  // Fetch and compile the custom prepackaged ruby.wasm module
28
- const prefix = window.rdoc_rel_prefix || "./";
29
29
  const response = await fetch(`${prefix}wasm/ruby.wasm?cb=${Date.now()}`);
30
30
  const buffer = await response.arrayBuffer();
31
31
  const module = await WebAssembly.compile(buffer);
@@ -61,7 +61,7 @@
61
61
  throw new Error("DefaultRubyVM is not available");
62
62
  }
63
63
  const { vm } = await vmInit;
64
-
64
+
65
65
  rubyVM = vm;
66
66
 
67
67
  isInitializing = false;
@@ -86,172 +86,268 @@
86
86
  });
87
87
  }
88
88
 
89
- // Setup playground UI elements around a code block
90
- function setupPlayground(block, index) {
89
+ // Setup lightweight wrapper and action buttons, deferring full editor instantiation until hover or click
90
+ function setupLazyPlayground(block, index) {
91
91
  const originalCode = block.textContent;
92
92
 
93
- const btn = document.createElement("button");
94
- btn.className = "wasm-try-btn";
95
- btn.innerHTML = `
96
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;">
97
- <polygon points="5 3 19 12 5 21 5 3"></polygon>
98
- </svg>Try it in Browser
99
- `;
100
-
93
+ // Create wrapper container
101
94
  const wrapper = document.createElement("div");
102
95
  wrapper.className = "wasm-playground-wrapper";
103
-
104
96
  block.parentNode.insertBefore(wrapper, block);
105
97
  wrapper.appendChild(block);
106
- wrapper.insertBefore(btn, block);
107
-
108
- btn.addEventListener("click", async () => {
109
- btn.style.display = "none";
110
-
111
- const loader = document.createElement("div");
112
- loader.className = "wasm-loader";
113
- loader.innerHTML = `
114
- <span class="wasm-loader-text">Loading Ruby Wasm Engine...</span>
115
- <div class="wasm-spinner"></div>
116
- `;
117
- wrapper.insertBefore(loader, block);
118
98
 
119
- try {
120
- const vm = await ensureRubyWasm();
121
- loader.remove();
122
- activatePlayground(wrapper, block, originalCode, vm);
123
- } catch (e) {
124
- loader.innerHTML = `<span class="wasm-error">Error loading Wasm: ${e.message}</span>`;
125
- console.error(e);
126
- }
127
- });
128
-
129
- // Auto-trigger load under E2E testing mode
130
- if (window.location.hash === "#test-mode" && index === 0) {
131
- setTimeout(() => {
132
- console.log("[Script Mock] Test mode detected. Triggering playground load...");
133
- btn.click();
134
-
135
- const interval = setInterval(() => {
136
- const runBtn = wrapper.querySelector(".wasm-run-btn");
137
- if (runBtn && !runBtn.disabled) {
138
- clearInterval(interval);
139
- console.log("[Script Mock] Playground loaded. Triggering run...");
140
- setTimeout(() => {
141
- runBtn.click();
142
- }, 1000);
143
- }
144
- }, 500);
145
- }, 1000);
146
- }
147
- }
99
+ // Create action button bar (always visible)
100
+ const btnBar = document.createElement("div");
101
+ btnBar.className = "wasm-quick-action-bar";
148
102
 
149
- // Replace block with the interactive editor and console
150
- function activatePlayground(wrapper, originalBlock, code, vm) {
151
- originalBlock.style.display = "none";
103
+ const previewBtn = document.createElement("button");
104
+ previewBtn.className = "wasm-quick-btn wasm-quick-preview-btn";
105
+ previewBtn.innerHTML = `
106
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;">
107
+ <polygon points="5 3 19 12 5 21 5 3"></polygon>
108
+ </svg>Live Preview
109
+ `;
152
110
 
153
- const playgroundContainer = document.createElement("div");
154
- playgroundContainer.className = "wasm-playground-container";
111
+ const downloadBtn = document.createElement("button");
112
+ downloadBtn.className = "wasm-quick-btn wasm-quick-download-btn";
113
+ downloadBtn.innerHTML = `
114
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;">
115
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
116
+ <polyline points="7 10 12 15 17 10"></polyline>
117
+ <line x1="12" y1="15" x2="12" y2="3"></line>
118
+ </svg>Download XLSX
119
+ `;
155
120
 
156
- const textarea = document.createElement("textarea");
157
- textarea.className = "wasm-editor";
158
- textarea.value = code;
159
- textarea.rows = Math.max(8, code.split("\n").length + 2);
121
+ const resetBtn = document.createElement("button");
122
+ resetBtn.className = "wasm-quick-btn wasm-quick-reset-btn";
123
+ resetBtn.innerHTML = `
124
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;">
125
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
126
+ <polyline points="3 3 3 8 8 8"></polyline>
127
+ </svg>Reset
128
+ `;
160
129
 
161
- const actionBar = document.createElement("div");
162
- actionBar.className = "wasm-action-bar";
130
+ btnBar.appendChild(previewBtn);
131
+ btnBar.appendChild(downloadBtn);
132
+ btnBar.appendChild(resetBtn);
133
+
134
+ // Insert button bar right below the wrapper in the DOM
135
+ wrapper.parentNode.insertBefore(btnBar, wrapper.nextSibling);
136
+
137
+ let isEditorInitialized = false;
138
+ let textarea = null;
139
+
140
+ // Full initialization of the transparent overlay editor (Deferred)
141
+ function initializeEditorOnDemand() {
142
+ if (isEditorInitialized) return;
143
+ isEditorInitialized = true;
144
+
145
+ // Create transparent textarea overlay
146
+ textarea = document.createElement("textarea");
147
+ textarea.className = "wasm-inline-editor";
148
+ textarea.value = originalCode;
149
+ textarea.spellcheck = false;
150
+ wrapper.insertBefore(textarea, block);
151
+
152
+ // Copy exact layout styles from pre block to align overlay perfectly
153
+ const preStyle = window.getComputedStyle(block);
154
+ const stylesToCopy = [
155
+ "fontFamily", "fontSize", "lineHeight", "fontWeight",
156
+ "paddingTop", "paddingBottom", "paddingLeft", "paddingRight",
157
+ "marginTop", "marginBottom", "marginLeft", "marginRight",
158
+ "textAlign"
159
+ ];
160
+ stylesToCopy.forEach(prop => {
161
+ textarea.style[prop] = preStyle[prop];
162
+ });
163
+
164
+ // Enforce overlapping styles
165
+ block.style.position = "relative";
166
+ block.style.pointerEvents = "none"; // Clicks pass to textarea
167
+ block.style.whiteSpace = "pre-wrap";
168
+ block.style.wordBreak = "break-all";
169
+ block.style.zIndex = "1";
170
+ block.style.margin = "0";
171
+
172
+ textarea.style.position = "absolute";
173
+ textarea.style.top = "0";
174
+ textarea.style.left = "0";
175
+ textarea.style.width = "100%";
176
+ textarea.style.height = "100%";
177
+ textarea.style.background = "transparent";
178
+ textarea.style.color = "transparent";
179
+ textarea.style.caretColor = "#2563eb"; // Sleek blue cursor
180
+ textarea.style.border = "none";
181
+ textarea.style.resize = "none";
182
+ textarea.style.overflow = "hidden";
183
+ textarea.style.whiteSpace = "pre-wrap";
184
+ textarea.style.wordBreak = "break-all";
185
+ textarea.style.zIndex = "2";
186
+ textarea.style.outline = "none";
187
+
188
+ textarea.addEventListener("input", updateHighlight);
189
+ updateHighlight();
190
+ }
163
191
 
164
- const runBtn = document.createElement("button");
165
- runBtn.className = "wasm-run-btn";
166
- runBtn.textContent = "Run & Download";
192
+ function updateHighlight() {
193
+ if (!textarea) return;
194
+ let code = textarea.value;
195
+ if (code.endsWith("\n")) {
196
+ code += " ";
197
+ }
198
+ block.innerHTML = highlightRuby(code);
167
199
 
168
- const resetBtn = document.createElement("button");
169
- resetBtn.className = "wasm-reset-btn";
170
- resetBtn.textContent = "Reset";
200
+ // Auto-grow height dynamically to fit text
201
+ textarea.style.height = "auto";
202
+ textarea.style.height = textarea.scrollHeight + "px";
203
+ block.style.height = textarea.scrollHeight + "px";
204
+ wrapper.style.height = textarea.scrollHeight + "px";
205
+ }
171
206
 
172
- const closeBtn = document.createElement("button");
173
- closeBtn.className = "wasm-close-btn";
174
- closeBtn.textContent = "Close";
207
+ // Trigger full initialization on mouse hover or click
208
+ wrapper.addEventListener("mouseenter", initializeEditorOnDemand, { once: true });
209
+ wrapper.addEventListener("click", initializeEditorOnDemand);
210
+
211
+ // RDoc-compatible Ruby Syntax Highlighter (Safe placeholder method)
212
+ function highlightRuby(code) {
213
+ let html = code
214
+ .replace(/&/g, "&amp;")
215
+ .replace(/</g, "&lt;")
216
+ .replace(/>/g, "&gt;");
217
+
218
+ const placeholders = [];
219
+ function addPlaceholder(val, className) {
220
+ const id = `__WASM_HL_${placeholders.length}__`;
221
+ placeholders.push({ id, val, className });
222
+ return id;
223
+ }
175
224
 
176
- actionBar.appendChild(runBtn);
177
- actionBar.appendChild(resetBtn);
178
- actionBar.appendChild(closeBtn);
225
+ // 1. Strings (double quotes)
226
+ html = html.replace(/"[^"\\]*(?:\\.[^"\\]*)*"/g, (match) => {
227
+ return addPlaceholder(match, "ruby-string");
228
+ });
229
+ // 2. Strings (single quotes)
230
+ html = html.replace(/'[^'\\]*(?:\\.[^'\\]*)*'/g, (match) => {
231
+ return addPlaceholder(match, "ruby-string");
232
+ });
233
+
234
+ // 3. Comments (safe from backtracking, only comments starting after whitespace or at line start)
235
+ html = html.replace(/(^|\s)(#[^\n]*)$/gm, (match, p1, p2) => {
236
+ return p1 + addPlaceholder(p2, "ruby-comment");
237
+ });
238
+
239
+ // 4. Keywords
240
+ const keywords = /\b(require|do|end|if|else|elsif|unless|while|until|for|in|class|module|def|return|true|false|nil)\b/g;
241
+ html = html.replace(keywords, (match) => {
242
+ return addPlaceholder(match, "ruby-keyword");
243
+ });
244
+
245
+ // 5. Constants (Capitalized names)
246
+ const constants = /\b([A-Z][a-zA-Z0-9_]*)\b/g;
247
+ html = html.replace(constants, (match) => {
248
+ return addPlaceholder(match, "ruby-constant");
249
+ });
250
+
251
+ // 6. Simple numeric values
252
+ const numericValues = /\b(\d+)\b/g;
253
+ html = html.replace(numericValues, (match) => {
254
+ return addPlaceholder(match, "ruby-value");
255
+ });
256
+
257
+ // 7. Specific xlsxrb APIs
258
+ const apis = /\b(generate|add_style|add_sheet|add_row|set_column|set_print_option|border_all|border_bottom|border_top|border_left|border_right|align_horizontal|align_vertical|bold|italic|size|fill_color|number_format)\b/g;
259
+ html = html.replace(apis, (match) => {
260
+ return addPlaceholder(match, "ruby-identifier");
261
+ });
262
+
263
+ // Rehydrate placeholders in reverse order to avoid nested conflicts
264
+ for (let i = placeholders.length - 1; i >= 0; i--) {
265
+ const item = placeholders[i];
266
+ html = html.replace(item.id, `<span class="${item.className}">${item.val}</span>`);
267
+ }
179
268
 
180
- const consoleArea = document.createElement("pre");
181
- consoleArea.className = "wasm-console";
182
- consoleArea.textContent = "Ready to run. Output and status will be shown here.";
269
+ return html;
270
+ }
183
271
 
184
- playgroundContainer.appendChild(textarea);
185
- playgroundContainer.appendChild(actionBar);
186
- playgroundContainer.appendChild(consoleArea);
187
- wrapper.appendChild(playgroundContainer);
188
272
 
189
- resetBtn.addEventListener("click", () => {
190
- textarea.value = code;
191
- consoleArea.textContent = "Reset to original code.";
192
- consoleArea.className = "wasm-console";
193
- });
194
273
 
195
- closeBtn.addEventListener("click", () => {
196
- playgroundContainer.remove();
197
- originalBlock.style.display = "block";
198
- wrapper.querySelector(".wasm-try-btn").style.display = "inline-flex";
274
+ // Click: Live Preview (1-click auto run)
275
+ previewBtn.addEventListener("click", () => {
276
+ const codeToRun = textarea ? textarea.value : originalCode;
277
+ triggerLivePreview(codeToRun);
199
278
  });
200
279
 
201
- runBtn.addEventListener("click", async () => {
202
- runBtn.disabled = true;
203
- runBtn.textContent = "Running...";
204
- consoleArea.textContent = "Executing Ruby code...";
205
- consoleArea.className = "wasm-console running";
206
- consoleBuffer = "";
207
-
208
- // Clear any preexisting XLSX files in the virtual FS root to avoid collision/leak
209
- clearXlsxFiles(vm, "/");
280
+ // Click: Download XLSX directly
281
+ downloadBtn.addEventListener("click", async () => {
282
+ downloadBtn.disabled = true;
283
+ const originalText = downloadBtn.innerHTML;
284
+ downloadBtn.innerHTML = `Running Wasm...`;
210
285
 
211
286
  try {
212
- let userCode = textarea.value;
213
- if (!userCode.includes('require "xlsxrb"') && !userCode.includes("require 'xlsxrb'")) {
287
+ const vm = await ensureRubyWasm();
288
+ clearXlsxFiles(vm, "/");
289
+ let userCode = textarea ? textarea.value : originalCode;
290
+ if (!/require\s+['"]xlsxrb['"]/.test(userCode)) {
214
291
  userCode = 'require "xlsxrb"\n' + userCode;
215
292
  }
216
- // Force include the current directory in load path
217
293
  userCode = '$LOAD_PATH << Dir.pwd unless $LOAD_PATH.include?(Dir.pwd)\n' + userCode;
218
294
 
219
- // Evaluate the user code in Wasm VM
220
295
  vm.eval(userCode);
221
-
222
- // Scan virtual FS for new XLSX output files
223
296
  const generatedFiles = scanXlsxFiles(vm, "/");
224
-
225
- consoleArea.className = "wasm-console success";
226
- let output = consoleBuffer;
227
-
228
297
  if (generatedFiles.length > 0) {
229
- const mainFile = generatedFiles[0];
230
- output += `\n[Success] Generated file: ${mainFile}\nTriggering download...`;
231
- downloadFileFromVfs(vm, mainFile);
298
+ downloadFileFromVfs(vm, generatedFiles[0]);
232
299
  } else {
233
- output += `\n[Warning] Execution succeeded, but no .xlsx file was generated in the root directory.`;
300
+ alert("Error: No XLSX file was generated.");
234
301
  }
235
-
236
- consoleArea.textContent = output || "Execution completed successfully (no stdout).";
237
-
238
302
  } catch (err) {
239
- consoleArea.className = "wasm-console error";
240
- consoleArea.textContent = `Error during execution:\n${err.message}\n\nConsole output:\n${consoleBuffer}`;
303
+ alert("Error generating XLSX: " + err.message);
241
304
  console.error(err);
242
-
243
- // Send runtime errors back to test server under E2E testing
244
- if (window.location.hash === "#test-mode") {
245
- fetch("http://localhost:9001/error", {
246
- method: "POST",
247
- body: `Error during execution:\n${err.message}\n\nConsole output:\n${consoleBuffer}`
248
- }).catch(e => console.error("Failed to post back error:", e));
249
- }
250
305
  } finally {
251
- runBtn.disabled = false;
252
- runBtn.textContent = "Run & Download";
306
+ downloadBtn.disabled = false;
307
+ downloadBtn.innerHTML = originalText;
308
+ }
309
+ });
310
+
311
+ // Click: Reset Code
312
+ resetBtn.addEventListener("click", () => {
313
+ if (textarea) {
314
+ textarea.value = originalCode;
315
+ updateHighlight();
253
316
  }
254
317
  });
318
+
319
+
320
+
321
+ }
322
+
323
+ // Open the sliding drawer panel and automatically execute code preview inside the iframe
324
+ function triggerLivePreview(code) {
325
+ const drawer = ensurePreviewDrawer();
326
+ const iframe = document.getElementById("wasmPreviewIframe");
327
+ const prefix = window.rdoc_rel_prefix || "./";
328
+
329
+ // Show the drawer
330
+ drawer.classList.add("open");
331
+
332
+ // Initialize or load the iframe
333
+ const targetSrc = `${prefix}preview.html`;
334
+ if (!iframe.src || !iframe.src.includes("preview.html")) {
335
+ iframe.src = targetSrc;
336
+ iframe.onload = () => {
337
+ sendCodeToIframe();
338
+ };
339
+ } else {
340
+ sendCodeToIframe();
341
+ }
342
+
343
+ function sendCodeToIframe() {
344
+ if (iframe.contentWindow) {
345
+ iframe.contentWindow.postMessage({
346
+ action: "load_code",
347
+ code: code
348
+ }, "*");
349
+ }
350
+ }
255
351
  }
256
352
 
257
353
  // Delete all .xlsx files recursively under the virtual directory
@@ -305,16 +401,39 @@
305
401
  document.body.removeChild(a);
306
402
  setTimeout(() => URL.revokeObjectURL(url), 10000);
307
403
 
308
- // Post binary data back to test server under E2E testing
309
- if (window.location.hash === "#test-mode") {
310
- console.log("[Script Mock] Test mode detected. Sending generated file back to test server...");
311
- fetch("http://localhost:9001/upload", {
312
- method: "POST",
313
- body: bytes
314
- }).catch(err => console.error("Failed to post back test file:", err));
315
- }
316
404
  } catch (e) {
317
405
  console.error("Error downloading file from Vfs:", e);
318
406
  }
319
407
  }
408
+
409
+ // Create or retrieve the side drawer preview panel
410
+ function ensurePreviewDrawer() {
411
+ let drawer = document.getElementById("wasmPreviewDrawer");
412
+ if (drawer) return drawer;
413
+
414
+ drawer = document.createElement("div");
415
+ drawer.id = "wasmPreviewDrawer";
416
+ drawer.className = "wasm-preview-drawer";
417
+ drawer.innerHTML = `
418
+ <div class="drawer-header">
419
+ <span class="drawer-title">Live Spreadsheet Preview</span>
420
+ <button class="drawer-close-btn">&times;</button>
421
+ </div>
422
+ <div class="drawer-body">
423
+ <iframe id="wasmPreviewIframe" class="drawer-iframe" src=""></iframe>
424
+ </div>
425
+ `;
426
+ document.body.appendChild(drawer);
427
+
428
+ // Setup close action
429
+ const closeBtn = drawer.querySelector(".drawer-close-btn");
430
+ closeBtn.addEventListener("click", () => {
431
+ drawer.classList.remove("open");
432
+ // Clear iframe src on close to stop background Wasm threads and free memory
433
+ const iframe = document.getElementById("wasmPreviewIframe");
434
+ if (iframe) iframe.src = "";
435
+ });
436
+
437
+ return drawer;
438
+ }
320
439
  })();