@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
@@ -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");
@@ -2666,7 +2909,6 @@ var LionRapid = (() => {
2666
2909
  this.bus = eventEmitter;
2667
2910
  this.handlerChain = handlerChain;
2668
2911
  this.currentLocale = defaultLocale;
2669
- this.fallbackLocale = fallbackLocale;
2670
2912
  this.defaultNamespace = defaultNamespace;
2671
2913
  this.cache = new CacheManager(
2672
2914
  this.handlerChain,
@@ -2731,8 +2973,7 @@ var LionRapid = (() => {
2731
2973
  this.currentLocale,
2732
2974
  firstArg,
2733
2975
  secondArg,
2734
- this.defaultNamespace,
2735
- this.fallbackLocale
2976
+ this.defaultNamespace
2736
2977
  );
2737
2978
  logger.debug(`Translation requested: ${request.cleanKey}`);
2738
2979
  const translateBus = this.bus.translation(request);
@@ -2782,8 +3023,7 @@ var LionRapid = (() => {
2782
3023
  this.currentLocale,
2783
3024
  firstArg,
2784
3025
  secondArg,
2785
- this.defaultNamespace,
2786
- this.fallbackLocale
3026
+ this.defaultNamespace
2787
3027
  );
2788
3028
  logger.debug(`HTML translation requested: ${request.cleanKey}`);
2789
3029
  const translateBus = this.bus.translation(request);
@@ -2991,8 +3231,7 @@ var LionRapid = (() => {
2991
3231
  this.currentLocale,
2992
3232
  params,
2993
3233
  defaultValue,
2994
- this.defaultNamespace,
2995
- this.fallbackLocale
3234
+ this.defaultNamespace
2996
3235
  );
2997
3236
  const syncResult = this.handlerChain.handleSync(request);
2998
3237
  const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
@@ -3113,8 +3352,7 @@ var LionRapid = (() => {
3113
3352
  this.currentLocale,
3114
3353
  void 0,
3115
3354
  void 0,
3116
- this.defaultNamespace,
3117
- this.fallbackLocale
3355
+ this.defaultNamespace
3118
3356
  );
3119
3357
  return this.handlerChain.isCached(request);
3120
3358
  }
@@ -3535,6 +3773,9 @@ var LionRapid = (() => {
3535
3773
  };
3536
3774
 
3537
3775
  // src/builder.ts
3776
+ function isLoudPreloadFailure(error) {
3777
+ return !(error instanceof TranslationNotFoundError || error instanceof TranslationLockedError);
3778
+ }
3538
3779
  var LionRapidBuilder = class _LionRapidBuilder {
3539
3780
  constructor(config) {
3540
3781
  __publicField(this, "config");
@@ -3545,7 +3786,6 @@ var LionRapid = (() => {
3545
3786
  __publicField(this, "syncQueue", null);
3546
3787
  this.config = {
3547
3788
  defaultLocale: config.defaultLocale || "en",
3548
- fallbackLocale: config.fallbackLocale || "en",
3549
3789
  namespace: config.namespace || "app"
3550
3790
  };
3551
3791
  }
@@ -3646,7 +3886,6 @@ var LionRapid = (() => {
3646
3886
  eventEmitter,
3647
3887
  handlerChain,
3648
3888
  this.config.defaultLocale,
3649
- this.config.fallbackLocale,
3650
3889
  this.config.namespace
3651
3890
  );
3652
3891
  for (const plugin of this.plugins) {
@@ -3677,7 +3916,17 @@ var LionRapid = (() => {
3677
3916
  logger.debug("Preload targets", {
3678
3917
  namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
3679
3918
  });
3919
+ let warnedLoudly = false;
3920
+ const warnLoudlyOnce = (namespaceKey, error) => {
3921
+ if (!isLoudPreloadFailure(error) || warnedLoudly) return;
3922
+ warnedLoudly = true;
3923
+ console.warn(
3924
+ `[LionRapid] Could not load translations for ${namespaceKey} \u2014 the page stays untranslated until the server is reachable.`,
3925
+ error
3926
+ );
3927
+ };
3680
3928
  const promises = eagerPreloads.map(async (preloadConfig) => {
3929
+ const namespaceKey = `${preloadConfig.locale}:${preloadConfig.namespace}`;
3681
3930
  try {
3682
3931
  const result = await networkHandler.loadNamespace(
3683
3932
  preloadConfig.locale,
@@ -3686,19 +3935,19 @@ var LionRapid = (() => {
3686
3935
  if (result && result.data && result.status === 200) {
3687
3936
  const context = {
3688
3937
  locale: preloadConfig.locale,
3689
- namespace: preloadConfig.namespace,
3690
- fallbackLocale: this.config.fallbackLocale
3938
+ namespace: preloadConfig.namespace
3691
3939
  };
3692
3940
  await handlerChain.saveNamespaceToHandlers(result.data, context);
3693
- logger.debug(
3694
- `Preloaded: ${preloadConfig.locale}:${preloadConfig.namespace}`
3695
- );
3941
+ logger.debug(`Preloaded: ${namespaceKey}`);
3942
+ return;
3943
+ }
3944
+ if (result?.error) {
3945
+ warnLoudlyOnce(namespaceKey, result.error);
3946
+ logger.warn(`Preload failed: ${namespaceKey}`, result.error);
3696
3947
  }
3697
3948
  } catch (error) {
3698
- logger.warn(
3699
- `Preload failed: ${preloadConfig.locale}:${preloadConfig.namespace}`,
3700
- error
3701
- );
3949
+ warnLoudlyOnce(namespaceKey, error);
3950
+ logger.warn(`Preload failed: ${namespaceKey}`, error);
3702
3951
  }
3703
3952
  });
3704
3953
  await Promise.allSettled(promises);
@@ -3712,8 +3961,16 @@ var LionRapid = (() => {
3712
3961
  __publicField(this, "name", "NetworkRepository");
3713
3962
  __publicField(this, "type", "network");
3714
3963
  __publicField(this, "config", {});
3715
- /** Cached version (opaque string from server) per namespace key "locale:namespace" */
3716
- __publicField(this, "namespaceVersions", /* @__PURE__ */ new Map());
3964
+ /**
3965
+ * Namespace snapshot per key "locale:namespace": the server version TOGETHER
3966
+ * with the payload it describes, persisted as ONE localStorage entry.
3967
+ *
3968
+ * Canonical 304 contract (same as edge KV bundle + WP transient cache):
3969
+ * never claim freshness you can't back. If-None-Match is only sent when the
3970
+ * snapshot is in hand; a 304 answers with that snapshot's payload; version
3971
+ * and payload can never desync because they live and die together.
3972
+ */
3973
+ __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3717
3974
  /**
3718
3975
  * Default endpoint templates with placeholder support
3719
3976
  * Placeholders: :key, :locale, :namespace
@@ -3826,36 +4083,50 @@ var LionRapid = (() => {
3826
4083
  logger.debug("API base URL is not set");
3827
4084
  throw new NetworkError("API base URL is not set");
3828
4085
  }
4086
+ try {
4087
+ return await this.fetchNamespaceRaw(locale, namespace);
4088
+ } catch (error) {
4089
+ this.handleCaughtError(error);
4090
+ }
4091
+ }
4092
+ /**
4093
+ * Shared namespace fetch with the canonical 304 contract:
4094
+ * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
4095
+ * - 304 answers with that snapshot's payload — never with "nothing".
4096
+ * - A 304 with no snapshot (server/proxy surprise) refetches unconditionally.
4097
+ * - A 200 stores version + payload atomically before either is trusted.
4098
+ */
4099
+ async fetchNamespaceRaw(locale, namespace, allowConditional = true) {
3829
4100
  const url = this.buildUrl("getNamespace", { locale, namespace });
3830
4101
  logger.debug(`API GET Namespace: ${url}`);
3831
4102
  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);
4103
+ const snapshot = allowConditional ? this.getSnapshot(namespaceKey) : void 0;
4104
+ const extraHeaders = {};
4105
+ if (snapshot) {
4106
+ extraHeaders["If-None-Match"] = `W/"${snapshot.version}"`;
4107
+ }
4108
+ const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
4109
+ if (response.status === 304) {
4110
+ if (snapshot) {
4111
+ logger.debug(`Namespace ${namespaceKey}: 304 Not Modified (snapshot served)`);
4112
+ return snapshot.translations;
3854
4113
  }
3855
- return data.translations;
3856
- } catch (error) {
3857
- this.handleCaughtError(error);
4114
+ logger.debug(`Namespace ${namespaceKey}: unexpected 304 with no snapshot \u2014 refetching`);
4115
+ return this.fetchNamespaceRaw(locale, namespace, false);
4116
+ }
4117
+ if (!response.ok) {
4118
+ this.handleHttpError(response, namespaceKey);
4119
+ }
4120
+ const data = await response.json();
4121
+ logger.debug("API response data:", data);
4122
+ if (!data.translations) {
4123
+ throw new TranslationNotFoundError(namespaceKey);
3858
4124
  }
4125
+ const version = data.meta?.version != null ? String(data.meta.version) : null;
4126
+ if (version) {
4127
+ this.setSnapshot(namespaceKey, { version, translations: data.translations });
4128
+ }
4129
+ return data.translations;
3859
4130
  }
3860
4131
  /**
3861
4132
  * Save translation to API (if supported)
@@ -3940,33 +4211,8 @@ var LionRapid = (() => {
3940
4211
  logger.debug("API base URL is not set");
3941
4212
  throw new NetworkError("API base URL is not set");
3942
4213
  }
3943
- const url = this.buildUrl("getNamespace", { locale, namespace });
3944
- logger.debug(`API GET Namespace ContentUnits: ${url}`);
3945
- const namespaceKey = `${locale}:${namespace}`;
3946
4214
  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;
4215
+ return await this.fetchNamespaceRaw(locale, namespace);
3970
4216
  } catch (error) {
3971
4217
  this.handleCaughtError(error);
3972
4218
  }
@@ -4076,25 +4322,37 @@ var LionRapid = (() => {
4076
4322
  getConfig() {
4077
4323
  return this.config;
4078
4324
  }
4079
- /** Read persisted version from localStorage, falling back to in-memory map */
4080
- getVersion(namespaceKey) {
4081
- const mem = this.namespaceVersions.get(namespaceKey);
4325
+ /** Read a persisted snapshot (version + payload), falling back to memory.
4326
+ * Legacy bare-version entries and corrupt snapshots are deleted on sight —
4327
+ * a version without its payload must never drive a conditional request. */
4328
+ getSnapshot(namespaceKey) {
4329
+ const mem = this.namespaceSnapshots.get(namespaceKey);
4082
4330
  if (mem) return mem;
4083
4331
  try {
4084
- const stored = localStorage.getItem(_NetworkRepository.VERSION_PREFIX + namespaceKey);
4085
- if (stored) {
4086
- this.namespaceVersions.set(namespaceKey, stored);
4087
- return stored;
4332
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4333
+ const raw = localStorage.getItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4334
+ if (raw) {
4335
+ const parsed = JSON.parse(raw);
4336
+ if (parsed && typeof parsed.version === "string" && parsed.version.length > 0 && parsed.translations && typeof parsed.translations === "object") {
4337
+ this.namespaceSnapshots.set(namespaceKey, parsed);
4338
+ return parsed;
4339
+ }
4340
+ localStorage.removeItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4088
4341
  }
4089
4342
  } catch {
4090
4343
  }
4091
4344
  return void 0;
4092
4345
  }
4093
- /** Persist version to both in-memory map and localStorage */
4094
- setVersion(namespaceKey, version) {
4095
- this.namespaceVersions.set(namespaceKey, version);
4346
+ /** Persist version + payload as ONE entry (memory + localStorage). On quota
4347
+ * failure the previous stored snapshot stays — still a consistent pair. */
4348
+ setSnapshot(namespaceKey, snapshot) {
4349
+ this.namespaceSnapshots.set(namespaceKey, snapshot);
4096
4350
  try {
4097
- localStorage.setItem(_NetworkRepository.VERSION_PREFIX + namespaceKey, version);
4351
+ localStorage.setItem(
4352
+ _NetworkRepository.SNAPSHOT_PREFIX + namespaceKey,
4353
+ JSON.stringify(snapshot)
4354
+ );
4355
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4098
4356
  } catch {
4099
4357
  }
4100
4358
  }
@@ -4183,8 +4441,11 @@ var LionRapid = (() => {
4183
4441
  }
4184
4442
  }
4185
4443
  };
4186
- /** localStorage key prefix for persisted versions */
4187
- __publicField(_NetworkRepository, "VERSION_PREFIX", "lionrapid:v:");
4444
+ /** localStorage key prefix for persisted snapshots (version + payload) */
4445
+ __publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
4446
+ /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4447
+ * desync the snapshot store exists to prevent; entries are deleted on sight. */
4448
+ __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4188
4449
  var NetworkRepository = _NetworkRepository;
4189
4450
 
4190
4451
  // src/content/validator/ContentValidator.ts
@@ -4340,6 +4601,159 @@ var LionRapid = (() => {
4340
4601
  }
4341
4602
  };
4342
4603
 
4604
+ // src/content/splitter/BlockSplitter.ts
4605
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
4606
+ "title",
4607
+ "p",
4608
+ "h1",
4609
+ "h2",
4610
+ "h3",
4611
+ "h4",
4612
+ "h5",
4613
+ "h6",
4614
+ "li",
4615
+ "td",
4616
+ "th",
4617
+ "label",
4618
+ "button",
4619
+ "figcaption",
4620
+ "dt",
4621
+ "dd",
4622
+ "summary",
4623
+ "caption"
4624
+ ]);
4625
+ var INLINE_BLOCK_TAGS = /* @__PURE__ */ new Set(["a"]);
4626
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
4627
+ "script",
4628
+ "style",
4629
+ "code",
4630
+ "pre",
4631
+ "textarea",
4632
+ "noscript",
4633
+ "svg"
4634
+ ]);
4635
+ var TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*)\s*(\/?)>/g;
4636
+ function hasNoTranslateAttr(attrsString) {
4637
+ if (attrsString.includes("data-no-translate")) return true;
4638
+ return /translate\s*=\s*["']no["']/.test(attrsString);
4639
+ }
4640
+ function containsBlockTag(innerHTML) {
4641
+ TAG_REGEX.lastIndex = 0;
4642
+ let match;
4643
+ while ((match = TAG_REGEX.exec(innerHTML)) !== null) {
4644
+ const [, closingSlash, tagName, , selfClosingSlash] = match;
4645
+ if (!closingSlash && !selfClosingSlash && BLOCK_TAGS.has(tagName.toLowerCase())) {
4646
+ return true;
4647
+ }
4648
+ }
4649
+ return false;
4650
+ }
4651
+ function findCloseTag(html, pos, tagName) {
4652
+ const lowerTag = tagName.toLowerCase();
4653
+ let depth = 1;
4654
+ TAG_REGEX.lastIndex = pos;
4655
+ let match;
4656
+ while ((match = TAG_REGEX.exec(html)) !== null) {
4657
+ const [, closingSlash, matchedTag, , selfClosingSlash] = match;
4658
+ if (matchedTag.toLowerCase() !== lowerTag) continue;
4659
+ if (closingSlash) {
4660
+ depth--;
4661
+ if (depth === 0) return match.index;
4662
+ } else if (!selfClosingSlash) {
4663
+ depth++;
4664
+ }
4665
+ }
4666
+ return -1;
4667
+ }
4668
+ function splitBlocks(html) {
4669
+ const blocks = [];
4670
+ let pos = 0;
4671
+ let skipDepth = 0;
4672
+ let iter = 0;
4673
+ const maxIter = 1e5;
4674
+ const noTranslateEnds = [];
4675
+ while (pos < html.length && iter < maxIter) {
4676
+ iter++;
4677
+ const ltPos = html.indexOf("<", pos);
4678
+ if (ltPos === -1) break;
4679
+ if (html.startsWith("<!--", ltPos)) {
4680
+ const commentEnd = html.indexOf("-->", ltPos + 4);
4681
+ if (commentEnd === -1) break;
4682
+ pos = commentEnd + 3;
4683
+ continue;
4684
+ }
4685
+ TAG_REGEX.lastIndex = ltPos;
4686
+ const match = TAG_REGEX.exec(html);
4687
+ if (!match || match.index !== ltPos) {
4688
+ pos = ltPos + 1;
4689
+ continue;
4690
+ }
4691
+ const [fullMatch, closingSlash, tagName, attrsString, selfClosingSlash] = match;
4692
+ const tag = tagName.toLowerCase();
4693
+ const afterMatch = match.index + fullMatch.length;
4694
+ if (SKIP_TAGS.has(tag)) {
4695
+ if (closingSlash) {
4696
+ if (skipDepth > 0) skipDepth--;
4697
+ } else if (!selfClosingSlash) {
4698
+ skipDepth++;
4699
+ }
4700
+ pos = afterMatch;
4701
+ continue;
4702
+ }
4703
+ if (skipDepth > 0) {
4704
+ pos = afterMatch;
4705
+ continue;
4706
+ }
4707
+ while (noTranslateEnds.length && pos >= noTranslateEnds[noTranslateEnds.length - 1]) {
4708
+ noTranslateEnds.pop();
4709
+ }
4710
+ if (!closingSlash && !selfClosingSlash && hasNoTranslateAttr(attrsString)) {
4711
+ const regionEnd = findCloseTag(html, afterMatch, tagName);
4712
+ if (regionEnd !== -1) {
4713
+ noTranslateEnds.push(regionEnd);
4714
+ }
4715
+ }
4716
+ if (noTranslateEnds.length > 0) {
4717
+ pos = afterMatch;
4718
+ continue;
4719
+ }
4720
+ if ((BLOCK_TAGS.has(tag) || INLINE_BLOCK_TAGS.has(tag)) && !closingSlash && !selfClosingSlash) {
4721
+ const closeTagStart = findCloseTag(html, afterMatch, tagName);
4722
+ if (closeTagStart === -1) {
4723
+ pos = afterMatch;
4724
+ continue;
4725
+ }
4726
+ const innerHTML = html.slice(afterMatch, closeTagStart);
4727
+ if (containsBlockTag(innerHTML)) {
4728
+ pos = afterMatch;
4729
+ continue;
4730
+ }
4731
+ blocks.push({
4732
+ tag,
4733
+ openTag: fullMatch,
4734
+ attrs: attrsString,
4735
+ inner: innerHTML,
4736
+ innerStart: afterMatch,
4737
+ innerEnd: closeTagStart
4738
+ });
4739
+ TAG_REGEX.lastIndex = closeTagStart;
4740
+ const closeMatch = TAG_REGEX.exec(html);
4741
+ pos = closeMatch ? closeMatch.index + closeMatch[0].length : closeTagStart + 1;
4742
+ continue;
4743
+ }
4744
+ pos = afterMatch;
4745
+ }
4746
+ return { blocks };
4747
+ }
4748
+ function replaceBlockInners(html, replacements) {
4749
+ const ordered = [...replacements].sort((a, b) => b.block.innerStart - a.block.innerStart);
4750
+ let result = html;
4751
+ for (const { block, inner } of ordered) {
4752
+ result = result.slice(0, block.innerStart) + inner + result.slice(block.innerEnd);
4753
+ }
4754
+ return result;
4755
+ }
4756
+
4343
4757
  // src/browser/md5.ts
4344
4758
  function safeAdd(x, y) {
4345
4759
  const lsw = (x & 65535) + (y & 65535);
@@ -4545,6 +4959,75 @@ var LionRapid = (() => {
4545
4959
  }
4546
4960
  };
4547
4961
 
4962
+ // src/browser/sanitizer.ts
4963
+ var REMOVE_TAGS = /* @__PURE__ */ new Set([
4964
+ "script",
4965
+ "iframe",
4966
+ "frame",
4967
+ "frameset",
4968
+ "object",
4969
+ "embed",
4970
+ "applet",
4971
+ "base",
4972
+ "meta",
4973
+ "link",
4974
+ "template",
4975
+ "portal"
4976
+ ]);
4977
+ var URL_ATTRS2 = /* @__PURE__ */ new Set([
4978
+ "href",
4979
+ "src",
4980
+ "action",
4981
+ "formaction",
4982
+ "poster",
4983
+ "data",
4984
+ "xlink:href"
4985
+ ]);
4986
+ function sanitizeElement(root) {
4987
+ const all = [root, ...Array.from(root.querySelectorAll("*"))];
4988
+ for (const el of all) {
4989
+ const tag = el.tagName.toLowerCase();
4990
+ if (REMOVE_TAGS.has(tag)) {
4991
+ el.remove();
4992
+ continue;
4993
+ }
4994
+ for (const name of el.getAttributeNames()) {
4995
+ const lower = name.toLowerCase();
4996
+ if (lower.startsWith("on") || lower === "srcdoc") {
4997
+ el.removeAttribute(name);
4998
+ continue;
4999
+ }
5000
+ if (URL_ATTRS2.has(lower)) {
5001
+ if (sanitizeUrl(el.getAttribute(name) ?? "") === null) {
5002
+ el.removeAttribute(name);
5003
+ }
5004
+ continue;
5005
+ }
5006
+ if (lower === "style") {
5007
+ const clean = sanitizeCssDeclarations(el.getAttribute(name) ?? "");
5008
+ if (clean === null) el.removeAttribute(name);
5009
+ else el.setAttribute(name, clean);
5010
+ }
5011
+ }
5012
+ }
5013
+ }
5014
+ function sanitizeStyleSheetText(css, baseHref) {
5015
+ let out = css.replace(/@import[^;]*(;|$)/gi, "");
5016
+ out = out.replace(
5017
+ /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi,
5018
+ (full, _quote, target) => {
5019
+ try {
5020
+ const resolved = new URL(target, baseHref);
5021
+ if (resolved.protocol === "data:") return full;
5022
+ if (resolved.origin === new URL(baseHref).origin) return full;
5023
+ } catch {
5024
+ }
5025
+ return 'url("")';
5026
+ }
5027
+ );
5028
+ return out.replace(/expression\s*\(/gi, "blocked(");
5029
+ }
5030
+
4548
5031
  // src/browser/storage/localstorage-repository.ts
4549
5032
  var STORAGE_PREFIX = "lionrapid:";
4550
5033
  var VERSION_KEY = `${STORAGE_PREFIX}version`;