258 lines
7.0 KiB
JavaScript
258 lines
7.0 KiB
JavaScript
(function () {
|
|
|
|
var sax;
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
// We're being used in a Node-like environment
|
|
sax = require('sax');
|
|
}
|
|
else {
|
|
// assume it's attached to the Window object in a browser
|
|
sax = this.sax;
|
|
|
|
if (!sax) // no sax for you!
|
|
throw new Error("Expected sax to be defined. Make sure you're including sax.js before this file.");
|
|
}
|
|
|
|
/*
|
|
XmlElement is our basic building block. Everything is an XmlElement; even XmlDocument
|
|
behaves like an XmlElement by inheriting its attributes and functions.
|
|
*/
|
|
|
|
function XmlElement(tag) {
|
|
// Capture the parser object off of the XmlDocument delegate
|
|
var parser = delegates[delegates.length - 1].parser;
|
|
|
|
this.name = tag.name;
|
|
this.attr = tag.attributes || {};
|
|
this.val = "";
|
|
this.isValCdata = false;
|
|
this.children = [];
|
|
this.firstChild = null;
|
|
this.lastChild = null;
|
|
|
|
// Assign parse information
|
|
this.line = parser.line;
|
|
this.column = parser.column;
|
|
this.position = parser.position;
|
|
this.startTagPosition = parser.startTagPosition;
|
|
}
|
|
|
|
// SaxParser handlers
|
|
|
|
XmlElement.prototype._opentag = function(tag) {
|
|
|
|
var child = new XmlElement(tag);
|
|
|
|
// add to our children array
|
|
this.children.push(child);
|
|
|
|
// update first/last pointers
|
|
if (!this.firstChild) this.firstChild = child;
|
|
this.lastChild = child;
|
|
|
|
delegates.unshift(child);
|
|
};
|
|
|
|
XmlElement.prototype._closetag = function() {
|
|
delegates.shift();
|
|
};
|
|
|
|
XmlElement.prototype._text = function(text) {
|
|
if (text) this.val += text;
|
|
};
|
|
|
|
XmlElement.prototype._cdata = function(cdata) {
|
|
if (cdata) {
|
|
this.val += cdata;
|
|
this.isValCdata=true;
|
|
}
|
|
};
|
|
|
|
XmlElement.prototype._error = function(err) {
|
|
throw err;
|
|
};
|
|
|
|
// Useful functions
|
|
|
|
XmlElement.prototype.eachChild = function(iterator, context) {
|
|
for (var i=0, l=this.children.length; i<l; i++)
|
|
if (iterator.call(context, this.children[i], i, this.children) === false) return;
|
|
};
|
|
|
|
XmlElement.prototype.childNamed = function(name) {
|
|
for (var i=0, l=this.children.length; i<l; i++) {
|
|
var child = this.children[i];
|
|
if (child.name === name) return child;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
XmlElement.prototype.childrenNamed = function(name) {
|
|
var matches = [];
|
|
|
|
for (var i=0, l=this.children.length; i<l; i++)
|
|
if (this.children[i].name === name)
|
|
matches.push(this.children[i]);
|
|
|
|
return matches;
|
|
};
|
|
|
|
XmlElement.prototype.childWithAttribute = function(name,value) {
|
|
for (var i=0, l=this.children.length; i<l; i++) {
|
|
var child = this.children[i];
|
|
if ( (value && child.attr[name] === value) || (!value && child.attr[name]) )
|
|
return child;
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
XmlElement.prototype.descendantWithPath = function(path) {
|
|
var descendant = this;
|
|
var components = path.split('.');
|
|
|
|
for (var i=0, l=components.length; i<l; i++)
|
|
if (descendant)
|
|
descendant = descendant.childNamed(components[i]);
|
|
else
|
|
return undefined;
|
|
|
|
return descendant;
|
|
};
|
|
|
|
XmlElement.prototype.valueWithPath = function(path) {
|
|
var components = path.split('@');
|
|
var descendant = this.descendantWithPath(components[0]);
|
|
if (descendant)
|
|
return components.length > 1 ? descendant.attr[components[1]] : descendant.val;
|
|
else
|
|
return undefined;
|
|
};
|
|
|
|
// String formatting (for debugging)
|
|
|
|
XmlElement.prototype.toString = function(options) {
|
|
return this.toStringWithIndent("", options);
|
|
};
|
|
|
|
XmlElement.prototype.toStringWithIndent = function(indent, options) {
|
|
var s = indent + "<" + this.name;
|
|
var linebreak = options && options.compressed ? "" : "\n";
|
|
var preserveWhitespace = options && options.preserveWhitespace;
|
|
|
|
for (var name in this.attr)
|
|
if (Object.prototype.hasOwnProperty.call(this.attr, name))
|
|
s += " " + name + '="' + escapeXML(this.attr[name]) + '"';
|
|
|
|
var finalVal = '';
|
|
if (this.isValCdata){
|
|
finalVal = '<![CDATA['+this.val+']]>';
|
|
} else if (preserveWhitespace) {
|
|
finalVal = escapeXML(this.val);
|
|
} else{
|
|
finalVal = escapeXML(this.val.trim());
|
|
}
|
|
if (options && options.trimmed && finalVal.length > 25)
|
|
finalVal = finalVal.substring(0,25).trim() + "…";
|
|
|
|
if (this.children.length) {
|
|
s += ">" + linebreak;
|
|
|
|
var childIndent = indent + (options && options.compressed ? "" : " ");
|
|
|
|
if (finalVal.length)
|
|
s += childIndent + finalVal + linebreak;
|
|
|
|
for (var i=0, l=this.children.length; i<l; i++)
|
|
s += this.children[i].toStringWithIndent(childIndent, options) + linebreak;
|
|
|
|
s += indent + "</" + this.name + ">";
|
|
}
|
|
else if (finalVal.length) {
|
|
s += ">" + finalVal + "</" + this.name +">";
|
|
}
|
|
else s += "/>";
|
|
|
|
return s;
|
|
};
|
|
|
|
/*
|
|
XmlDocument is the class we expose to the user; it uses the sax parser to create a hierarchy
|
|
of XmlElements.
|
|
*/
|
|
|
|
function XmlDocument(xml) {
|
|
xml && (xml = xml.toString().trim());
|
|
|
|
if (!xml)
|
|
throw new Error("No XML to parse!");
|
|
|
|
// Expose the parser to the other delegates while the parser is running
|
|
this.parser = sax.parser(true); // strict
|
|
addParserEvents(this.parser);
|
|
|
|
// We'll use the file-scoped "delegates" var to remember what elements we're currently
|
|
// parsing; they will push and pop off the stack as we get deeper into the XML hierarchy.
|
|
// It's safe to use a global because JS is single-threaded.
|
|
delegates = [this];
|
|
|
|
this.parser.write(xml);
|
|
|
|
// Remove the parser as it is no longer needed and should not be exposed to clients
|
|
delete this.parser;
|
|
}
|
|
|
|
// make XmlDocument inherit XmlElement's methods
|
|
extend(XmlDocument.prototype, XmlElement.prototype);
|
|
|
|
XmlDocument.prototype._opentag = function(tag) {
|
|
if (typeof this.children === 'undefined')
|
|
// the first tag we encounter should be the root - we'll "become" the root XmlElement
|
|
XmlElement.call(this,tag);
|
|
else
|
|
// all other tags will be the root element's children
|
|
XmlElement.prototype._opentag.apply(this,arguments);
|
|
};
|
|
|
|
// file-scoped global stack of delegates
|
|
var delegates = null;
|
|
|
|
/*
|
|
Helper functions
|
|
*/
|
|
|
|
function addParserEvents(parser) {
|
|
parser.onopentag = parser_opentag;
|
|
parser.onclosetag = parser_closetag;
|
|
parser.ontext = parser_text;
|
|
parser.oncdata = parser_cdata;
|
|
parser.onerror = parser_error;
|
|
}
|
|
|
|
// create these closures and cache them by keeping them file-scoped
|
|
function parser_opentag() { delegates[0]._opentag.apply(delegates[0],arguments) }
|
|
function parser_closetag() { delegates[0]._closetag.apply(delegates[0],arguments) }
|
|
function parser_text() { delegates[0]._text.apply(delegates[0],arguments) }
|
|
function parser_cdata() { delegates[0]._cdata.apply(delegates[0],arguments) }
|
|
function parser_error() { delegates[0]._error.apply(delegates[0],arguments) }
|
|
|
|
// a relatively standard extend method
|
|
function extend(destination, source) {
|
|
for (var prop in source)
|
|
if (source.hasOwnProperty(prop))
|
|
destination[prop] = source[prop];
|
|
}
|
|
|
|
// escapes XML entities like "<", "&", etc.
|
|
function escapeXML(value){
|
|
return value.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, '&').replace(/'/g, ''').replace(/"/g, '"');
|
|
}
|
|
|
|
// Are we being used in a Node-like environment?
|
|
if (typeof module !== 'undefined' && module.exports)
|
|
module.exports.XmlDocument = XmlDocument;
|
|
else
|
|
this.XmlDocument = XmlDocument;
|
|
|
|
})();
|