Comment puis-je générer une table des matières en XSLT?
Eric van der Vlist, Dyomedea (vdv@dyomedea.com).
jeudi 13 avril 2006
Table des matières
Introduction
Principes généraux
Génération de la table des matières
Structure générale
Génération de liens
Numérotation des sections
Génération d'identifiants lisibles et stables
Références
Introduction
Une difficulté peut en cacher une autre et au problème de création de la table des matières s'ajoute fréquemment celui de la structuration du document source lorsqu'il utilise un format n'imposant pas une structuration forte tel que XHTML ou NITF.
La structuration du document source est couvert par un autre article et nous ne nous intéresserons ici qu'au problème de la création de la table des matières à partir d'un document déjà structuré tel qu'un document DocBook.
Nous utiliserons dans cet article le document DocBook accessible à l'adresse « http://www.docbook.org/specs/cs-docbook-simple-1.1.xml » comme exemple.
Principes généraux
Une transformation XSLT produit un arbre résultat à partir d'un arbre source. Pour cela, elle accède en lecture à l'arbre source et en écriture à l'arbre résultat.
Par analogie aux méthodes d'accès à un fichier, l'accès à l'arbre source peut être qualifié d'accès aléatoire dans la mesure où la transformation peut accéder à tout fragment de l'arbre source au moyen du langage XPath.
Au contraire, l'accès à l'arbre résultat est un accès en mode « append » dans la mesure où la transformation ne peut que rajouter des noeuds à un point d'insertion courant que l'on ne peut pas remonter.
Ces modes d'accès conditionnent la manière dont nous allons devoir constituer notre table des matières : puisque nous ne maîtrisons pas le point d'insertion, nous allons devoir effectuer un parcours du document source spécifique à la génération de la table des matières.
Pour différencier ce parcours nous utiliserons un « mode » XSLT particulier.
La structure de notre transformation ressemblera donc à :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="/">
<html>
<head>
<!--
Création de l'entête du document
-->
</head>
<body>
<!--
Génération de contenu avant la table des mati
ères
-->
<div class="tableDesMatières">
<h1>Table des matières</h1>
<ul>
<xsl:apply-templates select="/*/section"
mode="tableDesMatières"/>
</ul>
</div>
<!--
Génération de contenu après la table des mati
ères
-->
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Génération de la table des matières
Structure générale
La structure du template qui génère la table des matière est assez simple :
<xsl:template match="section" mode="tableDesMatières">
<li>
<p>
<xsl:value-of select="title"/>
</p>
<xsl:if test="section">
<ul>
<xsl:apply-templates select="section"
mode="tableDesMatières"/>
</ul>
</xsl:if>
</li>
</xsl:template>
Génération de liens
Les choses se compliquent si l'on veut générer des liens vers la partie du document généré où la section elle-même sera affichée.
Pour cela, il va falloir générer une ancre lors de l'affichage de la section et un lien vers cette ancre dans la table des matières, le problème étant de calculer la valeur de cette ancre de la même manière lors des deux parcours.
La solution la plus simple est d'utiliser la fonction XPath « generate-id() ». Cette fonction génère un identifiant unique pour chaque noeud d'un document XML et il suffit de l'utiliser de la même manière lors de la définition et lors de la génération du lien :
<xsl:template match="section" mode="tableDesMatières">
<li>
<p>
<a href="#{generate-id()}" title="{title}">
<xsl:value-of select="title"/>
</a>
</p>
<xsl:if test="section">
<ul>
<xsl:apply-templates select="section" mode="t
ableDesMatières"/>
</ul>
</xsl:if>
</li>
</xsl:template>
<xsl:template match="section">
<!--
Ce template affiche la section
-->
<div id="{generate-id()}">
<xsl:apply-templates/>
</div>
</xsl:template>
Cette solution fonctionne mais elle présente deux inconvénients:
- Le format de ces identifiants n'est pas lisibles et dépend de l'implémentation utilisée. Dans le cas de Saxon, on obtient des valeurs du type « #d0e107 », Xalan génère des valeurs du type « #N10076 » et d'autres processeurs génèrent des valeurs ayant des formats totalement différents.
- La recommandation XSLT 1.0 spécifie explicitement que ces valeurs peuvent changer entre deux exécutions d'une même transformation : « Une implémentation n'est pas obligée de générer les mêmes identificateurs chaque fois qu'un document subi une transformation ».
Les URLs ainsi générés sont donc instables ce qui est une mauvaise pratique et il est préférable de constituer ses propres identifiants de manière à leur assurer une meilleur stabilité.
Il y a beaucoup de méthodes pour cela et nous verrons comment tirer profit de la numérotation des sections que nous allons maintenant mettre en place pour générer nos identifiants.
Numérotation des sections
Pour numéroter les sections, nous allons simplement compter, pour chaque niveau, le nombre de sections « soeurs » précédent la section courante et ajouter un « . » après ce numéro.
Cela se fait très simplement au moyen du template suivant :
<xsl:template match="section" mode="numérotation">
<xsl:value-of select="count(preceding-sibling::section) +
1"/>
<xsl:text>.</xsl:text>
</xsl:template>
L'utilisation de ce template se fait au moyen d'une instruction « xsl:apply-templates » en utilisant l'axe « ancestor-or-self » et notre template de génération de table des matière devient :
<xsl:template match="section" mode="tableDesMatières">
<xsl:variable name="numéro">
<xsl:apply-templates select="ancestor-or-self::sectio
n" mode="numérotation"/>
</xsl:variable>
<li>
<p>
<a href="#{generate-id()}" title="{title}">
<xsl:value-of select="$numéro"/>
<xsl:text> </xsl:text>
<xsl:value-of select="title"/>
</a>
</p>
<xsl:if test="section">
<ul>
<xsl:apply-templates select="section" mode="t
ableDesMatières"/>
</ul>
</xsl:if>
</li>
</xsl:template>
Génération d'identifiants lisibles et stables
Nous pouvons maintenant utiliser ces numéros comme identifiants à condition de les préfixer (les identifiants ne doivent pas commencer par un chiffre).
Après cette modification, nos templates deviennent :
<xsl:template match="section" mode="tableDesMatières">
<xsl:variable name="numéro">
<xsl:apply-templates select="ancestor-or-self::sectio
n" mode="numérotation"/>
</xsl:variable>
<li>
<p>
<a href="#section_{$numéro}" title="{title}">
<xsl:value-of select="$numéro"/>
<xsl:text> </xsl:text>
<xsl:value-of select="title"/>
</a>
</p>
<xsl:if test="section">
<ul>
<xsl:apply-templates select="section" mode="t
ableDesMatières"/>
</ul>
</xsl:if>
</li>
</xsl:template>
<xsl:template match="section">
<!--
Ce template affiche la section
-->
<xsl:variable name="numéro">
<xsl:apply-templates select="ancestor-or-self::sectio
n" mode="numérotation"/>
</xsl:variable>
<div id="section_{$numéro}">
<xsl:apply-templates/>
</div>
</xsl:template>
Les identifiants ainsi générés sont du type : « section_3.1. ». Ils sont à la fois lisibles et aussi stables que possible dans la mesure où ils ne changent que lorsque la structure du document source est modifiée.
La structure complète de la transformation (à améliorer et adapter à votre contexte) est la suivante :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="/">
<html>
<head>
<!--
Création de l'entête du document
-->
</head>
<body>
<!--
Génération de contenu avant la table des mati
ères
-->
<div class="tableDesMatières">
<h1>Table des matières</h1>
<ul>
<xsl:apply-templates select="/*/section"
mode="tableDesMatières"/>
</ul>
</div>
<!--
Génération de contenu après la table des mati
ères
-->
<xsl:apply-templates select="/*/section"/>
</body>
</html>
</xsl:template>
<xsl:template match="section" mode="numérotation">
<xsl:value-of select="count(preceding-sibling::section) +
1"/>
<xsl:text>.</xsl:text>
</xsl:template>
<xsl:template match="section" mode="tableDesMatières">
<xsl:variable name="numéro">
<xsl:apply-templates select="ancestor-or-self::sectio
n" mode="numérotation"/>
</xsl:variable>
<li>
<p>
<a href="#section_{$numéro}" title="{title}">
<xsl:value-of select="$numéro"/>
<xsl:text> </xsl:text>
<xsl:value-of select="title"/>
</a>
</p>
<xsl:if test="section">
<ul>
<xsl:apply-templates select="section" mode="t
ableDesMatières"/>
</ul>
</xsl:if>
</li>
</xsl:template>
<xsl:template match="section">
<!--
Ce template affiche la section
-->
<xsl:variable name="numéro">
<xsl:apply-templates select="ancestor-or-self::sectio
n" mode="numérotation"/>
</xsl:variable>
<div id="section_{$numéro}">
<xsl:apply-templates/>
</div>
</xsl:template>
<xsl:template match="title">
<xsl:variable name="numéro">
<xsl:apply-templates select="ancestor-or-self::sectio
n" mode="numérotation"/>
</xsl:variable>
<p>
<xsl:value-of select="$numéro"/>
<xsl:text> </xsl:text>
<xsl:value-of select="."/>
</p>
</xsl:template>
<xsl:template match="*">
<p>
<xsl:value-of select="."/>
</p>
</xsl:template>
</xsl:stylesheet>
Références
-
Structurer un document HTML avec XSLT
-
Recommandation XSLT 1.0
-
Recommandation XPath 1.0
Copyright 2006, Eric van der Vlist
|