sibujs 4.7.0 → 4.8.0

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 (81) hide show
  1. package/dist/browser.cjs +45 -4
  2. package/dist/browser.d.cts +23 -0
  3. package/dist/browser.d.ts +23 -0
  4. package/dist/browser.js +6 -5
  5. package/dist/build.cjs +513 -78
  6. package/dist/build.d.cts +22 -8
  7. package/dist/build.d.ts +22 -8
  8. package/dist/build.js +68 -21
  9. package/dist/cdn.dev.global.js +15 -13
  10. package/dist/cdn.full.dev.global.js +15 -13
  11. package/dist/cdn.full.global.js +14 -12
  12. package/dist/cdn.global.js +14 -12
  13. package/dist/chunk-3ACAAQIN.js +224 -0
  14. package/dist/{chunk-DKXRACVN.js → chunk-44MTL7ED.js} +6 -6
  15. package/dist/{chunk-3QWBSL5R.js → chunk-4WMSTZ7J.js} +8 -5
  16. package/dist/{chunk-H3SRKIYX.js → chunk-62BDYGAG.js} +8 -0
  17. package/dist/{chunk-BSY63EM6.js → chunk-C5PEWEGH.js} +20 -5
  18. package/dist/{chunk-BC2SECJD.js → chunk-C7AA2CZX.js} +1 -1
  19. package/dist/{chunk-SLM3IA34.js → chunk-CZPRSWSS.js} +1 -1
  20. package/dist/{chunk-FQRUXCEE.js → chunk-DSID757X.js} +1 -1
  21. package/dist/{chunk-ZVL7TY4K.js → chunk-EGUQLBHE.js} +22 -8
  22. package/dist/{chunk-2INLLLMZ.js → chunk-EXXSN3BW.js} +202 -26
  23. package/dist/{chunk-HURREPU2.js → chunk-FC6KCJSA.js} +87 -150
  24. package/dist/{chunk-Z3OHK6QT.js → chunk-GSGNX35Q.js} +5 -5
  25. package/dist/{chunk-XYV3EDB7.js → chunk-J3YQ457H.js} +37 -20
  26. package/dist/{chunk-VZSG24LS.js → chunk-JUTFX2RM.js} +18 -10
  27. package/dist/{chunk-RUSSKG6G.js → chunk-KJ3UKABJ.js} +1 -1
  28. package/dist/{chunk-UNWRJRKC.js → chunk-LI4W3U6C.js} +1 -1
  29. package/dist/{chunk-FNJXNGYZ.js → chunk-O6TRVMJI.js} +58 -6
  30. package/dist/{chunk-7LUJQAOJ.js → chunk-Q5J2LVKH.js} +1 -1
  31. package/dist/{chunk-4AWA2PVD.js → chunk-QGMSESDD.js} +10 -5
  32. package/dist/{chunk-XZR4PXRE.js → chunk-RGLPEBDE.js} +13 -10
  33. package/dist/{chunk-JWKYU5GV.js → chunk-TQQUN4WR.js} +1 -1
  34. package/dist/{chunk-WOLJZUFQ.js → chunk-UCNP3HVM.js} +12 -8
  35. package/dist/{chunk-WEQ3DMVL.js → chunk-V3LHZSU5.js} +1 -1
  36. package/dist/{chunk-UOL2ECCS.js → chunk-VWT3VXPL.js} +18 -12
  37. package/dist/{chunk-2BPG2XDA.js → chunk-ZVLHNEEL.js} +17 -35
  38. package/dist/{customElement-MmInOW1U.d.ts → customElement-pYDKUzWl.d.cts} +11 -3
  39. package/dist/{customElement-MmInOW1U.d.cts → customElement-pYDKUzWl.d.ts} +11 -3
  40. package/dist/data.cjs +9 -2
  41. package/dist/data.js +6 -6
  42. package/dist/devtools.cjs +1 -1
  43. package/dist/devtools.js +5 -5
  44. package/dist/ecosystem.cjs +400 -37
  45. package/dist/ecosystem.js +9 -8
  46. package/dist/extras.cjs +621 -252
  47. package/dist/extras.d.cts +2 -2
  48. package/dist/extras.d.ts +2 -2
  49. package/dist/extras.js +24 -23
  50. package/dist/index.cjs +464 -70
  51. package/dist/index.d.cts +28 -2
  52. package/dist/index.d.ts +28 -2
  53. package/dist/index.js +16 -12
  54. package/dist/motion.cjs +1 -1
  55. package/dist/motion.js +3 -3
  56. package/dist/patterns.cjs +10 -7
  57. package/dist/patterns.js +6 -6
  58. package/dist/performance.cjs +31 -2
  59. package/dist/performance.d.cts +12 -2
  60. package/dist/performance.d.ts +12 -2
  61. package/dist/performance.js +8 -6
  62. package/dist/plugins.cjs +558 -231
  63. package/dist/plugins.d.cts +8 -3
  64. package/dist/plugins.d.ts +8 -3
  65. package/dist/plugins.js +22 -28
  66. package/dist/{ssr-XOTUASDO.js → ssr-5OWAH3U2.js} +4 -2
  67. package/dist/{ssr-Bli9XRW5.d.ts → ssr-C8hgIAw7.d.cts} +6 -0
  68. package/dist/{ssr-Bli9XRW5.d.cts → ssr-C8hgIAw7.d.ts} +6 -0
  69. package/dist/ssr.cjs +457 -131
  70. package/dist/ssr.d.cts +55 -5
  71. package/dist/ssr.d.ts +55 -5
  72. package/dist/ssr.js +11 -9
  73. package/dist/testing.cjs +3 -3
  74. package/dist/testing.js +4 -4
  75. package/dist/ui.cjs +608 -240
  76. package/dist/ui.d.cts +1 -1
  77. package/dist/ui.d.ts +1 -1
  78. package/dist/ui.js +9 -8
  79. package/dist/widgets.cjs +1 -1
  80. package/dist/widgets.js +7 -7
  81. package/package.json +5 -4
package/dist/ui.cjs CHANGED
@@ -834,7 +834,7 @@ function forEachSubscriber(signal2, visit) {
834
834
  }
835
835
 
836
836
  // src/reactivity/track.ts
837
- var _runtimeVersion = true ? "4.7.0" : "dev";
837
+ var _runtimeVersion = true ? "4.8.0" : "dev";
838
838
  var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("sibujs.reactive.v1");
839
839
  function resolveReactiveApi() {
840
840
  const g = globalThis;
@@ -2179,33 +2179,402 @@ function createDialogAria(element, options = {}) {
2179
2179
  return { titleId, descriptionId };
2180
2180
  }
2181
2181
 
2182
- // src/ui/scopedStyle.ts
2183
- var _scope = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.scopedStyle.v1"), () => ({ n: 0 }));
2184
- function decodeCssEscapes(css) {
2185
- return css.replace(/\\([0-9a-f]{1,6})[ \t\n\r\f]?|\\([^\n])/gi, (_match, hex, ch) => {
2186
- if (hex) {
2187
- const code = Number.parseInt(hex, 16);
2188
- if (Number.isFinite(code) && code > 0 && code <= 1114111) {
2189
- try {
2190
- return String.fromCodePoint(code);
2191
- } catch {
2192
- return "";
2182
+ // src/utils/sanitize.ts
2183
+ function stripControlChars(value) {
2184
+ return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
2185
+ }
2186
+ var ASCII_UPPER = /[A-Z]/;
2187
+ var ASCII_UPPER_G = /[A-Z]/g;
2188
+ function canonicalAttrName(name) {
2189
+ return ASCII_UPPER.test(name) ? name.replace(ASCII_UPPER_G, (ch) => String.fromCharCode(ch.charCodeAt(0) + 32)) : name;
2190
+ }
2191
+ function isEventHandlerAttr(name) {
2192
+ if (name.length < 3) return false;
2193
+ const lower = name.toLowerCase();
2194
+ return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
2195
+ }
2196
+ var SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
2197
+ var OUTER_TRIM = /^[\x00-\x20\x7f-\x9f]+|[\x00-\x20\x7f-\x9f]+$/g;
2198
+ function sanitizeUrl(url) {
2199
+ const probe = stripControlChars(url).trim();
2200
+ if (!probe) return "";
2201
+ const output = url.replace(OUTER_TRIM, "");
2202
+ if (probe.length >= 2 && (probe[0] === "\\" || probe[0] === "/") && (probe[1] === "\\" || probe[1] === "/") && (probe[0] === "\\" || probe[1] === "\\")) {
2203
+ return "";
2204
+ }
2205
+ const lower = probe.toLowerCase();
2206
+ let schemeEnd = -1;
2207
+ for (let i = 0; i < lower.length; i++) {
2208
+ const ch = lower.charCodeAt(i);
2209
+ if (ch === 58) {
2210
+ schemeEnd = i;
2211
+ break;
2212
+ }
2213
+ if (ch === 47 || ch === 63 || ch === 35) break;
2214
+ }
2215
+ if (schemeEnd === -1) return output;
2216
+ const scheme = lower.slice(0, schemeEnd + 1);
2217
+ if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return output;
2218
+ if (SAFE_URL_PROTOCOLS.indexOf(scheme) === -1) return "";
2219
+ return output;
2220
+ }
2221
+ var SRCSET_DESCRIPTOR = /^\d+(\.\d+)?[wx]$/;
2222
+ function isSrcsetWhitespace(ch) {
2223
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
2224
+ }
2225
+ function sanitizeSrcset(value) {
2226
+ const input = String(value);
2227
+ const out = [];
2228
+ let i = 0;
2229
+ const n = input.length;
2230
+ while (i < n) {
2231
+ while (i < n && (isSrcsetWhitespace(input[i]) || input[i] === ",")) i++;
2232
+ if (i >= n) break;
2233
+ const urlStart = i;
2234
+ while (i < n && !isSrcsetWhitespace(input[i])) i++;
2235
+ let url = input.slice(urlStart, i);
2236
+ const descriptors = [];
2237
+ if (url.endsWith(",")) {
2238
+ url = url.replace(/,+$/, "");
2239
+ } else {
2240
+ let current = "";
2241
+ let inParens = false;
2242
+ while (i < n) {
2243
+ const ch = input[i];
2244
+ if (inParens) {
2245
+ current += ch;
2246
+ if (ch === ")") inParens = false;
2247
+ i++;
2248
+ continue;
2249
+ }
2250
+ if (isSrcsetWhitespace(ch)) {
2251
+ if (current) descriptors.push(current);
2252
+ current = "";
2253
+ i++;
2254
+ continue;
2255
+ }
2256
+ if (ch === ",") {
2257
+ i++;
2258
+ break;
2193
2259
  }
2260
+ if (ch === "(") inParens = true;
2261
+ current += ch;
2262
+ i++;
2194
2263
  }
2195
- return "";
2264
+ if (current) descriptors.push(current);
2196
2265
  }
2197
- return ch || "";
2266
+ if (url === "") continue;
2267
+ if (descriptors.length > 1) continue;
2268
+ if (descriptors.length === 1 && !SRCSET_DESCRIPTOR.test(descriptors[0])) continue;
2269
+ const safe = sanitizeUrl(url);
2270
+ if (!safe) continue;
2271
+ if (safe.startsWith(",") || safe.endsWith(",")) continue;
2272
+ out.push(descriptors.length === 1 ? `${safe} ${descriptors[0]}` : safe);
2273
+ }
2274
+ return out.join(", ");
2275
+ }
2276
+ var CSS_DANGER_GATE = /[(:@\\]/;
2277
+ function isCssWhitespace(ch) {
2278
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
2279
+ }
2280
+ function isHexDigit(ch) {
2281
+ return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
2282
+ }
2283
+ function decodeCssEscapes(value) {
2284
+ let out = "";
2285
+ for (let i = 0; i < value.length; i++) {
2286
+ const ch = value[i];
2287
+ if (ch !== "\\") {
2288
+ out += ch;
2289
+ continue;
2290
+ }
2291
+ if (i + 1 >= value.length) break;
2292
+ const next = value[i + 1];
2293
+ if (next === "\n" || next === "\f") {
2294
+ i += 1;
2295
+ continue;
2296
+ }
2297
+ if (next === "\r") {
2298
+ i += value[i + 2] === "\n" ? 2 : 1;
2299
+ continue;
2300
+ }
2301
+ if (isHexDigit(next)) {
2302
+ let hex = "";
2303
+ let j = i + 1;
2304
+ while (j < value.length && hex.length < 6 && isHexDigit(value[j])) {
2305
+ hex += value[j];
2306
+ j++;
2307
+ }
2308
+ if (j < value.length && isCssWhitespace(value[j])) {
2309
+ j += value[j] === "\r" && value[j + 1] === "\n" ? 2 : 1;
2310
+ }
2311
+ const code = Number.parseInt(hex, 16);
2312
+ const invalid = code === 0 || code > 1114111 || code >= 55296 && code <= 57343;
2313
+ out += invalid ? "\uFFFD" : String.fromCodePoint(code);
2314
+ i = j - 1;
2315
+ continue;
2316
+ }
2317
+ out += next;
2318
+ i += 1;
2319
+ }
2320
+ return out;
2321
+ }
2322
+ var BLOCKED_CSS_NEEDLES = [
2323
+ "url(",
2324
+ "expression(",
2325
+ "javascript:",
2326
+ "vbscript:",
2327
+ "-moz-binding",
2328
+ "behavior:",
2329
+ "@import",
2330
+ "image-set(",
2331
+ "filter:progid",
2332
+ // CSS Images 4 `image("…")` and CSS Values 5 `src("…")` take a URL as a
2333
+ // plain STRING, so they fetch without ever spelling `url(`. Support is thin
2334
+ // today, which is exactly when a blocklist should get ahead of it.
2335
+ "image(",
2336
+ "src("
2337
+ ];
2338
+ var CSS_REASON_MOZ_BINDING = 4;
2339
+ var CSS_REASON_BEHAVIOR = 5;
2340
+ var CSS_REASON_PROGID = 8;
2341
+ function describeElement(el) {
2342
+ if (!el) return "an element";
2343
+ let out = el.tagName.toLowerCase();
2344
+ const id = el.getAttribute("id");
2345
+ if (id) out += `#${id}`;
2346
+ const cls = el.getAttribute("class");
2347
+ if (cls) out += `.${cls.trim().split(/\s+/).slice(0, 3).join(".")}`;
2348
+ return `<${out}>`;
2349
+ }
2350
+ var warnedDeclarations = /* @__PURE__ */ new Set();
2351
+ var MAX_WARNED_DECLARATIONS = 100;
2352
+ var declarationCapAnnounced = false;
2353
+ function warnDroppedDeclaration(value, reason, ctx) {
2354
+ devWarnLazy(() => {
2355
+ const reasons = [
2356
+ "url() in an inline style is an exfiltration channel: the browser fetches it immediately and the URL's path or query can carry page data off-origin",
2357
+ "expression() executes arbitrary script in legacy engines",
2358
+ "the javascript: scheme executes script",
2359
+ "the vbscript: scheme executes script",
2360
+ "-moz-binding loads and runs remote XBL",
2361
+ "behavior: loads and runs a remote HTC script",
2362
+ "@import pulls in a remote stylesheet",
2363
+ "image-set() fetches remote images, the same exfiltration channel as url()",
2364
+ "filter:progid activates legacy scriptable filters",
2365
+ "image() fetches its string argument as a URL, the same exfiltration channel as url()",
2366
+ "src() fetches its string argument as a URL, the same exfiltration channel as url()"
2367
+ ];
2368
+ const declaration = ctx?.property ? `${ctx.property}: ${value}` : value;
2369
+ const where = describeElement(ctx?.element);
2370
+ const key = `${where}|${declaration}`;
2371
+ if (warnedDeclarations.has(key)) return "";
2372
+ if (warnedDeclarations.size >= MAX_WARNED_DECLARATIONS) {
2373
+ if (declarationCapAnnounced) return "";
2374
+ declarationCapAnnounced = true;
2375
+ return `${MAX_WARNED_DECLARATIONS} distinct dropped style declarations have been reported; suppressing further ones for the rest of this session so they cannot flood the console. The declarations are still dropped \u2014 only the reporting stops. Fix the reported ones and reload to see any that remain.`;
2376
+ }
2377
+ warnedDeclarations.add(key);
2378
+ return `style declaration "${declaration}" was dropped by the style sanitizer on ${where} \u2014 ${reasons[reason]}. The element renders without it, which usually looks like a missing image rather than an error. Sanctioned alternatives: for a content image use an <img> element (or a <picture>); for decoration, put the rule in a stylesheet and set a class instead \u2014 stylesheet CSS is not sanitized, only inline style attributes are. If this declaration is not yours, it came from untrusted data and the drop just prevented an exfiltration.`;
2198
2379
  });
2199
2380
  }
2200
- function sanitizeCSS(css) {
2201
- let sanitized = decodeCssEscapes(css);
2202
- sanitized = sanitized.replace(/@import\s+[^;]+;/gi, "/* @import removed */");
2203
- sanitized = sanitized.replace(/url\s*\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, "/* url() removed */");
2204
- sanitized = sanitized.replace(/expression\s*\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)/gi, "/* expression() removed */");
2205
- sanitized = sanitized.replace(/-moz-binding\s*:[^;]+;/gi, "/* -moz-binding removed */");
2206
- sanitized = sanitized.replace(/behavior\s*:[^;]+;/gi, "/* behavior removed */");
2207
- return sanitized;
2381
+ function normalizeCssForScan(value) {
2382
+ const normalized = value.includes("\\") ? decodeCssEscapes(value) : value;
2383
+ return normalized.toLowerCase().replace(/\s+/g, "");
2384
+ }
2385
+ function blockedCssReason(value) {
2386
+ if (!CSS_DANGER_GATE.test(value)) return -1;
2387
+ const lower = normalizeCssForScan(value);
2388
+ for (let i = 0; i < BLOCKED_CSS_NEEDLES.length; i++) {
2389
+ if (lower.includes(BLOCKED_CSS_NEEDLES[i])) return i;
2390
+ }
2391
+ return -1;
2392
+ }
2393
+ var STYLESHEET_BLOCKED_FUNCTIONS = ["url", "image-set", "image", "src", "expression"];
2394
+ function stripCssFunctions(css) {
2395
+ const pattern = new RegExp(`(?:-webkit-)?(${STYLESHEET_BLOCKED_FUNCTIONS.join("|")})\\s*\\(`, "gi");
2396
+ let out = "";
2397
+ let last = 0;
2398
+ for (let match = pattern.exec(css); match !== null; match = pattern.exec(css)) {
2399
+ const before = match.index > 0 ? css[match.index - 1] : "";
2400
+ if (/[\w-]/.test(before)) continue;
2401
+ let depth = 1;
2402
+ let i = pattern.lastIndex;
2403
+ let quote = "";
2404
+ for (; i < css.length && depth > 0; i++) {
2405
+ const ch = css[i];
2406
+ if (quote) {
2407
+ if (ch === "\\") i++;
2408
+ else if (ch === quote) quote = "";
2409
+ } else if (ch === '"' || ch === "'") quote = ch;
2410
+ else if (ch === "(") depth++;
2411
+ else if (ch === ")") depth--;
2412
+ }
2413
+ out += `${css.slice(last, match.index)}/* ${match[1].toLowerCase()}() removed */`;
2414
+ last = i;
2415
+ pattern.lastIndex = i;
2416
+ }
2417
+ return out + css.slice(last);
2418
+ }
2419
+ function sanitizeStylesheetText(css) {
2420
+ let text = css.includes("\\") ? decodeCssEscapes(css) : css;
2421
+ text = text.replace(/@import\b[^;{}]*;?/gi, "/* @import removed */");
2422
+ text = stripCssFunctions(text);
2423
+ text = text.replace(/(^|[;{\s])behavior\s*:[^;}]*;?/gi, "$1/* behavior removed */");
2424
+ text = text.replace(/(^|[;{\s])-moz-binding\s*:[^;}]*;?/gi, "$1/* -moz-binding removed */");
2425
+ text = text.replace(/(^|[;{\s])(-ms-)?filter\s*:[^;}]*progid[^;}]*;?/gi, "$1/* filter:progid removed */");
2426
+ return text;
2427
+ }
2428
+ function blockedDeclarationReason(property, value) {
2429
+ const reason = blockedCssReason(value);
2430
+ if (reason !== -1) return reason;
2431
+ const name = canonicalAttrName(property);
2432
+ if (name === "behavior") return CSS_REASON_BEHAVIOR;
2433
+ if (name === "-moz-binding") return CSS_REASON_MOZ_BINDING;
2434
+ if ((name === "filter" || name === "-ms-filter") && normalizeCssForScan(value).includes("progid:")) {
2435
+ return CSS_REASON_PROGID;
2436
+ }
2437
+ return -1;
2438
+ }
2439
+ function parseStyleDeclarations(css) {
2440
+ const chunks = [];
2441
+ let buf = "";
2442
+ let depth = 0;
2443
+ let quote = "";
2444
+ for (let i = 0; i < css.length; i++) {
2445
+ const ch = css[i];
2446
+ if (quote) {
2447
+ buf += ch;
2448
+ if (ch === "\\" && i + 1 < css.length) buf += css[++i];
2449
+ else if (ch === quote) quote = "";
2450
+ continue;
2451
+ }
2452
+ if (ch === "\\") {
2453
+ buf += ch;
2454
+ if (i + 1 < css.length) buf += css[++i];
2455
+ continue;
2456
+ }
2457
+ if (ch === "/" && css[i + 1] === "*") {
2458
+ const end = css.indexOf("*/", i + 2);
2459
+ if (end === -1) return null;
2460
+ i = end + 1;
2461
+ continue;
2462
+ }
2463
+ if (ch === '"' || ch === "'") quote = ch;
2464
+ else if (ch === "(" || ch === "[") depth++;
2465
+ else if (ch === ")" || ch === "]") {
2466
+ if (--depth < 0) return null;
2467
+ } else if (ch === "{" || ch === "}") return null;
2468
+ else if (ch === ";" && depth === 0) {
2469
+ chunks.push(buf);
2470
+ buf = "";
2471
+ continue;
2472
+ }
2473
+ buf += ch;
2474
+ }
2475
+ if (quote || depth !== 0) return null;
2476
+ chunks.push(buf);
2477
+ const out = [];
2478
+ for (const chunk of chunks) {
2479
+ const colon = chunk.indexOf(":");
2480
+ if (colon === -1) continue;
2481
+ const rawName = chunk.slice(0, colon).trim();
2482
+ if (!/^(--[\w-]+|-?[a-zA-Z_][\w-]*)$/.test(rawName)) continue;
2483
+ let value = chunk.slice(colon + 1).trim();
2484
+ const bang = /!\s*important\s*$/i.exec(value);
2485
+ if (bang) value = value.slice(0, bang.index).trim();
2486
+ if (value === "") continue;
2487
+ const property = rawName.startsWith("--") ? rawName : canonicalAttrName(rawName);
2488
+ out.push([property, value, bang !== null]);
2489
+ }
2490
+ return out;
2491
+ }
2492
+ function sanitizeStyleDeclarationList(cssText, context) {
2493
+ const input = String(cssText);
2494
+ const declarations = parseStyleDeclarations(input);
2495
+ if (declarations === null) {
2496
+ if (blockedCssReason(input) !== -1) warnDroppedDeclaration(input, blockedCssReason(input), context);
2497
+ return "";
2498
+ }
2499
+ const kept = [];
2500
+ for (const [property, value, important] of declarations) {
2501
+ const reason = blockedDeclarationReason(property, value);
2502
+ if (reason !== -1) {
2503
+ warnDroppedDeclaration(value, reason, { property, element: context?.element });
2504
+ continue;
2505
+ }
2506
+ kept.push(`${property}: ${value}${important ? " !important" : ""}`);
2507
+ }
2508
+ return kept.join("; ");
2509
+ }
2510
+ function sanitizeStyleAttribute(cssText, context) {
2511
+ const input = String(cssText);
2512
+ if (input.trim() === "") return "";
2513
+ if (typeof document === "undefined") return sanitizeStyleDeclarationList(input, context);
2514
+ const probe = document.createElement("div");
2515
+ try {
2516
+ probe.style.cssText = input;
2517
+ } catch {
2518
+ return "";
2519
+ }
2520
+ const declarations = [];
2521
+ for (let i = 0; i < probe.style.length; i++) {
2522
+ const property = probe.style[i];
2523
+ const value = probe.style.getPropertyValue(property);
2524
+ const reason = blockedDeclarationReason(property, value);
2525
+ if (reason !== -1) {
2526
+ warnDroppedDeclaration(value, reason, { property, element: context?.element });
2527
+ continue;
2528
+ }
2529
+ const priority = probe.style.getPropertyPriority(property);
2530
+ declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
2531
+ }
2532
+ return declarations.join("; ");
2533
+ }
2534
+ var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
2535
+ "href",
2536
+ "xlink:href",
2537
+ "src",
2538
+ "action",
2539
+ "formaction",
2540
+ "formtarget",
2541
+ "cite",
2542
+ "poster",
2543
+ "background",
2544
+ "srcset",
2545
+ "ping",
2546
+ "data",
2547
+ // `<html manifest>` is a URL sink too. It was carried only by the SSR
2548
+ // serializers' private copy of this set; folding that copy into this one would
2549
+ // otherwise have silently dropped its coverage.
2550
+ "manifest"
2551
+ ]);
2552
+ var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
2553
+ function isHtmlContentAttribute(name) {
2554
+ return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
2555
+ }
2556
+ function isUrlAttribute(attr) {
2557
+ return URL_ATTRIBUTES.has(attr.toLowerCase());
2208
2558
  }
2559
+ function isPolicyAttribute(name) {
2560
+ const lower = name.toLowerCase();
2561
+ return lower === "style" || URL_ATTRIBUTES.has(lower);
2562
+ }
2563
+ function sanitizeAttributeString(attr, value, context) {
2564
+ const lower = attr.toLowerCase();
2565
+ if (lower === "srcset") return sanitizeSrcset(value);
2566
+ if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
2567
+ if (lower === "style") return sanitizeStyleAttribute(value, context);
2568
+ return value;
2569
+ }
2570
+ function resolveAttributeValue(attr, value, context) {
2571
+ if (!isPolicyAttribute(attr)) return value;
2572
+ const safe = sanitizeAttributeString(attr, value, context);
2573
+ return safe === "" ? null : safe;
2574
+ }
2575
+
2576
+ // src/ui/scopedStyle.ts
2577
+ var _scope = globalSingleton(/* @__PURE__ */ Symbol.for("sibujs.scopedStyle.v1"), () => ({ n: 0 }));
2209
2578
  function firstCompoundEnd(selector) {
2210
2579
  let parens = 0;
2211
2580
  let brackets = 0;
@@ -2367,7 +2736,7 @@ function scopeStylesheet(css, attr) {
2367
2736
  function scopedStyle(css) {
2368
2737
  const id = `sibu-s${_scope.n++}`;
2369
2738
  const attr = `data-${id}`;
2370
- const safeCss = sanitizeCSS(css);
2739
+ const safeCss = sanitizeStylesheetText(css);
2371
2740
  const scopedCSS = scopeStylesheet(safeCss, attr);
2372
2741
  if (typeof document !== "undefined") {
2373
2742
  const styleEl = document.createElement("style");
@@ -2393,240 +2762,223 @@ function removeScopedStyle(scopeId) {
2393
2762
  if (el) el.remove();
2394
2763
  }
2395
2764
 
2396
- // src/utils/sanitize.ts
2397
- function stripControlChars(value) {
2398
- return value.replace(/[\x00-\x20\x7f-\x9f]+/g, "");
2399
- }
2400
- function isEventHandlerAttr(name) {
2401
- if (name.length < 3) return false;
2402
- const lower = name.toLowerCase();
2403
- return lower[0] === "o" && lower[1] === "n" && lower.charCodeAt(2) >= 97 && lower.charCodeAt(2) <= 122;
2404
- }
2405
- var SAFE_URL_PROTOCOLS = ["http:", "https:", "mailto:", "tel:", "ftp:"];
2406
- var OUTER_TRIM = /^[\x00-\x20\x7f-\x9f]+|[\x00-\x20\x7f-\x9f]+$/g;
2407
- function sanitizeUrl(url) {
2408
- const probe = stripControlChars(url).trim();
2409
- if (!probe) return "";
2410
- const output = url.replace(OUTER_TRIM, "");
2411
- const lower = probe.toLowerCase();
2412
- let schemeEnd = -1;
2413
- for (let i = 0; i < lower.length; i++) {
2414
- const ch = lower.charCodeAt(i);
2415
- if (ch === 58) {
2416
- schemeEnd = i;
2417
- break;
2418
- }
2419
- if (ch === 47 || ch === 63 || ch === 35) break;
2420
- }
2421
- if (schemeEnd === -1) return output;
2422
- const scheme = lower.slice(0, schemeEnd + 1);
2423
- if (!/^[a-z][a-z0-9+.-]*:$/.test(scheme)) return output;
2424
- if (SAFE_URL_PROTOCOLS.indexOf(scheme) === -1) return "";
2425
- return output;
2426
- }
2427
- var SRCSET_DESCRIPTOR = /^\s+\d+(\.\d+)?[wx]$/;
2428
- function sanitizeSrcset(value) {
2429
- const parts = value.split(",");
2430
- const out = [];
2431
- for (let i = 0; i < parts.length; i++) {
2432
- const part = parts[i].trim();
2433
- if (!part) continue;
2434
- const m = part.match(/^(\S+)(\s+.+)?$/);
2435
- if (!m) continue;
2436
- if (m[2] !== void 0 && !SRCSET_DESCRIPTOR.test(m[2])) continue;
2437
- const safe = sanitizeUrl(m[1]);
2438
- if (!safe) continue;
2439
- out.push(m[2] ? `${safe}${m[2]}` : safe);
2440
- }
2441
- return out.join(", ");
2442
- }
2443
- var CSS_DANGER_GATE = /[(:@\\]/;
2444
- function isCssWhitespace(ch) {
2765
+ // src/utils/metaRefresh.ts
2766
+ function isAsciiWhitespace(ch) {
2445
2767
  return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
2446
2768
  }
2447
- function isHexDigit(ch) {
2448
- return ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F";
2449
- }
2450
- function decodeCssEscapes2(value) {
2451
- let out = "";
2452
- for (let i = 0; i < value.length; i++) {
2453
- const ch = value[i];
2454
- if (ch !== "\\") {
2455
- out += ch;
2456
- continue;
2457
- }
2458
- if (i + 1 >= value.length) break;
2459
- const next = value[i + 1];
2460
- if (next === "\n" || next === "\f") {
2461
- i += 1;
2462
- continue;
2463
- }
2464
- if (next === "\r") {
2465
- i += value[i + 2] === "\n" ? 2 : 1;
2466
- continue;
2467
- }
2468
- if (isHexDigit(next)) {
2469
- let hex = "";
2470
- let j = i + 1;
2471
- while (j < value.length && hex.length < 6 && isHexDigit(value[j])) {
2472
- hex += value[j];
2473
- j++;
2474
- }
2475
- if (j < value.length && isCssWhitespace(value[j])) {
2476
- j += value[j] === "\r" && value[j + 1] === "\n" ? 2 : 1;
2477
- }
2478
- const code = Number.parseInt(hex, 16);
2479
- const invalid = code === 0 || code > 1114111 || code >= 55296 && code <= 57343;
2480
- out += invalid ? "\uFFFD" : String.fromCodePoint(code);
2481
- i = j - 1;
2482
- continue;
2483
- }
2484
- out += next;
2485
- i += 1;
2486
- }
2487
- return out;
2488
- }
2489
- var BLOCKED_CSS_NEEDLES = [
2490
- "url(",
2491
- "expression(",
2492
- "javascript:",
2493
- "vbscript:",
2494
- "-moz-binding",
2495
- "behavior:",
2496
- "@import",
2497
- "image-set(",
2498
- "filter:progid"
2499
- ];
2500
- function describeElement(el) {
2501
- if (!el) return "an element";
2502
- let out = el.tagName.toLowerCase();
2503
- const id = el.getAttribute("id");
2504
- if (id) out += `#${id}`;
2505
- const cls = el.getAttribute("class");
2506
- if (cls) out += `.${cls.trim().split(/\s+/).slice(0, 3).join(".")}`;
2507
- return `<${out}>`;
2508
- }
2509
- var warnedDeclarations = /* @__PURE__ */ new Set();
2510
- var MAX_WARNED_DECLARATIONS = 100;
2511
- var declarationCapAnnounced = false;
2512
- function warnDroppedDeclaration(value, reason, ctx) {
2513
- devWarnLazy(() => {
2514
- const reasons = [
2515
- "url() in an inline style is an exfiltration channel: the browser fetches it immediately and the URL's path or query can carry page data off-origin",
2516
- "expression() executes arbitrary script in legacy engines",
2517
- "the javascript: scheme executes script",
2518
- "the vbscript: scheme executes script",
2519
- "-moz-binding loads and runs remote XBL",
2520
- "behavior: loads and runs a remote HTC script",
2521
- "@import pulls in a remote stylesheet",
2522
- "image-set() fetches remote images, the same exfiltration channel as url()",
2523
- "filter:progid activates legacy scriptable filters"
2524
- ];
2525
- const declaration = ctx?.property ? `${ctx.property}: ${value}` : value;
2526
- const where = describeElement(ctx?.element);
2527
- const key = `${where}|${declaration}`;
2528
- if (warnedDeclarations.has(key)) return "";
2529
- if (warnedDeclarations.size >= MAX_WARNED_DECLARATIONS) {
2530
- if (declarationCapAnnounced) return "";
2531
- declarationCapAnnounced = true;
2532
- return `${MAX_WARNED_DECLARATIONS} distinct dropped style declarations have been reported; suppressing further ones for the rest of this session so they cannot flood the console. The declarations are still dropped \u2014 only the reporting stops. Fix the reported ones and reload to see any that remain.`;
2533
- }
2534
- warnedDeclarations.add(key);
2535
- return `style declaration "${declaration}" was dropped by the style sanitizer on ${where} \u2014 ${reasons[reason]}. The element renders without it, which usually looks like a missing image rather than an error. Sanctioned alternatives: for a content image use an <img> element (or a <picture>); for decoration, put the rule in a stylesheet and set a class instead \u2014 stylesheet CSS is not sanitized, only inline style attributes are. If this declaration is not yours, it came from untrusted data and the drop just prevented an exfiltration.`;
2536
- });
2537
- }
2538
- function blockedCssReason(value) {
2539
- if (!CSS_DANGER_GATE.test(value)) return -1;
2540
- const normalized = value.includes("\\") ? decodeCssEscapes2(value) : value;
2541
- const lower = normalized.toLowerCase().replace(/\s+/g, "");
2542
- for (let i = 0; i < BLOCKED_CSS_NEEDLES.length; i++) {
2543
- if (lower.includes(BLOCKED_CSS_NEEDLES[i])) return i;
2544
- }
2545
- return -1;
2546
- }
2547
- function sanitizeCSSValue(value, context) {
2548
- const reason = blockedCssReason(value);
2549
- if (reason === -1) return value;
2550
- warnDroppedDeclaration(value, reason, context);
2551
- return "";
2552
- }
2553
- function sanitizeStyleAttribute(cssText, context) {
2554
- const input = String(cssText);
2555
- if (input.trim() === "") return "";
2556
- if (typeof document === "undefined") {
2557
- return sanitizeCSSValue(input, context) === "" ? "" : input;
2558
- }
2559
- const probe = document.createElement("div");
2560
- try {
2561
- probe.style.cssText = input;
2562
- } catch {
2563
- return "";
2564
- }
2565
- const declarations = [];
2566
- for (let i = 0; i < probe.style.length; i++) {
2567
- const property = probe.style[i];
2568
- const value = probe.style.getPropertyValue(property);
2569
- let reason = blockedCssReason(value);
2570
- if (reason === -1) reason = blockedCssReason(`${property}:${value}`);
2571
- if (reason !== -1) {
2572
- warnDroppedDeclaration(value, reason, { property, element: context?.element });
2573
- continue;
2574
- }
2575
- const priority = probe.style.getPropertyPriority(property);
2576
- declarations.push(`${property}: ${value}${priority ? ` !${priority}` : ""}`);
2577
- }
2578
- return declarations.join("; ");
2769
+ function trimAsciiWhitespace(value) {
2770
+ let start = 0;
2771
+ let end = value.length;
2772
+ while (start < end && isAsciiWhitespace(value[start])) start++;
2773
+ while (end > start && isAsciiWhitespace(value[end - 1])) end--;
2774
+ return value.slice(start, end);
2775
+ }
2776
+ var forbidden = (reason) => ({ kind: "forbidden", reason });
2777
+ function parseMetaRefreshContent(content) {
2778
+ const raw = trimAsciiWhitespace(stripControlChars(content));
2779
+ if (raw === "") return forbidden(DEV ? "empty content" : "");
2780
+ let i = 0;
2781
+ while (i < raw.length && raw[i] >= "0" && raw[i] <= "9") i++;
2782
+ if (i === 0) return forbidden(DEV ? "missing or non-numeric delay" : "");
2783
+ const delay = Number.parseInt(raw.slice(0, i), 10);
2784
+ if (!Number.isFinite(delay)) return forbidden(DEV ? "unparseable delay" : "");
2785
+ while (i < raw.length && isAsciiWhitespace(raw[i])) i++;
2786
+ if (i === raw.length) return { kind: "delay-only", delay };
2787
+ if (raw[i] !== ";" && raw[i] !== ",") return forbidden(DEV ? "unexpected text after delay" : "");
2788
+ i++;
2789
+ while (i < raw.length && isAsciiWhitespace(raw[i])) i++;
2790
+ const keyStart = i;
2791
+ while (i < raw.length && !isAsciiWhitespace(raw[i]) && raw[i] !== "=") i++;
2792
+ const key = raw.slice(keyStart, i).toLowerCase();
2793
+ if (key !== "url") return forbidden(DEV ? `unsupported refresh key ${JSON.stringify(key)}` : "");
2794
+ while (i < raw.length && isAsciiWhitespace(raw[i])) i++;
2795
+ if (raw[i] !== "=") return forbidden(DEV ? "missing '=' after url" : "");
2796
+ i++;
2797
+ while (i < raw.length && isAsciiWhitespace(raw[i])) i++;
2798
+ let destination;
2799
+ const quote = raw[i];
2800
+ if (quote === '"' || quote === "'") {
2801
+ const close = raw.indexOf(quote, i + 1);
2802
+ if (close === -1) return forbidden(DEV ? "unterminated quoted destination" : "");
2803
+ destination = raw.slice(i + 1, close);
2804
+ const rest = trimAsciiWhitespace(raw.slice(close + 1));
2805
+ if (rest !== "") return forbidden(DEV ? "trailing text after quoted destination" : "");
2806
+ } else {
2807
+ destination = trimAsciiWhitespace(raw.slice(i));
2808
+ if (/[;,]/.test(destination)) return forbidden(DEV ? "multiple refresh assignments" : "");
2809
+ }
2810
+ if (destination === "") return forbidden(DEV ? "empty destination" : "");
2811
+ const sanitizedDestination = sanitizeUrl(destination);
2812
+ if (sanitizedDestination === "") return forbidden(DEV ? "destination uses a disallowed protocol" : "");
2813
+ return { kind: "allowed", delay, destination, sanitizedDestination };
2814
+ }
2815
+ function resolveMetaRefreshPolicy(attributes) {
2816
+ let httpEquiv;
2817
+ let content;
2818
+ let duplicateName;
2819
+ const seen = /* @__PURE__ */ new Set();
2820
+ for (const [key, value] of attributes) {
2821
+ const canonical = canonicalAttrName(key);
2822
+ if (seen.has(canonical)) duplicateName = canonical;
2823
+ seen.add(canonical);
2824
+ if (canonical === "http-equiv") httpEquiv = value;
2825
+ else if (canonical === "content") content = value;
2826
+ }
2827
+ if (duplicateName !== void 0) {
2828
+ return forbidden(DEV ? `duplicate case-insensitive attribute ${JSON.stringify(duplicateName)}` : "");
2829
+ }
2830
+ if (typeof httpEquiv !== "string") return { kind: "not-refresh" };
2831
+ if (trimAsciiWhitespace(stripControlChars(httpEquiv)).toLowerCase() !== "refresh") {
2832
+ return { kind: "not-refresh" };
2833
+ }
2834
+ if (typeof content !== "string") return { kind: "not-refresh" };
2835
+ return parseMetaRefreshContent(content);
2579
2836
  }
2580
- var URL_ATTRIBUTES = /* @__PURE__ */ new Set([
2581
- "href",
2582
- "xlink:href",
2583
- "src",
2584
- "action",
2585
- "formaction",
2586
- "formtarget",
2587
- "cite",
2588
- "poster",
2589
- "background",
2590
- "srcset",
2591
- "ping",
2592
- "data",
2593
- // `<html manifest>` is a URL sink too. It was carried only by the SSR
2594
- // serializers' private copy of this set; folding that copy into this one would
2595
- // otherwise have silently dropped its coverage.
2596
- "manifest"
2597
- ]);
2598
- var HTML_CONTENT_ATTRIBUTES = /* @__PURE__ */ new Set(["srcdoc"]);
2599
- function isHtmlContentAttribute(name) {
2600
- return HTML_CONTENT_ATTRIBUTES.has(name.toLowerCase());
2837
+
2838
+ // src/utils/elementPolicy.ts
2839
+ var HTML_NS = "http://www.w3.org/1999/xhtml";
2840
+ var SVG_NS = "http://www.w3.org/2000/svg";
2841
+ var BLOCKED_ELEMENTS = /* @__PURE__ */ new Set(["script", "iframe", "object", "embed", "frame", "frameset"]);
2842
+ function canonicalTagName(tag) {
2843
+ return canonicalAttrName(tag);
2844
+ }
2845
+ function isBlockedElement(tag) {
2846
+ return BLOCKED_ELEMENTS.has(canonicalTagName(tag));
2847
+ }
2848
+ var ContextualRefusal = {
2849
+ None: 0,
2850
+ /** The element would carry a refresh directive the shared policy forbids. */
2851
+ MetaRefreshForbidden: 1,
2852
+ /** A `<meta>` with a reactive binding may not carry any refresh directive. */
2853
+ MetaRefreshReactive: 2,
2854
+ /** A runtime value may not choose which program a `<script>` runs. */
2855
+ ScriptSource: 3,
2856
+ /** An SVG animation may not target a URL, handler or document attribute. */
2857
+ AnimationTarget: 4,
2858
+ /** A runtime-written `href` may not become an applied stylesheet. */
2859
+ StylesheetSource: 5
2860
+ };
2861
+ var SCRIPT_SOURCE_ATTRIBUTES = /* @__PURE__ */ new Set(["src", "href", "xlink:href", "type", "language"]);
2862
+ var SVG_ANIMATION_ELEMENTS = /* @__PURE__ */ new Set(["animate", "set", "animateMotion", "animateTransform", "animateColor"]);
2863
+ var reactiveMetaElements = /* @__PURE__ */ new WeakSet();
2864
+ function isHtmlNamespace(el) {
2865
+ return el.namespaceURI === HTML_NS || el.namespaceURI === null;
2866
+ }
2867
+ function metaSnapshot(el, canonical, value) {
2868
+ const snapshot = /* @__PURE__ */ new Map();
2869
+ const attrs = el.attributes;
2870
+ for (let i = 0; i < attrs.length; i++) {
2871
+ const attr = attrs[i];
2872
+ if (attr.name === canonical) continue;
2873
+ snapshot.set(attr.name, attr.value);
2874
+ }
2875
+ if (canonical !== null && value !== null) snapshot.set(canonical, value);
2876
+ return snapshot;
2877
+ }
2878
+ function claimReactiveMeta(el) {
2879
+ if (reactiveMetaElements.has(el)) return;
2880
+ reactiveMetaElements.add(el);
2881
+ if (el.getAttribute("content") === null) return;
2882
+ if (resolveMetaRefreshPolicy(metaSnapshot(el, null, null)).kind !== "not-refresh") {
2883
+ el.removeAttribute("content");
2884
+ }
2885
+ }
2886
+ function metaRefusal(el, name, value, origin) {
2887
+ if (!isHtmlNamespace(el)) return ContextualRefusal.None;
2888
+ const canonical = canonicalAttrName(name);
2889
+ if (canonical !== "http-equiv" && canonical !== "content") return ContextualRefusal.None;
2890
+ if (origin === "reactive") claimReactiveMeta(el);
2891
+ if (value === null) return ContextualRefusal.None;
2892
+ if (canonical === "content" && !el.hasAttribute("http-equiv")) return ContextualRefusal.None;
2893
+ const decision = resolveMetaRefreshPolicy(metaSnapshot(el, canonical, value));
2894
+ if (decision.kind === "forbidden") return ContextualRefusal.MetaRefreshForbidden;
2895
+ if (decision.kind !== "not-refresh" && reactiveMetaElements.has(el)) return ContextualRefusal.MetaRefreshReactive;
2896
+ return ContextualRefusal.None;
2897
+ }
2898
+ function animationRefusal(name, value) {
2899
+ if (value === null || canonicalAttrName(name) !== "attributename") return ContextualRefusal.None;
2900
+ const target = stripControlChars(value);
2901
+ return isUrlAttribute(target) || isEventHandlerAttr(target) || isHtmlContentAttribute(target) ? ContextualRefusal.AnimationTarget : ContextualRefusal.None;
2902
+ }
2903
+ var runtimeHrefLinks = /* @__PURE__ */ new WeakSet();
2904
+ function relHasStylesheet(rel) {
2905
+ if (rel === null) return false;
2906
+ const lower = canonicalAttrName(rel);
2907
+ if (!lower.includes("stylesheet")) return false;
2908
+ const tokens = lower.split(/[\t\n\f\r ]+/);
2909
+ for (let i = 0; i < tokens.length; i++) if (tokens[i] === "stylesheet") return true;
2910
+ return false;
2601
2911
  }
2602
- function sanitizeAttributeString(attr, value, context) {
2603
- const lower = attr.toLowerCase();
2604
- if (lower === "srcset") return sanitizeSrcset(value);
2605
- if (URL_ATTRIBUTES.has(lower)) return sanitizeUrl(value);
2606
- if (lower === "style") return sanitizeStyleAttribute(value, context);
2607
- return value;
2912
+ function stylesheetRefusal(el, name, value, origin) {
2913
+ if (!isHtmlNamespace(el)) return ContextualRefusal.None;
2914
+ const canonical = canonicalAttrName(name);
2915
+ if (canonical === "href") {
2916
+ if (origin === "static" || value === null) return ContextualRefusal.None;
2917
+ if (relHasStylesheet(el.getAttribute("rel"))) return ContextualRefusal.StylesheetSource;
2918
+ runtimeHrefLinks.add(el);
2919
+ return ContextualRefusal.None;
2920
+ }
2921
+ if (canonical === "rel") {
2922
+ if (!relHasStylesheet(value)) return ContextualRefusal.None;
2923
+ return runtimeHrefLinks.has(el) && el.hasAttribute("href") ? ContextualRefusal.StylesheetSource : ContextualRefusal.None;
2924
+ }
2925
+ return ContextualRefusal.None;
2926
+ }
2927
+ function contextualAttributeRefusal(el, name, value, origin) {
2928
+ const local = el.localName;
2929
+ if (local === "meta") return metaRefusal(el, name, value, origin);
2930
+ if (local === "link") return stylesheetRefusal(el, name, value, origin);
2931
+ if (local === "script") {
2932
+ if (origin === "static" || value === null) return ContextualRefusal.None;
2933
+ return SCRIPT_SOURCE_ATTRIBUTES.has(canonicalAttrName(name)) ? ContextualRefusal.ScriptSource : ContextualRefusal.None;
2934
+ }
2935
+ if (SVG_ANIMATION_ELEMENTS.has(local) && el.namespaceURI === SVG_NS) return animationRefusal(name, value);
2936
+ return ContextualRefusal.None;
2937
+ }
2938
+ function settleAttributeProvenance(el, name, origin) {
2939
+ if (el.localName !== "link" || canonicalAttrName(name) !== "href") return;
2940
+ if (origin !== "static" && el.hasAttribute("href")) runtimeHrefLinks.add(el);
2941
+ else runtimeHrefLinks.delete(el);
2608
2942
  }
2609
2943
 
2610
2944
  // src/utils/setSafeAttribute.ts
2611
- var HTML_NS = "http://www.w3.org/1999/xhtml";
2612
- var SVG_NS = "http://www.w3.org/2000/svg";
2945
+ var HTML_NS2 = "http://www.w3.org/1999/xhtml";
2946
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
2613
2947
  var XLINK_NS = "http://www.w3.org/1999/xlink";
2614
2948
  var XML_NS = "http://www.w3.org/XML/1998/namespace";
2615
2949
  var BOOLEAN_IDL_ATTRS = /* @__PURE__ */ new Set(["checked", "disabled", "selected"]);
2616
2950
  function isHtmlElement(el) {
2617
- return el.namespaceURI === null || el.namespaceURI === HTML_NS;
2951
+ return el.namespaceURI === null || el.namespaceURI === HTML_NS2;
2952
+ }
2953
+ function warnContextualRefusal(label, name, refusal) {
2954
+ devWarnLazy(() => {
2955
+ const reasons = [
2956
+ "",
2957
+ 'the element would become a <meta http-equiv="refresh"> directive the shared refresh policy forbids (a destination outside the URL allowlist, or a directive too malformed to read unambiguously)',
2958
+ "a <meta> whose http-equiv or content is reactive may never carry a refresh directive: a browser schedules the navigation as soon as the directive is valid, and nothing can withdraw it when the state changes back",
2959
+ "a runtime value may not choose the program a <script> runs; name the script in static template source, or load it through Head({ script }) as an explicit trust decision",
2960
+ "an SVG animation may not target a URL, event-handler or nested-document attribute; its to / values would be written into that attribute without passing the URL policy",
2961
+ 'a runtime-chosen href may not become an applied <link rel="stylesheet">: a well-formed URL is not a trusted stylesheet. Name the stylesheet in static template source, or load a runtime-chosen one through Head({ link }) as an explicit trust decision'
2962
+ ];
2963
+ return `${label}: refusing "${name}" in this element's context \u2014 ${reasons[refusal]}. The write was not performed.`;
2964
+ });
2618
2965
  }
2619
2966
  function setProp(el, key, val) {
2620
2967
  el[key] = val;
2621
2968
  }
2622
2969
  function namespaceFor(el, name) {
2623
- if (el.namespaceURI !== SVG_NS) return null;
2970
+ if (el.namespaceURI !== SVG_NS2) return null;
2624
2971
  const lower = name.toLowerCase();
2625
2972
  if (lower.startsWith("xlink:")) return XLINK_NS;
2626
2973
  if (lower.startsWith("xml:")) return XML_NS;
2627
2974
  return null;
2628
2975
  }
2629
2976
  function setSafeAttribute(el, name, value, options = {}) {
2977
+ const written = commitSafeAttribute(el, name, value, options);
2978
+ if (el.localName === "link") settleAttributeProvenance(el, name, options.reactive ? "reactive" : "runtime");
2979
+ return written;
2980
+ }
2981
+ function commitSafeAttribute(el, name, value, options) {
2630
2982
  const ns = namespaceFor(el, name);
2631
2983
  const localName = ns ? name.slice(name.indexOf(":") + 1) : name;
2632
2984
  if (isEventHandlerAttr(name)) {
@@ -2639,6 +2991,14 @@ function setSafeAttribute(el, name, value, options = {}) {
2639
2991
  else el.removeAttribute(name);
2640
2992
  return false;
2641
2993
  }
2994
+ const pending = value == null || value === false ? null : value === true ? "" : String(value);
2995
+ const refusal = contextualAttributeRefusal(el, name, pending, options.reactive ? "reactive" : "runtime");
2996
+ if (refusal !== ContextualRefusal.None) {
2997
+ warnContextualRefusal(options.label ?? "setSafeAttribute", name, refusal);
2998
+ if (ns) el.removeAttributeNS(ns, localName);
2999
+ else el.removeAttribute(name);
3000
+ return false;
3001
+ }
2642
3002
  if (value == null) {
2643
3003
  if (ns) el.removeAttributeNS(ns, localName);
2644
3004
  else el.removeAttribute(name);
@@ -2682,7 +3042,12 @@ function setSafeAttribute(el, name, value, options = {}) {
2682
3042
  setProp(el, idlName, idlName === "checked" ? Boolean(value) : str);
2683
3043
  return true;
2684
3044
  }
2685
- const safe = sanitizeAttributeString(name, str, { element: el });
3045
+ const safe = str === "" ? "" : resolveAttributeValue(name, str, { element: el });
3046
+ if (safe === null) {
3047
+ if (ns) el.removeAttributeNS(ns, localName);
3048
+ else el.removeAttribute(name);
3049
+ return true;
3050
+ }
2686
3051
  const current = ns ? el.getAttributeNS(ns, localName) : el.getAttribute(name);
2687
3052
  if (current === safe) return true;
2688
3053
  if (ns) el.setAttributeNS(ns, name, safe);
@@ -2705,7 +3070,7 @@ function bindAttribute(el, attr, getter) {
2705
3070
  reportError(err, { phase: "binding", name: `bindAttribute("${attr}")`, node: el });
2706
3071
  return;
2707
3072
  }
2708
- setSafeAttribute(el, attr, value, { label: `bindAttribute("${attr}")` });
3073
+ setSafeAttribute(el, attr, value, { label: `bindAttribute("${attr}")`, reactive: true });
2709
3074
  }
2710
3075
  return reactiveBinding2(commit, el);
2711
3076
  }
@@ -2742,7 +3107,7 @@ function bindBoolAttr(el, attr, getter) {
2742
3107
  reportError(err, { phase: "binding", name: "bindBoolAttr", node: el });
2743
3108
  return;
2744
3109
  }
2745
- setSafeAttribute(el, attr, Boolean(value), { label: "bindBoolAttr" });
3110
+ setSafeAttribute(el, attr, Boolean(value), { label: "bindBoolAttr", reactive: true });
2746
3111
  }
2747
3112
  return reactiveBinding2(commit, el);
2748
3113
  }
@@ -3388,7 +3753,7 @@ function scrollLock() {
3388
3753
  }
3389
3754
 
3390
3755
  // src/platform/customElement.ts
3391
- var SVG_NS2 = "http://www.w3.org/2000/svg";
3756
+ var SVG_NS3 = "http://www.w3.org/2000/svg";
3392
3757
  var MAX_RENDER_PASSES = 10;
3393
3758
  var ABANDONED_RENDER = /* @__PURE__ */ Symbol("sibujs.defineElement.abandonedRender");
3394
3759
  function defineElement(name, component, options = {}) {
@@ -3537,7 +3902,10 @@ function defineElement(name, component, options = {}) {
3537
3902
  customElements.define(name, SibuElement);
3538
3903
  }
3539
3904
  function svgElement(tag, props = {}, ...nodes) {
3540
- const el = document.createElementNS(SVG_NS2, tag);
3905
+ if (isBlockedElement(tag)) {
3906
+ throw new Error(`svgElement: refusing to create <${tag}> \u2014 tag is blocked for security reasons.`);
3907
+ }
3908
+ const el = document.createElementNS(SVG_NS3, tag);
3541
3909
  for (const [key, value] of Object.entries(props)) {
3542
3910
  if (key === "nodes") continue;
3543
3911
  if (typeof value === "function" && isEventHandlerAttr(key)) {