xmldoc.js 11 KB

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