"Script Junkie: An XML Document to JavaScript Object Converter"
by Alexander Hildyard
Web Techniques, January 1999

Web Techniques grants permission to use these listings (and code) for 
private or commercial use provided that credit to Web Techniques and 
the author is maintained within the comments of the source. For 
questions, contact editors@web-techniques.com. 



[Example 1]

<?XML VERSION="1.0" RMD="NONE"?>
     <AUTHORS>
          <AUTHOR>
               <FIRSTNAME>Alexander</>
               <INITIAL1>D</>
               <SURNAME>Hildyard</>
          </>
          <AUTHOR>
               <FIRSTNAME>Anon</>
               <EMAIL>nobody@nowhere.com</>
          </>
          <AUTHOR>
               <SURNAME>Twilite</>
               <INITIAL2>M</>
               <SPECIALFLAGS>
                    <FLAGS1>TRUE</>
                    <FLAGS4>TRUE</>
               </>
          </>
          <PSEUDOAUTHOR>
               <TITLE>I24X79</>
          </>
     </>>>



[Example 2]

FIRSTNAME : Alexander
INITIAL1 : D
SURNAME : Hildyard
FIRSTNAME : Anon
EMAIL : nobody@nowhere.com
SURNAME : Twilite
INITIAL2 : M
TITLE : I24X79
FLAGS1 : TRUE
FLAGS4 : TRUE



[Example 3]

document.AUTHORS = new AUTHORS();
document.AUTHORS.AUTHOR[ 0 ] = new AUTHOR();
document.AUTHORS.AUTHOR[ 1 ] = new AUTHOR();
document.AUTHORS.AUTHOR[ 2 ] = new AUTHOR();
document.AUTHORS.PSEUDOAUTHOR = new PSEUDOAUTHOR();
document.AUTHORS.AUTHOR[ 0 ].FIRSTNAME = 'Alexander';
document.AUTHORS.AUTHOR[ 0 ].INITIAL1 = 'D';
document.AUTHORS.AUTHOR[ 0 ].SURNAME = 'Hildyard';
document.AUTHORS.AUTHOR[ 1 ].FIRSTNAME = 'Anon';
document.AUTHORS.AUTHOR[ 1 ].EMAIL = 'nobody@nowhere.com';
document.AUTHORS.AUTHOR[ 2 ].SURNAME = 'Twilite';
document.AUTHORS.AUTHOR[ 2 ].INITIAL2 = 'M';
document.AUTHORS.AUTHOR[ 2 ].SPECIALFLAGS = new SPECIALFLAGS();
document.AUTHORS.PSEUDOAUTHOR.TITLE = 'I24X79';
document.AUTHORS.AUTHOR[ 2 ].SPECIALFLAGS.FLAGS1 = 'TRUE';
document.AUTHORS.AUTHOR[ 2 ].SPECIALFLAGS.FLAGS4 = 'TRUE';



[Example 4]

document.AUTHORS = new AUTHORS();
document.AUTHORS.AUTHOR[ 0 ] = new AUTHOR();
document.AUTHORS.AUTHOR[ 1 ] = new AUTHOR();
document.AUTHORS.AUTHOR[ 2 ] = new AUTHOR();
document.AUTHORS.PSEUDOAUTHOR = new PSEUDOAUTHOR();
document.AUTHORS.AUTHOR[ 0 ].FIRSTNAME = 'Alexander';
document.AUTHORS.AUTHOR[ 0 ].INITIAL1 = 'D';
document.AUTHORS.AUTHOR[ 0 ].SURNAME = 'Hildyard';
document.AUTHORS.AUTHOR[ 1 ].FIRSTNAME = 'Anon';
document.AUTHORS.AUTHOR[ 1 ].EMAIL = 'nobody@nowhere.com';
document.AUTHORS.AUTHOR[ 2 ].SURNAME = 'Twilite';
document.AUTHORS.AUTHOR[ 2 ].INITIAL2 = 'M';
document.AUTHORS.AUTHOR[ 2 ].SPECIALFLAGS = new SPECIALFLAGS();
document.AUTHORS.PSEUDOAUTHOR.TITLE = 'I24X79';
document.AUTHORS.AUTHOR[ 2 ].SPECIALFLAGS.FLAGS1 = 'TRUE';
document.AUTHORS.AUTHOR[ 2 ].SPECIALFLAGS.FLAGS4 = 'TRUE';

function AUTHORS()
{
    this.PSEUDOAUTHOR = new Array();
    this.AUTHOR = new Array();
    return this;
}

function AUTHOR()
{
    this.SPECIALFLAGS = new Array();
    this.INITIAL2 = new Array();
    this.EMAIL = new Array();
    this.SURNAME = new Array();
    this.INITIAL1 = new Array();
    this.FIRSTNAME = new Array();
    return this;
}

function PSEUDOAUTHOR()
{
    this.TITLE = new Array();
    return this;
}

function SPECIALFLAGS()
{
    this.FLAGS4 = new Array();
    this.FLAGS1 = new Array();
    return this;



[Example 5]

At top level, the loop in i continuously reiterates the collection of descent 
paths stored in stack d in the format root.node-collection...leaf.node. 
Starting from an initial depth of 0 (ie. the leaf-node itself), each such 
string is then parsed in turn to determine if it has a node at this depth. 
Obviously, every string in d with have a leaf-node, so the condition "if ( j < 
str.length )" will evaluate to true. The node in question is then stripped from 
the string and stored in the temporary variable baseObject, where it is tested 
against each entry in baseTerms, a stack of "parent" or "container" nodes 
(nodes which have children) already identified at this depth within the XML 
document. If no match is found an entry is generated in newTerms, an array of 
stacks which records each node tagname, followed by the set of all node 
tagnames that have been found to occur as immediate children of that node 
anywhere in the XML document. If, however, a match is found, then an entry must 
have already been generated in newTerms, so baseObject is added to the end of 
the relevant stack, so long as it hasn't been added before. After each string 
in d has been tested at depth 0, loop i repeats at depth 1, and so on, until 
the parsing depth exceeds the depth of the XML document.


For example, our control sample will have generated the following stack of "paths" in d:


	AUTHORS . AUTHOR . FIRSTNAME
	AUTHORS . AUTHOR . INITIAL1
	AUTHORS . AUTHOR . SURNAME
	AUTHORS . AUTHOR . FIRSTNAME
	AUTHORS . AUTHOR . EMAIL
	AUTHORS . AUTHOR . INITIAL2
	AUTHORS . AUTHOR . SPECIALFLAGS . FLAGS1
	AUTHORS . AUTHOR . SPECIALFLAGS . FLAGS4
	AUTHORS . PSEUDOAUTHOR . TITLE


Parsing at depth 1, and reading these paths from right to left, newTerms becomes primed as follows:


	[ 0 ] : [ 0 ] AUTHORS; [ 1 ] PSEUDOAUTHOR; [ 2 ] AUTHOR


Parsing at depth 2, newTerms becomes:


[ 0 ] : [ 0 ] AUTHOR; [ 1 ] SPECIALFLAGS; [ 2 ] INITIAL2; [ 3 ] EMAIL; 
[ 4 ] SURNAME; [ 5 ] INITIAL1; [ 6 ] FIRSTNAME 
	[ 1 ] : [ 0 ] PSEUDOATHOR; [ 1 ] TITLE


And at depth 3:


	[ 0 ] : [ 0 ] PSEUDOAUTHOR; [ 1 ] TITLE
	[ 1 ] : [ 0 ] SPECIALFLAGS; [ 1 ] FLAGS4; [ 2 ] FLAGS1





[LISTING ONE]

<HTML>
<HEAD>
<TITLE>XML to DOM Converter: Listing 3</TITLE>
<APPLET CODE=com.ms.xml.dso.XMLDSO.class WIDTH=100% HEIGHT=30 
             ID=xmldso MAYSCRIPT=true>
<PARAM NAME="url" VALUE="someXMLDocument.xml">
</APPLET>
</HEAD>
<BODY>

<SCRIPT>
_root = xmldso.getDocument().root
results = window.document;


// Simple stack ADT
function stack()
{
     this.depth = 0;
     this.data = new Array();
     this.push = push;
     this.pop = pop;
     this.popTop = popTop;
     this.length = length;
     this.exists = exists;
     return this;
}

// Add an item
function push( item )
{
     this.data[ this.depth ++ ] = item;
}

// Remove last item
function pop()
{
     if ( this.depth )
     {
          this.depth --;
          return this.data[ this.depth ];
     }

     return null;
}

// Remove item at head of stack
function popTop()
{
     if ( this.depth )
     {
          // Pop the top item
         _j = this.data[ 0 ];
         this.depth --;

         // Shuffle the stack up a place
         for ( _i = 0; _i < this.depth; _i ++ )
         {
              this.data[ _i ] = this.data[ _i + 1 ]
          }

              return _j;		
     }
     return null;
}

// Return the size of the stack
function length()
{
     return this.depth;
}

// Does the stack hold the specified item?
function exists( obj )
{
     for ( _i = 0; _i < this.depth; _i ++ )
     {
          if ( this.data[ _i ] == obj )
          {
               return true;
          }
     }

     return false;
}

function xmltodom()
{
     // Cache the XML root, since retrieving it more than once via 	
     // getDocument() causes an error
     var root = _root

     // Holds XML node objects
     var x = new stack();

     // Holds incremental document description tags
     var y = new stack();

     // Holds unique paths of descent to XML leaf nodes 
     var d = new stack();

     x.push( root );
     y.push( "document." + root.tagname + "." );

     while ( x.length() != 0 )
     {
          // Pop top items, so that DOM tree is same way up as XML
          // tree; it would be easier simply to push them in reverse 
          // order, but xmldso.class seems to have problems navigating
               // a node's child-list backwards rather than forwards
               var xmlTag = x.popTop();
                    var docTag = y.popTop();

          // If the XML node is a leaf, assign its DOM equivalent its
          // textual value; otherwise create a new DOM object 
          var docString = docTag.substring( 0, docTag.length - 1 ) + " = ";

          if ( xmlTag.children.length != 0 )
          {
               docString += "new " + xmlTag.tagname + "();<br>";
          }
          else
          {
               docString += "'" + xmlTag.text + "';<br>";
          }

          results.writeln( docString );

          // Check that our XML tag is of type "ELEMENT"
          if ( xmlTag.type == 0 )
          {
               if ( xmlTag.children.length == 0 )
               {
                    // Generate the complete path to the element
                    var iter = xmlTag;
                    var t = new stack();

                    while ( iter.parent != null )
                    {
                         t.push( iter.tagname );
                         iter = iter.parent;
                    }

                    // Reverse the path
                    var path = "";

                    while ( t.length() )
                    {
                         path += t.pop() + ".";
                    }

                    // Check whether this object path has already
                    // been generated
                    if ( !d.exists( path ) )
                    {
                         d.push( path );
                    }
               }
           }

           // Stack children of this XML node
           if ( xmlTag.children )
           {
                // Might seem logical to cache xmlTag.children, but if
                // you do XMLDSO won't work properly
                for ( i = 0; i < xmlTag.children.length; i ++ )
                {
                     x.push( xmlTag.children.item( i ) );

                     // How many instances of a given child node at
                     // this level?
                     var numInstances = 0;

                     for ( j = 0; j < xmlTag.children.length; j ++ )
                     {
                          if ( xmlTag.children.item( i )
                            .tagName == xmlTag.children.item( j ).tagName )
                          {
                               numInstances ++;
                          }
                     }

                     // Get instance index for current node
                     var thisInstance = -1;

                     for ( j = 0; j < i + 1; j ++ )
                     {
                          if ( xmlTag.children.item( j )
                            .tagname == xmlTag.children.item( i ).tagname )
                          {
                               thisInstance ++;
                          }
                     }

                     if ( numInstances > 1 )
                     {
                          y.push( docTag + xmlTag.children.item( i )
                            .tagname + "[ " + thisInstance + " ]." );
                     }
                     else
                     {
                          y.push( docTag + xmlTag.children.item( i )
                            .tagname + "." );
                     }
               }
          }
     }

     // Now generate the ADT function prototypes for this XML document
     results.writeln( "<br>" );

     // Store the tree depth we have parsed
     var objDepth = 0;

     while ( true )
     {
          // Create an array of stacks to hold the collections
          // of sub-elements
          var newTerms = new Array();
          newTerms[ newTerms.length ] = new stack();

          // This will hold the immediate parent of each
          // "newTerms" stack
          var baseTerms = new Array();

          for ( i = 0; i < d.length(); i ++ )
          {
               var str = d.data[ i ];
               var testDepth = 0;
               var lastDot = 0;

               for ( j = 0; j < str.length; j ++ )
               {
                    if ( testDepth == objDepth )
                    {
                         break;
                    }

                    if ( str.charAt( j ) == "." )
                    {
                         testDepth ++;

                         if ( testDepth == objDepth - 1 )
                         {
                              lastDot = j;
                         }
                    }
               }

               if ( j < str.length )
               {
                    var listNum = 0;
                    var baseObject = str.substring( lastDot ? lastDot + 
                                                    1 : 0, j - 1 );

                    // If this parent has already been assigned,
                    // locate it; otherwise assign another one and
                    // add it to the array
                    var fFound = false;

                    for ( k = 0; k < baseTerms.length; k ++ )
                    {
                         if ( baseTerms[ k ] == baseObject )
                         {
                              fFound = true;
                              break;
                         }
                    }

                    if ( !fFound )
                    {
                         listNum = baseTerms.length;
                         newTerms[ listNum ] = new stack();
                         baseTerms[ baseTerms.length ] = baseObject;
                    }
                    else
                    {
                         listNum = k;
                    }

                    str = str.substring( j, str.indexOf( ".", j + 1 ) );

                    // If the sub-element is unique, add it to the
                    // appropriate sub-element collection
                    if ( !newTerms[ listNum ].exists( str ) )
                    {
                         newTerms[ listNum ].push( str );
                    }
               }
          }

          // Parse the two stacks, creating a Javascript object named
          // after each parent, and containing the collection
          // of sub-elements dependent upon that parent
          if ( objDepth != 0 )
          {
               var fItems = false;

               for ( l = 0; l < baseTerms.length; l ++ )
               {
                    if ( baseTerms[ l ].length )
                    {
                         results.writeln( "function " + baseTerms[ l ]
                                          + "()<br>" );
                         results.writeln( "{<br>" );

                         while ( newTerms[ l ].length() )
                         {
                              fItems = true;
                              results.writeln( "&nbsp;&nbsp;&nbsp;&nbsp;
                                               this." + newTerms[ l ].pop()
                                               + " = new Array();<br>" );
                         }

                         results.writeln( "&nbsp;&nbsp;&nbsp;&nbsp;
                                          return this;<br>" );
                         results.writeln( "}<br><br>" );
                    }
               }

               // If there were no items, we have finished traversing
               if ( !fItems )
               {
                    break;
               }
          }

          objDepth ++;
     }
}

// Convert the loaded document
xmltodom()
</SCRIPT>

</BODY>
</HTML>





[LISTING TWO]

<HTML>
<HEAD>
<TITLE>XML to DOM Converter: Listing 1</TITLE>
<APPLET CODE=com.ms.xml.dso.XMLDSO.class WIDTH=100% HEIGHT=30 ID=xmldso MAYSCRIPT=true>
<PARAM NAME="url" VALUE="someXMLDocument.xml">
</APPLET>
</HEAD>
<BODY>

<SCRIPT>
_root = xmldso.getDocument().root
results = window.document;


// Simple stack ADT
function stack()
{
	this.depth = 0;
	this.data = new Array();
	this.push = push;
	this.pop = pop;
	this.popTop = popTop;
	this.length = length;
	this.exists = exists;
	return this;
}

// Add an item
function push( item )
{
	this.data[ this.depth ++ ] = item;
}

// Remove last item
function pop()
{
	if ( this.depth )
	{
		this.depth --;
		return this.data[ this.depth ];
	}

	return null;
}

// Remove item at head of stack
function popTop()
{
	if ( this.depth )
	{
		// Pop the top item
		_j = this.data[ 0 ];
		this.depth --;

		// Shuffle the stack up a place
		for ( _i = 0; _i < this.depth; _i ++ )
		{
			this.data[ _i ] = this.data[ _i + 1 ]
		}

		return _j;		
	}
	return null;
}

// Return the size of the stack
function length()
{
	return this.depth;
}

// Does the stack hold the specified item?
function exists( obj )
{
	for ( _i = 0; _i < this.depth; _i ++ )
	{
		if ( this.data[ _i ] == obj )
		{
			return true;
		}
	}
				
	return false;
}

// Get the XML root
var root = document.applets.xmldso.getDocument().root;
    
// Stack for XML elements
var x = new stack();
x.push( root );
	
while ( x.length() != 0 )
{
	// Pop top item
	var xmlTag = x.popTop();
		
	// If node is a leaf, write it out, else stack its children
	if ( xmlTag.children.length == 0 )
document.writeln( xmlTag.tagname + " : " + xmlTag.text 		
+ "<br>" );
	else	
	{
		for ( i = 0; i < xmlTag.children.length; i ++ )
			x.push( xmlTag.children.item( i ) );
	}		
}

</SCRIPT>

</BODY>
</HTML>




[LISTING THREE]

<HTML>
<HEAD>
<TITLE>XML to DOM Converter  Listing 2</TITLE>
<APPLET CODE=com.ms.xml.dso.XMLDSO.class WIDTH=100% HEIGHT=30 ID=xmldso MAYSCRIPT=true>
<PARAM NAME="url" VALUE="someXMLDocument.xml">
</APPLET>
</HEAD>
<BODY>

<SCRIPT>
_root = xmldso.getDocument().root
results = window.document;


// Simple stack ADT
function stack()
{
	this.depth = 0;
	this.data = new Array();
	this.push = push;
	this.pop = pop;
	this.popTop = popTop;
	this.length = length;
	this.exists = exists;
	return this;
}

// Add an item
function push( item )
{
	this.data[ this.depth ++ ] = item;
}

// Remove last item
function pop()
{
	if ( this.depth )
	{
		this.depth --;
		return this.data[ this.depth ];
	}

	return null;
}

// Remove item at head of stack
function popTop()
{
	if ( this.depth )
	{
		// Pop the top item
		_j = this.data[ 0 ];
		this.depth --;

		// Shuffle the stack up a place
		for ( _i = 0; _i < this.depth; _i ++ )
		{
			this.data[ _i ] = this.data[ _i + 1 ]
		}

		return _j;		
	}
	return null;
}

// Return the size of the stack
function length()
{
	return this.depth;
}

// Does the stack hold the specified item?
function exists( obj )
{
	for ( _i = 0; _i < this.depth; _i ++ )
	{
		if ( this.data[ _i ] == obj )
		{
			return true;
		}
	}
				
	return false;
}


// Cache the XML root
var root = document.applets.xmldso.getDocument().root;

// Holds XML node objects
var x = new stack();

// Holds incremental document description tags
var y = new stack();
	
x.push( root );
y.push( "document." + root.tagname + "." );

while ( x.length() != 0 )
{
	// Pop top items
	var xmlTag = x.popTop();
	var docTag = y.popTop();
	
	// If the XML node is a leaf, assign its DOM equivalent its 	
	// textual value; otherwise create a new DOM object 
	var docString = docTag.substring( 0, docTag.length - 1 ) + 
		" = ";
	
	// Write out a DOM representation
	if ( xmlTag.children.length != 0 )
		docString += "new " + xmlTag.tagname + "();<br>";
	else
		docString += "'" + xmlTag.text + "';<br>";
				
	window.document.writeln( docString );

	// Stack children of this XML node
	if ( xmlTag.children )
	{
		// Might seem logical to cache xmlTag.children, but if you 		
		// do XMLDSO won't work properly
		for ( i = 0; i < xmlTag.children.length; i ++ )
		{
			x.push( xmlTag.children.item( i ) );
			
			// How many instances of a given child node at this 
			// level?
			var numInstances = -1;
				
			for ( j = 0; j < xmlTag.children.length; j ++ )
			{
				if ( xmlTag.children.item( i ).tagName ==
				xmlTag.children.item( j ).tagName )
						numInstances ++;
			}
				
			// Get instance index for current node
			var thisInstance = -1;
	
			for ( j = 0; j < i + 1; j ++ )
			{
				if ( xmlTag.children.item( j ).tagname == 
				xmlTag.children.item( i ).tagname )
						thisInstance ++;
			}

			if ( numInstances > 1 )
			{
y.push( docTag + xmlTag.children.item( i )
  .tagname + "[ " + thisInstance + " ]." );
			}	
			else
			{
y.push( docTag + xmlTag.children.item( i )
  .tagname + "." );
			}
		}
	}
}

</SCRIPT>

</BODY>
</HTML>