XSLT: mover una agrupación de elementos html a niveles de sección

11 minutos de lectura

XSLT: mover una agrupación de elementos html a niveles de sección
jeff

Estoy tratando de escribir un XSLT que organice un archivo HTML en diferentes niveles de sección según el nivel del encabezado. Aquí está mi entrada:

<html>
 <head>
  <title></title>
 </head>
 <body>
  <h1>HEADER 1 CONTENT</h1>
  <p>Level 1 para</p>
  <p>Level 1 para</p>
  <p>Level 1 para</p>
  <p>Level 1 para</p>

  <h2>Header 2 CONTENT</h2>
  <p>Level 2 para</p>
  <p>Level 2 para</p>
  <p>Level 2 para</p>
  <p>Level 2 para</p>
 </body>
</html>

Estoy trabajando con una estructura bastante simple en este momento, por lo que este patrón será constante por el momento. Necesito una salida como esta…

<document> 
  <section level="1">
     <header1>Header 1 CONTENT</header1>
     <p>Level 1 para</p>
     <p>Level 1 para</p>
     <p>Level 1 para</p>
     <p>Level 1 para</p>
     <section level="2">
        <header2>Header 2 CONTENT</header2>
        <p>Level 2 para</p>
        <p>Level 2 para</p>
        <p>Level 2 para</p>
        <p>Level 2 para</p>
     </section>
  </section>
</document>

Estuve trabajando con este ejemplo: Respuesta de Stackoverflow

Sin embargo, no puedo conseguir que haga exactamente lo que necesito.

Estoy usando Saxon 9 para ejecutar el xslt dentro de Oxygen para desarrollo. Usaré un archivo cmd/bat en producción. Todavía Saxon 9. Me gustaría manejar hasta 4 niveles de sección anidados si es posible.

¡Cualquier ayuda es muy apreciada!

Necesito añadir a esto ya que me he encontrado con otra estipulación. Probablemente debería haber pensado en esto antes.

Estoy encontrando el siguiente ejemplo de código

<html>
<head>
<title></title>
</head>
<body>
<p>Level 1 para</p>
<p>Level 1 para</p>
<p>Level 1 para</p>
<p>Level 1 para</p>

<h1>Header 2 CONTENT</h1>
<p>Level 2 para</p>
<p>Level 2 para</p>
<p>Level 2 para</p>
<p>Level 2 para</p>
</body>
</html>

Como puedes ver, el <p> es un hijo de <body> mientras que en mi primer fragmento, <p> siempre fue un elemento secundario de un nivel de encabezado. Mi resultado deseado es el mismo que el anterior, excepto que cuando encuentro <p> como hijo de <body>debe estar envuelto en <section level="1">.

<document> 
<section level="1">     
<p>Level 1 para</p>
<p>Level 1 para</p>
<p>Level 1 para</p>
<p>Level 1 para</p>
</section>
<section level="1">
<header1>Header 2 CONTENT</header1>
<p>Level 2 para</p>
<p>Level 2 para</p>
<p>Level 2 para</p>
<p>Level 2 para</p>
</section>
</document>

  • Jeff, considere publicar el código fuente de la entrada XML, así como el código fuente de la salida correspondiente que desea crear con Saxon 9, luego podemos ayudarlo con el código XSLT 2.0. Y también explique cuántos niveles espera manejar (número fijo o arbitrario).

    – Martín Honnen

    28 dic. 10 a las 15:58

  • Debe mostrarse el código fuente de entrada y salida.

    – Jeff

    28 dic. 10 a las 16:11

  • Buena pregunta, +1. Vea mi respuesta para una solución XSLT 1.0 que no es perceptiblemente más larga que la solución XSLT 2.0 de Martin Honnen. 🙂

    – Dimitre Novatchev

    28 dic. 10 a las 19:09

  • Después de que @Alejandro proporcionara un documento fuente XML más complicado, he reescrito completamente mi solución y creo que merece una mirada. Una de las perlas casi olvidadas de Jeni Tennison.

    – Dimitre Novatchev

    29 dic. 10 en 1:31

Aquí hay una hoja de estilo XSLT 2.0:

<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:mf="http://example.com/mf"
  exclude-result-prefixes="xs mf"
  version="2.0">

  <xsl:output indent="yes"/>

  <xsl:function name="mf:group" as="node()*">
    <xsl:param name="elements" as="element()*"/>
    <xsl:param name="level" as="xs:integer"/>
    <xsl:for-each-group select="$elements" group-starting-with="*[local-name() eq concat('h', $level)]">
      <xsl:choose>
        <xsl:when test="self::*[local-name() eq concat('h', $level)]">
          <section level="{$level}">
            <xsl:element name="header{$level}"><xsl:apply-templates/></xsl:element>
            <xsl:sequence select="mf:group(current-group() except ., $level + 1)"/>
          </section>
        </xsl:when>
        <xsl:otherwise>
          <xsl:apply-templates select="current-group()"/>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each-group>
  </xsl:function>

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

  <xsl:template match="/html">
    <document>
      <xsl:apply-templates select="body"/>
    </document>
  </xsl:template>

  <xsl:template match="body">
    <xsl:sequence select="mf:group(*, 1)"/>
  </xsl:template>

</xsl:stylesheet>

Debería hacer lo que le pediste, aunque no se detiene en cuatro niveles anidados sino que se agrupa mientras encuentra h[n] elementos.

XSLT: mover una agrupación de elementos html a niveles de sección
dimitre novachev

Una solución XSLT 1.0 (esencialmente prestado por Jenni Tennison):

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

 <xsl:template match="html">
   <document><xsl:apply-templates/></document>
 </xsl:template>

 <xsl:template match="body">
   <xsl:apply-templates select="h1" />
 </xsl:template>

 <xsl:key name="next-headings" match="h6"
          use="generate-id(preceding-sibling::*[self::h1 or self::h2 or
                                               self::h3 or self::h4 or
                                               self::h5][1])" />
 <xsl:key name="next-headings" match="h5"
          use="generate-id(preceding-sibling::*[self::h1 or self::h2 or
                                               self::h3 or self::h4][1])" />
 <xsl:key name="next-headings" match="h4"
          use="generate-id(preceding-sibling::*[self::h1 or self::h2 or
                                               self::h3][1])" />
 <xsl:key name="next-headings" match="h3"
          use="generate-id(preceding-sibling::*[self::h1 or self::h2][1])" />
 <xsl:key name="next-headings" match="h2"
          use="generate-id(preceding-sibling::h1[1])" />

 <xsl:key name="immediate-nodes"
          match="node()[not(self::h1 | self::h2 | self::h3 | self::h4 |
                           self::h5 | self::h6)]"
          use="generate-id(preceding-sibling::*[self::h1 or self::h2 or
                                               self::h3 or self::h4 or
                                               self::h5 or self::h6][1])" />

 <xsl:template match="h1 | h2 | h3 | h4 | h5 | h6">
   <xsl:variable name="vLevel" select="substring-after(name(), 'h')" />
   <section level="{$vLevel}">
      <xsl:element name="header{$vLevel}">
        <xsl:apply-templates />
      </xsl:element>
      <xsl:apply-templates select="key('immediate-nodes', generate-id())" />
      <xsl:apply-templates select="key('next-headings', generate-id())" />
   </section>
 </xsl:template>

 <xsl:template match="/*/*/node()" priority="-20">
   <xsl:copy-of select="." />
 </xsl:template>
</xsl:stylesheet>

cuando esta transformación se aplica en el siguiente documento XML:

<html>
    <body>
        <h1>1</h1>
        <p>1</p>
        <h2>1.1</h2>
        <p>2</p>
        <h3>1.1.1</h3>
        <p>3</p>
        <h2>1.2</h2>
        <p>4</p>
        <h1>2</h1>
        <p>5</p>
        <h2>2.1</h2>
        <p>6</p>
    </body>
</html>

se produce el resultado deseado:

<document>
   <section level="1">
      <header1>1</header1>
      <p>1</p>
      <section level="2">
         <header2>1.1</header2>
         <p>2</p>
         <section level="3">
            <header3>1.1.1</header3>
            <p>3</p>
         </section>
      </section>
      <section level="2">
         <header2>1.2</header2>
         <p>4</p>
      </section>
   </section>
   <section level="1">
      <header1>2</header1>
      <p>5</p>
      <section level="2">
         <header2>2.1</header2>
         <p>6</p>
      </section>
   </section>
</document>

  • +1, buena solución. Tu *[starts-with(name(),'h') and (floor(substring... expression could be simplified to *[translate(name(), 'h123456', '') = '']. No hay elementos en HTML que generen falsos positivos con eso.

    – Tomalak

    28 dic. 10 a las 20:46


  • @Tomalak: Buen comentario: no he trabajado de cerca con HTML durante los últimos 10 años.

    – Dimitre Novatchev

    28 dic. 10 a las 21:11

  • Compruebe el resultado de <body> <h1>1</h1> <p>1</p> <h2>1.1</h2> <p>2</p> <h3>1.1.1</h3> <p>3</p> <h2>1.2</h2> <p>4</p> <h1>2</h1> <p>5</p> <h2>2.1</h2> <p>6</p> </body>

    usuario357812

    28 dic. 10 a las 22:06


  • @Alejandro: Gracias, he proporcionado una nueva solución ahora, espero que te guste 🙂

    – Dimitre Novatchev

    29 dic. 10 en 1:31

  • +1 Excelente ejemplo del uso de xsl:key. Echaba de menos esa función.

    usuario357812

    29 dic. 10 en 1:38

Una agrupación más general en XSLT 1.0

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="kHeaderByPreceding"
             match="body/*[starts-with(name(),'h')]"
             use="generate-id(preceding-sibling::*
                                 [starts-with(name(),'h')]
                                 [substring(name(current()),2)
                                   > substring(name(),2)][1])"/>
    <xsl:key name="kElementByPreceding"
             match="body/*[not(starts-with(name(),'h'))]"
             use="generate-id(preceding-sibling::*
                                 [starts-with(name(),'h')][1])"/>
    <xsl:template match="node()|@*" mode="copy">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*" mode="copy"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="body">
        <document>
            <xsl:apply-templates select="key('kHeaderByPreceding','')"/>
        </document>
    </xsl:template>
    <xsl:template match="body/*[starts-with(name(),'h')]">
        <section level="{substring(name(),2)}">
            <xsl:element name="header{substring(name(),2)}">
                <xsl:apply-templates mode="copy"/>
            </xsl:element>
            <xsl:apply-templates select="key('kElementByPreceding',
                                             generate-id())"
                                 mode="copy"/>
            <xsl:apply-templates select="key('kHeaderByPreceding',
                                             generate-id())"/>
        </section>
    </xsl:template>
    <xsl:template match="text()"/>
</xsl:stylesheet>

Producción:

<document>
    <section level="1">
        <header1>HEADER 1 CONTENT</header1>
        <p>Level 1 para</p>
        <p>Level 1 para</p>
        <p>Level 1 para</p>
        <p>Level 1 para</p>
        <section level="2">
            <header2>Header 2 CONTENT</header2>
            <p>Level 2 para</p>
            <p>Level 2 para</p>
            <p>Level 2 para</p>
            <p>Level 2 para</p>
        </section>
    </section>
</document>

Y con una muestra de entrada más compleja como:

<body>
    <h1>1</h1>
    <p>1</p>
    <h2>1.1</h2>
    <p>2</p>
    <h3>1.1.1</h3>
    <p>3</p>
    <h2>1.2</h2>
    <p>4</p>
    <h1>2</h1>
    <p>5</p>
    <h2>2.1</h2>
    <p>6</p>
</body>

Producción:

<document>
    <section level="1">
        <header1>1</header1>
        <p>1</p>
        <section level="2">
            <header2>1.1</header2>
            <p>2</p>
            <section level="3">
                <header3>1.1.1</header3>
                <p>3</p>
            </section>
        </section>
        <section level="2">
            <header2>1.2</header2>
            <p>4</p>
        </section>
    </section>
    <section level="1">
        <header1>2</header1>
        <p>5</p>
        <section level="2">
            <header2>2.1</header2>
            <p>6</p>
        </section>
    </section>
</document>

Pude hacer que algo funcionara para mi apéndice anterior. Agregué lógica en la plantilla del cuerpo para probar las etiquetas de encabezado. Puede que no funcione para todas las situaciones, pero está funcionando bien para mi tarea.

<xsl:template match="body">
<xsl:choose>
<xsl:when test="descendant::h1">
<xsl:apply-templates/>
</xsl:when>
<xsl:otherwise>
<section level="1">
<item>
<block ccm="yes" onbup="no" quickref="no" web="no">
<xsl:apply-templates/>
</block>
</item>
</section>              
</xsl:otherwise>
</xsl:choose>        
</xsl:template>

.

¿Ha sido útil esta solución?