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