0

This is the first time I have attempted to use XSL, but from my research this looked like the best method. I have a number of files to convert. I am planning on using the notepad++ xmltools for the conversion. If there is another solution to my issue I am open to it.

I need to convert this format of a XML file:

<?xml version="1.0" encoding="UTF-8"?>

<testcases>
<testcase name="Simple">
    <steps><![CDATA[<p>1. do something</p>
<p>2. do more</p>
<p>3. even more</p>]]></steps>
    <expectedresults><![CDATA[<p>1. result</p>
<p>2. more result</p>
<p>3 again</p>]]></expectedresults>
</testcase>
</testcases>

Into this end format:

<?xml version="1.0" encoding="UTF-8"?>
<testcases>
<testcase name="Simple new">
<steps>
 <step>
    <step_number><![CDATA[1]]></step_number>
    <actions><![CDATA[<p>step 1</p>]]></actions>
    <expectedresults><![CDATA[<p>do something</p>]]></expectedresults>
    <execution_type><![CDATA[1]]></execution_type>
 </step>

 <step>
    <step_number><![CDATA[2]]></step_number>
    <actions><![CDATA[<p>step 2</p>]]></actions>
    <expectedresults><![CDATA[<p>do more</p>]]></expectedresults>
    <execution_type><![CDATA[1]]></execution_type>
 </step>
  <step>
    <step_number><![CDATA[3]]></step_number>
    <actions><![CDATA[<p>step 3</p>]]></actions>
    <expectedresults><![CDATA[<p>even more</p>]]></expectedresults>
    <execution_type><![CDATA[1]]></execution_type>
 </step>
</steps>
</testcase>
</testcases>

Not all test cases will have multiple steps, and expected results.

I found this in another thread: http://xsltfiddle.liberty-development.net/gWmuiHV great tool for this process.

My XSL so far is not working great. I am only getting the expected results block. This occurs whether I add expected results code block or not.

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:template match="steps">
   <xsl:for-each select="p">
      <xsl:copy>
        <xsl:apply-templates select="p"/>
      </xsl:copy>
    </xsl:for-each>

   <!-- <xsl:for-each select="expectedresults">
      <xsl:copy>
        <xsl:apply-templates select="p"/>
      </xsl:copy>
    </xsl:for-each>-- I get the same results whether this code is included or not. >

 </xsl:template>
 </xsl:stylesheet>

But I am only get this for output:

<?xml version="1.0" encoding="utf-16"?>


    &lt;p&gt;1. result&lt;/p&gt;
&lt;p&gt;2. more result&lt;/p&gt;
&lt;p&gt;3 again&lt;/p&gt;

These files will be imported into Testlink not used for html.

3 Answers 3

1

Transforming your input XML to your desired output XML requires some serious contortions:

  1. Decoding the CDATA sections into an xsl:variable with parse-xml-fragment
  2. Get the current index of these steps|expectedresults elements with

    count(preceding-sibling::*)+1
    
  3. Iterate over the p elements

  4. Compartmentalise the string into the relevant parts
  5. Output the elements with their values wrapped in CDATA sections (here the <p> element has to be escaped)

This gives you the following XSLT-3.0 code:

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0" >
    <xsl:output method="xml" indent="yes" cdata-section-elements="step_number actions expectedresults execution_type" />

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

    <xsl:template match="steps|expectedresults">
        <xsl:variable name="st"  select="parse-xml-fragment(.)" />
        <xsl:variable name="pos" select="count(preceding-sibling::*)+1" />
        <steps>
            <xsl:for-each select="$st/p">
                <step>
                    <xsl:variable name="cur" select="substring-before(translate(.,'.','  '),' ')" />
                    <step_number>
                        <xsl:value-of select="$cur" />
                    </step_number>
                    <actions><xsl:value-of select="concat('&lt;p&gt;','step ',$cur,'&lt;/p&gt;')" /></actions>                    
                    <expectedresults>
                        <xsl:value-of select="concat('&lt;p&gt;',normalize-space(substring-after(.,' ')),'&lt;/p&gt;')" />
                    </expectedresults>
                    <execution_type>
                        <xsl:value-of select="$pos" />
                    </execution_type>
                </step>
            </xsl:for-each>
        </steps>
    </xsl:template>

</xsl:stylesheet>

The output is:

<?xml version="1.0" encoding="UTF-8"?>
<testcases>
    <testcase name="Simple">
        <steps>
            <step>
                <step_number><![CDATA[1]]></step_number>
                <actions><![CDATA[<p>step 1</p>]]></actions>
                <expectedresults><![CDATA[<p>do something</p>]]></expectedresults>
                <execution_type><![CDATA[1]]></execution_type>
            </step>
            <step>
                <step_number><![CDATA[2]]></step_number>
                <actions><![CDATA[<p>step 2</p>]]></actions>
                <expectedresults><![CDATA[<p>do more</p>]]></expectedresults>
                <execution_type><![CDATA[1]]></execution_type>
            </step>
            <step>
                <step_number><![CDATA[3]]></step_number>
                <actions><![CDATA[<p>step 3</p>]]></actions>
                <expectedresults><![CDATA[<p>even more</p>]]></expectedresults>
                <execution_type><![CDATA[1]]></execution_type>
            </step>
        </steps>
        <steps>
            <step>
                <step_number><![CDATA[1]]></step_number>
                <actions><![CDATA[<p>step 1</p>]]></actions>
                <expectedresults><![CDATA[<p>result</p>]]></expectedresults>
                <execution_type><![CDATA[2]]></execution_type>
            </step>
            <step>
                <step_number><![CDATA[2]]></step_number>
                <actions><![CDATA[<p>step 2</p>]]></actions>
                <expectedresults><![CDATA[<p>more result</p>]]></expectedresults>
                <execution_type><![CDATA[2]]></execution_type>
            </step>
            <step>
                <step_number><![CDATA[3]]></step_number>
                <actions><![CDATA[<p>step 3</p>]]></actions>
                <expectedresults><![CDATA[<p>again</p>]]></expectedresults>
                <execution_type><![CDATA[2]]></execution_type>
            </step>
        </steps>
    </testcase>
</testcases>
Sign up to request clarification or add additional context in comments.

Comments

1

I think in XSLT 3 you want to parse the contents of the two elements, merge them and then serialize them back:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    expand-text="yes"
    version="3.0">

  <xsl:output indent="yes" cdata-section-elements="actions expectedresults"/>

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

  <xsl:accumulator name="step-count" as="xs:integer" initial-value="0">
      <xsl:accumulator-rule match="p" select="$value + 1"/>
  </xsl:accumulator>

  <xsl:template match="testcase">
      <testcase name="{@name} new">
          <steps>
              <xsl:merge>
                  <xsl:merge-source select="parse-xml-fragment(steps)/*">
                      <xsl:merge-key select="accumulator-before('step-count')"/>
                  </xsl:merge-source>
                  <xsl:merge-source select="parse-xml-fragment(expectedresults)/*">
                      <xsl:merge-key select="accumulator-before('step-count')"/>
                  </xsl:merge-source>
                  <xsl:merge-action>
                      <step>
                          <step_number>{position()}</step_number>
                          <actions>{serialize(current-merge-group()[1])}</actions>
                          <expectedresults>{serialize(current-merge-group()[2])}</expectedresults>
                          <execution_type>1</execution_type>
                      </step>
                  </xsl:merge-action>
              </xsl:merge>              
          </steps>
      </testcase>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/jz1Q1yb

Or, to remove numbers from the steps and actions, you need an additional processing step:

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

  <xsl:function name="mf:strip-numbers" as="node()*">
      <xsl:param name="input" as="node()*"/>
      <xsl:apply-templates select="$input" mode="strip-numbers"/>
  </xsl:function>

  <xsl:template mode="strip-numbers" match="p[matches(., '^\d+\.\s*')]">
      <xsl:copy>{replace(., '^\d+\.\s*', '')}</xsl:copy>
  </xsl:template>

  <xsl:template match="testcase">
      <testcase name="{@name} new">
          <steps>
              <xsl:merge>
                  <xsl:merge-source select="mf:strip-numbers(parse-xml-fragment(steps))/*">
                      <xsl:merge-key select="accumulator-before('step-count')"/>
                  </xsl:merge-source>
                  <xsl:merge-source select="mf:strip-numbers(parse-xml-fragment(expectedresults))/*">
                      <xsl:merge-key select="accumulator-before('step-count')"/>
                  </xsl:merge-source>
                  <xsl:merge-action>
                      <step>
                          <step_number>{position()}</step_number>
                          <actions>{serialize(current-merge-group()[1])}</actions>
                          <expectedresults>{serialize(current-merge-group()[2])}</expectedresults>
                          <execution_type>1</execution_type>
                      </step>
                  </xsl:merge-action>
              </xsl:merge>              
          </steps>
      </testcase>
  </xsl:template>

https://xsltfiddle.liberty-development.net/jz1Q1yb/1

With the support for higher-order functions (i.e. with Saxon PE or EE or AltovaXML) it might also be possible to use the function for-each-pair https://www.w3.org/TR/xpath-functions/#func-for-each-pair instead of the rather verbose xsl:merge.

The use of the accumulator is also a bit tedious but required to have a merge source key based on the position, a more compact solution might be to use to construct a map of the position and the element on the fly:

  <xsl:template match="testcase">
      <testcase name="{@name} new">
          <steps>
              <xsl:merge>
                  <xsl:merge-source name="step" 
                    select="mf:strip-numbers(parse-xml-fragment(steps))/*!map { 'pos' : position(), 'element' : .}">
                      <xsl:merge-key select="?pos"/>
                  </xsl:merge-source>
                  <xsl:merge-source name="action" 
                    select="mf:strip-numbers(parse-xml-fragment(expectedresults))/*!map { 'pos' : position(), 'element' : .}">
                      <xsl:merge-key select="?pos"/>
                  </xsl:merge-source>
                  <xsl:merge-action>
                      <step>
                          <step_number>{position()}</step_number>
                          <actions>{current-merge-group('step')?element => serialize()}</actions>
                          <expectedresults>{current-merge-group('action')?element => serialize()}</expectedresults>
                          <execution_type>1</execution_type>
                      </step>
                  </xsl:merge-action>
              </xsl:merge>              
          </steps>
      </testcase>
  </xsl:template>

https://xsltfiddle.liberty-development.net/jz1Q1yb/2

1 Comment

Merge and accumulators examples should be always rewarded.
1

CDATA is not XML and cannot be processed directly by XSLT. In XSLT 3.0, there's a parse-xml-fragment function that can pre-process CDATA or otherwise escaped XML. However, you say that:

I am planning on using the notepad++ xmltools

AFAIK, this would limit you to XSLT 1.0. In such case, you need to process the input XML twice.

First, apply this transformation and save the result to a file:

XSLT 1.0 [1]

<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="steps | expectedresults">
    <xsl:copy>
        <xsl:value-of select="." disable-output-escaping="yes"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

This should result in the following XML:

XML [2]

<?xml version="1.0" encoding="UTF-8"?>
<testcases>
  <testcase name="Simple">
    <steps><p>1. do something</p>
<p>2. do more</p>
<p>3. even more</p></steps>
    <expectedresults><p>1. result</p>
<p>2. more result</p>
<p>3 again</p></expectedresults>
  </testcase>
</testcases>

Now you can apply the following stylesheet to the resulting file:

XSLT 1.0 [2]]

<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" 
cdata-section-elements="step_number actions expectedresults execution_type"/>
<xsl:strip-space elements="*"/>

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

<xsl:template match="testcase">
    <xsl:copy>
        <xsl:attribute name="name">
            <xsl:value-of select="@name" />
            <xsl:text> new</xsl:text>
        </xsl:attribute>
        <xsl:for-each select="steps/p">
            <step>
                <xsl:variable name="i" select="position()"/>
                <step_number>
                    <xsl:value-of select="$i"/>
                </step_number>
                <actions>
                    <xsl:text>&lt;p&gt;</xsl:text>
                    <xsl:value-of select="substring-after(., '. ')" />
                    <xsl:text>&lt;/p&gt;</xsl:text>
                </actions>                    
                <expectedresults>
                    <xsl:text>&lt;p&gt;</xsl:text>
                    <xsl:value-of select="substring-after(../following-sibling::expectedresults/p[$i], '. ')"/>
                    <xsl:text>&lt;/p&gt;</xsl:text>
                </expectedresults>
                <execution_type>1</execution_type>
            </step>
        </xsl:for-each>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

to get:

Final Result

<?xml version="1.0" encoding="UTF-8"?>
<testcases>
  <testcase name="Simple new">
    <step>
      <step_number><![CDATA[1]]></step_number>
      <actions><![CDATA[<p>do something</p>]]></actions>
      <expectedresults><![CDATA[<p>result</p>]]></expectedresults>
      <execution_type><![CDATA[1]]></execution_type>
    </step>
    <step>
      <step_number><![CDATA[2]]></step_number>
      <actions><![CDATA[<p>do more</p>]]></actions>
      <expectedresults><![CDATA[<p>more result</p>]]></expectedresults>
      <execution_type><![CDATA[1]]></execution_type>
    </step>
    <step>
      <step_number><![CDATA[3]]></step_number>
      <actions><![CDATA[<p>even more</p>]]></actions>
      <expectedresults><![CDATA[<p>again</p>]]></expectedresults>
      <execution_type><![CDATA[1]]></execution_type>
    </step>
  </testcase>
</testcases>

Notes:

  • My result is somewhat different than the one you show. However, I believe it is what you intended;

  • I have changed the input by adding a period after 3 in <p>3 again</p>.



Added:

If what I read is true and your tool is actually using the libxslt XSLT processor, then you can do it all in one pass with the help of the EXSLT str:split() extension function that libxslt supports:

XSLT 1.0 + EXSLT

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:str="http://exslt.org/strings"
extension-element-prefixes="str">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" 
cdata-section-elements="step_number actions expectedresults execution_type"/>
<xsl:strip-space elements="*"/>

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

<xsl:template match="testcase">
    <xsl:variable name="steps" select="str:split(steps, '&lt;p&gt;')"/>
    <xsl:variable name="expectedresults" select="str:split(expectedresults, '&lt;p&gt;')"/>
    <xsl:copy>
        <xsl:attribute name="name">
            <xsl:value-of select="@name" />
            <xsl:text> new</xsl:text>
        </xsl:attribute>
        <xsl:for-each select="$steps">
            <step>
                <xsl:variable name="i" select="position()"/>
                <step_number>
                    <xsl:value-of select="$i"/>
                </step_number>
                <actions>
                    <xsl:text>&lt;p&gt;</xsl:text>
                    <xsl:value-of select="substring-after(., '. ')" />
                </actions>                    
                <expectedresults>
                    <xsl:text>&lt;p&gt;</xsl:text>
                    <xsl:value-of select="substring-after($expectedresults[$i], '. ')"/>
                </expectedresults>
                <execution_type>1</execution_type>
            </step>
        </xsl:for-each>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

4 Comments

Sorry, I have no idea what you're talking about.
Yes, my previous comment was not clear, sorry. The end output was not in the correct format.
Yes, my previous comment was not clear, sorry. The end output was not in the correct format. testcase >steps>step>step_number should be the final format. Your answer is missing the <steps> from the final output. I also tried your XSLT 1.0 + EXSLT version but was getting an error from the EXSLT str:split() usage on line 15. Do I need to install an extension to support this?
I am still not sure I understand the problem. If you want to add a steps wrapper, then just add it as a child of testcase. This is a trivial task and I don't see why you would not be able to figure this out for yourself. Re EXSLT, there is nothing to install: either your processor supports it or it doesn't.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.