Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

XSLT create element in order when sibling elements arbitrarily exist?

Tags:

xml

xslt

Suppose you have a element with a content model as follows:

<!ELEMENT wrapper (a*,b*,c*,d*,e*,f*,g*,h*,i*,j*,k*,l*,m*,n*,o*,p*,q*,r*,s*,t*,u*,v*,w*,x*,y*,z*)>

In other words, within a wrapper element there is a specified order of child elements which may arbitrarily exist.

You need to create a new element (e.g. m) within the wrapper whilst preserving what was already there and ensuring that the output conforms to the content model.

This is sort of a solution:

<xsl:template match="@*|node()">
  <xsl:sequence select="."/>
</xsl:template>

<xsl:template match="wrapper">
  <xsl:copy>
    <xsl:apply-templates select="a,b,c,d,e,f,g,h,i,j,k,l,m"/>
    <m>This is new</m>
    <xsl:apply-templates select="n,o,p,q,r,s,t,u,v,w,x,y,z"/>
  </xsl:copy>
</xsl:template>

However, this solution will drop all whitespace, comments, or processing instructions within the wrapper element. I've come up with some solutions that don't drop those things but none that I'm happy with.

What's the most elegant solution to this problem that won't drop nodes? XSLT 3 and schema-aware solutions are fine.

Here are some example inputs and outputs:

<!-- Input -->
<wrapper/>

<!-- Output -->
<wrapper><m>This is new</m></wrapper>

<!-- Input -->
<wrapper><a/><z/></wrapper>

<!-- Output -->
<wrapper><a/><m>This is new</m><z/></wrapper>

<!-- Input -->
<wrapper><m/></wrapper>

<!-- Output -->
<wrapper><m/><m>This is new</m></wrapper>

<!-- Input -->
<wrapper>
  <a/>
  <!-- put m here -->
  <z/>
</wrapper>

<!-- Output -->
<!-- NB: The comment and whitespace could come after the inserted element instead. This case is ambiguous -->
<wrapper>
  <a/>
  <!-- put m here --><m>This is new</m>
  <z/>
</wrapper>

<!-- Input -->
<wrapper>
  <a/>
  <b/>
  <c/>
  <n/>
  <o/>
  <!-- p is not here -->
  <?do not drop this?>
</wrapper>

<!-- Output -->
<wrapper>
  <a/>
  <b/>
  <c/><m>This is new</m>
  <n/>
  <o/>
  <!-- p is not here -->
  <?do not drop this?>
</wrapper>

It's not critical that non-element nodes around the inserted element come before or after, just that they aren't dropped and their order with respect to the original elements is preserved.

like image 610
nine9ths Avatar asked Feb 14 '26 17:02

nine9ths


1 Answers

Here's one way you could look at it:

XSLT 2.0

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

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

<xsl:template match="wrapper">
    <xsl:variable name="all" select="node()"/>
    <xsl:variable name="middle" select="(n,o,p,q,r,s,t,u,v,w,x,y,z)[1]"/>
    <xsl:variable name="i" select="if($middle) then count($all[. &lt;&lt; $middle]) else count($all)"/>
    <xsl:variable name="new">
        <m>This is new</m>
    </xsl:variable>
    <xsl:copy>
        <xsl:apply-templates select="insert-before($all, $i+1, $new) "/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

Or, if you prefer:

<xsl:template match="wrapper">
    <xsl:variable name="all" select="node()"/>
    <xsl:variable name="middle" select="(a,b,c,d,e,f,g,h,i,j,k,l,m)[last()]"/>
    <xsl:variable name="i" select="if($middle) then index-of($all/generate-id(), generate-id($middle)) else 0"/>
    <xsl:variable name="new">
        <m>This is new</m>
    </xsl:variable>
    <xsl:copy>
        <xsl:apply-templates select="insert-before($all, $i+1, $new) "/>
    </xsl:copy> 
</xsl:template>
like image 64
michael.hor257k Avatar answered Feb 16 '26 06:02

michael.hor257k



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!