diff --git a/lib/xmldoc.js b/lib/xmldoc.js index d96128d..aab2545 100644 --- a/lib/xmldoc.js +++ b/lib/xmldoc.js @@ -26,8 +26,6 @@ function XmlElement(tag) { this.name = tag.name; this.attr = tag.attributes; this.val = ""; - this.isValCdata = false; - this.isValComment = false; this.children = []; this.firstChild = null; this.lastChild = null; @@ -39,18 +37,24 @@ function XmlElement(tag) { this.startTagPosition = parser.startTagPosition; } -// SaxParser handlers - -XmlElement.prototype._opentag = function(tag) { +// Private methods - var child = new XmlElement(tag); - +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); }; @@ -60,18 +64,26 @@ XmlElement.prototype._closetag = function() { }; XmlElement.prototype._text = function(text) { - this.val = text; + if (typeof this.children === 'undefined') + return + + this.val += text; + + this._addChild(new XmlTextNode(text)); }; XmlElement.prototype._cdata = function(cdata) { - this.val = cdata; - this.isValCdata=true; + this.val += cdata; + + this._addChild(new XmlCDataNode(cdata)); }; XmlElement.prototype._comment = function(comment) { - this.val = comment; - this.isValComment=true; -} + if (typeof this.children === 'undefined') + return + + this._addChild(new XmlCommentNode(comment)); +}; XmlElement.prototype._error = function(err) { throw err; @@ -81,7 +93,8 @@ XmlElement.prototype._error = function(err) { XmlElement.prototype.eachChild = function(iterator, context) { for (var i=0, l=this.children.length; i'; - } else if (preserveWhitespace) { - finalVal = escapeXML(this.val); - } else{ - finalVal = escapeXML(this.val.trim()); + if (this.children.length === 1 && this.children[0].type !== "element") { + s += ">" + this.children[0].toString(options) + ""; } - if (options && options.trimmed && finalVal.length > 25) - finalVal = finalVal.substring(0,25).trim() + "…"; - - if (this.children.length) { + else 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"; } - else if (finalVal.length) { - s += ">" + finalVal + ""; - } 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. @@ -263,6 +308,18 @@ function escapeXML(value){ return value.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; diff --git a/package.json b/package.json index 07f10cf..038f54b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,10 @@ { "name": "Nick Farina", "email": "nfarina@gmail.com" + }, + { + "name": "Caleb Meredith", + "email": "calebmeredith8@gmail.com" } ], "readmeFilename": "README.md", diff --git a/test/basic.js b/test/basic.js index 0af6dac..9ac0b39 100644 --- a/test/basic.js +++ b/test/basic.js @@ -5,10 +5,10 @@ t.test('verify sax global in browser', function (t) { // "un-require" the xmldoc module that we loaded up top delete require.cache[require.resolve('../')]; - + // also un-require the actual xmldoc module pulled in by index.js ('../') delete require.cache[require.resolve('../lib/xmldoc.js')]; - + // this signal will be picked up on by xmldoc.js global.xmldocAssumeBrowser = true; @@ -22,7 +22,7 @@ t.test('verify sax global in browser', function (t) { global.sax = {}; require('../'); t.ok(global.XmlDocument); - + t.end(); }) @@ -40,7 +40,7 @@ t.test('extend util', function(t) { }) t.test('parse xml', function (t) { - + var xmlString = 'world'; var parsed = new XmlDocument(xmlString); t.ok(parsed); @@ -50,19 +50,26 @@ t.test('parse xml', function (t) { }) t.test('cdata handling', function (t) { - + var xmlString = ']]>'; var parsed = new XmlDocument(xmlString); t.equal(parsed.val, ""); - t.equal(parsed.isValCdata, true); + t.end(); +}) + +t.test('cdata and text handling', function (t) { + + var xmlString = '(]]>)'; + var parsed = new XmlDocument(xmlString); + t.equal(parsed.val, "()"); t.end(); }) t.test('doctype handling', function (t) { - + var docWithType = new XmlDocument('world'); t.equal(docWithType.doctype, " HelloWorld"); - + var docWithoutType = new XmlDocument('world'); t.equal(docWithoutType.doctype, ""); @@ -74,30 +81,113 @@ t.test('doctype handling', function (t) { }) t.test('comment handling', function (t) { - + var xmlString = ''; var parsed = new XmlDocument(xmlString); - t.equal(parsed.val, " World "); - t.equal(parsed.isValComment, true); + t.equal(parsed.val, ""); + t.end(); +}) + +t.test('comment and text handling', function (t) { + + var xmlString = '()'; + var parsed = new XmlDocument(xmlString); + t.equal(parsed.val, "()"); + t.end(); +}) + +t.test('text, cdata, and comment handling', function (t) { + + var xmlString = 'Hello ]]>!'; + var parsed = new XmlDocument(xmlString); + t.equal(parsed.val, "Hello !"); + t.end(); +}) + +t.test('text with elements handling', function (t) { + + var xmlString = 'hello, !'; + var parsed = new XmlDocument(xmlString); + t.equal(parsed.val, "hello, !"); + t.end(); +}) + +t.test('text before root node', function (t) { + + var xmlString = '\n\n*'; + var xml = new XmlDocument(xmlString); + + t.equal(xml.val, '*'); + t.equal(xml.children.length, 1); + t.end(); +}) + +t.test('text after root node', function (t) { + + var xmlString = '*\n\n'; + var xml = new XmlDocument(xmlString); + + t.equal(xml.val, '*'); + t.equal(xml.children.length, 1); + t.end(); +}) + +t.test('text before root node with version', function (t) { + + var xmlString = '\n\n*'; + var xml = new XmlDocument(xmlString); + + t.equal(xml.val, '*'); + t.equal(xml.children.length, 1); + t.end(); +}) + +t.test('text after root node with version', function (t) { + + var xmlString = '*\n\n'; + var xml = new XmlDocument(xmlString); + + t.equal(xml.val, '*'); + t.equal(xml.children.length, 1); + t.end(); +}) + +t.test('comment before root node', function (t) { + + var xmlString = '*'; + var xml = new XmlDocument(xmlString); + + t.equal(xml.val, '*'); + t.equal(xml.children.length, 1); + t.end(); +}) + +t.test('comment after root node', function (t) { + + var xmlString = '*'; + var xml = new XmlDocument(xmlString); + + t.equal(xml.val, '*'); + t.equal(xml.children.length, 1); t.end(); }) t.test('error handling', function (t) { - + var xmlString = ''; - + t.throws(function() { var parsed = new XmlDocument(xmlString); }); - + t.end(); }) t.test('tag locations', function (t) { - + var xmlString = ''; var books = new XmlDocument(xmlString); - + var book = books.children[0]; t.equal(book.attr.title, "Twilight"); t.equal(book.startTagPosition, 8); @@ -108,34 +198,71 @@ t.test('tag locations', function (t) { }) t.test('eachChild', function (t) { - + var xmlString = ''; var books = new XmlDocument(xmlString); - + expectedTitles = ["Twilight", "Twister"]; - + books.eachChild(function(book, i, books) { t.equal(book.attr.title, expectedTitles[i]); }); - + + called = 0; + books.eachChild(function(book, i, books) { + called++; + return false; // test that returning false short-circuits the loop + }); + t.equal(called, 1); + + t.end(); +}) + +t.test('eachChild with text and comments', function (t) { + + var xmlString = 'text!'; + var books = new XmlDocument(xmlString); + + expectedTitles = ["Twilight", "Twister"]; + + var elI = 0; + + books.eachChild(function(book, i, books) { + t.equal(book.attr.title, expectedTitles[elI++]); + }); + called = 0; books.eachChild(function(book, i, books) { called++; return false; // test that returning false short-circuits the loop }); t.equal(called, 1); - + t.end(); }) t.test('childNamed', function (t) { - + var xmlString = ''; var books = new XmlDocument(xmlString); - + + var goodBook = books.childNamed('good-book'); + t.equal(goodBook.name, 'good-book'); + + var badBook = books.childNamed('bad-book'); + t.equal(badBook, undefined); + + t.end(); +}) + +t.test('childNamed with text', function (t) { + + var xmlString = 'text'; + var books = new XmlDocument(xmlString); + var goodBook = books.childNamed('good-book'); - t.equal(goodBook.name, 'good-book'); - + t.equal(goodBook.name, 'good-book'); + var badBook = books.childNamed('bad-book'); t.equal(badBook, undefined); @@ -143,10 +270,10 @@ t.test('childNamed', function (t) { }) t.test('childNamed', function (t) { - + var xmlString = ''; var fruits = new XmlDocument(xmlString); - + var apples = fruits.childrenNamed('apple'); t.equal(apples.length, 2); t.equal(apples[0].attr.sweet, 'yes'); @@ -155,48 +282,100 @@ t.test('childNamed', function (t) { }) t.test('childWithAttribute', function (t) { - + var xmlString = ''; var fruits = new XmlDocument(xmlString); - + var pickedFruit = fruits.childWithAttribute('pick', 'yes'); t.equal(pickedFruit.name, 'apple'); t.equal(pickedFruit.attr.pick, 'yes'); - + var rottenFruit = fruits.childWithAttribute('rotten'); t.equal(rottenFruit.name, 'orange'); - + var peeled = fruits.childWithAttribute('peeled'); t.equal(peeled, undefined); - + + t.end(); +}) + +t.test('childWithAttribute with text', function (t) { + + var xmlString = 'text'; + var fruits = new XmlDocument(xmlString); + + var pickedFruit = fruits.childWithAttribute('pick', 'yes'); + t.equal(pickedFruit.name, 'apple'); + t.equal(pickedFruit.attr.pick, 'yes'); + + var rottenFruit = fruits.childWithAttribute('rotten'); + t.equal(rottenFruit.name, 'orange'); + + var peeled = fruits.childWithAttribute('peeled'); + t.equal(peeled, undefined); + t.end(); }) t.test('descendantWithPath', function (t) { - + var xmlString = 'George R.R.Martin'; var book = new XmlDocument(xmlString); - + + var lastNameNode = book.descendantWithPath('author.last'); + t.equal(lastNameNode.val, 'Martin'); + + var middleNameNode = book.descendantWithPath('author.middle'); + t.equal(middleNameNode, undefined); + + var publisherNameNode = book.descendantWithPath('publisher.first'); + t.equal(publisherNameNode, undefined); + + t.end(); +}) + +t.test('descendantWithPath with text', function (t) { + + var xmlString = 'textGeorge R.R.Martin'; + var book = new XmlDocument(xmlString); + var lastNameNode = book.descendantWithPath('author.last'); t.equal(lastNameNode.val, 'Martin'); - + var middleNameNode = book.descendantWithPath('author.middle'); t.equal(middleNameNode, undefined); var publisherNameNode = book.descendantWithPath('publisher.first'); t.equal(publisherNameNode, undefined); - + t.end(); }) t.test('valueWithPath', function (t) { - + var xmlString = 'George R.R.Martin'; var book = new XmlDocument(xmlString); - + var lastName = book.valueWithPath('author.last'); t.equal(lastName, 'Martin'); - + + var lastNameHyphenated = book.valueWithPath('author.last@hyphenated'); + t.equal(lastNameHyphenated, "no"); + + var publisherName = book.valueWithPath('publisher.last@hyphenated'); + t.equal(publisherName, undefined); + + t.end(); +}) + +t.test('valueWithPath with text', function (t) { + + var xmlString = 'textGeorge R.R.Martin'; + var book = new XmlDocument(xmlString); + + var lastName = book.valueWithPath('author.last'); + t.equal(lastName, 'Martin'); + var lastNameHyphenated = book.valueWithPath('author.last@hyphenated'); t.equal(lastNameHyphenated, "no"); @@ -207,10 +386,10 @@ t.test('valueWithPath', function (t) { }) t.test('toString', function (t) { - + var xmlString = ''; var doc = new XmlDocument(xmlString); - + t.equal(doc.toString(), '\n \n'); t.equal(doc.toString({compressed:true}), ''); @@ -222,9 +401,19 @@ t.test('toString', function (t) { xmlString = ']]>'; doc = new XmlDocument(xmlString); - + t.equal(doc.toString(), ']]>'); + xmlString = 'Hello ]]>!'; + doc = new XmlDocument(xmlString); + + t.equal(doc.toString({preserveWhitespace:true}), '\n Hello\n \n \n ]]>\n !\n'); + + xmlString = 'hello, !'; + doc = new XmlDocument(xmlString); + + t.equal(doc.toString(), '\n hello,\n \n !\n'); + xmlString = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam et accumsan nisi.'; doc = new XmlDocument(xmlString); @@ -234,11 +423,11 @@ t.test('toString', function (t) { try { // test that adding stuff to the Object prototype doesn't interfere with attribute exporting Object.prototype.cruftyExtension = "You don't want this string to be exported!"; - + var xmlString = ''; var doc = new XmlDocument(xmlString); - - t.equal(doc.toString(), '\n \n'); + + t.equal(doc.toString(), '\n \n'); } finally { delete Object.prototype.cruftyExtensionMethod;