You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

414 lines
11 KiB

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