Randy H in CT
Randy H in CT

Reputation: 37

How to apply multiple XSLT transformations to the same XML table

I'm working with some XML documents that have been handed to me by the client, and sometimes they aren't as "clean" as they should be. So I'm using XSLT 2.0 to clean them up before they move on to the next step.

One of the obstacles I'm running into is that empty/orphan rows in tables (" < r o w / > ") are breaking the processing that happens further downstream, and I'm not in a position to change that processing. So I need to get rid of those empty rows during the XSLT transformation.

Below is a shortened version of the XML that I'm working with ...

    <table outputclass="OriginalTableClass">
        <tgroup cols="2">
            <tbody>
                <row>
                    <entry>
                        <p outputclass="TBL_Heading">Heading</p>
                    </entry>
                    <entry>
                        <p outputclass="TBL_Heading">Heading</p>
                    </entry>
                </row>
                <row>
                    <entry morerows="1">
                        <p outputclass="TBL_Body_Text">Content</p>
                    </entry>
                    <entry>
                        <p outputclass="TBL_Body_Text">Content</p>
                    </entry>
                </row>
                <row/>
                <row/>
            </tbody>
        </tgroup>
    </table>

So you'll see that those last two rows are empty. I need to get rid of those, and so I came up with this, which seemed to work fine ...

<?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"
    xmlns:x2w="http://www.orbistechnologies.com/xml2word"
    exclude-result-prefixes="xs"
    version="2.0">
    
    <xsl:template match="*" mode="ABC">
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:apply-templates mode="ABC"/>
        </xsl:copy>
    </xsl:template>
    
    <!-- get rid of any orphaned rows -->
    <xsl:template match="tbody/row[not(child::*)]" mode="ABC"/>

My logic here was that if a row doesn't have a child, get rid of it. And this worked perfectly fine ... until I later had to add a transformation to change the same table's class in cases where there's a header row. That looks like this ...

    <!-- change the outputclass for tables that have a header row -->
    <xsl:template match="table[@outputclass='OriginalTableClass']" mode="ABC">
        <xsl:copy>
            <xsl:element name="table">
                <xsl:choose>
                    <!-- does the table have a header row? -->
                    <xsl:when test="*/descendant::p[@outputclass='TBL_Heading']">
                        <!-- if yes, then assign the new class -->
                        <xsl:attribute name="outputclass">NewTableClass</xsl:attribute>
                    </xsl:when>
                    <xsl:otherwise>
                        <!-- if not, then let the current class ride -->
                        <xsl:attribute name="outputclass">
                            <xsl:value-of select="@outputclass"/>
                        </xsl:attribute>
                    </xsl:otherwise>
                </xsl:choose>
                <!-- copy all of the content inside the table -->
                <xsl:copy-of select="./node()"/>
            </xsl:element>
        </xsl:copy>
    </xsl:template>

And this transformation also works perfectly fine.

However ... the two transformations don't play nice together. Meaning, if I include the second transformation, then the orphan rows persist. And I kind of understand why -- because I'm trying to transform the same content twice, and that's a no-no. But ... I'm still relatively new to XSLT and can't figure out how to make both things happen in the same table.

What I've tried to do so far is somehow include the first transformation (getting rid of the orphaned rows) inside of the second transformation. Because it seems like I should be able to do this at the point where the "copy-of" happens. But what I can't quite wrap my head around is how to make a change to the child/descendant element (those rows), instead of to the parent, while I'm matching against the parent element (the table). Or how to cycle through the table's child/descendant content when the number of levels and the structure a table may have can vary (meaning: it's not always table/tgroup/tbody/row).

I also tried giving the first transformation a higher priority value and also giving them different "mode" values. Although I suspected neither of these would work ... and they didn't.

What's the basic XSLT concept that I'm probably missing here? And what if I have to make another transformation to the same table? Are all elements inside the table off-limits once I apply a parent-level transformation like this?

Any help would be appreciated. Thanks!

Upvotes: 1

Views: 89

Answers (2)

Martin Honnen
Martin Honnen

Reputation: 167716

I think instead of

<!-- change the outputclass for tables that have a header row -->
<xsl:template match="table[@outputclass='OriginalTableClass']" mode="ABC">
    <xsl:copy>
        <xsl:element name="table">
            <xsl:choose>
                <!-- does the table have a header row? -->
                <xsl:when test="*/descendant::p[@outputclass='TBL_Heading']">
                    <!-- if yes, then assign the new class -->
                    <xsl:attribute name="outputclass">NewTableClass</xsl:attribute>
                </xsl:when>
                <xsl:otherwise>
                    <!-- if not, then let the current class ride -->
                    <xsl:attribute name="outputclass">
                        <xsl:value-of select="@outputclass"/>
                    </xsl:attribute>
                </xsl:otherwise>
            </xsl:choose>
            <!-- copy all of the content inside the table -->
            <xsl:copy-of select="./node()"/>
        </xsl:element>
    </xsl:copy>
</xsl:template>

you just want and need a template for the attribute change i.e.

<xsl:template match="table[@outputclass='OriginalTableClass'][*/descendant::p[@outputclass='TBL_Heading']]/@outputclass" mode="ABC">
  <xsl:attribute name="{name()}">NewTableClass</xsl:attribute>
</xsl:template>

if you also ensure attributes are processed and transformed and not simply copied through by changing

<xsl:template match="*" mode="ABC">
    <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:apply-templates mode="ABC"/>
    </xsl:copy>
</xsl:template>

to

<xsl:template match="* | @*" mode="ABC">
    <xsl:copy>
        <xsl:apply-templates select="@*" mode="ABC"/>
        <xsl:apply-templates mode="ABC"/>
    </xsl:copy>
</xsl:template>

Upvotes: 0

Mads Hansen
Mads Hansen

Reputation: 66781

When you use xsl:copy-of you are bypassing all of the processing and ability to transform and match templates.

Import or include the first XSLT in the second one and change the template for the table where you have <xsl:copy-of select="./node()"/> and instead <xsl:apply-templates select="node()"/>.

That will allow template matches to occur, and the template match on the empty row will filter out the content.

Upvotes: 0

Related Questions