Dojos für Entwickler 2. Stefan Lieser
sichergestellt, dass spätere Anforderungen einfach umsetzbar sind.
Mit Markdown steht eine Textauszeichnung zur Verfügung, die einfach anzuwenden ist [1] [2]. Das liegt daran, dass auf Schnickschnack verzichtet wurde. Es handelt sich nicht um eine weitere Variante des Themas „Viele Klammern werden das Problem schon lösen“. Statt also wie bei HTML auf spitze Klammern oder bei RTF auf geschweifte Klammern zu setzen, verwendet Markdown Zeichen wie etwa das Sternchen, um damit Passagen eines Textes auszuzeichnen. Das macht die Sache beim Schreiben eines Textes deutlich einfacher, weil weniger Formalismen erforderlich sind. Allerdings wird, damit einhergehend, das Parsen eines Textes nicht unbedingt einfacher. So wird in Markdown beispielsweise ein einzelnes Sternchen sowohl zur Markierung von kursivem Text verwendet als auch zur Kennzeichnung von Aufzählungen. Das zu erkennen ist die Herausforderung beim Parsen eines Markdown-Textes.
Kleine Schritte
Im ersten Schritt habe ich die Aufgabe wieder klein gehalten. Mir ist es wichtig, jeweils einen Teil der insgesamt gewünschten Funktionalität komplett fertig zu kriegen. Was nützt es mir, wenn ich zwar Markdown-Texte nach allen Regeln der Kunst zerlegen kann, davon jedoch noch nichts visualisiert wird? Folglich habe ich mich erst einmal darauf beschränkt, nur kursiv und fett zu erkennen und in einem UserControl zu visualisieren. Text, der in einfache Sternchen eingefasst ist, soll *kursiv* dargestellt werden, Text in doppelten Sternchen **fett**.
Meine ersten Überlegungen drehten sich um die Frage, wie ich einen Text nach der Erkennung der Markdown-Auszeichnungen repräsentieren möchte. Wie sieht eine geeignete Datenstruktur aus? Um das Einfachste zu tun, habe ich mir überlegt, dass der Text nach der Zerlegung aus TextElementen bestehen könnte. Enthält ein Text keinerlei Markdown-Auszeichnungen, wäre der gesamte Text ein einzelnes TextElement.
Enthält der Text jedoch ein Wort in Fettschrift, würde ich diesen Text in drei TextElemente zerlegen:
ein TextElement in normaler Schrift,
ein TextElement in fetter Schrift,
T wieder ein TextElement in normaler Schrift.
Der Text „Ein **fettes** Wort“ würde also in die drei TextElemente „Ein “, „fettes“, „Wort“ zerlegt. Beachten Sie die Leerzeichen: nach „Ein“ und vor „Wort“ ist jeweils ein Leerzeichen.
Durch diese simple Datenstruktur kann die Zerlegung eines Markdown-Textes schrittweise erfolgen: Zuerst werden fett hervorgehobene TextElemente extrahiert. Anschließend werden aus den bereits gebildeten TextElementen weitere gebildet, um so kursive Texte darzustellen. Dazu muss die TextElement-Datenstruktur neben dem Text die Information mitführen, ob der Text fett und/oder kursiv dargestellt werden soll. Listing 1 zeigt die entsprechende Datenstruktur.
Listing 1
Grundlegende Datenstruktur.
public class TextElement { public string Text { get; set; } public bool Fett { get; set; } public bool Kursiv { get; set; } }
Aus diesen überlegungen hat sich der in Abbildung 1 gezeigte Flow ergeben.
[Abb. 1]
Fett und kursiv gesetzte Textteile extrahieren.
Im ersten Schritt wird der eingehende String in ein TextElement-Objekt umgewandelt. Weil anschließend jeweils eine Aufzählung von TextElement-Objekten bearbeitet wird, liefert die Funktionseinheit Verpacke_in_TextElement gleich eine Aufzählung, die allerdings immer nur ein einzelnes Objekt enthält.
Anschließend werden Texte extrahiert, die in Fettschrift ausgezeichnet sind. Es wäre denkbar, dass diese Funktionseinheit später auf mehr als einem TextElement arbeiten muss. Das könnte zum Beispiel der Fall sein, wenn vor der Fettschrift die Überschriften extrahiert werden. Somit ist es sinnvoll, als Eingang der Funktionseinheit gleich eine Aufzählung von TextElement-Objekten vorzusehen.
Nach dem Extrahieren von Fettschrift folgt das Extrahieren der kursiv ausgezeichneten Texte. Wieder wird eine Aufzählung von TextElement-Objekten behandelt. Werden im eingehenden Datenstrom kursiv ausgezeichnete Texte gefunden, dann wird ein TextElement möglicherweise in mehrere TextElement-Objekte zerlegt. Die Anzahl der ausgehenden TextElement-Objekte kann also höher sein als auf der Eingangsseite.
Nun mag dem einen oder anderen Leser durch den Kopf gehen, dass das gewählte Verfahren eventuell aus Laufzeitgründen ineffizient ist. Ja, das kann sein. Aber an dieser Stelle kümmert mich das nicht. Die Lösung ist dazu gedacht, Markdown-Texte in einem Label-Control zu visualisieren. Das heißt, es werden typischerweise wenige, noch dazu kurze Texte bearbeitet. Für die Verarbeitung von Massendaten ist dieser Ansatz möglicherweise nicht optimal.
Aber das ist gerade nicht die Aufgabenstellung. Es genügt hier also, das Einfachstmögliche zu tun. Der gewählte Weg sieht mir bis hierhin in jedem Fall gut evolvierbar aus. Weitere Markdown-Elemente zu erkennen bedeutet nur, eine entsprechende Funktionseinheit zu implementieren, die für das Zerlegen zuständig ist. Diese kann dann leicht in den bestehenden Flow eingehängt werden. Spannend bleibt dabei die Frage, ob die Datenstruktur TextElement dann weiterhin ausreicht. Doch bevor ich das herausfinde, folgt nun die Implementation für das Erkennen von Fettschrift. Listing 2 zeigt einige Tests dazu.
Listing 2
Erst mal testen: Fett-Formatierungen erkennen.
[TestFixture] public class Extrahiere_Fett_Tests { private Extrahiere_Fett sut; private IEnumerable<TextElement> result; [SetUp] public void Setup() { sut = new Extrahiere_Fett(); sut.Result += x => result = x; } [Test] public void Normaler_Text_ ohne_Fettschrift() { sut.Process(new[] {new TextElement{ Text = "x"}}); Assert.That(result.Select( x => x.Text), Is.EqualTo(new[]{"x"})); Assert.That(result.Select( x => x.Fett), Is.EqualTo(new[]{false})); Assert.That(result.Select( x => x.Kursiv), Is.EqualTo(new[]{false})); } [Test] public void Fettschrift_am_ Anfang_eines_TextElements() { sut.Process(new[] { new TextElement { Text = "**fett** x" } }); Assert.That(result.Select( x => x.Text).ToArray(), Is.EqualTo(new[] { "fett", " x" })); Assert.That(result.Select( x => x.Fett).ToArray(), Is.EqualTo(new[] { true, false })); Assert.That(result.Select( x => x.Kursiv).ToArray(), Is.EqualTo(new[] { false, false })); } [Test] public void Mehrfach_ Fettschrift() { sut.Process(new[] { new TextElement { Text = "**f1****f2** y**f3**" } }); Assert.That(result.Select( x => x.Text).ToArray(), Is.EqualTo(new[] { "f1", "f2", "y", "f3" })); Assert.That(result.Select( x => x.Fett).ToArray(), Is.EqualTo(new[] { true, true, false, true })); Assert.That(result.Select( x => x.Kursiv).ToArray(), Is.EqualTo(new[] { false, false, false, false })); } [...] }
Die Tests spielen diverse Szenarien durch, in denen fett ausgezeichneter Text auftreten kann. Da die Funktionseinheit als Event-Based Component (EBC) realisiert ist, hat sie für die Eingangsdaten eine Methode Process und für die Ausgangsdaten einen Event Result. Mithin genügt es für das Testen nicht, eine Methode aufzurufen, vielmehr muss das Ergebnis des Events ausgewertet werden. Daher binde ich im Setup der Testklasse an den Event die Lambda-Expression, welche das Ergebnis im Feld result der Testklasse ablegt:
sut.Result += x => result = x;
Der Inhalt dieses Feldes result wird dann in den einzelnen Tests jeweils nach Aufruf der Process-Methode durch ein Assert überprüft.
So weit zur Mechanik der Tests. Was mich an den Tests etwas stört, ist die Tatsache, dass auch die Kursiv-Eigenschaft in den TextElement-Objekten überprüft wird. Diese Eigenschaft zu prüfen ist sinnvoll, um sicherzustellen, dass die Funktionseinheit tatsächlich nur die Fettschrift aktiviert. Dennoch stellt sich die Frage, was kursive Schrift mit fetter Schrift zu tun haben mag. Das ist für mich ein Hinweis darauf, dass die Datenstruktur möglicherweise nicht optimal gewählt ist, denn so kann es zu Missverständnissen kommen. Eine