@kecan0406/aninames 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/text.js ADDED
@@ -0,0 +1,611 @@
1
+ const ROMAJI = {
2
+ あ: 'a',
3
+ い: 'i',
4
+ う: 'u',
5
+ え: 'e',
6
+ お: 'o',
7
+ か: 'ka',
8
+ き: 'ki',
9
+ く: 'ku',
10
+ け: 'ke',
11
+ こ: 'ko',
12
+ が: 'ga',
13
+ ぎ: 'gi',
14
+ ぐ: 'gu',
15
+ げ: 'ge',
16
+ ご: 'go',
17
+ さ: 'sa',
18
+ し: 'shi',
19
+ す: 'su',
20
+ せ: 'se',
21
+ そ: 'so',
22
+ ざ: 'za',
23
+ じ: 'ji',
24
+ ず: 'zu',
25
+ ぜ: 'ze',
26
+ ぞ: 'zo',
27
+ た: 'ta',
28
+ ち: 'chi',
29
+ つ: 'tsu',
30
+ て: 'te',
31
+ と: 'to',
32
+ だ: 'da',
33
+ ぢ: 'ji',
34
+ づ: 'zu',
35
+ で: 'de',
36
+ ど: 'do',
37
+ な: 'na',
38
+ に: 'ni',
39
+ ぬ: 'nu',
40
+ ね: 'ne',
41
+ の: 'no',
42
+ は: 'ha',
43
+ ひ: 'hi',
44
+ ふ: 'fu',
45
+ へ: 'he',
46
+ ほ: 'ho',
47
+ ば: 'ba',
48
+ び: 'bi',
49
+ ぶ: 'bu',
50
+ べ: 'be',
51
+ ぼ: 'bo',
52
+ ぱ: 'pa',
53
+ ぴ: 'pi',
54
+ ぷ: 'pu',
55
+ ぺ: 'pe',
56
+ ぽ: 'po',
57
+ ま: 'ma',
58
+ み: 'mi',
59
+ む: 'mu',
60
+ め: 'me',
61
+ も: 'mo',
62
+ や: 'ya',
63
+ ゆ: 'yu',
64
+ よ: 'yo',
65
+ ら: 'ra',
66
+ り: 'ri',
67
+ る: 'ru',
68
+ れ: 're',
69
+ ろ: 'ro',
70
+ わ: 'wa',
71
+ ゐ: 'i',
72
+ ゑ: 'e',
73
+ を: 'o',
74
+ ん: 'n',
75
+ ゔ: 'vu',
76
+ ぁ: 'a',
77
+ ぃ: 'i',
78
+ ぅ: 'u',
79
+ ぇ: 'e',
80
+ ぉ: 'o',
81
+ ゎ: 'wa',
82
+ };
83
+ const SMALL_Y = { ゃ: 'a', ゅ: 'u', ょ: 'o' };
84
+ const SMALL_V = { ぁ: 'a', ぃ: 'i', ぅ: 'u', ぇ: 'e', ぉ: 'o' };
85
+ const VOWELS = 'aeiou';
86
+ export function toHiragana(text) {
87
+ let out = '';
88
+ for (const ch of text) {
89
+ const code = ch.codePointAt(0) ?? 0;
90
+ out += code >= 0x30a1 && code <= 0x30f6 ? String.fromCodePoint(code - 0x60) : ch;
91
+ }
92
+ return out;
93
+ }
94
+ export function isKana(text) {
95
+ return (/^[\p{Script=Hiragana}\p{Script=Katakana}ー・\s]+$/u.test(text) &&
96
+ /[\p{Script=Hiragana}\p{Script=Katakana}]/u.test(text));
97
+ }
98
+ export function hasHangul(text) {
99
+ return /\p{Script=Hangul}/u.test(text);
100
+ }
101
+ export function hasKanji(text) {
102
+ return /\p{Script=Han}/u.test(text);
103
+ }
104
+ function syllables(kana) {
105
+ const chars = [...toHiragana(kana)];
106
+ const out = [];
107
+ for (let i = 0; i < chars.length; i++) {
108
+ const ch = chars[i] ?? '';
109
+ const next = chars[i + 1] ?? '';
110
+ if (SMALL_Y[next] && ROMAJI[ch] && ch !== 'ん') {
111
+ out.push(ch + next);
112
+ i++;
113
+ continue;
114
+ }
115
+ if (SMALL_V[next] && ROMAJI[ch] && 'うてでふゔしじちつ'.includes(ch)) {
116
+ out.push(ch + next);
117
+ i++;
118
+ continue;
119
+ }
120
+ out.push(ch);
121
+ }
122
+ return out;
123
+ }
124
+ function romajiOf(syllable) {
125
+ const [head = '', tail = ''] = [...syllable];
126
+ if (!tail) {
127
+ return ROMAJI[head];
128
+ }
129
+ const base = ROMAJI[head] ?? '';
130
+ if (SMALL_Y[tail]) {
131
+ const vowel = SMALL_Y[tail];
132
+ if (base.startsWith('sh') || base.startsWith('ch') || base.startsWith('j')) {
133
+ return base.slice(0, -1) + vowel;
134
+ }
135
+ return `${base.slice(0, -1)}y${vowel}`;
136
+ }
137
+ const vowel = SMALL_V[tail] ?? '';
138
+ if (head === 'う')
139
+ return `w${vowel}`;
140
+ if (head === 'て')
141
+ return `t${vowel}`;
142
+ if (head === 'で')
143
+ return `d${vowel}`;
144
+ if (head === 'ふ')
145
+ return `f${vowel}`;
146
+ if (head === 'ゔ')
147
+ return `v${vowel}`;
148
+ if (head === 'し')
149
+ return `sh${vowel}`;
150
+ if (head === 'じ')
151
+ return `j${vowel}`;
152
+ if (head === 'ち')
153
+ return `ch${vowel}`;
154
+ if (head === 'つ')
155
+ return `ts${vowel}`;
156
+ return base + vowel;
157
+ }
158
+ export function kanaToRomaji(kana) {
159
+ const parts = syllables(kana);
160
+ let out = '';
161
+ for (let i = 0; i < parts.length; i++) {
162
+ const part = parts[i] ?? '';
163
+ if (part === 'っ' || part === 'ッ') {
164
+ const next = romajiOf(parts[i + 1] ?? '') ?? '';
165
+ out += next.startsWith('ch') ? 't' : next.slice(0, 1).replace(/[aeiou]/, '');
166
+ continue;
167
+ }
168
+ if (part === 'ー') {
169
+ const last = out.at(-1) ?? '';
170
+ out += VOWELS.includes(last) ? last : '';
171
+ continue;
172
+ }
173
+ if (part === '・' || /\s/.test(part)) {
174
+ out += ' ';
175
+ continue;
176
+ }
177
+ const romaji = romajiOf(part);
178
+ if (romaji === undefined) {
179
+ out += part;
180
+ continue;
181
+ }
182
+ if (part === 'ん' && /^[aeiouy]/.test(romajiOf(parts[i + 1] ?? '') ?? '')) {
183
+ out += "n'";
184
+ continue;
185
+ }
186
+ out += romaji;
187
+ }
188
+ return out.replace(/\s+/g, ' ').trim();
189
+ }
190
+ const INITIAL = ['k', 'k', 'n', 't', 't', 'r', 'm', 'b', 'b', 's', 's', '', 'ch', 'ch', 'ch', 'k', 't', 'p', 'h'];
191
+ const MEDIAL = [
192
+ 'a',
193
+ 'e',
194
+ 'ya',
195
+ 'ye',
196
+ 'o',
197
+ 'e',
198
+ 'yo',
199
+ 'ye',
200
+ 'o',
201
+ 'wa',
202
+ 'we',
203
+ 'we',
204
+ 'yo',
205
+ 'u',
206
+ 'wo',
207
+ 'we',
208
+ 'wi',
209
+ 'yu',
210
+ 'u',
211
+ 'ui',
212
+ 'i',
213
+ ];
214
+ const FINAL = { 1: 'ku', 4: 'n', 8: 'ru', 16: 'mu', 19: 'q', 20: 'q', 21: 'n' };
215
+ export function hangulToLatin(text) {
216
+ let out = '';
217
+ for (const ch of text.normalize('NFC')) {
218
+ const code = (ch.codePointAt(0) ?? 0) - 0xac00;
219
+ if (code < 0 || code >= 11172) {
220
+ out += ch;
221
+ continue;
222
+ }
223
+ const initial = INITIAL[Math.floor(code / 588)] ?? '';
224
+ const medial = Math.floor((code % 588) / 28);
225
+ const final = code % 28;
226
+ let syllable = initial + (MEDIAL[medial] ?? '');
227
+ if (medial === 18) {
228
+ if (initial === 'ch')
229
+ syllable = 'tsu';
230
+ else if (initial === 's')
231
+ syllable = 'su';
232
+ }
233
+ out += syllable + (FINAL[final] ?? '');
234
+ }
235
+ return out.replace(/q(?=[a-z])/g, (_, at, all) => all[at + 1] ?? '').replace(/q/g, '');
236
+ }
237
+ export function foldLatin(text) {
238
+ return text
239
+ .normalize('NFKD')
240
+ .toLowerCase()
241
+ .replace(/([aeiou])[\u0304\u0302]/g, '$1$1')
242
+ .replace(/(?<=[a-z])\p{M}+/gu, '')
243
+ .normalize('NFC');
244
+ }
245
+ function coarse(latin) {
246
+ let r = foldLatin(latin).replace(/[^a-z]/g, '');
247
+ r = r.replace(/ts/g, 's').replace(/sh/g, 's').replace(/f/g, 'h').replace(/j/g, 'ch').replace(/z/g, 'ch');
248
+ r = r.replace(/chy/g, 'ch').replace(/sy/g, 's').replace(/g/g, 'k').replace(/d/g, 't').replace(/v/g, 'b');
249
+ r = r.replace(/([aeiou])h(?![aeiouy])/g, '$1');
250
+ r = r.replace(/([kstpcbmnrhw])\1+/g, '$1').replace(/tch/g, 'ch');
251
+ r = r.replace(/m(?=[bp])/g, 'n');
252
+ r = r.replace(/ou|oo/g, 'o').replace(/uu/g, 'u').replace(/aa/g, 'a').replace(/ii/g, 'i').replace(/ee/g, 'e');
253
+ return r.replace(/chu/g, 'su');
254
+ }
255
+ function latinOf(token) {
256
+ if (isKana(token))
257
+ return kanaToRomaji(token);
258
+ if (hasHangul(token))
259
+ return hangulToLatin(token);
260
+ return /^[\p{Script=Latin}\p{N}\p{P}\p{M}]+$/u.test(token.normalize('NFKD')) ? token : '';
261
+ }
262
+ export function soundKeys(text) {
263
+ const tokens = text
264
+ .normalize('NFKC')
265
+ .split(/[\s・·_]+/)
266
+ .map((token) => token.replace(/[\p{P}\p{S}]+/gu, ''))
267
+ .filter(Boolean)
268
+ .map((token) => coarse(latinOf(token)));
269
+ if (tokens.length === 0 || tokens.some((token) => !token)) {
270
+ return [];
271
+ }
272
+ const keys = new Set([tokens.join(''), [...tokens].sort().join('')]);
273
+ return [...keys].filter((key) => key.length >= 2);
274
+ }
275
+ const PUNCT = /[\s\p{P}\p{S}]+/gu;
276
+ export function flat(text) {
277
+ return toHiragana(foldLatin(text.normalize('NFKC'))).replace(PUNCT, '');
278
+ }
279
+ export function latinKey(text) {
280
+ const tokens = foldLatin(text.normalize('NFKC'))
281
+ .replace(/[_\-.,:;!?'"’・/]+/g, ' ')
282
+ .split(/\s+/)
283
+ .filter(Boolean)
284
+ .map((token) => token
285
+ .replace(/([aeiou])h(?![aeiouy])/g, '$1')
286
+ .replace(/ou|oo/g, 'o')
287
+ .replace(/uu/g, 'u')
288
+ .replace(/aa/g, 'a')
289
+ .replace(/ii/g, 'i')
290
+ .replace(/ee/g, 'e')
291
+ .replace(/nn/g, 'n'));
292
+ return tokens.sort().join(' ');
293
+ }
294
+ export function partKey(token) {
295
+ const key = flat(token);
296
+ return /^[a-z0-9]+$/.test(key) ? latinKey(token).replace(/\s+/g, '') : key;
297
+ }
298
+ export function qualifiers(text) {
299
+ const quals = [];
300
+ const base = text
301
+ .normalize('NFKC')
302
+ .replace(/[\s_]*\(([^)]*)\)/g, (_, inner) => {
303
+ quals.push(inner);
304
+ return '';
305
+ })
306
+ .trim();
307
+ return { base, quals };
308
+ }
309
+ const FAN = {
310
+ あ: '아',
311
+ い: '이',
312
+ う: '우',
313
+ え: '에',
314
+ お: '오',
315
+ か: '카',
316
+ き: '키',
317
+ く: '쿠',
318
+ け: '케',
319
+ こ: '코',
320
+ が: '가',
321
+ ぎ: '기',
322
+ ぐ: '구',
323
+ げ: '게',
324
+ ご: '고',
325
+ さ: '사',
326
+ し: '시',
327
+ す: '스',
328
+ せ: '세',
329
+ そ: '소',
330
+ ざ: '자',
331
+ じ: '지',
332
+ ず: '즈',
333
+ ぜ: '제',
334
+ ぞ: '조',
335
+ た: '타',
336
+ ち: '치',
337
+ つ: '츠',
338
+ て: '테',
339
+ と: '토',
340
+ だ: '다',
341
+ ぢ: '지',
342
+ づ: '즈',
343
+ で: '데',
344
+ ど: '도',
345
+ な: '나',
346
+ に: '니',
347
+ ぬ: '누',
348
+ ね: '네',
349
+ の: '노',
350
+ は: '하',
351
+ ひ: '히',
352
+ ふ: '후',
353
+ へ: '헤',
354
+ ほ: '호',
355
+ ば: '바',
356
+ び: '비',
357
+ ぶ: '부',
358
+ べ: '베',
359
+ ぼ: '보',
360
+ ぱ: '파',
361
+ ぴ: '피',
362
+ ぷ: '푸',
363
+ ぺ: '페',
364
+ ぽ: '포',
365
+ ま: '마',
366
+ み: '미',
367
+ む: '무',
368
+ め: '메',
369
+ も: '모',
370
+ や: '야',
371
+ ゆ: '유',
372
+ よ: '요',
373
+ ら: '라',
374
+ り: '리',
375
+ る: '루',
376
+ れ: '레',
377
+ ろ: '로',
378
+ わ: '와',
379
+ を: '오',
380
+ ゔ: '부',
381
+ きゃ: '캬',
382
+ きゅ: '큐',
383
+ きょ: '쿄',
384
+ ぎゃ: '갸',
385
+ ぎゅ: '규',
386
+ ぎょ: '교',
387
+ しゃ: '샤',
388
+ しゅ: '슈',
389
+ しょ: '쇼',
390
+ じゃ: '쟈',
391
+ じゅ: '쥬',
392
+ じょ: '죠',
393
+ ちゃ: '챠',
394
+ ちゅ: '츄',
395
+ ちょ: '쵸',
396
+ にゃ: '냐',
397
+ にゅ: '뉴',
398
+ にょ: '뇨',
399
+ ひゃ: '햐',
400
+ ひゅ: '휴',
401
+ ひょ: '효',
402
+ びゃ: '뱌',
403
+ びゅ: '뷰',
404
+ びょ: '뵤',
405
+ ぴゃ: '퍄',
406
+ ぴゅ: '퓨',
407
+ ぴょ: '표',
408
+ みゃ: '먀',
409
+ みゅ: '뮤',
410
+ みょ: '묘',
411
+ りゃ: '랴',
412
+ りゅ: '류',
413
+ りょ: '료',
414
+ ふぁ: '파',
415
+ ふぃ: '피',
416
+ ふぇ: '페',
417
+ ふぉ: '포',
418
+ てぃ: '티',
419
+ でぃ: '디',
420
+ うぃ: '위',
421
+ うぇ: '웨',
422
+ うぉ: '워',
423
+ しぇ: '셰',
424
+ じぇ: '제',
425
+ ちぇ: '체',
426
+ ゔぁ: '바',
427
+ ゔぃ: '비',
428
+ ゔぇ: '베',
429
+ ゔぉ: '보',
430
+ };
431
+ const STANDARD_INITIAL = {
432
+ か: '가',
433
+ き: '기',
434
+ く: '구',
435
+ け: '게',
436
+ こ: '고',
437
+ た: '다',
438
+ ち: '지',
439
+ つ: '쓰',
440
+ て: '데',
441
+ と: '도',
442
+ きゃ: '갸',
443
+ きゅ: '규',
444
+ きょ: '교',
445
+ ちゃ: '자',
446
+ ちゅ: '주',
447
+ ちょ: '조',
448
+ じゃ: '자',
449
+ じゅ: '주',
450
+ じょ: '조',
451
+ };
452
+ const STANDARD_MEDIAL = {
453
+ つ: '쓰',
454
+ ちゃ: '차',
455
+ ちゅ: '추',
456
+ ちょ: '초',
457
+ じゃ: '자',
458
+ じゅ: '주',
459
+ じょ: '조',
460
+ };
461
+ function addFinal(syllable, final) {
462
+ const code = (syllable.codePointAt(0) ?? 0) - 0xac00;
463
+ if (code < 0 || code >= 11172 || code % 28 !== 0) {
464
+ return syllable;
465
+ }
466
+ return String.fromCodePoint(0xac00 + code + (final === 'n' ? 4 : final === 's' ? 19 : 8));
467
+ }
468
+ function vowelOf(syllable) {
469
+ return romajiOf(syllable)?.at(-1) ?? '';
470
+ }
471
+ function isLong(previous, part) {
472
+ const vowel = vowelOf(previous);
473
+ return ((vowel === 'o' && (part === 'う' || part === 'お')) ||
474
+ (vowel === 'u' && part === 'う') ||
475
+ (vowel === 'i' && part === 'い') ||
476
+ (vowel === 'a' && part === 'あ') ||
477
+ (vowel === 'e' && part === 'え'));
478
+ }
479
+ function hangulWord(word, style) {
480
+ const out = [];
481
+ let previous = '';
482
+ for (const part of syllables(word)) {
483
+ if (part === 'ん' || part === 'っ') {
484
+ const last = out.pop();
485
+ if (last === undefined)
486
+ return '';
487
+ out.push(addFinal(last, part === 'ん' ? 'n' : 's'));
488
+ previous = part;
489
+ continue;
490
+ }
491
+ if (part === 'ー' || (previous && isLong(previous, part) && style !== 'fan-long')) {
492
+ previous = part;
493
+ continue;
494
+ }
495
+ const table = style === 'standard' ? (out.length === 0 ? STANDARD_INITIAL : STANDARD_MEDIAL) : {};
496
+ const syllable = table[part] ?? FAN[part];
497
+ if (!syllable)
498
+ return '';
499
+ out.push(syllable);
500
+ previous = part;
501
+ }
502
+ return out.join('');
503
+ }
504
+ export function kanaToHangul(kana, style) {
505
+ const words = toHiragana(kana.normalize('NFKC'))
506
+ .split(/[\s・·==]+/)
507
+ .filter(Boolean);
508
+ const converted = words.map((word) => hangulWord(word, style));
509
+ return converted.length === 0 || converted.some((word) => !word) ? '' : converted.join(' ');
510
+ }
511
+ export function initials(text) {
512
+ let out = '';
513
+ for (const ch of text.normalize('NFC')) {
514
+ const code = (ch.codePointAt(0) ?? 0) - 0xac00;
515
+ if (code >= 0 && code < 11172)
516
+ out += 'ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ'[Math.floor(code / 588)] ?? '';
517
+ }
518
+ return out;
519
+ }
520
+ export function editDistance(a, b, limit) {
521
+ const x = [...a];
522
+ const y = [...b];
523
+ if (Math.abs(x.length - y.length) > limit)
524
+ return limit + 1;
525
+ let before = Array.from({ length: y.length + 1 }, (_, j) => j);
526
+ let previous = before;
527
+ for (let i = 1; i <= x.length; i++) {
528
+ const current = [i];
529
+ let best = i;
530
+ for (let j = 1; j <= y.length; j++) {
531
+ const cost = x[i - 1] === y[j - 1] ? 0 : 1;
532
+ let value = Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, (previous[j - 1] ?? 0) + cost);
533
+ if (i > 1 && j > 1 && x[i - 1] === y[j - 2] && x[i - 2] === y[j - 1])
534
+ value = Math.min(value, (before[j - 2] ?? 0) + 1);
535
+ current.push(value);
536
+ best = Math.min(best, value);
537
+ }
538
+ if (best > limit)
539
+ return limit + 1;
540
+ before = previous;
541
+ previous = current;
542
+ }
543
+ return previous[y.length] ?? limit + 1;
544
+ }
545
+ const LOAN = {
546
+ く: '크',
547
+ ぐ: '그',
548
+ す: '스',
549
+ ず: '즈',
550
+ つ: '츠',
551
+ ふ: '프',
552
+ ぶ: '브',
553
+ ぷ: '프',
554
+ む: '므',
555
+ る: '르',
556
+ と: '트',
557
+ ど: '드',
558
+ };
559
+ function loanWord(word, double) {
560
+ const parts = syllables(word);
561
+ const out = [];
562
+ for (let i = 0; i < parts.length; i++) {
563
+ const part = parts[i] ?? '';
564
+ const next = parts[i + 1];
565
+ const vowelNext = next === undefined || /^[あいうえおー]/.test(next) || next === 'ん';
566
+ const ending = next === undefined;
567
+ const loose = LOAN[part] && ((part !== 'と' && part !== 'ど' && !vowelNext) || ending);
568
+ if (part === 'ー')
569
+ continue;
570
+ if (part === 'ん' || part === 'っ') {
571
+ const last = out.pop();
572
+ if (last === undefined)
573
+ return '';
574
+ out.push(addFinal(last, part === 'ん' ? 'n' : 's'));
575
+ continue;
576
+ }
577
+ const syllable = loose ? LOAN[part] : FAN[part];
578
+ if (!syllable)
579
+ return '';
580
+ const previous = out.at(-1);
581
+ if (double && previous && /^[크그프브]$/.test(previous) && /^[らりるれろ]/.test(part)) {
582
+ out.pop();
583
+ out.push(addFinal(previous, 'l'));
584
+ }
585
+ out.push(syllable);
586
+ }
587
+ return out.join('');
588
+ }
589
+ export function hangulForms(kana) {
590
+ const out = new Map();
591
+ const fan = kanaToHangul(kana, 'fan');
592
+ if (fan)
593
+ out.set(fan, true);
594
+ for (const style of ['fan-long', 'standard']) {
595
+ const text = kanaToHangul(kana, style);
596
+ if (text && !out.has(text))
597
+ out.set(text, false);
598
+ }
599
+ if (/^[\p{Script=Katakana}ー・\s]+$/u.test(kana)) {
600
+ const words = toHiragana(kana.normalize('NFKC'))
601
+ .split(/[\s・·==]+/)
602
+ .filter(Boolean);
603
+ for (const double of [false, true]) {
604
+ const loan = words.map((word) => loanWord(word, double));
605
+ const text = loan.every(Boolean) ? loan.join(' ') : '';
606
+ if (text && !out.has(text))
607
+ out.set(text, false);
608
+ }
609
+ }
610
+ return [...out].map(([text, preferred]) => ({ text, preferred }));
611
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@kecan0406/aninames",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Multilingual alias index for anime titles and characters",
6
+ "license": "MIT",
7
+ "author": "kecan0406",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kecan0406/aninames.git"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./lib/client/index.d.ts",
15
+ "default": "./lib/client/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "lib"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "devDependencies": {
25
+ "@biomejs/biome": "^2.3.0",
26
+ "@types/bun": "^1.3.0",
27
+ "typescript": "^5.8.2"
28
+ },
29
+ "engines": {
30
+ "bun": ">=1.3",
31
+ "node": ">=22"
32
+ },
33
+ "dependencies": {
34
+ "es-hangul": "2.4.0"
35
+ }
36
+ }