80 lines
3.4 KiB
C#
80 lines
3.4 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace VideoDownloader.Browser;
|
|
|
|
/// <summary>
|
|
/// 주소창 입력을 URL 또는 검색 의도로 판별한다(T1-4).
|
|
/// 판별 순서: ① 명시적 스킴 ② localhost/IPv4 ③ 도메인 형태 ④ 키워드 접두(`g `, `yt `) ⑤ 기본 엔진 검색.
|
|
/// </summary>
|
|
public sealed partial class SearchEngineResolver
|
|
{
|
|
private readonly SearchEngine _default;
|
|
private readonly List<SearchEngine> _custom;
|
|
|
|
public SearchEngineResolver(string defaultEngineName, IEnumerable<SearchEngine>? customEngines = null)
|
|
{
|
|
_default = SearchEngines.FindByName(defaultEngineName, customEngines)
|
|
?? SearchEngines.Presets[0];
|
|
_custom = customEngines?.ToList() ?? new();
|
|
}
|
|
|
|
public SearchEngine DefaultEngine => _default;
|
|
|
|
/// <summary>입력이 비었으면 null, URL 이면 URL 의도, 아니면 검색 결과 URL 의도.</summary>
|
|
public NavigationIntent? Resolve(string? input)
|
|
{
|
|
input = input?.Trim();
|
|
if (string.IsNullOrEmpty(input)) return null;
|
|
|
|
if (HasScheme(input))
|
|
return new NavigationIntent(true, input);
|
|
|
|
if (IsLocalHost(input) || IsIpv4(input))
|
|
return new NavigationIntent(true, "https://" + input);
|
|
|
|
if (LooksLikeDomain(input))
|
|
return new NavigationIntent(true, "https://" + input);
|
|
|
|
// 키워드 전환: "g 고양이" → Google 검색. 미등록 키워드는 검색어의 일부.
|
|
if (MatchKeyword(input) is { } kw)
|
|
return new NavigationIntent(false, Format(kw.engine, kw.query), kw.engine.Name);
|
|
|
|
return new NavigationIntent(false, Format(_default, input), _default.Name);
|
|
}
|
|
|
|
private static string Format(SearchEngine engine, string query)
|
|
=> string.Format(engine.QueryTemplate, Uri.EscapeDataString(query).Replace("%20", "+"));
|
|
|
|
private (SearchEngine engine, string query)? MatchKeyword(string input)
|
|
{
|
|
var m = KeywordPrefix().Match(input);
|
|
if (!m.Success) return null;
|
|
var engine = SearchEngines.FindByKeyword(m.Groups[1].Value, _custom);
|
|
return engine == null ? null : (engine, m.Groups[2].Value);
|
|
}
|
|
|
|
private static bool HasScheme(string s) =>
|
|
s.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|
|
|| s.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|
|
|| s.StartsWith("file://", StringComparison.OrdinalIgnoreCase)
|
|
|| s.StartsWith("about:", StringComparison.OrdinalIgnoreCase)
|
|
|| s.StartsWith("data:", StringComparison.OrdinalIgnoreCase)
|
|
|| s.StartsWith("blob:", StringComparison.OrdinalIgnoreCase);
|
|
|
|
private static bool IsLocalHost(string s) =>
|
|
s.Equals("localhost", StringComparison.OrdinalIgnoreCase)
|
|
|| s.StartsWith("localhost:", StringComparison.OrdinalIgnoreCase);
|
|
|
|
private static bool IsIpv4(string s) =>
|
|
s.Length > 7 && s[0] >= '0' && s[0] <= '9' && Regex.IsMatch(s, @"^\d{1,3}(\.\d{1,3}){3}(:\d+)?([/?#].*)?$");
|
|
|
|
private static bool LooksLikeDomain(string s) =>
|
|
DomainPattern().IsMatch(s);
|
|
|
|
// 라벨.라벨[.라벨...] 형태(최소 점 1개) + 선택적 포트/경로. 공백 포함 시 불일치.
|
|
[GeneratedRegex(@"^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)+(:\d+)?([/?#].*)?$")]
|
|
private static partial Regex DomainPattern();
|
|
|
|
[GeneratedRegex(@"^(\S+)\s+(.+)$", RegexOptions.Singleline)]
|
|
private static partial Regex KeywordPrefix();
|
|
}
|