♻️ refactor(substore): 重构 regex 缓存机制并升级公共脚本版本

This commit is contained in:
2026-06-08 10:23:12 +08:00
parent afc8c70b95
commit 9c9b78ded9
8 changed files with 84 additions and 47 deletions
+46 -19
View File
@@ -2,14 +2,16 @@
* Sub-Store 节点命名公共工具
*
* 远程引用地址:
* https://git.orionc.me/orion/script/raw/branch/main/substore/common.js?v=20260603-7
* https://git.orionc.me/orion/script/raw/branch/main/substore/common.js?v=20260608-1
*/
(function (root) {
const VERSION = '20260603-7';
const VERSION = '20260608-1';
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 leadingMojibakeFlagRegex = /^((?:ð[\u0080-\u00BF]{3}){2})\s*/;
const regionSequenceRegexCache = Object.create(null);
const lineDescriptorRegexCache = Object.create(null);
const escapeRegex = value => [...String(value)]
.map(char => regexSpecialChars.has(char) ? `\\${char}` : char)
@@ -112,6 +114,14 @@
const createRegionResolver = aliases => {
const sortedAliases = sortRegionAliases(aliases);
const compiledAliases = sortedAliases.map(([alias, replacement, aliasLower]) => [
alias,
replacement,
aliasLower,
/^[A-Z]{2}$/.test(alias)
? new RegExp(`(^|[^A-Za-z])${escapeRegex(alias)}([^A-Za-z]|$)`, 'i')
: null
]);
const regionCodeOf = name => {
const cleanName = normalizeLeadingMojibakeFlag(name);
@@ -119,9 +129,9 @@
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'));
for (const [alias, replacement, aliasLower, codeRegex] of compiledAliases) {
if (codeRegex) {
const codeMatch = cleanName.match(codeRegex);
if (codeMatch) return replacement;
continue;
}
@@ -134,7 +144,7 @@
const aliasRegionPrefixOf = name => {
const cleanNameLower = stripLeadingMojibakeFlag(name).toLowerCase();
for (const [, replacement, aliasLower] of sortedAliases) {
for (const [, replacement, aliasLower] of compiledAliases) {
if (cleanNameLower.includes(aliasLower)) return replacement;
}
@@ -145,7 +155,7 @@
const cleanName = stripLeadingMojibakeFlag(name);
const cleanNameLower = cleanName.toLowerCase();
for (const [alias, replacement, aliasLower] of sortedAliases) {
for (const [alias, replacement, aliasLower] of compiledAliases) {
if (!cleanNameLower.startsWith(aliasLower)) continue;
const rest = cleanName.slice(alias.length);
@@ -198,12 +208,22 @@
const normalizeRegionSequence = (name, options) => {
const allowCityCode = Boolean(options && options.allowCityCode);
const regionPattern = allowCityCode
? '[A-Z]{2}(?:\\s+[A-Z]{3})?'
: '[A-Z]{2}';
const cacheKey = allowCityCode ? 'city' : 'country';
const cachedRegexes = regionSequenceRegexCache[cacheKey] || (
regionSequenceRegexCache[cacheKey] = (() => {
const regionPattern = allowCityCode
? '[A-Z]{2}(?:\\s+[A-Z]{3})?'
: '[A-Z]{2}';
return {
sequenceRegex: new RegExp(`(^|\\s)(${regionPattern})[\\s|\\-]*(\\d{1,2})(?=$|[\\s|\\-[\\]])`, 'g'),
multiplierHyphenRegex: new RegExp(`(\\b${regionPattern} \\d{2})-\\s*(?=\\d+(?:\\.\\d+)?X\\b)`, 'g')
};
})()
);
return String(name)
.replace(new RegExp(`(^|\\s)(${regionPattern})[\\s|\\-]*(\\d{1,2})(?=$|[\\s|\\-[\\]])`, 'g'), (
.replace(cachedRegexes.sequenceRegex, (
match,
prefix,
regionCode,
@@ -221,7 +241,7 @@
return `${prefix}${regionCode} ${sequenceNumber.padStart(2, '0')}`;
})
.replace(new RegExp(`(\\b${regionPattern} \\d{2})-\\s*(?=\\d+(?:\\.\\d+)?X\\b)`, 'g'), '$1 ')
.replace(cachedRegexes.multiplierHyphenRegex, '$1 ')
.replace(/\s{2,}/g, ' ')
.trim();
};
@@ -284,19 +304,26 @@
};
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 descriptorCacheKey = descriptorKeywords.join('\u0000');
const descriptorRegexes = lineDescriptorRegexCache[descriptorCacheKey] || (
lineDescriptorRegexCache[descriptorCacheKey] = (() => {
const descriptorPattern = descriptorKeywords.map(escapeRegex).join('|');
return {
descriptorOnlyRegex: new RegExp(`^(?:${descriptorPattern})+$`, 'i'),
embeddedDescriptorRegex: new RegExp(`\\s*(?:${descriptorPattern})(?=\\s|\\d|$)`, 'gi'),
bracketDescriptorRegex: new RegExp(`\\s*\\[(?:${descriptorPattern})(?:\\s+(?:${descriptorPattern}))*]\\s*`, 'gi')
};
})()
);
const [head, ...segments] = stripLeadingMojibakeFlag(name).split('|');
const keptSegments = segments
.map(segment => segment.trim())
.filter(segment => segment && !descriptorOnlyRegex.test(segment));
.filter(segment => segment && !descriptorRegexes.descriptorOnlyRegex.test(segment));
const cleanHead = head
.replace(bracketDescriptorRegex, ' ')
.replace(embeddedDescriptorRegex, '')
.replace(descriptorRegexes.bracketDescriptorRegex, ' ')
.replace(descriptorRegexes.embeddedDescriptorRegex, '')
.replace(leadingFlagRegex, '$1 ')
.replace(/\s{2,}/g, ' ')
.trim();