Archiv für Mai, 2012

XSLT-SB 0.2.50: Bessere Dokumentation und neue Funktionen

XSLT-SB im Google-Code-Wiki

XSLT-SB im Google-Code-Wiki

Heute habe ich Release 0.2.50 der XSLT-SB veröffentlicht. Neben einigen neuen Funktionen bringt das Release vor allem eine stark verbesserte Dokumentation im Goolge-Code-Wiki. Zu jeder Funktion und zu jedem Template gibt es jetzt eine Einzelseite mit Beschreibung, Implementierung und Verweisen auf benutzte XSLT-SB-Funktionen und -Templates. Damit ist ein sehr einfacher Zugang zum Code – auch außerhalb der kompletten Stylesheets – möglich. Hier je ein Beispiel für eine Funktion und für ein Template.

Die neuen Funktionen – xsb:sort(), xsb:replace(), xsb:integer-to-hex(), xsb:hex-to-integer(), xsb:twos-complement(), xsb:reverse-twos-complement(), xsb:fill-left(), xsb:fill-right(), xsb:escape-for-regex(), xsb:escape-for-replacement(), xsb:count-matches(), xsb:index-of-first-match() und xsb:decode-from-url() haben sich im Laufe einiger Projekte bei mir angesammelt und sind nun in die XSLT-SB gewandert; einige möchte ich im Folgenden näher beschreiben.

xsb:sort()

Diese Funktion sortiert atomic values, also Zahlen, Strings usw. Das Fehlen dieser Funktion in den XPath-Funktionen ist ein Rätsel, deshalb liefert der XSLT-Standard wohl das Gerüst der Implementierung – als Wrapper für xsl:perform-sort – gleich mit. Ich habe das Beispiel um die Möglichkeit ergänzt, die Reihenfolge (aufsteigend/absteigend, englisch ascending/descending) in Funktionsaufruf zu übergeben. Das sieht dann so aus:

<xsl:function name="xsb:sort" as="xs:anyAtomicType*" intern:solved="EmptySequenceAllowed">
	<xsl:param name="input-sequence" as="xs:anyAtomicType*"/>
	<xsl:param name="order" as="xs:string"/>
	<xsl:perform-sort select="$input-sequence">
		<xsl:sort select="." order="{$order}"/>
	</xsl:perform-sort>
</xsl:function>

Sortiert werden können nur Werte, die mit dem lt-Operator verglichen werden können, also nur Sequenzen aus Strings, Zahlen, Daten; nicht aber gemischte Sequenzen aus diesen Typen. Gemischte Sequenzen können aber auf string gecastet werden, etwa mit for $i in $sequence return string($i).

Zur bequemeren Benutzung gibt es auch eine Version mit nur einem Argument, dann ist die Reihenfolge auf aufsteigend resp. ascending festgelegt.

xsb:index-of-first-match()

Diese Funktion ermittelt die Position des ersten Auftretens eines RegEx-Patterns in einem String. Mit fn:tokenize() wird der erste Teilstring vor dem Pattern ermittelt und zu dessen Länge 1 addiert:

<xsl:function name="xsb:index-of-first-match" as="xs:integer">
	<xsl:param name="input" as="xs:string?"/>
	<xsl:param name="pattern" as="xs:string?"/>
	<xsl:param name="flags" as="xs:string?"/>
	<xsl:choose>
		<xsl:when test="normalize-space($pattern) and matches($input, $pattern, $flags)">
			<xsl:sequence select="string-length(tokenize($input, $pattern, $flags)[1]) + 1"/>
		</xsl:when>
		<xsl:otherwise>0</xsl:otherwise>
	</xsl:choose>
</xsl:function>

Auch hier gibt es wieder die »bequeme« Version ohne flags.

xsb:replace()

xsb:replace() erweitert fn:replace() um die Möglichkeit, Listen (genauer Sequenzen) als Argumente für Such-Pattern und Ersetzungszeichenfolgen zu übergeben. Das ist praktisch, wenn in einem String paarweise verschiedene Fundstellen durch korrespondierende Texte ersetzt werden sollen, also z.B. alle »Jan.« durch »Januar«, »Feb.« durch »Februar«, »Apr.« durch »April« usw. Ohne benutzerdefinierte Funktionen müssen mehrere fn:replace() geschachtelt werden, es entstehen dann Ungetüme wie:

replace(
	replace(
		replace($input,
			'Apr\.', 'April'),
		Feb\.', 'Februar'),
	'Jan\.', 'Januar')

Viel übersichtlicher ist es dann doch so:

xsb:replace($input,
	('Jan\.', 'Feb.\', 'Apr\.'),
	('Januar', 'Februar', 'April') )

Neben der Übersichtlichkeit ist ein zweiter wesentlicher Vorteil, dass mit xsb:replace() die Länge der Sequenzen resp. die Anzahl der Ersetzungspaare nicht schon beim Schreiben des Stylesheets bekannt sein müssen. Dazu folgt weiter unter ein Beispiel. Aber erst noch ein paar Worte zur Implementierung, die so aussieht:

<xsl:function name="xsb:replace" as="xs:string">
	<xsl:param name="input" as="xs:string?"/>
	<xsl:param name="pattern" as="xs:string*"/>
	<xsl:param name="replacement" as="xs:string*"/>
	<xsl:param name="flags" as="xs:string?"/>
	<xsl:choose>
		<xsl:when test="exists($pattern[1])">
			<xsl:sequence select="
				xsb:replace(
					if (boolean($pattern[1]) )
						then replace($input, $pattern[1], string($replacement[1]), $flags)
						else $input,
					$pattern[position() gt 1],
					$replacement[position() gt 1],
					$flags
				)"/>
		</xsl:when>
		<xsl:otherwise>
			<xsl:sequence select="concat('', $input)"/>
		</xsl:otherwise>
	</xsl:choose>
</xsl:function>

Es handelt sich um eine rekursive Funktionsdefinition. Die Implementierung ist nicht ganz geradlinig, weil Leerstrings und Leersquenzen sinnvoll behandelt werden müssen. Der Reihe nach:

  • exists($pattern[1]) testet, ob ein weiteres pattern-Argument vorhanden ist. Wichtig ist, dass fn:exists() (im Unterschied zu fn:boolean()) den Leerstring zu true() evaluiert. Ist kein weiteres pattern-Argument vorhanden, sind die Ersetzungen abgeschlossen, und das Ergebnis der Funktion wird im xsl:otherwise-Zweig zurückgegeben. (Das Ergänzen eines Leerstrings dort stellt sicher, dass keine Leersequenz, sondern mindestens ein Leerstring ausgegeben wird.)
  • Wenn ein weiteres pattern-Argument (das auch ein Leerstring sein kann) vorhanden ist, wird xsb:replace() rekursiv aufgerufen
    • Für das zu übergebende input-Argument wird mit boolean($pattern[1]) geprüft, ob das aktuelle Such-Pattern Zeichen enthält, und nur in diesem Fall wird ein fn:replace() mit den aktuellen pattern und replacement aufgerufen. Im Fall eines Leerstrings wird input unverändert weitergereicht. Diese Akrobatik ist notwendig, weil der Leerstring kein gültiger Suchstring in fn:replace() ist, ich mir aber an dieser Stelle etwas Fehlertoleranz gewünscht habe.
    • Die in den pattern– und replacement-Sequenzen auf den jeweils ersten Wert folgenden Werte werden als neue pattern und replacement übergeben. Wenn keine weiteren Argumente vorhanden sind, wird halt eine Leersequenz weitergereicht, was im Fall von pattern im nächsten Durchlauf zum Abbruch der Rekursion und zur Ausgabe der Ergebnisses führt.
    • flags wird unverändert durchgereicht.

Leerstrings in der pattern-Sequenz werden als »nichts suchen« interpretiert und samt dem zugehörigen replacement übersprungen.

Sind mehr pattern-Werte als replacement-Werte vorhanden, werden die Fundstellen der »überzähligen« pattern-Werte gelöscht: xsb:replace('Affe Bär Elefant', ('Affe', 'Elefant') , ('monkey') ) ergibt »monkey Bär «. Dieses Verhalten entspricht dem des guten alten fn:translate().

xsb:replace kann man wunderbar zum Suchen-und-Ersetzen an Hand einer Ersetzungstabelle verwenden. Im folgenden Beispiel wird die Ersetzungstabelle als externes Dokument verwaltet, dass ggfs. unabhängig vom Stylesheet bearbeitet werden kann.

Ersetzungstabelle (search-and-replace_list.xml):

<root>
	<pair>
		<pattern>Affe</pattern>
		<replacement>monkey</replacement>
	</pair>
	<pair>
		<pattern>Wolf</pattern>
		<replacement>wolf</replacement>
	</pair>
</root>

Stylesheet:

<xsl:stylesheet
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	xmlns:xs="http://www.w3.org/2001/XMLSchema"
	xmlns:xsb="http://www.expedimentum.org/XSLT/SB"
	exclude-result-prefixes="xs xsb"
	version="2.0">
	<xsl:import href="strings.xsl"/>
	<xsl:param name="search-and-replace-list">search-and-replace_list.xml</xsl:param>
	<xsl:template match="p">
		<xsl:copy>
			<xsl:value-of select="xsb:replace(
				.,
				doc($search-and-replace-list)//pair/string(pattern),
				doc($search-and-replace-list)//pair/string(replacement)
			)"/>
		</xsl:copy>
	</xsl:template>
	<xsl:template match="@*|node()">
		<xsl:copy>
			<xsl:apply-templates select="@*, node()"/>
		</xsl:copy>
	</xsl:template>
</xsl:stylesheet>

Beispieldokument:

<root>
	<p>Affe</p>
	<p>Wolf</p>
	<p>Zebra</p>
</root>

Transformationsergebnis:

<root>
	<p>monkey</p>
	<p>wolf</p>
	<p>Zebra</p>
</root>

Achtung! Leersequenzen innerhalb der pattern-Sequenz (bzw. fehlende pattern-Elemente in der Ersetzungstabelle) führen dazu, dass die folgenden Werte innerhalb der pattern-Sequenz nach links rücken: xsb:replace('Affe Wolf Zebra', ('Affe', (), 'Zebra'), ('monkey', 'wolf', 'zebra') ) ergibt – ebenso wie xsb:replace('Affe Wolf Zebra', ('Affe', 'Zebra'), ('monkey', 'wolf', 'zebra') ) – »monkey Wolf wolf«. Das gilt analog auch für die replacement-Sequenz bzw. replacement-Elemente der Ersetzungstabelle. Diesem Problem kann man mit einem rigiden Schema samt Validierung oder durch geschickte Konstruktion der pattern– und replacement-Sequenzen begegnen: im obigen Stylesheet wurde nicht das naheliegende doc($search-and-replace-list)//pattern verwendet, sondern statt dessen doc($search-and-replace-list)//pair/string(pattern), womit bei fehlendem pattern-Element mit fn:string() ein Leerstring erzeugt wird.

Exkurs: Im Gegensatz zu fn:string() evaluiert xs:string() die Leersequenz zu einer Leersequenz. Ich bin mir sicher, dass es für diesen Unterschied eine sachliche Begründung gibt, aber solche Inkonsistenzen machen das Programmieren nicht nur für Einsteiger unnötig kompliziert.

Durch die Verwendung von fn:replace() werden die Suchstrings als reguläre Ausdrücke interpretiert. Entsprechend wird im Beispiel oben der Punkt mit dem Backslash escapet. Wenn das händische Escapen nicht sinnvoll oder möglich ist, hilft die Funktion xsb:escape-for-regex(). Analog dazu gibt es auch im Ersetzungstext Steuerzeichen, die mit xsb:escape-for-replacement() escapet werden können.

Auch von xsb:replace() gibt es die »bequeme« Version ohne flags.

xsb:escape-for-regex() und xsb:escape-for-replacement()

Die Funktion xsb:escape-for-regex() escapet in Strings Steuerzeichen für reguläre Ausdrücke mit einem Backslash (\). Damit können Strings, die Steuerzeichen enthalten, als Suchmuster in regulären Ausdrücken verwendet werden. Im Beispiel oben musste beispielsweise der Punkt (steht in regulären Ausdrücken für ein beliebiges Zeichen) in Jan. escapet werden (Jan\.), damit nicht auch »Jana« ersetzt wird. Die Implementierung ist sehr simpel:

<xsl:function name="xsb:escape-for-regex" as="xs:string">
	<xsl:param name="input" as="xs:string?"/>
	<xsl:sequence select="concat('', replace($input, '[\\*.+?\^\$()\[\]{}|]', '\\$0') )"/>
</xsl:function>

Im pattern von fn:replace() werden die »verbotenen« Zeichen gesucht. Bemerkenswert ist vielleicht das replacement: der doppelte Backslash ist ein escapeter einfacher Backslash, und $0 steht für den gesamten gematchten Teilstring. Es wird also ein Backslash ausgegeben und anschließend die Fundstelle wiederholt.

Da es also auch im Ersetzungstext von fn:replace() Steuerzeichen – »\« und »$« – geben kann, macht eine Funktione zu Escapen eben dieses Ersetzungstextes das Leben einfacher: xsb:escape-for-replacement().

Ein Beispiel für die Nutzung der beiden Funktionen ergibt sich im Zusammenhang mit der oben beschriebenen Ersetzungstabelle. Wenn man davon ausgehen kann, dass in search-and-replace_list.xml keine regulären Ausdrücke notiert werden, kann das Stylesheet zur Absicherung modifiziert werden:

<xsl:template match="p">
	<xsl:copy>
		<xsl:value-of select="xsb:replace(
			.,
			doc($search-and-replace-list)//pair/xsb:escape-for-regex(string(pattern)),
			doc($search-and-replace-list)//pair/xsb:escape-for-replacement(string(replacement))
		)"/>
	</xsl:copy>
</xsl:template>

Quelldokument, Ersetzungstabelle und Stylesheet habe ich wie üblich in der Beispielsammlung abgelegt.

Weblinks:

Keine Kommentare

Benutzerdefinierte Funktionen und externe Funktionsbibliotheken in Schematron

In manchen Situationen reicht der Umfang von XPath oder auch von XPath 2.0 nicht aus, um die gewünschten Tests zu formulieren, etwa wenn rekursive Funktionsaufrufe nötig sind. In anderen Situationen möchte man Algorithmen in verschiedenen Tests wiederverwenden. In solchen Situationen helfen benutzerdefinierte Funktionen weiter. Mit XSLT 2.0 geht das recht einfach:

<schema
	xmlns="http://purl.oclc.org/dsdl/schematron"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	queryBinding="xslt2"
	>
 
	<ns prefix="my" uri="test"/>
 
	<xsl:function name="my:literal-autor" as="xs:string?">
		<xsl:param name="vorname" as="xs:string?"/>
		<xsl:param name="nachname" as="xs:string?"/>
		<xsl:sequence select="concat($nachname, ', ', $vorname)"/>
	</xsl:function>
 
	<pattern id="p3">
		<rule context="autor">
			<assert test="my:literal-autor(//person[@xml:id eq current()/@ref]/vorname, //person[@xml:id eq current()/@ref]/nachname) eq .">[p3] autor muss eine gültige Kombination aus person/vorname und person/nachname sein.</assert>
		</rule>
	</pattern>
 
</schema>

Im äußersten schema-Element wird der XSL-Namespace definiert und mit queryBinding="xslt2" XSLT 2.0 als Abfragesprache festgelegt. Mit dem ns-Element wird analog zu XSLT 2.0 der Namespace für die benutzerdefinierte Funktionen deklariert.

Anschließend folgt die Funktionsdefinition 1:1 wie in XSLT 2.0. Das Beispiel fügt Nachname und Vorname – getrennt durch ein Komma – zusammen. xsl:function-Elemente können an beliebiger Position direkt unterhalb von schema stehen.

Schließlich wird im assert die so definierte Funktion verwendet. Das Beispiel testet, ob der Inhalt des autor-Elements mit dem referenzierten person-Element korrespondiert.

Das dazugehörige XML könnte so aussehen:

<literatur>
	<buecher>
		<buch xml:id="b1">
			<autor ref="p1">Mann, Thomas</autor>
			<titel>Der Zauberberg</titel>
			<isbn>978-3-596-29433-6</isbn>
			<href>http://d-nb.info/942764498</href>
		</buch>
		<buch xml:id="b2">
			<autor ref="p2">Mann,Klaus</autor>
			<titel>Mephisto</titel>
			<isbn>3-10-046705-1</isbn>
			<href>http://d nb.info/959653694</href>
		</buch>
		<buch xml:id="b3">
			<autor ref="b1"></autor>
			<titel></titel>
		</buch>
	</buecher>
	<autoren>
		<person xml:id="p1">
			<vorname>Thomas</vorname>
			<nachname>Mann</nachname>
		</person>
		<person xml:id="p2">
			<vorname>Klaus</vorname>
			<nachname>Mann</nachname>
		</person>
	</autoren>
</literatur>

OxygenXML-Einstellungsdialog für SchematronBei buch xml:id="b2" wird ein Fehler gemeldet, weil das Leerzeichen nach dem Komma fehlt, bei buch xml:id="b3" wegen des fehlenden Inhaltes. Schema und XML habe ich in der Beispielsammlung abgelegt.

In OxygenXML muss die Verarbeitung von XSLT innerhalb von Schematron ggfs. erst aktiviert werden. Dazu muss in den Einstellungen unter XML ⇒ XML-Parser ein Häkchen bei ISO Schematron ⇒ Fremde Elemente erlauben (allow-foreign) gesetzt werden, vgl. Bild rechts. [Edit: Ein Hinweis darauf, dass das Häkchen fehlt, ist die Fehlermeldung »unrecognized element … from namespace http://www.w3.org/1999/XSL/Transform«, wobei an Stelle der drei Pünktchen der Name eines XSL-Elements steht, bspw. xsl:function oder xsl:include]

externe Funktionsbibliotheken

Oft liegen die benötigten Funktionen bereits in einer Bibliothek vor. Beispielsweise lassen sich URLs mit misc:is-url() aus der XSLT-SB auf Gültigkeit testen. Auch das Einbinden externen Bibliotheken geht mit Schematron recht einfach:

<schema
	xmlns="http://purl.oclc.org/dsdl/schematron"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	queryBinding="xslt2"
	>
 
	<xsl:include href="http://www.expedimentum.org/example/xslt/xslt-sb/files.xsl"/>
 
	<ns prefix="xsb" uri="http://www.expedimentum.org/XSLT/SB"/>
 
	<pattern id="p4">
		<rule context="href">
			<assert test="xsb:is-url(.)">[p4] href muss eine gültige URL beinhalten</assert>
		</rule>
	</pattern>
 
</schema>

Mit diesem Schema wird das href-Element bei buch xml:id="b2" bemängelt, da ein Leerzeichen kein gültiges Zeichen in einer URL ist.

Übrigens hatte ich mit <xsl:import/> keinen Erfolg; mir fällt aber kein Beispiel ein, wo man nicht statt dessen per <xsl:include/> ein (ggfs. angepasstes) externes Stylesheet verwenden könnte. Über Beispiele und/oder Hinweise zur Lösung würde ich mich freuen.

Auch dieses Schema habe ich in der Beispielsammlung abgelegt.

1 Kommentar

xsl:key in Schematron verwenden

Schlüssel (xsl:key) sind eine Möglichkeit, um Transformationen über größere Dokumente mit vielen (internen oder externen) Verweisen zu beschleunigen. Im Sprachumfang von Schematron sind Daten-Schlüssel nicht enthalten, allerdings ist es sehr einfach, xsl:key zu verwenden. Ein einfaches Beispiel: in einer Literaturliste verweisen die autor-Elemente über das ref-Attribut auf ein korrespondierendes person-Element aus einer Liste:

<literatur>
	<buecher>
		<buch xml:id="b1">
			<autor ref="p1">Mann, Thomas</autor>
			<titel>Der Zauberberg</titel>
			<isbn>978-3-596-29433-6</isbn>
			<href>http://d-nb.info/942764498</href>
		</buch>
		<buch xml:id="b2">
			<autor ref="p2">Mann,Klaus</autor>
			<titel>Mephisto</titel>
			<isbn>3-10-046705-1</isbn>
			<href>http://d nb.info/959653694</href>
		</buch>
		<buch xml:id="b3">
			<autor ref="b1"></autor>
			<titel></titel>
		</buch>
	</buecher>
	<autoren>
		<person xml:id="p1">
			<vorname>Thomas</vorname>
			<nachname>Mann</nachname>
		</person>
		<person xml:id="p2">
			<vorname>Klaus</vorname>
			<nachname>Mann</nachname>
		</person>
	</autoren>
</literatur>

Natürlich lässt sich die Referenz problemlos über XPath-Lokalisierungsschritte wie //person[@xml:id eq current()/@ref] prüfen, aber eleganter und vermutlich schneller geht das mit Schlüsseln. Um xsl:key in Schematron zu verwenden, muss das queryBinding im äußersten schema-Element auf xslt (das ist ohnehin der Standardwert) oder xslt2 gesetzt und der XSL-Namespace deklariert werden, danach können xsl:key-Elemente und die key()-Funktion wie in XSLT verwendet werden:

<schema
	xmlns="http://purl.oclc.org/dsdl/schematron"
	xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
	queryBinding="xslt"
	>
 
	<xsl:key name="myKey" match="person" use="@xml:id"/>
 
	<pattern id="p3">
		<rule context="autor">
			<assert test="key('myKey', @ref)">[p3] autor/@ref muss auf ein person/@xml:id verweisen</assert>
		</rule>
	</pattern>
 
</schema>

Das assert überprüft, ob jedes autor-Element ein ref-Attribut hat, und ob dieses ref-Attribut auf ein person-Element in der Autorenliste verweist. Entsprechend wird im obigen XML bei buch xml:id="b3" die Referenz auf b1 als Fehler gemeldet.

xsl:key-Elemente sollen im Schema vor dem ersten pattern-Element stehen. Leider ist der Schematron-Standard diesbezüglich (und wie so oft) nicht sehr präzise; bei einem kurzen Test mit OxygenXML hat aber auch ein xsl:key nach dem pattern funktioniert.

XML-Beispiel und Schematron habe ich in der Beispielsammlung abgelegt.

Weblink

Keine Kommentare