543 lines
24 KiB
C#
543 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace MergeHub.Services
|
|
{
|
|
/// <summary>C# 여는 중괄호 · else/catch/finally 를 어디에 둘지.</summary>
|
|
public enum CSharpBraceStyle
|
|
{
|
|
/// <summary>맞추지 않는다. 파일 그대로 견준다.</summary>
|
|
None = 0,
|
|
|
|
/// <summary>
|
|
/// AURORA. Visual Studio [텍스트 편집기 > C# > 코드 스타일 > 서식 > 줄 바꿈] 에서
|
|
/// 형식 · 메서드와 로컬 함수의 여는 중괄호만 새 줄, 나머지(속성 · 접근자 · 익명 메서드 · 제어 블록 ·
|
|
/// 익명 형식 · 초기화 · 람다)는 같은 줄. else · catch · finally 는 같은 줄(<c>} else {</c>).
|
|
/// </summary>
|
|
Aurora = 1,
|
|
|
|
/// <summary>Allman. 여는 중괄호는 모두 새 줄, else · catch · finally 도 새 줄(Visual Studio 기본값과 같다).</summary>
|
|
Allman = 2,
|
|
}
|
|
|
|
/// <summary>
|
|
/// C# 코드의 중괄호 줄바꿈을 한 가지 스타일로 맞춘다(6차 추가 - C# 코드 스타일 맞춤).
|
|
///
|
|
/// 같은 코드인데 한쪽은 <c>if (x) {</c>, 다른 쪽은 <c>if (x)</c> + 다음 줄 <c>{</c> 로 쓰면
|
|
/// 줄 단위 비교에서 그 자리마다 차이가 둘씩 뜬다. 스타일이 다른 사람 코드를 병합할 때 진짜 변경이 묻힌다.
|
|
/// 그래서 비교하기 전에 양쪽을 같은 스타일로 맞춘다.
|
|
///
|
|
/// <b>줄의 위치만</b> 옮긴다 - 여는 중괄호를 앞 줄 끝으로 올리거나 다음 줄로 내리고,
|
|
/// <c>}</c> 와 else/catch/finally 를 붙이거나 뗀다. 들여쓰기 · 공백 · 그 밖의 서식은 건드리지 않는다.
|
|
/// 주석 · 문자열 · 보간 문자열 · 전처리 줄 안의 중괄호는 코드로 보지 않는다.
|
|
/// 앞 줄 끝에 주석이 있으면 올리지 않는다(중괄호가 주석 안으로 들어가 버린다).
|
|
/// 스타일을 판단할 수 없는 중괄호(그냥 블록 · 중첩 배열 초기화)는 그대로 둔다.
|
|
/// </summary>
|
|
public static class CSharpStyle
|
|
{
|
|
/// <summary>중괄호가 여는 것의 갈래. Visual Studio 옵션의 항목과 같게 나눈다.</summary>
|
|
public enum BraceKind
|
|
{
|
|
/// <summary>판단하지 않는다(그냥 블록 · 알 수 없음). 옮기지 않는다.</summary>
|
|
Unknown,
|
|
Type,
|
|
Method,
|
|
Property,
|
|
Accessor,
|
|
AnonymousMethod,
|
|
Control,
|
|
AnonymousType,
|
|
Initializer,
|
|
Lambda,
|
|
}
|
|
|
|
/// <summary>이 파일에 스타일 맞춤을 걸지. C# 파일(.cs)만.</summary>
|
|
public static bool Applies(string? path, CSharpBraceStyle style) =>
|
|
style != CSharpBraceStyle.None && !string.IsNullOrEmpty(path) &&
|
|
string.Equals(Path.GetExtension(path), ".cs", StringComparison.OrdinalIgnoreCase);
|
|
|
|
public static string Label(CSharpBraceStyle style) => style switch
|
|
{
|
|
CSharpBraceStyle.Aurora => "AURORA",
|
|
CSharpBraceStyle.Allman => "Allman",
|
|
_ => "끔",
|
|
};
|
|
|
|
/// <summary>
|
|
/// 글 전체를 스타일에 맞춘다. 바꿀 것이 없으면 <b>받은 글을 그대로</b> 돌려준다(줄끝도 그대로).
|
|
/// 바꾸면 줄끝은 글에 CRLF 가 하나라도 있으면 CRLF, 아니면 LF 로 잇는다.
|
|
/// </summary>
|
|
public static string Format(string text, CSharpBraceStyle style)
|
|
{
|
|
if (style == CSharpBraceStyle.None || string.IsNullOrEmpty(text)) return text;
|
|
|
|
var newLine = text.IndexOf("\r\n", StringComparison.Ordinal) >= 0 ? "\r\n" : "\n";
|
|
var endsWithNewLine = text.EndsWith("\n", StringComparison.Ordinal);
|
|
|
|
var lines = new List<string>(text.Replace("\r\n", "\n").Split('\n'));
|
|
if (endsWithNewLine) lines.RemoveAt(lines.Count - 1);
|
|
|
|
var any = false;
|
|
for (var pass = 0; pass < 8; pass++)
|
|
{
|
|
var changed = PlaceKeywords(lines, style);
|
|
changed |= PlaceBraces(lines, style);
|
|
if (!changed) break;
|
|
any = true;
|
|
}
|
|
|
|
if (!any) return text;
|
|
|
|
var result = string.Join(newLine, lines);
|
|
return endsWithNewLine ? result + newLine : result;
|
|
}
|
|
|
|
/// <summary>그 갈래의 여는 중괄호를 새 줄에 둘지.</summary>
|
|
public static bool OpenBraceOnNewLine(BraceKind kind, CSharpBraceStyle style) =>
|
|
style == CSharpBraceStyle.Allman || kind is BraceKind.Type or BraceKind.Method;
|
|
|
|
// ================= else · catch · finally =================
|
|
|
|
private static readonly Regex KeywordLine = new Regex(@"^(else|catch|finally)\b", RegexOptions.Compiled);
|
|
private static readonly Regex CloseThenKeyword = new Regex(@"^\}\s*(else|catch|finally)\b", RegexOptions.Compiled);
|
|
|
|
private static bool PlaceKeywords(List<string> lines, CSharpBraceStyle style)
|
|
{
|
|
var mask = Mask(lines);
|
|
var changed = false;
|
|
|
|
for (var i = lines.Count - 1; i >= 1; i--)
|
|
{
|
|
if (mask.Inside[i] || mask.Preprocessor[i]) continue;
|
|
var code = mask.Code[i].Trim();
|
|
|
|
if (style == CSharpBraceStyle.Aurora)
|
|
{
|
|
// } → } else {
|
|
// else {
|
|
if (!KeywordLine.IsMatch(code)) continue;
|
|
|
|
var p = i - 1;
|
|
if (mask.Inside[p] || mask.Preprocessor[p]) continue;
|
|
if (lines[p].Trim() != "}" || mask.Code[p].Trim() != "}") continue;
|
|
|
|
lines[p] = lines[p].TrimEnd() + " " + lines[i].TrimStart();
|
|
lines.RemoveAt(i);
|
|
changed = true;
|
|
}
|
|
else
|
|
{
|
|
// } else { → }
|
|
// else {
|
|
if (!CloseThenKeyword.IsMatch(code) || !lines[i].TrimStart().StartsWith("}", StringComparison.Ordinal)) continue;
|
|
|
|
var indent = Indent(lines[i]);
|
|
var rest = lines[i].TrimStart().Substring(1).TrimStart();
|
|
lines[i] = indent + "}";
|
|
lines.Insert(i + 1, indent + rest);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
// ================= 여는 중괄호 =================
|
|
|
|
private static bool PlaceBraces(List<string> lines, CSharpBraceStyle style)
|
|
{
|
|
var mask = Mask(lines);
|
|
var changed = false;
|
|
|
|
for (var i = lines.Count - 1; i >= 0; i--)
|
|
{
|
|
if (mask.Inside[i] || mask.Preprocessor[i]) continue;
|
|
|
|
var code = mask.Code[i];
|
|
var first = FirstNonSpace(code);
|
|
if (first < 0) continue;
|
|
|
|
// (가) 줄이 '{' 로 시작한다 → 같은 줄 스타일이면 앞 줄 끝으로 올린다.
|
|
if (code[first] == '{')
|
|
{
|
|
if (i == 0) continue;
|
|
var p = i - 1;
|
|
if (mask.Inside[p] || mask.Preprocessor[p]) continue;
|
|
|
|
var prevCode = mask.Code[p].TrimEnd();
|
|
if (prevCode.Trim().Length == 0) continue;
|
|
|
|
// 앞 줄 끝에 주석이 있으면 올리지 않는다 - 원문 끝과 코드 끝이 같아야 한다.
|
|
if (lines[p].TrimEnd().Length != prevCode.Length) continue;
|
|
|
|
var (kind, _) = Classify(mask, i, first);
|
|
if (kind == BraceKind.Unknown || OpenBraceOnNewLine(kind, style)) continue;
|
|
|
|
lines[p] = lines[p].TrimEnd() + " " + lines[i].TrimStart();
|
|
lines.RemoveAt(i);
|
|
changed = true;
|
|
continue;
|
|
}
|
|
|
|
// (나) 코드 뒤 줄 끝에 '{' 가 있다 → 새 줄 스타일이면 다음 줄로 내린다.
|
|
var trimmed = code.TrimEnd();
|
|
if (trimmed.Length == 0 || trimmed[trimmed.Length - 1] != '{') continue;
|
|
|
|
var brace = trimmed.Length - 1;
|
|
if (code.Substring(0, brace).Trim().Length == 0) continue;
|
|
if (code.Substring(0, brace).TrimStart().StartsWith("}", StringComparison.Ordinal)) continue;
|
|
|
|
var (k, headerLine) = Classify(mask, i, brace);
|
|
if (k == BraceKind.Unknown || !OpenBraceOnNewLine(k, style)) continue;
|
|
|
|
var original = lines[i];
|
|
lines[i] = original.Substring(0, brace).TrimEnd();
|
|
lines.Insert(i + 1, Indent(lines[headerLine]) + original.Substring(brace));
|
|
changed = true;
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// (line, col) 의 여는 중괄호가 무엇을 여는지. 앞쪽 코드를 거꾸로 읽어 머리(선언 · 제어문 등)를 모은다.
|
|
/// 머리가 시작한 줄 번호도 준다 - 중괄호를 내릴 때 그 줄의 들여쓰기를 따른다.
|
|
/// </summary>
|
|
private static (BraceKind Kind, int HeaderLine) Classify(Masked mask, int line, int col)
|
|
{
|
|
var chars = new List<char>(128);
|
|
int paren = 0, bracket = 0, angle = 0;
|
|
var start = line;
|
|
var pos = col - 1;
|
|
var l = line;
|
|
|
|
while (chars.Count < 4000)
|
|
{
|
|
if (pos < 0)
|
|
{
|
|
l--;
|
|
if (l < 0 || mask.Preprocessor[l]) break;
|
|
pos = mask.Code[l].Length - 1;
|
|
chars.Add(' ');
|
|
continue;
|
|
}
|
|
|
|
var text = mask.Code[l];
|
|
var ch = text[pos];
|
|
|
|
if (ch == ')') paren++;
|
|
else if (ch == '(') { if (paren == 0) break; paren--; }
|
|
else if (ch == ']') bracket++;
|
|
else if (ch == '[') { if (bracket == 0) break; bracket--; }
|
|
else if (ch == '>' && !(pos > 0 && (text[pos - 1] == '=' || text[pos - 1] == '-')) &&
|
|
!(pos + 1 < text.Length && text[pos + 1] == '='))
|
|
angle++;
|
|
else if (ch == '<') { if (angle > 0) angle--; }
|
|
else if (paren == 0 && bracket == 0 && (ch == ';' || ch == '{' || ch == '}')) break;
|
|
else if (paren == 0 && bracket == 0 && angle == 0 && ch == ',') break;
|
|
|
|
if (!char.IsWhiteSpace(ch)) start = l;
|
|
chars.Add(ch);
|
|
pos--;
|
|
}
|
|
|
|
chars.Reverse();
|
|
return (KindOfHeader(new string(chars.ToArray())), start);
|
|
}
|
|
|
|
private static readonly HashSet<string> ControlWords = new HashSet<string>(StringComparer.Ordinal)
|
|
{
|
|
"if", "else", "for", "foreach", "while", "do", "switch", "try", "catch", "finally",
|
|
"using", "lock", "fixed", "unsafe", "checked", "unchecked",
|
|
};
|
|
|
|
private static readonly Regex Spaces = new Regex(@"\s+", RegexOptions.Compiled);
|
|
private static readonly Regex CaseLabel = new Regex(@"^(case\b.*|default)\s*:$", RegexOptions.Compiled);
|
|
private static readonly Regex AccessorHeader = new Regex(@"^((public|private|protected|internal)\s+)*(get|set|init|add|remove)$", RegexOptions.Compiled);
|
|
private static readonly Regex TypeKeyword = new Regex(@"\b(class|struct|interface|enum|record|namespace)\b", RegexOptions.Compiled);
|
|
private static readonly Regex WhereClause = new Regex(@"\bwhere\s+\w+\s*:", RegexOptions.Compiled);
|
|
|
|
/// <summary>중괄호 앞 머리 글로 갈래를 정한다. 검사에서 직접 부를 수 있게 열어 둔다.</summary>
|
|
public static BraceKind KindOfHeader(string header)
|
|
{
|
|
var h = Spaces.Replace(header ?? string.Empty, " ").Trim();
|
|
|
|
// 앞에 붙은 특성([Test] 등)은 뗀다.
|
|
while (h.StartsWith("[", StringComparison.Ordinal))
|
|
{
|
|
var close = MatchingClose(h, 0, '[', ']');
|
|
if (close < 0) break;
|
|
h = h.Substring(close + 1).TrimStart();
|
|
}
|
|
|
|
if (h.Length == 0) return BraceKind.Unknown;
|
|
if (h.EndsWith("=>", StringComparison.Ordinal)) return BraceKind.Lambda;
|
|
if (CaseLabel.IsMatch(h)) return BraceKind.Control;
|
|
|
|
var firstWord = Regex.Match(h, @"^[A-Za-z_]\w*").Value;
|
|
if (ControlWords.Contains(firstWord)) return BraceKind.Control;
|
|
if (AccessorHeader.IsMatch(h)) return BraceKind.Accessor;
|
|
|
|
var paren = h.IndexOf('(');
|
|
var type = TypeKeyword.Match(h);
|
|
if (type.Success && (paren < 0 || type.Index < paren)) return BraceKind.Type;
|
|
|
|
if (Regex.IsMatch(h, @"\bdelegate\b")) return BraceKind.AnonymousMethod;
|
|
|
|
// 제약이 붙은 제네릭 메서드: void M<T>() where T : new()
|
|
if (WhereClause.IsMatch(h) && paren >= 0) return BraceKind.Method;
|
|
|
|
if (h == "new") return BraceKind.AnonymousType;
|
|
if (EndsWithCreation(h)) return BraceKind.Initializer;
|
|
if (HasAssignment(h)) return BraceKind.Initializer;
|
|
if (h.EndsWith("switch", StringComparison.Ordinal)) return BraceKind.Initializer;
|
|
if (h.EndsWith(")", StringComparison.Ordinal)) return BraceKind.Method;
|
|
|
|
var last = h[h.Length - 1];
|
|
if (char.IsLetterOrDigit(last) || last == '_' || last == '>' || last == ']' || last == '?') return BraceKind.Property;
|
|
|
|
return BraceKind.Unknown;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 머리가 <c>new 형식</c> · <c>new 형식(인자)</c> · <c>new[]</c> · <c>new()</c> 로 끝나는지 - 개체 · 컬렉션 · 배열 초기화.
|
|
/// 멤버 숨기기 한정자(<c>public new void M()</c>)는 new 뒤에 낱말이 둘 이상이라 아니다.
|
|
/// </summary>
|
|
private static bool EndsWithCreation(string h)
|
|
{
|
|
var at = LastWord(h, "new");
|
|
if (at < 0) return false;
|
|
|
|
var rest = h.Substring(at + 3).Trim();
|
|
|
|
// 끝의 (인자) 한 덩어리를 뗀다.
|
|
if (rest.EndsWith(")", StringComparison.Ordinal))
|
|
{
|
|
var open = MatchingOpen(rest, rest.Length - 1, '(', ')');
|
|
if (open < 0) return false;
|
|
rest = rest.Substring(0, open).Trim();
|
|
}
|
|
|
|
if (rest.Length == 0) return true; // new() · new (...)
|
|
|
|
// 형식 이름 하나(점 · 제네릭 · 배열 표시 포함), 공백으로 나뉜 낱말이 없어야 한다.
|
|
var depth = 0;
|
|
foreach (var c in rest)
|
|
{
|
|
if (c == '<' || c == '[') depth++;
|
|
else if (c == '>' || c == ']') depth--;
|
|
else if (c == ' ' && depth == 0) return false;
|
|
else if (depth == 0 && !(char.IsLetterOrDigit(c) || c == '_' || c == '.' || c == '?')) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>괄호 밖에 대입 '=' 가 있는지(==, !=, <=, >=, => 는 빼고).</summary>
|
|
private static bool HasAssignment(string h)
|
|
{
|
|
var depth = 0;
|
|
for (var i = 0; i < h.Length; i++)
|
|
{
|
|
var c = h[i];
|
|
if (c == '(' || c == '[') depth++;
|
|
else if (c == ')' || c == ']') depth--;
|
|
else if (c == '=' && depth == 0)
|
|
{
|
|
var prev = i > 0 ? h[i - 1] : ' ';
|
|
var next = i + 1 < h.Length ? h[i + 1] : ' ';
|
|
if (next == '=' || next == '>' || prev == '=' || prev == '!' || prev == '<' || prev == '>') continue;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static int LastWord(string h, string word)
|
|
{
|
|
var at = h.Length;
|
|
while (true)
|
|
{
|
|
at = h.LastIndexOf(word, at - 1, StringComparison.Ordinal);
|
|
if (at < 0) return -1;
|
|
|
|
var before = at == 0 || !(char.IsLetterOrDigit(h[at - 1]) || h[at - 1] == '_');
|
|
var after = at + word.Length >= h.Length || !(char.IsLetterOrDigit(h[at + word.Length]) || h[at + word.Length] == '_');
|
|
if (before && after) return at;
|
|
if (at == 0) return -1;
|
|
}
|
|
}
|
|
|
|
private static int MatchingClose(string s, int open, char o, char c)
|
|
{
|
|
var depth = 0;
|
|
for (var i = open; i < s.Length; i++)
|
|
{
|
|
if (s[i] == o) depth++;
|
|
else if (s[i] == c && --depth == 0) return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static int MatchingOpen(string s, int close, char o, char c)
|
|
{
|
|
var depth = 0;
|
|
for (var i = close; i >= 0; i--)
|
|
{
|
|
if (s[i] == c) depth++;
|
|
else if (s[i] == o && --depth == 0) return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static int FirstNonSpace(string s)
|
|
{
|
|
for (var i = 0; i < s.Length; i++)
|
|
if (!char.IsWhiteSpace(s[i])) return i;
|
|
return -1;
|
|
}
|
|
|
|
private static string Indent(string line) => line.Substring(0, line.Length - line.TrimStart().Length);
|
|
|
|
// ================= 주석 · 문자열 가리기 =================
|
|
|
|
/// <summary>줄마다 코드만 남긴 글(주석은 공백, 문자열 속은 '_'). 길이는 원문과 같다.</summary>
|
|
private sealed class Masked
|
|
{
|
|
public Masked(string[] code, bool[] inside, bool[] preprocessor)
|
|
{
|
|
Code = code;
|
|
Inside = inside;
|
|
Preprocessor = preprocessor;
|
|
}
|
|
|
|
public string[] Code { get; }
|
|
|
|
/// <summary>줄이 블록 주석 · 여러 줄 문자열 안에서 시작하는지.</summary>
|
|
public bool[] Inside { get; }
|
|
|
|
public bool[] Preprocessor { get; }
|
|
}
|
|
|
|
private enum Mode { Code, LineComment, BlockComment, Str, VerbatimStr, InterpStr, InterpVerbatimStr, Char }
|
|
|
|
private static Masked Mask(IReadOnlyList<string> lines)
|
|
{
|
|
var src = string.Join("\n", lines);
|
|
var mask = src.ToCharArray();
|
|
var modes = new Stack<Mode>();
|
|
modes.Push(Mode.Code);
|
|
var holes = new Stack<int>(); // 보간 구멍마다 그 안의 중괄호 깊이
|
|
|
|
var inside = new bool[lines.Count];
|
|
var line = 0;
|
|
var n = src.Length;
|
|
var i = 0;
|
|
|
|
while (i < n)
|
|
{
|
|
var ch = src[i];
|
|
var next = i + 1 < n ? src[i + 1] : '\0';
|
|
var next2 = i + 2 < n ? src[i + 2] : '\0';
|
|
|
|
if (ch == '\n')
|
|
{
|
|
var top = modes.Peek();
|
|
if (top is Mode.LineComment or Mode.Str or Mode.Char or Mode.InterpStr) { modes.Pop(); top = modes.Peek(); }
|
|
line++;
|
|
if (line < inside.Length) inside[line] = top != Mode.Code || holes.Count > 0;
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
switch (modes.Peek())
|
|
{
|
|
case Mode.Code:
|
|
if (ch == '/' && next == '/') { modes.Push(Mode.LineComment); mask[i] = ' '; mask[i + 1] = ' '; i += 2; continue; }
|
|
if (ch == '/' && next == '*') { modes.Push(Mode.BlockComment); mask[i] = ' '; mask[i + 1] = ' '; i += 2; continue; }
|
|
if ((ch == '$' && next == '@' && next2 == '"') || (ch == '@' && next == '$' && next2 == '"'))
|
|
{ modes.Push(Mode.InterpVerbatimStr); Hide(mask, i, 3, holes); i += 3; continue; }
|
|
if (ch == '$' && next == '"') { modes.Push(Mode.InterpStr); Hide(mask, i, 2, holes); i += 2; continue; }
|
|
if (ch == '@' && next == '"') { modes.Push(Mode.VerbatimStr); Hide(mask, i, 2, holes); i += 2; continue; }
|
|
if (ch == '"') { modes.Push(Mode.Str); Hide(mask, i, 1, holes); i++; continue; }
|
|
if (ch == '\'') { modes.Push(Mode.Char); Hide(mask, i, 1, holes); i++; continue; }
|
|
|
|
if (holes.Count > 0)
|
|
{
|
|
if (ch == '{') holes.Push(holes.Pop() + 1);
|
|
else if (ch == '}')
|
|
{
|
|
var depth = holes.Pop();
|
|
if (depth == 0) { modes.Pop(); mask[i] = '_'; i++; continue; }
|
|
holes.Push(depth - 1);
|
|
}
|
|
|
|
mask[i] = '_';
|
|
}
|
|
|
|
i++;
|
|
continue;
|
|
|
|
case Mode.LineComment:
|
|
mask[i] = ' ';
|
|
i++;
|
|
continue;
|
|
|
|
case Mode.BlockComment:
|
|
if (ch == '*' && next == '/') { mask[i] = ' '; mask[i + 1] = ' '; modes.Pop(); i += 2; continue; }
|
|
mask[i] = ' ';
|
|
i++;
|
|
continue;
|
|
|
|
case Mode.Str:
|
|
case Mode.Char:
|
|
var quote = modes.Peek() == Mode.Str ? '"' : '\'';
|
|
if (ch == '\\') { mask[i] = '_'; if (i + 1 < n && next != '\n') mask[i + 1] = '_'; i += next == '\n' ? 1 : 2; continue; }
|
|
if (ch == quote) { modes.Pop(); mask[i] = '_'; i++; continue; }
|
|
mask[i] = '_';
|
|
i++;
|
|
continue;
|
|
|
|
case Mode.VerbatimStr:
|
|
if (ch == '"' && next == '"') { mask[i] = '_'; mask[i + 1] = '_'; i += 2; continue; }
|
|
if (ch == '"') modes.Pop();
|
|
mask[i] = '_';
|
|
i++;
|
|
continue;
|
|
|
|
case Mode.InterpStr:
|
|
case Mode.InterpVerbatimStr:
|
|
var verbatim = modes.Peek() == Mode.InterpVerbatimStr;
|
|
if ((ch == '{' && next == '{') || (ch == '}' && next == '}') || (verbatim && ch == '"' && next == '"'))
|
|
{ mask[i] = '_'; mask[i + 1] = '_'; i += 2; continue; }
|
|
if (!verbatim && ch == '\\') { mask[i] = '_'; if (next != '\n' && i + 1 < n) mask[i + 1] = '_'; i += next == '\n' ? 1 : 2; continue; }
|
|
if (ch == '"') { modes.Pop(); mask[i] = '_'; i++; continue; }
|
|
if (ch == '{') { modes.Push(Mode.Code); holes.Push(0); mask[i] = '_'; i++; continue; }
|
|
mask[i] = '_';
|
|
i++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var code = new string(mask).Split('\n');
|
|
var preprocessor = new bool[code.Length];
|
|
for (var k = 0; k < code.Length; k++)
|
|
preprocessor[k] = !inside[k] && code[k].TrimStart().StartsWith("#", StringComparison.Ordinal);
|
|
|
|
return new Masked(code, inside, preprocessor);
|
|
}
|
|
|
|
/// <summary>문자열 여는 기호를 가린다. 보간 구멍 안에서 연 문자열이면 원래도 가려져야 할 자리다.</summary>
|
|
private static void Hide(char[] mask, int at, int count, Stack<int> holes)
|
|
{
|
|
for (var k = 0; k < count && at + k < mask.Length; k++) mask[at + k] = '_';
|
|
}
|
|
}
|
|
}
|