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
82 lines
2.6 KiB
TypeScript
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);
|
|
}
|