Joe
Joe

Reputation: 1399

How to count elements ignoring whether they are children or siblings?

This might be a novice question but, then again, I'm a novice :-)

OLD SITUATION:

I have XML files in this format:

<chapter>
  <title>chapter title</title>
  <para>p1</para>
  <para>p2</para>
  <para>p3</para>

    <!-- ....and so on for a lot of para elements -->

</chapter>

...which are handled by an XSL template like this...

<xsl:template match="para">   
   <xsl:if test="count(following-sibling::para) = 1 and count(preceding-sibling::para) &gt; 13">
       <!-- Insert Some Stuff -->
   </xsl:if>
</xsl:template>

The logic here is: "Insert Some Stuff" before the second-to-last element if there are enough preceding <para> siblings.

This code gets the job done and works fine.


NEW SITUATION

Now, for reasons beyond my control, I need to adapt that template to handle the following kind of files as well...

<chapter>
  <title>chapter title</title>
  <para>p1</para>
  <para>p2</para>
  <para>p3</para>
  <section>
     <para>p4</para>
     <para>p5</para>
  </section>
  <para>p6</para>
  <section>
     <title>section title</title> 
  </section>
  <para>p7</para>
  <section>
     <para>p8</para>
  </section>

  <!-- ....and so on for a lot of para elements -->

</chapter>

The difference with these files is that the <section> element can appear randomly, sometimes containing <para> elements and sometimes not. There is no way of predicting when and where <section> elements will appear.

I need the original XSL template to work with this format in the same way despite the <section> elements being there. So that means that the <para> elements still need to be counted in the same way even if they are sometimes children and sometimes siblings.

In summary, I need the logic of the old code to work as before, as though it is completely ignoring the presence of the <section> elements.

What should the original template <xsl:if test="..."> be rewritten as to make this work?

Performance is not an issue - these are manually-run ad-hoc transformations.

XSLT 1.0 only, please

Thanks.

Upvotes: 0

Views: 75

Answers (1)

michael.hor257k
michael.hor257k

Reputation: 116959

It is difficult to answer your question without seeing a complete stylesheet and the expected output/s.

Would not:

<xsl:template match="para">   
   <xsl:if test="count(following::para) = 1 and count(preceding::para) &gt; 13">
       <!-- Insert Some Stuff -->
   </xsl:if>
</xsl:template>

work for you?


Added:

If you're sure that what you've suggested should work for the example given in the question

Well, the example given in the question does not have the required minimum of para nodes - but if we reduce the threshold:

XSLT 1.0

<xsl:stylesheet version="1.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="para"> 
    <xsl:if test="count(following::para) = 1 and count(preceding::para) &gt; 5">
        <INSERTED_NODE/>
    </xsl:if>
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

Test Input A

<chapter>
  <title>Section Title 1</title>
  <para>Para 1</para>
  <para>Para 2</para>
  <para>Para 3</para>
  <section>
    <title>Section Title 2</title>
    <para>Para 4</para>
    <para>Para 5</para>
  </section>
  <para>Para 6</para>
  <section>
    <title>Section Title 3</title>
    <para>Para 7</para>
    <para>Para 8</para>
  </section>
  <para>Para 9</para>
  <para>Para 10</para>
</chapter>

Result A

<?xml version="1.0" encoding="UTF-8"?>
<chapter>
   <title>Section Title 1</title>
   <para>Para 1</para>
   <para>Para 2</para>
   <para>Para 3</para>
   <section>
      <title>Section Title 2</title>
      <para>Para 4</para>
      <para>Para 5</para>
   </section>
   <para>Para 6</para>
   <section>
      <title>Section Title 3</title>
      <para>Para 7</para>
      <para>Para 8</para>
   </section>
   <INSERTED_NODE/>
   <para>Para 9</para>
   <para>Para 10</para>
</chapter>

Test Input B

<chapter>
  <title>Section Title 1</title>
  <para>Para 1</para>
  <para>Para 2</para>
  <para>Para 3</para>
  <section>
    <title>Section Title 2</title>
    <para>Para 4</para>
    <para>Para 5</para>
  </section>
  <para>Para 6</para>
  <section>
    <title>Section Title 3</title>
    <para>Para 7</para>
    <para>Para 8</para>
  </section>
  <para>Para 9</para>
</chapter>

Result B

<?xml version="1.0" encoding="UTF-8"?>
<chapter>
   <title>Section Title 1</title>
   <para>Para 1</para>
   <para>Para 2</para>
   <para>Para 3</para>
   <section>
      <title>Section Title 2</title>
      <para>Para 4</para>
      <para>Para 5</para>
   </section>
   <para>Para 6</para>
   <section>
      <title>Section Title 3</title>
      <para>Para 7</para>
      <INSERTED_NODE/>
      <para>Para 8</para>
   </section>
   <para>Para 9</para>
</chapter>

Edit

Is there a way to force it to be inserted once per chapter element?

Yes, you could do it this way:

<xsl:template match="para"> 
    <xsl:variable name="i">
        <xsl:number count="para" from="chapter" level="any"/>
    </xsl:variable>
    <xsl:variable name="n" select="count(ancestor::chapter//para)" />   
    <xsl:if test="$i + 1 = $n and $n &gt; 5">
        <INSERTED_NODE/>
    </xsl:if>
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

A more efficient solution would do the counting once at the chapter level, and pass it down as a parameter to the paras.

Upvotes: 1

Related Questions