diff --git a/substore/rename.js b/substore/rename.js index 8febb58..6477aeb 100644 --- a/substore/rename.js +++ b/substore/rename.js @@ -7,7 +7,11 @@ function operator(proxies) { const globalCount = Object.create(null); const currentCount = Object.create(null); + const serialCount = Object.create(null); + const currentSerial = Object.create(null); + const countryOrder = Object.create(null); const baseNames = new Array(proxies.length); + const renamedItems = new Array(proxies.length); const suffixRegexCache = Object.create(null); const regionAliases = [ @@ -29,6 +33,7 @@ function operator(proxies) { ['United States Portland', 'US PDX'], ['United States Boston', 'US BOS'], ['United States Seattle', 'US SEA'], + ['United States Hawaii', 'US HNL'], ['Australia Sydney', 'AU SYD'], ['Australia Melbourne', 'AU MEL'], ['Australia Brisbane', 'AU BNE'], @@ -92,6 +97,7 @@ function operator(proxies) { ['USA Portland', 'US PDX'], ['USA Boston', 'US BOS'], ['USA Seattle', 'US SEA'], + ['USA Hawaii', 'US HNL'], ['United Kingdom', 'UK'], ['United States', 'US'], ['South Africa', 'ZA'], @@ -314,6 +320,7 @@ function operator(proxies) { ['美国拉斯维加斯', 'US LAS'], ['美国波特兰', 'US PDX'], ['美国波士顿', 'US BOS'], + ['美国夏威夷', 'US HNL'], ['日本东京', 'JP TYO'], ['日本大阪', 'JP OSA'], ['日本名古屋', 'JP NGO'], @@ -443,6 +450,7 @@ function operator(proxies) { ['德国', 'DE'], ['荷兰', 'NL'], ['法国', 'FR'], + ['美西', 'US'], ['巴西', 'BR'], ['智利', 'CL'], ['澳门', 'MO'], @@ -464,6 +472,7 @@ function operator(proxies) { ['捷克', 'CZ'], ['沙特', 'SA'], ['墨西哥', 'MX'], + ['夏威夷', 'US HNL'], ['迪拜', 'AE DXB'], ['悉尼', 'AU SYD'], ['墨尔本', 'AU MEL'], @@ -545,6 +554,9 @@ function operator(proxies) { const knownRegionCodes = new Set( regionAliases.map(([, replacement]) => replacement.split(/\s+/)[0]) ); + const knownRegionPrefixes = new Set( + regionAliases.map(([, replacement]) => replacement) + ); Object.values(flagCodeOverrides).forEach(code => knownRegionCodes.add(code)); const escapeRegex = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -568,6 +580,26 @@ function operator(proxies) { .join(''); }; + const regionCodeToFlag = regionCode => { + if (!regionCode) return ''; + + const flagCode = regionCode === 'UK' ? 'GB' : regionCode; + if (!/^[A-Z]{2}$/.test(flagCode)) return ''; + + return flagCode + .split('') + .map(char => String.fromCodePoint(char.charCodeAt(0) - 65 + 0x1F1E6)) + .join(''); + }; + + const normalizedFlagForCore = (flag, coreName) => { + if (!flag) return ''; + + const regionPrefix = leadingRegionPrefixOf(coreName) || regionPrefixInName(coreName); + const regionCode = regionPrefix.split(/\s+/)[0]; + return regionCodeToFlag(regionCode) || flag; + }; + const stripExistingSuffix = (name, subName) => { if (!subName) return name.trim(); const suffixRegex = suffixRegexCache[subName] || ( @@ -639,18 +671,171 @@ function operator(proxies) { .replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\uFE0F]/gu, '') .replace(/\u200d/g, '') .replace(/\s*\|\s*流媒体/gi, '') - .replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1x') - .replace(/(\d+(?:\.\d+)?)倍/gi, '$1x') - .replace(/\b(\d+(?:\.\d+)?)X\b/g, '$1x') + .replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1X') + .replace(/(\d+(?:\.\d+)?)倍/gi, '$1X') + .replace(/\b(\d+(?:\.\d+)?)x\b/gi, '$1X') .replace(/([A-Z]{2})\s*(专属|家宽|AI|高速|专线)(\d+)/g, '$1 $2 $3') .replace(/\b(AI)(\d+)/g, '$1 $2') .replace(/\b(Pro-)\s+/gi, '$1') .replace(/^((?:Pro-\s*)?)([A-Z]{2})(?=[\u4e00-\u9fff]|\d)/u, '$1$2 ') + .replace(/\s*\|\s*/g, '|') .replace(/\s+\[/g, ' [') .replace(/\s{2,}/g, ' ') .trim(); }; + const isProviderTag = tag => /^(JC|毕业)$/i.test(tag.trim()); + const isRouteTag = tag => /^(三网\d*|COP|CM)$/i.test(tag.trim()); + + const splitPipeFields = name => name + .split('|') + .map(part => part.trim()) + .filter(Boolean); + + const cleanupPipeFields = name => { + const fields = splitPipeFields(name); + if (fields.length <= 1) return name.trim(); + + const [head, ...tailFields] = fields; + const keptFields = tailFields.filter(field => !isRouteTag(field)); + return [head, ...keptFields].join('|').trim(); + }; + + const leadingRegionPrefixOf = name => { + const match = name.match(/^([A-Z]{2})(?:\s+([A-Z]{3}))?(?=\s|$|-|\d|\|)/); + if (!match) return ''; + + const fullPrefix = match[2] ? `${match[1]} ${match[2]}` : match[1]; + if (match[2] && knownRegionPrefixes.has(fullPrefix)) return fullPrefix; + + return knownRegionCodes.has(match[1]) ? match[1] : ''; + }; + + const hasAliasInName = (name, alias, aliasLower) => { + const nameLower = name.toLowerCase(); + const aliasIndex = nameLower.indexOf(aliasLower); + if (aliasIndex === -1) return false; + + if (/^[A-Za-z]/.test(alias)) { + const before = aliasIndex > 0 ? name[aliasIndex - 1] : ''; + const after = name[aliasIndex + alias.length] || ''; + return !/[A-Za-z]/.test(before) && !/[A-Za-z]/.test(after); + } + + return true; + }; + + const regionPrefixInName = name => { + for (const [alias, replacement, aliasLower] of sortedRegionAliases) { + if (hasAliasInName(name, alias, aliasLower)) return replacement; + } + + return leadingRegionPrefixOf(name); + }; + + const multiplierOf = name => { + const match = name.match(/\b(\d+(?:\.\d+)?X)\b/i); + return match ? match[1].toUpperCase() : ''; + }; + + const compactHomeBroadbandName = name => { + if (!/家宽/.test(name)) return name; + + const regionPrefix = /(?:Hinet|Seednet)/i.test(name) ? 'TW' : regionPrefixInName(name); + const multiplier = multiplierOf(name); + const compactFields = [regionPrefix, multiplier].filter(Boolean); + + return compactFields.join(' ').trim(); + }; + + const compactRegionOnlyName = name => { + const regionPrefix = regionPrefixInName(name); + return regionPrefix || name; + }; + + const compactKnownLineName = name => { + const cleanName = cleanupPipeFields(name); + const compactName = /家宽/.test(cleanName) + ? compactHomeBroadbandName(cleanName) + : compactRegionOnlyName(cleanName); + + return compactName + .replace(/\s*\|\s*/g, '|') + .replace(/\s{2,}/g, ' ') + .trim(); + }; + + const serialGroupOf = coreName => { + const nameWithoutFlag = coreName.replace(leadingFlagRegex, '').trim(); + const regionPrefix = leadingRegionPrefixOf(nameWithoutFlag) || regionPrefixInName(nameWithoutFlag); + if (!regionPrefix) return ''; + + return `${regionPrefix.split(/\s+/)[0]}|region`; + }; + + const appendSerialToCoreName = (coreName, serialNumber) => { + const fields = splitPipeFields(coreName); + const serialText = String(serialNumber).padStart(2, '0'); + + if (fields.length <= 1) return `${coreName} ${serialText}`; + + const providers = []; + const bodyFields = []; + fields.forEach((field, fieldIndex) => { + if (fieldIndex > 0 && isProviderTag(field)) { + providers.push(field); + } else { + bodyFields.push(field); + } + }); + + const body = `${bodyFields.join('|')} ${serialText}`.trim(); + return providers.length ? `${body}|${providers.join('|')}` : body; + }; + + const sortInfoOf = (coreName, originalIndex) => { + const nameWithoutFlag = coreName.replace(leadingFlagRegex, '').trim(); + const regionPrefix = leadingRegionPrefixOf(nameWithoutFlag) || regionPrefixInName(nameWithoutFlag); + const [countryCode, cityCode = ''] = regionPrefix.split(/\s+/); + const multiplierMatch = nameWithoutFlag.match(/\b(\d+(?:\.\d+)?)X\b/i); + + return { + originalIndex, + countryCode: countryCode || '', + cityCode, + regionPrefix, + multiplier: multiplierMatch ? Number(multiplierMatch[1]) : Number.NEGATIVE_INFINITY + }; + }; + + const compareSortInfo = (firstInfo, secondInfo) => { + if (!firstInfo.countryCode || !secondInfo.countryCode) { + return firstInfo.originalIndex - secondInfo.originalIndex; + } + + const firstCountryOrder = countryOrder[firstInfo.countryCode]; + const secondCountryOrder = countryOrder[secondInfo.countryCode]; + if (firstCountryOrder !== secondCountryOrder) { + return firstCountryOrder - secondCountryOrder; + } + + if (firstInfo.multiplier !== secondInfo.multiplier) { + return firstInfo.multiplier - secondInfo.multiplier; + } + + const firstCityRank = firstInfo.cityCode ? 1 : 0; + const secondCityRank = secondInfo.cityCode ? 1 : 0; + if (firstCityRank !== secondCityRank) { + return firstCityRank - secondCityRank; + } + + if (firstInfo.regionPrefix !== secondInfo.regionPrefix) { + return firstInfo.regionPrefix < secondInfo.regionPrefix ? -1 : 1; + } + + return firstInfo.originalIndex - secondInfo.originalIndex; + }; + const normalizeBilingualName = name => { const bilingualMatch = name.match(/^(.+?)\s*|\s*(.+)$/); if (!bilingualMatch) return name; @@ -714,9 +899,10 @@ function operator(proxies) { core = joinAlias(flagRegionCode, core); } - core = simplifyCoreName(core); + core = compactKnownLineName(simplifyCoreName(core)); - return flag ? `${flag} ${core}` : core; + const normalizedFlag = normalizedFlagForCore(flag, core); + return normalizedFlag ? `${normalizedFlag} ${core}` : core; }; proxies.forEach((proxy, proxyIndex) => { @@ -725,13 +911,53 @@ function operator(proxies) { const coreName = stripExistingSuffix(proxyName, subName); const normalizedCoreName = normalizeCoreName(coreName); const baseName = subName ? `${normalizedCoreName} - ${subName}` : normalizedCoreName; + const serialGroup = serialGroupOf(normalizedCoreName); + const scopedSerialGroup = serialGroup ? `${serialGroup}|${subName}` : ''; + const sortInfo = sortInfoOf(normalizedCoreName, proxyIndex); + + if (sortInfo.countryCode && countryOrder[sortInfo.countryCode] === undefined) { + countryOrder[sortInfo.countryCode] = Object.keys(countryOrder).length; + } baseNames[proxyIndex] = baseName; + renamedItems[proxyIndex] = { + normalizedCoreName, + subName, + scopedSerialGroup, + sortInfo + }; globalCount[baseName] = (globalCount[baseName] || 0) + 1; + if (scopedSerialGroup) { + serialCount[scopedSerialGroup] = (serialCount[scopedSerialGroup] || 0) + 1; + } }); - return proxies.map((proxy, proxyIndex) => { + const sortedProxyIndexes = proxies + .map((proxy, proxyIndex) => proxyIndex) + .sort((firstIndex, secondIndex) => { + return compareSortInfo( + renamedItems[firstIndex].sortInfo, + renamedItems[secondIndex].sortInfo + ); + }); + + return sortedProxyIndexes.map(proxyIndex => { + const proxy = proxies[proxyIndex]; const baseName = baseNames[proxyIndex]; + const renamedItem = renamedItems[proxyIndex]; + + if (renamedItem.scopedSerialGroup) { + currentSerial[renamedItem.scopedSerialGroup] = ( + currentSerial[renamedItem.scopedSerialGroup] || 0 + ) + 1; + + const serialCoreName = appendSerialToCoreName( + renamedItem.normalizedCoreName, + currentSerial[renamedItem.scopedSerialGroup] + ); + proxy.name = renamedItem.subName ? `${serialCoreName} - ${renamedItem.subName}` : serialCoreName; + return proxy; + } if (globalCount[baseName] > 1) { currentCount[baseName] = (currentCount[baseName] || 0) + 1;