@lionrapid/core 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base/event-emitter.d.ts.map +1 -1
- package/dist/base/event-emitter.js +2 -1
- package/dist/base/event-emitter.js.map +1 -1
- package/dist/browser/attribute-extractor.d.ts.map +1 -1
- package/dist/browser/attribute-extractor.js +2 -1
- package/dist/browser/attribute-extractor.js.map +1 -1
- package/dist/browser/auto-init.d.ts +73 -0
- package/dist/browser/auto-init.d.ts.map +1 -1
- package/dist/browser/auto-init.js +193 -62
- package/dist/browser/auto-init.js.map +1 -1
- package/dist/browser/block-extractor.d.ts.map +1 -1
- package/dist/browser/block-extractor.js +3 -1
- package/dist/browser/block-extractor.js.map +1 -1
- package/dist/browser/editor/editor-api.d.ts +50 -6
- package/dist/browser/editor/editor-api.d.ts.map +1 -1
- package/dist/browser/editor/editor-api.js +121 -19
- package/dist/browser/editor/editor-api.js.map +1 -1
- package/dist/browser/editor/editor-init.js +4 -1
- package/dist/browser/editor/editor-init.js.map +1 -1
- package/dist/browser/editor/sidebar.d.ts +4 -3
- package/dist/browser/editor/sidebar.d.ts.map +1 -1
- package/dist/browser/editor/sidebar.js +93 -27
- package/dist/browser/editor/sidebar.js.map +1 -1
- package/dist/browser/editor/snapshot-viewer.d.ts.map +1 -1
- package/dist/browser/editor/snapshot-viewer.js +32 -3
- package/dist/browser/editor/snapshot-viewer.js.map +1 -1
- package/dist/browser/lionrapid.auto.dev.js +444 -149
- package/dist/browser/lionrapid.auto.dev.js.map +4 -4
- package/dist/browser/lionrapid.auto.js +444 -149
- package/dist/browser/lionrapid.auto.js.map +4 -4
- package/dist/browser/lionrapid.auto.min.js +4 -3
- package/dist/browser/lionrapid.auto.min.js.map +4 -4
- package/dist/browser/lionrapid.editor.js +1467 -1115
- package/dist/browser/lionrapid.editor.js.map +4 -4
- package/dist/browser/lionrapid.editor.min.js +4 -3
- package/dist/browser/lionrapid.editor.min.js.map +4 -4
- package/dist/browser/lionrapid.esm.js +590 -101
- package/dist/browser/lionrapid.esm.js.map +4 -4
- package/dist/browser/lionrapid.esm.min.js +3 -2
- package/dist/browser/lionrapid.esm.min.js.map +4 -4
- package/dist/browser/lionrapid.iife.dev.js +590 -101
- package/dist/browser/lionrapid.iife.dev.js.map +4 -4
- package/dist/browser/lionrapid.iife.js +590 -101
- package/dist/browser/lionrapid.iife.js.map +4 -4
- package/dist/browser/lionrapid.iife.min.js +3 -2
- package/dist/browser/lionrapid.iife.min.js.map +4 -4
- package/dist/browser/lionrapid.umd.js +590 -101
- package/dist/browser/lionrapid.umd.js.map +4 -4
- package/dist/browser/lionrapid.umd.min.js +3 -2
- package/dist/browser/lionrapid.umd.min.js.map +4 -4
- package/dist/browser/sanitizer.d.ts +37 -0
- package/dist/browser/sanitizer.d.ts.map +1 -0
- package/dist/browser/sanitizer.js +93 -0
- package/dist/browser/sanitizer.js.map +1 -0
- package/dist/builder.d.ts +8 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +33 -5
- package/dist/builder.js.map +1 -1
- package/dist/config/lionrapid.config.d.ts +0 -1
- package/dist/config/lionrapid.config.d.ts.map +1 -1
- package/dist/config/repository.config.d.ts +2 -2
- package/dist/config/repository.config.d.ts.map +1 -1
- package/dist/content/index.d.ts +3 -0
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +5 -0
- package/dist/content/index.js.map +1 -1
- package/dist/content/sanitizer/HTMLSanitizer.d.ts +105 -0
- package/dist/content/sanitizer/HTMLSanitizer.d.ts.map +1 -0
- package/dist/content/sanitizer/HTMLSanitizer.js +257 -0
- package/dist/content/sanitizer/HTMLSanitizer.js.map +1 -0
- package/dist/content/serializer/HTMLSerializer.d.ts +6 -2
- package/dist/content/serializer/HTMLSerializer.d.ts.map +1 -1
- package/dist/content/serializer/HTMLSerializer.js +16 -8
- package/dist/content/serializer/HTMLSerializer.js.map +1 -1
- package/dist/content/splitter/BlockSplitter.d.ts +66 -0
- package/dist/content/splitter/BlockSplitter.d.ts.map +1 -0
- package/dist/content/splitter/BlockSplitter.js +219 -0
- package/dist/content/splitter/BlockSplitter.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/lionRapid.d.ts +2 -2
- package/dist/lionRapid.d.ts.map +1 -1
- package/dist/lionRapid.js +19 -7
- package/dist/lionRapid.js.map +1 -1
- package/dist/repositories/network-repository.d.ts +30 -8
- package/dist/repositories/network-repository.d.ts.map +1 -1
- package/dist/repositories/network-repository.js +88 -74
- package/dist/repositories/network-repository.js.map +1 -1
- package/dist/types/lionRapid.types.d.ts +2 -0
- package/dist/types/lionRapid.types.d.ts.map +1 -1
- package/dist/types/translation.types.d.ts +0 -3
- package/dist/types/translation.types.d.ts.map +1 -1
- package/dist/utils/context-utils.d.ts +1 -2
- package/dist/utils/context-utils.d.ts.map +1 -1
- package/dist/utils/context-utils.js +3 -6
- package/dist/utils/context-utils.js.map +1 -1
- package/dist/utils/locale.d.ts +57 -0
- package/dist/utils/locale.d.ts.map +1 -0
- package/dist/utils/locale.js +116 -0
- package/dist/utils/locale.js.map +1 -0
- package/dist/utils/translation-flow.d.ts +1 -1
- package/dist/utils/translation-flow.d.ts.map +1 -1
- package/dist/utils/translation-flow.js +2 -3
- package/dist/utils/translation-flow.js.map +1 -1
- package/package.json +2 -1
|
@@ -42,6 +42,7 @@ __export(index_exports, {
|
|
|
42
42
|
ExcludeFilter: () => ExcludeFilter,
|
|
43
43
|
HTMLSerializer: () => HTMLSerializer,
|
|
44
44
|
HandlerChain: () => HandlerChain,
|
|
45
|
+
LOCALE_TAG_REGEX: () => LOCALE_TAG_REGEX,
|
|
45
46
|
LionRapidBuilder: () => LionRapidBuilder,
|
|
46
47
|
LionRapidCore: () => LionRapidCore,
|
|
47
48
|
LocalStorageRepository: () => LocalStorageRepository,
|
|
@@ -52,6 +53,9 @@ __export(index_exports, {
|
|
|
52
53
|
MissingTranslationQueue: () => MissingTranslationQueue,
|
|
53
54
|
NetworkError: () => NetworkError,
|
|
54
55
|
NetworkRepository: () => NetworkRepository,
|
|
56
|
+
SAFE_ATTRS: () => SAFE_ATTRS,
|
|
57
|
+
SAFE_STYLE_PROPS: () => SAFE_STYLE_PROPS,
|
|
58
|
+
SAFE_TAGS: () => SAFE_TAGS,
|
|
55
59
|
ServerError: () => ServerError,
|
|
56
60
|
StaticRepository: () => StaticRepository,
|
|
57
61
|
TranslationError: () => TranslationError,
|
|
@@ -60,19 +64,33 @@ __export(index_exports, {
|
|
|
60
64
|
VERSION: () => VERSION,
|
|
61
65
|
VOID_TAGS: () => VOID_TAGS,
|
|
62
66
|
createPackageLogger: () => createPackageLogger,
|
|
67
|
+
escapeHtml: () => escapeHtml,
|
|
63
68
|
extractTagName: () => extractTagName,
|
|
64
69
|
generateAutoKey: () => generateAutoKey,
|
|
65
70
|
isClientError: () => isClientError,
|
|
66
71
|
isRetryableError: () => isRetryableError,
|
|
67
72
|
isTranslationError: () => isTranslationError,
|
|
73
|
+
localeDir: () => localeDir,
|
|
68
74
|
logger: () => logger,
|
|
75
|
+
matchLocale: () => matchLocale,
|
|
69
76
|
md5: () => md5,
|
|
70
77
|
measure: () => measure,
|
|
78
|
+
normalizeLocale: () => normalizeLocale,
|
|
79
|
+
parseAcceptLanguage: () => parseAcceptLanguage,
|
|
71
80
|
parseKeyPath: () => parseKeyPath,
|
|
72
81
|
prepareTranslationRequest: () => prepareTranslationRequest,
|
|
82
|
+
primarySubtag: () => primarySubtag,
|
|
73
83
|
readRawStyle: () => readRawStyle,
|
|
84
|
+
replaceBlockInners: () => replaceBlockInners,
|
|
74
85
|
resolveNestedKey: () => resolveNestedKey,
|
|
75
|
-
|
|
86
|
+
sanitizeContentUnit: () => sanitizeContentUnit,
|
|
87
|
+
sanitizeElement: () => sanitizeElement,
|
|
88
|
+
sanitizeOpeningTag: () => sanitizeOpeningTag,
|
|
89
|
+
sanitizeStyle: () => sanitizeStyle,
|
|
90
|
+
sanitizeStyleSheetText: () => sanitizeStyleSheetText,
|
|
91
|
+
sanitizeUrl: () => sanitizeUrl,
|
|
92
|
+
serializeOpeningTag: () => serializeOpeningTag,
|
|
93
|
+
splitBlocks: () => splitBlocks
|
|
76
94
|
});
|
|
77
95
|
module.exports = __toCommonJS(index_exports);
|
|
78
96
|
|
|
@@ -423,6 +441,57 @@ var measure = (name) => {
|
|
|
423
441
|
};
|
|
424
442
|
};
|
|
425
443
|
|
|
444
|
+
// src/utils/locale.ts
|
|
445
|
+
var LOCALE_TAG_REGEX = /^[a-z]{2,3}(?:-[a-z0-9]{2,4})*$/i;
|
|
446
|
+
var RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "fa", "he", "ur", "yi"]);
|
|
447
|
+
function primarySubtag(tag) {
|
|
448
|
+
return tag.split("-")[0].toLowerCase();
|
|
449
|
+
}
|
|
450
|
+
function normalizeLocale(tag) {
|
|
451
|
+
const trimmed = tag.trim();
|
|
452
|
+
if (!trimmed) return trimmed;
|
|
453
|
+
const [lang, ...rest] = trimmed.split("-");
|
|
454
|
+
const out = [lang.toLowerCase()];
|
|
455
|
+
for (const sub of rest) {
|
|
456
|
+
if (sub.length === 4) {
|
|
457
|
+
out.push(sub[0].toUpperCase() + sub.slice(1).toLowerCase());
|
|
458
|
+
} else if (sub.length === 2) {
|
|
459
|
+
out.push(sub.toUpperCase());
|
|
460
|
+
} else {
|
|
461
|
+
out.push(sub.toLowerCase());
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return out.join("-");
|
|
465
|
+
}
|
|
466
|
+
function matchLocale(requested, available, fallback) {
|
|
467
|
+
if (available.length === 0) return fallback;
|
|
468
|
+
const avail = available.map((raw) => ({
|
|
469
|
+
raw,
|
|
470
|
+
lower: normalizeLocale(raw).toLowerCase()
|
|
471
|
+
}));
|
|
472
|
+
for (const req of requested) {
|
|
473
|
+
if (!req) continue;
|
|
474
|
+
const reqLower = normalizeLocale(req).toLowerCase();
|
|
475
|
+
const exact = avail.find((a) => a.lower === reqLower);
|
|
476
|
+
if (exact) return exact.raw;
|
|
477
|
+
const primary = primarySubtag(req);
|
|
478
|
+
const byPrimary = avail.find((a) => a.lower === primary);
|
|
479
|
+
if (byPrimary) return byPrimary.raw;
|
|
480
|
+
}
|
|
481
|
+
return fallback;
|
|
482
|
+
}
|
|
483
|
+
function parseAcceptLanguage(header) {
|
|
484
|
+
return header.split(",").map((part) => {
|
|
485
|
+
const [tag, ...params] = part.trim().split(";");
|
|
486
|
+
const qParam = params.find((p) => p.trim().startsWith("q="));
|
|
487
|
+
const q = qParam ? parseFloat(qParam.trim().slice(2)) : 1;
|
|
488
|
+
return { tag: tag.trim(), q: Number.isNaN(q) ? 0 : q };
|
|
489
|
+
}).filter((x) => x.tag.length > 0 && x.tag !== "*").sort((a, b) => b.q - a.q).map((x) => x.tag);
|
|
490
|
+
}
|
|
491
|
+
function localeDir(tag) {
|
|
492
|
+
return RTL_LANGUAGES.has(primarySubtag(tag)) ? "rtl" : "ltr";
|
|
493
|
+
}
|
|
494
|
+
|
|
426
495
|
// src/base/event-emitter.ts
|
|
427
496
|
var EventEmitter = class {
|
|
428
497
|
constructor() {
|
|
@@ -711,7 +780,7 @@ var EventEmitter = class {
|
|
|
711
780
|
complete: () => {
|
|
712
781
|
this.emit("language:change:success", {
|
|
713
782
|
context: this.createContext(requestId, [...baseTrace, "complete"]),
|
|
714
|
-
payload: { oldLocale, newLocale }
|
|
783
|
+
payload: { oldLocale, newLocale, dir: localeDir(newLocale) }
|
|
715
784
|
});
|
|
716
785
|
},
|
|
717
786
|
error: (error) => {
|
|
@@ -1121,12 +1190,11 @@ function parseNamespaceKey(key) {
|
|
|
1121
1190
|
}
|
|
1122
1191
|
|
|
1123
1192
|
// src/utils/context-utils.ts
|
|
1124
|
-
function createTranslationContext(parsedKey, currentLocale, defaultNamespace
|
|
1193
|
+
function createTranslationContext(parsedKey, currentLocale, defaultNamespace) {
|
|
1125
1194
|
const effectiveNamespace = parsedKey.hasExplicitNamespace ? parsedKey.namespace : defaultNamespace;
|
|
1126
1195
|
return {
|
|
1127
1196
|
locale: currentLocale,
|
|
1128
|
-
namespace: effectiveNamespace
|
|
1129
|
-
fallbackLocale: fallbackLocale || currentLocale
|
|
1197
|
+
namespace: effectiveNamespace
|
|
1130
1198
|
};
|
|
1131
1199
|
}
|
|
1132
1200
|
|
|
@@ -1243,7 +1311,7 @@ function flattenTranslations(translations, prefix = "", locale, namespace) {
|
|
|
1243
1311
|
}
|
|
1244
1312
|
|
|
1245
1313
|
// src/utils/translation-flow.ts
|
|
1246
|
-
function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace
|
|
1314
|
+
function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, paramsOrDefaultValue, defaultNamespace) {
|
|
1247
1315
|
const startTime = performance.now();
|
|
1248
1316
|
const { defaultValue, params } = parseTranslationParams(
|
|
1249
1317
|
defaultValueOrParams,
|
|
@@ -1253,8 +1321,7 @@ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, par
|
|
|
1253
1321
|
const context = createTranslationContext(
|
|
1254
1322
|
parsedKey,
|
|
1255
1323
|
currentLocale,
|
|
1256
|
-
defaultNamespace
|
|
1257
|
-
fallbackLocale
|
|
1324
|
+
defaultNamespace
|
|
1258
1325
|
);
|
|
1259
1326
|
const individualCacheKey = createIndividualCacheKey(
|
|
1260
1327
|
context.locale,
|
|
@@ -1276,7 +1343,6 @@ function prepareTranslationRequest(key, currentLocale, defaultValueOrParams, par
|
|
|
1276
1343
|
locale: currentLocale,
|
|
1277
1344
|
namespace: context.namespace,
|
|
1278
1345
|
// ✅ Use effective namespace (explicit or default)
|
|
1279
|
-
fallbackLocale,
|
|
1280
1346
|
individualCacheKey,
|
|
1281
1347
|
// ✅ Ready-to-use cache key
|
|
1282
1348
|
namespaceCacheKey,
|
|
@@ -2392,6 +2458,183 @@ var MarkupParser = class {
|
|
|
2392
2458
|
}
|
|
2393
2459
|
};
|
|
2394
2460
|
|
|
2461
|
+
// src/content/sanitizer/HTMLSanitizer.ts
|
|
2462
|
+
var SAFE_TAGS = /* @__PURE__ */ new Set([
|
|
2463
|
+
"a",
|
|
2464
|
+
"abbr",
|
|
2465
|
+
"b",
|
|
2466
|
+
"bdi",
|
|
2467
|
+
"bdo",
|
|
2468
|
+
"br",
|
|
2469
|
+
"cite",
|
|
2470
|
+
"code",
|
|
2471
|
+
"del",
|
|
2472
|
+
"dfn",
|
|
2473
|
+
"em",
|
|
2474
|
+
"i",
|
|
2475
|
+
"img",
|
|
2476
|
+
"ins",
|
|
2477
|
+
"kbd",
|
|
2478
|
+
"mark",
|
|
2479
|
+
"q",
|
|
2480
|
+
"s",
|
|
2481
|
+
"samp",
|
|
2482
|
+
"small",
|
|
2483
|
+
"span",
|
|
2484
|
+
"strong",
|
|
2485
|
+
"sub",
|
|
2486
|
+
"sup",
|
|
2487
|
+
"time",
|
|
2488
|
+
"u",
|
|
2489
|
+
"var",
|
|
2490
|
+
"wbr"
|
|
2491
|
+
]);
|
|
2492
|
+
var SAFE_ATTRS = /* @__PURE__ */ new Set([
|
|
2493
|
+
"alt",
|
|
2494
|
+
"cite",
|
|
2495
|
+
"class",
|
|
2496
|
+
"datetime",
|
|
2497
|
+
"dir",
|
|
2498
|
+
"height",
|
|
2499
|
+
"href",
|
|
2500
|
+
"id",
|
|
2501
|
+
"lang",
|
|
2502
|
+
"loading",
|
|
2503
|
+
"rel",
|
|
2504
|
+
"role",
|
|
2505
|
+
"src",
|
|
2506
|
+
"target",
|
|
2507
|
+
"title",
|
|
2508
|
+
"width"
|
|
2509
|
+
]);
|
|
2510
|
+
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "cite"]);
|
|
2511
|
+
var SAFE_STYLE_PROPS = /* @__PURE__ */ new Set([
|
|
2512
|
+
"color",
|
|
2513
|
+
"background-color",
|
|
2514
|
+
"font-family",
|
|
2515
|
+
"font-size",
|
|
2516
|
+
"font-style",
|
|
2517
|
+
"font-weight",
|
|
2518
|
+
"letter-spacing",
|
|
2519
|
+
"line-height",
|
|
2520
|
+
"text-decoration",
|
|
2521
|
+
"text-decoration-color",
|
|
2522
|
+
"text-decoration-line",
|
|
2523
|
+
"text-decoration-style",
|
|
2524
|
+
"text-transform",
|
|
2525
|
+
"vertical-align",
|
|
2526
|
+
"white-space",
|
|
2527
|
+
"word-spacing"
|
|
2528
|
+
]);
|
|
2529
|
+
var DANGEROUS_CSS_VALUE = /url\s*\(|expression\s*\(|image-set\s*\(|element\s*\(|paint\s*\(|javascript:|\\|[<>{}@]/i;
|
|
2530
|
+
var SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
2531
|
+
var NAMED_ENTITIES = {
|
|
2532
|
+
amp: "&",
|
|
2533
|
+
lt: "<",
|
|
2534
|
+
gt: ">",
|
|
2535
|
+
quot: '"',
|
|
2536
|
+
apos: "'",
|
|
2537
|
+
nbsp: "\xA0",
|
|
2538
|
+
colon: ":",
|
|
2539
|
+
semi: ";",
|
|
2540
|
+
sol: "/",
|
|
2541
|
+
bsol: "\\",
|
|
2542
|
+
Tab: " ",
|
|
2543
|
+
NewLine: "\n"
|
|
2544
|
+
};
|
|
2545
|
+
function decodeEntities2(value) {
|
|
2546
|
+
return value.replace(
|
|
2547
|
+
/&(?:#[xX]([0-9a-fA-F]+);?|#(\d+);?|([a-zA-Z]+);)/g,
|
|
2548
|
+
(full, hex, dec, name) => {
|
|
2549
|
+
if (hex) return String.fromCodePoint(parseInt(hex, 16));
|
|
2550
|
+
if (dec) return String.fromCodePoint(parseInt(dec, 10));
|
|
2551
|
+
return NAMED_ENTITIES[name] ?? full;
|
|
2552
|
+
}
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
function escapeHtml(text) {
|
|
2556
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2557
|
+
}
|
|
2558
|
+
function sanitizeUrl(value) {
|
|
2559
|
+
const decoded = decodeEntities2(value);
|
|
2560
|
+
const probe = decoded.replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase();
|
|
2561
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(probe);
|
|
2562
|
+
if (!scheme) return value;
|
|
2563
|
+
return SAFE_SCHEMES.has(scheme[1]) ? value : null;
|
|
2564
|
+
}
|
|
2565
|
+
function sanitizeStyle(value) {
|
|
2566
|
+
return filterCssDeclarations(value, true);
|
|
2567
|
+
}
|
|
2568
|
+
function sanitizeCssDeclarations(value) {
|
|
2569
|
+
return filterCssDeclarations(value, false);
|
|
2570
|
+
}
|
|
2571
|
+
function filterCssDeclarations(value, requirePropAllowlist) {
|
|
2572
|
+
const decoded = decodeEntities2(value);
|
|
2573
|
+
const kept = [];
|
|
2574
|
+
for (const declaration of decoded.split(";")) {
|
|
2575
|
+
const colon = declaration.indexOf(":");
|
|
2576
|
+
if (colon === -1) continue;
|
|
2577
|
+
const prop = declaration.slice(0, colon).trim().toLowerCase();
|
|
2578
|
+
const val = declaration.slice(colon + 1).trim();
|
|
2579
|
+
if (!/^[a-z-]+$/.test(prop)) continue;
|
|
2580
|
+
if (requirePropAllowlist && !SAFE_STYLE_PROPS.has(prop)) continue;
|
|
2581
|
+
if (val === "" || DANGEROUS_CSS_VALUE.test(val)) continue;
|
|
2582
|
+
kept.push(`${prop}: ${val}`);
|
|
2583
|
+
}
|
|
2584
|
+
return kept.length > 0 ? kept.join("; ") : null;
|
|
2585
|
+
}
|
|
2586
|
+
var ATTR_PATTERN = /([^\s=/>"']+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*)))?/g;
|
|
2587
|
+
function sanitizeOpeningTag(rawOpen) {
|
|
2588
|
+
if (!rawOpen.startsWith("<")) return null;
|
|
2589
|
+
const tagName = extractTagName(rawOpen);
|
|
2590
|
+
if (!/^[a-z][a-z0-9-]*$/.test(tagName) || !SAFE_TAGS.has(tagName)) {
|
|
2591
|
+
return null;
|
|
2592
|
+
}
|
|
2593
|
+
const end = rawOpen.endsWith("/>") ? rawOpen.length - 2 : rawOpen.endsWith(">") ? rawOpen.length - 1 : rawOpen.length;
|
|
2594
|
+
const attrSource = rawOpen.slice(1 + tagName.length, end);
|
|
2595
|
+
const attrs = {};
|
|
2596
|
+
for (const match of attrSource.matchAll(ATTR_PATTERN)) {
|
|
2597
|
+
const name = match[1].toLowerCase();
|
|
2598
|
+
const value = match[2] ?? match[3] ?? match[4] ?? "";
|
|
2599
|
+
if (name in attrs) continue;
|
|
2600
|
+
if (name === "style") {
|
|
2601
|
+
const cleanStyle = sanitizeStyle(value);
|
|
2602
|
+
if (cleanStyle !== null) attrs[name] = cleanStyle;
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
const allowed = SAFE_ATTRS.has(name) || /^data-[a-z0-9-]*$/.test(name) || /^aria-[a-z0-9-]*$/.test(name);
|
|
2606
|
+
if (!allowed) continue;
|
|
2607
|
+
if (URL_ATTRS.has(name)) {
|
|
2608
|
+
if (sanitizeUrl(value) === null) continue;
|
|
2609
|
+
}
|
|
2610
|
+
attrs[name] = value;
|
|
2611
|
+
}
|
|
2612
|
+
return serializeOpeningTag(tagName, attrs);
|
|
2613
|
+
}
|
|
2614
|
+
function sanitizeContentUnit(unit) {
|
|
2615
|
+
let changed = false;
|
|
2616
|
+
const styles = {};
|
|
2617
|
+
for (const [index, styleList] of Object.entries(unit.styles)) {
|
|
2618
|
+
const raw = readRawStyle(styleList);
|
|
2619
|
+
if (raw === null) {
|
|
2620
|
+
styles[index] = styleList;
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
const safe = sanitizeOpeningTag(raw);
|
|
2624
|
+
if (safe === null) {
|
|
2625
|
+
changed = true;
|
|
2626
|
+
const rest = styleList.filter((s) => !s.startsWith("raw:"));
|
|
2627
|
+
if (rest.length > 0) styles[index] = rest;
|
|
2628
|
+
} else if (safe === raw) {
|
|
2629
|
+
styles[index] = styleList;
|
|
2630
|
+
} else {
|
|
2631
|
+
changed = true;
|
|
2632
|
+
styles[index] = styleList.map((s) => s.startsWith("raw:") ? `raw:${safe}` : s);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
return changed ? { text: unit.text, styles } : unit;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2395
2638
|
// src/content/serializer/HTMLSerializer.ts
|
|
2396
2639
|
var HTMLSerializer = class {
|
|
2397
2640
|
serialize(unit) {
|
|
@@ -2401,24 +2644,25 @@ var HTMLSerializer = class {
|
|
|
2401
2644
|
let lastIndex = 0;
|
|
2402
2645
|
let match;
|
|
2403
2646
|
while ((match = tagPattern.exec(text)) !== null) {
|
|
2404
|
-
result += text.slice(lastIndex, match.index);
|
|
2647
|
+
result += escapeHtml(text.slice(lastIndex, match.index));
|
|
2405
2648
|
const [fullMatch, closingSlash, indexStr] = match;
|
|
2406
2649
|
const styleList = styles[indexStr] ?? [];
|
|
2407
2650
|
const rawOpen = readRawStyle(styleList);
|
|
2408
|
-
|
|
2651
|
+
const safeOpen = rawOpen === null ? null : sanitizeOpeningTag(rawOpen);
|
|
2652
|
+
if (rawOpen && safeOpen !== null) {
|
|
2409
2653
|
const tagName = extractTagName(rawOpen);
|
|
2410
2654
|
const isVoid = VOID_TAGS.has(tagName);
|
|
2411
2655
|
if (closingSlash) {
|
|
2412
2656
|
if (!isVoid) result += `</${tagName}>`;
|
|
2413
2657
|
} else {
|
|
2414
|
-
result +=
|
|
2658
|
+
result += safeOpen;
|
|
2415
2659
|
}
|
|
2416
2660
|
} else {
|
|
2417
2661
|
result += fullMatch;
|
|
2418
2662
|
}
|
|
2419
2663
|
lastIndex = match.index + fullMatch.length;
|
|
2420
2664
|
}
|
|
2421
|
-
result += text.slice(lastIndex);
|
|
2665
|
+
result += escapeHtml(text.slice(lastIndex));
|
|
2422
2666
|
return result;
|
|
2423
2667
|
}
|
|
2424
2668
|
};
|
|
@@ -2657,10 +2901,9 @@ var CacheManager = class {
|
|
|
2657
2901
|
var INDEXED_TAG_REGEX = /<\/?[0-9]+\/?>/;
|
|
2658
2902
|
var HTML_TAG_REGEX = /<[a-z][a-z0-9]*(?:\s+[^>]*)?\/?>/i;
|
|
2659
2903
|
var LionRapidCore = class {
|
|
2660
|
-
constructor(eventEmitter, handlerChain, defaultLocale = "en",
|
|
2904
|
+
constructor(eventEmitter, handlerChain, defaultLocale = "en", defaultNamespace) {
|
|
2661
2905
|
__publicField(this, "handlerChain");
|
|
2662
2906
|
__publicField(this, "currentLocale");
|
|
2663
|
-
__publicField(this, "fallbackLocale");
|
|
2664
2907
|
__publicField(this, "defaultNamespace");
|
|
2665
2908
|
__publicField(this, "pendingTranslations", /* @__PURE__ */ new Map());
|
|
2666
2909
|
__publicField(this, "bus");
|
|
@@ -2668,6 +2911,7 @@ var LionRapidCore = class {
|
|
|
2668
2911
|
// 🔥 EMBEDDED: Core integration maps
|
|
2669
2912
|
__publicField(this, "formatters", /* @__PURE__ */ new Map());
|
|
2670
2913
|
__publicField(this, "processors", /* @__PURE__ */ new Map());
|
|
2914
|
+
__publicField(this, "warnedNoInterpolate", false);
|
|
2671
2915
|
// 📡 EVENT-DRIVEN: Just track for cleanup
|
|
2672
2916
|
__publicField(this, "eventPlugins", /* @__PURE__ */ new Set());
|
|
2673
2917
|
// 📦 CONTENT: Parser and serializer for ContentUnit processing
|
|
@@ -2678,7 +2922,6 @@ var LionRapidCore = class {
|
|
|
2678
2922
|
this.bus = eventEmitter;
|
|
2679
2923
|
this.handlerChain = handlerChain;
|
|
2680
2924
|
this.currentLocale = defaultLocale;
|
|
2681
|
-
this.fallbackLocale = fallbackLocale;
|
|
2682
2925
|
this.defaultNamespace = defaultNamespace;
|
|
2683
2926
|
this.cache = new CacheManager(
|
|
2684
2927
|
this.handlerChain,
|
|
@@ -2743,8 +2986,7 @@ var LionRapidCore = class {
|
|
|
2743
2986
|
this.currentLocale,
|
|
2744
2987
|
firstArg,
|
|
2745
2988
|
secondArg,
|
|
2746
|
-
this.defaultNamespace
|
|
2747
|
-
this.fallbackLocale
|
|
2989
|
+
this.defaultNamespace
|
|
2748
2990
|
);
|
|
2749
2991
|
logger.debug(`Translation requested: ${request.cleanKey}`);
|
|
2750
2992
|
const translateBus = this.bus.translation(request);
|
|
@@ -2794,8 +3036,7 @@ var LionRapidCore = class {
|
|
|
2794
3036
|
this.currentLocale,
|
|
2795
3037
|
firstArg,
|
|
2796
3038
|
secondArg,
|
|
2797
|
-
this.defaultNamespace
|
|
2798
|
-
this.fallbackLocale
|
|
3039
|
+
this.defaultNamespace
|
|
2799
3040
|
);
|
|
2800
3041
|
logger.debug(`HTML translation requested: ${request.cleanKey}`);
|
|
2801
3042
|
const translateBus = this.bus.translation(request);
|
|
@@ -2963,6 +3204,11 @@ var LionRapidCore = class {
|
|
|
2963
3204
|
resolvedLocale,
|
|
2964
3205
|
resolvedKey
|
|
2965
3206
|
);
|
|
3207
|
+
} else if (!this.warnedNoInterpolate && params && Object.keys(params).length > 0 && processedValue.includes("{")) {
|
|
3208
|
+
this.warnedNoInterpolate = true;
|
|
3209
|
+
console.warn(
|
|
3210
|
+
"[LionRapid] Translation params were passed but no interpolate formatter is registered \u2014 {placeholders} render literally. Install @lionrapid/formatters and add .use(coreFormattersPlugin) to the builder."
|
|
3211
|
+
);
|
|
2966
3212
|
}
|
|
2967
3213
|
if (this.processors.size > 0) {
|
|
2968
3214
|
for (const [name, processor] of this.processors.entries()) {
|
|
@@ -3003,8 +3249,7 @@ var LionRapidCore = class {
|
|
|
3003
3249
|
this.currentLocale,
|
|
3004
3250
|
params,
|
|
3005
3251
|
defaultValue,
|
|
3006
|
-
this.defaultNamespace
|
|
3007
|
-
this.fallbackLocale
|
|
3252
|
+
this.defaultNamespace
|
|
3008
3253
|
);
|
|
3009
3254
|
const syncResult = this.handlerChain.handleSync(request);
|
|
3010
3255
|
const rawValue = syncResult && syncResult.status === 200 ? syncResult.value : request.parsedDefaultValue || request.cleanKey;
|
|
@@ -3125,8 +3370,7 @@ var LionRapidCore = class {
|
|
|
3125
3370
|
this.currentLocale,
|
|
3126
3371
|
void 0,
|
|
3127
3372
|
void 0,
|
|
3128
|
-
this.defaultNamespace
|
|
3129
|
-
this.fallbackLocale
|
|
3373
|
+
this.defaultNamespace
|
|
3130
3374
|
);
|
|
3131
3375
|
return this.handlerChain.isCached(request);
|
|
3132
3376
|
}
|
|
@@ -3547,6 +3791,9 @@ var MissingTranslationQueue = class {
|
|
|
3547
3791
|
};
|
|
3548
3792
|
|
|
3549
3793
|
// src/builder.ts
|
|
3794
|
+
function isLoudPreloadFailure(error) {
|
|
3795
|
+
return !(error instanceof TranslationNotFoundError || error instanceof TranslationLockedError);
|
|
3796
|
+
}
|
|
3550
3797
|
var LionRapidBuilder = class _LionRapidBuilder {
|
|
3551
3798
|
constructor(config) {
|
|
3552
3799
|
__publicField(this, "config");
|
|
@@ -3557,7 +3804,6 @@ var LionRapidBuilder = class _LionRapidBuilder {
|
|
|
3557
3804
|
__publicField(this, "syncQueue", null);
|
|
3558
3805
|
this.config = {
|
|
3559
3806
|
defaultLocale: config.defaultLocale || "en",
|
|
3560
|
-
fallbackLocale: config.fallbackLocale || "en",
|
|
3561
3807
|
namespace: config.namespace || "app"
|
|
3562
3808
|
};
|
|
3563
3809
|
}
|
|
@@ -3658,7 +3904,6 @@ var LionRapidBuilder = class _LionRapidBuilder {
|
|
|
3658
3904
|
eventEmitter,
|
|
3659
3905
|
handlerChain,
|
|
3660
3906
|
this.config.defaultLocale,
|
|
3661
|
-
this.config.fallbackLocale,
|
|
3662
3907
|
this.config.namespace
|
|
3663
3908
|
);
|
|
3664
3909
|
for (const plugin of this.plugins) {
|
|
@@ -3689,7 +3934,17 @@ var LionRapidBuilder = class _LionRapidBuilder {
|
|
|
3689
3934
|
logger.debug("Preload targets", {
|
|
3690
3935
|
namespaces: eagerPreloads.map((c) => `${c.locale}:${c.namespace}`)
|
|
3691
3936
|
});
|
|
3937
|
+
let warnedLoudly = false;
|
|
3938
|
+
const warnLoudlyOnce = (namespaceKey, error) => {
|
|
3939
|
+
if (!isLoudPreloadFailure(error) || warnedLoudly) return;
|
|
3940
|
+
warnedLoudly = true;
|
|
3941
|
+
console.warn(
|
|
3942
|
+
`[LionRapid] Could not load translations for ${namespaceKey} \u2014 the page stays untranslated until the server is reachable.`,
|
|
3943
|
+
error
|
|
3944
|
+
);
|
|
3945
|
+
};
|
|
3692
3946
|
const promises = eagerPreloads.map(async (preloadConfig) => {
|
|
3947
|
+
const namespaceKey = `${preloadConfig.locale}:${preloadConfig.namespace}`;
|
|
3693
3948
|
try {
|
|
3694
3949
|
const result = await networkHandler.loadNamespace(
|
|
3695
3950
|
preloadConfig.locale,
|
|
@@ -3698,19 +3953,19 @@ var LionRapidBuilder = class _LionRapidBuilder {
|
|
|
3698
3953
|
if (result && result.data && result.status === 200) {
|
|
3699
3954
|
const context = {
|
|
3700
3955
|
locale: preloadConfig.locale,
|
|
3701
|
-
namespace: preloadConfig.namespace
|
|
3702
|
-
fallbackLocale: this.config.fallbackLocale
|
|
3956
|
+
namespace: preloadConfig.namespace
|
|
3703
3957
|
};
|
|
3704
3958
|
await handlerChain.saveNamespaceToHandlers(result.data, context);
|
|
3705
|
-
logger.debug(
|
|
3706
|
-
|
|
3707
|
-
|
|
3959
|
+
logger.debug(`Preloaded: ${namespaceKey}`);
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
if (result?.error) {
|
|
3963
|
+
warnLoudlyOnce(namespaceKey, result.error);
|
|
3964
|
+
logger.warn(`Preload failed: ${namespaceKey}`, result.error);
|
|
3708
3965
|
}
|
|
3709
3966
|
} catch (error) {
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
error
|
|
3713
|
-
);
|
|
3967
|
+
warnLoudlyOnce(namespaceKey, error);
|
|
3968
|
+
logger.warn(`Preload failed: ${namespaceKey}`, error);
|
|
3714
3969
|
}
|
|
3715
3970
|
});
|
|
3716
3971
|
await Promise.allSettled(promises);
|
|
@@ -3724,8 +3979,16 @@ var _NetworkRepository = class _NetworkRepository {
|
|
|
3724
3979
|
__publicField(this, "name", "NetworkRepository");
|
|
3725
3980
|
__publicField(this, "type", "network");
|
|
3726
3981
|
__publicField(this, "config", {});
|
|
3727
|
-
/**
|
|
3728
|
-
|
|
3982
|
+
/**
|
|
3983
|
+
* Namespace snapshot per key "locale:namespace": the server version TOGETHER
|
|
3984
|
+
* with the payload it describes, persisted as ONE localStorage entry.
|
|
3985
|
+
*
|
|
3986
|
+
* Canonical 304 contract (same as edge KV bundle + WP transient cache):
|
|
3987
|
+
* never claim freshness you can't back. If-None-Match is only sent when the
|
|
3988
|
+
* snapshot is in hand; a 304 answers with that snapshot's payload; version
|
|
3989
|
+
* and payload can never desync because they live and die together.
|
|
3990
|
+
*/
|
|
3991
|
+
__publicField(this, "namespaceSnapshots", /* @__PURE__ */ new Map());
|
|
3729
3992
|
/**
|
|
3730
3993
|
* Default endpoint templates with placeholder support
|
|
3731
3994
|
* Placeholders: :key, :locale, :namespace
|
|
@@ -3838,36 +4101,50 @@ var _NetworkRepository = class _NetworkRepository {
|
|
|
3838
4101
|
logger.debug("API base URL is not set");
|
|
3839
4102
|
throw new NetworkError("API base URL is not set");
|
|
3840
4103
|
}
|
|
4104
|
+
try {
|
|
4105
|
+
return await this.fetchNamespaceRaw(locale, namespace);
|
|
4106
|
+
} catch (error) {
|
|
4107
|
+
this.handleCaughtError(error);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
/**
|
|
4111
|
+
* Shared namespace fetch with the canonical 304 contract:
|
|
4112
|
+
* - If-None-Match is sent ONLY when a snapshot (version + payload) is in hand.
|
|
4113
|
+
* - 304 answers with that snapshot's payload — never with "nothing".
|
|
4114
|
+
* - A 304 with no snapshot (server/proxy surprise) refetches unconditionally.
|
|
4115
|
+
* - A 200 stores version + payload atomically before either is trusted.
|
|
4116
|
+
*/
|
|
4117
|
+
async fetchNamespaceRaw(locale, namespace, allowConditional = true) {
|
|
3841
4118
|
const url = this.buildUrl("getNamespace", { locale, namespace });
|
|
3842
4119
|
logger.debug(`API GET Namespace: ${url}`);
|
|
3843
4120
|
const namespaceKey = `${locale}:${namespace}`;
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
if (
|
|
3852
|
-
logger.debug(`Namespace ${namespaceKey}: 304 Not Modified`);
|
|
3853
|
-
return
|
|
3854
|
-
}
|
|
3855
|
-
if (!response.ok) {
|
|
3856
|
-
this.handleHttpError(response, namespaceKey);
|
|
3857
|
-
}
|
|
3858
|
-
const data = await response.json();
|
|
3859
|
-
logger.debug("API response data:", data);
|
|
3860
|
-
if (!data.translations) {
|
|
3861
|
-
throw new TranslationNotFoundError(namespaceKey);
|
|
3862
|
-
}
|
|
3863
|
-
const version = data.meta?.version != null ? String(data.meta.version) : null;
|
|
3864
|
-
if (version) {
|
|
3865
|
-
this.setVersion(namespaceKey, version);
|
|
4121
|
+
const snapshot = allowConditional ? this.getSnapshot(namespaceKey) : void 0;
|
|
4122
|
+
const extraHeaders = {};
|
|
4123
|
+
if (snapshot) {
|
|
4124
|
+
extraHeaders["If-None-Match"] = `W/"${snapshot.version}"`;
|
|
4125
|
+
}
|
|
4126
|
+
const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
|
|
4127
|
+
if (response.status === 304) {
|
|
4128
|
+
if (snapshot) {
|
|
4129
|
+
logger.debug(`Namespace ${namespaceKey}: 304 Not Modified (snapshot served)`);
|
|
4130
|
+
return snapshot.translations;
|
|
3866
4131
|
}
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
4132
|
+
logger.debug(`Namespace ${namespaceKey}: unexpected 304 with no snapshot \u2014 refetching`);
|
|
4133
|
+
return this.fetchNamespaceRaw(locale, namespace, false);
|
|
4134
|
+
}
|
|
4135
|
+
if (!response.ok) {
|
|
4136
|
+
this.handleHttpError(response, namespaceKey);
|
|
3870
4137
|
}
|
|
4138
|
+
const data = await response.json();
|
|
4139
|
+
logger.debug("API response data:", data);
|
|
4140
|
+
if (!data.translations) {
|
|
4141
|
+
throw new TranslationNotFoundError(namespaceKey);
|
|
4142
|
+
}
|
|
4143
|
+
const version = data.meta?.version != null ? String(data.meta.version) : null;
|
|
4144
|
+
if (version) {
|
|
4145
|
+
this.setSnapshot(namespaceKey, { version, translations: data.translations });
|
|
4146
|
+
}
|
|
4147
|
+
return data.translations;
|
|
3871
4148
|
}
|
|
3872
4149
|
/**
|
|
3873
4150
|
* Save translation to API (if supported)
|
|
@@ -3952,33 +4229,8 @@ var _NetworkRepository = class _NetworkRepository {
|
|
|
3952
4229
|
logger.debug("API base URL is not set");
|
|
3953
4230
|
throw new NetworkError("API base URL is not set");
|
|
3954
4231
|
}
|
|
3955
|
-
const url = this.buildUrl("getNamespace", { locale, namespace });
|
|
3956
|
-
logger.debug(`API GET Namespace ContentUnits: ${url}`);
|
|
3957
|
-
const namespaceKey = `${locale}:${namespace}`;
|
|
3958
4232
|
try {
|
|
3959
|
-
|
|
3960
|
-
const cachedVersion = this.getVersion(namespaceKey);
|
|
3961
|
-
if (cachedVersion) {
|
|
3962
|
-
extraHeaders["If-None-Match"] = `W/"${cachedVersion}"`;
|
|
3963
|
-
}
|
|
3964
|
-
const response = await this.makeRequest(url, "GET", void 0, extraHeaders);
|
|
3965
|
-
if (response.status === 304) {
|
|
3966
|
-
logger.debug(`Namespace ContentUnits ${namespaceKey}: 304 Not Modified`);
|
|
3967
|
-
return null;
|
|
3968
|
-
}
|
|
3969
|
-
if (!response.ok) {
|
|
3970
|
-
this.handleHttpError(response, namespaceKey);
|
|
3971
|
-
}
|
|
3972
|
-
const data = await response.json();
|
|
3973
|
-
logger.debug("API response data:", data);
|
|
3974
|
-
if (!data.translations) {
|
|
3975
|
-
throw new TranslationNotFoundError(namespaceKey);
|
|
3976
|
-
}
|
|
3977
|
-
const version = data.meta?.version != null ? String(data.meta.version) : null;
|
|
3978
|
-
if (version) {
|
|
3979
|
-
this.setVersion(namespaceKey, version);
|
|
3980
|
-
}
|
|
3981
|
-
return data.translations;
|
|
4233
|
+
return await this.fetchNamespaceRaw(locale, namespace);
|
|
3982
4234
|
} catch (error) {
|
|
3983
4235
|
this.handleCaughtError(error);
|
|
3984
4236
|
}
|
|
@@ -4088,25 +4340,37 @@ var _NetworkRepository = class _NetworkRepository {
|
|
|
4088
4340
|
getConfig() {
|
|
4089
4341
|
return this.config;
|
|
4090
4342
|
}
|
|
4091
|
-
/** Read persisted version
|
|
4092
|
-
|
|
4093
|
-
|
|
4343
|
+
/** Read a persisted snapshot (version + payload), falling back to memory.
|
|
4344
|
+
* Legacy bare-version entries and corrupt snapshots are deleted on sight —
|
|
4345
|
+
* a version without its payload must never drive a conditional request. */
|
|
4346
|
+
getSnapshot(namespaceKey) {
|
|
4347
|
+
const mem = this.namespaceSnapshots.get(namespaceKey);
|
|
4094
4348
|
if (mem) return mem;
|
|
4095
4349
|
try {
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4350
|
+
localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
|
|
4351
|
+
const raw = localStorage.getItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
|
|
4352
|
+
if (raw) {
|
|
4353
|
+
const parsed = JSON.parse(raw);
|
|
4354
|
+
if (parsed && typeof parsed.version === "string" && parsed.version.length > 0 && parsed.translations && typeof parsed.translations === "object") {
|
|
4355
|
+
this.namespaceSnapshots.set(namespaceKey, parsed);
|
|
4356
|
+
return parsed;
|
|
4357
|
+
}
|
|
4358
|
+
localStorage.removeItem(_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey);
|
|
4100
4359
|
}
|
|
4101
4360
|
} catch {
|
|
4102
4361
|
}
|
|
4103
4362
|
return void 0;
|
|
4104
4363
|
}
|
|
4105
|
-
/** Persist version
|
|
4106
|
-
|
|
4107
|
-
|
|
4364
|
+
/** Persist version + payload as ONE entry (memory + localStorage). On quota
|
|
4365
|
+
* failure the previous stored snapshot stays — still a consistent pair. */
|
|
4366
|
+
setSnapshot(namespaceKey, snapshot) {
|
|
4367
|
+
this.namespaceSnapshots.set(namespaceKey, snapshot);
|
|
4108
4368
|
try {
|
|
4109
|
-
localStorage.setItem(
|
|
4369
|
+
localStorage.setItem(
|
|
4370
|
+
_NetworkRepository.SNAPSHOT_PREFIX + namespaceKey,
|
|
4371
|
+
JSON.stringify(snapshot)
|
|
4372
|
+
);
|
|
4373
|
+
localStorage.removeItem(_NetworkRepository.LEGACY_VERSION_PREFIX + namespaceKey);
|
|
4110
4374
|
} catch {
|
|
4111
4375
|
}
|
|
4112
4376
|
}
|
|
@@ -4195,8 +4459,11 @@ var _NetworkRepository = class _NetworkRepository {
|
|
|
4195
4459
|
}
|
|
4196
4460
|
}
|
|
4197
4461
|
};
|
|
4198
|
-
/** localStorage key prefix for persisted
|
|
4199
|
-
__publicField(_NetworkRepository, "
|
|
4462
|
+
/** localStorage key prefix for persisted snapshots (version + payload) */
|
|
4463
|
+
__publicField(_NetworkRepository, "SNAPSHOT_PREFIX", "lionrapid:ns:");
|
|
4464
|
+
/** Legacy bare-version prefix — a version WITHOUT its payload is exactly the
|
|
4465
|
+
* desync the snapshot store exists to prevent; entries are deleted on sight. */
|
|
4466
|
+
__publicField(_NetworkRepository, "LEGACY_VERSION_PREFIX", "lionrapid:v:");
|
|
4200
4467
|
var NetworkRepository = _NetworkRepository;
|
|
4201
4468
|
|
|
4202
4469
|
// src/content/validator/ContentValidator.ts
|
|
@@ -4352,6 +4619,159 @@ var ContentValidator = class {
|
|
|
4352
4619
|
}
|
|
4353
4620
|
};
|
|
4354
4621
|
|
|
4622
|
+
// src/content/splitter/BlockSplitter.ts
|
|
4623
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
4624
|
+
"title",
|
|
4625
|
+
"p",
|
|
4626
|
+
"h1",
|
|
4627
|
+
"h2",
|
|
4628
|
+
"h3",
|
|
4629
|
+
"h4",
|
|
4630
|
+
"h5",
|
|
4631
|
+
"h6",
|
|
4632
|
+
"li",
|
|
4633
|
+
"td",
|
|
4634
|
+
"th",
|
|
4635
|
+
"label",
|
|
4636
|
+
"button",
|
|
4637
|
+
"figcaption",
|
|
4638
|
+
"dt",
|
|
4639
|
+
"dd",
|
|
4640
|
+
"summary",
|
|
4641
|
+
"caption"
|
|
4642
|
+
]);
|
|
4643
|
+
var INLINE_BLOCK_TAGS = /* @__PURE__ */ new Set(["a"]);
|
|
4644
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
4645
|
+
"script",
|
|
4646
|
+
"style",
|
|
4647
|
+
"code",
|
|
4648
|
+
"pre",
|
|
4649
|
+
"textarea",
|
|
4650
|
+
"noscript",
|
|
4651
|
+
"svg"
|
|
4652
|
+
]);
|
|
4653
|
+
var TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:=(?:"[^"]*"|'[^']*'|[^\s>]*))?)*)\s*(\/?)>/g;
|
|
4654
|
+
function hasNoTranslateAttr(attrsString) {
|
|
4655
|
+
if (attrsString.includes("data-no-translate")) return true;
|
|
4656
|
+
return /translate\s*=\s*["']no["']/.test(attrsString);
|
|
4657
|
+
}
|
|
4658
|
+
function containsBlockTag(innerHTML) {
|
|
4659
|
+
TAG_REGEX.lastIndex = 0;
|
|
4660
|
+
let match;
|
|
4661
|
+
while ((match = TAG_REGEX.exec(innerHTML)) !== null) {
|
|
4662
|
+
const [, closingSlash, tagName, , selfClosingSlash] = match;
|
|
4663
|
+
if (!closingSlash && !selfClosingSlash && BLOCK_TAGS.has(tagName.toLowerCase())) {
|
|
4664
|
+
return true;
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
return false;
|
|
4668
|
+
}
|
|
4669
|
+
function findCloseTag(html, pos, tagName) {
|
|
4670
|
+
const lowerTag = tagName.toLowerCase();
|
|
4671
|
+
let depth = 1;
|
|
4672
|
+
TAG_REGEX.lastIndex = pos;
|
|
4673
|
+
let match;
|
|
4674
|
+
while ((match = TAG_REGEX.exec(html)) !== null) {
|
|
4675
|
+
const [, closingSlash, matchedTag, , selfClosingSlash] = match;
|
|
4676
|
+
if (matchedTag.toLowerCase() !== lowerTag) continue;
|
|
4677
|
+
if (closingSlash) {
|
|
4678
|
+
depth--;
|
|
4679
|
+
if (depth === 0) return match.index;
|
|
4680
|
+
} else if (!selfClosingSlash) {
|
|
4681
|
+
depth++;
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
return -1;
|
|
4685
|
+
}
|
|
4686
|
+
function splitBlocks(html) {
|
|
4687
|
+
const blocks = [];
|
|
4688
|
+
let pos = 0;
|
|
4689
|
+
let skipDepth = 0;
|
|
4690
|
+
let iter = 0;
|
|
4691
|
+
const maxIter = 1e5;
|
|
4692
|
+
const noTranslateEnds = [];
|
|
4693
|
+
while (pos < html.length && iter < maxIter) {
|
|
4694
|
+
iter++;
|
|
4695
|
+
const ltPos = html.indexOf("<", pos);
|
|
4696
|
+
if (ltPos === -1) break;
|
|
4697
|
+
if (html.startsWith("<!--", ltPos)) {
|
|
4698
|
+
const commentEnd = html.indexOf("-->", ltPos + 4);
|
|
4699
|
+
if (commentEnd === -1) break;
|
|
4700
|
+
pos = commentEnd + 3;
|
|
4701
|
+
continue;
|
|
4702
|
+
}
|
|
4703
|
+
TAG_REGEX.lastIndex = ltPos;
|
|
4704
|
+
const match = TAG_REGEX.exec(html);
|
|
4705
|
+
if (!match || match.index !== ltPos) {
|
|
4706
|
+
pos = ltPos + 1;
|
|
4707
|
+
continue;
|
|
4708
|
+
}
|
|
4709
|
+
const [fullMatch, closingSlash, tagName, attrsString, selfClosingSlash] = match;
|
|
4710
|
+
const tag = tagName.toLowerCase();
|
|
4711
|
+
const afterMatch = match.index + fullMatch.length;
|
|
4712
|
+
if (SKIP_TAGS.has(tag)) {
|
|
4713
|
+
if (closingSlash) {
|
|
4714
|
+
if (skipDepth > 0) skipDepth--;
|
|
4715
|
+
} else if (!selfClosingSlash) {
|
|
4716
|
+
skipDepth++;
|
|
4717
|
+
}
|
|
4718
|
+
pos = afterMatch;
|
|
4719
|
+
continue;
|
|
4720
|
+
}
|
|
4721
|
+
if (skipDepth > 0) {
|
|
4722
|
+
pos = afterMatch;
|
|
4723
|
+
continue;
|
|
4724
|
+
}
|
|
4725
|
+
while (noTranslateEnds.length && pos >= noTranslateEnds[noTranslateEnds.length - 1]) {
|
|
4726
|
+
noTranslateEnds.pop();
|
|
4727
|
+
}
|
|
4728
|
+
if (!closingSlash && !selfClosingSlash && hasNoTranslateAttr(attrsString)) {
|
|
4729
|
+
const regionEnd = findCloseTag(html, afterMatch, tagName);
|
|
4730
|
+
if (regionEnd !== -1) {
|
|
4731
|
+
noTranslateEnds.push(regionEnd);
|
|
4732
|
+
}
|
|
4733
|
+
}
|
|
4734
|
+
if (noTranslateEnds.length > 0) {
|
|
4735
|
+
pos = afterMatch;
|
|
4736
|
+
continue;
|
|
4737
|
+
}
|
|
4738
|
+
if ((BLOCK_TAGS.has(tag) || INLINE_BLOCK_TAGS.has(tag)) && !closingSlash && !selfClosingSlash) {
|
|
4739
|
+
const closeTagStart = findCloseTag(html, afterMatch, tagName);
|
|
4740
|
+
if (closeTagStart === -1) {
|
|
4741
|
+
pos = afterMatch;
|
|
4742
|
+
continue;
|
|
4743
|
+
}
|
|
4744
|
+
const innerHTML = html.slice(afterMatch, closeTagStart);
|
|
4745
|
+
if (containsBlockTag(innerHTML)) {
|
|
4746
|
+
pos = afterMatch;
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4749
|
+
blocks.push({
|
|
4750
|
+
tag,
|
|
4751
|
+
openTag: fullMatch,
|
|
4752
|
+
attrs: attrsString,
|
|
4753
|
+
inner: innerHTML,
|
|
4754
|
+
innerStart: afterMatch,
|
|
4755
|
+
innerEnd: closeTagStart
|
|
4756
|
+
});
|
|
4757
|
+
TAG_REGEX.lastIndex = closeTagStart;
|
|
4758
|
+
const closeMatch = TAG_REGEX.exec(html);
|
|
4759
|
+
pos = closeMatch ? closeMatch.index + closeMatch[0].length : closeTagStart + 1;
|
|
4760
|
+
continue;
|
|
4761
|
+
}
|
|
4762
|
+
pos = afterMatch;
|
|
4763
|
+
}
|
|
4764
|
+
return { blocks };
|
|
4765
|
+
}
|
|
4766
|
+
function replaceBlockInners(html, replacements) {
|
|
4767
|
+
const ordered = [...replacements].sort((a, b) => b.block.innerStart - a.block.innerStart);
|
|
4768
|
+
let result = html;
|
|
4769
|
+
for (const { block, inner } of ordered) {
|
|
4770
|
+
result = result.slice(0, block.innerStart) + inner + result.slice(block.innerEnd);
|
|
4771
|
+
}
|
|
4772
|
+
return result;
|
|
4773
|
+
}
|
|
4774
|
+
|
|
4355
4775
|
// src/browser/md5.ts
|
|
4356
4776
|
function safeAdd(x, y) {
|
|
4357
4777
|
const lsw = (x & 65535) + (y & 65535);
|
|
@@ -4557,6 +4977,75 @@ var ExcludeFilter = class {
|
|
|
4557
4977
|
}
|
|
4558
4978
|
};
|
|
4559
4979
|
|
|
4980
|
+
// src/browser/sanitizer.ts
|
|
4981
|
+
var REMOVE_TAGS = /* @__PURE__ */ new Set([
|
|
4982
|
+
"script",
|
|
4983
|
+
"iframe",
|
|
4984
|
+
"frame",
|
|
4985
|
+
"frameset",
|
|
4986
|
+
"object",
|
|
4987
|
+
"embed",
|
|
4988
|
+
"applet",
|
|
4989
|
+
"base",
|
|
4990
|
+
"meta",
|
|
4991
|
+
"link",
|
|
4992
|
+
"template",
|
|
4993
|
+
"portal"
|
|
4994
|
+
]);
|
|
4995
|
+
var URL_ATTRS2 = /* @__PURE__ */ new Set([
|
|
4996
|
+
"href",
|
|
4997
|
+
"src",
|
|
4998
|
+
"action",
|
|
4999
|
+
"formaction",
|
|
5000
|
+
"poster",
|
|
5001
|
+
"data",
|
|
5002
|
+
"xlink:href"
|
|
5003
|
+
]);
|
|
5004
|
+
function sanitizeElement(root) {
|
|
5005
|
+
const all = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
5006
|
+
for (const el of all) {
|
|
5007
|
+
const tag = el.tagName.toLowerCase();
|
|
5008
|
+
if (REMOVE_TAGS.has(tag)) {
|
|
5009
|
+
el.remove();
|
|
5010
|
+
continue;
|
|
5011
|
+
}
|
|
5012
|
+
for (const name of el.getAttributeNames()) {
|
|
5013
|
+
const lower = name.toLowerCase();
|
|
5014
|
+
if (lower.startsWith("on") || lower === "srcdoc") {
|
|
5015
|
+
el.removeAttribute(name);
|
|
5016
|
+
continue;
|
|
5017
|
+
}
|
|
5018
|
+
if (URL_ATTRS2.has(lower)) {
|
|
5019
|
+
if (sanitizeUrl(el.getAttribute(name) ?? "") === null) {
|
|
5020
|
+
el.removeAttribute(name);
|
|
5021
|
+
}
|
|
5022
|
+
continue;
|
|
5023
|
+
}
|
|
5024
|
+
if (lower === "style") {
|
|
5025
|
+
const clean = sanitizeCssDeclarations(el.getAttribute(name) ?? "");
|
|
5026
|
+
if (clean === null) el.removeAttribute(name);
|
|
5027
|
+
else el.setAttribute(name, clean);
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
function sanitizeStyleSheetText(css, baseHref) {
|
|
5033
|
+
let out = css.replace(/@import[^;]*(;|$)/gi, "");
|
|
5034
|
+
out = out.replace(
|
|
5035
|
+
/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi,
|
|
5036
|
+
(full, _quote, target) => {
|
|
5037
|
+
try {
|
|
5038
|
+
const resolved = new URL(target, baseHref);
|
|
5039
|
+
if (resolved.protocol === "data:") return full;
|
|
5040
|
+
if (resolved.origin === new URL(baseHref).origin) return full;
|
|
5041
|
+
} catch {
|
|
5042
|
+
}
|
|
5043
|
+
return 'url("")';
|
|
5044
|
+
}
|
|
5045
|
+
);
|
|
5046
|
+
return out.replace(/expression\s*\(/gi, "blocked(");
|
|
5047
|
+
}
|
|
5048
|
+
|
|
4560
5049
|
// src/browser/storage/localstorage-repository.ts
|
|
4561
5050
|
var STORAGE_PREFIX = "lionrapid:";
|
|
4562
5051
|
var VERSION_KEY = `${STORAGE_PREFIX}version`;
|