1

While developing a Single Page Application, I'm generating some XML that I'd like to be able to read in-browser.

While developing, I'm usually working in whatever latest version of Safari and it all works great, but when I wanted to show some progress to someone, and they were using Firefox, the generated XML was all on one line.

I picked the identity transform to pretty print based on research I did because it seemed like the cleanest solution to my problem, but now it doesn't seem to work on Gecko based browsers.

Firefox and Safari side by side

The code I'm using in Typescript is:

private prettifyXml(unformattedDocument:XMLDocument) : XMLDocument {

    let identityTransformSheet = '\<' +
        'xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">\
            <xsl:output omit-xml-declaration="yes" indent="yes"/>\
            <xsl:template match="node()|@*">\
                <xsl:copy>\
                    <xsl:apply-templates select="node()|@*"/>\
                </xsl:copy>\
            </xsl:template>\
        </xsl:stylesheet>';

    let parser = new DOMParser();
    let processor = new XSLTProcessor();
    processor.importStylesheet(parser.parseFromString(identityTransformSheet, 'text/xml'));
    let result = processor.transformToDocument(unformattedDocument);
    return result;
}

And this is used:

    // `xml` holds a document created by DOMParser and filled with a serialized representation of the model.

    let result = (new XMLSerializer()).serializeToString(this.prettifyXml(xml));
    this.xmlEquivalent(result);
    return result;

This works fine in Safari but seems to do nothing in Firefox. Is there a way to make this functionality cross browser or is there perhaps a better way to do it?

(I'm hoping for a standards compliant solution without adding another library)

Kris
  • 40,604
  • 9
  • 72
  • 101
  • That code takes a DOM document and transforms to a DOM document are not printed or rendered anywhere. So where/how do you see that "one line"? In general what you define on `xsl:output` holds for the case where the XSLT processor serializes the result document to a stream or string, Firefox/Mozilla is known to create DOM result nodes and does not serialize at all, so I am not sure the `indent="yes"` will have any effect. – Martin Honnen Jul 19 '17 at 15:46
  • Basically `(new XMLSerializer()).serializeToString(this.prettifyXml(xml))`. a click in the gray bit on the "generated objects" will force the output there on the page. – Kris Jul 19 '17 at 15:54
  • As I said, Firefox's XSLT implementation with the offered methods like `transformToDocument` does a node to node transformation where serialization options like given in `xsl:output` are not taken into account as far as I understand it. DOM Level 3 has LSSerializer with a pretty print option but the web API/browser guys have abandoned that, I think only Opera some years ago had an implementation in the browser. – Martin Honnen Jul 19 '17 at 16:15
  • @martin then the question has to become why is identity transform considered a good idea for pretty printing xml (https://stackoverflow.com/questions/376373/pretty-printing-xml-with-javascript) if it clearly doesn't work. and still, what are the alternatives? – Kris Jul 19 '17 at 21:18
  • Well, there is one answer in there that suggests using the identity transformation when you run the XSLT with XslCompiledTransform or Saxon or Altova, all processors where you can choose to serialize the result. I am afraid inside the browser the Mozilla XSLT processor does not allow this. And there are certainly lots of other answers and comments in there for more advanced or sophisticated uses or needs or simply alternatives, like a stylesheet transforming to a collapsible tree or the highly advanced Spectrum. – Martin Honnen Jul 19 '17 at 21:33
  • This is becoming fairly chatty, but last I checked, XSLT was a well known standard, so the various implementations should be able to produce the same results. If not; at least one of them is wrong. Obviously I'm doing a client side browser thing so 99% of suggestions in similar pretty printing threads are utterly useless to me and I don't want to work with strings when there's a perfectly good object model I should be working with. throwing a ton of regex against it also seems like creating instead of solving problems. Just stating "firefox sucks, deal with it." is very unsatisfying – Kris Jul 20 '17 at 01:37
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/149660/discussion-between-kris-and-martin-honnen). – Kris Jul 20 '17 at 01:45
  • @Kris was there any useful information in your chat? The link is broken and I'm still searching for a way to format xml in Firefox in 2020 – klamann Dec 19 '20 at 13:08
  • @klamann: If I recall correctly the chat wasn't ever actually used here, so no. I believe I eventually wrote a simple visitor and traversed the tree. under 20 lines of code probably. If possible, maybe move to JSON instead of XML? `JSON.stringify` can format nicely everywhere _and_ json is easier on the eyes too.. – Kris Dec 20 '20 at 01:53
  • yeah, if it was my implementation, I would have used json, but I have to work with data that is provided by another system. I'm using a simple regex-based solution now which of course will break on a ton of edge cases, but for simple xml files it's ok and I only need it for debugging purposes anyways... still, I would have preferred a proper solution ;) – klamann Dec 20 '20 at 08:09

1 Answers1

1

As you asked for alternatives, the post you linked to discussing the use of the identity transformation proposed by Dimitre Novatchev also has a comment suggesting the use of the XPath visualizer stylesheet instead of the identity transformation. So to demonstrate that that is possible I made some adaptions to the XSLT and CSS and Javascript to allow it to be used inside another HTML document instead of creating a separate, complete HTML document, the result is https://martin-honnen.github.io/js/2017/pretty-print/pretty-print-test1.html which does work for me in on Windows 10 with current versions of Edge, Chrome and Firefox. I don't have Safari to test. It will certainly not work as written with IE as I have solely used the XSLTProcessor API in Javascript first introduced in Mozilla and and now be supported in anything but IE; if you need IE support then you should be able to have that by using IE specific code to run the XSLT transformation.

Here is the code of the HTML:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Testing a proof of concept of pretty-printing of an XML DOM tree as a HTML collapsible table structure</title>
        <link rel="stylesheet" type="text/css" href="pretty-print1.css"/>
        <script>
            var prettyPrinter = new XSLTProcessor();
            (function() {
            var req = new XMLHttpRequest();
            req.open('GET', 'pretty-print1.xsl');
            req.onload = function() {
              prettyPrinter.importStylesheet(req.responseXML);
            };
            req.send();
            }())

            function prettyPrint(doc) {
               return prettyPrinter.transformToFragment(doc, document);
            }

            function prettyPrintCollapseExpandHandler(event) {
  try {
    var thisNode = event.target;
    var par = event.target.parentNode;
    if (thisNode.nodeName == 'TD' && thisNode.className == 'expander') {
      if (par.parentNode.className == 'expander-closed') {
        par.parentNode.className = '';
        thisNode.textContent = '-';
      }
      else {
        par.parentNode.className = 'expander-closed';
        thisNode.textContent = '+';
      }
    }
  } catch (e) {
  }
}
        </script>
        <script>
            document.addEventListener('DOMContentLoaded',
              function() {
                var req = new XMLHttpRequest();
                req.open('GET', 'input1.xml');
                req.onload = function() {
                  document.getElementById('result').appendChild(prettyPrint(req.responseXML));
                };
                req.send();
             },
             false
           );
        </script>
    </head>
    <body>
        <section>
            <h1>Testing a proof of concept of pretty-printing of an XML DOM tree as a HTML collapsible table structure</h1>
            <section id="result">
                <h2>Example result</h2>
            </section>
        </section>
    </body>
</html>

the XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">

<!-- The following is not used because of a bug in Mozilla :( -->
<!--
  <xsl:key name="kattPref" match="@*" 
   use="concat(generate-id(..), '|', substring-before(., ':'))"/>
-->  
  <xsl:output method="html"/>  
  <xsl:template match="/">                

      <div class="pretty-print" onclick="prettyPrintCollapseExpandHandler(event);">        
        <xsl:apply-templates/>      
      </div>    

  </xsl:template>  

  <xsl:template match="*">        
    <div class="indent">      
      <span class="markup">&lt;</span>      

      <xsl:variable name="class" select="'elemname'"/>


      <span class="{$class}">        
        <xsl:value-of select="name(.)"/>      
      </span>

      <xsl:call-template name="findNamespace"/>

      <xsl:apply-templates select="@*"/>      
      <span class="markup">/></span>    
    </div>  
  </xsl:template>  

  <xsl:template match="*[text()]">    

    <xsl:variable name="class" select="'elemname'"/>   

    <div class="indent">      
      <span class="markup">&lt;</span>      
      <span class="{$class}">        
        <xsl:value-of select="name(.)"/>      
      </span>

      <xsl:call-template name="findNamespace"/>

      <xsl:apply-templates select="@*"/>      
      <span class="markup">></span>      
      <!--<span class="text">        
        <xsl:value-of select="."/>      -->
        <xsl:apply-templates/>
      <!--</span>-->      
      <span class="markup">&lt;/</span>      
      <span class="elemname">        
        <xsl:value-of select="name(.)"/>      
      </span>      
      <span class="markup">></span>    
    </div>  
  </xsl:template>  

  <xsl:template match="*[* or processing-instruction() or comment() 
                         or string-length(text()) > 50]" priority="10">    

    <xsl:variable name="class" select="'elemname'"/>    

    <table>      
      <tr>        
        <td class="expander">
          -
          <div/>        
        </td>        
        <td>          
          <span class="markup">&lt;</span>          
          <span class="{$class}">            
            <xsl:value-of select="name(.)"/>          
          </span>          
          <xsl:call-template name="findNamespace"/>
          <xsl:apply-templates select="@*"/>          
          <span class="markup">></span>          
          <div class="expander-content">            
            <xsl:apply-templates/>          
          </div>          
          <span class="markup">&lt;/</span>          
          <span class="elemname">            
            <xsl:value-of select="name(.)"/>          
          </span>          
          <span class="markup">></span>        
        </td>      
      </tr>    
    </table>  
  </xsl:template>  
  <xsl:template match="@*">        

    <xsl:variable name="vPos" select="position()"/>

    <xsl:variable name="vPref" select="substring-before(name(), ':')"/>

    <xsl:if test="$vPref 
               and 
                  not(../@*[position() &lt; $vPos]
                           [substring-before(name(), ':') 
                           = $vPref]
                      )">
      <xsl:call-template name="findNamespace"/>
    </xsl:if>

<!-- The following is not used because of a bug in Mozilla :( -->

<!--
    <xsl:if test=
    "generate-id() 
    = 
     generate-id(key('kattPref', 
                      concat(generate-id(..), '|', substring-before(., ':'))
                     )[1]
                )">
      <xsl:call-template name="findNamespace"/>
    </xsl:if>
-->
    <xsl:variable name="class" select="'attrname'"/>

    <xsl:variable name="class2" select="'markup'"/>

    <xsl:variable name="class3" select="'attrvalue'"/>

    <xsl:text> </xsl:text>    
    <span class="{$class}">      
      <xsl:value-of select="name(.)"/>    
    </span>    
    <span class="{$class2}">="</span>    
    <span class="{$class3}">      
      <!-- <xsl:value-of select="."/> -->    
      <xsl:call-template name="replaceAmpersands">
        <xsl:with-param name="vString" select="string(.)"/>
      </xsl:call-template>
    </span>    
    <span class="{$class2}">"</span>  
  </xsl:template>  

  <xsl:template match="text()">    

    <xsl:variable name="class" select="'text'"/>


    <span class="{$class}">        
      <!-- <xsl:value-of select="."/>       -->
      <xsl:call-template name="replaceAmpersands">
        <xsl:with-param name="vString" select="string(.)"/>
      </xsl:call-template>
    </span>    
  </xsl:template>  

  <xsl:template match="processing-instruction()">    

    <xsl:variable name="class" select="'indent pi'"/>

    <div class="{$class}">

      &lt;?
      <xsl:value-of select="name(.)"/>      
      <xsl:text> </xsl:text>      
      <xsl:value-of select="."/>
?>

    </div>  
  </xsl:template>  

  <xsl:template match="processing-instruction()[string-length(.) > 50]">    

    <xsl:variable name="class" select="'pi'"/>

    <xsl:variable name="class2" select="'indent expander-content'"/>

    <table>      
      <tr>        
        <td class="expander">
          -          
          <div/>        
        </td>        
        <td class="{$class}">

          &lt;?
          <xsl:value-of select="name(.)"/>          
          <div class="{$class2}">            
            <xsl:value-of select="."/>          
          </div>          
          <xsl:text>?></xsl:text>        
        </td>      
      </tr>    
    </table>  
  </xsl:template>  

  <xsl:template match="comment()">    

    <xsl:variable name="class" select="'comment indent'"/>

    <div class="{$class}">      
      &lt;!--
      <xsl:value-of select="."/>
      -->    
    </div>  
  </xsl:template>  

  <xsl:template match="comment()[string-length(.) > 50]">    

    <xsl:variable name="class" select="'comment'"/>

    <xsl:variable name="class2" select="'indent expander-content'"/>

    <table>      
      <tr>        
        <td class="expander">
          -          
          <div/>        
        </td>        
        <td class="{$class}">          
          &lt;!--            
          <div class="{$class2}">              
            <xsl:value-of select="."/>            
          </div>          
          -->        
        </td>      
      </tr>    
    </table>  
  </xsl:template>

  <xsl:template name="findNamespace">

    <xsl:variable name="vName" select="substring-before(name(), ':')"/>
    <xsl:variable name="vUri" select="namespace-uri(.)"/>

    <xsl:variable name="vAncestNamespace">
      <xsl:call-template name="findAncNamespace">
        <xsl:with-param name="pName" select="$vName"/>
        <xsl:with-param name="pUri" select="$vUri"/>
      </xsl:call-template>
    </xsl:variable>

    <xsl:if test="not(number($vAncestNamespace))">
      <xsl:if test="namespace-uri()
                  or
                    not(generate-id() 
                       = 
                        generate-id(../@*[name() 
                                         = 
                                          name(current())]
                                    )
                        )">
        <xsl:if test="parent::* or namespace-uri() or contains(name(), ':')">
          <xsl:text> </xsl:text>    
          <span class="namespace">      
            <xsl:value-of select="'xmlns'"/>
            <xsl:if test="contains(name(), ':')">
              <xsl:value-of select="concat(':', $vName)"/>
            </xsl:if>
          </span>    
          <span class="markup">="</span>    
          <span class="namespace">      
            <xsl:value-of select="namespace-uri()"/>    
          </span>    
          <span class="markup">"</span> 
        </xsl:if> 
      </xsl:if>
    </xsl:if>
  </xsl:template>

  <xsl:template name="findAncNamespace">
    <xsl:param name="pNode" select="."/>
    <xsl:param name="pName" select="substring-before(name(), ':')"/>
    <xsl:param name="pUri" select="namespace-uri(.)"/>

     <xsl:choose>
      <xsl:when test="not($pNode/parent::*) 
                     and not($pName) and not($pUri)">1</xsl:when>
      <xsl:when test="not($pNode/parent::*)">0</xsl:when>
      <xsl:otherwise>
        <xsl:variable name="vSamePrefs" 
        select="number($pName
                      = substring-before(name($pNode/..), ':')
                      )"/>

        <xsl:variable name="vSameUris" 
         select="number($pUri  = namespace-uri($pNode/..))"/>

        <xsl:choose>
          <xsl:when test="$vSamePrefs and not($vSameUris)">0</xsl:when>
          <xsl:when test="not($vSamePrefs)">
            <xsl:call-template name="findAncNamespace">
              <xsl:with-param name="pNode" select="$pNode/.."/>
              <xsl:with-param name="pName" select="$pName"/>
              <xsl:with-param name="pUri" select="$pUri"/>
            </xsl:call-template>
          </xsl:when>
           <xsl:otherwise>1</xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>

  </xsl:template>

  <xsl:template name="replaceAmpersands">
    <xsl:param name="vString"/>

   <xsl:variable name="vAmp">&amp;</xsl:variable>

   <xsl:choose>
   <xsl:when test="contains($vString, $vAmp)">
     <xsl:value-of select="substring-before($vString, $vAmp)"/>
     <xsl:value-of select="concat($vAmp, 'amp;')"/>
     <xsl:call-template name="replaceAmpersands">
       <xsl:with-param name="vString" 
       select="substring-after($vString, $vAmp)"/>
     </xsl:call-template>
   </xsl:when>
   <xsl:otherwise>
     <xsl:value-of select="$vString"/>
   </xsl:otherwise>
   </xsl:choose>

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

Take the CSS from https://martin-honnen.github.io/js/2017/pretty-print/pretty-print1.css, it is all meant anyway as a proof of concept, not as polished, finished code.

These days I would rather suggest to a worked out solution like https://github.com/pgfearo/xmlspectrum that might work in the browser with the help of Saxon-CE or Saxon-JS.

Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
  • I'm unable to test this right now but it looks like I could adapt it to something usable for _much_ more than the instant debugging xml dump. Hope to get some time to play with it tomorrow! – Kris Jul 21 '17 at 15:20
  • It's a heck of a lot more code than I thought would be needed, but it's nicely extensible and also solves a few future problems. Thanks! – Kris Jul 22 '17 at 19:32