using System.Text; using System.Text.RegularExpressions; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L18-2: 텍스트 케이스 변환 핸들러. "text" 프리픽스로 사용합니다. /// /// 예: text → 클립보드 텍스트 모든 케이스 변환 목록 /// text camel → camelCase /// text pascal → PascalCase /// text snake → snake_case /// text kebab → kebab-case /// text slug → url-slug (소문자 + 하이픈) /// text upper → UPPER CASE /// text lower → lower case /// text title → Title Case /// text sentence → Sentence case /// text const → SCREAMING_SNAKE_CASE /// text dot → dot.case /// text reverse → 문자 순서 뒤집기 /// text trim → 앞뒤 공백·줄바꿈 제거 /// Enter → 결과 복사. /// public partial class TextCaseHandler : IActionHandler { public string? Prefix => "text"; public PluginMetadata Metadata => new( "Text", "텍스트 케이스 변환 — camelCase · snake_case · PascalCase · slug 등", "1.0", "AX"); private record CaseItem(string Name, string Key, Func Convert); private static readonly CaseItem[] Cases = [ new("camelCase", "camel", ToCamel), new("PascalCase", "pascal", ToPascal), new("snake_case", "snake", ToSnake), new("SCREAMING_SNAKE_CASE", "const", ToConst), new("kebab-case", "kebab", ToKebab), new("URL slug", "slug", ToSlug), new("dot.case", "dot", ToDot), new("UPPER CASE", "upper", s => s.ToUpperInvariant()), new("lower case", "lower", s => s.ToLowerInvariant()), new("Title Case", "title", ToTitle), new("Sentence case", "sentence", ToSentence), new("뒤집기 (reverse)", "reverse", s => new string(s.Reverse().ToArray())), new("공백 정리 (trim)", "trim", s => s.Trim()), ]; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); string? clipboard = null; try { System.Windows.Application.Current.Dispatcher.Invoke(() => { if (Clipboard.ContainsText()) clipboard = Clipboard.GetText().Trim(); }); } catch { } if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("텍스트 케이스 변환기", "클립보드 텍스트를 다양한 케이스로 변환 · text camel / snake / pascal / kebab…", null, null, Symbol: "\uE8AB")); if (string.IsNullOrWhiteSpace(clipboard)) { items.Add(new LauncherItem("클립보드가 비어 있습니다", "텍스트를 복사한 뒤 사용하세요", null, null, Symbol: "\uE946")); // 케이스 목록만 안내 foreach (var c in Cases) items.Add(new LauncherItem($"text {c.Key}", c.Name, null, null, Symbol: "\uE8AB")); return Task.FromResult>(items); } // 클립보드 텍스트 → 모든 케이스 변환 목록 var preview = clipboard.Length > 30 ? clipboard[..30] + "…" : clipboard; items.Add(new LauncherItem($"입력: \"{preview}\"", $"{clipboard.Length}자 · 아래에서 선택", null, null, Symbol: "\uE8AB")); foreach (var c in Cases) { var result = TrySafeConvert(c.Convert, clipboard); items.Add(new LauncherItem(result, c.Name, null, ("copy", result), Symbol: "\uE8AB")); } return Task.FromResult>(items); } // 서브커맨드로 특정 케이스 변환 var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); // 인라인 텍스트 입력 지원: text camel hello world → helloWorld var inlineText = parts.Length > 1 ? parts[1] : clipboard; if (string.IsNullOrWhiteSpace(inlineText)) { items.Add(new LauncherItem("텍스트가 없습니다", "클립보드에 텍스트를 복사하거나 text camel <직접입력> 형식으로 사용하세요", null, null, Symbol: "\uE946")); return Task.FromResult>(items); } var caseItem = Cases.FirstOrDefault(c => c.Key.Equals(sub, StringComparison.OrdinalIgnoreCase)); if (caseItem != null) { var result = TrySafeConvert(caseItem.Convert, inlineText); var sourceLabel = parts.Length > 1 ? $"입력: \"{inlineText}\"" : $"클립보드: \"{(inlineText.Length > 30 ? inlineText[..30] + "…" : inlineText)}\""; items.Add(new LauncherItem(result, $"{caseItem.Name} · Enter 복사", null, ("copy", result), Symbol: "\uE8AB")); items.Add(new LauncherItem(sourceLabel, "원본", null, ("copy", inlineText), Symbol: "\uE8AB")); // 다른 케이스도 함께 표시 items.Add(new LauncherItem("── 다른 케이스 ──", "", null, null, Symbol: "\uE8AB")); foreach (var c in Cases.Where(c => c.Key != sub)) { var r = TrySafeConvert(c.Convert, inlineText); if (r != result) items.Add(new LauncherItem(r, c.Name, null, ("copy", r), Symbol: "\uE8AB")); } } else { // 알 수 없는 서브커맨드 → 모든 케이스 변환 items.Add(new LauncherItem($"알 수 없는 케이스: '{sub}'", "camel · pascal · snake · const · kebab · slug · dot · upper · lower · title · sentence · reverse · trim", null, null, Symbol: "\uE783")); if (!string.IsNullOrWhiteSpace(clipboard)) { foreach (var c in Cases) { var r = TrySafeConvert(c.Convert, clipboard); items.Add(new LauncherItem(r, c.Name, null, ("copy", r), Symbol: "\uE8AB")); } } } 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("Text", "클립보드에 복사했습니다."); } catch { } } return Task.CompletedTask; } // ── 변환 함수 ───────────────────────────────────────────────────────────── private static string TrySafeConvert(Func fn, string input) { try { return fn(input); } catch { return input; } } /// 입력을 단어 토큰 배열로 분리 (공백, 언더스코어, 하이픈, 대문자 경계) private static string[] Tokenize(string s) { // camelCase/PascalCase 분리 var withSpaces = CamelBoundaryRegex().Replace(s, "$1 $2"); // 구분자 → 공백 var normalized = SeparatorRegex().Replace(withSpaces, " "); return normalized.Split(' ', StringSplitOptions.RemoveEmptyEntries); } private static string ToCamel(string s) { var words = Tokenize(s); if (words.Length == 0) return s; var sb = new StringBuilder(words[0].ToLowerInvariant()); for (var i = 1; i < words.Length; i++) sb.Append(char.ToUpperInvariant(words[i][0]) + words[i][1..].ToLowerInvariant()); return sb.ToString(); } private static string ToPascal(string s) { var words = Tokenize(s); var sb = new StringBuilder(); foreach (var w in words) sb.Append(char.ToUpperInvariant(w[0]) + w[1..].ToLowerInvariant()); return sb.ToString(); } private static string ToSnake(string s) => string.Join("_", Tokenize(s).Select(w => w.ToLowerInvariant())); private static string ToConst(string s) => string.Join("_", Tokenize(s).Select(w => w.ToUpperInvariant())); private static string ToKebab(string s) => string.Join("-", Tokenize(s).Select(w => w.ToLowerInvariant())); private static string ToSlug(string s) { var normalized = s.Normalize(NormalizationForm.FormD); var ascii = new StringBuilder(); foreach (var c in normalized) if (c < 128) ascii.Append(c); var slug = SeparatorRegex().Replace(ascii.ToString().ToLowerInvariant(), "-"); slug = NonSlugRegex().Replace(slug, ""); slug = MultipleDashRegex().Replace(slug, "-"); return slug.Trim('-'); } private static string ToDot(string s) => string.Join(".", Tokenize(s).Select(w => w.ToLowerInvariant())); private static string ToTitle(string s) { var words = s.Split(' '); return string.Join(" ", words.Select(w => w.Length == 0 ? w : char.ToUpperInvariant(w[0]) + w[1..].ToLowerInvariant())); } private static string ToSentence(string s) { if (string.IsNullOrEmpty(s)) return s; var lower = s.ToLowerInvariant(); return char.ToUpperInvariant(lower[0]) + lower[1..]; } [GeneratedRegex(@"([a-z])([A-Z])")] private static partial Regex CamelBoundaryRegex(); [GeneratedRegex(@"[\s\-_./\\]+")] private static partial Regex SeparatorRegex(); [GeneratedRegex(@"[^a-z0-9\-]")] private static partial Regex NonSlugRegex(); [GeneratedRegex(@"-{2,}")] private static partial Regex MultipleDashRegex(); }