diff --git a/danmu_api/utils/auto-match-mapping-util.js b/danmu_api/utils/auto-match-mapping-util.js index ad35f10..c45b5ca 100644 --- a/danmu_api/utils/auto-match-mapping-util.js +++ b/danmu_api/utils/auto-match-mapping-util.js @@ -1,7 +1,16 @@ +const NON_RULE_TITLE_CHARACTERS = (() => { + try { + return new RegExp('[^\\p{L}\\p{N}]', 'gu'); + } catch { + // nodejs-mobile builds without Unicode property escapes still need CJK title matching. + return /[^0-9A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u052F\u05D0-\u05EA\u05EF-\u05F2\u0620-\u063F\u0640-\u064A\u0660-\u0669\u0671-\u06D3\u06D5\u06EE-\u06FC\u06FF\u1100-\u11FF\u3005-\u3007\u3031-\u3035\u3038-\u303B\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\u9FFF\uA960-\uA97F\uAC00-\uD7A3\uD7B0-\uD7FF\uF900-\uFAFF]/g; + } +})(); + function normalizeRuleTitle(value) { return String(value || '') .normalize('NFKC') - .replace(/[^\p{L}\p{N}]/gu, '') + .replace(NON_RULE_TITLE_CHARACTERS, '') .toLowerCase(); } diff --git a/danmu_api/worker.test.js b/danmu_api/worker.test.js index 8a95089..78c3851 100644 --- a/danmu_api/worker.test.js +++ b/danmu_api/worker.test.js @@ -297,6 +297,25 @@ test('worker.js API endpoints', async (t) => { }); await t.test('auto match mapping table', async t => { + await t.test('falls back when Unicode property escapes are unavailable', async () => { + const NativeRegExp = globalThis.RegExp; + globalThis.RegExp = function (pattern, flags) { + if (String(pattern).includes('\\p{')) throw new SyntaxError('Unicode property escapes are unavailable'); + return new NativeRegExp(pattern, flags); + }; + + try { + const compat = await import('./utils/auto-match-mapping-util.js?unicode-properties=unavailable'); + const { rules, warnings } = compat.parseAutoMatchMappingRules('進撃の巨人 S01E01->SPY×FAMILY S01E03'); + + assert.deepEqual(warnings, []); + assert.equal(compat.resolveAutoMatchMapping(rules, { title: '進撃の 巨人!', season: 1, episode: 2 }).targetEpisode, 4); + assert.equal(compat.candidateMatchesMappingTitle({ animeTitle: 'SPY FAMILY' }, rules[0]), true); + } finally { + globalThis.RegExp = NativeRegExp; + } + }); + await t.test('parses and resolves open, bounded, qualified, and platform rules', () => { const { rules, warnings } = parseAutoMatchMappingRules([ '永生 S05E02->永生 S01E58',