AX Commander 비교본 런처 기능 대량 이식
변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
This commit is contained in:
459
src/AxCopilot/Handlers/StrHandler.cs
Normal file
459
src/AxCopilot/Handlers/StrHandler.cs
Normal file
@@ -0,0 +1,459 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L20-3: 문자열 조작 도구 핸들러. "str" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: str → 클립보드 텍스트 조작 메뉴
|
||||
/// str escape html → HTML 특수문자 이스케이프
|
||||
/// str unescape html → HTML 이스케이프 해제
|
||||
/// str escape url → URL 인코딩 (퍼센트)
|
||||
/// str unescape url → URL 디코딩
|
||||
/// str escape json → JSON 문자열 이스케이프
|
||||
/// str escape regex → 정규식 이스케이프
|
||||
/// str repeat 3 → 클립보드 텍스트 3회 반복
|
||||
/// str repeat 5 , → 쉼표 구분 5회 반복
|
||||
/// str pad 20 → 20자 우측 공백 패딩
|
||||
/// str pad 20 left → 좌측 패딩
|
||||
/// str pad 20 * right → 지정 문자로 우측 패딩
|
||||
/// str wrap 80 → 80자 줄바꿈
|
||||
/// str lines → 줄 수·단어·문자 통계
|
||||
/// str sort → 줄 정렬 (오름차순)
|
||||
/// str sort desc → 줄 정렬 (내림차순)
|
||||
/// str unique → 중복 줄 제거
|
||||
/// str join , → 여러 줄 → 쉼표 구분 한 줄
|
||||
/// str split , → 쉼표 구분 → 여러 줄
|
||||
/// str replace a b → 텍스트 내 a를 b로 교체
|
||||
/// str extract email → 이메일 주소 추출
|
||||
/// str extract url → URL 추출
|
||||
/// str extract number → 숫자 추출
|
||||
/// Enter → 결과 복사.
|
||||
/// </summary>
|
||||
public partial class StrHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "str";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Str",
|
||||
"문자열 조작 도구 — HTML/URL/JSON 이스케이프·반복·패딩·줄 정렬·추출",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
// 클립보드 읽기
|
||||
string? clipboard = null;
|
||||
try
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (Clipboard.ContainsText()) clipboard = Clipboard.GetText();
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
items.Add(new LauncherItem("문자열 조작 도구",
|
||||
"str escape/unescape / str repeat / str pad / str sort / str extract …",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
if (!string.IsNullOrWhiteSpace(clipboard))
|
||||
{
|
||||
var preview = clipboard!.Length > 40 ? clipboard[..40] + "…" : clipboard;
|
||||
items.Add(new LauncherItem($"클립보드: \"{preview}\"",
|
||||
$"{clipboard.Length}자 · 아래 서브커맨드로 조작", null, null, Symbol: "\uE8AB"));
|
||||
BuildQuickMenu(items, clipboard!);
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem("클립보드가 비어 있습니다", "텍스트를 복사한 뒤 사용하세요",
|
||||
null, null, Symbol: "\uE946"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
var text = parts.Length > 1
|
||||
? string.Join(" ", parts[1..]) // 인라인 텍스트 (없으면 클립보드)
|
||||
: clipboard ?? "";
|
||||
|
||||
// escape / unescape
|
||||
if (sub is "escape" or "esc" or "unescape" or "unesc")
|
||||
{
|
||||
var isEscape = sub is "escape" or "esc";
|
||||
var target = parts.Length >= 2 ? parts[1].ToLowerInvariant() : "html";
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("처리할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var (html, url, json, reg) = (
|
||||
isEscape ? HtmlEncode(src) : HtmlDecode(src),
|
||||
isEscape ? Uri.EscapeDataString(src) : Uri.UnescapeDataString(src),
|
||||
isEscape ? JsonEscape(src) : JsonUnescape(src),
|
||||
isEscape ? RegexEscape(src) : src
|
||||
);
|
||||
var label = isEscape ? "이스케이프" : "이스케이프 해제";
|
||||
items.Add(new LauncherItem($"── {label} ──", "", null, null, Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("HTML", isEscape ? HtmlEncode(src) : HtmlDecode(src)));
|
||||
items.Add(CopyItem("URL", isEscape ? Uri.EscapeDataString(src) : Uri.UnescapeDataString(src)));
|
||||
items.Add(CopyItem("JSON", isEscape ? JsonEscape(src) : JsonUnescape(src)));
|
||||
if (isEscape) items.Add(CopyItem("Regex", RegexEscape(src)));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// repeat
|
||||
if (sub == "repeat")
|
||||
{
|
||||
int count = 3;
|
||||
string sep = "";
|
||||
string src = clipboard ?? "";
|
||||
|
||||
if (parts.Length >= 2 && int.TryParse(parts[1], out var n)) count = Math.Clamp(n, 1, 100);
|
||||
if (parts.Length >= 3) sep = parts[2] == "\\n" ? "\n" : parts[2] == "\\t" ? "\t" : parts[2];
|
||||
if (parts.Length >= 4) src = string.Join(" ", parts[3..]);
|
||||
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("반복할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var result = string.Join(sep, Enumerable.Repeat(src, count));
|
||||
items.Add(new LauncherItem(result.Length > 60 ? result[..60] + "…" : result,
|
||||
$"{count}회 반복 · Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("결과", result));
|
||||
items.Add(CopyItem("결과 길이", $"{result.Length}자"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// pad
|
||||
if (sub == "pad")
|
||||
{
|
||||
if (parts.Length < 2 || !int.TryParse(parts[1], out var width))
|
||||
{ items.Add(ErrorItem("예: str pad 20 / str pad 20 left / str pad 20 * right")); }
|
||||
else
|
||||
{
|
||||
var side = parts.Length >= 3 ? parts[2].ToLowerInvariant() : "right";
|
||||
var padChar = ' ';
|
||||
if (parts.Length >= 4 && parts[2].Length == 1) { padChar = parts[2][0]; side = parts[3].ToLowerInvariant(); }
|
||||
if (parts.Length >= 3 && parts[2].Length == 1 && !"left right both".Contains(parts[2])) padChar = parts[2][0];
|
||||
|
||||
var src = clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("클립보드에 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var result = side switch
|
||||
{
|
||||
"left" => src.PadLeft(width, padChar),
|
||||
"both" => src.PadLeft((src.Length + width) / 2, padChar).PadRight(width, padChar),
|
||||
_ => src.PadRight(width, padChar)
|
||||
};
|
||||
items.Add(new LauncherItem($"\"{result}\"", $"{side} 패딩 {width}자 · Enter 복사",
|
||||
null, ("copy", result), Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("결과", result));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// wrap
|
||||
if (sub == "wrap")
|
||||
{
|
||||
if (parts.Length < 2 || !int.TryParse(parts[1], out var cols))
|
||||
{ items.Add(ErrorItem("예: str wrap 80")); }
|
||||
else
|
||||
{
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("줄바꿈할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var result = WordWrap(src, cols);
|
||||
var preview = result.Split('\n').Take(5);
|
||||
items.Add(new LauncherItem($"{cols}자 줄바꿈",
|
||||
$"{result.Split('\n').Length}줄 · Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
foreach (var line in preview)
|
||||
items.Add(new LauncherItem(line.Length > 60 ? line[..60] + "…" : line, "",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// sort
|
||||
if (sub == "sort")
|
||||
{
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
var desc = parts.Length >= 2 && parts[1].ToLowerInvariant() is "desc" or "d";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("정렬할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var lines = src.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sorted = desc ? lines.OrderByDescending(l => l).ToArray()
|
||||
: lines.OrderBy(l => l).ToArray();
|
||||
var result = string.Join("\n", sorted);
|
||||
items.Add(new LauncherItem($"{sorted.Length}줄 {(desc ? "내림차순" : "오름차순")} 정렬",
|
||||
"Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
foreach (var line in sorted.Take(6))
|
||||
items.Add(new LauncherItem(line.Length > 60 ? line[..60] : line, "",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// unique
|
||||
if (sub is "unique" or "dedup")
|
||||
{
|
||||
var src = parts.Length >= 2 ? string.Join(" ", parts[1..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("중복 제거할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var lines = src.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var unique = lines.Distinct(StringComparer.Ordinal).ToArray();
|
||||
var result = string.Join("\n", unique);
|
||||
items.Add(new LauncherItem($"{lines.Length}줄 → {unique.Length}줄 (중복 {lines.Length - unique.Length}개 제거)",
|
||||
"Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
foreach (var line in unique.Take(6))
|
||||
items.Add(new LauncherItem(line.Length > 60 ? line[..60] : line, "",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// join
|
||||
if (sub == "join")
|
||||
{
|
||||
var sep = parts.Length >= 2 ? parts[1] : ",";
|
||||
if (sep == "\\n") sep = "\n"; else if (sep == "\\t") sep = "\t";
|
||||
var src = clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("연결할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var lines = src.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
var result = string.Join(sep, lines);
|
||||
items.Add(new LauncherItem(result.Length > 60 ? result[..60] + "…" : result,
|
||||
$"{lines.Length}줄 연결 · Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("결과", result));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// split
|
||||
if (sub == "split")
|
||||
{
|
||||
var sep = parts.Length >= 2 ? parts[1] : ",";
|
||||
if (sep == "\\n") sep = "\n"; else if (sep == "\\t") sep = "\t";
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("분리할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var splitted = src.Split(sep, StringSplitOptions.None);
|
||||
var result = string.Join("\n", splitted);
|
||||
items.Add(new LauncherItem($"'{sep}'로 분리 → {splitted.Length}개",
|
||||
"Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
foreach (var item in splitted.Take(8))
|
||||
items.Add(new LauncherItem(item.Length > 60 ? item[..60] : item, "",
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// replace
|
||||
if (sub == "replace")
|
||||
{
|
||||
if (parts.Length < 3) { items.Add(ErrorItem("예: str replace 찾을텍스트 바꿀텍스트")); }
|
||||
else
|
||||
{
|
||||
var from = parts[1];
|
||||
var to = parts[2];
|
||||
var src = parts.Length >= 4 ? string.Join(" ", parts[3..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("처리할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var count = 0;
|
||||
var result = ReplaceCount(src, from, to, out count);
|
||||
items.Add(new LauncherItem($"'{from}' → '{to}' ({count}개 교체)",
|
||||
"Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("결과", result));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// extract
|
||||
if (sub == "extract")
|
||||
{
|
||||
var target = parts.Length >= 2 ? parts[1].ToLowerInvariant() : "url";
|
||||
var src = parts.Length >= 3 ? string.Join(" ", parts[2..]) : clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("추출할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var matches = target switch
|
||||
{
|
||||
"email" => EmailRegex().Matches(src).Select(m => m.Value).Distinct().ToList(),
|
||||
"url" => UrlRegex().Matches(src).Select(m => m.Value).Distinct().ToList(),
|
||||
"num" or "number" or "숫자" =>
|
||||
NumberRegex().Matches(src).Select(m => m.Value).ToList(),
|
||||
"ip" => IpRegex().Matches(src).Select(m => m.Value).Distinct().ToList(),
|
||||
_ => new List<string>()
|
||||
};
|
||||
if (matches.Count == 0)
|
||||
items.Add(new LauncherItem($"'{target}' 패턴 없음", src.Length > 40 ? src[..40] : src,
|
||||
null, null, Symbol: "\uE8AB"));
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem($"{target} {matches.Count}개 추출",
|
||||
"Enter로 전체 복사", null, ("copy", string.Join("\n", matches)), Symbol: "\uE8AB"));
|
||||
foreach (var m in matches.Take(10))
|
||||
items.Add(new LauncherItem(m, "", null, ("copy", m), Symbol: "\uE8AB"));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// lines
|
||||
if (sub is "lines" or "info" or "count")
|
||||
{
|
||||
var src = clipboard ?? "";
|
||||
if (string.IsNullOrEmpty(src)) { items.Add(ErrorItem("분석할 텍스트가 없습니다")); }
|
||||
else
|
||||
{
|
||||
var lineArr = src.Split('\n');
|
||||
var words = src.Split(new[] {' ','\t','\n','\r'}, StringSplitOptions.RemoveEmptyEntries);
|
||||
items.Add(new LauncherItem($"{lineArr.Length}줄 · {words.Length}단어 · {src.Length}자",
|
||||
"텍스트 분석", null, null, Symbol: "\uE8AB"));
|
||||
items.Add(CopyItem("전체 줄 수", lineArr.Length.ToString()));
|
||||
items.Add(CopyItem("빈 줄 수", lineArr.Count(l => string.IsNullOrWhiteSpace(l)).ToString()));
|
||||
items.Add(CopyItem("단어 수", words.Length.ToString()));
|
||||
items.Add(CopyItem("전체 문자 수", src.Length.ToString()));
|
||||
items.Add(CopyItem("공백 제외 문자", src.Count(c => !char.IsWhiteSpace(c)).ToString()));
|
||||
items.Add(CopyItem("바이트 (UTF-8)", Encoding.UTF8.GetByteCount(src).ToString()));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 알 수 없는 커맨드
|
||||
items.Add(new LauncherItem($"알 수 없는 서브커맨드: '{sub}'",
|
||||
"escape · unescape · repeat · pad · wrap · sort · unique · join · split · replace · extract · lines",
|
||||
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("Str", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static void BuildQuickMenu(List<LauncherItem> items, string src)
|
||||
{
|
||||
items.Add(CopyItem("HTML 이스케이프", HtmlEncode(src)));
|
||||
items.Add(CopyItem("URL 인코딩", Uri.EscapeDataString(src)));
|
||||
items.Add(CopyItem("JSON 이스케이프", JsonEscape(src)));
|
||||
}
|
||||
|
||||
private static string HtmlEncode(string s) => s
|
||||
.Replace("&", "&").Replace("<", "<").Replace(">", ">")
|
||||
.Replace("\"", """).Replace("'", "'");
|
||||
|
||||
private static string HtmlDecode(string s) =>
|
||||
HttpUtility.HtmlDecode(s);
|
||||
|
||||
private static string JsonEscape(string s)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var c in s)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"': sb.Append("\\\""); break;
|
||||
case '\\': sb.Append("\\\\"); break;
|
||||
case '\n': sb.Append("\\n"); break;
|
||||
case '\r': sb.Append("\\r"); break;
|
||||
case '\t': sb.Append("\\t"); break;
|
||||
default:
|
||||
if (c < 0x20) sb.Append($"\\u{(int)c:X4}");
|
||||
else sb.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string JsonUnescape(string s) =>
|
||||
s.Replace("\\\"", "\"").Replace("\\\\", "\\")
|
||||
.Replace("\\n", "\n").Replace("\\r", "\r").Replace("\\t", "\t");
|
||||
|
||||
private static string RegexEscape(string s) => Regex.Escape(s);
|
||||
|
||||
private static string WordWrap(string text, int cols)
|
||||
{
|
||||
var words = text.Split(' ');
|
||||
var sb = new StringBuilder();
|
||||
int colPos = 0;
|
||||
foreach (var word in words)
|
||||
{
|
||||
if (colPos + word.Length + 1 > cols && colPos > 0) { sb.Append('\n'); colPos = 0; }
|
||||
else if (colPos > 0) { sb.Append(' '); colPos++; }
|
||||
sb.Append(word);
|
||||
colPos += word.Length;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ReplaceCount(string src, string from, string to, out int count)
|
||||
{
|
||||
count = 0;
|
||||
var sb = new StringBuilder();
|
||||
int pos = 0;
|
||||
while (true)
|
||||
{
|
||||
var idx = src.IndexOf(from, pos, StringComparison.Ordinal);
|
||||
if (idx < 0) { sb.Append(src[pos..]); break; }
|
||||
sb.Append(src[pos..idx]);
|
||||
sb.Append(to);
|
||||
count++;
|
||||
pos = idx + from.Length;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static LauncherItem CopyItem(string label, string value) =>
|
||||
new(label, value, null, ("copy", value), Symbol: "\uE8AB");
|
||||
|
||||
private static LauncherItem ErrorItem(string msg) =>
|
||||
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
||||
|
||||
[GeneratedRegex(@"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}")]
|
||||
private static partial Regex EmailRegex();
|
||||
|
||||
[GeneratedRegex(@"https?://[^\s""'<>]+")]
|
||||
private static partial Regex UrlRegex();
|
||||
|
||||
[GeneratedRegex(@"-?\d+(\.\d+)?")]
|
||||
private static partial Regex NumberRegex();
|
||||
|
||||
[GeneratedRegex(@"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")]
|
||||
private static partial Regex IpRegex();
|
||||
}
|
||||
Reference in New Issue
Block a user