lilact 0.26.14 → 0.26.16

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.
Files changed (35) hide show
  1. package/dist/lilact.development.js +528 -291
  2. package/dist/lilact.development.js.map +3 -3
  3. package/dist/lilact.development.min.js +59 -50
  4. package/dist/lilact.development.min.js.map +3 -3
  5. package/dist/lilact.production.min.js +59 -50
  6. package/docs/assets/navigation.js +1 -1
  7. package/docs/assets/search.js +1 -1
  8. package/docs/functions/errors.globalErrorHandler.html +2 -8
  9. package/docs/functions/errors.mapLocation.html +1 -0
  10. package/docs/functions/errors.scanBlockLabels.html +2 -0
  11. package/docs/functions/errors.traceError.html +3 -7
  12. package/docs/functions/run.lazy.html +2 -13
  13. package/docs/functions/run.require.html +2 -9
  14. package/docs/functions/run.run.html +2 -5
  15. package/docs/functions/run.runScripts.html +1 -7
  16. package/docs/functions/timers.timeoutPromise.html +2 -2
  17. package/docs/modules/errors.html +1 -1
  18. package/docs/static/lilact.development.js +528 -291
  19. package/docs/static/lilact.development.js.map +3 -3
  20. package/docs/static/lilact.development.min.js +59 -50
  21. package/docs/static/lilact.development.min.js.map +3 -3
  22. package/docs/static/lilact.production.min.js +59 -50
  23. package/docs/variables/errors.blocks_info.html +1 -0
  24. package/docs/variables/errors.error.html +1 -0
  25. package/examples/lilact.development.js +528 -291
  26. package/examples/lilact.development.js.map +3 -3
  27. package/examples/lilact.development.min.js +59 -50
  28. package/examples/lilact.development.min.js.map +3 -3
  29. package/examples/lilact.production.min.js +59 -50
  30. package/package.json +1 -1
  31. package/scripts/esbuild-preprocessor-plugin.cjs +1 -0
  32. package/src/errors.jsx +525 -237
  33. package/src/jsx.js +4 -2
  34. package/src/lilact.jsx +1 -1
  35. package/src/run.jsx +347 -263
package/src/errors.jsx CHANGED
@@ -27,273 +27,561 @@
27
27
  THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
28
 
29
29
  */
30
- import Lilact from './lilact.jsx';
30
+ /*
31
+ Lilact
32
+ Copyright (C) 2024-2026 Arash Kazemi
33
+ BSD-2-Clause
34
+ */
31
35
 
32
- import { required_scripts } from './run.jsx'
33
- import { pauseTimers } from './timers.jsx'
36
+ import Lilact from "./lilact.jsx";
37
+ import { required_scripts } from "./run.jsx";
34
38
  import { css } from "@emotion/css";
35
39
 
36
- // Lilact API
40
+ function numberOrNull(value) {
41
+ const n = Number(value);
42
+ return Number.isFinite(n) ? n : null;
43
+ }
44
+
45
+ function firstNumber(...values) {
46
+ for (const value of values) {
47
+ const n = numberOrNull(value);
48
+ if (n !== null) return n;
49
+ }
50
+ return null;
51
+ }
52
+
53
+ function asError(value) {
54
+ if (value instanceof Error) return value;
55
+
56
+ if (value && typeof value === "object") {
57
+ if (value.error instanceof Error) return value.error;
58
+
59
+ const error = new Error(
60
+ value.message == null ? String(value) : String(value.message)
61
+ );
62
+
63
+ if (value.name) error.name = value.name;
64
+ if (value.stack) error.stack = value.stack;
65
+
66
+ for (const key of Object.keys(value)) {
67
+ if (!(key in error)) error[key] = value[key];
68
+ }
37
69
 
38
- function getErrorLocation(err) // works for both error and error-event, and also in node env
39
- {
40
- if(err.lineno!==undefined || err.line!==undefined || err.lineNumber!==undefined) {
41
- const l = err.lineNumber || err.lineno || err.line;
42
- const c = err.columnNumber || err.colno || err.column;
70
+ return error;
71
+ }
43
72
 
44
- return {line: l, col: c};
45
- }
73
+ return new Error(String(value));
74
+ }
46
75
 
47
- let match = /:(\d+):(\d+)[\n].*/m.exec(err.stack);
48
- if(match===null) {
49
- match = /:(\d+):(\d+)\)\s+at .*/m.exec(err.stack);
50
- }
76
+ function isParserError(error) {
77
+ return error?.name === "JSXParserError";
78
+ }
51
79
 
52
- if(match) {
53
- return {line: parseInt(match[1]), col: parseInt(match[2])}
54
- }
80
+ /*
81
+ Returns zero-based line and column numbers.
55
82
 
56
- return null;
83
+ Parser errors produced by jsx.js already use zero-based positions.
84
+ Browser Error objects and ErrorEvents normally use one-based positions.
85
+ */
86
+ function getErrorLocation(error, { zeroBased = false } = {}) {
87
+ const line = firstNumber(
88
+ error?.lineNumber,
89
+ error?.lineno,
90
+ error?.line
91
+ );
92
+
93
+ const column = firstNumber(
94
+ error?.columnNumber,
95
+ error?.colno,
96
+ error?.column
97
+ );
98
+
99
+ if (line !== null || column !== null) {
100
+ return {
101
+ line:
102
+ line === null
103
+ ? null
104
+ : zeroBased
105
+ ? line
106
+ : Math.max(0, line - 1),
107
+
108
+ col:
109
+ column === null
110
+ ? null
111
+ : zeroBased
112
+ ? column
113
+ : Math.max(0, column - 1),
114
+ };
115
+ }
116
+
117
+ return null;
57
118
  }
58
119
 
59
- // Extract { url, line, col } from an Error.stack.
60
- // Works with common formats like:
61
- // Chrome/Edge: at fn (eval:xxx:LINE:COL)
62
- // Firefox: fn@eval:xxx:LINE:COL
63
- // Safari: @eval:xxx:LINE:COL (sometimes)
64
- /** @ignore */
65
- function parseEvalLocationFromStack(stack, urlPrefix = "eval:/")
66
- {
67
- const raw = typeof stack === "string" ? stack : String(stack || "");
68
- const lines = raw.split(/\r?\n/);
69
-
70
- // Match "... (eval:path:LINE:COL)" or "... eval:path:LINE:COL"
71
- // Group 1: url, group 2: line, group 3: col (col optional in some formats)
72
- const re = new RegExp(`\\(?((?:${escapeRegExp(urlPrefix)})[^\\s):]+):(\\d+):(?:(\\d+))?\\)?$`);
73
-
74
- for (const l of lines) {
75
- const line = l.trim();
76
- if (!line.includes(urlPrefix)) continue;
77
-
78
- const m = line.match(re);
79
- if (!m) continue;
80
-
81
- const url = m[1];
82
- const parsedLine = Number(m[2]);
83
- const parsedCol = m[3] == null ? null : Number(m[3]);
84
-
85
- if (Number.isFinite(parsedLine) && (parsedCol === null || Number.isFinite(parsedCol))) {
86
- return { url, line: parsedLine, col: parsedCol, matched: line };
120
+ function sourceUrlFromStack(stack) {
121
+ const text = typeof stack === "string" ? stack : "";
122
+
123
+ /*
124
+ * The final two colon-separated numeric values are the line and column.
125
+ * The path is intentionally greedy so paths containing ':' still work.
126
+ *
127
+ * Examples:
128
+ *
129
+ * at fn (eval:/components/App.jsx:12:8)
130
+ * fn@eval:/components/App.jsx:12:8
131
+ * eval:/components/App.jsx:12:8
132
+ */
133
+ const expression = /(?:^|[\s(@])((?:eval:\/)[\s\S]*):(\d+):(\d+)(?:\)?$)/;
134
+
135
+ for (const line of text.split(/\r?\n/)) {
136
+ const match = line.trim().match(expression);
137
+ if (!match) continue;
138
+
139
+ return {
140
+ url: match[1],
141
+ line: Number(match[2]),
142
+ col: Number(match[3]),
143
+ };
144
+ }
145
+
146
+ return null;
147
+ }
148
+
149
+ function sourcePathFromEvalUrl(url) {
150
+ if (typeof url !== "string") return null;
151
+
152
+ if (url.startsWith("eval:/")) {
153
+ return url.slice("eval:/".length);
154
+ }
155
+
156
+ if (url.startsWith("eval:")) {
157
+ return url.slice("eval:".length).replace(/^\/+/, "");
158
+ }
159
+
160
+ return null;
161
+ }
162
+
163
+ /*
164
+ Mappings generated by jsx.js have this shape:
165
+
166
+ [
167
+ generatedLine,
168
+ generatedColumn,
169
+ sourceLine,
170
+ sourceColumn,
171
+ node
172
+ ]
173
+
174
+ All four coordinates are zero-based.
175
+
176
+ The correct mapping is the closest mapping at or before the generated
177
+ location. The old implementation searched in the opposite direction and
178
+ used the final mapping for errors occurring before the first mapping.
179
+ */
180
+ export function mapLocation(mappings, generatedLine, generatedColumn) {
181
+ if (!Array.isArray(mappings) || mappings.length === 0) {
182
+ return {
183
+ line: generatedLine,
184
+ col: generatedColumn,
185
+ };
186
+ }
187
+
188
+ const line = Number(generatedLine);
189
+ const col = Number(generatedColumn);
190
+
191
+ if (!Number.isFinite(line) || !Number.isFinite(col)) {
192
+ return {
193
+ line: null,
194
+ col: null,
195
+ };
196
+ }
197
+
198
+ const validMappings = mappings
199
+ .filter(
200
+ (mapping) =>
201
+ Array.isArray(mapping) &&
202
+ mapping.length >= 4 &&
203
+ Number.isFinite(Number(mapping[0])) &&
204
+ Number.isFinite(Number(mapping[1])) &&
205
+ Number.isFinite(Number(mapping[2])) &&
206
+ Number.isFinite(Number(mapping[3]))
207
+ )
208
+ .slice()
209
+ .sort((a, b) => {
210
+ const lineDifference = Number(a[0]) - Number(b[0]);
211
+
212
+ if (lineDifference !== 0) {
213
+ return lineDifference;
214
+ }
215
+
216
+ return Number(a[1]) - Number(b[1]);
217
+ });
218
+
219
+ if (validMappings.length === 0) {
220
+ return {
221
+ line,
222
+ col,
223
+ };
224
+ }
225
+
226
+ /*
227
+ * Find the last mapping whose generated position is before or equal
228
+ * to the runtime position.
229
+ */
230
+ let mapping = validMappings[0];
231
+
232
+ for (const candidate of validMappings) {
233
+ const generatedMappingLine = Number(candidate[0]);
234
+ const generatedMappingColumn = Number(candidate[1]);
235
+
236
+ if (
237
+ generatedMappingLine < line ||
238
+ (
239
+ generatedMappingLine === line &&
240
+ generatedMappingColumn <= col
241
+ )
242
+ ) {
243
+ mapping = candidate;
244
+ continue;
87
245
  }
246
+
247
+ break;
88
248
  }
89
249
 
90
- return { url: null, line: null, col: null, matched: null, stackPreview: lines.slice(0, 6).join("\n") };
250
+ const mappingGeneratedLine = Number(mapping[0]);
251
+ const mappingGeneratedColumn = Number(mapping[1]);
252
+ const mappingSourceLine = Number(mapping[2]);
253
+ const mappingSourceColumn = Number(mapping[3]);
254
+
255
+ /*
256
+ * A mapping points to the beginning of a generated region. Preserve
257
+ * the relative line offset inside that region.
258
+ *
259
+ * This is the important part. Returning mappingSourceLine directly
260
+ * causes errors inside a generated multi-line block to point to the
261
+ * first line of the block.
262
+ */
263
+ const sourceLine =
264
+ mappingSourceLine + (line - mappingGeneratedLine);
265
+
266
+ /*
267
+ * Columns are only safely relative when the generated error is on the
268
+ * same generated line as the mapping. On later lines, use the source
269
+ * anchor column instead of carrying the generated column across lines.
270
+ */
271
+ const sourceColumn =
272
+ line === mappingGeneratedLine
273
+ ? Math.max(
274
+ 0,
275
+ mappingSourceColumn +
276
+ (col - mappingGeneratedColumn)
277
+ )
278
+ : mappingSourceColumn;
279
+
280
+ return {
281
+ line: sourceLine,
282
+ col: sourceColumn,
283
+ };
91
284
  }
92
285
 
93
- function escapeRegExp(s)
94
- {
95
- return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
286
+ function getBlockTrace(error) {
287
+ const trace = error?.lilact_trace;
288
+
289
+ if (Array.isArray(trace)) {
290
+ return trace[0];
291
+ }
292
+
293
+ return trace;
96
294
  }
97
295
 
98
- function mapLocation(mps, r,c)
99
- {
100
- let map = null;
101
-
102
- for(const i in mps) {
103
- if(mps[i][0]<r) continue;
104
- if(mps[i][0]>r || (mps[i][0]===r && mps[i][1]>=c)) {
105
- map = mps[i-1];
106
- break;
107
- }
108
- }
109
- if(!map) map = mps[mps.length-1];
110
- return {line: r-map[0]+map[2], col: ((r-map[0]===0)?map[3]:0) };
296
+ function getBlockInfo(error) {
297
+ const trace = getBlockTrace(error);
298
+
299
+ if (trace === undefined || trace === null) {
300
+ return null;
301
+ }
302
+
303
+ return Lilact.blocks_info?.labels?.[trace] ?? null;
111
304
  }
112
305
 
306
+ function applyBlockFallback(error, result, currentPath) {
307
+ const block = getBlockInfo(error);
308
+ if (!block) return result;
113
309
 
114
- /** @ignore */
115
- export function scanBlockLabels(code, path)
116
- {
117
- const ls = Array.from( code.matchAll(/LILACTBLOCK(\d+):(\d+),(\d+):([^*]+)\*\//mg) );
310
+ if (block.path) {
311
+ result.fileName = block.path;
312
+ } else if (!result.fileName) {
313
+ result.fileName = currentPath;
314
+ }
315
+
316
+ /*
317
+ * scanBlockLabels() stores the original source position of the block.
318
+ * This is more reliable than using the location of the generated catch
319
+ * wrapper.
320
+ */
321
+ if (
322
+ Number.isFinite(block.line) &&
323
+ Number.isFinite(block.col)
324
+ ) {
325
+ result.lineNumber = block.line;
326
+ result.columnNumber = block.col;
327
+ }
328
+
329
+ if (block.desc) {
330
+ result.label = block.desc;
331
+ }
118
332
 
119
- // todo: this is not memory efficient, the structure should be upside-down
120
- ls.forEach(
121
- (x) => {
122
- Lilact.blocks_info.labels[x[1]] = {
123
- path,
124
- desc: x[4]
125
- }
126
- } );
333
+ return result;
127
334
  }
128
335
 
336
+ function copyErrorMetadata(source, target) {
337
+ if (!source || typeof source !== "object") return;
338
+
339
+ for (const key of [
340
+ "fileName",
341
+ "lineNumber",
342
+ "columnNumber",
343
+ "lineno",
344
+ "colno",
345
+ "line",
346
+ "column",
347
+ "componentStackLog",
348
+ "lilact_trace",
349
+ ]) {
350
+ if (source[key] !== undefined && target[key] === undefined) {
351
+ target[key] = source[key];
352
+ }
353
+ }
354
+ }
129
355
 
130
356
  /**
131
- * Debug tool to get the Lilact traced location of an error. It can also produce some block based stack trace
132
- * if `Lilact.transpilerConfig.enableLabelStack` is set to `true` before loading the script. This is `false`
133
- * by default for efficiency.
134
- *
135
- * @param error - The error object
136
- *
137
- * @returns A new object that includes `path`, `row`, `col`, `msg`, `name`, and optional `stack`.
138
- * The exception itself is stored as `err`.
357
+ * Converts parser errors, runtime errors, ErrorEvents, and rejected
358
+ * promises into one consistent zero-based representation.
139
359
  */
140
- export function traceError(error, run_path)
141
- {
142
- if(error?.is_traced) {
143
- return error;
144
- }
145
-
146
- const loc = parseEvalLocationFromStack(error.stack);
147
-
148
- const obj = {
149
- fileName: loc.url?.slice(6) || run_path || error.fileName,
150
-
151
- lineNumber: loc.line,
152
- columnNumber: loc.col,
153
-
154
- message: error.message,
155
- name: error.name,
156
-
157
- stack: error.stack,
158
- _error: error,
159
-
160
- is_traced: true
161
- };
162
-
163
- if( error.name!=='JSXParseError' ) {
164
-
165
- let mps;
166
-
167
- if(loc.url) {
168
- const rm = required_scripts[obj.fileName];
169
- mps = rm.mappings;
170
-
171
- const mloc = mapLocation(mps, obj.lineNumber-1, obj.columnNumber-1);
172
-
173
- obj.lineNumber = mloc.line;
174
- obj.columnNumber = mloc.col;
175
- }
176
- else {
177
-
178
- let loc = getErrorLocation(error);
179
-
180
- if( error.lilact_trace!==undefined) {
181
- let mps;
182
- let blk;
183
-
184
- if(typeof(error.lilact_trace)==='object') {
185
- blk = Lilact.blocks_info.labels[error.lilact_trace[0]];
186
- }
187
- else {
188
- blk = Lilact.blocks_info.labels[error.lilact_trace];
189
- }
190
-
191
- if(blk) {
192
- obj.fileName = blk.path;
193
- obj.label = blk.label;
194
-
195
- mps = required_scripts[blk.path].mappings;
196
-
197
- loc = mapLocation(mps, loc.line-1, loc.col-1);
198
- }
199
- }
200
-
201
- obj.lineNumber = loc.line;
202
- obj.columnNumber = loc.col;
203
- }
204
-
205
- }
206
- else {
207
- const loc = getErrorLocation(error);
208
- if(error.fileName) obj.fileName = error.fileName;
209
- else if(run_path) obj.fileName = run_path;
210
- obj.lineNumber = loc.line;
211
- obj.columnNumber = loc.col;
212
- }
213
-
214
- Lilact.error = obj;
215
- return obj;
216
-
360
+ export function traceError(value, runPath) {
361
+ if (value?.isTraced) return value;
362
+
363
+ const original =
364
+ value?.error instanceof Error ? value.error : asError(value);
365
+
366
+ const parserError = isParserError(original);
367
+
368
+ const stackLocation = parserError
369
+ ? null
370
+ : sourceUrlFromStack(original.stack);
371
+
372
+ const eventLocation = parserError
373
+ ? getErrorLocation(original, { zeroBased: true })
374
+ : getErrorLocation(original);
375
+
376
+ const result = {
377
+ fileName:
378
+ sourcePathFromEvalUrl(stackLocation?.url) ||
379
+ original.fileName ||
380
+ runPath ||
381
+ null,
382
+
383
+ lineNumber:
384
+ stackLocation?.line != null
385
+ ? Math.max(0, stackLocation.line - 1)
386
+ : eventLocation?.line ?? null,
387
+
388
+ columnNumber:
389
+ stackLocation?.col != null
390
+ ? Math.max(0, stackLocation.col - 1)
391
+ : eventLocation?.col ?? null,
392
+
393
+ message:
394
+ original.message == null
395
+ ? String(original)
396
+ : String(original.message),
397
+
398
+ name: original.name || "Error",
399
+ stack: original.stack || null,
400
+ _error: original,
401
+ isTraced: true,
402
+ };
403
+
404
+ copyErrorMetadata(value, result);
405
+ copyErrorMetadata(original, result);
406
+
407
+ /*
408
+ * Parser errors already point into the original JSX source. Do not map
409
+ * them again.
410
+ */
411
+ if (!parserError && stackLocation?.url) {
412
+ const module = required_scripts[result.fileName];
413
+
414
+ if (module?.mappings) {
415
+ const mapped = mapLocation(
416
+ module.mappings,
417
+ result.lineNumber,
418
+ result.columnNumber
419
+ );
420
+
421
+ result.lineNumber = mapped.line;
422
+ result.columnNumber = mapped.col;
423
+ }
424
+ }
425
+
426
+ /*
427
+ * When the browser does not preserve the eval sourceURL, the injected
428
+ * Lilact block label is the reliable fallback.
429
+ */
430
+ if (!parserError && !stackLocation?.url) {
431
+ applyBlockFallback(original, result, runPath);
432
+ }
433
+
434
+ Lilact.error = result;
435
+ return result;
436
+ }
437
+
438
+ function escapeHtml(value) {
439
+ return String(value ?? "")
440
+ .replaceAll("&", "&amp;")
441
+ .replaceAll("<", "&lt;")
442
+ .replaceAll(">", "&gt;")
443
+ .replaceAll('"', "&quot;")
444
+ .replaceAll("'", "&#39;");
445
+ }
446
+
447
+ function sourceExcerpt(module, zeroBasedLine) {
448
+ if (!module || !Number.isFinite(zeroBasedLine)) {
449
+ return null;
450
+ }
451
+
452
+ const lines = String(module.code ?? "").split(/\r?\n/);
453
+ const before = lines[zeroBasedLine - 1] ?? "";
454
+ const current = lines[zeroBasedLine] ?? "";
455
+ const after = lines[zeroBasedLine + 1] ?? "";
456
+
457
+ return { before, current, after };
217
458
  }
218
459
 
219
460
  /**
220
- * The global Lilact error handler that shows a modal containing information
221
- * when an exception is not handled.
222
- *
223
- * It is by default attached to the `window.onerror` when using the development
224
- * bundle. It is not attached in the production mode, but it is available in the
225
- * bundle and can be attached.
226
- *
227
- *
228
- * `
229
- * window.addEventListener('error', (e) => {
230
- * Lilact.globalErrorHandler(e);
231
- * });
232
- * `
233
- * @param error - The error object
234
- *
461
+ * Displays an unhandled error.
235
462
  */
236
- export function globalErrorHandler(error)
237
- {
238
- if(error.error) error = error.error;
239
-
240
- error = traceError(error);
241
-
242
- const cls = css(`
243
- background: linear-gradient(135deg, #fff2f2d4, #ffffffd4);
244
- backdrop-filter: blur(10px);
245
- border: 1px solid rgba(255,255,255,.25);
246
- border-radius: 5px;
247
- box-shadow: 0 10px 30px rgba(0,0,0,.35);
248
- overflow:hidden;
249
- min-width: 400px;
250
- width: 66%;
251
- red {
252
- color:#d00;
253
- }
254
- code {
255
- border: 1px solid #0003;
256
- overflow: auto;
257
- padding: 10px;
258
- display: block;
259
- }
260
- `);
261
-
262
- const el = document.createElement('dialog');
263
-
264
- el.className=cls;
265
- //<b>⚠</b>
266
- el.innerHTML =
267
- `<h3 style=""><red>Error!</red></h3>
268
- <b>${error.fileName?'At '+error.fileName:''}
269
- ${Number.isFinite(error.lineNumber)?": Line "+(error.lineNumber+1):""}</b><br><br>
270
- <b>${error.name}</b>:&nbsp;<span>${error.message}</span><br><br>
271
- ${required_scripts[error.fileName]?'<code><pre></pre><pre><red></red></pre><pre></pre></code>':''}
272
- ${error._error.componentStackLog?'<br>Component Stack:<br><code><pre>'+error._error.componentStackLog+'</pre></code>':''}
273
- `;
274
-
275
-
276
- document.body.appendChild(el);
277
-
278
- const pres = el.querySelectorAll('pre');
279
-
280
- if(required_scripts[error.fileName]) {
281
- const lines = required_scripts[error.fileName].code.split("\n");
282
-
283
- if(lines?.[error.lineNumber-1])
284
- pres[0].innerText = lines[error.lineNumber-1];
285
-
286
- if(lines?.[error.lineNumber]) el.querySelector('pre red').innerText = lines[error.lineNumber];
287
-
288
- if(lines?.[error.lineNumber+1])
289
- pres[2].innerText = lines[error.lineNumber+1];
290
- }
291
-
292
- el.showModal();
463
+ export function globalErrorHandler(eventOrError) {
464
+ const raw =
465
+ eventOrError?.error instanceof Error
466
+ ? eventOrError.error
467
+ : eventOrError?.reason !== undefined
468
+ ? eventOrError.reason
469
+ : eventOrError;
470
+
471
+ const runPath =
472
+ eventOrError?.fileName ||
473
+ null;
474
+
475
+ const traced = traceError(raw, runPath);
476
+ const module = required_scripts[traced.fileName];
477
+ const excerpt = sourceExcerpt(module, traced.lineNumber);
478
+
479
+ const className = css(`
480
+ background: linear-gradient(135deg, #fff2f2d4, #ffffffd4);
481
+ backdrop-filter: blur(10px);
482
+ border: 1px solid rgba(255,255,255,.25);
483
+ border-radius: 5px;
484
+ box-shadow: 0 10px 30px rgba(0,0,0,.35);
485
+ overflow: hidden;
486
+ min-width: 400px;
487
+ width: 66%;
488
+
489
+ red {
490
+ color: #d00;
491
+ }
492
+
493
+ code {
494
+ border: 1px solid #0003;
495
+ overflow: auto;
496
+ padding: 10px;
497
+ display: block;
498
+ }
499
+ `);
500
+
501
+ const dialog = document.createElement("dialog");
502
+ dialog.className = className;
503
+
504
+ const location =
505
+ traced.fileName
506
+ ? `At ${escapeHtml(traced.fileName)}`
507
+ : "";
508
+
509
+ const line =
510
+ Number.isFinite(traced.lineNumber)
511
+ ? `: Line ${traced.lineNumber + 1}`
512
+ : "";
513
+
514
+ const componentStack =
515
+ traced._error?.componentStackLog ||
516
+ traced._error?.componentStack ||
517
+ "";
518
+
519
+ dialog.innerHTML = `
520
+ <h3><red>Error!</red></h3>
521
+ <b>${location}${line}</b><br><br>
522
+ <b>${escapeHtml(traced.name)}</b>:
523
+ <span>${escapeHtml(traced.message)}</span>
524
+ <br><br>
525
+
526
+ ${
527
+ excerpt
528
+ ? "<code><pre></pre><red><pre></pre></red><pre></pre></code>"
529
+ : ""
530
+ }
531
+
532
+ ${
533
+ componentStack
534
+ ? `
535
+ <br>
536
+ Component Stack:
537
+ <br>
538
+ <code><pre>${escapeHtml(componentStack)}</pre></code>
539
+ `
540
+ : ""
541
+ }
542
+ `;
543
+
544
+ if (excerpt) {
545
+ const pre = dialog.querySelectorAll("pre");
546
+ pre[0].innerText = excerpt.before;
547
+ pre[1].innerText = excerpt.current;
548
+ pre[2].innerText = excerpt.after;
549
+ }
550
+
551
+ document.body.appendChild(dialog);
552
+
553
+ if (typeof dialog.showModal === "function") {
554
+ dialog.showModal();
555
+ } else {
556
+ dialog.setAttribute("open", "");
557
+ }
558
+
559
+ return traced;
560
+ }
561
+
562
+ /**
563
+ * Registers the source location of an injected Lilact block.
564
+ */
565
+ export function scanBlockLabels(code, path) {
566
+ const labels = String(code).matchAll(
567
+ /LILACTBLOCK(\d+):(\d+),(\d+):([^*]+)\*\//gm
568
+ );
569
+
570
+ for (const match of labels) {
571
+ const id = match[1];
572
+
573
+ Lilact.blocks_info.labels[id] = {
574
+ path,
575
+ line: Number(match[2]),
576
+ col: Number(match[3]),
577
+ desc: match[4],
578
+ };
579
+ }
293
580
  }
294
581
 
295
- /** @ignore */
296
- export const blocks_info = { counter: 0, labels: {} };
582
+ export const blocks_info = {
583
+ counter: 0,
584
+ labels: {},
585
+ };
297
586
 
298
- /** @ignore */
299
- export let error = null; // this is only to ease debuggin,
587
+ export let error = null;