@lionrapid/core 0.4.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 (115) 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 +69 -0
  8. package/dist/browser/auto-init.d.ts.map +1 -1
  9. package/dist/browser/auto-init.js +193 -61
  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 +599 -245
  28. package/dist/browser/lionrapid.auto.dev.js.map +4 -4
  29. package/dist/browser/lionrapid.auto.js +599 -245
  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 +951 -196
  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 +951 -196
  42. package/dist/browser/lionrapid.iife.dev.js.map +4 -4
  43. package/dist/browser/lionrapid.iife.js +951 -196
  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 +951 -196
  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 +16 -0
  56. package/dist/builder.d.ts.map +1 -1
  57. package/dist/builder.js +43 -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 +9 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +28 -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/index.d.ts +1 -0
  88. package/dist/repositories/index.d.ts.map +1 -1
  89. package/dist/repositories/index.js +1 -0
  90. package/dist/repositories/index.js.map +1 -1
  91. package/dist/repositories/network-repository.d.ts +30 -8
  92. package/dist/repositories/network-repository.d.ts.map +1 -1
  93. package/dist/repositories/network-repository.js +88 -74
  94. package/dist/repositories/network-repository.js.map +1 -1
  95. package/dist/repositories/static-repository.d.ts +40 -0
  96. package/dist/repositories/static-repository.d.ts.map +1 -0
  97. package/dist/repositories/static-repository.js +60 -0
  98. package/dist/repositories/static-repository.js.map +1 -0
  99. package/dist/types/lionRapid.types.d.ts +2 -0
  100. package/dist/types/lionRapid.types.d.ts.map +1 -1
  101. package/dist/types/translation.types.d.ts +0 -3
  102. package/dist/types/translation.types.d.ts.map +1 -1
  103. package/dist/utils/context-utils.d.ts +1 -2
  104. package/dist/utils/context-utils.d.ts.map +1 -1
  105. package/dist/utils/context-utils.js +3 -6
  106. package/dist/utils/context-utils.js.map +1 -1
  107. package/dist/utils/locale.d.ts +57 -0
  108. package/dist/utils/locale.d.ts.map +1 -0
  109. package/dist/utils/locale.js +116 -0
  110. package/dist/utils/locale.js.map +1 -0
  111. package/dist/utils/translation-flow.d.ts +1 -1
  112. package/dist/utils/translation-flow.d.ts.map +1 -1
  113. package/dist/utils/translation-flow.js +2 -3
  114. package/dist/utils/translation-flow.js.map +1 -1
  115. package/package.json +2 -1
@@ -28,8 +28,10 @@ var LionRapid = (() => {
28
28
  Colors: () => Colors,
29
29
  ContentValidator: () => ContentValidator,
30
30
  EventEmitter: () => EventEmitter,
31
+ ExcludeFilter: () => ExcludeFilter,
31
32
  HTMLSerializer: () => HTMLSerializer,
32
33
  HandlerChain: () => HandlerChain,
34
+ LOCALE_TAG_REGEX: () => LOCALE_TAG_REGEX,
33
35
  LionRapidBuilder: () => LionRapidBuilder,
34
36
  LionRapidCore: () => LionRapidCore,
35
37
  LocalStorageRepository: () => LocalStorageRepository,
@@ -40,24 +42,44 @@ var LionRapid = (() => {
40
42
  MissingTranslationQueue: () => MissingTranslationQueue,
41
43
  NetworkError: () => NetworkError,
42
44
  NetworkRepository: () => NetworkRepository,
45
+ SAFE_ATTRS: () => SAFE_ATTRS,
46
+ SAFE_STYLE_PROPS: () => SAFE_STYLE_PROPS,
47
+ SAFE_TAGS: () => SAFE_TAGS,
43
48
  ServerError: () => ServerError,
49
+ StaticRepository: () => StaticRepository,
44
50
  TranslationError: () => TranslationError,
45
51
  TranslationLockedError: () => TranslationLockedError,
46
52
  TranslationNotFoundError: () => TranslationNotFoundError,
47
53
  VERSION: () => VERSION,
48
54
  VOID_TAGS: () => VOID_TAGS,
49
55
  createPackageLogger: () => createPackageLogger,
56
+ escapeHtml: () => escapeHtml,
50
57
  extractTagName: () => extractTagName,
58
+ generateAutoKey: () => generateAutoKey,
51
59
  isClientError: () => isClientError,
52
60
  isRetryableError: () => isRetryableError,
53
61
  isTranslationError: () => isTranslationError,
62
+ localeDir: () => localeDir,
54
63
  logger: () => logger,
64
+ matchLocale: () => matchLocale,
65
+ md5: () => md5,
55
66
  measure: () => measure,
67
+ normalizeLocale: () => normalizeLocale,
68
+ parseAcceptLanguage: () => parseAcceptLanguage,
56
69
  parseKeyPath: () => parseKeyPath,
57
70
  prepareTranslationRequest: () => prepareTranslationRequest,
71
+ primarySubtag: () => primarySubtag,
58
72
  readRawStyle: () => readRawStyle,
73
+ replaceBlockInners: () => replaceBlockInners,
59
74
  resolveNestedKey: () => resolveNestedKey,
60
- 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
61
83
  });
62
84
 
63
85
  // src/config/logger.config.ts
@@ -407,6 +429,57 @@ var LionRapid = (() => {
407
429
  };
408
430
  };
409
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
+
410
483
  // src/base/event-emitter.ts
411
484
  var EventEmitter = class {
412
485
  constructor() {
@@ -695,7 +768,7 @@ var LionRapid = (() => {
695
768
  complete: () => {
696
769
  this.emit("language:change:success", {
697
770
  context: this.createContext(requestId, [...baseTrace, "complete"]),
698
- payload: { oldLocale, newLocale }
771
+ payload: { oldLocale, newLocale, dir: localeDir(newLocale) }
699
772
  });
700
773
  },
701
774
  error: (error) => {
@@ -1105,12 +1178,11 @@ var LionRapid = (() => {
1105
1178
  }
1106
1179
 
1107
1180
  // src/utils/context-utils.ts
1108
- function createTranslationContext(parsedKey, currentLocale, defaultNamespace, fallbackLocale) {
1181
+ function createTranslationContext(parsedKey, currentLocale, defaultNamespace) {
1109
1182
  const effectiveNamespace = parsedKey.hasExplicitNamespace ? parsedKey.namespace : defaultNamespace;
1110
1183
  return {
1111
1184
  locale: currentLocale,
1112
- namespace: effectiveNamespace,
1113
- fallbackLocale: fallbackLocale || currentLocale
1185
+ namespace: effectiveNamespace
1114
1186
  };
1115
1187
  }
1116
1188
 
@@ -1227,7 +1299,7 @@ var LionRapid = (() => {
1227
1299
  }
1228
1300
 
1229
1301
  // src/utils/translation-flow.ts
1230
- function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace, fallbackLocale) {
1302
+ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace) {
1231
1303
  const startTime = performance.now();
1232
1304
  const { defaultValue, params } = parseTranslationParams(
1233
1305
  defaultValueOrParams,
@@ -1237,8 +1309,7 @@ var LionRapid = (() => {
1237
1309
  const context = createTranslationContext(
1238
1310
  parsedKey,
1239
1311
  currentLocale,
1240
- defaultNamespace,
1241
- fallbackLocale
1312
+ defaultNamespace
1242
1313
  );
1243
1314
  const individualCacheKey = createIndividualCacheKey(
1244
1315
  context.locale,
@@ -1260,7 +1331,6 @@ var LionRapid = (() => {
1260
1331
  locale: currentLocale,
1261
1332
  namespace: context.namespace,
1262
1333
  // ✅ Use effective namespace (explicit or default)
1263
- fallbackLocale,
1264
1334
  individualCacheKey,
1265
1335
  // ✅ Ready-to-use cache key
1266
1336
  namespaceCacheKey,
@@ -2376,6 +2446,183 @@ var LionRapid = (() => {
2376
2446
  }
2377
2447
  };
2378
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
+
2379
2626
  // src/content/serializer/HTMLSerializer.ts
2380
2627
  var HTMLSerializer = class {
2381
2628
  serialize(unit) {
@@ -2385,24 +2632,25 @@ var LionRapid = (() => {
2385
2632
  let lastIndex = 0;
2386
2633
  let match;
2387
2634
  while ((match = tagPattern.exec(text)) !== null) {
2388
- result += text.slice(lastIndex, match.index);
2635
+ result += escapeHtml(text.slice(lastIndex, match.index));
2389
2636
  const [fullMatch, closingSlash, indexStr] = match;
2390
2637
  const styleList = styles[indexStr] ?? [];
2391
2638
  const rawOpen = readRawStyle(styleList);
2392
- if (rawOpen) {
2639
+ const safeOpen = rawOpen === null ? null : sanitizeOpeningTag(rawOpen);
2640
+ if (rawOpen && safeOpen !== null) {
2393
2641
  const tagName = extractTagName(rawOpen);
2394
2642
  const isVoid = VOID_TAGS.has(tagName);
2395
2643
  if (closingSlash) {
2396
2644
  if (!isVoid) result += `</${tagName}>`;
2397
2645
  } else {
2398
- result += rawOpen;
2646
+ result += safeOpen;
2399
2647
  }
2400
2648
  } else {
2401
2649
  result += fullMatch;
2402
2650
  }
2403
2651
  lastIndex = match.index + fullMatch.length;
2404
2652
  }
2405
- result += text.slice(lastIndex);
2653
+ result += escapeHtml(text.slice(lastIndex));
2406
2654
  return result;
2407
2655
  }
2408
2656
  };
@@ -2641,10 +2889,9 @@ var LionRapid = (() => {
2641
2889
  var INDEXED_TAG_REGEX = /<\/?[0-9]+\/?>/;
2642
2890
  var HTML_TAG_REGEX = /<[a-z][a-z0-9]*(?:\s+[^>]*)?\/?>/i;
2643
2891
  var LionRapidCore = class {
2644
- constructor(eventEmitter, handlerChain, defaultLocale = "en", fallbackLocale = "en", defaultNamespace) {
2892
+ constructor(eventEmitter, handlerChain, defaultLocale = "en", defaultNamespace) {
2645
2893
  __publicField(this, "handlerChain");
2646
2894
  __publicField(this, "currentLocale");
2647
- __publicField(this, "fallbackLocale");
2648
2895
  __publicField(this, "defaultNamespace");
2649
2896
  __publicField(this, "pendingTranslations", /* @__PURE__ */ new Map());
2650
2897
  __publicField(this, "bus");
@@ -2662,7 +2909,6 @@ var LionRapid = (() => {
2662
2909
  this.bus = eventEmitter;
2663
2910
  this.handlerChain = handlerChain;
2664
2911
  this.currentLocale = defaultLocale;
2665
- this.fallbackLocale = fallbackLocale;
2666
2912
  this.defaultNamespace = defaultNamespace;
2667
2913
  this.cache = new CacheManager(
2668
2914
  this.handlerChain,
@@ -2727,8 +2973,7 @@ var LionRapid = (() => {
2727
2973
  this.currentLocale,
2728
2974
  firstArg,
2729
2975
  secondArg,
2730
- this.defaultNamespace,
2731
- this.fallbackLocale
2976
+ this.defaultNamespace
2732
2977
  );
2733
2978
  logger.debug(`Translation requested: ${request.cleanKey}`);
2734
2979
  const translateBus = this.bus.translation(request);
@@ -2778,8 +3023,7 @@ var LionRapid = (() => {
2778
3023
  this.currentLocale,
2779
3024
  firstArg,
2780
3025
  secondArg,
2781
- this.defaultNamespace,
2782
- this.fallbackLocale
3026
+ this.defaultNamespace
2783
3027
  );
2784
3028
  logger.debug(`HTML translation requested: ${request.cleanKey}`);
2785
3029
  const translateBus = this.bus.translation(request);
@@ -2987,8 +3231,7 @@ var LionRapid = (() => {
2987
3231
  this.currentLocale,
2988
3232
  params,
2989
3233
  defaultValue,
2990
- this.defaultNamespace,
2991
- this.fallbackLocale
3234
+ this.defaultNamespace
2992
3235
  );
2993
3236
  const syncResult = this.handlerChain.handleSync(request);
2994
3237
  const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
@@ -3109,8 +3352,7 @@ var LionRapid = (() => {
3109
3352
  this.currentLocale,
3110
3353
  void 0,
3111
3354
  void 0,
3112
- this.defaultNamespace,
3113
- this.fallbackLocale
3355
+ this.defaultNamespace
3114
3356
  );
3115
3357
  return this.handlerChain.isCached(request);
3116
3358
  }
@@ -3220,6 +3462,155 @@ var LionRapid = (() => {
3220
3462
  }
3221
3463
  };
3222
3464
 
3465
+ // src/repositories/memory-repository.ts
3466
+ var MemoryRepository = class {
3467
+ constructor(config) {
3468
+ __publicField(this, "name", "MemoryRepository");
3469
+ __publicField(this, "type", "memory");
3470
+ __publicField(this, "config", {});
3471
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
3472
+ __publicField(this, "accessOrder", /* @__PURE__ */ new Map());
3473
+ __publicField(this, "maxSize", 1e3);
3474
+ this.config = config;
3475
+ this.maxSize = this.config.options?.maxSize || 1e3;
3476
+ }
3477
+ get(key) {
3478
+ const value = this.cache.get(key);
3479
+ if (value !== void 0) {
3480
+ this.accessOrder.set(key, Date.now());
3481
+ return value;
3482
+ }
3483
+ return null;
3484
+ }
3485
+ set(key, value) {
3486
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
3487
+ this.evictOldest();
3488
+ }
3489
+ this.cache.set(key, value);
3490
+ this.accessOrder.set(key, Date.now());
3491
+ }
3492
+ remove(key) {
3493
+ this.cache.delete(key);
3494
+ this.accessOrder.delete(key);
3495
+ }
3496
+ clear() {
3497
+ this.cache.clear();
3498
+ this.accessOrder.clear();
3499
+ }
3500
+ /**
3501
+ * Clear all entries for a specific namespace
3502
+ * Removes both the namespace cache entry and all individual translation keys
3503
+ */
3504
+ clearNamespace(locale, namespace) {
3505
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3506
+ const keyPrefix = `${locale}:${namespace}:`;
3507
+ this.cache.delete(namespaceCacheKey);
3508
+ this.accessOrder.delete(namespaceCacheKey);
3509
+ for (const key of this.cache.keys()) {
3510
+ if (key.startsWith(keyPrefix)) {
3511
+ this.cache.delete(key);
3512
+ this.accessOrder.delete(key);
3513
+ }
3514
+ }
3515
+ }
3516
+ getNamespace(locale, namespace) {
3517
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3518
+ const namespaceData = this.cache.get(namespaceCacheKey);
3519
+ if (!namespaceData) return null;
3520
+ try {
3521
+ return JSON.parse(namespaceData);
3522
+ } catch (error) {
3523
+ this.cache.delete(namespaceCacheKey);
3524
+ return null;
3525
+ }
3526
+ }
3527
+ setNamespace(locale, namespace, translations) {
3528
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3529
+ this.cache.set(namespaceCacheKey, JSON.stringify(translations));
3530
+ }
3531
+ evictOldest() {
3532
+ let oldestKey = "";
3533
+ let oldestTime = Date.now();
3534
+ for (const [key, time] of this.accessOrder.entries()) {
3535
+ if (time < oldestTime) {
3536
+ oldestTime = time;
3537
+ oldestKey = key;
3538
+ }
3539
+ }
3540
+ if (oldestKey) {
3541
+ this.cache.delete(oldestKey);
3542
+ this.accessOrder.delete(oldestKey);
3543
+ }
3544
+ }
3545
+ /**
3546
+ * Get all cache entries for debugging
3547
+ */
3548
+ getEntries() {
3549
+ const entries = {};
3550
+ for (const [key, value] of this.cache.entries()) {
3551
+ entries[key] = value;
3552
+ }
3553
+ return entries;
3554
+ }
3555
+ getConfig() {
3556
+ return this.config;
3557
+ }
3558
+ };
3559
+
3560
+ // src/repositories/static-repository.ts
3561
+ var StaticRepository = class {
3562
+ constructor(bundle, config = {}) {
3563
+ __publicField(this, "name", "StaticRepository");
3564
+ __publicField(this, "type", "memory");
3565
+ __publicField(this, "config");
3566
+ __publicField(this, "mem");
3567
+ this.config = {
3568
+ enabled: config.enabled ?? true,
3569
+ priority: config.priority ?? 0.5,
3570
+ // after the live memory (0), before storage
3571
+ options: {
3572
+ ttl: Number.MAX_SAFE_INTEGER,
3573
+ maxSize: Number.MAX_SAFE_INTEGER,
3574
+ ...config.options
3575
+ }
3576
+ };
3577
+ this.mem = new MemoryRepository({ options: { maxSize: Number.MAX_SAFE_INTEGER } });
3578
+ for (const [locale, namespaces] of Object.entries(bundle)) {
3579
+ for (const [namespace, translations] of Object.entries(namespaces)) {
3580
+ this.mem.setNamespace(locale, namespace, translations);
3581
+ const flat = flattenTranslations(
3582
+ translations,
3583
+ "",
3584
+ locale,
3585
+ namespace
3586
+ );
3587
+ for (const [key, value] of Object.entries(flat)) this.mem.set(key, value);
3588
+ }
3589
+ }
3590
+ }
3591
+ get(key) {
3592
+ return this.mem.get(key);
3593
+ }
3594
+ getNamespace(locale, namespace) {
3595
+ return this.mem.getNamespace(locale, namespace);
3596
+ }
3597
+ getEntries() {
3598
+ return this.mem.getEntries();
3599
+ }
3600
+ getConfig() {
3601
+ return this.config;
3602
+ }
3603
+ // Read-only: the live memory layer owns fresh network data; this stays pristine.
3604
+ set() {
3605
+ }
3606
+ remove() {
3607
+ }
3608
+ clear() {
3609
+ }
3610
+ setNamespace() {
3611
+ }
3612
+ };
3613
+
3223
3614
  // src/sync/missing-translation-queue.ts
3224
3615
  var COOLDOWN_MS = 6e4;
3225
3616
  var MissingTranslationQueue = class {
@@ -3382,6 +3773,9 @@ var LionRapid = (() => {
3382
3773
  };
3383
3774
 
3384
3775
  // src/builder.ts
3776
+ function isLoudPreloadFailure(error) {
3777
+ return !(error instanceof TranslationNotFoundError || error instanceof TranslationLockedError);
3778
+ }
3385
3779
  var LionRapidBuilder = class _LionRapidBuilder {
3386
3780
  constructor(config) {
3387
3781
  __publicField(this, "config");
@@ -3392,7 +3786,6 @@ var LionRapid = (() => {
3392
3786
  __publicField(this, "syncQueue", null);
3393
3787
  this.config = {
3394
3788
  defaultLocale: config.defaultLocale || "en",
3395
- fallbackLocale: config.fallbackLocale || "en",
3396
3789
  namespace: config.namespace || "app"
3397
3790
  };
3398
3791
  }
@@ -3428,6 +3821,15 @@ var LionRapid = (() => {
3428
3821
  }
3429
3822
  return this;
3430
3823
  }
3824
+ /**
3825
+ * Offline resilience: seed a translation bundle (or the CLI's snapshot files)
3826
+ * as a read-only sync layer that resolves synchronously even with the server
3827
+ * down and nothing cached. Sits after the live memory layer (priority 0) at
3828
+ * `priority` (default 0.5), so fresh network data always shadows it.
3829
+ */
3830
+ useStaticBundle(bundle, priority = 0.5) {
3831
+ return this.use(new StaticRepository(bundle, { priority }));
3832
+ }
3431
3833
  async build() {
3432
3834
  const eventEmitter = new EventEmitter();
3433
3835
  const buildBus = eventEmitter.builder();
@@ -3484,7 +3886,6 @@ var LionRapid = (() => {
3484
3886
  eventEmitter,
3485
3887
  handlerChain,
3486
3888
  this.config.defaultLocale,
3487
- this.config.fallbackLocale,
3488
3889
  this.config.namespace
3489
3890
  );
3490
3891
  for (const plugin of this.plugins) {
@@ -3515,7 +3916,17 @@ var LionRapid = (() => {
3515
3916
  logger.debug("Preload targets", {
3516
3917
  namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
3517
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
+ };
3518
3928
  const promises = eagerPreloads.map(async (preloadConfig) => {
3929
+ const namespaceKey = `${preloadConfig.locale}:${preloadConfig.namespace}`;
3519
3930
  try {
3520
3931
  const result = await networkHandler.loadNamespace(
3521
3932
  preloadConfig.locale,
@@ -3524,19 +3935,19 @@ var LionRapid = (() => {
3524
3935
  if (result && result.data && result.status === 200) {
3525
3936
  const context = {
3526
3937
  locale: preloadConfig.locale,
3527
- namespace: preloadConfig.namespace,
3528
- fallbackLocale: this.config.fallbackLocale
3938
+ namespace: preloadConfig.namespace
3529
3939
  };
3530
3940
  await handlerChain.saveNamespaceToHandlers(result.data, context);
3531
- logger.debug(
3532
- `Preloaded: ${preloadConfig.locale}:${preloadConfig.namespace}`
3533
- );
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);
3534
3947
  }
3535
3948
  } catch (error) {
3536
- logger.warn(
3537
- `Preload failed: ${preloadConfig.locale}:${preloadConfig.namespace}`,
3538
- error
3539
- );
3949
+ warnLoudlyOnce(namespaceKey, error);
3950
+ logger.warn(`Preload failed: ${namespaceKey}`, error);
3540
3951
  }
3541
3952
  });
3542
3953
  await Promise.allSettled(promises);
@@ -3550,8 +3961,16 @@ var LionRapid = (() => {
3550
3961
  __publicField(this, "name", "NetworkRepository");
3551
3962
  __publicField(this, "type", "network");
3552
3963
  __publicField(this, "config", {});
3553
- /** Cached version (opaque string from server) per namespace key "locale:namespace" */
3554
- __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());
3555
3974
  /**
3556
3975
  * Default endpoint templates with placeholder support
3557
3976
  * Placeholders: :key, :locale, :namespace
@@ -3664,36 +4083,50 @@ var LionRapid = (() => {
3664
4083
  logger.debug("API base URL is not set");
3665
4084
  throw new NetworkError("API base URL is not set");
3666
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) {
3667
4100
  const url = this.buildUrl("getNamespace", { locale, namespace });
3668
4101
  logger.debug(`API GET Namespace: ${url}`);
3669
4102
  const namespaceKey = `${locale}:${namespace}`;
3670
- try {
3671
- const extraHeaders = {};
3672
- const cachedVersion = this.getVersion(namespaceKey);
3673
- if (cachedVersion) {
3674
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3675
- }
3676
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3677
- if (response.status === 304) {
3678
- logger.debug(`Namespace ${namespaceKey}: 304 Not Modified`);
3679
- return null;
3680
- }
3681
- if (!response.ok) {
3682
- this.handleHttpError(response, namespaceKey);
3683
- }
3684
- const data = await response.json();
3685
- logger.debug("API response data:", data);
3686
- if (!data.translations) {
3687
- throw new TranslationNotFoundError(namespaceKey);
3688
- }
3689
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3690
- if (version) {
3691
- 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;
3692
4113
  }
3693
- return data.translations;
3694
- } catch (error) {
3695
- 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);
3696
4119
  }
4120
+ const data = await response.json();
4121
+ logger.debug("API response data:", data);
4122
+ if (!data.translations) {
4123
+ throw new TranslationNotFoundError(namespaceKey);
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;
3697
4130
  }
3698
4131
  /**
3699
4132
  * Save translation to API (if supported)
@@ -3778,33 +4211,8 @@ var LionRapid = (() => {
3778
4211
  logger.debug("API base URL is not set");
3779
4212
  throw new NetworkError("API base URL is not set");
3780
4213
  }
3781
- const url = this.buildUrl("getNamespace", { locale, namespace });
3782
- logger.debug(`API GET Namespace ContentUnits: ${url}`);
3783
- const namespaceKey = `${locale}:${namespace}`;
3784
4214
  try {
3785
- const extraHeaders = {};
3786
- const cachedVersion = this.getVersion(namespaceKey);
3787
- if (cachedVersion) {
3788
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3789
- }
3790
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3791
- if (response.status === 304) {
3792
- logger.debug(`Namespace ContentUnits ${namespaceKey}: 304 Not Modified`);
3793
- return null;
3794
- }
3795
- if (!response.ok) {
3796
- this.handleHttpError(response, namespaceKey);
3797
- }
3798
- const data = await response.json();
3799
- logger.debug("API response data:", data);
3800
- if (!data.translations) {
3801
- throw new TranslationNotFoundError(namespaceKey);
3802
- }
3803
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3804
- if (version) {
3805
- this.setVersion(namespaceKey, version);
3806
- }
3807
- return data.translations;
4215
+ return await this.fetchNamespaceRaw(locale, namespace);
3808
4216
  } catch (error) {
3809
4217
  this.handleCaughtError(error);
3810
4218
  }
@@ -3914,25 +4322,37 @@ var LionRapid = (() => {
3914
4322
  getConfig() {
3915
4323
  return this.config;
3916
4324
  }
3917
- /** Read persisted version from localStorage, falling back to in-memory map */
3918
- getVersion(namespaceKey) {
3919
- 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);
3920
4330
  if (mem) return mem;
3921
4331
  try {
3922
- const stored = localStorage.getItem(_NetworkRepository.VERSION_PREFIX + namespaceKey);
3923
- if (stored) {
3924
- this.namespaceVersions.set(namespaceKey, stored);
3925
- 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);
3926
4341
  }
3927
4342
  } catch {
3928
4343
  }
3929
4344
  return void 0;
3930
4345
  }
3931
- /** Persist version to both in-memory map and localStorage */
3932
- setVersion(namespaceKey, version) {
3933
- 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);
3934
4350
  try {
3935
- 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);
3936
4356
  } catch {
3937
4357
  }
3938
4358
  }
@@ -4021,105 +4441,13 @@ var LionRapid = (() => {
4021
4441
  }
4022
4442
  }
4023
4443
  };
4024
- /** localStorage key prefix for persisted versions */
4025
- __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:");
4026
4449
  var NetworkRepository = _NetworkRepository;
4027
4450
 
4028
- // src/repositories/memory-repository.ts
4029
- var MemoryRepository = class {
4030
- constructor(config) {
4031
- __publicField(this, "name", "MemoryRepository");
4032
- __publicField(this, "type", "memory");
4033
- __publicField(this, "config", {});
4034
- __publicField(this, "cache", /* @__PURE__ */ new Map());
4035
- __publicField(this, "accessOrder", /* @__PURE__ */ new Map());
4036
- __publicField(this, "maxSize", 1e3);
4037
- this.config = config;
4038
- this.maxSize = this.config.options?.maxSize || 1e3;
4039
- }
4040
- get(key) {
4041
- const value = this.cache.get(key);
4042
- if (value !== void 0) {
4043
- this.accessOrder.set(key, Date.now());
4044
- return value;
4045
- }
4046
- return null;
4047
- }
4048
- set(key, value) {
4049
- if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
4050
- this.evictOldest();
4051
- }
4052
- this.cache.set(key, value);
4053
- this.accessOrder.set(key, Date.now());
4054
- }
4055
- remove(key) {
4056
- this.cache.delete(key);
4057
- this.accessOrder.delete(key);
4058
- }
4059
- clear() {
4060
- this.cache.clear();
4061
- this.accessOrder.clear();
4062
- }
4063
- /**
4064
- * Clear all entries for a specific namespace
4065
- * Removes both the namespace cache entry and all individual translation keys
4066
- */
4067
- clearNamespace(locale, namespace) {
4068
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
4069
- const keyPrefix = `${locale}:${namespace}:`;
4070
- this.cache.delete(namespaceCacheKey);
4071
- this.accessOrder.delete(namespaceCacheKey);
4072
- for (const key of this.cache.keys()) {
4073
- if (key.startsWith(keyPrefix)) {
4074
- this.cache.delete(key);
4075
- this.accessOrder.delete(key);
4076
- }
4077
- }
4078
- }
4079
- getNamespace(locale, namespace) {
4080
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
4081
- const namespaceData = this.cache.get(namespaceCacheKey);
4082
- if (!namespaceData) return null;
4083
- try {
4084
- return JSON.parse(namespaceData);
4085
- } catch (error) {
4086
- this.cache.delete(namespaceCacheKey);
4087
- return null;
4088
- }
4089
- }
4090
- setNamespace(locale, namespace, translations) {
4091
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
4092
- this.cache.set(namespaceCacheKey, JSON.stringify(translations));
4093
- }
4094
- evictOldest() {
4095
- let oldestKey = "";
4096
- let oldestTime = Date.now();
4097
- for (const [key, time] of this.accessOrder.entries()) {
4098
- if (time < oldestTime) {
4099
- oldestTime = time;
4100
- oldestKey = key;
4101
- }
4102
- }
4103
- if (oldestKey) {
4104
- this.cache.delete(oldestKey);
4105
- this.accessOrder.delete(oldestKey);
4106
- }
4107
- }
4108
- /**
4109
- * Get all cache entries for debugging
4110
- */
4111
- getEntries() {
4112
- const entries = {};
4113
- for (const [key, value] of this.cache.entries()) {
4114
- entries[key] = value;
4115
- }
4116
- return entries;
4117
- }
4118
- getConfig() {
4119
- return this.config;
4120
- }
4121
- };
4122
-
4123
4451
  // src/content/validator/ContentValidator.ts
4124
4452
  var ContentValidator = class {
4125
4453
  /**
@@ -4273,6 +4601,433 @@ var LionRapid = (() => {
4273
4601
  }
4274
4602
  };
4275
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
+
4757
+ // src/browser/md5.ts
4758
+ function safeAdd(x, y) {
4759
+ const lsw = (x & 65535) + (y & 65535);
4760
+ const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
4761
+ return msw << 16 | lsw & 65535;
4762
+ }
4763
+ function bitRotateLeft(num, cnt) {
4764
+ return num << cnt | num >>> 32 - cnt;
4765
+ }
4766
+ function md5cmn(q, a, b, x, s, t) {
4767
+ return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
4768
+ }
4769
+ function md5ff(a, b, c, d, x, s, t) {
4770
+ return md5cmn(b & c | ~b & d, a, b, x, s, t);
4771
+ }
4772
+ function md5gg(a, b, c, d, x, s, t) {
4773
+ return md5cmn(b & d | c & ~d, a, b, x, s, t);
4774
+ }
4775
+ function md5hh(a, b, c, d, x, s, t) {
4776
+ return md5cmn(b ^ c ^ d, a, b, x, s, t);
4777
+ }
4778
+ function md5ii(a, b, c, d, x, s, t) {
4779
+ return md5cmn(c ^ (b | ~d), a, b, x, s, t);
4780
+ }
4781
+ function binlMD5(x, len) {
4782
+ x[len >> 5] |= 128 << len % 32;
4783
+ x[(len + 64 >>> 9 << 4) + 14] = len;
4784
+ let a = 1732584193;
4785
+ let b = -271733879;
4786
+ let c = -1732584194;
4787
+ let d = 271733878;
4788
+ for (let i = 0; i < x.length; i += 16) {
4789
+ const olda = a;
4790
+ const oldb = b;
4791
+ const oldc = c;
4792
+ const oldd = d;
4793
+ a = md5ff(a, b, c, d, x[i], 7, -680876936);
4794
+ d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
4795
+ c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
4796
+ b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
4797
+ a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
4798
+ d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
4799
+ c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
4800
+ b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
4801
+ a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
4802
+ d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
4803
+ c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
4804
+ b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
4805
+ a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
4806
+ d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
4807
+ c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
4808
+ b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
4809
+ a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
4810
+ d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
4811
+ c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
4812
+ b = md5gg(b, c, d, a, x[i], 20, -373897302);
4813
+ a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
4814
+ d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
4815
+ c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
4816
+ b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
4817
+ a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
4818
+ d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
4819
+ c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
4820
+ b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
4821
+ a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
4822
+ d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
4823
+ c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
4824
+ b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
4825
+ a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
4826
+ d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
4827
+ c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
4828
+ b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
4829
+ a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
4830
+ d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
4831
+ c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
4832
+ b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
4833
+ a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
4834
+ d = md5hh(d, a, b, c, x[i + 0], 11, -358537222);
4835
+ c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
4836
+ b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
4837
+ a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
4838
+ d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
4839
+ c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
4840
+ b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
4841
+ a = md5ii(a, b, c, d, x[i], 6, -198630844);
4842
+ d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
4843
+ c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
4844
+ b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
4845
+ a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
4846
+ d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
4847
+ c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
4848
+ b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
4849
+ a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
4850
+ d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
4851
+ c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
4852
+ b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
4853
+ a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
4854
+ d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
4855
+ c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
4856
+ b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
4857
+ a = safeAdd(a, olda);
4858
+ b = safeAdd(b, oldb);
4859
+ c = safeAdd(c, oldc);
4860
+ d = safeAdd(d, oldd);
4861
+ }
4862
+ return [a, b, c, d];
4863
+ }
4864
+ function binl2hex(binarray) {
4865
+ const hexTab = "0123456789abcdef";
4866
+ let str = "";
4867
+ for (let i = 0; i < binarray.length * 4; i++) {
4868
+ str += hexTab.charAt(binarray[i >> 2] >> i % 4 * 8 + 4 & 15) + hexTab.charAt(binarray[i >> 2] >> i % 4 * 8 & 15);
4869
+ }
4870
+ return str;
4871
+ }
4872
+ function utf8Bytes(str) {
4873
+ if (typeof TextEncoder !== "undefined") {
4874
+ return new TextEncoder().encode(str);
4875
+ }
4876
+ const out = [];
4877
+ for (let i = 0; i < str.length; i++) {
4878
+ const c = str.charCodeAt(i);
4879
+ if (c < 128) {
4880
+ out.push(c);
4881
+ } else if (c < 2048) {
4882
+ out.push(192 | c >> 6, 128 | c & 63);
4883
+ } else if (c >= 55296 && c < 56320 && i + 1 < str.length) {
4884
+ const c2 = str.charCodeAt(++i);
4885
+ const cp = 65536 + (c - 55296 << 10) + (c2 - 56320);
4886
+ out.push(
4887
+ 240 | cp >> 18,
4888
+ 128 | cp >> 12 & 63,
4889
+ 128 | cp >> 6 & 63,
4890
+ 128 | cp & 63
4891
+ );
4892
+ } else {
4893
+ out.push(
4894
+ 224 | c >> 12,
4895
+ 128 | c >> 6 & 63,
4896
+ 128 | c & 63
4897
+ );
4898
+ }
4899
+ }
4900
+ return new Uint8Array(out);
4901
+ }
4902
+ function bytes2binl(bytes) {
4903
+ const bin = [];
4904
+ for (let i = 0; i < bytes.length; i++) {
4905
+ bin[i >> 2] |= bytes[i] << i % 4 * 8;
4906
+ }
4907
+ return bin;
4908
+ }
4909
+ function md5(str) {
4910
+ const bytes = utf8Bytes(str);
4911
+ return binl2hex(binlMD5(bytes2binl(bytes), bytes.length * 8));
4912
+ }
4913
+ function generateAutoKey(text) {
4914
+ const trimmed = text.trim();
4915
+ return "auto_" + md5(trimmed).substring(0, 12);
4916
+ }
4917
+
4918
+ // src/browser/exclude-filter.ts
4919
+ var DEFAULT_PATTERNS = {
4920
+ emails: true,
4921
+ urls: true,
4922
+ numbers: true,
4923
+ phones: true
4924
+ };
4925
+ var PATTERNS = {
4926
+ email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
4927
+ url: /^(https?:\/\/|www\.)[^\s]+$/i,
4928
+ number: /^[\d\s.,+-]+$/,
4929
+ phone: /^[+\d][\d\s().-]{6,}$/
4930
+ };
4931
+ var ExcludeFilter = class {
4932
+ constructor(patterns = {}, minTextLength = 2) {
4933
+ __publicField(this, "patterns");
4934
+ __publicField(this, "minTextLength");
4935
+ this.patterns = { ...DEFAULT_PATTERNS, ...patterns };
4936
+ this.minTextLength = minTextLength;
4937
+ }
4938
+ /**
4939
+ * Check if text should be excluded from translation
4940
+ */
4941
+ shouldExclude(text) {
4942
+ const trimmed = text.trim();
4943
+ if (trimmed.length < this.minTextLength) {
4944
+ return true;
4945
+ }
4946
+ if (this.patterns.emails && PATTERNS.email.test(trimmed)) {
4947
+ return true;
4948
+ }
4949
+ if (this.patterns.urls && PATTERNS.url.test(trimmed)) {
4950
+ return true;
4951
+ }
4952
+ if (this.patterns.numbers && PATTERNS.number.test(trimmed)) {
4953
+ return true;
4954
+ }
4955
+ if (this.patterns.phones && PATTERNS.phone.test(trimmed)) {
4956
+ return true;
4957
+ }
4958
+ return false;
4959
+ }
4960
+ };
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
+
4276
5031
  // src/browser/storage/localstorage-repository.ts
4277
5032
  var STORAGE_PREFIX = "lionrapid:";
4278
5033
  var VERSION_KEY = `${STORAGE_PREFIX}version`;