Antony
Antony

Reputation: 135

how do I nest elements dynamically using XSLT?

I am trying to do an identity transformation. Here is an example of my source xml:

<?xml version="1.0" encoding="UTF-8"?>
<text>
    <p id="542">This is a parapgraph</p>
    <p id="561">This is a first level bullet</p>
    <p id="561">This is a first level bullet</p>
    <p id="561">This is a first level bullet</p>
    <p id="561">This is a first level bullet</p>
    <p id="561">This is a first level bullet</p>
    <p id="542">This is a parapgraph</p>
    <p id="561">This is a first level bullet</p>
    <p id="562">This is a second level bullet</p>
    <p id="562">This is a second level bullet</p>
    <p id="561">This is a first level bullet</p>
    <p id="561">This is a first level bullet</p>
    <p id="542">This is a parapgraph</p>
    <p id="542">This is a parapgraph</p>
    <p id="560">This is a first ordered list</p>
    <p id="560">This is a first ordered list</p>
    <p id="560">This is a first ordered list</p>
    <p id="562">This is a second level bullet</p>
    <p id="562">This is a second level bullet</p>

</text>enter code here

I am looking for an output that looks like the following:

<?xml version="1.0" encoding="UTF-8"?>
<text>
    <p id="">This is a parapgraph</p>
    <ul>
    <li>This is a first level bullet</li>
    <li>This is a first level bullet</li>
    <li>This is a first level bullet</li>
    <li>This is a first level bullet</li>
    <li>This is a first level bullet</li>
    </ul>
    <p id="">This is a parapgraph</p>
    <ul>
    <li>This is a first level bullet
        <ul>
            <li>This is a second level bullet</li>
            <li>This is a second level bullet</li>
            <li>This is a second level bullet</li>
        </ul></li>
    <li>This is a first level bullet</li>
    <li>This is a first level bullet</li>
    </ul>
    <p id="">This is a parapgraph</p>
    <p id="">This is a parapgraph</p>
    <ol>
    <li>This is a first ordered list</li>
    <li>This is a first ordered list</li>
    <li>This is a first ordered list</li>
    <li>This is a first level bullet
        <ul>
            <li>This is a second level bullet</li>
            <li>This is a second level bullet</li>
        </ul>
    </li>
    <li>This is a first level bullet</li>
    </ol>
</text>

Could you please help me understand how that can be done? I am new to XSLT. I was looking for a tutorial that does something like this but could not find one. My current XSLT looks like the following:

<?xml version='1.0' encoding="utf-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >

    <xsl:output method="xml" standalone="no"/>

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


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

    <xsl:template match="topic">
        <book>
        <xsl:attribute name="id">
            <xsl:value-of select="generate-id(.)"/>
        </xsl:attribute>
        <xsl:apply-templates select="@*|node()"/>

        </book>
    </xsl:template>

    <xsl:template match="body">
        <text>

                <xsl:apply-templates select="p[@id='542']"/>
                <xsl:call-template name="f-ul"/>
                <xsl:call-template name="s-ul"/>
                <xsl:call-template name="o-l"/>

        </text>
    </xsl:template>

    <!-- all body text style to p tag> -->
    <xsl:template match="p[@id='542']">
        <p><xsl:apply-templates select="@*|node()" /></p>
    </xsl:template>

    <xsl:template name="f-ul">
    <xsl:if test="p[@id='561']">
        <ul>
            <xsl:attribute name="id">
                <xsl:text></xsl:text>
            </xsl:attribute>
            <xsl:apply-templates select="p[@id='561']"/>
        </ul>
    </xsl:if>
    </xsl:template>

    <xsl:template name="s-ul">
    <xsl:if test="p[@id='562']">
        <ul>
            <xsl:attribute name="id">
                <xsl:text></xsl:text>
            </xsl:attribute>
            <xsl:apply-templates select="p[@id='562']"/>
        </ul>
    </xsl:if>
    </xsl:template>

    <xsl:template name="o-l">
    <xsl:if test="p[@id='560']">
        <ol>
            <xsl:attribute name="id">
                <xsl:text></xsl:text>
            </xsl:attribute>
            <xsl:apply-templates select="p[@id='560']"/>
        </ol>
    </xsl:if>
    </xsl:template>



    <!-- all list number style to step tag> -->

    <xsl:template match="p[@id='560']">
        <li><xsl:apply-templates select="@*|node()" /></li>
    </xsl:template>


    <!-- all list bullet style to li tag> -->

    <xsl:template match="p[@id='561']">
        <li><xsl:apply-templates select="node()" /></li>
    </xsl:template>

    <!-- all list bullet 2 style to li tag> -->
    <xsl:template match="p[@id='562']">
        <li><xsl:apply-templates select="node()" /></li>
    </xsl:template>

</xsl:stylesheet>

If you can help me find a solution or at least direct me to the correct resources, that will be much appreciated. Thank you all in advance.

Upvotes: 0

Views: 113

Answers (1)

Martin Honnen
Martin Honnen

Reputation: 167716

It is always difficult to work from examples and I am not sure the shown wanted result is consistent, I would suggest a two step transformation, the first identifies li items and sets up ol/ul, the second then makes sure any inner list is wrapped into the preceding li.

Here is an XSLT 3 (available with Saxon 9.8 or 9.9 for Java and .NET and within XML IDEs like oXygen or Stylus Studio, also implemented by Altova Raptor in their product lines since the 2017 releases) approach of the first step:

<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="#all"
    version="3.0">

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

  <xsl:output method="xml" indent="yes"/>

  <xsl:param name="list-levels" as="map(xs:integer, xs:integer+)"
    select="map { 1 : (560, 561), 2: (562) }"/>

  <xsl:param name="list-map" as="map(xs:integer, xs:QName)"
    select="map { 560 : QName('', 'ol'), 561 : QName('', 'ul'), 562 : QName('', 'ul') }"/>

  <xsl:function name="mf:group" as="node()*">
      <xsl:param name="nodes" as="node()*"/>
      <xsl:param name="level" as="xs:integer"/>
      <xsl:for-each-group select="$nodes" group-adjacent="boolean(self::p[@id = $list-levels?($level to 6)])">
          <xsl:choose>
              <xsl:when test="current-grouping-key()">
                  <xsl:element name="{$list-map(xs:integer(@id))}">
                      <xsl:sequence select="mf:group(current-group(), $level + 1)"/>
                  </xsl:element>                  
              </xsl:when>
              <xsl:otherwise>
                  <xsl:apply-templates select="current-group()"/>
              </xsl:otherwise>
          </xsl:choose>
      </xsl:for-each-group>
  </xsl:function>

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

  <xsl:template match="p[@id = 542]">
      <xsl:copy>
          <xsl:apply-templates/>
      </xsl:copy>
  </xsl:template>

  <xsl:template match="p[@id = $list-levels?*]">
      <li>
          <xsl:apply-templates/>
      </li>
  </xsl:template>

</xsl:stylesheet>

You can set it working at https://xsltfiddle.liberty-development.net/pPzifoZ with the following result:

<text>
   <p>This is a parapgraph</p>
   <ul>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
   </ul>
   <p>This is a parapgraph</p>
   <ul>
      <li>This is a first level bullet</li>
      <ul>
         <li>This is a second level bullet</li>
         <li>This is a second level bullet</li>
      </ul>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
   </ul>
   <p>This is a parapgraph</p>
   <p>This is a parapgraph</p>
   <ol>
      <li>This is a first ordered list</li>
      <li>This is a first ordered list</li>
      <li>This is a first ordered list</li>
      <ul>
         <li>This is a second level bullet</li>
         <li>This is a second level bullet</li>
      </ul>
   </ol>
</text>

For the second step I have added a mode to handle the wrapping of nested ul or ol:

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

  <xsl:template match="ul | ol" mode="wrap">
      <xsl:copy>
          <xsl:for-each-group select="*" group-starting-with="li">
              <xsl:copy>
                  <xsl:apply-templates select="node(), current-group() except ." mode="#current"/>
              </xsl:copy>
          </xsl:for-each-group>
      </xsl:copy>
  </xsl:template>

  <xsl:template match="text">
      <xsl:copy>
          <xsl:apply-templates select="mf:group(*, 1)" mode="wrap"/>
      </xsl:copy>
  </xsl:template>

Example at https://xsltfiddle.liberty-development.net/pPzifoZ/1 produces the output

<text>
   <p>This is a parapgraph</p>
   <ul>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
   </ul>
   <p>This is a parapgraph</p>
   <ul>
      <li>This is a first level bullet<ul>
            <li>This is a second level bullet</li>
            <li>This is a second level bullet</li>
         </ul>
      </li>
      <li>This is a first level bullet</li>
      <li>This is a first level bullet</li>
   </ul>
   <p>This is a parapgraph</p>
   <p>This is a parapgraph</p>
   <ol>
      <li>This is a first ordered list</li>
      <li>This is a first ordered list</li>
      <li>This is a first ordered list<ul>
            <li>This is a second level bullet</li>
            <li>This is a second level bullet</li>
         </ul>
      </li>
   </ol>
</text>

Full code is

<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="#all"
    version="3.0">

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

  <xsl:output method="xml" indent="yes"/>

  <xsl:param name="list-levels" as="map(xs:integer, xs:integer+)"
    select="map { 1 : (560, 561), 2: (562) }"/>

  <xsl:param name="list-map" as="map(xs:integer, xs:QName)"
    select="map { 560 : QName('', 'ol'), 561 : QName('', 'ul'), 562 : QName('', 'ul') }"/>

  <xsl:function name="mf:group" as="node()*">
      <xsl:param name="nodes" as="node()*"/>
      <xsl:param name="level" as="xs:integer"/>
      <xsl:for-each-group select="$nodes" group-adjacent="boolean(self::p[@id = $list-levels?($level to 6)])">
          <xsl:choose>
              <xsl:when test="current-grouping-key()">
                  <xsl:element name="{$list-map(xs:integer(@id))}">
                      <xsl:sequence select="mf:group(current-group(), $level + 1)"/>
                  </xsl:element>                  
              </xsl:when>
              <xsl:otherwise>
                  <xsl:apply-templates select="current-group()"/>
              </xsl:otherwise>
          </xsl:choose>
      </xsl:for-each-group>
  </xsl:function>

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

  <xsl:template match="ul | ol" mode="wrap">
      <xsl:copy>
          <xsl:for-each-group select="*" group-starting-with="li">
              <xsl:copy>
                  <xsl:apply-templates select="node(), current-group() except ." mode="#current"/>
              </xsl:copy>
          </xsl:for-each-group>
      </xsl:copy>
  </xsl:template>

  <xsl:template match="text">
      <xsl:copy>
          <xsl:apply-templates select="mf:group(*, 1)" mode="wrap"/>
      </xsl:copy>
  </xsl:template>

  <xsl:template match="p[@id = 542]">
      <xsl:copy>
          <xsl:apply-templates/>
      </xsl:copy>
  </xsl:template>

  <xsl:template match="p[@id = $list-levels?*]">
      <li>
          <xsl:apply-templates/>
      </li>
  </xsl:template>

</xsl:stylesheet>

Examples use XSLT 3 but the grouping would be the same in XSLT 2, only storing the mapping of p ids to element and list levels would need to be done in some XML structure instead of the XPath maps I have used above.

Restriction: in $level to 6 I have chosen some maximum nesting level of six but it can of course be easily adjusted.

However, choosing some smarter representation like an array would allow us to avoid having to define a maximum level, so with <xsl:param name="list-levels" as="array(xs:integer+)" select="[ (560, 561), (562) ]"/> and then <xsl:for-each-group select="$nodes" group-adjacent="boolean(self::p[@id = array:subarray($list-levels, $level)])">, as in https://xsltfiddle.liberty-development.net/pPzifoZ/2, there is no need to define a maximum level hardcoded into the XSLT.

Upvotes: 1

Related Questions