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