XML truncar com XSLT
-
22-08-2019 - |
Pergunta
Eu tenho uma pergunta para as pessoas inteligentes da comunidade SO.
Abaixo está um trecho de XML gerado pelo 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>
O que eu preciso fazer é dar uma parte do elemento de <body>
, com base em um determinado período, para exibição no estilo blog:
Lorem ipsum dolor sit amet, consectetur adipiscing elit. sed malesuada auctor magna. Vivamus urna justo, nec pulvinar, sagittis malesuada, accumsan em, massa. Quisque mi Purus, gravida eget, ultricies um, porta dentro, sem ... mais
... onde mais é um link para a notícia completa. Eu sei que eu posso selecionar parágrafos específicos e também sei que eu posso usar a função substring para trazer um número especificado de caracteres. No entanto, eu preciso preservar a formatação do texto, ou seja, as tags HTML dentro do elemento <body>
.
Sei que isso levanta questões de fechamento tag mas certamente deve haver uma maneira. Esperemos que alguém mais experiente com XSLT pode lançar alguma luz sobre esta questão.
Solução
Aqui está a minha versão. Eu testei-o sobre a sua amostra XML e ele funciona.
Para invocá-lo, use <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>
Outras dicas
Aqui está uma completa transformação XSLT 1.0 que resolve exatamente o problema.
Esta transformação 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>
quando aplicado sobre o documento XML fonte original :
<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>
produz o resultado queria :
<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>
Do nota ??strong> o seguinte:
-
O
scanl
estilo do biblioteca FXSL é importado. Este modelo é comumente usado para dados acumular de processamento de uma lista de itens. A função de (amyAdd:*
molde correspondente) que faz o processamento real é transmitido como um parâmetro para o modeloscanl
. O outro parâmetro que deve ser passado para ele é o valor "inicial" de processamento, que deve ser devolvido se a lista passada de itens está vazia. -
O parâmetro global
$pTruncateLength
contém o comprimento máximo da cadeia superior a qual o texto deve ser truncado
O que você está pedindo é um XSLT reticências gerador.
Pode ser este XSLT 1.0 template que lhe pode dar alguma idéia:
Aqui é a principal essência do mesmo:
<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>
Nota:
- você precisará substituir as reticências por um link, mas a idéia principal é lá.
- isso representa apenas um pequeno extrato da todos os script
- Você pode não precisar de tudo nele: Se você precisar "
<use xlink:href="...
", você precisa declarar a xlink namespace
Depois de muita pirataria, eu vim para esta solução:
<?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>
Gostaria de usar a solução padrão caótico, porém, é mais elegante; -)
Este será um episódio de dor usando XSLT. Eu recomendaria fortemente usando uma linguagem de script como Perl / Python para tentar isso.