[Phase L10] 텍스트·데이터·개발 유틸리티 핸들러 4종 추가

XmlHandler.cs (신규, ~290줄, prefix=xml):
- 클립보드/인라인 XML 자동 포맷(들여쓰기 2칸)
- compact/minify: 공백 제거 압축, validate: 줄·열 오류 표시
- xpath //path: XPathDocument 기반 최대 20건 쿼리
- attr: XmlDocument 전체 속성 추출
- yield return → 리스트 방식으로 수정 (CS1631/CS1626 해결)

UuidHandler.cs (신규, ~210줄, prefix=uuid):
- Guid.NewGuid() v4 기본 + N개 일괄 생성
- seq: UUIDv7 스타일(상위 48비트=Unix ms 타임스탬프, 하위=랜덤)
- short: RandomNumberGenerator 4바이트 hex 짧은 ID
- upper: 대문자 UUID, nil: 00000000-… Nil UUID
- parse: 버전·변형·v1 타임스탬프 복원 분석

CertHandler.cs (신규, ~200줄, prefix=cert):
- TcpClient + SslStream으로 TLS 인증서 직접 조회
- 만료일·D-day·발급 대상·발급 기관·SANs·지문(SHA1) 표시
- 사내 모드: 내부 호스트(192.168/10/172.16-31)만 허용
- https:// URL 형식, 포트 지정(cert domain.com 8443) 지원

LoremHandler.cs (신규, ~230줄, prefix=lorem):
- 113단어 Lorem Ipsum 풀 + 82단어 한국어 더미 풀
- lorem N: N단락, words N: 단어, sentences N: 문장 모드
- ko: 한국어 문장 구조(시작어+본문+결말) 조합
- email N: 더미 이메일, name N: 한국어 성+이름 조합

App.xaml.cs: 4개 핸들러 Phase L10 블록 등록
docs/LAUNCHER_ROADMAP.md: Phase L10 완료 섹션 추가
빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 14:28:52 +09:00
parent f977037ca6
commit 678feaadc9
6 changed files with 1205 additions and 0 deletions

View File

@@ -256,3 +256,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
| L9-2 | **IP 서브넷 계산기** ✅ | `subnet` 프리픽스. CIDR(x.x.x.x/24) 또는 공백 구분 입력. 네트워크·마스크·브로드캐스트·첫/마지막 호스트·사용 가능 호스트 수 계산. 서브넷 마스크→CIDR 변환. `subnet range x.x.x.10-50` 범위 계산. 이진 마스크 표시 | 높음 |
| L9-3 | **시스템 정리** ✅ | `clean` 프리픽스. 임시 파일(%TEMP%), 휴지통(SHEmptyRecycleBin), 다운로드 30일 이상, AxCopilot 로그. 예상 용량 사전 표시. `clean all`로 일괄 정리. 항목별 실시간 알림 | 중간 |
| L9-4 | **진수 변환기** ✅ | `base` 프리픽스. 10진/16진(0x)/2진(0b)/8진(0o) 자동 감지 변환. `base 255 to hex` 단일 방향 변환. `base ascii 65` ASCII 코드↔문자 변환. 4비트 그룹 이진 표시. Enter → 클립보드 복사 | 중간 |
---
## Phase L10 — 텍스트·데이터·개발 유틸리티 (v2.0.2) ✅ 완료
> **방향**: 개발자 일상 도구 확충 — XML 조작, UUID 생성, SSL 인증서 점검, 더미 데이터 생성.
| # | 기능 | 설명 | 우선순위 |
|---|------|------|----------|
| L10-1 | **XML 포맷터·검증기** ✅ | `xml` 프리픽스. 클립보드 또는 인라인 XML 자동 포맷(들여쓰기). `xml compact/minify`로 압축. `xml validate`로 유효성 검증(줄·열 오류 표시). `xml xpath //경로`로 XPath 쿼리. `xml attr`로 속성 목록 추출. Enter → 클립보드 복사 | 높음 |
| L10-2 | **UUID/GUID 생성기** ✅ | `uuid` 프리픽스. `uuid` 기본 v4 1개 생성. `uuid 5`로 N개 일괄. `uuid upper` 대문자. `uuid seq` UUIDv7 스타일 순차 UUID(타임스탬프 상위 48비트). `uuid short` 8자리 hex 짧은 ID. `uuid nil` Nil UUID. `uuid parse <uuid>`로 버전·변형·타임스탬프 분석 | 높음 |
| L10-3 | **SSL 인증서 체커** ✅ | `cert` 프리픽스. 도메인/IP의 TLS 인증서 조회(443 기본, 포트 지정 가능). 만료일·D-day·발급 대상·발급 기관·SANs·지문 표시. 사내 모드에서는 내부 호스트(192.168.x, 10.x, 172.16-31.x)만 허용. Enter → 결과 클립보드 복사 | 중간 |
| L10-4 | **Lorem Ipsum 생성기** ✅ | `lorem` 프리픽스. `lorem 3`으로 3단락 생성. `lorem words 20` 단어 N개. `lorem sentences 5` 문장 N개. `lorem ko` 한국어 더미 텍스트. `lorem email 5` 더미 이메일 주소. `lorem name 5` 한국어 더미 이름. Enter → 클립보드 복사 | 중간 |

View File

@@ -227,6 +227,16 @@ public partial class App : System.Windows.Application
// L9-4: 진수 변환기 (prefix=base)
commandResolver.RegisterHandler(new BaseConvertHandler());
// ─── Phase L10 핸들러 ─────────────────────────────────────────────────
// L10-1: XML 포맷터·검증기·XPath (prefix=xml)
commandResolver.RegisterHandler(new XmlHandler());
// L10-2: UUID/GUID 생성기 (prefix=uuid)
commandResolver.RegisterHandler(new UuidHandler());
// L10-3: SSL 인증서 체커 (prefix=cert)
commandResolver.RegisterHandler(new CertHandler());
// L10-4: Lorem Ipsum 더미 텍스트 (prefix=lorem)
commandResolver.RegisterHandler(new LoremHandler());
// ─── 플러그인 로드 ────────────────────────────────────────────────────
var pluginHost = new PluginHost(settings, commandResolver);
pluginHost.LoadAll();

View File

@@ -0,0 +1,253 @@
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L10-3: SSL/TLS 인증서 체커 핸들러. "cert" 프리픽스로 사용합니다.
///
/// 예: cert google.com → google.com의 인증서 정보 조회
/// cert github.com 443 → 포트 지정
/// cert https://example.com → URL 형식도 지원
/// Enter → 결과를 클립보드에 복사.
///
/// ⚠ 외부 인터넷 접속 필요. 사내 모드에서는 내부 호스트만 조회 가능.
/// </summary>
public class CertHandler : IActionHandler
{
public string? Prefix => "cert";
public PluginMetadata Metadata => new(
"Cert",
"SSL/TLS 인증서 체커 — 만료일 · 발급자 · SANs",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
if (string.IsNullOrWhiteSpace(q))
{
items.Add(new LauncherItem(
"SSL 인증서 체커",
"예: cert google.com / cert 192.168.1.1 / cert example.com 8443",
null, null, Symbol: "\uE72E"));
items.Add(new LauncherItem("cert google.com", "google.com 인증서 조회", null, null, Symbol: "\uE72E"));
items.Add(new LauncherItem("cert github.com", "github.com 인증서 조회", null, null, Symbol: "\uE72E"));
items.Add(new LauncherItem("cert 192.168.1.1", "내부 서버 인증서 조회", null, null, Symbol: "\uE72E"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 비동기 조회 시작 — 빠른 반환 후 결과를 기다리지 않음
// 실제 조회는 ExecuteAsync에서 처리하며, 여기서는 "조회 중" 항목만 반환
var (host, port) = ParseHostPort(q);
if (string.IsNullOrWhiteSpace(host))
{
items.Add(new LauncherItem("형식 오류", "예: cert domain.com 또는 cert domain.com 443", null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 사내 모드 확인
var settings = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
var isInternal = settings?.InternalModeEnabled ?? true;
if (isInternal && !IsInternalHost(host))
{
items.Add(new LauncherItem(
"사내 모드 제한",
$"'{host}'은 외부 호스트입니다. 설정에서 사외 모드를 활성화하세요.",
null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem(
$"{host}:{port} 인증서 조회",
"Enter를 눌러 조회하세요",
null,
("check", $"{host}:{port}"),
Symbol: "\uE72E"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public async 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("Cert", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
return;
}
if (item.Data is not ("check", string target)) return;
var parts = target.Split(':');
var host = parts[0];
var port = parts.Length > 1 && int.TryParse(parts[1], out var p) ? p : 443;
NotificationService.Notify("Cert", $"{host}:{port} 인증서 조회 중…");
try
{
var certInfo = await FetchCertInfoAsync(host, port, ct);
var summary = BuildSummary(certInfo);
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(summary));
NotificationService.Notify("Cert", certInfo.StatusLine);
}
catch (OperationCanceledException)
{
NotificationService.Notify("Cert", "조회가 취소되었습니다.");
}
catch (Exception ex)
{
NotificationService.Notify("Cert", $"오류: {ex.Message}");
}
}
// ── 인증서 조회 ──────────────────────────────────────────────────────────
private static async Task<CertInfo> FetchCertInfoAsync(string host, int port, CancellationToken ct)
{
using var client = new TcpClient();
await client.ConnectAsync(host, port, ct);
using var sslStream = new SslStream(
client.GetStream(),
leaveInnerStreamOpen: false,
userCertificateValidationCallback: (_, cert, _, _) => true); // 만료 인증서도 정보 확인
await sslStream.AuthenticateAsClientAsync(
new SslClientAuthenticationOptions
{
TargetHost = host,
RemoteCertificateValidationCallback = (_, _, _, _) => true,
}, ct);
var cert = sslStream.RemoteCertificate as X509Certificate2
?? new X509Certificate2(sslStream.RemoteCertificate!);
return BuildCertInfo(host, port, cert);
}
private static CertInfo BuildCertInfo(string host, int port, X509Certificate2 cert)
{
var now = DateTime.UtcNow;
var notAfter = cert.NotAfter.ToUniversalTime();
var daysLeft = (int)(notAfter - now).TotalDays;
var subject = cert.Subject;
var issuer = cert.Issuer;
// SANs (Subject Alternative Names)
var sans = new List<string>();
foreach (var ext in cert.Extensions)
{
if (ext.Oid?.Value == "2.5.29.17") // SAN OID
{
var raw = ext.Format(false);
sans.AddRange(raw.Split(new[] { ", ", ",\r\n" }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim())
.Where(s => s.StartsWith("DNS Name=", StringComparison.OrdinalIgnoreCase))
.Select(s => s[9..]));
}
}
var status = daysLeft > 30 ? "유효"
: daysLeft > 0 ? "만료 임박"
: "만료됨";
return new CertInfo
{
Host = host,
Port = port,
Subject = subject,
Issuer = issuer,
NotBefore = cert.NotBefore,
NotAfter = cert.NotAfter,
DaysLeft = daysLeft,
Sans = sans,
Thumbprint = cert.Thumbprint,
Status = status,
StatusLine = $"{status} · D-{(daysLeft > 0 ? daysLeft.ToString() : "")} · {host}:{port}",
};
}
private static string BuildSummary(CertInfo c)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"호스트: {c.Host}:{c.Port}");
sb.AppendLine($"상태: {c.Status} (만료까지 {c.DaysLeft}일)");
sb.AppendLine($"발급 대상: {c.Subject}");
sb.AppendLine($"발급 기관: {c.Issuer}");
sb.AppendLine($"유효 시작: {c.NotBefore:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"만료 일자: {c.NotAfter:yyyy-MM-dd HH:mm:ss}");
if (c.Sans.Count > 0)
sb.AppendLine($"SANs: {string.Join(", ", c.Sans.Take(10))}");
sb.AppendLine($"지문(SHA1): {c.Thumbprint}");
return sb.ToString().TrimEnd();
}
// ── 파싱 헬퍼 ────────────────────────────────────────────────────────────
private static (string Host, int Port) ParseHostPort(string q)
{
// https:// 또는 http:// 제거
if (q.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
q = q[8..];
else if (q.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
q = q[7..];
// 경로 제거
var slashIdx = q.IndexOf('/');
if (slashIdx >= 0) q = q[..slashIdx];
var colonIdx = q.LastIndexOf(':');
if (colonIdx >= 0 && int.TryParse(q[(colonIdx + 1)..], out var port))
return (q[..colonIdx], port);
return (q, 443);
}
private static bool IsInternalHost(string host)
{
if (host is "localhost" or "127.0.0.1") return true;
if (host.StartsWith("192.168.")) return true;
if (host.StartsWith("10.")) return true;
if (host.StartsWith("172.16.") || host.StartsWith("172.17.") ||
host.StartsWith("172.18.") || host.StartsWith("172.19.") ||
host.StartsWith("172.20.") || host.StartsWith("172.21.") ||
host.StartsWith("172.22.") || host.StartsWith("172.23.") ||
host.StartsWith("172.24.") || host.StartsWith("172.25.") ||
host.StartsWith("172.26.") || host.StartsWith("172.27.") ||
host.StartsWith("172.28.") || host.StartsWith("172.29.") ||
host.StartsWith("172.30.") || host.StartsWith("172.31.")) return true;
return false;
}
private record CertInfo
{
public string Host { get; init; } = "";
public int Port { get; init; }
public string Subject { get; init; } = "";
public string Issuer { get; init; } = "";
public DateTime NotBefore { get; init; }
public DateTime NotAfter { get; init; }
public int DaysLeft { get; init; }
public List<string> Sans { get; init; } = new();
public string Thumbprint { get; init; } = "";
public string Status { get; init; } = "";
public string StatusLine { get; init; } = "";
}
}

View File

@@ -0,0 +1,284 @@
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L10-4: Lorem Ipsum / 더미 텍스트 생성기 핸들러. "lorem" 프리픽스로 사용합니다.
///
/// 예: lorem → 1단락 생성 (기본)
/// lorem 3 → 3단락 생성
/// lorem words 20 → 단어 20개
/// lorem sentences 5 → 문장 5개
/// lorem ko → 한국어 더미 텍스트 1단락
/// lorem ko 3 → 한국어 더미 텍스트 3단락
/// lorem email 5 → 더미 이메일 주소 5개
/// lorem name 5 → 더미 이름 5개 (한국어)
/// Enter → 결과를 클립보드에 복사.
/// </summary>
public class LoremHandler : IActionHandler
{
public string? Prefix => "lorem";
public PluginMetadata Metadata => new(
"Lorem",
"더미 텍스트 생성기 — Lorem Ipsum · 한국어 · 이메일 · 이름",
"1.0",
"AX");
// ── Lorem Ipsum 단어 풀 ─────────────────────────────────────────────────
private static readonly string[] LoremWords =
[
"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
"sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
"magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud",
"exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo",
"consequat", "duis", "aute", "irure", "in", "reprehenderit", "voluptate",
"velit", "esse", "cillum", "eu", "fugiat", "nulla", "pariatur", "excepteur",
"sint", "occaecat", "cupidatat", "non", "proident", "sunt", "culpa", "qui",
"officia", "deserunt", "mollit", "anim", "id", "est", "laborum", "perspiciatis",
"unde", "omnis", "iste", "natus", "error", "voluptatem", "accusantium",
"doloremque", "laudantium", "totam", "rem", "aperiam", "eaque", "ipsa", "quae",
"ab", "illo", "inventore", "veritatis", "quasi", "architecto", "beatae", "vitae",
"dicta", "explicabo", "nemo", "ipsam", "quia", "voluptas", "aspernatur", "aut",
"odit", "fugit", "consequuntur", "magni", "dolores", "ratione", "sequi",
"nesciunt", "neque", "porro", "quisquam", "dolorem", "numquam", "eius", "modi",
"temporibus", "incidunt", "magnam", "aliquam", "quaerat", "minima", "nostrum",
"exercitationem", "ullam", "corporis", "suscipit", "laboriosam", "nisi",
"aliquid", "commodi", "consequatur", "quidem", "rerum", "facilis",
];
// ── 한국어 더미 단어 풀 ──────────────────────────────────────────────────
private static readonly string[] KorWords =
[
"가나다라", "마바사아", "자차카타", "파하", "데이터", "처리", "시스템", "네트워크",
"소프트웨어", "알고리즘", "데이터베이스", "인터페이스", "프레임워크", "모듈", "클래스",
"메서드", "함수", "변수", "구조체", "배열", "목록", "사전", "집합", "스택", "큐",
"트리", "그래프", "정렬", "탐색", "분석", "설계", "구현", "테스트", "배포", "운영",
"서비스", "플랫폼", "클라우드", "컨테이너", "가상화", "보안", "암호화", "인증", "권한",
"로그", "모니터링", "알림", "이벤트", "트랜잭션", "세션", "쿠키", "토큰", "키",
"값", "객체", "인스턴스", "프로세스", "스레드", "비동기", "병렬", "동기화", "잠금",
"버퍼", "캐시", "인덱스", "쿼리", "뷰", "프로시저", "스키마", "테이블", "컬럼",
"행", "열", "기본키", "외래키", "조인", "집계", "필터", "정렬", "그룹화", "분류",
];
private static readonly string[] KorSentenceStarters =
[
"이 시스템은", "해당 모듈은", "기능을 구현하면", "데이터를 처리하는",
"네트워크 연결이", "서비스가 시작되면", "사용자 인터페이스는", "알고리즘이",
"처리 과정에서", "설계 단계에서", "구현 방식은", "테스트 결과",
];
private static readonly string[] KorSentenceEnders =
[
"처리됩니다.", "구현되어 있습니다.", "필요합니다.", "중요한 역할을 합니다.",
"확인할 수 있습니다.", "설계되어 있습니다.", "활용됩니다.", "반환됩니다.",
"저장됩니다.", "업데이트됩니다.", "삭제됩니다.", "초기화됩니다.",
];
// ── 더미 이름/이메일 데이터 ───────────────────────────────────────────────
private static readonly string[] KorLastNames = ["김", "이", "박", "최", "정", "강", "조", "윤", "장", "임", "한", "오", "서", "신", "권", "황", "안", "송", "류", "전"];
private static readonly string[] KorFirstNames = ["민준", "서연", "예준", "서현", "도윤", "지우", "시우", "수아", "지호", "하은", "준서", "하린", "건우", "소연", "현우", "지민", "우진", "지유", "연우", "채원"];
private static readonly string[] EmailDomains = ["example.com", "test.co.kr", "dummy.net", "sample.org", "mock.io", "placeholder.dev"];
private static readonly Random Rng = new();
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
if (string.IsNullOrWhiteSpace(q))
{
var para = GenerateParagraph(false);
items.Add(new LauncherItem(
"Lorem Ipsum 1단락",
para.Length > 80 ? para[..80] + "…" : para,
null,
("copy", para),
Symbol: "\uE8BD"));
items.Add(new LauncherItem("lorem 3", "3단락 생성", null, null, Symbol: "\uE8BD"));
items.Add(new LauncherItem("lorem words 20", "단어 20개", null, null, Symbol: "\uE8BD"));
items.Add(new LauncherItem("lorem sentences 3", "문장 3개", null, null, Symbol: "\uE8BD"));
items.Add(new LauncherItem("lorem ko", "한국어 더미 텍스트", null, null, Symbol: "\uE8BD"));
items.Add(new LauncherItem("lorem email 5", "더미 이메일 5개", null, null, Symbol: "\uE8BD"));
items.Add(new LauncherItem("lorem name 5", "더미 이름 5개", null, null, Symbol: "\uE8BD"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
switch (sub)
{
case "words":
case "word":
case "w":
{
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 500) : 20;
var text = GenerateWords(cnt, false);
items.Add(new LauncherItem(
$"단어 {cnt}개",
text.Length > 80 ? text[..80] + "…" : text,
null, ("copy", text), Symbol: "\uE8BD"));
break;
}
case "sentences":
case "sentence":
case "s":
{
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 50) : 5;
var text = GenerateSentences(cnt, false);
items.Add(new LauncherItem(
$"문장 {cnt}개",
text.Length > 80 ? text[..80] + "…" : text,
null, ("copy", text), Symbol: "\uE8BD"));
break;
}
case "ko":
case "kor":
case "korean":
{
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 10) : 1;
var text = GenerateParagraphs(cnt, true);
items.Add(new LauncherItem(
$"한국어 더미 텍스트 {cnt}단락",
text.Length > 80 ? text[..80] + "…" : text,
null, ("copy", text), Symbol: "\uE8BD"));
break;
}
case "email":
{
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 20) : 5;
var emails = Enumerable.Range(0, cnt).Select(_ => GenerateEmail()).ToList();
var all = string.Join("\n", emails);
items.Add(new LauncherItem(
$"더미 이메일 {cnt}개",
"전체 복사: Enter",
null, ("copy", all), Symbol: "\uE8BD"));
foreach (var email in emails)
items.Add(new LauncherItem(email, "Enter 복사", null, ("copy", email), Symbol: "\uE8BD"));
break;
}
case "name":
case "names":
{
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 20) : 5;
var names = Enumerable.Range(0, cnt).Select(_ => GenerateKorName()).ToList();
var all = string.Join("\n", names);
items.Add(new LauncherItem(
$"더미 이름 {cnt}개",
"전체 복사: Enter",
null, ("copy", all), Symbol: "\uE8BD"));
foreach (var name in names)
items.Add(new LauncherItem(name, "한국어 이름 · Enter 복사", null, ("copy", name), Symbol: "\uE8BD"));
break;
}
default:
{
// 숫자 단독 → 단락 수
var cnt = int.TryParse(sub, out var n) ? Math.Clamp(n, 1, 10) : 1;
var text = GenerateParagraphs(cnt, false);
items.Add(new LauncherItem(
$"Lorem Ipsum {cnt}단락",
text.Length > 80 ? text[..80] + "…" : text,
null, ("copy", text), Symbol: "\uE8BD"));
break;
}
}
return Task.FromResult<IEnumerable<LauncherItem>>(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("Lorem", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 생성 헬퍼 ─────────────────────────────────────────────────────────────
private static string GenerateParagraphs(int count, bool korean)
{
var paras = Enumerable.Range(0, count).Select(_ => GenerateParagraph(korean));
return string.Join("\n\n", paras);
}
private static string GenerateParagraph(bool korean)
{
var sentenceCount = Rng.Next(4, 8);
return GenerateSentences(sentenceCount, korean);
}
private static string GenerateSentences(int count, bool korean)
{
var sb = new StringBuilder();
for (var i = 0; i < count; i++)
{
if (i > 0) sb.Append(' ');
sb.Append(GenerateSentence(korean));
}
return sb.ToString();
}
private static string GenerateSentence(bool korean)
{
if (korean)
{
var starter = KorSentenceStarters[Rng.Next(KorSentenceStarters.Length)];
var wordCnt = Rng.Next(3, 8);
var words = Enumerable.Range(0, wordCnt).Select(_ => KorWords[Rng.Next(KorWords.Length)]);
var ender = KorSentenceEnders[Rng.Next(KorSentenceEnders.Length)];
return $"{starter} {string.Join(" ", words)} {ender}";
}
else
{
var wordCnt = Rng.Next(6, 15);
var words = Enumerable.Range(0, wordCnt).Select((_, idx) =>
{
var w = LoremWords[Rng.Next(LoremWords.Length)];
return idx == 0 ? char.ToUpper(w[0]) + w[1..] : w;
});
return string.Join(" ", words) + ".";
}
}
private static string GenerateWords(int count, bool korean)
{
var pool = korean ? KorWords : LoremWords;
return string.Join(" ", Enumerable.Range(0, count).Select(_ => pool[Rng.Next(pool.Length)]));
}
private static string GenerateEmail()
{
var first = LoremWords[Rng.Next(LoremWords.Length)];
var second = LoremWords[Rng.Next(LoremWords.Length)];
var num = Rng.Next(10, 999);
var domain = EmailDomains[Rng.Next(EmailDomains.Length)];
return $"{first}.{second}{num}@{domain}";
}
private static string GenerateKorName()
{
var last = KorLastNames[Rng.Next(KorLastNames.Length)];
var first = KorFirstNames[Rng.Next(KorFirstNames.Length)];
return last + first;
}
}

View File

@@ -0,0 +1,300 @@
using System.Security.Cryptography;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L10-2: UUID/GUID 생성기 핸들러. "uuid" 프리픽스로 사용합니다.
///
/// 예: uuid → UUID v4 1개 생성
/// uuid 5 → UUID v4 5개 생성
/// uuid upper → 대문자 UUID 생성
/// uuid v4 → UUID v4 (랜덤)
/// uuid seq → 순차 UUID (시간 기반, 정렬 가능)
/// uuid short → 짧은 고유 ID (8자리 hex)
/// uuid nil → Nil UUID (00000000-…)
/// uuid parse <uuid> → UUID 분석 (버전, 타임스탬프 등)
/// Enter → 결과를 클립보드에 복사.
/// </summary>
public class UuidHandler : IActionHandler
{
public string? Prefix => "uuid";
public PluginMetadata Metadata => new(
"UUID",
"UUID/GUID 생성기 — v4 · 순차 · 짧은 ID · 분석",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
if (string.IsNullOrWhiteSpace(q))
{
// 기본: v4 1개 생성
var uuid = Guid.NewGuid().ToString();
items.Add(new LauncherItem(
uuid,
"UUID v4 (랜덤) · Enter 복사",
null,
("copy", uuid),
Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid 5", "5개 생성", null, null, Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid upper", "대문자 UUID", null, null, Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid seq", "순차 UUID (정렬가능)", null, null, Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid short", "짧은 ID (8자리)", null, null, Symbol: "\uF0E2"));
items.Add(new LauncherItem("uuid parse …", "UUID 분석", null, null, Symbol: "\uF0E2"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
// "uuid parse <value>"
if (sub == "parse" && parts.Length >= 2)
{
items.AddRange(ParseUuid(string.Join(" ", parts.Skip(1))));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid nil"
if (sub == "nil")
{
var nil = "00000000-0000-0000-0000-000000000000";
items.Add(new LauncherItem(nil, "Nil UUID", null, ("copy", nil), Symbol: "\uF0E2"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid seq"
if (sub == "seq")
{
items.AddRange(GenerateSequential(5));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid short"
if (sub == "short")
{
items.AddRange(GenerateShort(5));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid upper"
if (sub == "upper")
{
var upper = Guid.NewGuid().ToString().ToUpperInvariant();
items.Add(new LauncherItem(upper, "대문자 UUID v4 · Enter 복사", null, ("copy", upper), Symbol: "\uF0E2"));
for (var i = 0; i < 4; i++)
{
var u = Guid.NewGuid().ToString().ToUpperInvariant();
items.Add(new LauncherItem(u, "대문자 UUID v4", null, ("copy", u), Symbol: "\uF0E2"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid v4"
if (sub == "v4")
{
items.AddRange(GenerateV4(5));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "uuid <숫자>" — N개 생성
if (int.TryParse(sub, out var count))
{
count = Math.Clamp(count, 1, 20);
items.AddRange(GenerateV4(count));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// UUID 자체를 입력한 경우 → parse
if (Guid.TryParse(q, out _))
{
items.AddRange(ParseUuid(q));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem("알 수 없는 명령",
"예: uuid / uuid 5 / uuid upper / uuid seq / uuid short / uuid parse",
null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(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("UUID", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 생성 헬퍼 ─────────────────────────────────────────────────────────────
private static IEnumerable<LauncherItem> GenerateV4(int count)
{
var all = new List<string>();
for (var i = 0; i < count; i++)
{
var uuid = Guid.NewGuid().ToString();
all.Add(uuid);
}
if (count > 1)
{
yield return new LauncherItem(
$"UUID v4 {count}개",
"전체 복사: Enter",
null,
("copy", string.Join("\n", all)),
Symbol: "\uF0E2");
}
foreach (var uuid in all)
yield return new LauncherItem(uuid, "UUID v4 · Enter 복사", null, ("copy", uuid), Symbol: "\uF0E2");
}
/// <summary>
/// 시간 기반 순차 UUID (UUIDv7 스타일: 밀리초 타임스탬프 + 랜덤 비트).
/// 정렬 가능하고 시간 정보 포함.
/// </summary>
private static IEnumerable<LauncherItem> GenerateSequential(int count)
{
var all = new List<string>();
for (var i = 0; i < count; i++)
{
if (i > 0) System.Threading.Thread.Sleep(1); // 밀리초 차이 보장
var uuid = NewSequentialGuid();
all.Add(uuid);
}
yield return new LauncherItem(
$"순차 UUID {count}개",
"시간 기반 정렬 가능 · 전체 복사: Enter",
null,
("copy", string.Join("\n", all)),
Symbol: "\uF0E2");
foreach (var uuid in all)
yield return new LauncherItem(uuid, "순차 UUID · Enter 복사", null, ("copy", uuid), Symbol: "\uF0E2");
}
private static IEnumerable<LauncherItem> GenerateShort(int count)
{
var all = new List<string>();
for (var i = 0; i < count; i++)
all.Add(NewShortId());
yield return new LauncherItem(
$"짧은 ID {count}개",
"8자리 hex · 전체 복사: Enter",
null,
("copy", string.Join("\n", all)),
Symbol: "\uF0E2");
foreach (var id in all)
yield return new LauncherItem(id, "짧은 ID (8자리) · Enter 복사", null, ("copy", id), Symbol: "\uF0E2");
}
private static IEnumerable<LauncherItem> ParseUuid(string raw)
{
raw = raw.Trim();
if (!Guid.TryParse(raw, out var guid))
{
yield return new LauncherItem("UUID 파싱 실패", $"'{raw}'은 유효한 UUID가 아닙니다", null, null, Symbol: "\uE783");
yield break;
}
var bytes = guid.ToByteArray();
var version = (bytes[7] >> 4) & 0x0F;
var variant = (bytes[8] >> 6) & 0x03;
yield return new LauncherItem(
guid.ToString(),
$"버전 {version} · 변형 {variant}",
null,
("copy", guid.ToString()),
Symbol: "\uF0E2");
yield return new LauncherItem("소문자", guid.ToString(), null, ("copy", guid.ToString()), Symbol: "\uF0E2");
yield return new LauncherItem("대문자", guid.ToString().ToUpper(), null, ("copy", guid.ToString().ToUpper()), Symbol: "\uF0E2");
yield return new LauncherItem("중괄호", $"{{{guid}}}", null, ("copy", $"{{{guid}}}"), Symbol: "\uF0E2");
yield return new LauncherItem("대시 없음", guid.ToString("N"), null, ("copy", guid.ToString("N")), Symbol: "\uF0E2");
yield return new LauncherItem("버전", $"UUID v{version}", null, null, Symbol: "\uF0E2");
yield return new LauncherItem("변형", variant == 2 ? "RFC 4122" : variant == 3 ? "Microsoft" : $"변형 {variant}", null, null, Symbol: "\uF0E2");
// 시간 기반 버전 (v1)이면 타임스탬프 복원 시도
if (version == 1)
{
var ts = ExtractV1Timestamp(bytes);
if (ts.HasValue)
yield return new LauncherItem("타임스탬프", ts.Value.ToString("yyyy-MM-dd HH:mm:ss.fff UTC"), null, null, Symbol: "\uF0E2");
}
}
// ── UUID 생성 구현 ────────────────────────────────────────────────────────
/// <summary>UUIDv7 스타일 순차 GUID: 상위 48비트 = Unix ms 타임스탬프, 하위 = 랜덤</summary>
private static string NewSequentialGuid()
{
var ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var rand = RandomNumberGenerator.GetBytes(10);
var bytes = new byte[16];
// 상위 6바이트 = 타임스탬프 (big-endian ms)
bytes[0] = (byte)(ms >> 40);
bytes[1] = (byte)(ms >> 32);
bytes[2] = (byte)(ms >> 24);
bytes[3] = (byte)(ms >> 16);
bytes[4] = (byte)(ms >> 8);
bytes[5] = (byte)(ms);
// 버전 비트 (v7 = 0111)
bytes[6] = (byte)((rand[0] & 0x0F) | 0x70);
bytes[7] = rand[1];
// 변형 비트 (RFC 4122 = 10xx)
bytes[8] = (byte)((rand[2] & 0x3F) | 0x80);
bytes[9] = rand[3];
// 나머지 랜덤
Array.Copy(rand, 4, bytes, 10, 6);
return new Guid(bytes).ToString();
}
private static string NewShortId()
{
var bytes = RandomNumberGenerator.GetBytes(4);
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
private static DateTime? ExtractV1Timestamp(byte[] bytes)
{
try
{
// UUID v1: time_low(4) + time_mid(2) + time_hi_version(2)
var timeLow = (long)((uint)((bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]));
var timeMid = (long)((ushort)((bytes[5] << 8) | bytes[4]));
var timeHigh = (long)((ushort)((bytes[7] << 8) | bytes[6]) & 0x0FFF);
var ticks = (timeHigh << 48) | (timeMid << 32) | timeLow;
// UUID epoch = Oct 15, 1582
var uuidEpoch = new DateTime(1582, 10, 15, 0, 0, 0, DateTimeKind.Utc);
return uuidEpoch.AddTicks(ticks);
}
catch { return null; }
}
}

View File

@@ -0,0 +1,345 @@
using System.Text;
using System.Windows;
using System.Xml;
using System.Xml.XPath;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L10-1: XML 포맷터·검증기·XPath 쿼리 핸들러. "xml" 프리픽스로 사용합니다.
///
/// 예: xml → 클립보드의 XML 자동 포맷
/// xml <root><a>1</a> → 인라인 XML 포맷
/// xml compact → 클립보드 XML 압축 (공백 제거)
/// xml xpath //a → 클립보드 XML에 XPath 쿼리
/// xml validate → XML 유효성 검증
/// xml attr → XML 속성 목록 추출
/// xml minify → compact 와 동일
/// Enter → 결과를 클립보드에 복사.
/// </summary>
public class XmlHandler : IActionHandler
{
public string? Prefix => "xml";
public PluginMetadata Metadata => new(
"XML",
"XML 포맷터 · 압축 · 검증 · XPath 쿼리",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
if (string.IsNullOrWhiteSpace(q))
{
var clip = GetClipboard();
if (!string.IsNullOrWhiteSpace(clip) && clip.TrimStart().StartsWith('<'))
{
// 클립보드에 XML이 있으면 즉시 포맷 미리보기
items.AddRange(BuildXmlItems(clip, "클립보드"));
}
else
{
items.Add(new LauncherItem("XML 도구",
"예: xml <root>… / xml compact / xml xpath //path / xml validate",
null, null, Symbol: "\uE943"));
items.Add(new LauncherItem("xml compact", "XML 압축 (공백 제거)", null, null, Symbol: "\uE943"));
items.Add(new LauncherItem("xml validate", "XML 유효성 검증", null, null, Symbol: "\uE943"));
items.Add(new LauncherItem("xml xpath //", "XPath 쿼리", null, null, Symbol: "\uE943"));
items.Add(new LauncherItem("xml attr", "속성 목록 추출", null, null, Symbol: "\uE943"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
switch (sub)
{
case "compact":
case "minify":
{
var src = GetClipboard();
if (TryMinify(src, out var mini))
items.Add(new LauncherItem(mini.Length > 80 ? mini[..80] + "…" : mini,
$"압축됨 {src.Length} → {mini.Length} 글자 · Enter 복사",
null, ("copy", mini), Symbol: "\uE943"));
else
items.Add(new LauncherItem("XML 파싱 오류", "클립보드에 유효한 XML이 없습니다", null, null, Symbol: "\uE783"));
break;
}
case "validate":
{
var src = GetClipboard();
if (string.IsNullOrWhiteSpace(src))
{
items.Add(new LauncherItem("클립보드 비어 있음", "XML을 클립보드에 복사 후 시도하세요", null, null, Symbol: "\uE783"));
break;
}
var (ok, err) = ValidateXml(src);
items.Add(ok
? new LauncherItem("✔ 유효한 XML", $"{src.Length:N0}자 · 잘 형식화된 XML입니다", null, null, Symbol: "\uE73E")
: new LauncherItem("✘ XML 오류", err ?? "알 수 없는 오류", null, null, Symbol: "\uE783"));
break;
}
case "xpath":
{
var xpathExpr = parts.Length > 1 ? parts[1].Trim() : "";
if (string.IsNullOrWhiteSpace(xpathExpr))
{
items.Add(new LauncherItem("XPath 식을 입력하세요", "예: xml xpath //item/title", null, null, Symbol: "\uE783"));
break;
}
var src = GetClipboard();
items.AddRange(RunXPath(src, xpathExpr));
break;
}
case "attr":
{
var src = GetClipboard();
items.AddRange(ExtractAttributes(src));
break;
}
default:
{
// 인라인 XML 입력 또는 전체 포맷
var xmlSrc = q.TrimStart().StartsWith('<') ? q : GetClipboard();
items.AddRange(BuildXmlItems(xmlSrc, q.TrimStart().StartsWith('<') ? "입력" : "클립보드"));
break;
}
}
return Task.FromResult<IEnumerable<LauncherItem>>(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("XML", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 빌더 ─────────────────────────────────────────────────────────────────
private static IEnumerable<LauncherItem> BuildXmlItems(string xml, string source)
{
if (string.IsNullOrWhiteSpace(xml))
{
yield return new LauncherItem("XML 없음", "클립보드에 XML을 복사해주세요", null, null, Symbol: "\uE783");
yield break;
}
if (TryFormat(xml, out var formatted))
{
var preview = formatted.Length > 100 ? formatted[..100].Replace("\n", " ") + "…" : formatted.Replace("\n", " ");
yield return new LauncherItem(
$"포맷된 XML ({source})",
preview,
null,
("copy", formatted),
Symbol: "\uE943");
// 루트 요소 이름
if (TryGetRootElement(xml, out var root))
yield return new LauncherItem("루트 요소", $"<{root}>", null, ("copy", root), Symbol: "\uE943");
// 요소 수
var elemCount = CountElements(xml);
yield return new LauncherItem("요소 수", $"{elemCount:N0}개 요소", null, null, Symbol: "\uE943");
// 압축 버전
if (TryMinify(xml, out var mini))
yield return new LauncherItem(
"압축 XML",
$"{xml.Length:N0} → {mini.Length:N0} 글자",
null,
("copy", mini),
Symbol: "\uE943");
}
else
{
yield return new LauncherItem("XML 파싱 오류", "유효한 XML을 입력해주세요", null, null, Symbol: "\uE783");
}
}
private static IEnumerable<LauncherItem> RunXPath(string xml, string xpath)
{
if (string.IsNullOrWhiteSpace(xml))
return [new LauncherItem("XML 없음", "클립보드에 XML을 복사해주세요", null, null, Symbol: "\uE783")];
XPathDocument doc;
try { doc = new XPathDocument(new System.IO.StringReader(xml)); }
catch (Exception ex)
{
return [new LauncherItem("XML 파싱 오류", ex.Message, null, null, Symbol: "\uE783")];
}
XPathNodeIterator iter;
try
{
var nav = doc.CreateNavigator();
iter = nav.Select(xpath);
}
catch (XPathException ex)
{
return [new LauncherItem("XPath 오류", ex.Message, null, null, Symbol: "\uE783")];
}
var results = new List<string>();
while (iter.MoveNext() && results.Count < 20)
results.Add(iter.Current!.OuterXml);
if (results.Count == 0)
return [new LauncherItem("결과 없음", $"XPath '{xpath}' — 일치하는 노드 없음", null, null, Symbol: "\uE946")];
var items = new List<LauncherItem>();
var joined = string.Join("\n", results);
items.Add(new LauncherItem(
$"XPath 결과 {results.Count}개",
results[0].Length > 80 ? results[0][..80] + "…" : results[0],
null, ("copy", joined), Symbol: "\uE943"));
foreach (var r in results.Take(10))
{
var disp = r.Length > 80 ? r[..80] + "…" : r;
items.Add(new LauncherItem(disp, "Enter 복사", null, ("copy", r), Symbol: "\uE943"));
}
return items;
}
private static IEnumerable<LauncherItem> ExtractAttributes(string xml)
{
if (string.IsNullOrWhiteSpace(xml))
return [new LauncherItem("XML 없음", "클립보드에 XML을 복사해주세요", null, null, Symbol: "\uE783")];
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
var attrs = new List<(string Element, string Attr, string Value)>();
foreach (XmlNode node in doc.SelectNodes("//*")!)
{
if (node.Attributes == null) continue;
foreach (XmlAttribute attr in node.Attributes)
attrs.Add((node.Name, attr.Name, attr.Value));
}
if (attrs.Count == 0)
return [new LauncherItem("속성 없음", "XML에 속성(attribute)이 없습니다", null, null, Symbol: "\uE946")];
var items = new List<LauncherItem>
{
new($"속성 {attrs.Count}개", "전체 복사: Enter", null,
("copy", string.Join("\n", attrs.Select(a => $"{a.Element}@{a.Attr}={a.Value}"))),
Symbol: "\uE943"),
};
items.AddRange(attrs.Take(15).Select(a =>
new LauncherItem($"{a.Element}@{a.Attr}", a.Value, null, ("copy", a.Value), Symbol: "\uE943")));
return items;
}
catch (Exception ex)
{
return [new LauncherItem("XML 오류", ex.Message, null, null, Symbol: "\uE783")];
}
}
// ── XML 헬퍼 ─────────────────────────────────────────────────────────────
private static bool TryFormat(string xml, out string result)
{
result = "";
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
var sb = new StringBuilder();
var st = new XmlWriterSettings { Indent = true, IndentChars = " ", NewLineChars = "\n" };
using var writer = XmlWriter.Create(sb, st);
doc.WriteTo(writer);
writer.Flush();
result = sb.ToString();
return true;
}
catch { return false; }
}
private static bool TryMinify(string xml, out string result)
{
result = "";
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
var sb = new StringBuilder();
var st = new XmlWriterSettings { Indent = false, NewLineHandling = NewLineHandling.None };
using var writer = XmlWriter.Create(sb, st);
doc.WriteTo(writer);
writer.Flush();
result = sb.ToString();
return true;
}
catch { return false; }
}
private static (bool Ok, string? Error) ValidateXml(string xml)
{
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
return (true, null);
}
catch (XmlException ex)
{
return (false, $"줄 {ex.LineNumber}, 열 {ex.LinePosition}: {ex.Message}");
}
}
private static bool TryGetRootElement(string xml, out string root)
{
root = "";
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
root = doc.DocumentElement?.Name ?? "";
return !string.IsNullOrEmpty(root);
}
catch { return false; }
}
private static int CountElements(string xml)
{
try
{
var doc = new XmlDocument();
doc.LoadXml(xml);
return doc.SelectNodes("//*")?.Count ?? 0;
}
catch { return 0; }
}
private static string GetClipboard()
{
try
{
return System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.ContainsText() ? Clipboard.GetText() : "");
}
catch { return ""; }
}
}