using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
namespace MergeHub.Services
{
/// 공백을 얼마나 따질지.
public enum WhitespaceMode
{
/// 공백 하나까지 그대로 비교한다.
Exact,
/// 공백의 개수만 무시한다. 들여쓰기 폭을 바꿔도 같은 줄로 본다.
IgnoreAmount,
/// 공백을 아예 없는 셈 친다. 탭/스페이스 정리를 무시할 때.
IgnoreAll
}
///
/// "무엇을 차이로 볼 것인가"를 정하는 규칙.
///
/// 실무에서 diff 를 어지럽히는 것은 대개 내용이 아니라 형식이다 - 들여쓰기 정리,
/// 대소문자, 매번 바뀌는 타임스탬프 줄. 이런 것을 걸러야 진짜 변경이 눈에 들어온다.
///
/// 규칙은 두 갈래로 나뉘어 쓰인다.
/// · — 정렬할 때 "같은 줄인가"를 판단하는 비교용 문자열.
/// 화면에 보이는 원문은 건드리지 않는다.
/// · — 줄 자체를 차이 계산에서 빼는 정규식 필터.
/// 정렬을 마친 뒤에 적용한다. 필터에 걸린 줄까지 같은 것으로 묶어 정렬하면
/// 엉뚱한 줄끼리 짝지어지기 때문이다.
///
public sealed class CompareRules
{
public WhitespaceMode Whitespace { get; set; } = WhitespaceMode.Exact;
public bool IgnoreCase { get; set; }
/// 이 정규식에 걸리는 줄은 차이로 세지 않는다. 잘못된 식은 조용히 버린다.
public IReadOnlyList LinePatterns { get; set; } = Array.Empty();
/// 줄 필터를 켤지. 식은 그대로 두고 잠시 끄고 싶을 때가 있다.
public bool LineFilterEnabled { get; set; } = true;
private Regex[]? _compiled;
private string? _compiledFrom;
/// 어떤 규칙도 켜져 있지 않으면 비교용 변환을 건너뛸 수 있다.
public bool IsPlain =>
Whitespace == WhitespaceMode.Exact && !IgnoreCase;
///
/// 정렬에서 "같은 줄인가"를 따질 때 쓰는 문자열.
/// 원문은 그대로 두고 이 값만 비교하므로 화면 표시는 달라지지 않는다.
///
public string KeyOf(string line)
{
if (line.Length == 0) return line;
var text = Whitespace switch
{
WhitespaceMode.IgnoreAll => StripSpaces(line),
WhitespaceMode.IgnoreAmount => CollapseSpaces(line),
_ => line
};
return IgnoreCase ? text.ToUpperInvariant() : text;
}
/// 줄 필터에 걸리는 줄인지. 걸리면 차이로 세지 않는다.
public bool IsFiltered(string? line)
{
if (!LineFilterEnabled || line is null) return false;
foreach (var rule in Compiled())
{
if (rule.IsMatch(line)) return true;
}
return false;
}
/// 상태바에 한 줄로 요약한다. 켜진 게 없으면 null.
public string? Describe()
{
var parts = new List(3);
if (Whitespace == WhitespaceMode.IgnoreAmount) parts.Add("공백 개수");
else if (Whitespace == WhitespaceMode.IgnoreAll) parts.Add("공백 전부");
if (IgnoreCase) parts.Add("대소문자");
var rules = Compiled().Length;
if (rules > 0) parts.Add($"줄필터 {rules}");
return parts.Count == 0 ? null : "무시: " + string.Join(" · ", parts);
}
///
/// 정규식은 쓸 때마다 다시 만들면 느리므로 캐시한다.
/// 사용자가 식을 고치면 목록 문자열이 달라지므로 그때만 다시 만든다.
///
private Regex[] Compiled()
{
if (!LineFilterEnabled) return Array.Empty();
var signature = string.Join("", LinePatterns);
if (_compiled is not null && _compiledFrom == signature) return _compiled;
var list = new List(LinePatterns.Count);
foreach (var pattern in LinePatterns)
{
if (string.IsNullOrWhiteSpace(pattern)) continue;
try
{
list.Add(new Regex(pattern, RegexOptions.CultureInvariant));
}
catch (ArgumentException)
{
// 사용자가 식을 고치는 중일 수 있다. 잘못된 식 하나 때문에 비교가 멈추면 안 된다.
}
}
_compiledFrom = signature;
_compiled = list.ToArray();
return _compiled;
}
/// 연속된 공백을 하나로 줄이고 앞뒤를 다듬는다.
private static string CollapseSpaces(string line)
{
var sb = new StringBuilder(line.Length);
var pendingSpace = false;
foreach (var c in line)
{
if (char.IsWhiteSpace(c))
{
if (sb.Length > 0) pendingSpace = true;
continue;
}
if (pendingSpace)
{
sb.Append(' ');
pendingSpace = false;
}
sb.Append(c);
}
return sb.ToString();
}
private static string StripSpaces(string line)
{
var sb = new StringBuilder(line.Length);
foreach (var c in line)
if (!char.IsWhiteSpace(c)) sb.Append(c);
return sb.ToString();
}
}
}