Markdown-Dokumente mit Avalonia rendern

Markdown ist eine sehr einfache und leicht erlernbare Auszeichnungssprache, welche heute von vielen Produkten unterstützt wird. Häufig dient Markdown dazu, Html-Dokumente zu generieren. Der große Vorteil für mich gegenüber Html oder andere Auszeichnungssprachen ist, dass das Markdown-Dokument selbst bereits gut durch einen Menschen lesbar und der Funktionsumfang auf das notwendige beschränkt ist. Der Einstieg ist entsprechend schnell und selbst ohne vorher die Syntax zu kennen, kann diese leicht verstanden werden. Unter [1] sind weitere Detailinfos über Markdown zu finden. Ein sehr gutes Beispiel zur Verwendung von Markdown ist GitHub. Markdown-Dokumente im Repository erscheinen im Browser direkt als daraus generierte Html-Seiten. In diesem Artikel beschreibe ich etwas ähnliches mithilfe des Cross-Plattform Frameworks Avalonia. Auch hiermit ist es möglich, ein Dokument in der Markdown-Syntax zu schreiben und in der Anwendung anzuzeigen. Sinnvoll ist das etwa bei der Anzeige von in der Anwendung integrierten Hilfeseiten. Der Entwickler schreibt damit innerhalb seiner Entwicklungsumgebung in der Markdown-Syntax und in der Oberfläche erscheint es als sauber formatierte Seite.

Der Use-Case

In diesem Artikel gehe ich vom Use-Case eines in einer Desktop-Applikation integrierten Hilfe-Browsers aus. Umgesetzt habe ich das etwa bei meinem OpenSource-Projekt MessageCommunicator [2]. Nachfolgende Abbildung 1 stammt entsprechend aus diesem Projekt. Die Datei im Hintergrund ist aber nicht Xaml, sondern wie in Abbildung 2 zu sehen ein Markdown-Dokument. Visual Studio Code unterstützt Markdown von Haus aus und zeigt zum Dokument eine Vorschau an. Im großen Visual Studio kann man dies etwa mittels der Erweiterung „Markdown Editor“ [3] nachrüsten. Der Hilfe-Browser selbst ist relativ einfach. Er erlaubt es, die im Projekt erstellten Markdown-Dokumente durchzublättern. Der Vorteil von Markdown in diesem User-Case gegenüber Xaml ist, dass die Dokumente gleichzeitig als Grundlage einer Dokumentation im Webbrowser verwendet werden können. Markdown-Dokumente lassen sich durch verschiedenste Tools in Html umwandeln. Auch Blog-Engines wie etwa Jekyll [4] unterstützen Markdown meist direkt.

Hilfe-Browser
Abbildung 1: Screenshot des Hilfe-Browsers aus dem Programm MessageCommunicator
Abbildung 2: Editieren eines Markdown-Dokuments in Visual Studio mit Vorschau

Markdown in Xaml konvertieren

Glücklicherweise brauchen wir uns nicht selbst um die Konvertierung von Markdown nach Xaml kümmern. Avalonia unterstützt Markdown zwar nicht von Haus aus, die Community schafft an dieser Stelle aber Abhilfe. Mit dem Projekt Markdown.Avalonia [4] existiert ein einfacher Weg, Markdown-Dokumente in Avalonia-Applikationen zu integrieren. Das Projekt Markdown.Avalonia ist hierbei eine Portierung des Projekts MdXaml [5], welches die gleiche Aufgabe für Wpf-Applikationen übernimmt. Nachfolgender Codeausschnitt zeigt die Integration von Markdown.Avalonia. Der MarkdownScrollViewer ist für die Darstellung der Markdown-Dokumente verantwortlich. Über die Eigenschaft Markdown kann das Markdown-Dokument direkt als String übergeben werden. Im nachfolgenden Beispiel erfolgt das entsprechend über ein Binding. Mehr ist im ersten Schritt gar nicht notwendig.

<md:MarkdownScrollViewer Markdown="{Binding Path=SelectedDocument.MarkdownContentString, FallbackValue={x:Null}}" />

Ein einfaches Beispiel-Projekt

Auf GitHub unter [6] habe ich ein Beispiel eines Hilfe-Browsers abgelegt. Wie in Abbildung 3 zu sehen, werden die Markdown-Dokumente hier unter Assets/Docs abgelegt. Neben den Markdown-Dokumenten liegen Bilder, auf die von den Markdown-Dokumenten aus verwiesen wird. Weiterhin verweisen die Markdown-Dokumente über Links aufeinander. Alle Markdown-Dokumente und Bilder im Beispiel sind hierbei als eingebettete Ressourcen im C#-Projekt eingebunden. Letzteres hat nichts mit dem Projekt Markdown.Avalonia zu tun, sondern kommt eher von der Art, wie ich das C#-Projekt organisiere und die Markdown-Dokumente zum MarkdownScrollViewer bringe.

Abbildung 3: Ordnerstruktur mit Markdown-Dokumenten im Hilfe-Browser

Nachfolgender Codeausschnitt zeigt den Inhalt der Datei Test3.md. Die Markdown-Syntax ist wie vorher schon an einigen Stellen erwähnt sehr einfach. Ein # gibt eine Überschrift an, zwei ## entsprechend eine Unterüberschrift. Links werden mit der Syntax [LinkText](Link) geschrieben, Bilder per ![ResourceImage](Bilddatei) eingefügt. Ein Zeilenumbruch im Markdown-Dokument bleibt ein Zeilenumbruch. Ansonsten war es das auch schon mit der Syntax in diesem Beispiel.

# Test 3
## Links
[Link to Test 1](Test1.md)

[Link to Test 2](Test2.md)

[Link to Hirschstein](Subfolder/Test4.md)

## Image

![ResourceImage](Fichtelgebirge.jpg)

## Part a
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.   

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.   

Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.   

Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.   

Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis.   

At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur

Im Ergebnis sieht obiges Dokument dann wie im nachfolgenden Screenshot (Abbildung 4) aus. Markdown.Avalonia kümmert sich hierbei um die Konvertierung der Markdown-Syntax zu Xaml. Meine Beispiel-Applikation selbst erfüllt aber auch noch einige Aufgaben, die man nicht vergessen sollte. Dazu gehört das Finden aller relevanten Markdown-Dokumente. Auch das Auflösen von Bildern aus den eingebetteten Ressourcen wird hier im Beispiel gesondert implementiert. Ebenso die Navigation zu einem anderen Markdown-Dokument nach Klick auf einem Link. Im nachfolgenden Kapitel gehe ich auf diese Funktionen grob ein.

Markdown mit Avalonia
Abbildung 4: Screenshot aus dem Beispiel Hilfe-Browser

Umgesetzte Funktionen im Beispiel Hilfe-Browser

Dokumentensuche und Titel

Bevor ein Markdown-Dokument angezeigt wird, muss es zunächst einmal gefunden werden. Im Beispiel Hilfe-Browser erfolgt das durch die Klasse IntegratedDocRepository. Diese bekommt im Konstruktor die Assembly übergeben, welche nach Markdown-Dokumenten durchsucht werden soll. Jedes Markdown-Dokument wird dabei gelesen und der Name des Dokuments ermittelt. Da der Name des Dokuments nicht wie in Html in einem <title> Tag steht, hat man hier verschiedene Möglichkeiten:

  • Der Dateiname des Markdown-Dokuments (ohne .md)
  • Der Text in der ersten Hauptüberschrift (#-Symbol)
  • Über einen eigens definierten Yaml-Header

Daneben sind beliebig weitere Möglichkeiten denkbar. Im Beispiel Hilfe-Browser sind obige drei Varianten implementiert. Auf letztere, den Yaml-Header, möchte ich hier noch kurz eingehen. Yaml-Header bei Markdown habe ich schon bei verschiedenen Applikationen gesehen, ist aber nicht standardisiert. Es handelt sich aber um einen eleganten Weg, zusätzliche Metadaten in das Markdown-Dokument zu bringen. Nachfolgender Codeausschnitt zeigt einen Yaml-Header mit den Eigenschaften „title“ und „author“. Start und Ende des Yaml-Headers werden hier über die — Zeichen markiert. Das Yaml im Yaml-Header kann beispielsweise mittels der Bibliothek YamlDotNet [7] deserialisiert werden.

---
title: Hirschstein
author: RolandK
---
# Hirschstein
## Links
[Link to Test 1](../Test1.md)

...

Einbindung von Bildern

Wie weiter oben beschrieben werden die Markdown-Dokumente und zugehörigen Bilddateien im Beispiel Hilfe-Browser als eingebettete Ressourcen eingebunden. Markdown.Avalonia weiß davon standardmäßig nichts und hat somit zunächst keine Chance, ein Bild aufzulösen. Glücklicherweise lässt sich die Bibliothek genau an solchen Stellen flexibel erweitern. Nachfolgender Codeausschnitt zeigt die Klasse ResourceBitmapLoader, welche für das Laden von Bildern im Markdown-Dokument zuständig ist. Markdown.Avalonia ruft dazu die Methode Get(string urlText) mit der im Markdown-Dokument angegebenen Bild-Url auf.

public class ResourceBitmapLoader : IBitmapLoader
{
    /// <inheritdoc />
    public string? AssetPathRoot { get; set; }

    private Assembly _assetAssembly;
    private ConcurrentDictionary<string, WeakReference<Bitmap>> _bitmapCache;

    public ResourceBitmapLoader(Assembly assetAssembly)
    {
        _assetAssembly = assetAssembly;
        this.AssetPathRoot = string.Empty;

        this._bitmapCache = new ConcurrentDictionary<string, WeakReference<Bitmap>>();
    }

    private void Compact()
    {
        foreach (var entry in _bitmapCache.ToArray())
        {
            if (!entry.Value.TryGetTarget(out var dummy))
            {
                ((IDictionary<string, WeakReference<Bitmap>>)_bitmapCache).Remove(entry.Key);
            }
        }
    }

    public static string BuildEmbeddedResourceName(string assetPathRoot, string urlText)
    {
        return EmbeddedResourceHelper.FollowLocalPath(
            assetPathRoot,
            urlText);
    }

    public Bitmap? Get(string urlTxt)
    {
        var assetPathRoot = this.AssetPathRoot ?? string.Empty;
        var embeddedResourceName = BuildEmbeddedResourceName(assetPathRoot, urlTxt);

        if (_bitmapCache.TryGetValue(embeddedResourceName, out var reference))
        {
            if (reference.TryGetTarget(out var cachedBitmap))
            {
                return cachedBitmap;
            }
        }

        using var inStream = _assetAssembly.GetManifestResourceStream(embeddedResourceName);
        if (inStream != null)
        {
            var newBitmap = new Bitmap(inStream);
            _bitmapCache[embeddedResourceName] = new WeakReference<Bitmap>(newBitmap);
            return newBitmap;
        }

        return null;
    }
}

Wichtig zu erwähnen an dieser Stelle ist noch die Eigenschaft AssetPathRoot. Diese gibt an, von welchem Pfad aus nach Bilddateien gesucht wird. Die Eigenschaft kann direkt am MarkdownScrollViewer gesetzt werden und wird von Markdown.Avalonia direkt an die IBitmapLoader Implementierung weitergegeben. Ich habe es wie in den beiden nachfolgenden Codeausschnitten zu sehen gelöst. Hinter dem AssetPathRoot steht ein Binding, welches immer auf den Ordner des aktuellen Markdown-Dokuments zeigt. Somit können Bilder stets relativ zum aktuellen Markdown-Dokument referenziert werden. Im Codebehind bekommt der MarkdownScrollViewer noch unseren ResourceBitmapLoader zugewiesen.

<md:MarkdownScrollViewer x:Name="CtrlMarkdownViewer"
                         Markdown="{Binding Path=SelectedDocument.MarkdownContentString, FallbackValue={x:Null}}"
                         AssetPathRoot="{Binding Path=SelectedDocument.DocumentPath.EmbeddedResourceDirectory, FallbackValue={x:Null}}" />
var ctrlMarkdownViewer = this.Find<MarkdownScrollViewer>("CtrlMarkdownViewer");
if(ctrlMarkdownViewer != null)
{
    ctrlMarkdownViewer.Engine.BitmapLoader = new ResourceBitmapLoader(Assembly.GetExecutingAssembly());
    ctrlMarkdownViewer.Engine.HyperlinkCommand = viewModel.CommandNavigateTo;
}

Abschließend noch das Feature der Navigation. Das Markdown-Dokument kann Links auf andere Markdown-Dokumente erhalten. Markdown.Avalonia kann einen Command triggern, sobald der Benutzer auf einen Link geklickt hat. Als Parameter wird der im Markdown-Dokument angegebene Pfad übergeben.

Fazit

In diesem Artikel habe ich gezeigt, wie sich Markdown-Dokumente in einer Avalonia-Applikation integrieren lassen. Erste Ergebnisse bekommt man dabei sehr schnell. Für weitere Funktionen wie Navigation, etc. ist noch etwas Tipparbeit notwendig. Der vorgestellte Beispiel Hilfe-Browser ist zudem eine gute Grundlage, falls man genau so etwas in der eigenen Applikation benötigt.

Downloads

  1. Beispiel-Quellcode
    https://www.rolandk.de/files/2021/HappyCoding.AvaloniaMarkdownHelpBrowser.zip

Verweise

  1. Allgemeine Infos zum Markdown Format
    https://markdown.de/
  2. Repository von MessageCommunicator auf GitHub
    https://github.com/RolandKoenig/MessageCommunicator
  3. VisualStudio Extension für Markdown
    https://marketplace.visualstudio.com/items?itemName=MadsKristensen.MarkdownEditor
  4. Bibliothek zum Rendern von Markdowns in Avalonia
    https://github.com/whistyun/Markdown.Avalonia
  5. Bibliothek zum Konvertieren von Markdown nach Xaml (WPF)
    https://github.com/whistyun/MdXaml
  6. Repository des Beispiel-Quellcodes
    https://github.com/RolandKoenig/HappyCoding/tree/main/2021/HappyCoding.AvaloniaMarkdownHelpBrowser
  7. Bibliothek zum Laden / Speichern von Yaml-Dateien für .Net
    https://github.com/aaubry/YamlDotNet

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.