변경 목적: 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개를 확인했습니다.
460 lines
21 KiB
C#
460 lines
21 KiB
C#
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();
|
|
}
|