0
votes

I have an XML without structural markup and need to extract all nodes in between two nodes with specific strings. Here my XML:

    <singlenode>
  <title>C-2.6.8</title>
           <p><b>Test Objective:</b> Verify that reprinting partial labels is possible.</p>
            <p><b>Acceptance Criterion:</b> Partial unit labels are reprinted correctly.</p>
            <p><b>Prerequisite:</b> You are currently running a <b>track and trace</b> test order with
                partial label printing enabled.</p>
            <p><b>Test Note:</b></p>
            <ul type="disc">
                <li>
                    <p><b>Line:</b> This test assumes an automatic device for aggregating items to
                        cases.</p>
                </li>
            </ul>
            <p/>
    </singlenode>

In bewtween the nodes containing the strings "Test Objective" and "Acceptance Criterion" can be a combination of tags, the same goes for the tags between the node containing "Acceptance Criterion" and "Prerequisite". How do I extrac all tags until the node contains a specific string?

I have tried to name all tags that might occur, but due to the wild combination of tags, this gives unwanted results.

My XSLT (3.0):

        <testcases>
            <testcase> 
                <xsl:attribute name="internalid"/>
                <xsl:attribute name="name">
                <xsl:value-of select="title"/>
            </xsl:attribute>

        <node_order/>
        <externalid/>
        <version/>
                <summary>  <xsl:text disable-output-escaping="yes">&lt;![CDATA[</xsl:text>
                    <xsl:for-each select="/singlenode/p/b[contains(text(),'Test Objective') or contains(text(),'Test Objectives')]">

                        <xsl:copy-of select=".."/> 
                        <xsl:copy-of select="/singlenode/p/b[contains(text(),'Test Objective') or contains(text(),'Test Objectives')]/following::ul[1]"/>
                        <xsl:copy-of select="/singlenode/p/b[contains(text(),'Test Objective') or contains(text(),'Test Objectives')]/following::ol[1]"/>


                    </xsl:for-each>     
                    <xsl:for-each select="/singlenode/p/b[contains(text(),'Acceptance Criterion') or contains(text(),'Acceptance Criteria')]">

                        <xsl:copy-of select=".."/> 
                        <xsl:copy-of select="/singlenode/p/b[contains(text(),'Acceptance Criterion') or contains(text(),'Acceptance Criteria')]/following::ul[1]"/>
                        <xsl:copy-of select="/singlenode/p/b[contains(text(),'Acceptance Criterion') or contains(text(),'Acceptance Criteria')]/following::ol[1]"/>

                    </xsl:for-each>
                    <xsl:for-each select="/singlenode/p/b[contains(text(),'Test Instruction') or contains(text(),'Test Instructions')]">

                        <xsl:copy-of select=".."/>   
                        <xsl:copy-of select="/singlenode/p/b[contains(text(),'Test Instruction') or contains(text(),'Test Instructions')]/following::ul[1]"/>
                        <xsl:copy-of select="/singlenode/p/b[contains(text(),'Test Instruction') or contains(text(),'Test Instructions')]/following::ol[1]"/>


                    </xsl:for-each>
                    <xsl:for-each select="/singlenode/p/b[contains(text(),'Test Notes') or contains(text(),'Note') or contains(text(),'Notes' )or contains(text(),'Test notes')]">

                        <xsl:copy-of select=".."/>    
                        <xsl:copy-of select="/singlenode/p/b[contains(text(),'Test Notes') or contains(text(),'Note') or contains(text(),'Notes' )or contains(text(),'Test notes')]/following::ul[1]"/>
                        <xsl:copy-of select="/singlenode/p/b[contains(text(),'Test Notes') or contains(text(),'Note') or contains(text(),'Notes' )or contains(text(),'Test notes')]/following::ol[1]"/>

                    </xsl:for-each>
                    <xsl:text disable-output-escaping="yes">]]&gt;</xsl:text>                   
</summary>
        <preconditions>
            <xsl:for-each select="/singlenode/p/b[contains(text(),'Prerequisite') or contains(text(), 'Prerequisites')]">
                <xsl:text disable-output-escaping="yes">&lt;![CDATA[&lt;p&gt;</xsl:text>
                <xsl:copy-of select=".."/>    
                <xsl:copy-of select="/singlenode/p/b[contains(text(),'Prerequisite') or contains(text(), 'Prerequisites')]/following::ul[1]"/>
                <xsl:copy-of select="/singlenode/p/b[contains(text(),'Prerequisite') or contains(text(), 'Prerequisites')]/following::ol[1]"/>
                <xsl:text disable-output-escaping="yes">&lt;/p&gt;]]&gt;</xsl:text>

                    </xsl:for-each> </preconditions>
   </testcase>
        </testcases>
    </xsl:template>

Gives me the result:

 <testcases>
       <testcase internalid=""
                 name="C-2.6.8">
          <node_order/>
          <externalid/>
          <version/>
          <summary><![CDATA[<p>
                <b>Test Objective:</b> Verify that reprinting partial labels is possible.</p>
             <ul type="disc">
                <li>
                   <p>
                      <b>Line:</b> This test assumes an automatic device for aggregating items to
                    cases.</p>
                </li>
             </ul>
             <p>
                <b>Acceptance Criterion:</b> Partial unit labels are reprinted correctly.</p>
             <ul type="disc">
                <li>
                   <p>
                      <b>Line:</b> This test assumes an automatic device for aggregating items to
                    cases.</p>
                </li>
             </ul>
             <p>
                <b>Test Note:</b>
             </p>
             <ul type="disc">
                <li>
                   <p>
                      <b>Line:</b> This test assumes an automatic device for aggregating items to
                    cases.</p>
                </li>
             </ul>]]></summary>
          <preconditions><![CDATA[<p><p>
                <b>Prerequisite:</b> You are currently running a <b>track and trace</b> test order with
            partial label printing enabled.</p>
             <ul type="disc">
                <li>
                   <p>
                      <b>Line:</b> This test assumes an automatic device for aggregating items to
                    cases.</p>
                </li>
             </ul></p>]]></preconditions>
      </testcase>
    </testcases>

Expected result is that the "Test Note" shall be written only into the summary.

How do I extract exactly the nodes between the <p>s with strings "Test Objective", "Acceptance Criterion" and "Prerequisites"?

1
XPath has operators << and >> to select based on document order (simply remember that inside XSLT code you need to escape any < as &lt;. Often such problems can also be solved using a combinatation of for-each-group starting-with/ending-with. I have not quite understood which result you want, you might want to simplify the samples but show the exact result you want, perhaps in a first step without the CDATA stuff as that, with XSLT 3, is probably better solved by using the serialize function and cdata-section-elements, once we have established which nodes you want to select. - Martin Honnen
Thanx! I find very little nformation on the web on the syntax of for-each-group and how to use the <<>> operators in XPATH. Do you have any reading tips for that? - Avela
The syntax of XSLT and XPath is specified by the W3C so claiming it is not "on the web" is rather odd, w3.org/TR/xpath-31/#id-node-comparisons is online, w3.org/TR/xslt-30/#grouping as well. Companies like Saxon or Altova that produce XSLT processors also have their documentations online, altova.com/training/xpath3/… or saxonica.com/html/documentation/expressions/comparisons.html, saxonica.com/html/documentation/xsl-elements/… - Martin Honnen

1 Answers

0
votes

I think I see what you're after. Try this XSL:

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

    <xsl:variable name="sections" select="'Objective|Acceptance|Instruction|Note|Prerequisite'"/>
    <xsl:variable name="total.num.sections" select="count(//p[b[matches(., $sections)]])"/>

    <xsl:template match="/">
        <testcases>
            <testcase> 
                <xsl:attribute name="internalid"/>
                <xsl:attribute name="name">
                    <xsl:value-of select="title"/>
                </xsl:attribute>

                <node_order/>
                <externalid/>
                <version/>
                <summary>
                    <xsl:text disable-output-escaping="yes">&lt;![CDATA[</xsl:text>

                    <xsl:apply-templates select="//p[b[matches(., 'Objective')]]"/>
                    <xsl:apply-templates select="//p[b[matches(., 'Acceptance')]]"/>
                    <xsl:apply-templates select="//p[b[matches(., 'Instruction')]]"/>
                    <xsl:apply-templates select="//p[b[matches(., 'Note')]]"/>

                    <xsl:text disable-output-escaping="yes">]]&gt;</xsl:text>                   
                </summary>
                <preconditions>               
                    <xsl:apply-templates select="//p[b[matches(., 'Prerequisite')]]"/>
                </preconditions>
            </testcase>
        </testcases>
    </xsl:template>

    <xsl:template match="p[b[matches(., $sections)]]">
        <xsl:if test="contains(., 'Prerequisite')">
            <xsl:text disable-output-escaping="yes">&lt;![CDATA[&lt;p&gt;</xsl:text>
        </xsl:if>

        <xsl:copy-of select="."/>
        <xsl:variable name="pos" select="count(preceding::p[b])+1"/>

        <xsl:variable name="content.between.sections" select="following::p[b[matches(., $sections)]][1]/preceding::*[preceding::p[b[matches(., $sections)]][$pos]]"/>

        <xsl:choose>
            <xsl:when test="$pos = $total.num.sections">
                <!-- Last section -->
                <xsl:copy-of select="following-sibling::*"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:copy-of select="$content.between.sections"/>
            </xsl:otherwise>
        </xsl:choose>

        <xsl:if test="contains(., 'Prerequisite')">
            <xsl:text disable-output-escaping="yes">&lt;/p&gt;]]&gt;</xsl:text>
        </xsl:if>
    </xsl:template>

</xsl:stylesheet>

Run on your sample data, it gives this output:

<?xml version="1.0" encoding="UTF-8"?>
<testcases>
    <testcase internalid="" name="">
        <node_order/>
        <externalid/>
        <version/>
        <summary><![CDATA[<p><b>Test Objective:</b> Verify that reprinting partial labels is possible.</p><p><b>Acceptance Criterion:</b> Partial unit labels are reprinted correctly.</p><p><b>Test Note:</b></p><ul type="disc">
        <li>
            <p><b>Line:</b> This test assumes an automatic device for aggregating items to cases.</p>
        </li>
    </ul><p/>]]></summary>
        <preconditions><![CDATA[<p><p><b>Prerequisite:</b> You are currently running a <b>track and trace</b> test order with partial label printing enabled.</p></p>]]></preconditions>
    </testcase>
</testcases>

...which I believe is what you were after.

Commentary on technique:

  • Doesn't require XSLT 3.0, but might very well benefit from it.

  • Utilizes a catch-all template which can handle all possible section "headers" or "cues", with special logic inserted for the last-section case, as well as the Prerequisite case, which gets its own CDATA wrapping according to your original intent. As Martin Honnen said, you might very well benefit from XSLT 3.0 there.

  • Can handle your unstructured data in any order in terms of how the section cues appear, and if what I gleaned from your samples is somehow slightly off, I think it easy for you to tweak at this point, as you can now benefit from the techniques I've employed.

  • Also notice the magic in the content.between.sections variable. That lovely xpath, translated into English, takes everything after an encountered cue but before the next cue. It does so dynamically via that all-important pos variable, which figures out exactly where the cue is in terms of numerical position.

NOTE: If you have further content to process after content that is considered part of the last section, you can account for that with the following change to the <xsl:copy-of> beneath the "Last section" comment. In the case of content enclosed in <table> tags:

<!-- Last section -->
<xsl:copy-of select="following-sibling::*[not(name() = 'table')]"/>

Then just define your table template and call it wherever.

Hope that helps!