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();
}