Tuesday, October 25, 2011

Prettifying XML files for web browsers

I am a big user of public XML formatters, such as http://www.xmlformatter.net/ , as part of of my work, largely because I need to understand some glob of XML returned without any formatting by a server presumably tuned for maximum performance.

More recently, I was needed to produce those nice XML representations as examples inside the documentation automatically generated during our build process, specially now that version 0.9.0 of the excellent Lunatech Jaxdoclets library (http://www.lunatech-labs.com/open-source/jax-doclets) can also include external HTML contents in the final jaxdoc output of a JAX-RS implementation.

What I needed was a formatter that could be embedded into our build structure, or even incorporated into a live server to create a private XML formatter service where we did not have to worry about obfuscating XML contents before sending them out to the extranet (e.g. IP addresses, passwords, someone’s address, etc) .

Since our build is based on Ant and our live server based on Tomcat, it made sense to use XSLT. This link (http://www.dpawson.co.uk/xsl/sect2/pretty.html) is a good start, but I really wanted output in HTML with all the niceties of CSS styles, etc.

For now, the transformation is listed below. I am not terribly happy with the solution for namespaces and the “pad”  template would need a serious performance optimization if used in mainline processing (not my case) , probably by a sub-string function call using a long chunk of whitespaces as parameters since XSLT does not have native constructs for loops.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:fn="http://www.w3.org/2005/xpath-functions"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" >
    <xsl:output method="html" />
    <xsl:strip-space elements="*" />
   
    <!-- 
       - Root match
      -->
    <xsl:template match="/">
        <html>
            <head>
                <title>HTML version of XML resource</title>
                <link rel="stylesheet" type="text/css" href="frs-jaxdoc.css" />
            </head>
            <body>
                <xsl:apply-templates>
                    <xsl:with-param name="indent" select="0" />
                </xsl:apply-templates>
            </body>
        </html>
    </xsl:template>





    <!--  
      - Processes XML elements
      -
      - param indent              indentation count
      - param ancestorsNamespaces concatenation of all namespace URIs from all parents, which is
      -                           used to determine if a namespace has already been declared in
      -                           a parent XML element.
      -->
    <xsl:template match="node()[name()]">


        <xsl:param name="indent" select="0" />
        <xsl:param name="ancestorsNamespaces" select="''" />


        <xsl:variable name="hasChildren" select="count(child::node()) > 0"/>
        <xsl:variable name="hasTextNode" select="count(child::text()) > 0"/>





        <!--  Format opening of XML element -->
        <xsl:if test="$indent &gt; 0"><br/>
        </xsl:if>
        <xsl:call-template name="pad">
            <xsl:with-param name="indent" select="$indent * 2" />
        </xsl:call-template>
        <span class="xmlElement">&lt;<xsl:value-of select="name()" /></span>
       
        <!-- Add namespaces to XML element -->
        <xsl:variable name="selfNamespaces">
            <xsl:for-each select="namespace::node()">
                <xsl:value-of select="." />
            </xsl:for-each>
        </xsl:variable>
        <xsl:variable name="hasAttributes" select="count(@*) > 0" />
        <xsl:for-each select="namespace::node()">
            <xsl:if test="contains($ancestorsNamespaces, .) = false()">
                <xsl:if test="position() = 1">&#160;</xsl:if>
                <xsl:if test="position() &gt; 1">
                    <br/>
                    <xsl:call-template name="pad">
                        <xsl:with-param name="indent" select="($indent+1) * 2" />
                    </xsl:call-template>
                </xsl:if>
                <span class="xmlNamespacePrefix">xmlns:<xsl:value-of select="name()" /></span>=<span class="xmlNamespaceUri">"<xsl:value-of select="." />"</span>
                <xsl:if test="position()=last() and $hasAttributes">
                    <br/>
                    <xsl:call-template name="pad">
                        <xsl:with-param name="indent" select="($indent+1) * 2" />
                    </xsl:call-template>
                </xsl:if>
            </xsl:if>
        </xsl:for-each>
       
        <!-- Add attributes to XML element -->
        <xsl:apply-templates select="@*" />
       
        <!-- Close opening XML element tag, with special handling for element without children node -->
        <span class="xmlElement"><xsl:if test="$hasChildren = false()">/</xsl:if>&gt;</span>  








        <!-- Add children to XML element -->
        <xsl:apply-templates select="node()">
            <xsl:with-param name="indent" select="$indent + 1" />
            <xsl:with-param name="ancestorsNamespaces" select="concat($ancestorsNamespaces,$selfNamespaces)" />
        </xsl:apply-templates>








        <!--  Close XML element, but only if not already closed with XML element abbreviation -->
        <xsl:if test="$hasChildren = true()">
            <xsl:if test="$hasTextNode = false()">
                <br/>
                <xsl:call-template name="pad">
                    <xsl:with-param name="indent" select="$indent * 2" />
                </xsl:call-template>
            </xsl:if>
            <span class="xmlElement">&lt;/<xsl:value-of select="name()" />&gt;</span>
        </xsl:if>


    </xsl:template>



    <!-- 
      - XML text
      -->
    <xsl:template match="text()">
        <xsl:variable name="nodeValue" select="." />
        <xsl:if test="string-length($nodeValue) > 0">
            <span class="xmlText"><xsl:copy-of select="$nodeValue" /></span>
        </xsl:if>
    </xsl:template>



    <!--
      - XML attributes
      -->
    <xsl:template match="@*">
    <xsl:if test="position() &gt; 0">&#160;</xsl:if>
     <span class="xmlAttr"> <xsl:value-of select="name()" /> </span>=<span class="xmlAttrValue">"<xsl:value-of select="." />"</span>
    </xsl:template>



    <!-- 
      - Padding for HTML output, used for indentation purposes
      -->
    <xsl:template name="pad">
        <xsl:param name="indent" select="0" />





        <xsl:if test="$indent &gt; 0">
            &#160;
            <xsl:call-template name="pad">
                <xsl:with-param name="indent" select="$indent - 1" />
            </xsl:call-template>
        </xsl:if>
    </xsl:template>


</xsl:stylesheet>






And this is the companion CSS file:





.xmlElement {
color: #990099;
}

.xmlAttr {
color: #660066;
}
.xmlAttrValue {
color: #0000CC
}

.xmlNamespacePrefix {
color: #666600;
}

.xmlNamespaceUri {
color: #000099
}

xmlText {
}

BODY {
font-size: 10pt;
font-family: "Courier New"
}




The style names for classes are not that verbose, but I thought I would make it more legible for this entry.