import CallingNumberKorean from "./callingNumberKorean.js"; import FloatKorean from "./floatKorean.js"; import IntegerKorean from "./integerKorean.js"; import PhoneNumberKorean from "./phoneNumberKorean.js"; import EmojiDescriptions from "./emoji-descriptions.json" with { type: "json" }; // Process trim tailing dots export function processUnsounds(input: string): string { return ( input // Change tailing dots .replace(/[.,]+$/, "") .replace(/[.,]{2,}/g, "") .replace(/[.,]\s/g, " ") .replace(/[(){}[]]/g, " ") ); } // 여러가지 이상한 소리를 내도록 만드는 프롬프트나 // 소리를 지르도록 하는 프롬프트를 필터링 합니다. // 예를들어... // 흐아..하아아.. // 아!아!아!아!아! // 흐으..흐아아..헤...하아.. // 혀어어어어어어어엉........ 핫. 혀엉..... 흑... 하앗... 흐윽... 형. 하앙. // 혀엉.... 하앙... 흐윽... 항. 항. 형... 하앙. 흐으윽... 형... 흡... 혀엉.. // 하아아앗. 혀엉.. 흡... 흐읍... 형.. 하앗. 하아앙... 형... 하앙... 흐윽... // 혀어어엉.. 하앙. 항. 형... 하앙. 혀엉.... 하앙. 흑... 항. 형... 흡 하앗. // 혀엉..... 흑. 흣 // .. 하앗!. 하!아앙~형..~. 하!~앙... 흐윽... // 혀!어어엉.. !앙. 항. !형...~ 하앙.!... 하앙~흑!... 항. 형..~! 흡 하앗. // 혀엉...!.. 흑. 흣! export function processCensor(input: string): string { return input .replace( /([\S!?~]+)([!?~]*)/g, (_, content: string, tailSymbol: string) => content + tailSymbol, ) .replace( /([흐하해헤혀형][아으앙응앗웅응ㅡ!?.,><~'"/]+)/g, (content: string) => content[0] ?? "", ) .replace(processCensor.StrangeRepeatableRegex, (content: string) => content.substring(0, 3), ); } export namespace processCensor { // prettier-ignore export const StrangeRepeatable = [ "아", "ㅏ", "어", "ㅓ", "으", "ㅡ", "우", "ㅜ", "에", "오", "ㅗ", "야", "ㅑ", "읍", "앙", "읏", "웃", "엉", "앗", "엣", "웅", "응", "흐", "해", "헤", "헼", "헥", "하", "형", "혀", "흡", "흑", "협", "혓", "핫", "헵", "햅", "잇", "あ", "ア", "う", "お", "ー", "a", "A", "o", "O", "u", "U", ]; export const StrangeRepeatableRegex = new RegExp( `[${StrangeRepeatable.join("")}][${StrangeRepeatable.join("")}!.,><~'"/]{2,}`, "g", ); } // 핵토파스칼, 바, 핵타르 AU (에이커 인치 피트 야드) // Process korean letter, choseong shortens export function processKorean(input: string): string { return input.replace(/[ㄱ-ㅎㄲㄸㅃㅆㅉ]+/g, (i) => i .replace(processKorean.DoubleMixedChoseongMapRegex, (content: string) => { // ㅇㅋ => 오키, ㅇㄴ => 아니, ... return processKorean.DoubleMixedChoseongMap[ content as keyof typeof processKorean.DoubleMixedChoseongMap ]; }) .replace(processKorean.RepeatedChoseongMapRegex, (content: string) => { // process ㄴㄴ ㄱㄱ ㅋㅋ ㄷㄷ, ... const key = (content[0] ?? "") as keyof typeof processKorean.RepeatedChoseongMap; const item = processKorean.RepeatedChoseongMap[key]; if (typeof item == "string") { return item; } else if (typeof item == "function") { return item(content); } return content; }) .replace( /[ㄱ-ㅎㄲㄸㅃㅆㅉ]/g, (char: string) => processKorean.ChoseongMap[ char as keyof typeof processKorean.ChoseongMap ] ?? char, ), ); } export namespace processKorean { export const DoubleMixedChoseongMap = { ㅈㅇㅅㄹㅎㅅㅇ: "좋은사랑하세요", ㅇㅃㅅㄹㅎㅅㅇ: "이쁜사랑하세요", ㅈㅇㅅㅀㅅㅇ: "좋은사랑하세요", ㅇㅃㅅㅀㅅㅇ: "이쁜사랑하세요", ㅇㅃㅅㄹ: "이쁜사랑", ㅇㅈㄹ: "이지랄", ㅈㄹㄴ: "지랄노", ㅁㅇ: "모야", ㅎㅇ: "하이", ㅅㄹ: "싫어", ㄱㄷ: "기달", ㅈㅂ: "제발", ㅁㄹ: "몰라", ㅅㅂ: "시바", ㅇㄷ: "어디", ㄴㅈ: "노잼", ㅂㅂ: "바바", ㅂㅇ: "바이", ㅈㅅ: "죄송", ㅇㄴ: "아니", ㅃㄹ: "빨리", ㅇㅈ: "인정", ㄴㄴ: "노노", ㄱㅅ: "감사", ㅉㅉ: "쯧쯧", ㅈㄹ: "지랄", ㄹㅇ: "리얼", ㅇㅎ: "아하", ㅇㅋ: "오키", ㅊㅋ: "추카", ㄲㅈ: "꺼져", ㅈㅁ: "잠깐만", ㅈㄴ: "존나", ㄱㄴ: "가능", }; export const DoubleMixedChoseongMapRegex = new RegExp( Object.keys(DoubleMixedChoseongMap) .map((k) => `(?:${k})`) .join("|"), "g", ); export const RepeatedChoseongMap = { ㅌ: "틔틔", ㄷ: "덜덜", ㄴ: "노노", ㅇ: "응응", ㅊ: "추추", ㅠ: "유유", ㅑ: "야야", ㅋ: (content: string) => "크".repeat(content.length), ㅎ: (content: string) => "흐".repeat(content.length), ㄱ: (content: string) => { if (content.length == 2) { return "고고"; } else if (content.length == 3) { return "고고고"; } return content; }, }; export const RepeatedChoseongMapRegex = new RegExp( Object.keys(RepeatedChoseongMap) .map((k) => `${k}{2,}`) .join("|"), "g", ); // prettier-ignore export const ChoseongMap = { ㄱ: "기역", ㄴ: "니은", ㄷ: "디귿", ㄹ: "리을", ㅁ: "미음", ㅂ: "비읍", ㅅ: "시옷", ㅇ: "이응", ㅈ: "지읒", ㅊ: "치읓", ㅋ: "키읔", ㅌ: "티읕", ㅍ: "피읖", ㅎ: "히읗", ㄲ: "쌍기역", ㄸ: "쌍디귿", ㅃ: "쌍비읍", ㅆ: "쌍시옷", ㅉ: "쌍지읒", }; } // Process 10km 1,000 1.1, ... numbers export function processNumber(input: string): string { return input .replace( /(\+\d+[\s-]+)?([\d-]+)/g, (_, prefix: string | undefined, phone: string) => { const all = (prefix ?? "") + phone; if (!phone.includes("-")) return all; return PhoneNumberKorean.convert(all); }, ) .replace( /([\d,]+)(?:(?[kKMmgGtTpPeEzZyY][iI]?)(?[bB])|(?[m]?)(?[lL])|(?[mck]?)(?m))(?[^a-zA-Z])/g, (_, num: string, ...last: any): string => { const group = last[last.length - 1] as { prefix: string; unit: string; tail: string; }; const tail = group.tail; const unit = group.unit.toLocaleLowerCase(); const numStr = IntegerKorean.convertFromString(num); let prefix = group.prefix; if (unit == "b") { // 10kib => 십키비바이트 prefix = processNumber.DatasizePrefix[ prefix.toLowerCase() as keyof typeof processNumber.DatasizePrefix ]; return `${numStr} ${prefix}바이트 ${tail}`; } if (unit == "l") { // 10l => 십리터 prefix = processNumber.LiterPrefix[ prefix.toLowerCase() as keyof typeof processNumber.LiterPrefix ]; return `${numStr} ${prefix}리터 ${tail}`; } if (unit == "m") { // 10m => 십미터 prefix = processNumber.MeterPrefix[ prefix as keyof typeof processNumber.MeterPrefix ]; return `${numStr} ${prefix}미터 ${tail}`; } return `${num}${prefix}${unit}${tail}`; }, ) .replace( /([\d.,]+)\s*([개살시평명자벌장달병잔번채])/g, (_, num: string, postfix: string) => { // 10명 => 열명 if (num.includes(".")) { return num + postfix; } const intNum = parseInt(num.replace(/,/g, "")); if (CallingNumberKorean.canConvert(intNum)) { return CallingNumberKorean.convert(intNum) + postfix; } else { return IntegerKorean.convertFromString(num) + postfix; } }, ) .replace(/[\d,]+/g, (num: string) => { // 1,000 원 => 천원 if (!num.includes(",")) return num; return IntegerKorean.convertFromString(num); }) .replace( /(v?)([\d.]+)([ab]?)/g, (_, suffix: string, num: string, postfix: string) => { const dotCount = [...num.matchAll(/\./g)].length; const hasNoSuffix = suffix == ""; if (hasNoSuffix && dotCount == 0) { // 일반 숫자는 인트로 읽음 return IntegerKorean.convertFromString(num) + postfix; } else if (hasNoSuffix && dotCount == 1) { // 소수는 . 앞은 인트로, 뒤는 플로트로 읽음 const [intPart, floatPart] = num.split(/\./); return ( IntegerKorean.convertFromString(intPart ?? "") + "쩜" + FloatKorean.convert(floatPart ?? "") + postfix ); } else if ((suffix == "v" || postfix.length) && dotCount > 1) { // 버전표기는 버전을 붙여서 return ( "버전" + FloatKorean.convert(num) + (processNumber.VersionPostfix[ postfix as keyof typeof processNumber.VersionPostfix ] ?? "") ); } else { // 모든 경우에 속하지 않으면 영일이삼사 형태로 읽음 // (예: 111.111.111.111 ip address) return FloatKorean.convert(num) + postfix; } }, ); } export namespace processNumber { // prettier-ignore export const DatasizePrefix = { k: "킬로", ki: "키비", m: "메가", mi: "메비", g: "기가", gi: "기비", t: "테라", ti: "테비", p: "페타", pi: "페비", e: "엑사", ei: "엑시", z: "제타", zi: "제비", y: "요타", yi: "요비", }; // prettier-ignore export const LiterPrefix = { m: "밀리", "": "" }; // prettier-ignore export const MeterPrefix = { m: "밀리", c: "센치", "": "", k: "킬로", }; // prettier-ignore export const VersionPostfix = { a: "알파", b: "베타", }; } // Process unicode emojis and unicode symbols export function processEmoji(input: string): string { return input .replace( processEmoji.UnicodeSymbolsRegex, (content: string) => processEmoji.UnicodeSymbols[ content as keyof typeof processEmoji.UnicodeSymbols ] ?? content, ) .replace(/\p{Extended_Pictographic}/gu, (content: string) => { return ( EmojiDescriptions[content as keyof typeof EmojiDescriptions] ?? content ); }) .replace(/\p{Emoji}/u, " 이모지 "); } export namespace processEmoji { export const UnicodeSymbols = { "㎢": "제곱킬로미터", "㎡": "제곱미터", "↑": "위쪽 화살표", "↓": "아래쪽 화살표", "←": "왼쪽 화살표", "→": "오른쪽 화살표", "↔": "좌우 화살표", "↖": "왼쪽 위 화살표", "↗": "오른쪽 위 화살표", "↘": "오른쪽 아래 화살표", "↙": "왼쪽 아래 화살표", "™": "트레이드마크", }; export const UnicodeSymbolsRegex = new RegExp( "[" + Object.keys(UnicodeSymbols).join() + "]", "gu", ); } // Process ```codeblock``` and https://link export function processMarkdown(input: string): string { return input .replace(/```([\s\S]*?)```/g, (_, content: string) => { // Process codeblock const code = content .substring(0, processMarkdown.LangPrefixMaxLength) .toLowerCase(); let lang = ""; for (const [key, value] of Object.entries(processMarkdown.LangPrefixes)) { if (code.startsWith(key + "\n")) { lang = value + " "; break; } } return lang + "코드블럭"; }) .replace(/[hH][tT]{2}[pP][sS]?:\/\/(\S+)/g, (_, url: string) => { // Process link const mapped = processMarkdown.GIFMap[ url as keyof typeof processMarkdown.GIFMap ] as string | undefined; if (mapped) return mapped; if ( url.startsWith("tenor.com/view") || url.startsWith("images-ext-1.discordapp.net/external/") ) { return "움짤! "; } if ( url.startsWith("www.youtube.com/") || url.startsWith("youtube.com/") || url.startsWith("youtu.be/") ) { return "유튜브 영상! "; } if (url.startsWith("www.reddit.com/") || url.startsWith("reddit.com/")) { return "레딧 링크! "; } if ( url.startsWith("www.instagram.com/") || url.startsWith("instagram.com/") ) { return "인스타 링크! "; } if (url.startsWith("x.com/")) { return "엑스 링크! "; } if (url.startsWith("github.com/")) { return "깃허브 링크! "; } if (url.startsWith("store.steampowered.com")) { return "스팀 스토어 링크! "; } if (url.startsWith("steamcommunity.com")) { return "스팀 커뮤니티 링크! "; } return "링크 "; }); } export namespace processMarkdown { export const LangPrefixes = { typescript: "타입스크립트", javascript: "자바스크립트", java: "자바", kotlin: "코틀린", rust: "러스트", lua: "루아", json: "제이슨", yaml: "야믈", yml: "야믈", toml: "토믈", xml: "엑스엠엘", julia: "줄리아", matlab: "매트랩", erlang: "얼랭", elxir: "엘릭서", zig: "지그", txt: "텍스트", vim: "빔", perl: "펄", php: "피에이치피", lisp: "리스프", postscript: "포스트스크립트", ghostscript: "고스트스크립트", fortran: "포트란", algol: "알골", scala: "스칼라", haskell: "하스켈", basic: "베이직", cpp: "씨플플", "c++": "씨플플", csharp: "씨샵", cs: "씨샵", "c#": "씨샵", c: "씨", h: "헤더", d: "디", awk: "에이더블류케이", pl: "펄", pwsh: "파워쉘", powershell: "파워쉘", cmd: "씨엠디", sh: "쉘", ps1: "파워셀", bat: "배치파일", bash: "베시스크립트", tex: "텍", dart: "다트", go: "고랭", python: "파이썬", swift: "스위프트", css: "씨에스에스", html: "에이치티엠엘", latex: "레이텍", md: "마크다운", markdown: "마크다운", py: "파이썬", hs: "하스켈", rs: "러스트", kt: "코틀린", js: "자스", ts: "타스", tsx: "리액트 타입스크립트", jsx: "리액트 자바스크립트", an: "에이엔", parlance: "팔렌스", }; export const LangPrefixMaxLength = (() => { let max = 0; for (const key in LangPrefixes) { max = Math.max(key.length, max); } return max; })(); export const GIFMap = { "tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-windy-hair-gif-19187698": "화난 일레이나", "tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-sparkle-amazed-gif-18827847": "일레이나 반짝반짝!", "images-ext-1.discordapp.net/external/C3xPFuUxs16jY25AR3NvsIDezaOtib9wozhLBWejZk4/https/media.tenor.com/bUd8mk4ufwsAAAPo/anime-disappointment.mp4": "일레이나 절래절래", "images-ext-1.discordapp.net/external/SXv4qgpy2r1Gx-dNxhcfJle6AXDaH_SToRjEBYYaup0/https/media.tenor.com/nDDxJc4FDwEAAAPo/cute.mp4": "일레이나 끄덕", "tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-what-gif-19011602": "당황한 일레이나", "images-ext-1.discordapp.net/external/2R41WcvNJwYMD69UKls2cDa_hEL-rzCRCFvOi2DDOVo/https/media.tenor.com/sU3RCOixDbgAAAPo/majo-no-tabitabi-the-journey-of-elaina.mp4": "일레이나 손짓", }; } // Process %$*&... symbols to readable korean export function processSymbol(input: string): string { return input .replace( processSymbol.SymbolMapRegExp, (t) => processSymbol.SymbolMap[t as keyof typeof processSymbol.SymbolMap], ) .replace(/([?!~]+)/g, (_, content: string): string => content[0] ?? "") .replace(/[ \t\f\r]+/g, " "); } export namespace processSymbol { export const SymbolMap = { "%": "퍼센트", $: "달러", "^": "캐럿", "&": "엔드", "*": "별", "#": "샵", "@": "엣", ".": "쩜", "-": "마이너스", "+": "플러스", _: "언더바", "=": "이퀄", "/": "슬래쉬", // "~": "물결표", "\\": "역슬래쉬", "♡": "하트 ", "|": "", ">": "", "<": "", ":": "콜론", ";": "세미콜론", }; export const SymbolMapRegExp = new RegExp( "[" + Object.keys(SymbolMap) .map((i) => "\\" + i) .join() + "]", "g", ); } // Process isolated symbols export function processIsolatedSymbol(input: string): string { return input .replace(/^ * (\?+) *$/, "어?") .replace(/^ *[?!'"]+ *$/, (total) => [...total] .map( (element) => processIsolatedSymbol.IsolatedSymbolMap[ element as keyof typeof processIsolatedSymbol.IsolatedSymbolMap ], ) .join(""), ) .replace(/\s\|\|\s/g, " 오얼 ") .replace(/\s&&\s/g, " 엔드 "); } export namespace processIsolatedSymbol { export const IsolatedSymbolMap = { "?": "물음표", "!": "느낌표", "'": "쿼트", '"': "더블쿼트", }; } export function processFullWidth(input: string): string { return input.replace( /[1234567890]/g, (i) => processFullWidth.FullWidthNumberMap[ i as keyof typeof processFullWidth.FullWidthNumberMap ], ); } export namespace processFullWidth { // prettier-ignore export const FullWidthNumberMap = { "0": "0", "1": "1", "2": "2", "3": "3", "4": "4", "5": "5", "6": "6", "7": "7", "8": "8", "9": "9", }; } export function saferKorean(input: string): string { return (input.normalize() + " ") .let(processCensor) .let(processUnsounds) .let(processFullWidth) .let(processIsolatedSymbol) .let(processMarkdown) .let(processKorean) .let(processNumber) .let(processSymbol) .let(processEmoji) .replace(/\s+/g, " ") .trim(); }