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 my regular work, largely because I need to understand some glob of an XML returned without any formatting by a server presumably tuned for maximum performance.

More recently, I was faced with the need to produce those nice XML representation within automatically generated documentation, 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.

So 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 settle into using 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 generic 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 > 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() > 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() > 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 > 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.

0 comments:

Post a Comment