1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ひとりで完走_C# is GODAdvent Calendar 2024

Day 11

WinUI3 MarkdownControlをTextMate対応した話

Last updated at Posted at 2024-12-10

WinUI3 MarkdownControl

WinUI3のCommunityToolkitには現在Labの中ですが、MarkdownControlが存在します。

文字通りこれはWinUI3にMarkdownを表示するコントロールを追加するライブラリなのですが、少し気に入らないところが、、、

普通のMarkdownだと```で囲うとその中に書いたコードがSyntaxHighlightされて表示されるのですが、そのまま使用する場合だとこれが使えません。
また、ライブラリの中身を見るとColorCodeというライブラリでSyntaxHighlightできるようになっているのですが、これは簡易的なもので、クラス名とメソッド名が同じ色になってしまうというものでした。

どうしてもクラス名とメソッド名が別の色であってほしかったので、前回紹介したTextMateSharpで再構築してみました。

CommunityToolkit.Labsをフォーク

今回はCommunityToolkitのMarkdownTextBlockをフォークして再構築します。
フォークして、gitCloneを行い、プロジェクトに変更を加えていくのですが、少し注意点があります。
まず、gitCloneはそのまま行うのではなく、以下のコードでgitCloneを行います。
git clone --recurse-submodules https://github.com/{CommunityToolkit/Labs-Windows(フォークした先のアドレス)}.git
これは上記のReadmeに記述されていますね。

そしてgitCloneが完了したら、ソリューションに展開するのに、GenerateAllSolution.batファイルを実行する必要があります。
これを実行するために、ファイル内のすべての.ps1ファイルに実行するためのポリシーを設定する必要があるので、注意です。

すべての.ps1ファイルにポリシーを付け終わりGenerateAllSolution.batVisual Studioの開発者コンソールで実行するとソリューションが作られるので、やっと作業が始められます。

TextMateをMarkdown仕様へ

前回のサンプルコードを元にMarkdownTextBlockで使えるようにします。
一応元々のColorCodeのほうでUWPとWinUIが分かれて書いてあったので、そうしていますが、WinUI3での想定の場合は#if !WINAPPSDKの中は無視して問題ないです。

using TextMateSharp.Grammars;
using TextMateSharp.Registry;
using TextMateSharp.Themes;

namespace CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns.TextMate;
public class TextMateFormatter
{
    // GrammerとThemeをフィールドへ
    private IGrammar grammar;
    private Theme theme;
#if !WINAPPSDK
    private FontFamily fontFamily;
#else
    private FontFamily fontFamily;
#endif

    // 構造体。文法、テーマ、フォントの設定
    public TextMateFormatter(ThemeName themeName, string extention, FontFamily font)
    {
        fontFamily = font;
        // 今回はTextMate組み込みのテーマとグラマーを使用します。
        RegistryOptions options = new RegistryOptions(themeName);
        Registry registry = new Registry(options);
        theme = registry.GetTheme();
        // 与えられた拡張子から文法を取得
        grammar = registry.LoadGrammar(options.GetScopeByExtension(extention));
    }

    // このメソッドをを呼び出して色付きのコードを吐き出す
    public void FormatRichTextBlock(Stack<string> codeText, RichTextBlock textBlock)
    {
        IStateStack? ruleStack = null;
        foreach (string line in codeText)
        {
#if !WINAPPSDK
            var paragraph = new Windows.UI.Xaml.Documents.Paragraph();
#else
            // TextBlockに表示するためにParagraphを作成。このParagraphを与えられたtextBlockに詰める
            var paragraph = new Microsoft.UI.Xaml.Documents.Paragraph();
#endif
            // 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;

                TextMateSharp.Themes.FontStyle fontStyle = TextMateSharp.Themes.FontStyle.NotSet;

                foreach (var themeRule in theme.Match(token.Scopes))
                {
                    // テーマに沿ってテキストの色を決める。背景は実装が難しいので今回はしない
                    if (foreground == -1 && themeRule.foreground > 0)
                        foreground = themeRule.foreground;
                    // フォントスタイルを決める
                    if (fontStyle == TextMateSharp.Themes.FontStyle.NotSet && themeRule.fontStyle > 0)
                        fontStyle = themeRule.fontStyle;
                }
                // paragraphに色を付けた行を追加していく
                paragraph.Inlines.Add(WriteToken(line.SubstringAtIndexes(startIndex, endIndex), foreground, fontStyle));
            }
            // すべての行を詰めて、最後にtextBlockにpragraphを詰める。
            // あとはWinUI側でこのtextBlockを表示すればよい
            textBlock.Blocks.Add(paragraph);
        }
    }
    // トークンに色を付ける処理
    private Run WriteToken(string text, int foreground, TextMateSharp.Themes.FontStyle fontStyle)
    {
#if !WINAPPSDK
        Windows.UI.Xaml.Documents.Run run = new Run
        {
            Text = text,
        };
        run.FontFamily = fontFamily;
#else
        // textBlockの最小単位Runにトークンの文字を詰める
        Microsoft.UI.Xaml.Documents.Run run = new Run
        {
            Text = text,
        };
        run.FontFamily = fontFamily;
#endif
        // 何も変化しないならそのままの黒で返す
        if (foreground == -1)
        {
            return run;
        }

        if (fontStyle == TextMateSharp.Themes.FontStyle.Bold)
        {
#if !WINAPPSDK
            run.FontWeight = Windows.UI.Text.FontWeights.Bold;
#else
            // fontStyleが組み込みのBoldと同じならrunをBoldにする
            run.FontWeight = Microsoft.UI.Text.FontWeights.Bold;
#endif
        }
        if (fontStyle == TextMateSharp.Themes.FontStyle.Italic)
        {
            // 同様にitalicにする
            run.FontStyle = Windows.UI.Text.FontStyle.Italic;
        }

        if (foreground != -1)
        {
            // textの色を取得し、runの文字色とする
            string hexString = theme.GetColor(foreground);
            run.Foreground = hexString.GetSolidColorBrush();
        }
        // BackGroundは単純にrunの色を変えるというのではなく、TextBlockの何文字目から何文字目が〇〇色のように指定していくため、めんどくさいのでつけてない
        // runを返す
        return run;
    }
}
// 一応別クラスにわけた
namespace CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns.TextMate;
internal static class TextMateExtensions
{
    // 文字を決定する便利関数
    internal static string SubstringAtIndexes(this string str, int startIndex, int endIndex)
    {
        return str.Substring(startIndex, endIndex - startIndex);
    }
    // 色を決定する便利関数
    internal static SolidColorBrush GetSolidColorBrush(this string hex)
    {
        hex = hex.Replace("#", string.Empty);

        byte a = 255;
        int index = 0;

        if (hex.Length == 8)
        {
            a = (byte)(Convert.ToUInt32(hex.Substring(index, 2), 16));
            index += 2;
        }

        byte r = (byte)(Convert.ToUInt32(hex.Substring(index, 2), 16));
        index += 2;
        byte g = (byte)(Convert.ToUInt32(hex.Substring(index, 2), 16));
        index += 2;
        byte b = (byte)(Convert.ToUInt32(hex.Substring(index, 2), 16));
        SolidColorBrush myBrush = new SolidColorBrush(Windows.UI.Color.FromArgb(a, r, g, b));
        return myBrush;
    }
}

MarkdownTextBlockの改造

MarkdownTextBlockの改造箇所は以下になります。
components/MarkdownTextBlock/src/TextElements/CodeBlockElement.cs
コードブロックの部分だけ直せば問題ないでしょう。

+using CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns.TextMate;
using Markdig.Syntax;
+using TextMateSharp.Grammars;

namespace CommunityToolkit.Labs.WinUI.MarkdownTextBlock.TextElements;

internal class MyCodeBlock : IAddChild
{
    private CodeBlock _codeBlock;
    private Paragraph _paragraph;
    private MarkdownConfig _config;

    public TextElement TextElement
    {
        get => _paragraph;
    }

    public MyCodeBlock(CodeBlock codeBlock, MarkdownConfig config)
    {
        _codeBlock = codeBlock;
        _config = config;
        _paragraph = new Paragraph();
        var container = new InlineUIContainer();
        var border = new Border();
+       border.Background = new (Brush)Application.Current.Resources["ExpanderHeaderBackground"];
        border.Padding = _config.Themes.Padding;
        border.Margin = _config.Themes.InternalMargin;
        border.CornerRadius = _config.Themes.CornerRadius;
        var richTextBlock = new RichTextBlock();
+       var themeName = _config.Themes.CodeBlockThemeName; //指定したテーマを与える
+       var fontFamiry = _config.Themes.CodeBlockFontFamily; //指定したフォントファミリーを与える

        if (codeBlock is FencedCodeBlock fencedCodeBlock)
        {
//#if !WINAPPSDK
//            var formatter = new ColorCode.RichTextBlockFormatter(Extensions.GetOneDarkProStyle());
//#else
//            var formatter = new ColorCode.RichTextBlockFormatter(Extensions.GetOneDarkProStyle());
//#endif
            // コンストラクタを呼び出し、テーマ、文法、フォントファミリーを設定
+           var formatter = new TextMateFormatter(_config.Themes.CodeBlockThemeName, Extensions.ToExtension(fencedCodeBlock),fontFamiry);

-            //var stringBuilder = new StringBuilder();

            // go through all the lines backwards and only add the lines to a stack if we have encountered the first non-empty line
            var lines = fencedCodeBlock.Lines.Lines;
            var stack = new Stack<string>();
            var encounteredFirstNonEmptyLine = false;
            if (lines != null)
            {
                for (var i = lines.Length - 1; i >= 0; i--)
                {
                    var line = lines[i];
                    if (String.IsNullOrWhiteSpace(line.ToString()) && !encounteredFirstNonEmptyLine)
                    {
                        continue;
                    }

                    encounteredFirstNonEmptyLine = true;
                    stack.Push(line.ToString());
                }
                
                // 先ほど作成したTextMateの処理を呼び出す
                formatter.FormatRichTextBlock(stack, richTextBlock);
                
-               //// append all the lines in the stack to the string builder
-               //while (stack.Count > 0)
-               //{
-               //    stringBuilder.AppendLine(stack.Pop());
-               //}
            }

            //formatter.FormatRichTextBlock(stringBuilder.ToString(), fencedCodeBlock.ToLanguage(), richTextBlock);
        }
        else
        {
            foreach (var line in codeBlock.Lines.Lines)
            {
                var paragraph = new Paragraph();
                var lineString = line.ToString();
                if (!String.IsNullOrWhiteSpace(lineString))
                {
                    paragraph.Inlines.Add(new Run() { Text = lineString });
                }
                richTextBlock.Blocks.Add(paragraph);
            }
        }
        border.Child = richTextBlock;
        container.Child = border;
        _paragraph.Inlines.Add(container);
    }

    public void AddChild(ITextElement child) { }
}

これでTextMateSharpを使用してMarkdownのコードブロックにSyntaxHighlightがつけられるようになりました。

おまけ、テーマや、文法を決める設定

上記のコードのテーマ名やフォントファミリー、文法用に必要な拡張子を以下の方法で設定しています。
MarkdownTextBlock/src/MarkdownThemes.csにて

public sealed class MarkdownThemes : DependencyObject
{
    .
    .
    .
    public Thickness InlineCodeBorderThickness { get; set; } = new (1);

    public CornerRadius InlineCodeCornerRadius { get; set; } = new (2);

    public Thickness InlineCodePadding { get; set; } = new(0);

    public double InlineCodeFontSize { get; set; } = 10;

    public FontWeight InlineCodeFontWeight { get; set; } = FontWeights.Normal;

    public Brush CodeBlockBackground { get; set; } = (Brush) Application.Current.Resources["ExpanderHeaderBackground"];
    // Themeの指定
+   public ThemeName CodeBlockThemeName { get; set; } = ThemeName.LightPlus;
    // FontFamilyの設定
+   public FontFamily CodeBlockFontFamily { get; set; } = new FontFamily("Consolas");
}

文法を決めるための拡張子は
MarkdownTextBlock/src/Extensions.csに以下を追加

    public static string ToExtension(this FencedCodeBlock fencedCodeBlock)
    {
        switch (fencedCodeBlock.Info?.ToLower())
        {
            case "cs":
            case "csharp":
            case "c#":
                return ".cs";
            case "xhtml":
            case "html":
            case "hta":
            case "htm":
            case "html.hl":
            case "inc":
            case "xht":
                return ".html";
            case "java":
            case "jav":
            case "jsh":
                return ".java";
            case "js":
            case "node":
            case "_js":
            case "bones":
            case "cjs":
            case "njs":
            case "pac":
            case "sjs":
            case "ssjs":
            case "xsjs":
            case "xsjslib":
                return ".js";
            .
            . 割愛
            .
            case "python":
            case "py":
            case "cgi":
            case "gyp":
            case "gypi":
            case "lmi":
            case "py3":
            case "wsgi":
            case "xpy":
                return ".py";
            //case "matlab":
            //case "m":
            //    return Languages.MATLAB;
            default:
                return ".js";
        }
    }

これにより、Markdownで書いた```〇〇を読み取り、拡張子に変換し、文法を決定します。

Local Nuget

一応作ったものは以下のgitHubリンクにあります。

ここにlocalで作ったnugetも入れておきますので、作るのめんどくさい場合はこれを素直に使ってください。

NugetPackage

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?