4

I'm at my wits end with this and wondering if this is even possible with XSL. Lets say that I have this XML data:

<?xml version="1.0" standalone="yes"?>
<Data>
  <Row>
    <F1>Created By</F1>    
    <F2>City</F2>
  </Row>
  <Row>
    <F1>John Doe</F1> 
    <F2>Los Angeles</F2>   
  </Row>
  <Row>
    <F1>Jane Doe</F1> 
    <F2>San Diego</F2>   
  </Row>
</Data>

I would like to repeat the first row element while iterating through the rest of the data. In short I would like the output to be:

[Created By] [City]
-----------  ------------
[John Doe]   [Los Angeles]

[Created By] [City]
----------   ------------
[Jane Doe]   [San Diego]

What would be the best approach? I tried setting the first element 'Created By' as a variable but I it does not render when I try to use it. I'm pretty new at XSL and any help would be appreciated.

Thank you

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • Do you actually want plaintext like that as the output, or do you want this as HTML? If so, could you show us what you want the HTML (the code itself) to look like? – JLRishe Mar 16 '13 at 17:36
  • I would like plain text. I just updated the output from my question. – user2177441 Mar 16 '13 at 18:01
  • Ok, thank you for clarifying. So do you want brackets around the column names, or the column names _and_ the values? – JLRishe Mar 16 '13 at 18:05
  • No problem, I would like brackets around the column names and the values. – user2177441 Mar 16 '13 at 18:11

3 Answers3

4

Here is a solution, that, unlike the currently-accepted answer, processes correctly data with various, unknown in advance lengths -- see further an update that handles unlimited number of columns, too:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>

 <xsl:variable name="vMax1">
  <xsl:call-template name="maxLength">
   <xsl:with-param name="pNodes" select="/*/*/*[1]"/>
  </xsl:call-template>
 </xsl:variable>
 <xsl:variable name="vMax2">
  <xsl:call-template name="maxLength">
   <xsl:with-param name="pNodes" select="/*/*/*[2]"/>
  </xsl:call-template>
 </xsl:variable>

 <xsl:variable name="vLongest1" select=
  "/*/*/*[1][string-length() = $vMax1][1]"/>
 <xsl:variable name="vLongest2" select=
  "/*/*/*[2][string-length() = $vMax2][1]"/>

 <xsl:variable name="vUnderscores1" select=
  "concat('__',
          translate($vLongest1,translate($vLongest1, '_', ''),
                    '_________________________________________________________')
          )"/>
 <xsl:variable name="vUnderscores2" select=
  "concat('__',
          translate($vLongest2,translate($vLongest2, '_', ''),
                    '_________________________________________________________')
          )"/>
 <xsl:variable name="vBlanks1"
   select="translate($vUnderscores1,'_', ' ')"/>
 <xsl:variable name="vBlanks2"
   select="translate($vUnderscores2,'_', ' ')"/>

 <xsl:variable name="vTitle1" select=
 "concat('[',/*/*/*[1],']',
        substring($vBlanks1,1,
                  string-length($vBlanks1)-string-length(/*/*/*[1]))
        )"/>
 <xsl:variable name="vTitle2" select=
 "concat('[',/*/*/*[2],']',
        substring($vBlanks2,1,
                  string-length($vBlanks2)-string-length(/*/*/*[2]))
        )"/>

 <xsl:template match="Row">
  <xsl:value-of select=
   "concat('&#xA;', $vTitle1, $vTitle2)"/>
  <xsl:value-of select=
   "concat('&#xA;',$vUnderscores1, '  ', $vUnderscores2, '&#xA;')"/>
  <xsl:value-of select=
   "concat(F1,
           substring($vBlanks1,1,
                     string-length($vBlanks1)-string-length(F1)),
           '  ',
           F2,
           substring($vBlanks1,1,
                     string-length($vBlanks1)-string-length(F2)),
           '&#xA;'
          )"/>
 </xsl:template>

 <xsl:template name="maxLength">
  <xsl:param name="pNodes" select="/.."/>

  <xsl:for-each select="$pNodes">
   <xsl:sort select="string-length()"
        data-type="number" order="descending"/>
   <xsl:if test="position() = 1">
    <xsl:value-of select="string-length()"/>
   </xsl:if>
  </xsl:for-each>
 </xsl:template>
 <xsl:template match="Row[1]|text()"/>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<Data>
  <Row>
    <F1>Created By</F1>
    <F2>City</F2>
  </Row>
  <Row>
    <F1>John Doe</F1>
    <F2>Los Angeles</F2>
  </Row>
  <Row>
    <F1>Jane Doe</F1>
    <F2>San Diego</F2>
  </Row>
</Data>

the wanted, correct result is produced:

[Created By]  [City]         
____________  _____________
John Doe      Los Angeles 

[Created By]  [City]         
____________  _____________
Jane Doe      San Diego   

More interestingly, when applied on this XML document:

<Data>
    <Row>
        <F1>Created By</F1>
        <F2>City</F2>
    </Row>
    <Row>
        <F1>John Doe</F1>
        <F2>La Villa Real de la Santa Fe de San Francisco de Asis</F2>
    </Row>
    <Row>
        <F1>Josiah Willard Gibbs</F1>
        <F2>San Diego</F2>
    </Row>
</Data>

again the correct results are produced:

[Created By]            [City]                                                   
______________________  _______________________________________________________
John Doe                La Villa Real de la Santa Fe de San Francisco de Asis

[Created By]            [City]                                                   
______________________  _______________________________________________________
Josiah Willard Gibbs    San Diego             

Compare this correct result with the one produced by the currently-accepted answer:

Created By  City        
----------- ----------- 
John Doe    La Villa Real de la Santa Fe de San Francisco de Asis

Created By  City        
----------- ----------- 
Josiah Willard GibbsSan Diego   

II An XSLT 2.0 variant of the same solution: shorter and easier to write:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output method="text"/>

 <xsl:variable name="vMax" as="xs:integer*" select=
  "for $i in 1 to 2
    return
      max(/*/*/*[$i]/string-length(.))"/>

 <xsl:variable name="vUnderscores" select=
  "for $i in 1 to 2
    return
           concat('__',
                  string-join((for $len in 1 to $vMax[$i]
                                            return '_'), '')
                  )"/>

 <xsl:variable name="vBlanks" select=
  "for $i in 1 to 2
     return
       translate($vUnderscores[$i],'_', ' ')

  "/>

 <xsl:variable name="vTitle" select=
  "for $i in 1 to 2
    return
       concat('[',(*/*/*)[$i],']',
        substring($vBlanks[$i],1,
                  $vMax[$i]+2 -string-length((/*/*/*)[$i]))
        )

  "/>
 <xsl:template match="Row">
  <xsl:value-of select=
   "concat('&#xA;', $vTitle[1], $vTitle[2])"/>
  <xsl:value-of select=
   "concat('&#xA;',$vUnderscores[1], '  ', $vUnderscores[2], '&#xA;')"/>
  <xsl:value-of select=
   "concat(F1,
           substring($vBlanks[1],1,
                     $vMax[1]+2 -string-length(F1)),
           '  ',
           F2,
           substring($vBlanks[2],1,
                     $vMax[1]+2 -string-length(F2)),
           '&#xA;'
          )"/>
 </xsl:template>
 <xsl:template match="Row[1]|text()"/>
</xsl:stylesheet>

III. Handling idefinite number of columns:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output method="text"/>

 <xsl:variable name="vNumCols" select="max(/*/*/count(*))"/>

 <xsl:variable name="vMax" as="xs:integer*" select=
  "for $i in 1 to $vNumCols
    return
      max(/*/*/*[$i]/string-length(.))"/>

 <xsl:variable name="vUnderscores" select=
  "for $i in 1 to $vNumCols
    return
       concat('__',
              string-join((for $len in 1 to $vMax[$i]
                            return '_'), '')
             )"/>

 <xsl:variable name="vBlanks" select=
  "for $i in 1 to $vNumCols
     return
       translate($vUnderscores[$i],'_', ' ')

  "/>

 <xsl:variable name="vTitle" select=
  "for $i in 1 to $vNumCols
    return
       concat('[',(*/*/*)[$i],']',
        substring($vBlanks[$i],1,
                  $vMax[$i]+2 -string-length((/*/*/*)[$i]))
        )

  "/>
 <xsl:template match="Row">
  <xsl:value-of separator="" select=
   "'&#xA;', string-join($vTitle, '')"/>
  <xsl:value-of separator="" select=
   "'&#xA;', string-join($vUnderscores, '  '), '&#xA;'"/>

  <xsl:value-of select=
   "string-join((for $i in 1 to $vNumCols,
               $vChild in *[$i]
            return
              ($vChild,
               substring($vBlanks[$i],1,
                         $vMax[$i]+2 -string-length($vChild)
                        ),
              '  '
                     ),
                 '&#xA;'
                 ),
                 ''
               )"/>
 </xsl:template>
 <xsl:template match="Row[1]|text()"/>
</xsl:stylesheet>

When applied to this XML document (3 columns):

<Data>
  <Row>
    <F1>Created By</F1>
    <F2>City</F2>
    <F3>Region</F3>
  </Row>
  <Row>
    <F1>Pablo Diego Ruiz y Picasso</F1>
    <F2>Los Angeles</F2>
    <F3>CA</F3>
  </Row>
  <Row>
    <F1>Jane Doe</F1>
    <F2>La Villa Real de la Santa Fe de San Francisco de Asis</F2>
    <F3>NM</F3>
  </Row>
</Data>

the wanted, correct result is produced:

[Created By]                  [City]                                                   [Region]  
____________________________  _______________________________________________________  ________
Pablo Diego Ruiz y Picasso    Los Angeles                                              CA        

[Created By]                  [City]                                                   [Region]  
____________________________  _______________________________________________________  ________
Jane Doe                      La Villa Real de la Santa Fe de San Francisco de Asis    NM     

IV. Translation to XSLT 1.0 of Part III (above):

Do note:

The solution that JLRishe provided as a correction of his initial answer, will perform sort 400 times if there are 100 rows each with 4 columns.

No such inefficiencies in the code below:

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

 <xsl:variable name="vMaxCols">
  <xsl:call-template name="maxChildren">
   <xsl:with-param name="pNodes" select="/*/*"/>
  </xsl:call-template>
 </xsl:variable>

 <xsl:variable name="vrtfMax">
  <xsl:for-each select="(/*/*/*)[not(position() > $vMaxCols)]">
   <xsl:variable name="vPos" select="position()"/>
   <length>
     <xsl:call-template name="maxLength">
       <xsl:with-param name="pNodes" select="/*/*/*[position()=$vPos]"/>
     </xsl:call-template>
   </length>
  </xsl:for-each>
 </xsl:variable>

 <xsl:variable name="vMax" select="ext:node-set($vrtfMax)/length"/>

 <xsl:variable name="vrtfUnderscores">
  <xsl:for-each select="(/*/*/*)[not(position() > $vMaxCols)]">
    <xsl:variable name="vPos" select="position()"/>
    <xsl:variable name="vLongestDataNode" select=
     "/*/*/*[position()=$vPos
           and string-length() = $vMax[position() = $vPos]][1]"/>
    <t>
       <xsl:value-of select=
       "concat('__',
                   translate($vLongestDataNode,translate($vLongestDataNode, '_', ''),
                             '_________________________________________________________')
               )"/>
    </t>
  </xsl:for-each>
 </xsl:variable>

 <xsl:variable name="vUnderscores" select="ext:node-set($vrtfUnderscores)/t"/>

 <xsl:variable name="vrtfBlanks">
  <xsl:for-each select="$vUnderscores">
   <xsl:variable name="vPos" select="position()"/>
   <t><xsl:value-of select=
           "translate($vUnderscores[position()=$vPos],'_', ' ')"/>
   </t>
  </xsl:for-each>
 </xsl:variable>

 <xsl:variable name="vBlanks" select="ext:node-set($vrtfBlanks)/t"/>

 <xsl:variable name="vrtfTitle">
  <xsl:for-each select="/*/*[1]/*">
   <xsl:variable name="vPos" select="position()"/>
   <t>
     <xsl:value-of select=
     "concat('[',.,']',
        substring($vBlanks[position()=$vPos],1,
                  2+$vMax[position()=$vPos]-string-length())
        )
     "/>
   </t>
  </xsl:for-each>
 </xsl:variable>

 <xsl:variable name="vTitle" select="ext:node-set($vrtfTitle)/t"/>

 <xsl:template match="Row">
  <xsl:text>&#xA;</xsl:text>
  <xsl:for-each select="$vTitle">
      <xsl:value-of select="."/>
  </xsl:for-each>

  <xsl:text>&#xA;</xsl:text>
  <xsl:for-each select="$vUnderscores">
      <xsl:value-of select="."/>
      <xsl:text>  </xsl:text>
  </xsl:for-each>
  <xsl:text>&#xA;</xsl:text>

  <xsl:for-each select="*[not(position() > $vMaxCols)]">
    <xsl:variable name="vPos" select="position()"/>

    <xsl:value-of select=
     "concat(.,
           substring($vBlanks[position()=$vPos],
                     1,
                     string-length($vBlanks[position()=$vPos])
                     -string-length()),
           '  '
           )"/>
  </xsl:for-each>
  <xsl:text>&#xA;</xsl:text>
 </xsl:template>

 <xsl:template name="maxChildren">
  <xsl:param name="pNodes" select="/.."/>

  <xsl:for-each select="$pNodes">
   <xsl:sort select="count(*)"
        data-type="number" order="descending"/>
   <xsl:if test="position() = 1">
    <xsl:value-of select="count(*)"/>
   </xsl:if>
  </xsl:for-each>
 </xsl:template>

 <xsl:template name="maxLength">
  <xsl:param name="pNodes" select="/.."/>

  <xsl:for-each select="$pNodes">
   <xsl:sort select="string-length()"
        data-type="number" order="descending"/>
   <xsl:if test="position() = 1">
    <xsl:value-of select="string-length()"/>
   </xsl:if>
  </xsl:for-each>
 </xsl:template>
 <xsl:template match="Row[1]|text()"/>
</xsl:stylesheet>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • Thank you Dimitre! Very nice!...I just tried it out and added more columns. – user2177441 Mar 16 '13 at 19:15
  • This answer can't handle more than two columns, and the column element places are hardcoded in some places and not in others. I'd think dynamically handling columns would be a higher priority than dynamically handling their widths. My modified answer does both. – JLRishe Mar 16 '13 at 20:01
  • @JLRishe, Why do you think so? :) – Dimitre Novatchev Mar 16 '13 at 20:52
  • Column widths can easily be modified to encompass the dimensions of any realistic data (in my original answer, this could be done by modifying a single line, _or_ the value could be passed in as a parameter), but if an XSLT is designed for a fixed number of columns, changing that to accommodate a differnt number of columns requires a lot of modification, and it means that it's impossible to handle inputs with varying number of columns. And often, that also means introducing a lot of repetition into the XSLT (imagine what your Part I or Part II answers would look like with 8 columns). – JLRishe Mar 16 '13 at 21:10
  • @JLRishe, It seems that you are not aware of Part III Nothing is fixed there. :) And your solution handles limited length data... – Dimitre Novatchev Mar 16 '13 at 21:16
  • I am aware of Part III. Are you trying to pretend it was already there when I made that comment 1 hour ago? :) In my more recent comment, I was responding to your question about (I can only assume) why I think handling columns dynamically is more important than handling widths dynamically, and as both comments directly relate to Parts I and II, I mentioned them. – JLRishe Mar 16 '13 at 21:21
  • The comment you made 10 minutes ago wasn't aware of Part III. Are you trying "to pretend" that your updates were there when I first posted this answer? – Dimitre Novatchev Mar 16 '13 at 21:21
  • 1
    In fact, it was aware of Part III. That's why it specifically mentions Parts I and II. It would be nonsensical to mention Parts I and II by name if they were the only parts; I would have just said "both examples." And no, I'm not trying to pretend that my updates were already there. That's why I specifically said "I have modified my answer to dynamically handle column widths" right after the update. Frankly, between the two of us, I don't think I'm the one with the problem admitting his errors. – JLRishe Mar 16 '13 at 21:33
2

Would this suffice:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text"/>
  <xsl:key name="kColumn" match="Row/*" use="count(preceding-sibling::*) + 1" />

  <xsl:param name="columnWidth" select="13" />

  <xsl:variable name="firstRow" select="/Data/Row[1]" />
  <xsl:variable name="spaces" 
                select="'                                                                           '" />
  <xsl:variable name="separatorChars"
                select="'---------------------------------------------------------------------------'" />

  <xsl:template match="/*">
    <xsl:apply-templates select="Row[position() > 1]" />
  </xsl:template>

  <xsl:template match="Row">
    <xsl:apply-templates select="$firstRow/*" />
    <xsl:text>&#xA;</xsl:text>
    <xsl:apply-templates select="$firstRow/*" mode="separator" />
    <xsl:text>&#xA;</xsl:text>
    <xsl:apply-templates select="*"/>
    <xsl:text>&#xA;&#xA;</xsl:text>
  </xsl:template>

  <xsl:template match="Row/*" name="Cell">
    <xsl:param name="value" select="concat('[', ., ']')" />
    <xsl:param name="width">
      <xsl:call-template name="FindWidth" />
    </xsl:param>

    <xsl:variable name="numSpaces"
                  select="$width + 1 - string-length($value)" />
    <xsl:variable name="trailingSpace" select="substring($spaces, 1, $numSpaces)" />
    <xsl:value-of select="concat($value, $trailingSpace)"/>
  </xsl:template>

  <xsl:template match="Row/*" mode="separator">
    <xsl:variable name="width">
      <xsl:call-template name="FindWidth" />
    </xsl:variable>
    <xsl:call-template name="Cell">
      <xsl:with-param name="value" select="substring($separatorChars, 1, $width)" />
      <xsl:with-param name="width" select="$width" />
    </xsl:call-template>
  </xsl:template>

  <xsl:template name="FindWidth">
    <xsl:apply-templates select="key('kColumn', position())" mode="findLength">
      <xsl:sort select="string-length()" data-type="number" order="descending" />
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template match="Row/*" mode="findLength">
    <xsl:if test="position() = 1">
      <xsl:variable name="len" select="string-length() + 2" />
      <xsl:value-of select="$len * ($len > $columnWidth) + $columnWidth * ($columnWidth > $len)"/>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

When run on this input:

<Data>
  <Row>
    <F1>Created By</F1>
    <F2>City</F2>
    <F3>Region</F3>
  </Row>
  <Row>
    <F1>Pablo Diego Ruiz y Picasso</F1>
    <F2>Los Angeles</F2>
    <F3>CA</F3>
  </Row>
  <Row>
    <F1>Jane Doe</F1>
    <F2>La Villa Real de la Santa Fe de San Francisco de Asis</F2>
    <F3>NM</F3>
  </Row>
</Data>

It produces:

[Created By]                 [City]                                                  [Region]      
---------------------------- ------------------------------------------------------- ------------- 
[Pablo Diego Ruiz y Picasso] [Los Angeles]                                           [CA]          

[Created By]                 [City]                                                  [Region]      
---------------------------- ------------------------------------------------------- ------------- 
[Jane Doe]                   [La Villa Real de la Santa Fe de San Francisco de Asis] [NM]          
JLRishe
  • 99,490
  • 19
  • 131
  • 169
1

Try this:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <!-- Output HTML - as an example -->
  <xsl:output method="html" indent="yes"/>

  <!-- Template that skips the first row, so it does not get processed -->
  <xsl:template match="Row[position()=1]">
  </xsl:template>

  <!-- Template that process all rows except the first one -->
  <xsl:template match="Row[position()>1]">
    <!-- Copy the first row-->
    <tr>
      <xsl:for-each select="../Row[1]/*">
        <td>
          <xsl:value-of select="."/>
        </td>
      </xsl:for-each>
    </tr>
    <!-- Process this row -->
    <tr>
      <xsl:for-each select="*">
        <td>
          <xsl:value-of select="."/>
        </td>
      </xsl:for-each>
    </tr>
  </xsl:template>

  <!-- Root template: output the HTML scaffolding and then processes the individual rows -->
  <xsl:template match="/">
    <html>
      <head></head>
      <body>
        <table>
          <xsl:apply-templates/>
        </table>
      </body>
    </html>
  </xsl:template>

</xsl:stylesheet>

It outputs an HTML table as an example - it should be easy to adapt it for other outputs.

MiMo
  • 11,793
  • 1
  • 33
  • 48