/**
|
|
* Simple XML DOM implementation based on sax that works with Strings.
|
|
*
|
|
* If you have an XML string and want a DOM this utility is convenient.
|
|
*
|
|
* var domjs = new DomJS();
|
|
* domjs.parse(xmlString, function(err, dom) {
|
|
*
|
|
* });
|
|
*
|
|
* If you want to compile C there are versions based on libxml2
|
|
* and jsdom is full featured but complicated.
|
|
*
|
|
* This is "lightweight" meaning simple and serves my purpose, it does not support namespaces or all
|
|
* of the features of XML 1.0 it just takes a string and returns a JavaScript object graph.
|
|
*
|
|
* There are only 5 types of object supported
|
|
* Element, Text, ProcessingInstruction, CDATASection and Comment.
|
|
*
|
|
* e.g.
|
|
*
|
|
* take <xml><elem att="val1"/><elem att="val1"/><elem att="val1"/></xml>
|
|
*
|
|
* return { name : "xml",
|
|
* attributes : {}
|
|
* children [
|
|
* { name : "elem", attributes : {att:'val1'}, children [] },
|
|
* { name : "elem", attributes : {att:'val1'}, children [] },
|
|
* { name : "elem", attributes : {att:'val1'}, children [] }
|
|
* ]
|
|
* }
|
|
*
|
|
* The object returned is an instance of Element and can be serialized back out with obj.toXml();
|
|
*
|
|
*
|
|
* @constructor DomJS
|
|
*/
|
|
var DomJS = function() {
|
|
/**
|
|
* The root element of the XML document currently being parsed.
|
|
*/
|
|
this.root = null;
|
|
this.stack = new Array();
|
|
this.currElement = null;
|
|
/**
|
|
* Flag that is set to true if there was a Sax error parsing the XML.
|
|
*/
|
|
this.error = false;
|
|
/**
|
|
* Use strict parsing, this value is passed to the sax parser.
|
|
*/
|
|
this.strict = true;
|
|
/**
|
|
* Set to true to parse and write ProcessingInstructions
|
|
* By default false for backwards comatability
|
|
*/
|
|
this.parseProcessingInstructions = false;
|
|
// undefined by default
|
|
// this.processingInstructions = new Array();
|
|
};
|
|
|
|
DomJS.prototype.parse = function(string, cb) {
|
|
if (typeof string != 'string') {
|
|
cb(true, 'Data is not a string');
|
|
return;
|
|
}
|
|
var self = this;
|
|
var parser = sax.parser(this.strict);
|
|
|
|
parser.onerror = function (err) {
|
|
self.error = true;
|
|
cb(true, err);
|
|
};
|
|
|
|
parser.ontext = function (text) {
|
|
if (self.currElement == null) {
|
|
// console.log("Content in the prolog " + text);
|
|
return;
|
|
}
|
|
var textNode = new Text(text);
|
|
self.currElement.children.push(textNode);
|
|
};
|
|
|
|
parser.onopencdata = function () {
|
|
var cdataNode = new CDATASection();
|
|
self.currElement.children.push(cdataNode);
|
|
};
|
|
|
|
parser.oncdata = function (data) {
|
|
var cdataNode = self.currElement.children[self.currElement.children.length - 1];
|
|
cdataNode.appendData(data);
|
|
};
|
|
|
|
// do nothing on parser.onclosecdata
|
|
parser.onopentag = function (node) {
|
|
var elem = new Element(node.name, node.attributes);
|
|
if (self.root == null) {
|
|
self.root = elem;
|
|
if ( self.processingInstructions ) {
|
|
elem.processingInstructions = self.processingInstructions;
|
|
}
|
|
}
|
|
if (self.currElement != null) {
|
|
self.currElement.children.push(elem);
|
|
}
|
|
self.currElement = elem;
|
|
self.stack.push(self.currElement);
|
|
};
|
|
|
|
parser.onclosetag = function (node) {
|
|
self.stack.pop();
|
|
self.currElement = self.stack[self.stack.length - 1 ];// self.stack.peek();
|
|
};
|
|
|
|
parser.oncomment = function (comment) {
|
|
if (self.currElement == null) {
|
|
//console.log("Comments in the prolog discarded " + comment);
|
|
return;
|
|
}
|
|
var commentNode = new Comment(comment);
|
|
self.currElement.children.push(commentNode);
|
|
};
|
|
|
|
parser.onprocessinginstruction = function (node) {
|
|
if (self.parseProcessingInstructions === true) {
|
|
if ( self.processingInstructions === undefined) {
|
|
self.processingInstructions = new Array();
|
|
}
|
|
var pi = new ProcessingInstruction(node.name, node.body);
|
|
self.processingInstructions.push(pi);
|
|
}
|
|
};
|
|
|
|
parser.onend = function () {
|
|
if ( self.error == false) {
|
|
cb(false, self.root);
|
|
}
|
|
};
|
|
|
|
parser.write(string).close();
|
|
};
|
|
|
|
DomJS.prototype.reset = function() {
|
|
this.root = null;
|
|
this.stack = new Array();
|
|
this.currElement = null;
|
|
this.error = false;
|
|
};
|
|
|
|
var escape = function(string) {
|
|
return string.replace(/&/g, '&')
|
|
.replace(/>/g, '>')
|
|
.replace(/</g, '<')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
};
|
|
|
|
/**
|
|
*
|
|
* @constructor Element
|
|
*/
|
|
var Element = function(name, attributes, children ) {
|
|
this.name = name;
|
|
this.attributes = attributes || [];
|
|
this.children = children || [];
|
|
// undefined by default
|
|
// this.processingInstructions = new Array();
|
|
};
|
|
Element.prototype.toXml = function(sb) {
|
|
if (typeof sb == 'undefined') {
|
|
sb = {buf:''}; // Strings are pass by value in JS it seems
|
|
}
|
|
|
|
if (this.processingInstructions) {
|
|
for (var i = 0 ; i < this.processingInstructions.length ; i++) {
|
|
sb.buf += '<?' + this.processingInstructions[i].name + ' ' + this.processingInstructions[i].body + '?>\n';
|
|
}
|
|
}
|
|
|
|
sb.buf += '<' + this.name;
|
|
for (att in this.attributes) {
|
|
|
|
sb.buf += ' ' + att + '="' + escape(this.attributes[att]) + '"';
|
|
}
|
|
if (this.children.length != 0) {
|
|
sb.buf += '>';
|
|
for (var i = 0 ; i < this.children.length ; i++) {
|
|
this.children[i].toXml(sb);
|
|
}
|
|
sb.buf += '</' + this.name + '>';
|
|
}
|
|
else {
|
|
sb.buf += '/>';
|
|
}
|
|
return sb.buf;
|
|
};
|
|
Element.prototype.firstChild = function() {
|
|
if ( this.children.length > 0) {
|
|
return this.children[0];
|
|
}
|
|
return null;
|
|
};
|
|
Element.prototype.text = function() {
|
|
if ( this.children.length > 0) {
|
|
if (typeof this.children[0].text == 'string') {
|
|
return this.children[0].text;
|
|
};
|
|
}
|
|
return null;
|
|
};
|
|
|
|
var Text = function(data){
|
|
this.text = data;
|
|
};
|
|
Text.prototype.toXml = function(sb) {
|
|
sb.buf += escape(this.text);
|
|
};
|
|
|
|
var Comment = function(comment) {
|
|
this.comment = comment;
|
|
};
|
|
Comment.prototype.toXml = function(sb) {
|
|
sb.buf += '<!--' + this.comment + '-->';
|
|
};
|
|
|
|
var ProcessingInstruction = function(name, body) {
|
|
this.name = name;
|
|
this.body = body;
|
|
};
|
|
ProcessingInstruction.prototype.toXml = function(sb) {
|
|
sb.buf += '<?' + this.name + ' ' + this.body + '?>';
|
|
};
|
|
|
|
var CDATASection = function(data){
|
|
this.text = data || '';
|
|
};
|
|
CDATASection.prototype.toXml = function(sb) {
|
|
sb.buf += '<![CDATA[' + this.text + ']]>';
|
|
};
|
|
CDATASection.prototype.appendData = function(data) {
|
|
this.text += data;
|
|
};
|