@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
@@ -31,6 +31,7 @@ var LionRapid = (() => {
31
31
  ExcludeFilter: () => ExcludeFilter,
32
32
  HTMLSerializer: () => HTMLSerializer,
33
33
  HandlerChain: () => HandlerChain,
34
+ LOCALE_TAG_REGEX: () => LOCALE_TAG_REGEX,
34
35
  LionRapidBuilder: () => LionRapidBuilder,
35
36
  LionRapidCore: () => LionRapidCore,
36
37
  LocalStorageRepository: () => LocalStorageRepository,
@@ -41,6 +42,9 @@ var LionRapid = (() => {
41
42
  MissingTranslationQueue: () => MissingTranslationQueue,
42
43
  NetworkError: () => NetworkError,
43
44
  NetworkRepository: () => NetworkRepository,
45
+ SAFE_ATTRS: () => SAFE_ATTRS,
46
+ SAFE_STYLE_PROPS: () => SAFE_STYLE_PROPS,
47
+ SAFE_TAGS: () => SAFE_TAGS,
44
48
  ServerError: () => ServerError,
45
49
  StaticRepository: () => StaticRepository,
46
50
  TranslationError: () => TranslationError,
@@ -49,19 +53,33 @@ var LionRapid = (() => {
49
53
  VERSION: () => VERSION,
50
54
  VOID_TAGS: () => VOID_TAGS,
51
55
  createPackageLogger: () => createPackageLogger,
56
+ escapeHtml: () => escapeHtml,
52
57
  extractTagName: () => extractTagName,
53
58
  generateAutoKey: () => generateAutoKey,
54
59
  isClientError: () => isClientError,
55
60
  isRetryableError: () => isRetryableError,
56
61
  isTranslationError: () => isTranslationError,
62
+ localeDir: () => localeDir,
57
63
  logger: () => logger,
64
+ matchLocale: () => matchLocale,
58
65
  md5: () => md5,
59
66
  measure: () => measure,
67
+ normalizeLocale: () => normalizeLocale,
68
+ parseAcceptLanguage: () => parseAcceptLanguage,
60
69
  parseKeyPath: () => parseKeyPath,
61
70
  prepareTranslationRequest: () => prepareTranslationRequest,
71
+ primarySubtag: () => primarySubtag,
62
72
  readRawStyle: () => readRawStyle,
73
+ replaceBlockInners: () => replaceBlockInners,
63
74
  resolveNestedKey: () => resolveNestedKey,
64
- serializeOpeningTag: () => serializeOpeningTag
75
+ sanitizeContentUnit: () => sanitizeContentUnit,
76
+ sanitizeElement: () => sanitizeElement,
77
+ sanitizeOpeningTag: () => sanitizeOpeningTag,
78
+ sanitizeStyle: () => sanitizeStyle,
79
+ sanitizeStyleSheetText: () => sanitizeStyleSheetText,
80
+ sanitizeUrl: () => sanitizeUrl,
81
+ serializeOpeningTag: () => serializeOpeningTag,
82
+ splitBlocks: () => splitBlocks
65
83
  });
66
84
 
67
85
  // src/config/logger.config.ts
@@ -411,6 +429,57 @@ var LionRapid = (() => {
411
429
  };
412
430
  };
413
431
 
432
+ // src/utils/locale.ts
433
+ var LOCALE_TAG_REGEX = /^[a-z]{2,3}(?:-[a-z0-9]{2,4})*$/i;
434
+ var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "fa", "he", "ur", "yi"]);
435
+ function primarySubtag(tag) {
436
+ return tag.split("-")[0].toLowerCase();
437
+ }
438
+ function normalizeLocale(tag) {
439
+ const trimmed = tag.trim();
440
+ if (!trimmed) return trimmed;
441
+ const [lang, ...rest] = trimmed.split("-");
442
+ const out = [lang.toLowerCase()];
443
+ for (const sub of rest) {
444
+ if (sub.length === 4) {
445
+ out.push(sub[0].toUpperCase() + sub.slice(1).toLowerCase());
446
+ } else if (sub.length === 2) {
447
+ out.push(sub.toUpperCase());
448
+ } else {
449
+ out.push(sub.toLowerCase());
450
+ }
451
+ }
452
+ return out.join("-");
453
+ }
454
+ function matchLocale(requested, available, fallback) {
455
+ if (available.length === 0) return fallback;
456
+ const avail = available.map((raw) => ({
457
+ raw,
458
+ lower: normalizeLocale(raw).toLowerCase()
459
+ }));
460
+ for (const req of requested) {
461
+ if (!req) continue;
462
+ const reqLower = normalizeLocale(req).toLowerCase();
463
+ const exact = avail.find((a) => a.lower === reqLower);
464
+ if (exact) return exact.raw;
465
+ const primary = primarySubtag(req);
466
+ const byPrimary = avail.find((a) => a.lower === primary);
467
+ if (byPrimary) return byPrimary.raw;
468
+ }
469
+ return fallback;
470
+ }
471
+ function parseAcceptLanguage(header) {
472
+ return header.split(",").map((part) => {
473
+ const [tag, ...params] = part.trim().split(";");
474
+ const qParam = params.find((p) => p.trim().startsWith("q="));
475
+ const q = qParam ? parseFloat(qParam.trim().slice(2)) : 1;
476
+ return { tag: tag.trim(), q: Number.isNaN(q) ? 0 : q };
477
+ }).filter((x) => x.tag.length > 0 && x.tag !== "*").sort((a, b) => b.q - a.q).map((x) => x.tag);
478
+ }
479
+ function localeDir(tag) {
480
+ return RTL_LANGUAGES.has(primarySubtag(tag)) ? "rtl" : "ltr";
481
+ }
482
+
414
483
  // src/base/event-emitter.ts
415
484
  var EventEmitter = class {
416
485
  constructor() {
@@ -699,7 +768,7 @@ var LionRapid = (() => {
699
768
  complete: () => {
700
769
  this.emit("language:change:success", {
701
770
  context: this.createContext(requestId, [...baseTrace, "complete"]),
702
- payload: { oldLocale, newLocale }
771
+ payload: { oldLocale, newLocale, dir: localeDir(newLocale) }
703
772
  });
704
773
  },
705
774
  error: (error) => {
@@ -1109,12 +1178,11 @@ var LionRapid = (() => {
1109
1178
  }
1110
1179
 
1111
1180
  // src/utils/context-utils.ts
1112
- function createTranslationContext(parsedKey, currentLocale, defaultNamespace, fallbackLocale) {
1181
+ function createTranslationContext(parsedKey, currentLocale, defaultNamespace) {
1113
1182
  const effectiveNamespace = parsedKey.hasExplicitNamespace ? parsedKey.namespace : defaultNamespace;
1114
1183
  return {
1115
1184
  locale: currentLocale,
1116
- namespace: effectiveNamespace,
1117
- fallbackLocale: fallbackLocale || currentLocale
1185
+ namespace: effectiveNamespace
1118
1186
  };
1119
1187
  }
1120
1188
 
@@ -1231,7 +1299,7 @@ var LionRapid = (() => {
1231
1299
  }
1232
1300
 
1233
1301
  // src/utils/translation-flow.ts
1234
- function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace, fallbackLocale) {
1302
+ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace) {
1235
1303
  const startTime = performance.now();
1236
1304
  const { defaultValue, params } = parseTranslationParams(
1237
1305
  defaultValueOrParams,
@@ -1241,8 +1309,7 @@ var LionRapid = (() => {
1241
1309
  const context = createTranslationContext(
1242
1310
  parsedKey,
1243
1311
  currentLocale,
1244
- defaultNamespace,
1245
- fallbackLocale
1312
+ defaultNamespace
1246
1313
  );
1247
1314
  const individualCacheKey = createIndividualCacheKey(
1248
1315
  context.locale,
@@ -1264,7 +1331,6 @@ var LionRapid = (() => {
1264
1331
  locale: currentLocale,
1265
1332
  namespace: context.namespace,
1266
1333
  // ✅ Use effective namespace (explicit or default)
1267
- fallbackLocale,
1268
1334
  individualCacheKey,
1269
1335
  // ✅ Ready-to-use cache key
1270
1336
  namespaceCacheKey,
@@ -2380,6 +2446,183 @@ var LionRapid = (() => {
2380
2446
  }
2381
2447
  };
2382
2448
 
2449
+ // src/content/sanitizer/HTMLSanitizer.ts
2450
+ var SAFE_TAGS = /* @__PURE__ */ new Set([
2451
+ "a",
2452
+ "abbr",
2453
+ "b",
2454
+ "bdi",
2455
+ "bdo",
2456
+ "br",
2457
+ "cite",
2458
+ "code",
2459
+ "del",
2460
+ "dfn",
2461
+ "em",
2462
+ "i",
2463
+ "img",
2464
+ "ins",
2465
+ "kbd",
2466
+ "mark",
2467
+ "q",
2468
+ "s",
2469
+ "samp",
2470
+ "small",
2471
+ "span",
2472
+ "strong",
2473
+ "sub",
2474
+ "sup",
2475
+ "time",
2476
+ "u",
2477
+ "var",
2478
+ "wbr"
2479
+ ]);
2480
+ var SAFE_ATTRS = /* @__PURE__ */ new Set([
2481
+ "alt",
2482
+ "cite",
2483
+ "class",
2484
+ "datetime",
2485
+ "dir",
2486
+ "height",
2487
+ "href",
2488
+ "id",
2489
+ "lang",
2490
+ "loading",
2491
+ "rel",
2492
+ "role",
2493
+ "src",
2494
+ "target",
2495
+ "title",
2496
+ "width"
2497
+ ]);
2498
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "cite"]);
2499
+ var SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
2500
+ "color",
2501
+ "background-color",
2502
+ "font-family",
2503
+ "font-size",
2504
+ "font-style",
2505
+ "font-weight",
2506
+ "letter-spacing",
2507
+ "line-height",
2508
+ "text-decoration",
2509
+ "text-decoration-color",
2510
+ "text-decoration-line",
2511
+ "text-decoration-style",
2512
+ "text-transform",
2513
+ "vertical-align",
2514
+ "white-space",
2515
+ "word-spacing"
2516
+ ]);
2517
+ var DANGEROUS_CSS_VALUE = /url\s*\(|expression\s*\(|image-set\s*\(|element\s*\(|paint\s*\(|javascript:|\\|[<>{}@]/i;
2518
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
2519
+ var NAMED_ENTITIES = {
2520
+ amp: "&",
2521
+ lt: "<",
2522
+ gt: ">",
2523
+ quot: '"',
2524
+ apos: "'",
2525
+ nbsp: "\xA0",
2526
+ colon: ":",
2527
+ semi: ";",
2528
+ sol: "/",
2529
+ bsol: "\\",
2530
+ Tab: " ",
2531
+ NewLine: "\n"
2532
+ };
2533
+ function decodeEntities2(value) {
2534
+ return value.replace(
2535
+ /&(?:#[xX]([0-9a-fA-F]+);?|#(\d+);?|([a-zA-Z]+);)/g,
2536
+ (full, hex, dec, name) => {
2537
+ if (hex) return String.fromCodePoint(parseInt(hex, 16));
2538
+ if (dec) return String.fromCodePoint(parseInt(dec, 10));
2539
+ return NAMED_ENTITIES[name] ?? full;
2540
+ }
2541
+ );
2542
+ }
2543
+ function escapeHtml(text) {
2544
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2545
+ }
2546
+ function sanitizeUrl(value) {
2547
+ const decoded = decodeEntities2(value);
2548
+ const probe = decoded.replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase();
2549
+ const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe);
2550
+ if (!scheme) return value;
2551
+ return SAFE_SCHEMES.has(scheme[1]) ? value : null;
2552
+ }
2553
+ function sanitizeStyle(value) {
2554
+ return filterCssDeclarations(value, true);
2555
+ }
2556
+ function sanitizeCssDeclarations(value) {
2557
+ return filterCssDeclarations(value, false);
2558
+ }
2559
+ function filterCssDeclarations(value, requirePropAllowlist) {
2560
+ const decoded = decodeEntities2(value);
2561
+ const kept = [];
2562
+ for (const declaration of decoded.split(";")) {
2563
+ const colon = declaration.indexOf(":");
2564
+ if (colon === -1) continue;
2565
+ const prop = declaration.slice(0, colon).trim().toLowerCase();
2566
+ const val = declaration.slice(colon + 1).trim();
2567
+ if (!/^[a-z-]+$/.test(prop)) continue;
2568
+ if (requirePropAllowlist && !SAFE_STYLE_PROPS.has(prop)) continue;
2569
+ if (val === "" || DANGEROUS_CSS_VALUE.test(val)) continue;
2570
+ kept.push(`${prop}: ${val}`);
2571
+ }
2572
+ return kept.length > 0 ? kept.join("; ") : null;
2573
+ }
2574
+ var ATTR_PATTERN = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
2575
+ function sanitizeOpeningTag(rawOpen) {
2576
+ if (!rawOpen.startsWith("<")) return null;
2577
+ const tagName = extractTagName(rawOpen);
2578
+ if (!/^[a-z][a-z0-9-]*$/.test(tagName) || !SAFE_TAGS.has(tagName)) {
2579
+ return null;
2580
+ }
2581
+ const end = rawOpen.endsWith("/>") ? rawOpen.length - 2 : rawOpen.endsWith(">") ? rawOpen.length - 1 : rawOpen.length;
2582
+ const attrSource = rawOpen.slice(1 + tagName.length, end);
2583
+ const attrs = {};
2584
+ for (const match of attrSource.matchAll(ATTR_PATTERN)) {
2585
+ const name = match[1].toLowerCase();
2586
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
2587
+ if (name in attrs) continue;
2588
+ if (name === "style") {
2589
+ const cleanStyle = sanitizeStyle(value);
2590
+ if (cleanStyle !== null) attrs[name] = cleanStyle;
2591
+ continue;
2592
+ }
2593
+ const allowed = SAFE_ATTRS.has(name) || /^data-[a-z0-9-]*$/.test(name) || /^aria-[a-z0-9-]*$/.test(name);
2594
+ if (!allowed) continue;
2595
+ if (URL_ATTRS.has(name)) {
2596
+ if (sanitizeUrl(value) === null) continue;
2597
+ }
2598
+ attrs[name] = value;
2599
+ }
2600
+ return serializeOpeningTag(tagName, attrs);
2601
+ }
2602
+ function sanitizeContentUnit(unit) {
2603
+ let changed = false;
2604
+ const styles = {};
2605
+ for (const [index, styleList] of Object.entries(unit.styles)) {
2606
+ const raw = readRawStyle(styleList);
2607
+ if (raw === null) {
2608
+ styles[index] = styleList;
2609
+ continue;
2610
+ }
2611
+ const safe = sanitizeOpeningTag(raw);
2612
+ if (safe === null) {
2613
+ changed = true;
2614
+ const rest = styleList.filter((s) => !s.startsWith("raw:"));
2615
+ if (rest.length > 0) styles[index] = rest;
2616
+ } else if (safe === raw) {
2617
+ styles[index] = styleList;
2618
+ } else {
2619
+ changed = true;
2620
+ styles[index] = styleList.map((s) => s.startsWith("raw:") ? `raw:${safe}` : s);
2621
+ }
2622
+ }
2623
+ return changed ? { text: unit.text, styles } : unit;
2624
+ }
2625
+
2383
2626
  // src/content/serializer/HTMLSerializer.ts
2384
2627
  var HTMLSerializer = class {
2385
2628
  serialize(unit) {
@@ -2389,24 +2632,25 @@ var LionRapid = (() => {
2389
2632
  let lastIndex = 0;
2390
2633
  let match;
2391
2634
  while ((match = tagPattern.exec(text)) !== null) {
2392
- result += text.slice(lastIndex, match.index);
2635
+ result += escapeHtml(text.slice(lastIndex, match.index));
2393
2636
  const [fullMatch, closingSlash, indexStr] = match;
2394
2637
  const styleList = styles[indexStr] ?? [];
2395
2638
  const rawOpen = readRawStyle(styleList);
2396
- if (rawOpen) {
2639
+ const safeOpen = rawOpen === null ? null : sanitizeOpeningTag(rawOpen);
2640
+ if (rawOpen && safeOpen !== null) {
2397
2641
  const tagName = extractTagName(rawOpen);
2398
2642
  const isVoid = VOID_TAGS.has(tagName);
2399
2643
  if (closingSlash) {
2400
2644
  if (!isVoid) result += `</${tagName}>`;
2401
2645
  } else {
2402
- result += rawOpen;
2646
+ result += safeOpen;
2403
2647
  }
2404
2648
  } else {
2405
2649
  result += fullMatch;
2406
2650
  }
2407
2651
  lastIndex = match.index + fullMatch.length;
2408
2652
  }
2409
- result += text.slice(lastIndex);
2653
+ result += escapeHtml(text.slice(lastIndex));
2410
2654
  return result;
2411
2655
  }
2412
2656
  };
@@ -2645,10 +2889,9 @@ var LionRapid = (() => {
2645
2889
  var INDEXED_TAG_REGEX = /<\/?[0-9]+\/?>/;
2646
2890
  var HTML_TAG_REGEX = /<[a-z][a-z0-9]*(?:\s+[^>]*)?\/?>/i;
2647
2891
  var LionRapidCore = class {
2648
- constructor(eventEmitter, handlerChain, defaultLocale = "en", fallbackLocale = "en", defaultNamespace) {
2892
+ constructor(eventEmitter, handlerChain, defaultLocale = "en", defaultNamespace) {
2649
2893
  __publicField(this, "handlerChain");
2650
2894
  __publicField(this, "currentLocale");
2651
- __publicField(this, "fallbackLocale");
2652
2895
  __publicField(this, "defaultNamespace");
2653
2896
  __publicField(this, "pendingTranslations", /* @__PURE__ */ new Map());
2654
2897
  __publicField(this, "bus");
@@ -2656,6 +2899,7 @@ var LionRapid = (() => {
2656
2899
  // 🔥 EMBEDDED: Core integration maps
2657
2900
  __publicField(this, "formatters", /* @__PURE__ */ new Map());
2658
2901
  __publicField(this, "processors", /* @__PURE__ */ new Map());
2902
+ __publicField(this, "warnedNoInterpolate", false);
2659
2903
  // 📡 EVENT-DRIVEN: Just track for cleanup
2660
2904
  __publicField(this, "eventPlugins", /* @__PURE__ */ new Set());
2661
2905
  // 📦 CONTENT: Parser and serializer for ContentUnit processing
@@ -2666,7 +2910,6 @@ var LionRapid = (() => {
2666
2910
  this.bus = eventEmitter;
2667
2911
  this.handlerChain = handlerChain;
2668
2912
  this.currentLocale = defaultLocale;
2669
- this.fallbackLocale = fallbackLocale;
2670
2913
  this.defaultNamespace = defaultNamespace;
2671
2914
  this.cache = new CacheManager(
2672
2915
  this.handlerChain,
@@ -2731,8 +2974,7 @@ var LionRapid = (() => {
2731
2974
  this.currentLocale,
2732
2975
  firstArg,
2733
2976
  secondArg,
2734
- this.defaultNamespace,
2735
- this.fallbackLocale
2977
+ this.defaultNamespace
2736
2978
  );
2737
2979
  logger.debug(`Translation requested: ${request.cleanKey}`);
2738
2980
  const translateBus = this.bus.translation(request);
@@ -2782,8 +3024,7 @@ var LionRapid = (() => {
2782
3024
  this.currentLocale,
2783
3025
  firstArg,
2784
3026
  secondArg,
2785
- this.defaultNamespace,
2786
- this.fallbackLocale
3027
+ this.defaultNamespace
2787
3028
  );
2788
3029
  logger.debug(`HTML translation requested: ${request.cleanKey}`);
2789
3030
  const translateBus = this.bus.translation(request);
@@ -2951,6 +3192,11 @@ var LionRapid = (() => {
2951
3192
  resolvedLocale,
2952
3193
  resolvedKey
2953
3194
  );
3195
+ } else if (!this.warnedNoInterpolate && params && Object.keys(params).length > 0 && processedValue.includes("{")) {
3196
+ this.warnedNoInterpolate = true;
3197
+ console.warn(
3198
+ "[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."
3199
+ );
2954
3200
  }
2955
3201
  if (this.processors.size > 0) {
2956
3202
  for (const [name, processor] of this.processors.entries()) {
@@ -2991,8 +3237,7 @@ var LionRapid = (() => {
2991
3237
  this.currentLocale,
2992
3238
  params,
2993
3239
  defaultValue,
2994
- this.defaultNamespace,
2995
- this.fallbackLocale
3240
+ this.defaultNamespace
2996
3241
  );
2997
3242
  const syncResult = this.handlerChain.handleSync(request);
2998
3243
  const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
@@ -3113,8 +3358,7 @@ var LionRapid = (() => {
3113
3358
  this.currentLocale,
3114
3359
  void 0,
3115
3360
  void 0,
3116
- this.defaultNamespace,
3117
- this.fallbackLocale
3361
+ this.defaultNamespace
3118
3362
  );
3119
3363
  return this.handlerChain.isCached(request);
3120
3364
  }
@@ -3535,6 +3779,9 @@ var LionRapid = (() => {
3535
3779
  };
3536
3780
 
3537
3781
  // src/builder.ts
3782
+ function isLoudPreloadFailure(error) {
3783
+ return !(error instanceof TranslationNotFoundError || error instanceof TranslationLockedError);
3784
+ }
3538
3785
  var LionRapidBuilder = class _LionRapidBuilder {
3539
3786
  constructor(config) {
3540
3787
  __publicField(this, "config");
@@ -3545,7 +3792,6 @@ var LionRapid = (() => {
3545
3792
  __publicField(this, "syncQueue", null);
3546
3793
  this.config = {
3547
3794
  defaultLocale: config.defaultLocale || "en",
3548
- fallbackLocale: config.fallbackLocale || "en",
3549
3795
  namespace: config.namespace || "app"
3550
3796
  };
3551
3797
  }
@@ -3646,7 +3892,6 @@ var LionRapid = (() => {
3646
3892
  eventEmitter,
3647
3893
  handlerChain,
3648
3894
  this.config.defaultLocale,
3649
- this.config.fallbackLocale,
3650
3895
  this.config.namespace
3651
3896
  );
3652
3897
  for (const plugin of this.plugins) {
@@ -3677,7 +3922,17 @@ var LionRapid = (() => {
3677
3922
  logger.debug("Preload targets", {
3678
3923
  namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
3679
3924
  });
3925
+ let warnedLoudly = false;
3926
+ const warnLoudlyOnce = (namespaceKey, error) => {
3927
+ if (!isLoudPreloadFailure(error) || warnedLoudly) return;
3928
+ warnedLoudly = true;
3929
+ console.warn(
3930
+ `[LionRapid] Could not load translations for ${namespaceKey} \u2014 the page stays untranslated until the server is reachable.`,
3931
+ error
3932
+ );
3933
+ };
3680
3934
  const promises = eagerPreloads.map(async (preloadConfig) => {
3935
+ const namespaceKey = `${preloadConfig.locale}:${preloadConfig.namespace}`;
3681
3936
  try {
3682
3937
  const result = await networkHandler.loadNamespace(
3683
3938
  preloadConfig.locale,
@@ -3686,19 +3941,19 @@ var LionRapid = (() => {
3686
3941
  if (result && result.data && result.status === 200) {
3687
3942
  const context = {
3688
3943
  locale: preloadConfig.locale,
3689
- namespace: preloadConfig.namespace,
3690
- fallbackLocale: this.config.fallbackLocale
3944
+ namespace: preloadConfig.namespace
3691
3945
  };
3692
3946
  await handlerChain.saveNamespaceToHandlers(result.data, context);
3693
- logger.debug(
3694
- `Preloaded: ${preloadConfig.locale}:${preloadConfig.namespace}`
3695
- );
3947
+ logger.debug(`Preloaded: ${namespaceKey}`);
3948
+ return;
3949
+ }
3950
+ if (result?.error) {
3951
+ warnLoudlyOnce(namespaceKey, result.error);
3952
+ logger.warn(`Preload failed: ${namespaceKey}`, result.error);
3696
3953
  }
3697
3954
  } catch (error) {
3698
- logger.warn(
3699
- `Preload failed: ${preloadConfig.locale}:${preloadConfig.namespace}`,
3700
- error
3701
- );
3955
+ warnLoudlyOnce(namespaceKey, error);
3956
+ logger.warn(`Preload failed: ${namespaceKey}`, error);
3702
3957
  }
3703
3958
  });
3704
3959
  await Promise.allSettled(promises);
@@ -3712,8 +3967,16 @@ var LionRapid = (() => {
3712
3967
  __publicField(this, "name", "NetworkRepository");
3713
3968
  __publicField(this, "type", "network");
3714
3969
  __publicField(this, "config", {});
3715
- /** Cached version (opaque string from server) per namespace key "locale:namespace" */
3716
- __publicField(this, "namespaceVersions", /* @__PURE__ */ new Map());
3970
+ /**
3971
+ * Namespace snapshot per key "locale:namespace": the server version TOGETHER
3972
+ * with the payload it describes, persisted as ONE localStorage entry.
3973
+ *
3974
+ * Canonical 304 contract (same as edge KV bundle + WP transient cache):
3975
+ * never claim freshness you can't back. If-None-Match is only sent when the
3976
+ * snapshot is in hand; a 304 answers with that snapshot's payload; version
3977
+ * and payload can never desync because they live and die together.
3978
+ */
3979
+ __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3717
3980
  /**
3718
3981
  * Default endpoint templates with placeholder support
3719
3982
  * Placeholders: :key, :locale, :namespace
@@ -3826,36 +4089,50 @@ var LionRapid = (() => {
3826
4089
  logger.debug("API base URL is not set");
3827
4090
  throw new NetworkError("API base URL is not set");
3828
4091
  }
4092
+ try {
4093
+ return await this.fetchNamespaceRaw(locale, namespace);
4094
+ } catch (error) {
4095
+ this.handleCaughtError(error);
4096
+ }
4097
+ }
4098
+ /**
4099
+ * Shared namespace fetch with the canonical 304 contract:
4100
+ * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
4101
+ * - 304 answers with that snapshot's payload — never with "nothing".
4102
+ * - A 304 with no snapshot (server/proxy surprise) refetches unconditionally.
4103
+ * - A 200 stores version + payload atomically before either is trusted.
4104
+ */
4105
+ async fetchNamespaceRaw(locale, namespace, allowConditional = true) {
3829
4106
  const url = this.buildUrl("getNamespace", { locale, namespace });
3830
4107
  logger.debug(`API GET Namespace: ${url}`);
3831
4108
  const namespaceKey = `${locale}:${namespace}`;
3832
- try {
3833
- const extraHeaders = {};
3834
- const cachedVersion = this.getVersion(namespaceKey);
3835
- if (cachedVersion) {
3836
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3837
- }
3838
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3839
- if (response.status === 304) {
3840
- logger.debug(`Namespace ${namespaceKey}: 304 Not Modified`);
3841
- return null;
3842
- }
3843
- if (!response.ok) {
3844
- this.handleHttpError(response, namespaceKey);
3845
- }
3846
- const data = await response.json();
3847
- logger.debug("API response data:", data);
3848
- if (!data.translations) {
3849
- throw new TranslationNotFoundError(namespaceKey);
3850
- }
3851
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3852
- if (version) {
3853
- this.setVersion(namespaceKey, version);
4109
+ const snapshot = allowConditional ? this.getSnapshot(namespaceKey) : void 0;
4110
+ const extraHeaders = {};
4111
+ if (snapshot) {
4112
+ extraHeaders["If-None-Match"] = `W/"${snapshot.version}"`;
4113
+ }
4114
+ const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
4115
+ if (response.status === 304) {
4116
+ if (snapshot) {
4117
+ logger.debug(`Namespace ${namespaceKey}: 304 Not Modified (snapshot served)`);
4118
+ return snapshot.translations;
3854
4119
  }
3855
- return data.translations;
3856
- } catch (error) {
3857
- this.handleCaughtError(error);
4120
+ logger.debug(`Namespace ${namespaceKey}: unexpected 304 with no snapshot \u2014 refetching`);
4121
+ return this.fetchNamespaceRaw(locale, namespace, false);
4122
+ }
4123
+ if (!response.ok) {
4124
+ this.handleHttpError(response, namespaceKey);
3858
4125
  }
4126
+ const data = await response.json();
4127
+ logger.debug("API response data:", data);
4128
+ if (!data.translations) {
4129
+ throw new TranslationNotFoundError(namespaceKey);
4130
+ }
4131
+ const version = data.meta?.version != null ? String(data.meta.version) : null;
4132
+ if (version) {
4133
+ this.setSnapshot(namespaceKey, { version, translations: data.translations });
4134
+ }
4135
+ return data.translations;
3859
4136
  }
3860
4137
  /**
3861
4138
  * Save translation to API (if supported)
@@ -3940,33 +4217,8 @@ var LionRapid = (() => {
3940
4217
  logger.debug("API base URL is not set");
3941
4218
  throw new NetworkError("API base URL is not set");
3942
4219
  }
3943
- const url = this.buildUrl("getNamespace", { locale, namespace });
3944
- logger.debug(`API GET Namespace ContentUnits: ${url}`);
3945
- const namespaceKey = `${locale}:${namespace}`;
3946
4220
  try {
3947
- const extraHeaders = {};
3948
- const cachedVersion = this.getVersion(namespaceKey);
3949
- if (cachedVersion) {
3950
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3951
- }
3952
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3953
- if (response.status === 304) {
3954
- logger.debug(`Namespace ContentUnits ${namespaceKey}: 304 Not Modified`);
3955
- return null;
3956
- }
3957
- if (!response.ok) {
3958
- this.handleHttpError(response, namespaceKey);
3959
- }
3960
- const data = await response.json();
3961
- logger.debug("API response data:", data);
3962
- if (!data.translations) {
3963
- throw new TranslationNotFoundError(namespaceKey);
3964
- }
3965
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3966
- if (version) {
3967
- this.setVersion(namespaceKey, version);
3968
- }
3969
- return data.translations;
4221
+ return await this.fetchNamespaceRaw(locale, namespace);
3970
4222
  } catch (error) {
3971
4223
  this.handleCaughtError(error);
3972
4224
  }
@@ -4076,25 +4328,37 @@ var LionRapid = (() => {
4076
4328
  getConfig() {
4077
4329
  return this.config;
4078
4330
  }
4079
- /** Read persisted version from localStorage, falling back to in-memory map */
4080
- getVersion(namespaceKey) {
4081
- const mem = this.namespaceVersions.get(namespaceKey);
4331
+ /** Read a persisted snapshot (version + payload), falling back to memory.
4332
+ * Legacy bare-version entries and corrupt snapshots are deleted on sight —
4333
+ * a version without its payload must never drive a conditional request. */
4334
+ getSnapshot(namespaceKey) {
4335
+ const mem = this.namespaceSnapshots.get(namespaceKey);
4082
4336
  if (mem) return mem;
4083
4337
  try {
4084
- const stored = localStorage.getItem(_NetworkRepository.VERSION_PREFIX + namespaceKey);
4085
- if (stored) {
4086
- this.namespaceVersions.set(namespaceKey, stored);
4087
- return stored;
4338
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4339
+ const raw = localStorage.getItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4340
+ if (raw) {
4341
+ const parsed = JSON.parse(raw);
4342
+ if (parsed && typeof parsed.version === "string" && parsed.version.length > 0 && parsed.translations && typeof parsed.translations === "object") {
4343
+ this.namespaceSnapshots.set(namespaceKey, parsed);
4344
+ return parsed;
4345
+ }
4346
+ localStorage.removeItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4088
4347
  }
4089
4348
  } catch {
4090
4349
  }
4091
4350
  return void 0;
4092
4351
  }
4093
- /** Persist version to both in-memory map and localStorage */
4094
- setVersion(namespaceKey, version) {
4095
- this.namespaceVersions.set(namespaceKey, version);
4352
+ /** Persist version + payload as ONE entry (memory + localStorage). On quota
4353
+ * failure the previous stored snapshot stays — still a consistent pair. */
4354
+ setSnapshot(namespaceKey, snapshot) {
4355
+ this.namespaceSnapshots.set(namespaceKey, snapshot);
4096
4356
  try {
4097
- localStorage.setItem(_NetworkRepository.VERSION_PREFIX + namespaceKey, version);
4357
+ localStorage.setItem(
4358
+ _NetworkRepository.SNAPSHOT_PREFIX + namespaceKey,
4359
+ JSON.stringify(snapshot)
4360
+ );
4361
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4098
4362
  } catch {
4099
4363
  }
4100
4364
  }
@@ -4183,8 +4447,11 @@ var LionRapid = (() => {
4183
4447
  }
4184
4448
  }
4185
4449
  };
4186
- /** localStorage key prefix for persisted versions */
4187
- __publicField(_NetworkRepository, "VERSION_PREFIX", "lionrapid:v:");
4450
+ /** localStorage key prefix for persisted snapshots (version + payload) */
4451
+ __publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
4452
+ /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4453
+ * desync the snapshot store exists to prevent; entries are deleted on sight. */
4454
+ __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4188
4455
  var NetworkRepository = _NetworkRepository;
4189
4456
 
4190
4457
  // src/content/validator/ContentValidator.ts
@@ -4340,6 +4607,159 @@ var LionRapid = (() => {
4340
4607
  }
4341
4608
  };
4342
4609
 
4610
+ // src/content/splitter/BlockSplitter.ts
4611
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
4612
+ "title",
4613
+ "p",
4614
+ "h1",
4615
+ "h2",
4616
+ "h3",
4617
+ "h4",
4618
+ "h5",
4619
+ "h6",
4620
+ "li",
4621
+ "td",
4622
+ "th",
4623
+ "label",
4624
+ "button",
4625
+ "figcaption",
4626
+ "dt",
4627
+ "dd",
4628
+ "summary",
4629
+ "caption"
4630
+ ]);
4631
+ var INLINE_BLOCK_TAGS = /* @__PURE__ */ new Set(["a"]);
4632
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
4633
+ "script",
4634
+ "style",
4635
+ "code",
4636
+ "pre",
4637
+ "textarea",
4638
+ "noscript",
4639
+ "svg"
4640
+ ]);
4641
+ var TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*)\s*(\/?)>/g;
4642
+ function hasNoTranslateAttr(attrsString) {
4643
+ if (attrsString.includes("data-no-translate")) return true;
4644
+ return /translate\s*=\s*["']no["']/.test(attrsString);
4645
+ }
4646
+ function containsBlockTag(innerHTML) {
4647
+ TAG_REGEX.lastIndex = 0;
4648
+ let match;
4649
+ while ((match = TAG_REGEX.exec(innerHTML)) !== null) {
4650
+ const [, closingSlash, tagName, , selfClosingSlash] = match;
4651
+ if (!closingSlash && !selfClosingSlash && BLOCK_TAGS.has(tagName.toLowerCase())) {
4652
+ return true;
4653
+ }
4654
+ }
4655
+ return false;
4656
+ }
4657
+ function findCloseTag(html, pos, tagName) {
4658
+ const lowerTag = tagName.toLowerCase();
4659
+ let depth = 1;
4660
+ TAG_REGEX.lastIndex = pos;
4661
+ let match;
4662
+ while ((match = TAG_REGEX.exec(html)) !== null) {
4663
+ const [, closingSlash, matchedTag, , selfClosingSlash] = match;
4664
+ if (matchedTag.toLowerCase() !== lowerTag) continue;
4665
+ if (closingSlash) {
4666
+ depth--;
4667
+ if (depth === 0) return match.index;
4668
+ } else if (!selfClosingSlash) {
4669
+ depth++;
4670
+ }
4671
+ }
4672
+ return -1;
4673
+ }
4674
+ function splitBlocks(html) {
4675
+ const blocks = [];
4676
+ let pos = 0;
4677
+ let skipDepth = 0;
4678
+ let iter = 0;
4679
+ const maxIter = 1e5;
4680
+ const noTranslateEnds = [];
4681
+ while (pos < html.length && iter < maxIter) {
4682
+ iter++;
4683
+ const ltPos = html.indexOf("<", pos);
4684
+ if (ltPos === -1) break;
4685
+ if (html.startsWith("<!--", ltPos)) {
4686
+ const commentEnd = html.indexOf("-->", ltPos + 4);
4687
+ if (commentEnd === -1) break;
4688
+ pos = commentEnd + 3;
4689
+ continue;
4690
+ }
4691
+ TAG_REGEX.lastIndex = ltPos;
4692
+ const match = TAG_REGEX.exec(html);
4693
+ if (!match || match.index !== ltPos) {
4694
+ pos = ltPos + 1;
4695
+ continue;
4696
+ }
4697
+ const [fullMatch, closingSlash, tagName, attrsString, selfClosingSlash] = match;
4698
+ const tag = tagName.toLowerCase();
4699
+ const afterMatch = match.index + fullMatch.length;
4700
+ if (SKIP_TAGS.has(tag)) {
4701
+ if (closingSlash) {
4702
+ if (skipDepth > 0) skipDepth--;
4703
+ } else if (!selfClosingSlash) {
4704
+ skipDepth++;
4705
+ }
4706
+ pos = afterMatch;
4707
+ continue;
4708
+ }
4709
+ if (skipDepth > 0) {
4710
+ pos = afterMatch;
4711
+ continue;
4712
+ }
4713
+ while (noTranslateEnds.length && pos >= noTranslateEnds[noTranslateEnds.length - 1]) {
4714
+ noTranslateEnds.pop();
4715
+ }
4716
+ if (!closingSlash && !selfClosingSlash && hasNoTranslateAttr(attrsString)) {
4717
+ const regionEnd = findCloseTag(html, afterMatch, tagName);
4718
+ if (regionEnd !== -1) {
4719
+ noTranslateEnds.push(regionEnd);
4720
+ }
4721
+ }
4722
+ if (noTranslateEnds.length > 0) {
4723
+ pos = afterMatch;
4724
+ continue;
4725
+ }
4726
+ if ((BLOCK_TAGS.has(tag) || INLINE_BLOCK_TAGS.has(tag)) && !closingSlash && !selfClosingSlash) {
4727
+ const closeTagStart = findCloseTag(html, afterMatch, tagName);
4728
+ if (closeTagStart === -1) {
4729
+ pos = afterMatch;
4730
+ continue;
4731
+ }
4732
+ const innerHTML = html.slice(afterMatch, closeTagStart);
4733
+ if (containsBlockTag(innerHTML)) {
4734
+ pos = afterMatch;
4735
+ continue;
4736
+ }
4737
+ blocks.push({
4738
+ tag,
4739
+ openTag: fullMatch,
4740
+ attrs: attrsString,
4741
+ inner: innerHTML,
4742
+ innerStart: afterMatch,
4743
+ innerEnd: closeTagStart
4744
+ });
4745
+ TAG_REGEX.lastIndex = closeTagStart;
4746
+ const closeMatch = TAG_REGEX.exec(html);
4747
+ pos = closeMatch ? closeMatch.index + closeMatch[0].length : closeTagStart + 1;
4748
+ continue;
4749
+ }
4750
+ pos = afterMatch;
4751
+ }
4752
+ return { blocks };
4753
+ }
4754
+ function replaceBlockInners(html, replacements) {
4755
+ const ordered = [...replacements].sort((a, b) => b.block.innerStart - a.block.innerStart);
4756
+ let result = html;
4757
+ for (const { block, inner } of ordered) {
4758
+ result = result.slice(0, block.innerStart) + inner + result.slice(block.innerEnd);
4759
+ }
4760
+ return result;
4761
+ }
4762
+
4343
4763
  // src/browser/md5.ts
4344
4764
  function safeAdd(x, y) {
4345
4765
  const lsw = (x & 65535) + (y & 65535);
@@ -4545,6 +4965,75 @@ var LionRapid = (() => {
4545
4965
  }
4546
4966
  };
4547
4967
 
4968
+ // src/browser/sanitizer.ts
4969
+ var REMOVE_TAGS = /* @__PURE__ */ new Set([
4970
+ "script",
4971
+ "iframe",
4972
+ "frame",
4973
+ "frameset",
4974
+ "object",
4975
+ "embed",
4976
+ "applet",
4977
+ "base",
4978
+ "meta",
4979
+ "link",
4980
+ "template",
4981
+ "portal"
4982
+ ]);
4983
+ var URL_ATTRS2 = /* @__PURE__ */ new Set([
4984
+ "href",
4985
+ "src",
4986
+ "action",
4987
+ "formaction",
4988
+ "poster",
4989
+ "data",
4990
+ "xlink:href"
4991
+ ]);
4992
+ function sanitizeElement(root) {
4993
+ const all = [root, ...Array.from(root.querySelectorAll("*"))];
4994
+ for (const el of all) {
4995
+ const tag = el.tagName.toLowerCase();
4996
+ if (REMOVE_TAGS.has(tag)) {
4997
+ el.remove();
4998
+ continue;
4999
+ }
5000
+ for (const name of el.getAttributeNames()) {
5001
+ const lower = name.toLowerCase();
5002
+ if (lower.startsWith("on") || lower === "srcdoc") {
5003
+ el.removeAttribute(name);
5004
+ continue;
5005
+ }
5006
+ if (URL_ATTRS2.has(lower)) {
5007
+ if (sanitizeUrl(el.getAttribute(name) ?? "") === null) {
5008
+ el.removeAttribute(name);
5009
+ }
5010
+ continue;
5011
+ }
5012
+ if (lower === "style") {
5013
+ const clean = sanitizeCssDeclarations(el.getAttribute(name) ?? "");
5014
+ if (clean === null) el.removeAttribute(name);
5015
+ else el.setAttribute(name, clean);
5016
+ }
5017
+ }
5018
+ }
5019
+ }
5020
+ function sanitizeStyleSheetText(css, baseHref) {
5021
+ let out = css.replace(/@import[^;]*(;|$)/gi, "");
5022
+ out = out.replace(
5023
+ /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi,
5024
+ (full, _quote, target) => {
5025
+ try {
5026
+ const resolved = new URL(target, baseHref);
5027
+ if (resolved.protocol === "data:") return full;
5028
+ if (resolved.origin === new URL(baseHref).origin) return full;
5029
+ } catch {
5030
+ }
5031
+ return 'url("")';
5032
+ }
5033
+ );
5034
+ return out.replace(/expression\s*\(/gi, "blocked(");
5035
+ }
5036
+
4548
5037
  // src/browser/storage/localstorage-repository.ts
4549
5038
  var STORAGE_PREFIX = "lionrapid:";
4550
5039
  var VERSION_KEY = `${STORAGE_PREFIX}version`;