10.4. XML processing with JavaScript

XML processing with JavaScript in the context of a browser is somewhat simplified as it can leverage the DOM API already part of the browser for dealing with HTML which is a dialect of XML. This is the approach we present in this section. To use JavaScript outside of a web browsing context, then one should consider using the jsdom [70] package for Node.js.

Parsing a string containing an XML structure to get the corresponding DOM structure is done by calling

new DOMParser().parseFromString(xmlStr,"text/xml")

which returns a DOM structure that can be manipulated using similar calls as in Java. For example, children to get the list of children elements, attributes to get a mapping of attribute names with the corresponding values, appendChild(node) to add a new child to the current element, etc.

Once the DOM structure has been modified, a string with the XML structure can be obtained by calling

new XMLSerializer().serializeToString(document)

The resulting string is a correct XML structure, but is not indented so some further processing is needed to get a more readable display. One way is to use an XSLT transformation within the browser, but browsers often differ on how they handle such transformations. See this example for such an approach. We instead use a specialized string formatter.

We illustrate JavaScript XML processing through a very simple web page (see Figure 10.1) that embeds our running example, compacting and expanding XML structures.

Figure 10.1. Display of the webpage for exercising JavaScript compaction and expansion.

Display of the webpage for exercising JavaScript compaction and expansion.

The left part shows the XML content and the right part shows the compact form once the user has clicked on the compact button.


Figure 10.2. HTML to create the web page for compaction and expansion

  1 <!DOCTYPE html>
    <html lang="en">
    <head>                                                               (1)
        <title>Compaction and expansion of XML</title>
  5     <script src="https://code.jquery.com/jquery-latest.min.js"></script>
        <script src="DOMCompact.js"></script>
        <script src="CompactTokenizer.js"></script>
        <script src="DOMExpand.js"></script>
        <script src="FormatXML.js"></script>
 10     <style>
            textarea{font-family: 'Courier New', Courier, monospace;font-size: large;}
            .center {text-align: center;}
        </style>
    </head>
 15 <body>                                                               (2)
        <h1>Compaction and expansion of XML</h1>
        <table>
            <tr><th>XML</th><th>Compact form</th></tr>
            <tr>
 20             <td><textarea name="xml" id="xml" cols="50" rows="20"></textarea></td>       (3)
                <td><textarea name="text" id="text" cols="50" rows="20"></textarea></td>     (4)
            </tr>
            <tr>
                <td class="center">                                      (5)
 25                 <input type="button" id="compact" value="compact">
                    <input type="button" id="pprint" value="pretty-print">
                </td>
                <td class="center"><input type="button" id="expand" value="expand"></td>
            </tr>
 30     </table>
    </body>
    </html>

1

HEAD section that defines the title of the page, makes links to JavaScript files and add some elementary formatting using CSS.

2

body with a title and table containing two text boxes and buttons.

3

Text box in which the XML content can be pasted.

4

Text box where the compaction will appear.

5

Buttons for launching the compaction, expansion and pretty-print of the expanded XML.


10.4.1. DOM parsing using JavaScript

Example 10.26. [DOMCompact.js] Text compaction of the cellar book (Example 2.2) with JavaScript using the DOM model

Compare this program with Example 8.1.

  1 // taken from https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
    const ELEMENT_NODE = 1;                                              (1)
    const TEXT_NODE    = 3;
    const COMMENT_NODE = 8;
  5 
    function domCompact(node, indent){                                   (2)
        const nodeType = node.nodeType;                                  (3)
        switch (nodeType) {
            case ELEMENT_NODE:                                           (4)
 10             let out=[]; // create list of compacted strings
                indent+=" ".repeat(node.nodeName.length+1)
                const attributes = node.attributes;                      (5)
                for (let i = 0; i < attributes.length; i++) {
                    const attr = attributes[i];
 15                 out.push(`@${attr.nodeName}[${attr.nodeValue}]`);
                }
                const children=node.childNodes;                          (6)
                for (let i = 0; i < children.length; i++) {
                    const child = children[i];
 20                 // skip comment and empty text nodes
                    if (child.nodeType==COMMENT_NODE || 
                        (child.nodeType==TEXT_NODE && 
                         child.textContent.trim().length==0))continue;
                    out.push(domCompact(child,indent));
 25             }
                return `${node.nodeName}[${out.join("\n"+indent)}]`;     (7)
            case TEXT_NODE:                                              (8)
                return node.textContent.trim()
            default:
 30             console.log("Should never happen: bad nodeType",nodeType)
        }
    }
    $(document).ready(function(){
        $("#compact").click(function(){                                  (9)
 35         const xmlStr=$("#xml").val();
            const doc=new DOMParser().parseFromString(xmlStr,"text/xml");
            $("#text").val(domCompact(doc.documentElement,""));
        })
    })

1

Constants for relevant DOM node types.

2

Start of function to produce a compact string from a node with a given indent (a string of spaces).

3

Determines the node type and switch to the appropriate code. Only element and text types are dealt with.

4

If the node is an element, a list of compact string is initialized for adding attributes and children nodes. The new indentation value is computed for the children nodes.

5

Each attribute is added with its name preceded by @ and its value within square brackets.

6

Each child node is added with a recursive call to compact.

7

Returns a new string in which the compacted strings for the attributes and children are joined with a new line and the appropriate indentation.

8

A text node is the trimmed content of the node.

9

The callback function for the compact button gets the XML string in the left text box, parses it to produce an XML document which is then compacted and added to textbox on the right.


10.4.2. Creating an XML document using JavaScript

Parsing a compact form in order to create an XML document follows an organization similar to the one we have shown in Chapter 9 and Section 10.1.3. We first define an object for a specialized tokenizer which returns only meaningful units that will be used to create the XML elements and attributes of the resulting XML document.

Example 10.27. [CompactTokenizer.js] JavaScript specialized string scanner that returns tokens of compact form

Compare this program with Example 9.1.

  1 function CompactTokenizer(source){                                   (1)
        this.token="";                                                   (2)
        this.tokens=source.split(/(\[|\]|\n|@|[^\[\]\n@]+)/) // separate all tokens          (3)
              .filter((e)=>e.length>0 && !e.match(/^\n? *$/) // keep only non empty tokens
  5     );
        
        this.nextToken = function(){                                     (4)
            return this.token = this.tokens.length==0 ? null : this.tokens.shift();
        }
 10     
        this.skip=function(sym){                                         (5)
            if(this.token==sym)
                return this.nextToken();
            else 
 15             throw new ArgumentError("skip:"+sym+" expected but "+
                                        this.token+ " found");
        }
    }
    

1

Constructor for building a specialized tokenizer for the string given as source.

2

Keeps the current token.

3

Builds the array of all tokens by splitting according to important elements of the inputs. Here we are interested in the content matched by the regular expressions, so we capture the content matched. However as the input is matched by the regular expression, empty strings between each match are produced. These empty matches and those containing only spaces are removed by the call to filter.

4

Retrieves the first element of the tokens array, removes it and saves it as the current token, which is then returned as the value of the call.

5

Checks that the current token corresponds to the parameter. If this is the case, returns the next token, otherwise raises an exception. In principle, this exception should never be raised but this function might be useful (and it was!) for debugging purposes.


The creation of a new XML document is done by a call to the domExpand function which launches a series of recursive calls to expand.

Example 10.28. [DOMExpand.js] JavaScript compact form parsing in order to create an XML document

A sample input for this program is Example 5.8 to yield Example 2.2. Compare this program with Example 9.2.

  1 function expand(st,doc,defaultNS,elementName){                       (1)
        let elem;
        const colonPos=elementName.indexOf(":")                          (2)
        if (colonPos>0) // qualified element name
  5         elem = doc.createElementNS(elementName.substring(0,colonPos),
                                       elementName.substring(colonPos+1))
        else
            elem=doc.createElementNS(defaultNS,elementName);
        st.skip("[");
 10     while (st.token=="@"){ // process attributes                     (3)
            const attName=st.skip("@");
            st.nextToken();
            const attValue=st.skip("[");
            if (attName=="xmlns")defaultNS=attValue; // set new default namespace
 15         elem.setAttribute(attName,attValue);
            st.nextToken();
            st.skip("]")
        }
        while (st.token!="]"){                                           (4)
 20         const str=st.token.trim();
            st.nextToken();
            if (st.token=="["){
                elem.appendChild(expand(st,doc,defaultNS,str))           (5)
            } else {
 25             elem.appendChild(doc.createTextNode(str))
            }
        }
        st.skip("]");
        return elem;
 30 }
    
    function domExpand(s){                                               (6)
        let st=new CompactTokenizer(s);
        let rootName;
 35     while(st.nextToken()!="[")rootName=st.token;
        let doc= new Document()
        doc=expand(st,doc,"",rootName);
        return doc;
    }
 40 
    $(document).ready(function(){                                        (7)
        $("#expand").click(function(){                                   (8)
            const text=$("#text").val();
            const xmlStr=new XMLSerializer().serializeToString(domExpand(text))
 45         $("#xml").val(xmlStr)
        })
        $("#pprint").click(function(){                                   (9)
            const xmlStr=$("#xml").val();
            $("#xml").val(formatXml(xmlStr))
 50     })
    })

1

Defines the function with the following parameters: the CompactTokenizer object, the document node necessary for creating new elements, the default namespace and the element name to be created during expansion.

2

Creates a new element checking if the element name is qualified, in which case it sets the appropriate namespace on creation otherwise it uses the default namespace.

3

Each attribute is created with a call to setAttribute. The element name is the result of the call to skip("[") which returns the token after the opening bracket. If the name of the attribute is xmlns, it resets the default namespace.

4

Loops on each child. Gets the string that follows, which is then trimmed.

5

If the current token is an opening bracket, then we create a new element by a recursive call to expand which is added as a child of the current node. Otherwise the string is added as a text node.

6

The function receives a string with the compact form for which the CompactTokenizer is created. The root element name is the identifier preceding the first left bracket. A new document object is then created and given as parameter to the expand function.

7

Associates callback functions with the expand and pretty-print buttons.

8

Function that gets the value of the expand string (the right text box), gives it to the domExpand function whose result is serialized in the left box. Unfortunately, this result is not indented.

9

Function that gets the value of the raw XML and formats it using a function that it not shown here.