using System; using System.Collections.Generic; using MergeHub.Views.Controls; namespace MergeHub.Services { /// /// 언어 표()를 보고 한 줄을 토큰으로 쪼갠다. /// /// C# 은 전용 토크나이저로 넘긴다 - 축자·보간 문자열과 접근제한자 구분이 표로 담기지 않고, /// 이미 테스트로 굳어 있어 굳이 다시 만들 이유가 없다. /// 마크업(XML · XAML · HTML)도 따로 보낸다 - 꺾쇠 안이냐 밖이냐로 갈리는 규칙이라 /// 여기의 단어 단위 훑기에 얹을 수가 없다. 를 보라. /// 평문·Markdown 처럼 코드가 아닌 형식은 토큰을 만들지 않는다(그냥 검은 글씨로 나온다). /// public static class CodeTokenizer { private const string OperatorChars = "=+-*/%!<>&|^~?:"; private const string PunctuationChars = "(){}[];,."; private static readonly IReadOnlyList Empty = Array.Empty(); public static IReadOnlyList Tokenize(string line, LanguageProfile profile, CarryOver carry, out CarryOver next) { if (profile.UseCSharpTokenizer) { var wasInComment = (carry & CarryOver.BlockComment) != 0; var result = CSharpTokenizer.Tokenize(line, wasInComment, out var stillInComment); next = stillInComment ? CarryOver.BlockComment : CarryOver.None; return result; } if (profile.UseMarkupTokenizer) return MarkupTokenizer.Tokenize(line, profile, carry, out next); // 아래로는 꺾쇠 상태를 쓰지 않는다. 블록 주석만 넘긴다. var inBlockComment = (carry & CarryOver.BlockComment) != 0; next = carry & CarryOver.BlockComment; if (!profile.IsCode || string.IsNullOrEmpty(line)) return Empty; var tokens = new List(); var i = 0; // 앞 줄에서 넘어온 블록 주석을 먼저 닫는다. if (inBlockComment && profile.HasBlockComment) { var close = line.IndexOf(profile.BlockEnd!, StringComparison.Ordinal); if (close < 0) { tokens.Add(new CodeToken(0, line.Length, CodeTokenKind.Comment)); return tokens; } i = close + profile.BlockEnd!.Length; tokens.Add(new CodeToken(0, i, CodeTokenKind.Comment)); next = CarryOver.None; } // ---- 전처리 지시문 ---- // // 줄 첫머리가 # 이면 그 줄 전체가 지시문이다(#include · #define · #ifdef). // C# 쪽과 같은 규칙으로 칠한다 - 같은 모양인데 언어마다 다르게 보이면 더 헷갈린다. // 블록 주석이 이미 열려 있던 줄은 해당 없다(i 가 0 이 아니다). if (profile.HasPreprocessor && i == 0) { var firstVisible = 0; while (firstVisible < line.Length && char.IsWhiteSpace(line[firstVisible])) firstVisible++; if (firstVisible < line.Length && line[firstVisible] == '#') { tokens.Add(new CodeToken(firstVisible, line.Length - firstVisible, CodeTokenKind.Preprocessor)); return tokens; } } while (i < line.Length) { var start = i; var c = line[i]; // ---- 한 줄 주석 ---- if (TryMatchLineComment(line, i, profile)) { tokens.Add(new CodeToken(start, line.Length - start, CodeTokenKind.Comment)); break; } // ---- 블록 주석 ---- if (profile.HasBlockComment && Matches(line, i, profile.BlockStart!)) { var end = line.IndexOf(profile.BlockEnd!, i + profile.BlockStart!.Length, StringComparison.Ordinal); if (end < 0) { tokens.Add(new CodeToken(start, line.Length - start, CodeTokenKind.Comment)); next = CarryOver.BlockComment; break; } i = end + profile.BlockEnd!.Length; tokens.Add(new CodeToken(start, i - start, CodeTokenKind.Comment)); continue; } // ---- 문자열 ---- if (Array.IndexOf(profile.Quotes, c) >= 0) { i += ReadQuoted(line, i, c); tokens.Add(new CodeToken(start, i - start, CodeTokenKind.Text)); continue; } // ---- 숫자 ---- if (char.IsDigit(c)) { i++; while (i < line.Length && (char.IsLetterOrDigit(line[i]) || line[i] == '.' || line[i] == '_')) { if (line[i] == '.' && i + 1 < line.Length && line[i + 1] == '.') break; i++; } tokens.Add(new CodeToken(start, i - start, CodeTokenKind.Number)); continue; } // ---- 식별자 / 예약어 ---- if (char.IsLetter(c) || c == '_') { i++; while (i < line.Length && (char.IsLetterOrDigit(line[i]) || line[i] == '_')) i++; var word = line.Substring(start, i - start); tokens.Add(new CodeToken(start, i - start, Classify(word, line, i, profile))); continue; } if (OperatorChars.IndexOf(c) >= 0) { i++; while (i < line.Length && OperatorChars.IndexOf(line[i]) >= 0) i++; tokens.Add(new CodeToken(start, i - start, CodeTokenKind.Operator)); continue; } if (PunctuationChars.IndexOf(c) >= 0) { tokens.Add(new CodeToken(start, 1, CodeTokenKind.Punctuation)); i++; continue; } i++; // 공백 등 나머지 } return tokens; } /// /// 한 줄 주석 표시가 여기서 시작하는지. /// 배치 파일의 rem 처럼 글자로 된 표시가 있어 대소문자 규칙을 따른다. /// private static bool TryMatchLineComment(string line, int index, LanguageProfile profile) { foreach (var marker in profile.LineComments) { if (marker.Length == 0) continue; if (Matches(line, index, marker, profile.CaseSensitive)) return true; } return false; } private static bool Matches(string line, int index, string token, bool caseSensitive = true) { if (index + token.Length > line.Length) return false; return string.Compare(line, index, token, 0, token.Length, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) == 0; } private static CodeTokenKind Classify(string word, string line, int afterIndex, LanguageProfile profile) { if (profile.ControlKeywords.Contains(word)) return CodeTokenKind.ControlKeyword; if (profile.TypeKeywords.Contains(word)) return CodeTokenKind.TypeKeyword; if (profile.Keywords.Contains(word)) return CodeTokenKind.Keyword; var j = afterIndex; while (j < line.Length && char.IsWhiteSpace(line[j])) j++; if (j < line.Length && line[j] == '(') return CodeTokenKind.Method; return char.IsUpper(word[0]) ? CodeTokenKind.Type : CodeTokenKind.Default; } /// 여는 따옴표부터 닫는 따옴표까지의 길이. 닫히지 않으면 줄 끝까지. private static int ReadQuoted(string line, int index, char quote) { var i = index + 1; while (i < line.Length) { if (line[i] == '\\') { i += 2; continue; } if (line[i] == quote) return i + 1 - index; i++; } return line.Length - index; } } }