Files
l-ui/frontend/src/lib/xray/geoTokens.ts
T
Sanaei f727d04f65
Release 3X-UI for Docker / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
v3.7.0
2026-08-24 15:07:15 +02:00

82 lines
2.6 KiB
TypeScript

import type { GeoKind } from '@/generated/types';
const DEFAULT_SITE_FILE = 'geosite.dat';
const DEFAULT_IP_FILE = 'geoip.dat';
const LONG_FORMS: Array<[RegExp, string]> = [
[/^ext(?:-domain|-site)?:geosite\.dat:/, 'geosite:'],
[/^ext(?:-ip)?:geoip\.dat:/, 'geoip:'],
];
export function parseTokens(value: string): string[] {
return value
.split(',')
.map((token) => token.trim())
.filter((token) => token !== '');
}
export function formatTokens(tokens: string[]): string {
return tokens.join(', ');
}
export function tokenFor(file: string, code: string, kind: GeoKind): string {
if (kind === 'ip' && file === DEFAULT_IP_FILE) return `geoip:${code}`;
if (kind === 'site' && file === DEFAULT_SITE_FILE) return `geosite:${code}`;
return `ext:${file}:${code}`;
}
/**
* Xray treats category codes case-insensitively and accepts both the
* `geosite:cn` shorthand and its `ext:geosite.dat:cn` long form, so tokens are
* compared through this normal form. Only comparison uses it — whatever the
* user typed is what stays in the rule.
*/
export function canonicalToken(token: string): string {
const lowered = token.trim().toLowerCase();
for (const [pattern, shorthand] of LONG_FORMS) {
if (pattern.test(lowered)) return lowered.replace(pattern, shorthand);
}
return lowered;
}
export function selectionFromValue(value: string, known: ReadonlySet<string>): string[] {
const canonicalKnown = new Set([...known].map(canonicalToken));
const selection: string[] = [];
const seen = new Set<string>();
for (const token of parseTokens(value)) {
const canonical = canonicalToken(token);
if (!canonicalKnown.has(canonical) || seen.has(canonical)) continue;
seen.add(canonical);
selection.push(token);
}
return selection;
}
export function mergeSelection(
value: string,
selected: string[],
known: ReadonlySet<string>,
): string {
const canonicalKnown = new Set([...known].map(canonicalToken));
const kept = new Set(
selected.map((token) => canonicalToken(token)).filter((token) => token !== ''),
);
const merged: string[] = [];
const seen = new Set<string>();
const append = (token: string) => {
const canonical = canonicalToken(token);
if (canonical === '' || seen.has(canonical)) return;
seen.add(canonical);
merged.push(token);
};
for (const token of parseTokens(value)) {
const canonical = canonicalToken(token);
if (canonicalKnown.has(canonical) && !kept.has(canonical)) continue;
append(token);
}
for (const token of selected) {
append(token.trim());
}
return formatTokens(merged);
}