]> Pierre Choffet | Git repositories - wdef_tools.git/commitdiff
Add scripts to work with RDF
authorPierre Choffet <peuc@wanadoo.fr>
Fri, 14 Jan 2022 21:49:55 +0000 (16:49 -0500)
committerPierre Choffet <peuc@wanadoo.fr>
Fri, 14 Jan 2022 21:49:55 +0000 (16:49 -0500)
We can now manage a local RDFs cache, and reduce a wdef element from its already known values in Wikidata.

README
scripts/get_merged_element.sh [new file with mode: 0755]
scripts/rdf.sh [new file with mode: 0644]
xslts/merge_rdf.xslt [new file with mode: 0644]

diff --git a/README b/README
index ce966d130b3285dfc2d8bf6bf236340128d4ab42..2217d9f05b8c31d408f8cbc668ceaaa6031d7186 100644 (file)
--- a/README
+++ b/README
@@ -19,12 +19,18 @@ Runtime dependencies are:
 
 
 Description of the provided tools:
+  - scripts/cache_rdf.sh
+    Ensure a Wikidata's RDF element is available in local cache, return its path.
+
   - scripts/get_qid_from_property.sh
     Search Wikidata elements based on a value, return its QID when found.
 
   - xslts/canonicalize.xslt
     Return a wdef under its normal form.
 
+  - xslts/merge_rdf.xslt
+    Read a Wikidata RDF and remove already known values in a wdef file.
+
   - xslts/remove_labels_descriptions.xslt
     Return a wdef with labels and description removed for a given element.
 
diff --git a/scripts/get_merged_element.sh b/scripts/get_merged_element.sh
new file mode 100755 (executable)
index 0000000..c8b4596
--- /dev/null
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+# get_merged_element.sh - Return an element in wdef:knowledge after it's been
+#                         merged with a RDF.
+# Copyright (C) 2022  Pierre Choffet
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of version 3 of the GNU General Public License as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+readonly SCRIPT_DIR="$(dirname "$0")"
+
+source "${SCRIPT_DIR}/rdf.sh"
+
+set -euo pipefail
+
+# Any rdf cache older than this (in minutes) will be updated
+RDFS_MAX_AGE=${RDFS_MAX_AGE:=1440}
+
+readonly CANONICALIZE_XSLT_PATH="${SCRIPT_DIR}/../xslts/canonicalize.xslt"
+readonly MERGE_XSLT_PATH="${SCRIPT_DIR}/../xslts/merge_rdf.xslt"
+
+function usage() {
+       cat << EOF
+USAGE: get_merged_element.sh <wdef_path> <QID>
+
+Merge a RDF into an element inside a wdef file. A wdef file containing this
+single element is returned.
+EOF
+}
+
+if [ "$#" -ne 2 ]
+then
+       usage >&2
+       exit 1
+fi
+
+readonly WDEF_PATH="${1}"
+readonly ELEMENT_QID="${2}"
+
+# Check wdef exists
+if [ ! -s "${WDEF_PATH}" ]
+then
+       echo "WDEF file doesn't exist. Exiting" >&2
+       exit
+fi
+
+# Export element from wdef
+readonly ELEMENT_PATH="$(mktemp)"
+xmlstarlet sel -D -t -e 'wdef:knowledge' -m '/wdef:knowledge' -c "wdef:element[@wdef:id = '${ELEMENT_QID}']" "${WDEF_PATH}" | xmlstarlet fo - > "${ELEMENT_PATH}"
+
+# Check element is in temp file
+if [ "$(xmlstarlet sel -t -i "/wdef:knowledge/wdef:element[@wdef:id = '${ELEMENT_QID}']" -v "'true'" "${ELEMENT_PATH}")" != 'true' ]
+then
+       echo "Element not available in wdef. Exiting." >&2
+       exit
+fi
+
+# Cache RDF
+readonly RDF_PATH=$(cacheRDFMaxAge "${ELEMENT_QID}" "${RDFS_MAX_AGE}")
+
+# Merge and return canonicalized result
+xmlstarlet tr "${MERGE_XSLT_PATH}" -s action=reduce \
+           -s "rdf-path=${RDF_PATH}" \
+           "${WDEF_PATH}" | xmlstarlet tr ${CANONICALIZE_XSLT_PATH} -
+
+rm "${ELEMENT_PATH}"
diff --git a/scripts/rdf.sh b/scripts/rdf.sh
new file mode 100644 (file)
index 0000000..7679251
--- /dev/null
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+# rdf.sh - Set of Bash functions to work with wdef files.
+# Copyright (C) 2022  Pierre Choffet
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of version 3 of the GNU General Public License as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+set -euo pipefail
+
+readonly RDFS_CACHE_DIR=${CACHE_DIR:-"${HOME}/.cache/wdef_tools/rdfs/"}
+
+# Get RDF and return a path to the result into cache dir
+# Parameter:
+#   $1: Element QID
+# Output:
+#   Path to the file containing the RDF, in cache dir
+function cacheRDF() {
+       local -r element_qid="${1}"
+       
+       local -r rdf_url="https://www.wikidata.org/wiki/Special:EntityData/${element_qid}.rdf"
+       local -r rdf_path="${RDFS_CACHE_DIR}${element_qid}.xml"
+       
+       # Create cache dir
+       mkdir -p "${RDFS_CACHE_DIR}"
+       
+       curl "${rdf_url}" > "${rdf_path}"
+       
+       echo "${rdf_path}"
+}
+
+# Ensure RDF cache is not older than given age
+# Parameter:
+#   $1: Element QID
+#   $2: Max age (in minutes)
+# Output:
+#   Path to the file containing the RDF, in cache dir
+function cacheRDFMaxAge() {
+       local -r element_qid="${1}"
+       local -r max_age="${2}"
+
+       local -r rdf_path="${RDFS_CACHE_DIR}${element_qid}.xml"
+       
+       if [ ! -f "${rdf_path}" ]||[[ $(find "${rdf_path}" -mmin "+${max_age}") ]]
+       then
+               cacheRDF "${element_qid}"
+       else
+               echo "${rdf_path}"
+       fi
+}
diff --git a/xslts/merge_rdf.xslt b/xslts/merge_rdf.xslt
new file mode 100644 (file)
index 0000000..bb37306
--- /dev/null
@@ -0,0 +1,373 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- merge_rdf.xslt - Merge Wikidata element properties from its RDF.
+     Copyright (C) 2020, 2021, 2022  Pierre Choffet
+
+     This program is free software: you can redistribute it and/or modify
+     it under the terms of version 3 of the GNU General Public License as
+     published by the Free Software Foundation.
+
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+
+     You should have received a copy of the GNU General Public License
+     along with this program.  If not, see <http://www.gnu.org/licenses/>.
+     -->
+
+<!-- LIMITATIONS:
+  - If WD already has P31, we don't use our value to prevent subclasses to be added
+-->
+<xsl:stylesheet version="1.0" exclude-result-prefixes=""
+                xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+                xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
+                xmlns:schema="http://schema.org/"
+                xmlns:wdef="https://purl.choffet.net/wdef"
+                xmlns:wdt="http://www.wikidata.org/prop/direct/"
+                xmlns:wikibase="http://wikiba.se/ontology#"
+                xmlns:xml="http://www.w3.org/XML/1998/namespace"
+                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+       <xsl:output method="xml" encoding="utf-8" indent="yes" />
+       <xsl:strip-space elements="*" />
+       
+       <xsl:param name="action" select='reduce' />
+       <xsl:param name="rdf-path" />
+       
+       <xsl:variable name="wd-doc" select="document($rdf-path)" />
+       <xsl:key name="wd-description" match="rdf:RDF/rdf:Description" use="@rdf:about" />
+       
+       <xsl:variable name="element-id">
+               <xsl:call-template name="substring-after-last">
+                       <xsl:with-param name="string" select="$wd-doc/rdf:RDF/rdf:Description[1]/@rdf:about" />
+                       <xsl:with-param name="delimiter" select="'/'" />
+               </xsl:call-template>
+       </xsl:variable>
+       <xsl:variable name="wd-resource-prefix" select="'http://www.wikidata.org/entity/'" />
+       <xsl:variable name="wd-resource" select="$wd-doc/rdf:RDF/rdf:Description[@rdf:about = concat($wd-resource-prefix, $element-id)]" />
+       
+       
+       <xsl:template match="@*|node()">
+               <xsl:copy>
+                       <xsl:apply-templates select="@*|node()" />
+               </xsl:copy>
+       </xsl:template>
+       
+       <xsl:template match="/">
+               <xsl:if test="$action != 'reduce'">
+                       <xsl:message terminate="yes">"reduce" is the only available action for now.</xsl:message>
+               </xsl:if>
+               
+               <xsl:apply-templates />
+       </xsl:template>
+       
+       <!-- Take action on WDEF label if wd has any -->
+       <xsl:template match="/wdef:knowledge/wdef:element/wdef:label">
+               <xsl:if test="../@wdef:id != $element-id or not($wd-resource/schema:name[@xml:lang = current()/@xml:lang])">
+                       <xsl:copy-of select="." />
+               </xsl:if>
+       </xsl:template>
+       
+       <!-- Take action on WDEF description if wd has any -->
+       <xsl:template match="/wdef:knowledge/wdef:element/wdef:description">
+               <xsl:if test="../@wdef:id != $element-id or not($wd-resource/schema:description[@xml:lang = current()/@xml:lang])">
+                       <xsl:copy-of select="." />
+               </xsl:if>
+       </xsl:template>
+       
+       <xsl:template match="/wdef:knowledge/wdef:element/wdef:property">
+               <xsl:choose>
+                       <xsl:when test="../@wdef:id != $element-id">
+                               <xsl:copy>
+                                       <xsl:apply-templates select="@*|node()" />
+                               </xsl:copy>
+                       </xsl:when>
+                       <xsl:otherwise>
+                               <xsl:variable name="all-values-have-wd-equivalent">
+                                       <xsl:call-template name="all-values-have-wd-equivalent" />
+                               </xsl:variable>
+                               <xsl:if test="$all-values-have-wd-equivalent = 'no'">
+                                       <xsl:copy>
+                                               <xsl:apply-templates select="@*|node()" />
+                                       </xsl:copy>
+                               </xsl:if>
+                       </xsl:otherwise>
+               </xsl:choose>
+       </xsl:template>
+       
+       <xsl:template match="/wdef:knowledge/wdef:element/wdef:property/wdef:value">
+               <xsl:choose>
+                       <xsl:when test="../../@wdef:id != $element-id">
+                               <xsl:copy-of select="." />
+                       </xsl:when>
+                       <xsl:otherwise>
+                               <xsl:variable name="has-wd-equivalent">
+                                       <xsl:call-template name="value-has-wd-equivalent" />
+                               </xsl:variable>
+                               <xsl:if test="$has-wd-equivalent = 'no'">
+                                       <!-- <xsl:copy-of select="." /> -->
+                                       <xsl:copy>
+                                               <xsl:apply-templates select="@*|node()" />
+                                       </xsl:copy>
+                               </xsl:if>
+                       </xsl:otherwise>
+               </xsl:choose>
+       </xsl:template>
+       
+       <xsl:template match="/wdef:knowledge/wdef:element/wdef:property/wdef:novalue">
+               <xsl:if test="../../@wdef:id != $element-id or not($wd-resource/rdf:type[@rdf:resource = concat('http://www.wikidata.org/prop/novalue/', ../@wdef:pid)])">
+                       <xsl:copy-of select="." />
+               </xsl:if>
+       </xsl:template>
+       
+       <xsl:template match="/wdef:knowledge/wdef:element/wdef:property/wdef:somevalue">
+               <xsl:message terminate="yes">Cannot deal with wdef:somevalue for now</xsl:message>
+       </xsl:template>
+       
+       <xsl:template match="wdef:qualifier">
+               <xsl:variable name="has-wd-equivalent">
+                       <xsl:call-template name="qualifier-has-wd-equivalent" />
+               </xsl:variable>
+               
+               <xsl:if test="$has-wd-equivalent != 'yes'" >
+                       <xsl:copy-of select="." />
+               </xsl:if>
+       </xsl:template>
+       
+       <!-- To be called in a property context -->
+       <xsl:template name="all-values-have-wd-equivalent">
+               <xsl:variable name="all-outputs">
+                       <xsl:for-each select="*">
+                               <xsl:call-template name="value-has-wd-equivalent" />
+                       </xsl:for-each>
+               </xsl:variable>
+               
+               <xsl:choose>
+                       <xsl:when test="contains($all-outputs, 'no')">
+                               <xsl:text>no</xsl:text>
+                       </xsl:when>
+                       <xsl:otherwise>
+                               <xsl:text>yes</xsl:text>
+                       </xsl:otherwise>
+               </xsl:choose>
+       </xsl:template>
+       
+       <!-- To be called in a property context -->
+       <xsl:template name="any-value-has-wd-equivalent">
+               <xsl:variable name="all-outputs">
+                       <xsl:for-each select="*">
+                               <xsl:call-template name="value-has-wd-equivalent" />
+                       </xsl:for-each>
+               </xsl:variable>
+               
+               <xsl:choose>
+                       <xsl:when test="contains($all-outputs, 'yes')">
+                               <xsl:text>yes</xsl:text>
+                       </xsl:when>
+                       <xsl:otherwise>
+                               <xsl:text>no</xsl:text>
+                       </xsl:otherwise>
+               </xsl:choose>
+       </xsl:template>
+       
+       <!-- To be called in a value context -->
+       <xsl:template name="value-has-wd-equivalent">
+               <xsl:variable name="PID" select="../@wdef:pid" />
+               
+               <xsl:choose>
+                       <xsl:when test="wdef:literal">
+                               <xsl:choose>
+                                       <xsl:when test="$wd-resource/*[name(.) = concat('wdt:', $PID) and text() = current()/wdef:literal/text()]">
+                                               <xsl:text>yes</xsl:text>
+                                       </xsl:when>
+                                       <xsl:otherwise>
+                                               <xsl:text>no</xsl:text>
+                                       </xsl:otherwise>
+                               </xsl:choose>
+                       </xsl:when>
+                       <xsl:when test="wdef:ref-element">
+                               <xsl:choose>
+                                       <!-- If WD already has P31, we take action on our value to prevent subclasses to be added -->
+                                       <xsl:when test="($PID = 'P31' and $wd-resource/*[name(.) = 'wdt:P31']) or ($wd-resource/*[name(.) = concat('wdt:', $PID) and @rdf:resource = concat($wd-resource-prefix, current()/wdef:ref-element)])">
+                                               <xsl:text>yes</xsl:text>
+                                       </xsl:when>
+                                       <xsl:otherwise>
+                                               <xsl:text>no</xsl:text>
+                                       </xsl:otherwise>
+                               </xsl:choose>
+                       </xsl:when>
+                       <xsl:when test="wdef:translation">
+                               <xsl:choose>
+                                       <xsl:when test="$wd-resource/*[name(.) = concat('wdt:', $PID) and @xml:lang = current()/wdef:translation/@xml:lang and text() = current()/wdef:translation/text()]">
+                                               <xsl:text>yes</xsl:text>
+                                       </xsl:when>
+                                       <xsl:otherwise>
+                                               <xsl:text>no</xsl:text>
+                                       </xsl:otherwise>
+                               </xsl:choose>
+                       </xsl:when>
+                       <xsl:when test="wdef:quantity">
+                               <xsl:variable name="wd-quantity-description" select="$wd-doc/rdf:RDF/rdf:Description[@rdf:about = $wd-doc/rdf:RDF/rdf:Description/*[name(.) = concat('psv:', $PID)]/@rdf:resource]" />
+                               
+                               <xsl:choose>
+                                       <xsl:when test="not($wd-quantity-description)">
+                                               <xsl:text>no</xsl:text>
+                                       </xsl:when>
+                                       <xsl:otherwise>
+                                               <xsl:variable name="quantity-wdef-format">
+                                                       <xsl:if test="substring(wdef:quantity, 1, 1) != '-' and substring(wdef:quantity, 1, 1) != '+'">
+                                                               <xsl:text>+</xsl:text>
+                                                       </xsl:if>
+                                                       <xsl:value-of select="wdef:quantity" />
+                                               </xsl:variable>
+                                               
+                                               <xsl:choose>
+                                                       <xsl:when test="$wd-quantity-description/wikibase:quantityAmount = $quantity-wdef-format and $wd-quantity-description/wikibase:quantityUnit[@rdf:resource = concat($wd-resource-prefix, current()/wdef:quantity/@wdef:unit)]">
+                                                               <xsl:text>yes</xsl:text>
+                                                       </xsl:when>
+                                                       <xsl:otherwise>
+                                                               <xsl:text>no</xsl:text>
+                                                       </xsl:otherwise>
+                                               </xsl:choose>
+                                       </xsl:otherwise>
+                               </xsl:choose>
+                       </xsl:when>
+                       <xsl:when test="wdef:time">
+                               <!-- Generate wikidata date format -->
+                               <xsl:variable name="wd-date-description" select="$wd-doc/rdf:RDF/rdf:Description[@rdf:about = $wd-doc/rdf:RDF/rdf:Description/*[name(.) = concat('psv:', $PID)]/@rdf:resource]" />
+                               
+                               <xsl:choose>
+                                       <xsl:when test="not($wd-date-description)">
+                                               <xsl:text>no</xsl:text>
+                                       </xsl:when>
+                                       <xsl:otherwise>
+                                               <xsl:variable name="date-wd-format">
+                                                       <xsl:choose>
+                                                               <xsl:when test="$wd-date-description/wikibase:timePrecision = 9">
+                                                                       <xsl:value-of select="substring($wd-date-description/wikibase:timeValue, 1, 4)" />
+                                                               </xsl:when>
+                                                               <xsl:when test="$wd-date-description/wikibase:timePrecision = 10">
+                                                                       <xsl:value-of select="substring($wd-date-description/wikibase:timeValue, 1, 7)" />
+                                                               </xsl:when>
+                                                               <xsl:when test="$wd-date-description/wikibase:timePrecision = 11">
+                                                                       <xsl:value-of select="substring($wd-date-description/wikibase:timeValue, 1, 10)" />
+                                                               </xsl:when>
+                                                               <xsl:otherwise>
+                                                                       <xsl:message terminate="yes">Can only deal with precision between 9 and 11</xsl:message>
+                                                               </xsl:otherwise>
+                                                       </xsl:choose>
+                                               </xsl:variable>
+                                               <xsl:variable name="date-wdef-format">
+                                                       <xsl:choose>
+                                                               <xsl:when test="wdef:time/@wdef:precision = 9">
+                                                                       <xsl:value-of select="substring(wdef:time, 2, 4)" />
+                                                               </xsl:when>
+                                                               <xsl:when test="wdef:time/@wdef:precision = 10">
+                                                                       <xsl:value-of select="substring(wdef:time, 2, 7)" />
+                                                               </xsl:when>
+                                                               <xsl:when test="wdef:time/@wdef:precision = 11">
+                                                                       <xsl:value-of select="substring(wdef:time, 2, 10)" />
+                                                               </xsl:when>
+                                                               <xsl:otherwise>
+                                                                       <xsl:message terminate="yes">Can only deal with precision between 9 and 11</xsl:message>
+                                                               </xsl:otherwise>
+                                                       </xsl:choose>
+                                               </xsl:variable>
+                                               <xsl:variable name="wd-time-at-least-precise">
+                                                       <xsl:choose>
+                                                               <xsl:when test="string-length($date-wd-format) &gt;= string-length($date-wdef-format)">
+                                                                       <xsl:text>yes</xsl:text>
+                                                               </xsl:when>
+                                                               <xsl:otherwise>
+                                                                       <xsl:text>no</xsl:text>
+                                                               </xsl:otherwise>
+                                                       </xsl:choose>
+                                               </xsl:variable>
+                                               <xsl:variable name="wd-time-compatible">
+                                                       <xsl:choose>
+                                                               <xsl:when test="$wd-time-at-least-precise = 'yes' and substring($date-wd-format, 1, string-length($date-wdef-format)) = $date-wdef-format">
+                                                                       <xsl:text>yes</xsl:text>
+                                                               </xsl:when>
+                                                               <xsl:otherwise>
+                                                                       <xsl:text>no</xsl:text>
+                                                               </xsl:otherwise>
+                                                       </xsl:choose>
+                                               </xsl:variable>
+                                               
+                                               <xsl:if test="not($wd-date-description/wikibase:timeCalendarModel[@rdf:resource = concat($wd-resource-prefix, 'Q1985727')])">
+                                                       <xsl:message terminate="yes">Can only deal with gregorian calendar for now</xsl:message>
+                                               </xsl:if>
+                                               
+                                               <xsl:choose>
+                                                       <!-- Return true if wd is the same time, at least as precise as wdef -->
+                                                       <xsl:when test="$wd-time-at-least-precise = 'yes' and $wd-time-compatible = 'yes'">
+                                                               <xsl:text>yes</xsl:text>
+                                                       </xsl:when>
+                                                       <xsl:otherwise>
+                                                               <xsl:if test="$wd-time-at-least-precise = 'no' or $wd-time-compatible = 'no'">
+                                                                       <xsl:message terminate="yes">WD has time data but incompatible or less precise. We cannot deal with that for now.</xsl:message>
+                                                               </xsl:if>
+                                                               <xsl:text>no</xsl:text>
+                                                       </xsl:otherwise>
+                                               </xsl:choose>
+                                       </xsl:otherwise>
+                               </xsl:choose>
+                       </xsl:when>
+                       <xsl:when test="wdef:qualifier">
+                               <xsl:call-template name="all-qualifiers-have-wd-equivalent" />
+                       </xsl:when>
+                       <xsl:when test="count(*) > 1">
+                               <xsl:text>no</xsl:text>
+                               <xsl:message terminate="yes">cannot deal with more than one value for now</xsl:message>
+                       </xsl:when>
+                       <xsl:otherwise>
+                               <xsl:text>no</xsl:text>
+                       </xsl:otherwise>
+               </xsl:choose>
+       </xsl:template>
+       
+       <xsl:template name="qualifier-has-wd-equivalent">
+               <!-- WARNING: Not extensively tested for now -->
+               <xsl:choose>
+                       <xsl:when test="substring(../@wdef:id, 1, 1) = '?' or not($wd-doc/rdf:RDF/rdf:Description[@rdf:about = ../@wdef:id] and *[name(.) = concat('pq:', wdef:property/@wdef:pid) and @rdf:resource = concat($wd-resource-prefix, wdef:property/wdef:value/wdef:ref-element)])">
+                               <xsl:text>no</xsl:text>
+                       </xsl:when>
+                       <xsl:otherwise>
+                               <xsl:text>yes</xsl:text>
+                       </xsl:otherwise>
+               </xsl:choose>
+       </xsl:template>
+       
+       <xsl:template name="all-qualifiers-have-wd-equivalent">
+               <xsl:variable name="all-outputs">
+                       <xsl:for-each select="wdef:qualifier">
+                               <xsl:call-template name="qualifier-has-wd-equivalent" />
+                       </xsl:for-each>
+               </xsl:variable>
+               
+               <xsl:choose>
+                       <xsl:when test="contains($all-outputs, 'no')">
+                               <xsl:text>no</xsl:text>
+                       </xsl:when>
+                       <xsl:otherwise>
+                               <xsl:text>yes</xsl:text>
+                       </xsl:otherwise>
+               </xsl:choose>
+       </xsl:template>
+       
+       <xsl:template name="substring-after-last">
+               <xsl:param name="string" />
+               <xsl:param name="delimiter" />
+               
+               <xsl:choose>
+                       <xsl:when test="contains($string, $delimiter)">
+                               <xsl:call-template name="substring-after-last">
+                                       <xsl:with-param name="string" select="substring-after($string, $delimiter)" />
+                                       <xsl:with-param name="delimiter" select="$delimiter" />
+                               </xsl:call-template>
+                       </xsl:when>
+                       <xsl:otherwise>
+                               <xsl:value-of select="$string" />
+                       </xsl:otherwise>
+               </xsl:choose>
+       </xsl:template>
+</xsl:stylesheet>