using System;
using System.Collections.Generic;
using System.IO;
namespace MergeHub.Services
{
///
/// 한 가지 언어(또는 문서 형식)를 어떻게 읽을지 적어 둔 표.
///
/// 언어마다 토크나이저를 따로 만들지 않는 이유가 있다. 색칠에 필요한 것은
/// "주석은 어디서 시작하나 · 문자열은 무엇으로 묶나 · 예약어는 무엇인가" 이 셋뿐이고,
/// 나머지(숫자·식별자·연산자)는 어느 언어나 거의 같다. 그래서
/// 하나가 이 표를 보고 동작한다.
///
/// C# 만 예외로 전용 토크나이저를 그대로 쓴다 - 축자/보간 문자열(@"" $"")과
/// 접근제한자 구분처럼 표로 적기 어려운 규칙이 있고, 이미 테스트로 굳어 있다.
///
public sealed class LanguageProfile
{
public LanguageProfile(string name,
string[] extensions,
string[]? lineComments = null,
string? blockStart = null,
string? blockEnd = null,
char[]? quotes = null,
IEnumerable? keywords = null,
IEnumerable? controlKeywords = null,
IEnumerable? typeKeywords = null,
bool caseSensitive = true,
bool isCode = true,
bool hasBraces = false,
bool useCSharpTokenizer = false)
{
Name = name;
Extensions = extensions;
LineComments = lineComments ?? Array.Empty();
BlockStart = blockStart;
BlockEnd = blockEnd;
Quotes = quotes ?? Array.Empty();
CaseSensitive = caseSensitive;
IsCode = isCode;
HasBraces = hasBraces;
UseCSharpTokenizer = useCSharpTokenizer;
var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
Keywords = new HashSet(keywords ?? Array.Empty(), comparer);
ControlKeywords = new HashSet(controlKeywords ?? Array.Empty(), comparer);
TypeKeywords = new HashSet(typeKeywords ?? Array.Empty(), comparer);
}
public string Name { get; }
public string[] Extensions { get; }
public string[] LineComments { get; }
public string? BlockStart { get; }
public string? BlockEnd { get; }
public char[] Quotes { get; }
public bool CaseSensitive { get; }
/// 문법 색을 입힐 대상인지. 평문·표 형식 문서는 false 다.
public bool IsCode { get; }
/// 중괄호로 블록을 여닫는지. 코드 검사(짝 맞추기)를 걸 수 있는 조건이다.
public bool HasBraces { get; }
/// C# 전용 토크나이저를 쓸지.
public bool UseCSharpTokenizer { get; }
public HashSet Keywords { get; }
public HashSet ControlKeywords { get; }
public HashSet TypeKeywords { get; }
public bool HasBlockComment => BlockStart is { Length: > 0 } && BlockEnd is { Length: > 0 };
}
///
/// 확장자로 언어를 고른다. 모르는 확장자는 평문으로 본다 -
/// 이 도구는 소스만이 아니라 설정 파일·레시피·문서도 비교해야 한다.
///
public static class LanguageCatalog
{
// 여러 언어가 나눠 쓰는 예약어 묶음. 표를 짧게 유지하려고 따로 뺐다.
private static readonly string[] CFamilyControl =
{
"break", "case", "catch", "continue", "default", "do", "else", "finally",
"for", "goto", "if", "return", "switch", "throw", "try", "while"
};
private static readonly string[] CFamilyTypes =
{
"auto", "bool", "char", "const", "double", "float", "int", "long",
"short", "signed", "unsigned", "void", "size_t", "wchar_t"
};
public static readonly LanguageProfile Plain = new LanguageProfile(
"평문", new[] { ".txt", ".log", ".csv", ".text" }, isCode: false);
public static readonly LanguageProfile CSharp = new LanguageProfile(
"C#", new[] { ".cs", ".csx" },
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
quotes: new[] { '"', '\'' },
hasBraces: true, useCSharpTokenizer: true);
public static readonly LanguageProfile CFamily = new LanguageProfile(
"C / C++", new[] { ".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".inl" },
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
quotes: new[] { '"', '\'' },
keywords: new[]
{
"class", "struct", "enum", "union", "namespace", "template", "typename",
"public", "private", "protected", "virtual", "override", "static", "extern",
"inline", "operator", "new", "delete", "this", "nullptr", "true", "false",
"using", "typedef", "sizeof", "friend", "explicit", "mutable", "constexpr"
},
controlKeywords: CFamilyControl, typeKeywords: CFamilyTypes, hasBraces: true);
public static readonly LanguageProfile Java = new LanguageProfile(
"Java", new[] { ".java" },
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
quotes: new[] { '"', '\'' },
keywords: new[]
{
"abstract", "class", "extends", "final", "implements", "import", "instanceof",
"interface", "native", "new", "null", "package", "private", "protected",
"public", "static", "super", "synchronized", "this", "throws", "transient",
"true", "false", "volatile", "enum", "record", "var"
},
controlKeywords: CFamilyControl, typeKeywords: CFamilyTypes, hasBraces: true);
public static readonly LanguageProfile JavaScript = new LanguageProfile(
"JavaScript / TypeScript", new[] { ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs" },
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
quotes: new[] { '"', '\'', '`' },
keywords: new[]
{
"class", "const", "let", "var", "function", "new", "this", "super", "extends",
"import", "export", "from", "as", "default", "async", "await", "null",
"undefined", "true", "false", "typeof", "instanceof", "in", "of", "delete",
"interface", "type", "enum", "implements", "public", "private", "readonly"
},
controlKeywords: CFamilyControl, typeKeywords: new[] { "number", "string", "boolean", "any", "void", "never", "unknown" },
hasBraces: true);
public static readonly LanguageProfile Python = new LanguageProfile(
"Python", new[] { ".py", ".pyw", ".pyi" },
lineComments: new[] { "#" },
quotes: new[] { '"', '\'' },
keywords: new[]
{
"and", "as", "assert", "async", "await", "class", "def", "del", "from",
"global", "import", "in", "is", "lambda", "None", "nonlocal", "not", "or",
"pass", "True", "False", "with", "yield", "self"
},
controlKeywords: new[] { "break", "continue", "elif", "else", "except", "finally", "for", "if", "raise", "return", "try", "while" },
typeKeywords: new[] { "bool", "bytes", "dict", "float", "int", "list", "set", "str", "tuple" });
public static readonly LanguageProfile Sql = new LanguageProfile(
"SQL", new[] { ".sql" },
lineComments: new[] { "--" }, blockStart: "/*", blockEnd: "*/",
quotes: new[] { '\'', '"' },
keywords: new[]
{
"select", "from", "where", "insert", "into", "values", "update", "set",
"delete", "create", "alter", "drop", "table", "view", "index", "join",
"inner", "left", "right", "outer", "on", "group", "order", "by", "having",
"union", "distinct", "as", "and", "or", "not", "null", "primary", "key",
"foreign", "references", "default", "constraint"
},
controlKeywords: new[] { "begin", "end", "if", "else", "while", "case", "when", "then", "return" },
typeKeywords: new[] { "int", "bigint", "smallint", "varchar", "nvarchar", "char", "nchar", "text", "date", "datetime", "decimal", "float", "bit" },
caseSensitive: false);
public static readonly LanguageProfile Shell = new LanguageProfile(
"셸 / 배치", new[] { ".sh", ".bash", ".zsh", ".bat", ".cmd", ".ps1", ".psm1" },
lineComments: new[] { "#", "::", "rem " },
quotes: new[] { '"', '\'' },
keywords: new[] { "echo", "set", "export", "function", "local", "source", "param", "return" },
controlKeywords: new[] { "if", "then", "else", "elif", "fi", "for", "while", "do", "done", "case", "esac", "goto", "exit" },
caseSensitive: false);
public static readonly LanguageProfile Markup = new LanguageProfile(
"XML / HTML", new[] { ".xml", ".xaml", ".html", ".htm", ".svg", ".xsd", ".xsl", ".csproj", ".vbproj", ".config", ".props", ".targets", ".resx" },
blockStart: "",
quotes: new[] { '"', '\'' },
caseSensitive: false);
public static readonly LanguageProfile Json = new LanguageProfile(
"JSON", new[] { ".json", ".jsonc" },
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
quotes: new[] { '"' },
keywords: new[] { "true", "false", "null" });
public static readonly LanguageProfile Yaml = new LanguageProfile(
"YAML", new[] { ".yml", ".yaml" },
lineComments: new[] { "#" },
quotes: new[] { '"', '\'' },
keywords: new[] { "true", "false", "null", "yes", "no" },
caseSensitive: false);
public static readonly LanguageProfile Ini = new LanguageProfile(
"설정 / INI", new[] { ".ini", ".cfg", ".conf", ".properties", ".toml", ".editorconfig", ".gitconfig" },
lineComments: new[] { ";", "#" },
quotes: new[] { '"', '\'' },
keywords: new[] { "true", "false" },
caseSensitive: false);
public static readonly LanguageProfile Markdown = new LanguageProfile(
"Markdown", new[] { ".md", ".markdown" }, isCode: false);
/// 화면에 고를 수 있게 늘어놓는 순서. 평문이 맨 앞이다(가장 넓게 쓰인다).
public static readonly IReadOnlyList All = new[]
{
Plain, CSharp, CFamily, Java, JavaScript, Python, Sql, Shell, Markup, Json, Yaml, Ini, Markdown
};
/// 경로의 확장자로 언어를 고른다. 모르면 평문.
public static LanguageProfile ForPath(string? path)
{
if (string.IsNullOrWhiteSpace(path)) return Plain;
string extension;
try { extension = Path.GetExtension(path)!.ToLowerInvariant(); }
catch (ArgumentException) { return Plain; }
if (string.IsNullOrEmpty(extension)) return Plain;
foreach (var profile in All)
{
foreach (var candidate in profile.Extensions)
{
if (string.Equals(candidate, extension, StringComparison.OrdinalIgnoreCase)) return profile;
}
}
return Plain;
}
}
}