@lionrapid/core 0.5.0 → 0.6.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 (107) hide show
  1. package/dist/base/event-emitter.d.ts.map +1 -1
  2. package/dist/base/event-emitter.js +2 -1
  3. package/dist/base/event-emitter.js.map +1 -1
  4. package/dist/browser/attribute-extractor.d.ts.map +1 -1
  5. package/dist/browser/attribute-extractor.js +2 -1
  6. package/dist/browser/attribute-extractor.js.map +1 -1
  7. package/dist/browser/auto-init.d.ts +64 -0
  8. package/dist/browser/auto-init.d.ts.map +1 -1
  9. package/dist/browser/auto-init.js +171 -59
  10. package/dist/browser/auto-init.js.map +1 -1
  11. package/dist/browser/block-extractor.d.ts.map +1 -1
  12. package/dist/browser/block-extractor.js +3 -1
  13. package/dist/browser/block-extractor.js.map +1 -1
  14. package/dist/browser/editor/editor-api.d.ts +50 -6
  15. package/dist/browser/editor/editor-api.d.ts.map +1 -1
  16. package/dist/browser/editor/editor-api.js +121 -19
  17. package/dist/browser/editor/editor-api.js.map +1 -1
  18. package/dist/browser/editor/editor-init.js +4 -1
  19. package/dist/browser/editor/editor-init.js.map +1 -1
  20. package/dist/browser/editor/sidebar.d.ts +4 -3
  21. package/dist/browser/editor/sidebar.d.ts.map +1 -1
  22. package/dist/browser/editor/sidebar.js +93 -27
  23. package/dist/browser/editor/sidebar.js.map +1 -1
  24. package/dist/browser/editor/snapshot-viewer.d.ts.map +1 -1
  25. package/dist/browser/editor/snapshot-viewer.js +32 -3
  26. package/dist/browser/editor/snapshot-viewer.js.map +1 -1
  27. package/dist/browser/lionrapid.auto.dev.js +424 -147
  28. package/dist/browser/lionrapid.auto.dev.js.map +4 -4
  29. package/dist/browser/lionrapid.auto.js +424 -147
  30. package/dist/browser/lionrapid.auto.js.map +4 -4
  31. package/dist/browser/lionrapid.auto.min.js +4 -3
  32. package/dist/browser/lionrapid.auto.min.js.map +4 -4
  33. package/dist/browser/lionrapid.editor.js +1467 -1115
  34. package/dist/browser/lionrapid.editor.js.map +4 -4
  35. package/dist/browser/lionrapid.editor.min.js +4 -3
  36. package/dist/browser/lionrapid.editor.min.js.map +4 -4
  37. package/dist/browser/lionrapid.esm.js +584 -101
  38. package/dist/browser/lionrapid.esm.js.map +4 -4
  39. package/dist/browser/lionrapid.esm.min.js +3 -2
  40. package/dist/browser/lionrapid.esm.min.js.map +4 -4
  41. package/dist/browser/lionrapid.iife.dev.js +584 -101
  42. package/dist/browser/lionrapid.iife.dev.js.map +4 -4
  43. package/dist/browser/lionrapid.iife.js +584 -101
  44. package/dist/browser/lionrapid.iife.js.map +4 -4
  45. package/dist/browser/lionrapid.iife.min.js +3 -2
  46. package/dist/browser/lionrapid.iife.min.js.map +4 -4
  47. package/dist/browser/lionrapid.umd.js +584 -101
  48. package/dist/browser/lionrapid.umd.js.map +4 -4
  49. package/dist/browser/lionrapid.umd.min.js +3 -2
  50. package/dist/browser/lionrapid.umd.min.js.map +4 -4
  51. package/dist/browser/sanitizer.d.ts +37 -0
  52. package/dist/browser/sanitizer.d.ts.map +1 -0
  53. package/dist/browser/sanitizer.js +93 -0
  54. package/dist/browser/sanitizer.js.map +1 -0
  55. package/dist/builder.d.ts +8 -0
  56. package/dist/builder.d.ts.map +1 -1
  57. package/dist/builder.js +33 -5
  58. package/dist/builder.js.map +1 -1
  59. package/dist/config/lionrapid.config.d.ts +0 -1
  60. package/dist/config/lionrapid.config.d.ts.map +1 -1
  61. package/dist/config/repository.config.d.ts +2 -2
  62. package/dist/config/repository.config.d.ts.map +1 -1
  63. package/dist/content/index.d.ts +3 -0
  64. package/dist/content/index.d.ts.map +1 -1
  65. package/dist/content/index.js +5 -0
  66. package/dist/content/index.js.map +1 -1
  67. package/dist/content/sanitizer/HTMLSanitizer.d.ts +105 -0
  68. package/dist/content/sanitizer/HTMLSanitizer.d.ts.map +1 -0
  69. package/dist/content/sanitizer/HTMLSanitizer.js +257 -0
  70. package/dist/content/sanitizer/HTMLSanitizer.js.map +1 -0
  71. package/dist/content/serializer/HTMLSerializer.d.ts +6 -2
  72. package/dist/content/serializer/HTMLSerializer.d.ts.map +1 -1
  73. package/dist/content/serializer/HTMLSerializer.js +16 -8
  74. package/dist/content/serializer/HTMLSerializer.js.map +1 -1
  75. package/dist/content/splitter/BlockSplitter.d.ts +66 -0
  76. package/dist/content/splitter/BlockSplitter.d.ts.map +1 -0
  77. package/dist/content/splitter/BlockSplitter.js +219 -0
  78. package/dist/content/splitter/BlockSplitter.js.map +1 -0
  79. package/dist/index.d.ts +4 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +17 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/lionRapid.d.ts +1 -2
  84. package/dist/lionRapid.d.ts.map +1 -1
  85. package/dist/lionRapid.js +5 -7
  86. package/dist/lionRapid.js.map +1 -1
  87. package/dist/repositories/network-repository.d.ts +30 -8
  88. package/dist/repositories/network-repository.d.ts.map +1 -1
  89. package/dist/repositories/network-repository.js +88 -74
  90. package/dist/repositories/network-repository.js.map +1 -1
  91. package/dist/types/lionRapid.types.d.ts +2 -0
  92. package/dist/types/lionRapid.types.d.ts.map +1 -1
  93. package/dist/types/translation.types.d.ts +0 -3
  94. package/dist/types/translation.types.d.ts.map +1 -1
  95. package/dist/utils/context-utils.d.ts +1 -2
  96. package/dist/utils/context-utils.d.ts.map +1 -1
  97. package/dist/utils/context-utils.js +3 -6
  98. package/dist/utils/context-utils.js.map +1 -1
  99. package/dist/utils/locale.d.ts +57 -0
  100. package/dist/utils/locale.d.ts.map +1 -0
  101. package/dist/utils/locale.js +116 -0
  102. package/dist/utils/locale.js.map +1 -0
  103. package/dist/utils/translation-flow.d.ts +1 -1
  104. package/dist/utils/translation-flow.d.ts.map +1 -1
  105. package/dist/utils/translation-flow.js +2 -3
  106. package/dist/utils/translation-flow.js.map +1 -1
  107. package/package.json +2 -1
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  ExcludeFilter: () => ExcludeFilter,
43
43
  HTMLSerializer: () => HTMLSerializer,
44
44
  HandlerChain: () => HandlerChain,
45
+ LOCALE_TAG_REGEX: () => LOCALE_TAG_REGEX,
45
46
  LionRapidBuilder: () => LionRapidBuilder,
46
47
  LionRapidCore: () => LionRapidCore,
47
48
  LocalStorageRepository: () => LocalStorageRepository,
@@ -52,6 +53,9 @@ __export(index_exports, {
52
53
  MissingTranslationQueue: () => MissingTranslationQueue,
53
54
  NetworkError: () => NetworkError,
54
55
  NetworkRepository: () => NetworkRepository,
56
+ SAFE_ATTRS: () => SAFE_ATTRS,
57
+ SAFE_STYLE_PROPS: () => SAFE_STYLE_PROPS,
58
+ SAFE_TAGS: () => SAFE_TAGS,
55
59
  ServerError: () => ServerError,
56
60
  StaticRepository: () => StaticRepository,
57
61
  TranslationError: () => TranslationError,
@@ -60,19 +64,33 @@ __export(index_exports, {
60
64
  VERSION: () => VERSION,
61
65
  VOID_TAGS: () => VOID_TAGS,
62
66
  createPackageLogger: () => createPackageLogger,
67
+ escapeHtml: () => escapeHtml,
63
68
  extractTagName: () => extractTagName,
64
69
  generateAutoKey: () => generateAutoKey,
65
70
  isClientError: () => isClientError,
66
71
  isRetryableError: () => isRetryableError,
67
72
  isTranslationError: () => isTranslationError,
73
+ localeDir: () => localeDir,
68
74
  logger: () => logger,
75
+ matchLocale: () => matchLocale,
69
76
  md5: () => md5,
70
77
  measure: () => measure,
78
+ normalizeLocale: () => normalizeLocale,
79
+ parseAcceptLanguage: () => parseAcceptLanguage,
71
80
  parseKeyPath: () => parseKeyPath,
72
81
  prepareTranslationRequest: () => prepareTranslationRequest,
82
+ primarySubtag: () => primarySubtag,
73
83
  readRawStyle: () => readRawStyle,
84
+ replaceBlockInners: () => replaceBlockInners,
74
85
  resolveNestedKey: () => resolveNestedKey,
75
- serializeOpeningTag: () => serializeOpeningTag
86
+ sanitizeContentUnit: () => sanitizeContentUnit,
87
+ sanitizeElement: () => sanitizeElement,
88
+ sanitizeOpeningTag: () => sanitizeOpeningTag,
89
+ sanitizeStyle: () => sanitizeStyle,
90
+ sanitizeStyleSheetText: () => sanitizeStyleSheetText,
91
+ sanitizeUrl: () => sanitizeUrl,
92
+ serializeOpeningTag: () => serializeOpeningTag,
93
+ splitBlocks: () => splitBlocks
76
94
  });
77
95
  module.exports = __toCommonJS(index_exports);
78
96
 
@@ -423,6 +441,57 @@ var measure = (name) => {
423
441
  };
424
442
  };
425
443
 
444
+ // src/utils/locale.ts
445
+ var LOCALE_TAG_REGEX = /^[a-z]{2,3}(?:-[a-z0-9]{2,4})*$/i;
446
+ var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "fa", "he", "ur", "yi"]);
447
+ function primarySubtag(tag) {
448
+ return tag.split("-")[0].toLowerCase();
449
+ }
450
+ function normalizeLocale(tag) {
451
+ const trimmed = tag.trim();
452
+ if (!trimmed) return trimmed;
453
+ const [lang, ...rest] = trimmed.split("-");
454
+ const out = [lang.toLowerCase()];
455
+ for (const sub of rest) {
456
+ if (sub.length === 4) {
457
+ out.push(sub[0].toUpperCase() + sub.slice(1).toLowerCase());
458
+ } else if (sub.length === 2) {
459
+ out.push(sub.toUpperCase());
460
+ } else {
461
+ out.push(sub.toLowerCase());
462
+ }
463
+ }
464
+ return out.join("-");
465
+ }
466
+ function matchLocale(requested, available, fallback) {
467
+ if (available.length === 0) return fallback;
468
+ const avail = available.map((raw) => ({
469
+ raw,
470
+ lower: normalizeLocale(raw).toLowerCase()
471
+ }));
472
+ for (const req of requested) {
473
+ if (!req) continue;
474
+ const reqLower = normalizeLocale(req).toLowerCase();
475
+ const exact = avail.find((a) => a.lower === reqLower);
476
+ if (exact) return exact.raw;
477
+ const primary = primarySubtag(req);
478
+ const byPrimary = avail.find((a) => a.lower === primary);
479
+ if (byPrimary) return byPrimary.raw;
480
+ }
481
+ return fallback;
482
+ }
483
+ function parseAcceptLanguage(header) {
484
+ return header.split(",").map((part) => {
485
+ const [tag, ...params] = part.trim().split(";");
486
+ const qParam = params.find((p) => p.trim().startsWith("q="));
487
+ const q = qParam ? parseFloat(qParam.trim().slice(2)) : 1;
488
+ return { tag: tag.trim(), q: Number.isNaN(q) ? 0 : q };
489
+ }).filter((x) => x.tag.length > 0 && x.tag !== "*").sort((a, b) => b.q - a.q).map((x) => x.tag);
490
+ }
491
+ function localeDir(tag) {
492
+ return RTL_LANGUAGES.has(primarySubtag(tag)) ? "rtl" : "ltr";
493
+ }
494
+
426
495
  // src/base/event-emitter.ts
427
496
  var EventEmitter = class {
428
497
  constructor() {
@@ -711,7 +780,7 @@ var EventEmitter = class {
711
780
  complete: () => {
712
781
  this.emit("language:change:success", {
713
782
  context: this.createContext(requestId, [...baseTrace, "complete"]),
714
- payload: { oldLocale, newLocale }
783
+ payload: { oldLocale, newLocale, dir: localeDir(newLocale) }
715
784
  });
716
785
  },
717
786
  error: (error) => {
@@ -1121,12 +1190,11 @@ function parseNamespaceKey(key) {
1121
1190
  }
1122
1191
 
1123
1192
  // src/utils/context-utils.ts
1124
- function createTranslationContext(parsedKey, currentLocale, defaultNamespace, fallbackLocale) {
1193
+ function createTranslationContext(parsedKey, currentLocale, defaultNamespace) {
1125
1194
  const effectiveNamespace = parsedKey.hasExplicitNamespace ? parsedKey.namespace : defaultNamespace;
1126
1195
  return {
1127
1196
  locale: currentLocale,
1128
- namespace: effectiveNamespace,
1129
- fallbackLocale: fallbackLocale || currentLocale
1197
+ namespace: effectiveNamespace
1130
1198
  };
1131
1199
  }
1132
1200
 
@@ -1243,7 +1311,7 @@ function flattenTranslations(translations, prefix = "", locale, namespace) {
1243
1311
  }
1244
1312
 
1245
1313
  // src/utils/translation-flow.ts
1246
- function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace, fallbackLocale) {
1314
+ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace) {
1247
1315
  const startTime = performance.now();
1248
1316
  const { defaultValue, params } = parseTranslationParams(
1249
1317
  defaultValueOrParams,
@@ -1253,8 +1321,7 @@ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, par
1253
1321
  const context = createTranslationContext(
1254
1322
  parsedKey,
1255
1323
  currentLocale,
1256
- defaultNamespace,
1257
- fallbackLocale
1324
+ defaultNamespace
1258
1325
  );
1259
1326
  const individualCacheKey = createIndividualCacheKey(
1260
1327
  context.locale,
@@ -1276,7 +1343,6 @@ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, par
1276
1343
  locale: currentLocale,
1277
1344
  namespace: context.namespace,
1278
1345
  // ✅ Use effective namespace (explicit or default)
1279
- fallbackLocale,
1280
1346
  individualCacheKey,
1281
1347
  // ✅ Ready-to-use cache key
1282
1348
  namespaceCacheKey,
@@ -2392,6 +2458,183 @@ var MarkupParser = class {
2392
2458
  }
2393
2459
  };
2394
2460
 
2461
+ // src/content/sanitizer/HTMLSanitizer.ts
2462
+ var SAFE_TAGS = /* @__PURE__ */ new Set([
2463
+ "a",
2464
+ "abbr",
2465
+ "b",
2466
+ "bdi",
2467
+ "bdo",
2468
+ "br",
2469
+ "cite",
2470
+ "code",
2471
+ "del",
2472
+ "dfn",
2473
+ "em",
2474
+ "i",
2475
+ "img",
2476
+ "ins",
2477
+ "kbd",
2478
+ "mark",
2479
+ "q",
2480
+ "s",
2481
+ "samp",
2482
+ "small",
2483
+ "span",
2484
+ "strong",
2485
+ "sub",
2486
+ "sup",
2487
+ "time",
2488
+ "u",
2489
+ "var",
2490
+ "wbr"
2491
+ ]);
2492
+ var SAFE_ATTRS = /* @__PURE__ */ new Set([
2493
+ "alt",
2494
+ "cite",
2495
+ "class",
2496
+ "datetime",
2497
+ "dir",
2498
+ "height",
2499
+ "href",
2500
+ "id",
2501
+ "lang",
2502
+ "loading",
2503
+ "rel",
2504
+ "role",
2505
+ "src",
2506
+ "target",
2507
+ "title",
2508
+ "width"
2509
+ ]);
2510
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "cite"]);
2511
+ var SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
2512
+ "color",
2513
+ "background-color",
2514
+ "font-family",
2515
+ "font-size",
2516
+ "font-style",
2517
+ "font-weight",
2518
+ "letter-spacing",
2519
+ "line-height",
2520
+ "text-decoration",
2521
+ "text-decoration-color",
2522
+ "text-decoration-line",
2523
+ "text-decoration-style",
2524
+ "text-transform",
2525
+ "vertical-align",
2526
+ "white-space",
2527
+ "word-spacing"
2528
+ ]);
2529
+ var DANGEROUS_CSS_VALUE = /url\s*\(|expression\s*\(|image-set\s*\(|element\s*\(|paint\s*\(|javascript:|\\|[<>{}@]/i;
2530
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
2531
+ var NAMED_ENTITIES = {
2532
+ amp: "&",
2533
+ lt: "<",
2534
+ gt: ">",
2535
+ quot: '"',
2536
+ apos: "'",
2537
+ nbsp: "\xA0",
2538
+ colon: ":",
2539
+ semi: ";",
2540
+ sol: "/",
2541
+ bsol: "\\",
2542
+ Tab: " ",
2543
+ NewLine: "\n"
2544
+ };
2545
+ function decodeEntities2(value) {
2546
+ return value.replace(
2547
+ /&(?:#[xX]([0-9a-fA-F]+);?|#(\d+);?|([a-zA-Z]+);)/g,
2548
+ (full, hex, dec, name) => {
2549
+ if (hex) return String.fromCodePoint(parseInt(hex, 16));
2550
+ if (dec) return String.fromCodePoint(parseInt(dec, 10));
2551
+ return NAMED_ENTITIES[name] ?? full;
2552
+ }
2553
+ );
2554
+ }
2555
+ function escapeHtml(text) {
2556
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2557
+ }
2558
+ function sanitizeUrl(value) {
2559
+ const decoded = decodeEntities2(value);
2560
+ const probe = decoded.replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase();
2561
+ const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe);
2562
+ if (!scheme) return value;
2563
+ return SAFE_SCHEMES.has(scheme[1]) ? value : null;
2564
+ }
2565
+ function sanitizeStyle(value) {
2566
+ return filterCssDeclarations(value, true);
2567
+ }
2568
+ function sanitizeCssDeclarations(value) {
2569
+ return filterCssDeclarations(value, false);
2570
+ }
2571
+ function filterCssDeclarations(value, requirePropAllowlist) {
2572
+ const decoded = decodeEntities2(value);
2573
+ const kept = [];
2574
+ for (const declaration of decoded.split(";")) {
2575
+ const colon = declaration.indexOf(":");
2576
+ if (colon === -1) continue;
2577
+ const prop = declaration.slice(0, colon).trim().toLowerCase();
2578
+ const val = declaration.slice(colon + 1).trim();
2579
+ if (!/^[a-z-]+$/.test(prop)) continue;
2580
+ if (requirePropAllowlist && !SAFE_STYLE_PROPS.has(prop)) continue;
2581
+ if (val === "" || DANGEROUS_CSS_VALUE.test(val)) continue;
2582
+ kept.push(`${prop}: ${val}`);
2583
+ }
2584
+ return kept.length > 0 ? kept.join("; ") : null;
2585
+ }
2586
+ var ATTR_PATTERN = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
2587
+ function sanitizeOpeningTag(rawOpen) {
2588
+ if (!rawOpen.startsWith("<")) return null;
2589
+ const tagName = extractTagName(rawOpen);
2590
+ if (!/^[a-z][a-z0-9-]*$/.test(tagName) || !SAFE_TAGS.has(tagName)) {
2591
+ return null;
2592
+ }
2593
+ const end = rawOpen.endsWith("/>") ? rawOpen.length - 2 : rawOpen.endsWith(">") ? rawOpen.length - 1 : rawOpen.length;
2594
+ const attrSource = rawOpen.slice(1 + tagName.length, end);
2595
+ const attrs = {};
2596
+ for (const match of attrSource.matchAll(ATTR_PATTERN)) {
2597
+ const name = match[1].toLowerCase();
2598
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
2599
+ if (name in attrs) continue;
2600
+ if (name === "style") {
2601
+ const cleanStyle = sanitizeStyle(value);
2602
+ if (cleanStyle !== null) attrs[name] = cleanStyle;
2603
+ continue;
2604
+ }
2605
+ const allowed = SAFE_ATTRS.has(name) || /^data-[a-z0-9-]*$/.test(name) || /^aria-[a-z0-9-]*$/.test(name);
2606
+ if (!allowed) continue;
2607
+ if (URL_ATTRS.has(name)) {
2608
+ if (sanitizeUrl(value) === null) continue;
2609
+ }
2610
+ attrs[name] = value;
2611
+ }
2612
+ return serializeOpeningTag(tagName, attrs);
2613
+ }
2614
+ function sanitizeContentUnit(unit) {
2615
+ let changed = false;
2616
+ const styles = {};
2617
+ for (const [index, styleList] of Object.entries(unit.styles)) {
2618
+ const raw = readRawStyle(styleList);
2619
+ if (raw === null) {
2620
+ styles[index] = styleList;
2621
+ continue;
2622
+ }
2623
+ const safe = sanitizeOpeningTag(raw);
2624
+ if (safe === null) {
2625
+ changed = true;
2626
+ const rest = styleList.filter((s) => !s.startsWith("raw:"));
2627
+ if (rest.length > 0) styles[index] = rest;
2628
+ } else if (safe === raw) {
2629
+ styles[index] = styleList;
2630
+ } else {
2631
+ changed = true;
2632
+ styles[index] = styleList.map((s) => s.startsWith("raw:") ? `raw:${safe}` : s);
2633
+ }
2634
+ }
2635
+ return changed ? { text: unit.text, styles } : unit;
2636
+ }
2637
+
2395
2638
  // src/content/serializer/HTMLSerializer.ts
2396
2639
  var HTMLSerializer = class {
2397
2640
  serialize(unit) {
@@ -2401,24 +2644,25 @@ var HTMLSerializer = class {
2401
2644
  let lastIndex = 0;
2402
2645
  let match;
2403
2646
  while ((match = tagPattern.exec(text)) !== null) {
2404
- result += text.slice(lastIndex, match.index);
2647
+ result += escapeHtml(text.slice(lastIndex, match.index));
2405
2648
  const [fullMatch, closingSlash, indexStr] = match;
2406
2649
  const styleList = styles[indexStr] ?? [];
2407
2650
  const rawOpen = readRawStyle(styleList);
2408
- if (rawOpen) {
2651
+ const safeOpen = rawOpen === null ? null : sanitizeOpeningTag(rawOpen);
2652
+ if (rawOpen && safeOpen !== null) {
2409
2653
  const tagName = extractTagName(rawOpen);
2410
2654
  const isVoid = VOID_TAGS.has(tagName);
2411
2655
  if (closingSlash) {
2412
2656
  if (!isVoid) result += `</${tagName}>`;
2413
2657
  } else {
2414
- result += rawOpen;
2658
+ result += safeOpen;
2415
2659
  }
2416
2660
  } else {
2417
2661
  result += fullMatch;
2418
2662
  }
2419
2663
  lastIndex = match.index + fullMatch.length;
2420
2664
  }
2421
- result += text.slice(lastIndex);
2665
+ result += escapeHtml(text.slice(lastIndex));
2422
2666
  return result;
2423
2667
  }
2424
2668
  };
@@ -2657,10 +2901,9 @@ var CacheManager = class {
2657
2901
  var INDEXED_TAG_REGEX = /<\/?[0-9]+\/?>/;
2658
2902
  var HTML_TAG_REGEX = /<[a-z][a-z0-9]*(?:\s+[^>]*)?\/?>/i;
2659
2903
  var LionRapidCore = class {
2660
- constructor(eventEmitter, handlerChain, defaultLocale = "en", fallbackLocale = "en", defaultNamespace) {
2904
+ constructor(eventEmitter, handlerChain, defaultLocale = "en", defaultNamespace) {
2661
2905
  __publicField(this, "handlerChain");
2662
2906
  __publicField(this, "currentLocale");
2663
- __publicField(this, "fallbackLocale");
2664
2907
  __publicField(this, "defaultNamespace");
2665
2908
  __publicField(this, "pendingTranslations", /* @__PURE__ */ new Map());
2666
2909
  __publicField(this, "bus");
@@ -2678,7 +2921,6 @@ var LionRapidCore = class {
2678
2921
  this.bus = eventEmitter;
2679
2922
  this.handlerChain = handlerChain;
2680
2923
  this.currentLocale = defaultLocale;
2681
- this.fallbackLocale = fallbackLocale;
2682
2924
  this.defaultNamespace = defaultNamespace;
2683
2925
  this.cache = new CacheManager(
2684
2926
  this.handlerChain,
@@ -2743,8 +2985,7 @@ var LionRapidCore = class {
2743
2985
  this.currentLocale,
2744
2986
  firstArg,
2745
2987
  secondArg,
2746
- this.defaultNamespace,
2747
- this.fallbackLocale
2988
+ this.defaultNamespace
2748
2989
  );
2749
2990
  logger.debug(`Translation requested: ${request.cleanKey}`);
2750
2991
  const translateBus = this.bus.translation(request);
@@ -2794,8 +3035,7 @@ var LionRapidCore = class {
2794
3035
  this.currentLocale,
2795
3036
  firstArg,
2796
3037
  secondArg,
2797
- this.defaultNamespace,
2798
- this.fallbackLocale
3038
+ this.defaultNamespace
2799
3039
  );
2800
3040
  logger.debug(`HTML translation requested: ${request.cleanKey}`);
2801
3041
  const translateBus = this.bus.translation(request);
@@ -3003,8 +3243,7 @@ var LionRapidCore = class {
3003
3243
  this.currentLocale,
3004
3244
  params,
3005
3245
  defaultValue,
3006
- this.defaultNamespace,
3007
- this.fallbackLocale
3246
+ this.defaultNamespace
3008
3247
  );
3009
3248
  const syncResult = this.handlerChain.handleSync(request);
3010
3249
  const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
@@ -3125,8 +3364,7 @@ var LionRapidCore = class {
3125
3364
  this.currentLocale,
3126
3365
  void 0,
3127
3366
  void 0,
3128
- this.defaultNamespace,
3129
- this.fallbackLocale
3367
+ this.defaultNamespace
3130
3368
  );
3131
3369
  return this.handlerChain.isCached(request);
3132
3370
  }
@@ -3547,6 +3785,9 @@ var MissingTranslationQueue = class {
3547
3785
  };
3548
3786
 
3549
3787
  // src/builder.ts
3788
+ function isLoudPreloadFailure(error) {
3789
+ return !(error instanceof TranslationNotFoundError || error instanceof TranslationLockedError);
3790
+ }
3550
3791
  var LionRapidBuilder = class _LionRapidBuilder {
3551
3792
  constructor(config) {
3552
3793
  __publicField(this, "config");
@@ -3557,7 +3798,6 @@ var LionRapidBuilder = class _LionRapidBuilder {
3557
3798
  __publicField(this, "syncQueue", null);
3558
3799
  this.config = {
3559
3800
  defaultLocale: config.defaultLocale || "en",
3560
- fallbackLocale: config.fallbackLocale || "en",
3561
3801
  namespace: config.namespace || "app"
3562
3802
  };
3563
3803
  }
@@ -3658,7 +3898,6 @@ var LionRapidBuilder = class _LionRapidBuilder {
3658
3898
  eventEmitter,
3659
3899
  handlerChain,
3660
3900
  this.config.defaultLocale,
3661
- this.config.fallbackLocale,
3662
3901
  this.config.namespace
3663
3902
  );
3664
3903
  for (const plugin of this.plugins) {
@@ -3689,7 +3928,17 @@ var LionRapidBuilder = class _LionRapidBuilder {
3689
3928
  logger.debug("Preload targets", {
3690
3929
  namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
3691
3930
  });
3931
+ let warnedLoudly = false;
3932
+ const warnLoudlyOnce = (namespaceKey, error) => {
3933
+ if (!isLoudPreloadFailure(error) || warnedLoudly) return;
3934
+ warnedLoudly = true;
3935
+ console.warn(
3936
+ `[LionRapid] Could not load translations for ${namespaceKey} \u2014 the page stays untranslated until the server is reachable.`,
3937
+ error
3938
+ );
3939
+ };
3692
3940
  const promises = eagerPreloads.map(async (preloadConfig) => {
3941
+ const namespaceKey = `${preloadConfig.locale}:${preloadConfig.namespace}`;
3693
3942
  try {
3694
3943
  const result = await networkHandler.loadNamespace(
3695
3944
  preloadConfig.locale,
@@ -3698,19 +3947,19 @@ var LionRapidBuilder = class _LionRapidBuilder {
3698
3947
  if (result && result.data && result.status === 200) {
3699
3948
  const context = {
3700
3949
  locale: preloadConfig.locale,
3701
- namespace: preloadConfig.namespace,
3702
- fallbackLocale: this.config.fallbackLocale
3950
+ namespace: preloadConfig.namespace
3703
3951
  };
3704
3952
  await handlerChain.saveNamespaceToHandlers(result.data, context);
3705
- logger.debug(
3706
- `Preloaded: ${preloadConfig.locale}:${preloadConfig.namespace}`
3707
- );
3953
+ logger.debug(`Preloaded: ${namespaceKey}`);
3954
+ return;
3955
+ }
3956
+ if (result?.error) {
3957
+ warnLoudlyOnce(namespaceKey, result.error);
3958
+ logger.warn(`Preload failed: ${namespaceKey}`, result.error);
3708
3959
  }
3709
3960
  } catch (error) {
3710
- logger.warn(
3711
- `Preload failed: ${preloadConfig.locale}:${preloadConfig.namespace}`,
3712
- error
3713
- );
3961
+ warnLoudlyOnce(namespaceKey, error);
3962
+ logger.warn(`Preload failed: ${namespaceKey}`, error);
3714
3963
  }
3715
3964
  });
3716
3965
  await Promise.allSettled(promises);
@@ -3724,8 +3973,16 @@ var _NetworkRepository = class _NetworkRepository {
3724
3973
  __publicField(this, "name", "NetworkRepository");
3725
3974
  __publicField(this, "type", "network");
3726
3975
  __publicField(this, "config", {});
3727
- /** Cached version (opaque string from server) per namespace key "locale:namespace" */
3728
- __publicField(this, "namespaceVersions", /* @__PURE__ */ new Map());
3976
+ /**
3977
+ * Namespace snapshot per key "locale:namespace": the server version TOGETHER
3978
+ * with the payload it describes, persisted as ONE localStorage entry.
3979
+ *
3980
+ * Canonical 304 contract (same as edge KV bundle + WP transient cache):
3981
+ * never claim freshness you can't back. If-None-Match is only sent when the
3982
+ * snapshot is in hand; a 304 answers with that snapshot's payload; version
3983
+ * and payload can never desync because they live and die together.
3984
+ */
3985
+ __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3729
3986
  /**
3730
3987
  * Default endpoint templates with placeholder support
3731
3988
  * Placeholders: :key, :locale, :namespace
@@ -3838,36 +4095,50 @@ var _NetworkRepository = class _NetworkRepository {
3838
4095
  logger.debug("API base URL is not set");
3839
4096
  throw new NetworkError("API base URL is not set");
3840
4097
  }
4098
+ try {
4099
+ return await this.fetchNamespaceRaw(locale, namespace);
4100
+ } catch (error) {
4101
+ this.handleCaughtError(error);
4102
+ }
4103
+ }
4104
+ /**
4105
+ * Shared namespace fetch with the canonical 304 contract:
4106
+ * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
4107
+ * - 304 answers with that snapshot's payload — never with "nothing".
4108
+ * - A 304 with no snapshot (server/proxy surprise) refetches unconditionally.
4109
+ * - A 200 stores version + payload atomically before either is trusted.
4110
+ */
4111
+ async fetchNamespaceRaw(locale, namespace, allowConditional = true) {
3841
4112
  const url = this.buildUrl("getNamespace", { locale, namespace });
3842
4113
  logger.debug(`API GET Namespace: ${url}`);
3843
4114
  const namespaceKey = `${locale}:${namespace}`;
3844
- try {
3845
- const extraHeaders = {};
3846
- const cachedVersion = this.getVersion(namespaceKey);
3847
- if (cachedVersion) {
3848
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3849
- }
3850
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3851
- if (response.status === 304) {
3852
- logger.debug(`Namespace ${namespaceKey}: 304 Not Modified`);
3853
- return null;
3854
- }
3855
- if (!response.ok) {
3856
- this.handleHttpError(response, namespaceKey);
3857
- }
3858
- const data = await response.json();
3859
- logger.debug("API response data:", data);
3860
- if (!data.translations) {
3861
- throw new TranslationNotFoundError(namespaceKey);
3862
- }
3863
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3864
- if (version) {
3865
- this.setVersion(namespaceKey, version);
4115
+ const snapshot = allowConditional ? this.getSnapshot(namespaceKey) : void 0;
4116
+ const extraHeaders = {};
4117
+ if (snapshot) {
4118
+ extraHeaders["If-None-Match"] = `W/"${snapshot.version}"`;
4119
+ }
4120
+ const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
4121
+ if (response.status === 304) {
4122
+ if (snapshot) {
4123
+ logger.debug(`Namespace ${namespaceKey}: 304 Not Modified (snapshot served)`);
4124
+ return snapshot.translations;
3866
4125
  }
3867
- return data.translations;
3868
- } catch (error) {
3869
- this.handleCaughtError(error);
4126
+ logger.debug(`Namespace ${namespaceKey}: unexpected 304 with no snapshot \u2014 refetching`);
4127
+ return this.fetchNamespaceRaw(locale, namespace, false);
4128
+ }
4129
+ if (!response.ok) {
4130
+ this.handleHttpError(response, namespaceKey);
4131
+ }
4132
+ const data = await response.json();
4133
+ logger.debug("API response data:", data);
4134
+ if (!data.translations) {
4135
+ throw new TranslationNotFoundError(namespaceKey);
3870
4136
  }
4137
+ const version = data.meta?.version != null ? String(data.meta.version) : null;
4138
+ if (version) {
4139
+ this.setSnapshot(namespaceKey, { version, translations: data.translations });
4140
+ }
4141
+ return data.translations;
3871
4142
  }
3872
4143
  /**
3873
4144
  * Save translation to API (if supported)
@@ -3952,33 +4223,8 @@ var _NetworkRepository = class _NetworkRepository {
3952
4223
  logger.debug("API base URL is not set");
3953
4224
  throw new NetworkError("API base URL is not set");
3954
4225
  }
3955
- const url = this.buildUrl("getNamespace", { locale, namespace });
3956
- logger.debug(`API GET Namespace ContentUnits: ${url}`);
3957
- const namespaceKey = `${locale}:${namespace}`;
3958
4226
  try {
3959
- const extraHeaders = {};
3960
- const cachedVersion = this.getVersion(namespaceKey);
3961
- if (cachedVersion) {
3962
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3963
- }
3964
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3965
- if (response.status === 304) {
3966
- logger.debug(`Namespace ContentUnits ${namespaceKey}: 304 Not Modified`);
3967
- return null;
3968
- }
3969
- if (!response.ok) {
3970
- this.handleHttpError(response, namespaceKey);
3971
- }
3972
- const data = await response.json();
3973
- logger.debug("API response data:", data);
3974
- if (!data.translations) {
3975
- throw new TranslationNotFoundError(namespaceKey);
3976
- }
3977
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3978
- if (version) {
3979
- this.setVersion(namespaceKey, version);
3980
- }
3981
- return data.translations;
4227
+ return await this.fetchNamespaceRaw(locale, namespace);
3982
4228
  } catch (error) {
3983
4229
  this.handleCaughtError(error);
3984
4230
  }
@@ -4088,25 +4334,37 @@ var _NetworkRepository = class _NetworkRepository {
4088
4334
  getConfig() {
4089
4335
  return this.config;
4090
4336
  }
4091
- /** Read persisted version from localStorage, falling back to in-memory map */
4092
- getVersion(namespaceKey) {
4093
- const mem = this.namespaceVersions.get(namespaceKey);
4337
+ /** Read a persisted snapshot (version + payload), falling back to memory.
4338
+ * Legacy bare-version entries and corrupt snapshots are deleted on sight —
4339
+ * a version without its payload must never drive a conditional request. */
4340
+ getSnapshot(namespaceKey) {
4341
+ const mem = this.namespaceSnapshots.get(namespaceKey);
4094
4342
  if (mem) return mem;
4095
4343
  try {
4096
- const stored = localStorage.getItem(_NetworkRepository.VERSION_PREFIX + namespaceKey);
4097
- if (stored) {
4098
- this.namespaceVersions.set(namespaceKey, stored);
4099
- return stored;
4344
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4345
+ const raw = localStorage.getItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4346
+ if (raw) {
4347
+ const parsed = JSON.parse(raw);
4348
+ if (parsed && typeof parsed.version === "string" && parsed.version.length > 0 && parsed.translations && typeof parsed.translations === "object") {
4349
+ this.namespaceSnapshots.set(namespaceKey, parsed);
4350
+ return parsed;
4351
+ }
4352
+ localStorage.removeItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4100
4353
  }
4101
4354
  } catch {
4102
4355
  }
4103
4356
  return void 0;
4104
4357
  }
4105
- /** Persist version to both in-memory map and localStorage */
4106
- setVersion(namespaceKey, version) {
4107
- this.namespaceVersions.set(namespaceKey, version);
4358
+ /** Persist version + payload as ONE entry (memory + localStorage). On quota
4359
+ * failure the previous stored snapshot stays — still a consistent pair. */
4360
+ setSnapshot(namespaceKey, snapshot) {
4361
+ this.namespaceSnapshots.set(namespaceKey, snapshot);
4108
4362
  try {
4109
- localStorage.setItem(_NetworkRepository.VERSION_PREFIX + namespaceKey, version);
4363
+ localStorage.setItem(
4364
+ _NetworkRepository.SNAPSHOT_PREFIX + namespaceKey,
4365
+ JSON.stringify(snapshot)
4366
+ );
4367
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4110
4368
  } catch {
4111
4369
  }
4112
4370
  }
@@ -4195,8 +4453,11 @@ var _NetworkRepository = class _NetworkRepository {
4195
4453
  }
4196
4454
  }
4197
4455
  };
4198
- /** localStorage key prefix for persisted versions */
4199
- __publicField(_NetworkRepository, "VERSION_PREFIX", "lionrapid:v:");
4456
+ /** localStorage key prefix for persisted snapshots (version + payload) */
4457
+ __publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
4458
+ /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4459
+ * desync the snapshot store exists to prevent; entries are deleted on sight. */
4460
+ __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4200
4461
  var NetworkRepository = _NetworkRepository;
4201
4462
 
4202
4463
  // src/content/validator/ContentValidator.ts
@@ -4352,6 +4613,159 @@ var ContentValidator = class {
4352
4613
  }
4353
4614
  };
4354
4615
 
4616
+ // src/content/splitter/BlockSplitter.ts
4617
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
4618
+ "title",
4619
+ "p",
4620
+ "h1",
4621
+ "h2",
4622
+ "h3",
4623
+ "h4",
4624
+ "h5",
4625
+ "h6",
4626
+ "li",
4627
+ "td",
4628
+ "th",
4629
+ "label",
4630
+ "button",
4631
+ "figcaption",
4632
+ "dt",
4633
+ "dd",
4634
+ "summary",
4635
+ "caption"
4636
+ ]);
4637
+ var INLINE_BLOCK_TAGS = /* @__PURE__ */ new Set(["a"]);
4638
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
4639
+ "script",
4640
+ "style",
4641
+ "code",
4642
+ "pre",
4643
+ "textarea",
4644
+ "noscript",
4645
+ "svg"
4646
+ ]);
4647
+ var TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*)\s*(\/?)>/g;
4648
+ function hasNoTranslateAttr(attrsString) {
4649
+ if (attrsString.includes("data-no-translate")) return true;
4650
+ return /translate\s*=\s*["']no["']/.test(attrsString);
4651
+ }
4652
+ function containsBlockTag(innerHTML) {
4653
+ TAG_REGEX.lastIndex = 0;
4654
+ let match;
4655
+ while ((match = TAG_REGEX.exec(innerHTML)) !== null) {
4656
+ const [, closingSlash, tagName, , selfClosingSlash] = match;
4657
+ if (!closingSlash && !selfClosingSlash && BLOCK_TAGS.has(tagName.toLowerCase())) {
4658
+ return true;
4659
+ }
4660
+ }
4661
+ return false;
4662
+ }
4663
+ function findCloseTag(html, pos, tagName) {
4664
+ const lowerTag = tagName.toLowerCase();
4665
+ let depth = 1;
4666
+ TAG_REGEX.lastIndex = pos;
4667
+ let match;
4668
+ while ((match = TAG_REGEX.exec(html)) !== null) {
4669
+ const [, closingSlash, matchedTag, , selfClosingSlash] = match;
4670
+ if (matchedTag.toLowerCase() !== lowerTag) continue;
4671
+ if (closingSlash) {
4672
+ depth--;
4673
+ if (depth === 0) return match.index;
4674
+ } else if (!selfClosingSlash) {
4675
+ depth++;
4676
+ }
4677
+ }
4678
+ return -1;
4679
+ }
4680
+ function splitBlocks(html) {
4681
+ const blocks = [];
4682
+ let pos = 0;
4683
+ let skipDepth = 0;
4684
+ let iter = 0;
4685
+ const maxIter = 1e5;
4686
+ const noTranslateEnds = [];
4687
+ while (pos < html.length && iter < maxIter) {
4688
+ iter++;
4689
+ const ltPos = html.indexOf("<", pos);
4690
+ if (ltPos === -1) break;
4691
+ if (html.startsWith("<!--", ltPos)) {
4692
+ const commentEnd = html.indexOf("-->", ltPos + 4);
4693
+ if (commentEnd === -1) break;
4694
+ pos = commentEnd + 3;
4695
+ continue;
4696
+ }
4697
+ TAG_REGEX.lastIndex = ltPos;
4698
+ const match = TAG_REGEX.exec(html);
4699
+ if (!match || match.index !== ltPos) {
4700
+ pos = ltPos + 1;
4701
+ continue;
4702
+ }
4703
+ const [fullMatch, closingSlash, tagName, attrsString, selfClosingSlash] = match;
4704
+ const tag = tagName.toLowerCase();
4705
+ const afterMatch = match.index + fullMatch.length;
4706
+ if (SKIP_TAGS.has(tag)) {
4707
+ if (closingSlash) {
4708
+ if (skipDepth > 0) skipDepth--;
4709
+ } else if (!selfClosingSlash) {
4710
+ skipDepth++;
4711
+ }
4712
+ pos = afterMatch;
4713
+ continue;
4714
+ }
4715
+ if (skipDepth > 0) {
4716
+ pos = afterMatch;
4717
+ continue;
4718
+ }
4719
+ while (noTranslateEnds.length && pos >= noTranslateEnds[noTranslateEnds.length - 1]) {
4720
+ noTranslateEnds.pop();
4721
+ }
4722
+ if (!closingSlash && !selfClosingSlash && hasNoTranslateAttr(attrsString)) {
4723
+ const regionEnd = findCloseTag(html, afterMatch, tagName);
4724
+ if (regionEnd !== -1) {
4725
+ noTranslateEnds.push(regionEnd);
4726
+ }
4727
+ }
4728
+ if (noTranslateEnds.length > 0) {
4729
+ pos = afterMatch;
4730
+ continue;
4731
+ }
4732
+ if ((BLOCK_TAGS.has(tag) || INLINE_BLOCK_TAGS.has(tag)) && !closingSlash && !selfClosingSlash) {
4733
+ const closeTagStart = findCloseTag(html, afterMatch, tagName);
4734
+ if (closeTagStart === -1) {
4735
+ pos = afterMatch;
4736
+ continue;
4737
+ }
4738
+ const innerHTML = html.slice(afterMatch, closeTagStart);
4739
+ if (containsBlockTag(innerHTML)) {
4740
+ pos = afterMatch;
4741
+ continue;
4742
+ }
4743
+ blocks.push({
4744
+ tag,
4745
+ openTag: fullMatch,
4746
+ attrs: attrsString,
4747
+ inner: innerHTML,
4748
+ innerStart: afterMatch,
4749
+ innerEnd: closeTagStart
4750
+ });
4751
+ TAG_REGEX.lastIndex = closeTagStart;
4752
+ const closeMatch = TAG_REGEX.exec(html);
4753
+ pos = closeMatch ? closeMatch.index + closeMatch[0].length : closeTagStart + 1;
4754
+ continue;
4755
+ }
4756
+ pos = afterMatch;
4757
+ }
4758
+ return { blocks };
4759
+ }
4760
+ function replaceBlockInners(html, replacements) {
4761
+ const ordered = [...replacements].sort((a, b) => b.block.innerStart - a.block.innerStart);
4762
+ let result = html;
4763
+ for (const { block, inner } of ordered) {
4764
+ result = result.slice(0, block.innerStart) + inner + result.slice(block.innerEnd);
4765
+ }
4766
+ return result;
4767
+ }
4768
+
4355
4769
  // src/browser/md5.ts
4356
4770
  function safeAdd(x, y) {
4357
4771
  const lsw = (x & 65535) + (y & 65535);
@@ -4557,6 +4971,75 @@ var ExcludeFilter = class {
4557
4971
  }
4558
4972
  };
4559
4973
 
4974
+ // src/browser/sanitizer.ts
4975
+ var REMOVE_TAGS = /* @__PURE__ */ new Set([
4976
+ "script",
4977
+ "iframe",
4978
+ "frame",
4979
+ "frameset",
4980
+ "object",
4981
+ "embed",
4982
+ "applet",
4983
+ "base",
4984
+ "meta",
4985
+ "link",
4986
+ "template",
4987
+ "portal"
4988
+ ]);
4989
+ var URL_ATTRS2 = /* @__PURE__ */ new Set([
4990
+ "href",
4991
+ "src",
4992
+ "action",
4993
+ "formaction",
4994
+ "poster",
4995
+ "data",
4996
+ "xlink:href"
4997
+ ]);
4998
+ function sanitizeElement(root) {
4999
+ const all = [root, ...Array.from(root.querySelectorAll("*"))];
5000
+ for (const el of all) {
5001
+ const tag = el.tagName.toLowerCase();
5002
+ if (REMOVE_TAGS.has(tag)) {
5003
+ el.remove();
5004
+ continue;
5005
+ }
5006
+ for (const name of el.getAttributeNames()) {
5007
+ const lower = name.toLowerCase();
5008
+ if (lower.startsWith("on") || lower === "srcdoc") {
5009
+ el.removeAttribute(name);
5010
+ continue;
5011
+ }
5012
+ if (URL_ATTRS2.has(lower)) {
5013
+ if (sanitizeUrl(el.getAttribute(name) ?? "") === null) {
5014
+ el.removeAttribute(name);
5015
+ }
5016
+ continue;
5017
+ }
5018
+ if (lower === "style") {
5019
+ const clean = sanitizeCssDeclarations(el.getAttribute(name) ?? "");
5020
+ if (clean === null) el.removeAttribute(name);
5021
+ else el.setAttribute(name, clean);
5022
+ }
5023
+ }
5024
+ }
5025
+ }
5026
+ function sanitizeStyleSheetText(css, baseHref) {
5027
+ let out = css.replace(/@import[^;]*(;|$)/gi, "");
5028
+ out = out.replace(
5029
+ /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi,
5030
+ (full, _quote, target) => {
5031
+ try {
5032
+ const resolved = new URL(target, baseHref);
5033
+ if (resolved.protocol === "data:") return full;
5034
+ if (resolved.origin === new URL(baseHref).origin) return full;
5035
+ } catch {
5036
+ }
5037
+ return 'url("")';
5038
+ }
5039
+ );
5040
+ return out.replace(/expression\s*\(/gi, "blocked(");
5041
+ }
5042
+
4560
5043
  // src/browser/storage/localstorage-repository.ts
4561
5044
  var STORAGE_PREFIX = "lionrapid:";
4562
5045
  var VERSION_KEY = `${STORAGE_PREFIX}version`;