kanabarum 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -43,7 +43,8 @@ var SpecialDictionary = [
43
43
  { word: "\u304B\u308F\u3044\u3044", answer: "\uCE74\uC640\uC774" },
44
44
  { word: "\u3064\u306A\u307F", answer: "\uC4F0\uB098\uBBF8" },
45
45
  { word: "\u3086\u3046\u308A", answer: "\uC720\uC6B0\uB9AC" },
46
- { word: "\u30DF\u30E5\u30FC\u30B8\u30C3\u30AF", answer: "\uBBA4\uC9C0\uCFE0" }
46
+ { word: "\u30DF\u30E5\u30FC\u30B8\u30C3\u30AF", answer: "\uBBA4\uC9C0\uCFE0" },
47
+ { word: "\u3061\u3083\u3093", answer: "\uCA29" }
47
48
  ];
48
49
 
49
50
  // src/particleRewriter.ts
@@ -51,12 +52,27 @@ function isKatakanaChar(ch) {
51
52
  const c = ch.codePointAt(0);
52
53
  return c >= 12448 && c <= 12543;
53
54
  }
55
+ function containsKanji(s) {
56
+ for (const ch of s) {
57
+ const c = ch.codePointAt(0);
58
+ if (c >= 19968 && c <= 40959 || c >= 13312 && c <= 19903) {
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ }
54
64
  function toHiragana2(s) {
55
65
  const n = s.normalize("NFKC");
56
66
  return Array.from(n).map((ch) => {
67
+ if (ch === "\u30FC")
68
+ return "";
57
69
  if (!isKatakanaChar(ch))
58
70
  return ch;
59
- return String.fromCodePoint(ch.codePointAt(0) - 96);
71
+ const code = ch.codePointAt(0);
72
+ if (code >= 12449 && code <= 12534) {
73
+ return String.fromCodePoint(code - 96);
74
+ }
75
+ return ch;
60
76
  }).join("");
61
77
  }
62
78
  function dictKeysForHiraganaText(dict) {
@@ -166,6 +182,7 @@ function rewriteParticlesFromTokenization(originalText, hiraganaText, tokenizerT
166
182
  dictKeysForHiraganaText(SpecialDictionary)
167
183
  );
168
184
  let out = "";
185
+ let origOut = "";
169
186
  const spans = [];
170
187
  let cursorInText = 0;
171
188
  for (let i = 0; i < tokenizerTokens.length; i += 1) {
@@ -176,12 +193,15 @@ function rewriteParticlesFromTokenization(originalText, hiraganaText, tokenizerT
176
193
  const start = typeof wp === "number" ? wp - 1 : cursorInText;
177
194
  const end = start + surfCpLen;
178
195
  cursorInText = end;
179
- const hiraSurf = hiraChars.slice(start, end).join("");
196
+ const hasKanji = containsKanji(surf) && tok.pronunciation;
197
+ const hiraSurf = hasKanji ? toHiragana2(tok.pronunciation) : hiraChars.slice(start, end).join("");
198
+ const origSurfForOut = hasKanji ? tok.pronunciation.replace(/ー/g, "") : originChars.slice(start, end).join("");
180
199
  const originSurf = originChars.slice(start, end).join("");
181
200
  const originHadKatakana = /[\u30A0-\u30FF]/.test(originSurf);
182
201
  if (isProtectedSpan(protectedRanges, start, end)) {
183
202
  const outCpStart2 = [...out].length;
184
203
  out += hiraSurf;
204
+ origOut += origSurfForOut;
185
205
  spans.push({
186
206
  start: outCpStart2,
187
207
  end: [...out].length,
@@ -233,6 +253,7 @@ function rewriteParticlesFromTokenization(originalText, hiraganaText, tokenizerT
233
253
  }
234
254
  const outCpStart = [...out].length;
235
255
  out += replaced;
256
+ origOut += origSurfForOut;
236
257
  spans.push({
237
258
  start: outCpStart,
238
259
  end: [...out].length,
@@ -244,16 +265,12 @@ function rewriteParticlesFromTokenization(originalText, hiraganaText, tokenizerT
244
265
  originHadKatakana
245
266
  });
246
267
  }
247
- return { rewritten: out, spans };
268
+ return { rewritten: out, spans, rewrittenOriginal: origOut };
248
269
  }
249
270
  function tokenizeAndRewriteParticles(originalText, hiraganaText, tokenizer) {
250
271
  const rawTokens = tokenizer.tokenize(originalText);
251
- const { rewritten, spans } = rewriteParticlesFromTokenization(
252
- originalText,
253
- hiraganaText,
254
- rawTokens
255
- );
256
- return { rewritten, spans, rawTokens };
272
+ const { rewritten, spans, rewrittenOriginal } = rewriteParticlesFromTokenization(originalText, hiraganaText, rawTokens);
273
+ return { rewritten, spans, rewrittenOriginal, rawTokens };
257
274
  }
258
275
 
259
276
  // src/mora.ts
@@ -531,6 +548,28 @@ function coreKanaToHangulConvert(s, opts) {
531
548
  return { key: c0, len: 1, info };
532
549
  return { key: c0, len: 1, info: void 0 };
533
550
  }
551
+ function readOriginalMoraAt(idx) {
552
+ if (idx >= origChars.length)
553
+ return null;
554
+ const c0 = origChars[idx];
555
+ const c1 = origChars[idx + 1];
556
+ if (c1 && SMALL_V.has(c1)) {
557
+ const key2 = c0 + c1;
558
+ const info2 = LOAN[key2];
559
+ if (info2)
560
+ return { key: key2, len: 2, info: info2 };
561
+ }
562
+ if (c1 && SMALL_Y.has(c1)) {
563
+ const key2 = c0 + c1;
564
+ const info2 = YOUON[key2];
565
+ if (info2)
566
+ return { key: key2, len: 2, info: info2 };
567
+ }
568
+ const info = SINGLE[c0];
569
+ if (info)
570
+ return { key: c0, len: 1, info };
571
+ return { key: c0, len: 1, info: void 0 };
572
+ }
534
573
  function isLabialStart(cons) {
535
574
  return cons === "m" || cons === "b" || cons === "p";
536
575
  }
@@ -631,17 +670,17 @@ function coreKanaToHangulConvert(s, opts) {
631
670
  }
632
671
  if (matchedSpecial)
633
672
  continue;
634
- if (chars.slice(i, i + 3).join("") === "\u3061\u3083\u3093") {
635
- out += "\uCA29";
636
- i += 3;
637
- lastMora = { out: "\uCA29", vowelMain: "a", consClass: "t", wasYouon: true };
638
- continue;
639
- }
640
673
  const ch = chars[i];
641
674
  if (ch === "\u30FC") {
642
675
  i += 1;
643
676
  continue;
644
677
  }
678
+ if (!isKana(ch)) {
679
+ out += ch;
680
+ i += 1;
681
+ lastMora = null;
682
+ continue;
683
+ }
645
684
  if (ch === "\u3063") {
646
685
  if (!out || !isHangulSyllable(out[out.length - 1])) {
647
686
  out += "\u30C3";
@@ -665,12 +704,6 @@ function coreKanaToHangulConvert(s, opts) {
665
704
  i += 1;
666
705
  continue;
667
706
  }
668
- if (!isKana(ch)) {
669
- out += ch;
670
- i += 1;
671
- lastMora = null;
672
- continue;
673
- }
674
707
  if (ch === "\u304A" && chars[i + 1] === "\u304A") {
675
708
  let j = i;
676
709
  while (chars[j] === "\u304A")
@@ -686,12 +719,16 @@ function coreKanaToHangulConvert(s, opts) {
686
719
  continue;
687
720
  }
688
721
  const mora = readMoraAt(i);
722
+ const originalMora = readOriginalMoraAt(i);
689
723
  if (!mora) {
690
724
  out += chars[i];
691
725
  i += 1;
692
726
  lastMora = null;
693
727
  continue;
694
728
  }
729
+ if (!originalMora) {
730
+ throw Error("\uC6D0\uBCF8 \uBAA8\uB77C \uC190\uC2E4");
731
+ }
695
732
  if (mora.key === "\u3093") {
696
733
  const next = readMoraAt(i + 1);
697
734
  const nextInfo = next?.info;
@@ -764,36 +801,24 @@ function coreKanaToHangulConvert(s, opts) {
764
801
  out += outSyl;
765
802
  lastMora = { ...info, out: outSyl };
766
803
  const next1 = chars[i + mora.len];
767
- const afterLen = chars[i + mora.len + 1];
804
+ chars[i + mora.len + 1];
768
805
  if (next1 === "\u3046" && info.vowelMain === "o") {
769
806
  i += mora.len + 1;
770
807
  continue;
771
808
  }
772
- if (next1 === "\u3046" && (mora.key === "\u3086" || U_DROP_KEYS.has(mora.key))) {
809
+ if (next1 === "\u3046" && U_DROP_KEYS.has(mora.key)) {
773
810
  i += mora.len + 1;
774
811
  continue;
775
812
  }
776
813
  if (next1 === "\u3044") {
777
814
  if (mora.key === "\u305B") {
778
- if (afterLen !== "\u306A" && afterLen !== "\u304B") {
779
- i += mora.len + 1;
780
- continue;
781
- }
815
+ i += mora.len + 1;
816
+ continue;
782
817
  } else if (mora.key === "\u3051") {
783
- if (afterLen !== "\u3068") {
784
- i += mora.len + 1;
785
- continue;
786
- }
787
- } else if (mora.key === "\u3048") {
788
- if (afterLen !== "\u3053" && afterLen !== "\u304F" && afterLen !== "\u304D") {
789
- i += mora.len + 1;
790
- continue;
791
- }
792
- } else if (mora.key === "\u3058") {
793
818
  i += mora.len + 1;
794
819
  continue;
795
- } else if (mora.key === "\u304D") {
796
- if (!afterLen) {
820
+ } else if (mora.key === "\u3048") {
821
+ if (originalMora.key !== "\u3078") {
797
822
  i += mora.len + 1;
798
823
  continue;
799
824
  }
@@ -818,15 +843,14 @@ function createKanaToHangul(tokenizer) {
818
843
  function convertWithTokenizer(input, tokenizer) {
819
844
  const normalized = normalizeInputText(input);
820
845
  const hiragana = toHiragana(normalized);
821
- const { rewritten, spans } = tokenizeAndRewriteParticles(
846
+ const { rewritten, spans, rewrittenOriginal } = tokenizeAndRewriteParticles(
822
847
  normalized,
823
848
  hiragana,
824
849
  tokenizer
825
850
  );
826
851
  return coreKanaToHangulConvert(rewritten, {
827
852
  tokens: spans,
828
- original: normalized
829
- // ✅ 이게 핵심
853
+ original: rewrittenOriginal
830
854
  });
831
855
  }
832
856
  var require2 = createRequire(import.meta.url);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/normalizer.ts","../src/dictionary.ts","../src/particleRewriter.ts","../src/mora.ts","../src/coreConverter.ts","../src/kanaToHangul.ts","../src/tokenizer.ts","../src/kanaBarum.ts"],"names":["toHiragana","outCpStart","isKatakanaChar","out","info","require"],"mappings":"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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,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AACjE;AAEO,IAAM,OAAiC;AAAA,EAC5C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC/C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AACjD;AAEO,IAAM,UAAU,oBAAI,IAAI,CAAC,UAAK,UAAK,QAAG,CAAC;AACvC,IAAM,UAAU,oBAAI,IAAI,CAAC,UAAK,UAAK,UAAK,UAAK,QAAG,CAAC;AAEjD,IAAM,cAAc,oBAAI,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;;;ACrMD,SAAS,eAAe,IAAY;AAClC,QAAM,IAAI,GAAG,YAAY,CAAC;AAC1B,SAAO,KAAK,SAAU,KAAK;AAC7B;AACA,SAASC,gBAAe,IAAY;AAClC,QAAM,IAAI,GAAG,YAAY,CAAC;AAC1B,SAAO,KAAK,SAAU,KAAK;AAC7B;AACA,SAAS,cAAc,GAAmB;AACxC,QAAM,IAAI,EAAE,UAAU,MAAM;AAC5B,SAAO,MAAM,KAAK,CAAC,EAChB;AAAA,IAAI,CAAC,OACJA,gBAAe,EAAE,IAAI,OAAO,cAAc,GAAG,YAAY,CAAC,IAAK,EAAI,IAAI;AAAA,EACzE,EACC,KAAK,EAAE;AACZ;AACA,SAAS,cAAc,GAAmB;AACxC,QAAM,IAAI,EAAE,UAAU,MAAM;AAC5B,SAAO,MAAM,KAAK,CAAC,EAChB;AAAA,IAAI,CAAC,OACJ,eAAe,EAAE,IAAI,OAAO,cAAc,GAAG,YAAY,CAAC,IAAK,EAAI,IAAI;AAAA,EACzE,EACC,KAAK,EAAE;AACZ;AASA,SAAS,yBACP,MACoB;AACpB,QAAM,QAA4B,CAAC;AAEnC,aAAW,KAAK,MAAM;AAEpB,UAAM,KAAK;AAAA,MACT,UAAU,MAAM,KAAK,EAAE,IAAI;AAAA,MAC3B,QAAQ,EAAE;AAAA,MACV,QAAQ;AAAA,IACV,CAAC;AAGD,QAAI,EAAE,MAAM;AACV,YAAM,IAAI,cAAc,EAAE,IAAI;AAC9B,YAAM,KAAK;AAAA,QACT,UAAU,MAAM,KAAK,CAAC;AAAA,QACtB,QAAQ,EAAE;AAAA,QACV,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAGA,QAAI,EAAE,MAAM;AACV,YAAM,IAAI,cAAc,EAAE,IAAI;AAC9B,YAAM,KAAK,EAAE,UAAU,MAAM,KAAK,CAAC,GAAG,QAAQ,EAAE,QAAQ,QAAQ,OAAO,CAAC;AAAA,IAC1E;AAAA,EACF;AAGA,QAAM,KAAK,CAAC,GAAG,MAAM,EAAE,SAAS,SAAS,EAAE,SAAS,MAAM;AAC1D,SAAO;AACT;AAEA,IAAM,wBAAwB,yBAAyB,iBAAiB;AAWjE,SAAS,wBACd,GACA,MACQ;AAER,QAAM,QAAQ,MAAM,KAAK,CAAC;AAC1B,QAAM,YAAY,MAAM,KAAK,MAAM,YAAY,CAAC;AAGhD,QAAM,cAAc;AACpB,QAAM,aAAa;AAEnB,WAAS,iBAAiB,IAAqB;AAC7C,UAAM,IAAI,GAAG,YAAY,CAAC;AAC1B,WAAO,KAAK,eAAe,KAAK;AAAA,EAClC;AAEA,QAAM,OAAO;AAAA,IACX,MAAM;AAAA,IACN,GAAG;AAAA;AAAA,IACH,GAAG;AAAA;AAAA,IACH,GAAG;AAAA;AAAA,IACH,GAAG;AAAA;AAAA,IACH,GAAG;AAAA;AAAA,IACH,IAAI;AAAA;AAAA,EACN;AAEA,WAAS,SAAS,KAAa,MAAsB;AACnD,QAAI,CAAC,iBAAiB,GAAG;AAAG,aAAO;AACnC,UAAM,OAAO,IAAI,YAAY,CAAC,IAAK;AACnC,UAAM,MAAM,KAAK,MAAM,OAAO,GAAG;AACjC,UAAM,OAAO,KAAK,MAAO,OAAO,MAAO,EAAE;AACzC,WAAO,OAAO,cAAc,cAAc,MAAM,MAAM,OAAO,KAAK,IAAI;AAAA,EACxE;AAEA,WAAS,kBAAkBC,MAAa,MAAsB;AAC5D,QAAI,CAACA;AAAK,aAAOA;AACjB,UAAM,OAAOA,KAAIA,KAAI,SAAS,CAAC;AAC/B,QAAI,CAAC,iBAAiB,IAAI;AAAG,aAAOA;AACpC,WAAOA,KAAI,MAAM,GAAG,EAAE,IAAI,SAAS,MAAM,IAAI;AAAA,EAC/C;AAGA,WAAS,WAAW,IAAqB;AACvC,UAAM,IAAI,GAAG,YAAY,CAAC;AAC1B,WAAO,KAAK,SAAU,KAAK;AAAA,EAC7B;AACA,WAAS,OAAO,IAAqB;AACnC,WAAO,WAAW,EAAE,KAAK,OAAO;AAAA,EAClC;AAIA,WAAS,WAAW,KAAuB;AACzC,QAAI,OAAO,MAAM;AAAQ,aAAO;AAEhC,UAAM,KAAK,MAAM,GAAG;AACpB,UAAM,KAAK,MAAM,MAAM,CAAC;AAExB,QAAI,MAAM,QAAQ,IAAI,EAAE,GAAG;AACzB,YAAM,OAAO,KAAK;AAClB,YAAMC,QAAO,KAAK,IAAI;AACtB,UAAIA;AAAM,eAAO,EAAE,KAAK,MAAM,KAAK,GAAG,MAAAA,MAAK;AAAA,IAC7C;AAEA,QAAI,MAAM,QAAQ,IAAI,EAAE,GAAG;AACzB,YAAM,OAAO,KAAK;AAClB,YAAMA,QAAO,MAAM,IAAI;AACvB,UAAIA;AAAM,eAAO,EAAE,KAAK,MAAM,KAAK,GAAG,MAAAA,MAAK;AAAA,IAC7C;AAEA,UAAM,OAAO,OAAO,EAAE;AACtB,QAAI;AAAM,aAAO,EAAE,KAAK,IAAI,KAAK,GAAG,KAAK;AAEzC,WAAO,EAAE,KAAK,IAAI,KAAK,GAAG,MAAM,OAAU;AAAA,EAC5C;AAEA,WAAS,cAAc,MAA0B;AAC/C,WAAO,SAAS,OAAO,SAAS,OAAO,SAAS;AAAA,EAClD;AAGA,QAAM,SAAS,MAAM,UAAU;AAC/B,MAAI,SAAS;AAEb,WAAS,eAAe,WAAmB;AACzC,QAAI,CAAC;AAAQ;AACb,WAAO,SAAS,IAAI,OAAO,UAAU,OAAO,MAAM,EAAE,OAAO,WAAW;AACpE;AAAA,IACF;AAAA,EACF;AAEA,WAAS,SAAS,WAAqC;AACrD,QAAI,CAAC;AAAQ,aAAO;AACpB,mBAAe,SAAS;AACxB,UAAM,IAAI,OAAO,MAAM;AACvB,QAAI,KAAK,EAAE,SAAS,aAAa,YAAY,EAAE;AAAK,aAAO;AAC3D,WAAO;AAAA,EACT;AAEA,WAAS,YAA8B;AACrC,QAAI,CAAC;AAAQ,aAAO;AACpB,WAAO,SAAS,KAAK,IAAI,OAAO,SAAS,CAAC,IAAI;AAAA,EAChD;AACA,WAAS,YAA8B;AACrC,QAAI,CAAC;AAAQ,aAAO;AACpB,WAAO,SAAS,IAAI,OAAO,SAAS,OAAO,SAAS,CAAC,IAAI;AAAA,EAC3D;AAGA,QAAM,6BAA6B,oBAAI,IAAI,CAAC,UAAK,UAAK,UAAK,QAAG,CAAC;AAE/D,WAAS,+BAA+B,SAAgC;AACtE,QAAI,IAAI;AACR,WAAO,IAAI,MAAM,UAAU,MAAM,CAAC,MAAM;AAAK;AAC7C,UAAM,IAAI,WAAW,CAAC;AACtB,WAAO,GAAG,OAAO;AAAA,EACnB;AAGA,QAAM,gBAAgB,oBAAI,IAAI,CAAC,UAAK,UAAK,UAAK,UAAK,UAAK,QAAG,CAAC;AAC5D,WAAS,iBAAiB,OAAwB;AAChD,UAAM,IAAI,SAAS,KAAK;AACxB,QAAI,CAAC;AAAG,aAAO;AACf,QAAI,QAAQ,KAAK,MAAM,QAAQ,CAAC,MAAM;AAAK,aAAO;AAGlD,UAAM,QAAQ,MAAM,MAAM,EAAE,OAAO,QAAQ,CAAC,EAAE,KAAK,EAAE;AACrD,QAAI,CAAC,MAAM,SAAS,cAAI;AAAG,aAAO;AAElC,UAAM,uBAAuB,QAAQ,IAAI,EAAE;AAC3C,UAAM,IAAI,UAAU;AACpB,UAAM,mBACJ,CAAC,CAAC,KACF,EAAE,QAAQ,EAAE,SACZ,EAAE,QAAQ,SAAS,KACnB,CAAC,mBAAmB,IAAI,EAAE,OAAO;AAEnC,QAAI,CAAC,wBAAwB,CAAC;AAAkB,aAAO;AAEvD,UAAM,IAAI,UAAU;AACpB,QAAI,CAAC;AAAG,aAAO;AACf,QAAI,EAAE,QAAQ,kBAAQ,mBAAmB,IAAI,EAAE,OAAO;AAAG,aAAO;AAChE,QAAI,EAAE,QAAQ,kBAAQ,cAAc,IAAI,EAAE,OAAO;AAAG,aAAO;AAC3D,WAAO;AAAA,EACT;AAEA,MAAI,MAAM;AACV,MAAI,IAAI;AAER,MAAI,WAA4B;AAEhC,SAAO,IAAI,MAAM,QAAQ;AAEvB,QAAI,eAAe;AACnB,QAAI,UAA4B;AAEhC,QAAI,QAAQ;AACV,gBAAU,SAAS,CAAC;AAEpB,qBAAe,CAAC,CAAC,WAAW,QAAQ,UAAU;AAG9C,UAAI,SAAS,QAAQ;AAAM,uBAAe;AAAA,IAC5C,OAAO;AACL,qBAAe,MAAM;AAAA,IACvB;AAKA,QAAI,iBAAiB;AAGrB,eAAW,MAAM,uBAAuB;AACtC,YAAM,MAAM,GAAG,WAAW,SAAS,YAAY;AAC/C,YAAM,MAAM,GAAG,SAAS;AACxB,UAAI,IAAI,MAAM,IAAI;AAAQ;AAE1B,UAAI,KAAK;AACT,eAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,YAAI,IAAI,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,GAAG;AACjC,eAAK;AACL;AAAA,QACF;AAAA,MACF;AACA,UAAI,CAAC;AAAI;AAET,aAAO,GAAG;AACV,WAAK;AAGL,iBAAW;AAEX,uBAAiB;AACjB;AAAA,IACF;AACA,QAAI;AAAgB;AAEpB,QAAI,MAAM,MAAM,GAAG,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,sBAAO;AAC5C,aAAO;AACP,WAAK;AACL,iBAAW,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AACtE;AAAA,IACF;AAEA,UAAM,KAAK,MAAM,CAAC;AAElB,QAAI,OAAO,UAAK;AACd,WAAK;AACL;AAAA,IACF;AAEA,QAAI,OAAO,UAAK;AACd,UAAI,CAAC,OAAO,CAAC,iBAAiB,IAAI,IAAI,SAAS,CAAC,CAAC,GAAG;AAClD,eAAO;AACP,aAAK;AACL,mBAAW;AACX;AAAA,MACF;AAEA,YAAM,OAAO,WAAW,IAAI,CAAC;AAE7B,YAAM,QAAQ,UAAU,aAAa;AACrC,YAAM,WAAW,MAAM;AACvB,YAAM,WAAsB,UAAU,aAAa;AAEnD,UAAI,OAAe,KAAK;AACxB,UAAI,aAAa,OAAO,aAAa;AAAK,eAAO,KAAK;AAAA,eAC7C,aAAa,OAAO,aAAa,KAAK;AAC7C,eAAO,UAAU,OAAO,UAAU,MAAM,KAAK,IAAI,KAAK;AAAA,MACxD,OAAO;AACL,eAAO,KAAK;AAAA,MACd;AAEA,YAAM,kBAAkB,KAAK,IAAI;AACjC,WAAK;AACL;AAAA,IACF;AAGA,QAAI,CAAC,OAAO,EAAE,GAAG;AACf,aAAO;AACP,WAAK;AACL,iBAAW;AACX;AAAA,IACF;AAGA,QAAI,OAAO,YAAO,MAAM,IAAI,CAAC,MAAM,UAAK;AACtC,UAAI,IAAI;AACR,aAAO,MAAM,CAAC,MAAM;AAAK;AACzB,aAAO;AACP,UAAI;AACJ,iBAAW;AAAA,QACT,KAAK;AAAA,QACL,WAAW;AAAA,QACX,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AACA;AAAA,IACF;AAEA,UAAM,OAAO,WAAW,CAAC;AACzB,QAAI,CAAC,MAAM;AACT,aAAO,MAAM,CAAC;AACd,WAAK;AACL,iBAAW;AACX;AAAA,IACF;AAGA,QAAI,KAAK,QAAQ,UAAK;AACpB,YAAM,OAAO,WAAW,IAAI,CAAC;AAC7B,YAAM,WAAW,MAAM;AAEvB,YAAM,gBACJ,IAAI,SAAS,KAAK,iBAAiB,IAAI,IAAI,SAAS,CAAC,CAAC;AACxD,UAAI,CAAC,eAAe;AAClB,eAAO;AACP,aAAK;AACL,mBAAW;AACX;AAAA,MACF;AAGA,UAAI,UAAU,QAAQ,YAAO,iBAAiB,CAAC,GAAG;AAChD,cAAM,kBAAkB,KAAK,KAAK,EAAE;AACpC,aAAK;AACL;AAAA,MACF;AAGA,UAAI,OAAe,KAAK;AACxB,UAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,GAAG;AAC9C,eAAO,UAAU,WAAW,KAAK,KAAK,KAAK;AAAA,MAC7C,OAAO;AACL,cAAM,KAAK,SAAS;AACpB,YAAI,OAAO,OAAO,OAAO,KAAK;AAC5B,iBAAO,KAAK;AAAA,QACd,WAAW,OAAO,WAAW,OAAO,OAAO,OAAO,KAAK;AACrD,iBAAO,KAAK;AAAA,QACd,WAAW,cAAc,EAAE,GAAG;AAC5B,cAAI,UAAU;AAAW,mBAAO,KAAK;AAAA;AAChC,mBAAO,KAAK;AAAA,QACnB,OAAO;AACL,iBAAO,KAAK;AAAA,QACd;AAAA,MACF;AAEA,YAAM,kBAAkB,KAAK,IAAI;AACjC,WAAK;AACL;AAAA,IACF;AAEA,UAAM,OAAO,KAAK;AAClB,QAAI,CAAC,MAAM;AACT,aAAO,KAAK;AACZ,WAAK,KAAK;AACV,iBAAW;AACX;AAAA,IACF;AAGA,QAAI,SAAS,KAAK;AAClB,QAAI,iBAAiB,KAAK,QAAQ,YAAO,KAAK,QAAQ,WAAM;AAC1D,YAAM,eAAe,IAAI,KAAK,MAAM,IAAI,CAAC,MAAM;AAE/C,YAAM,UAAU,+BAA+B,IAAI,KAAK,GAAG;AAC3D,YAAM,gBACJ,CAAC,CAAC,WAAW,2BAA2B,IAAI,OAAO;AAErD,YAAM,QAAQ,KAAK,QAAQ,YAAO,MAAM,IAAI,KAAK,GAAG,MAAM;AAK1D,UAAI,yBAAyB;AAC7B,UAAI,UAAU,SAAS;AACrB,cAAM,oBAAoB,QAAQ,QAAQ,WAAW;AACrD,cAAM,mBAAmB,QAAQ,YAAY,KAAK;AAElD,YAAI,qBAAqB,kBAAkB;AACzC,gBAAM,IAAI,UAAU;AACpB,gBAAM,uBACJ,CAAC,CAAC,KACF,EAAE,QAAQ,kBACV,EAAE,QAAQ,kBACV,EAAE,QAAQ,wBACV,CAAC,mBAAmB,IAAI,EAAE,OAAO;AAEnC,cAAI;AAAsB,qCAAyB;AAAA,QACrD;AAAA,MACF;AAEA,YAAM,eACJ,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,SAAS,CAAC;AAEhD,UAAI,cAAc;AAChB,YAAI,KAAK,QAAQ;AAAK,mBAAS;AAAA,iBACtB,KAAK,QAAQ;AAAK,mBAAS;AAAA,MACtC;AAAA,IACF;AAEA,WAAO;AACP,eAAW,EAAE,GAAG,MAAM,KAAK,OAAO;AAElC,UAAM,QAAQ,MAAM,IAAI,KAAK,GAAG;AAChC,UAAM,WAAW,MAAM,IAAI,KAAK,MAAM,CAAC;AAGvC,QAAI,UAAU,YAAO,KAAK,cAAc,KAAK;AAC3C,WAAK,KAAK,MAAM;AAChB;AAAA,IACF;AAEA,QAAI,UAAU,aAAQ,KAAK,QAAQ,YAAO,YAAY,IAAI,KAAK,GAAG,IAAI;AACpE,WAAK,KAAK,MAAM;AAChB;AAAA,IACF;AAEA,QAAI,UAAU,UAAK;AACjB,UAAI,KAAK,QAAQ,UAAK;AACpB,YAAI,aAAa,YAAO,aAAa,UAAK;AACxC,eAAK,KAAK,MAAM;AAChB;AAAA,QACF;AAAA,MACF,WAAW,KAAK,QAAQ,UAAK;AAC3B,YAAI,aAAa,UAAK;AACpB,eAAK,KAAK,MAAM;AAChB;AAAA,QACF;AAAA,MACF,WAAW,KAAK,QAAQ,UAAK;AAC3B,YAAI,aAAa,YAAO,aAAa,YAAO,aAAa,UAAK;AAC5D,eAAK,KAAK,MAAM;AAChB;AAAA,QACF;AAAA,MACF,WAAW,KAAK,QAAQ,UAAK;AAC3B,aAAK,KAAK,MAAM;AAChB;AAAA,MACF,WAAW,KAAK,QAAQ,UAAK;AAC3B,YAAI,CAAC,UAAU;AACb,eAAK,KAAK,MAAM;AAChB;AAAA,QACF;AAAA,MACF,WAAW,KAAK,QAAQ,UAAK;AAC3B,aAAK,KAAK,MAAM;AAChB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,UAAU,YAAO,KAAK,QAAQ,UAAK;AACrC,WAAK,KAAK,MAAM;AAChB;AAAA,IACF;AAEA,SAAK,KAAK;AAAA,EACZ;AAEA,SAAO;AACT;;;AC1fO,SAAS,mBAAmB,WAAoC;AACrE,SAAO,CAAC,UAAkB,qBAAqB,OAAO,SAAS;AACjE;AAEO,SAAS,qBACd,OACA,WACQ;AACR,QAAM,aAAa,mBAAmB,KAAK;AAG3C,QAAM,WAAW,WAAW,UAAU;AAGtC,QAAM,EAAE,WAAW,MAAM,IAAI;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAGA,SAAO,wBAAwB,WAAW;AAAA,IACxC,QAAQ;AAAA,IACR,UAAU;AAAA;AAAA,EACZ,CAAC;AACH;;;AClCA,OAAO,cAAc;AACrB,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,IAAMC,WAAU,cAAc,YAAY,GAAG;AAI7C,eAAe,iBAAqC;AAClD,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,UAAU,KAAK,KAAKA,SAAQ,QAAQ,UAAU,GAAG,MAAM,MAAM,MAAM;AAEzE,aAAS,QAAQ,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,KAAK,OAAO;AAC/C,UAAI,OAAO,CAAC;AAAI,eAAO,GAAG;AAAA;AACrB,gBAAQ,EAAE;AAAA,IACjB,CAAC;AAAA,EACH,CAAC;AACH;AAEA,IAAI,mBAA8C;AAElD,eAAsB,eAAmC;AACvD,MAAI,CAAC,kBAAkB;AACrB,uBAAmB,eAAe;AAAA,EACpC;AACA,SAAO;AACT;;;ACjBA,IAAI,kBAAuC;AAC3C,IAAI,cAA4C;AAEhD,eAAe,mBAA0C;AACvD,MAAI;AAAiB,WAAO;AAC5B,MAAI;AAAa,WAAO;AAExB,iBAAe,YAAY;AACzB,UAAM,YAAY,MAAM,aAAa;AACrC,UAAM,YAAY,mBAAmB,SAAS;AAC9C,sBAAkB;AAClB,WAAO;AAAA,EACT,GAAG;AAEH,SAAO;AACT;AAEA,eAAsB,aAAa,OAAgC;AACjE,QAAM,YAAY,MAAM,iBAAiB;AACzC,SAAO,UAAU,KAAK;AACxB;AAEO,IAAM,YAAY,OAAO,OAAO;AAAA,EACrC,MAAM;AACR,CAAC","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"]}
1
+ {"version":3,"sources":["../src/normalizer.ts","../src/dictionary.ts","../src/particleRewriter.ts","../src/mora.ts","../src/coreConverter.ts","../src/kanaToHangul.ts","../src/tokenizer.ts","../src/kanaBarum.ts"],"names":["toHiragana","outCpStart","isKatakanaChar","out","info","require"],"mappings":"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C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAC9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAAA,EAE9C,QAAG,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,IAAI;AAChD;AAEO,IAAM,QAAkC;AAAA,EAC7C,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAE/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D,cAAI,EAAE,KAAK,UAAK,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AAAA,EAC/D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","sourcesContent":["export function normalizeInputText(input: string): string {\n // 1) NFD 결합문자(が/ぱ 등) 합성\n let normalized = input.normalize(\"NFC\");\n\n // 2) 반각 가타카나만 전각으로 (구두점/특수문자 최대한 보존)\n normalized = normalizeHalfwidthKatakanaOnly(normalized);\n\n // 3) 장음 기호 변종 최소 치환\n // - U+2015 HORIZONTAL BAR\n // - U+2500 BOX DRAWINGS LIGHT HORIZONTAL\n normalized = normalized.replace(/[\\u2015\\u2500]/g, \"ー\");\n\n // 4) ASCII hyphen이 가타카나 사이에 있을 때 장음 처리\n normalized = normalized\n .replace(/(?<=([\\u30A0-\\u30FF]))-/g, \"ー\")\n .replace(/-(?=[\\u30A0-\\u30FF])/g, \"ー\");\n\n return normalized;\n}\n\nfunction normalizeHalfwidthKatakanaOnly(s: string): string {\n // ✅ 반각 가타카나 + 탁점/반탁점(゙゚) + 반각 장음(ー)까지 함께 NFKC\n return s.replace(/[\\uFF66-\\uFF9F\\uFF70]+/g, (chunk) =>\n chunk.normalize(\"NFKC\"),\n );\n}\n\nexport function toHiragana(input: string): string {\n let out = \"\";\n for (const ch of input) {\n const code = ch.codePointAt(0)!;\n // カタカナ → ひらがな\n if (code >= 0x30a1 && code <= 0x30f6) {\n out += String.fromCodePoint(code - 0x60);\n continue;\n }\n if (ch === \"ー\") {\n out += ch;\n continue;\n }\n out += ch;\n }\n return out;\n}\n","// dictionary.ts\n// 한국인이 익숙한 발음을 담은 특수 사전\nexport interface SpecialDictionaryEntry {\n word: string;\n answer: string;\n hira?: boolean; // true면 히라가나 입력에도 적용\n kata?: boolean; // true면 카타카나 입력에도 적용\n}\n\nexport const SpecialDictionary: SpecialDictionaryEntry[] = [\n // [\"とうきょう\", \"도쿄\"],\n { word: \"こんにちは\", answer: \"곤니치와\", hira: true, kata: true },\n { word: \"こんばんは\", answer: \"곰방와\" },\n { word: \"すみません\", answer: \"스미마셍\" },\n { word: \"はひふへほ\", answer: \"하히후헤호\" },\n { word: \"かわいい\", answer: \"카와이\" },\n { word: \"つなみ\", answer: \"쓰나미\" },\n { word: \"ゆうり\", answer: \"유우리\" },\n { word: \"ミュージック\", answer: \"뮤지쿠\" },\n { word: \"ちゃん\", answer: \"쨩\" },\n];\n","// particleRewriter.ts\nimport type kuromoji from \"kuromoji\";\nimport type { Tokenizer } from \"./tokenizer\";\nimport { SpecialDictionary, SpecialDictionaryEntry } from \"./dictionary\";\n\n// --------------------------\n// local helper: toHiragana (protectedRanges는 hiraganaText 기준)\n// --------------------------\nfunction isKatakanaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x30a0 && c <= 0x30ff;\n}\n\nfunction containsKanji(s: string): boolean {\n for (const ch of s) {\n const c = ch.codePointAt(0)!;\n // CJK Unified Ideographs (U+4E00-U+9FFF) + Extension A (U+3400-U+4DBF)\n if ((c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3400 && c <= 0x4dbf)) {\n return true;\n }\n }\n return false;\n}\nfunction toHiragana(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) => {\n // 장음은 드랍\n if (ch === \"ー\") return \"\";\n if (!isKatakanaChar(ch)) return ch;\n const code = ch.codePointAt(0)!;\n // カタカナ 문자 범위 (ァ~ヶ)만 변환\n if (code >= 0x30a1 && code <= 0x30f6) {\n return String.fromCodePoint(code - 0x60);\n }\n return ch;\n })\n .join(\"\");\n}\n\nfunction dictKeysForHiraganaText(dict: SpecialDictionaryEntry[]): string[] {\n // hiraganaText에서 실제로 등장할 수 있는 키만 모으기:\n // - entry.word 자체는 넣어도 되고(못 찾으면 무해)\n // - hira:true면 toHiragana(word)를 추가로 넣는다\n const keys: string[] = [];\n for (const e of dict) {\n keys.push(e.word);\n if (e.hira) keys.push(toHiragana(e.word));\n }\n // 중복 제거\n return [...new Set(keys)].filter(Boolean);\n}\n\ntype Range = { start: number; end: number }; // [start, end)\n\nfunction rangesOverlap(a: Range, b: Range): boolean {\n return a.start < b.end && b.start < a.end;\n}\n\nfunction buildProtectedRanges(text: string, keys: string[]): Range[] {\n const sorted = [...new Set(keys)].sort((a, b) => b.length - a.length);\n const ranges: Range[] = [];\n\n for (const key of sorted) {\n if (!key) continue;\n let from = 0;\n while (true) {\n const idx = text.indexOf(key, from);\n if (idx === -1) break;\n\n const cand: Range = { start: idx, end: idx + key.length };\n if (!ranges.some((r) => rangesOverlap(r, cand))) {\n ranges.push(cand);\n }\n from = idx + 1;\n }\n }\n\n ranges.sort((a, b) => a.start - b.start);\n return ranges;\n}\n\nfunction isProtectedSpan(protectedRanges: Range[], start: number, end: number) {\n for (const r of protectedRanges) {\n if (start < r.end && end > r.start) return true;\n }\n return false;\n}\n\nexport const HARD_BOUNDARY_SURF = new Set([\n \"。\",\n \"、\",\n \"!\",\n \"?\",\n \"!\",\n \"?\",\n \" \",\n \" \",\n]);\nconst HARD_BOUNDARY_DETAIL1 = new Set([\n \"句点\",\n \"読点\",\n \"括弧開\",\n \"括弧閉\",\n \"空白\",\n]);\n\nconst LEXICAL_HE_ENDINGS = [\n \"いにしへ\",\n \"おきへ\",\n \"もとへ\",\n \"すえへ\",\n \"すゑへ\",\n \"かみへ\",\n \"くにへ\",\n \"きしへ\",\n] as const;\n\nfunction isHardBoundaryToken(t: kuromoji.IpadicFeatures): boolean {\n if (t.pos !== \"記号\") return false;\n if (HARD_BOUNDARY_SURF.has(t.surface_form)) return true;\n return HARD_BOUNDARY_DETAIL1.has(t.pos_detail_1 ?? \"\");\n}\nfunction isContentToken(t: kuromoji.IpadicFeatures): boolean {\n if (t.pos === \"記号\") return !isHardBoundaryToken(t);\n return true;\n}\n\nfunction prevContentIdx(tokens: kuromoji.IpadicFeatures[], i: number): number {\n for (let j = i - 1; j >= 0; j -= 1) if (isContentToken(tokens[j])) return j;\n return -1;\n}\nfunction nextContentIdx(tokens: kuromoji.IpadicFeatures[], i: number): number {\n for (let j = i + 1; j < tokens.length; j += 1)\n if (isContentToken(tokens[j])) return j;\n return -1;\n}\nfunction nextBoundaryOrEnd(\n tokens: kuromoji.IpadicFeatures[],\n i: number,\n): boolean {\n for (let j = i + 1; j < tokens.length; j += 1) {\n if (isHardBoundaryToken(tokens[j])) continue;\n return false;\n }\n return true;\n}\n\nexport type TokenSpan = {\n start: number; // rewritten 기준\n end: number; // rewritten 기준\n surface: string;\n\n pos?: string;\n pos1?: string;\n pos2?: string;\n pos3?: string;\n\n // ✅ 원문 기반 힌트: katakana 포함 여부(노ート 같은 케이스 차단용)\n originHadKatakana?: boolean;\n};\n\n/**\n * ✅ 핵심:\n * - 토큰화는 \"prewrite 이전\"에 수행 (원문/정규화 기준)\n * - prewrite는 \"토큰 품사\"를 쓰되, 실제 replace는 hiraganaText slice로 수행\n * - 결과로 rewrittenText + rewrittenTokenSpans를 만들어 core로 넘김\n *\n * 가정: hiraganaText와 originalText는 길이가 동일 (toHiragana는 1:1 치환)\n */\nexport function rewriteParticlesFromTokenization(\n originalText: string,\n hiraganaText: string,\n tokenizerTokens: kuromoji.IpadicFeatures[],\n): { rewritten: string; spans: TokenSpan[]; rewrittenOriginal: string } {\n // 코드포인트 배열로 변환 (kuromoji word_position이 코드포인트 기준)\n const hiraChars = Array.from(hiraganaText);\n const originChars = Array.from(originalText);\n\n // entry 기반\n const protectedRanges = buildProtectedRanges(\n hiraganaText,\n dictKeysForHiraganaText(SpecialDictionary),\n );\n\n let out = \"\";\n let origOut = \"\"; // 한자→pronunciation 변환된 원본\n const spans: TokenSpan[] = [];\n\n let cursorInText = 0;\n\n for (let i = 0; i < tokenizerTokens.length; i += 1) {\n const tok = tokenizerTokens[i];\n const wp = (tok as any).word_position as number | undefined;\n const surf = tok.surface_form;\n const surfCpLen = [...surf].length; // 코드포인트 길이\n\n // kuromoji word_position은 코드포인트 기준 (1-based)\n const start = typeof wp === \"number\" ? wp - 1 : cursorInText;\n const end = start + surfCpLen;\n cursorInText = end;\n\n // 한자가 포함된 경우 pronunciation을 사용\n const hasKanji = containsKanji(surf) && tok.pronunciation;\n const hiraSurf = hasKanji\n ? toHiragana(tok.pronunciation!)\n : hiraChars.slice(start, end).join(\"\");\n // 원본도 한자면 pronunciation에서 장음 제거 (길이 맞추기)\n const origSurfForOut = hasKanji\n ? tok.pronunciation!.replace(/ー/g, \"\")\n : originChars.slice(start, end).join(\"\");\n const originSurf = originChars.slice(start, end).join(\"\");\n\n const originHadKatakana = /[\\u30A0-\\u30FF]/.test(originSurf);\n\n // ✅ 불필요하고 위험한 isProtected(튜플 기반 + includes 난사) 제거\n // protectedRanges 기반으로만 판단\n if (isProtectedSpan(protectedRanges, start, end)) {\n const outCpStart = [...out].length;\n out += hiraSurf;\n origOut += origSurfForOut;\n spans.push({\n start: outCpStart,\n end: [...out].length,\n surface: hiraSurf,\n pos: tok.pos,\n pos1: tok.pos_detail_1 ?? undefined,\n pos2: tok.pos_detail_2 ?? undefined,\n pos3: tok.pos_detail_3 ?? undefined,\n originHadKatakana,\n });\n continue;\n }\n\n let replaced = hiraSurf;\n\n // --- は -> わ (계조사) ---\n if (tok.pos === \"助詞\" && hiraSurf === \"は\") {\n if (i > 0 && tokenizerTokens[i - 1].surface_form === \"は\") {\n // keep\n } else if (\n i + 1 < tokenizerTokens.length &&\n tokenizerTokens[i + 1].surface_form === \"は\"\n ) {\n // keep\n } else {\n const prevIdx = prevContentIdx(tokenizerTokens, i);\n if (prevIdx >= 0) {\n const nextIdx = nextContentIdx(tokenizerTokens, i);\n const hasNextContent = nextIdx >= 0;\n const isEndOrPunct = nextBoundaryOrEnd(tokenizerTokens, i);\n\n const prevTok = tokenizerTokens[prevIdx];\n const prevWpHa = (prevTok as any).word_position as number | undefined;\n const prevStartHa = typeof prevWpHa === \"number\" ? prevWpHa - 1 : 0;\n const prevEndHa = prevStartHa + [...prevTok.surface_form].length;\n const prevHira = hiraChars.slice(prevStartHa, prevEndHa).join(\"\");\n\n if (\n !prevHira.includes(\"っ\") &&\n tok.pos_detail_1 === \"係助詞\" &&\n (hasNextContent || isEndOrPunct) &&\n prevTok.pos !== \"助詞\"\n ) {\n replaced = \"わ\";\n }\n }\n }\n }\n\n // --- へ -> え (격조사) ---\n if (tok.pos === \"助詞\" && hiraSurf === \"へ\") {\n // 바로 왼쪽이 공백이면 keep (코드포인트 배열 사용)\n if (start > 0) {\n const left = hiraChars[start - 1];\n if (\n left === \" \" ||\n left === \" \" ||\n left === \"\\t\" ||\n left === \"\\n\" ||\n left === \"\\r\"\n ) {\n // keep\n } else {\n const prevIdx = prevContentIdx(tokenizerTokens, i);\n if (prevIdx >= 0) {\n const prevTok = tokenizerTokens[prevIdx];\n const prevWp = (prevTok as any).word_position as number | undefined;\n const prevStart = typeof prevWp === \"number\" ? prevWp - 1 : 0;\n const prevEnd = prevStart + [...prevTok.surface_form].length;\n\n const prevHiraSurf = hiraChars.slice(prevStart, prevEnd).join(\"\");\n\n if (\n LEXICAL_HE_ENDINGS.some((w) => (prevHiraSurf + \"へ\").endsWith(w))\n ) {\n // keep lexical endings\n } else if (prevHiraSurf.endsWith(\"の\")) {\n // keep \"...のへ\"\n } else if (tok.pos_detail_1 === \"格助詞\") {\n replaced = \"え\";\n }\n }\n }\n }\n }\n\n const outCpStart = [...out].length;\n out += replaced;\n origOut += origSurfForOut;\n\n spans.push({\n start: outCpStart,\n end: [...out].length,\n surface: replaced,\n pos: tok.pos,\n pos1: tok.pos_detail_1 ?? undefined,\n pos2: tok.pos_detail_2 ?? undefined,\n pos3: tok.pos_detail_3 ?? undefined,\n originHadKatakana,\n });\n }\n\n return { rewritten: out, spans, rewrittenOriginal: origOut };\n}\n\n/**\n * 외부에서 쓰기 편한 래퍼:\n * - originalText를 tokenizer로 먼저 tokenize\n * - hiraganaText는 호출자가 넘겨줌(길이 동일 가정)\n */\nexport function tokenizeAndRewriteParticles(\n originalText: string,\n hiraganaText: string,\n tokenizer: Tokenizer,\n): {\n rewritten: string;\n spans: TokenSpan[];\n rewrittenOriginal: string;\n rawTokens: kuromoji.IpadicFeatures[];\n} {\n const rawTokens = tokenizer.tokenize(originalText);\n // console.log(rawTokens);\n const { rewritten, spans, rewrittenOriginal } =\n rewriteParticlesFromTokenization(originalText, hiraganaText, rawTokens);\n return { rewritten, spans, rewrittenOriginal, rawTokens };\n}\n","// --- Tables (당신 코드 그대로) ---\nexport type VowelMain = \"a\" | \"i\" | \"u\" | \"e\" | \"o\";\nexport type ConsClass =\n | \"vowel\"\n | \"k\"\n | \"s\"\n | \"t\"\n | \"n\"\n | \"h\"\n | \"m\"\n | \"y\"\n | \"r\"\n | \"w\"\n | \"g\"\n | \"z\"\n | \"d\"\n | \"b\"\n | \"p\";\n\nexport type MoraInfo = {\n out: string;\n vowelMain: VowelMain;\n consClass: ConsClass;\n vowelOnly?: boolean;\n wasYouon?: boolean;\n};\n\nexport const SINGLE: Record<string, MoraInfo> = {\n あ: { out: \"아\", vowelMain: \"a\", consClass: \"vowel\", vowelOnly: true },\n い: { out: \"이\", vowelMain: \"i\", consClass: \"vowel\", vowelOnly: true },\n う: { out: \"우\", vowelMain: \"u\", consClass: \"vowel\", vowelOnly: true },\n え: { out: \"에\", vowelMain: \"e\", consClass: \"vowel\", vowelOnly: true },\n お: { out: \"오\", vowelMain: \"o\", consClass: \"vowel\", vowelOnly: true },\n\n か: { out: \"카\", vowelMain: \"a\", consClass: \"k\" },\n き: { out: \"키\", vowelMain: \"i\", consClass: \"k\" },\n く: { out: \"쿠\", vowelMain: \"u\", consClass: \"k\" },\n け: { out: \"케\", vowelMain: \"e\", consClass: \"k\" },\n こ: { out: \"코\", vowelMain: \"o\", consClass: \"k\" },\n\n さ: { out: \"사\", vowelMain: \"a\", consClass: \"s\" },\n し: { out: \"시\", vowelMain: \"i\", consClass: \"s\" },\n す: { out: \"스\", vowelMain: \"u\", consClass: \"s\" },\n せ: { out: \"세\", vowelMain: \"e\", consClass: \"s\" },\n そ: { out: \"소\", vowelMain: \"o\", consClass: \"s\" },\n\n た: { out: \"타\", vowelMain: \"a\", consClass: \"t\" },\n ち: { out: \"치\", vowelMain: \"i\", consClass: \"t\" },\n つ: { out: \"츠\", vowelMain: \"u\", consClass: \"t\" },\n て: { out: \"테\", vowelMain: \"e\", consClass: \"t\" },\n と: { out: \"토\", vowelMain: \"o\", consClass: \"t\" },\n\n な: { out: \"나\", vowelMain: \"a\", consClass: \"n\" },\n に: { out: \"니\", vowelMain: \"i\", consClass: \"n\" },\n ぬ: { out: \"누\", vowelMain: \"u\", consClass: \"n\" },\n ね: { out: \"네\", vowelMain: \"e\", consClass: \"n\" },\n の: { out: \"노\", vowelMain: \"o\", consClass: \"n\" },\n\n は: { out: \"하\", vowelMain: \"a\", consClass: \"h\" },\n ひ: { out: \"히\", vowelMain: \"i\", consClass: \"h\" },\n ふ: { out: \"후\", vowelMain: \"u\", consClass: \"h\" },\n へ: { out: \"헤\", vowelMain: \"e\", consClass: \"h\" },\n ほ: { out: \"호\", vowelMain: \"o\", consClass: \"h\" },\n\n ま: { out: \"마\", vowelMain: \"a\", consClass: \"m\" },\n み: { out: \"미\", vowelMain: \"i\", consClass: \"m\" },\n む: { out: \"무\", vowelMain: \"u\", consClass: \"m\" },\n め: { out: \"메\", vowelMain: \"e\", consClass: \"m\" },\n も: { out: \"모\", vowelMain: \"o\", consClass: \"m\" },\n\n や: { out: \"야\", vowelMain: \"a\", consClass: \"y\" },\n ゆ: { out: \"유\", vowelMain: \"u\", consClass: \"y\" },\n よ: { out: \"요\", vowelMain: \"o\", consClass: \"y\" },\n\n ら: { out: \"라\", vowelMain: \"a\", consClass: \"r\" },\n り: { out: \"리\", vowelMain: \"i\", consClass: \"r\" },\n る: { out: \"루\", vowelMain: \"u\", consClass: \"r\" },\n れ: { out: \"레\", vowelMain: \"e\", consClass: \"r\" },\n ろ: { out: \"로\", vowelMain: \"o\", consClass: \"r\" },\n\n わ: { out: \"와\", vowelMain: \"a\", consClass: \"w\" },\n を: { out: \"오\", vowelMain: \"o\", consClass: \"w\" },\n\n が: { out: \"가\", vowelMain: \"a\", consClass: \"g\" },\n ぎ: { out: \"기\", vowelMain: \"i\", consClass: \"g\" },\n ぐ: { out: \"구\", vowelMain: \"u\", consClass: \"g\" },\n げ: { out: \"게\", vowelMain: \"e\", consClass: \"g\" },\n ご: { out: \"고\", vowelMain: \"o\", consClass: \"g\" },\n\n ざ: { out: \"자\", vowelMain: \"a\", consClass: \"z\" },\n じ: { out: \"지\", vowelMain: \"i\", consClass: \"z\" },\n ず: { out: \"즈\", vowelMain: \"u\", consClass: \"z\" },\n ぜ: { out: \"제\", vowelMain: \"e\", consClass: \"z\" },\n ぞ: { out: \"조\", vowelMain: \"o\", consClass: \"z\" },\n\n だ: { out: \"다\", vowelMain: \"a\", consClass: \"d\" },\n ぢ: { out: \"지\", vowelMain: \"i\", consClass: \"d\" },\n づ: { out: \"즈\", vowelMain: \"u\", consClass: \"d\" },\n で: { out: \"데\", vowelMain: \"e\", consClass: \"d\" },\n ど: { out: \"도\", vowelMain: \"o\", consClass: \"d\" },\n\n ば: { out: \"바\", vowelMain: \"a\", consClass: \"b\" },\n び: { out: \"비\", vowelMain: \"i\", consClass: \"b\" },\n ぶ: { out: \"부\", vowelMain: \"u\", consClass: \"b\" },\n べ: { out: \"베\", vowelMain: \"e\", consClass: \"b\" },\n ぼ: { out: \"보\", vowelMain: \"o\", consClass: \"b\" },\n\n ぱ: { out: \"파\", vowelMain: \"a\", consClass: \"p\" },\n ぴ: { out: \"피\", vowelMain: \"i\", consClass: \"p\" },\n ぷ: { out: \"푸\", vowelMain: \"u\", consClass: \"p\" },\n ぺ: { out: \"페\", vowelMain: \"e\", consClass: \"p\" },\n ぽ: { out: \"포\", vowelMain: \"o\", consClass: \"p\" },\n\n ゔ: { out: \"부\", vowelMain: \"u\", consClass: \"b\" },\n};\n\nexport const YOUON: Record<string, MoraInfo> = {\n きゃ: { out: \"캬\", vowelMain: \"a\", consClass: \"k\", wasYouon: true },\n きゅ: { out: \"큐\", vowelMain: \"u\", consClass: \"k\", wasYouon: true },\n きょ: { out: \"쿄\", vowelMain: \"o\", consClass: \"k\", wasYouon: true },\n\n しゃ: { out: \"샤\", vowelMain: \"a\", consClass: \"s\", wasYouon: true },\n しゅ: { out: \"슈\", vowelMain: \"u\", consClass: \"s\", wasYouon: true },\n しょ: { out: \"쇼\", vowelMain: \"o\", consClass: \"s\", wasYouon: true },\n\n ちゃ: { out: \"챠\", vowelMain: \"a\", consClass: \"t\", wasYouon: true },\n ちゅ: { out: \"츄\", vowelMain: \"u\", consClass: \"t\", wasYouon: true },\n ちょ: { out: \"쵸\", vowelMain: \"o\", consClass: \"t\", wasYouon: true },\n てゅ: { out: \"튜\", vowelMain: \"u\", consClass: \"t\", wasYouon: true },\n でゅ: { out: \"듀\", vowelMain: \"u\", consClass: \"d\", wasYouon: true },\n\n にゃ: { out: \"냐\", vowelMain: \"a\", consClass: \"n\", wasYouon: true },\n にゅ: { out: \"뉴\", vowelMain: \"u\", consClass: \"n\", wasYouon: true },\n にょ: { out: \"뇨\", vowelMain: \"o\", consClass: \"n\", wasYouon: true },\n\n ひゃ: { out: \"햐\", vowelMain: \"a\", consClass: \"h\", wasYouon: true },\n ひゅ: { out: \"휴\", vowelMain: \"u\", consClass: \"h\", wasYouon: true },\n ひょ: { out: \"효\", vowelMain: \"o\", consClass: \"h\", wasYouon: true },\n ふゃ: { out: \"퍄\", vowelMain: \"a\", consClass: \"p\", wasYouon: true },\n ふゅ: { out: \"퓨\", vowelMain: \"u\", consClass: \"p\", wasYouon: true },\n ふょ: { out: \"표\", vowelMain: \"o\", consClass: \"p\", wasYouon: true },\n\n みゃ: { out: \"먀\", vowelMain: \"a\", consClass: \"m\", wasYouon: true },\n みゅ: { out: \"뮤\", vowelMain: \"u\", consClass: \"m\", wasYouon: true },\n みょ: { out: \"묘\", vowelMain: \"o\", consClass: \"m\", wasYouon: true },\n\n りゃ: { out: \"랴\", vowelMain: \"a\", consClass: \"r\", wasYouon: true },\n りゅ: { out: \"류\", vowelMain: \"u\", consClass: \"r\", wasYouon: true },\n りょ: { out: \"료\", vowelMain: \"o\", consClass: \"r\", wasYouon: true },\n\n ぎゃ: { out: \"갸\", vowelMain: \"a\", consClass: \"g\", wasYouon: true },\n ぎゅ: { out: \"규\", vowelMain: \"u\", consClass: \"g\", wasYouon: true },\n ぎょ: { out: \"교\", vowelMain: \"o\", consClass: \"g\", wasYouon: true },\n\n じゃ: { out: \"쟈\", vowelMain: \"a\", consClass: \"z\", wasYouon: true },\n じゅ: { out: \"쥬\", vowelMain: \"u\", consClass: \"z\", wasYouon: true },\n じょ: { out: \"죠\", vowelMain: \"o\", consClass: \"z\", wasYouon: true },\n\n びゃ: { out: \"뱌\", vowelMain: \"a\", consClass: \"b\", wasYouon: true },\n びゅ: { out: \"뷰\", vowelMain: \"u\", consClass: \"b\", wasYouon: true },\n びょ: { out: \"뵤\", vowelMain: \"o\", consClass: \"b\", wasYouon: true },\n\n ぴゃ: { out: \"퍄\", vowelMain: \"a\", consClass: \"p\", wasYouon: true },\n ぴゅ: { out: \"퓨\", vowelMain: \"u\", consClass: \"p\", wasYouon: true },\n ぴょ: { out: \"표\", vowelMain: \"o\", consClass: \"p\", wasYouon: true },\n};\n\nexport const LOAN: Record<string, MoraInfo> = {\n てぃ: { out: \"티\", vowelMain: \"i\", consClass: \"t\" },\n でぃ: { out: \"디\", vowelMain: \"i\", consClass: \"d\" },\n ちぇ: { out: \"체\", vowelMain: \"e\", consClass: \"t\" },\n しぇ: { out: \"셰\", vowelMain: \"e\", consClass: \"s\" },\n じぇ: { out: \"제\", vowelMain: \"e\", consClass: \"z\" },\n つぁ: { out: \"차\", vowelMain: \"a\", consClass: \"t\" },\n つぃ: { out: \"치\", vowelMain: \"i\", consClass: \"t\" },\n つぇ: { out: \"체\", vowelMain: \"e\", consClass: \"t\" },\n つぉ: { out: \"초\", vowelMain: \"o\", consClass: \"t\" },\n ふぁ: { out: \"파\", vowelMain: \"a\", consClass: \"p\" },\n ふぃ: { out: \"피\", vowelMain: \"i\", consClass: \"p\" },\n ふぇ: { out: \"페\", vowelMain: \"e\", consClass: \"p\" },\n ふぉ: { out: \"포\", vowelMain: \"o\", consClass: \"p\" },\n ぐぁ: { out: \"과\", vowelMain: \"a\", consClass: \"g\" },\n ぐぃ: { out: \"귀\", vowelMain: \"i\", consClass: \"g\" },\n ぐぇ: { out: \"궤\", vowelMain: \"e\", consClass: \"g\" },\n ぐぉ: { out: \"궈\", vowelMain: \"o\", consClass: \"g\" },\n くぁ: { out: \"콰\", vowelMain: \"a\", consClass: \"k\" },\n くぃ: { out: \"퀴\", vowelMain: \"i\", consClass: \"k\" },\n くぇ: { out: \"퀘\", vowelMain: \"e\", consClass: \"k\" },\n くぉ: { out: \"쿼\", vowelMain: \"o\", consClass: \"k\" },\n どぁ: { out: \"돠\", vowelMain: \"a\", consClass: \"d\" },\n どぅ: { out: \"두\", vowelMain: \"u\", consClass: \"d\" },\n どぉ: { out: \"둬\", vowelMain: \"o\", consClass: \"d\" },\n ゔぁ: { out: \"바\", vowelMain: \"a\", consClass: \"b\" },\n ゔぃ: { out: \"비\", vowelMain: \"i\", consClass: \"b\" },\n ゔぇ: { out: \"베\", vowelMain: \"e\", consClass: \"b\" },\n ゔぉ: { out: \"보\", vowelMain: \"o\", consClass: \"b\" },\n};\n\nexport const SMALL_Y = new Set([\"ゃ\", \"ゅ\", \"ょ\"]);\nexport const SMALL_V = new Set([\"ぁ\", \"ぃ\", \"ぅ\", \"ぇ\", \"ぉ\"]);\n\nexport const U_DROP_KEYS = new Set([\n \"ゆ\",\n \"きゅ\",\n \"しゅ\",\n \"ちゅ\",\n \"にゅ\",\n \"ひゅ\",\n \"みゅ\",\n \"りゅ\",\n \"ぎゅ\",\n \"じゅ\",\n \"びゅ\",\n \"ぴゅ\",\n]);\n","// coreConverter.ts\nimport { SpecialDictionary, type SpecialDictionaryEntry } from \"./dictionary\";\nimport { HARD_BOUNDARY_SURF, type TokenSpan } from \"./particleRewriter\";\nimport {\n type ConsClass,\n type MoraInfo,\n SINGLE,\n YOUON,\n LOAN,\n SMALL_Y,\n SMALL_V,\n U_DROP_KEYS,\n} from \"./mora\";\n\n// --------------------------\n// Kana normalize helpers\n// --------------------------\nfunction isHiraganaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n}\nfunction isKatakanaChar(ch: string) {\n const c = ch.codePointAt(0)!;\n return c >= 0x30a0 && c <= 0x30ff;\n}\nfunction toHiraganaKey(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) =>\n isKatakanaChar(ch) ? String.fromCodePoint(ch.codePointAt(0)! - 0x60) : ch,\n )\n .join(\"\");\n}\nfunction toKatakanaKey(s: string): string {\n const n = s.normalize(\"NFKC\");\n return Array.from(n)\n .map((ch) =>\n isHiraganaChar(ch) ? String.fromCodePoint(ch.codePointAt(0)! + 0x60) : ch,\n )\n .join(\"\");\n}\n\ntype DictStream = \"orig\" | \"rewritten\";\ntype CompiledDictItem = {\n keyChars: string[];\n answer: string;\n stream: DictStream;\n};\n\nfunction compileSpecialDictionary(\n dict: SpecialDictionaryEntry[],\n): CompiledDictItem[] {\n const items: CompiledDictItem[] = [];\n\n for (const e of dict) {\n // 기본: exact word는 원본에서만\n items.push({\n keyChars: Array.from(e.word),\n answer: e.answer,\n stream: \"orig\",\n });\n\n // hira:true => hiragana 스트림에서만 (입력 전체가 히라로 바뀌는 파이프라인이기 때문)\n if (e.hira) {\n const k = toHiraganaKey(e.word);\n items.push({\n keyChars: Array.from(k),\n answer: e.answer,\n stream: \"rewritten\",\n });\n }\n\n // kata:true => 원본에서만\n if (e.kata) {\n const k = toKatakanaKey(e.word);\n items.push({ keyChars: Array.from(k), answer: e.answer, stream: \"orig\" });\n }\n }\n\n // 긴 키 우선\n items.sort((a, b) => b.keyChars.length - a.keyChars.length);\n return items;\n}\n\nconst COMPILED_SPECIAL_DICT = compileSpecialDictionary(SpecialDictionary);\n\nfunction isHiragana(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n}\n\nfunction isKana(ch: string): boolean {\n return isHiragana(ch) || ch === \"ー\";\n}\n\nexport function coreKanaToHangulConvert(\n s: string,\n opts: { tokens: TokenSpan[]; original: string },\n): string {\n // 코드포인트 배열로 변환 (surrogate pair 문제 해결)\n const chars = Array.from(s);\n const origChars = Array.from(opts?.original ?? s);\n\n // --- Hangul utilities ---\n const HANGUL_BASE = 0xac00;\n const HANGUL_END = 0xd7a3;\n\n function isHangulSyllable(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= HANGUL_BASE && c <= HANGUL_END;\n }\n\n const JONG = {\n NONE: 0,\n G: 1, // ㄱ\n N: 4, // ㄴ\n M: 16, // ㅁ\n B: 17, // ㅂ\n S: 19, // ㅅ\n NG: 21, // ㅇ\n } as const;\n\n function addFinal(syl: string, jong: number): string {\n if (!isHangulSyllable(syl)) return syl;\n const code = syl.codePointAt(0)! - HANGUL_BASE;\n const cho = Math.floor(code / 588);\n const jung = Math.floor((code % 588) / 28);\n return String.fromCodePoint(HANGUL_BASE + cho * 588 + jung * 28 + jong);\n }\n\n function replaceLastHangul(out: string, jong: number): string {\n if (!out) return out;\n const last = out[out.length - 1];\n if (!isHangulSyllable(last)) return out;\n return out.slice(0, -1) + addFinal(last, jong);\n }\n\n // --- Kana classification ---\n function isHiragana(ch: string): boolean {\n const c = ch.codePointAt(0)!;\n return c >= 0x3040 && c <= 0x309f;\n }\n function isKana(ch: string): boolean {\n return isHiragana(ch) || ch === \"ー\";\n }\n\n type ReadMora = { key: string; len: number; info?: MoraInfo } | null;\n\n function readMoraAt(idx: number): ReadMora {\n if (idx >= chars.length) return null;\n\n const c0 = chars[idx];\n const c1 = chars[idx + 1];\n\n if (c1 && SMALL_V.has(c1)) {\n const key2 = c0 + c1;\n const info = LOAN[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n if (c1 && SMALL_Y.has(c1)) {\n const key2 = c0 + c1;\n const info = YOUON[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n const info = SINGLE[c0];\n if (info) return { key: c0, len: 1, info };\n\n return { key: c0, len: 1, info: undefined };\n }\n\n function readOriginalMoraAt(idx: number): ReadMora {\n if (idx >= origChars.length) return null;\n\n const c0 = origChars[idx];\n const c1 = origChars[idx + 1];\n\n if (c1 && SMALL_V.has(c1)) {\n const key2 = c0 + c1;\n const info = LOAN[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n if (c1 && SMALL_Y.has(c1)) {\n const key2 = c0 + c1;\n const info = YOUON[key2];\n if (info) return { key: key2, len: 2, info };\n }\n\n const info = SINGLE[c0];\n if (info) return { key: c0, len: 1, info };\n\n return { key: c0, len: 1, info: undefined };\n }\n\n function isLabialStart(cons: ConsClass): boolean {\n return cons === \"m\" || cons === \"b\" || cons === \"p\";\n }\n\n // 토큰 컨텍스트 탐색용\n const tokens = opts?.tokens ?? null;\n let tokIdx = 0;\n\n function syncTokenIndex(charIndex: number) {\n if (!tokens) return;\n while (tokIdx + 1 < tokens.length && tokens[tokIdx].end <= charIndex) {\n tokIdx++;\n }\n }\n\n function curToken(charIndex: number): TokenSpan | null {\n if (!tokens) return null;\n syncTokenIndex(charIndex);\n const t = tokens[tokIdx];\n if (t && t.start <= charIndex && charIndex < t.end) return t;\n return null;\n }\n\n function prevToken(): TokenSpan | null {\n if (!tokens) return null;\n return tokIdx - 1 >= 0 ? tokens[tokIdx - 1] : null;\n }\n function nextToken(): TokenSpan | null {\n if (!tokens) return null;\n return tokIdx + 1 < tokens.length ? tokens[tokIdx + 1] : null;\n }\n\n // ✅ 유성화 차단 next\n const INITIAL_VOICING_BLOCK_NEXT = new Set([\"い\", \"ひ\", \"ん\", \"て\"]);\n\n function peekNextMoraKeySkippingChoonpu(fromIdx: number): string | null {\n let j = fromIdx;\n while (j < chars.length && chars[j] === \"ー\") j++;\n const m = readMoraAt(j);\n return m?.key ?? null;\n }\n\n // ✅ \"-san\" 판별 (코드포인트 인덱스 기준)\n const SAN_PARTICLES = new Set([\"は\", \"わ\", \"へ\", \"え\", \"を\", \"お\"]);\n function isSanHonorificAt(cpIdx: number): boolean {\n const t = curToken(cpIdx);\n if (!t) return false;\n if (cpIdx < 1 || chars[cpIdx - 1] !== \"さ\") return false;\n\n // t.start는 코드포인트 인덱스, chars.slice 사용\n const local = chars.slice(t.start, cpIdx + 1).join(\"\");\n if (!local.endsWith(\"さん\")) return false;\n\n const hasPrefixInsideToken = cpIdx - 1 > t.start;\n const p = prevToken();\n const prevIsAttachable =\n !!p &&\n p.end === t.start &&\n p.surface.length > 0 &&\n !HARD_BOUNDARY_SURF.has(p.surface);\n\n if (!hasPrefixInsideToken && !prevIsAttachable) return false;\n\n const n = nextToken();\n if (!n) return true;\n if (n.pos === \"記号\" && HARD_BOUNDARY_SURF.has(n.surface)) return true;\n if (n.pos === \"助詞\" && SAN_PARTICLES.has(n.surface)) return true;\n return false;\n }\n\n let out = \"\";\n let i = 0;\n\n let lastMora: MoraInfo | null = null;\n\n while (i < chars.length) {\n // 토큰 기반 \"단어 시작\" 정의: i가 content 토큰 start면 true\n let atTokenStart = false;\n let tokForI: TokenSpan | null = null;\n\n if (tokens) {\n tokForI = curToken(i);\n // ✅ 토큰 시작이면 일단 단어 시작 후보로 인정\n atTokenStart = !!tokForI && tokForI.start === i;\n\n // ✅ 유성화/단어시작 판정에서 \"기호\"와 \"원문 카타카나 토큰\"만 컷\n if (tokForI?.pos === \"記号\") atTokenStart = false;\n } else {\n atTokenStart = i === 0;\n }\n\n // --------------------------\n // ✅ SpecialDictionary (entry 기반 + hira/kata 옵션)\n // --------------------------\n let matchedSpecial = false;\n\n // 긴 키부터 순회하므로, 앞에서 걸리면 끝\n for (const it of COMPILED_SPECIAL_DICT) {\n const src = it.stream === \"orig\" ? origChars : chars; // chars=rewritten\n const len = it.keyChars.length;\n if (i + len > src.length) continue;\n\n let ok = true;\n for (let k = 0; k < len; k++) {\n if (src[i + k] !== it.keyChars[k]) {\n ok = false;\n break;\n }\n }\n if (!ok) continue;\n\n out += it.answer;\n i += len;\n\n // 사전 치환은 단어 단위 => 상태 초기화\n lastMora = null;\n\n matchedSpecial = true;\n break;\n }\n if (matchedSpecial) continue;\n\n const ch = chars[i];\n\n /**\n * ー 표시는 그냥 드랍\n */\n if (ch === \"ー\") {\n i += 1;\n continue;\n }\n\n /**\n * 비가나: 그대로\n */\n if (!isKana(ch)) {\n out += ch;\n i += 1;\n lastMora = null;\n continue;\n }\n\n /**\n * 촉음 규칙\n */\n if (ch === \"っ\") {\n if (!out || !isHangulSyllable(out[out.length - 1])) {\n // \"ッ\"으로 바꾸기\n out += \"ッ\";\n i += 1;\n lastMora = null;\n continue;\n }\n\n const next = readMoraAt(i + 1);\n\n const prevV = lastMora?.vowelMain ?? \"a\";\n const nextInfo = next?.info;\n const nextCons: ConsClass = nextInfo?.consClass ?? \"t\";\n\n let jong: number = JONG.S;\n if (nextCons === \"p\" || nextCons === \"b\") jong = JONG.B;\n else if (nextCons === \"k\" || nextCons === \"g\") {\n jong = prevV === \"e\" || prevV === \"i\" ? JONG.S : JONG.G;\n } else {\n jong = JONG.S;\n }\n\n out = replaceLastHangul(out, jong);\n i += 1;\n continue;\n }\n\n /**\n * おお 장음 규칙\n */\n if (ch === \"お\" && chars[i + 1] === \"お\") {\n let j = i;\n while (chars[j] === \"お\") j++;\n out += \"오\";\n i = j;\n lastMora = {\n out: \"오\",\n vowelMain: \"o\",\n consClass: \"vowel\",\n vowelOnly: true,\n };\n continue;\n }\n\n const mora = readMoraAt(i);\n const originalMora = readOriginalMoraAt(i);\n if (!mora) {\n out += chars[i];\n i += 1;\n lastMora = null;\n continue;\n }\n // 에러\n if (!originalMora) {\n throw Error(\"원본 모라 손실\");\n }\n\n /**\n * ん 규칙\n */\n if (mora.key === \"ん\") {\n const next = readMoraAt(i + 1);\n const nextInfo = next?.info;\n\n const hasPrevHangul =\n out.length > 0 && isHangulSyllable(out[out.length - 1]);\n if (!hasPrevHangul) {\n out += \"ㄴ\";\n i += 1;\n lastMora = null;\n continue;\n }\n\n // 토큰 컨텍스트 기반 \"-san\" → '상'\n if (lastMora?.out === \"사\" && isSanHonorificAt(i)) {\n out = replaceLastHangul(out, JONG.NG);\n i += 1;\n continue;\n }\n\n // --- 기존 ん 동화 규칙 ---\n let jong: number = JONG.N;\n if (!next || !nextInfo || !isKana(next.key[0])) {\n jong = lastMora?.wasYouon ? JONG.NG : JONG.N;\n } else {\n const nc = nextInfo.consClass;\n if (nc === \"k\" || nc === \"g\") {\n jong = JONG.NG;\n } else if (nc === \"vowel\" || nc === \"y\" || nc === \"w\") {\n jong = JONG.N;\n } else if (isLabialStart(nc)) {\n if (lastMora?.vowelOnly) jong = JONG.N;\n else jong = JONG.M;\n } else {\n jong = JONG.N;\n }\n }\n\n out = replaceLastHangul(out, jong);\n i += 1;\n continue;\n }\n\n /**\n * 유성화 규칙\n */\n const info = mora.info;\n if (!info) {\n out += mora.key;\n i += mora.len;\n lastMora = null;\n continue;\n }\n\n // ✅ 단어(토큰) 시작 유성화: と/こ만 + 예외(이/히/ん/테) + (앞이 っ이면 금지)\n let outSyl = info.out;\n if (atTokenStart && (mora.key === \"と\" || mora.key === \"こ\")) {\n const prevIsSokuon = i > 0 && chars[i - 1] === \"っ\";\n\n const nextKey = peekNextMoraKeySkippingChoonpu(i + mora.len);\n const blockedByNext =\n !!nextKey && INITIAL_VOICING_BLOCK_NEXT.has(nextKey);\n\n const isKou = mora.key === \"こ\" && chars[i + mora.len] === \"う\";\n\n // ✅ 추가: \"진짜 조사 と/こ\"로 쓰인 경우만 유성화 차단\n // - 현재 토큰이 1글자 'と'/'こ'이고,\n // - 이전 토큰이 내용어(명사/동사/형용사 등)면 => 조사로 판단 => 유성화 금지\n let blockedByParticleUsage = false;\n if (tokens && tokForI) {\n const isSingleCharToken = tokForI.surface.length === 1;\n const tokenMatchesMora = tokForI.surface === mora.key;\n\n if (isSingleCharToken && tokenMatchesMora) {\n const p = prevToken(); // curToken(i) 호출로 tokIdx는 sync된 상태\n const prevLooksLikeContent =\n !!p &&\n p.pos !== \"記号\" &&\n p.pos !== \"助詞\" &&\n p.pos !== \"助動詞\" &&\n !HARD_BOUNDARY_SURF.has(p.surface);\n\n if (prevLooksLikeContent) blockedByParticleUsage = true;\n }\n }\n\n const allowVoicing =\n !prevIsSokuon && !blockedByNext && !isKou && !blockedByParticleUsage;\n\n if (allowVoicing) {\n if (mora.key === \"と\") outSyl = \"도\";\n else if (mora.key === \"こ\") outSyl = \"고\";\n }\n }\n\n out += outSyl;\n lastMora = { ...info, out: outSyl };\n\n /**\n * 연음 드랍\n */\n const next1 = chars[i + mora.len]; // 다음 언어\n const afterLen = chars[i + mora.len + 1]; // 다다음언어\n\n // o + う 드랍\n if (next1 === \"う\" && info.vowelMain === \"o\") {\n i += mora.len + 1;\n continue;\n }\n\n // ゅう 드랍 (きゅう) // ゆう는 드랍할까말까? (유우리) 일단 ゆう도 드랍함!\n if (next1 === \"う\" && U_DROP_KEYS.has(mora.key)) {\n i += mora.len + 1;\n continue;\n }\n\n // い 드랍\n if (next1 === \"い\") {\n // せんせい -> 센세\n if (mora.key === \"せ\") {\n i += mora.len + 1;\n continue;\n }\n // 케도 장음인데, 케이사츠라고 검색하는 경우가 더 많으려나? 어떻게 할까...\n else if (mora.key === \"け\") {\n i += mora.len + 1;\n continue;\n }\n // えいご -> 에고\n else if (mora.key === \"え\") {\n // 조사 へ 감지\n if (originalMora.key !== \"へ\") {\n i += mora.len + 1;\n continue;\n }\n }\n // 오이시, 야사시 대응\n else if (mora.key === \"し\") {\n i += mora.len + 1;\n continue;\n }\n }\n\n // おねえさん 대응\n if (next1 === \"え\" && mora.key === \"ね\") {\n i += mora.len + 1;\n continue;\n }\n\n i += mora.len;\n }\n\n return out;\n}\n","// kanaToHangul.ts\n// 전체 변환 파이프라인을 orchestration만 담당하도록 정리했습니다.\nimport type { Tokenizer } from \"./tokenizer\";\nimport { normalizeInputText, toHiragana } from \"./normalizer\";\nimport { tokenizeAndRewriteParticles } from \"./particleRewriter\";\nimport { coreKanaToHangulConvert } from \"./coreConverter\";\n\nexport type KanaToHangul = (input: string) => string;\n\nexport function createKanaToHangul(tokenizer: Tokenizer): KanaToHangul {\n return (input: string) => convertWithTokenizer(input, tokenizer);\n}\n\nexport function convertWithTokenizer(\n input: string,\n tokenizer: Tokenizer,\n): string {\n const normalized = normalizeInputText(input);\n\n // ✅ 길이 1:1 보장되는 kana 변환을 먼저 수행(스팬 유지)\n const hiragana = toHiragana(normalized);\n\n // ✅ 핵심: prewrite 이전에 토큰화(=normalized 기준), prewrite는 토큰(pos)을 사용하되 slice는 hiragana 기준\n const { rewritten, spans, rewrittenOriginal } = tokenizeAndRewriteParticles(\n normalized,\n hiragana,\n tokenizer,\n );\n\n // ✅ rewritten + rewritten spans 로 core\n // 한자가 pronunciation으로 변환된 경우 rewrittenOriginal도 같은 길이로 변환됨\n return coreKanaToHangulConvert(rewritten, {\n tokens: spans,\n original: rewrittenOriginal,\n });\n}\n","import kuromoji from \"kuromoji\";\nimport path from \"node:path\";\nimport { createRequire } from \"node:module\";\n\nconst require = createRequire(import.meta.url);\n\nexport type Tokenizer = kuromoji.Tokenizer<kuromoji.IpadicFeatures>;\n\nasync function buildTokenizer(): Promise<Tokenizer> {\n return new Promise((resolve, reject) => {\n const dicPath = path.join(require.resolve(\"kuromoji\"), \"..\", \"..\", \"dict\");\n\n kuromoji.builder({ dicPath }).build((err, tk) => {\n if (err || !tk) reject(err);\n else resolve(tk);\n });\n });\n}\n\nlet tokenizerPromise: Promise<Tokenizer> | null = null;\n\nexport async function getTokenizer(): Promise<Tokenizer> {\n if (!tokenizerPromise) {\n tokenizerPromise = buildTokenizer();\n }\n return tokenizerPromise;\n}\n","import type { KanaToHangul } from \"./kanaToHangul\";\nimport { createKanaToHangul } from \"./kanaToHangul\";\nimport { getTokenizer } from \"./tokenizer\";\n\n/**\n * Lazy-initialized public API wrapper.\n * 토크나이저를 빌드/캐시하고, 외부에서는 await kanaToHangul(...)만 호출하면 됩니다.\n */\n\nlet cachedConverter: KanaToHangul | null = null;\nlet pendingInit: Promise<KanaToHangul> | null = null;\n\nasync function initKanaToHangul(): Promise<KanaToHangul> {\n if (cachedConverter) return cachedConverter;\n if (pendingInit) return pendingInit;\n\n pendingInit = (async () => {\n const tokenizer = await getTokenizer();\n const converter = createKanaToHangul(tokenizer);\n cachedConverter = converter;\n return converter;\n })();\n\n return pendingInit;\n}\n\nexport async function kanaToHangul(input: string): Promise<string> {\n const converter = await initKanaToHangul();\n return converter(input);\n}\n\nexport const KanaBarum = Object.freeze({\n init: initKanaToHangul,\n});\n\nexport type { KanaToHangul };\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kanabarum",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "일본어 가나를 한국어 발음으로 바꿔주는 변환기",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",