xmldoc.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /**
  2. * github https://github.com/nfarina/xmldoc/blob/master/README.md
  3. */
  4. function createXmldoc() {
  5. var xmldoc = {};
  6. (function(xmldoc) {
  7. var sax;
  8. if (typeof module !== "undefined" && module.exports && !global.xmldocAssumeBrowser) {
  9. // We're being used in a Node-like environment
  10. sax = require("sax");
  11. } else {
  12. // assume it's attached to the Window object in a browser
  13. sax = this.sax;
  14. if (!sax) {
  15. // no sax for you!
  16. throw new Error("Expected sax to be defined. Make sure you're including sax.js before this file.");
  17. }
  18. }
  19. /**
  20. * XmlElement is our basic building block. Everything is an XmlElement; even XmlDocument
  21. * behaves like an XmlElement by inheriting its attributes and functions.
  22. */
  23. function XmlElement(tag, parser) {
  24. // If you didn't hand us a parser (common case) see if we can grab one
  25. // from the current execution stack.
  26. if (!parser) {
  27. var delegate = delegates[delegates.length - 1];
  28. if (delegate.parser) {
  29. parser = delegate.parser;
  30. }
  31. }
  32. this.name = tag.name;
  33. this.attr = tag.attributes;
  34. this.val = "";
  35. this.children = [];
  36. this.firstChild = null;
  37. this.lastChild = null;
  38. // Assign parse information
  39. this.line = parser ? parser.line : null;
  40. this.column = parser ? parser.column : null;
  41. this.position = parser ? parser.position : null;
  42. this.startTagPosition = parser ? parser.startTagPosition : null;
  43. }
  44. // Private methods
  45. XmlElement.prototype._addChild = function(child) {
  46. // add to our children array
  47. this.children.push(child);
  48. // update first/last pointers
  49. if (!this.firstChild) this.firstChild = child;
  50. this.lastChild = child;
  51. };
  52. // SaxParser handlers
  53. XmlElement.prototype._opentag = function(tag) {
  54. var child = new XmlElement(tag);
  55. this._addChild(child);
  56. delegates.unshift(child);
  57. };
  58. XmlElement.prototype._closetag = function() {
  59. delegates.shift();
  60. };
  61. XmlElement.prototype._text = function(text) {
  62. if (typeof this.children === "undefined") return;
  63. this.val += text;
  64. this._addChild(new XmlTextNode(text));
  65. };
  66. XmlElement.prototype._cdata = function(cdata) {
  67. this.val += cdata;
  68. this._addChild(new XmlCDataNode(cdata));
  69. };
  70. XmlElement.prototype._comment = function(comment) {
  71. if (typeof this.children === "undefined") return;
  72. this._addChild(new XmlCommentNode(comment));
  73. };
  74. XmlElement.prototype._error = function(err) {
  75. throw err;
  76. };
  77. // Useful functions
  78. XmlElement.prototype.eachChild = function(iterator, context) {
  79. for (var i = 0, l = this.children.length; i < l; i++) if (this.children[i].type === "element") if (iterator.call(context, this.children[i], i, this.children) === false) return;
  80. };
  81. XmlElement.prototype.childNamed = function(name) {
  82. for (var i = 0, l = this.children.length; i < l; i++) {
  83. var child = this.children[i];
  84. if (child.name === name) return child;
  85. }
  86. return undefined;
  87. };
  88. XmlElement.prototype.childrenNamed = function(name) {
  89. var matches = [];
  90. for (var i = 0, l = this.children.length; i < l; i++) if (this.children[i].name === name) matches.push(this.children[i]);
  91. return matches;
  92. };
  93. XmlElement.prototype.childWithAttribute = function(name, value) {
  94. for (var i = 0, l = this.children.length; i < l; i++) {
  95. var child = this.children[i];
  96. if (child.type === "element" && (value && child.attr[name] === value || !value && child.attr[name])) return child;
  97. }
  98. return undefined;
  99. };
  100. XmlElement.prototype.descendantsNamed = function(name) {
  101. var matches = [];
  102. for (var i = 0, l = this.children.length; i < l; i++) {
  103. var child = this.children[i];
  104. if (child.type === "element") {
  105. if (child.name === name) matches.push(child);
  106. matches = matches.concat(child.descendantsNamed(name));
  107. }
  108. }
  109. return matches;
  110. };
  111. XmlElement.prototype.descendantWithPath = function(path) {
  112. var descendant = this;
  113. var components = path.split(".");
  114. for (var i = 0, l = components.length; i < l; i++) if (descendant && descendant.type === "element") descendant = descendant.childNamed(components[i]); else return undefined;
  115. return descendant;
  116. };
  117. XmlElement.prototype.valueWithPath = function(path) {
  118. var components = path.split("@");
  119. var descendant = this.descendantWithPath(components[0]);
  120. if (descendant) return components.length > 1 ? descendant.attr[components[1]] : descendant.val; else return undefined;
  121. };
  122. // String formatting (for debugging)
  123. XmlElement.prototype.toString = function(options) {
  124. return this.toStringWithIndent("", options);
  125. };
  126. XmlElement.prototype.toStringWithIndent = function(indent, options) {
  127. var s = indent + "<" + this.name;
  128. var linebreak = options && options.compressed ? "" : "\n";
  129. var preserveWhitespace = options && options.preserveWhitespace;
  130. for (var name in this.attr) if (Object.prototype.hasOwnProperty.call(this.attr, name)) s += " " + name + '="' + escapeXML(this.attr[name]) + '"';
  131. if (this.children.length === 1 && this.children[0].type !== "element") {
  132. s += ">" + this.children[0].toString(options) + "</" + this.name + ">";
  133. } else if (this.children.length) {
  134. s += ">" + linebreak;
  135. var childIndent = indent + (options && options.compressed ? "" : " ");
  136. for (var i = 0, l = this.children.length; i < l; i++) {
  137. s += this.children[i].toStringWithIndent(childIndent, options) + linebreak;
  138. }
  139. s += indent + "</" + this.name + ">";
  140. } else if (options && options.html) {
  141. var whiteList = [ "area", "base", "br", "col", "embed", "frame", "hr", "img", "input", "keygen", "link", "menuitem", "meta", "param", "source", "track", "wbr" ];
  142. if (whiteList.indexOf(this.name) !== -1) s += "/>"; else s += "></" + this.name + ">";
  143. } else {
  144. s += "/>";
  145. }
  146. return s;
  147. };
  148. // Alternative XML nodes
  149. function XmlTextNode(text) {
  150. this.text = text;
  151. }
  152. XmlTextNode.prototype.toString = function(options) {
  153. return formatText(escapeXML(this.text), options);
  154. };
  155. XmlTextNode.prototype.toStringWithIndent = function(indent, options) {
  156. return indent + this.toString(options);
  157. };
  158. function XmlCDataNode(cdata) {
  159. this.cdata = cdata;
  160. }
  161. XmlCDataNode.prototype.toString = function(options) {
  162. return "<![CDATA[" + formatText(this.cdata, options) + "]]>";
  163. };
  164. XmlCDataNode.prototype.toStringWithIndent = function(indent, options) {
  165. return indent + this.toString(options);
  166. };
  167. function XmlCommentNode(comment) {
  168. this.comment = comment;
  169. }
  170. XmlCommentNode.prototype.toString = function(options) {
  171. return "\x3c!--" + formatText(escapeXML(this.comment), options) + "--\x3e";
  172. };
  173. XmlCommentNode.prototype.toStringWithIndent = function(indent, options) {
  174. return indent + this.toString(options);
  175. };
  176. // Node type tag
  177. XmlElement.prototype.type = "element";
  178. XmlTextNode.prototype.type = "text";
  179. XmlCDataNode.prototype.type = "cdata";
  180. XmlCommentNode.prototype.type = "comment";
  181. /**
  182. * XmlDocument is the class we expose to the user; it uses the sax parser to create a hierarchy
  183. * of XmlElements.
  184. */
  185. function XmlDocument(xml) {
  186. xml && (xml = xml.toString().trim());
  187. if (!xml) throw new Error("No XML to parse!");
  188. // Stores doctype (if defined)
  189. this.doctype = "";
  190. // Expose the parser to the other delegates while the parser is running
  191. this.parser = sax.parser(true); // strict
  192. addParserEvents(this.parser);
  193. // We'll use the file-scoped "delegates" var to remember what elements we're currently
  194. // parsing; they will push and pop off the stack as we get deeper into the XML hierarchy.
  195. // It's safe to use a global because JS is single-threaded.
  196. delegates = [ this ];
  197. try {
  198. this.parser.write(xml);
  199. } finally {
  200. // Remove the parser as it is no longer needed and should not be exposed to clients
  201. delete this.parser;
  202. }
  203. }
  204. // make XmlDocument inherit XmlElement's methods
  205. extend(XmlDocument.prototype, XmlElement.prototype);
  206. XmlDocument.prototype._opentag = function(tag) {
  207. if (typeof this.children === "undefined")
  208. // the first tag we encounter should be the root - we'll "become" the root XmlElement
  209. XmlElement.call(this, tag);
  210. // all other tags will be the root element's children
  211. else XmlElement.prototype._opentag.apply(this, arguments);
  212. };
  213. XmlDocument.prototype._doctype = function(doctype) {
  214. this.doctype += doctype;
  215. };
  216. // file-scoped global stack of delegates
  217. var delegates = null;
  218. /*
  219. * Helper functions
  220. */
  221. function addParserEvents(parser) {
  222. parser.onopentag = parser_opentag;
  223. parser.onclosetag = parser_closetag;
  224. parser.ontext = parser_text;
  225. parser.oncdata = parser_cdata;
  226. parser.oncomment = parser_comment;
  227. parser.ondoctype = parser_doctype;
  228. parser.onerror = parser_error;
  229. }
  230. // create these closures and cache them by keeping them file-scoped
  231. function parser_opentag() {
  232. delegates[0] && delegates[0]._opentag.apply(delegates[0], arguments);
  233. }
  234. function parser_closetag() {
  235. delegates[0] && delegates[0]._closetag.apply(delegates[0], arguments);
  236. }
  237. function parser_text() {
  238. delegates[0] && delegates[0]._text.apply(delegates[0], arguments);
  239. }
  240. function parser_cdata() {
  241. delegates[0] && delegates[0]._cdata.apply(delegates[0], arguments);
  242. }
  243. function parser_comment() {
  244. delegates[0] && delegates[0]._comment.apply(delegates[0], arguments);
  245. }
  246. function parser_doctype() {
  247. delegates[0] && delegates[0]._doctype.apply(delegates[0], arguments);
  248. }
  249. function parser_error() {
  250. delegates[0] && delegates[0]._error.apply(delegates[0], arguments);
  251. }
  252. // a relatively standard extend method
  253. function extend(destination, source) {
  254. for (var prop in source) if (source.hasOwnProperty(prop)) destination[prop] = source[prop];
  255. }
  256. // escapes XML entities like "<", "&", etc.
  257. function escapeXML(value) {
  258. return value.toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&apos;").replace(/"/g, "&quot;");
  259. }
  260. // formats some text for debugging given a few options
  261. function formatText(text, options) {
  262. var finalText = text;
  263. if (options && options.trimmed && text.length > 25) {
  264. finalText = finalText.substring(0, 25).trim() + "…";
  265. }
  266. if (!(options && options.preserveWhitespace)) {
  267. finalText = finalText.trim();
  268. }
  269. return finalText;
  270. }
  271. // // Are we being used in a Node-like environment?
  272. // if (typeof define === "function" && define.amd) {
  273. // define(function() {
  274. // return {
  275. // XmlDocument: XmlDocument,
  276. // XmlElement: XmlElement,
  277. // XmlTextNode: XmlTextNode,
  278. // XmlCDataNode: XmlCDataNode,
  279. // XmlCommentNode: XmlCommentNode
  280. // };
  281. // });
  282. // } else if (typeof module === "object" && module.exports) {
  283. // module.exports.XmlDocument = XmlDocument;
  284. // module.exports.XmlElement = XmlElement;
  285. // module.exports.XmlTextNode = XmlTextNode;
  286. // module.exports.XmlCDataNode = XmlCDataNode;
  287. // module.exports.XmlCommentNode = XmlCommentNode;
  288. // } else {
  289. // xmldoc.XmlDocument = XmlDocument;
  290. // xmldoc.XmlElement = XmlElement;
  291. // xmldoc.XmlTextNode = XmlTextNode;
  292. // xmldoc.XmlCDataNode = XmlCDataNode;
  293. // xmldoc.XmlCommentNode = XmlCommentNode;
  294. // }
  295. xmldoc.XmlDocument = XmlDocument;
  296. xmldoc.XmlElement = XmlElement;
  297. xmldoc.XmlTextNode = XmlTextNode;
  298. xmldoc.XmlCDataNode = XmlCDataNode;
  299. xmldoc.XmlCommentNode = XmlCommentNode;
  300. })(xmldoc = {});
  301. return xmldoc;
  302. }