4.1.9.2  Zweistufige Leerzeichen-Eliminierung

Mein Ehrgeiz für dieses Problem wurde durch die sehr gute Lösung meines Kollegen geweckt, der einen anderen Programmierstil pflegt. Deshalb musste ich beweisen, dass man auch "old-school" regelbasiert mit XSLT zu einer vernünftigen Lösung kommt. Glücklicherweise gibt es mittlerweile sehr ausgeklügelte XSLT Konstrukte XSLT Konstrukte .

Wenn man sich das obige Beispiel anschaut, dann lässt sich die Aufgabe in zwei Teile zerlegen:

1. Entferne alle Textknoten unterhalb von <title> bis zum ersten Textknoten, der auch Buchstaben und sichtbare Zeichen enthält.

2. Danach kannst Du am ersten Textknoten unterhalb von <title> die führenden Leereichen abschneiden.

Hört sich simpel an, ist es aber leider nicht.

Zunächst recherchierte ich, ob denn auch wirklich an einem PCDATA Element nur ein Textknoten dranhängt. Diese Information war nötig, weil mein erster Algorithmus noch nicht ganz so ausgefeilt war, wie in den zwei Punkten oben beschrieben.

Man kann in einer Transformation mehrere Textknoten hintereinander erzeugen, wie:

<xsl:value-of select="'erster Textknoten'"/><xsl:value-of select="'zweiter Textknoten'"/>
Plain Text

Diese werden aber bei einem "Save-Load Cycle" zu einem Textknoten normalisiert. So steht das zumindest in der DOM Core Spezifikation. Inwieweit das dann in den XML Prozessoren umgesetzt ist, musste noch geprüft werden. Dazu habe ich den Saxon Quellcode herangezogen:

Quellcode Schnippsel aus dem Saxon XSLT Prozessor, das zeigt, dass der EndElement-Listener im Parser einen Normalisierungsschritt auf den beteiligten DOM Knoten aufruft.
figure: 9  endElement() Funktion im Saxon XSLT Prozessor

Die Normalisierungsfunktion lässt Mike Kay XML Gurus Michael Kay dann mit einem aussagekräftigen Kommentar frei...

Methodenrumpf der Normalisierungsfunktion im Saxon XSLT Prozessor
figure: 10  normalize() Funktion im Saxon XSLT Prozessor

Damit war ich zufrieden - ob das jetzt stimmt oder nicht, ist glücklicherweise für die endgültige Lösung irrelevant.

Mein erster Versuch alle Textknoten auszuschneiden, die nur Leerzeichen enthalten, sah so aus:

<!-- Entferne alle Leerzeichen-Only Knoten, die kein Vorgänger Inline-Element haben -->
<xsl:template match="text()[ancestor::title 
                            and not((.|..)/preceding-sibling::node()[1][self::*]) and
                                not(translate(.,' ','')]"/>
Plain Text

Leider konnte dadurch der folgende - Nicht-Real-Welt - Testfall nicht gelöst werden:

<title><b>•Fettes</b><b><i><b><i> </i></b></i>Editierproblem</b></title>
Plain Text

Das Leerzeichen im <i> Tag wurde verschluckt. Das kam wegen der zweiten Bedingung, die nur maximal eine Verschachtelungsebene beachtet. Man könnte zwar den Ausdruck noch aufbohren, z.B. so:

not((.|..|../..|../../..)/preceding-sibling::node()[1][self::*])
Plain Text

Das sieht aber schon wirklich sehr unschön aus.

Da ich mir aber zuvor schon den zweiten Schritt überlegt hatte, der so aussieht:

<!-- Entferne am ersten Textknoten unterhalb von title führende Leerzeichen -->
<xsl:template match="text()[current() is ancestor::title[1]/(descendant::text())[1]]" 
              priority="10"  mode="pass-2">
    <xsl:value-of select="replace(.,'^\s+','')"/>
</xsl:template>
Plain Text

...fiel mir schliesslich die Korrektur für den ersten Schritt leicht:

<xsl:template match="text()[current() &lt;&lt; ancestor::title[1]/
                           (descendant::text()[normalize-space(.)])[1]]" mode="pass-1"/>
Plain Text

Abschliessend ist noch der ganze Quelltext der Lösung abgebildet. Dieser zeigt auch nochmal das Pattern bzgl. der Vortransformationen (imported):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs"
    version="2.0">
    
    <xsl:template match="text()[current() &lt;&lt; ancestor::title[1]/
                               (descendant::text()[normalize-space(.)])[1]]" 
                  mode="pass-1"/>
    
    <xsl:template match="text()[current() is ancestor::title[1]/(descendant::text())[1]]" 
                  priority="10"  mode="pass-2">
        <xsl:value-of select="replace(.,'^\s+','')"/>
    </xsl:template>
    
    <xsl:template match="/">
        <xsl:variable name="pass-1">
            <xsl:apply-templates mode="pass-1"/>
        </xsl:variable>
        <xsl:apply-templates select="$pass-1" mode="pass-2"/>
    </xsl:template>
    
    <xsl:template match="node()|@*" mode="#all">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*" mode="#current"/>          
        </xsl:copy>
    </xsl:template>
    
</xsl:stylesheet>
Plain Text