TextMateSharpについての紹介

# TextMateSharp
Visual Studio CodeでSyntaxHighlightを行っている技術にTextMateというものがあります。
文法(`Grammer`)やテーマ(`Theme`)を`json`形式で記述することで、様々な言語のSyntaxHighlightの対応やテーマの対応ができる広く知られたフォーマットになります。
これをC#用にしたのが`TextMateSharp`です。

https://github.com/danipen/TextMateSharp

`TextMateSharp`を使用することで、C#のフレームワークで簡単にSyntaxHighlightを実装することができます。

# サンプルについて
上記のGitHubリンクにサンプルアプリがあるのでこれで解説しようと思います。
必要に応じて`GitClone`してください。

TextMateSharp.Demoは以下のようになっています。
```csharp
using System;
using System.Globalization;
using System.IO;

using TextMateSharp.Grammars;
using TextMateSharp.Themes;

using Spectre.Console;

namespace TextMateSharp
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                // コマンドライン引数がないとき、何もしない
                if (args.Length < 1)
                {
                    // エラーのみ表示
                    Console.WriteLine("Usage TextMateSharp.Demo <fileToParse.cs>");
                    Console.WriteLine("EXAMPLE TextMateSharp.Demo .\\testdata\\samplefiles\\sample.cs");

                    return;
                }

                // 表示する文字列を引数から取得
                //(今回はコマンドライン引数に".\\testdata\\samplefiles\\sample.cs"を指定して表示するファイルを取得)
                string fileToParse = Path.GetFullPath(args[0]);

                if (!File.Exists(fileToParse))
                {
                    Console.WriteLine("No such file to parse: {0}", args[0]);
                    return;
                }
                // RegistryOptionsを作成
                // RegistryOptionは文法やテーマを設定するオプション（引数に使いたい組み込みテーマを設定）
                RegistryOptions options = new RegistryOptions(ThemeName.DarkPlus);

                Registry.Registry registry = new Registry.Registry(options);
                
                // 設定したテーマをthemeに格納
                Theme theme = registry.GetTheme();

                int ini = Environment.TickCount;
                // 使用する文法を指定したファイルの拡張子から取得(今回はsample.csなのでcsharpの文法を使うという意味)
                IGrammar grammar = registry.LoadGrammar(options.GetScopeByExtension(Path.GetExtension(fileToParse)));

                if (grammar == null)
                {
                    Console.WriteLine(File.ReadAllText(fileToParse));
                    return;
                }

                Console.WriteLine("Grammar loaded in {0}ms.",
                    Environment.TickCount - ini);

                int tokenizeIni = Environment.TickCount;

                IStateStack? ruleStack = null;
                // 指定したファイルを読み込み、
                using (StreamReader sr = new StreamReader(fileToParse))
                {
                    string? line = sr.ReadLine();
                    
                    //　各行ずつ処理
                    while (line != null)
                    {
                        // 1行を指定した文法でトークン化
                        ITokenizeLineResult result = grammar.TokenizeLine(line, ruleStack, TimeSpan.MaxValue);

                        ruleStack = result.RuleStack;

                        // トークン化された文字に対しての処理
                        foreach (IToken token in result.Tokens)
                        {
                            // トークンの最初の文字
                            int startIndex = (token.StartIndex > line.Length) ?
                                line.Length : token.StartIndex;
                            // トークンの最後の文字
                            int endIndex = (token.EndIndex > line.Length) ?
                                line.Length : token.EndIndex;

                            // 文字の色、背景の色、フォントスタイルを初期化
                            int foreground = -1;
                            int background = -1;
                            FontStyle fontStyle = FontStyle.NotSet;

                            // トークンに文字色、背景色、フォントスタイルを割り当てる
                            foreach (var themeRule in theme.Match(token.Scopes))
                            {
                                if (foreground == -1 && themeRule.foreground > 0)
                                    foreground = themeRule.foreground;

                                if (background == -1 && themeRule.background > 0)
                                    background = themeRule.background;

                                if (fontStyle == FontStyle.NotSet && themeRule.fontStyle > 0)
                                    fontStyle = themeRule.fontStyle;
                            }
                            // トークンをコンソールに書き出す
                            WriteToken(line.SubstringAtIndexes(startIndex, endIndex), foreground, background, fontStyle, theme);
                        }

                        Console.WriteLine();
                        line = sr.ReadLine();
                    }
                }
                // 以下の分はTextMateSharpは関係なく、このテーマは何の色ですよと羅列しているだけ
                var colorDictionary = theme.GetGuiColorDictionary();
                if (colorDictionary is { Count: > 0 })
                {
                    Console.WriteLine("Gui Control Colors");
                    foreach (var kvp in colorDictionary)
                    {
                        Console.WriteLine( $"  {kvp.Key}, {kvp.Value}");
                    }
                }
                // 処理にかかった時間
                Console.WriteLine("File {0} tokenized in {1}ms.",
                    Path.GetFileName(fileToParse),
                    Environment.TickCount - tokenizeIni);
            }
            catch (Exception ex)
            {
                Console.WriteLine("ERROR: " + ex.Message);
            }
        }

        // トークンを書き出す処理
        static void WriteToken(string text, int foreground, int background, FontStyle fontStyle, Theme theme)
        {
            // 文字色が-1のまま(変化なし)ならばそのままの色でテキストを書き出す
            if (foreground == -1)
            {
                Console.Write(text);
                return;
            }
            // フォントスタイルを設定
            Decoration decoration = GetDecoration(fontStyle);
            // 背景色を設定
            Color backgroundColor = GetColor(background, theme);
            // 文字色を設定
            Color foregroundColor = GetColor(foreground, theme);

            // 文字色、背景色、フォントスタイルをまとめたものをstyleとして保持
            Style style = new Style(foregroundColor, backgroundColor, decoration);
            // そのstyleで文字を修飾
            Markup markup = new Markup(text.Replace("[", "[[").Replace("]", "]]"), style);
            // コンソールに文字を修飾して書き込めるライブラリ"AnsiConsole"を使って色のついた文字をコンソールに書き込む
            AnsiConsole.Write(markup);
        }

        // 色をthemeに入っているカラーIDを元に取得
        static Color GetColor(int colorId, Theme theme)
        {
            if (colorId == -1)
                return Color.Default;

            return HexToColor(theme.GetColor(colorId));
        }
        // フォントスタイルを取得
        static Decoration GetDecoration(FontStyle fontStyle)
        {
            Decoration result = Decoration.None;

            if (fontStyle == FontStyle.NotSet)
                return result;

            if ((fontStyle & FontStyle.Italic) != 0)
                result |= Decoration.Italic;

            if ((fontStyle & FontStyle.Underline) != 0)
                result |= Decoration.Underline;

            if ((fontStyle & FontStyle.Bold) != 0)
                result |= Decoration.Bold;

            return result;
        }

        // ColorIDをColorにしている
        static Color HexToColor(string hexString)
        {
            //replace # occurences
            if (hexString.IndexOf('#') != -1)
                hexString = hexString.Replace("#", "");

            byte r, g, b = 0;

            r = byte.Parse(hexString.Substring(0, 2), NumberStyles.AllowHexSpecifier);
            g = byte.Parse(hexString.Substring(2, 2), NumberStyles.AllowHexSpecifier);
            b = byte.Parse(hexString.Substring(4, 2), NumberStyles.AllowHexSpecifier);

            return new Color(r, g, b);
        }
    }

    // 文字を加工
    internal static class StringExtensions
    {
        internal static string SubstringAtIndexes(this string str, int startIndex, int endIndex)
        {
            return str.Substring(startIndex, endIndex - startIndex);
        }
    }
}
```
上記を実行するとコンソールに".\\testdata\\samplefiles\\sample.cs"の中身がテーマに沿って色付けされ、表示されます。

TextMateSharpのポイントとして
1. `RegistryOptions()`に組み込みのテーマの種類を渡す
2. `registry.LoadGrammer()`でファイルの拡張子を渡す
3. `grammer`で文字列をトークン化し、トークンのスコープにより、色を決める
4. 決めた色をColorに変換して出力する

です。
これを行うことで文字に文法に沿ったテーマで色を付けることができます。

# 外部から文法やテーマを取り込む
TextMateSharpで色を付けることができましたが、現在組み込まれている文法、テーマにのみ対応しています。
Visual Studio Codeのように外部からjsonを追加し様々な文法やテーマに対応したいと思われるでしょう。
その対応について紹介します。

本来であれば、`TextMateSharp.Grammer`の`RegistryOption`を参考に`IRegistryOption`を実装する必要があるのですが、今回は手抜きをして簡単に外部から文法やテーマを与える方法を教えます。

文法は以下の通りです。
```cs
// RegistryOptionsを継承,上書きできるようIRegistryOptionsも実装
class GrammerRegistryOptions : RegistryOptions, IRegistryOptions
{
    // 継承元の`RegistryOptions`のコンストラクタを呼び出す
    public GrammerRegistryOptions(ThemeName defaultTheme) : base(defaultTheme)
    {
    }
    // 修飾子を`new`にし`GetGrammar`を上書き
    public new IRawGrammar GetGrammar(string scopeName)
    {
        // 追加したい外部のTextMate文法ファイルを取得する
        string grammerPath = Path.GetFullPath(
            @"C:\Users\isisg\source\repos\newlanguage-syntax-grammer.json");

        using (StreamReader reader = new StreamReader(grammerPath))
        {
            // 文法を外部ファイルのものとする
            return GrammarReader.ReadGrammarSync(reader);
        }
    }
}
```
テーマは以下の通りです。
```cs
// RegistryOptionsを継承,上書きできるようIRegistryOptionsも実装
class ThemeRegistryOptions : RegistryOptions, IRegistryOptions
{
    // 継承元の`RegistryOptions`のコンストラクタを呼び出す
    public ThemeRegistryOptions(ThemeName defaultTheme) : base(defaultTheme)
    {
    }
    // 修飾子を`new`にし`GetTheme`を上書き
    public new IRawTheme GetTheme(string scopeName)
    {
        // 追加したい外部のTextMateテーマファイルを取得する
        string themePath = Path.GetFullPath(
        @"C:\Users\isisg\source\repos\monokai-color-theme.json");

        using (StreamReader reader = new StreamReader(themePath))
        {
            // 文法を外部ファイルのものとする
            return ThemeReader.ReadThemeSync(reader);
        }
    }
}
```
そしてGrammerを使うときですが、`GetExtension`が使用できません。
指定したGrammerの拡張子をgrammerのjsonファイルに書いてあるのでそれを使用します。
例えば以下のようなgrammerのjsonでは
```diff_json
"information_for_contributors": [
		"This file has been converted from https://github.com/dotnet/csharp-tmLanguage/blob/master/grammars/csharp.tmLanguage",
		"If you want to provide a fix or improvement, please create a pull request against the original repository.",
		"Once accepted there, we are happy to receive an update request."
	],
	"version": "https://github.com/dotnet/csharp-tmLanguage/commit/7a7482ffc72a6677a87eb1ed76005593a4f7f131",
	"name": "New",
+	"scopeName": "source.new",
	"patterns": [
		{,,,
```
の"scopeName"を確認し、
`LoadGrammer`の部分で以下のように指定します。
```diff_c_sharp
  Registry.Registry registry = new Registry.Registry(options);

  Theme theme = registry.GetTheme();

  int ini = Environment.TickCount;
+ IGrammar grammar = registry.LoadGrammar("source.new");

  if (grammar == null)
  {
      Console.WriteLine(File.ReadAllText(fileToParse));
      return;
  }
```
`LoadGrammer`の引数に"scopeName"を合わせる形になります。

これで、デフォルトの文法やテーマを維持しながら、外部から文法やテーマを取り込むことができるようになります。