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;
}
}
}