Estoy tratando de mover tokens delimitados por espacios de un atributo a otro en XSLT-2.0. Por ejemplo, dado

<!-- SOURCE DOCUMENT -->
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <p class="foo"/>
    <p class="foo bar baz"/>
    <p class="foo bar baz" outputclass="BAR"/>
    <p class="foo bar baz" outputclass="BAR HELLO"/>
</root>

Necesito mover @ class = "foo" a @ outputclass = "FOO" y @ class = "bar" a @ outputclass = "BAR", eliminando el atributo de origen si queda vacío y aumentando el atributo de destino si existe (simple operaciones de conjunto de tokens):

<!-- RESULTING DOCUMENT -->
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <p             outputclass="FOO"/>
    <p class="baz" outputclass="FOO BAR"/>
    <p class="baz" outputclass="FOO BAR"/>
    <p class="baz" outputclass="FOO BAR HELLO"/>
</root>

Creo que tengo todo resuelto, excepto la parte real de movimiento de fichas. Cada dirección que bajo termina complicada y rota, y siento que XSLT-2.0 seguramente tiene un enfoque simple que me falta.

Esto es lo que tengo hasta ahora:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:mine="mine:local"
    exclude-result-prefixes="xs"
    version="2.0">

    <!-- baseline identity transform -->
    <!-- (for non-elements - attributes, whitespace PCDATA, etc.)  -->
    <xsl:template match="@*|(node() except *)">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <!-- for element nodes, remap attributes then copy element -->
    <xsl:template match="*">
        <!-- get original attribute sequence -->
        <xsl:variable name="atts1" select="@*"/>

        <!-- use our function to remap two attribute tokens -->
        <xsl:variable name="atts2" select="mine:remap($atts1, 'class', 'foo', 'outputclass', 'FOO')"/>
        <xsl:variable name="atts3" select="mine:remap($atts2, 'class', 'bar', 'outputclass', 'BAR')"/>

        <!-- stuff updated attribute sequence into element -->
        <xsl:copy>
            <xsl:sequence select="$atts3"/>
            <xsl:apply-templates select="node()"/>
        </xsl:copy>
    </xsl:template>

    <!-- remap  @from_att~="$from_token"  to  @to_att~="$to_token" -->
    <xsl:function name="mine:remap">
        <xsl:param name="orig_atts"/>
        <xsl:param name="from_att"/>
        <xsl:param name="from_token"/>
        <xsl:param name="to_att"/>
        <xsl:param name="to_token"/>

        <!-- ******** TOKEN-MOVING MAGIC!?! ******** -->

        <xsl:sequence select="$orig_atts"/>
    </xsl:function>
</xsl:stylesheet>

Básicamente, necesito descubrir cómo TOKEN-MOVING MAGIC!?! puede mover un solo token (incluida la eliminación de los atributos "de" vacíos). He buscado bastante pero no he visto este problema en particular cubierto.

Editar: el número y los nombres de los atributos para reasignar pueden ser cualquier cosa, y sus valores distinguen entre mayúsculas y minúsculas. Es la magia dentro de la función mine:remap reasignar un solo valor en una secuencia de atributos que estoy buscando.

Editar: La razón para abordar la modificación de atributos con una función es que tenemos una serie de reasignaciones de tokens diferentes para aplicar a diferentes archivos, y esperaba permitir que nuestros usuarios no expertos en XSLT ajusten fácilmente las reasignaciones a sus necesidades. No pude descubrir cómo proporcionar una generalización similar con un enfoque basado en la coincidencia de plantillas.

¡Gracias!

1
chrispitude 26 jun. 2020 a las 18:27

3 respuestas

La mejor respuesta

Esto es lo que terminé con la función mine:remap():

<!-- remap  @from_att~="$from_token"  to  @to_att~="$to_token" -->
<xsl:function name="mine:remap">
    <xsl:param name="orig_atts" as="attribute()*"/>
    <xsl:param name="from_att"/>
    <xsl:param name="from_token"/>
    <xsl:param name="to_att"/>
    <xsl:param name="to_token"/>

    <!-- get tokenized list of values of "from" attributes -->
    <xsl:variable name="from_att_values" select="tokenize($orig_atts[name() = $from_att], ' ')"/>

    <xsl:choose>
        <!-- does the "from" attribute contain our value to replace? -->
        <xsl:when test="$from_att_values = $from_token">

            <!-- if so, iterate through attributes to preserve their order -->
            <xsl:for-each select="$orig_atts">
                <xsl:choose>
                    <!-- if "from" and "to" attributes are the same, replace $from_token with $to_token in-place -->
                    <xsl:when test="(name(.) = $from_att) and ($from_att = $to_att)">
                        <xsl:attribute name="{name(.)}" select="for $t in $from_att_values
                            return ($t[$t != $from_token], $to_token[$t = $from_token])"/>                        
                    </xsl:when>
                    <!-- if "from" attribute, define with $from_token value removed -->
                    <xsl:when test="name(.) = $from_att">
                        <xsl:variable name="new_from_att_values" select="$from_att_values[not(. = $from_token)]"/>
                        <xsl:if test="count($new_from_att_values) > 0">
                            <xsl:attribute name="{$from_att}" select="$new_from_att_values"/>
                        </xsl:if>
                    </xsl:when>
                    <!-- if "to" attribute, define with $to_token value added -->
                    <xsl:when test="name(.) = $to_att">
                        <xsl:attribute name="{$to_att}" select="distinct-values((tokenize(., ' '), $to_token))"/>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:copy/>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each>
            <!-- if there was no "from" attribute to modify above, create it here -->
            <xsl:if test="not($orig_atts[name() = $to_att])">
                <xsl:attribute name="{$to_att}" select="$to_token"/>
            </xsl:if>
        </xsl:when>

        <!-- if not, return original attributes -->
        <xsl:otherwise>
            <xsl:sequence select="$orig_atts"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:function>

Repito los atributos para preservar su orden, luego uso xsl: elijo manejar los atributos de (eliminar un token), a (agregar un token) u otros (copiar).

0
chrispitude 1 jul. 2020 a las 11:36

Aquí hay una breve solución XSLT 2.0 (solo 26 líneas):

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match="p/@class[tokenize(., ' ') = ('foo', 'bar')]">
    <xsl:if test="tokenize(., ' ')[not(. = ('foo', 'bar'))]">
        <xsl:attribute name="class" 
             select="string-join(tokenize(., ' ')[not(. = ('foo', 'bar'))], ' ')"/>
    </xsl:if>
    <xsl:attribute name="outputclass" select=
      "upper-case(string-join(
                   (
                    tokenize(., ' ')[. = ('foo', 'bar')],
                    tokenize(../@outputclass, ' ')
                                 [not(lower-case(.) = tokenize(current(), ' '))]
                    ),
                    ' '
                              )
                  )"/>
  </xsl:template>
  
  <xsl:template match="p/@outputclass[../@class[tokenize(., ' ') = ('foo', 'bar')]]"/>
</xsl:stylesheet>

Cuando esta transformación se aplica en el documento XML proporcionado :

<root>
    <p class="foo"/>
    <p class="foo bar baz"/>
    <p class="foo bar baz" outputclass="BAR"/>
    <p class="foo bar baz" outputclass="BAR HELLO"/>
</root>

se produce el resultado deseado y correcto :

<root>
    <p outputclass="FOO"/>
    <p class="baz" outputclass="FOO BAR"/>
    <p class="baz" outputclass="FOO BAR"/>
    <p class="baz" outputclass="FOO BAR HELLO"/>
</root>

Actualización :

Aquí está la misma transformación con casi todo parametrizado, según lo solicitado en un comentario por el OP, solo 32 líneas:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:param name="pfromName" select="'class'"/>
 <xsl:param name="ptoName" select="'outputclass'"/>
 <xsl:param name="pTokens" select="'foo', 'bar'"/>
 <xsl:param name="pnewNames" select="'FOO', 'BAR'"/>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="p/@*[name() = $pfromName][tokenize(., ' ') = $pTokens]">
    <xsl:if test="tokenize(., ' ')[not(. = $pTokens)]">
        <xsl:attribute name="{$pfromName}"
             select="string-join(tokenize(., ' ')[not(. = $pTokens)], ' ')"/>
    </xsl:if>
    <xsl:attribute name="{$ptoName}" select=
      "upper-case(string-join(
                   (
                    tokenize(., ' ')[. = $pTokens],
                    tokenize(../@*[name()=$ptoName], ' ')
                                 [not(lower-case(.) = tokenize(current(), ' '))]
                    ),
                    ' '
                              )
                  )"/>
  </xsl:template>

  <xsl:template 
    match="p/@*[name()=$ptoName][../@*[name()=$pfromName][tokenize(., ' ') = $pTokens]]"/>
</xsl:stylesheet>

Actualización2 :

Aquí hay una transformación XSLT 2.0 completamente parametrizada (que no utiliza las funciones upper-case() y lower-case()), solo 37 líneas:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:param name="pfromName" select="'class'"/>
 <xsl:param name="ptoName" select="'outputclass'"/>
 <xsl:param name="pTokens" select="'foo', 'bar'"/>
 <xsl:param name="pnewNames" select="'FOO', 'BAR'"/>

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="p/@*[name() = $pfromName][tokenize(., ' ') = $pTokens]">
    <xsl:if test="tokenize(., ' ')[not(. = $pTokens)]">
        <xsl:attribute name="{$pfromName}"
             select="string-join(tokenize(., ' ')[not(. = $pTokens)], ' ')"/>
    </xsl:if>
    <xsl:attribute name="{$ptoName}" select=
      "string-join(
                   distinct-values(
                            (for $token in tokenize(., ' ')[. = $pTokens],
                                    $n in 1 to count($pTokens),
                                    $ind in $n[$token eq $pTokens[$n]]
                                  return $pnewNames[$ind]
                             ,
                              tokenize(../@*[name()=$ptoName], ' ')
                              )
                                    ),
                    ' '
                    )
                  "/>
  </xsl:template>

  <xsl:template
  match="p/@*[name()=$ptoName][../@*[name()=$pfromName][tokenize(., ' ') = $pTokens]]"/>
</xsl:stylesheet>
0
Dimitre Novatchev 5 jul. 2020 a las 18:48

En el siguiente ejemplo, he intentado delegar tanto como sea posible a las plantillas:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    version="3.0">
    
  <xsl:param name="tokens" as="xs:string*"
    select="'foo', 'bar'"/>
    
  <xsl:param name="collation" as="xs:string">http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive</xsl:param>

  <xsl:mode on-no-match="shallow-copy"/>

  <xsl:template match="*[@class][exists($tokens[contains-token(current()/@class, ., $collation)])]">
      <xsl:copy>
          <xsl:variable name="new-att" as="attribute()">
              <xsl:attribute name="outputclass"/>
          </xsl:variable>
          <xsl:apply-templates select="@*, $new-att[not(current()/@outputclass)]">
              <xsl:with-param name="tokens-found" 
                select="$tokens[contains-token(current()/@class, ., $collation)]"/>
          </xsl:apply-templates>
          <xsl:apply-templates/>
      </xsl:copy>
  </xsl:template>
  
  <xsl:template match="@class">
      <xsl:param name="tokens-found"/>
      <xsl:variable name="remaining-tokens" select="tokenize(., ' ')[not(. = $tokens-found)]"/>
      <xsl:if test="exists($remaining-tokens)">
          <xsl:attribute name="{name()}" select="$remaining-tokens"/>
      </xsl:if>
  </xsl:template>
  
  <xsl:template match="@outputclass">
      <xsl:param name="tokens-found"/>
      <xsl:variable name="new-tokens" select="$tokens-found[not(contains-token(current(), ., $collation))]"/>
      <xsl:attribute name="{name()}" select="$new-tokens, ."/>
  </xsl:template>
  
</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bEzkTcx/1

No he implementado la transformación de mayúsculas de los tokens para mover, supongo que debería ser fácil agregar eso.

El código usa XSLT 3 con XPath 3 y la función https: // www.w3.org/TR/xpath-functions/#func-contains-token pero tiene una definición en la especificación que puede usar en una función XSLT 2 definida por el usuario. Por supuesto, también es fácil no declarar la transformación de identidad usando xsl:mode sino deletreando.

XSLT 3 está disponible con Saxon 9.8 o posterior para Java y .NET, con Saxon-C para C / C ++, con enlaces para PHP y Python y con Saxon-JS 2 dentro de los navegadores web modernos y para Node.js.

0
Martin Honnen 27 jun. 2020 a las 10:31