kanabarum 0.1.1 → 0.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
- # Kanabarum
1
+ # Kanabarum - 가나발음
2
2
 
3
- 일본어 가나 문자열을 한국어 발음 표기로 바꿔주는 TypeScript 라이브러리입니다.
3
+ 일본어 히라가나/가타카나 문자열을 한국어 발음 표기로 바꿔주는 TypeScript 라이브러리입니다.
4
4
  `kuromoji` 품사 분석을 이용해 は/へ 같은 조사를 안전하게 치환합니다.
5
5
 
6
6
  ## 설치
@@ -33,8 +33,8 @@ const converter = await KanaBarum.init();
33
33
 
34
34
  // 인사말
35
35
  converter("おはよう"); // => "오하요"
36
- converter("こんにちは"); // => "콘니치와"
37
- converter("こんばんは"); // => "콤방와"
36
+ converter("こんにちは"); // => "곤니치와"
37
+ converter("こんばんは"); // => "곰방와"
38
38
  converter("ありがとう"); // => "아리가토"
39
39
  converter("すみません"); // => "스미마셍"
40
40
 
@@ -45,6 +45,10 @@ converter("ちょっと"); // => "춋토"
45
45
  converter("きゅう"); // => "큐"
46
46
  converter("りょこう"); // => "료코"
47
47
 
48
+ // つ 발음
49
+ converter("つき"); // => "츠키"
50
+ converter("つなみ"); // => "쓰나미"
51
+
48
52
  // 촉음
49
53
  converter("きって"); // => "킷테"
50
54
  converter("がっこう"); // => "각코"
@@ -84,9 +88,9 @@ converter("パーティー"); // => "파티"
84
88
  converter("ゲーム"); // => "게무"
85
89
 
86
90
  // 커스텀 사전
87
- converter("とうきょう"); // => "도쿄"
88
91
  converter("すみません"); // => "스미마셍"
89
- converter("こんばんは"); // => "콤방와"
92
+ converter("かわいい"); // => "카와이"
93
+ converter("はひふへほ"); // => "하히후헤호"
90
94
 
91
95
  // 한자포함
92
96
  converter("誕生日(たんじょうび)"); // => "誕生日(탄죠비)"
@@ -98,15 +102,4 @@ converter("コーヒー, ください。"); // => "코히, 쿠다사이。"
98
102
  converter("「きょう」"); // => "「쿄」"
99
103
  converter("(がっこう)"); // => "(각코)"
100
104
 
101
- // 전각/반각
102
- converter("ハングル"); // => "항구루"
103
- converter("カタカナ"); // => "카타카나"
104
- converter("ト-キョ-"); // => "토쿄"
105
-
106
- // NFC 합성
107
- converter("がくせい"); // => "가쿠세"
108
- converter("ぱーてぃー"); // => "파티"
109
- converter("べんごし"); // => "벵고시"
110
-
111
-
112
105
  ```
package/dist/index.js CHANGED
@@ -33,7 +33,72 @@ function toHiragana(input) {
33
33
  return out;
34
34
  }
35
35
 
36
+ // src/dictionary.ts
37
+ var SpecialDictionary = [
38
+ // ["とうきょう", "도쿄"],
39
+ { word: "\u3053\u3093\u306B\u3061\u306F", answer: "\uACE4\uB2C8\uCE58\uC640", hira: true, kata: true },
40
+ { word: "\u3053\u3093\u3070\u3093\u306F", answer: "\uACF0\uBC29\uC640" },
41
+ { word: "\u3059\u307F\u307E\u305B\u3093", answer: "\uC2A4\uBBF8\uB9C8\uC14D" },
42
+ { word: "\u306F\u3072\u3075\u3078\u307B", answer: "\uD558\uD788\uD6C4\uD5E4\uD638" },
43
+ { word: "\u304B\u308F\u3044\u3044", answer: "\uCE74\uC640\uC774" },
44
+ { word: "\u3064\u306A\u307F", answer: "\uC4F0\uB098\uBBF8" },
45
+ { word: "\u3086\u3046\u308A", answer: "\uC720\uC6B0\uB9AC" },
46
+ { word: "\u30DF\u30E5\u30FC\u30B8\u30C3\u30AF", answer: "\uBBA4\uC9C0\uCFE0" }
47
+ ];
48
+
36
49
  // src/particleRewriter.ts
50
+ function isKatakanaChar(ch) {
51
+ const c = ch.codePointAt(0);
52
+ return c >= 12448 && c <= 12543;
53
+ }
54
+ function toHiragana2(s) {
55
+ const n = s.normalize("NFKC");
56
+ return Array.from(n).map((ch) => {
57
+ if (!isKatakanaChar(ch))
58
+ return ch;
59
+ return String.fromCodePoint(ch.codePointAt(0) - 96);
60
+ }).join("");
61
+ }
62
+ function dictKeysForHiraganaText(dict) {
63
+ const keys = [];
64
+ for (const e of dict) {
65
+ keys.push(e.word);
66
+ if (e.hira)
67
+ keys.push(toHiragana2(e.word));
68
+ }
69
+ return [...new Set(keys)].filter(Boolean);
70
+ }
71
+ function rangesOverlap(a, b) {
72
+ return a.start < b.end && b.start < a.end;
73
+ }
74
+ function buildProtectedRanges(text, keys) {
75
+ const sorted = [...new Set(keys)].sort((a, b) => b.length - a.length);
76
+ const ranges = [];
77
+ for (const key of sorted) {
78
+ if (!key)
79
+ continue;
80
+ let from = 0;
81
+ while (true) {
82
+ const idx = text.indexOf(key, from);
83
+ if (idx === -1)
84
+ break;
85
+ const cand = { start: idx, end: idx + key.length };
86
+ if (!ranges.some((r) => rangesOverlap(r, cand))) {
87
+ ranges.push(cand);
88
+ }
89
+ from = idx + 1;
90
+ }
91
+ }
92
+ ranges.sort((a, b) => a.start - b.start);
93
+ return ranges;
94
+ }
95
+ function isProtectedSpan(protectedRanges, start, end) {
96
+ for (const r of protectedRanges) {
97
+ if (start < r.end && end > r.start)
98
+ return true;
99
+ }
100
+ return false;
101
+ }
37
102
  var HARD_BOUNDARY_SURF = /* @__PURE__ */ new Set([
38
103
  "\u3002",
39
104
  "\u3001",
@@ -61,59 +126,6 @@ var LEXICAL_HE_ENDINGS = [
61
126
  "\u304F\u306B\u3078",
62
127
  "\u304D\u3057\u3078"
63
128
  ];
64
- var isSingleKana = (x) => x.length === 1 && /^[\u3040-\u309F\u30A0-\u30FF]$/.test(x);
65
- function rewriteParticlesWithKuromoji(text, tokenizer) {
66
- const tokens = tokenizer.tokenize(text);
67
- let out = "";
68
- for (let i = 0; i < tokens.length; i += 1) {
69
- const token = tokens[i];
70
- const surf = token.surface_form;
71
- let replaced = surf;
72
- if (token.pos === "\u52A9\u8A5E" && surf === "\u306F") {
73
- if (i > 0 && tokens[i - 1].surface_form === "\u306F") {
74
- out += surf;
75
- continue;
76
- }
77
- if (i + 1 < tokens.length && tokens[i + 1].surface_form === "\u306F") {
78
- out += surf;
79
- continue;
80
- }
81
- const prevIdx = prevContentIdx(tokens, i);
82
- if (prevIdx >= 0) {
83
- const nextIdx = nextContentIdx(tokens, i);
84
- const hasNextContent = nextIdx >= 0;
85
- const isEndOrPunct = nextBoundaryOrEnd(tokens, i);
86
- const prev = tokens[prevIdx];
87
- if (!prev.surface_form.includes("\u3063") && token.pos_detail_1 === "\u4FC2\u52A9\u8A5E" && (hasNextContent || isEndOrPunct) && prev.pos !== "\u52A9\u8A5E") {
88
- out += "\u308F";
89
- continue;
90
- }
91
- }
92
- out += surf;
93
- continue;
94
- }
95
- if (token.pos === "\u52A9\u8A5E" && surf === "\u3078") {
96
- const prevIdx = prevContentIdx(tokens, i);
97
- if (prevIdx >= 0) {
98
- const nextIdx = nextContentIdx(tokens, i);
99
- const hasNextContent = nextIdx >= 0;
100
- const isEndOrPunct = nextBoundaryOrEnd(tokens, i);
101
- const prevSurf = tokens[prevIdx].surface_form;
102
- if (LEXICAL_HE_ENDINGS.some((w) => (prevSurf + "\u3078").endsWith(w))) ; else if (prevSurf.endsWith("\u306E")) ; else if (token.pos_detail_1 === "\u683C\u52A9\u8A5E") {
103
- const nextPos = hasNextContent ? tokens[nextIdx].pos : "";
104
- const looksDirectionalByVerb = nextPos === "\u52D5\u8A5E" || nextPos === "\u52A9\u52D5\u8A5E";
105
- const nextSurf = hasNextContent ? tokens[nextIdx].surface_form : "";
106
- const nextIsSingleKana = hasNextContent && isSingleKana(nextSurf);
107
- if (!nextIsSingleKana && (looksDirectionalByVerb || isEndOrPunct)) {
108
- replaced = "\u3048";
109
- }
110
- }
111
- }
112
- }
113
- out += replaced;
114
- }
115
- return out;
116
- }
117
129
  function isHardBoundaryToken(t) {
118
130
  if (t.pos !== "\u8A18\u53F7")
119
131
  return false;
@@ -146,17 +158,314 @@ function nextBoundaryOrEnd(tokens, i) {
146
158
  }
147
159
  return true;
148
160
  }
161
+ function rewriteParticlesFromTokenization(originalText, hiraganaText, tokenizerTokens) {
162
+ const hiraChars = Array.from(hiraganaText);
163
+ const originChars = Array.from(originalText);
164
+ const protectedRanges = buildProtectedRanges(
165
+ hiraganaText,
166
+ dictKeysForHiraganaText(SpecialDictionary)
167
+ );
168
+ let out = "";
169
+ const spans = [];
170
+ let cursorInText = 0;
171
+ for (let i = 0; i < tokenizerTokens.length; i += 1) {
172
+ const tok = tokenizerTokens[i];
173
+ const wp = tok.word_position;
174
+ const surf = tok.surface_form;
175
+ const surfCpLen = [...surf].length;
176
+ const start = typeof wp === "number" ? wp - 1 : cursorInText;
177
+ const end = start + surfCpLen;
178
+ cursorInText = end;
179
+ const hiraSurf = hiraChars.slice(start, end).join("");
180
+ const originSurf = originChars.slice(start, end).join("");
181
+ const originHadKatakana = /[\u30A0-\u30FF]/.test(originSurf);
182
+ if (isProtectedSpan(protectedRanges, start, end)) {
183
+ const outCpStart2 = [...out].length;
184
+ out += hiraSurf;
185
+ spans.push({
186
+ start: outCpStart2,
187
+ end: [...out].length,
188
+ surface: hiraSurf,
189
+ pos: tok.pos,
190
+ pos1: tok.pos_detail_1 ?? void 0,
191
+ pos2: tok.pos_detail_2 ?? void 0,
192
+ pos3: tok.pos_detail_3 ?? void 0,
193
+ originHadKatakana
194
+ });
195
+ continue;
196
+ }
197
+ let replaced = hiraSurf;
198
+ if (tok.pos === "\u52A9\u8A5E" && hiraSurf === "\u306F") {
199
+ if (i > 0 && tokenizerTokens[i - 1].surface_form === "\u306F") ; else if (i + 1 < tokenizerTokens.length && tokenizerTokens[i + 1].surface_form === "\u306F") ; else {
200
+ const prevIdx = prevContentIdx(tokenizerTokens, i);
201
+ if (prevIdx >= 0) {
202
+ const nextIdx = nextContentIdx(tokenizerTokens, i);
203
+ const hasNextContent = nextIdx >= 0;
204
+ const isEndOrPunct = nextBoundaryOrEnd(tokenizerTokens, i);
205
+ const prevTok = tokenizerTokens[prevIdx];
206
+ const prevWpHa = prevTok.word_position;
207
+ const prevStartHa = typeof prevWpHa === "number" ? prevWpHa - 1 : 0;
208
+ const prevEndHa = prevStartHa + [...prevTok.surface_form].length;
209
+ const prevHira = hiraChars.slice(prevStartHa, prevEndHa).join("");
210
+ if (!prevHira.includes("\u3063") && tok.pos_detail_1 === "\u4FC2\u52A9\u8A5E" && (hasNextContent || isEndOrPunct) && prevTok.pos !== "\u52A9\u8A5E") {
211
+ replaced = "\u308F";
212
+ }
213
+ }
214
+ }
215
+ }
216
+ if (tok.pos === "\u52A9\u8A5E" && hiraSurf === "\u3078") {
217
+ if (start > 0) {
218
+ const left = hiraChars[start - 1];
219
+ if (left === " " || left === "\u3000" || left === " " || left === "\n" || left === "\r") ; else {
220
+ const prevIdx = prevContentIdx(tokenizerTokens, i);
221
+ if (prevIdx >= 0) {
222
+ const prevTok = tokenizerTokens[prevIdx];
223
+ const prevWp = prevTok.word_position;
224
+ const prevStart = typeof prevWp === "number" ? prevWp - 1 : 0;
225
+ const prevEnd = prevStart + [...prevTok.surface_form].length;
226
+ const prevHiraSurf = hiraChars.slice(prevStart, prevEnd).join("");
227
+ if (LEXICAL_HE_ENDINGS.some((w) => (prevHiraSurf + "\u3078").endsWith(w))) ; else if (prevHiraSurf.endsWith("\u306E")) ; else if (tok.pos_detail_1 === "\u683C\u52A9\u8A5E") {
228
+ replaced = "\u3048";
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+ const outCpStart = [...out].length;
235
+ out += replaced;
236
+ spans.push({
237
+ start: outCpStart,
238
+ end: [...out].length,
239
+ surface: replaced,
240
+ pos: tok.pos,
241
+ pos1: tok.pos_detail_1 ?? void 0,
242
+ pos2: tok.pos_detail_2 ?? void 0,
243
+ pos3: tok.pos_detail_3 ?? void 0,
244
+ originHadKatakana
245
+ });
246
+ }
247
+ return { rewritten: out, spans };
248
+ }
249
+ function tokenizeAndRewriteParticles(originalText, hiraganaText, tokenizer) {
250
+ const rawTokens = tokenizer.tokenize(originalText);
251
+ const { rewritten, spans } = rewriteParticlesFromTokenization(
252
+ originalText,
253
+ hiraganaText,
254
+ rawTokens
255
+ );
256
+ return { rewritten, spans, rawTokens };
257
+ }
258
+
259
+ // src/mora.ts
260
+ var SINGLE = {
261
+ \u3042: { out: "\uC544", vowelMain: "a", consClass: "vowel", vowelOnly: true },
262
+ \u3044: { out: "\uC774", vowelMain: "i", consClass: "vowel", vowelOnly: true },
263
+ \u3046: { out: "\uC6B0", vowelMain: "u", consClass: "vowel", vowelOnly: true },
264
+ \u3048: { out: "\uC5D0", vowelMain: "e", consClass: "vowel", vowelOnly: true },
265
+ \u304A: { out: "\uC624", vowelMain: "o", consClass: "vowel", vowelOnly: true },
266
+ \u304B: { out: "\uCE74", vowelMain: "a", consClass: "k" },
267
+ \u304D: { out: "\uD0A4", vowelMain: "i", consClass: "k" },
268
+ \u304F: { out: "\uCFE0", vowelMain: "u", consClass: "k" },
269
+ \u3051: { out: "\uCF00", vowelMain: "e", consClass: "k" },
270
+ \u3053: { out: "\uCF54", vowelMain: "o", consClass: "k" },
271
+ \u3055: { out: "\uC0AC", vowelMain: "a", consClass: "s" },
272
+ \u3057: { out: "\uC2DC", vowelMain: "i", consClass: "s" },
273
+ \u3059: { out: "\uC2A4", vowelMain: "u", consClass: "s" },
274
+ \u305B: { out: "\uC138", vowelMain: "e", consClass: "s" },
275
+ \u305D: { out: "\uC18C", vowelMain: "o", consClass: "s" },
276
+ \u305F: { out: "\uD0C0", vowelMain: "a", consClass: "t" },
277
+ \u3061: { out: "\uCE58", vowelMain: "i", consClass: "t" },
278
+ \u3064: { out: "\uCE20", vowelMain: "u", consClass: "t" },
279
+ \u3066: { out: "\uD14C", vowelMain: "e", consClass: "t" },
280
+ \u3068: { out: "\uD1A0", vowelMain: "o", consClass: "t" },
281
+ \u306A: { out: "\uB098", vowelMain: "a", consClass: "n" },
282
+ \u306B: { out: "\uB2C8", vowelMain: "i", consClass: "n" },
283
+ \u306C: { out: "\uB204", vowelMain: "u", consClass: "n" },
284
+ \u306D: { out: "\uB124", vowelMain: "e", consClass: "n" },
285
+ \u306E: { out: "\uB178", vowelMain: "o", consClass: "n" },
286
+ \u306F: { out: "\uD558", vowelMain: "a", consClass: "h" },
287
+ \u3072: { out: "\uD788", vowelMain: "i", consClass: "h" },
288
+ \u3075: { out: "\uD6C4", vowelMain: "u", consClass: "h" },
289
+ \u3078: { out: "\uD5E4", vowelMain: "e", consClass: "h" },
290
+ \u307B: { out: "\uD638", vowelMain: "o", consClass: "h" },
291
+ \u307E: { out: "\uB9C8", vowelMain: "a", consClass: "m" },
292
+ \u307F: { out: "\uBBF8", vowelMain: "i", consClass: "m" },
293
+ \u3080: { out: "\uBB34", vowelMain: "u", consClass: "m" },
294
+ \u3081: { out: "\uBA54", vowelMain: "e", consClass: "m" },
295
+ \u3082: { out: "\uBAA8", vowelMain: "o", consClass: "m" },
296
+ \u3084: { out: "\uC57C", vowelMain: "a", consClass: "y" },
297
+ \u3086: { out: "\uC720", vowelMain: "u", consClass: "y" },
298
+ \u3088: { out: "\uC694", vowelMain: "o", consClass: "y" },
299
+ \u3089: { out: "\uB77C", vowelMain: "a", consClass: "r" },
300
+ \u308A: { out: "\uB9AC", vowelMain: "i", consClass: "r" },
301
+ \u308B: { out: "\uB8E8", vowelMain: "u", consClass: "r" },
302
+ \u308C: { out: "\uB808", vowelMain: "e", consClass: "r" },
303
+ \u308D: { out: "\uB85C", vowelMain: "o", consClass: "r" },
304
+ \u308F: { out: "\uC640", vowelMain: "a", consClass: "w" },
305
+ \u3092: { out: "\uC624", vowelMain: "o", consClass: "w" },
306
+ \u304C: { out: "\uAC00", vowelMain: "a", consClass: "g" },
307
+ \u304E: { out: "\uAE30", vowelMain: "i", consClass: "g" },
308
+ \u3050: { out: "\uAD6C", vowelMain: "u", consClass: "g" },
309
+ \u3052: { out: "\uAC8C", vowelMain: "e", consClass: "g" },
310
+ \u3054: { out: "\uACE0", vowelMain: "o", consClass: "g" },
311
+ \u3056: { out: "\uC790", vowelMain: "a", consClass: "z" },
312
+ \u3058: { out: "\uC9C0", vowelMain: "i", consClass: "z" },
313
+ \u305A: { out: "\uC988", vowelMain: "u", consClass: "z" },
314
+ \u305C: { out: "\uC81C", vowelMain: "e", consClass: "z" },
315
+ \u305E: { out: "\uC870", vowelMain: "o", consClass: "z" },
316
+ \u3060: { out: "\uB2E4", vowelMain: "a", consClass: "d" },
317
+ \u3062: { out: "\uC9C0", vowelMain: "i", consClass: "d" },
318
+ \u3065: { out: "\uC988", vowelMain: "u", consClass: "d" },
319
+ \u3067: { out: "\uB370", vowelMain: "e", consClass: "d" },
320
+ \u3069: { out: "\uB3C4", vowelMain: "o", consClass: "d" },
321
+ \u3070: { out: "\uBC14", vowelMain: "a", consClass: "b" },
322
+ \u3073: { out: "\uBE44", vowelMain: "i", consClass: "b" },
323
+ \u3076: { out: "\uBD80", vowelMain: "u", consClass: "b" },
324
+ \u3079: { out: "\uBCA0", vowelMain: "e", consClass: "b" },
325
+ \u307C: { out: "\uBCF4", vowelMain: "o", consClass: "b" },
326
+ \u3071: { out: "\uD30C", vowelMain: "a", consClass: "p" },
327
+ \u3074: { out: "\uD53C", vowelMain: "i", consClass: "p" },
328
+ \u3077: { out: "\uD478", vowelMain: "u", consClass: "p" },
329
+ \u307A: { out: "\uD398", vowelMain: "e", consClass: "p" },
330
+ \u307D: { out: "\uD3EC", vowelMain: "o", consClass: "p" },
331
+ \u3094: { out: "\uBD80", vowelMain: "u", consClass: "b" }
332
+ };
333
+ var YOUON = {
334
+ \u304D\u3083: { out: "\uCEAC", vowelMain: "a", consClass: "k", wasYouon: true },
335
+ \u304D\u3085: { out: "\uD050", vowelMain: "u", consClass: "k", wasYouon: true },
336
+ \u304D\u3087: { out: "\uCFC4", vowelMain: "o", consClass: "k", wasYouon: true },
337
+ \u3057\u3083: { out: "\uC0E4", vowelMain: "a", consClass: "s", wasYouon: true },
338
+ \u3057\u3085: { out: "\uC288", vowelMain: "u", consClass: "s", wasYouon: true },
339
+ \u3057\u3087: { out: "\uC1FC", vowelMain: "o", consClass: "s", wasYouon: true },
340
+ \u3061\u3083: { out: "\uCC60", vowelMain: "a", consClass: "t", wasYouon: true },
341
+ \u3061\u3085: { out: "\uCE04", vowelMain: "u", consClass: "t", wasYouon: true },
342
+ \u3061\u3087: { out: "\uCD78", vowelMain: "o", consClass: "t", wasYouon: true },
343
+ \u3066\u3085: { out: "\uD29C", vowelMain: "u", consClass: "t", wasYouon: true },
344
+ \u3067\u3085: { out: "\uB4C0", vowelMain: "u", consClass: "d", wasYouon: true },
345
+ \u306B\u3083: { out: "\uB0D0", vowelMain: "a", consClass: "n", wasYouon: true },
346
+ \u306B\u3085: { out: "\uB274", vowelMain: "u", consClass: "n", wasYouon: true },
347
+ \u306B\u3087: { out: "\uB1E8", vowelMain: "o", consClass: "n", wasYouon: true },
348
+ \u3072\u3083: { out: "\uD590", vowelMain: "a", consClass: "h", wasYouon: true },
349
+ \u3072\u3085: { out: "\uD734", vowelMain: "u", consClass: "h", wasYouon: true },
350
+ \u3072\u3087: { out: "\uD6A8", vowelMain: "o", consClass: "h", wasYouon: true },
351
+ \u3075\u3083: { out: "\uD344", vowelMain: "a", consClass: "p", wasYouon: true },
352
+ \u3075\u3085: { out: "\uD4E8", vowelMain: "u", consClass: "p", wasYouon: true },
353
+ \u3075\u3087: { out: "\uD45C", vowelMain: "o", consClass: "p", wasYouon: true },
354
+ \u307F\u3083: { out: "\uBA00", vowelMain: "a", consClass: "m", wasYouon: true },
355
+ \u307F\u3085: { out: "\uBBA4", vowelMain: "u", consClass: "m", wasYouon: true },
356
+ \u307F\u3087: { out: "\uBB18", vowelMain: "o", consClass: "m", wasYouon: true },
357
+ \u308A\u3083: { out: "\uB7B4", vowelMain: "a", consClass: "r", wasYouon: true },
358
+ \u308A\u3085: { out: "\uB958", vowelMain: "u", consClass: "r", wasYouon: true },
359
+ \u308A\u3087: { out: "\uB8CC", vowelMain: "o", consClass: "r", wasYouon: true },
360
+ \u304E\u3083: { out: "\uAC38", vowelMain: "a", consClass: "g", wasYouon: true },
361
+ \u304E\u3085: { out: "\uADDC", vowelMain: "u", consClass: "g", wasYouon: true },
362
+ \u304E\u3087: { out: "\uAD50", vowelMain: "o", consClass: "g", wasYouon: true },
363
+ \u3058\u3083: { out: "\uC7C8", vowelMain: "a", consClass: "z", wasYouon: true },
364
+ \u3058\u3085: { out: "\uC96C", vowelMain: "u", consClass: "z", wasYouon: true },
365
+ \u3058\u3087: { out: "\uC8E0", vowelMain: "o", consClass: "z", wasYouon: true },
366
+ \u3073\u3083: { out: "\uBC4C", vowelMain: "a", consClass: "b", wasYouon: true },
367
+ \u3073\u3085: { out: "\uBDF0", vowelMain: "u", consClass: "b", wasYouon: true },
368
+ \u3073\u3087: { out: "\uBD64", vowelMain: "o", consClass: "b", wasYouon: true },
369
+ \u3074\u3083: { out: "\uD344", vowelMain: "a", consClass: "p", wasYouon: true },
370
+ \u3074\u3085: { out: "\uD4E8", vowelMain: "u", consClass: "p", wasYouon: true },
371
+ \u3074\u3087: { out: "\uD45C", vowelMain: "o", consClass: "p", wasYouon: true }
372
+ };
373
+ var LOAN = {
374
+ \u3066\u3043: { out: "\uD2F0", vowelMain: "i", consClass: "t" },
375
+ \u3067\u3043: { out: "\uB514", vowelMain: "i", consClass: "d" },
376
+ \u3061\u3047: { out: "\uCCB4", vowelMain: "e", consClass: "t" },
377
+ \u3057\u3047: { out: "\uC170", vowelMain: "e", consClass: "s" },
378
+ \u3058\u3047: { out: "\uC81C", vowelMain: "e", consClass: "z" },
379
+ \u3064\u3041: { out: "\uCC28", vowelMain: "a", consClass: "t" },
380
+ \u3064\u3043: { out: "\uCE58", vowelMain: "i", consClass: "t" },
381
+ \u3064\u3047: { out: "\uCCB4", vowelMain: "e", consClass: "t" },
382
+ \u3064\u3049: { out: "\uCD08", vowelMain: "o", consClass: "t" },
383
+ \u3075\u3041: { out: "\uD30C", vowelMain: "a", consClass: "p" },
384
+ \u3075\u3043: { out: "\uD53C", vowelMain: "i", consClass: "p" },
385
+ \u3075\u3047: { out: "\uD398", vowelMain: "e", consClass: "p" },
386
+ \u3075\u3049: { out: "\uD3EC", vowelMain: "o", consClass: "p" },
387
+ \u3050\u3041: { out: "\uACFC", vowelMain: "a", consClass: "g" },
388
+ \u3050\u3043: { out: "\uADC0", vowelMain: "i", consClass: "g" },
389
+ \u3050\u3047: { out: "\uADA4", vowelMain: "e", consClass: "g" },
390
+ \u3050\u3049: { out: "\uAD88", vowelMain: "o", consClass: "g" },
391
+ \u304F\u3041: { out: "\uCF70", vowelMain: "a", consClass: "k" },
392
+ \u304F\u3043: { out: "\uD034", vowelMain: "i", consClass: "k" },
393
+ \u304F\u3047: { out: "\uD018", vowelMain: "e", consClass: "k" },
394
+ \u304F\u3049: { out: "\uCFFC", vowelMain: "o", consClass: "k" },
395
+ \u3069\u3041: { out: "\uB3E0", vowelMain: "a", consClass: "d" },
396
+ \u3069\u3045: { out: "\uB450", vowelMain: "u", consClass: "d" },
397
+ \u3069\u3049: { out: "\uB46C", vowelMain: "o", consClass: "d" },
398
+ \u3094\u3041: { out: "\uBC14", vowelMain: "a", consClass: "b" },
399
+ \u3094\u3043: { out: "\uBE44", vowelMain: "i", consClass: "b" },
400
+ \u3094\u3047: { out: "\uBCA0", vowelMain: "e", consClass: "b" },
401
+ \u3094\u3049: { out: "\uBCF4", vowelMain: "o", consClass: "b" }
402
+ };
403
+ var SMALL_Y = /* @__PURE__ */ new Set(["\u3083", "\u3085", "\u3087"]);
404
+ var SMALL_V = /* @__PURE__ */ new Set(["\u3041", "\u3043", "\u3045", "\u3047", "\u3049"]);
405
+ var U_DROP_KEYS = /* @__PURE__ */ new Set([
406
+ "\u3086",
407
+ "\u304D\u3085",
408
+ "\u3057\u3085",
409
+ "\u3061\u3085",
410
+ "\u306B\u3085",
411
+ "\u3072\u3085",
412
+ "\u307F\u3085",
413
+ "\u308A\u3085",
414
+ "\u304E\u3085",
415
+ "\u3058\u3085",
416
+ "\u3073\u3085",
417
+ "\u3074\u3085"
418
+ ]);
149
419
 
150
420
  // src/coreConverter.ts
151
- function coreKanaToHangulConvert(s) {
152
- const SPECIAL = [
153
- ["\u3068\u3046\u304D\u3087\u3046", "\uB3C4\uCFC4"],
154
- ["\u3044\u3044\u3067\u3057\u3087\u3046\u304B", "\uC774\uB370\uC1FC\uCE74"],
155
- ["\u3044\u3044\u3067\u3057\u3087\u3046", "\uC774\uB370\uC1FC"],
156
- ["\u3053\u3093\u306B\u3061\u306F", "\uCF58\uB2C8\uCE58\uC640"],
157
- ["\u3053\u3093\u3070\u3093\u306F", "\uCF64\uBC29\uC640"],
158
- ["\u3059\u307F\u307E\u305B\u3093", "\uC2A4\uBBF8\uB9C8\uC14D"]
159
- ];
421
+ function isHiraganaChar(ch) {
422
+ const c = ch.codePointAt(0);
423
+ return c >= 12352 && c <= 12447;
424
+ }
425
+ function isKatakanaChar2(ch) {
426
+ const c = ch.codePointAt(0);
427
+ return c >= 12448 && c <= 12543;
428
+ }
429
+ function toHiraganaKey(s) {
430
+ const n = s.normalize("NFKC");
431
+ return Array.from(n).map(
432
+ (ch) => isKatakanaChar2(ch) ? String.fromCodePoint(ch.codePointAt(0) - 96) : ch
433
+ ).join("");
434
+ }
435
+ function toKatakanaKey(s) {
436
+ const n = s.normalize("NFKC");
437
+ return Array.from(n).map(
438
+ (ch) => isHiraganaChar(ch) ? String.fromCodePoint(ch.codePointAt(0) + 96) : ch
439
+ ).join("");
440
+ }
441
+ function compileSpecialDictionary(dict) {
442
+ const items = [];
443
+ for (const e of dict) {
444
+ items.push({
445
+ keyChars: Array.from(e.word),
446
+ answer: e.answer,
447
+ stream: "orig"
448
+ });
449
+ if (e.hira) {
450
+ const k = toHiraganaKey(e.word);
451
+ items.push({
452
+ keyChars: Array.from(k),
453
+ answer: e.answer,
454
+ stream: "rewritten"
455
+ });
456
+ }
457
+ if (e.kata) {
458
+ const k = toKatakanaKey(e.word);
459
+ items.push({ keyChars: Array.from(k), answer: e.answer, stream: "orig" });
460
+ }
461
+ }
462
+ items.sort((a, b) => b.keyChars.length - a.keyChars.length);
463
+ return items;
464
+ }
465
+ var COMPILED_SPECIAL_DICT = compileSpecialDictionary(SpecialDictionary);
466
+ function coreKanaToHangulConvert(s, opts) {
467
+ const chars = Array.from(s);
468
+ const origChars = Array.from(opts?.original ?? s);
160
469
  const HANGUL_BASE = 44032;
161
470
  const HANGUL_END = 55203;
162
471
  function isHangulSyllable(ch) {
@@ -200,171 +509,11 @@ function coreKanaToHangulConvert(s) {
200
509
  function isKana(ch) {
201
510
  return isHiragana(ch) || ch === "\u30FC";
202
511
  }
203
- const SINGLE = {
204
- \u3042: { out: "\uC544", vowelMain: "a", consClass: "vowel", vowelOnly: true },
205
- \u3044: { out: "\uC774", vowelMain: "i", consClass: "vowel", vowelOnly: true },
206
- \u3046: { out: "\uC6B0", vowelMain: "u", consClass: "vowel", vowelOnly: true },
207
- \u3048: { out: "\uC5D0", vowelMain: "e", consClass: "vowel", vowelOnly: true },
208
- \u304A: { out: "\uC624", vowelMain: "o", consClass: "vowel", vowelOnly: true },
209
- \u304B: { out: "\uCE74", vowelMain: "a", consClass: "k" },
210
- \u304D: { out: "\uD0A4", vowelMain: "i", consClass: "k" },
211
- \u304F: { out: "\uCFE0", vowelMain: "u", consClass: "k" },
212
- \u3051: { out: "\uCF00", vowelMain: "e", consClass: "k" },
213
- \u3053: { out: "\uCF54", vowelMain: "o", consClass: "k" },
214
- \u3055: { out: "\uC0AC", vowelMain: "a", consClass: "s" },
215
- \u3057: { out: "\uC2DC", vowelMain: "i", consClass: "s" },
216
- \u3059: { out: "\uC2A4", vowelMain: "u", consClass: "s" },
217
- \u305B: { out: "\uC138", vowelMain: "e", consClass: "s" },
218
- \u305D: { out: "\uC18C", vowelMain: "o", consClass: "s" },
219
- \u305F: { out: "\uD0C0", vowelMain: "a", consClass: "t" },
220
- \u3061: { out: "\uCE58", vowelMain: "i", consClass: "t" },
221
- \u3064: { out: "\uCE20", vowelMain: "u", consClass: "t" },
222
- \u3066: { out: "\uD14C", vowelMain: "e", consClass: "t" },
223
- \u3068: { out: "\uD1A0", vowelMain: "o", consClass: "t" },
224
- \u306A: { out: "\uB098", vowelMain: "a", consClass: "n" },
225
- \u306B: { out: "\uB2C8", vowelMain: "i", consClass: "n" },
226
- \u306C: { out: "\uB204", vowelMain: "u", consClass: "n" },
227
- \u306D: { out: "\uB124", vowelMain: "e", consClass: "n" },
228
- \u306E: { out: "\uB178", vowelMain: "o", consClass: "n" },
229
- \u306F: { out: "\uD558", vowelMain: "a", consClass: "h" },
230
- \u3072: { out: "\uD788", vowelMain: "i", consClass: "h" },
231
- \u3075: { out: "\uD6C4", vowelMain: "u", consClass: "h" },
232
- \u3078: { out: "\uD5E4", vowelMain: "e", consClass: "h" },
233
- \u307B: { out: "\uD638", vowelMain: "o", consClass: "h" },
234
- \u307E: { out: "\uB9C8", vowelMain: "a", consClass: "m" },
235
- \u307F: { out: "\uBBF8", vowelMain: "i", consClass: "m" },
236
- \u3080: { out: "\uBB34", vowelMain: "u", consClass: "m" },
237
- \u3081: { out: "\uBA54", vowelMain: "e", consClass: "m" },
238
- \u3082: { out: "\uBAA8", vowelMain: "o", consClass: "m" },
239
- \u3084: { out: "\uC57C", vowelMain: "a", consClass: "y" },
240
- \u3086: { out: "\uC720", vowelMain: "u", consClass: "y" },
241
- \u3088: { out: "\uC694", vowelMain: "o", consClass: "y" },
242
- \u3089: { out: "\uB77C", vowelMain: "a", consClass: "r" },
243
- \u308A: { out: "\uB9AC", vowelMain: "i", consClass: "r" },
244
- \u308B: { out: "\uB8E8", vowelMain: "u", consClass: "r" },
245
- \u308C: { out: "\uB808", vowelMain: "e", consClass: "r" },
246
- \u308D: { out: "\uB85C", vowelMain: "o", consClass: "r" },
247
- \u308F: { out: "\uC640", vowelMain: "a", consClass: "w" },
248
- \u3092: { out: "\uC624", vowelMain: "o", consClass: "w" },
249
- \u304C: { out: "\uAC00", vowelMain: "a", consClass: "g" },
250
- \u304E: { out: "\uAE30", vowelMain: "i", consClass: "g" },
251
- \u3050: { out: "\uAD6C", vowelMain: "u", consClass: "g" },
252
- \u3052: { out: "\uAC8C", vowelMain: "e", consClass: "g" },
253
- \u3054: { out: "\uACE0", vowelMain: "o", consClass: "g" },
254
- \u3056: { out: "\uC790", vowelMain: "a", consClass: "z" },
255
- \u3058: { out: "\uC9C0", vowelMain: "i", consClass: "z" },
256
- \u305A: { out: "\uC988", vowelMain: "u", consClass: "z" },
257
- \u305C: { out: "\uC81C", vowelMain: "e", consClass: "z" },
258
- \u305E: { out: "\uC870", vowelMain: "o", consClass: "z" },
259
- \u3060: { out: "\uB2E4", vowelMain: "a", consClass: "d" },
260
- \u3062: { out: "\uC9C0", vowelMain: "i", consClass: "d" },
261
- \u3065: { out: "\uC988", vowelMain: "u", consClass: "d" },
262
- \u3067: { out: "\uB370", vowelMain: "e", consClass: "d" },
263
- \u3069: { out: "\uB3C4", vowelMain: "o", consClass: "d" },
264
- \u3070: { out: "\uBC14", vowelMain: "a", consClass: "b" },
265
- \u3073: { out: "\uBE44", vowelMain: "i", consClass: "b" },
266
- \u3076: { out: "\uBD80", vowelMain: "u", consClass: "b" },
267
- \u3079: { out: "\uBCA0", vowelMain: "e", consClass: "b" },
268
- \u307C: { out: "\uBCF4", vowelMain: "o", consClass: "b" },
269
- \u3071: { out: "\uD30C", vowelMain: "a", consClass: "p" },
270
- \u3074: { out: "\uD53C", vowelMain: "i", consClass: "p" },
271
- \u3077: { out: "\uD478", vowelMain: "u", consClass: "p" },
272
- \u307A: { out: "\uD398", vowelMain: "e", consClass: "p" },
273
- \u307D: { out: "\uD3EC", vowelMain: "o", consClass: "p" },
274
- // 이거는 う에 탁점 붙인 유니코드임.
275
- \u3094: { out: "\uBD80", vowelMain: "u", consClass: "b" }
276
- };
277
- const YOUON = {
278
- \u304D\u3083: { out: "\uCEAC", vowelMain: "a", consClass: "k", wasYouon: true },
279
- \u304D\u3085: { out: "\uD050", vowelMain: "u", consClass: "k", wasYouon: true },
280
- \u304D\u3087: { out: "\uCFC4", vowelMain: "o", consClass: "k", wasYouon: true },
281
- \u3057\u3083: { out: "\uC0E4", vowelMain: "a", consClass: "s", wasYouon: true },
282
- \u3057\u3085: { out: "\uC288", vowelMain: "u", consClass: "s", wasYouon: true },
283
- \u3057\u3087: { out: "\uC1FC", vowelMain: "o", consClass: "s", wasYouon: true },
284
- \u3061\u3083: { out: "\uCC60", vowelMain: "a", consClass: "t", wasYouon: true },
285
- \u3061\u3085: { out: "\uCE04", vowelMain: "u", consClass: "t", wasYouon: true },
286
- \u3061\u3087: { out: "\uCD78", vowelMain: "o", consClass: "t", wasYouon: true },
287
- \u3066\u3085: { out: "\uD29C", vowelMain: "u", consClass: "t", wasYouon: true },
288
- \u3067\u3085: { out: "\uB4C0", vowelMain: "u", consClass: "d", wasYouon: true },
289
- \u306B\u3083: { out: "\uB0D0", vowelMain: "a", consClass: "n", wasYouon: true },
290
- \u306B\u3085: { out: "\uB274", vowelMain: "u", consClass: "n", wasYouon: true },
291
- \u306B\u3087: { out: "\uB1E8", vowelMain: "o", consClass: "n", wasYouon: true },
292
- \u3072\u3083: { out: "\uD590", vowelMain: "a", consClass: "h", wasYouon: true },
293
- \u3072\u3085: { out: "\uD734", vowelMain: "u", consClass: "h", wasYouon: true },
294
- \u3072\u3087: { out: "\uD6A8", vowelMain: "o", consClass: "h", wasYouon: true },
295
- \u3075\u3083: { out: "\uD344", vowelMain: "a", consClass: "p", wasYouon: true },
296
- \u3075\u3085: { out: "\uD4E8", vowelMain: "u", consClass: "p", wasYouon: true },
297
- \u3075\u3087: { out: "\uD45C", vowelMain: "o", consClass: "p", wasYouon: true },
298
- \u307F\u3083: { out: "\uBA00", vowelMain: "a", consClass: "m", wasYouon: true },
299
- \u307F\u3085: { out: "\uBBA4", vowelMain: "u", consClass: "m", wasYouon: true },
300
- \u307F\u3087: { out: "\uBB18", vowelMain: "o", consClass: "m", wasYouon: true },
301
- \u308A\u3083: { out: "\uB7B4", vowelMain: "a", consClass: "r", wasYouon: true },
302
- \u308A\u3085: { out: "\uB958", vowelMain: "u", consClass: "r", wasYouon: true },
303
- \u308A\u3087: { out: "\uB8CC", vowelMain: "o", consClass: "r", wasYouon: true },
304
- \u304E\u3083: { out: "\uAC38", vowelMain: "a", consClass: "g", wasYouon: true },
305
- \u304E\u3085: { out: "\uADDC", vowelMain: "u", consClass: "g", wasYouon: true },
306
- \u304E\u3087: { out: "\uAD50", vowelMain: "o", consClass: "g", wasYouon: true },
307
- \u3058\u3083: { out: "\uC7C8", vowelMain: "a", consClass: "z", wasYouon: true },
308
- \u3058\u3085: { out: "\uC96C", vowelMain: "u", consClass: "z", wasYouon: true },
309
- \u3058\u3087: { out: "\uC8E0", vowelMain: "o", consClass: "z", wasYouon: true },
310
- \u3073\u3083: { out: "\uBC4C", vowelMain: "a", consClass: "b", wasYouon: true },
311
- \u3073\u3085: { out: "\uBDF0", vowelMain: "u", consClass: "b", wasYouon: true },
312
- \u3073\u3087: { out: "\uBD64", vowelMain: "o", consClass: "b", wasYouon: true },
313
- \u3074\u3083: { out: "\uD344", vowelMain: "a", consClass: "p", wasYouon: true },
314
- \u3074\u3085: { out: "\uD4E8", vowelMain: "u", consClass: "p", wasYouon: true },
315
- \u3074\u3087: { out: "\uD45C", vowelMain: "o", consClass: "p", wasYouon: true }
316
- };
317
- const LOAN = {
318
- \u3066\u3043: { out: "\uD2F0", vowelMain: "i", consClass: "t" },
319
- \u3067\u3043: { out: "\uB514", vowelMain: "i", consClass: "d" },
320
- \u3061\u3047: { out: "\uCCB4", vowelMain: "e", consClass: "t" },
321
- \u3057\u3047: { out: "\uC170", vowelMain: "e", consClass: "s" },
322
- \u3058\u3047: { out: "\uC81C", vowelMain: "e", consClass: "z" },
323
- \u3064\u3041: { out: "\uCC28", vowelMain: "a", consClass: "t" },
324
- \u3064\u3043: { out: "\uCE58", vowelMain: "i", consClass: "t" },
325
- \u3064\u3047: { out: "\uCCB4", vowelMain: "e", consClass: "t" },
326
- \u3064\u3049: { out: "\uCD08", vowelMain: "o", consClass: "t" },
327
- \u3075\u3041: { out: "\uD30C", vowelMain: "a", consClass: "p" },
328
- \u3075\u3043: { out: "\uD53C", vowelMain: "i", consClass: "p" },
329
- \u3075\u3047: { out: "\uD398", vowelMain: "e", consClass: "p" },
330
- \u3075\u3049: { out: "\uD3EC", vowelMain: "o", consClass: "p" },
331
- \u3050\u3041: { out: "\uACFC", vowelMain: "a", consClass: "g" },
332
- \u3050\u3043: { out: "\uADC0", vowelMain: "i", consClass: "g" },
333
- \u3050\u3047: { out: "\uADA4", vowelMain: "e", consClass: "g" },
334
- \u3050\u3049: { out: "\uAD88", vowelMain: "o", consClass: "g" },
335
- \u304F\u3041: { out: "\uCF70", vowelMain: "a", consClass: "k" },
336
- \u304F\u3043: { out: "\uD034", vowelMain: "i", consClass: "k" },
337
- \u304F\u3047: { out: "\uD018", vowelMain: "e", consClass: "k" },
338
- \u304F\u3049: { out: "\uCFFC", vowelMain: "o", consClass: "k" },
339
- \u3069\u3041: { out: "\uB3E0", vowelMain: "a", consClass: "d" },
340
- \u3069\u3045: { out: "\uB450", vowelMain: "u", consClass: "d" },
341
- \u3069\u3049: { out: "\uB46C", vowelMain: "o", consClass: "d" },
342
- \u3094\u3041: { out: "\uBC14", vowelMain: "a", consClass: "b" },
343
- \u3094\u3043: { out: "\uBE44", vowelMain: "i", consClass: "b" },
344
- \u3094\u3047: { out: "\uBCA0", vowelMain: "e", consClass: "b" },
345
- \u3094\u3049: { out: "\uBCF4", vowelMain: "o", consClass: "b" }
346
- };
347
- const SMALL_Y = /* @__PURE__ */ new Set(["\u3083", "\u3085", "\u3087"]);
348
- const SMALL_V = /* @__PURE__ */ new Set(["\u3041", "\u3043", "\u3045", "\u3047", "\u3049"]);
349
- const U_DROP_KEYS = /* @__PURE__ */ new Set([
350
- "\u3086",
351
- "\u304D\u3085",
352
- "\u3057\u3085",
353
- "\u3061\u3085",
354
- "\u306B\u3085",
355
- "\u3072\u3085",
356
- "\u307F\u3085",
357
- "\u308A\u3085",
358
- "\u304E\u3085",
359
- "\u3058\u3085",
360
- "\u3073\u3085",
361
- "\u3074\u3085"
362
- ]);
363
512
  function readMoraAt(idx) {
364
- if (idx >= s.length)
513
+ if (idx >= chars.length)
365
514
  return null;
366
- const c0 = s[idx];
367
- const c1 = s[idx + 1];
515
+ const c0 = chars[idx];
516
+ const c1 = chars[idx + 1];
368
517
  if (c1 && SMALL_V.has(c1)) {
369
518
  const key2 = c0 + c1;
370
519
  const info2 = LOAN[key2];
@@ -385,63 +534,122 @@ function coreKanaToHangulConvert(s) {
385
534
  function isLabialStart(cons) {
386
535
  return cons === "m" || cons === "b" || cons === "p";
387
536
  }
388
- const isBoundary = (ch) => {
389
- if (!ch)
537
+ const tokens = opts?.tokens ?? null;
538
+ let tokIdx = 0;
539
+ function syncTokenIndex(charIndex) {
540
+ if (!tokens)
541
+ return;
542
+ while (tokIdx + 1 < tokens.length && tokens[tokIdx].end <= charIndex) {
543
+ tokIdx++;
544
+ }
545
+ }
546
+ function curToken(charIndex) {
547
+ if (!tokens)
548
+ return null;
549
+ syncTokenIndex(charIndex);
550
+ const t = tokens[tokIdx];
551
+ if (t && t.start <= charIndex && charIndex < t.end)
552
+ return t;
553
+ return null;
554
+ }
555
+ function prevToken() {
556
+ if (!tokens)
557
+ return null;
558
+ return tokIdx - 1 >= 0 ? tokens[tokIdx - 1] : null;
559
+ }
560
+ function nextToken() {
561
+ if (!tokens)
562
+ return null;
563
+ return tokIdx + 1 < tokens.length ? tokens[tokIdx + 1] : null;
564
+ }
565
+ const INITIAL_VOICING_BLOCK_NEXT = /* @__PURE__ */ new Set(["\u3044", "\u3072", "\u3093", "\u3066"]);
566
+ function peekNextMoraKeySkippingChoonpu(fromIdx) {
567
+ let j = fromIdx;
568
+ while (j < chars.length && chars[j] === "\u30FC")
569
+ j++;
570
+ const m = readMoraAt(j);
571
+ return m?.key ?? null;
572
+ }
573
+ const SAN_PARTICLES = /* @__PURE__ */ new Set(["\u306F", "\u308F", "\u3078", "\u3048", "\u3092", "\u304A"]);
574
+ function isSanHonorificAt(cpIdx) {
575
+ const t = curToken(cpIdx);
576
+ if (!t)
577
+ return false;
578
+ if (cpIdx < 1 || chars[cpIdx - 1] !== "\u3055")
579
+ return false;
580
+ const local = chars.slice(t.start, cpIdx + 1).join("");
581
+ if (!local.endsWith("\u3055\u3093"))
582
+ return false;
583
+ const hasPrefixInsideToken = cpIdx - 1 > t.start;
584
+ const p = prevToken();
585
+ const prevIsAttachable = !!p && p.end === t.start && p.surface.length > 0 && !HARD_BOUNDARY_SURF.has(p.surface);
586
+ if (!hasPrefixInsideToken && !prevIsAttachable)
587
+ return false;
588
+ const n = nextToken();
589
+ if (!n)
390
590
  return true;
391
- return /\s|[、。!?!?\(\)\[\]{}「」『』()【】]/.test(ch);
392
- };
591
+ if (n.pos === "\u8A18\u53F7" && HARD_BOUNDARY_SURF.has(n.surface))
592
+ return true;
593
+ if (n.pos === "\u52A9\u8A5E" && SAN_PARTICLES.has(n.surface))
594
+ return true;
595
+ return false;
596
+ }
393
597
  let out = "";
394
598
  let i = 0;
395
599
  let lastMora = null;
396
- let leadingSokuon = false;
397
- while (i < s.length) {
600
+ while (i < chars.length) {
601
+ let atTokenStart = false;
602
+ let tokForI = null;
603
+ if (tokens) {
604
+ tokForI = curToken(i);
605
+ atTokenStart = !!tokForI && tokForI.start === i;
606
+ if (tokForI?.pos === "\u8A18\u53F7")
607
+ atTokenStart = false;
608
+ } else {
609
+ atTokenStart = i === 0;
610
+ }
398
611
  let matchedSpecial = false;
399
- for (const [k, v] of SPECIAL) {
400
- if (s.startsWith(k, i)) {
401
- out += v;
402
- i += k.length;
403
- lastMora = null;
404
- matchedSpecial = true;
405
- break;
612
+ for (const it of COMPILED_SPECIAL_DICT) {
613
+ const src = it.stream === "orig" ? origChars : chars;
614
+ const len = it.keyChars.length;
615
+ if (i + len > src.length)
616
+ continue;
617
+ let ok = true;
618
+ for (let k = 0; k < len; k++) {
619
+ if (src[i + k] !== it.keyChars[k]) {
620
+ ok = false;
621
+ break;
622
+ }
406
623
  }
624
+ if (!ok)
625
+ continue;
626
+ out += it.answer;
627
+ i += len;
628
+ lastMora = null;
629
+ matchedSpecial = true;
630
+ break;
407
631
  }
408
632
  if (matchedSpecial)
409
633
  continue;
410
- if (s.startsWith("\u3061\u3083\u3093", i)) {
634
+ if (chars.slice(i, i + 3).join("") === "\u3061\u3083\u3093") {
411
635
  out += "\uCA29";
412
636
  i += 3;
413
637
  lastMora = { out: "\uCA29", vowelMain: "a", consClass: "t", wasYouon: true };
414
638
  continue;
415
639
  }
416
- const ch = s[i];
640
+ const ch = chars[i];
417
641
  if (ch === "\u30FC") {
418
642
  i += 1;
419
643
  continue;
420
644
  }
421
- if (leadingSokuon) {
422
- if (isHiragana(ch)) {
423
- out += hiraToKata(ch);
424
- i += 1;
425
- leadingSokuon = false;
426
- lastMora = null;
427
- continue;
428
- } else {
429
- leadingSokuon = false;
430
- }
431
- }
432
645
  if (ch === "\u3063") {
433
646
  if (!out || !isHangulSyllable(out[out.length - 1])) {
434
647
  out += "\u30C3";
435
648
  i += 1;
436
- leadingSokuon = true;
437
649
  lastMora = null;
438
650
  continue;
439
651
  }
440
652
  const next = readMoraAt(i + 1);
441
- if (lastMora && lastMora.out === "\uC9C0" && next && next.key === "\u304F") {
442
- i += 1;
443
- continue;
444
- }
445
653
  const prevV = lastMora?.vowelMain ?? "a";
446
654
  const nextInfo = next?.info;
447
655
  const nextCons = nextInfo?.consClass ?? "t";
@@ -463,9 +671,9 @@ function coreKanaToHangulConvert(s) {
463
671
  lastMora = null;
464
672
  continue;
465
673
  }
466
- if (ch === "\u304A" && s[i + 1] === "\u304A") {
674
+ if (ch === "\u304A" && chars[i + 1] === "\u304A") {
467
675
  let j = i;
468
- while (s[j] === "\u304A")
676
+ while (chars[j] === "\u304A")
469
677
  j++;
470
678
  out += "\uC624";
471
679
  i = j;
@@ -479,7 +687,7 @@ function coreKanaToHangulConvert(s) {
479
687
  }
480
688
  const mora = readMoraAt(i);
481
689
  if (!mora) {
482
- out += ch;
690
+ out += chars[i];
483
691
  i += 1;
484
692
  lastMora = null;
485
693
  continue;
@@ -487,7 +695,6 @@ function coreKanaToHangulConvert(s) {
487
695
  if (mora.key === "\u3093") {
488
696
  const next = readMoraAt(i + 1);
489
697
  const nextInfo = next?.info;
490
- let jong = JONG.N;
491
698
  const hasPrevHangul = out.length > 0 && isHangulSyllable(out[out.length - 1]);
492
699
  if (!hasPrevHangul) {
493
700
  out += "\u3134";
@@ -495,17 +702,12 @@ function coreKanaToHangulConvert(s) {
495
702
  lastMora = null;
496
703
  continue;
497
704
  }
498
- if (lastMora?.out === "\uC0AC") {
499
- const nextCh = s[i + 1];
500
- const isBoundaryOrEnd = !nextCh || /\s|[、。!?!?\(\)\[\]{}「」『』()【】]/.test(nextCh);
501
- const isParticleAfterSan = nextCh === "\u306F" || nextCh === "\u3078" || nextCh === "\u3092" || nextCh === "\u308F" || nextCh === "\u3048" || nextCh === "\u304A";
502
- const hasPrefixBeforeSan = out.length >= 2;
503
- if (hasPrefixBeforeSan && (isBoundaryOrEnd || isParticleAfterSan)) {
504
- out = replaceLastHangul(out, JONG.NG);
505
- i += 1;
506
- continue;
507
- }
705
+ if (lastMora?.out === "\uC0AC" && isSanHonorificAt(i)) {
706
+ out = replaceLastHangul(out, JONG.NG);
707
+ i += 1;
708
+ continue;
508
709
  }
710
+ let jong = JONG.N;
509
711
  if (!next || !nextInfo || !isKana(next.key[0])) {
510
712
  jong = lastMora?.wasYouon ? JONG.NG : JONG.N;
511
713
  } else {
@@ -534,10 +736,35 @@ function coreKanaToHangulConvert(s) {
534
736
  lastMora = null;
535
737
  continue;
536
738
  }
537
- out += info.out;
538
- lastMora = info;
539
- const next1 = s[i + mora.len];
540
- const afterLen = s[i + mora.len + 1];
739
+ let outSyl = info.out;
740
+ if (atTokenStart && (mora.key === "\u3068" || mora.key === "\u3053")) {
741
+ const prevIsSokuon = i > 0 && chars[i - 1] === "\u3063";
742
+ const nextKey = peekNextMoraKeySkippingChoonpu(i + mora.len);
743
+ const blockedByNext = !!nextKey && INITIAL_VOICING_BLOCK_NEXT.has(nextKey);
744
+ const isKou = mora.key === "\u3053" && chars[i + mora.len] === "\u3046";
745
+ let blockedByParticleUsage = false;
746
+ if (tokens && tokForI) {
747
+ const isSingleCharToken = tokForI.surface.length === 1;
748
+ const tokenMatchesMora = tokForI.surface === mora.key;
749
+ if (isSingleCharToken && tokenMatchesMora) {
750
+ const p = prevToken();
751
+ const prevLooksLikeContent = !!p && p.pos !== "\u8A18\u53F7" && p.pos !== "\u52A9\u8A5E" && p.pos !== "\u52A9\u52D5\u8A5E" && !HARD_BOUNDARY_SURF.has(p.surface);
752
+ if (prevLooksLikeContent)
753
+ blockedByParticleUsage = true;
754
+ }
755
+ }
756
+ const allowVoicing = !prevIsSokuon && !blockedByNext && !isKou && !blockedByParticleUsage;
757
+ if (allowVoicing) {
758
+ if (mora.key === "\u3068")
759
+ outSyl = "\uB3C4";
760
+ else if (mora.key === "\u3053")
761
+ outSyl = "\uACE0";
762
+ }
763
+ }
764
+ out += outSyl;
765
+ lastMora = { ...info, out: outSyl };
766
+ const next1 = chars[i + mora.len];
767
+ const afterLen = chars[i + mora.len + 1];
541
768
  if (next1 === "\u3046" && info.vowelMain === "o") {
542
769
  i += mora.len + 1;
543
770
  continue;
@@ -566,7 +793,7 @@ function coreKanaToHangulConvert(s) {
566
793
  i += mora.len + 1;
567
794
  continue;
568
795
  } else if (mora.key === "\u304D") {
569
- if (isBoundary(afterLen) || !afterLen) {
796
+ if (!afterLen) {
570
797
  i += mora.len + 1;
571
798
  continue;
572
799
  }
@@ -583,13 +810,6 @@ function coreKanaToHangulConvert(s) {
583
810
  }
584
811
  return out;
585
812
  }
586
- function hiraToKata(hira) {
587
- const c = hira.codePointAt(0);
588
- if (c >= 12353 && c <= 12438) {
589
- return String.fromCodePoint(c + 96);
590
- }
591
- return hira;
592
- }
593
813
 
594
814
  // src/kanaToHangul.ts
595
815
  function createKanaToHangul(tokenizer) {
@@ -598,8 +818,16 @@ function createKanaToHangul(tokenizer) {
598
818
  function convertWithTokenizer(input, tokenizer) {
599
819
  const normalized = normalizeInputText(input);
600
820
  const hiragana = toHiragana(normalized);
601
- const rewritten = rewriteParticlesWithKuromoji(hiragana, tokenizer);
602
- return coreKanaToHangulConvert(rewritten);
821
+ const { rewritten, spans } = tokenizeAndRewriteParticles(
822
+ normalized,
823
+ hiragana,
824
+ tokenizer
825
+ );
826
+ return coreKanaToHangulConvert(rewritten, {
827
+ tokens: spans,
828
+ original: normalized
829
+ // ✅ 이게 핵심
830
+ });
603
831
  }
604
832
  var require2 = createRequire(import.meta.url);
605
833
  async function buildTokenizer() {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/normalizer.ts","../src/particleRewriter.ts","../src/coreConverter.ts","../src/kanaToHangul.ts","../src/tokenizer.ts","../src/kanaBarum.ts"],"names":["out","info","require"],"mappings":"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,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,IAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA;AAAA,IAG9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAChD;AAEA,QAAM,QAAkC;AAAA,IACtC,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,IAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,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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","import type kuromoji from \"kuromoji\";\nimport type { Tokenizer } from \"./tokenizer\";\n\nconst 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\nconst isSingleKana = (x: string) =>\n x.length === 1 && /^[\\u3040-\\u309F\\u30A0-\\u30FF]$/.test(x);\n\nexport function rewriteParticlesWithKuromoji(\n text: string,\n tokenizer: Tokenizer,\n): string {\n const tokens = tokenizer.tokenize(text);\n let out = \"\";\n\n for (let i = 0; i < tokens.length; i += 1) {\n const token = tokens[i];\n const surf = token.surface_form;\n let replaced = surf;\n\n if (token.pos === \"助詞\" && surf === \"は\") {\n if (i > 0 && tokens[i - 1].surface_form === \"は\") {\n out += surf;\n continue;\n }\n if (i + 1 < tokens.length && tokens[i + 1].surface_form === \"は\") {\n out += surf;\n continue;\n }\n\n const prevIdx = prevContentIdx(tokens, i);\n if (prevIdx >= 0) {\n const nextIdx = nextContentIdx(tokens, i);\n const hasNextContent = nextIdx >= 0;\n const isEndOrPunct = nextBoundaryOrEnd(tokens, i);\n\n const prev = tokens[prevIdx];\n if (\n !prev.surface_form.includes(\"っ\") &&\n token.pos_detail_1 === \"係助詞\" &&\n (hasNextContent || isEndOrPunct) &&\n prev.pos !== \"助詞\"\n ) {\n out += \"わ\";\n continue;\n }\n }\n out += surf;\n continue;\n }\n\n if (token.pos === \"助詞\" && surf === \"へ\") {\n const prevIdx = prevContentIdx(tokens, i);\n if (prevIdx >= 0) {\n const nextIdx = nextContentIdx(tokens, i);\n const hasNextContent = nextIdx >= 0;\n const isEndOrPunct = nextBoundaryOrEnd(tokens, i);\n\n const prevSurf = tokens[prevIdx].surface_form;\n if (LEXICAL_HE_ENDINGS.some((w) => (prevSurf + \"へ\").endsWith(w))) {\n // keep lexical endings\n } else if (prevSurf.endsWith(\"の\")) {\n // \"...のへ\" pattern\n } else if (token.pos_detail_1 === \"格助詞\") {\n const nextPos = hasNextContent ? tokens[nextIdx].pos : \"\";\n const looksDirectionalByVerb =\n nextPos === \"動詞\" || nextPos === \"助動詞\";\n\n const nextSurf = hasNextContent ? tokens[nextIdx].surface_form : \"\";\n const nextIsSingleKana = hasNextContent && isSingleKana(nextSurf);\n\n if (!nextIsSingleKana && (looksDirectionalByVerb || isEndOrPunct)) {\n replaced = \"え\";\n }\n }\n }\n }\n\n out += replaced;\n }\n\n return out;\n}\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}\n\nfunction isContentToken(t: kuromoji.IpadicFeatures): boolean {\n if (t.pos === \"記号\") return !isHardBoundaryToken(t);\n return true;\n}\n\nfunction prevContentIdx(\n tokens: kuromoji.IpadicFeatures[],\n i: number,\n): number {\n for (let j = i - 1; j >= 0; j -= 1) if (isContentToken(tokens[j])) return j;\n return -1;\n}\n\nfunction nextContentIdx(\n tokens: kuromoji.IpadicFeatures[],\n i: number,\n): number {\n for (let j = i + 1; j < tokens.length; j += 1)\n if (isContentToken(tokens[j])) return j;\n return -1;\n}\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","export function coreKanaToHangulConvert(s: string): string {\n // 특별 사전 매핑: 한국인이 익숙한 발음\n const SPECIAL: Array<[string, string]> = [\n [\"とうきょう\", \"도쿄\"],\n [\"いいでしょうか\", \"이데쇼카\"],\n [\"いいでしょう\", \"이데쇼\"],\n [\"こんにちは\", \"콘니치와\"],\n [\"こんばんは\", \"콤방와\"],\n [\"すみません\", \"스미마셍\"],\n ];\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 // --- Tables ---\n type VowelMain = \"a\" | \"i\" | \"u\" | \"e\" | \"o\";\n 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\n type MoraInfo = {\n out: string;\n vowelMain: VowelMain;\n consClass: ConsClass;\n vowelOnly?: boolean;\n wasYouon?: boolean;\n };\n\n 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 // 이거는 う에 탁점 붙인 유니코드임.\n ゔ: { out: \"부\", vowelMain: \"u\", consClass: \"b\" },\n };\n\n 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\n 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\n const SMALL_Y = new Set([\"ゃ\", \"ゅ\", \"ょ\"]);\n const SMALL_V = new Set([\"ぁ\", \"ぃ\", \"ぅ\", \"ぇ\", \"ぉ\"]);\n\n const U_DROP_KEYS = new Set([\n \"ゆ\",\n \"きゅ\",\n \"しゅ\",\n \"ちゅ\",\n \"にゅ\",\n \"ひゅ\",\n \"みゅ\",\n \"りゅ\",\n \"ぎゅ\",\n \"じゅ\",\n \"びゅ\",\n \"ぴゅ\",\n ]);\n\n type ReadMora = { key: string; len: number; info?: MoraInfo } | null;\n\n function readMoraAt(idx: number): ReadMora {\n if (idx >= s.length) return null;\n\n const c0 = s[idx];\n const c1 = s[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 const isBoundary = (ch: string | undefined): boolean => {\n if (!ch) return true;\n return /\\s|[、。!?!?\\(\\)\\[\\]{}「」『』()【】]/.test(ch);\n };\n\n let out = \"\";\n let i = 0;\n\n let lastMora: MoraInfo | null = null;\n let leadingSokuon = false;\n\n while (i < s.length) {\n let matchedSpecial = false;\n for (const [k, v] of SPECIAL) {\n if (s.startsWith(k, i)) {\n // SPECIAL 값도 \"가나\" 형태로 들어와야 테이블이 자연스럽게 이어짐.\n // 여기서는 그대로 한글로 박는 기존 정책 유지.\n out += v\n i += k.length;\n lastMora = null;\n matchedSpecial = true;\n break;\n }\n }\n if (matchedSpecial) continue;\n\n if (s.startsWith(\"ちゃん\", i)) {\n out += \"쨩\";\n i += 3;\n lastMora = { out: \"쨩\", vowelMain: \"a\", consClass: \"t\", wasYouon: true };\n continue;\n }\n\n const ch = s[i];\n\n if (ch === \"ー\") {\n i += 1;\n continue;\n }\n\n if (leadingSokuon) {\n if (isHiragana(ch)) {\n out += hiraToKata(ch);\n i += 1;\n leadingSokuon = false;\n lastMora = null;\n continue;\n } else {\n leadingSokuon = false;\n }\n }\n\n if (ch === \"っ\") {\n if (!out || !isHangulSyllable(out[out.length - 1])) {\n out += \"ッ\";\n i += 1;\n leadingSokuon = true;\n lastMora = null;\n continue;\n }\n\n const next = readMoraAt(i + 1);\n if (lastMora && lastMora.out === \"지\" && next && next.key === \"く\") {\n i += 1;\n continue;\n }\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 if (!isKana(ch)) {\n out += ch;\n i += 1;\n lastMora = null;\n continue;\n }\n\n if (ch === \"お\" && s[i + 1] === \"お\") {\n let j = i;\n while (s[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 if (!mora) {\n out += ch;\n i += 1;\n lastMora = null;\n continue;\n }\n\n if (mora.key === \"ん\") {\n const next = readMoraAt(i + 1);\n const nextInfo = next?.info;\n\n let jong: number = JONG.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 // ✅ \"さん\"(호칭)일 때만 '상'(받침 ㅇ)\n // 주의: 조사 리라이트가 먼저라서 다음 글자가 'は'가 아니라 'わ'일 수 있음!\n if (lastMora?.out === \"사\") {\n const nextCh = s[i + 1];\n\n const isBoundaryOrEnd =\n !nextCh || /\\s|[、。!?!?\\(\\)\\[\\]{}「」『』()【】]/.test(nextCh);\n\n // 원문 조사 + 리라이트된 조사까지 모두 허용\n const isParticleAfterSan =\n nextCh === \"は\" ||\n nextCh === \"へ\" ||\n nextCh === \"を\" ||\n nextCh === \"わ\" ||\n nextCh === \"え\" ||\n nextCh === \"お\";\n\n // ✅ 핵심: \"사\" 앞에 뭔가가 있어야(-san) 인정.\n // out는 지금 \"...사\" 까지 찍힌 상태.\n // \"さんは\"는 out === \"사\"라서 여기서 걸러져야 함.\n const hasPrefixBeforeSan = out.length >= 2;\n\n // 숫자/로마자 앞도 허용해야 \"3さん\" => 3상 유지됨\n if (hasPrefixBeforeSan && (isBoundaryOrEnd || isParticleAfterSan)) {\n out = replaceLastHangul(out, JONG.NG); // 사 + ん => 상\n i += 1;\n continue;\n }\n }\n\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 const info = mora.info;\n if (!info) {\n out += mora.key;\n i += mora.len;\n lastMora = null;\n continue;\n }\n\n out += info.out;\n lastMora = info;\n\n const next1 = s[i + mora.len];\n const afterLen = s[i + mora.len + 1];\n\n if (next1 === \"う\" && info.vowelMain === \"o\") {\n i += mora.len + 1;\n continue;\n }\n\n if (next1 === \"う\" && (mora.key === \"ゆ\" || U_DROP_KEYS.has(mora.key))) {\n i += mora.len + 1;\n continue;\n }\n\n if (next1 === \"い\") {\n if (mora.key === \"せ\") {\n if (afterLen !== \"な\" && afterLen !== \"か\") {\n i += mora.len + 1;\n continue;\n }\n } else if (mora.key === \"け\") {\n if (afterLen !== \"と\") {\n i += mora.len + 1;\n continue;\n }\n } else if (mora.key === \"え\") {\n if (afterLen !== \"こ\" && afterLen !== \"く\" && afterLen !== \"き\") {\n i += mora.len + 1;\n continue;\n }\n } else if (mora.key === \"じ\") {\n i += mora.len + 1;\n continue;\n } else if (mora.key === \"き\") {\n // \"おおきい\" 줄임 정책 유지: 뒤에 이어지면 keep\n if (isBoundary(afterLen) || !afterLen) {\n i += mora.len + 1;\n continue;\n }\n } else if (mora.key === \"し\") {\n // しい adjectives pronounce as '시'\n i += mora.len + 1;\n continue;\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\nfunction hiraToKata(hira: string): string {\n const c = hira.codePointAt(0)!;\n if (c >= 0x3041 && c <= 0x3096) {\n return String.fromCodePoint(c + 0x60);\n }\n return hira;\n}\n","// lib/kanaToHangul.ts\n// 전체 변환 파이프라인을 orchestration만 담당하도록 정리했습니다.\nimport type { Tokenizer } from \"./tokenizer\";\nimport { normalizeInputText, toHiragana } from \"./normalizer\";\nimport { rewriteParticlesWithKuromoji } 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\nfunction convertWithTokenizer(input: string, tokenizer: Tokenizer): string {\n const normalized = normalizeInputText(input);\n const hiragana = toHiragana(normalized);\n const rewritten = rewriteParticlesWithKuromoji(hiragana, tokenizer);\n return coreKanaToHangulConvert(rewritten);\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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AI;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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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];\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}\nfunction toHiragana(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) => {\n if (!isKatakanaChar(ch)) return ch;\n return String.fromCodePoint(ch.codePointAt(0)! - 0x60);\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[] } {\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 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 const hiraSurf = hiraChars.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 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\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 };\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 rawTokens: kuromoji.IpadicFeatures[];\n} {\n const rawTokens = tokenizer.tokenize(originalText);\n // console.log(rawTokens)\n const { rewritten, spans } = rewriteParticlesFromTokenization(\n originalText,\n hiraganaText,\n rawTokens,\n );\n return { rewritten, spans, 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, SpecialDictionaryEntry } from \"./dictionary\";\nimport { HARD_BOUNDARY_SURF, TokenSpan } from \"./particleRewriter\";\nimport {\n ConsClass,\n 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 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 if (chars.slice(i, i + 3).join(\"\") === \"ちゃん\") {\n out += \"쨩\";\n i += 3;\n lastMora = { out: \"쨩\", vowelMain: \"a\", consClass: \"t\", wasYouon: true };\n continue;\n }\n\n const ch = chars[i];\n\n if (ch === \"ー\") {\n i += 1;\n continue;\n }\n\n if (ch === \"っ\") {\n if (!out || !isHangulSyllable(out[out.length - 1])) {\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 if (!isKana(ch)) {\n out += ch;\n i += 1;\n lastMora = null;\n continue;\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 if (!mora) {\n out += chars[i];\n i += 1;\n lastMora = null;\n continue;\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 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 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 if (next1 === \"う\" && (mora.key === \"ゆ\" || U_DROP_KEYS.has(mora.key))) {\n i += mora.len + 1;\n continue;\n }\n\n if (next1 === \"い\") {\n if (mora.key === \"せ\") {\n if (afterLen !== \"な\" && afterLen !== \"か\") {\n i += mora.len + 1;\n continue;\n }\n } else if (mora.key === \"け\") {\n if (afterLen !== \"と\") {\n i += mora.len + 1;\n continue;\n }\n } else if (mora.key === \"え\") {\n if (afterLen !== \"こ\" && afterLen !== \"く\" && afterLen !== \"き\") {\n i += mora.len + 1;\n continue;\n }\n } else if (mora.key === \"じ\") {\n i += mora.len + 1;\n continue;\n } else if (mora.key === \"き\") {\n if (!afterLen) {\n i += mora.len + 1;\n continue;\n }\n } else if (mora.key === \"し\") {\n i += mora.len + 1;\n continue;\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\nfunction hiraToKata(hira: string): string {\n const c = hira.codePointAt(0)!;\n if (c >= 0x3041 && c <= 0x3096) {\n return String.fromCodePoint(c + 0x60);\n }\n return hira;\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 } = tokenizeAndRewriteParticles(\n normalized,\n hiragana,\n tokenizer,\n );\n\n // ✅ rewritten + rewritten spans 로 core\n return coreKanaToHangulConvert(rewritten, {\n tokens: spans,\n original: normalized, // ✅ 이게 핵심\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kanabarum",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "일본어 가나를 한국어 발음으로 바꿔주는 변환기",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -36,12 +36,12 @@
36
36
  "kuromoji": "^0.1.2"
37
37
  },
38
38
  "devDependencies": {
39
- "@biomejs/biome": "^1.5.0",
39
+ "@biomejs/biome": "2.0.6",
40
40
  "@types/kuromoji": "^0.1.3",
41
41
  "@types/node": "^20.11.5",
42
42
  "tsup": "^7.2.0",
43
- "vitest": "^1.2.2",
44
- "typescript": "^5.3.3"
43
+ "typescript": "^5.3.3",
44
+ "vitest": "^1.2.2"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsup --config tsup.config.ts",