@lionrapid/core 0.5.0 → 0.6.1

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 +73 -0
  8. package/dist/browser/auto-init.d.ts.map +1 -1
  9. package/dist/browser/auto-init.js +193 -62
  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 +444 -149
  28. package/dist/browser/lionrapid.auto.dev.js.map +4 -4
  29. package/dist/browser/lionrapid.auto.js +444 -149
  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 +590 -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 +590 -101
  42. package/dist/browser/lionrapid.iife.dev.js.map +4 -4
  43. package/dist/browser/lionrapid.iife.js +590 -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 +590 -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 +2 -2
  84. package/dist/lionRapid.d.ts.map +1 -1
  85. package/dist/lionRapid.js +19 -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");
@@ -2668,6 +2911,7 @@ var LionRapidCore = class {
2668
2911
  // 🔥 EMBEDDED: Core integration maps
2669
2912
  __publicField(this, "formatters", /* @__PURE__ */ new Map());
2670
2913
  __publicField(this, "processors", /* @__PURE__ */ new Map());
2914
+ __publicField(this, "warnedNoInterpolate", false);
2671
2915
  // 📡 EVENT-DRIVEN: Just track for cleanup
2672
2916
  __publicField(this, "eventPlugins", /* @__PURE__ */ new Set());
2673
2917
  // 📦 CONTENT: Parser and serializer for ContentUnit processing
@@ -2678,7 +2922,6 @@ var LionRapidCore = class {
2678
2922
  this.bus = eventEmitter;
2679
2923
  this.handlerChain = handlerChain;
2680
2924
  this.currentLocale = defaultLocale;
2681
- this.fallbackLocale = fallbackLocale;
2682
2925
  this.defaultNamespace = defaultNamespace;
2683
2926
  this.cache = new CacheManager(
2684
2927
  this.handlerChain,
@@ -2743,8 +2986,7 @@ var LionRapidCore = class {
2743
2986
  this.currentLocale,
2744
2987
  firstArg,
2745
2988
  secondArg,
2746
- this.defaultNamespace,
2747
- this.fallbackLocale
2989
+ this.defaultNamespace
2748
2990
  );
2749
2991
  logger.debug(`Translation requested: ${request.cleanKey}`);
2750
2992
  const translateBus = this.bus.translation(request);
@@ -2794,8 +3036,7 @@ var LionRapidCore = class {
2794
3036
  this.currentLocale,
2795
3037
  firstArg,
2796
3038
  secondArg,
2797
- this.defaultNamespace,
2798
- this.fallbackLocale
3039
+ this.defaultNamespace
2799
3040
  );
2800
3041
  logger.debug(`HTML translation requested: ${request.cleanKey}`);
2801
3042
  const translateBus = this.bus.translation(request);
@@ -2963,6 +3204,11 @@ var LionRapidCore = class {
2963
3204
  resolvedLocale,
2964
3205
  resolvedKey
2965
3206
  );
3207
+ } else if (!this.warnedNoInterpolate && params && Object.keys(params).length > 0 && processedValue.includes("{")) {
3208
+ this.warnedNoInterpolate = true;
3209
+ console.warn(
3210
+ "[LionRapid] Translation params were passed but no interpolate formatter is registered \u2014 {placeholders} render literally. Install @lionrapid/formatters and add .use(coreFormattersPlugin) to the builder."
3211
+ );
2966
3212
  }
2967
3213
  if (this.processors.size > 0) {
2968
3214
  for (const [name, processor] of this.processors.entries()) {
@@ -3003,8 +3249,7 @@ var LionRapidCore = class {
3003
3249
  this.currentLocale,
3004
3250
  params,
3005
3251
  defaultValue,
3006
- this.defaultNamespace,
3007
- this.fallbackLocale
3252
+ this.defaultNamespace
3008
3253
  );
3009
3254
  const syncResult = this.handlerChain.handleSync(request);
3010
3255
  const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
@@ -3125,8 +3370,7 @@ var LionRapidCore = class {
3125
3370
  this.currentLocale,
3126
3371
  void 0,
3127
3372
  void 0,
3128
- this.defaultNamespace,
3129
- this.fallbackLocale
3373
+ this.defaultNamespace
3130
3374
  );
3131
3375
  return this.handlerChain.isCached(request);
3132
3376
  }
@@ -3547,6 +3791,9 @@ var MissingTranslationQueue = class {
3547
3791
  };
3548
3792
 
3549
3793
  // src/builder.ts
3794
+ function isLoudPreloadFailure(error) {
3795
+ return !(error instanceof TranslationNotFoundError || error instanceof TranslationLockedError);
3796
+ }
3550
3797
  var LionRapidBuilder = class _LionRapidBuilder {
3551
3798
  constructor(config) {
3552
3799
  __publicField(this, "config");
@@ -3557,7 +3804,6 @@ var LionRapidBuilder = class _LionRapidBuilder {
3557
3804
  __publicField(this, "syncQueue", null);
3558
3805
  this.config = {
3559
3806
  defaultLocale: config.defaultLocale || "en",
3560
- fallbackLocale: config.fallbackLocale || "en",
3561
3807
  namespace: config.namespace || "app"
3562
3808
  };
3563
3809
  }
@@ -3658,7 +3904,6 @@ var LionRapidBuilder = class _LionRapidBuilder {
3658
3904
  eventEmitter,
3659
3905
  handlerChain,
3660
3906
  this.config.defaultLocale,
3661
- this.config.fallbackLocale,
3662
3907
  this.config.namespace
3663
3908
  );
3664
3909
  for (const plugin of this.plugins) {
@@ -3689,7 +3934,17 @@ var LionRapidBuilder = class _LionRapidBuilder {
3689
3934
  logger.debug("Preload targets", {
3690
3935
  namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
3691
3936
  });
3937
+ let warnedLoudly = false;
3938
+ const warnLoudlyOnce = (namespaceKey, error) => {
3939
+ if (!isLoudPreloadFailure(error) || warnedLoudly) return;
3940
+ warnedLoudly = true;
3941
+ console.warn(
3942
+ `[LionRapid] Could not load translations for ${namespaceKey} \u2014 the page stays untranslated until the server is reachable.`,
3943
+ error
3944
+ );
3945
+ };
3692
3946
  const promises = eagerPreloads.map(async (preloadConfig) => {
3947
+ const namespaceKey = `${preloadConfig.locale}:${preloadConfig.namespace}`;
3693
3948
  try {
3694
3949
  const result = await networkHandler.loadNamespace(
3695
3950
  preloadConfig.locale,
@@ -3698,19 +3953,19 @@ var LionRapidBuilder = class _LionRapidBuilder {
3698
3953
  if (result && result.data && result.status === 200) {
3699
3954
  const context = {
3700
3955
  locale: preloadConfig.locale,
3701
- namespace: preloadConfig.namespace,
3702
- fallbackLocale: this.config.fallbackLocale
3956
+ namespace: preloadConfig.namespace
3703
3957
  };
3704
3958
  await handlerChain.saveNamespaceToHandlers(result.data, context);
3705
- logger.debug(
3706
- `Preloaded: ${preloadConfig.locale}:${preloadConfig.namespace}`
3707
- );
3959
+ logger.debug(`Preloaded: ${namespaceKey}`);
3960
+ return;
3961
+ }
3962
+ if (result?.error) {
3963
+ warnLoudlyOnce(namespaceKey, result.error);
3964
+ logger.warn(`Preload failed: ${namespaceKey}`, result.error);
3708
3965
  }
3709
3966
  } catch (error) {
3710
- logger.warn(
3711
- `Preload failed: ${preloadConfig.locale}:${preloadConfig.namespace}`,
3712
- error
3713
- );
3967
+ warnLoudlyOnce(namespaceKey, error);
3968
+ logger.warn(`Preload failed: ${namespaceKey}`, error);
3714
3969
  }
3715
3970
  });
3716
3971
  await Promise.allSettled(promises);
@@ -3724,8 +3979,16 @@ var _NetworkRepository = class _NetworkRepository {
3724
3979
  __publicField(this, "name", "NetworkRepository");
3725
3980
  __publicField(this, "type", "network");
3726
3981
  __publicField(this, "config", {});
3727
- /** Cached version (opaque string from server) per namespace key "locale:namespace" */
3728
- __publicField(this, "namespaceVersions", /* @__PURE__ */ new Map());
3982
+ /**
3983
+ * Namespace snapshot per key "locale:namespace": the server version TOGETHER
3984
+ * with the payload it describes, persisted as ONE localStorage entry.
3985
+ *
3986
+ * Canonical 304 contract (same as edge KV bundle + WP transient cache):
3987
+ * never claim freshness you can't back. If-None-Match is only sent when the
3988
+ * snapshot is in hand; a 304 answers with that snapshot's payload; version
3989
+ * and payload can never desync because they live and die together.
3990
+ */
3991
+ __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3729
3992
  /**
3730
3993
  * Default endpoint templates with placeholder support
3731
3994
  * Placeholders: :key, :locale, :namespace
@@ -3838,36 +4101,50 @@ var _NetworkRepository = class _NetworkRepository {
3838
4101
  logger.debug("API base URL is not set");
3839
4102
  throw new NetworkError("API base URL is not set");
3840
4103
  }
4104
+ try {
4105
+ return await this.fetchNamespaceRaw(locale, namespace);
4106
+ } catch (error) {
4107
+ this.handleCaughtError(error);
4108
+ }
4109
+ }
4110
+ /**
4111
+ * Shared namespace fetch with the canonical 304 contract:
4112
+ * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
4113
+ * - 304 answers with that snapshot's payload — never with "nothing".
4114
+ * - A 304 with no snapshot (server/proxy surprise) refetches unconditionally.
4115
+ * - A 200 stores version + payload atomically before either is trusted.
4116
+ */
4117
+ async fetchNamespaceRaw(locale, namespace, allowConditional = true) {
3841
4118
  const url = this.buildUrl("getNamespace", { locale, namespace });
3842
4119
  logger.debug(`API GET Namespace: ${url}`);
3843
4120
  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);
4121
+ const snapshot = allowConditional ? this.getSnapshot(namespaceKey) : void 0;
4122
+ const extraHeaders = {};
4123
+ if (snapshot) {
4124
+ extraHeaders["If-None-Match"] = `W/"${snapshot.version}"`;
4125
+ }
4126
+ const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
4127
+ if (response.status === 304) {
4128
+ if (snapshot) {
4129
+ logger.debug(`Namespace ${namespaceKey}: 304 Not Modified (snapshot served)`);
4130
+ return snapshot.translations;
3866
4131
  }
3867
- return data.translations;
3868
- } catch (error) {
3869
- this.handleCaughtError(error);
4132
+ logger.debug(`Namespace ${namespaceKey}: unexpected 304 with no snapshot \u2014 refetching`);
4133
+ return this.fetchNamespaceRaw(locale, namespace, false);
4134
+ }
4135
+ if (!response.ok) {
4136
+ this.handleHttpError(response, namespaceKey);
3870
4137
  }
4138
+ const data = await response.json();
4139
+ logger.debug("API response data:", data);
4140
+ if (!data.translations) {
4141
+ throw new TranslationNotFoundError(namespaceKey);
4142
+ }
4143
+ const version = data.meta?.version != null ? String(data.meta.version) : null;
4144
+ if (version) {
4145
+ this.setSnapshot(namespaceKey, { version, translations: data.translations });
4146
+ }
4147
+ return data.translations;
3871
4148
  }
3872
4149
  /**
3873
4150
  * Save translation to API (if supported)
@@ -3952,33 +4229,8 @@ var _NetworkRepository = class _NetworkRepository {
3952
4229
  logger.debug("API base URL is not set");
3953
4230
  throw new NetworkError("API base URL is not set");
3954
4231
  }
3955
- const url = this.buildUrl("getNamespace", { locale, namespace });
3956
- logger.debug(`API GET Namespace ContentUnits: ${url}`);
3957
- const namespaceKey = `${locale}:${namespace}`;
3958
4232
  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;
4233
+ return await this.fetchNamespaceRaw(locale, namespace);
3982
4234
  } catch (error) {
3983
4235
  this.handleCaughtError(error);
3984
4236
  }
@@ -4088,25 +4340,37 @@ var _NetworkRepository = class _NetworkRepository {
4088
4340
  getConfig() {
4089
4341
  return this.config;
4090
4342
  }
4091
- /** Read persisted version from localStorage, falling back to in-memory map */
4092
- getVersion(namespaceKey) {
4093
- const mem = this.namespaceVersions.get(namespaceKey);
4343
+ /** Read a persisted snapshot (version + payload), falling back to memory.
4344
+ * Legacy bare-version entries and corrupt snapshots are deleted on sight —
4345
+ * a version without its payload must never drive a conditional request. */
4346
+ getSnapshot(namespaceKey) {
4347
+ const mem = this.namespaceSnapshots.get(namespaceKey);
4094
4348
  if (mem) return mem;
4095
4349
  try {
4096
- const stored = localStorage.getItem(_NetworkRepository.VERSION_PREFIX + namespaceKey);
4097
- if (stored) {
4098
- this.namespaceVersions.set(namespaceKey, stored);
4099
- return stored;
4350
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4351
+ const raw = localStorage.getItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4352
+ if (raw) {
4353
+ const parsed = JSON.parse(raw);
4354
+ if (parsed && typeof parsed.version === "string" && parsed.version.length > 0 && parsed.translations && typeof parsed.translations === "object") {
4355
+ this.namespaceSnapshots.set(namespaceKey, parsed);
4356
+ return parsed;
4357
+ }
4358
+ localStorage.removeItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4100
4359
  }
4101
4360
  } catch {
4102
4361
  }
4103
4362
  return void 0;
4104
4363
  }
4105
- /** Persist version to both in-memory map and localStorage */
4106
- setVersion(namespaceKey, version) {
4107
- this.namespaceVersions.set(namespaceKey, version);
4364
+ /** Persist version + payload as ONE entry (memory + localStorage). On quota
4365
+ * failure the previous stored snapshot stays — still a consistent pair. */
4366
+ setSnapshot(namespaceKey, snapshot) {
4367
+ this.namespaceSnapshots.set(namespaceKey, snapshot);
4108
4368
  try {
4109
- localStorage.setItem(_NetworkRepository.VERSION_PREFIX + namespaceKey, version);
4369
+ localStorage.setItem(
4370
+ _NetworkRepository.SNAPSHOT_PREFIX + namespaceKey,
4371
+ JSON.stringify(snapshot)
4372
+ );
4373
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4110
4374
  } catch {
4111
4375
  }
4112
4376
  }
@@ -4195,8 +4459,11 @@ var _NetworkRepository = class _NetworkRepository {
4195
4459
  }
4196
4460
  }
4197
4461
  };
4198
- /** localStorage key prefix for persisted versions */
4199
- __publicField(_NetworkRepository, "VERSION_PREFIX", "lionrapid:v:");
4462
+ /** localStorage key prefix for persisted snapshots (version + payload) */
4463
+ __publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
4464
+ /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4465
+ * desync the snapshot store exists to prevent; entries are deleted on sight. */
4466
+ __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4200
4467
  var NetworkRepository = _NetworkRepository;
4201
4468
 
4202
4469
  // src/content/validator/ContentValidator.ts
@@ -4352,6 +4619,159 @@ var ContentValidator = class {
4352
4619
  }
4353
4620
  };
4354
4621
 
4622
+ // src/content/splitter/BlockSplitter.ts
4623
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
4624
+ "title",
4625
+ "p",
4626
+ "h1",
4627
+ "h2",
4628
+ "h3",
4629
+ "h4",
4630
+ "h5",
4631
+ "h6",
4632
+ "li",
4633
+ "td",
4634
+ "th",
4635
+ "label",
4636
+ "button",
4637
+ "figcaption",
4638
+ "dt",
4639
+ "dd",
4640
+ "summary",
4641
+ "caption"
4642
+ ]);
4643
+ var INLINE_BLOCK_TAGS = /* @__PURE__ */ new Set(["a"]);
4644
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
4645
+ "script",
4646
+ "style",
4647
+ "code",
4648
+ "pre",
4649
+ "textarea",
4650
+ "noscript",
4651
+ "svg"
4652
+ ]);
4653
+ var TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*)\s*(\/?)>/g;
4654
+ function hasNoTranslateAttr(attrsString) {
4655
+ if (attrsString.includes("data-no-translate")) return true;
4656
+ return /translate\s*=\s*["']no["']/.test(attrsString);
4657
+ }
4658
+ function containsBlockTag(innerHTML) {
4659
+ TAG_REGEX.lastIndex = 0;
4660
+ let match;
4661
+ while ((match = TAG_REGEX.exec(innerHTML)) !== null) {
4662
+ const [, closingSlash, tagName, , selfClosingSlash] = match;
4663
+ if (!closingSlash && !selfClosingSlash && BLOCK_TAGS.has(tagName.toLowerCase())) {
4664
+ return true;
4665
+ }
4666
+ }
4667
+ return false;
4668
+ }
4669
+ function findCloseTag(html, pos, tagName) {
4670
+ const lowerTag = tagName.toLowerCase();
4671
+ let depth = 1;
4672
+ TAG_REGEX.lastIndex = pos;
4673
+ let match;
4674
+ while ((match = TAG_REGEX.exec(html)) !== null) {
4675
+ const [, closingSlash, matchedTag, , selfClosingSlash] = match;
4676
+ if (matchedTag.toLowerCase() !== lowerTag) continue;
4677
+ if (closingSlash) {
4678
+ depth--;
4679
+ if (depth === 0) return match.index;
4680
+ } else if (!selfClosingSlash) {
4681
+ depth++;
4682
+ }
4683
+ }
4684
+ return -1;
4685
+ }
4686
+ function splitBlocks(html) {
4687
+ const blocks = [];
4688
+ let pos = 0;
4689
+ let skipDepth = 0;
4690
+ let iter = 0;
4691
+ const maxIter = 1e5;
4692
+ const noTranslateEnds = [];
4693
+ while (pos < html.length && iter < maxIter) {
4694
+ iter++;
4695
+ const ltPos = html.indexOf("<", pos);
4696
+ if (ltPos === -1) break;
4697
+ if (html.startsWith("<!--", ltPos)) {
4698
+ const commentEnd = html.indexOf("-->", ltPos + 4);
4699
+ if (commentEnd === -1) break;
4700
+ pos = commentEnd + 3;
4701
+ continue;
4702
+ }
4703
+ TAG_REGEX.lastIndex = ltPos;
4704
+ const match = TAG_REGEX.exec(html);
4705
+ if (!match || match.index !== ltPos) {
4706
+ pos = ltPos + 1;
4707
+ continue;
4708
+ }
4709
+ const [fullMatch, closingSlash, tagName, attrsString, selfClosingSlash] = match;
4710
+ const tag = tagName.toLowerCase();
4711
+ const afterMatch = match.index + fullMatch.length;
4712
+ if (SKIP_TAGS.has(tag)) {
4713
+ if (closingSlash) {
4714
+ if (skipDepth > 0) skipDepth--;
4715
+ } else if (!selfClosingSlash) {
4716
+ skipDepth++;
4717
+ }
4718
+ pos = afterMatch;
4719
+ continue;
4720
+ }
4721
+ if (skipDepth > 0) {
4722
+ pos = afterMatch;
4723
+ continue;
4724
+ }
4725
+ while (noTranslateEnds.length && pos >= noTranslateEnds[noTranslateEnds.length - 1]) {
4726
+ noTranslateEnds.pop();
4727
+ }
4728
+ if (!closingSlash && !selfClosingSlash && hasNoTranslateAttr(attrsString)) {
4729
+ const regionEnd = findCloseTag(html, afterMatch, tagName);
4730
+ if (regionEnd !== -1) {
4731
+ noTranslateEnds.push(regionEnd);
4732
+ }
4733
+ }
4734
+ if (noTranslateEnds.length > 0) {
4735
+ pos = afterMatch;
4736
+ continue;
4737
+ }
4738
+ if ((BLOCK_TAGS.has(tag) || INLINE_BLOCK_TAGS.has(tag)) && !closingSlash && !selfClosingSlash) {
4739
+ const closeTagStart = findCloseTag(html, afterMatch, tagName);
4740
+ if (closeTagStart === -1) {
4741
+ pos = afterMatch;
4742
+ continue;
4743
+ }
4744
+ const innerHTML = html.slice(afterMatch, closeTagStart);
4745
+ if (containsBlockTag(innerHTML)) {
4746
+ pos = afterMatch;
4747
+ continue;
4748
+ }
4749
+ blocks.push({
4750
+ tag,
4751
+ openTag: fullMatch,
4752
+ attrs: attrsString,
4753
+ inner: innerHTML,
4754
+ innerStart: afterMatch,
4755
+ innerEnd: closeTagStart
4756
+ });
4757
+ TAG_REGEX.lastIndex = closeTagStart;
4758
+ const closeMatch = TAG_REGEX.exec(html);
4759
+ pos = closeMatch ? closeMatch.index + closeMatch[0].length : closeTagStart + 1;
4760
+ continue;
4761
+ }
4762
+ pos = afterMatch;
4763
+ }
4764
+ return { blocks };
4765
+ }
4766
+ function replaceBlockInners(html, replacements) {
4767
+ const ordered = [...replacements].sort((a, b) => b.block.innerStart - a.block.innerStart);
4768
+ let result = html;
4769
+ for (const { block, inner } of ordered) {
4770
+ result = result.slice(0, block.innerStart) + inner + result.slice(block.innerEnd);
4771
+ }
4772
+ return result;
4773
+ }
4774
+
4355
4775
  // src/browser/md5.ts
4356
4776
  function safeAdd(x, y) {
4357
4777
  const lsw = (x & 65535) + (y & 65535);
@@ -4557,6 +4977,75 @@ var ExcludeFilter = class {
4557
4977
  }
4558
4978
  };
4559
4979
 
4980
+ // src/browser/sanitizer.ts
4981
+ var REMOVE_TAGS = /* @__PURE__ */ new Set([
4982
+ "script",
4983
+ "iframe",
4984
+ "frame",
4985
+ "frameset",
4986
+ "object",
4987
+ "embed",
4988
+ "applet",
4989
+ "base",
4990
+ "meta",
4991
+ "link",
4992
+ "template",
4993
+ "portal"
4994
+ ]);
4995
+ var URL_ATTRS2 = /* @__PURE__ */ new Set([
4996
+ "href",
4997
+ "src",
4998
+ "action",
4999
+ "formaction",
5000
+ "poster",
5001
+ "data",
5002
+ "xlink:href"
5003
+ ]);
5004
+ function sanitizeElement(root) {
5005
+ const all = [root, ...Array.from(root.querySelectorAll("*"))];
5006
+ for (const el of all) {
5007
+ const tag = el.tagName.toLowerCase();
5008
+ if (REMOVE_TAGS.has(tag)) {
5009
+ el.remove();
5010
+ continue;
5011
+ }
5012
+ for (const name of el.getAttributeNames()) {
5013
+ const lower = name.toLowerCase();
5014
+ if (lower.startsWith("on") || lower === "srcdoc") {
5015
+ el.removeAttribute(name);
5016
+ continue;
5017
+ }
5018
+ if (URL_ATTRS2.has(lower)) {
5019
+ if (sanitizeUrl(el.getAttribute(name) ?? "") === null) {
5020
+ el.removeAttribute(name);
5021
+ }
5022
+ continue;
5023
+ }
5024
+ if (lower === "style") {
5025
+ const clean = sanitizeCssDeclarations(el.getAttribute(name) ?? "");
5026
+ if (clean === null) el.removeAttribute(name);
5027
+ else el.setAttribute(name, clean);
5028
+ }
5029
+ }
5030
+ }
5031
+ }
5032
+ function sanitizeStyleSheetText(css, baseHref) {
5033
+ let out = css.replace(/@import[^;]*(;|$)/gi, "");
5034
+ out = out.replace(
5035
+ /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi,
5036
+ (full, _quote, target) => {
5037
+ try {
5038
+ const resolved = new URL(target, baseHref);
5039
+ if (resolved.protocol === "data:") return full;
5040
+ if (resolved.origin === new URL(baseHref).origin) return full;
5041
+ } catch {
5042
+ }
5043
+ return 'url("")';
5044
+ }
5045
+ );
5046
+ return out.replace(/expression\s*\(/gi, "blocked(");
5047
+ }
5048
+
4560
5049
  // src/browser/storage/localstorage-repository.ts
4561
5050
  var STORAGE_PREFIX = "lionrapid:";
4562
5051
  var VERSION_KEY = `${STORAGE_PREFIX}version`;