Tronquer XML avec XSLT
-
22-08-2019 - |
Question
J'ai une question pour les gens intelligents de la communauté SO.
Ci-dessous un extrait de XML généré par le Symphony CMS .
<news>
<entry>
<title>Lorem Ipsum</title>
<body>
<p><strong>Lorem Ipsum</strong></p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed malesuada auctor magna. Vivamus urna justo, pulvinar nec, sagittis malesuada, accumsan in, massa. Quisque mi purus, gravida eget, ultricies a, porta in, sem. Maecenas justo elit, elementum vel, feugiat vulputate, pulvinar nec, velit. Fusce vel ante et diam bibendum euismod. Nunc vel nulla non lorem dignissim placerat. Nulla magna massa, auctor et, tempor nec, auctor sit amet, turpis. Quisque odio lacus, auctor at, posuere id, suscipit eget, dui. Phasellus aliquam. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin varius. Phasellus cursus. Cras mattis adipiscing turpis. Sed.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed malesuada auctor magna.</p>
</body>
</entry>
</news>
Ce que je dois faire est de prendre une partie de l'élément de <body>
, sur la base d'une longueur spécifiée, pour l'affichage dans le style de blog:
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed Malesuada auctor magna. vivamus Urna justo, nec pulvinar, sagittis Malesuada, accumsan dans, massa. quisque mi Purus, primigeste eget, ultricies a, Porta, ... ETM Plus
... où Plus est un lien vers l'article complet de nouvelles. Je sais que je peux choisir certains paragraphes et je sais aussi que je peux utiliser la fonction de sous-chaîne pour apporter un certain nombre de caractères. Cependant, je dois conserver la mise en forme du texte, à savoir les balises HTML dans l'élément <body>
.
Je sais que cela pose des problèmes de fermeture de l'étiquette, mais il doit sûrement être un moyen. Espérons que quelqu'un de plus expérimenté avec XSLT peut faire la lumière sur cette question.
La solution
Voici ma version. Je l'ai testé sur votre échantillon XML et il fonctionne.
Pour appeler, utilisez <xsl:apply-templates select="path/to/body/*" mode="truncate"/>
.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<!-- limit: the truncation limit -->
<xsl:variable name="limit" select="250"/>
<!-- t: Total number of characters in the set -->
<xsl:variable name="t" select="string-length(normalize-space(//body))"/>
<xsl:template match="*" mode="truncate">
<xsl:variable name="preceding-strings">
<xsl:copy-of select="preceding::text()[ancestor::body]"/>
</xsl:variable>
<!-- p: number of characters up to the current node -->
<xsl:variable name="p" select="string-length(normalize-space($preceding-strings))"/>
<xsl:if test="$p < $limit">
<xsl:element name="{name()}">
<xsl:apply-templates select="@*" mode="truncate"/>
<xsl:apply-templates mode="truncate"/>
</xsl:element>
</xsl:if>
</xsl:template>
<xsl:template match="text()" mode="truncate">
<xsl:variable name="preceding-strings">
<xsl:copy-of select="preceding::text()[ancestor::body]"/>
</xsl:variable>
<!-- p: number of characters up to the current node -->
<xsl:variable name="p" select="string-length(normalize-space($preceding-strings))"/>
<!-- c: number of characters including current node -->
<xsl:variable name="c" select="$p + string-length(.)"/>
<xsl:choose>
<xsl:when test="$limit <= $c">
<xsl:value-of select="substring(., 1, ($limit - $p))"/>
<xsl:text>…</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="@*" mode="truncate">
<xsl:attribute name="{name(.)}"><xsl:value-of select="."/></xsl:attribute>
</xsl:template>
</xsl:stylesheet>
Autres conseils
Voici une transformation XSLT complète 1.0 qui résout exactement le problème.
Cette transformation XSLT:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
xmlns:f="http://fxsl.sf.net/"
xmlns:myAdd="f:myAdd"
xmlns:myParam="f:myParam"
exclude-result-prefixes="ext f myAdd myParam"
>
<xsl:import href="scanl.xsl"/>
<!-- -->
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- -->
<myAdd:myAdd/>
<myParam:myParam>0</myParam:myParam>
<!-- -->
<xsl:param name="pTruncateLength" select="772"/>
<!-- -->
<xsl:variable name="vFun" select="document('')/*/myAdd:*[1]"/>
<xsl:variable name="vZero" select="document('')/*/myParam:*[1]"/>
<!-- -->
<xsl:variable name="vrtfScanResults">
<xsl:call-template name="scanl">
<xsl:with-param name="pFun" select="$vFun"/>
<xsl:with-param name="pQ0" select="$vZero" />
<xsl:with-param name="pList" select="/*/*/body//text()"/>
</xsl:call-template>
</xsl:variable>
<!-- -->
<xsl:variable name="vScanResults"
select="ext:node-set($vrtfScanResults)"/>
<xsl:variable name="vindNode" select=
"count($vScanResults/*[. > $pTruncateLength][1]
/preceding-sibling::*)"/>
<!-- -->
<xsl:variable name="vrtfTruncInfo">
<xsl:for-each select="/*/*/body//text()">
<!-- -->
<xsl:variable name="vPos" select="position()"/>
<tNode id="{generate-id()}">
<xsl:attribute name="preserve">
<xsl:if test="$vPos < $vindNode">
<xsl:value-of select="string-length(.)"/>
</xsl:if>
<xsl:if test="$vPos > $vindNode">
<xsl:value-of select="0"/>
</xsl:if>
<xsl:if test="$vPos = $vindNode">
<xsl:value-of select=
"$vScanResults/*[$vindNode+1]
-
$pTruncateLength"/>
</xsl:if>
</xsl:attribute>
</tNode>
</xsl:for-each>
</xsl:variable>
<!-- -->
<xsl:variable name="vTruncInfo" select="ext:node-set($vrtfTruncInfo)"/>
<!-- -->
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<!-- -->
<xsl:template match="text()[ancestor::body]">
<xsl:variable name="vAllowedLength"
select="$vTruncInfo/*[@id = generate-id(current())]/@preserve"
/>
<!-- -->
<xsl:value-of select="substring(.,1,$vAllowedLength)"/>
<xsl:if test="string-length(.) > $vAllowedLength
and
$vAllowedLength > 0
">
<strong> ...more</strong>
</xsl:if>
</xsl:template>
<!-- -->
<xsl:template match="myAdd:*" mode="f:FXSL">
<xsl:param name="pArg1"/>
<xsl:param name="pArg2"/>
<xsl:value-of select="$pArg1 + string-length($pArg2)"/>
</xsl:template>
</xsl:stylesheet>
lorsqu'il est appliqué sur le document XML source originale :
<news>
<entry>
<title>Lorem Ipsum</title>
<body>
<p>
<strong>Lorem Ipsum</strong>
</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed malesuada auctor magna. Vivamus urna justo, pulvinar nec, sagittis malesuada, accumsan in, massa. Quisque mi purus, gravida eget, ultricies a, porta in, sem. Maecenas justo elit, elementum vel, feugiat vulputate, pulvinar nec, velit. Fusce vel ante et diam bibendum euismod. Nunc vel nulla non lorem dignissim placerat. Nulla magna massa, auctor et, tempor nec, auctor sit amet, turpis. Quisque odio lacus, auctor at, posuere id, suscipit eget, dui. Phasellus aliquam. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin varius. Phasellus cursus. Cras mattis adipiscing turpis. Sed.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed malesuada auctor magna.</p>
<p>This text should not be displayed</p>
</body>
</entry>
</news>
produit le résultat souhaité :
<news>
<entry>
<title>Lorem Ipsum</title>
<body>
<p>
<strong>Lorem Ipsum</strong>
</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed malesuada auctor magna. Vivamus urna justo, pulvinar nec, sagittis malesuada, accumsan in, massa. Quisque mi purus, gravida eget, ultricies a, porta in, sem. Maecenas justo elit, elementum vel, feugiat vulputate, pulvinar nec, velit. Fusce vel ante et diam bibendum euismod. Nunc vel nulla non lorem dignissim placerat. Nulla magna massa, auctor et, tempor nec, auctor sit amet, turpis. Quisque odio lacus, auctor at, posuere id, suscipit eget, dui. Phasellus aliquam. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Proin varius. Phasellus cursus. Cras mattis adipiscing turpis. Sed.</p>
<p>Lorem <strong> ...more</strong>
</p>
<p/>
</body>
</entry>
</news>
Prenez note ce qui suit:
-
scanl
bibliothèque FXSL est importé. Ce modèle est couramment utilisé pour accumuler des données de traitement d'une liste d'éléments. La fonction (lemyAdd:*
d'adaptation de modèle) qui effectue le traitement proprement dit est transmis en tant que paramètre dans le modèle descanl
. L'autre paramètre qui doit être transmise est la valeur « initiale » de traitement, qui doit être retourné si la liste d'articles transmis est vide. -
Le paramètre global
$pTruncateLength
tient la longueur de chaîne maximale supérieure à laquelle le texte doit être tronqué
Ce que vous demandez est un générateur XSLT ellipsis .
Peut-être cette modèle xslt 1.0 pourrait vous donner une idée:
Voici l'essentiel principal de celui-ci:
<xsl:template match="text()" mode="label">
<xsl:param name="self-x"/>
<xsl:param name="self-y"/>
<xsl:variable name="text" select="normalize-space(.)"/>
<!-- a quick and dirty way to avoid problems with line breaks -->
<!-- replace the select attribute with this call
if you want to use a fancier way to escape whitespace
characters:
<xsl:call-template name="escape-ws"
<xsl:with-param name="text" select="." /
</xsl:call-template
-->
<use xlink:href="#text-box" transform="translate({$self-x}
{$self-y})"/>
<!-- text nodes are marked with a little box -->
<text x="{$self-x + $writing-bump-over}"
y="{$self-y - $writing-bump-up}"
style="{$text-font-style}; stroke:none; fill:{$text-color}">
<xsl:text>"</xsl:text>
<xsl:value-of select="substring($text,1,$max-text-length)"/>
<!-- truncate the text node to $max-text-length -->
<xsl:if test="string-length($text) > $max-text-length">
<!-- add an ellipsis if necessary -->
<xsl:text>...</xsl:text>
</xsl:if>
<xsl:text>"</xsl:text>
</text>
</xsl:template>
Note:
- vous devrez remplacer par les points de suspension un lien, mais l'idée principale est là.
- cela représente seulement un petit extrait des tous les scripts
- vous ne pouvez pas besoin de tout: si vous avez besoin «
<use xlink:href="...
», vous devez déclarer l'espace de noms XLink
Après beaucoup de piratage informatique, je suis venu à cette solution:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!--
Author: Neil Albrock
Version: 1.0
Description: Truncate by a character limit and retain HTML content.
Usage:
<xsl:call-template name="truncate">
<xsl:with-param name="data" select="path/to/your/body" />
<xsl:with-param name="length" select="250" />
<xsl:with-param name="link" select="'href'" />
</xsl:call-template>
-->
<xsl:template name="truncate">
<!-- The node set to be worked on. -->
<xsl:param name="data"/>
<!-- The desired truncate length. Default to length of data. -->
<xsl:param name="length" select="string-length($data)"/>
<!-- More link -->
<xsl:param name="link"/>
<xsl:choose>
<!-- Return whole data if it's within length. -->
<xsl:when test="string-length($data) <= $length">
<xsl:copy-of select="$data" />
</xsl:when>
<!-- Truncate to desired length. -->
<xsl:otherwise>
<xsl:for-each select="$data/*">
<xsl:variable name="this-node" select="string-length(.)"/>
<xsl:variable name="preceding-nodes">
<xsl:copy-of select="preceding-sibling::*"/>
</xsl:variable>
<xsl:variable name="node-sum" select="string-length(normalize-space($preceding-nodes))"/>
<xsl:variable name="limit" select="$node-sum + $this-node"/>
<xsl:choose>
<xsl:when test="$limit > $length and $node-sum <= $length">
<p>
<xsl:value-of select="substring(.,1,$length - $node-sum)"/>
<xsl:text>…</xsl:text>
<a>
<xsl:attribute name="href">
<xsl:value-of select="$link"/>
</xsl:attribute>
<xsl:text>more</xsl:text>
</a>
</p>
</xsl:when>
<xsl:when test="$limit < $length">
<xsl:copy-of select="."/>
</xsl:when>
<xsl:otherwise/>
</xsl:choose>
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
J'utiliser la solution par Chaotic Motif cependant, il est plus élégant; -)
Ce sera un épisode dans la douleur en utilisant XSLT. Je recommande fortement d'utiliser un langage de script comme Perl / Python pour tenter cela.