using System.Globalization;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
///
/// L11-4: 유니코드 문자 조회 핸들러. "unicode" 프리픽스로 사용합니다.
///
/// 예: unicode A → 문자 'A'의 코드포인트·카테고리·이름 조회
/// unicode U+1F600 → 코드포인트로 문자 조회
/// unicode 0x1F600 → 16진수 코드포인트
/// unicode 128512 → 10진수 코드포인트
/// unicode 가 → 한글 문자 분석
/// unicode smile → 문자 설명으로 검색 (이모지 이름 포함)
/// Enter → 문자를 클립보드에 복사.
///
public class UnicodeHandler : IActionHandler
{
public string? Prefix => "unicode";
public PluginMetadata Metadata => new(
"Unicode",
"유니코드 문자 조회 — 코드포인트 · 카테고리 · 블록",
"1.0",
"AX");
// 주요 유니코드 블록 범위
private static readonly (int Start, int End, string Name)[] UnicodeBlocks =
[
(0x0000, 0x007F, "Basic Latin"),
(0x0080, 0x00FF, "Latin-1 Supplement"),
(0x0100, 0x017F, "Latin Extended-A"),
(0x0370, 0x03FF, "Greek and Coptic"),
(0x0400, 0x04FF, "Cyrillic"),
(0x0600, 0x06FF, "Arabic"),
(0x0900, 0x097F, "Devanagari"),
(0x1100, 0x11FF, "Hangul Jamo"),
(0x2000, 0x206F, "General Punctuation"),
(0x2100, 0x214F, "Letterlike Symbols"),
(0x2200, 0x22FF, "Mathematical Operators"),
(0x2300, 0x23FF, "Miscellaneous Technical"),
(0x2600, 0x26FF, "Miscellaneous Symbols"),
(0x2700, 0x27BF, "Dingbats"),
(0x3000, 0x303F, "CJK Symbols and Punctuation"),
(0x3040, 0x309F, "Hiragana"),
(0x30A0, 0x30FF, "Katakana"),
(0x4E00, 0x9FFF, "CJK Unified Ideographs"),
(0xAC00, 0xD7AF, "Hangul Syllables"),
(0xE000, 0xF8FF, "Private Use Area"),
(0xF000, 0xF0FF, "Segoe MDL2 Assets (PUA)"),
(0x1F300, 0x1F5FF, "Miscellaneous Symbols and Pictographs"),
(0x1F600, 0x1F64F, "Emoticons"),
(0x1F680, 0x1F6FF, "Transport and Map Symbols"),
(0x1F900, 0x1F9FF, "Supplemental Symbols and Pictographs"),
];
// 자주 쓰는 특수 문자 예제
private static readonly (string Char, string Desc)[] QuickChars =
[
("©", "Copyright Sign (U+00A9)"),
("®", "Registered Sign (U+00AE)"),
("™", "Trade Mark Sign (U+2122)"),
("•", "Bullet (U+2022)"),
("→", "Rightwards Arrow (U+2192)"),
("←", "Leftwards Arrow (U+2190)"),
("✓", "Check Mark (U+2713)"),
("✗", "Ballot X (U+2717)"),
("★", "Black Star (U+2605)"),
("♥", "Black Heart Suit (U+2665)"),
("😀", "Grinning Face (U+1F600)"),
("한", "Korean Syllable (AC00~D7AF)"),
];
public Task> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List();
if (string.IsNullOrWhiteSpace(q))
{
items.Add(new LauncherItem("유니코드 문자 조회",
"예: unicode A / unicode U+1F600 / unicode 가 / unicode 0x2665",
null, null, Symbol: "\uE8D2"));
foreach (var (ch, desc) in QuickChars.Take(8))
items.Add(new LauncherItem(ch, desc, null, ("copy", ch), Symbol: "\uE8D2"));
return Task.FromResult>(items);
}
// U+XXXX 형식
if (q.StartsWith("U+", StringComparison.OrdinalIgnoreCase) ||
q.StartsWith("u+", StringComparison.OrdinalIgnoreCase))
{
if (int.TryParse(q[2..], NumberStyles.HexNumber, null, out var cp))
items.AddRange(BuildCodePointItems(cp));
else
items.Add(new LauncherItem("형식 오류", "예: U+1F600", null, null, Symbol: "\uE783"));
return Task.FromResult>(items);
}
// 0x 16진수
if (q.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ||
q.StartsWith("0X", StringComparison.OrdinalIgnoreCase))
{
if (int.TryParse(q[2..], NumberStyles.HexNumber, null, out var cp))
items.AddRange(BuildCodePointItems(cp));
else
items.Add(new LauncherItem("형식 오류", "예: 0x1F600", null, null, Symbol: "\uE783"));
return Task.FromResult>(items);
}
// 순수 10진수 코드포인트
if (int.TryParse(q, out var decCp) && decCp >= 0)
{
items.AddRange(BuildCodePointItems(decCp));
return Task.FromResult>(items);
}
// 문자(1~2자) 직접 입력 → 분석
var codePoints = GetCodePoints(q);
if (codePoints.Count > 0 && codePoints.Count <= 6)
{
if (codePoints.Count == 1)
{
items.AddRange(BuildCodePointItems(codePoints[0]));
}
else
{
// 여러 문자 일괄 분석
items.Add(new LauncherItem($"'{q}' {codePoints.Count}개 코드포인트", "전체 분석", null, null, Symbol: "\uE8D2"));
foreach (var cp in codePoints)
items.AddRange(BuildCodePointItems(cp));
}
return Task.FromResult>(items);
}
// 6자 초과 → 통계만
if (codePoints.Count > 0)
{
items.Add(new LauncherItem(
$"'{(q.Length > 10 ? q[..10] + "…" : q)}' {codePoints.Count}개 코드포인트",
$"범위: U+{codePoints.Min():X4} ~ U+{codePoints.Max():X4}",
null,
("copy", string.Join(" ", codePoints.Select(c => $"U+{c:X4}"))),
Symbol: "\uE8D2"));
}
else
{
items.Add(new LauncherItem("조회 실패",
$"'{q}'을(를) 인식할 수 없습니다. 예: unicode A / unicode U+1F600",
null, null, Symbol: "\uE783"));
}
return Task.FromResult>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string text))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(text));
NotificationService.Notify("Unicode", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 코드포인트 분석 ────────────────────────────────────────────────────────
private static IEnumerable BuildCodePointItems(int codePoint)
{
if (codePoint < 0 || codePoint > 0x10FFFF)
{
yield return new LauncherItem("범위 초과", "유효한 유니코드 범위: U+0000 ~ U+10FFFF", null, null, Symbol: "\uE783");
yield break;
}
var charStr = char.ConvertFromUtf32(codePoint);
var category = GetCategoryName(charStr);
var block = GetBlock(codePoint);
var name = GetCharName(codePoint);
var display = codePoint < 32 || (codePoint >= 127 && codePoint < 160) ? $"(제어문자 U+{codePoint:X4})" : charStr;
yield return new LauncherItem(
display,
$"U+{codePoint:X4} · {name}",
null,
("copy", charStr),
Symbol: "\uE8D2");
yield return new LauncherItem("코드포인트", $"U+{codePoint:X4}", null, ("copy", $"U+{codePoint:X4}"), Symbol: "\uE8D2");
yield return new LauncherItem("10진수", $"{codePoint}", null, ("copy", codePoint.ToString()), Symbol: "\uE8D2");
yield return new LauncherItem("HTML 엔티티", $"{codePoint};", null, ("copy", $"{codePoint};"), Symbol: "\uE8D2");
yield return new LauncherItem("HTML Hex", $"{codePoint:X};", null, ("copy", $"{codePoint:X};"), Symbol: "\uE8D2");
// UTF-8 바이트
var utf8 = System.Text.Encoding.UTF8.GetBytes(charStr);
var utf8Hex = string.Join(" ", utf8.Select(b => $"{b:X2}"));
yield return new LauncherItem("UTF-8", utf8Hex, null, ("copy", utf8Hex), Symbol: "\uE8D2");
// UTF-16
var utf16 = System.Text.Encoding.Unicode.GetBytes(charStr);
var utf16Hex = string.Join(" ", utf16.Select(b => $"{b:X2}"));
yield return new LauncherItem("UTF-16 LE", utf16Hex, null, ("copy", utf16Hex), Symbol: "\uE8D2");
yield return new LauncherItem("카테고리", category, null, null, Symbol: "\uE8D2");
yield return new LauncherItem("블록", block, null, null, Symbol: "\uE8D2");
// 한글 음절이면 분해
if (codePoint >= 0xAC00 && codePoint <= 0xD7A3)
{
var (initial, vowel, final) = DecomposeHangul(codePoint);
yield return new LauncherItem("초성", initial, null, ("copy", initial), Symbol: "\uE8D2");
yield return new LauncherItem("중성", vowel, null, ("copy", vowel), Symbol: "\uE8D2");
if (!string.IsNullOrEmpty(final))
yield return new LauncherItem("종성", final, null, ("copy", final), Symbol: "\uE8D2");
}
}
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
private static List GetCodePoints(string s)
{
var result = new List();
for (var i = 0; i < s.Length; )
{
var cp = char.ConvertToUtf32(s, i);
result.Add(cp);
i += char.IsSurrogatePair(s, i) ? 2 : 1;
}
return result;
}
private static string GetCategoryName(string charStr)
{
if (string.IsNullOrEmpty(charStr)) return "Unknown";
var cat = char.GetUnicodeCategory(charStr, 0);
return cat switch
{
UnicodeCategory.UppercaseLetter => "대문자 (Lu)",
UnicodeCategory.LowercaseLetter => "소문자 (Ll)",
UnicodeCategory.TitlecaseLetter => "타이틀케이스 (Lt)",
UnicodeCategory.ModifierLetter => "수정 문자 (Lm)",
UnicodeCategory.OtherLetter => "기타 문자 (Lo)",
UnicodeCategory.DecimalDigitNumber => "10진수 숫자 (Nd)",
UnicodeCategory.LetterNumber => "문자 숫자 (Nl)",
UnicodeCategory.OtherNumber => "기타 숫자 (No)",
UnicodeCategory.SpaceSeparator => "공백 (Zs)",
UnicodeCategory.LineSeparator => "줄 구분자 (Zl)",
UnicodeCategory.ParagraphSeparator => "단락 구분자 (Zp)",
UnicodeCategory.Control => "제어 문자 (Cc)",
UnicodeCategory.MathSymbol => "수학 기호 (Sm)",
UnicodeCategory.CurrencySymbol => "통화 기호 (Sc)",
UnicodeCategory.ModifierSymbol => "수정 기호 (Sk)",
UnicodeCategory.OtherSymbol => "기타 기호 (So)",
UnicodeCategory.OpenPunctuation => "여는 구두점 (Ps)",
UnicodeCategory.ClosePunctuation => "닫는 구두점 (Pe)",
UnicodeCategory.DashPunctuation => "대시 구두점 (Pd)",
UnicodeCategory.ConnectorPunctuation => "연결 구두점 (Pc)",
UnicodeCategory.OtherPunctuation => "기타 구두점 (Po)",
_ => cat.ToString(),
};
}
private static string GetBlock(int cp)
{
foreach (var (start, end, name) in UnicodeBlocks)
if (cp >= start && cp <= end) return $"{name} (U+{start:X4}~U+{end:X4})";
if (cp >= 0x10000) return $"Supplementary Planes (U+{cp:X4})";
return $"U+{cp:X4} 범위 불명";
}
private static string GetCharName(int cp) => cp switch
{
0x0020 => "Space",
0x0021 => "Exclamation Mark",
0x0022 => "Quotation Mark",
0x0023 => "Number Sign",
0x0024 => "Dollar Sign",
0x0025 => "Percent Sign",
0x0026 => "Ampersand",
0x0027 => "Apostrophe",
0x0028 => "Left Parenthesis",
0x0029 => "Right Parenthesis",
0x002A => "Asterisk",
0x002B => "Plus Sign",
0x002C => "Comma",
0x002D => "Hyphen-Minus",
0x002E => "Full Stop",
0x002F => "Solidus",
>= 0x0030 and <= 0x0039 => $"Digit {(char)cp}",
>= 0x0041 and <= 0x005A => $"Latin Capital Letter {(char)cp}",
>= 0x0061 and <= 0x007A => $"Latin Small Letter {(char)cp}",
0x00A9 => "Copyright Sign",
0x00AE => "Registered Sign",
0x2122 => "Trade Mark Sign",
0x2022 => "Bullet",
0x2192 => "Rightwards Arrow",
0x2190 => "Leftwards Arrow",
0x2191 => "Upwards Arrow",
0x2193 => "Downwards Arrow",
0x2713 => "Check Mark",
0x2717 => "Ballot X",
0x2605 => "Black Star",
0x2606 => "White Star",
0x2665 => "Black Heart Suit",
0x2764 => "Heavy Black Heart",
0x1F600 => "Grinning Face",
0x1F601 => "Grinning Face With Smiling Eyes",
0x1F602 => "Face With Tears of Joy",
0x1F603 => "Smiling Face With Open Mouth",
0x1F609 => "Winking Face",
0x1F60D => "Smiling Face With Heart-Eyes",
0x1F621 => "Pouting Face",
0x1F625 => "Disappointed but Relieved Face",
>= 0xAC00 and <= 0xD7A3 => "Hangul Syllable",
>= 0x1100 and <= 0x11FF => "Hangul Jamo",
>= 0x3131 and <= 0x318E => "Hangul Compatibility Jamo",
>= 0x4E00 and <= 0x9FFF => "CJK Unified Ideograph",
>= 0x3040 and <= 0x309F => "Hiragana",
>= 0x30A0 and <= 0x30FF => "Katakana",
_ => $"U+{cp:X4}",
};
private static (string Initial, string Vowel, string Final) DecomposeHangul(int cp)
{
string[] initials = ["ㄱ","ㄲ","ㄴ","ㄷ","ㄸ","ㄹ","ㅁ","ㅂ","ㅃ","ㅅ","ㅆ","ㅇ","ㅈ","ㅉ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"];
string[] vowels = ["ㅏ","ㅐ","ㅑ","ㅒ","ㅓ","ㅔ","ㅕ","ㅖ","ㅗ","ㅘ","ㅙ","ㅚ","ㅛ","ㅜ","ㅝ","ㅞ","ㅟ","ㅠ","ㅡ","ㅢ","ㅣ"];
string[] finals = ["", "ㄱ","ㄲ","ㄳ","ㄴ","ㄵ","ㄶ","ㄷ","ㄹ","ㄺ","ㄻ","ㄼ","ㄽ","ㄾ","ㄿ","ㅀ","ㅁ","ㅂ","ㅄ","ㅅ","ㅆ","ㅇ","ㅈ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"];
var offset = cp - 0xAC00;
var finIdx = offset % 28;
var vowIdx = (offset / 28) % 21;
var iniIdx = offset / 28 / 21;
return (initials[iniIdx], vowels[vowIdx], finals[finIdx]);
}
}