2
votes

Given a deeply nested XML tree, I want to find a certain element. At that point I want to wrap X in a new element that is at the same level as a higher element. I then want to continue on with the rest of the original tree from the point after the "certain" element.

For example, given this input:

<root>
    <branch att="yo">
        <div stuff="no">
            <ul>
                <li>Item 1</li>
                <li>Item 2</li>
                <li>Item 3</li>
            </ul>
        </div>        
    </branch>
</root>

I want to find Item 2 in the <ul> (easy peasy). I want to insert a new branch-level element before item 2. And then I want to continue on with item 2 (so continuing the ancestor nodes). That is, I want this output:

<root>
    <branch att="yo">
        <div stuff="no">
            <ul>
                <li>Item 1</li>
            </ul>
        </div>
    </branch>
    <branch>
        <div>
            <p>New branch here</p>
        </div>
    </branch>
    <branch att="yo">
        <div stuff="no">
            <ul>
                <li>Item 2</li>
                <li>Item 3</li>
            </ul>
        </div>        
    </branch>
</root>

I am having a hard problem getting started with this in order to make a generalized solution. I'm thinking it will involve either multiple modes or keys along with the processing of ancestor nodes to find node names and attributes. Any help is appreciated.

Alright this is what I have so far. It partially works (e.g., I copy some nodes but no attributes; I copy too many nodes as well but it is a start I think). My thinking here is that I need a recursive function. I start at the furthest ancestor I'm concerned with (branch) and for each child node whose ultimate descendant is the certain element (li "item 2"), I copy each node and its attributes, appending it to the previous node (that's what newNewTree is for). And I recurse until I reach the certain element (li's Item 2), at which point I return the NewNewTree variable, which my intent is for it to be a tree of the right ancestor elements (with no text). To make this work I think what I need to do is process each node from the function through an identity template override that copies the node and its attributes. But too tired to attempt that tonight.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                xmlns:my="http://www.example.com"
                exclude-result-prefixes="xs my"
                version="2.0">
  <xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="/">
    <xsl:sequence select="$origNodesNoText" />
  </xsl:template>
  <xsl:variable name="ancestorElemName" select="'div'" />
  <xsl:variable name="origNodesNoText">
    <xsl:apply-templates select="/" mode="origNodesNoText"/>
  </xsl:variable>
  <xsl:template match="text()" mode="origNodesNoText"/>
  <xsl:template match="li[.='Item 2']" mode="origNodesNoText">
    <xsl:variable name="newTree">
      <xsl:element name="{local-name(ancestor::*[local-name()=$ancestorElemName][1])}">
        <xsl:for-each select="ancestor::*[local-name()=$ancestorElemName][1]/attribute::*">
          <xsl:attribute name="{local-name()}" select="."/>
        </xsl:for-each>
      </xsl:element>
    </xsl:variable>
    <xsl:sequence select="my:split(ancestor::*[local-name()=$ancestorElemName][1],.,$newTree)" />
  </xsl:template>

  <xsl:function name="my:split">
    <xsl:param name="ancestorElemNode" />
    <xsl:param name="callingNode" />
    <xsl:param name="newTree" />
    <xsl:message>Calling my split</xsl:message>
    <xsl:choose>
        <xsl:when test="$ancestorElemNode ne $callingNode">
            <xsl:message>Found a node to copy its name and attributes</xsl:message>
            <xsl:variable name="newNewTree">
                <xsl:for-each select="$newTree/node()">
                    <xsl:copy /> <!-- doesn't copy attribute nodes so not what we want -->
                </xsl:for-each>
                <xsl:element name="{local-name($ancestorElemNode)}">
                    <xsl:for-each select="$ancestorElemNode/attribute::*">
                        <xsl:attribute name="{local-name()}" select="."/>
                    </xsl:for-each>
                </xsl:element>    
            </xsl:variable>
            <xsl:sequence select="my:split($ancestorElemNode/child::*[descendant::*[. eq $callingNode]][1],$callingNode,$newNewTree)" />
        </xsl:when>
        <xsl:otherwise>
            <xsl:message>Found the end point</xsl:message>
            <xsl:sequence select="$newTree" />
        </xsl:otherwise>
    </xsl:choose>
  </xsl:function>

</xsl:stylesheet>
2

2 Answers

0
votes

You wish to process the nodes of the input in the following way. First, let's define two terms for nodes we'll need to refer to often:

  • The second list item is our target element; we'll call it the Bullseye.
  • The branch element enclosing the Bullseye will need to be split; we'll call it Banana.

Now we can define several classes of node in the input tree, in terms of their correspondences to the output tree.

  • Everything outside the Banana is copied as in an identity transform; it corresponds to a node of the same type in the output, with the same name, and with one exception it has a similar sequence of attributes and children. The exception is the parent of the Banana: its sequence of children contains two elements corresponding to the Banana, and a third element (here named branch) between them.
  • Every element which is an ancestor of the Bullseye and a descendant of the Banana (or the Banana itself) corresponds to two elements in the output tree, with similar names and attributes.
  • Every descendant of the Banana which precedes the Bullseye in document order and is not an ancestor of the Bullseye and is not an attribute on an ancestor of the Bullseye is copied once to the output as in an identity transform, within the first copy of the Banana.
  • Every descendant of the Banana which follows the Bullseye in document order and is not an ancestor of the Bullseye and is not an attribute on an ancestor of the Bullseye is copied once to the output as in an identity transform, within the second copy of the Banana. So is the Bullseye itself.

I'd do this with a near-identity transform. We'll need special treatment for the Banana and its descendants. Some of them will need to be copied twice, and some only once, but the simplest thing to do will be just to process the entire subtree twice.

The stylesheet as a whole might look something like this:

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

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

  <xsl:variable name="Bullseye" select="//li[.='Item 2']"/>

  <!--* Default identity transform *-->
  <xsl:template match="comment()">
    <xsl:comment><xsl:value-of select="."/></xsl:comment>
  </xsl:template>

  <xsl:template match="processing-instruction()">
    <xsl:variable name="pitarget" select="name()"/>
    <xsl:processing-instruction name="{$pitarget}">
      <xsl:value-of select="."/>
    </xsl:processing-instruction>
  </xsl:template>

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

  <!--* Banana *-->
  <xsl:template match="branch[descendant::* intersect $Bullseye]">    
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" mode="pre-bullseye"/>
    </xsl:copy>
    <xsl:call-template name="insertion"/>
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" mode="post-bullseye"/>
    </xsl:copy>
  </xsl:template>

  <!--* first pass over subtree rooted at Banana *-->
  <xsl:template match="comment()" mode="pre-bullseye">
    <xsl:apply-templates select="."/>
  </xsl:template>
  <xsl:template match="processing-instruction()" mode="pre-bullseye">
    <xsl:apply-templates select="."/>
  </xsl:template>
  <xsl:template match="@*|*|text()" mode="pre-bullseye">
    <xsl:choose>
      <xsl:when test="descendant::* intersect $Bullseye">
        <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:apply-templates select="node()" mode="pre-bullseye"/>
        </xsl:copy>
      </xsl:when>
      <xsl:when test=". &lt;&lt; $Bullseye">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
      </xsl:when>
      <xsl:when test=". is $Bullseye"/>      
      <xsl:when test=". >> $Bullseye"/>
      <xsl:otherwise>
        <xsl:message terminate="yes"
          >Unexpected case in mode pre-bullseye, dying now.</xsl:message>
      </xsl:otherwise>
    </xsl:choose>

  </xsl:template>

  <!--* Second pass over subtree rooted at Banana *-->
  <xsl:template match="comment()" mode="post-bullseye">
    <xsl:apply-templates select="."/>
  </xsl:template>
  <xsl:template match="processing-instruction()" mode="post-bullseye">
    <xsl:apply-templates select="."/>
  </xsl:template>
  <xsl:template match="@*|*|text()" mode="post-bullseye">
    <xsl:choose>
      <xsl:when test="descendant::* intersect $Bullseye">
        <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:apply-templates select="node()" mode="post-bullseye"/>
        </xsl:copy>
      </xsl:when>
      <xsl:when test=". &lt;&lt; $Bullseye"/>

      <xsl:when test=". is $Bullseye">
        <xsl:copy-of select="."/>
      </xsl:when>
      <xsl:when test=". >> $Bullseye">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
      </xsl:when>
      <xsl:otherwise>
        <xsl:message terminate="yes"
         >Unexpected case in post-bullseye, dying now.</xsl:message>
      </xsl:otherwise>
    </xsl:choose>

  </xsl:template>

  <xsl:template name="insertion">
    <branch>
      <div>
        <p>New branch here.</p>
      </div>
    </branch>
  </xsl:template>

</xsl:stylesheet>

When run on the input you provide, this stylesheet produces the following output.

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <branch att="yo">
        <div stuff="no">
            <ul>
                <li>Item 1</li>
                </ul>
      </div>
   </branch>
   <branch>
      <div>
         <p>New branch here.</p>
      </div>
   </branch>
   <branch>
      <div stuff="no">
         <ul>
            <li>Item 2</li>
                <li>Item 3</li>
            </ul>
        </div>        
    </branch>
</root>
0
votes

Here's another option. Basically you:

  • Define a value for the target element (the target param).
  • Identify the target element (the key branch) and apply the templates for that element. (If there is more than one element with that value, it branches on the first occurrence.)
  • Apply templates for the preceding-siblings starting at top. Note: The "top" is /*/*. If the root element has more than one child, this stylesheet will need some tweaking.
  • Apply the templates to the target element. We pass a param (branch) so we know that this is where the new branch is inserted.
  • Apply the templates to the target element again (without the branch param) along with the following-siblings.

XML Input

<root>
    <branch att="yo">
        <div stuff="no">
            <ul>
                <li>Item 1</li>
                <li>Item 2</li>
                <li>Item 3</li>
            </ul>
        </div>        
    </branch>
</root>

XSLT 2.0

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

    <xsl:param name="target" select="'Item 2'"/>
    <xsl:key name="branch" match="*[.=$target]" use="."/>

    <xsl:template match="/*">
        <xsl:copy>
            <xsl:apply-templates select="@*|(//*[key('branch',.)])[1]"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="*">
        <xsl:variable name="precID" select="for $id in preceding-sibling::* return generate-id($id)"/>
        <xsl:variable name="follID" select="for $id in (self::*,following-sibling::*) return generate-id($id)"/>
        <xsl:apply-templates select="/*/*" mode="ident">
            <xsl:with-param name="restricted2IDs" select="$precID" tunnel="yes"/>           
        </xsl:apply-templates>
        <xsl:apply-templates select="/*/*" mode="ident">
            <xsl:with-param name="restricted2IDs" select="generate-id(current())" tunnel="yes"/>
            <xsl:with-param name="branch" select="';-)'" tunnel="yes"/>
        </xsl:apply-templates>
        <xsl:apply-templates select="/*/*" mode="ident">
            <xsl:with-param name="restricted2IDs" select="$follID" tunnel="yes"/>           
        </xsl:apply-templates>      
    </xsl:template>

    <xsl:template match="@*|node()" mode="ident">
        <xsl:param name="restricted2IDs" tunnel="yes"/>
        <xsl:param name="branch" tunnel="yes"/>
            <xsl:choose>
                <xsl:when test="*[generate-id()=$restricted2IDs]">
                    <xsl:choose>
                        <xsl:when test="$branch">
                            <p>New branch here</p>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:copy>
                                <xsl:apply-templates select="@*|node()[generate-id()=$restricted2IDs]" mode="ident"/>                                                       
                            </xsl:copy>                         
                        </xsl:otherwise>
                    </xsl:choose>
                </xsl:when>
                <xsl:when test="descendant::*[generate-id()=$restricted2IDs]">
                    <xsl:copy>
                        <!--If you want the attributes in the new branch, remove the "if" and just use "@*|node".-->
                        <xsl:apply-templates select="if ($branch) then node() else @*|node()" mode="ident"/>                        
                    </xsl:copy>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:copy>
                        <xsl:apply-templates select="@*|node()" mode="ident"/>
                    </xsl:copy>
                </xsl:otherwise>
            </xsl:choose>
    </xsl:template>

</xsl:stylesheet>

XML Output

<root>
   <branch att="yo">
      <div stuff="no">
         <ul>
            <li>Item 1</li>
         </ul>
      </div>
   </branch>
   <branch>
      <div>
         <p>New branch here</p>
      </div>
   </branch>
   <branch att="yo">
      <div stuff="no">
         <ul>
            <li>Item 2</li>
            <li>Item 3</li>
         </ul>
      </div>
   </branch>
</root>