kanabarum 0.2.0 → 0.2.1
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/README.md +57 -53
- package/dist/index.d.ts +30 -5
- package/dist/index.js +36 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Kanabarum - 가나발음
|
|
2
2
|
|
|
3
|
-
일본어
|
|
4
|
-
`kuromoji` 품사 분석을 이용해 は/へ 같은 조사를 안전하게
|
|
3
|
+
일본어 히라가나/가타카나/한자 문자열을 한국어 발음 표기로 바꿔주는 TypeScript 라이브러리입니다.
|
|
4
|
+
`kuromoji` 품사 분석을 이용해 は/へ 같은 조사를 안전하게 치환하고, 한자는 발음으로 변환합니다.
|
|
5
5
|
|
|
6
6
|
## 설치
|
|
7
7
|
|
|
@@ -20,86 +20,90 @@ npm install kanabarum
|
|
|
20
20
|
```ts
|
|
21
21
|
import { kanaToHangul } from "kanabarum";
|
|
22
22
|
|
|
23
|
-
const text = await kanaToHangul("さようなら");
|
|
23
|
+
const text = await kanaToHangul("さようなら");
|
|
24
24
|
// => "사요나라"
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
###
|
|
27
|
+
### 클래스 기반 호출
|
|
28
28
|
|
|
29
29
|
```ts
|
|
30
|
-
import {
|
|
30
|
+
import { Kanabarum } from "kanabarum";
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const kanabarum = new Kanabarum();
|
|
33
|
+
await kanabarum.init();
|
|
33
34
|
|
|
34
35
|
// 인사말
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
kanabarum.kanaToHangul("おはよう"); // => "오하요"
|
|
37
|
+
kanabarum.kanaToHangul("こんにちは"); // => "곤니치와"
|
|
38
|
+
kanabarum.kanaToHangul("こんばんは"); // => "곰방와"
|
|
39
|
+
kanabarum.kanaToHangul("ありがとう"); // => "아리가토"
|
|
40
|
+
kanabarum.kanaToHangul("すみません"); // => "스미마셍"
|
|
40
41
|
|
|
41
42
|
// 요음
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
kanabarum.kanaToHangul("きゃく"); // => "캬쿠"
|
|
44
|
+
kanabarum.kanaToHangul("しゅくだい"); // => "슈쿠다이"
|
|
45
|
+
kanabarum.kanaToHangul("ちょっと"); // => "춋토"
|
|
46
|
+
kanabarum.kanaToHangul("きゅう"); // => "큐"
|
|
47
|
+
kanabarum.kanaToHangul("りょこう"); // => "료코"
|
|
47
48
|
|
|
48
49
|
// つ 발음
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
kanabarum.kanaToHangul("つき"); // => "츠키"
|
|
51
|
+
kanabarum.kanaToHangul("つなみ"); // => "쓰나미"
|
|
51
52
|
|
|
52
53
|
// 촉음
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
kanabarum.kanaToHangul("きって"); // => "킷테"
|
|
55
|
+
kanabarum.kanaToHangul("がっこう"); // => "각코"
|
|
56
|
+
kanabarum.kanaToHangul("けっこん"); // => "켓콘"
|
|
57
|
+
kanabarum.kanaToHangul("ざっし"); // => "잣시"
|
|
58
|
+
kanabarum.kanaToHangul("やっちゃった"); // => "얏챳타"
|
|
58
59
|
|
|
59
60
|
// ん 규칙
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
kanabarum.kanaToHangul("にゃんこ"); // => "냥코"
|
|
62
|
+
kanabarum.kanaToHangul("さんぽ"); // => "삼포"
|
|
63
|
+
kanabarum.kanaToHangul("しんぶん"); // => "심분"
|
|
64
|
+
kanabarum.kanaToHangul("りんご"); // => "링고"
|
|
65
|
+
kanabarum.kanaToHangul("まんいち"); // => "만이치"
|
|
65
66
|
|
|
66
67
|
// 조사치환 は → わ
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
kanabarum.kanaToHangul("わたしはがくせいです"); // => "와타시와가쿠세데스"
|
|
69
|
+
kanabarum.kanaToHangul("これはペンです"); // => "코레와펜데스"
|
|
70
|
+
kanabarum.kanaToHangul("きょうはあつい"); // => "쿄와아츠이"
|
|
70
71
|
|
|
71
72
|
// 조사치환 へ → え
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
kanabarum.kanaToHangul("がっこうへいく"); // => "각코에이쿠"
|
|
74
|
+
kanabarum.kanaToHangul("うちへかえる"); // => "우치에카에루"
|
|
74
75
|
|
|
75
76
|
// 장모음(おう/よう/えい) 축약
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
kanabarum.kanaToHangul("さようなら"); // => "사요나라"
|
|
78
|
+
kanabarum.kanaToHangul("せんせい"); // => "센세"
|
|
79
|
+
kanabarum.kanaToHangul("おおさか"); // => "오사카"
|
|
79
80
|
|
|
80
81
|
// 가타카나
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
kanabarum.kanaToHangul("カタカナ"); // => "카타카나"
|
|
83
|
+
kanabarum.kanaToHangul("コーヒー"); // => "코히"
|
|
84
|
+
kanabarum.kanaToHangul("アイドル"); // => "아이도루"
|
|
84
85
|
|
|
85
86
|
// 장음 기호 변형(ー variants)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
kanabarum.kanaToHangul("コーヒー"); // => "코히"
|
|
88
|
+
kanabarum.kanaToHangul("パーティー"); // => "파티"
|
|
89
|
+
kanabarum.kanaToHangul("ゲーム"); // => "게무"
|
|
89
90
|
|
|
90
91
|
// 커스텀 사전
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
kanabarum.kanaToHangul("すみません"); // => "스미마셍"
|
|
93
|
+
kanabarum.kanaToHangul("かわいい"); // => "카와이"
|
|
94
|
+
kanabarum.kanaToHangul("はひふへほ"); // => "하히후헤호"
|
|
94
95
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
// 한자 (발음으로 자동 변환)
|
|
97
|
+
kanabarum.kanaToHangul("東京"); // => "도쿄"
|
|
98
|
+
kanabarum.kanaToHangul("日本語"); // => "니홍고"
|
|
99
|
+
kanabarum.kanaToHangul("東京とりんご"); // => "도쿄토링고"
|
|
99
100
|
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
converter("(がっこう)"); // => "(각코)"
|
|
101
|
+
// 한자 + 후리가나
|
|
102
|
+
kanabarum.kanaToHangul("誕生日(たんじょうび)"); // => "탄죠비(탄죠비)"
|
|
103
|
+
kanabarum.kanaToHangul("京都(きょうと)"); // => "쿄토(쿄토)"
|
|
104
104
|
|
|
105
|
+
// 특수문자, 마침표
|
|
106
|
+
kanabarum.kanaToHangul("コーヒー, ください。"); // => "코히, 쿠다사이。"
|
|
107
|
+
kanabarum.kanaToHangul("「きょう」"); // => "「쿄」"
|
|
108
|
+
kanabarum.kanaToHangul("(がっこう)"); // => "(각코)"
|
|
105
109
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
type KanaToHangul = (input: string) => string;
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Kanabarum - 일본어 가나를 한글로 변환하는 클래스
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const kanabarum = new Kanabarum();
|
|
8
|
+
* await kanabarum.init();
|
|
9
|
+
* kanabarum.kanaToHangul("こんにちは"); // "곤니치와"
|
|
10
|
+
*/
|
|
11
|
+
declare class Kanabarum {
|
|
12
|
+
private converter;
|
|
13
|
+
/**
|
|
14
|
+
* 토크나이저를 초기화합니다. 변환 전에 반드시 호출해야 합니다.
|
|
15
|
+
*/
|
|
16
|
+
init(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* 일본어 가나를 한글로 변환합니다.
|
|
19
|
+
* @param input 변환할 일본어 문자열
|
|
20
|
+
* @returns 한글로 변환된 문자열
|
|
21
|
+
* @throws init()이 호출되지 않은 경우 에러
|
|
22
|
+
*/
|
|
23
|
+
kanaToHangul(input: string): string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 일본어 가나를 한글로 변환합니다. (async helper)
|
|
27
|
+
* init 없이 바로 호출 가능합니다.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* await kanaToHangul("こんにちは"); // "곤니치와"
|
|
31
|
+
*/
|
|
4
32
|
declare function kanaToHangul(input: string): Promise<string>;
|
|
5
|
-
declare const KanaBarum: Readonly<{
|
|
6
|
-
init: typeof initKanaToHangul;
|
|
7
|
-
}>;
|
|
8
33
|
|
|
9
|
-
export {
|
|
34
|
+
export { type KanaToHangul, Kanabarum, kanaToHangul };
|
package/dist/index.js
CHANGED
|
@@ -445,9 +445,17 @@ function isKatakanaChar2(ch) {
|
|
|
445
445
|
}
|
|
446
446
|
function toHiraganaKey(s) {
|
|
447
447
|
const n = s.normalize("NFKC");
|
|
448
|
-
return Array.from(n).map(
|
|
449
|
-
|
|
450
|
-
|
|
448
|
+
return Array.from(n).map((ch) => {
|
|
449
|
+
if (ch === "\u30FC")
|
|
450
|
+
return ch;
|
|
451
|
+
if (!isKatakanaChar2(ch))
|
|
452
|
+
return ch;
|
|
453
|
+
const code = ch.codePointAt(0);
|
|
454
|
+
if (code >= 12449 && code <= 12534) {
|
|
455
|
+
return String.fromCodePoint(code - 96);
|
|
456
|
+
}
|
|
457
|
+
return ch;
|
|
458
|
+
}).join("");
|
|
451
459
|
}
|
|
452
460
|
function toKatakanaKey(s) {
|
|
453
461
|
const n = s.normalize("NFKC");
|
|
@@ -874,6 +882,30 @@ async function getTokenizer() {
|
|
|
874
882
|
}
|
|
875
883
|
|
|
876
884
|
// src/kanaBarum.ts
|
|
885
|
+
var Kanabarum = class {
|
|
886
|
+
constructor() {
|
|
887
|
+
this.converter = null;
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* 토크나이저를 초기화합니다. 변환 전에 반드시 호출해야 합니다.
|
|
891
|
+
*/
|
|
892
|
+
async init() {
|
|
893
|
+
const tokenizer = await getTokenizer();
|
|
894
|
+
this.converter = createKanaToHangul(tokenizer);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* 일본어 가나를 한글로 변환합니다.
|
|
898
|
+
* @param input 변환할 일본어 문자열
|
|
899
|
+
* @returns 한글로 변환된 문자열
|
|
900
|
+
* @throws init()이 호출되지 않은 경우 에러
|
|
901
|
+
*/
|
|
902
|
+
kanaToHangul(input) {
|
|
903
|
+
if (!this.converter) {
|
|
904
|
+
throw new Error("Kanabarum is not initialized. Call init() first.");
|
|
905
|
+
}
|
|
906
|
+
return this.converter(input);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
877
909
|
var cachedConverter = null;
|
|
878
910
|
var pendingInit = null;
|
|
879
911
|
async function initKanaToHangul() {
|
|
@@ -893,10 +925,7 @@ async function kanaToHangul(input) {
|
|
|
893
925
|
const converter = await initKanaToHangul();
|
|
894
926
|
return converter(input);
|
|
895
927
|
}
|
|
896
|
-
var KanaBarum = Object.freeze({
|
|
897
|
-
init: initKanaToHangul
|
|
898
|
-
});
|
|
899
928
|
|
|
900
|
-
export {
|
|
929
|
+
export { Kanabarum, kanaToHangul };
|
|
901
930
|
//# sourceMappingURL=out.js.map
|
|
902
931
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/normalizer.ts","../src/dictionary.ts","../src/particleRewriter.ts","../src/mora.ts","../src/coreConverter.ts","../src/kanaToHangul.ts","../src/tokenizer.ts","../src/kanaBarum.ts"],"names":["toHiragana","outCpStart","isKatakanaChar","out","info","require"],"mappings":"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C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAChD;AAEO,IAAM,QAAkC;AAAA,EAC7C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D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","sourcesContent":["export function normalizeInputText(input: string): string {\n // 1) NFD 결합문자(が/ぱ 등) 합성\n let normalized = input.normalize(\"NFC\");\n\n // 2) 반각 가타카나만 전각으로 (구두점/특수문자 최대한 보존)\n normalized = normalizeHalfwidthKatakanaOnly(normalized);\n\n // 3) 장음 기호 변종 최소 치환\n // - U+2015 HORIZONTAL BAR\n // - U+2500 BOX DRAWINGS LIGHT HORIZONTAL\n normalized = normalized.replace(/[\\u2015\\u2500]/g, \"ー\");\n\n // 4) ASCII hyphen이 가타카나 사이에 있을 때 장음 처리\n normalized = normalized\n .replace(/(?<=([\\u30A0-\\u30FF]))-/g, \"ー\")\n .replace(/-(?=[\\u30A0-\\u30FF])/g, \"ー\");\n\n return normalized;\n}\n\nfunction normalizeHalfwidthKatakanaOnly(s: string): string {\n // ✅ 반각 가타카나 + 탁점/반탁점(゙゚) + 반각 장음(ー)까지 함께 NFKC\n return s.replace(/[\\uFF66-\\uFF9F\\uFF70]+/g, (chunk) =>\n chunk.normalize(\"NFKC\"),\n );\n}\n\nexport function toHiragana(input: string): string {\n let out = \"\";\n for (const ch of input) {\n const code = ch.codePointAt(0)!;\n // カタカナ → ひらがな\n if (code >= 0x30a1 && code <= 0x30f6) {\n out += String.fromCodePoint(code - 0x60);\n continue;\n }\n if (ch === \"ー\") {\n out += ch;\n continue;\n }\n out += ch;\n }\n return out;\n}\n","// dictionary.ts\n// 한국인이 익숙한 발음을 담은 특수 사전\nexport interface SpecialDictionaryEntry {\n word: string;\n answer: string;\n hira?: boolean; // true면 히라가나 입력에도 적용\n kata?: boolean; // true면 카타카나 입력에도 적용\n}\n\nexport const SpecialDictionary: SpecialDictionaryEntry[] = [\n // [\"とうきょう\", \"도쿄\"],\n { word: \"こんにちは\", answer: \"곤니치와\", hira: true, kata: true },\n { word: \"こんばんは\", answer: \"곰방와\" },\n { word: \"すみません\", answer: \"스미마셍\" },\n { word: \"はひふへほ\", answer: \"하히후헤호\" },\n { word: \"かわいい\", answer: \"카와이\" },\n { word: \"つなみ\", answer: \"쓰나미\" },\n { word: \"ゆうり\", answer: \"유우리\" },\n { word: \"ミュージック\", answer: \"뮤지쿠\" },\n { word: \"ちゃん\", answer: \"쨩\" },\n];\n","// particleRewriter.ts\nimport type kuromoji from \"kuromoji\";\nimport type { Tokenizer } from \"./tokenizer\";\nimport { SpecialDictionary, SpecialDictionaryEntry } from \"./dictionary\";\n\n// --------------------------\n// local helper: toHiragana (protectedRanges는 hiraganaText 기준)\n// --------------------------\nfunction isKatakanaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x30a0 && c <= 0x30ff;\n}\n\nfunction containsKanji(s: string): boolean {\n for (const ch of s) {\n const c = ch.codePointAt(0)!;\n // CJK Unified Ideographs (U+4E00-U+9FFF) + Extension A (U+3400-U+4DBF)\n if ((c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf)) {\n return true;\n }\n }\n return false;\n}\nfunction toHiragana(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) => {\n // 장음은 드랍\n if (ch === \"ー\") return \"\";\n if (!isKatakanaChar(ch)) return ch;\n const code = ch.codePointAt(0)!;\n // カタカナ 문자 범위 (ァ~ヶ)만 변환\n if (code >= 0x30a1 && code <= 0x30f6) {\n return String.fromCodePoint(code - 0x60);\n }\n return ch;\n })\n .join(\"\");\n}\n\nfunction dictKeysForHiraganaText(dict: SpecialDictionaryEntry[]): string[] {\n // hiraganaText에서 실제로 등장할 수 있는 키만 모으기:\n // - entry.word 자체는 넣어도 되고(못 찾으면 무해)\n // - hira:true면 toHiragana(word)를 추가로 넣는다\n const keys: string[] = [];\n for (const e of dict) {\n keys.push(e.word);\n if (e.hira) keys.push(toHiragana(e.word));\n }\n // 중복 제거\n return [...new Set(keys)].filter(Boolean);\n}\n\ntype Range = { start: number; end: number }; // [start, end)\n\nfunction rangesOverlap(a: Range, b: Range): boolean {\n return a.start < b.end && b.start < a.end;\n}\n\nfunction buildProtectedRanges(text: string, keys: string[]): Range[] {\n const sorted = [...new Set(keys)].sort((a, b) => b.length - a.length);\n const ranges: Range[] = [];\n\n for (const key of sorted) {\n if (!key) continue;\n let from = 0;\n while (true) {\n const idx = text.indexOf(key, from);\n if (idx === -1) break;\n\n const cand: Range = { start: idx, end: idx + key.length };\n if (!ranges.some((r) => rangesOverlap(r, cand))) {\n ranges.push(cand);\n }\n from = idx + 1;\n }\n }\n\n ranges.sort((a, b) => a.start - b.start);\n return ranges;\n}\n\nfunction isProtectedSpan(protectedRanges: Range[], start: number, end: number) {\n for (const r of protectedRanges) {\n if (start < r.end && end > r.start) return true;\n }\n return false;\n}\n\nexport const HARD_BOUNDARY_SURF = new Set([\n \"。\",\n \"、\",\n \"!\",\n \"?\",\n \"!\",\n \"?\",\n \" \",\n \" \",\n]);\nconst HARD_BOUNDARY_DETAIL1 = new Set([\n \"句点\",\n \"読点\",\n \"括弧開\",\n \"括弧閉\",\n \"空白\",\n]);\n\nconst LEXICAL_HE_ENDINGS = [\n \"いにしへ\",\n \"おきへ\",\n \"もとへ\",\n \"すえへ\",\n \"すゑへ\",\n \"かみへ\",\n \"くにへ\",\n \"きしへ\",\n] as const;\n\nfunction isHardBoundaryToken(t: kuromoji.IpadicFeatures): boolean {\n if (t.pos !== \"記号\") return false;\n if (HARD_BOUNDARY_SURF.has(t.surface_form)) return true;\n return HARD_BOUNDARY_DETAIL1.has(t.pos_detail_1 ?? \"\");\n}\nfunction isContentToken(t: kuromoji.IpadicFeatures): boolean {\n if (t.pos === \"記号\") return !isHardBoundaryToken(t);\n return true;\n}\n\nfunction prevContentIdx(tokens: kuromoji.IpadicFeatures[], i: number): number {\n for (let j = i - 1; j >= 0; j -= 1) if (isContentToken(tokens[j])) return j;\n return -1;\n}\nfunction nextContentIdx(tokens: kuromoji.IpadicFeatures[], i: number): number {\n for (let j = i + 1; j < tokens.length; j += 1)\n if (isContentToken(tokens[j])) return j;\n return -1;\n}\nfunction nextBoundaryOrEnd(\n tokens: kuromoji.IpadicFeatures[],\n i: number,\n): boolean {\n for (let j = i + 1; j < tokens.length; j += 1) {\n if (isHardBoundaryToken(tokens[j])) continue;\n return false;\n }\n return true;\n}\n\nexport type TokenSpan = {\n start: number; // rewritten 기준\n end: number; // rewritten 기준\n surface: string;\n\n pos?: string;\n pos1?: string;\n pos2?: string;\n pos3?: string;\n\n // ✅ 원문 기반 힌트: katakana 포함 여부(노ート 같은 케이스 차단용)\n originHadKatakana?: boolean;\n};\n\n/**\n * ✅ 핵심:\n * - 토큰화는 \"prewrite 이전\"에 수행 (원문/정규화 기준)\n * - prewrite는 \"토큰 품사\"를 쓰되, 실제 replace는 hiraganaText slice로 수행\n * - 결과로 rewrittenText + rewrittenTokenSpans를 만들어 core로 넘김\n *\n * 가정: hiraganaText와 originalText는 길이가 동일 (toHiragana는 1:1 치환)\n */\nexport function rewriteParticlesFromTokenization(\n originalText: string,\n hiraganaText: string,\n tokenizerTokens: kuromoji.IpadicFeatures[],\n): { rewritten: string; spans: TokenSpan[]; rewrittenOriginal: string } {\n // 코드포인트 배열로 변환 (kuromoji word_position이 코드포인트 기준)\n const hiraChars = Array.from(hiraganaText);\n const originChars = Array.from(originalText);\n\n // entry 기반\n const protectedRanges = buildProtectedRanges(\n hiraganaText,\n dictKeysForHiraganaText(SpecialDictionary),\n );\n\n let out = \"\";\n let origOut = \"\"; // 한자→pronunciation 변환된 원본\n const spans: TokenSpan[] = [];\n\n let cursorInText = 0;\n\n for (let i = 0; i < tokenizerTokens.length; i += 1) {\n const tok = tokenizerTokens[i];\n const wp = (tok as any).word_position as number | undefined;\n const surf = tok.surface_form;\n const surfCpLen = [...surf].length; // 코드포인트 길이\n\n // kuromoji word_position은 코드포인트 기준 (1-based)\n const start = typeof wp === \"number\" ? wp - 1 : cursorInText;\n const end = start + surfCpLen;\n cursorInText = end;\n\n // 한자가 포함된 경우 pronunciation을 사용\n const hasKanji = containsKanji(surf) && tok.pronunciation;\n const hiraSurf = hasKanji\n ? toHiragana(tok.pronunciation!)\n : hiraChars.slice(start, end).join(\"\");\n // 원본도 한자면 pronunciation에서 장음 제거 (길이 맞추기)\n const origSurfForOut = hasKanji\n ? tok.pronunciation!.replace(/ー/g, \"\")\n : originChars.slice(start, end).join(\"\");\n const originSurf = originChars.slice(start, end).join(\"\");\n\n const originHadKatakana = /[\\u30A0-\\u30FF]/.test(originSurf);\n\n // ✅ 불필요하고 위험한 isProtected(튜플 기반 + includes 난사) 제거\n // protectedRanges 기반으로만 판단\n if (isProtectedSpan(protectedRanges, start, end)) {\n const outCpStart = [...out].length;\n out += hiraSurf;\n origOut += origSurfForOut;\n spans.push({\n start: outCpStart,\n end: [...out].length,\n surface: hiraSurf,\n pos: tok.pos,\n pos1: tok.pos_detail_1 ?? undefined,\n pos2: tok.pos_detail_2 ?? undefined,\n pos3: tok.pos_detail_3 ?? undefined,\n originHadKatakana,\n });\n continue;\n }\n\n let replaced = hiraSurf;\n\n // --- は -> わ (계조사) ---\n if (tok.pos === \"助詞\" && hiraSurf === \"は\") {\n if (i > 0 && tokenizerTokens[i - 1].surface_form === \"は\") {\n // keep\n } else if (\n i + 1 < tokenizerTokens.length &&\n tokenizerTokens[i + 1].surface_form === \"は\"\n ) {\n // keep\n } else {\n const prevIdx = prevContentIdx(tokenizerTokens, i);\n if (prevIdx >= 0) {\n const nextIdx = nextContentIdx(tokenizerTokens, i);\n const hasNextContent = nextIdx >= 0;\n const isEndOrPunct = nextBoundaryOrEnd(tokenizerTokens, i);\n\n const prevTok = tokenizerTokens[prevIdx];\n const prevWpHa = (prevTok as any).word_position as number | undefined;\n const prevStartHa = typeof prevWpHa === \"number\" ? prevWpHa - 1 : 0;\n const prevEndHa = prevStartHa + [...prevTok.surface_form].length;\n const prevHira = hiraChars.slice(prevStartHa, prevEndHa).join(\"\");\n\n if (\n !prevHira.includes(\"っ\") &&\n tok.pos_detail_1 === \"係助詞\" &&\n (hasNextContent || isEndOrPunct) &&\n prevTok.pos !== \"助詞\"\n ) {\n replaced = \"わ\";\n }\n }\n }\n }\n\n // --- へ -> え (격조사) ---\n if (tok.pos === \"助詞\" && hiraSurf === \"へ\") {\n // 바로 왼쪽이 공백이면 keep (코드포인트 배열 사용)\n if (start > 0) {\n const left = hiraChars[start - 1];\n if (\n left === \" \" ||\n left === \" \" ||\n left === \"\\t\" ||\n left === \"\\n\" ||\n left === \"\\r\"\n ) {\n // keep\n } else {\n const prevIdx = prevContentIdx(tokenizerTokens, i);\n if (prevIdx >= 0) {\n const prevTok = tokenizerTokens[prevIdx];\n const prevWp = (prevTok as any).word_position as number | undefined;\n const prevStart = typeof prevWp === \"number\" ? prevWp - 1 : 0;\n const prevEnd = prevStart + [...prevTok.surface_form].length;\n\n const prevHiraSurf = hiraChars.slice(prevStart, prevEnd).join(\"\");\n\n if (\n LEXICAL_HE_ENDINGS.some((w) => (prevHiraSurf + \"へ\").endsWith(w))\n ) {\n // keep lexical endings\n } else if (prevHiraSurf.endsWith(\"の\")) {\n // keep \"...のへ\"\n } else if (tok.pos_detail_1 === \"格助詞\") {\n replaced = \"え\";\n }\n }\n }\n }\n }\n\n const outCpStart = [...out].length;\n out += replaced;\n origOut += origSurfForOut;\n\n spans.push({\n start: outCpStart,\n end: [...out].length,\n surface: replaced,\n pos: tok.pos,\n pos1: tok.pos_detail_1 ?? undefined,\n pos2: tok.pos_detail_2 ?? undefined,\n pos3: tok.pos_detail_3 ?? undefined,\n originHadKatakana,\n });\n }\n\n return { rewritten: out, spans, rewrittenOriginal: origOut };\n}\n\n/**\n * 외부에서 쓰기 편한 래퍼:\n * - originalText를 tokenizer로 먼저 tokenize\n * - hiraganaText는 호출자가 넘겨줌(길이 동일 가정)\n */\nexport function tokenizeAndRewriteParticles(\n originalText: string,\n hiraganaText: string,\n tokenizer: Tokenizer,\n): {\n rewritten: string;\n spans: TokenSpan[];\n rewrittenOriginal: string;\n rawTokens: kuromoji.IpadicFeatures[];\n} {\n const rawTokens = tokenizer.tokenize(originalText);\n // console.log(rawTokens);\n const { rewritten, spans, rewrittenOriginal } =\n rewriteParticlesFromTokenization(originalText, hiraganaText, rawTokens);\n return { rewritten, spans, rewrittenOriginal, rawTokens };\n}\n","// --- Tables (당신 코드 그대로) ---\nexport type VowelMain = \"a\" | \"i\" | \"u\" | \"e\" | \"o\";\nexport type ConsClass =\n | \"vowel\"\n | \"k\"\n | \"s\"\n | \"t\"\n | \"n\"\n | \"h\"\n | \"m\"\n | \"y\"\n | \"r\"\n | \"w\"\n | \"g\"\n | \"z\"\n | \"d\"\n | \"b\"\n | \"p\";\n\nexport type MoraInfo = {\n out: string;\n vowelMain: VowelMain;\n consClass: ConsClass;\n vowelOnly?: boolean;\n wasYouon?: boolean;\n};\n\nexport const SINGLE: Record<string, MoraInfo> = {\n あ: { out: \"아\", vowelMain: \"a\", consClass: \"vowel\", vowelOnly: true },\n い: { out: \"이\", vowelMain: \"i\", consClass: \"vowel\", vowelOnly: true },\n う: { out: \"우\", vowelMain: \"u\", consClass: \"vowel\", vowelOnly: true },\n え: { out: \"에\", vowelMain: \"e\", consClass: \"vowel\", vowelOnly: true },\n お: { out: \"오\", vowelMain: \"o\", consClass: \"vowel\", vowelOnly: true },\n\n か: { out: \"카\", vowelMain: \"a\", consClass: \"k\" },\n き: { out: \"키\", vowelMain: \"i\", consClass: \"k\" },\n く: { out: \"쿠\", vowelMain: \"u\", consClass: \"k\" },\n け: { out: \"케\", vowelMain: \"e\", consClass: \"k\" },\n こ: { out: \"코\", vowelMain: \"o\", consClass: \"k\" },\n\n さ: { out: \"사\", vowelMain: \"a\", consClass: \"s\" },\n し: { out: \"시\", vowelMain: \"i\", consClass: \"s\" },\n す: { out: \"스\", vowelMain: \"u\", consClass: \"s\" },\n せ: { out: \"세\", vowelMain: \"e\", consClass: \"s\" },\n そ: { out: \"소\", vowelMain: \"o\", consClass: \"s\" },\n\n た: { out: \"타\", vowelMain: \"a\", consClass: \"t\" },\n ち: { out: \"치\", vowelMain: \"i\", consClass: \"t\" },\n つ: { out: \"츠\", vowelMain: \"u\", consClass: \"t\" },\n て: { out: \"테\", vowelMain: \"e\", consClass: \"t\" },\n と: { out: \"토\", vowelMain: \"o\", consClass: \"t\" },\n\n な: { out: \"나\", vowelMain: \"a\", consClass: \"n\" },\n に: { out: \"니\", vowelMain: \"i\", consClass: \"n\" },\n ぬ: { out: \"누\", vowelMain: \"u\", consClass: \"n\" },\n ね: { out: \"네\", vowelMain: \"e\", consClass: \"n\" },\n の: { out: \"노\", vowelMain: \"o\", consClass: \"n\" },\n\n は: { out: \"하\", vowelMain: \"a\", consClass: \"h\" },\n ひ: { out: \"히\", vowelMain: \"i\", consClass: \"h\" },\n ふ: { out: \"후\", vowelMain: \"u\", consClass: \"h\" },\n へ: { out: \"헤\", vowelMain: \"e\", consClass: \"h\" },\n ほ: { out: \"호\", vowelMain: \"o\", consClass: \"h\" },\n\n ま: { out: \"마\", vowelMain: \"a\", consClass: \"m\" },\n み: { out: \"미\", vowelMain: \"i\", consClass: \"m\" },\n む: { out: \"무\", vowelMain: \"u\", consClass: \"m\" },\n め: { out: \"메\", vowelMain: \"e\", consClass: \"m\" },\n も: { out: \"모\", vowelMain: \"o\", consClass: \"m\" },\n\n や: { out: \"야\", vowelMain: \"a\", consClass: \"y\" },\n ゆ: { out: \"유\", vowelMain: \"u\", consClass: \"y\" },\n よ: { out: \"요\", vowelMain: \"o\", consClass: \"y\" },\n\n ら: { out: \"라\", vowelMain: \"a\", consClass: \"r\" },\n り: { out: \"리\", vowelMain: \"i\", consClass: \"r\" },\n る: { out: \"루\", vowelMain: \"u\", consClass: \"r\" },\n れ: { out: \"레\", vowelMain: \"e\", consClass: \"r\" },\n ろ: { out: \"로\", vowelMain: \"o\", consClass: \"r\" },\n\n わ: { out: \"와\", vowelMain: \"a\", consClass: \"w\" },\n を: { out: \"오\", vowelMain: \"o\", consClass: \"w\" },\n\n が: { out: \"가\", vowelMain: \"a\", consClass: \"g\" },\n ぎ: { out: \"기\", vowelMain: \"i\", consClass: \"g\" },\n ぐ: { out: \"구\", vowelMain: \"u\", consClass: \"g\" },\n げ: { out: \"게\", vowelMain: \"e\", consClass: \"g\" },\n ご: { out: \"고\", vowelMain: \"o\", consClass: \"g\" },\n\n ざ: { out: \"자\", vowelMain: \"a\", consClass: \"z\" },\n じ: { out: \"지\", vowelMain: \"i\", consClass: \"z\" },\n ず: { out: \"즈\", vowelMain: \"u\", consClass: \"z\" },\n ぜ: { out: \"제\", vowelMain: \"e\", consClass: \"z\" },\n ぞ: { out: \"조\", vowelMain: \"o\", consClass: \"z\" },\n\n だ: { out: \"다\", vowelMain: \"a\", consClass: \"d\" },\n ぢ: { out: \"지\", vowelMain: \"i\", consClass: \"d\" },\n づ: { out: \"즈\", vowelMain: \"u\", consClass: \"d\" },\n で: { out: \"데\", vowelMain: \"e\", consClass: \"d\" },\n ど: { out: \"도\", vowelMain: \"o\", consClass: \"d\" },\n\n ば: { out: \"바\", vowelMain: \"a\", consClass: \"b\" },\n び: { out: \"비\", vowelMain: \"i\", consClass: \"b\" },\n ぶ: { out: \"부\", vowelMain: \"u\", consClass: \"b\" },\n べ: { out: \"베\", vowelMain: \"e\", consClass: \"b\" },\n ぼ: { out: \"보\", vowelMain: \"o\", consClass: \"b\" },\n\n ぱ: { out: \"파\", vowelMain: \"a\", consClass: \"p\" },\n ぴ: { out: \"피\", vowelMain: \"i\", consClass: \"p\" },\n ぷ: { out: \"푸\", vowelMain: \"u\", consClass: \"p\" },\n ぺ: { out: \"페\", vowelMain: \"e\", consClass: \"p\" },\n ぽ: { out: \"포\", vowelMain: \"o\", consClass: \"p\" },\n\n ゔ: { out: \"부\", vowelMain: \"u\", consClass: \"b\" },\n};\n\nexport const YOUON: Record<string, MoraInfo> = {\n きゃ: { out: \"캬\", vowelMain: \"a\", consClass: \"k\", wasYouon: true },\n きゅ: { out: \"큐\", vowelMain: \"u\", consClass: \"k\", wasYouon: true },\n きょ: { out: \"쿄\", vowelMain: \"o\", consClass: \"k\", wasYouon: true },\n\n しゃ: { out: \"샤\", vowelMain: \"a\", consClass: \"s\", wasYouon: true },\n しゅ: { out: \"슈\", vowelMain: \"u\", consClass: \"s\", wasYouon: true },\n しょ: { out: \"쇼\", vowelMain: \"o\", consClass: \"s\", wasYouon: true },\n\n ちゃ: { out: \"챠\", vowelMain: \"a\", consClass: \"t\", wasYouon: true },\n ちゅ: { out: \"츄\", vowelMain: \"u\", consClass: \"t\", wasYouon: true },\n ちょ: { out: \"쵸\", vowelMain: \"o\", consClass: \"t\", wasYouon: true },\n てゅ: { out: \"튜\", vowelMain: \"u\", consClass: \"t\", wasYouon: true },\n でゅ: { out: \"듀\", vowelMain: \"u\", consClass: \"d\", wasYouon: true },\n\n にゃ: { out: \"냐\", vowelMain: \"a\", consClass: \"n\", wasYouon: true },\n にゅ: { out: \"뉴\", vowelMain: \"u\", consClass: \"n\", wasYouon: true },\n にょ: { out: \"뇨\", vowelMain: \"o\", consClass: \"n\", wasYouon: true },\n\n ひゃ: { out: \"햐\", vowelMain: \"a\", consClass: \"h\", wasYouon: true },\n ひゅ: { out: \"휴\", vowelMain: \"u\", consClass: \"h\", wasYouon: true },\n ひょ: { out: \"효\", vowelMain: \"o\", consClass: \"h\", wasYouon: true },\n ふゃ: { out: \"퍄\", vowelMain: \"a\", consClass: \"p\", wasYouon: true },\n ふゅ: { out: \"퓨\", vowelMain: \"u\", consClass: \"p\", wasYouon: true },\n ふょ: { out: \"표\", vowelMain: \"o\", consClass: \"p\", wasYouon: true },\n\n みゃ: { out: \"먀\", vowelMain: \"a\", consClass: \"m\", wasYouon: true },\n みゅ: { out: \"뮤\", vowelMain: \"u\", consClass: \"m\", wasYouon: true },\n みょ: { out: \"묘\", vowelMain: \"o\", consClass: \"m\", wasYouon: true },\n\n りゃ: { out: \"랴\", vowelMain: \"a\", consClass: \"r\", wasYouon: true },\n りゅ: { out: \"류\", vowelMain: \"u\", consClass: \"r\", wasYouon: true },\n りょ: { out: \"료\", vowelMain: \"o\", consClass: \"r\", wasYouon: true },\n\n ぎゃ: { out: \"갸\", vowelMain: \"a\", consClass: \"g\", wasYouon: true },\n ぎゅ: { out: \"규\", vowelMain: \"u\", consClass: \"g\", wasYouon: true },\n ぎょ: { out: \"교\", vowelMain: \"o\", consClass: \"g\", wasYouon: true },\n\n じゃ: { out: \"쟈\", vowelMain: \"a\", consClass: \"z\", wasYouon: true },\n じゅ: { out: \"쥬\", vowelMain: \"u\", consClass: \"z\", wasYouon: true },\n じょ: { out: \"죠\", vowelMain: \"o\", consClass: \"z\", wasYouon: true },\n\n びゃ: { out: \"뱌\", vowelMain: \"a\", consClass: \"b\", wasYouon: true },\n びゅ: { out: \"뷰\", vowelMain: \"u\", consClass: \"b\", wasYouon: true },\n びょ: { out: \"뵤\", vowelMain: \"o\", consClass: \"b\", wasYouon: true },\n\n ぴゃ: { out: \"퍄\", vowelMain: \"a\", consClass: \"p\", wasYouon: true },\n ぴゅ: { out: \"퓨\", vowelMain: \"u\", consClass: \"p\", wasYouon: true },\n ぴょ: { out: \"표\", vowelMain: \"o\", consClass: \"p\", wasYouon: true },\n};\n\nexport const LOAN: Record<string, MoraInfo> = {\n てぃ: { out: \"티\", vowelMain: \"i\", consClass: \"t\" },\n でぃ: { out: \"디\", vowelMain: \"i\", consClass: \"d\" },\n ちぇ: { out: \"체\", vowelMain: \"e\", consClass: \"t\" },\n しぇ: { out: \"셰\", vowelMain: \"e\", consClass: \"s\" },\n じぇ: { out: \"제\", vowelMain: \"e\", consClass: \"z\" },\n つぁ: { out: \"차\", vowelMain: \"a\", consClass: \"t\" },\n つぃ: { out: \"치\", vowelMain: \"i\", consClass: \"t\" },\n つぇ: { out: \"체\", vowelMain: \"e\", consClass: \"t\" },\n つぉ: { out: \"초\", vowelMain: \"o\", consClass: \"t\" },\n ふぁ: { out: \"파\", vowelMain: \"a\", consClass: \"p\" },\n ふぃ: { out: \"피\", vowelMain: \"i\", consClass: \"p\" },\n ふぇ: { out: \"페\", vowelMain: \"e\", consClass: \"p\" },\n ふぉ: { out: \"포\", vowelMain: \"o\", consClass: \"p\" },\n ぐぁ: { out: \"과\", vowelMain: \"a\", consClass: \"g\" },\n ぐぃ: { out: \"귀\", vowelMain: \"i\", consClass: \"g\" },\n ぐぇ: { out: \"궤\", vowelMain: \"e\", consClass: \"g\" },\n ぐぉ: { out: \"궈\", vowelMain: \"o\", consClass: \"g\" },\n くぁ: { out: \"콰\", vowelMain: \"a\", consClass: \"k\" },\n くぃ: { out: \"퀴\", vowelMain: \"i\", consClass: \"k\" },\n くぇ: { out: \"퀘\", vowelMain: \"e\", consClass: \"k\" },\n くぉ: { out: \"쿼\", vowelMain: \"o\", consClass: \"k\" },\n どぁ: { out: \"돠\", vowelMain: \"a\", consClass: \"d\" },\n どぅ: { out: \"두\", vowelMain: \"u\", consClass: \"d\" },\n どぉ: { out: \"둬\", vowelMain: \"o\", consClass: \"d\" },\n ゔぁ: { out: \"바\", vowelMain: \"a\", consClass: \"b\" },\n ゔぃ: { out: \"비\", vowelMain: \"i\", consClass: \"b\" },\n ゔぇ: { out: \"베\", vowelMain: \"e\", consClass: \"b\" },\n ゔぉ: { out: \"보\", vowelMain: \"o\", consClass: \"b\" },\n};\n\nexport const SMALL_Y = new Set([\"ゃ\", \"ゅ\", \"ょ\"]);\nexport const SMALL_V = new Set([\"ぁ\", \"ぃ\", \"ぅ\", \"ぇ\", \"ぉ\"]);\n\nexport const U_DROP_KEYS = new Set([\n \"ゆ\",\n \"きゅ\",\n \"しゅ\",\n \"ちゅ\",\n \"にゅ\",\n \"ひゅ\",\n \"みゅ\",\n \"りゅ\",\n \"ぎゅ\",\n \"じゅ\",\n \"びゅ\",\n \"ぴゅ\",\n]);\n","// coreConverter.ts\nimport { SpecialDictionary, type SpecialDictionaryEntry } from \"./dictionary\";\nimport { HARD_BOUNDARY_SURF, type TokenSpan } from \"./particleRewriter\";\nimport {\n type ConsClass,\n type MoraInfo,\n SINGLE,\n YOUON,\n LOAN,\n SMALL_Y,\n SMALL_V,\n U_DROP_KEYS,\n} from \"./mora\";\n\n// --------------------------\n// Kana normalize helpers\n// --------------------------\nfunction isHiraganaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n}\nfunction isKatakanaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x30a0 && c <= 0x30ff;\n}\nfunction toHiraganaKey(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) =>\n isKatakanaChar(ch) ? String.fromCodePoint(ch.codePointAt(0)! - 0x60) : ch,\n )\n .join(\"\");\n}\nfunction toKatakanaKey(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) =>\n isHiraganaChar(ch) ? String.fromCodePoint(ch.codePointAt(0)! + 0x60) : ch,\n )\n .join(\"\");\n}\n\ntype DictStream = \"orig\" | \"rewritten\";\ntype CompiledDictItem = {\n keyChars: string[];\n answer: string;\n stream: DictStream;\n};\n\nfunction compileSpecialDictionary(\n dict: SpecialDictionaryEntry[],\n): CompiledDictItem[] {\n const items: CompiledDictItem[] = [];\n\n for (const e of dict) {\n // 기본: exact word는 원본에서만\n items.push({\n keyChars: Array.from(e.word),\n answer: e.answer,\n stream: \"orig\",\n });\n\n // hira:true => hiragana 스트림에서만 (입력 전체가 히라로 바뀌는 파이프라인이기 때문)\n if (e.hira) {\n const k = toHiraganaKey(e.word);\n items.push({\n keyChars: Array.from(k),\n answer: e.answer,\n stream: \"rewritten\",\n });\n }\n\n // kata:true => 원본에서만\n if (e.kata) {\n const k = toKatakanaKey(e.word);\n items.push({ keyChars: Array.from(k), answer: e.answer, stream: \"orig\" });\n }\n }\n\n // 긴 키 우선\n items.sort((a, b) => b.keyChars.length - a.keyChars.length);\n return items;\n}\n\nconst COMPILED_SPECIAL_DICT = compileSpecialDictionary(SpecialDictionary);\n\nfunction isHiragana(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n}\n\nfunction isKana(ch: string): boolean {\n return isHiragana(ch) || ch === \"ー\";\n}\n\nexport function coreKanaToHangulConvert(\n s: string,\n opts: { tokens: TokenSpan[]; original: string },\n): string {\n // 코드포인트 배열로 변환 (surrogate pair 문제 해결)\n const chars = Array.from(s);\n const origChars = Array.from(opts?.original ?? s);\n\n // --- Hangul utilities ---\n const HANGUL_BASE = 0xac00;\n const HANGUL_END = 0xd7a3;\n\n function isHangulSyllable(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= HANGUL_BASE && c <= HANGUL_END;\n }\n\n const JONG = {\n NONE: 0,\n G: 1, // ㄱ\n N: 4, // ㄴ\n M: 16, // ㅁ\n B: 17, // ㅂ\n S: 19, // ㅅ\n NG: 21, // ㅇ\n } as const;\n\n function addFinal(syl: string, jong: number): string {\n if (!isHangulSyllable(syl)) return syl;\n const code = syl.codePointAt(0)! - HANGUL_BASE;\n const cho = Math.floor(code / 588);\n const jung = Math.floor((code % 588) / 28);\n return String.fromCodePoint(HANGUL_BASE + cho * 588 + jung * 28 + jong);\n }\n\n function replaceLastHangul(out: string, jong: number): string {\n if (!out) return out;\n const last = out[out.length - 1];\n if (!isHangulSyllable(last)) return out;\n return out.slice(0, -1) + addFinal(last, jong);\n }\n\n // --- Kana classification ---\n function isHiragana(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n }\n function isKana(ch: string): boolean {\n return isHiragana(ch) || ch === \"ー\";\n }\n\n type ReadMora = { key: string; len: number; info?: MoraInfo } | null;\n\n function readMoraAt(idx: number): ReadMora {\n if (idx >= chars.length) return null;\n\n const c0 = chars[idx];\n const c1 = chars[idx + 1];\n\n if (c1 && SMALL_V.has(c1)) {\n const key2 = c0 + c1;\n const info = LOAN[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n if (c1 && SMALL_Y.has(c1)) {\n const key2 = c0 + c1;\n const info = YOUON[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n const info = SINGLE[c0];\n if (info) return { key: c0, len: 1, info };\n\n return { key: c0, len: 1, info: undefined };\n }\n\n function readOriginalMoraAt(idx: number): ReadMora {\n if (idx >= origChars.length) return null;\n\n const c0 = origChars[idx];\n const c1 = origChars[idx + 1];\n\n if (c1 && SMALL_V.has(c1)) {\n const key2 = c0 + c1;\n const info = LOAN[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n if (c1 && SMALL_Y.has(c1)) {\n const key2 = c0 + c1;\n const info = YOUON[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n const info = SINGLE[c0];\n if (info) return { key: c0, len: 1, info };\n\n return { key: c0, len: 1, info: undefined };\n }\n\n function isLabialStart(cons: ConsClass): boolean {\n return cons === \"m\" || cons === \"b\" || cons === \"p\";\n }\n\n // 토큰 컨텍스트 탐색용\n const tokens = opts?.tokens ?? null;\n let tokIdx = 0;\n\n function syncTokenIndex(charIndex: number) {\n if (!tokens) return;\n while (tokIdx + 1 < tokens.length && tokens[tokIdx].end <= charIndex) {\n tokIdx++;\n }\n }\n\n function curToken(charIndex: number): TokenSpan | null {\n if (!tokens) return null;\n syncTokenIndex(charIndex);\n const t = tokens[tokIdx];\n if (t && t.start <= charIndex && charIndex < t.end) return t;\n return null;\n }\n\n function prevToken(): TokenSpan | null {\n if (!tokens) return null;\n return tokIdx - 1 >= 0 ? tokens[tokIdx - 1] : null;\n }\n function nextToken(): TokenSpan | null {\n if (!tokens) return null;\n return tokIdx + 1 < tokens.length ? tokens[tokIdx + 1] : null;\n }\n\n // ✅ 유성화 차단 next\n const INITIAL_VOICING_BLOCK_NEXT = new Set([\"い\", \"ひ\", \"ん\", \"て\"]);\n\n function peekNextMoraKeySkippingChoonpu(fromIdx: number): string | null {\n let j = fromIdx;\n while (j < chars.length && chars[j] === \"ー\") j++;\n const m = readMoraAt(j);\n return m?.key ?? null;\n }\n\n // ✅ \"-san\" 판별 (코드포인트 인덱스 기준)\n const SAN_PARTICLES = new Set([\"は\", \"わ\", \"へ\", \"え\", \"を\", \"お\"]);\n function isSanHonorificAt(cpIdx: number): boolean {\n const t = curToken(cpIdx);\n if (!t) return false;\n if (cpIdx < 1 || chars[cpIdx - 1] !== \"さ\") return false;\n\n // t.start는 코드포인트 인덱스, chars.slice 사용\n const local = chars.slice(t.start, cpIdx + 1).join(\"\");\n if (!local.endsWith(\"さん\")) return false;\n\n const hasPrefixInsideToken = cpIdx - 1 > t.start;\n const p = prevToken();\n const prevIsAttachable =\n !!p &&\n p.end === t.start &&\n p.surface.length > 0 &&\n !HARD_BOUNDARY_SURF.has(p.surface);\n\n if (!hasPrefixInsideToken && !prevIsAttachable) return false;\n\n const n = nextToken();\n if (!n) return true;\n if (n.pos === \"記号\" && HARD_BOUNDARY_SURF.has(n.surface)) return true;\n if (n.pos === \"助詞\" && SAN_PARTICLES.has(n.surface)) return true;\n return false;\n }\n\n let out = \"\";\n let i = 0;\n\n let lastMora: MoraInfo | null = null;\n\n while (i < chars.length) {\n // 토큰 기반 \"단어 시작\" 정의: i가 content 토큰 start면 true\n let atTokenStart = false;\n let tokForI: TokenSpan | null = null;\n\n if (tokens) {\n tokForI = curToken(i);\n // ✅ 토큰 시작이면 일단 단어 시작 후보로 인정\n atTokenStart = !!tokForI && tokForI.start === i;\n\n // ✅ 유성화/단어시작 판정에서 \"기호\"와 \"원문 카타카나 토큰\"만 컷\n if (tokForI?.pos === \"記号\") atTokenStart = false;\n } else {\n atTokenStart = i === 0;\n }\n\n // --------------------------\n // ✅ SpecialDictionary (entry 기반 + hira/kata 옵션)\n // --------------------------\n let matchedSpecial = false;\n\n // 긴 키부터 순회하므로, 앞에서 걸리면 끝\n for (const it of COMPILED_SPECIAL_DICT) {\n const src = it.stream === \"orig\" ? origChars : chars; // chars=rewritten\n const len = it.keyChars.length;\n if (i + len > src.length) continue;\n\n let ok = true;\n for (let k = 0; k < len; k++) {\n if (src[i + k] !== it.keyChars[k]) {\n ok = false;\n break;\n }\n }\n if (!ok) continue;\n\n out += it.answer;\n i += len;\n\n // 사전 치환은 단어 단위 => 상태 초기화\n lastMora = null;\n\n matchedSpecial = true;\n break;\n }\n if (matchedSpecial) continue;\n\n const ch = chars[i];\n\n /**\n * ー 표시는 그냥 드랍\n */\n if (ch === \"ー\") {\n i += 1;\n continue;\n }\n\n /**\n * 비가나: 그대로\n */\n if (!isKana(ch)) {\n out += ch;\n i += 1;\n lastMora = null;\n continue;\n }\n\n /**\n * 촉음 규칙\n */\n if (ch === \"っ\") {\n if (!out || !isHangulSyllable(out[out.length - 1])) {\n // \"ッ\"으로 바꾸기\n out += \"ッ\";\n i += 1;\n lastMora = null;\n continue;\n }\n\n const next = readMoraAt(i + 1);\n\n const prevV = lastMora?.vowelMain ?? \"a\";\n const nextInfo = next?.info;\n const nextCons: ConsClass = nextInfo?.consClass ?? \"t\";\n\n let jong: number = JONG.S;\n if (nextCons === \"p\" || nextCons === \"b\") jong = JONG.B;\n else if (nextCons === \"k\" || nextCons === \"g\") {\n jong = prevV === \"e\" || prevV === \"i\" ? JONG.S : JONG.G;\n } else {\n jong = JONG.S;\n }\n\n out = replaceLastHangul(out, jong);\n i += 1;\n continue;\n }\n\n /**\n * おお 장음 규칙\n */\n if (ch === \"お\" && chars[i + 1] === \"お\") {\n let j = i;\n while (chars[j] === \"お\") j++;\n out += \"오\";\n i = j;\n lastMora = {\n out: \"오\",\n vowelMain: \"o\",\n consClass: \"vowel\",\n vowelOnly: true,\n };\n continue;\n }\n\n const mora = readMoraAt(i);\n const originalMora = readOriginalMoraAt(i);\n if (!mora) {\n out += chars[i];\n i += 1;\n lastMora = null;\n continue;\n }\n // 에러\n if (!originalMora) {\n throw Error(\"원본 모라 손실\");\n }\n\n /**\n * ん 규칙\n */\n if (mora.key === \"ん\") {\n const next = readMoraAt(i + 1);\n const nextInfo = next?.info;\n\n const hasPrevHangul =\n out.length > 0 && isHangulSyllable(out[out.length - 1]);\n if (!hasPrevHangul) {\n out += \"ㄴ\";\n i += 1;\n lastMora = null;\n continue;\n }\n\n // 토큰 컨텍스트 기반 \"-san\" → '상'\n if (lastMora?.out === \"사\" && isSanHonorificAt(i)) {\n out = replaceLastHangul(out, JONG.NG);\n i += 1;\n continue;\n }\n\n // --- 기존 ん 동화 규칙 ---\n let jong: number = JONG.N;\n if (!next || !nextInfo || !isKana(next.key[0])) {\n jong = lastMora?.wasYouon ? JONG.NG : JONG.N;\n } else {\n const nc = nextInfo.consClass;\n if (nc === \"k\" || nc === \"g\") {\n jong = JONG.NG;\n } else if (nc === \"vowel\" || nc === \"y\" || nc === \"w\") {\n jong = JONG.N;\n } else if (isLabialStart(nc)) {\n if (lastMora?.vowelOnly) jong = JONG.N;\n else jong = JONG.M;\n } else {\n jong = JONG.N;\n }\n }\n\n out = replaceLastHangul(out, jong);\n i += 1;\n continue;\n }\n\n /**\n * 유성화 규칙\n */\n const info = mora.info;\n if (!info) {\n out += mora.key;\n i += mora.len;\n lastMora = null;\n continue;\n }\n\n // ✅ 단어(토큰) 시작 유성화: と/こ만 + 예외(이/히/ん/테) + (앞이 っ이면 금지)\n let outSyl = info.out;\n if (atTokenStart && (mora.key === \"と\" || mora.key === \"こ\")) {\n const prevIsSokuon = i > 0 && chars[i - 1] === \"っ\";\n\n const nextKey = peekNextMoraKeySkippingChoonpu(i + mora.len);\n const blockedByNext =\n !!nextKey && INITIAL_VOICING_BLOCK_NEXT.has(nextKey);\n\n const isKou = mora.key === \"こ\" && chars[i + mora.len] === \"う\";\n\n // ✅ 추가: \"진짜 조사 と/こ\"로 쓰인 경우만 유성화 차단\n // - 현재 토큰이 1글자 'と'/'こ'이고,\n // - 이전 토큰이 내용어(명사/동사/형용사 등)면 => 조사로 판단 => 유성화 금지\n let blockedByParticleUsage = false;\n if (tokens && tokForI) {\n const isSingleCharToken = tokForI.surface.length === 1;\n const tokenMatchesMora = tokForI.surface === mora.key;\n\n if (isSingleCharToken && tokenMatchesMora) {\n const p = prevToken(); // curToken(i) 호출로 tokIdx는 sync된 상태\n const prevLooksLikeContent =\n !!p &&\n p.pos !== \"記号\" &&\n p.pos !== \"助詞\" &&\n p.pos !== \"助動詞\" &&\n !HARD_BOUNDARY_SURF.has(p.surface);\n\n if (prevLooksLikeContent) blockedByParticleUsage = true;\n }\n }\n\n const allowVoicing =\n !prevIsSokuon && !blockedByNext && !isKou && !blockedByParticleUsage;\n\n if (allowVoicing) {\n if (mora.key === \"と\") outSyl = \"도\";\n else if (mora.key === \"こ\") outSyl = \"고\";\n }\n }\n\n out += outSyl;\n lastMora = { ...info, out: outSyl };\n\n /**\n * 연음 드랍\n */\n const next1 = chars[i + mora.len]; // 다음 언어\n const afterLen = chars[i + mora.len + 1]; // 다다음언어\n\n // o + う 드랍\n if (next1 === \"う\" && info.vowelMain === \"o\") {\n i += mora.len + 1;\n continue;\n }\n\n // ゅう 드랍 (きゅう) // ゆう는 드랍할까말까? (유우리) 일단 ゆう도 드랍함!\n if (next1 === \"う\" && U_DROP_KEYS.has(mora.key)) {\n i += mora.len + 1;\n continue;\n }\n\n // い 드랍\n if (next1 === \"い\") {\n // せんせい -> 센세\n if (mora.key === \"せ\") {\n i += mora.len + 1;\n continue;\n }\n // 케도 장음인데, 케이사츠라고 검색하는 경우가 더 많으려나? 어떻게 할까...\n else if (mora.key === \"け\") {\n i += mora.len + 1;\n continue;\n }\n // えいご -> 에고\n else if (mora.key === \"え\") {\n // 조사 へ 감지\n if (originalMora.key !== \"へ\") {\n i += mora.len + 1;\n continue;\n }\n }\n // 오이시, 야사시 대응\n else if (mora.key === \"し\") {\n i += mora.len + 1;\n continue;\n }\n }\n\n // おねえさん 대응\n if (next1 === \"え\" && mora.key === \"ね\") {\n i += mora.len + 1;\n continue;\n }\n\n i += mora.len;\n }\n\n return out;\n}\n","// kanaToHangul.ts\n// 전체 변환 파이프라인을 orchestration만 담당하도록 정리했습니다.\nimport type { Tokenizer } from \"./tokenizer\";\nimport { normalizeInputText, toHiragana } from \"./normalizer\";\nimport { tokenizeAndRewriteParticles } from \"./particleRewriter\";\nimport { coreKanaToHangulConvert } from \"./coreConverter\";\n\nexport type KanaToHangul = (input: string) => string;\n\nexport function createKanaToHangul(tokenizer: Tokenizer): KanaToHangul {\n return (input: string) => convertWithTokenizer(input, tokenizer);\n}\n\nexport function convertWithTokenizer(\n input: string,\n tokenizer: Tokenizer,\n): string {\n const normalized = normalizeInputText(input);\n\n // ✅ 길이 1:1 보장되는 kana 변환을 먼저 수행(스팬 유지)\n const hiragana = toHiragana(normalized);\n\n // ✅ 핵심: prewrite 이전에 토큰화(=normalized 기준), prewrite는 토큰(pos)을 사용하되 slice는 hiragana 기준\n const { rewritten, spans, rewrittenOriginal } = tokenizeAndRewriteParticles(\n normalized,\n hiragana,\n tokenizer,\n );\n\n // ✅ rewritten + rewritten spans 로 core\n // 한자가 pronunciation으로 변환된 경우 rewrittenOriginal도 같은 길이로 변환됨\n return coreKanaToHangulConvert(rewritten, {\n tokens: spans,\n original: rewrittenOriginal,\n });\n}\n","import kuromoji from \"kuromoji\";\nimport path from \"node:path\";\nimport { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\n\nexport type Tokenizer = kuromoji.Tokenizer<kuromoji.IpadicFeatures>;\n\nasync function buildTokenizer(): Promise<Tokenizer> {\n return new Promise((resolve, reject) => {\n const dicPath = path.join(require.resolve(\"kuromoji\"), \"..\", \"..\", \"dict\");\n\n kuromoji.builder({ dicPath }).build((err, tk) => {\n if (err || !tk) reject(err);\n else resolve(tk);\n });\n });\n}\n\nlet tokenizerPromise: Promise<Tokenizer> | null = null;\n\nexport async function getTokenizer(): Promise<Tokenizer> {\n if (!tokenizerPromise) {\n tokenizerPromise = buildTokenizer();\n }\n return tokenizerPromise;\n}\n","import type { KanaToHangul } from \"./kanaToHangul\";\nimport { createKanaToHangul } from \"./kanaToHangul\";\nimport { getTokenizer } from \"./tokenizer\";\n\n/**\n * Lazy-initialized public API wrapper.\n * 토크나이저를 빌드/캐시하고, 외부에서는 await kanaToHangul(...)만 호출하면 됩니다.\n */\n\nlet cachedConverter: KanaToHangul | null = null;\nlet pendingInit: Promise<KanaToHangul> | null = null;\n\nasync function initKanaToHangul(): Promise<KanaToHangul> {\n if (cachedConverter) return cachedConverter;\n if (pendingInit) return pendingInit;\n\n pendingInit = (async () => {\n const tokenizer = await getTokenizer();\n const converter = createKanaToHangul(tokenizer);\n cachedConverter = converter;\n return converter;\n })();\n\n return pendingInit;\n}\n\nexport async function kanaToHangul(input: string): Promise<string> {\n const converter = await initKanaToHangul();\n return converter(input);\n}\n\nexport const KanaBarum = Object.freeze({\n init: initKanaToHangul,\n});\n\nexport type { KanaToHangul };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/normalizer.ts","../src/dictionary.ts","../src/particleRewriter.ts","../src/mora.ts","../src/coreConverter.ts","../src/kanaToHangul.ts","../src/tokenizer.ts","../src/kanaBarum.ts"],"names":["toHiragana","outCpStart","isKatakanaChar","out","info","require"],"mappings":"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C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAChD;AAEO,IAAM,QAAkC;AAAA,EAC7C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D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","sourcesContent":["export function normalizeInputText(input: string): string {\n // 1) NFD 결합문자(が/ぱ 등) 합성\n let normalized = input.normalize(\"NFC\");\n\n // 2) 반각 가타카나만 전각으로 (구두점/특수문자 최대한 보존)\n normalized = normalizeHalfwidthKatakanaOnly(normalized);\n\n // 3) 장음 기호 변종 최소 치환\n // - U+2015 HORIZONTAL BAR\n // - U+2500 BOX DRAWINGS LIGHT HORIZONTAL\n normalized = normalized.replace(/[\\u2015\\u2500]/g, \"ー\");\n\n // 4) ASCII hyphen이 가타카나 사이에 있을 때 장음 처리\n normalized = normalized\n .replace(/(?<=([\\u30A0-\\u30FF]))-/g, \"ー\")\n .replace(/-(?=[\\u30A0-\\u30FF])/g, \"ー\");\n\n return normalized;\n}\n\nfunction normalizeHalfwidthKatakanaOnly(s: string): string {\n // ✅ 반각 가타카나 + 탁점/반탁점(゙゚) + 반각 장음(ー)까지 함께 NFKC\n return s.replace(/[\\uFF66-\\uFF9F\\uFF70]+/g, (chunk) =>\n chunk.normalize(\"NFKC\"),\n );\n}\n\nexport function toHiragana(input: string): string {\n let out = \"\";\n for (const ch of input) {\n const code = ch.codePointAt(0)!;\n // カタカナ → ひらがな\n if (code >= 0x30a1 && code <= 0x30f6) {\n out += String.fromCodePoint(code - 0x60);\n continue;\n }\n if (ch === \"ー\") {\n out += ch;\n continue;\n }\n out += ch;\n }\n return out;\n}\n","// dictionary.ts\n// 한국인이 익숙한 발음을 담은 특수 사전\nexport interface SpecialDictionaryEntry {\n word: string;\n answer: string;\n hira?: boolean; // true면 히라가나 입력에도 적용\n kata?: boolean; // true면 카타카나 입력에도 적용\n}\n\nexport const SpecialDictionary: SpecialDictionaryEntry[] = [\n // [\"とうきょう\", \"도쿄\"],\n { word: \"こんにちは\", answer: \"곤니치와\", hira: true, kata: true },\n { word: \"こんばんは\", answer: \"곰방와\" },\n { word: \"すみません\", answer: \"스미마셍\" },\n { word: \"はひふへほ\", answer: \"하히후헤호\" },\n { word: \"かわいい\", answer: \"카와이\" },\n { word: \"つなみ\", answer: \"쓰나미\" },\n { word: \"ゆうり\", answer: \"유우리\" },\n { word: \"ミュージック\", answer: \"뮤지쿠\" },\n { word: \"ちゃん\", answer: \"쨩\" },\n];\n","// particleRewriter.ts\nimport type kuromoji from \"kuromoji\";\nimport type { Tokenizer } from \"./tokenizer\";\nimport { SpecialDictionary, SpecialDictionaryEntry } from \"./dictionary\";\n\n// --------------------------\n// local helper: toHiragana (protectedRanges는 hiraganaText 기준)\n// --------------------------\nfunction isKatakanaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x30a0 && c <= 0x30ff;\n}\n\nfunction containsKanji(s: string): boolean {\n for (const ch of s) {\n const c = ch.codePointAt(0)!;\n // CJK Unified Ideographs (U+4E00-U+9FFF) + Extension A (U+3400-U+4DBF)\n if ((c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf)) {\n return true;\n }\n }\n return false;\n}\nfunction toHiragana(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) => {\n // 장음은 드랍\n if (ch === \"ー\") return \"\";\n if (!isKatakanaChar(ch)) return ch;\n const code = ch.codePointAt(0)!;\n // カタカナ 문자 범위 (ァ~ヶ)만 변환\n if (code >= 0x30a1 && code <= 0x30f6) {\n return String.fromCodePoint(code - 0x60);\n }\n return ch;\n })\n .join(\"\");\n}\n\nfunction dictKeysForHiraganaText(dict: SpecialDictionaryEntry[]): string[] {\n // hiraganaText에서 실제로 등장할 수 있는 키만 모으기:\n // - entry.word 자체는 넣어도 되고(못 찾으면 무해)\n // - hira:true면 toHiragana(word)를 추가로 넣는다\n const keys: string[] = [];\n for (const e of dict) {\n keys.push(e.word);\n if (e.hira) keys.push(toHiragana(e.word));\n }\n // 중복 제거\n return [...new Set(keys)].filter(Boolean);\n}\n\ntype Range = { start: number; end: number }; // [start, end)\n\nfunction rangesOverlap(a: Range, b: Range): boolean {\n return a.start < b.end && b.start < a.end;\n}\n\nfunction buildProtectedRanges(text: string, keys: string[]): Range[] {\n const sorted = [...new Set(keys)].sort((a, b) => b.length - a.length);\n const ranges: Range[] = [];\n\n for (const key of sorted) {\n if (!key) continue;\n let from = 0;\n while (true) {\n const idx = text.indexOf(key, from);\n if (idx === -1) break;\n\n const cand: Range = { start: idx, end: idx + key.length };\n if (!ranges.some((r) => rangesOverlap(r, cand))) {\n ranges.push(cand);\n }\n from = idx + 1;\n }\n }\n\n ranges.sort((a, b) => a.start - b.start);\n return ranges;\n}\n\nfunction isProtectedSpan(protectedRanges: Range[], start: number, end: number) {\n for (const r of protectedRanges) {\n if (start < r.end && end > r.start) return true;\n }\n return false;\n}\n\nexport const HARD_BOUNDARY_SURF = new Set([\n \"。\",\n \"、\",\n \"!\",\n \"?\",\n \"!\",\n \"?\",\n \" \",\n \" \",\n]);\nconst HARD_BOUNDARY_DETAIL1 = new Set([\n \"句点\",\n \"読点\",\n \"括弧開\",\n \"括弧閉\",\n \"空白\",\n]);\n\nconst LEXICAL_HE_ENDINGS = [\n \"いにしへ\",\n \"おきへ\",\n \"もとへ\",\n \"すえへ\",\n \"すゑへ\",\n \"かみへ\",\n \"くにへ\",\n \"きしへ\",\n] as const;\n\nfunction isHardBoundaryToken(t: kuromoji.IpadicFeatures): boolean {\n if (t.pos !== \"記号\") return false;\n if (HARD_BOUNDARY_SURF.has(t.surface_form)) return true;\n return HARD_BOUNDARY_DETAIL1.has(t.pos_detail_1 ?? \"\");\n}\nfunction isContentToken(t: kuromoji.IpadicFeatures): boolean {\n if (t.pos === \"記号\") return !isHardBoundaryToken(t);\n return true;\n}\n\nfunction prevContentIdx(tokens: kuromoji.IpadicFeatures[], i: number): number {\n for (let j = i - 1; j >= 0; j -= 1) if (isContentToken(tokens[j])) return j;\n return -1;\n}\nfunction nextContentIdx(tokens: kuromoji.IpadicFeatures[], i: number): number {\n for (let j = i + 1; j < tokens.length; j += 1)\n if (isContentToken(tokens[j])) return j;\n return -1;\n}\nfunction nextBoundaryOrEnd(\n tokens: kuromoji.IpadicFeatures[],\n i: number,\n): boolean {\n for (let j = i + 1; j < tokens.length; j += 1) {\n if (isHardBoundaryToken(tokens[j])) continue;\n return false;\n }\n return true;\n}\n\nexport type TokenSpan = {\n start: number; // rewritten 기준\n end: number; // rewritten 기준\n surface: string;\n\n pos?: string;\n pos1?: string;\n pos2?: string;\n pos3?: string;\n\n // ✅ 원문 기반 힌트: katakana 포함 여부(노ート 같은 케이스 차단용)\n originHadKatakana?: boolean;\n};\n\n/**\n * ✅ 핵심:\n * - 토큰화는 \"prewrite 이전\"에 수행 (원문/정규화 기준)\n * - prewrite는 \"토큰 품사\"를 쓰되, 실제 replace는 hiraganaText slice로 수행\n * - 결과로 rewrittenText + rewrittenTokenSpans를 만들어 core로 넘김\n *\n * 가정: hiraganaText와 originalText는 길이가 동일 (toHiragana는 1:1 치환)\n */\nexport function rewriteParticlesFromTokenization(\n originalText: string,\n hiraganaText: string,\n tokenizerTokens: kuromoji.IpadicFeatures[],\n): { rewritten: string; spans: TokenSpan[]; rewrittenOriginal: string } {\n // 코드포인트 배열로 변환 (kuromoji word_position이 코드포인트 기준)\n const hiraChars = Array.from(hiraganaText);\n const originChars = Array.from(originalText);\n\n // entry 기반\n const protectedRanges = buildProtectedRanges(\n hiraganaText,\n dictKeysForHiraganaText(SpecialDictionary),\n );\n\n let out = \"\";\n let origOut = \"\"; // 한자→pronunciation 변환된 원본\n const spans: TokenSpan[] = [];\n\n let cursorInText = 0;\n\n for (let i = 0; i < tokenizerTokens.length; i += 1) {\n const tok = tokenizerTokens[i];\n const wp = (tok as any).word_position as number | undefined;\n const surf = tok.surface_form;\n const surfCpLen = [...surf].length; // 코드포인트 길이\n\n // kuromoji word_position은 코드포인트 기준 (1-based)\n const start = typeof wp === \"number\" ? wp - 1 : cursorInText;\n const end = start + surfCpLen;\n cursorInText = end;\n\n // 한자가 포함된 경우 pronunciation을 사용\n const hasKanji = containsKanji(surf) && tok.pronunciation;\n const hiraSurf = hasKanji\n ? toHiragana(tok.pronunciation!)\n : hiraChars.slice(start, end).join(\"\");\n // 원본도 한자면 pronunciation에서 장음 제거 (길이 맞추기)\n const origSurfForOut = hasKanji\n ? tok.pronunciation!.replace(/ー/g, \"\")\n : originChars.slice(start, end).join(\"\");\n const originSurf = originChars.slice(start, end).join(\"\");\n\n const originHadKatakana = /[\\u30A0-\\u30FF]/.test(originSurf);\n\n // ✅ 불필요하고 위험한 isProtected(튜플 기반 + includes 난사) 제거\n // protectedRanges 기반으로만 판단\n if (isProtectedSpan(protectedRanges, start, end)) {\n const outCpStart = [...out].length;\n out += hiraSurf;\n origOut += origSurfForOut;\n spans.push({\n start: outCpStart,\n end: [...out].length,\n surface: hiraSurf,\n pos: tok.pos,\n pos1: tok.pos_detail_1 ?? undefined,\n pos2: tok.pos_detail_2 ?? undefined,\n pos3: tok.pos_detail_3 ?? undefined,\n originHadKatakana,\n });\n continue;\n }\n\n let replaced = hiraSurf;\n\n // --- は -> わ (계조사) ---\n if (tok.pos === \"助詞\" && hiraSurf === \"は\") {\n if (i > 0 && tokenizerTokens[i - 1].surface_form === \"は\") {\n // keep\n } else if (\n i + 1 < tokenizerTokens.length &&\n tokenizerTokens[i + 1].surface_form === \"は\"\n ) {\n // keep\n } else {\n const prevIdx = prevContentIdx(tokenizerTokens, i);\n if (prevIdx >= 0) {\n const nextIdx = nextContentIdx(tokenizerTokens, i);\n const hasNextContent = nextIdx >= 0;\n const isEndOrPunct = nextBoundaryOrEnd(tokenizerTokens, i);\n\n const prevTok = tokenizerTokens[prevIdx];\n const prevWpHa = (prevTok as any).word_position as number | undefined;\n const prevStartHa = typeof prevWpHa === \"number\" ? prevWpHa - 1 : 0;\n const prevEndHa = prevStartHa + [...prevTok.surface_form].length;\n const prevHira = hiraChars.slice(prevStartHa, prevEndHa).join(\"\");\n\n if (\n !prevHira.includes(\"っ\") &&\n tok.pos_detail_1 === \"係助詞\" &&\n (hasNextContent || isEndOrPunct) &&\n prevTok.pos !== \"助詞\"\n ) {\n replaced = \"わ\";\n }\n }\n }\n }\n\n // --- へ -> え (격조사) ---\n if (tok.pos === \"助詞\" && hiraSurf === \"へ\") {\n // 바로 왼쪽이 공백이면 keep (코드포인트 배열 사용)\n if (start > 0) {\n const left = hiraChars[start - 1];\n if (\n left === \" \" ||\n left === \" \" ||\n left === \"\\t\" ||\n left === \"\\n\" ||\n left === \"\\r\"\n ) {\n // keep\n } else {\n const prevIdx = prevContentIdx(tokenizerTokens, i);\n if (prevIdx >= 0) {\n const prevTok = tokenizerTokens[prevIdx];\n const prevWp = (prevTok as any).word_position as number | undefined;\n const prevStart = typeof prevWp === \"number\" ? prevWp - 1 : 0;\n const prevEnd = prevStart + [...prevTok.surface_form].length;\n\n const prevHiraSurf = hiraChars.slice(prevStart, prevEnd).join(\"\");\n\n if (\n LEXICAL_HE_ENDINGS.some((w) => (prevHiraSurf + \"へ\").endsWith(w))\n ) {\n // keep lexical endings\n } else if (prevHiraSurf.endsWith(\"の\")) {\n // keep \"...のへ\"\n } else if (tok.pos_detail_1 === \"格助詞\") {\n replaced = \"え\";\n }\n }\n }\n }\n }\n\n const outCpStart = [...out].length;\n out += replaced;\n origOut += origSurfForOut;\n\n spans.push({\n start: outCpStart,\n end: [...out].length,\n surface: replaced,\n pos: tok.pos,\n pos1: tok.pos_detail_1 ?? undefined,\n pos2: tok.pos_detail_2 ?? undefined,\n pos3: tok.pos_detail_3 ?? undefined,\n originHadKatakana,\n });\n }\n\n return { rewritten: out, spans, rewrittenOriginal: origOut };\n}\n\n/**\n * 외부에서 쓰기 편한 래퍼:\n * - originalText를 tokenizer로 먼저 tokenize\n * - hiraganaText는 호출자가 넘겨줌(길이 동일 가정)\n */\nexport function tokenizeAndRewriteParticles(\n originalText: string,\n hiraganaText: string,\n tokenizer: Tokenizer,\n): {\n rewritten: string;\n spans: TokenSpan[];\n rewrittenOriginal: string;\n rawTokens: kuromoji.IpadicFeatures[];\n} {\n const rawTokens = tokenizer.tokenize(originalText);\n // console.log(rawTokens);\n const { rewritten, spans, rewrittenOriginal } =\n rewriteParticlesFromTokenization(originalText, hiraganaText, rawTokens);\n return { rewritten, spans, rewrittenOriginal, rawTokens };\n}\n","// --- Tables (당신 코드 그대로) ---\nexport type VowelMain = \"a\" | \"i\" | \"u\" | \"e\" | \"o\";\nexport type ConsClass =\n | \"vowel\"\n | \"k\"\n | \"s\"\n | \"t\"\n | \"n\"\n | \"h\"\n | \"m\"\n | \"y\"\n | \"r\"\n | \"w\"\n | \"g\"\n | \"z\"\n | \"d\"\n | \"b\"\n | \"p\";\n\nexport type MoraInfo = {\n out: string;\n vowelMain: VowelMain;\n consClass: ConsClass;\n vowelOnly?: boolean;\n wasYouon?: boolean;\n};\n\nexport const SINGLE: Record<string, MoraInfo> = {\n あ: { out: \"아\", vowelMain: \"a\", consClass: \"vowel\", vowelOnly: true },\n い: { out: \"이\", vowelMain: \"i\", consClass: \"vowel\", vowelOnly: true },\n う: { out: \"우\", vowelMain: \"u\", consClass: \"vowel\", vowelOnly: true },\n え: { out: \"에\", vowelMain: \"e\", consClass: \"vowel\", vowelOnly: true },\n お: { out: \"오\", vowelMain: \"o\", consClass: \"vowel\", vowelOnly: true },\n\n か: { out: \"카\", vowelMain: \"a\", consClass: \"k\" },\n き: { out: \"키\", vowelMain: \"i\", consClass: \"k\" },\n く: { out: \"쿠\", vowelMain: \"u\", consClass: \"k\" },\n け: { out: \"케\", vowelMain: \"e\", consClass: \"k\" },\n こ: { out: \"코\", vowelMain: \"o\", consClass: \"k\" },\n\n さ: { out: \"사\", vowelMain: \"a\", consClass: \"s\" },\n し: { out: \"시\", vowelMain: \"i\", consClass: \"s\" },\n す: { out: \"스\", vowelMain: \"u\", consClass: \"s\" },\n せ: { out: \"세\", vowelMain: \"e\", consClass: \"s\" },\n そ: { out: \"소\", vowelMain: \"o\", consClass: \"s\" },\n\n た: { out: \"타\", vowelMain: \"a\", consClass: \"t\" },\n ち: { out: \"치\", vowelMain: \"i\", consClass: \"t\" },\n つ: { out: \"츠\", vowelMain: \"u\", consClass: \"t\" },\n て: { out: \"테\", vowelMain: \"e\", consClass: \"t\" },\n と: { out: \"토\", vowelMain: \"o\", consClass: \"t\" },\n\n な: { out: \"나\", vowelMain: \"a\", consClass: \"n\" },\n に: { out: \"니\", vowelMain: \"i\", consClass: \"n\" },\n ぬ: { out: \"누\", vowelMain: \"u\", consClass: \"n\" },\n ね: { out: \"네\", vowelMain: \"e\", consClass: \"n\" },\n の: { out: \"노\", vowelMain: \"o\", consClass: \"n\" },\n\n は: { out: \"하\", vowelMain: \"a\", consClass: \"h\" },\n ひ: { out: \"히\", vowelMain: \"i\", consClass: \"h\" },\n ふ: { out: \"후\", vowelMain: \"u\", consClass: \"h\" },\n へ: { out: \"헤\", vowelMain: \"e\", consClass: \"h\" },\n ほ: { out: \"호\", vowelMain: \"o\", consClass: \"h\" },\n\n ま: { out: \"마\", vowelMain: \"a\", consClass: \"m\" },\n み: { out: \"미\", vowelMain: \"i\", consClass: \"m\" },\n む: { out: \"무\", vowelMain: \"u\", consClass: \"m\" },\n め: { out: \"메\", vowelMain: \"e\", consClass: \"m\" },\n も: { out: \"모\", vowelMain: \"o\", consClass: \"m\" },\n\n や: { out: \"야\", vowelMain: \"a\", consClass: \"y\" },\n ゆ: { out: \"유\", vowelMain: \"u\", consClass: \"y\" },\n よ: { out: \"요\", vowelMain: \"o\", consClass: \"y\" },\n\n ら: { out: \"라\", vowelMain: \"a\", consClass: \"r\" },\n り: { out: \"리\", vowelMain: \"i\", consClass: \"r\" },\n る: { out: \"루\", vowelMain: \"u\", consClass: \"r\" },\n れ: { out: \"레\", vowelMain: \"e\", consClass: \"r\" },\n ろ: { out: \"로\", vowelMain: \"o\", consClass: \"r\" },\n\n わ: { out: \"와\", vowelMain: \"a\", consClass: \"w\" },\n を: { out: \"오\", vowelMain: \"o\", consClass: \"w\" },\n\n が: { out: \"가\", vowelMain: \"a\", consClass: \"g\" },\n ぎ: { out: \"기\", vowelMain: \"i\", consClass: \"g\" },\n ぐ: { out: \"구\", vowelMain: \"u\", consClass: \"g\" },\n げ: { out: \"게\", vowelMain: \"e\", consClass: \"g\" },\n ご: { out: \"고\", vowelMain: \"o\", consClass: \"g\" },\n\n ざ: { out: \"자\", vowelMain: \"a\", consClass: \"z\" },\n じ: { out: \"지\", vowelMain: \"i\", consClass: \"z\" },\n ず: { out: \"즈\", vowelMain: \"u\", consClass: \"z\" },\n ぜ: { out: \"제\", vowelMain: \"e\", consClass: \"z\" },\n ぞ: { out: \"조\", vowelMain: \"o\", consClass: \"z\" },\n\n だ: { out: \"다\", vowelMain: \"a\", consClass: \"d\" },\n ぢ: { out: \"지\", vowelMain: \"i\", consClass: \"d\" },\n づ: { out: \"즈\", vowelMain: \"u\", consClass: \"d\" },\n で: { out: \"데\", vowelMain: \"e\", consClass: \"d\" },\n ど: { out: \"도\", vowelMain: \"o\", consClass: \"d\" },\n\n ば: { out: \"바\", vowelMain: \"a\", consClass: \"b\" },\n び: { out: \"비\", vowelMain: \"i\", consClass: \"b\" },\n ぶ: { out: \"부\", vowelMain: \"u\", consClass: \"b\" },\n べ: { out: \"베\", vowelMain: \"e\", consClass: \"b\" },\n ぼ: { out: \"보\", vowelMain: \"o\", consClass: \"b\" },\n\n ぱ: { out: \"파\", vowelMain: \"a\", consClass: \"p\" },\n ぴ: { out: \"피\", vowelMain: \"i\", consClass: \"p\" },\n ぷ: { out: \"푸\", vowelMain: \"u\", consClass: \"p\" },\n ぺ: { out: \"페\", vowelMain: \"e\", consClass: \"p\" },\n ぽ: { out: \"포\", vowelMain: \"o\", consClass: \"p\" },\n\n ゔ: { out: \"부\", vowelMain: \"u\", consClass: \"b\" },\n};\n\nexport const YOUON: Record<string, MoraInfo> = {\n きゃ: { out: \"캬\", vowelMain: \"a\", consClass: \"k\", wasYouon: true },\n きゅ: { out: \"큐\", vowelMain: \"u\", consClass: \"k\", wasYouon: true },\n きょ: { out: \"쿄\", vowelMain: \"o\", consClass: \"k\", wasYouon: true },\n\n しゃ: { out: \"샤\", vowelMain: \"a\", consClass: \"s\", wasYouon: true },\n しゅ: { out: \"슈\", vowelMain: \"u\", consClass: \"s\", wasYouon: true },\n しょ: { out: \"쇼\", vowelMain: \"o\", consClass: \"s\", wasYouon: true },\n\n ちゃ: { out: \"챠\", vowelMain: \"a\", consClass: \"t\", wasYouon: true },\n ちゅ: { out: \"츄\", vowelMain: \"u\", consClass: \"t\", wasYouon: true },\n ちょ: { out: \"쵸\", vowelMain: \"o\", consClass: \"t\", wasYouon: true },\n てゅ: { out: \"튜\", vowelMain: \"u\", consClass: \"t\", wasYouon: true },\n でゅ: { out: \"듀\", vowelMain: \"u\", consClass: \"d\", wasYouon: true },\n\n にゃ: { out: \"냐\", vowelMain: \"a\", consClass: \"n\", wasYouon: true },\n にゅ: { out: \"뉴\", vowelMain: \"u\", consClass: \"n\", wasYouon: true },\n にょ: { out: \"뇨\", vowelMain: \"o\", consClass: \"n\", wasYouon: true },\n\n ひゃ: { out: \"햐\", vowelMain: \"a\", consClass: \"h\", wasYouon: true },\n ひゅ: { out: \"휴\", vowelMain: \"u\", consClass: \"h\", wasYouon: true },\n ひょ: { out: \"효\", vowelMain: \"o\", consClass: \"h\", wasYouon: true },\n ふゃ: { out: \"퍄\", vowelMain: \"a\", consClass: \"p\", wasYouon: true },\n ふゅ: { out: \"퓨\", vowelMain: \"u\", consClass: \"p\", wasYouon: true },\n ふょ: { out: \"표\", vowelMain: \"o\", consClass: \"p\", wasYouon: true },\n\n みゃ: { out: \"먀\", vowelMain: \"a\", consClass: \"m\", wasYouon: true },\n みゅ: { out: \"뮤\", vowelMain: \"u\", consClass: \"m\", wasYouon: true },\n みょ: { out: \"묘\", vowelMain: \"o\", consClass: \"m\", wasYouon: true },\n\n りゃ: { out: \"랴\", vowelMain: \"a\", consClass: \"r\", wasYouon: true },\n りゅ: { out: \"류\", vowelMain: \"u\", consClass: \"r\", wasYouon: true },\n りょ: { out: \"료\", vowelMain: \"o\", consClass: \"r\", wasYouon: true },\n\n ぎゃ: { out: \"갸\", vowelMain: \"a\", consClass: \"g\", wasYouon: true },\n ぎゅ: { out: \"규\", vowelMain: \"u\", consClass: \"g\", wasYouon: true },\n ぎょ: { out: \"교\", vowelMain: \"o\", consClass: \"g\", wasYouon: true },\n\n じゃ: { out: \"쟈\", vowelMain: \"a\", consClass: \"z\", wasYouon: true },\n じゅ: { out: \"쥬\", vowelMain: \"u\", consClass: \"z\", wasYouon: true },\n じょ: { out: \"죠\", vowelMain: \"o\", consClass: \"z\", wasYouon: true },\n\n びゃ: { out: \"뱌\", vowelMain: \"a\", consClass: \"b\", wasYouon: true },\n びゅ: { out: \"뷰\", vowelMain: \"u\", consClass: \"b\", wasYouon: true },\n びょ: { out: \"뵤\", vowelMain: \"o\", consClass: \"b\", wasYouon: true },\n\n ぴゃ: { out: \"퍄\", vowelMain: \"a\", consClass: \"p\", wasYouon: true },\n ぴゅ: { out: \"퓨\", vowelMain: \"u\", consClass: \"p\", wasYouon: true },\n ぴょ: { out: \"표\", vowelMain: \"o\", consClass: \"p\", wasYouon: true },\n};\n\nexport const LOAN: Record<string, MoraInfo> = {\n てぃ: { out: \"티\", vowelMain: \"i\", consClass: \"t\" },\n でぃ: { out: \"디\", vowelMain: \"i\", consClass: \"d\" },\n ちぇ: { out: \"체\", vowelMain: \"e\", consClass: \"t\" },\n しぇ: { out: \"셰\", vowelMain: \"e\", consClass: \"s\" },\n じぇ: { out: \"제\", vowelMain: \"e\", consClass: \"z\" },\n つぁ: { out: \"차\", vowelMain: \"a\", consClass: \"t\" },\n つぃ: { out: \"치\", vowelMain: \"i\", consClass: \"t\" },\n つぇ: { out: \"체\", vowelMain: \"e\", consClass: \"t\" },\n つぉ: { out: \"초\", vowelMain: \"o\", consClass: \"t\" },\n ふぁ: { out: \"파\", vowelMain: \"a\", consClass: \"p\" },\n ふぃ: { out: \"피\", vowelMain: \"i\", consClass: \"p\" },\n ふぇ: { out: \"페\", vowelMain: \"e\", consClass: \"p\" },\n ふぉ: { out: \"포\", vowelMain: \"o\", consClass: \"p\" },\n ぐぁ: { out: \"과\", vowelMain: \"a\", consClass: \"g\" },\n ぐぃ: { out: \"귀\", vowelMain: \"i\", consClass: \"g\" },\n ぐぇ: { out: \"궤\", vowelMain: \"e\", consClass: \"g\" },\n ぐぉ: { out: \"궈\", vowelMain: \"o\", consClass: \"g\" },\n くぁ: { out: \"콰\", vowelMain: \"a\", consClass: \"k\" },\n くぃ: { out: \"퀴\", vowelMain: \"i\", consClass: \"k\" },\n くぇ: { out: \"퀘\", vowelMain: \"e\", consClass: \"k\" },\n くぉ: { out: \"쿼\", vowelMain: \"o\", consClass: \"k\" },\n どぁ: { out: \"돠\", vowelMain: \"a\", consClass: \"d\" },\n どぅ: { out: \"두\", vowelMain: \"u\", consClass: \"d\" },\n どぉ: { out: \"둬\", vowelMain: \"o\", consClass: \"d\" },\n ゔぁ: { out: \"바\", vowelMain: \"a\", consClass: \"b\" },\n ゔぃ: { out: \"비\", vowelMain: \"i\", consClass: \"b\" },\n ゔぇ: { out: \"베\", vowelMain: \"e\", consClass: \"b\" },\n ゔぉ: { out: \"보\", vowelMain: \"o\", consClass: \"b\" },\n};\n\nexport const SMALL_Y = new Set([\"ゃ\", \"ゅ\", \"ょ\"]);\nexport const SMALL_V = new Set([\"ぁ\", \"ぃ\", \"ぅ\", \"ぇ\", \"ぉ\"]);\n\nexport const U_DROP_KEYS = new Set([\n \"ゆ\",\n \"きゅ\",\n \"しゅ\",\n \"ちゅ\",\n \"にゅ\",\n \"ひゅ\",\n \"みゅ\",\n \"りゅ\",\n \"ぎゅ\",\n \"じゅ\",\n \"びゅ\",\n \"ぴゅ\",\n]);\n","// coreConverter.ts\nimport { SpecialDictionary, type SpecialDictionaryEntry } from \"./dictionary\";\nimport { HARD_BOUNDARY_SURF, type TokenSpan } from \"./particleRewriter\";\nimport {\n type ConsClass,\n type MoraInfo,\n SINGLE,\n YOUON,\n LOAN,\n SMALL_Y,\n SMALL_V,\n U_DROP_KEYS,\n} from \"./mora\";\n\n// --------------------------\n// Kana normalize helpers\n// --------------------------\nfunction isHiraganaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n}\nfunction isKatakanaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x30a0 && c <= 0x30ff;\n}\nfunction toHiraganaKey(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) => {\n // 장음은 그대로 유지\n if (ch === \"ー\") return ch;\n if (!isKatakanaChar(ch)) return ch;\n const code = ch.codePointAt(0)!;\n // カタカナ 문자 범위 (ァ~ヶ)만 변환\n if (code >= 0x30a1 && code <= 0x30f6) {\n return String.fromCodePoint(code - 0x60);\n }\n return ch;\n })\n .join(\"\");\n}\nfunction toKatakanaKey(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) =>\n isHiraganaChar(ch) ? String.fromCodePoint(ch.codePointAt(0)! + 0x60) : ch,\n )\n .join(\"\");\n}\n\ntype DictStream = \"orig\" | \"rewritten\";\ntype CompiledDictItem = {\n keyChars: string[];\n answer: string;\n stream: DictStream;\n};\n\nfunction compileSpecialDictionary(\n dict: SpecialDictionaryEntry[],\n): CompiledDictItem[] {\n const items: CompiledDictItem[] = [];\n\n for (const e of dict) {\n // 기본: exact word는 원본에서만\n items.push({\n keyChars: Array.from(e.word),\n answer: e.answer,\n stream: \"orig\",\n });\n\n // hira:true => hiragana 스트림에서만 (입력 전체가 히라로 바뀌는 파이프라인이기 때문)\n if (e.hira) {\n const k = toHiraganaKey(e.word);\n items.push({\n keyChars: Array.from(k),\n answer: e.answer,\n stream: \"rewritten\",\n });\n }\n\n // kata:true => 원본에서만\n if (e.kata) {\n const k = toKatakanaKey(e.word);\n items.push({ keyChars: Array.from(k), answer: e.answer, stream: \"orig\" });\n }\n }\n\n // 긴 키 우선\n items.sort((a, b) => b.keyChars.length - a.keyChars.length);\n return items;\n}\n\nconst COMPILED_SPECIAL_DICT = compileSpecialDictionary(SpecialDictionary);\n\nfunction isHiragana(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n}\n\nfunction isKana(ch: string): boolean {\n return isHiragana(ch) || ch === \"ー\";\n}\n\nexport function coreKanaToHangulConvert(\n s: string,\n opts: { tokens: TokenSpan[]; original: string },\n): string {\n // 코드포인트 배열로 변환 (surrogate pair 문제 해결)\n const chars = Array.from(s);\n const origChars = Array.from(opts?.original ?? s);\n\n // --- Hangul utilities ---\n const HANGUL_BASE = 0xac00;\n const HANGUL_END = 0xd7a3;\n\n function isHangulSyllable(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= HANGUL_BASE && c <= HANGUL_END;\n }\n\n const JONG = {\n NONE: 0,\n G: 1, // ㄱ\n N: 4, // ㄴ\n M: 16, // ㅁ\n B: 17, // ㅂ\n S: 19, // ㅅ\n NG: 21, // ㅇ\n } as const;\n\n function addFinal(syl: string, jong: number): string {\n if (!isHangulSyllable(syl)) return syl;\n const code = syl.codePointAt(0)! - HANGUL_BASE;\n const cho = Math.floor(code / 588);\n const jung = Math.floor((code % 588) / 28);\n return String.fromCodePoint(HANGUL_BASE + cho * 588 + jung * 28 + jong);\n }\n\n function replaceLastHangul(out: string, jong: number): string {\n if (!out) return out;\n const last = out[out.length - 1];\n if (!isHangulSyllable(last)) return out;\n return out.slice(0, -1) + addFinal(last, jong);\n }\n\n // --- Kana classification ---\n function isHiragana(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n }\n function isKana(ch: string): boolean {\n return isHiragana(ch) || ch === \"ー\";\n }\n\n type ReadMora = { key: string; len: number; info?: MoraInfo } | null;\n\n function readMoraAt(idx: number): ReadMora {\n if (idx >= chars.length) return null;\n\n const c0 = chars[idx];\n const c1 = chars[idx + 1];\n\n if (c1 && SMALL_V.has(c1)) {\n const key2 = c0 + c1;\n const info = LOAN[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n if (c1 && SMALL_Y.has(c1)) {\n const key2 = c0 + c1;\n const info = YOUON[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n const info = SINGLE[c0];\n if (info) return { key: c0, len: 1, info };\n\n return { key: c0, len: 1, info: undefined };\n }\n\n function readOriginalMoraAt(idx: number): ReadMora {\n if (idx >= origChars.length) return null;\n\n const c0 = origChars[idx];\n const c1 = origChars[idx + 1];\n\n if (c1 && SMALL_V.has(c1)) {\n const key2 = c0 + c1;\n const info = LOAN[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n if (c1 && SMALL_Y.has(c1)) {\n const key2 = c0 + c1;\n const info = YOUON[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n const info = SINGLE[c0];\n if (info) return { key: c0, len: 1, info };\n\n return { key: c0, len: 1, info: undefined };\n }\n\n function isLabialStart(cons: ConsClass): boolean {\n return cons === \"m\" || cons === \"b\" || cons === \"p\";\n }\n\n // 토큰 컨텍스트 탐색용\n const tokens = opts?.tokens ?? null;\n let tokIdx = 0;\n\n function syncTokenIndex(charIndex: number) {\n if (!tokens) return;\n while (tokIdx + 1 < tokens.length && tokens[tokIdx].end <= charIndex) {\n tokIdx++;\n }\n }\n\n function curToken(charIndex: number): TokenSpan | null {\n if (!tokens) return null;\n syncTokenIndex(charIndex);\n const t = tokens[tokIdx];\n if (t && t.start <= charIndex && charIndex < t.end) return t;\n return null;\n }\n\n function prevToken(): TokenSpan | null {\n if (!tokens) return null;\n return tokIdx - 1 >= 0 ? tokens[tokIdx - 1] : null;\n }\n function nextToken(): TokenSpan | null {\n if (!tokens) return null;\n return tokIdx + 1 < tokens.length ? tokens[tokIdx + 1] : null;\n }\n\n // ✅ 유성화 차단 next\n const INITIAL_VOICING_BLOCK_NEXT = new Set([\"い\", \"ひ\", \"ん\", \"て\"]);\n\n function peekNextMoraKeySkippingChoonpu(fromIdx: number): string | null {\n let j = fromIdx;\n while (j < chars.length && chars[j] === \"ー\") j++;\n const m = readMoraAt(j);\n return m?.key ?? null;\n }\n\n // ✅ \"-san\" 판별 (코드포인트 인덱스 기준)\n const SAN_PARTICLES = new Set([\"は\", \"わ\", \"へ\", \"え\", \"を\", \"お\"]);\n function isSanHonorificAt(cpIdx: number): boolean {\n const t = curToken(cpIdx);\n if (!t) return false;\n if (cpIdx < 1 || chars[cpIdx - 1] !== \"さ\") return false;\n\n // t.start는 코드포인트 인덱스, chars.slice 사용\n const local = chars.slice(t.start, cpIdx + 1).join(\"\");\n if (!local.endsWith(\"さん\")) return false;\n\n const hasPrefixInsideToken = cpIdx - 1 > t.start;\n const p = prevToken();\n const prevIsAttachable =\n !!p &&\n p.end === t.start &&\n p.surface.length > 0 &&\n !HARD_BOUNDARY_SURF.has(p.surface);\n\n if (!hasPrefixInsideToken && !prevIsAttachable) return false;\n\n const n = nextToken();\n if (!n) return true;\n if (n.pos === \"記号\" && HARD_BOUNDARY_SURF.has(n.surface)) return true;\n if (n.pos === \"助詞\" && SAN_PARTICLES.has(n.surface)) return true;\n return false;\n }\n\n let out = \"\";\n let i = 0;\n\n let lastMora: MoraInfo | null = null;\n\n while (i < chars.length) {\n // 토큰 기반 \"단어 시작\" 정의: i가 content 토큰 start면 true\n let atTokenStart = false;\n let tokForI: TokenSpan | null = null;\n\n if (tokens) {\n tokForI = curToken(i);\n // ✅ 토큰 시작이면 일단 단어 시작 후보로 인정\n atTokenStart = !!tokForI && tokForI.start === i;\n\n // ✅ 유성화/단어시작 판정에서 \"기호\"와 \"원문 카타카나 토큰\"만 컷\n if (tokForI?.pos === \"記号\") atTokenStart = false;\n } else {\n atTokenStart = i === 0;\n }\n\n // --------------------------\n // ✅ SpecialDictionary (entry 기반 + hira/kata 옵션)\n // --------------------------\n let matchedSpecial = false;\n\n // 긴 키부터 순회하므로, 앞에서 걸리면 끝\n for (const it of COMPILED_SPECIAL_DICT) {\n const src = it.stream === \"orig\" ? origChars : chars; // chars=rewritten\n const len = it.keyChars.length;\n if (i + len > src.length) continue;\n\n let ok = true;\n for (let k = 0; k < len; k++) {\n if (src[i + k] !== it.keyChars[k]) {\n ok = false;\n break;\n }\n }\n if (!ok) continue;\n\n out += it.answer;\n i += len;\n\n // 사전 치환은 단어 단위 => 상태 초기화\n lastMora = null;\n\n matchedSpecial = true;\n break;\n }\n if (matchedSpecial) continue;\n\n const ch = chars[i];\n\n /**\n * ー 표시는 그냥 드랍\n */\n if (ch === \"ー\") {\n i += 1;\n continue;\n }\n\n /**\n * 비가나: 그대로\n */\n if (!isKana(ch)) {\n out += ch;\n i += 1;\n lastMora = null;\n continue;\n }\n\n /**\n * 촉음 규칙\n */\n if (ch === \"っ\") {\n if (!out || !isHangulSyllable(out[out.length - 1])) {\n // \"ッ\"으로 바꾸기\n out += \"ッ\";\n i += 1;\n lastMora = null;\n continue;\n }\n\n const next = readMoraAt(i + 1);\n\n const prevV = lastMora?.vowelMain ?? \"a\";\n const nextInfo = next?.info;\n const nextCons: ConsClass = nextInfo?.consClass ?? \"t\";\n\n let jong: number = JONG.S;\n if (nextCons === \"p\" || nextCons === \"b\") jong = JONG.B;\n else if (nextCons === \"k\" || nextCons === \"g\") {\n jong = prevV === \"e\" || prevV === \"i\" ? JONG.S : JONG.G;\n } else {\n jong = JONG.S;\n }\n\n out = replaceLastHangul(out, jong);\n i += 1;\n continue;\n }\n\n /**\n * おお 장음 규칙\n */\n if (ch === \"お\" && chars[i + 1] === \"お\") {\n let j = i;\n while (chars[j] === \"お\") j++;\n out += \"오\";\n i = j;\n lastMora = {\n out: \"오\",\n vowelMain: \"o\",\n consClass: \"vowel\",\n vowelOnly: true,\n };\n continue;\n }\n\n const mora = readMoraAt(i);\n const originalMora = readOriginalMoraAt(i);\n if (!mora) {\n out += chars[i];\n i += 1;\n lastMora = null;\n continue;\n }\n // 에러\n if (!originalMora) {\n throw Error(\"원본 모라 손실\");\n }\n\n /**\n * ん 규칙\n */\n if (mora.key === \"ん\") {\n const next = readMoraAt(i + 1);\n const nextInfo = next?.info;\n\n const hasPrevHangul =\n out.length > 0 && isHangulSyllable(out[out.length - 1]);\n if (!hasPrevHangul) {\n out += \"ㄴ\";\n i += 1;\n lastMora = null;\n continue;\n }\n\n // 토큰 컨텍스트 기반 \"-san\" → '상'\n if (lastMora?.out === \"사\" && isSanHonorificAt(i)) {\n out = replaceLastHangul(out, JONG.NG);\n i += 1;\n continue;\n }\n\n // --- 기존 ん 동화 규칙 ---\n let jong: number = JONG.N;\n if (!next || !nextInfo || !isKana(next.key[0])) {\n jong = lastMora?.wasYouon ? JONG.NG : JONG.N;\n } else {\n const nc = nextInfo.consClass;\n if (nc === \"k\" || nc === \"g\") {\n jong = JONG.NG;\n } else if (nc === \"vowel\" || nc === \"y\" || nc === \"w\") {\n jong = JONG.N;\n } else if (isLabialStart(nc)) {\n if (lastMora?.vowelOnly) jong = JONG.N;\n else jong = JONG.M;\n } else {\n jong = JONG.N;\n }\n }\n\n out = replaceLastHangul(out, jong);\n i += 1;\n continue;\n }\n\n /**\n * 유성화 규칙\n */\n const info = mora.info;\n if (!info) {\n out += mora.key;\n i += mora.len;\n lastMora = null;\n continue;\n }\n\n // ✅ 단어(토큰) 시작 유성화: と/こ만 + 예외(이/히/ん/테) + (앞이 っ이면 금지)\n let outSyl = info.out;\n if (atTokenStart && (mora.key === \"と\" || mora.key === \"こ\")) {\n const prevIsSokuon = i > 0 && chars[i - 1] === \"っ\";\n\n const nextKey = peekNextMoraKeySkippingChoonpu(i + mora.len);\n const blockedByNext =\n !!nextKey && INITIAL_VOICING_BLOCK_NEXT.has(nextKey);\n\n const isKou = mora.key === \"こ\" && chars[i + mora.len] === \"う\";\n\n // ✅ 추가: \"진짜 조사 と/こ\"로 쓰인 경우만 유성화 차단\n // - 현재 토큰이 1글자 'と'/'こ'이고,\n // - 이전 토큰이 내용어(명사/동사/형용사 등)면 => 조사로 판단 => 유성화 금지\n let blockedByParticleUsage = false;\n if (tokens && tokForI) {\n const isSingleCharToken = tokForI.surface.length === 1;\n const tokenMatchesMora = tokForI.surface === mora.key;\n\n if (isSingleCharToken && tokenMatchesMora) {\n const p = prevToken(); // curToken(i) 호출로 tokIdx는 sync된 상태\n const prevLooksLikeContent =\n !!p &&\n p.pos !== \"記号\" &&\n p.pos !== \"助詞\" &&\n p.pos !== \"助動詞\" &&\n !HARD_BOUNDARY_SURF.has(p.surface);\n\n if (prevLooksLikeContent) blockedByParticleUsage = true;\n }\n }\n\n const allowVoicing =\n !prevIsSokuon && !blockedByNext && !isKou && !blockedByParticleUsage;\n\n if (allowVoicing) {\n if (mora.key === \"と\") outSyl = \"도\";\n else if (mora.key === \"こ\") outSyl = \"고\";\n }\n }\n\n out += outSyl;\n lastMora = { ...info, out: outSyl };\n\n /**\n * 연음 드랍\n */\n const next1 = chars[i + mora.len]; // 다음 언어\n const afterLen = chars[i + mora.len + 1]; // 다다음언어\n\n // o + う 드랍\n if (next1 === \"う\" && info.vowelMain === \"o\") {\n i += mora.len + 1;\n continue;\n }\n\n // ゅう 드랍 (きゅう) // ゆう는 드랍할까말까? (유우리) 일단 ゆう도 드랍함!\n if (next1 === \"う\" && U_DROP_KEYS.has(mora.key)) {\n i += mora.len + 1;\n continue;\n }\n\n // い 드랍\n if (next1 === \"い\") {\n // せんせい -> 센세\n if (mora.key === \"せ\") {\n i += mora.len + 1;\n continue;\n }\n // 케도 장음인데, 케이사츠라고 검색하는 경우가 더 많으려나? 어떻게 할까...\n else if (mora.key === \"け\") {\n i += mora.len + 1;\n continue;\n }\n // えいご -> 에고\n else if (mora.key === \"え\") {\n // 조사 へ 감지\n if (originalMora.key !== \"へ\") {\n i += mora.len + 1;\n continue;\n }\n }\n // 오이시, 야사시 대응\n else if (mora.key === \"し\") {\n i += mora.len + 1;\n continue;\n }\n }\n\n // おねえさん 대응\n if (next1 === \"え\" && mora.key === \"ね\") {\n i += mora.len + 1;\n continue;\n }\n\n i += mora.len;\n }\n\n return out;\n}\n","// kanaToHangul.ts\n// 전체 변환 파이프라인을 orchestration만 담당하도록 정리했습니다.\nimport type { Tokenizer } from \"./tokenizer\";\nimport { normalizeInputText, toHiragana } from \"./normalizer\";\nimport { tokenizeAndRewriteParticles } from \"./particleRewriter\";\nimport { coreKanaToHangulConvert } from \"./coreConverter\";\n\nexport type KanaToHangul = (input: string) => string;\n\nexport function createKanaToHangul(tokenizer: Tokenizer): KanaToHangul {\n return (input: string) => convertWithTokenizer(input, tokenizer);\n}\n\nexport function convertWithTokenizer(\n input: string,\n tokenizer: Tokenizer,\n): string {\n const normalized = normalizeInputText(input);\n\n // ✅ 길이 1:1 보장되는 kana 변환을 먼저 수행(스팬 유지)\n const hiragana = toHiragana(normalized);\n\n // ✅ 핵심: prewrite 이전에 토큰화(=normalized 기준), prewrite는 토큰(pos)을 사용하되 slice는 hiragana 기준\n const { rewritten, spans, rewrittenOriginal } = tokenizeAndRewriteParticles(\n normalized,\n hiragana,\n tokenizer,\n );\n\n // ✅ rewritten + rewritten spans 로 core\n // 한자가 pronunciation으로 변환된 경우 rewrittenOriginal도 같은 길이로 변환됨\n return coreKanaToHangulConvert(rewritten, {\n tokens: spans,\n original: rewrittenOriginal,\n });\n}\n","import kuromoji from \"kuromoji\";\nimport path from \"node:path\";\nimport { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\n\nexport type Tokenizer = kuromoji.Tokenizer<kuromoji.IpadicFeatures>;\n\nasync function buildTokenizer(): Promise<Tokenizer> {\n return new Promise((resolve, reject) => {\n const dicPath = path.join(require.resolve(\"kuromoji\"), \"..\", \"..\", \"dict\");\n\n kuromoji.builder({ dicPath }).build((err, tk) => {\n if (err || !tk) reject(err);\n else resolve(tk);\n });\n });\n}\n\nlet tokenizerPromise: Promise<Tokenizer> | null = null;\n\nexport async function getTokenizer(): Promise<Tokenizer> {\n if (!tokenizerPromise) {\n tokenizerPromise = buildTokenizer();\n }\n return tokenizerPromise;\n}\n","import type { KanaToHangul } from \"./kanaToHangul\";\nimport { createKanaToHangul } from \"./kanaToHangul\";\nimport { getTokenizer } from \"./tokenizer\";\n\n/**\n * Kanabarum - 일본어 가나를 한글로 변환하는 클래스\n *\n * @example\n * const kanabarum = new Kanabarum();\n * await kanabarum.init();\n * kanabarum.kanaToHangul(\"こんにちは\"); // \"곤니치와\"\n */\nexport class Kanabarum {\n private converter: KanaToHangul | null = null;\n\n /**\n * 토크나이저를 초기화합니다. 변환 전에 반드시 호출해야 합니다.\n */\n async init(): Promise<void> {\n const tokenizer = await getTokenizer();\n this.converter = createKanaToHangul(tokenizer);\n }\n\n /**\n * 일본어 가나를 한글로 변환합니다.\n * @param input 변환할 일본어 문자열\n * @returns 한글로 변환된 문자열\n * @throws init()이 호출되지 않은 경우 에러\n */\n kanaToHangul(input: string): string {\n if (!this.converter) {\n throw new Error(\"Kanabarum is not initialized. Call init() first.\");\n }\n return this.converter(input);\n }\n}\n\n// ============================================\n// Async helper (기존 API 호환)\n// ============================================\n\nlet cachedConverter: KanaToHangul | null = null;\nlet pendingInit: Promise<KanaToHangul> | null = null;\n\nasync function initKanaToHangul(): Promise<KanaToHangul> {\n if (cachedConverter) return cachedConverter;\n if (pendingInit) return pendingInit;\n\n pendingInit = (async () => {\n const tokenizer = await getTokenizer();\n const converter = createKanaToHangul(tokenizer);\n cachedConverter = converter;\n return converter;\n })();\n\n return pendingInit;\n}\n\n/**\n * 일본어 가나를 한글로 변환합니다. (async helper)\n * init 없이 바로 호출 가능합니다.\n *\n * @example\n * await kanaToHangul(\"こんにちは\"); // \"곤니치와\"\n */\nexport async function kanaToHangul(input: string): Promise<string> {\n const converter = await initKanaToHangul();\n return converter(input);\n}\n\nexport type { KanaToHangul };\n"]}
|