Praxisbeispiel: Den ersten Buchstaben markieren

Ein Stylesheet soll den ersten Buchstaben eines Absatzes in ein span mit der Klasse initial einschließen. Bei einem einfachen Absatz funktioniert das schnell. Interessant wird es, sobald mitten im Absatz ein em-Element steht oder der Text auf mehrere Knoten verteilt ist. Genau darum geht es in der Initialen-Übung aus unserer Schulung.

Der Fehler steckt hier nicht in einer komplizierten Berechnung, sondern in einem zu weit gefassten XPath-Muster. Ein Test mit nur einem einfachen Absatz würde ihn übersehen.

Was soll herauskommen?

Für dieses Beispiel gilt eine einfache Regel: Bei jedem p ohne Namensraum wird das erste Zeichen des ersten direkten Textkindes markiert. Alles andere bleibt erhalten, also auch Attribute, Kommentare und Inline-Auszeichnungen.

Diese Eingabe enthält mehrere Textknoten:

examples/02-initial/fixtures/multiple-text.xml

<p>Before <em>inside</em> after.</p>

So soll sie nach der Verarbeitung aussehen:

examples/02-initial/expected/multiple-text.xml

<p><span class="initial">B</span>efore <em>inside</em> after.</p>

Wichtig ist das Wort "direkt". Beginnt ein Absatz mit einem Inline-Element, bleibt dessen Text unverändert. Beginnt das erste direkte Textkind mit einem Leerzeichen, wird dieses Leerzeichen markiert. Das ist noch keine fertige Lösung für typografische Initialen, sondern eine klar begrenzte Übung.

Auch bei Unicode gibt es eine Grenze: Die Zeichenoperation verarbeitet einen Codepoint. Ein sichtbares Zeichen kann aber aus mehreren Codepoints bestehen. Eine Lösung, die solche Zeichen zusammenhalten oder Satzzeichen am Absatzanfang überspringen soll, braucht zusätzliche Regeln und Tests.

Den Fehler mit einem Test sichtbar machen

Die fehlerhafte Variante verwendet p//text(). Damit trifft die Regel auf alle untergeordneten Textknoten zu, auch auf den Text in einem em-Element. Dort entsteht dann eine weitere Initiale. Das historische Beispiel zeigt genau dieses Problem.

Die fehlerhafte Fassung ist im Download absichtlich enthalten. So lässt sich ausprobieren, ob die Tests den Fehler erkennen:

python scripts/run-tests.py --only deliberate-failure

Drei Fälle werden geprüft. Der einfache Textabsatz soll bestehen, die beiden Fälle mit gemischtem Inhalt sollen scheitern. Der Runner erwartet genau diese Verteilung. Ein Programmabbruch wegen einer fehlenden Bibliothek wäre dagegen nicht das gewünschte Ergebnis.

Das Stylesheet korrigieren

Mit p/text()[1] wird nur das erste direkte Textkind ausgewählt. Die korrigierte Fassung sieht so aus:

examples/02-initial/initial.xsl

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="no"/>

  <!-- Preserve every node and attribute unless a specific rule applies. -->
  <xsl:template match="@*|node()">
    <xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy>
  </xsl:template>

  <!-- First code point of the first direct text child of each unnamespaced p. -->
  <xsl:template match="p/text()[1]">
    <span class="initial"><xsl:value-of select="substring(., 1, 1)"/></span>
    <xsl:value-of select="substring(., 2)"/>
  </xsl:template>
</xsl:stylesheet>

Die Vorlage von 2013 verwendet xsl:analyze-string aus XSLT 2.0. Für das neue Beispiel reichen substring-Operationen aus XSLT 1.0. Dadurch lässt sich die Transformation zusätzlich direkt mit libxslt ausprobieren. Die Anforderungen an XSpec selbst ändern sich dadurch nicht.

Der passende Test lädt Eingabe und erwartetes Ergebnis aus zwei Dateien:

examples/02-initial/initial.xspec

<x:scenario xmlns:x="http://www.jenitennison.com/xslt/xspec" label="Handle multiple text input">
    <x:context href="fixtures/multiple-text.xml"/>
    <x:expect label="Preserve the contract" href="expected/multiple-text.xml" select="/element()"/>
  </x:scenario>

Nicht beim ersten grünen Test aufhören

Die vollständige Testsuite enthält zehn kleine Fälle. Dazu gehören einfacher Text, Inline-Auszeichnungen, mehrere Textknoten, ein leerer Absatz, Attribute und ein Kommentar. Hinzu kommen ein führendes Inline-Element, ein führendes Leerzeichen, ein anderes Element als p und ein Absatz im XHTML-Namensraum.

Jeder Fall beantwortet eine konkrete Frage. Bleiben Attribute erhalten? Werden andere Elemente in Ruhe gelassen? Wird ein XHTML-Absatz anders behandelt als ein p ohne Namensraum? Das ist hilfreicher als zehn fast gleiche Absätze.

Die zehn korrigierten Transformationen wurden bereits direkt mit libxslt geprüft. Auch die zwei vorgesehenen Abweichungen der fehlerhaften Variante wurden dabei erkannt. Die Ergebnisse stehen in reports/local-checks.json. Die Ausführung derselben Tests mit XSpec steht im aktuellen Paket noch aus.

<< zurück                                                                                                             vor >>