/** * Sub-Store 节点命名公共工具 * * 远程引用地址: * https://git.orionc.me/orion/script/raw/branch/main/substore/common.js */ (function (root) { const regexSpecialChars = new Set(['.', '*', '+', '?', '^', '$', '{', '}', '(', ')', '|', '[', ']', '\\']); const leadingFlagRegex = /^([\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*/u; const inlineFlagRegex = /^(.{1,24}?)([\u{1F1E6}-\u{1F1FF}]{2}|🏴‍☠️)\s*(.*)$/u; const escapeRegex = value => [...String(value)] .map(char => regexSpecialChars.has(char) ? `\\${char}` : char) .join(''); const normalizeWhitespace = value => String(value) .replace(/\s{2,}/g, ' ') .trim(); const normalizeLeadingFlagSpacing = value => String(value) .replace(leadingFlagRegex, '$1 ') .replace(/\s{2,}/g, ' ') .trim(); const flagToRegionCode = (flag, overrides) => { if (!flag) return ''; if (overrides && overrides[flag]) return overrides[flag]; const codePoints = [...flag].map(char => char.codePointAt(0)); if ( codePoints.length !== 2 || codePoints.some(codePoint => codePoint < 0x1F1E6 || codePoint > 0x1F1FF) ) { return ''; } const regionCode = codePoints .map(codePoint => String.fromCharCode(codePoint - 0x1F1E6 + 65)) .join(''); return regionCode === 'GB' ? 'UK' : regionCode; }; const regionCodeToFlag = regionCode => { 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 splitLeadingFlag = name => { const leadingFlagMatch = String(name).match(leadingFlagRegex); if (!leadingFlagMatch) return { flag: '', text: String(name).trim() }; return { flag: leadingFlagMatch[1], text: String(name).slice(leadingFlagMatch[0].length).trim() }; }; const splitLeadingOrInlineFlag = name => { const cleanName = String(name); const leadingFlagMatch = cleanName.match(leadingFlagRegex); if (leadingFlagMatch) { return { flag: leadingFlagMatch[1], text: cleanName.slice(leadingFlagMatch[0].length).trim() }; } const inlineFlagMatch = cleanName.match(inlineFlagRegex); if (!inlineFlagMatch) return { flag: '', text: cleanName.trim() }; const prefix = inlineFlagMatch[1].trim(); const rest = inlineFlagMatch[3].trim(); return { flag: inlineFlagMatch[2], text: rest ? `${prefix} ${rest}`.trim() : prefix }; }; const sortRegionAliases = aliases => aliases .map(([alias, replacement]) => [alias, replacement, String(alias).toLowerCase()]) .sort((firstAlias, secondAlias) => secondAlias[0].length - firstAlias[0].length); const createRegionResolver = aliases => { const sortedAliases = sortRegionAliases(aliases); const regionCodeOf = name => { const cleanName = String(name); const leadingFlagMatch = cleanName.match(leadingFlagRegex); if (leadingFlagMatch) return flagToRegionCode(leadingFlagMatch[1]); const cleanNameLower = cleanName.toLowerCase(); for (const [alias, replacement, aliasLower] of sortedAliases) { if (/^[A-Z]{2}$/.test(alias)) { const codeMatch = cleanName.match(new RegExp(`(^|[^A-Za-z])${escapeRegex(alias)}([^A-Za-z]|$)`, 'i')); if (codeMatch) return replacement; continue; } if (cleanNameLower.includes(aliasLower)) return replacement; } return ''; }; const aliasRegionPrefixOf = name => { const cleanNameLower = String(name).toLowerCase(); for (const [, replacement, aliasLower] of sortedAliases) { if (cleanNameLower.includes(aliasLower)) return replacement; } return ''; }; const normalizeLeadingRegion = name => { const cleanName = String(name).trim(); const cleanNameLower = cleanName.toLowerCase(); for (const [alias, replacement, aliasLower] of sortedAliases) { if (!cleanNameLower.startsWith(aliasLower)) continue; const rest = cleanName.slice(alias.length); if (/^[A-Za-z]/.test(alias) && rest && !/^[\s\d[|\-.x]/i.test(rest)) { continue; } return joinAlias(replacement, rest); } return cleanName; }; return { sortedAliases, regionCodeOf, aliasRegionPrefixOf, normalizeLeadingRegion }; }; const joinAlias = (replacement, rest) => { const cleanRest = String(rest || '').trim(); if (!cleanRest) return replacement; if (/^[|/\-]/.test(cleanRest)) { return `${replacement}${cleanRest}`; } return `${replacement} ${cleanRest}`; }; const normalizeMultiplierText = value => String(value) .replace(/-\s*(\d+(?:\.\d+)?)倍/gi, ' $1X') .replace(/(\d+(?:\.\d+)?)倍/gi, '$1X') .replace(/\b(\d+(?:\.\d+)?)x\b/gi, '$1X'); const moveMultiplierToEnd = name => { const multipliers = []; const nameWithoutMultiplier = String(name) .replace(/\b(\d+(?:\.\d+)?X)\b/gi, multiplier => { multipliers.push(multiplier.toUpperCase()); return ' '; }) .replace(/\s+\[/g, ' [') .replace(/\s{2,}/g, ' ') .trim(); if (!multipliers.length) return name; return `${nameWithoutMultiplier} ${multipliers.join(' ')}`.trim(); }; const normalizeRegionSequence = (name, options) => { const allowCityCode = Boolean(options && options.allowCityCode); const regionPattern = allowCityCode ? '[A-Z]{2}(?:\\s+[A-Z]{3})?' : '[A-Z]{2}'; return String(name) .replace(new RegExp(`(^|\\s)(${regionPattern})[\\s|\\-]*(\\d{1,2})(?=$|[\\s|\\-[\\]])`, 'g'), ( match, prefix, regionCode, sequenceNumber ) => { return `${prefix}${regionCode} ${sequenceNumber.padStart(2, '0')}`; }) .replace(new RegExp(`(\\b${regionPattern} \\d{2})-\\s*(?=\\d+(?:\\.\\d+)?X\\b)`, 'g'), '$1 ') .replace(/\s{2,}/g, ' ') .trim(); }; const normalizeHyphenatedRegionFeatures = name => { const hyphenatedMatch = String(name).match(/^([A-Z]{2})(?:\s+([A-Z]{3}))?-(.+)$/); if (!hyphenatedMatch) return name; const countryCode = hyphenatedMatch[1]; let regionCode = hyphenatedMatch[2] ? `${countryCode} ${hyphenatedMatch[2]}` : countryCode; const tokens = hyphenatedMatch[3].split('-').map(token => token.trim()).filter(Boolean); if ( !hyphenatedMatch[2] && tokens.length >= 2 && /^[A-Z]{3}$/.test(tokens[0]) && /^\d{1,2}$/.test(tokens[1]) ) { regionCode = `${countryCode} ${tokens.shift()}`; } const sequenceIndex = tokens.findIndex(token => /^\d{1,2}$/.test(token)); if (sequenceIndex === -1) return name; const sequenceNumber = tokens[sequenceIndex].padStart(2, '0'); const ingressTokens = tokens .slice(0, sequenceIndex) .map(token => token.toUpperCase()); const egressTokens = tokens.slice(sequenceIndex + 1); const featureText = ingressTokens.length && egressTokens.length ? `${ingressTokens.join('-')}-${egressTokens.join('-')}` : [...ingressTokens, ...egressTokens].join('-'); return [regionCode, sequenceNumber, featureText] .filter(Boolean) .join(' ') .replace(/\s{2,}/g, ' ') .trim(); }; const stripLineDescriptors = (name, descriptorKeywords) => { const descriptorPattern = descriptorKeywords.map(escapeRegex).join('|'); const descriptorOnlyRegex = new RegExp(`^(?:${descriptorPattern})+$`, 'i'); const embeddedDescriptorRegex = new RegExp(`\\s*(?:${descriptorPattern})(?=\\s|\\d|$)`, 'gi'); const bracketDescriptorRegex = new RegExp(`\\s*\\[(?:${descriptorPattern})(?:\\s+(?:${descriptorPattern}))*]\\s*`, 'gi'); const [head, ...segments] = String(name).split('|'); const keptSegments = segments .map(segment => segment.trim()) .filter(segment => segment && !descriptorOnlyRegex.test(segment)); const cleanHead = head .replace(bracketDescriptorRegex, ' ') .replace(embeddedDescriptorRegex, '') .replace(leadingFlagRegex, '$1 ') .replace(/\s{2,}/g, ' ') .trim(); return [cleanHead, ...keptSegments] .filter(Boolean) .join('|') .replace(/\s+\|/g, '|') .replace(/\|\s+/g, '|') .trim(); }; root.SubStoreRenameUtils = { createRegionResolver, escapeRegex, flagToRegionCode, joinAlias, moveMultiplierToEnd, normalizeHyphenatedRegionFeatures, normalizeLeadingFlagSpacing, normalizeMultiplierText, normalizeRegionSequence, normalizeWhitespace, regionCodeToFlag, sortRegionAliases, splitLeadingFlag, splitLeadingOrInlineFlag, stripLineDescriptors }; })(typeof globalThis !== 'undefined' ? globalThis : this);