Comment transformer en XML un nœud texte dans lequel les caractères de balisages sont représentés par des références à des entités prédéfinies (« & », « < », « > », ...)?
Eric van der Vlist, Dyomedea (vdv@dyomedea.com).
mercredi 11 octobre 2006
Table des matières
Introduction
Utilisation de disable-output-escaping="yes"
Facile à mettre en œuvre
Mais pas universelle
Et limitée
Contournements
Côté serveur
Côté client
Conclusion
Introduction
Certains vocabulaires XML tels que RSS 1.0 permettent d'encoder des fragments XHTML (ou HTML) sous forme de texte. C'est le cas par exemple dans le fragment de flux RSS 1.0 suivant (extrait du canal RSS du carnet Web de Karl Dubost publié sous licence CC by-nc-sa) :
<content:encoded><p xmlns="http://www.w3.org/1999/xhtml"
class="resume">L'<a href="http://www.w3.org/QA/"
shape="rect">activité QA du W3C</a> vient de lancer un
e
première version de Unicorn. C'est un nouvel outil de
communication pour faciliter le processus de vérification de qu
alité
d'un site Web. Le système fonctionne comme une gare de triage.
Le produit accepte des ressources, les envoie à des observateur
s
(validateurs par exemple), collecte les résultats et renvoie le
résultat compilé. Le tout fonctionne grâce à des services Web e
t
donc il n'est pas nécessaire de développer un module pour
unicorn, mais plutôt le service Web qui permettra de communique
r
avec ce dernier. Le projet est open source et il est ouvert à
participation de tous.</p><p
xmlns="http://www.w3.org/1999/xhtml" class="cat">W3C,
validateur, qualité, QA</p></content:encoded>
Comment traiter ce type de contenu avec XSLT pour générer une page (X)HTML?
Utilisation de disable-output-escaping="yes"
Facile à mettre en œuvre
La première solution pour traiter ce type de contenu est d'utiliser l'attribut disable-output-escaping="yes".
L'attribut disable-output-escaping peut être placé sur les élements xsl:text et xsl:value-of pour demander à un processeur XSLT de ne pas remplacer les caractères de balisage par des références à des entités prédéfinies. Dans notre cas, il suffit donc d'écrire :
<xsl:value-of select="content:encoded" disable-output-escaping="y
es"/>
[encodage-preservespace.xsl] pour que l'élément en question soit « décodé » et transformé en :
<p xmlns="http://www.w3.org/1999/xhtml" class="resume">L'<a
href="http://www.w3.org/QA/" shape="rect">activité QA du
W3C</a> vient de lancer une première version de Unicorn.
C'est un nouvel outil de communication pour faciliter le
processus de vérification de qualité d'un site Web. Le
système fonctionne comme une gare de triage. Le produit
accepte des ressources, les envoie à des observateurs
(validateurs par exemple), collecte les résultats et renvoie
le résultat compilé. Le tout fonctionne grâce à des services
Web et donc il n'est pas nécessaire de développer un module
pour unicorn, mais plutôt le service Web qui permettra de
communiquer avec ce dernier. Le projet est open source et il
est ouvert à participation de tous.</p>
<p xmlns="http://www.w3.org/1999/xhtml" class="cat">W3C,
validateur, qualité, QA</p>
Mais pas universelle
Cette solution pose hélas un certain nombre de problèmes. Cette fonctionnalité a été introduite comme à regret dans XSLT 1.0 et la recommandation écrit :
"Désactiver la production littérale des caractères sur un nœud textuel utilisé pour autre chose (qu'un nœud textuel) dans l'arbre de sortie est une erreur. Aussi, désactiver la production littérale sur l'élément xsl:value-of ou xsl:text qui est utilisé pour produire un commentaire, une instruction de traitement ou un nœud d'attribut est une erreur; c'est également une erreur que de convertir un fragment de l'arbre résultat en un nombre ou une chaîne si ce fragment contient un nœud textuel pour lequel la production littérale a été désactivée. Dans les deux cas, un processeur XSLT peut signaler une erreur; s'il ne le fait pas, il doit la rattraper en ignorant l'attribut disable-output-escaping."
Dans notre cas, nous générons des nœuds de type élément et texte ainsi que des attributs en utilisant cette fonctionnalités. Suivant l'interprétation de ce paragraphe, un processeur XSLT est en droit de considérer que c'est une erreur et soit générer une erreur soit simplement ignorer notre attribut disable-output-escaping.
Les versions récentes de Saxon, Xalan, libxslt et MSXML ont une lecture plus libérale de la recommandation et réagissent comme nous le souhaitons. Par contre les processeurs XSLT de Firefox et Opera ignorent silencieusement l'attribut disable-output-escaping et cette méthode ne peut donc pas être utilisée de manière fiable dans des transformations effectuées dans les navigateurs Web.
Et limitée
Cette méthode permet uniquement de recopier les nœuds dans le document résultat et elle ne permet pas d'y accéder pour les modifier ou rechercher des informations. Ainsi par exemple, dans le canal RSS de Karl Dubost, les images utilisent des URIs relatives, de la forme :
<img src="/2006/09/10-cafe" alt="Montage de photos prises dans un
café"/>
Si l'on ne fait rien pour résoudre ce problème, les images ne seront donc pas visibles sur la page XHTML que nous générons. On peut certes utiliser l'élément XHTML base et préciser que l'URI de base est la page d'accueil du carnet de Karl Dubost mais cela impose de préciser les adresses de toutes les autres ressources de manière absolue et cela ne serait pas généralisable à une page qui réunirait plusieurs canaux RSS posant le même problème. On peut donc préférer au contraire transformer les adresses des images en adresses relatives. Ce serait facile si le contenu était exprimé en XML, c'est beaucoup plus complexe s'il est encodé comme c'est le cas ici.
Contournements
Les solutions de contournement sont différentes suivant que l'on se trouve sur un serveur ou dans un navigateur.
Côté serveur
La technique côté serveur consiste à écrire les fragments à décoder dans des documents séparés en utilisant la méthode de sortie « text » pour éviter que les caractères de balisage ne soient encodés. Cette écriture pourra se faire en utilisant une extension XSLT 1.0 ou en XSLT 2.0 en utilisant l'instruction xsl:result-document. Les fragments sont ensuite lus en tant que documents XML avec la fonction document() et peuvent être manipulés comme on le souhaite.
Pour modifier les adresses des images, on pourra par exemple écrire :
<xsl:template match="rdf:li">
<xsl:for-each select="key('items', @rdf:resource)">
<div>
<h2>
<xsl:value-of select="rss:title"/>
</h2>
<xsl:variable name="tmpfile" select="concat('file:///tmp/
fragment', generate-id(), '.xml')"/>
<exslt:document href="{$tmpfile}" method="text" encoding=
"utf-8">
<xsl:text><![CDATA[<root>]]></xsl:text>
<xsl:value-of select="content:encoded"/>
<xsl:text><![CDATA[</root>]]></xsl:text>
</exslt:document>
<xsl:apply-templates select="document($tmpfile)/root/*" m
ode="content"/>
</div>
</xsl:for-each>
</xsl:template>
<xsl:template match="@*|node()" mode="content">
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="content"/>
</xsl:copy>
</xsl:template>
<xsl:template match="@src[starts-with(., '/')]" mode="content">
<xsl:attribute name="href">
<xsl:value-of select="concat('http://www.la-grange.net', .)
"/>
</xsl:attribute>
</xsl:template>
[encodage-document.xsl]
On notera que les noms de fichiers temporaires ne doivent pas être réutilisés dans une transformation. En effet, la plupart des processeurs XSLT « cachent » la lecture des documents externes et risquent de réutiliser la première version du document à la place des versions suivantes.
Côté client
Lorsque la transformation s'exécute côté client dans un navigateur, pour des raisons de sécurité évidentes, la transformation ne peut pas enregistrer de documents. La méthode précédente n'est donc pas applicable. Par contre, le document généré peut utiliser JavaScript pour se modifier. Le contournement consistera donc à générer un document HTML dans lequel le contenu ne sera pas nécessairement décodé et à effectuer le décodage en JavaScript.
La transformation XSLT pourra être :
<xsl:template match="rdf:li">
<xsl:for-each select="key('items', @rdf:resource)">
<div>
<h2>
<xsl:value-of select="rss:title"/>
</h2>
<div class="encoded">
<xsl:value-of select="content:encoded"/>
</div>
</div>
</xsl:for-each>
</xsl:template>
[encodage-js.xsl]
L'attribut « class » permet d'identifier les éléments qu'il faudra décoder en JavaScript.
La fonction JavaScript qui permet de décoder ces éléments div est la suivante :
function decode () {
var divs = document.getElementsByTagName('div');
for(var i = 0; i < divs.length; i++) {
var div = divs[i];
if (div.className == "encoded") {
div.innerHTML = div.firstChild.nodeValue;
changeBaseUrl(div);
}
}
}
[encodage.js]
Cette fonction boucle sur tout les éléments div, et décode ceux dont la classe est « encoded ». Le décodage lui-même se fait en copiant le nœud texte dans la propriété « innerHTML » de l'élément, ce qui a comme effet de parser le texte comme si c'était un fragment de HTML.
La fonction appelle ensuite la fonction changeBaseUrl pour modifier les URIs des images. Cette fonction est la suivante :
function changeBaseUrl(e) {
if (e.nodeName.toLowerCase() == "img") {
var src = e.src;
e.src = "/";
if (src.indexOf(e.src) == 0) {
e.src = "http://www.la-grange.net/" + src.substring(e.src.l
ength);
} else {
e.src = src;
}
} else {
if (e.hasChildNodes)
for (var i = 0; i < e.childNodes.length; i++ ) {
changeBaseUrl(e.childNodes[i]);
}
}
}
[encodage.js]
Elle boucle de manière récursive sur tout les nœuds enfants et lorsqu'elle rencontre une image, elle fait de son mieux pour tester si son adresse est relative. Internet Explorer convertit systématiquement les URIs relatives en URIs absolues, que ce soit dans la propriété src ou dans l'attribut correspondant et le principe du test consiste à regarder si l'URI de l'image commence de la même manière que l'URI relative « / » convertie en URI absolue.
Lorsque c'est le cas, l'URI est modifiée pour pointer sur le carnet Web de Karl Dubost.
Cette transformation et le script qui l'accompagne ont été testés sur Firefox, Opera et Internet Explorer.
Conclusion
Manipuler des fragments XML encodés sous forme de texte (au moyen de sections CDATA ou en utilisant les entités prédéfinies) en XSLT n'est pas possible de manière fiable et générique.
Il existe néanmoins des solutions de contournement dont le choix est guidé par le fait que l'on doive ou non accéder au contenu des fragments XML, que l'on dispose d'un processeur XSLT qui accepte l'attribut disable-output-escaping="yes" dans ce contexte et que l'on exécute la transformation côté client ou côté serveur.
Ces solutions de contournement restent cependant lourdes à mettre en œuvre et il est préférable d'éviter d'utiliser des documents XML contenant des fragments XML encodés sous forme de texte.
Dans le cas de figure de RSS 1.0, il est possible pour cela d'utiliser l'attribut rdf:parseType= ce qui permet d'éviter totalement le problème :
<rss:description rdf:parseType="literal"
xmlns="http://www.w3.org/1999/xhtml">
<p class="resume">L'<a href="http://www.w3.org/QA/"
shape="rect">activité QA du W3C</a> vient
de lancer une première version de Unicorn.
.../...</p>
<p class="cat">W3C, validateur, qualité, QA</p>
</rss:description>
Atom offre également la possibilité d'inclure des fragments XHTML sans encodage mais RSS 2.0 ne le permet pas.
Les exemples sont téléchargeables : [archive]
Copyright 2006, Eric van der Vlist
|