using System.Text; using System.Text.RegularExpressions; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L21-1: TOML 파서·분석기 핸들러. "toml" 프리픽스로 사용합니다. /// /// 예: toml → 클립보드 TOML 전체 키 목록 /// toml validate → 유효성 검사 /// toml keys → 최상위 키 목록 /// toml get key → 특정 키 값 조회 /// toml get server.port → 점 표기법 중첩 키 조회 /// toml stats → 줄·키·섹션·배열 통계 /// toml flat → 점 표기법 평탄화 (모든 키·값) /// toml sections → [section] 목록 /// Enter → 값 복사. /// public partial class TomlHandler : IActionHandler { public string? Prefix => "toml"; public PluginMetadata Metadata => new( "TOML", "TOML 파서·분석기 — 키 조회·유효성 검사·평탄화", "1.0", "AX"); // TOML 노드 (경량 표현) private sealed class TomlTable : Dictionary { } private sealed class TomlArray : List { } 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)) { if (string.IsNullOrWhiteSpace(clipboard)) { items.Add(new LauncherItem("TOML 파서·분석기", "클립보드에 TOML을 복사하세요 · toml validate / keys / get / stats / flat", null, null, Symbol: "\uE8EC")); return Task.FromResult>(items); } // 기본: 유효성 확인 + 최상위 키 var (tbl, err) = ParseToml(clipboard!); if (err != null) { items.Add(ErrorItem($"TOML 파싱 오류: {err}")); return Task.FromResult>(items); } items.Add(new LauncherItem("TOML 파싱 성공 ✓", $"최상위 키 {tbl!.Count}개 · toml get / flat / stats", null, null, Symbol: "\uE8EC")); BuildTopKeys(items, tbl!); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); var src = parts.Length > 1 && (sub != "get") ? string.Join(" ", parts[1..]) : clipboard ?? ""; // validate if (sub is "validate" or "check" or "검사") { var text = clipboard ?? ""; var (_, verr) = ParseToml(text); if (verr != null) items.Add(ErrorItem($"유효성 오류: {verr}")); else { var (vt, _) = ParseToml(text); items.Add(new LauncherItem("✓ 유효한 TOML", $"최상위 키 {vt!.Count}개", null, null, Symbol: "\uE8EC")); BuildTopKeys(items, vt!); } return Task.FromResult>(items); } var (table, parseErr) = ParseToml(clipboard ?? ""); if (parseErr != null && sub != "validate") { items.Add(ErrorItem($"TOML 파싱 오류: {parseErr}")); return Task.FromResult>(items); } switch (sub) { case "keys" or "key": items.Add(new LauncherItem("최상위 키 목록", $"{table!.Count}개", null, null, Symbol: "\uE8EC")); BuildTopKeys(items, table!); break; case "sections" or "section": { var secs = table!.Where(kv => kv.Value is TomlTable).Select(kv => kv.Key).ToList(); items.Add(new LauncherItem($"섹션 {secs.Count}개", "", null, null, Symbol: "\uE8EC")); foreach (var s in secs) { var secCount = table![s] is TomlTable st ? st.Count : 0; items.Add(new LauncherItem($"[{s}]", $"{secCount}개 키", null, ("copy", s), Symbol: "\uE8EC")); } break; } case "get": { if (parts.Length < 2) { items.Add(ErrorItem("예: toml get server.port")); break; } var keyPath = parts[1]; var val = GetByPath(table!, keyPath); if (val == null) items.Add(new LauncherItem($"'{keyPath}' 키를 찾을 수 없습니다", "", null, null, Symbol: "\uE8EC")); else { var strVal = TomlValueToString(val); items.Add(new LauncherItem($"{keyPath} = {TruncateStr(strVal, 60)}", "Enter 복사", null, ("copy", strVal), Symbol: "\uE8EC")); items.Add(CopyItem("값", strVal)); items.Add(CopyItem("키 경로", keyPath)); items.Add(CopyItem("타입", GetTomlType(val))); } break; } case "stats": { var flat = new Dictionary(); FlattenTable(table!, "", flat); var lines = (clipboard ?? "").Split('\n').Length; var arrays = flat.Values.Count(v => v is TomlArray); var tables2 = flat.Values.Count(v => v is TomlTable); var scalars = flat.Count - arrays - tables2; items.Add(new LauncherItem("TOML 통계", "", null, null, Symbol: "\uE8EC")); items.Add(CopyItem("전체 줄", lines.ToString())); items.Add(CopyItem("최상위 키", table!.Count.ToString())); items.Add(CopyItem("전체 키(flat)", flat.Count.ToString())); items.Add(CopyItem("스칼라 값", scalars.ToString())); items.Add(CopyItem("배열", arrays.ToString())); items.Add(CopyItem("섹션(테이블)", table.Values.Count(v => v is TomlTable).ToString())); break; } case "flat" or "flatten": { var flat = new Dictionary(); FlattenTable(table!, "", flat); var all = string.Join("\n", flat.Select(kv => $"{kv.Key} = {TomlValueToString(kv.Value)}")); items.Add(new LauncherItem($"평탄화 결과 {flat.Count}개", "전체 복사 → Enter", null, ("copy", all), Symbol: "\uE8EC")); foreach (var (k, v) in flat.Take(20)) items.Add(new LauncherItem($"{k} = {TruncateStr(TomlValueToString(v), 50)}", GetTomlType(v), null, ("copy", TomlValueToString(v)), Symbol: "\uE8EC")); if (flat.Count > 20) items.Add(new LauncherItem($"... ({flat.Count - 20}개 더)", "전체는 Enter로 복사", null, null, Symbol: "\uE8EC")); break; } default: items.Add(new LauncherItem($"알 수 없는 서브커맨드: '{sub}'", "validate · keys · sections · get · stats · flat", null, null, Symbol: "\uE783")); break; } 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("TOML", "클립보드에 복사했습니다."); } catch { } } return Task.CompletedTask; } // ── 경량 TOML 파서 ──────────────────────────────────────────────────── private static (TomlTable? table, string? error) ParseToml(string src) { var root = new TomlTable(); var current = root; var lines = src.Split('\n'); string? parseError = null; for (int lineNo = 0; lineNo < lines.Length; lineNo++) { var raw = lines[lineNo]; var line = StripComment(raw).Trim(); if (string.IsNullOrWhiteSpace(line)) continue; // [section] 또는 [[array-of-tables]] if (line.StartsWith("[[")) { var name = line.Trim('[', ']').Trim(); // 배열 섹션 처리 (간략화: 마지막 테이블만 유지) var parts = name.Split('.'); current = root; foreach (var p in parts) { if (!current.TryGetValue(p, out var existing) || existing is not TomlTable child) { child = new TomlTable(); current[p] = child; } current = (TomlTable)current[p]!; } } else if (line.StartsWith("[")) { var name = line.Trim('[', ']').Trim(); var parts = name.Split('.'); current = root; foreach (var p in parts) { if (!current.TryGetValue(p, out var existing) || existing is not TomlTable child) { child = new TomlTable(); current[p] = child; } current = (TomlTable)current[p]!; } } // key = value else if (line.Contains('=')) { var eqIdx = line.IndexOf('='); var key = line[..eqIdx].Trim().Trim('"'); var val = line[(eqIdx + 1)..].Trim(); try { current[key] = ParseTomlValue(val); } catch (Exception ex) { parseError ??= $"줄 {lineNo + 1}: {ex.Message}"; } } } return parseError != null ? (null, parseError) : (root, null); } private static object? ParseTomlValue(string val) { if (val.StartsWith('"') || val.StartsWith('\'')) return val.Trim('"', '\''); if (val.StartsWith('[')) { var arr = new TomlArray(); var inner = val.Trim('[', ']'); foreach (var item in inner.Split(',')) { var t = item.Trim(); if (!string.IsNullOrEmpty(t)) arr.Add(ParseTomlValue(t)); } return arr; } if (val.StartsWith('{')) { var tbl = new TomlTable(); var inner = val.Trim('{', '}'); foreach (var pair in inner.Split(',')) { var parts = pair.Split('=', 2); if (parts.Length == 2) tbl[parts[0].Trim().Trim('"')] = ParseTomlValue(parts[1].Trim()); } return tbl; } if (val is "true") return true; if (val is "false") return false; if (long.TryParse(val, out var lv)) return lv; if (double.TryParse(val, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var dv)) return dv; return val.Trim('"', '\''); } private static string StripComment(string line) { // '#' 앞에 따옴표가 홀수개인 경우 내부 → 유지, 아니면 제거 bool inStr = false; char quote = '"'; for (int i = 0; i < line.Length; i++) { var c = line[i]; if (!inStr && (c == '"' || c == '\'')) { inStr = true; quote = c; } else if (inStr && c == quote) inStr = false; else if (!inStr && c == '#') return line[..i]; } return line; } private static object? GetByPath(TomlTable table, string path) { var parts = path.Split('.'); object? cur = table; foreach (var p in parts) { if (cur is TomlTable t && t.TryGetValue(p, out var next)) cur = next; else return null; } return cur; } private static void FlattenTable(TomlTable table, string prefix, Dictionary result) { foreach (var (k, v) in table) { var key = string.IsNullOrEmpty(prefix) ? k : $"{prefix}.{k}"; if (v is TomlTable child) FlattenTable(child, key, result); else result[key] = v; } } private static void BuildTopKeys(List items, TomlTable table) { foreach (var (k, v) in table) { var type = GetTomlType(v); var disp = v is TomlTable t ? $"{{ {t.Count}개 키 }}" : v is TomlArray a ? $"[ {a.Count}개 항목 ]" : TruncateStr(TomlValueToString(v), 50); items.Add(new LauncherItem($"{k} = {disp}", type, null, ("copy", TomlValueToString(v)), Symbol: "\uE8EC")); } } private static string TomlValueToString(object? v) => v switch { null => "null", bool b => b ? "true" : "false", TomlTable t => "{" + string.Join(", ", t.Select(kv => $"{kv.Key} = {TomlValueToString(kv.Value)}")) + "}", TomlArray a => "[" + string.Join(", ", a.Select(TomlValueToString)) + "]", _ => v.ToString() ?? "" }; private static string GetTomlType(object? v) => v switch { null => "null", bool => "Boolean", long => "Integer", double => "Float", string => "String", TomlTable => "Table", TomlArray => "Array", _ => v.GetType().Name }; private static string TruncateStr(string s, int max) => s.Length <= max ? s : s[..max] + "…"; private static LauncherItem CopyItem(string label, string value) => new(label, value, null, ("copy", value), Symbol: "\uE8EC"); private static LauncherItem ErrorItem(string msg) => new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783"); }