@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
@@ -39,8 +39,10 @@ __export(index_exports, {
39
39
  Colors: () => Colors,
40
40
  ContentValidator: () => ContentValidator,
41
41
  EventEmitter: () => EventEmitter,
42
+ ExcludeFilter: () => ExcludeFilter,
42
43
  HTMLSerializer: () => HTMLSerializer,
43
44
  HandlerChain: () => HandlerChain,
45
+ LOCALE_TAG_REGEX: () => LOCALE_TAG_REGEX,
44
46
  LionRapidBuilder: () => LionRapidBuilder,
45
47
  LionRapidCore: () => LionRapidCore,
46
48
  LocalStorageRepository: () => LocalStorageRepository,
@@ -51,24 +53,44 @@ __export(index_exports, {
51
53
  MissingTranslationQueue: () => MissingTranslationQueue,
52
54
  NetworkError: () => NetworkError,
53
55
  NetworkRepository: () => NetworkRepository,
56
+ SAFE_ATTRS: () => SAFE_ATTRS,
57
+ SAFE_STYLE_PROPS: () => SAFE_STYLE_PROPS,
58
+ SAFE_TAGS: () => SAFE_TAGS,
54
59
  ServerError: () => ServerError,
60
+ StaticRepository: () => StaticRepository,
55
61
  TranslationError: () => TranslationError,
56
62
  TranslationLockedError: () => TranslationLockedError,
57
63
  TranslationNotFoundError: () => TranslationNotFoundError,
58
64
  VERSION: () => VERSION,
59
65
  VOID_TAGS: () => VOID_TAGS,
60
66
  createPackageLogger: () => createPackageLogger,
67
+ escapeHtml: () => escapeHtml,
61
68
  extractTagName: () => extractTagName,
69
+ generateAutoKey: () => generateAutoKey,
62
70
  isClientError: () => isClientError,
63
71
  isRetryableError: () => isRetryableError,
64
72
  isTranslationError: () => isTranslationError,
73
+ localeDir: () => localeDir,
65
74
  logger: () => logger,
75
+ matchLocale: () => matchLocale,
76
+ md5: () => md5,
66
77
  measure: () => measure,
78
+ normalizeLocale: () => normalizeLocale,
79
+ parseAcceptLanguage: () => parseAcceptLanguage,
67
80
  parseKeyPath: () => parseKeyPath,
68
81
  prepareTranslationRequest: () => prepareTranslationRequest,
82
+ primarySubtag: () => primarySubtag,
69
83
  readRawStyle: () => readRawStyle,
84
+ replaceBlockInners: () => replaceBlockInners,
70
85
  resolveNestedKey: () => resolveNestedKey,
71
- serializeOpeningTag: () => serializeOpeningTag
86
+ sanitizeContentUnit: () => sanitizeContentUnit,
87
+ sanitizeElement: () => sanitizeElement,
88
+ sanitizeOpeningTag: () => sanitizeOpeningTag,
89
+ sanitizeStyle: () => sanitizeStyle,
90
+ sanitizeStyleSheetText: () => sanitizeStyleSheetText,
91
+ sanitizeUrl: () => sanitizeUrl,
92
+ serializeOpeningTag: () => serializeOpeningTag,
93
+ splitBlocks: () => splitBlocks
72
94
  });
73
95
  module.exports = __toCommonJS(index_exports);
74
96
 
@@ -419,6 +441,57 @@ var measure = (name) => {
419
441
  };
420
442
  };
421
443
 
444
+ // src/utils/locale.ts
445
+ var LOCALE_TAG_REGEX = /^[a-z]{2,3}(?:-[a-z0-9]{2,4})*$/i;
446
+ var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "fa", "he", "ur", "yi"]);
447
+ function primarySubtag(tag) {
448
+ return tag.split("-")[0].toLowerCase();
449
+ }
450
+ function normalizeLocale(tag) {
451
+ const trimmed = tag.trim();
452
+ if (!trimmed) return trimmed;
453
+ const [lang, ...rest] = trimmed.split("-");
454
+ const out = [lang.toLowerCase()];
455
+ for (const sub of rest) {
456
+ if (sub.length === 4) {
457
+ out.push(sub[0].toUpperCase() + sub.slice(1).toLowerCase());
458
+ } else if (sub.length === 2) {
459
+ out.push(sub.toUpperCase());
460
+ } else {
461
+ out.push(sub.toLowerCase());
462
+ }
463
+ }
464
+ return out.join("-");
465
+ }
466
+ function matchLocale(requested, available, fallback) {
467
+ if (available.length === 0) return fallback;
468
+ const avail = available.map((raw) => ({
469
+ raw,
470
+ lower: normalizeLocale(raw).toLowerCase()
471
+ }));
472
+ for (const req of requested) {
473
+ if (!req) continue;
474
+ const reqLower = normalizeLocale(req).toLowerCase();
475
+ const exact = avail.find((a) => a.lower === reqLower);
476
+ if (exact) return exact.raw;
477
+ const primary = primarySubtag(req);
478
+ const byPrimary = avail.find((a) => a.lower === primary);
479
+ if (byPrimary) return byPrimary.raw;
480
+ }
481
+ return fallback;
482
+ }
483
+ function parseAcceptLanguage(header) {
484
+ return header.split(",").map((part) => {
485
+ const [tag, ...params] = part.trim().split(";");
486
+ const qParam = params.find((p) => p.trim().startsWith("q="));
487
+ const q = qParam ? parseFloat(qParam.trim().slice(2)) : 1;
488
+ return { tag: tag.trim(), q: Number.isNaN(q) ? 0 : q };
489
+ }).filter((x) => x.tag.length > 0 && x.tag !== "*").sort((a, b) => b.q - a.q).map((x) => x.tag);
490
+ }
491
+ function localeDir(tag) {
492
+ return RTL_LANGUAGES.has(primarySubtag(tag)) ? "rtl" : "ltr";
493
+ }
494
+
422
495
  // src/base/event-emitter.ts
423
496
  var EventEmitter = class {
424
497
  constructor() {
@@ -707,7 +780,7 @@ var EventEmitter = class {
707
780
  complete: () => {
708
781
  this.emit("language:change:success", {
709
782
  context: this.createContext(requestId, [...baseTrace, "complete"]),
710
- payload: { oldLocale, newLocale }
783
+ payload: { oldLocale, newLocale, dir: localeDir(newLocale) }
711
784
  });
712
785
  },
713
786
  error: (error) => {
@@ -1117,12 +1190,11 @@ function parseNamespaceKey(key) {
1117
1190
  }
1118
1191
 
1119
1192
  // src/utils/context-utils.ts
1120
- function createTranslationContext(parsedKey, currentLocale, defaultNamespace, fallbackLocale) {
1193
+ function createTranslationContext(parsedKey, currentLocale, defaultNamespace) {
1121
1194
  const effectiveNamespace = parsedKey.hasExplicitNamespace ? parsedKey.namespace : defaultNamespace;
1122
1195
  return {
1123
1196
  locale: currentLocale,
1124
- namespace: effectiveNamespace,
1125
- fallbackLocale: fallbackLocale || currentLocale
1197
+ namespace: effectiveNamespace
1126
1198
  };
1127
1199
  }
1128
1200
 
@@ -1239,7 +1311,7 @@ function flattenTranslations(translations, prefix = "", locale, namespace) {
1239
1311
  }
1240
1312
 
1241
1313
  // src/utils/translation-flow.ts
1242
- function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace, fallbackLocale) {
1314
+ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace) {
1243
1315
  const startTime = performance.now();
1244
1316
  const { defaultValue, params } = parseTranslationParams(
1245
1317
  defaultValueOrParams,
@@ -1249,8 +1321,7 @@ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, par
1249
1321
  const context = createTranslationContext(
1250
1322
  parsedKey,
1251
1323
  currentLocale,
1252
- defaultNamespace,
1253
- fallbackLocale
1324
+ defaultNamespace
1254
1325
  );
1255
1326
  const individualCacheKey = createIndividualCacheKey(
1256
1327
  context.locale,
@@ -1272,7 +1343,6 @@ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, par
1272
1343
  locale: currentLocale,
1273
1344
  namespace: context.namespace,
1274
1345
  // ✅ Use effective namespace (explicit or default)
1275
- fallbackLocale,
1276
1346
  individualCacheKey,
1277
1347
  // ✅ Ready-to-use cache key
1278
1348
  namespaceCacheKey,
@@ -2388,6 +2458,183 @@ var MarkupParser = class {
2388
2458
  }
2389
2459
  };
2390
2460
 
2461
+ // src/content/sanitizer/HTMLSanitizer.ts
2462
+ var SAFE_TAGS = /* @__PURE__ */ new Set([
2463
+ "a",
2464
+ "abbr",
2465
+ "b",
2466
+ "bdi",
2467
+ "bdo",
2468
+ "br",
2469
+ "cite",
2470
+ "code",
2471
+ "del",
2472
+ "dfn",
2473
+ "em",
2474
+ "i",
2475
+ "img",
2476
+ "ins",
2477
+ "kbd",
2478
+ "mark",
2479
+ "q",
2480
+ "s",
2481
+ "samp",
2482
+ "small",
2483
+ "span",
2484
+ "strong",
2485
+ "sub",
2486
+ "sup",
2487
+ "time",
2488
+ "u",
2489
+ "var",
2490
+ "wbr"
2491
+ ]);
2492
+ var SAFE_ATTRS = /* @__PURE__ */ new Set([
2493
+ "alt",
2494
+ "cite",
2495
+ "class",
2496
+ "datetime",
2497
+ "dir",
2498
+ "height",
2499
+ "href",
2500
+ "id",
2501
+ "lang",
2502
+ "loading",
2503
+ "rel",
2504
+ "role",
2505
+ "src",
2506
+ "target",
2507
+ "title",
2508
+ "width"
2509
+ ]);
2510
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "cite"]);
2511
+ var SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
2512
+ "color",
2513
+ "background-color",
2514
+ "font-family",
2515
+ "font-size",
2516
+ "font-style",
2517
+ "font-weight",
2518
+ "letter-spacing",
2519
+ "line-height",
2520
+ "text-decoration",
2521
+ "text-decoration-color",
2522
+ "text-decoration-line",
2523
+ "text-decoration-style",
2524
+ "text-transform",
2525
+ "vertical-align",
2526
+ "white-space",
2527
+ "word-spacing"
2528
+ ]);
2529
+ var DANGEROUS_CSS_VALUE = /url\s*\(|expression\s*\(|image-set\s*\(|element\s*\(|paint\s*\(|javascript:|\\|[<>{}@]/i;
2530
+ var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
2531
+ var NAMED_ENTITIES = {
2532
+ amp: "&",
2533
+ lt: "<",
2534
+ gt: ">",
2535
+ quot: '"',
2536
+ apos: "'",
2537
+ nbsp: "\xA0",
2538
+ colon: ":",
2539
+ semi: ";",
2540
+ sol: "/",
2541
+ bsol: "\\",
2542
+ Tab: " ",
2543
+ NewLine: "\n"
2544
+ };
2545
+ function decodeEntities2(value) {
2546
+ return value.replace(
2547
+ /&(?:#[xX]([0-9a-fA-F]+);?|#(\d+);?|([a-zA-Z]+);)/g,
2548
+ (full, hex, dec, name) => {
2549
+ if (hex) return String.fromCodePoint(parseInt(hex, 16));
2550
+ if (dec) return String.fromCodePoint(parseInt(dec, 10));
2551
+ return NAMED_ENTITIES[name] ?? full;
2552
+ }
2553
+ );
2554
+ }
2555
+ function escapeHtml(text) {
2556
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2557
+ }
2558
+ function sanitizeUrl(value) {
2559
+ const decoded = decodeEntities2(value);
2560
+ const probe = decoded.replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase();
2561
+ const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe);
2562
+ if (!scheme) return value;
2563
+ return SAFE_SCHEMES.has(scheme[1]) ? value : null;
2564
+ }
2565
+ function sanitizeStyle(value) {
2566
+ return filterCssDeclarations(value, true);
2567
+ }
2568
+ function sanitizeCssDeclarations(value) {
2569
+ return filterCssDeclarations(value, false);
2570
+ }
2571
+ function filterCssDeclarations(value, requirePropAllowlist) {
2572
+ const decoded = decodeEntities2(value);
2573
+ const kept = [];
2574
+ for (const declaration of decoded.split(";")) {
2575
+ const colon = declaration.indexOf(":");
2576
+ if (colon === -1) continue;
2577
+ const prop = declaration.slice(0, colon).trim().toLowerCase();
2578
+ const val = declaration.slice(colon + 1).trim();
2579
+ if (!/^[a-z-]+$/.test(prop)) continue;
2580
+ if (requirePropAllowlist && !SAFE_STYLE_PROPS.has(prop)) continue;
2581
+ if (val === "" || DANGEROUS_CSS_VALUE.test(val)) continue;
2582
+ kept.push(`${prop}: ${val}`);
2583
+ }
2584
+ return kept.length > 0 ? kept.join("; ") : null;
2585
+ }
2586
+ var ATTR_PATTERN = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
2587
+ function sanitizeOpeningTag(rawOpen) {
2588
+ if (!rawOpen.startsWith("<")) return null;
2589
+ const tagName = extractTagName(rawOpen);
2590
+ if (!/^[a-z][a-z0-9-]*$/.test(tagName) || !SAFE_TAGS.has(tagName)) {
2591
+ return null;
2592
+ }
2593
+ const end = rawOpen.endsWith("/>") ? rawOpen.length - 2 : rawOpen.endsWith(">") ? rawOpen.length - 1 : rawOpen.length;
2594
+ const attrSource = rawOpen.slice(1 + tagName.length, end);
2595
+ const attrs = {};
2596
+ for (const match of attrSource.matchAll(ATTR_PATTERN)) {
2597
+ const name = match[1].toLowerCase();
2598
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
2599
+ if (name in attrs) continue;
2600
+ if (name === "style") {
2601
+ const cleanStyle = sanitizeStyle(value);
2602
+ if (cleanStyle !== null) attrs[name] = cleanStyle;
2603
+ continue;
2604
+ }
2605
+ const allowed = SAFE_ATTRS.has(name) || /^data-[a-z0-9-]*$/.test(name) || /^aria-[a-z0-9-]*$/.test(name);
2606
+ if (!allowed) continue;
2607
+ if (URL_ATTRS.has(name)) {
2608
+ if (sanitizeUrl(value) === null) continue;
2609
+ }
2610
+ attrs[name] = value;
2611
+ }
2612
+ return serializeOpeningTag(tagName, attrs);
2613
+ }
2614
+ function sanitizeContentUnit(unit) {
2615
+ let changed = false;
2616
+ const styles = {};
2617
+ for (const [index, styleList] of Object.entries(unit.styles)) {
2618
+ const raw = readRawStyle(styleList);
2619
+ if (raw === null) {
2620
+ styles[index] = styleList;
2621
+ continue;
2622
+ }
2623
+ const safe = sanitizeOpeningTag(raw);
2624
+ if (safe === null) {
2625
+ changed = true;
2626
+ const rest = styleList.filter((s) => !s.startsWith("raw:"));
2627
+ if (rest.length > 0) styles[index] = rest;
2628
+ } else if (safe === raw) {
2629
+ styles[index] = styleList;
2630
+ } else {
2631
+ changed = true;
2632
+ styles[index] = styleList.map((s) => s.startsWith("raw:") ? `raw:${safe}` : s);
2633
+ }
2634
+ }
2635
+ return changed ? { text: unit.text, styles } : unit;
2636
+ }
2637
+
2391
2638
  // src/content/serializer/HTMLSerializer.ts
2392
2639
  var HTMLSerializer = class {
2393
2640
  serialize(unit) {
@@ -2397,24 +2644,25 @@ var HTMLSerializer = class {
2397
2644
  let lastIndex = 0;
2398
2645
  let match;
2399
2646
  while ((match = tagPattern.exec(text)) !== null) {
2400
- result += text.slice(lastIndex, match.index);
2647
+ result += escapeHtml(text.slice(lastIndex, match.index));
2401
2648
  const [fullMatch, closingSlash, indexStr] = match;
2402
2649
  const styleList = styles[indexStr] ?? [];
2403
2650
  const rawOpen = readRawStyle(styleList);
2404
- if (rawOpen) {
2651
+ const safeOpen = rawOpen === null ? null : sanitizeOpeningTag(rawOpen);
2652
+ if (rawOpen && safeOpen !== null) {
2405
2653
  const tagName = extractTagName(rawOpen);
2406
2654
  const isVoid = VOID_TAGS.has(tagName);
2407
2655
  if (closingSlash) {
2408
2656
  if (!isVoid) result += `</${tagName}>`;
2409
2657
  } else {
2410
- result += rawOpen;
2658
+ result += safeOpen;
2411
2659
  }
2412
2660
  } else {
2413
2661
  result += fullMatch;
2414
2662
  }
2415
2663
  lastIndex = match.index + fullMatch.length;
2416
2664
  }
2417
- result += text.slice(lastIndex);
2665
+ result += escapeHtml(text.slice(lastIndex));
2418
2666
  return result;
2419
2667
  }
2420
2668
  };
@@ -2653,10 +2901,9 @@ var CacheManager = class {
2653
2901
  var INDEXED_TAG_REGEX = /<\/?[0-9]+\/?>/;
2654
2902
  var HTML_TAG_REGEX = /<[a-z][a-z0-9]*(?:\s+[^>]*)?\/?>/i;
2655
2903
  var LionRapidCore = class {
2656
- constructor(eventEmitter, handlerChain, defaultLocale = "en", fallbackLocale = "en", defaultNamespace) {
2904
+ constructor(eventEmitter, handlerChain, defaultLocale = "en", defaultNamespace) {
2657
2905
  __publicField(this, "handlerChain");
2658
2906
  __publicField(this, "currentLocale");
2659
- __publicField(this, "fallbackLocale");
2660
2907
  __publicField(this, "defaultNamespace");
2661
2908
  __publicField(this, "pendingTranslations", /* @__PURE__ */ new Map());
2662
2909
  __publicField(this, "bus");
@@ -2674,7 +2921,6 @@ var LionRapidCore = class {
2674
2921
  this.bus = eventEmitter;
2675
2922
  this.handlerChain = handlerChain;
2676
2923
  this.currentLocale = defaultLocale;
2677
- this.fallbackLocale = fallbackLocale;
2678
2924
  this.defaultNamespace = defaultNamespace;
2679
2925
  this.cache = new CacheManager(
2680
2926
  this.handlerChain,
@@ -2739,8 +2985,7 @@ var LionRapidCore = class {
2739
2985
  this.currentLocale,
2740
2986
  firstArg,
2741
2987
  secondArg,
2742
- this.defaultNamespace,
2743
- this.fallbackLocale
2988
+ this.defaultNamespace
2744
2989
  );
2745
2990
  logger.debug(`Translation requested: ${request.cleanKey}`);
2746
2991
  const translateBus = this.bus.translation(request);
@@ -2790,8 +3035,7 @@ var LionRapidCore = class {
2790
3035
  this.currentLocale,
2791
3036
  firstArg,
2792
3037
  secondArg,
2793
- this.defaultNamespace,
2794
- this.fallbackLocale
3038
+ this.defaultNamespace
2795
3039
  );
2796
3040
  logger.debug(`HTML translation requested: ${request.cleanKey}`);
2797
3041
  const translateBus = this.bus.translation(request);
@@ -2999,8 +3243,7 @@ var LionRapidCore = class {
2999
3243
  this.currentLocale,
3000
3244
  params,
3001
3245
  defaultValue,
3002
- this.defaultNamespace,
3003
- this.fallbackLocale
3246
+ this.defaultNamespace
3004
3247
  );
3005
3248
  const syncResult = this.handlerChain.handleSync(request);
3006
3249
  const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
@@ -3121,8 +3364,7 @@ var LionRapidCore = class {
3121
3364
  this.currentLocale,
3122
3365
  void 0,
3123
3366
  void 0,
3124
- this.defaultNamespace,
3125
- this.fallbackLocale
3367
+ this.defaultNamespace
3126
3368
  );
3127
3369
  return this.handlerChain.isCached(request);
3128
3370
  }
@@ -3232,6 +3474,155 @@ var LionRapidCore = class {
3232
3474
  }
3233
3475
  };
3234
3476
 
3477
+ // src/repositories/memory-repository.ts
3478
+ var MemoryRepository = class {
3479
+ constructor(config) {
3480
+ __publicField(this, "name", "MemoryRepository");
3481
+ __publicField(this, "type", "memory");
3482
+ __publicField(this, "config", {});
3483
+ __publicField(this, "cache", /* @__PURE__ */ new Map());
3484
+ __publicField(this, "accessOrder", /* @__PURE__ */ new Map());
3485
+ __publicField(this, "maxSize", 1e3);
3486
+ this.config = config;
3487
+ this.maxSize = this.config.options?.maxSize || 1e3;
3488
+ }
3489
+ get(key) {
3490
+ const value = this.cache.get(key);
3491
+ if (value !== void 0) {
3492
+ this.accessOrder.set(key, Date.now());
3493
+ return value;
3494
+ }
3495
+ return null;
3496
+ }
3497
+ set(key, value) {
3498
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
3499
+ this.evictOldest();
3500
+ }
3501
+ this.cache.set(key, value);
3502
+ this.accessOrder.set(key, Date.now());
3503
+ }
3504
+ remove(key) {
3505
+ this.cache.delete(key);
3506
+ this.accessOrder.delete(key);
3507
+ }
3508
+ clear() {
3509
+ this.cache.clear();
3510
+ this.accessOrder.clear();
3511
+ }
3512
+ /**
3513
+ * Clear all entries for a specific namespace
3514
+ * Removes both the namespace cache entry and all individual translation keys
3515
+ */
3516
+ clearNamespace(locale, namespace) {
3517
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3518
+ const keyPrefix = `${locale}:${namespace}:`;
3519
+ this.cache.delete(namespaceCacheKey);
3520
+ this.accessOrder.delete(namespaceCacheKey);
3521
+ for (const key of this.cache.keys()) {
3522
+ if (key.startsWith(keyPrefix)) {
3523
+ this.cache.delete(key);
3524
+ this.accessOrder.delete(key);
3525
+ }
3526
+ }
3527
+ }
3528
+ getNamespace(locale, namespace) {
3529
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3530
+ const namespaceData = this.cache.get(namespaceCacheKey);
3531
+ if (!namespaceData) return null;
3532
+ try {
3533
+ return JSON.parse(namespaceData);
3534
+ } catch (error) {
3535
+ this.cache.delete(namespaceCacheKey);
3536
+ return null;
3537
+ }
3538
+ }
3539
+ setNamespace(locale, namespace, translations) {
3540
+ const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
3541
+ this.cache.set(namespaceCacheKey, JSON.stringify(translations));
3542
+ }
3543
+ evictOldest() {
3544
+ let oldestKey = "";
3545
+ let oldestTime = Date.now();
3546
+ for (const [key, time] of this.accessOrder.entries()) {
3547
+ if (time < oldestTime) {
3548
+ oldestTime = time;
3549
+ oldestKey = key;
3550
+ }
3551
+ }
3552
+ if (oldestKey) {
3553
+ this.cache.delete(oldestKey);
3554
+ this.accessOrder.delete(oldestKey);
3555
+ }
3556
+ }
3557
+ /**
3558
+ * Get all cache entries for debugging
3559
+ */
3560
+ getEntries() {
3561
+ const entries = {};
3562
+ for (const [key, value] of this.cache.entries()) {
3563
+ entries[key] = value;
3564
+ }
3565
+ return entries;
3566
+ }
3567
+ getConfig() {
3568
+ return this.config;
3569
+ }
3570
+ };
3571
+
3572
+ // src/repositories/static-repository.ts
3573
+ var StaticRepository = class {
3574
+ constructor(bundle, config = {}) {
3575
+ __publicField(this, "name", "StaticRepository");
3576
+ __publicField(this, "type", "memory");
3577
+ __publicField(this, "config");
3578
+ __publicField(this, "mem");
3579
+ this.config = {
3580
+ enabled: config.enabled ?? true,
3581
+ priority: config.priority ?? 0.5,
3582
+ // after the live memory (0), before storage
3583
+ options: {
3584
+ ttl: Number.MAX_SAFE_INTEGER,
3585
+ maxSize: Number.MAX_SAFE_INTEGER,
3586
+ ...config.options
3587
+ }
3588
+ };
3589
+ this.mem = new MemoryRepository({ options: { maxSize: Number.MAX_SAFE_INTEGER } });
3590
+ for (const [locale, namespaces] of Object.entries(bundle)) {
3591
+ for (const [namespace, translations] of Object.entries(namespaces)) {
3592
+ this.mem.setNamespace(locale, namespace, translations);
3593
+ const flat = flattenTranslations(
3594
+ translations,
3595
+ "",
3596
+ locale,
3597
+ namespace
3598
+ );
3599
+ for (const [key, value] of Object.entries(flat)) this.mem.set(key, value);
3600
+ }
3601
+ }
3602
+ }
3603
+ get(key) {
3604
+ return this.mem.get(key);
3605
+ }
3606
+ getNamespace(locale, namespace) {
3607
+ return this.mem.getNamespace(locale, namespace);
3608
+ }
3609
+ getEntries() {
3610
+ return this.mem.getEntries();
3611
+ }
3612
+ getConfig() {
3613
+ return this.config;
3614
+ }
3615
+ // Read-only: the live memory layer owns fresh network data; this stays pristine.
3616
+ set() {
3617
+ }
3618
+ remove() {
3619
+ }
3620
+ clear() {
3621
+ }
3622
+ setNamespace() {
3623
+ }
3624
+ };
3625
+
3235
3626
  // src/sync/missing-translation-queue.ts
3236
3627
  var COOLDOWN_MS = 6e4;
3237
3628
  var MissingTranslationQueue = class {
@@ -3394,6 +3785,9 @@ var MissingTranslationQueue = class {
3394
3785
  };
3395
3786
 
3396
3787
  // src/builder.ts
3788
+ function isLoudPreloadFailure(error) {
3789
+ return !(error instanceof TranslationNotFoundError || error instanceof TranslationLockedError);
3790
+ }
3397
3791
  var LionRapidBuilder = class _LionRapidBuilder {
3398
3792
  constructor(config) {
3399
3793
  __publicField(this, "config");
@@ -3404,7 +3798,6 @@ var LionRapidBuilder = class _LionRapidBuilder {
3404
3798
  __publicField(this, "syncQueue", null);
3405
3799
  this.config = {
3406
3800
  defaultLocale: config.defaultLocale || "en",
3407
- fallbackLocale: config.fallbackLocale || "en",
3408
3801
  namespace: config.namespace || "app"
3409
3802
  };
3410
3803
  }
@@ -3440,6 +3833,15 @@ var LionRapidBuilder = class _LionRapidBuilder {
3440
3833
  }
3441
3834
  return this;
3442
3835
  }
3836
+ /**
3837
+ * Offline resilience: seed a translation bundle (or the CLI's snapshot files)
3838
+ * as a read-only sync layer that resolves synchronously even with the server
3839
+ * down and nothing cached. Sits after the live memory layer (priority 0) at
3840
+ * `priority` (default 0.5), so fresh network data always shadows it.
3841
+ */
3842
+ useStaticBundle(bundle, priority = 0.5) {
3843
+ return this.use(new StaticRepository(bundle, { priority }));
3844
+ }
3443
3845
  async build() {
3444
3846
  const eventEmitter = new EventEmitter();
3445
3847
  const buildBus = eventEmitter.builder();
@@ -3496,7 +3898,6 @@ var LionRapidBuilder = class _LionRapidBuilder {
3496
3898
  eventEmitter,
3497
3899
  handlerChain,
3498
3900
  this.config.defaultLocale,
3499
- this.config.fallbackLocale,
3500
3901
  this.config.namespace
3501
3902
  );
3502
3903
  for (const plugin of this.plugins) {
@@ -3527,7 +3928,17 @@ var LionRapidBuilder = class _LionRapidBuilder {
3527
3928
  logger.debug("Preload targets", {
3528
3929
  namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
3529
3930
  });
3931
+ let warnedLoudly = false;
3932
+ const warnLoudlyOnce = (namespaceKey, error) => {
3933
+ if (!isLoudPreloadFailure(error) || warnedLoudly) return;
3934
+ warnedLoudly = true;
3935
+ console.warn(
3936
+ `[LionRapid] Could not load translations for ${namespaceKey} \u2014 the page stays untranslated until the server is reachable.`,
3937
+ error
3938
+ );
3939
+ };
3530
3940
  const promises = eagerPreloads.map(async (preloadConfig) => {
3941
+ const namespaceKey = `${preloadConfig.locale}:${preloadConfig.namespace}`;
3531
3942
  try {
3532
3943
  const result = await networkHandler.loadNamespace(
3533
3944
  preloadConfig.locale,
@@ -3536,19 +3947,19 @@ var LionRapidBuilder = class _LionRapidBuilder {
3536
3947
  if (result && result.data && result.status === 200) {
3537
3948
  const context = {
3538
3949
  locale: preloadConfig.locale,
3539
- namespace: preloadConfig.namespace,
3540
- fallbackLocale: this.config.fallbackLocale
3950
+ namespace: preloadConfig.namespace
3541
3951
  };
3542
3952
  await handlerChain.saveNamespaceToHandlers(result.data, context);
3543
- logger.debug(
3544
- `Preloaded: ${preloadConfig.locale}:${preloadConfig.namespace}`
3545
- );
3953
+ logger.debug(`Preloaded: ${namespaceKey}`);
3954
+ return;
3955
+ }
3956
+ if (result?.error) {
3957
+ warnLoudlyOnce(namespaceKey, result.error);
3958
+ logger.warn(`Preload failed: ${namespaceKey}`, result.error);
3546
3959
  }
3547
3960
  } catch (error) {
3548
- logger.warn(
3549
- `Preload failed: ${preloadConfig.locale}:${preloadConfig.namespace}`,
3550
- error
3551
- );
3961
+ warnLoudlyOnce(namespaceKey, error);
3962
+ logger.warn(`Preload failed: ${namespaceKey}`, error);
3552
3963
  }
3553
3964
  });
3554
3965
  await Promise.allSettled(promises);
@@ -3562,8 +3973,16 @@ var _NetworkRepository = class _NetworkRepository {
3562
3973
  __publicField(this, "name", "NetworkRepository");
3563
3974
  __publicField(this, "type", "network");
3564
3975
  __publicField(this, "config", {});
3565
- /** Cached version (opaque string from server) per namespace key "locale:namespace" */
3566
- __publicField(this, "namespaceVersions", /* @__PURE__ */ new Map());
3976
+ /**
3977
+ * Namespace snapshot per key "locale:namespace": the server version TOGETHER
3978
+ * with the payload it describes, persisted as ONE localStorage entry.
3979
+ *
3980
+ * Canonical 304 contract (same as edge KV bundle + WP transient cache):
3981
+ * never claim freshness you can't back. If-None-Match is only sent when the
3982
+ * snapshot is in hand; a 304 answers with that snapshot's payload; version
3983
+ * and payload can never desync because they live and die together.
3984
+ */
3985
+ __publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
3567
3986
  /**
3568
3987
  * Default endpoint templates with placeholder support
3569
3988
  * Placeholders: :key, :locale, :namespace
@@ -3676,36 +4095,50 @@ var _NetworkRepository = class _NetworkRepository {
3676
4095
  logger.debug("API base URL is not set");
3677
4096
  throw new NetworkError("API base URL is not set");
3678
4097
  }
4098
+ try {
4099
+ return await this.fetchNamespaceRaw(locale, namespace);
4100
+ } catch (error) {
4101
+ this.handleCaughtError(error);
4102
+ }
4103
+ }
4104
+ /**
4105
+ * Shared namespace fetch with the canonical 304 contract:
4106
+ * - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
4107
+ * - 304 answers with that snapshot's payload — never with "nothing".
4108
+ * - A 304 with no snapshot (server/proxy surprise) refetches unconditionally.
4109
+ * - A 200 stores version + payload atomically before either is trusted.
4110
+ */
4111
+ async fetchNamespaceRaw(locale, namespace, allowConditional = true) {
3679
4112
  const url = this.buildUrl("getNamespace", { locale, namespace });
3680
4113
  logger.debug(`API GET Namespace: ${url}`);
3681
4114
  const namespaceKey = `${locale}:${namespace}`;
3682
- try {
3683
- const extraHeaders = {};
3684
- const cachedVersion = this.getVersion(namespaceKey);
3685
- if (cachedVersion) {
3686
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3687
- }
3688
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3689
- if (response.status === 304) {
3690
- logger.debug(`Namespace ${namespaceKey}: 304 Not Modified`);
3691
- return null;
3692
- }
3693
- if (!response.ok) {
3694
- this.handleHttpError(response, namespaceKey);
3695
- }
3696
- const data = await response.json();
3697
- logger.debug("API response data:", data);
3698
- if (!data.translations) {
3699
- throw new TranslationNotFoundError(namespaceKey);
3700
- }
3701
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3702
- if (version) {
3703
- this.setVersion(namespaceKey, version);
4115
+ const snapshot = allowConditional ? this.getSnapshot(namespaceKey) : void 0;
4116
+ const extraHeaders = {};
4117
+ if (snapshot) {
4118
+ extraHeaders["If-None-Match"] = `W/"${snapshot.version}"`;
4119
+ }
4120
+ const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
4121
+ if (response.status === 304) {
4122
+ if (snapshot) {
4123
+ logger.debug(`Namespace ${namespaceKey}: 304 Not Modified (snapshot served)`);
4124
+ return snapshot.translations;
3704
4125
  }
3705
- return data.translations;
3706
- } catch (error) {
3707
- this.handleCaughtError(error);
4126
+ logger.debug(`Namespace ${namespaceKey}: unexpected 304 with no snapshot \u2014 refetching`);
4127
+ return this.fetchNamespaceRaw(locale, namespace, false);
4128
+ }
4129
+ if (!response.ok) {
4130
+ this.handleHttpError(response, namespaceKey);
3708
4131
  }
4132
+ const data = await response.json();
4133
+ logger.debug("API response data:", data);
4134
+ if (!data.translations) {
4135
+ throw new TranslationNotFoundError(namespaceKey);
4136
+ }
4137
+ const version = data.meta?.version != null ? String(data.meta.version) : null;
4138
+ if (version) {
4139
+ this.setSnapshot(namespaceKey, { version, translations: data.translations });
4140
+ }
4141
+ return data.translations;
3709
4142
  }
3710
4143
  /**
3711
4144
  * Save translation to API (if supported)
@@ -3790,33 +4223,8 @@ var _NetworkRepository = class _NetworkRepository {
3790
4223
  logger.debug("API base URL is not set");
3791
4224
  throw new NetworkError("API base URL is not set");
3792
4225
  }
3793
- const url = this.buildUrl("getNamespace", { locale, namespace });
3794
- logger.debug(`API GET Namespace ContentUnits: ${url}`);
3795
- const namespaceKey = `${locale}:${namespace}`;
3796
4226
  try {
3797
- const extraHeaders = {};
3798
- const cachedVersion = this.getVersion(namespaceKey);
3799
- if (cachedVersion) {
3800
- extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
3801
- }
3802
- const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
3803
- if (response.status === 304) {
3804
- logger.debug(`Namespace ContentUnits ${namespaceKey}: 304 Not Modified`);
3805
- return null;
3806
- }
3807
- if (!response.ok) {
3808
- this.handleHttpError(response, namespaceKey);
3809
- }
3810
- const data = await response.json();
3811
- logger.debug("API response data:", data);
3812
- if (!data.translations) {
3813
- throw new TranslationNotFoundError(namespaceKey);
3814
- }
3815
- const version = data.meta?.version != null ? String(data.meta.version) : null;
3816
- if (version) {
3817
- this.setVersion(namespaceKey, version);
3818
- }
3819
- return data.translations;
4227
+ return await this.fetchNamespaceRaw(locale, namespace);
3820
4228
  } catch (error) {
3821
4229
  this.handleCaughtError(error);
3822
4230
  }
@@ -3926,25 +4334,37 @@ var _NetworkRepository = class _NetworkRepository {
3926
4334
  getConfig() {
3927
4335
  return this.config;
3928
4336
  }
3929
- /** Read persisted version from localStorage, falling back to in-memory map */
3930
- getVersion(namespaceKey) {
3931
- const mem = this.namespaceVersions.get(namespaceKey);
4337
+ /** Read a persisted snapshot (version + payload), falling back to memory.
4338
+ * Legacy bare-version entries and corrupt snapshots are deleted on sight —
4339
+ * a version without its payload must never drive a conditional request. */
4340
+ getSnapshot(namespaceKey) {
4341
+ const mem = this.namespaceSnapshots.get(namespaceKey);
3932
4342
  if (mem) return mem;
3933
4343
  try {
3934
- const stored = localStorage.getItem(_NetworkRepository.VERSION_PREFIX + namespaceKey);
3935
- if (stored) {
3936
- this.namespaceVersions.set(namespaceKey, stored);
3937
- return stored;
4344
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
4345
+ const raw = localStorage.getItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
4346
+ if (raw) {
4347
+ const parsed = JSON.parse(raw);
4348
+ if (parsed && typeof parsed.version === "string" && parsed.version.length > 0 && parsed.translations && typeof parsed.translations === "object") {
4349
+ this.namespaceSnapshots.set(namespaceKey, parsed);
4350
+ return parsed;
4351
+ }
4352
+ localStorage.removeItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
3938
4353
  }
3939
4354
  } catch {
3940
4355
  }
3941
4356
  return void 0;
3942
4357
  }
3943
- /** Persist version to both in-memory map and localStorage */
3944
- setVersion(namespaceKey, version) {
3945
- this.namespaceVersions.set(namespaceKey, version);
4358
+ /** Persist version + payload as ONE entry (memory + localStorage). On quota
4359
+ * failure the previous stored snapshot stays — still a consistent pair. */
4360
+ setSnapshot(namespaceKey, snapshot) {
4361
+ this.namespaceSnapshots.set(namespaceKey, snapshot);
3946
4362
  try {
3947
- localStorage.setItem(_NetworkRepository.VERSION_PREFIX + namespaceKey, version);
4363
+ localStorage.setItem(
4364
+ _NetworkRepository.SNAPSHOT_PREFIX + namespaceKey,
4365
+ JSON.stringify(snapshot)
4366
+ );
4367
+ localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
3948
4368
  } catch {
3949
4369
  }
3950
4370
  }
@@ -4033,105 +4453,13 @@ var _NetworkRepository = class _NetworkRepository {
4033
4453
  }
4034
4454
  }
4035
4455
  };
4036
- /** localStorage key prefix for persisted versions */
4037
- __publicField(_NetworkRepository, "VERSION_PREFIX", "lionrapid:v:");
4456
+ /** localStorage key prefix for persisted snapshots (version + payload) */
4457
+ __publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
4458
+ /** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
4459
+ * desync the snapshot store exists to prevent; entries are deleted on sight. */
4460
+ __publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
4038
4461
  var NetworkRepository = _NetworkRepository;
4039
4462
 
4040
- // src/repositories/memory-repository.ts
4041
- var MemoryRepository = class {
4042
- constructor(config) {
4043
- __publicField(this, "name", "MemoryRepository");
4044
- __publicField(this, "type", "memory");
4045
- __publicField(this, "config", {});
4046
- __publicField(this, "cache", /* @__PURE__ */ new Map());
4047
- __publicField(this, "accessOrder", /* @__PURE__ */ new Map());
4048
- __publicField(this, "maxSize", 1e3);
4049
- this.config = config;
4050
- this.maxSize = this.config.options?.maxSize || 1e3;
4051
- }
4052
- get(key) {
4053
- const value = this.cache.get(key);
4054
- if (value !== void 0) {
4055
- this.accessOrder.set(key, Date.now());
4056
- return value;
4057
- }
4058
- return null;
4059
- }
4060
- set(key, value) {
4061
- if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
4062
- this.evictOldest();
4063
- }
4064
- this.cache.set(key, value);
4065
- this.accessOrder.set(key, Date.now());
4066
- }
4067
- remove(key) {
4068
- this.cache.delete(key);
4069
- this.accessOrder.delete(key);
4070
- }
4071
- clear() {
4072
- this.cache.clear();
4073
- this.accessOrder.clear();
4074
- }
4075
- /**
4076
- * Clear all entries for a specific namespace
4077
- * Removes both the namespace cache entry and all individual translation keys
4078
- */
4079
- clearNamespace(locale, namespace) {
4080
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
4081
- const keyPrefix = `${locale}:${namespace}:`;
4082
- this.cache.delete(namespaceCacheKey);
4083
- this.accessOrder.delete(namespaceCacheKey);
4084
- for (const key of this.cache.keys()) {
4085
- if (key.startsWith(keyPrefix)) {
4086
- this.cache.delete(key);
4087
- this.accessOrder.delete(key);
4088
- }
4089
- }
4090
- }
4091
- getNamespace(locale, namespace) {
4092
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
4093
- const namespaceData = this.cache.get(namespaceCacheKey);
4094
- if (!namespaceData) return null;
4095
- try {
4096
- return JSON.parse(namespaceData);
4097
- } catch (error) {
4098
- this.cache.delete(namespaceCacheKey);
4099
- return null;
4100
- }
4101
- }
4102
- setNamespace(locale, namespace, translations) {
4103
- const namespaceCacheKey = createNamespaceCacheKey(locale, namespace);
4104
- this.cache.set(namespaceCacheKey, JSON.stringify(translations));
4105
- }
4106
- evictOldest() {
4107
- let oldestKey = "";
4108
- let oldestTime = Date.now();
4109
- for (const [key, time] of this.accessOrder.entries()) {
4110
- if (time < oldestTime) {
4111
- oldestTime = time;
4112
- oldestKey = key;
4113
- }
4114
- }
4115
- if (oldestKey) {
4116
- this.cache.delete(oldestKey);
4117
- this.accessOrder.delete(oldestKey);
4118
- }
4119
- }
4120
- /**
4121
- * Get all cache entries for debugging
4122
- */
4123
- getEntries() {
4124
- const entries = {};
4125
- for (const [key, value] of this.cache.entries()) {
4126
- entries[key] = value;
4127
- }
4128
- return entries;
4129
- }
4130
- getConfig() {
4131
- return this.config;
4132
- }
4133
- };
4134
-
4135
4463
  // src/content/validator/ContentValidator.ts
4136
4464
  var ContentValidator = class {
4137
4465
  /**
@@ -4285,6 +4613,433 @@ var ContentValidator = class {
4285
4613
  }
4286
4614
  };
4287
4615
 
4616
+ // src/content/splitter/BlockSplitter.ts
4617
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
4618
+ "title",
4619
+ "p",
4620
+ "h1",
4621
+ "h2",
4622
+ "h3",
4623
+ "h4",
4624
+ "h5",
4625
+ "h6",
4626
+ "li",
4627
+ "td",
4628
+ "th",
4629
+ "label",
4630
+ "button",
4631
+ "figcaption",
4632
+ "dt",
4633
+ "dd",
4634
+ "summary",
4635
+ "caption"
4636
+ ]);
4637
+ var INLINE_BLOCK_TAGS = /* @__PURE__ */ new Set(["a"]);
4638
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
4639
+ "script",
4640
+ "style",
4641
+ "code",
4642
+ "pre",
4643
+ "textarea",
4644
+ "noscript",
4645
+ "svg"
4646
+ ]);
4647
+ var TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*)\s*(\/?)>/g;
4648
+ function hasNoTranslateAttr(attrsString) {
4649
+ if (attrsString.includes("data-no-translate")) return true;
4650
+ return /translate\s*=\s*["']no["']/.test(attrsString);
4651
+ }
4652
+ function containsBlockTag(innerHTML) {
4653
+ TAG_REGEX.lastIndex = 0;
4654
+ let match;
4655
+ while ((match = TAG_REGEX.exec(innerHTML)) !== null) {
4656
+ const [, closingSlash, tagName, , selfClosingSlash] = match;
4657
+ if (!closingSlash && !selfClosingSlash && BLOCK_TAGS.has(tagName.toLowerCase())) {
4658
+ return true;
4659
+ }
4660
+ }
4661
+ return false;
4662
+ }
4663
+ function findCloseTag(html, pos, tagName) {
4664
+ const lowerTag = tagName.toLowerCase();
4665
+ let depth = 1;
4666
+ TAG_REGEX.lastIndex = pos;
4667
+ let match;
4668
+ while ((match = TAG_REGEX.exec(html)) !== null) {
4669
+ const [, closingSlash, matchedTag, , selfClosingSlash] = match;
4670
+ if (matchedTag.toLowerCase() !== lowerTag) continue;
4671
+ if (closingSlash) {
4672
+ depth--;
4673
+ if (depth === 0) return match.index;
4674
+ } else if (!selfClosingSlash) {
4675
+ depth++;
4676
+ }
4677
+ }
4678
+ return -1;
4679
+ }
4680
+ function splitBlocks(html) {
4681
+ const blocks = [];
4682
+ let pos = 0;
4683
+ let skipDepth = 0;
4684
+ let iter = 0;
4685
+ const maxIter = 1e5;
4686
+ const noTranslateEnds = [];
4687
+ while (pos < html.length && iter < maxIter) {
4688
+ iter++;
4689
+ const ltPos = html.indexOf("<", pos);
4690
+ if (ltPos === -1) break;
4691
+ if (html.startsWith("<!--", ltPos)) {
4692
+ const commentEnd = html.indexOf("-->", ltPos + 4);
4693
+ if (commentEnd === -1) break;
4694
+ pos = commentEnd + 3;
4695
+ continue;
4696
+ }
4697
+ TAG_REGEX.lastIndex = ltPos;
4698
+ const match = TAG_REGEX.exec(html);
4699
+ if (!match || match.index !== ltPos) {
4700
+ pos = ltPos + 1;
4701
+ continue;
4702
+ }
4703
+ const [fullMatch, closingSlash, tagName, attrsString, selfClosingSlash] = match;
4704
+ const tag = tagName.toLowerCase();
4705
+ const afterMatch = match.index + fullMatch.length;
4706
+ if (SKIP_TAGS.has(tag)) {
4707
+ if (closingSlash) {
4708
+ if (skipDepth > 0) skipDepth--;
4709
+ } else if (!selfClosingSlash) {
4710
+ skipDepth++;
4711
+ }
4712
+ pos = afterMatch;
4713
+ continue;
4714
+ }
4715
+ if (skipDepth > 0) {
4716
+ pos = afterMatch;
4717
+ continue;
4718
+ }
4719
+ while (noTranslateEnds.length && pos >= noTranslateEnds[noTranslateEnds.length - 1]) {
4720
+ noTranslateEnds.pop();
4721
+ }
4722
+ if (!closingSlash && !selfClosingSlash && hasNoTranslateAttr(attrsString)) {
4723
+ const regionEnd = findCloseTag(html, afterMatch, tagName);
4724
+ if (regionEnd !== -1) {
4725
+ noTranslateEnds.push(regionEnd);
4726
+ }
4727
+ }
4728
+ if (noTranslateEnds.length > 0) {
4729
+ pos = afterMatch;
4730
+ continue;
4731
+ }
4732
+ if ((BLOCK_TAGS.has(tag) || INLINE_BLOCK_TAGS.has(tag)) && !closingSlash && !selfClosingSlash) {
4733
+ const closeTagStart = findCloseTag(html, afterMatch, tagName);
4734
+ if (closeTagStart === -1) {
4735
+ pos = afterMatch;
4736
+ continue;
4737
+ }
4738
+ const innerHTML = html.slice(afterMatch, closeTagStart);
4739
+ if (containsBlockTag(innerHTML)) {
4740
+ pos = afterMatch;
4741
+ continue;
4742
+ }
4743
+ blocks.push({
4744
+ tag,
4745
+ openTag: fullMatch,
4746
+ attrs: attrsString,
4747
+ inner: innerHTML,
4748
+ innerStart: afterMatch,
4749
+ innerEnd: closeTagStart
4750
+ });
4751
+ TAG_REGEX.lastIndex = closeTagStart;
4752
+ const closeMatch = TAG_REGEX.exec(html);
4753
+ pos = closeMatch ? closeMatch.index + closeMatch[0].length : closeTagStart + 1;
4754
+ continue;
4755
+ }
4756
+ pos = afterMatch;
4757
+ }
4758
+ return { blocks };
4759
+ }
4760
+ function replaceBlockInners(html, replacements) {
4761
+ const ordered = [...replacements].sort((a, b) => b.block.innerStart - a.block.innerStart);
4762
+ let result = html;
4763
+ for (const { block, inner } of ordered) {
4764
+ result = result.slice(0, block.innerStart) + inner + result.slice(block.innerEnd);
4765
+ }
4766
+ return result;
4767
+ }
4768
+
4769
+ // src/browser/md5.ts
4770
+ function safeAdd(x, y) {
4771
+ const lsw = (x & 65535) + (y & 65535);
4772
+ const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
4773
+ return msw << 16 | lsw & 65535;
4774
+ }
4775
+ function bitRotateLeft(num, cnt) {
4776
+ return num << cnt | num >>> 32 - cnt;
4777
+ }
4778
+ function md5cmn(q, a, b, x, s, t) {
4779
+ return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b);
4780
+ }
4781
+ function md5ff(a, b, c, d, x, s, t) {
4782
+ return md5cmn(b & c | ~b & d, a, b, x, s, t);
4783
+ }
4784
+ function md5gg(a, b, c, d, x, s, t) {
4785
+ return md5cmn(b & d | c & ~d, a, b, x, s, t);
4786
+ }
4787
+ function md5hh(a, b, c, d, x, s, t) {
4788
+ return md5cmn(b ^ c ^ d, a, b, x, s, t);
4789
+ }
4790
+ function md5ii(a, b, c, d, x, s, t) {
4791
+ return md5cmn(c ^ (b | ~d), a, b, x, s, t);
4792
+ }
4793
+ function binlMD5(x, len) {
4794
+ x[len >> 5] |= 128 << len % 32;
4795
+ x[(len + 64 >>> 9 << 4) + 14] = len;
4796
+ let a = 1732584193;
4797
+ let b = -271733879;
4798
+ let c = -1732584194;
4799
+ let d = 271733878;
4800
+ for (let i = 0; i < x.length; i += 16) {
4801
+ const olda = a;
4802
+ const oldb = b;
4803
+ const oldc = c;
4804
+ const oldd = d;
4805
+ a = md5ff(a, b, c, d, x[i], 7, -680876936);
4806
+ d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
4807
+ c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
4808
+ b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
4809
+ a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
4810
+ d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
4811
+ c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
4812
+ b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
4813
+ a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
4814
+ d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
4815
+ c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
4816
+ b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
4817
+ a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
4818
+ d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
4819
+ c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
4820
+ b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);
4821
+ a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
4822
+ d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
4823
+ c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
4824
+ b = md5gg(b, c, d, a, x[i], 20, -373897302);
4825
+ a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
4826
+ d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
4827
+ c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
4828
+ b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
4829
+ a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
4830
+ d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
4831
+ c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
4832
+ b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
4833
+ a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
4834
+ d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
4835
+ c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
4836
+ b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);
4837
+ a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
4838
+ d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
4839
+ c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
4840
+ b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
4841
+ a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
4842
+ d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
4843
+ c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
4844
+ b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
4845
+ a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
4846
+ d = md5hh(d, a, b, c, x[i + 0], 11, -358537222);
4847
+ c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
4848
+ b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
4849
+ a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
4850
+ d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
4851
+ c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
4852
+ b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);
4853
+ a = md5ii(a, b, c, d, x[i], 6, -198630844);
4854
+ d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
4855
+ c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
4856
+ b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
4857
+ a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
4858
+ d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
4859
+ c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
4860
+ b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
4861
+ a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
4862
+ d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
4863
+ c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
4864
+ b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
4865
+ a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
4866
+ d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
4867
+ c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
4868
+ b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);
4869
+ a = safeAdd(a, olda);
4870
+ b = safeAdd(b, oldb);
4871
+ c = safeAdd(c, oldc);
4872
+ d = safeAdd(d, oldd);
4873
+ }
4874
+ return [a, b, c, d];
4875
+ }
4876
+ function binl2hex(binarray) {
4877
+ const hexTab = "0123456789abcdef";
4878
+ let str = "";
4879
+ for (let i = 0; i < binarray.length * 4; i++) {
4880
+ str += hexTab.charAt(binarray[i >> 2] >> i % 4 * 8 + 4 & 15) + hexTab.charAt(binarray[i >> 2] >> i % 4 * 8 & 15);
4881
+ }
4882
+ return str;
4883
+ }
4884
+ function utf8Bytes(str) {
4885
+ if (typeof TextEncoder !== "undefined") {
4886
+ return new TextEncoder().encode(str);
4887
+ }
4888
+ const out = [];
4889
+ for (let i = 0; i < str.length; i++) {
4890
+ const c = str.charCodeAt(i);
4891
+ if (c < 128) {
4892
+ out.push(c);
4893
+ } else if (c < 2048) {
4894
+ out.push(192 | c >> 6, 128 | c & 63);
4895
+ } else if (c >= 55296 && c < 56320 && i + 1 < str.length) {
4896
+ const c2 = str.charCodeAt(++i);
4897
+ const cp = 65536 + (c - 55296 << 10) + (c2 - 56320);
4898
+ out.push(
4899
+ 240 | cp >> 18,
4900
+ 128 | cp >> 12 & 63,
4901
+ 128 | cp >> 6 & 63,
4902
+ 128 | cp & 63
4903
+ );
4904
+ } else {
4905
+ out.push(
4906
+ 224 | c >> 12,
4907
+ 128 | c >> 6 & 63,
4908
+ 128 | c & 63
4909
+ );
4910
+ }
4911
+ }
4912
+ return new Uint8Array(out);
4913
+ }
4914
+ function bytes2binl(bytes) {
4915
+ const bin = [];
4916
+ for (let i = 0; i < bytes.length; i++) {
4917
+ bin[i >> 2] |= bytes[i] << i % 4 * 8;
4918
+ }
4919
+ return bin;
4920
+ }
4921
+ function md5(str) {
4922
+ const bytes = utf8Bytes(str);
4923
+ return binl2hex(binlMD5(bytes2binl(bytes), bytes.length * 8));
4924
+ }
4925
+ function generateAutoKey(text) {
4926
+ const trimmed = text.trim();
4927
+ return "auto_" + md5(trimmed).substring(0, 12);
4928
+ }
4929
+
4930
+ // src/browser/exclude-filter.ts
4931
+ var DEFAULT_PATTERNS = {
4932
+ emails: true,
4933
+ urls: true,
4934
+ numbers: true,
4935
+ phones: true
4936
+ };
4937
+ var PATTERNS = {
4938
+ email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
4939
+ url: /^(https?:\/\/|www\.)[^\s]+$/i,
4940
+ number: /^[\d\s.,+-]+$/,
4941
+ phone: /^[+\d][\d\s().-]{6,}$/
4942
+ };
4943
+ var ExcludeFilter = class {
4944
+ constructor(patterns = {}, minTextLength = 2) {
4945
+ __publicField(this, "patterns");
4946
+ __publicField(this, "minTextLength");
4947
+ this.patterns = { ...DEFAULT_PATTERNS, ...patterns };
4948
+ this.minTextLength = minTextLength;
4949
+ }
4950
+ /**
4951
+ * Check if text should be excluded from translation
4952
+ */
4953
+ shouldExclude(text) {
4954
+ const trimmed = text.trim();
4955
+ if (trimmed.length < this.minTextLength) {
4956
+ return true;
4957
+ }
4958
+ if (this.patterns.emails && PATTERNS.email.test(trimmed)) {
4959
+ return true;
4960
+ }
4961
+ if (this.patterns.urls && PATTERNS.url.test(trimmed)) {
4962
+ return true;
4963
+ }
4964
+ if (this.patterns.numbers && PATTERNS.number.test(trimmed)) {
4965
+ return true;
4966
+ }
4967
+ if (this.patterns.phones && PATTERNS.phone.test(trimmed)) {
4968
+ return true;
4969
+ }
4970
+ return false;
4971
+ }
4972
+ };
4973
+
4974
+ // src/browser/sanitizer.ts
4975
+ var REMOVE_TAGS = /* @__PURE__ */ new Set([
4976
+ "script",
4977
+ "iframe",
4978
+ "frame",
4979
+ "frameset",
4980
+ "object",
4981
+ "embed",
4982
+ "applet",
4983
+ "base",
4984
+ "meta",
4985
+ "link",
4986
+ "template",
4987
+ "portal"
4988
+ ]);
4989
+ var URL_ATTRS2 = /* @__PURE__ */ new Set([
4990
+ "href",
4991
+ "src",
4992
+ "action",
4993
+ "formaction",
4994
+ "poster",
4995
+ "data",
4996
+ "xlink:href"
4997
+ ]);
4998
+ function sanitizeElement(root) {
4999
+ const all = [root, ...Array.from(root.querySelectorAll("*"))];
5000
+ for (const el of all) {
5001
+ const tag = el.tagName.toLowerCase();
5002
+ if (REMOVE_TAGS.has(tag)) {
5003
+ el.remove();
5004
+ continue;
5005
+ }
5006
+ for (const name of el.getAttributeNames()) {
5007
+ const lower = name.toLowerCase();
5008
+ if (lower.startsWith("on") || lower === "srcdoc") {
5009
+ el.removeAttribute(name);
5010
+ continue;
5011
+ }
5012
+ if (URL_ATTRS2.has(lower)) {
5013
+ if (sanitizeUrl(el.getAttribute(name) ?? "") === null) {
5014
+ el.removeAttribute(name);
5015
+ }
5016
+ continue;
5017
+ }
5018
+ if (lower === "style") {
5019
+ const clean = sanitizeCssDeclarations(el.getAttribute(name) ?? "");
5020
+ if (clean === null) el.removeAttribute(name);
5021
+ else el.setAttribute(name, clean);
5022
+ }
5023
+ }
5024
+ }
5025
+ }
5026
+ function sanitizeStyleSheetText(css, baseHref) {
5027
+ let out = css.replace(/@import[^;]*(;|$)/gi, "");
5028
+ out = out.replace(
5029
+ /url\(\s*(['"]?)([^'")]*)\1\s*\)/gi,
5030
+ (full, _quote, target) => {
5031
+ try {
5032
+ const resolved = new URL(target, baseHref);
5033
+ if (resolved.protocol === "data:") return full;
5034
+ if (resolved.origin === new URL(baseHref).origin) return full;
5035
+ } catch {
5036
+ }
5037
+ return 'url("")';
5038
+ }
5039
+ );
5040
+ return out.replace(/expression\s*\(/gi, "blocked(");
5041
+ }
5042
+
4288
5043
  // src/browser/storage/localstorage-repository.ts
4289
5044
  var STORAGE_PREFIX = "lionrapid:";
4290
5045
  var VERSION_KEY = `${STORAGE_PREFIX}version`;