@stacksjs/stx 0.2.336 → 0.2.338

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 (197) hide show
  1. package/dist/app-shell.js +9 -8
  2. package/dist/{chunk-9v0fsdsf.js → chunk-066yt1hy.js} +1 -1
  3. package/dist/{chunk-eth22r93.js → chunk-0xgc5t3y.js} +1 -1
  4. package/dist/{chunk-vd5xvsnv.js → chunk-18018jkn.js} +1 -1
  5. package/dist/{chunk-5tefa8qb.js → chunk-1vb2rj09.js} +2 -2
  6. package/dist/chunk-1wsewc5h.js +2 -0
  7. package/dist/{chunk-p1hy2bwn.js → chunk-1yj4xd6n.js} +3 -3
  8. package/dist/{chunk-fqq5k9ve.js → chunk-1z1efvtw.js} +1 -1
  9. package/dist/{chunk-cdqx2c2h.js → chunk-22hch2me.js} +1 -1
  10. package/dist/{chunk-kpeqn5tx.js → chunk-2gc4cevb.js} +1 -1
  11. package/dist/{chunk-mgmn1vxy.js → chunk-39xtzfzf.js} +1 -1
  12. package/dist/{chunk-zsnrcbw1.js → chunk-3cjvqa0s.js} +2 -2
  13. package/dist/chunk-3sgdth20.js +2 -0
  14. package/dist/chunk-41dfsdy9.js +2 -0
  15. package/dist/{chunk-ymrxpwr8.js → chunk-46tmg3k4.js} +2 -2
  16. package/dist/{chunk-e1sxe3q4.js → chunk-4904n5v0.js} +2 -2
  17. package/dist/chunk-4cw0s9f4.js +4 -0
  18. package/dist/chunk-4jj4kk18.js +2 -0
  19. package/dist/chunk-4np12rj1.js +2 -0
  20. package/dist/chunk-4pcjt99d.js +2 -0
  21. package/dist/{chunk-dvvx1wny.js → chunk-57atgbyq.js} +1 -1
  22. package/dist/chunk-58j32d7n.js +2 -0
  23. package/dist/{chunk-hyfjnn7g.js → chunk-5g6rprcy.js} +1 -1
  24. package/dist/{chunk-mm59b0gg.js → chunk-5qsg9q46.js} +1 -1
  25. package/dist/{chunk-m8kezfrt.js → chunk-5r7gfwpj.js} +5 -5
  26. package/dist/chunk-799s9mwc.js +47 -0
  27. package/dist/{chunk-yxppshfd.js → chunk-7a21q2q4.js} +1 -1
  28. package/dist/chunk-7d74am4w.js +2 -0
  29. package/dist/chunk-7ngnyy5k.js +2 -0
  30. package/dist/{chunk-r8rbf28v.js → chunk-7r79j1hs.js} +1 -1
  31. package/dist/{chunk-cr8v8k53.js → chunk-7r9vgzg4.js} +1 -1
  32. package/dist/{chunk-xwxdwqn9.js → chunk-7we3xfde.js} +1 -1
  33. package/dist/{chunk-fr66xp8q.js → chunk-823h67tv.js} +1 -1
  34. package/dist/{chunk-42en0cxm.js → chunk-86v2rps7.js} +1 -1
  35. package/dist/{chunk-xz3evxqp.js → chunk-8bxtvsgm.js} +1 -1
  36. package/dist/{chunk-09jdvqhg.js → chunk-8cg15rr7.js} +2 -2
  37. package/dist/chunk-95t7pked.js +2 -0
  38. package/dist/{chunk-xdmb578h.js → chunk-97ej3abh.js} +1 -1
  39. package/dist/{chunk-kw98dfza.js → chunk-9btdax2s.js} +1 -1
  40. package/dist/{chunk-pbexz0d4.js → chunk-9bvgzqw1.js} +2 -2
  41. package/dist/{chunk-te6qym2g.js → chunk-9jc2z17q.js} +2 -2
  42. package/dist/chunk-9m5sxaen.js +2 -0
  43. package/dist/{chunk-tjag2cmm.js → chunk-a26hcag1.js} +1 -1
  44. package/dist/chunk-a3m7mgjc.js +2 -0
  45. package/dist/{chunk-9368t9jm.js → chunk-a959m68j.js} +1 -1
  46. package/dist/{chunk-fkdnfjc2.js → chunk-aftqqqcg.js} +2 -2
  47. package/dist/chunk-aved735s.js +64 -0
  48. package/dist/{chunk-q0g5c39d.js → chunk-ay336sqb.js} +1 -1
  49. package/dist/{chunk-cvphepjb.js → chunk-b3pn6dh5.js} +1 -1
  50. package/dist/{chunk-5swem3qy.js → chunk-b4cabzk9.js} +1 -1
  51. package/dist/{chunk-ej8mv2k6.js → chunk-bgrkxzr7.js} +1 -1
  52. package/dist/{chunk-2cm03y4y.js → chunk-d3kjaae0.js} +1 -1
  53. package/dist/chunk-d7zxg9bp.js +16 -0
  54. package/dist/{chunk-1ebyt1ba.js → chunk-dekxwgc2.js} +1 -1
  55. package/dist/chunk-dvafkj8w.js +2 -0
  56. package/dist/chunk-e3rgky2r.js +3747 -0
  57. package/dist/{chunk-t41aaehf.js → chunk-e4g8wz6t.js} +1 -1
  58. package/dist/chunk-e9y48d60.js +2 -0
  59. package/dist/{chunk-pbzdjhp8.js → chunk-eq2j7w8p.js} +1 -1
  60. package/dist/chunk-f08ttkqr.js +2 -0
  61. package/dist/{chunk-4jdj52c5.js → chunk-f6sejeg3.js} +1 -1
  62. package/dist/chunk-fha0nyet.js +2 -0
  63. package/dist/{chunk-6k8h1fj1.js → chunk-fjrkyhm7.js} +1 -1
  64. package/dist/chunk-fw56y3cb.js +30 -0
  65. package/dist/chunk-fx5m9d3t.js +2 -0
  66. package/dist/{chunk-mwh3ymwc.js → chunk-g6czgcg4.js} +1 -1
  67. package/dist/chunk-gtfcw1gc.js +12 -0
  68. package/dist/chunk-gz539gk6.js +2 -0
  69. package/dist/chunk-h0y4mb2m.js +2 -0
  70. package/dist/chunk-hfzv07az.js +3 -0
  71. package/dist/{chunk-nat3actx.js → chunk-hrdd26r4.js} +1 -1
  72. package/dist/{chunk-cdsbxghb.js → chunk-jkyvk4x6.js} +1 -1
  73. package/dist/{chunk-4ty8j3xn.js → chunk-jx03a693.js} +1 -1
  74. package/dist/{chunk-vvd659y4.js → chunk-k3dtvay1.js} +1 -1
  75. package/dist/chunk-kawdj1j7.js +2 -0
  76. package/dist/{chunk-d4etph79.js → chunk-kdhrk7x5.js} +1 -1
  77. package/dist/chunk-kdwerx8e.js +2 -0
  78. package/dist/{chunk-n66k2v2q.js → chunk-kermxyfe.js} +2 -2
  79. package/dist/{chunk-x7yc7e9v.js → chunk-m11pb9cx.js} +1 -1
  80. package/dist/{chunk-k6ky5et7.js → chunk-m1p2n3nf.js} +1 -1
  81. package/dist/chunk-m46edxtt.js +3 -0
  82. package/dist/chunk-meqz313r.js +2 -0
  83. package/dist/{chunk-q05eh1n4.js → chunk-mnyehz4t.js} +2 -2
  84. package/dist/{chunk-0vv5s039.js → chunk-mys0rat0.js} +1 -1
  85. package/dist/chunk-n0a55zv2.js +2 -0
  86. package/dist/{chunk-7kw3grq5.js → chunk-nf7k53ap.js} +1 -1
  87. package/dist/{chunk-kffndzch.js → chunk-nhbc464j.js} +3 -3
  88. package/dist/{chunk-1hzgxk2m.js → chunk-nvp546bb.js} +1 -1
  89. package/dist/chunk-p1cddybx.js +2 -0
  90. package/dist/chunk-q0srntf6.js +2 -0
  91. package/dist/chunk-q3g4yf5r.js +2 -0
  92. package/dist/{chunk-c2ta859c.js → chunk-q9ckfwhh.js} +1 -1
  93. package/dist/chunk-qhdp0ycf.js +2 -0
  94. package/dist/{chunk-y4nzmkea.js → chunk-qsbjtp60.js} +1 -1
  95. package/dist/{chunk-1tva0f8f.js → chunk-qtc28zy1.js} +1 -1
  96. package/dist/{chunk-2vgkc0kg.js → chunk-r10fypcd.js} +1 -1
  97. package/dist/chunk-rkykwy3z.js +2 -0
  98. package/dist/{chunk-64djh88a.js → chunk-sqvqwd0p.js} +1 -1
  99. package/dist/{chunk-67bh5vrj.js → chunk-stbns4bg.js} +1 -1
  100. package/dist/chunk-szfk6pw6.js +2 -0
  101. package/dist/{chunk-amxtypkp.js → chunk-t00ga194.js} +1 -1
  102. package/dist/chunk-temapbqe.js +2 -0
  103. package/dist/{chunk-9kxfz708.js → chunk-tr1174hz.js} +1 -1
  104. package/dist/{chunk-q85qp5we.js → chunk-tr41gn5j.js} +1 -1
  105. package/dist/{chunk-crj19mqr.js → chunk-tr9ynxdw.js} +1 -1
  106. package/dist/{chunk-71y3bhcw.js → chunk-tsmdmf5h.js} +123 -37
  107. package/dist/{chunk-gptwwv3c.js → chunk-vaggjvsm.js} +1 -1
  108. package/dist/{chunk-ya5m5vbn.js → chunk-vgfd9582.js} +1 -1
  109. package/dist/chunk-vgfnre7b.js +2 -0
  110. package/dist/{chunk-n9c975db.js → chunk-vhzc08aa.js} +1 -1
  111. package/dist/{chunk-08w4gtyf.js → chunk-vm5jbgb1.js} +3 -3
  112. package/dist/{chunk-drqdc0mh.js → chunk-vxt5c5aq.js} +1 -1
  113. package/dist/{chunk-hzfd8cz0.js → chunk-w0hzaka3.js} +78 -67
  114. package/dist/{chunk-yk6446kr.js → chunk-w72v0ab1.js} +2 -2
  115. package/dist/{chunk-c65yrgck.js → chunk-wakb8z5q.js} +10 -10
  116. package/dist/{chunk-q569zmrr.js → chunk-wb90259a.js} +1 -1
  117. package/dist/chunk-x01sw9wy.js +2 -0
  118. package/dist/{chunk-dfejgnny.js → chunk-x48crnwa.js} +1 -1
  119. package/dist/{chunk-24b4711v.js → chunk-x4vhn880.js} +1 -1
  120. package/dist/{chunk-j2dj21ay.js → chunk-xe6zmdfq.js} +1 -1
  121. package/dist/chunk-y8vj3rtt.js +2 -0
  122. package/dist/chunk-yj1dxdfr.js +2 -0
  123. package/dist/{chunk-wx5zf66r.js → chunk-yne23w3c.js} +1 -1
  124. package/dist/{chunk-zz8qgqa1.js → chunk-ypzm47q3.js} +2 -2
  125. package/dist/chunk-z07knk3p.js +2 -0
  126. package/dist/{chunk-27xfkzks.js → chunk-zj039s33.js} +1 -1
  127. package/dist/{chunk-c79nfdyj.js → chunk-zk6ghjnj.js} +1 -1
  128. package/dist/chunk-zt4pf776.js +2 -0
  129. package/dist/{chunk-4k0ce745.js → chunk-zye9r15n.js} +2 -2
  130. package/dist/chunk-zzfpwxpp.js +2 -0
  131. package/dist/cli.js +14 -14
  132. package/dist/component-contract-checks.d.ts +18 -15
  133. package/dist/component-contract.d.ts +6 -14
  134. package/dist/craft-entry.js +1 -1
  135. package/dist/craft.js +1 -1
  136. package/dist/database.js +1 -1
  137. package/dist/expressions.d.ts +45 -0
  138. package/dist/expressions.js +60 -11
  139. package/dist/includes.js +4 -0
  140. package/dist/loops.js +10 -0
  141. package/dist/pwa.js +2 -2
  142. package/dist/server-components.js +2 -1
  143. package/dist/signals.js +121 -35
  144. package/dist/slots.js +86 -12
  145. package/dist/ssg.js +2 -2
  146. package/dist/stx-virtual-ts.d.ts +205 -95
  147. package/dist/template-syntax-escape.d.ts +36 -0
  148. package/dist/template-syntax-escape.js +8 -0
  149. package/dist/utils.js +9 -2
  150. package/dist/variable-extractor.d.ts +1 -0
  151. package/dist/variable-extractor.js +8 -0
  152. package/dist/visual-testing.js +1 -1
  153. package/package.json +8 -8
  154. package/dist/chunk-046dp691.js +0 -2
  155. package/dist/chunk-1brnvyhv.js +0 -2
  156. package/dist/chunk-3ntf8f5e.js +0 -2
  157. package/dist/chunk-4tnxnzja.js +0 -2
  158. package/dist/chunk-63yfp05p.js +0 -2
  159. package/dist/chunk-7y4eqkv4.js +0 -2
  160. package/dist/chunk-7ywnkv1f.js +0 -2
  161. package/dist/chunk-80zj5516.js +0 -2
  162. package/dist/chunk-8730k51r.js +0 -2
  163. package/dist/chunk-907qxn09.js +0 -2
  164. package/dist/chunk-9fcq5x2r.js +0 -2
  165. package/dist/chunk-a97tp43x.js +0 -2
  166. package/dist/chunk-atw841yx.js +0 -2
  167. package/dist/chunk-c1m1gng4.js +0 -2
  168. package/dist/chunk-caaenc3w.js +0 -60
  169. package/dist/chunk-dbfq77qe.js +0 -4
  170. package/dist/chunk-dnesmvvb.js +0 -2
  171. package/dist/chunk-eg8ksaft.js +0 -2
  172. package/dist/chunk-ewsx2qxm.js +0 -2
  173. package/dist/chunk-farwrdr2.js +0 -3
  174. package/dist/chunk-fcenm8n1.js +0 -2
  175. package/dist/chunk-gbz6pfs8.js +0 -2
  176. package/dist/chunk-gdbkqatx.js +0 -43
  177. package/dist/chunk-jnahcd4d.js +0 -2
  178. package/dist/chunk-ke88kpva.js +0 -30
  179. package/dist/chunk-krpkfndf.js +0 -3
  180. package/dist/chunk-kxtqbrfr.js +0 -3759
  181. package/dist/chunk-m1yrakpk.js +0 -2
  182. package/dist/chunk-q2hvne0v.js +0 -2
  183. package/dist/chunk-qpbk50xd.js +0 -2
  184. package/dist/chunk-rxdd3k6n.js +0 -2
  185. package/dist/chunk-ssd10kje.js +0 -2
  186. package/dist/chunk-szx6pxh3.js +0 -2
  187. package/dist/chunk-tpmgq2dx.js +0 -12
  188. package/dist/chunk-tte4m2s9.js +0 -2
  189. package/dist/chunk-v7gjsb3f.js +0 -2
  190. package/dist/chunk-vv289x82.js +0 -2
  191. package/dist/chunk-vx6mxybh.js +0 -2
  192. package/dist/chunk-x510wr8z.js +0 -2
  193. package/dist/chunk-x95sxapm.js +0 -2
  194. package/dist/chunk-xjrhzkx2.js +0 -2
  195. package/dist/chunk-y1x2ebhv.js +0 -2
  196. package/dist/chunk-yvec3f1c.js +0 -2
  197. package/dist/chunk-yzxm75jp.js +0 -2
@@ -4,6 +4,7 @@ import { stripCommentsAndLiterals } from "./strip-literals.js";
4
4
  import { createSafeFunction, getExpressionSafetyRule, safeEvaluate } from "./safe-evaluator.js";
5
5
  import { createDetailedErrorMessage } from "./utils.js";
6
6
  import { createPlaceholder } from "./placeholder.js";
7
+ import { neutralizeTemplateSyntax } from "./template-syntax-escape.js";
7
8
  import { maskAtElementPosition, matchScriptElement, matchStyleElement, restoreStashedScripts, stashScriptElements } from "./html-masking.js";
8
9
  import { importsSignalDeclarations } from "./imported-signals.js";
9
10
  const customFilters = {}, translateRegexCache = new Map;
@@ -236,15 +237,63 @@ export function clearCustomFilters() {
236
237
  for (const key of Object.keys(customFilters))
237
238
  delete customFilters[key];
238
239
  }
240
+ export function findMustacheEnd(input, openIndex, braces = 2) {
241
+ let depth = 0, quote = null;
242
+ for (let i = openIndex + braces;i < input.length; i++) {
243
+ const character = input[i];
244
+ if (quote) {
245
+ if (character === "\\") {
246
+ i++;
247
+ continue;
248
+ }
249
+ if (character === quote)
250
+ quote = null;
251
+ continue;
252
+ }
253
+ if (character === '"' || character === "'" || character === "`") {
254
+ quote = character;
255
+ continue;
256
+ }
257
+ if (character === "{") {
258
+ depth++;
259
+ continue;
260
+ }
261
+ if (character === "}") {
262
+ if (depth > 0) {
263
+ depth--;
264
+ continue;
265
+ }
266
+ let run = 0;
267
+ while (input[i + run] === "}")
268
+ run++;
269
+ if (run >= braces)
270
+ return i;
271
+ }
272
+ }
273
+ return -1;
274
+ }
275
+ export function replaceInterpolations(input, replacer, braces = 2) {
276
+ const opener = "{".repeat(braces);
277
+ if (!input.includes(opener))
278
+ return input;
279
+ let output = "", cursor = 0;
280
+ for (;; ) {
281
+ const open = input.indexOf(opener, cursor);
282
+ if (open === -1)
283
+ break;
284
+ const close = findMustacheEnd(input, open, braces);
285
+ if (close === -1)
286
+ break;
287
+ output += input.slice(cursor, open) + replacer(input.slice(open, close + braces), input.slice(open + braces, close), open);
288
+ cursor = close + braces;
289
+ }
290
+ return cursor === 0 ? input : output + input.slice(cursor);
291
+ }
239
292
  export function escapeHtml(unsafe) {
240
293
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
241
294
  }
242
295
  export function escapeHtmlValue(unsafe) {
243
- return escapeHtml(unsafe).replace(/\{{2,}/g, (match) => joinBraces(match.length)).replace(/\{(?=!)/g, `&#123;${WORD_JOINER}`);
244
- }
245
- const WORD_JOINER = "&#8288;";
246
- function joinBraces(length) {
247
- return Array.from({ length }, () => "&#123;").join(WORD_JOINER);
296
+ return neutralizeTemplateSyntax(escapeHtml(unsafe));
248
297
  }
249
298
  export function usesSignalsInScript(template, filePath) {
250
299
  const { scripts } = stashScriptElements(template);
@@ -333,7 +382,7 @@ export function interpolateScriptExpressions(scriptBody, context) {
333
382
  return match;
334
383
  }
335
384
  });
336
- output = output.replace(/\{\{([\s\S]*?)\}\}/g, (match, expr) => {
385
+ output = replaceInterpolations(output, (match, expr) => {
337
386
  const trimmed = expr.trim();
338
387
  if (/^__[A-Z_]+__$/.test(trimmed))
339
388
  return match;
@@ -362,7 +411,7 @@ export function interpolateScriptAttributes(attrs, context) {
362
411
  return match;
363
412
  }
364
413
  });
365
- output = output.replace(/\{\{([\s\S]*?)\}\}/g, (match, expr) => {
414
+ output = replaceInterpolations(output, (match, expr) => {
366
415
  const trimmed = expr.trim();
367
416
  if (/^__[A-Z_]+__$/.test(trimmed))
368
417
  return match;
@@ -372,7 +421,7 @@ export function interpolateScriptAttributes(attrs, context) {
372
421
  return match;
373
422
  if (value === null)
374
423
  return "";
375
- return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
424
+ return escapeHtmlValue(String(value));
376
425
  } catch {
377
426
  return match;
378
427
  }
@@ -412,7 +461,7 @@ export function processExpressions(template, context, filePath) {
412
461
  const scriptBlocks = [];
413
462
  output = maskInto(output, matchScriptElement, scriptBlocks, (n) => `<!--__STX_SCRIPT_EXPR_${n}__-->`);
414
463
  const hasSignals = context?.__stx_signals_gate === !0 || usesSignalsInScript(template), clientSignalNames = new Set(Array.isArray(context?.__stx_client_signal_names) ? context.__stx_client_signal_names : []), preservedClientExpressions = new Set(Array.isArray(context?.__stx_preserved_client_expressions) ? context.__stx_preserved_client_expressions : []);
415
- output = output.replace(/\{\{\{([\s\S]*?)\}\}\}/g, (match, expr, offset) => {
464
+ output = replaceInterpolations(output, (match, expr, offset) => {
416
465
  try {
417
466
  const value = evaluateExpression(expr, context, !1, (r) => warnRejectedExpression(r, match, filePath, template));
418
467
  return value !== void 0 && value !== null ? String(value) : "";
@@ -420,7 +469,7 @@ export function processExpressions(template, context, filePath) {
420
469
  const msg = error instanceof Error ? error.message : String(error);
421
470
  return createDetailedErrorMessage("Expression", `Error evaluating: {{{ ${expr.trim()}}}}: ${msg}`, filePath, template, offset, match);
422
471
  }
423
- });
472
+ }, 3);
424
473
  output = output.replace(/\{!!([\s\S]*?)!!\}/g, (match, expr, offset) => {
425
474
  try {
426
475
  const value = evaluateExpression(expr, context, !1, (r) => warnRejectedExpression(r, match, filePath, template));
@@ -436,7 +485,7 @@ export function processExpressions(template, context, filePath) {
436
485
  });
437
486
  output = maskInto(output, matchScriptElement, scriptBlocks, (n) => `<!--__STX_SCRIPT_EXPR_${n}__-->`);
438
487
  output = maskInto(output, styleMatcher, styleBlocks, (n) => `<!--__STX_STYLE_${n}__-->`);
439
- output = output.replace(/\{\{([\s\S]*?)\}\}/g, (match, expr, offset) => {
488
+ output = replaceInterpolations(output, (match, expr, offset) => {
440
489
  const trimmedExpr = expr.trim();
441
490
  if (preservedClientExpressions.has(trimmedExpr))
442
491
  return match;
package/dist/includes.js CHANGED
@@ -707,6 +707,10 @@ export async function processIncludes(template, context, filePath, options, depe
707
707
  let processedContent = processLoops(workingContent, includeContext, includeFilePath, void 0, !0);
708
708
  processedContent = processConditionals(processedContent, includeContext, includeFilePath);
709
709
  processedContent = reportDeferredLoopWarnings(processedContent);
710
+ const { processMetaDirectives, processSeoDirective, processStructuredData } = await import("./seo.js");
711
+ processedContent = processMetaDirectives(processedContent, includeContext, includeFilePath, options);
712
+ processedContent = processStructuredData(processedContent, includeContext, includeFilePath);
713
+ processedContent = processSeoDirective(processedContent, includeContext, includeFilePath, options);
710
714
  processedContent = processExpressions(processedContent, includeContext, includeFilePath);
711
715
  if (preservedStyle)
712
716
  processedContent += `
package/dist/loops.js CHANGED
@@ -493,6 +493,16 @@ export function processLoops(template, context, filePath, options, deferWarnings
493
493
  });
494
494
  else
495
495
  itemContext[itemName] = item;
496
+ } else if (itemName.startsWith("{") && itemName.endsWith("}")) {
497
+ const fields = itemName.slice(1, -1).split(",").map((field) => field.trim()).filter(Boolean);
498
+ if (item !== null && typeof item === "object")
499
+ fields.forEach((field) => {
500
+ const separator = field.indexOf(":"), property = (separator === -1 ? field : field.slice(0, separator)).trim(), alias = separator === -1 ? property : field.slice(separator + 1).trim();
501
+ if (property && alias)
502
+ itemContext[alias] = item[property];
503
+ });
504
+ else
505
+ itemContext[itemName] = item;
496
506
  } else
497
507
  itemContext[itemName] = item;
498
508
  if (indexName)
package/dist/pwa.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // @bun
2
- import{Pk as wq,Qk as dq,Rk as Cq,Sk as mq,Tk as gq}from"./chunk-kw98dfza.js";import{$k as vq,Uk as S,Vk as Tq,Wk as _q,Xk as z,Yk as D,Zk as P,_k as kq,al as w,bl as Iq,cl as d,dl as Hq,el as Oq,fl as Mq}from"./chunk-zsnrcbw1.js";import{jw as Aq,kw as bq,lw as Sq,mw as zq,nw as Dq,ow as Pq}from"./chunk-q569zmrr.js";import{sw as N}from"./chunk-hyfjnn7g.js";import p from"fs";import E from"path";import C from"fs";import L from"path";var k=[72,96,128,144,152,192,384,512],v=[120,152,167,180],I=[16,32];async function H(G,B){let q={success:!0,generatedFiles:[],errors:[],warnings:[]},K=G.pwa?.icons;if(!K?.src)return q.errors.push("No source icon specified in pwa.icons.src"),q.success=!1,q;let Y=L.isAbsolute(K.src)?K.src:L.resolve(process.cwd(),K.src);if(!await Bun.file(Y).exists())return q.errors.push(`Source icon not found: ${Y}`),q.success=!1,q;let Q=L.resolve(B,K.outputDir||"pwa-icons");try{C.mkdirSync(Q,{recursive:!0})}catch{return q.errors.push(`Failed to create output directory: ${Q}`),q.success=!1,q}let X=K.sizes||k,j=K.generateWebP??!0,J=K.generateAppleIcons??!0;try{let y;try{y=await import("sharp")}catch{return q.errors.push(`sharp is not installed. Install it with: bun add sharp
3
- Icon generation requires the sharp package for image processing.`),q.success=!1,q}let R=await y.default(Y).metadata();if(R.width&&R.width<512)q.warnings.push(`Source icon is ${R.width}x${R.height}. Recommend using at least 512x512 for best quality.`);for(let $ of X){let x=L.join(Q,`icon-${$}x${$}.png`);if(await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).png().toFile(x),q.generatedFiles.push(x),j){let F=L.join(Q,`icon-${$}x${$}.webp`);await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).webp({quality:90}).toFile(F),q.generatedFiles.push(F)}}if(J){for(let x of v){let F=L.join(Q,`apple-touch-icon-${x}x${x}.png`);await y.default(Y).resize(x,x,{fit:"contain",background:{r:255,g:255,b:255,alpha:1}}).png().toFile(F),q.generatedFiles.push(F)}let $=L.join(Q,"apple-touch-icon.png");await y.default(Y).resize(180,180,{fit:"contain",background:{r:255,g:255,b:255,alpha:1}}).png().toFile($),q.generatedFiles.push($)}for(let $ of I){let x=L.join(Q,`favicon-${$}x${$}.png`);await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).png().toFile(x),q.generatedFiles.push(x)}let U=512,T=L.join(Q,`icon-maskable-${U}x${U}.png`),Z=Math.floor(U*0.1),_=U-Z*2;await y.default(Y).resize(_,_,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).extend({top:Z,bottom:Z,left:Z,right:Z,background:{r:255,g:255,b:255,alpha:1}}).png().toFile(T),q.generatedFiles.push(T)}catch(y){let V=y instanceof Error?y.message:String(y);q.errors.push(`Icon generation failed: ${V}`),q.success=!1}return q}function n(G){let B=G.sizes||k,q=G.outputDir?`/${G.outputDir}`:"/pwa-icons",K=G.generateWebP??!0,Y=G.generateAppleIcons??!0,Q=[];for(let X of B)if(Q.push(`${q}/icon-${X}x${X}.png`),K)Q.push(`${q}/icon-${X}x${X}.webp`);if(Y){Q.push(`${q}/apple-touch-icon.png`);for(let X of v)Q.push(`${q}/apple-touch-icon-${X}x${X}.png`)}for(let X of I)Q.push(`${q}/favicon-${X}x${X}.png`);return Q.push(`${q}/icon-maskable-512x512.png`),Q}async function O(){try{return await import("sharp"),!0}catch{return!1}}function m(G,B,q,K){let Y=[],Q=K.join(" ");for(let X of G)if(Y.push({src:`${B}/icon-${X}x${X}.png`,sizes:`${X}x${X}`,type:"image/png",purpose:Q}),q)Y.push({src:`${B}/icon-${X}x${X}.webp`,sizes:`${X}x${X}`,type:"image/webp",purpose:Q});return Y}function g(G){return G.map((B)=>({name:B.name,short_name:B.shortName,description:B.description,url:B.url,icons:B.icons?.map((q)=>({src:q.src,sizes:q.sizes,type:q.type}))}))}function h(G){let B=G.pwa?.shareTarget;if(!B?.enabled)return;let q={action:B.action||"/share",method:B.method||"POST",enctype:B.enctype||"multipart/form-data",params:{title:B.params?.title||"title",text:B.params?.text||"text",url:B.params?.url||"url"}};if(B.acceptFiles&&B.acceptFiles.length>0)q.params.files=B.acceptFiles.map((K)=>({name:K.name,accept:K.accept}));return q}function c(G){let B=G.pwa?.fileHandlers;if(!B?.enabled||!B.accept)return;let q=[{action:B.action||"/",accept:B.accept,launch_type:B.launchType||"single-client"}];if(B.icons)q[0].icons=B.icons.map((K)=>({src:K.src,sizes:K.sizes,type:K.type}));return q}function l(G){let B=G.pwa?.protocolHandlers;if(!B||B.length===0)return;return B.map((q)=>({protocol:q.protocol,url:q.url}))}function u(G){let B=G.pwa;if(!B?.enabled||!B.manifest)return null;let{manifest:q,icons:K}=B,Y=K?.sizes||[72,96,128,144,152,192,384,512],Q=K?.outputDir?`/${K.outputDir}`:"/pwa-icons",X=K?.generateWebP??!0,j=K?.purpose||["any","maskable"],J={name:q.name,short_name:q.shortName||q.name,description:q.description,start_url:q.startUrl||"/",display:q.display||"standalone",display_override:q.displayOverride,orientation:q.orientation,theme_color:q.themeColor,background_color:q.backgroundColor,scope:q.scope||"/",lang:q.lang,dir:q.dir,id:q.id,categories:q.categories,icons:m(Y,Q,X,j)};if(q.shortcuts&&q.shortcuts.length>0)J.shortcuts=g(q.shortcuts);if(q.screenshots&&q.screenshots.length>0)J.screenshots=q.screenshots.map((U)=>({src:U.src,sizes:U.sizes,type:U.type,label:U.label,form_factor:U.platform}));let y=h(G);if(y)J.share_target=y;let V=c(G);if(V)J.file_handlers=V;let R=l(G);if(R)J.protocol_handlers=R;if(q.launchHandler)J.launch_handler={client_mode:q.launchHandler};if(q.handleLinks)J.handle_links=q.handleLinks;if(q.edgeSidePanel?.enabled)J.edge_side_panel={preferred_width:q.edgeSidePanel.preferredWidth};return J}function M(G){let B=u(G);if(!B)return"";let q=JSON.parse(JSON.stringify(B));return JSON.stringify(q,null,2)}function i(G,B){return{...B,...G,shortcuts:G.shortcuts||B.shortcuts,screenshots:G.screenshots||B.screenshots,categories:G.categories||B.categories}}function s(G){let B=[],q=G.pwa;if(!q?.enabled)return B;let K=q.manifest;if(!K)return B.push("PWA manifest configuration is missing"),B;if(!K.name)B.push("Manifest name is required");let Y=["fullscreen","standalone","minimal-ui","browser"];if(K.display&&!Y.includes(K.display))B.push(`Invalid display mode: ${K.display}. Must be one of: ${Y.join(", ")}`);let Q=["any","natural","landscape","landscape-primary","landscape-secondary","portrait","portrait-primary","portrait-secondary"];if(K.orientation&&!Q.includes(K.orientation))B.push(`Invalid orientation: ${K.orientation}`);let X=/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;if(K.themeColor&&!X.test(K.themeColor))B.push(`Invalid theme color: ${K.themeColor}. Must be a valid hex color.`);if(K.backgroundColor&&!X.test(K.backgroundColor))B.push(`Invalid background color: ${K.backgroundColor}. Must be a valid hex color.`);if(q.shareTarget?.enabled){if(!q.shareTarget.action)B.push("Share target action URL is required")}if(q.fileHandlers?.enabled){if(!q.fileHandlers.accept||Object.keys(q.fileHandlers.accept).length===0)B.push("File handlers must specify accepted MIME types")}if(q.protocolHandlers)for(let j of q.protocolHandlers){if(!j.protocol)B.push("Protocol handler missing protocol");if(!j.url||!j.url.includes("%s"))B.push(`Protocol handler URL must contain %s placeholder: ${j.protocol}`)}return B}import A from"path";async function b(G){let B=G.pwa?.offline;if(!B?.enabled)return"";if(B.page){let q=A.isAbsolute(B.page)?B.page:A.resolve(process.cwd(),B.page);if(await Bun.file(q).exists())try{let{processDirectives:K}=await import("./chunk-63yfp05p.js"),Y=await Bun.file(q).text();return await K(Y,{},q,G,new Set)}catch(K){console.warn(`[PWA] Failed to process custom offline page: ${K}`)}else console.warn(`[PWA] Custom offline page not found: ${q}`)}return f(G)}function f(G){let B=G.pwa?.offline,q=G.pwa?.manifest,K=B?.fallbackTitle||"You are offline",Y=B?.fallbackMessage||"Please check your internet connection and try again.",Q=q?.themeColor||"#4f46e5",X=q?.shortName||q?.name||"App";return`<!DOCTYPE html>
2
+ import{Ek as wq,Fk as dq,Gk as Cq,Hk as mq,Ik as gq}from"./chunk-9btdax2s.js";import{Jk as S,Kk as Tq,Lk as _q,Mk as z,Nk as D,Ok as P,Pk as kq,Qk as vq,Rk as w,Sk as Iq,Tk as d,Uk as Hq,Vk as Oq,Wk as Mq}from"./chunk-3cjvqa0s.js";import{kw as Aq,lw as bq,mw as Sq,nw as zq,ow as Dq,pw as Pq}from"./chunk-wb90259a.js";import{tw as N}from"./chunk-5g6rprcy.js";import p from"fs";import E from"path";import C from"fs";import L from"path";var k=[72,96,128,144,152,192,384,512],v=[120,152,167,180],I=[16,32];async function H(G,B){let q={success:!0,generatedFiles:[],errors:[],warnings:[]},K=G.pwa?.icons;if(!K?.src)return q.errors.push("No source icon specified in pwa.icons.src"),q.success=!1,q;let Y=L.isAbsolute(K.src)?K.src:L.resolve(process.cwd(),K.src);if(!await Bun.file(Y).exists())return q.errors.push(`Source icon not found: ${Y}`),q.success=!1,q;let Q=L.resolve(B,K.outputDir||"pwa-icons");try{C.mkdirSync(Q,{recursive:!0})}catch{return q.errors.push(`Failed to create output directory: ${Q}`),q.success=!1,q}let X=K.sizes||k,j=K.generateWebP??!0,J=K.generateAppleIcons??!0;try{let y;try{y=await import("sharp")}catch{return q.errors.push(`sharp is not installed. Install it with: bun add sharp
3
+ Icon generation requires the sharp package for image processing.`),q.success=!1,q}let R=await y.default(Y).metadata();if(R.width&&R.width<512)q.warnings.push(`Source icon is ${R.width}x${R.height}. Recommend using at least 512x512 for best quality.`);for(let $ of X){let x=L.join(Q,`icon-${$}x${$}.png`);if(await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).png().toFile(x),q.generatedFiles.push(x),j){let F=L.join(Q,`icon-${$}x${$}.webp`);await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).webp({quality:90}).toFile(F),q.generatedFiles.push(F)}}if(J){for(let x of v){let F=L.join(Q,`apple-touch-icon-${x}x${x}.png`);await y.default(Y).resize(x,x,{fit:"contain",background:{r:255,g:255,b:255,alpha:1}}).png().toFile(F),q.generatedFiles.push(F)}let $=L.join(Q,"apple-touch-icon.png");await y.default(Y).resize(180,180,{fit:"contain",background:{r:255,g:255,b:255,alpha:1}}).png().toFile($),q.generatedFiles.push($)}for(let $ of I){let x=L.join(Q,`favicon-${$}x${$}.png`);await y.default(Y).resize($,$,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).png().toFile(x),q.generatedFiles.push(x)}let U=512,T=L.join(Q,`icon-maskable-${U}x${U}.png`),Z=Math.floor(U*0.1),_=U-Z*2;await y.default(Y).resize(_,_,{fit:"contain",background:{r:0,g:0,b:0,alpha:0}}).extend({top:Z,bottom:Z,left:Z,right:Z,background:{r:255,g:255,b:255,alpha:1}}).png().toFile(T),q.generatedFiles.push(T)}catch(y){let V=y instanceof Error?y.message:String(y);q.errors.push(`Icon generation failed: ${V}`),q.success=!1}return q}function n(G){let B=G.sizes||k,q=G.outputDir?`/${G.outputDir}`:"/pwa-icons",K=G.generateWebP??!0,Y=G.generateAppleIcons??!0,Q=[];for(let X of B)if(Q.push(`${q}/icon-${X}x${X}.png`),K)Q.push(`${q}/icon-${X}x${X}.webp`);if(Y){Q.push(`${q}/apple-touch-icon.png`);for(let X of v)Q.push(`${q}/apple-touch-icon-${X}x${X}.png`)}for(let X of I)Q.push(`${q}/favicon-${X}x${X}.png`);return Q.push(`${q}/icon-maskable-512x512.png`),Q}async function O(){try{return await import("sharp"),!0}catch{return!1}}function m(G,B,q,K){let Y=[],Q=K.join(" ");for(let X of G)if(Y.push({src:`${B}/icon-${X}x${X}.png`,sizes:`${X}x${X}`,type:"image/png",purpose:Q}),q)Y.push({src:`${B}/icon-${X}x${X}.webp`,sizes:`${X}x${X}`,type:"image/webp",purpose:Q});return Y}function g(G){return G.map((B)=>({name:B.name,short_name:B.shortName,description:B.description,url:B.url,icons:B.icons?.map((q)=>({src:q.src,sizes:q.sizes,type:q.type}))}))}function h(G){let B=G.pwa?.shareTarget;if(!B?.enabled)return;let q={action:B.action||"/share",method:B.method||"POST",enctype:B.enctype||"multipart/form-data",params:{title:B.params?.title||"title",text:B.params?.text||"text",url:B.params?.url||"url"}};if(B.acceptFiles&&B.acceptFiles.length>0)q.params.files=B.acceptFiles.map((K)=>({name:K.name,accept:K.accept}));return q}function c(G){let B=G.pwa?.fileHandlers;if(!B?.enabled||!B.accept)return;let q=[{action:B.action||"/",accept:B.accept,launch_type:B.launchType||"single-client"}];if(B.icons)q[0].icons=B.icons.map((K)=>({src:K.src,sizes:K.sizes,type:K.type}));return q}function l(G){let B=G.pwa?.protocolHandlers;if(!B||B.length===0)return;return B.map((q)=>({protocol:q.protocol,url:q.url}))}function u(G){let B=G.pwa;if(!B?.enabled||!B.manifest)return null;let{manifest:q,icons:K}=B,Y=K?.sizes||[72,96,128,144,152,192,384,512],Q=K?.outputDir?`/${K.outputDir}`:"/pwa-icons",X=K?.generateWebP??!0,j=K?.purpose||["any","maskable"],J={name:q.name,short_name:q.shortName||q.name,description:q.description,start_url:q.startUrl||"/",display:q.display||"standalone",display_override:q.displayOverride,orientation:q.orientation,theme_color:q.themeColor,background_color:q.backgroundColor,scope:q.scope||"/",lang:q.lang,dir:q.dir,id:q.id,categories:q.categories,icons:m(Y,Q,X,j)};if(q.shortcuts&&q.shortcuts.length>0)J.shortcuts=g(q.shortcuts);if(q.screenshots&&q.screenshots.length>0)J.screenshots=q.screenshots.map((U)=>({src:U.src,sizes:U.sizes,type:U.type,label:U.label,form_factor:U.platform}));let y=h(G);if(y)J.share_target=y;let V=c(G);if(V)J.file_handlers=V;let R=l(G);if(R)J.protocol_handlers=R;if(q.launchHandler)J.launch_handler={client_mode:q.launchHandler};if(q.handleLinks)J.handle_links=q.handleLinks;if(q.edgeSidePanel?.enabled)J.edge_side_panel={preferred_width:q.edgeSidePanel.preferredWidth};return J}function M(G){let B=u(G);if(!B)return"";let q=JSON.parse(JSON.stringify(B));return JSON.stringify(q,null,2)}function i(G,B){return{...B,...G,shortcuts:G.shortcuts||B.shortcuts,screenshots:G.screenshots||B.screenshots,categories:G.categories||B.categories}}function s(G){let B=[],q=G.pwa;if(!q?.enabled)return B;let K=q.manifest;if(!K)return B.push("PWA manifest configuration is missing"),B;if(!K.name)B.push("Manifest name is required");let Y=["fullscreen","standalone","minimal-ui","browser"];if(K.display&&!Y.includes(K.display))B.push(`Invalid display mode: ${K.display}. Must be one of: ${Y.join(", ")}`);let Q=["any","natural","landscape","landscape-primary","landscape-secondary","portrait","portrait-primary","portrait-secondary"];if(K.orientation&&!Q.includes(K.orientation))B.push(`Invalid orientation: ${K.orientation}`);let X=/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;if(K.themeColor&&!X.test(K.themeColor))B.push(`Invalid theme color: ${K.themeColor}. Must be a valid hex color.`);if(K.backgroundColor&&!X.test(K.backgroundColor))B.push(`Invalid background color: ${K.backgroundColor}. Must be a valid hex color.`);if(q.shareTarget?.enabled){if(!q.shareTarget.action)B.push("Share target action URL is required")}if(q.fileHandlers?.enabled){if(!q.fileHandlers.accept||Object.keys(q.fileHandlers.accept).length===0)B.push("File handlers must specify accepted MIME types")}if(q.protocolHandlers)for(let j of q.protocolHandlers){if(!j.protocol)B.push("Protocol handler missing protocol");if(!j.url||!j.url.includes("%s"))B.push(`Protocol handler URL must contain %s placeholder: ${j.protocol}`)}return B}import A from"path";async function b(G){let B=G.pwa?.offline;if(!B?.enabled)return"";if(B.page){let q=A.isAbsolute(B.page)?B.page:A.resolve(process.cwd(),B.page);if(await Bun.file(q).exists())try{let{processDirectives:K}=await import("./chunk-x01sw9wy.js"),Y=await Bun.file(q).text();return await K(Y,{},q,G,new Set)}catch(K){console.warn(`[PWA] Failed to process custom offline page: ${K}`)}else console.warn(`[PWA] Custom offline page not found: ${q}`)}return f(G)}function f(G){let B=G.pwa?.offline,q=G.pwa?.manifest,K=B?.fallbackTitle||"You are offline",Y=B?.fallbackMessage||"Please check your internet connection and try again.",Q=q?.themeColor||"#4f46e5",X=q?.shortName||q?.name||"App";return`<!DOCTYPE html>
4
4
  <html lang="en">
5
5
  <head>
6
6
  <meta charset="UTF-8">
@@ -1,3 +1,4 @@
1
+ import { neutralizeTemplateSyntax } from "./template-syntax-escape.js";
1
2
  const componentRegistry = new Map, componentCache = new Map;
2
3
  export function registerServerComponent(component) {
3
4
  componentRegistry.set(component.name, {
@@ -285,7 +286,7 @@ function escapeHtml(str) {
285
286
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
286
287
  }
287
288
  function escapeHtmlValue(str) {
288
- return escapeHtml(str).replace(/\{{2,}/g, (match) => Array.from({ length: match.length }, () => "&#123;").join("&#8288;")).replace(/\{(?=!)/g, "&#123;&#8288;");
289
+ return neutralizeTemplateSyntax(escapeHtml(str));
289
290
  }
290
291
  export const serverComponentDirective = {
291
292
  name: "serverComponent",
package/dist/signals.js CHANGED
@@ -4053,24 +4053,23 @@ catch (e) {
4053
4053
  // Helper to hide empty state
4054
4054
  const hideEmpty = () => {
4055
4055
  if (emptyElement) {
4056
- disposeSubtreeEffects(emptyElement);
4056
+ // A string @empty renders a text node, which has no subtree to dispose.
4057
+ if (emptyElement.nodeType === 1) disposeSubtreeEffects(emptyElement);
4057
4058
  emptyElement.remove();
4058
4059
  emptyElement = null;
4059
4060
  }
4060
4061
  };
4061
4062
 
4062
- // Map of key \u2192 item signal. When a keyed item is reused and its data
4063
- // changes, we update the signal \u2014 all bindings (:text, :if, :class)
4064
- // that track it automatically re-evaluate.
4065
- const itemSignalMap = new Map();
4066
-
4067
4063
  // Helper: create DOM element(s) for a single list item.
4068
- // The item is wrapped in a signal so bindings track it reactively.
4064
+ // The item is wrapped in a signal so bindings track it reactively. The pair
4065
+ // rides on the returned group, so reuse always reaches the signals the
4066
+ // bindings of THAT row actually read. A Map from key to signals could not:
4067
+ // two rows sharing a key overwrote each other in it, and the row that
4068
+ // reused the surviving entry was then bound to a signal nothing updated.
4069
4069
  const createItemElements = (item, index, key) => {
4070
4070
  const itemSignal = state(item);
4071
4071
  itemSignal._isStxLoopItem = true;
4072
4072
  const indexSignal = state(index);
4073
- if (key) itemSignalMap.set(key, { item: itemSignal, index: indexSignal });
4074
4073
 
4075
4074
  const itemScope = { ...globalHelpers, ...passedScope, ...(capturedScope || {}) };
4076
4075
  itemScope[itemName] = itemSignal;
@@ -4098,6 +4097,7 @@ catch (e) {
4098
4097
  });
4099
4098
  }
4100
4099
  remapForComponentScopes(elements);
4100
+ elements.__stx_signals = { item: itemSignal, index: indexSignal };
4101
4101
  return elements;
4102
4102
  };
4103
4103
 
@@ -4210,10 +4210,16 @@ catch (e) {
4210
4210
  clearRenderedItems();
4211
4211
  if (emptyExpr) {
4212
4212
  const emptyContent = evalLazy(emptyExpr);
4213
+ // Held in the same slot the @for-empty template uses, so hideEmpty
4214
+ // takes it away when the list fills and a second empty render
4215
+ // replaces it instead of stacking another one. It used to go into
4216
+ // currentElements, which teardown does not walk -- it walks
4217
+ // currentGroups -- so the text stayed in the document forever: an
4218
+ // empty list that filled read "Nothing herea".
4219
+ hideEmpty();
4213
4220
  if (emptyContent && typeof emptyContent === 'string') {
4214
- const textNode = document.createTextNode(emptyContent);
4215
- parent.insertBefore(textNode, placeholder);
4216
- currentElements.push(textNode);
4221
+ emptyElement = document.createTextNode(emptyContent);
4222
+ parent.insertBefore(emptyElement, placeholder);
4217
4223
  }
4218
4224
  } else if (emptyTemplate) {
4219
4225
  showEmpty();
@@ -4247,7 +4253,15 @@ catch (e) {
4247
4253
  // Build new element list, reusing existing DOM nodes by key
4248
4254
  const newElements = [];
4249
4255
  const newGroups = [];
4250
- const usedKeys = new Set();
4256
+ // Keys seen in THIS pass, for the duplicate warning only. Reuse is decided
4257
+ // by whether a group under the key is still unconsumed, not by whether the
4258
+ // key has been seen: gating it on the key made every duplicate occurrence
4259
+ // build a NEW group, while the removal pass below skipped the key as used
4260
+ // and left the old ones in the document. The list grew by one row on every
4261
+ // update, without bound, and the first duplicate stayed bound to a signal
4262
+ // the reconciliation never wrote to.
4263
+ const seenKeys = new Set();
4264
+ let warnedDuplicateKey = false;
4251
4265
 
4252
4266
  // Minimal-move reconciliation.
4253
4267
  //
@@ -4274,7 +4288,13 @@ catch (e) {
4274
4288
  const key = newKeys[i];
4275
4289
  const existing = oldKeyMap.get(key);
4276
4290
 
4277
- if (existing && existing.length > 0 && !usedKeys.has(key)) {
4291
+ if (seenKeys.has(key) && !warnedDuplicateKey) {
4292
+ warnedDuplicateKey = true;
4293
+ console.warn('[STX] :for has more than one item with the key ' + key + '. Rows are matched in order, so give each item a key of its own.');
4294
+ }
4295
+ seenKeys.add(key);
4296
+
4297
+ if (existing && existing.length > 0) {
4278
4298
  // Reuse this item's nodes \u2014 move the whole group into position, in
4279
4299
  // order, so a multi-node row stays contiguous and correctly ordered.
4280
4300
  // A structural directive may intentionally have detached one of the
@@ -4298,11 +4318,10 @@ catch (e) {
4298
4318
  }
4299
4319
  newElements.push(...group);
4300
4320
  newGroups.push(group);
4301
- usedKeys.add(key);
4302
4321
  // Update the item signal so bindings re-evaluate with new data.
4303
4322
  // This is the fix for #1669 \u2014 without this, reused elements show
4304
4323
  // stale data because the old bindings captured the old item via closure.
4305
- const signals = itemSignalMap.get(key);
4324
+ const signals = group.__stx_signals;
4306
4325
  if (signals) {
4307
4326
  signals.item.set(list[i]);
4308
4327
  signals.index.set(i);
@@ -4341,8 +4360,12 @@ catch (e) {
4341
4360
  // Remove old elements whose keys are no longer in the list.
4342
4361
  // Dispose any scopes registered inside removed items before the
4343
4362
  // remove() call (#1727).
4344
- for (const [key, groups] of oldKeyMap) {
4345
- if (!usedKeys.has(key)) {
4363
+ // Whatever is still in here was not consumed above -- either the key left
4364
+ // the list, or the list now has fewer rows under that key than it did.
4365
+ // The test used to be "this key was never used", which could not see the
4366
+ // second case, so a duplicate key's leftover rows stayed on screen.
4367
+ for (const groups of oldKeyMap.values()) {
4368
+ if (groups.length > 0) {
4346
4369
  // In a transition group, run the leave animation and let it remove the
4347
4370
  // node; otherwise remove immediately. Scopes are disposed up-front
4348
4371
  // either way (their destroy hooks shouldn't wait on a CSS transition).
@@ -4367,7 +4390,8 @@ catch (e) {
4367
4390
  removeGroup(group);
4368
4391
  }
4369
4392
  });
4370
- itemSignalMap.delete(key);
4393
+ // The row's signals went with its group, so there is no key-indexed
4394
+ // map left to prune here.
4371
4395
  }
4372
4396
  }
4373
4397
 
@@ -4461,9 +4485,27 @@ catch (e) {
4461
4485
  // branch rendered blank the moment the markup was well-formed enough to
4462
4486
  // survive as a real <template> (the #1784 markup fix). See #1784.
4463
4487
  b.isTemplate = b.el.tagName === 'TEMPLATE';
4464
- b.nodes = b.isTemplate
4465
- ? Array.prototype.slice.call(b.el.content.cloneNode(true).childNodes)
4466
- : [b.el];
4488
+ // A template branch renders CLONES, re-cloned on every show, and marks
4489
+ // where they end -- the same contract the single-template :if got in
4490
+ // #1955. Its content was cloned once here and that one node set was
4491
+ // toggled in and out, which broke two ways as soon as the branch held a
4492
+ // structural directive: a :for inside it replaces its own source element
4493
+ // with a placeholder comment and renders rows as SIBLINGS, so hiding
4494
+ // removed neither (the rows were never in this list, and the source node
4495
+ // it did hold was already detached, so the parentNode guard skipped it)
4496
+ // and both branches stayed on screen at once. Re-showing then re-inserted
4497
+ // that source element, painting the literal text of its interpolations.
4498
+ // An element branch still owns exactly the node it was bound to: whatever
4499
+ // a directive inside it renders is inside that element.
4500
+ // Snapshot the content BEFORE anything processes it, and clone each show
4501
+ // from the snapshot rather than from the live template. A nested :for
4502
+ // does not leave content pristine -- processing moves its row source out
4503
+ // and the attributes it consumed (x-model, :id) do not come back -- so a
4504
+ // second clone taken from the element would arrive already stripped, with
4505
+ // no binding and no way to acquire one.
4506
+ b.pristine = b.isTemplate ? b.el.content.cloneNode(true) : null;
4507
+ b.nodes = b.isTemplate ? [] : [b.el];
4508
+ b.endMarker = null;
4467
4509
  b.el.remove();
4468
4510
  b.childrenProcessed = false;
4469
4511
  });
@@ -4547,17 +4589,53 @@ catch (e2) {
4547
4589
  // components under reactive conditionals (#1737). Permanent disposal is
4548
4590
  // still handled by bindFor (item removal) and cleanupContainer (SPA nav).
4549
4591
  if (currentIdx !== -1) {
4550
- chain[currentIdx].el.__stx_chain_active = false;
4551
- chain[currentIdx].nodes.forEach(function (n) {
4552
- if (!n.parentNode) return;
4553
- // Detached nodes are out of reach of a DOM walk, so a row removed
4554
- // while this branch is hidden could not dispose the effects it was
4555
- // processed with (#1954). Same registry the single-element :if uses.
4556
- var host = n.parentNode;
4557
- if (!host.__stx_detached_if) host.__stx_detached_if = new Set();
4558
- host.__stx_detached_if.add(n);
4559
- host.removeChild(n);
4560
- });
4592
+ var gone = chain[currentIdx];
4593
+ gone.el.__stx_chain_active = false;
4594
+
4595
+ if (gone.isTemplate) {
4596
+ // Everything this branch rendered goes, not just the clones it owns:
4597
+ // a nested :for or :if renders siblings of its own that no clone list
4598
+ // contains. The marker bounds the range, so a sibling that belongs to
4599
+ // a later branch or to ordinary markup is never touched.
4600
+ if (gone.endMarker && gone.endMarker.parentNode) {
4601
+ var host = gone.endMarker.parentNode;
4602
+ var doomed = gone.placeholder.nextSibling;
4603
+ while (doomed && doomed !== gone.endMarker) {
4604
+ var nextDoomed = doomed.nextSibling;
4605
+ disposeSubtreeEffects(doomed);
4606
+ host.removeChild(doomed);
4607
+ doomed = nextDoomed;
4608
+ }
4609
+ gone.endMarker.remove();
4610
+ gone.endMarker = null;
4611
+ }
4612
+ gone.nodes.forEach(function (n) {
4613
+ if (!n.parentNode) return;
4614
+ disposeSubtreeEffects(n);
4615
+ n.parentNode.removeChild(n);
4616
+ });
4617
+ // The next show clones afresh and processes again, so these effects
4618
+ // are finished with; leaving them bound only accumulates work (#1954).
4619
+ if (typeof gone.el.__stx_chain_disposers === 'function') {
4620
+ try { gone.el.__stx_chain_disposers(); }
4621
+ catch (e) { /* a disposer of its own is not this branch's problem */ }
4622
+ gone.el.__stx_chain_disposers = null;
4623
+ }
4624
+ gone.nodes = [];
4625
+ gone.childrenProcessed = false;
4626
+ }
4627
+ else {
4628
+ gone.nodes.forEach(function (n) {
4629
+ if (!n.parentNode) return;
4630
+ // Detached nodes are out of reach of a DOM walk, so a row removed
4631
+ // while this branch is hidden could not dispose the effects it was
4632
+ // processed with (#1954). Same registry the single-element :if uses.
4633
+ var host = n.parentNode;
4634
+ if (!host.__stx_detached_if) host.__stx_detached_if = new Set();
4635
+ host.__stx_detached_if.add(n);
4636
+ host.removeChild(n);
4637
+ });
4638
+ }
4561
4639
  }
4562
4640
 
4563
4641
  // Insert + process the newly-picked branch.
@@ -4567,11 +4645,19 @@ catch (e2) {
4567
4645
  // Insert this branch's node(s) before the placeholder's next sibling.
4568
4646
  // The anchor is captured once so the nodes stack in source order.
4569
4647
  var anchor = pick.placeholder.nextSibling;
4648
+ var pickHost = pick.placeholder.parentNode;
4649
+ if (pick.isTemplate && pick.nodes.length === 0)
4650
+ pick.nodes = Array.prototype.slice.call(pick.pristine.cloneNode(true).childNodes);
4570
4651
  pick.nodes.forEach(function (n) {
4571
- var host = pick.placeholder.parentNode;
4572
- if (host.__stx_detached_if) host.__stx_detached_if.delete(n);
4573
- host.insertBefore(n, anchor);
4652
+ if (pickHost.__stx_detached_if) pickHost.__stx_detached_if.delete(n);
4653
+ pickHost.insertBefore(n, anchor);
4574
4654
  });
4655
+ // Bound AFTER the clones, so hiding can remove whatever a structural
4656
+ // directive inside the branch renders as their sibling.
4657
+ if (pick.isTemplate) {
4658
+ if (!pick.endMarker) pick.endMarker = document.createComment('stx-if-chain-end');
4659
+ pickHost.insertBefore(pick.endMarker, anchor);
4660
+ }
4575
4661
  pick.el.__stx_shown_at = performance.now();
4576
4662
  if (!pick.childrenProcessed) {
4577
4663
  pick.childrenProcessed = true;
package/dist/slots.js CHANGED
@@ -65,6 +65,88 @@ function findTagEnd(content, startIndex) {
65
65
  }
66
66
  return -1;
67
67
  }
68
+ function findMatchingElementClose(content, tagName, startIndex) {
69
+ const wanted = tagName.toLowerCase();
70
+ let depth = 1, cursor = startIndex;
71
+ while (cursor < content.length) {
72
+ const tagStart = content.indexOf("<", cursor);
73
+ if (tagStart === -1)
74
+ return -1;
75
+ if (content.startsWith("<!--", tagStart)) {
76
+ const commentEnd = content.indexOf("-->", tagStart + 4);
77
+ cursor = commentEnd === -1 ? content.length : commentEnd + 3;
78
+ continue;
79
+ }
80
+ const tagEnd = findTagEnd(content, tagStart + 1);
81
+ if (tagEnd === -1)
82
+ return -1;
83
+ const tag = content.slice(tagStart, tagEnd), tagMatch = tag.match(/^<\s*(\/?)\s*([a-zA-Z][\w-]*)/);
84
+ if (tagMatch && tagMatch[2].toLowerCase() === wanted) {
85
+ if (tagMatch[1] === "/") {
86
+ depth--;
87
+ if (depth === 0)
88
+ return tagEnd;
89
+ } else if (!/\/\s*>$/.test(tag))
90
+ depth++;
91
+ }
92
+ cursor = tagEnd;
93
+ }
94
+ return -1;
95
+ }
96
+ function findDirectSlotAttrElements(content) {
97
+ const found = [], stack = [], slotAttrPattern = /\bslot\s*=\s*(?:"([^"]+)"|'([^']+)')/i;
98
+ let cursor = 0;
99
+ while (cursor < content.length) {
100
+ const tagStart = content.indexOf("<", cursor);
101
+ if (tagStart === -1)
102
+ break;
103
+ if (content.startsWith("<!--", tagStart)) {
104
+ const commentEnd = content.indexOf("-->", tagStart + 4);
105
+ cursor = commentEnd === -1 ? content.length : commentEnd + 3;
106
+ continue;
107
+ }
108
+ const tagEnd = findTagEnd(content, tagStart + 1);
109
+ if (tagEnd === -1)
110
+ break;
111
+ const tag = content.slice(tagStart, tagEnd), tagMatch = tag.match(/^<\s*(\/?)\s*([a-zA-Z][\w-]*)/);
112
+ if (!tagMatch) {
113
+ cursor = tagEnd;
114
+ continue;
115
+ }
116
+ const tagName = tagMatch[2].toLowerCase();
117
+ if (tagMatch[1] === "/") {
118
+ const matchingIndex = stack.lastIndexOf(tagName);
119
+ if (matchingIndex !== -1)
120
+ stack.splice(matchingIndex);
121
+ cursor = tagEnd;
122
+ continue;
123
+ }
124
+ const selfClosing = /\/\s*>$/.test(tag), slotMatch = stack.length === 0 ? tag.match(slotAttrPattern) : null;
125
+ if (slotMatch) {
126
+ const slotName = slotMatch[1] || slotMatch[2], openTag = tag.replace(slotAttrPattern, "").replace(/\s+(?=\/?>$)/, "").replace(/\s{2,}/g, " "), closeEnd = selfClosing || VOID_ELEMENTS.has(tagName) ? -1 : findMatchingElementClose(content, tagName, tagEnd);
127
+ if (closeEnd === -1) {
128
+ found.push({ start: tagStart, end: tagEnd, slotName, element: openTag });
129
+ cursor = tagEnd;
130
+ } else {
131
+ found.push({ start: tagStart, end: closeEnd, slotName, element: openTag + content.slice(tagEnd, closeEnd) });
132
+ cursor = closeEnd;
133
+ }
134
+ continue;
135
+ }
136
+ if (!selfClosing && !VOID_ELEMENTS.has(tagName)) {
137
+ stack.push(tagName);
138
+ if (tagName === "script" || tagName === "style") {
139
+ const rawClose = content.toLowerCase().indexOf(`</${tagName}>`, tagEnd);
140
+ if (rawClose !== -1) {
141
+ cursor = rawClose;
142
+ continue;
143
+ }
144
+ }
145
+ }
146
+ cursor = tagEnd;
147
+ }
148
+ return found;
149
+ }
68
150
  function findDirectNamedSlotTemplates(content) {
69
151
  const slots = [], stack = [], slotOpenPattern = /^<template\s+(?:#([a-zA-Z][\w-]*)|v-slot:([a-zA-Z][\w-]*)|slot="([a-zA-Z][\w-]*)")(?:\s*=\s*"([^"]*)")?[^>]*>$/i;
70
152
  let cursor = 0;
@@ -141,24 +223,16 @@ export function parseSlots(childContent) {
141
223
  slotsToRemove.sort((a, b) => b.start - a.start);
142
224
  for (const slot of slotsToRemove)
143
225
  workingContent = workingContent.slice(0, slot.start) + workingContent.slice(slot.end);
144
- const slotAttrElements = [], pairedSlotAttrRegex = /<([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*?)\bslot="([^"]+)"([^>]*)>([\s\S]*?)<\/\1>/gi;
145
- let slotAttrMatch;
146
- while ((slotAttrMatch = pairedSlotAttrRegex.exec(workingContent)) !== null) {
147
- const [fullMatch, tag, before, slotName, after, inner] = slotAttrMatch, cleanBefore = before.replace(/\s*slot="[^"]*"\s*/, " ").trimEnd(), cleanAfter = after.replace(/\s*slot="[^"]*"\s*/, " ").trimEnd(), attrs = (cleanBefore + cleanAfter).trim(), element = `<${tag}${attrs ? " " + attrs : ""}>${inner}</${tag}>`;
148
- slotAttrElements.push({ start: slotAttrMatch.index, end: slotAttrMatch.index + fullMatch.length, slotName, element });
149
- }
150
- const selfClosingSlotAttrRegex = /<([a-zA-Z][a-zA-Z0-9-]*)\b([^>]*?)\bslot="([^"]+)"([^>]*?)\/>/gi;
151
- while ((slotAttrMatch = selfClosingSlotAttrRegex.exec(workingContent)) !== null) {
152
- const [fullMatch, tag, before, slotName, after] = slotAttrMatch, cleanBefore = before.replace(/\s*slot="[^"]*"\s*/, " ").trimEnd(), cleanAfter = after.replace(/\s*slot="[^"]*"\s*/, " ").trimEnd(), attrs = (cleanBefore + cleanAfter).trim(), element = `<${tag}${attrs ? " " + attrs : ""} />`;
153
- slotAttrElements.push({ start: slotAttrMatch.index, end: slotAttrMatch.index + fullMatch.length, slotName, element });
154
- }
155
- slotAttrElements.sort((a, b) => b.start - a.start);
226
+ const slotAttrElements = findDirectSlotAttrElements(workingContent);
156
227
  for (const item of slotAttrElements) {
157
228
  const existing = result.named.get(item.slotName);
158
229
  if (existing)
159
230
  existing.content += item.element;
160
231
  else
161
232
  result.named.set(item.slotName, { name: item.slotName, content: item.element });
233
+ }
234
+ for (let i = slotAttrElements.length - 1;i >= 0; i--) {
235
+ const item = slotAttrElements[i];
162
236
  workingContent = workingContent.slice(0, item.start) + workingContent.slice(item.end);
163
237
  }
164
238
  result.default = workingContent.trim();