简体   繁体   中英

Changing XML format based off CDATA using a XSL template

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.

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>

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 (ie 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

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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM