(function () { var sax; if (typeof module !== 'undefined' && module.exports && !global.xmldocAssumeBrowser) { // 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.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; } // Private methods XmlElement.prototype._addChild = function(child) { // add to our children array this.children.push(child); // update first/last pointers if (!this.firstChild) this.firstChild = child; this.lastChild = child; }; // SaxParser handlers XmlElement.prototype._opentag = function(tag) { var child = new XmlElement(tag); this._addChild(child); delegates.unshift(child); }; XmlElement.prototype._closetag = function() { delegates.shift(); }; XmlElement.prototype._text = function(text) { if (typeof this.children === 'undefined') return this.val += text; this._addChild(new XmlTextNode(text)); }; XmlElement.prototype._cdata = function(cdata) { this.val += cdata; this._addChild(new XmlCDataNode(cdata)); }; XmlElement.prototype._comment = function(comment) { if (typeof this.children === 'undefined') return this._addChild(new XmlCommentNode(comment)); }; XmlElement.prototype._error = function(err) { throw err; }; // Useful functions XmlElement.prototype.eachChild = function(iterator, context) { for (var i=0, l=this.children.length; i 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]) + '"'; if (this.children.length === 1 && this.children[0].type !== "element") { s += ">" + this.children[0].toString(options) + ""; } else if (this.children.length) { s += ">" + linebreak; var childIndent = indent + (options && options.compressed ? "" : " "); for (var i=0, l=this.children.length; i"; } else if (options && options.html) { var whiteList = [ "area", "base", "br", "col", "embed", "frame", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr" ]; if (whiteList.indexOf(this.name) !== -1) s += "/>"; else s += ">"; } else { s += "/>"; } return s; }; // Alternative XML nodes function XmlTextNode (text) { this.text = text; } XmlTextNode.prototype.toString = function(options) { return formatText(escapeXML(this.text), options); }; XmlTextNode.prototype.toStringWithIndent = function(indent, options) { return indent+this.toString(options); }; function XmlCDataNode (cdata) { this.cdata = cdata; } XmlCDataNode.prototype.toString = function(options) { return ""; }; XmlCDataNode.prototype.toStringWithIndent = function(indent, options) { return indent+this.toString(options); }; function XmlCommentNode (comment) { this.comment = comment; } XmlCommentNode.prototype.toString = function(options) { return ""; }; XmlCommentNode.prototype.toStringWithIndent = function(indent, options) { return indent+this.toString(options); }; // Node type tag XmlElement.prototype.type = "element"; XmlTextNode.prototype.type = "text"; XmlCDataNode.prototype.type = "cdata"; XmlCommentNode.prototype.type = "comment"; /* 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!"); // Stores doctype (if defined) this.doctype = ""; // 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); }; XmlDocument.prototype._doctype = function(doctype) { this.doctype += doctype; } // 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.oncomment = parser_comment; parser.ondoctype = parser_doctype; parser.onerror = parser_error; } // create these closures and cache them by keeping them file-scoped function parser_opentag() { delegates[0] && delegates[0]._opentag.apply(delegates[0],arguments) } function parser_closetag() { delegates[0] && delegates[0]._closetag.apply(delegates[0],arguments) } function parser_text() { delegates[0] && delegates[0]._text.apply(delegates[0],arguments) } function parser_cdata() { delegates[0] && delegates[0]._cdata.apply(delegates[0],arguments) } function parser_comment() { delegates[0] && delegates[0]._comment.apply(delegates[0],arguments) } function parser_doctype() { delegates[0] && delegates[0]._doctype.apply(delegates[0],arguments) } function parser_error() { delegates[0] && 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.toString().replace(/&/g, '&').replace(//g, ">").replace(/'/g, ''').replace(/"/g, '"'); } // formats some text for debugging given a few options function formatText(text, options) { var finalText = text; if (options && options.trimmed && text.length > 25) finalText = finalText.substring(0,25).trim() + "…"; if (!(options && options.preserveWhitespace)) finalText = finalText.trim(); return finalText; } // Are we being used in a Node-like environment? if (typeof module !== 'undefined' && module.exports && !global.xmldocAssumeBrowser) module.exports.XmlDocument = XmlDocument; else this.XmlDocument = XmlDocument; })();