1 // -----------------------------------------------------------------------------
\r
2 // IWF - Interactive Website Framework. Javascript library for creating
\r
3 // responsive thin client interfaces.
\r
5 // Copyright (C) 2005 Brock Weaver brockweaver@gmail.com
\r
7 // This library is free software; you can redistribute it and/or modify
\r
8 // it under the terms of the GNU Lesser General Public License as published
\r
9 // by the Free Software Foundation; either version 2.1 of the License, or
\r
10 // (at your option) any later version.
\r
12 // This library is distributed in the hope that it will be useful, but
\r
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
\r
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
\r
15 // License for more details.
\r
17 // You should have received a copy of the GNU Lesser General Public License
\r
18 // along with this library; if not, write to the Free Software Foundation,
\r
19 // Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
\r
22 // brockweaver@gmail.com
\r
25 // -----------------------------------------------------------------------------
\r
27 // --------------------------------------------------------------------------
\r
30 // Javascript-based xml parser
\r
35 // Brock Weaver - brockweaver@sourceforge.net - iwf.sourceforge.net
\r
36 // v 0.1 - 2005-06-05
\r
38 // --------------------------------------------------------------------------
\r
39 // This class is meant to ease the burden of parsing xml.
\r
40 // Xml is parsed into sets of arrays, in a hierarchical format.
\r
41 // Each node is an array (of length 1, if applicable)
\r
42 // The root node is an exception, as it is simply a node and not an array,
\r
43 // as there is always only a single root node.
\r
44 // Each attribute is a property of the current element in the node array.
\r
45 // Useful for loading xml from server into a nice, easy-to-use format
\r
46 // --------------------------------------------------------------------------
\r
48 if (!window.iwfGetById){
\r
49 iwfLog("IWF Dependency Error: iwfxml.js is dependent upon iwfcore.js, so you *must* reference that file first.\n\nExample:\n\n<script type='text/javascript' src='iwfcore.js'></script>\n<script type='text/javascript' src='iwfxml.js'></script>", true);
\r
53 // -----------------------------------
\r
54 // Begin: Xml Document Object
\r
55 // -----------------------------------
\r
57 function iwfXmlDoc(xml){
\r
58 if (!xml || xml.length == 0){
\r
64 iwfXmlDoc.prototype.loadXml = function(xml){
\r
66 // note this root node is not an array, as there will always
\r
67 // be exactly 1 root.
\r
68 var node = new iwfXmlNode(xml);
\r
69 this.childNodes = new Array();
\r
70 this.childNodes.push(node);
\r
71 this[node.nodeName] = node;
\r
77 iwfXmlDoc.prototype.toString = function(pretty){
\r
79 return this.outerXml(pretty);
\r
82 iwfXmlDoc.prototype.outerXml = function(pretty){
\r
83 if (!this.childNodes || this.childNodes.length < 1){
\r
86 var writer = new iwfWriter(true, pretty, this._html);
\r
87 return this.childNodes[0].outerXml(writer);
\r
91 // -----------------------------------
\r
92 // End: Xml Document Object
\r
93 // -----------------------------------
\r
96 // -----------------------------------
\r
97 // Begin: Xml Node Object
\r
98 // -----------------------------------
\r
101 // --------------------------------------------------------------------------
\r
104 // Brock Weaver - brockweaver@gmail.com
\r
105 // v 0.1 - 2005-06-05
\r
106 // Initial release.
\r
107 // --------------------------------------------------------------------------
\r
108 // This class is used to represent a single xml node.
\r
109 // All xml is kept except processing instructions.
\r
110 // There are issues with "<" and ">" chars embedded within
\r
111 // the xml as text. This is technically not valid xml, so
\r
112 // it is technically not a problem I guess.
\r
114 function iwfXmlNode(xml, parent){
\r
116 this.childNodes = new Array();
\r
118 this.attributes = new Array();
\r
119 this.attributeNames = new Array();
\r
120 this.attributeQuotes = new Array();
\r
121 this.nodeName = 'UNKNOWN';
\r
122 this._parent = parent;
\r
123 this._isEmpty = false;
\r
126 if (!xml || xml.length == 0){
\r
129 this._parseXml(xml);
\r
132 this._htmlMode = false;
\r
137 iwfXmlNode.prototype.toString = function(innerOnly){
\r
139 return this.innerXml();
\r
141 return this.outerXml();
\r
145 iwfXmlNode.prototype.outerXml = function(writer){
\r
146 return this._serialize(writer, false);
\r
149 iwfXmlNode.prototype.innerXml = function(writer){
\r
150 return this._serialize(writer, true)
\r
153 iwfXmlNode.prototype.outerHtml = function() {
\r
154 this._htmlMode = true;
\r
155 var ret = this.outerXml();
\r
156 this._htmlMode = false;
\r
160 iwfXmlNode.prototype.innerHtml = function() {
\r
161 this._htmlMode = true;
\r
162 var ret = this.innerXml();
\r
163 this._htmlMode = false;
\r
167 iwfXmlNode.prototype._serialize = function(writer, innerOnly){
\r
168 var pretty = false;
\r
169 if (typeof(writer) == 'boolean'){
\r
174 writer = new iwfWriter(true, pretty, this._htmlMode);
\r
178 writer.writeNodeOpen(this.nodeName);
\r
179 for(var i=0;i<this.attributes.length;i++){
\r
180 writer.writeAttribute(this.attributeNames[i], this.attributes[i], this.attributeQuotes[i]);
\r
184 writer._writeRawText(this._text);
\r
185 for(var i=0;i<this.childNodes.length;i++){
\r
186 this.childNodes[i].outerXml(writer);
\r
190 writer.writeNodeClose();
\r
193 return writer.toString();
\r
196 iwfXmlNode.prototype.removeAttribute = function(name){
\r
197 for(var i=0;i<this.attributeNames.length;i++){
\r
198 if (this.attributeNames[i] == name){
\r
200 // found the attribute. splice it out of all the arrays.
\r
201 this.attributeQuotes.splice(j, 1);
\r
202 this.attributeNames.splice(j, 1);
\r
203 this.attributes.splice(j, 1);
\r
204 // remove from our object and jump out of the loop
\r
209 // // found the attribute. move all ones following it down one
\r
210 // for(var j=i;j<this.attributeNames.length-1;j++){
\r
211 // this.attributeQuotes[j] = this.attributeQuotes[j+1];
\r
212 // this.attributeNames[j] = this.attributeNames[j+1];
\r
213 // this.attributes[j] = this.attributes[j+1];
\r
215 // // pop off the last one, as it's now a duplicate
\r
216 // this.attributeQuotes.pop();
\r
217 // this.attributeNames.pop();
\r
218 // this.attributes.pop();
\r
219 // delete this[name];
\r
225 iwfXmlNode.prototype.removeChildNode = function(node){
\r
226 for(var i=0;i<this.childNodes.length;i++){
\r
227 if (node == this.childNodes[i]){
\r
228 // remove from childNodes array
\r
229 this.childNodes.splice(k, 1);
\r
230 var arr = this[node.nodeName];
\r
232 if (arr.length > 1){
\r
234 for(j=0;i<arr.length;i++){
\r
235 if (arr[j] == node){
\r
249 iwfXmlNode.prototype.addAttribute = function(name, val, quotesToUse){
\r
251 // assume apostrophes
\r
255 this.attributeQuotes.push(quotesToUse);
\r
256 this.attributeNames.push(name);
\r
257 this.attributes.push(val);
\r
262 iwfXmlNode.prototype.addChildNode = function(node, nodeName){
\r
263 if (nodeName && nodeName.length > 0 && nodeName.indexOf("#") == 0){
\r
265 node = new iwfXmlNode('', this);
\r
266 node.nodeName = nodeName;
\r
270 // push it onto the childNodes array
\r
271 this.childNodes.push(node);
\r
275 // make a node object out of it if they gave us a string...
\r
276 if (typeof(node) == 'string'){
\r
277 node = new iwfXmlNode(node, this);
\r
280 if (node.nodeName.indexOf("#") == 0){
\r
281 // is a special node -- duplicate names may exist...
\r
284 // push it onto the childNodes array
\r
285 this.childNodes.push(node);
\r
289 if (!this.childNodes[node.nodeName]){
\r
290 // no other child node with this name exists yet.
\r
292 // make a new array off of our object
\r
293 this[node.nodeName] = new Array();
\r
296 // push it onto our object's array
\r
297 this[node.nodeName].push(node);
\r
299 // push it onto the childNodes array
\r
300 this.childNodes.push(node);
\r
305 iwfXmlNode.prototype.getText = function(){
\r
307 if (this.nodeName.indexOf('#') == 0){
\r
310 for(var i=0;i<this.childNodes.length;i++){
\r
311 txt += this.childNodes[i].getText();
\r
316 iwfXmlNode.prototype._parseXml = function(xml){
\r
318 var remainingXml = xml;
\r
320 while(remainingXml.length > 0){
\r
321 var type = this._determineNodeType(remainingXml);
\r
324 remainingXml = this._parseName(remainingXml);
\r
325 remainingXml = this._parseAtts(remainingXml);
\r
327 this._parent.addChildNode(this, null);
\r
330 if (!this._isEmpty){
\r
331 remainingXml = this._parseChildren(remainingXml);
\r
332 // we still need to parse out the close node, so don't jump out yet!
\r
334 return remainingXml;
\r
339 return this._parseText(remainingXml);
\r
342 return this._parseClosing(remainingXml);
\r
348 // return this._parsePI(remainingXml);
\r
349 remainingXml = this._parsePI(remainingXml);
\r
353 return this._parseComment(remainingXml);
\r
356 return this._parseCDATA(remainingXml);
\r
359 remainingXml = this._parseWhitespace(remainingXml);
\r
361 return remainingXml;
\r
366 iwfLog('IWF Xml Parsing Error: undefined type of ' + type + ' returned for xml starting with "' + remainingXml + '"', true);
\r
374 iwfXmlNode.prototype._determineNodeType = function(xml){
\r
377 if (!xml || xml.length == 0){
\r
381 var trimmed = this.ltrim(xml);
\r
383 var firstTrimmedLt = trimmed.indexOf("<");
\r
385 switch(firstTrimmedLt){
\r
387 // this is either insignificant whitespace or text
\r
388 if (trimmed.length == 0){
\r
389 return 'whitespace';
\r
394 // this is either an open node or insignificant whitespace.
\r
395 var firstLt = xml.indexOf("<");
\r
397 return 'whitespace'
\r
399 switch(trimmed.charAt(1)){
\r
403 if (trimmed.substr(0,4) == '<!--'){
\r
405 } else if (trimmed.substr(0,9) == '<![CDATA[') {
\r
408 return 'unknown: ' + trimmed.substr(0,10);
\r
418 // this is a text node
\r
423 iwfXmlNode.prototype._parseName = function(xml){
\r
424 // we know xml starts with <.
\r
426 var firstSpace = xml.indexOf(" ");
\r
427 var firstApos = xml.indexOf("'");
\r
428 var firstQuote = xml.indexOf('"');
\r
429 var firstGt = xml.indexOf(">");
\r
431 if (firstGt == -1){
\r
432 iwfLog("IWF Xml Parsing Error: Bad xml; no > found for an open node.", true);
\r
436 // see if it's an empty node...
\r
437 if (xml.charAt(firstGt-1) == '/'){
\r
438 this._isEmpty = true;
\r
441 // see if there is a possibility that atts exist
\r
442 if (firstSpace > firstGt || firstSpace == -1){
\r
443 if (this._isEmpty){
\r
445 this.nodeName = xml.substr(1,firstGt-2);
\r
448 this.nodeName = xml.substr(1,firstGt-1);
\r
450 // return starting at > so parseAtts knows there are no atts.
\r
451 return xml.substr(firstGt);
\r
456 this.nodeName = xml.substr(1,firstSpace-1);
\r
459 // eat everything up to the space, return the rest
\r
460 return xml.substr(firstSpace);
\r
465 iwfXmlNode.prototype._parseAtts = function(xml){
\r
468 xml = this.ltrim(xml);
\r
469 var firstGt = xml.indexOf(">");
\r
470 if (firstGt == -1){
\r
471 iwfLog("IWF Xml Parsing Error: Bad xml; no > found when parsing atts for " + this.nodeName, true);
\r
473 } else if (firstGt == 0){
\r
475 return xml.substr(firstGt+1);
\r
477 // at least one att exists.
\r
478 var attxml = xml.substr(0, firstGt);
\r
479 var re = /\s*?([^=]*?)=(['"])(.*?)(\2)/;
\r
480 var matches= re.exec(attxml)
\r
482 attxml = this.ltrim(attxml.substr(matches[0].length));
\r
483 var attname = this.ltrim(matches[1]);
\r
484 var attval = matches[3];
\r
485 var quotesToUse = matches[2];
\r
486 this.addAttribute(attname, attval, quotesToUse);
\r
488 re = /\s*?([^=]*?)=(['"])(.*?)(\2)/;
\r
489 matches = re.exec(attxml);
\r
493 // return everything after the end of the att list and the > which closes the start of the node.
\r
494 return xml.substr(firstGt+1);
\r
498 iwfXmlNode.prototype._parseChildren = function(xml){
\r
499 // we are at the > which closes the open node.
\r
501 if (xml && xml.length > 0){
\r
502 var endnode = "</" + this.nodeName + ">";
\r
503 while (xml && xml.length > 0 && xml.indexOf(endnode) > 0){
\r
505 // do not pass the xml to the constructor, as that may cause an infinite loop.
\r
506 var childNode = new iwfXmlNode('', this);
\r
507 xml = childNode._parseXml(xml);
\r
510 // note we don't cut off the close node here, as the _parseXml function will do this for us.
\r
519 iwfXmlNode.prototype._parseClosing = function(xml){
\r
520 var firstGt = xml.indexOf("</");
\r
521 if (firstGt == -1){
\r
522 iwfLog('IWF Xml Parsing Error: Bad xml; no </' + this.nodeName + ' found', true);
\r
525 var firstLt = xml.indexOf(">",firstGt);
\r
526 if (firstLt == -1){
\r
527 iwfLog('IWF Xml Parsing Error: Bad xml; no > found after </' + this.nodeName, true);
\r
530 var result = xml.substr(firstLt+1);
\r
536 iwfXmlNode.prototype._parseText = function(xml){
\r
537 return this._parsePoundNode(xml, "#text", "", "<", false);
\r
540 iwfXmlNode.prototype._parsePI = function(xml){
\r
541 var result = this._eatXml(xml, "?>", true);
\r
545 iwfXmlNode.prototype._parseComment = function(xml){
\r
546 return this._parsePoundNode(xml, "#comment", "<!--", "-->", true);
\r
549 iwfXmlNode.prototype._parseCDATA = function(xml){
\r
550 return this._parsePoundNode(xml, "#cdata", "<![CDATA[", "]]>", true);
\r
553 iwfXmlNode.prototype._parseWhitespace = function(xml){
\r
555 if (this._parent && this._parent.nodeName.toLowerCase() == 'pre'){
\r
556 // hack for supporting HTML's <pre> node. ugly, I know :)
\r
557 result = this._parsePoundNode(xml, "#text", "", "<", false);
\r
559 // whitespace is insignificant otherwise
\r
560 result = this._eatXml(xml, "<", false);
\r
565 iwfXmlNode.prototype._parsePoundNode = function(xml, nodeName, beginMoniker, endMoniker, eatMoniker){
\r
566 // simply slurp everything up until the first endMoniker, starting after the beginMoniker
\r
567 var end = xml.indexOf(endMoniker);
\r
569 iwfLog("IWF Xml Parsing Error: Bad xml: " + nodeName + " does not have ending " + endMoniker, true);
\r
572 var len = beginMoniker.length;
\r
573 var s = xml.substr(len, end - len);
\r
575 this._parent.addChildNode(s, nodeName);
\r
577 var result = xml.substr(end + (eatMoniker ? endMoniker.length : 0));
\r
582 iwfXmlNode.prototype._eatXml = function(xml, moniker, eatMoniker){
\r
583 var pos = xml.indexOf(moniker);
\r
586 pos += moniker.length;
\r
589 return xml.substr(pos);
\r
593 iwfXmlNode.prototype.trim = function(s){
\r
594 return s.replace(/^\s*|\s*$/g,"");
\r
597 iwfXmlNode.prototype.ltrim = function(s){
\r
598 return s.replace(/^\s*/g,"");
\r
601 iwfXmlNode.prototype.rtrim = function(s){
\r
602 return s.replace(/\s*$/g,"");
\r
605 // -----------------------------------
\r
606 // End: Xml Node Object
\r
607 // -----------------------------------
\r
615 // -----------------------------------
\r
616 // Begin: Xml Writer Object
\r
617 // -----------------------------------
\r
620 // --------------------------------------------------------------------------
\r
623 // Brock Weaver - brockweaver@gmail.com
\r
625 // v 0.1 - 2005-06-05
\r
626 // Initial release.
\r
627 // --------------------------------------------------------------------------
\r
628 // This class is meant to ease the creation of xml strings.
\r
629 // Note it is not a fully-compliant xml generator.
\r
630 // --------------------------------------------------------------------------
\r
633 function iwfWriter(suppressProcessingInstruction, prettyPrint, htmlMode) {
\r
634 this._buffer = new String();
\r
635 if (!suppressProcessingInstruction){
\r
636 this.writeRaw("<?xml version='1.0' ?>");
\r
638 this._nodeStack = new Array();
\r
639 this._nodeOpened = false;
\r
640 this._prettyPrint = prettyPrint;
\r
641 this._htmlMode = htmlMode
\r
644 iwfWriter.prototype.writePretty = function(lfBeforeTabbing){
\r
645 if (this._prettyPrint){
\r
646 if (lfBeforeTabbing){
\r
647 this.writeRaw('\n');
\r
650 // assumption is most xml won't have a maximum node depth exceeding 30...
\r
651 var tabs = '\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t';
\r
652 while (tabs.length < this._nodeStack.length){
\r
653 // but some xml might exceed node depth of 30, so keep tacking on another 30 tabs until we have enough...
\r
654 // I know this is an awkward way of doing it, but I wanted to avoid looping and concatenating for "most" xml...
\r
657 this.writeRaw(tabs.substr(0,this._nodeStack.length));
\r
661 iwfWriter.prototype.writeNode = function(name, txt){
\r
662 this.writeNodeOpen(name);
\r
663 this.writeText(txt);
\r
664 this.writeNodeClose();
\r
667 iwfWriter.prototype.writeNodeOpen = function(name){
\r
668 if (this._nodeOpened){
\r
669 this.writeRaw(">");
\r
671 this._nodeOpened = false;
\r
673 this.writePretty(true);
\r
677 this.writeRaw('<?');
\r
683 this.writeRaw('<![CDATA[');
\r
686 this.writeRaw('<!--');
\r
689 this.writeRaw("<" + name);
\r
690 this._nodeOpened = true;
\r
693 this._nodeStack.push(name);
\r
696 iwfWriter.prototype.writeAttribute = function(name, val, quotes){
\r
697 if (!this._nodeOpened){
\r
698 iwfLog("IWF Xml Parsing Error: Appending attribute '" + name +
\r
699 "' when no open node exists!", true);
\r
708 // browsers handle html attributes in a special way:
\r
709 // only the character that matches the html delimiter for the attribute should be xml-escaped.
\r
710 // the OTHER quote character (' or ", whichever) needs to be javascript-escaped,
\r
712 // the iwfXmlNode class also needs the > char escaped to be functional -- I need to fix that!
\r
713 // But for right now, this hack gets things working at least.
\r
715 // Like I said, I'm getting lazy :)
\r
718 if (this._htmlMode){
\r
719 // we're kicking out html, so xml escape only the quote character that matches the delimiter and the > sign.
\r
720 // We also need to javascript-escape the quote char that DOES NOT match the delimiter but is xml-escaped.
\r
721 if (quotes == "'"){
\r
722 // we're using apostrophes to delimit html.
\r
723 attval = attval.replace(/"/gi, '\\"').replace(/'/gi, ''').replace(/>/gi, '>');
\r
725 // we're using quotes to delimit html
\r
726 attval = attval.replace(/'/gi, "\\'").replace(/"/gi, '"').replace(/>/gi, '>');
\r
727 // attval = attval.replace(/&/gi, '&').replace(/"/gi, '"').replace(/</gi, '<').replace(/>/gi, '>');
\r
730 attval = iwfXmlEncode(attval);
\r
733 this.writeRaw(" " + name + "=" + quotes + attval + quotes);
\r
736 iwfWriter.prototype.writeAtt = function(name, val, quotes){
\r
737 this.writeAttribute(name, val, quotes);
\r
740 iwfWriter.prototype._writeRawText = function(txt){
\r
741 if (!txt || txt.length == 0){
\r
742 // no text to write. do nothing.
\r
745 if (this._nodeOpened){
\r
746 this.writeRaw(">");
\r
747 this._nodeOpened = false;
\r
750 this.writeRaw(txt);
\r
754 iwfWriter.prototype.writeText = function(txt){
\r
755 if (!txt || txt.length == 0){
\r
756 // no text to write. do nothing.
\r
759 if (this._nodeOpened){
\r
760 this.writeRaw(">");
\r
761 this._nodeOpened = false;
\r
762 this.writePretty(true);
\r
765 this.writeRaw(iwfXmlEncode(txt));
\r
769 iwfWriter.prototype.writeNodeClose = function(){
\r
770 if (this._nodeStack.length > 0){
\r
771 var name = this._nodeStack.pop();
\r
773 if (!this._nodeOpened && name != '#text'){
\r
774 this.writePretty(true);
\r
779 this.writeRaw("?>");
\r
785 this.writeRaw("]]>");
\r
788 this.writeRaw("-->");
\r
791 if (this._nodeOpened){
\r
792 // hack for <script /> needing to be <script></script>
\r
795 this.writeRaw("></" + name + ">");
\r
798 this.writeRaw("/>");;
\r
802 this.writeRaw("</" + name + ">");
\r
806 this._nodeOpened = false;
\r
810 iwfWriter.prototype.writeRaw = function(xml){
\r
811 this._buffer += xml;
\r
814 iwfWriter.prototype.toString = function(){
\r
815 return this._buffer;
\r
818 iwfWriter.prototype.clear = function(){
\r
819 this.buffer = new String();
\r
822 // -----------------------------------
\r
823 // End: Xml Writer Object
\r
824 // -----------------------------------
\r