juxscript 1.1.153 → 1.1.155
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.
- package/machinery/compiler4.js +6 -0
- package/machinery/errors.js +294 -110
- package/package.json +1 -1
package/machinery/compiler4.js
CHANGED
|
@@ -508,6 +508,12 @@ export class JuxCompiler {
|
|
|
508
508
|
fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
|
|
509
509
|
console.log(`📸 Source snapshot written`);
|
|
510
510
|
|
|
511
|
+
// ✅ Also write to dist root so the error overlay can fetch it via /__jux_sources.json
|
|
512
|
+
const distSnapshotPath = path.join(this.config.distDir, '__jux_sources.json');
|
|
513
|
+
if (distSnapshotPath !== snapshotPath) {
|
|
514
|
+
fs.writeFileSync(distSnapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
|
|
515
|
+
}
|
|
516
|
+
|
|
511
517
|
const validation = this.reportValidationIssues();
|
|
512
518
|
if (!validation.isValid) {
|
|
513
519
|
console.log('🛑 BUILD FAILED\n');
|
package/machinery/errors.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generates an injectable client-side error collector script.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Full-screen overlay similar to Vite's error screen.
|
|
4
|
+
* Resolves bundle line numbers back to original .jux source using __jux_sources.json.
|
|
5
5
|
*
|
|
6
6
|
* @param {Object} options
|
|
7
|
-
* @param {boolean} [options.enabled=true]
|
|
8
|
-
* @param {number} [options.maxErrors=50]
|
|
9
|
-
* @param {string} [options.position='bottom-right'] - Panel position
|
|
7
|
+
* @param {boolean} [options.enabled=true]
|
|
8
|
+
* @param {number} [options.maxErrors=50]
|
|
10
9
|
* @returns {string} Inline script to inject into HTML
|
|
11
10
|
*/
|
|
12
11
|
export function generateErrorCollector(options = {}) {
|
|
13
12
|
const {
|
|
14
13
|
enabled = true,
|
|
15
|
-
maxErrors = 50
|
|
16
|
-
position = 'bottom-right'
|
|
14
|
+
maxErrors = 50
|
|
17
15
|
} = options;
|
|
18
16
|
|
|
19
17
|
if (!enabled) return '';
|
|
@@ -21,93 +19,249 @@ export function generateErrorCollector(options = {}) {
|
|
|
21
19
|
return `
|
|
22
20
|
<script>
|
|
23
21
|
(function() {
|
|
24
|
-
var
|
|
25
|
-
var
|
|
26
|
-
var
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
22
|
+
var __errors = [];
|
|
23
|
+
var __maxErrors = ${maxErrors};
|
|
24
|
+
var __overlay = null;
|
|
25
|
+
var __list = null;
|
|
26
|
+
var __sources = null;
|
|
27
|
+
var __sourcesFetched = false;
|
|
28
|
+
var __bundleLines = null;
|
|
29
|
+
|
|
30
|
+
// Fetch source map + bundle on first error
|
|
31
|
+
function loadSources(cb) {
|
|
32
|
+
if (__sourcesFetched) return cb();
|
|
33
|
+
__sourcesFetched = true;
|
|
34
|
+
|
|
35
|
+
var done = 0;
|
|
36
|
+
function check() { if (++done >= 2) cb(); }
|
|
37
|
+
|
|
38
|
+
// Fetch source snapshot
|
|
39
|
+
var xhr1 = new XMLHttpRequest();
|
|
40
|
+
xhr1.open('GET', '/__jux_sources.json', true);
|
|
41
|
+
xhr1.onload = function() {
|
|
42
|
+
try { __sources = JSON.parse(xhr1.responseText); } catch(e) {}
|
|
43
|
+
check();
|
|
44
|
+
};
|
|
45
|
+
xhr1.onerror = check;
|
|
46
|
+
xhr1.send();
|
|
47
|
+
|
|
48
|
+
// Fetch bundle to split into lines
|
|
49
|
+
var xhr2 = new XMLHttpRequest();
|
|
50
|
+
xhr2.open('GET', '/bundle.js', true);
|
|
51
|
+
xhr2.onload = function() {
|
|
52
|
+
try { __bundleLines = xhr2.responseText.split('\\n'); } catch(e) {}
|
|
53
|
+
check();
|
|
54
|
+
};
|
|
55
|
+
xhr2.onerror = check;
|
|
56
|
+
xhr2.send();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Given a bundle line number, find which view function it belongs to
|
|
60
|
+
// and map back to the original .jux source line
|
|
61
|
+
function resolveSourceLocation(bundleLine) {
|
|
62
|
+
if (!__sources || !__bundleLines || !bundleLine) return null;
|
|
63
|
+
|
|
64
|
+
// Find the renderJuxN function that contains this line
|
|
65
|
+
// by scanning backwards from bundleLine for "async function renderJux"
|
|
66
|
+
var funcName = null;
|
|
67
|
+
var funcStartLine = 0;
|
|
68
|
+
for (var i = bundleLine - 1; i >= 0; i--) {
|
|
69
|
+
var line = __bundleLines[i];
|
|
70
|
+
if (!line) continue;
|
|
71
|
+
var match = line.match(/async\\s+function\\s+(renderJux\\d+)/);
|
|
72
|
+
if (match) {
|
|
73
|
+
funcName = match[1];
|
|
74
|
+
funcStartLine = i + 1; // 1-based
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!funcName) return null;
|
|
80
|
+
|
|
81
|
+
// Find the source entry that maps to this function
|
|
82
|
+
var sourceEntry = null;
|
|
83
|
+
for (var key in __sources) {
|
|
84
|
+
if (__sources[key].functionName === funcName) {
|
|
85
|
+
sourceEntry = __sources[key];
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!sourceEntry) return null;
|
|
91
|
+
|
|
92
|
+
// Calculate approximate original line
|
|
93
|
+
// bundleLine is inside the function body, funcStartLine is the function declaration
|
|
94
|
+
// +1 because function body starts after the declaration line
|
|
95
|
+
var offsetInFunc = bundleLine - funcStartLine - 1;
|
|
96
|
+
var originalLines = sourceEntry.lines || [];
|
|
97
|
+
var approxLine = Math.max(0, Math.min(offsetInFunc, originalLines.length - 1));
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
file: sourceEntry.file,
|
|
101
|
+
name: sourceEntry.name,
|
|
102
|
+
functionName: funcName,
|
|
103
|
+
originalLine: approxLine + 1,
|
|
104
|
+
originalCode: originalLines[approxLine] || '',
|
|
105
|
+
// Provide surrounding context (3 lines before/after)
|
|
106
|
+
context: originalLines.slice(
|
|
107
|
+
Math.max(0, approxLine - 3),
|
|
108
|
+
Math.min(originalLines.length, approxLine + 4)
|
|
109
|
+
),
|
|
110
|
+
contextStartLine: Math.max(0, approxLine - 3) + 1,
|
|
111
|
+
highlightLine: approxLine + 1
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse stack trace and extract bundle line numbers
|
|
116
|
+
function parseStack(stack) {
|
|
117
|
+
if (!stack) return [];
|
|
118
|
+
var frames = [];
|
|
119
|
+
var lines = stack.split('\\n');
|
|
120
|
+
for (var i = 0; i < lines.length; i++) {
|
|
121
|
+
// Match patterns like "at funcName (url:line:col)" or "at url:line:col"
|
|
122
|
+
var m = lines[i].match(/(?:at\\s+(.+?)\\s+\\()?(?:https?:\\/\\/[^:]+):([\\d]+):([\\d]+)\\)?/);
|
|
123
|
+
if (m) {
|
|
124
|
+
frames.push({
|
|
125
|
+
raw: lines[i].trim(),
|
|
126
|
+
func: m[1] || '(anonymous)',
|
|
127
|
+
line: parseInt(m[2], 10),
|
|
128
|
+
col: parseInt(m[3], 10)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return frames;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createOverlay() {
|
|
136
|
+
__overlay = document.createElement('div');
|
|
137
|
+
__overlay.id = '__jux-error-overlay';
|
|
138
|
+
__overlay.setAttribute('style', [
|
|
139
|
+
'position:fixed',
|
|
140
|
+
'top:0',
|
|
141
|
+
'left:0',
|
|
142
|
+
'width:100vw',
|
|
143
|
+
'height:100vh',
|
|
144
|
+
'z-index:99999',
|
|
145
|
+
'background:rgba(0,0,0,0.85)',
|
|
146
|
+
'color:#f8f8f8',
|
|
147
|
+
'font-family:monospace',
|
|
148
|
+
'font-size:14px',
|
|
149
|
+
'overflow-y:auto',
|
|
150
|
+
'padding:0',
|
|
151
|
+
'margin:0',
|
|
152
|
+
'box-sizing:border-box'
|
|
153
|
+
].join(';'));
|
|
154
|
+
|
|
155
|
+
var container = document.createElement('div');
|
|
156
|
+
container.setAttribute('style', [
|
|
157
|
+
'max-width:960px',
|
|
158
|
+
'margin:40px auto',
|
|
159
|
+
'padding:24px 32px',
|
|
160
|
+
'border:2px solid #ff5555',
|
|
161
|
+
'border-radius:8px',
|
|
162
|
+
'background:#1a1a1a'
|
|
163
|
+
].join(';'));
|
|
164
|
+
|
|
66
165
|
var header = document.createElement('div');
|
|
67
|
-
header.setAttribute('style',
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
|
|
72
|
-
|
|
166
|
+
header.setAttribute('style', [
|
|
167
|
+
'display:flex',
|
|
168
|
+
'justify-content:space-between',
|
|
169
|
+
'align-items:center',
|
|
170
|
+
'margin-bottom:16px',
|
|
171
|
+
'padding-bottom:12px',
|
|
172
|
+
'border-bottom:1px solid #333'
|
|
173
|
+
].join(';'));
|
|
174
|
+
|
|
175
|
+
var title = document.createElement('div');
|
|
176
|
+
title.setAttribute('style', 'color:#ff5555;font-size:16px;font-weight:bold;');
|
|
177
|
+
title.textContent = 'Runtime Error';
|
|
178
|
+
header.appendChild(title);
|
|
179
|
+
|
|
180
|
+
var actions = document.createElement('div');
|
|
181
|
+
actions.setAttribute('style', 'display:flex;gap:8px;');
|
|
73
182
|
|
|
74
183
|
var clearBtn = document.createElement('button');
|
|
75
184
|
clearBtn.textContent = 'Clear';
|
|
76
|
-
clearBtn.setAttribute('style',
|
|
77
|
-
'background:transparent
|
|
78
|
-
'
|
|
79
|
-
|
|
185
|
+
clearBtn.setAttribute('style', [
|
|
186
|
+
'background:transparent',
|
|
187
|
+
'border:1px solid #555',
|
|
188
|
+
'color:#aaa',
|
|
189
|
+
'padding:4px 12px',
|
|
190
|
+
'border-radius:4px',
|
|
191
|
+
'cursor:pointer',
|
|
192
|
+
'font-family:monospace',
|
|
193
|
+
'font-size:12px'
|
|
194
|
+
].join(';'));
|
|
80
195
|
clearBtn.addEventListener('click', clearErrors);
|
|
81
|
-
header.appendChild(clearBtn);
|
|
82
196
|
|
|
83
|
-
|
|
197
|
+
var closeBtn = document.createElement('button');
|
|
198
|
+
closeBtn.textContent = 'Close';
|
|
199
|
+
closeBtn.setAttribute('style', [
|
|
200
|
+
'background:transparent',
|
|
201
|
+
'border:1px solid #555',
|
|
202
|
+
'color:#aaa',
|
|
203
|
+
'padding:4px 12px',
|
|
204
|
+
'border-radius:4px',
|
|
205
|
+
'cursor:pointer',
|
|
206
|
+
'font-family:monospace',
|
|
207
|
+
'font-size:12px'
|
|
208
|
+
].join(';'));
|
|
209
|
+
closeBtn.addEventListener('click', function() {
|
|
210
|
+
__overlay.style.display = 'none';
|
|
211
|
+
});
|
|
84
212
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
90
|
-
__juxPanel.appendChild(__juxList);
|
|
213
|
+
actions.appendChild(clearBtn);
|
|
214
|
+
actions.appendChild(closeBtn);
|
|
215
|
+
header.appendChild(actions);
|
|
216
|
+
container.appendChild(header);
|
|
91
217
|
|
|
92
|
-
document.
|
|
93
|
-
|
|
218
|
+
__list = document.createElement('div');
|
|
219
|
+
container.appendChild(__list);
|
|
94
220
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
__juxPanel.style.display = __juxVisible ? 'flex' : 'none';
|
|
221
|
+
__overlay.appendChild(container);
|
|
222
|
+
document.body.appendChild(__overlay);
|
|
98
223
|
}
|
|
99
224
|
|
|
100
225
|
function clearErrors() {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
226
|
+
__errors = [];
|
|
227
|
+
if (__list) __list.innerHTML = '';
|
|
228
|
+
if (__overlay) __overlay.style.display = 'none';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderSourceContext(resolved) {
|
|
232
|
+
if (!resolved) return '';
|
|
233
|
+
|
|
234
|
+
var lines = resolved.context || [];
|
|
235
|
+
var startLine = resolved.contextStartLine || 1;
|
|
236
|
+
var highlight = resolved.highlightLine || -1;
|
|
237
|
+
|
|
238
|
+
var html = '<div style="margin-top:8px;padding:8px;background:#111;border:1px solid #333;border-radius:4px;">';
|
|
239
|
+
html += '<div style="color:#ff5555;font-size:12px;margin-bottom:6px;font-weight:bold;">' +
|
|
240
|
+
escapeHtml(resolved.file) + ':' + resolved.originalLine +
|
|
241
|
+
'</div>';
|
|
242
|
+
|
|
243
|
+
for (var i = 0; i < lines.length; i++) {
|
|
244
|
+
var lineNum = startLine + i;
|
|
245
|
+
var isHighlight = lineNum === highlight;
|
|
246
|
+
var bg = isHighlight ? 'background:#3a1a1a;' : '';
|
|
247
|
+
var color = isHighlight ? 'color:#ff8888;' : 'color:#888;';
|
|
248
|
+
var numColor = isHighlight ? 'color:#ff5555;' : 'color:#555;';
|
|
249
|
+
var marker = isHighlight ? '>' : ' ';
|
|
250
|
+
|
|
251
|
+
html += '<div style="' + bg + 'padding:1px 8px;white-space:pre;">' +
|
|
252
|
+
'<span style="' + numColor + 'display:inline-block;width:40px;text-align:right;margin-right:12px;user-select:none;">' +
|
|
253
|
+
marker + ' ' + lineNum +
|
|
254
|
+
'</span>' +
|
|
255
|
+
'<span style="' + color + '">' + escapeHtml(lines[i]) + '</span>' +
|
|
256
|
+
'</div>';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
html += '</div>';
|
|
260
|
+
return html;
|
|
107
261
|
}
|
|
108
262
|
|
|
109
263
|
function addError(type, message, source, line, col, stack) {
|
|
110
|
-
if (
|
|
264
|
+
if (__errors.length >= __maxErrors) __errors.shift();
|
|
111
265
|
|
|
112
266
|
var entry = {
|
|
113
267
|
type: type,
|
|
@@ -118,39 +272,73 @@ export function generateErrorCollector(options = {}) {
|
|
|
118
272
|
stack: stack || '',
|
|
119
273
|
time: new Date().toLocaleTimeString()
|
|
120
274
|
};
|
|
121
|
-
|
|
275
|
+
__errors.push(entry);
|
|
122
276
|
|
|
123
|
-
if (!
|
|
124
|
-
|
|
125
|
-
// Update badge
|
|
126
|
-
__juxBadge.textContent = String(__juxErrors.length);
|
|
127
|
-
__juxBadge.style.display = 'flex';
|
|
128
|
-
|
|
129
|
-
// ✅ Auto-open panel on first error
|
|
130
|
-
if (!__juxVisible) {
|
|
131
|
-
__juxVisible = true;
|
|
132
|
-
__juxPanel.style.display = 'flex';
|
|
133
|
-
}
|
|
277
|
+
if (!__overlay) createOverlay();
|
|
278
|
+
__overlay.style.display = 'block';
|
|
134
279
|
|
|
135
|
-
// Add to list
|
|
136
280
|
var item = document.createElement('div');
|
|
137
|
-
item.setAttribute('style',
|
|
138
|
-
'padding:
|
|
139
|
-
'
|
|
140
|
-
|
|
281
|
+
item.setAttribute('style', [
|
|
282
|
+
'padding:12px 16px',
|
|
283
|
+
'margin-bottom:8px',
|
|
284
|
+
'background:#222',
|
|
285
|
+
'border-left:3px solid #ff5555',
|
|
286
|
+
'border-radius:4px'
|
|
287
|
+
].join(';'));
|
|
141
288
|
|
|
142
|
-
var
|
|
289
|
+
var typeLabel = type === 'error' ? 'Error' : type === 'rejection' ? 'Unhandled Rejection' : 'console.error';
|
|
143
290
|
|
|
144
291
|
item.innerHTML =
|
|
145
|
-
'<div style="color:#888;font-size:
|
|
146
|
-
|
|
147
|
-
|
|
292
|
+
'<div style="color:#888;font-size:12px;margin-bottom:4px;">' +
|
|
293
|
+
typeLabel +
|
|
294
|
+
'<span style="float:right;">' + entry.time + '</span>' +
|
|
148
295
|
'</div>' +
|
|
149
|
-
'<div style="word-break:break-word;">' +
|
|
150
|
-
|
|
296
|
+
'<div style="color:#ff8888;font-size:14px;white-space:pre-wrap;word-break:break-word;font-weight:bold;">' +
|
|
297
|
+
escapeHtml(entry.message) +
|
|
298
|
+
'</div>';
|
|
299
|
+
|
|
300
|
+
__list.appendChild(item);
|
|
301
|
+
|
|
302
|
+
// Resolve source location asynchronously
|
|
303
|
+
loadSources(function() {
|
|
304
|
+
var frames = parseStack(entry.stack);
|
|
305
|
+
var resolved = null;
|
|
306
|
+
|
|
307
|
+
// Try each frame until we find one that maps to a .jux source
|
|
308
|
+
for (var i = 0; i < frames.length; i++) {
|
|
309
|
+
var r = resolveSourceLocation(frames[i].line);
|
|
310
|
+
if (r) {
|
|
311
|
+
resolved = r;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Also try the direct line/col from the error itself
|
|
317
|
+
if (!resolved && entry.line) {
|
|
318
|
+
resolved = resolveSourceLocation(entry.line);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (resolved) {
|
|
322
|
+
// Add source context block
|
|
323
|
+
var sourceBlock = document.createElement('div');
|
|
324
|
+
sourceBlock.innerHTML = renderSourceContext(resolved);
|
|
325
|
+
item.appendChild(sourceBlock);
|
|
326
|
+
}
|
|
151
327
|
|
|
152
|
-
|
|
153
|
-
|
|
328
|
+
// Add raw stack (collapsed) if present
|
|
329
|
+
if (entry.stack) {
|
|
330
|
+
var stackBlock = document.createElement('details');
|
|
331
|
+
stackBlock.setAttribute('style', 'margin-top:8px;');
|
|
332
|
+
stackBlock.innerHTML =
|
|
333
|
+
'<summary style="cursor:pointer;color:#555;font-size:12px;user-select:none;">Raw stack trace</summary>' +
|
|
334
|
+
'<pre style="margin:4px 0 0;padding:8px;font-size:12px;color:#666;' +
|
|
335
|
+
'white-space:pre-wrap;background:#111;border-radius:4px;' +
|
|
336
|
+
'border:1px solid #333;max-height:200px;overflow-y:auto;">' +
|
|
337
|
+
escapeHtml(entry.stack) +
|
|
338
|
+
'</pre>';
|
|
339
|
+
item.appendChild(stackBlock);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
154
342
|
}
|
|
155
343
|
|
|
156
344
|
function escapeHtml(str) {
|
|
@@ -159,12 +347,10 @@ export function generateErrorCollector(options = {}) {
|
|
|
159
347
|
|
|
160
348
|
// --- Hooks ---
|
|
161
349
|
|
|
162
|
-
// 1. window.onerror
|
|
163
350
|
window.onerror = function(msg, source, line, col, err) {
|
|
164
351
|
addError('error', msg, source, line, col, err && err.stack ? err.stack : '');
|
|
165
352
|
};
|
|
166
353
|
|
|
167
|
-
// 2. Unhandled promise rejections
|
|
168
354
|
window.addEventListener('unhandledrejection', function(e) {
|
|
169
355
|
var reason = e.reason;
|
|
170
356
|
var msg = reason instanceof Error ? reason.message : String(reason);
|
|
@@ -172,7 +358,6 @@ export function generateErrorCollector(options = {}) {
|
|
|
172
358
|
addError('rejection', msg, '', 0, 0, stack);
|
|
173
359
|
});
|
|
174
360
|
|
|
175
|
-
// 3. Intercept console.error
|
|
176
361
|
var _origConsoleError = console.error;
|
|
177
362
|
console.error = function() {
|
|
178
363
|
var args = Array.prototype.slice.call(arguments);
|
|
@@ -183,11 +368,10 @@ export function generateErrorCollector(options = {}) {
|
|
|
183
368
|
_origConsoleError.apply(console, arguments);
|
|
184
369
|
};
|
|
185
370
|
|
|
186
|
-
// Expose API for programmatic access
|
|
187
371
|
window.__juxErrors = {
|
|
188
|
-
list: function() { return
|
|
372
|
+
list: function() { return __errors.slice(); },
|
|
189
373
|
clear: clearErrors,
|
|
190
|
-
count: function() { return
|
|
374
|
+
count: function() { return __errors.length; }
|
|
191
375
|
};
|
|
192
376
|
})();
|
|
193
377
|
</script>`;
|