Skip to content

Commit 7aa4d5f

Browse files
committed
feat: Improve analysis by separating matches into chunks
1 parent 221e0dc commit 7aa4d5f

2 files changed

Lines changed: 142 additions & 11 deletions

File tree

lib/romanize.dart

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,20 @@ class TextRomanizer {
110110
return romanizers.where((romanizer) => romanizer.isValid(input)).toSet();
111111
}
112112

113-
static final _separatorPattern = RegExp(
114-
r'[^\p{L}\p{N}\p{M}]+',
113+
static final _separatorPattern = RegExp(r'[\s\p{P}\p{S}]+', unicode: true);
114+
115+
// Matches chunks of text that share the same script system.
116+
// Grouping Kanji/Kana ensures Japanese sentences stay together.
117+
static final RegExp _scriptChunkPattern = RegExp(
118+
r'('
119+
r'[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]+|' // CJK (Keep together)
120+
r'[\p{Script=Hangul}]+|' // Korean
121+
r'[\p{Script=Arabic}]+|' // Arabic
122+
r'[\p{Script=Hebrew}]+|' // Hebrew
123+
r'[\p{Script=Cyrillic}]+|' // Cyrillic
124+
r'[\p{Script=Latin}]+|' // Latin
125+
r'[0-9]+' // ASCII Digits
126+
r')',
115127
unicode: true,
116128
);
117129

@@ -186,6 +198,7 @@ class TextRomanizer {
186198
final parts = <RomanizedText>[];
187199
final wordCache = <String, RomanizedText>{};
188200

201+
// 1. Outer Split: Handles spaces and punctuation
189202
input.splitMapJoin(
190203
_separatorPattern,
191204
onMatch: (Match match) {
@@ -198,18 +211,47 @@ class TextRomanizer {
198211
);
199212
return match[0]!;
200213
},
201-
// Handle the content (words):
202214
onNonMatch: (String word) {
203215
if (word.isEmpty) return '';
204-
final romanizedPart = wordCache.putIfAbsent(word, () {
205-
final romanizer = detectLanguage(word);
206-
return RomanizedText(
207-
rawText: word,
208-
language: romanizer.language,
209-
romanizedText: romanizer.romanize(word),
216+
217+
// 2. Inner Split: Handles mixed scripts (e.g., "abc가나다")
218+
// We find all "chunks" of consistent script within the word.
219+
final matches = _scriptChunkPattern.allMatches(word);
220+
221+
// Track position to handle any un-matched gaps (symbols skipped by regex?)
222+
int currentPos = 0;
223+
224+
for (final m in matches) {
225+
// Handle gap if any (rare, but good for safety)
226+
if (m.start > currentPos) {
227+
final gap = word.substring(currentPos, m.start);
228+
parts.add(
229+
RomanizedText(rawText: gap, language: '', romanizedText: gap),
230+
);
231+
}
232+
233+
final chunk = m[0]!;
234+
final romanizedPart = wordCache.putIfAbsent(chunk, () {
235+
final romanizer = detectLanguage(chunk);
236+
return RomanizedText(
237+
rawText: chunk,
238+
language: romanizer.language,
239+
romanizedText: romanizer.romanize(chunk),
240+
);
241+
});
242+
parts.add(romanizedPart);
243+
244+
currentPos = m.end;
245+
}
246+
247+
// Handle trailing characters
248+
if (currentPos < word.length) {
249+
final tail = word.substring(currentPos);
250+
parts.add(
251+
RomanizedText(rawText: tail, language: '', romanizedText: tail),
210252
);
211-
});
212-
parts.add(romanizedPart);
253+
}
254+
213255
return word;
214256
},
215257
);

test/romanize_test.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,95 @@ void main() {
470470
final japanesePart = result.firstWhere((r) => r.rawText == 'こんにちは');
471471
expect(japanesePart.language, equals('japanese'));
472472
});
473+
474+
test('should split mixed Latin and Hangul (abc가나다)', () {
475+
const input = 'abc가나다';
476+
final result = TextRomanizer.analyze(input);
477+
478+
// Expect: [Latin(abc), Hangul(가나다)]
479+
expect(result, hasLength(2));
480+
481+
expect(result[0].rawText, equals('abc'));
482+
expect(result[0].language, equals('empty'));
483+
484+
expect(result[1].rawText, equals('가나다'));
485+
expect(result[1].language, equals('korean'));
486+
});
487+
488+
test('should split mixed Numbers and Scripts', () {
489+
// "Room101" -> Room (Latin), 101 (Digits)
490+
const input = 'Room101';
491+
final result = TextRomanizer.analyze(input);
492+
493+
expect(result, hasLength(2));
494+
expect(result[0].rawText, equals('Room'));
495+
expect(result[1].rawText, equals('101'));
496+
});
497+
498+
test('should NOT split natural Japanese (Kanji + Kana)', () {
499+
// "日本語です" (Kanji + Hiragana) should stay as ONE chunk
500+
const input = '日本語です';
501+
final result = TextRomanizer.analyze(input);
502+
503+
expect(result, hasLength(1));
504+
expect(result[0].rawText, equals('日本語です'));
505+
expect(result[0].language, equals('japanese'));
506+
});
507+
508+
test('should handle a complex snippet', () {
509+
// Input: 123٤٥٦(Numbers)abc가나다(Hangul)カキク(Katakana)
510+
// 123 -> Digits
511+
// ٤٥٦ -> Arabic
512+
// ( -> Separator
513+
// Numbers -> Latin
514+
// ) -> Separator
515+
// abc -> Latin
516+
// 가나다 -> Korean
517+
// ( -> Separator
518+
// Hangul -> Latin
519+
// ) -> Separator
520+
// ... etc
521+
522+
const input = '123٤٥٦(Numbers)abc가나다';
523+
final result = TextRomanizer.analyze(input);
524+
525+
// 1. Check "123٤٥٦" split
526+
// ASCII digits [0-9] are separate from Arabic script in the regex
527+
final part123 = result.firstWhere((r) => r.rawText == '123');
528+
final partArabicNum = result.firstWhere((r) => r.rawText == '٤٥٦');
529+
530+
expect(part123, isNotNull);
531+
expect(partArabicNum, isNotNull);
532+
expect(partArabicNum.language, equals('arabic'));
533+
534+
// 2. Check "abc가나다" split
535+
// Find the sequence where abc is followed immediately by 가나다
536+
final indexAbc = result.indexWhere((r) => r.rawText == 'abc');
537+
expect(indexAbc, isNot(-1));
538+
539+
final partKorean = result[indexAbc + 1];
540+
expect(partKorean.rawText, equals('가나다'));
541+
expect(partKorean.language, equals('korean'));
542+
});
543+
544+
test('should split CJK vs Latin', () {
545+
const input = 'Hello你好';
546+
final result = TextRomanizer.analyze(input);
547+
548+
expect(result, hasLength(2));
549+
expect(result[0].rawText, equals('Hello'));
550+
expect(result[1].rawText, equals('你好'));
551+
});
552+
553+
test('should split Cyrillic vs Latin', () {
554+
const input = 'TestТест'; // Latin 'Test', Cyrillic 'Test'
555+
final result = TextRomanizer.analyze(input);
556+
557+
expect(result, hasLength(2));
558+
expect(result[0].rawText, equals('Test'));
559+
expect(result[1].rawText, equals('Тест'));
560+
expect(result[1].language, equals('cyrillic'));
561+
});
473562
});
474563

475564
group('supportedLanguages', () {

0 commit comments

Comments
 (0)