ipa-hangul 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 YSW
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # ipa-hangul
2
+
3
+ Convert IPA (International Phonetic Alphabet) pronunciation to Korean Hangul.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **Accurate IPA-to-Hangul conversion** based on phonetic rules
8
+ - 📏 **Long vowels marked with dash (-)** - `/siː/` → `시-`
9
+ - 🔤 **Consonant clusters as Jamo** - `/wɝld/` → `월ㄷ`
10
+ - 🏗️ **Modular & maintainable** code structure
11
+ - 📦 **Zero dependencies**
12
+ - 💯 **TypeScript support** with full type definitions
13
+ - 🚀 **Dual format** - ESM and CommonJS
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install ipa-hangul
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```typescript
24
+ import { ipaToHangul } from 'ipa-hangul';
25
+
26
+ // Basic examples
27
+ ipaToHangul('/ˈhɛloʊ/'); // "헤로"
28
+ ipaToHangul('/kæt/'); // "캩"
29
+ ipaToHangul('/bʊk/'); // "붘"
30
+
31
+ // Long vowels (marked with -)
32
+ ipaToHangul('/siː/'); // "시-"
33
+ ipaToHangul('/kɑːr/'); // "카-ㄹ"
34
+
35
+ // Consonant clusters (as Jamo)
36
+ ipaToHangul('/wɝld/'); // "월ㄷ"
37
+ ipaToHangul('/fɪlm/'); // "필ㅁ"
38
+ ipaToHangul('/strɛŋkθs/'); // "ㅅㅌ렝ㅋㅅㅅ"
39
+
40
+ // Optional sounds (removed)
41
+ ipaToHangul('/ˈɹʌmb(ə)l/'); // "럼ㅂㄹ"
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - **Accurate conversion**: Based on Korean phonetic rules and Jamo assembly
47
+ - **Handles complex IPA**: Supports diphthongs, consonant clusters, syllabic consonants
48
+ - **Clean API**: Single function with string input/output
49
+ - **TypeScript**: Full type definitions included
50
+ - **Zero dependencies**: No runtime dependencies
51
+ - **Dual format**: ESM and CommonJS support
52
+
53
+ ## Supported IPA Features
54
+
55
+ ### Consonants
56
+ - Simple consonants: p, b, t, d, k, g, m, n, ŋ, f, v, θ, ð, s, z, ʃ, ʒ, h, l, r, ɹ
57
+ - Affricates: tʃ, dʒ
58
+ - Consonant clusters: pɹ, bɹ, tɹ, dɹ, kɹ, gɹ, fɹ, pl, bl, kl, gl, fl, sl
59
+
60
+ ### Vowels
61
+ - Simple vowels: i, ɪ, e, ɛ, æ, ɑ, ɒ, ɔ, ʌ, ə, ɜ, ʊ, u
62
+ - Long vowels: iː, ɑː, ɔː, ɜː, uː
63
+ - Diphthongs: eɪ, aɪ, ɔɪ, aʊ, əʊ, oʊ, ɪə, eə, ʊə
64
+ - Semi-vowel combinations: w + vowel, j + vowel
65
+ - Syllabic consonants: l̩, n̩, m̩
66
+
67
+ ### Special handling
68
+ - Stress markers (ˈ, ˌ) are removed
69
+ - Optional sounds in parentheses are removed
70
+ - Delimiters (/, [, ], .) are ignored
71
+
72
+ ## How it works
73
+
74
+ The converter uses Korean Jamo (자모) assembly to construct Hangul syllables:
75
+
76
+ 1. **Choseong (초성)**: Initial consonant (19 options)
77
+ 2. **Jungseong (중성)**: Vowel (21 options)
78
+ 3. **Jongseong (종성)**: Final consonant (27 options + none)
79
+
80
+ Each IPA sound is mapped to the closest Korean equivalent, then assembled into valid Hangul syllables.
81
+
82
+ ## Examples
83
+
84
+ | Word | IPA | Hangul | Notes |
85
+ |------|-----|--------|-------|
86
+ | hello | /ˈhɛloʊ/ | 헤로 | Stress marker removed |
87
+ | cat | /kæt/ | 캩 | Final 't' → ㅌ |
88
+ | internet | /ˈɪntərnɛt/ | 인털넽 | Multi-syllable |
89
+ | world | /wɜːrld/ | 월르드 | Consonant cluster 'rld' |
90
+ | pretty | /ˈprɪti/ | 프리티 | Consonant cluster 'pr' |
91
+ | button | /ˈbʌtn̩/ | 버튼 | Syllabic 'n' |
92
+
93
+ ## API
94
+
95
+ ### `ipaToHangul(ipa: string): string`
96
+
97
+ Converts IPA notation to Korean Hangul pronunciation.
98
+
99
+ **Parameters:**
100
+ - `ipa`: IPA notation string (can include stress markers, brackets, optional sounds)
101
+
102
+ **Returns:**
103
+ - Korean Hangul pronunciation string
104
+
105
+ **Example:**
106
+ ```typescript
107
+ import { ipaToHangul } from 'ipa-hangul';
108
+
109
+ const pronunciation = ipaToHangul('/ˈhɛloʊ/');
110
+ console.log(pronunciation); // "헤로"
111
+ ```
112
+
113
+ ## Limitations
114
+
115
+ - **Approximation**: Korean Hangul cannot perfectly represent all English sounds
116
+ - **Mapping choices**: Some IPA sounds map to the same Korean consonant (e.g., f/p → ㅍ)
117
+ - **Simplified finals**: Some final consonants use unconventional mappings (e.g., t → ㅌ instead of ㄷ)
118
+ - **No tone support**: Only segmental features are converted, not suprasegmental features
119
+
120
+ ## Contributing
121
+
122
+ Contributions are welcome! Please feel free to submit issues or pull requests.
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,31 @@
1
+ /**
2
+ * IPA (International Phonetic Alphabet) to Korean (Hangul) pronunciation converter
3
+ *
4
+ * Features:
5
+ * - Long vowels (ː) are marked with dash (-) and split syllables
6
+ * - Consonant-only sequences use compatibility Jamo (ㄱㄴㄷ...)
7
+ * - Vowel+consonant combinations form complete Hangul syllables
8
+ *
9
+ * Example:
10
+ * /wɜːrld/ → 워-ㄹㄷ
11
+ * /hɛloʊ/ → 헤로
12
+ */
13
+ /**
14
+ * Convert IPA notation to Korean Hangul pronunciation
15
+ *
16
+ * Features:
17
+ * - Long vowels (ː) create segments separated by dash (-)
18
+ * - Consonant-only sequences use compatibility Jamo (ㄱㄴㄷ...)
19
+ * - Vowel+consonant combinations form complete Hangul syllables
20
+ *
21
+ * @param ipa - IPA notation string (e.g., "/ˈhɛloʊ/", "/wɜːrld/")
22
+ * @returns Korean Hangul pronunciation string
23
+ *
24
+ * @example
25
+ * ipaToHangul("/ˈhɛloʊ/") // "헤로"
26
+ * ipaToHangul("/wɜːrld/") // "워-ㄹㄷ"
27
+ * ipaToHangul("/kæt/") // "캩"
28
+ */
29
+ declare function ipaToHangul(ipa: string): string;
30
+
31
+ export { ipaToHangul };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * IPA (International Phonetic Alphabet) to Korean (Hangul) pronunciation converter
3
+ *
4
+ * Features:
5
+ * - Long vowels (ː) are marked with dash (-) and split syllables
6
+ * - Consonant-only sequences use compatibility Jamo (ㄱㄴㄷ...)
7
+ * - Vowel+consonant combinations form complete Hangul syllables
8
+ *
9
+ * Example:
10
+ * /wɜːrld/ → 워-ㄹㄷ
11
+ * /hɛloʊ/ → 헤로
12
+ */
13
+ /**
14
+ * Convert IPA notation to Korean Hangul pronunciation
15
+ *
16
+ * Features:
17
+ * - Long vowels (ː) create segments separated by dash (-)
18
+ * - Consonant-only sequences use compatibility Jamo (ㄱㄴㄷ...)
19
+ * - Vowel+consonant combinations form complete Hangul syllables
20
+ *
21
+ * @param ipa - IPA notation string (e.g., "/ˈhɛloʊ/", "/wɜːrld/")
22
+ * @returns Korean Hangul pronunciation string
23
+ *
24
+ * @example
25
+ * ipaToHangul("/ˈhɛloʊ/") // "헤로"
26
+ * ipaToHangul("/wɜːrld/") // "워-ㄹㄷ"
27
+ * ipaToHangul("/kæt/") // "캩"
28
+ */
29
+ declare function ipaToHangul(ipa: string): string;
30
+
31
+ export { ipaToHangul };
package/dist/index.js ADDED
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ipaToHangul: () => ipaToHangul
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ var CHOSEONG = {
27
+ GIYEOK: 0,
28
+ SSANGGIYEOK: 1,
29
+ NIEUN: 2,
30
+ DIGEUT: 3,
31
+ SSANGDIGEUT: 4,
32
+ RIEUL: 5,
33
+ MIEUM: 6,
34
+ BIEUP: 7,
35
+ SSANGBIEUP: 8,
36
+ SIOS: 9,
37
+ SSANGSIOS: 10,
38
+ IEUNG: 11,
39
+ JIEUT: 12,
40
+ SSANGJIEUT: 13,
41
+ CHIEUT: 14,
42
+ KIEUK: 15,
43
+ TIEUT: 16,
44
+ PIEUP: 17,
45
+ HIEUT: 18
46
+ };
47
+ var JUNGSEONG = {
48
+ A: 0,
49
+ AE: 1,
50
+ YA: 2,
51
+ YAE: 3,
52
+ EO: 4,
53
+ E: 5,
54
+ YEO: 6,
55
+ YE: 7,
56
+ O: 8,
57
+ WA: 9,
58
+ WAE: 10,
59
+ OE: 11,
60
+ YO: 12,
61
+ U: 13,
62
+ WO: 14,
63
+ WE: 15,
64
+ WI: 16,
65
+ YU: 17,
66
+ EU: 18,
67
+ UI: 19,
68
+ I: 20
69
+ };
70
+ var JONGSEONG = {
71
+ NONE: 0,
72
+ GIYEOK: 1,
73
+ NIEUN: 4,
74
+ DIGEUT: 7,
75
+ RIEUL: 8,
76
+ MIEUM: 16,
77
+ BIEUP: 17,
78
+ SIOS: 19,
79
+ IEUNG: 21,
80
+ JIEUT: 22,
81
+ CHIEUT: 23,
82
+ KIEUK: 24,
83
+ TIEUT: 25,
84
+ PIEUP: 26
85
+ };
86
+ var COMPAT_JAMO = {
87
+ "\u3131": "\u3131",
88
+ "\u3132": "\u3132",
89
+ "\u3134": "\u3134",
90
+ "\u3137": "\u3137",
91
+ "\u3139": "\u3139",
92
+ "\u3141": "\u3141",
93
+ "\u3142": "\u3142",
94
+ "\u3145": "\u3145",
95
+ "\u3146": "\u3146",
96
+ "\u3147": "\u3147",
97
+ "\u3148": "\u3148",
98
+ "\u314A": "\u314A",
99
+ "\u314B": "\u314B",
100
+ "\u314C": "\u314C",
101
+ "\u314D": "\u314D",
102
+ "\u314E": "\u314E"
103
+ };
104
+ var CONSONANT_TO_CHOSEONG = {
105
+ "p": CHOSEONG.PIEUP,
106
+ "b": CHOSEONG.BIEUP,
107
+ "t": CHOSEONG.TIEUT,
108
+ "d": CHOSEONG.DIGEUT,
109
+ "k": CHOSEONG.KIEUK,
110
+ "g": CHOSEONG.GIYEOK,
111
+ "m": CHOSEONG.MIEUM,
112
+ "n": CHOSEONG.NIEUN,
113
+ "\u014B": CHOSEONG.IEUNG,
114
+ "f": CHOSEONG.PIEUP,
115
+ "v": CHOSEONG.BIEUP,
116
+ "\u03B8": CHOSEONG.SIOS,
117
+ "\xF0": CHOSEONG.DIGEUT,
118
+ "s": CHOSEONG.SIOS,
119
+ "z": CHOSEONG.JIEUT,
120
+ "\u0283": CHOSEONG.SIOS,
121
+ "\u0292": CHOSEONG.JIEUT,
122
+ "h": CHOSEONG.HIEUT,
123
+ "t\u0283": CHOSEONG.CHIEUT,
124
+ "d\u0292": CHOSEONG.JIEUT,
125
+ "l": CHOSEONG.RIEUL,
126
+ "r": CHOSEONG.RIEUL,
127
+ "\u0279": CHOSEONG.RIEUL,
128
+ "w": CHOSEONG.IEUNG,
129
+ "j": CHOSEONG.IEUNG
130
+ };
131
+ var CONSONANT_TO_JONGSEONG = {
132
+ "p": JONGSEONG.BIEUP,
133
+ "b": JONGSEONG.BIEUP,
134
+ "t": JONGSEONG.TIEUT,
135
+ "d": JONGSEONG.DIGEUT,
136
+ "k": JONGSEONG.KIEUK,
137
+ "g": JONGSEONG.GIYEOK,
138
+ "m": JONGSEONG.MIEUM,
139
+ "n": JONGSEONG.NIEUN,
140
+ "\u014B": JONGSEONG.IEUNG,
141
+ "f": JONGSEONG.PIEUP,
142
+ "v": JONGSEONG.BIEUP,
143
+ "\u03B8": JONGSEONG.SIOS,
144
+ "\xF0": JONGSEONG.DIGEUT,
145
+ "s": JONGSEONG.SIOS,
146
+ "z": JONGSEONG.JIEUT,
147
+ "\u0283": JONGSEONG.SIOS,
148
+ "\u0292": JONGSEONG.JIEUT,
149
+ "l": JONGSEONG.RIEUL,
150
+ "r": JONGSEONG.RIEUL,
151
+ "\u0279": JONGSEONG.RIEUL,
152
+ "t\u0283": JONGSEONG.CHIEUT,
153
+ "d\u0292": JONGSEONG.JIEUT
154
+ };
155
+ var CONSONANT_TO_JAMO = {
156
+ "p": COMPAT_JAMO["\u314D"],
157
+ "b": COMPAT_JAMO["\u3142"],
158
+ "t": COMPAT_JAMO["\u314C"],
159
+ "d": COMPAT_JAMO["\u3137"],
160
+ "k": COMPAT_JAMO["\u314B"],
161
+ "g": COMPAT_JAMO["\u3131"],
162
+ "m": COMPAT_JAMO["\u3141"],
163
+ "n": COMPAT_JAMO["\u3134"],
164
+ "\u014B": COMPAT_JAMO["\u3147"],
165
+ "f": COMPAT_JAMO["\u314D"],
166
+ "v": COMPAT_JAMO["\u3142"],
167
+ "\u03B8": COMPAT_JAMO["\u3145"],
168
+ "\xF0": COMPAT_JAMO["\u3137"],
169
+ "s": COMPAT_JAMO["\u3145"],
170
+ "z": COMPAT_JAMO["\u3148"],
171
+ "\u0283": COMPAT_JAMO["\u3145"],
172
+ "\u0292": COMPAT_JAMO["\u3148"],
173
+ "h": COMPAT_JAMO["\u314E"],
174
+ "l": COMPAT_JAMO["\u3139"],
175
+ "r": COMPAT_JAMO["\u3139"],
176
+ "\u0279": COMPAT_JAMO["\u3139"],
177
+ "t\u0283": COMPAT_JAMO["\u314A"],
178
+ "d\u0292": COMPAT_JAMO["\u3148"]
179
+ };
180
+ var VOWEL_TO_JUNGSEONG = {
181
+ // Semi-vowel + vowel combinations
182
+ "w\u025C\u02D0": [JUNGSEONG.WO],
183
+ "w\u025C": [JUNGSEONG.WO],
184
+ "w\u0259": [JUNGSEONG.WO],
185
+ "w\u025D": [JUNGSEONG.WO],
186
+ "w\u0254\u02D0": [JUNGSEONG.WO],
187
+ "w\u0254": [JUNGSEONG.WO],
188
+ "w\u0251\u02D0": [JUNGSEONG.WA],
189
+ "w\u0251": [JUNGSEONG.WA],
190
+ "w\u026A": [JUNGSEONG.WI],
191
+ "wi": [JUNGSEONG.WI],
192
+ "we\u026A": [JUNGSEONG.WE],
193
+ "ju\u02D0": [JUNGSEONG.YU],
194
+ "ju": [JUNGSEONG.YU],
195
+ "j\u0259": [JUNGSEONG.YEO],
196
+ "j\u025B": [JUNGSEONG.YEO],
197
+ "j\u0251\u02D0": [JUNGSEONG.YA],
198
+ "j\u0251": [JUNGSEONG.YA],
199
+ "j\u0254\u02D0": [JUNGSEONG.YO],
200
+ "j\u0254": [JUNGSEONG.YO],
201
+ "ji": [JUNGSEONG.I],
202
+ "j\u026A": [JUNGSEONG.I],
203
+ // Simple vowels
204
+ "i\u02D0": [JUNGSEONG.I],
205
+ "i": [JUNGSEONG.I],
206
+ "\u026A": [JUNGSEONG.I],
207
+ "e": [JUNGSEONG.E],
208
+ "\u025B": [JUNGSEONG.E],
209
+ "\xE6": [JUNGSEONG.AE],
210
+ "\u0251\u02D0": [JUNGSEONG.A],
211
+ "\u0251": [JUNGSEONG.A],
212
+ "\u0252": [JUNGSEONG.O],
213
+ "\u0254\u02D0": [JUNGSEONG.O],
214
+ "\u0254": [JUNGSEONG.O],
215
+ "\u028C": [JUNGSEONG.EO],
216
+ "\u0259": [JUNGSEONG.EO],
217
+ "\u025C\u02D0": [JUNGSEONG.EO],
218
+ "\u025C": [JUNGSEONG.EO],
219
+ "\u025D": [JUNGSEONG.EO],
220
+ "\u028A": [JUNGSEONG.U],
221
+ "u\u02D0": [JUNGSEONG.U],
222
+ "u": [JUNGSEONG.U],
223
+ // Diphthongs
224
+ "e\u026A": [JUNGSEONG.E, JUNGSEONG.I],
225
+ "a\u026A": [JUNGSEONG.A, JUNGSEONG.I],
226
+ "\u0254\u026A": [JUNGSEONG.O, JUNGSEONG.I],
227
+ "a\u028A": [JUNGSEONG.A, JUNGSEONG.U],
228
+ "\u0259\u028A": [JUNGSEONG.O],
229
+ "o\u028A": [JUNGSEONG.O],
230
+ "\u026A\u0259": [JUNGSEONG.I, JUNGSEONG.EO],
231
+ "e\u0259": [JUNGSEONG.E, JUNGSEONG.EO],
232
+ "\u028A\u0259": [JUNGSEONG.U, JUNGSEONG.EO]
233
+ };
234
+ function assembleHangul(cho, jung, jong = JONGSEONG.NONE) {
235
+ const code = 44032 + cho * 588 + jung * 28 + jong;
236
+ return String.fromCharCode(code);
237
+ }
238
+ function matchVowel(text, pos) {
239
+ for (let len = 4; len >= 1; len--) {
240
+ const substr = text.substring(pos, pos + len);
241
+ if (VOWEL_TO_JUNGSEONG[substr]) {
242
+ return substr;
243
+ }
244
+ }
245
+ return null;
246
+ }
247
+ function matchConsonant(text, pos) {
248
+ const twoChar = text.substring(pos, pos + 2);
249
+ if (CONSONANT_TO_CHOSEONG[twoChar] || CONSONANT_TO_JAMO[twoChar]) {
250
+ return twoChar;
251
+ }
252
+ const oneChar = text[pos];
253
+ if (CONSONANT_TO_CHOSEONG[oneChar] || CONSONANT_TO_JAMO[oneChar]) {
254
+ return oneChar;
255
+ }
256
+ return null;
257
+ }
258
+ function preprocessIPA(ipa) {
259
+ return ipa.replace(/\([^)]*\)/g, "").replace(/[ˈˌ′'\/\[\].]/g, "").trim();
260
+ }
261
+ function splitByLongVowel(text) {
262
+ const segments = [];
263
+ let current = "";
264
+ for (let i = 0; i < text.length; i++) {
265
+ if (text[i] === "\u02D0") {
266
+ if (current) {
267
+ segments.push({ text: current, hasLongVowel: true });
268
+ current = "";
269
+ }
270
+ } else {
271
+ current += text[i];
272
+ }
273
+ }
274
+ if (current) {
275
+ segments.push({ text: current, hasLongVowel: false });
276
+ }
277
+ return segments;
278
+ }
279
+ function tokenizeSegment(segment) {
280
+ const tokens = [];
281
+ let i = 0;
282
+ while (i < segment.length) {
283
+ const vowel = matchVowel(segment, i);
284
+ if (vowel) {
285
+ tokens.push({ type: "vowel", ipa: vowel, length: vowel.length });
286
+ i += vowel.length;
287
+ continue;
288
+ }
289
+ const consonant = matchConsonant(segment, i);
290
+ if (consonant) {
291
+ tokens.push({ type: "consonant", ipa: consonant, length: consonant.length });
292
+ i += consonant.length;
293
+ continue;
294
+ }
295
+ i++;
296
+ }
297
+ return tokens;
298
+ }
299
+ function convertSegment(tokens) {
300
+ const result = [];
301
+ let i = 0;
302
+ while (i < tokens.length) {
303
+ const token = tokens[i];
304
+ if (token.type === "consonant" && i + 1 < tokens.length && tokens[i + 1].type === "vowel") {
305
+ const consonant = token.ipa;
306
+ const vowel = tokens[i + 1].ipa;
307
+ const choIdx = CONSONANT_TO_CHOSEONG[consonant];
308
+ const jungIndices = VOWEL_TO_JUNGSEONG[vowel];
309
+ if (choIdx !== void 0 && jungIndices) {
310
+ let jongIdx = JONGSEONG.NONE;
311
+ let consumed = 2;
312
+ if (i + 2 < tokens.length && tokens[i + 2].type === "consonant") {
313
+ const nextCons = tokens[i + 2].ipa;
314
+ const jongMapping = CONSONANT_TO_JONGSEONG[nextCons];
315
+ const hasVowelAfter = i + 3 < tokens.length && tokens[i + 3].type === "vowel";
316
+ if (jongMapping !== void 0 && !hasVowelAfter) {
317
+ jongIdx = jongMapping;
318
+ consumed = 3;
319
+ }
320
+ }
321
+ if (jungIndices.length === 1) {
322
+ result.push(assembleHangul(choIdx, jungIndices[0], jongIdx));
323
+ } else {
324
+ result.push(assembleHangul(choIdx, jungIndices[0], JONGSEONG.NONE));
325
+ result.push(assembleHangul(CHOSEONG.IEUNG, jungIndices[1], jongIdx));
326
+ }
327
+ i += consumed;
328
+ continue;
329
+ }
330
+ }
331
+ if (token.type === "vowel") {
332
+ const vowel = token.ipa;
333
+ const jungIndices = VOWEL_TO_JUNGSEONG[vowel];
334
+ if (jungIndices) {
335
+ let jongIdx = JONGSEONG.NONE;
336
+ let consumed = 1;
337
+ if (i + 1 < tokens.length && tokens[i + 1].type === "consonant") {
338
+ const nextCons = tokens[i + 1].ipa;
339
+ const jongMapping = CONSONANT_TO_JONGSEONG[nextCons];
340
+ const hasVowelAfter = i + 2 < tokens.length && tokens[i + 2].type === "vowel";
341
+ if (jongMapping !== void 0 && !hasVowelAfter) {
342
+ jongIdx = jongMapping;
343
+ consumed = 2;
344
+ }
345
+ }
346
+ if (jungIndices.length === 1) {
347
+ result.push(assembleHangul(CHOSEONG.IEUNG, jungIndices[0], jongIdx));
348
+ } else {
349
+ result.push(assembleHangul(CHOSEONG.IEUNG, jungIndices[0], JONGSEONG.NONE));
350
+ result.push(assembleHangul(CHOSEONG.IEUNG, jungIndices[1], jongIdx));
351
+ }
352
+ i += consumed;
353
+ continue;
354
+ }
355
+ }
356
+ if (token.type === "consonant") {
357
+ const consonant = token.ipa;
358
+ const jamo = CONSONANT_TO_JAMO[consonant];
359
+ if (jamo) {
360
+ result.push(jamo);
361
+ }
362
+ }
363
+ i++;
364
+ }
365
+ return result.join("");
366
+ }
367
+ function ipaToHangul(ipa) {
368
+ if (!ipa) return "";
369
+ const cleaned = preprocessIPA(ipa);
370
+ if (!cleaned) return "";
371
+ const segments = splitByLongVowel(cleaned);
372
+ const convertedSegments = segments.map((seg) => {
373
+ const tokens = tokenizeSegment(seg.text);
374
+ const hangul = convertSegment(tokens);
375
+ if (seg.hasLongVowel) {
376
+ return hangul + "-";
377
+ }
378
+ return hangul;
379
+ });
380
+ return convertedSegments.join("");
381
+ }
382
+ // Annotate the CommonJS export names for ESM import in node:
383
+ 0 && (module.exports = {
384
+ ipaToHangul
385
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,360 @@
1
+ // src/index.ts
2
+ var CHOSEONG = {
3
+ GIYEOK: 0,
4
+ SSANGGIYEOK: 1,
5
+ NIEUN: 2,
6
+ DIGEUT: 3,
7
+ SSANGDIGEUT: 4,
8
+ RIEUL: 5,
9
+ MIEUM: 6,
10
+ BIEUP: 7,
11
+ SSANGBIEUP: 8,
12
+ SIOS: 9,
13
+ SSANGSIOS: 10,
14
+ IEUNG: 11,
15
+ JIEUT: 12,
16
+ SSANGJIEUT: 13,
17
+ CHIEUT: 14,
18
+ KIEUK: 15,
19
+ TIEUT: 16,
20
+ PIEUP: 17,
21
+ HIEUT: 18
22
+ };
23
+ var JUNGSEONG = {
24
+ A: 0,
25
+ AE: 1,
26
+ YA: 2,
27
+ YAE: 3,
28
+ EO: 4,
29
+ E: 5,
30
+ YEO: 6,
31
+ YE: 7,
32
+ O: 8,
33
+ WA: 9,
34
+ WAE: 10,
35
+ OE: 11,
36
+ YO: 12,
37
+ U: 13,
38
+ WO: 14,
39
+ WE: 15,
40
+ WI: 16,
41
+ YU: 17,
42
+ EU: 18,
43
+ UI: 19,
44
+ I: 20
45
+ };
46
+ var JONGSEONG = {
47
+ NONE: 0,
48
+ GIYEOK: 1,
49
+ NIEUN: 4,
50
+ DIGEUT: 7,
51
+ RIEUL: 8,
52
+ MIEUM: 16,
53
+ BIEUP: 17,
54
+ SIOS: 19,
55
+ IEUNG: 21,
56
+ JIEUT: 22,
57
+ CHIEUT: 23,
58
+ KIEUK: 24,
59
+ TIEUT: 25,
60
+ PIEUP: 26
61
+ };
62
+ var COMPAT_JAMO = {
63
+ "\u3131": "\u3131",
64
+ "\u3132": "\u3132",
65
+ "\u3134": "\u3134",
66
+ "\u3137": "\u3137",
67
+ "\u3139": "\u3139",
68
+ "\u3141": "\u3141",
69
+ "\u3142": "\u3142",
70
+ "\u3145": "\u3145",
71
+ "\u3146": "\u3146",
72
+ "\u3147": "\u3147",
73
+ "\u3148": "\u3148",
74
+ "\u314A": "\u314A",
75
+ "\u314B": "\u314B",
76
+ "\u314C": "\u314C",
77
+ "\u314D": "\u314D",
78
+ "\u314E": "\u314E"
79
+ };
80
+ var CONSONANT_TO_CHOSEONG = {
81
+ "p": CHOSEONG.PIEUP,
82
+ "b": CHOSEONG.BIEUP,
83
+ "t": CHOSEONG.TIEUT,
84
+ "d": CHOSEONG.DIGEUT,
85
+ "k": CHOSEONG.KIEUK,
86
+ "g": CHOSEONG.GIYEOK,
87
+ "m": CHOSEONG.MIEUM,
88
+ "n": CHOSEONG.NIEUN,
89
+ "\u014B": CHOSEONG.IEUNG,
90
+ "f": CHOSEONG.PIEUP,
91
+ "v": CHOSEONG.BIEUP,
92
+ "\u03B8": CHOSEONG.SIOS,
93
+ "\xF0": CHOSEONG.DIGEUT,
94
+ "s": CHOSEONG.SIOS,
95
+ "z": CHOSEONG.JIEUT,
96
+ "\u0283": CHOSEONG.SIOS,
97
+ "\u0292": CHOSEONG.JIEUT,
98
+ "h": CHOSEONG.HIEUT,
99
+ "t\u0283": CHOSEONG.CHIEUT,
100
+ "d\u0292": CHOSEONG.JIEUT,
101
+ "l": CHOSEONG.RIEUL,
102
+ "r": CHOSEONG.RIEUL,
103
+ "\u0279": CHOSEONG.RIEUL,
104
+ "w": CHOSEONG.IEUNG,
105
+ "j": CHOSEONG.IEUNG
106
+ };
107
+ var CONSONANT_TO_JONGSEONG = {
108
+ "p": JONGSEONG.BIEUP,
109
+ "b": JONGSEONG.BIEUP,
110
+ "t": JONGSEONG.TIEUT,
111
+ "d": JONGSEONG.DIGEUT,
112
+ "k": JONGSEONG.KIEUK,
113
+ "g": JONGSEONG.GIYEOK,
114
+ "m": JONGSEONG.MIEUM,
115
+ "n": JONGSEONG.NIEUN,
116
+ "\u014B": JONGSEONG.IEUNG,
117
+ "f": JONGSEONG.PIEUP,
118
+ "v": JONGSEONG.BIEUP,
119
+ "\u03B8": JONGSEONG.SIOS,
120
+ "\xF0": JONGSEONG.DIGEUT,
121
+ "s": JONGSEONG.SIOS,
122
+ "z": JONGSEONG.JIEUT,
123
+ "\u0283": JONGSEONG.SIOS,
124
+ "\u0292": JONGSEONG.JIEUT,
125
+ "l": JONGSEONG.RIEUL,
126
+ "r": JONGSEONG.RIEUL,
127
+ "\u0279": JONGSEONG.RIEUL,
128
+ "t\u0283": JONGSEONG.CHIEUT,
129
+ "d\u0292": JONGSEONG.JIEUT
130
+ };
131
+ var CONSONANT_TO_JAMO = {
132
+ "p": COMPAT_JAMO["\u314D"],
133
+ "b": COMPAT_JAMO["\u3142"],
134
+ "t": COMPAT_JAMO["\u314C"],
135
+ "d": COMPAT_JAMO["\u3137"],
136
+ "k": COMPAT_JAMO["\u314B"],
137
+ "g": COMPAT_JAMO["\u3131"],
138
+ "m": COMPAT_JAMO["\u3141"],
139
+ "n": COMPAT_JAMO["\u3134"],
140
+ "\u014B": COMPAT_JAMO["\u3147"],
141
+ "f": COMPAT_JAMO["\u314D"],
142
+ "v": COMPAT_JAMO["\u3142"],
143
+ "\u03B8": COMPAT_JAMO["\u3145"],
144
+ "\xF0": COMPAT_JAMO["\u3137"],
145
+ "s": COMPAT_JAMO["\u3145"],
146
+ "z": COMPAT_JAMO["\u3148"],
147
+ "\u0283": COMPAT_JAMO["\u3145"],
148
+ "\u0292": COMPAT_JAMO["\u3148"],
149
+ "h": COMPAT_JAMO["\u314E"],
150
+ "l": COMPAT_JAMO["\u3139"],
151
+ "r": COMPAT_JAMO["\u3139"],
152
+ "\u0279": COMPAT_JAMO["\u3139"],
153
+ "t\u0283": COMPAT_JAMO["\u314A"],
154
+ "d\u0292": COMPAT_JAMO["\u3148"]
155
+ };
156
+ var VOWEL_TO_JUNGSEONG = {
157
+ // Semi-vowel + vowel combinations
158
+ "w\u025C\u02D0": [JUNGSEONG.WO],
159
+ "w\u025C": [JUNGSEONG.WO],
160
+ "w\u0259": [JUNGSEONG.WO],
161
+ "w\u025D": [JUNGSEONG.WO],
162
+ "w\u0254\u02D0": [JUNGSEONG.WO],
163
+ "w\u0254": [JUNGSEONG.WO],
164
+ "w\u0251\u02D0": [JUNGSEONG.WA],
165
+ "w\u0251": [JUNGSEONG.WA],
166
+ "w\u026A": [JUNGSEONG.WI],
167
+ "wi": [JUNGSEONG.WI],
168
+ "we\u026A": [JUNGSEONG.WE],
169
+ "ju\u02D0": [JUNGSEONG.YU],
170
+ "ju": [JUNGSEONG.YU],
171
+ "j\u0259": [JUNGSEONG.YEO],
172
+ "j\u025B": [JUNGSEONG.YEO],
173
+ "j\u0251\u02D0": [JUNGSEONG.YA],
174
+ "j\u0251": [JUNGSEONG.YA],
175
+ "j\u0254\u02D0": [JUNGSEONG.YO],
176
+ "j\u0254": [JUNGSEONG.YO],
177
+ "ji": [JUNGSEONG.I],
178
+ "j\u026A": [JUNGSEONG.I],
179
+ // Simple vowels
180
+ "i\u02D0": [JUNGSEONG.I],
181
+ "i": [JUNGSEONG.I],
182
+ "\u026A": [JUNGSEONG.I],
183
+ "e": [JUNGSEONG.E],
184
+ "\u025B": [JUNGSEONG.E],
185
+ "\xE6": [JUNGSEONG.AE],
186
+ "\u0251\u02D0": [JUNGSEONG.A],
187
+ "\u0251": [JUNGSEONG.A],
188
+ "\u0252": [JUNGSEONG.O],
189
+ "\u0254\u02D0": [JUNGSEONG.O],
190
+ "\u0254": [JUNGSEONG.O],
191
+ "\u028C": [JUNGSEONG.EO],
192
+ "\u0259": [JUNGSEONG.EO],
193
+ "\u025C\u02D0": [JUNGSEONG.EO],
194
+ "\u025C": [JUNGSEONG.EO],
195
+ "\u025D": [JUNGSEONG.EO],
196
+ "\u028A": [JUNGSEONG.U],
197
+ "u\u02D0": [JUNGSEONG.U],
198
+ "u": [JUNGSEONG.U],
199
+ // Diphthongs
200
+ "e\u026A": [JUNGSEONG.E, JUNGSEONG.I],
201
+ "a\u026A": [JUNGSEONG.A, JUNGSEONG.I],
202
+ "\u0254\u026A": [JUNGSEONG.O, JUNGSEONG.I],
203
+ "a\u028A": [JUNGSEONG.A, JUNGSEONG.U],
204
+ "\u0259\u028A": [JUNGSEONG.O],
205
+ "o\u028A": [JUNGSEONG.O],
206
+ "\u026A\u0259": [JUNGSEONG.I, JUNGSEONG.EO],
207
+ "e\u0259": [JUNGSEONG.E, JUNGSEONG.EO],
208
+ "\u028A\u0259": [JUNGSEONG.U, JUNGSEONG.EO]
209
+ };
210
+ function assembleHangul(cho, jung, jong = JONGSEONG.NONE) {
211
+ const code = 44032 + cho * 588 + jung * 28 + jong;
212
+ return String.fromCharCode(code);
213
+ }
214
+ function matchVowel(text, pos) {
215
+ for (let len = 4; len >= 1; len--) {
216
+ const substr = text.substring(pos, pos + len);
217
+ if (VOWEL_TO_JUNGSEONG[substr]) {
218
+ return substr;
219
+ }
220
+ }
221
+ return null;
222
+ }
223
+ function matchConsonant(text, pos) {
224
+ const twoChar = text.substring(pos, pos + 2);
225
+ if (CONSONANT_TO_CHOSEONG[twoChar] || CONSONANT_TO_JAMO[twoChar]) {
226
+ return twoChar;
227
+ }
228
+ const oneChar = text[pos];
229
+ if (CONSONANT_TO_CHOSEONG[oneChar] || CONSONANT_TO_JAMO[oneChar]) {
230
+ return oneChar;
231
+ }
232
+ return null;
233
+ }
234
+ function preprocessIPA(ipa) {
235
+ return ipa.replace(/\([^)]*\)/g, "").replace(/[ˈˌ′'\/\[\].]/g, "").trim();
236
+ }
237
+ function splitByLongVowel(text) {
238
+ const segments = [];
239
+ let current = "";
240
+ for (let i = 0; i < text.length; i++) {
241
+ if (text[i] === "\u02D0") {
242
+ if (current) {
243
+ segments.push({ text: current, hasLongVowel: true });
244
+ current = "";
245
+ }
246
+ } else {
247
+ current += text[i];
248
+ }
249
+ }
250
+ if (current) {
251
+ segments.push({ text: current, hasLongVowel: false });
252
+ }
253
+ return segments;
254
+ }
255
+ function tokenizeSegment(segment) {
256
+ const tokens = [];
257
+ let i = 0;
258
+ while (i < segment.length) {
259
+ const vowel = matchVowel(segment, i);
260
+ if (vowel) {
261
+ tokens.push({ type: "vowel", ipa: vowel, length: vowel.length });
262
+ i += vowel.length;
263
+ continue;
264
+ }
265
+ const consonant = matchConsonant(segment, i);
266
+ if (consonant) {
267
+ tokens.push({ type: "consonant", ipa: consonant, length: consonant.length });
268
+ i += consonant.length;
269
+ continue;
270
+ }
271
+ i++;
272
+ }
273
+ return tokens;
274
+ }
275
+ function convertSegment(tokens) {
276
+ const result = [];
277
+ let i = 0;
278
+ while (i < tokens.length) {
279
+ const token = tokens[i];
280
+ if (token.type === "consonant" && i + 1 < tokens.length && tokens[i + 1].type === "vowel") {
281
+ const consonant = token.ipa;
282
+ const vowel = tokens[i + 1].ipa;
283
+ const choIdx = CONSONANT_TO_CHOSEONG[consonant];
284
+ const jungIndices = VOWEL_TO_JUNGSEONG[vowel];
285
+ if (choIdx !== void 0 && jungIndices) {
286
+ let jongIdx = JONGSEONG.NONE;
287
+ let consumed = 2;
288
+ if (i + 2 < tokens.length && tokens[i + 2].type === "consonant") {
289
+ const nextCons = tokens[i + 2].ipa;
290
+ const jongMapping = CONSONANT_TO_JONGSEONG[nextCons];
291
+ const hasVowelAfter = i + 3 < tokens.length && tokens[i + 3].type === "vowel";
292
+ if (jongMapping !== void 0 && !hasVowelAfter) {
293
+ jongIdx = jongMapping;
294
+ consumed = 3;
295
+ }
296
+ }
297
+ if (jungIndices.length === 1) {
298
+ result.push(assembleHangul(choIdx, jungIndices[0], jongIdx));
299
+ } else {
300
+ result.push(assembleHangul(choIdx, jungIndices[0], JONGSEONG.NONE));
301
+ result.push(assembleHangul(CHOSEONG.IEUNG, jungIndices[1], jongIdx));
302
+ }
303
+ i += consumed;
304
+ continue;
305
+ }
306
+ }
307
+ if (token.type === "vowel") {
308
+ const vowel = token.ipa;
309
+ const jungIndices = VOWEL_TO_JUNGSEONG[vowel];
310
+ if (jungIndices) {
311
+ let jongIdx = JONGSEONG.NONE;
312
+ let consumed = 1;
313
+ if (i + 1 < tokens.length && tokens[i + 1].type === "consonant") {
314
+ const nextCons = tokens[i + 1].ipa;
315
+ const jongMapping = CONSONANT_TO_JONGSEONG[nextCons];
316
+ const hasVowelAfter = i + 2 < tokens.length && tokens[i + 2].type === "vowel";
317
+ if (jongMapping !== void 0 && !hasVowelAfter) {
318
+ jongIdx = jongMapping;
319
+ consumed = 2;
320
+ }
321
+ }
322
+ if (jungIndices.length === 1) {
323
+ result.push(assembleHangul(CHOSEONG.IEUNG, jungIndices[0], jongIdx));
324
+ } else {
325
+ result.push(assembleHangul(CHOSEONG.IEUNG, jungIndices[0], JONGSEONG.NONE));
326
+ result.push(assembleHangul(CHOSEONG.IEUNG, jungIndices[1], jongIdx));
327
+ }
328
+ i += consumed;
329
+ continue;
330
+ }
331
+ }
332
+ if (token.type === "consonant") {
333
+ const consonant = token.ipa;
334
+ const jamo = CONSONANT_TO_JAMO[consonant];
335
+ if (jamo) {
336
+ result.push(jamo);
337
+ }
338
+ }
339
+ i++;
340
+ }
341
+ return result.join("");
342
+ }
343
+ function ipaToHangul(ipa) {
344
+ if (!ipa) return "";
345
+ const cleaned = preprocessIPA(ipa);
346
+ if (!cleaned) return "";
347
+ const segments = splitByLongVowel(cleaned);
348
+ const convertedSegments = segments.map((seg) => {
349
+ const tokens = tokenizeSegment(seg.text);
350
+ const hangul = convertSegment(tokens);
351
+ if (seg.hasLongVowel) {
352
+ return hangul + "-";
353
+ }
354
+ return hangul;
355
+ });
356
+ return convertedSegments.join("");
357
+ }
358
+ export {
359
+ ipaToHangul
360
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "ipa-hangul",
3
+ "version": "1.0.0",
4
+ "description": "Convert IPA (International Phonetic Alphabet) pronunciation to Korean Hangul",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "ipa",
24
+ "hangul",
25
+ "korean",
26
+ "pronunciation",
27
+ "phonetic",
28
+ "converter"
29
+ ],
30
+ "author": "YSW",
31
+ "license": "MIT",
32
+ "devDependencies": {
33
+ "typescript": "^5.0.0",
34
+ "tsup": "^8.0.0"
35
+ }
36
+ }