UNPKG

koroman

Version:

KOROMAN: Korean Romanizer with pronunciation rules based on 국립국어원 표기법

238 lines (196 loc) 11.1 kB
// @name: koroman.core.js // @project: Koroman // @author: Donghe Youn (Daissue) // @date: 2025-04-02 // @description: This module provides core functionality for romanizing Korean Hangul text. // It includes functions to decompose Hangul syllables into their constituent jamo (initial, medial, final), // apply Korean pronunciation rules, and convert them into Latin (Romanized) script. // This module is used by both CommonJS and ESModule entry points and is not intended to be used directly. // @license: MIT License // @version: 1.0.0 // @dependencies: None // @usage: Import this from koroman.mjs or koroman.cjs to access the romanize() function. // 자모 분해 및 조합 + 로마자 변환 (초성/중성/종성 실제 유니코드 문자 사용 버전) // 초성: 19자 (U+1100~U+1112) const CHO = [ "ᄀ", "ᄁ", "ᄂ", "ᄃ", "ᄄ", "ᄅ", "ᄆ", "ᄇ", "ᄈ", "ᄉ", "ᄊ", "ᄋ", "ᄌ", "ᄍ", "ᄎ", "ᄏ", "ᄐ", "ᄑ", "ᄒ" ]; // 중성: 21자 (U+1161~U+1175) const JUNG = [ "ᅡ", "ᅢ", "ᅣ", "ᅤ", "ᅥ", "ᅦ", "ᅧ", "ᅨ", "ᅩ", "ᅪ", "ᅫ", "ᅬ", "ᅭ", "ᅮ", "ᅯ", "ᅰ", "ᅱ", "ᅲ", "ᅳ", "ᅴ", "ᅵ" ]; // 종성: 28자 (첫 번째는 없음, 나머지 U+11A8~U+11C2) const JONG = [ "", "ᆨ", "ᆩ", "ᆪ", "ᆫ", "ᆬ", "ᆭ", "ᆮ", "ᆯ", "ᆰ", "ᆱ", "ᆲ", "ᆳ", "ᆴ", "ᆵ", "ᆶ", "ᆷ", "ᆸ", "ᆹ", "ᆺ", "ᆻ", "ᆼ", "ᆽ", "ᆾ", "ᆿ", "ᇀ", "ᇁ", "ᇂ" ]; const ZWSP = '\u200A'; // Hair Space (조합 방지용) // const ZWSP = '\u200B'; // zero-width space (조합 방지용) function formatRoman(str, casingOption = "lowercase") { switch (casingOption) { case "uppercase": return str.toUpperCase(); case "capitalize-line": return str.split('\n').map(line => line.length > 0 ? line.charAt(0).toUpperCase() + line.slice(1) : '').join('\n'); case "capitalize-word": return str.split('\n').map(line => line.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')).join('\n'); default: return str.toLowerCase(); } } function splitHangulToJamos(str) { const result = []; let jamoString = ''; let plainJamoString = ''; for (let char of str) { const code = char.charCodeAt(0); if (code < 0xAC00 || code > 0xD7A3) { result.push({ char, type: 'non-hangul' }); jamoString += char; plainJamoString += char; continue; } const syllableIndex = code - 0xAC00; const cho = Math.floor(syllableIndex / (21 * 28)); const jung = Math.floor((syllableIndex % (21 * 28)) / 28); const jong = syllableIndex % 28; const jamo = { 초성: CHO[cho], 중성: JUNG[jung], 종성: JONG[jong] || null }; result.push(jamo); jamoString += jamo.초성 + jamo.중성 + (jamo.종성 || ''); plainJamoString += jamo.초성 + ZWSP + jamo.중성; if (jamo.종성) plainJamoString += ZWSP + jamo.종성; } return { jamoArray: result, jamoString, plainJamoString }; } function composeJamos(jamoArray) { let result = ''; for (let jamo of jamoArray) { if (jamo.type === 'non-hangul') { result += jamo.char; continue; } const choIndex = CHO.indexOf(jamo.초성); const jungIndex = JUNG.indexOf(jamo.중성); const jongIndex = jamo.종성 ? JONG.indexOf(jamo.종성) : 0; if (choIndex < 0 || jungIndex < 0 || jongIndex < 0) { result += '?'; continue; } const code = 0xAC00 + (choIndex * 21 + jungIndex) * 28 + jongIndex; result += String.fromCharCode(code); } return result; } function applyPronunciationRules(jamoStr) { const replaceArr = [ // ============================== // 1. 무효화 처리 // ============================== { p: /\u11a7/g, r: "" }, // ''(U+11A7) → 제거 (사용되지 않는 종성) // ============================== // 2. 비음화 (ㄴ, ㅁ, ㅇ) // ============================== { p: /[\u11b8\u11c1\u11b9\u11b2\u11b5](?=[\u1102\u1106])/g, r: "ᆷ" }, // 종성 'ᆸ(ㅂ)' 'ᇁ(ㅍ)' 'ᆹ(ㅂㅅ)' 'ᆲ(ㄹㅂ)' 'ᆵ(ㄹㅍ)' + 다음 초성 'ᄂ(ㄴ)' or 'ᄆ(ㅁ)' → '' { p: /[\u11ae\u11c0\u11bd\u11be\u11ba\u11bb\u11c2](?=[\u1102\u1106])/g, r: "ᆫ" }, // 종성 'ᆮ(ㄷ)' 'ᇀ(ㅌ)' 'ᆽ(ㅈ)' 'ᆾ(ㅊ)' 'ᆺ(ㅅ)' 'ᆻ(ㅆ)' 'ᇂ(ㅎ)' + 다음 초성 'ᄂ(ㄴ)' or 'ᄆ(ㅁ)' → '' { p: /[\u11a8\u11a9\u11bf\u11aa\u11b0](?=[\u1102\u1106])/g, r: "ᆼ" }, // 종성 'ᆨ(ㄱ)' 'ᆩ(ㄲ)' 'ᆿ(ㅋ)' 'ᆪ(ㄱㅅ)' 'ᆰ(ㄹㄱ)' + 다음 초성 ''/'' → '' // ============================== // 3. 연음/연철 // ============================== { p: /\u11a8\u110b(?=[\u1163\u1164\u1167\u1168\u116d\u1172])/g, r: "ᆼᄂ" }, // '' + '' + 중성 'ㅑㅒㅕㅖㅛㅠ' → 'ᆼᄂ' (연음화) { p: /\u11af\u110b(?=[\u1163\u1164\u1167\u1168\u116d\u1172])/g, r: "ᆯᄅ" }, // '' + '' + 중성 위와 같음 → 'ᆯᄅ' { p: /[\u11a8\u11bc]\u1105/g, r: "ᆼᄂ" }, // 'ᆨ(ㄱ)', 'ᆼ(ㅇ)' + 'ᄅ(ㄹ)' → 'ᆼᄂ' { p: /\u11ab\u1105(?=\u1169)/g, r: "ᆫᄂ" }, // 'ᆫ(ㄴ)' + '' + 중성 '' → 'ᆫᄂ' { p: /\u11af\u1102|\u11ab\u1105/g, r: "ᆯᄅ" }, // 'ᆯ(ㄹ)' + 'ᄂ(ㄴ)', 'ᆫ(ㄴ)' + 'ᄅ(ㄹ)' → 'ᆯᄅ' { p: /[\u11b7\u11b8]\u1105/g, r: "ᆷᄂ" }, // 'ᆷ(ㅁ)', 'ᆸ(ㅂ)' + '' → 'ᆷᄂ' { p: /\u11b0\u1105/g, r: "ᆨᄅ" }, // 'ᆰ(ㄹㄱ)' + '' → 'ᆨᄅ' // ============================== // 4. 격음화 / 자음군 분해 // ============================== { p: /\u11a8\u110f/g, r: "ᆨ-ᄏ" }, // '' + '' → 'ᆨ-ᄏ' { p: /\u11b8\u1111/g, r: "ᆸ-ᄑ" }, // '' + '' → 'ᆸ-ᄑ' { p: /\u11ae\u1110/g, r: "ᆮ-ᄐ" }, // '' + '' → 'ᆮ-ᄐ' // ============================== // 5. 복합 종성 분해 // ============================== { p: /\u11aa/g, r: "ᆨᆺ" }, // 'ᆪ(ㄱㅅ)' → 'ᆨᆺ' { p: /\u11ac/g, r: "ᆫᆽ" }, // 'ᆬ(ㄴㅈ)' → 'ᆫᆽ' { p: /\u11ad/g, r: "ᆫᇂ" }, // 'ᆭ(ㄴㅎ)' → 'ᆫᇂ' { p: /\u11b0/g, r: "ᆯᆨ" }, // 'ᆰ(ㄹㄱ)' → 'ᆯᆨ' { p: /\u11b1/g, r: "ᆯᆷ" }, // 'ᆱ(ㄹㅁ)' → 'ᆯᆷ' { p: /\u11b2/g, r: "ᆯᆸ" }, // 'ᆲ(ㄹㅂ)' → 'ᆯᆸ' { p: /\u11b3/g, r: "ᆯᆺ" }, // 'ᆳ(ㄹㅅ)' → 'ᆯᆺ' { p: /\u11b4/g, r: "ᆯᇀ" }, // 'ᆴ(ㄹㅌ)' → 'ᆯᇀ' { p: /\u11b5/g, r: "ᆯᇁ" }, // 'ᆵ(ㄹㅍ)' → 'ᆯᇁ' { p: /\u11b6/g, r: "ᆯᇂ" }, // 'ᆶ(ㄹㅎ)' → 'ᆯᇂ' { p: /\u11b9/g, r: "ᆸᆺ" }, // 'ᆹ(ㅂㅅ)' → 'ᆸᆺ' // ============================== // 6. 경음화/축약 등 특수 규칙 // ============================== { p: /\u11ae\u110b\u1175/g, r: "지" }, // '' + '' + '' → '' { p: /\u11c0\u110b\u1175/g, r: "치" }, // '' + '' + '' → '' // ============================== // 7. 받침 탈락 또는 이음자 제거 // ============================== { p: /\u11a8\u110b/g, r: "ᄀ" }, // '' + '' → '' { p: /\u11a9\u110b/g, r: "ᄁ" }, // '' + '' → '' { p: /\u11ae\u110b/g, r: "ᄃ" }, // '' + '' → '' { p: /\u11af\u110b/g, r: "ᄅ" }, // '' + '' → '' { p: /\u11b8\u110b/g, r: "ᄇ" }, // '' + '' → '' { p: /\u11ba\u110b/g, r: "ᄉ" }, // '' + '' → '' { p: /\u11bb\u110b/g, r: "ᄊ" }, // '' + '' → '' { p: /\u11bd\u110b/g, r: "ᄌ" }, // '' + '' → '' { p: /\u11be\u110b/g, r: "ᄎ" }, // '' + '' → '' { p: /\u11c2\u110b/g, r: "" }, // '' + '' → 제거 // ============================== // 8. 격음화 (종성 + ㅎ/히읗) // ============================== { p: /\u11c2\u1100|\u11a8\u1112/g, r: "ᄏ" }, // ''+'' 또는 ''+'' → '' { p: /\u11c2\u1103|\u11ae\u1112/g, r: "ᄐ" }, // ''+'' 또는 ''+'' → '' { p: /\u11c2\u110c|\u11bd\u1112/g, r: "ᄎ" }, // ''+'' 또는 ''+'' → '' { p: /\u11c2\u1107/g, r: "ᄇ" }, // ''+'' → '' { p: /\u11b8\u1112/g, r: "ᄑ" }, // ''+'' → '' // ============================== // 9. 특수 처리 및 최종 정리 // ============================== { p: /\u11af\u1105/g, r: "ll" }, // '' + '' → ll { p: /\u11c2(?!\s|$)/g, r: "" }, // '' (종성) 단독 → 제거 { p: /([\u11a8-\u11c2])([\u11a8-\u11c2])/g, r: "$1" } // 이중 종성 제거 ]; for (const { p, r } of replaceArr) { jamoStr = jamoStr.replace(p, r); } return jamoStr; } function applyRomanMapping(jamoStr) { const map = { '': 'g', '': 'kk', '': 'n', '': 'd', '': 'tt', '': 'r', '': 'm', '': 'b', '': 'pp', '': 's', '': 'ss', '': '', '': 'j', '': 'jj', '': 'ch', '': 'k', '': 't', '': 'p', '': 'h', '': 'a', '': 'ae', '': 'ya', '': 'yae', '': 'eo', '': 'e', '': 'yeo', '': 'ye', '': 'o', '': 'wa', '': 'wae', '': 'oe', '': 'yo', '': 'u', '': 'wo', '': 'we', '': 'wi', '': 'yu', '': 'eu', '': 'ui', '': 'i', '': 'k', '': 'k', '': 'k', '': 'n', '': 'n', '': 'n', '': 'd', '': 'l', '': 'k', '': 'm', '': 'p', '': 't', '': 't', '': 'p', '': 'h', '': 'm', '': 'p', '': 'p', '': 't', '': 't', '': 'ng', '': 't', '': 't', '': 'k', '': 't', '': 'p', '': 'h' }; return [...jamoStr].map(ch => map[ch] ?? ch).join(''); } function romanize(str, { usePronunciationRules = true, casingOption = "lowercase" } = {}) { const { jamoString } = splitHangulToJamos(str); const replaced = usePronunciationRules ? applyPronunciationRules(jamoString) : jamoString; const romanized = applyRomanMapping(replaced); return formatRoman(romanized, casingOption); } export { romanize, splitHangulToJamos, composeJamos, formatRoman, applyPronunciationRules, applyRomanMapping };