Files
script/substore/common.js
T

295 lines
10 KiB
JavaScript

/**
* Sub-Store 节点命名公共工具
*
* 远程引用地址:
* https://git.orionc.me/orion/script/raw/branch/main/substore/common.js?v=20260603-3
*/
(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 misplacedSequenceMatch = String(name).match(/^([A-Z]{2}(?:\s+[A-Z]{3})?)\s+01\s+(\d{1,2})-(.+)$/);
if (misplacedSequenceMatch) {
return [
misplacedSequenceMatch[1],
misplacedSequenceMatch[2].padStart(2, '0'),
misplacedSequenceMatch[3].trim()
]
.filter(Boolean)
.join(' ')
.replace(/\s{2,}/g, ' ')
.trim();
}
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 [regionCode, tokens.join('-')]
.filter(Boolean)
.join(' ')
.replace(/\s{2,}/g, ' ')
.trim();
}
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
};
})(globalThis);