changeset 106:c06ec02217ee

many changes
author Sonny Piers <sonny@fastmail.net>
date Tue, 26 Jun 2012 12:02:14 +0200
parents fb50311997b5
children 704ce44c1a22
files README console/console.css console/console.js console/console.xhtml jid.js lib/EventEmitter.js lib/bind.js lib/md5.js lib/vkbeautify.0.97.00.beta.js lightstring.js plugins/ANONYMOUS.js plugins/DIGEST-MD5.js plugins/PLAIN.js plugins/dataforms.js plugins/disco.js plugins/im.js plugins/ping.js plugins/presence.js plugins/pubsub.js plugins/roster.js plugins/vcard.js stanza.js transports/bosh.js transports/websocket.js
diffstat 24 files changed, 739 insertions(+), 104 deletions(-) [+]
line wrap: on
line diff
old mode 100644
new mode 100755
new file mode 100755
--- /dev/null
+++ b/console/console.css
@@ -0,0 +1,44 @@
+html, body {
+  width: 100%;
+  height: 100%;
+}
+body {
+  margin: 0;
+  /*FIXME: delete me*/
+  overflow: hidden;
+}
+.entry {
+  width: 100%;
+  margin-bottom: 15px;
+}
+.entry pre {
+  margin: 0;
+}
+#entries {
+  overflow: auto;
+  height: -webkit-calc(100% - (30px + 100px));
+  height: -moz-calc(100% - (30px + 100px));
+}
+#toolbar {
+  width: 100%;
+  height: 30px;
+}
+#input {
+  height: 100px;
+  width: 100%;
+}
+#input textarea {
+  /*border: none;*/
+  padding: 0;
+  resize: none;
+  width: -webkit-calc(100% - 80px);
+  width: -moz-calc(100% - 80px);
+}
+#input input {
+  width: 65px;
+}
+#input > * {
+  display: inline-block;
+  height: 100%;
+  vertical-align: top;
+}
\ No newline at end of file
new file mode 100755
--- /dev/null
+++ b/console/console.js
@@ -0,0 +1,100 @@
+'use strict';
+
+var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
+
+if(!Lightstring)
+  var Lightstring = {};
+
+Lightstring.console = {
+  invertedScroll: true,
+  log: function(aLog) {
+    var stanza = vkbeautify.xml(aLog.data, 2, ' ');
+    var entry = document.createElement('div');
+    entry.classList.add('entry');
+    entry.classList.add(aLog.dir);
+    var header = document.createElement('header');
+    //FIXME date? should come from the origin?
+    header.textContent = aLog.dir + ': ';
+    entry.appendChild(header)
+    var pre = document.createElement('pre');
+    pre.textContent = stanza;
+    entry.appendChild(pre);
+    var entriesEl = document.querySelector('#entries')
+    entriesEl.appendChild(entry);
+  },
+  filter: function(aFilter) {
+    var entries = document.querySelectorAll('.entry pre');
+    for (var i = 0, length = entries.length; i < length; i++) {
+      var entry = entries[i];
+      if (!entry.textContent.match(aFilter))
+        entry.parentNode.hidden = true;
+      else
+        entry.parentNode.hidden = false;
+
+    //FIXME use the Mutation Observer? get back to the previous scroll state?
+    this.scrollToBottom();
+    }
+  },
+  send: function(aData) {
+    Lightstring.console.source.postMessage({
+      'send': aData}, document.location.protocol + '//' + document.location.host);
+  },
+  scrollToBottom: function() {
+    this.entriesEl.scrollTop = (this.entriesEl.scrollHeight - this.entriesEl.clientHeight);
+  }
+};
+
+(function() {
+  document.addEventListener('DOMContentLoaded', function() {
+
+    var entriesEl = document.getElementById('entries');
+    entriesEl.addEventListener('scroll', function(e) {
+      if (entriesEl.scrollTop === (entriesEl.scrollHeight - entriesEl.clientHeight))
+        Lightstring.console.invertedScroll = true;
+      else
+        Lightstring.console.invertedScroll = false;
+    })
+
+    new MutationObserver(function(mutations) {
+      if(Lightstring.console.invertedScroll === true)
+        Lightstring.console.scrollToBottom();
+    }).observe(entriesEl, {
+      childList: true,
+      // attributes: false,
+      // characterData: false
+    });
+
+    Lightstring.console.entriesEl = entriesEl;
+    if (Lightstring.console.invertedScroll)
+      Lightstring.console.scrollToBottom();
+
+    window.addEventListener("message", function(e) {
+      if(!Lightstring.console.source)
+        Lightstring.console.source = e.source;
+
+      Lightstring.console.log(e.data);
+    }, false);
+
+    document.getElementById('input').addEventListener('submit', function(e) {
+      e.preventDefault();
+      Lightstring.console.send(this.elements['field'].value)
+    });
+    document.getElementById('clear').addEventListener('click', function(e) {
+      Lightstring.console.entriesEl.innerHTML = '';
+    });
+    //FIXME allow xpath, xquery, E4X, whatever XML query syntax
+    document.getElementById('filter').addEventListener('input', function(e) {
+      Lightstring.console.filter(this.value);
+    });
+    document.getElementById('input').addEventListener('keypress', function(e) {
+      if (e.keyCode === 13) {
+        if (e.shiftKey) {
+          e.preventDefault();
+          var event = document.createEvent('Event');
+          event.initEvent('submit', true, true);
+          this.dispatchEvent(event);
+        }
+      }
+    });
+  });
+})();
\ No newline at end of file
new file mode 100755
--- /dev/null
+++ b/console/console.xhtml
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+  <head>
+    <title>Lightstring Console</title>
+    <meta charset="UTF-8"/>
+    <script type="application/javascript" src="console.js"/>
+    <script type="application/javascript" src="../lib/vkbeautify.0.97.00.beta.js"/>
+    <link type="text/css" rel="stylesheet" href="console.css"/>
+  </head>
+  <body>
+    <div id="entries"/>
+    <div id="toolbar">
+      <button id="clear">Clear</button>
+      <input type="search" id="filter" placeholder="Filter"/>
+    </div>
+    <form id="input">
+      <textarea name="field" spellcheck="false"/>
+      <input type="submit" value="send"/>
+    </form>
+  </body>
+</html>
\ No newline at end of file
old mode 100644
new mode 100755
--- a/jid.js
+++ b/jid.js
@@ -28,7 +28,7 @@ Lightstring.JID = function(aJID) {
   this.resource = null;
 
   if (aJID)
-    this.full = aJID;
+    this.full = aJID.toString();
 
   //TODO: use a stringprep library to validate the input.
 };
@@ -91,6 +91,7 @@ Lightstring.JID.prototype = {
   },
 
   set full(aJID) {
+
     if (!aJID)
       return;
 
old mode 100644
new mode 100755
old mode 100644
new mode 100755
old mode 100644
new mode 100755
new file mode 100755
--- /dev/null
+++ b/lib/vkbeautify.0.97.00.beta.js
@@ -0,0 +1,376 @@
+/**
+* vkBeautify - javascript plugin to pretty-print or minify text in XML, JSON and CSS formats.
+*  
+* Version - 0.97.00.beta 
+* Copyright (c) 2012 Vadim Kiryukhin
+* vkiryukhin @ gmail.com
+* http://www.eslinstructor.net/vkbeautify/
+* 
+* Dual licensed under the MIT and GPL licenses:
+*   http://www.opensource.org/licenses/mit-license.php
+*   http://www.gnu.org/licenses/gpl.html
+*
+*   Pretty print
+*
+*        vkbeautify.xml(text [,indent_pattern]);
+*        vkbeautify.json(text [,indent_pattern]);
+*        vkbeautify.css(text [,indent_pattern]);
+*        vkbeautify.sql(text [,indent_pattern]);
+*
+*        @text - String; text to beatufy;
+*        @indent_pattern - Integer | String;
+*                Integer:  number of white spaces;
+*                String:   character string to visualize indentation ( can also be a set of white spaces )
+*   Minify
+*
+*        vkbeautify.xmlmin(text [,preserve_comments]);
+*        vkbeautify.jsonmin(text);
+*        vkbeautify.cssmin(text [,preserve_comments]);
+*        vkbeautify.sqlmin(text);
+*
+*        @text - String; text to minify;
+*        @preserve_comments - Bool; [optional];
+*                Set this flag to true to prevent removing comments from @text ( minxml and mincss functions only. )
+*
+*   Examples:
+*        vkbeautify.xml(text); // pretty print XML
+*        vkbeautify.json(text, 4 ); // pretty print JSON
+*        vkbeautify.css(text, '. . . .'); // pretty print CSS
+*        vkbeautify.sql(text, '----'); // pretty print SQL
+*
+*        vkbeautify.xmlmin(text, true);// minify XML, preserve comments
+*        vkbeautify.jsonmin(text);// minify JSON
+*        vkbeautify.cssmin(text);// minify CSS, remove comments ( default )
+*        vkbeautify.sqlmin(text);// minify SQL
+*
+*/
+
+(function() {
+
+function createShiftArr(step) {
+
+	var space = '    ';
+	
+	if ( isNaN(parseInt(step)) ) {  // argument is string
+		space = step;
+	} else { // argument is integer
+		switch(step) {
+			case 1: space = ' '; break;
+			case 2: space = '  '; break;
+			case 3: space = '   '; break;
+			case 4: space = '    '; break;
+			case 5: space = '     '; break;
+			case 6: space = '      '; break;
+			case 7: space = '       '; break;
+			case 8: space = '        '; break;
+			case 9: space = '         '; break;
+			case 10: space = '          '; break;
+			case 11: space = '           '; break;
+			case 12: space = '            '; break;
+		}
+	}
+
+	var shift = ['\n']; // array of shifts
+	for(ix=0;ix<100;ix++){
+		shift.push(shift[ix]+space); 
+	}
+	return shift;
+}
+
+function vkbeautify(){
+	this.step = '    '; // 4 spaces
+	this.shift = createShiftArr(this.step);
+};
+
+vkbeautify.prototype.xml = function(text,step) {
+
+	var ar = text.replace(/>\s{0,}</g,"><").replace(/</g,"~::~<").split('~::~'),
+		len = ar.length,
+		inComment = false,
+		deep = 0,
+		str = '',
+		ix = 0,
+		shift = step ? createShiftArr(step) : this.shift;
+
+		for(ix=0;ix<len;ix++) {
+			// start comment or <![CDATA[...]]> or <!DOCTYPE //
+			if(ar[ix].search(/<!/) > -1) { 
+				str += shift[deep]+ar[ix];
+				inComment = true; 
+				// end comment  or <![CDATA[...]]> //
+				if(ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1 || ar[ix].search(/!DOCTYPE/) > -1 ) { 
+					inComment = false; 
+				}
+			} else 
+			// end comment  or <![CDATA[...]]> //
+			if(ar[ix].search(/-->/) > -1 || ar[ix].search(/\]>/) > -1) { 
+				str += ar[ix];
+				inComment = false; 
+			} else 
+			// <elm></elm> //
+			if( /^<\w/.exec(ar[ix-1]) && /^<\/\w/.exec(ar[ix]) &&
+				/^<[\w:\-\.\,]+/.exec(ar[ix-1]) == /^<\/[\w:\-\.\,]+/.exec(ar[ix])[0].replace('/','')) { 
+				str += ar[ix];
+				if(!inComment) deep--;
+			} else
+			 // <elm> //
+			if(ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) == -1 && ar[ix].search(/\/>/) == -1 ) {
+				str = !inComment ? str += shift[deep++]+ar[ix] : str += ar[ix];
+			} else 
+			 // <elm>...</elm> //
+			if(ar[ix].search(/<\w/) > -1 && ar[ix].search(/<\//) > -1) {
+				str = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix];
+			} else 
+			// </elm> //
+			if(ar[ix].search(/<\//) > -1) { 
+				str = !inComment ? str += shift[--deep]+ar[ix] : str += ar[ix];
+			} else 
+			// <elm/> //
+			if(ar[ix].search(/\/>/) > -1 ) { 
+				str = !inComment ? str += shift[deep]+ar[ix] : str += ar[ix];
+			} else 
+			// <? xml ... ?> //
+			if(ar[ix].search(/<\?/) > -1) { 
+				str += shift[deep]+ar[ix];
+			} else {
+				str += ar[ix];
+			}
+		}
+		
+	return  (str[0] == '\n') ? str.slice(1) : str;
+}
+
+vkbeautify.prototype.json = function(text,step) {
+
+	var ar = this.jsonmin(text).replace(/\{/g,"~::~{~::~")
+								.replace(/\[/g,"[~::~")
+								.replace(/\}/g,"~::~}")
+								.replace(/\]/g,"~::~]")
+								.replace(/\"\,/g,'",~::~')
+								.replace(/\,\"/g,',~::~"')
+								.replace(/\]\,/g,'],~::~')
+								.replace(/~::~\s{0,}~::~/g,"~::~")
+								.split('~::~'),
+				
+		len = ar.length,
+		deep = 0,
+		str = '',
+		ix = 0,
+		shift = step ? createShiftArr(step) : this.shift;;
+
+	for(ix=0;ix<len;ix++) {
+		if( /\{/.exec(ar[ix]))  { 
+			str += shift[deep++]+ar[ix];
+		} else 
+		if( /\[/.exec(ar[ix]))  { 
+			str += shift[deep++]+ar[ix];
+		}  else 
+		if( /\]/.exec(ar[ix]))  { 
+			str += shift[--deep]+ar[ix];
+		}  else 
+		if( /\}/.exec(ar[ix]))  { 
+			str += shift[--deep]+ar[ix];
+		} else {
+			str += shift[deep]+ar[ix];
+		}
+	}
+	return str.replace(/^\n{1,}/,'');
+}
+
+vkbeautify.prototype.css = function(text, step) {
+
+	var ar = text.replace(/\s{1,}/g,' ')
+				.replace(/\{/g,"{~::~")
+				.replace(/\}/g,"~::~}~::~")
+				.replace(/\;/g,";~::~")
+				.replace(/\/\*/g,"~::~/*")
+				.replace(/\*\//g,"*/~::~")
+				.replace(/~::~\s{0,}~::~/g,"~::~")
+				.split('~::~'),
+		len = ar.length,
+		deep = 0,
+		str = '',
+		ix = 0,
+		shift = step ? createShiftArr(step) : this.shift;
+		
+		for(ix=0;ix<len;ix++) {
+
+			if( /\{/.exec(ar[ix]))  { 
+				str += shift[deep++]+ar[ix];
+			} else 
+			if( /\}/.exec(ar[ix]))  { 
+				str += shift[--deep]+ar[ix];
+			} else
+			if( /\*\\/.exec(ar[ix]))  { 
+				str += shift[deep]+ar[ix];
+			}
+			else {
+				str += shift[deep]+ar[ix];
+			}
+		}
+		return str.replace(/^\n{1,}/,'');
+}
+
+//----------------------------------------------------------------------------
+
+function isSubquery(str, parenthesisLevel) {
+	return  parenthesisLevel - (str.replace(/\(/g,'').length - str.replace(/\)/g,'').length )
+}
+
+function split_sql(str, tab) {
+
+	return str.replace(/\s{1,}/g," ")
+
+				.replace(/ AND /ig,"~::~"+tab+tab+"AND ")
+				.replace(/ BETWEEN /ig,"~::~"+tab+"BETWEEN ")
+				.replace(/ CASE /ig,"~::~"+tab+"CASE ")
+				.replace(/ ELSE /ig,"~::~"+tab+"ELSE ")
+				.replace(/ END /ig,"~::~"+tab+"END ")
+				.replace(/ FROM /ig,"~::~FROM ")
+				.replace(/ GROUP\s{1,}BY/ig,"~::~GROUP BY ")
+				.replace(/ HAVING /ig,"~::~HAVING ")
+				//.replace(/ IN /ig,"~::~"+tab+"IN ")
+				.replace(/ IN /ig," IN ")
+				
+				.replace(/ JOIN /ig,"~::~JOIN ")
+				.replace(/ CROSS~::~{1,}JOIN /ig,"~::~CROSS JOIN ")
+				.replace(/ INNER~::~{1,}JOIN /ig,"~::~INNER JOIN ")
+				.replace(/ LEFT~::~{1,}JOIN /ig,"~::~LEFT JOIN ")
+				.replace(/ RIGHT~::~{1,}JOIN /ig,"~::~RIGHT JOIN ")
+				
+				.replace(/ ON /ig,"~::~"+tab+"ON ")
+				.replace(/ OR /ig,"~::~"+tab+tab+"OR ")
+				.replace(/ ORDER\s{1,}BY/ig,"~::~ORDER BY ")
+				.replace(/ OVER /ig,"~::~"+tab+"OVER ")
+
+				.replace(/\(\s{0,}SELECT /ig,"~::~(SELECT ")
+				.replace(/\)\s{0,}SELECT /ig,")~::~SELECT ")
+				
+				.replace(/ THEN /ig," THEN~::~"+tab+"")
+				.replace(/ UNION /ig,"~::~UNION~::~")
+				.replace(/ USING /ig,"~::~USING ")
+				.replace(/ WHEN /ig,"~::~"+tab+"WHEN ")
+				.replace(/ WHERE /ig,"~::~WHERE ")
+				.replace(/ WITH /ig,"~::~WITH ")
+				
+				//.replace(/\,\s{0,}\(/ig,",~::~( ")
+				//.replace(/\,/ig,",~::~"+tab+tab+"")
+
+				.replace(/ ALL /ig," ALL ")
+				.replace(/ AS /ig," AS ")
+				.replace(/ ASC /ig," ASC ")	
+				.replace(/ DESC /ig," DESC ")	
+				.replace(/ DISTINCT /ig," DISTINCT ")
+				.replace(/ EXISTS /ig," EXISTS ")
+				.replace(/ NOT /ig," NOT ")
+				.replace(/ NULL /ig," NULL ")
+				.replace(/ LIKE /ig," LIKE ")
+				.replace(/\s{0,}SELECT /ig,"SELECT ")
+							
+				.replace(/~::~{1,}/g,"~::~")
+				.split('~::~');
+}
+
+vkbeautify.prototype.sql = function(text,step) {
+
+	var ar_by_quote = text.replace(/\s{1,}/g," ")
+							.replace(/\'/ig,"~::~\'")
+							.split('~::~'),
+		len = ar_by_quote.length,
+		ar = [],
+		deep = 0,
+		tab = this.step,//+this.step,
+		inComment = true,
+		inQuote = false,
+		parenthesisLevel = 0,
+		str = '',
+		ix = 0,
+		shift = step ? createShiftArr(step) : this.shift;;
+
+		for(ix=0;ix<len;ix++) {
+			if(ix%2) {
+				ar = ar.concat(ar_by_quote[ix]);
+			} else {
+				ar = ar.concat(split_sql(ar_by_quote[ix], tab) );
+			}
+		}
+		
+		len = ar.length;
+		for(ix=0;ix<len;ix++) {
+			
+			parenthesisLevel = isSubquery(ar[ix], parenthesisLevel);
+			
+			if( /\s{0,}\s{0,}SELECT\s{0,}/.exec(ar[ix]))  { 
+				ar[ix] = ar[ix].replace(/\,/g,",\n"+tab+tab+"")
+			} 
+		
+			if( /\s{0,}\(\s{0,}SELECT\s{0,}/.exec(ar[ix]))  { 
+				deep++;
+				str += shift[deep]+ar[ix];
+			} else 
+			if( /\'/.exec(ar[ix]) )  { 
+				if(parenthesisLevel<1 && deep) {
+					deep--;
+				}
+				str += ar[ix];
+			}
+			else  { 
+				str += shift[deep]+ar[ix];
+				if(parenthesisLevel<1 && deep) {
+					deep--;
+				}
+			} 
+			var junk = 0;
+		}
+
+		str = str.replace(/^\n{1,}/,'').replace(/\n{1,}/g,"\n");
+		return str;
+}
+
+
+vkbeautify.prototype.xmlmin = function(text, preserveComments) {
+
+	var str = preserveComments ? text
+							   : text.replace(/\<![ \r\n\t]*(--([^\-]|[\r\n]|-[^\-])*--[ \r\n\t]*)\>/g,"");
+	return  str.replace(/>\s{0,}</g,"><"); 
+}
+
+vkbeautify.prototype.jsonmin = function(text) {
+								  
+	return  text.replace(/\s{0,}\{\s{0,}/g,"{")
+				.replace(/\s{0,}\[$/g,"[")
+				.replace(/\[\s{0,}/g,"[")
+				.replace(/:\s{0,}\[/g,':[')
+		  		.replace(/\s{0,}\}\s{0,}/g,"}")
+				.replace(/\s{0,}\]\s{0,}/g,"]")
+				.replace(/\"\s{0,}\,/g,'",')
+				.replace(/\,\s{0,}\"/g,',"')
+				.replace(/\"\s{0,}:/g,'":')
+				.replace(/:\s{0,}\"/g,':"')
+				.replace(/:\s{0,}\[/g,':[')
+				.replace(/\,\s{0,}\[/g,',[')
+				.replace(/\,\s{2,}/g,', ')
+				.replace(/\]\s{0,},\s{0,}\[/g,'],[');							  
+}
+
+vkbeautify.prototype.cssmin = function(text, preserveComments) {
+	
+	var str = preserveComments ? text
+							   : text.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g,"") ;
+
+	return str.replace(/\s{1,}/g,' ')
+			  .replace(/\{\s{1,}/g,"{")
+			  .replace(/\}\s{1,}/g,"}")
+			  .replace(/\;\s{1,}/g,";")
+			  .replace(/\/\*\s{1,}/g,"/*")
+			  .replace(/\*\/\s{1,}/g,"*/");
+}
+
+vkbeautify.prototype.sqlmin = function(text) {
+	return text.replace(/\s{1,}/g," ").replace(/\s{1,}\(/,"(").replace(/\s{1,}\)/,")");
+}
+
+window.vkbeautify = new vkbeautify();
+
+})();
+
old mode 100644
new mode 100755
--- a/lightstring.js
+++ b/lightstring.js
@@ -72,16 +72,17 @@ var Lightstring = {
    * @param {String} aString XML string.
    * @return {Object} Domified XML.
    */
-  XML2DOM: function(aString) {
-    var DOM = null;
+  parse: function(aString) {
+    var el = null;
+    //FIXME webkit doesn't throw an error when the parsing fails
     try {
-      DOM = this.parser.parseFromString(aString, 'text/xml').documentElement;
+      el = this.parser.parseFromString(aString, 'text/xml').documentElement;
     }
     catch (e) {
       //TODO: error
     }
     finally {
-      return DOM;
+      return el;
     };
   },
   /**
@@ -89,16 +90,16 @@ var Lightstring = {
    * @param {Object} aString DOM object.
    * @return {String} Stringified DOM.
    */
-  DOM2XML: function(aElement) {
-    var XML = null;
+  serialize: function(aElement) {
+    var string = null;
     try {
-      XML = this.serializer.serializeToString(aElement);
+      string = this.serializer.serializeToString(aElement);
     }
     catch (e) {
       //TODO: error
     }
     finally {
-      return XML;
+      return string;
     };
   },
   /**
@@ -122,6 +123,10 @@ var Lightstring = {
  * @memberOf Lightstring
  */
 Lightstring.Connection = function(aService) {
+  var that = this;
+  window.addEventListener('message', function(e) {
+    that.send(e.data.send)
+  });
   if (aService)
     this.service = aService;
   /**
@@ -158,9 +163,9 @@ Lightstring.Connection.prototype.connect
   var protocol = getProtocol(this.service);
 
   if (protocol.match('http'))
-    this.connection = new Lightstring.BOSHConnection(this.service);
+    this.connection = new Lightstring.BOSHConnection(this.service, this.jid);
   else if (protocol.match('ws'))
-    this.connection = new Lightstring.WebSocketConnection(this.service);
+    this.connection = new Lightstring.WebSocketConnection(this.service, this.jid);
 
   this.connection.open();
 
@@ -173,54 +178,56 @@ Lightstring.Connection.prototype.connect
     that.emit('out', stanza);
   });
   this.connection.on('in', function(stanza) {
-    var stanza = new Lightstring.Stanza(stanza);
-
     //FIXME: node-xmpp-bosh sends a self-closing stream:stream tag; it is wrong!
     that.emit('stanza', stanza);
 
-    if (!stanza.DOM)
+    if (!stanza.el)
       return;
 
-    var name = stanza.DOM.localName;
+    var el = stanza.el;
 
     //Authentication
     //FIXME SASL mechanisms and XMPP features can be both in a stream:features
-    if (name === 'features') {
-      //SASL mechanisms
-      if (stanza.DOM.firstChild.localName === 'mechanisms') {
-        stanza.mechanisms = [];
-        var nodes = stanza.DOM.getElementsByTagName('mechanism');
-        for (var i = 0; i < nodes.length; i++)
-          stanza.mechanisms.push(nodes[i].textContent);
-        that.emit('mechanisms', stanza);
+    if (el.localName === 'features') {
+      var children = el.childNodes;
+      for (var i = 0, length = children.length; i < length; i++) {
+        //SASL mechanisms
+        if(children[i].localName === 'mechanisms') {
+          stanza.mechanisms = [];
+          var nodes = el.getElementsByTagName('mechanism');
+          for (var i = 0; i < nodes.length; i++)
+            stanza.mechanisms.push(nodes[i].textContent);
+          that.emit('mechanisms', stanza);
+          return;
+        }
       }
       //XMPP features
-      else {
+      // else {
         //TODO: stanza.features
         that.emit('features', stanza);
-      }
+      // }
     }
-    else if (name === 'challenge') {
+    else if (el.localName === 'challenge') {
       that.emit('challenge', stanza);
     }
-    else if (name === 'failure') {
+    else if (el.localName === 'failure') {
       that.emit('failure', stanza);
     }
-    else if (name === 'success') {
+    else if (el.localName === 'success') {
       that.emit('success', stanza);
     }
 
     //Iq callbacks
-    else if (name === 'iq') {
-      var payload = stanza.DOM.firstChild;
+    else if (el.localName === 'iq') {
+      var payload = el.firstChild;
       if (payload)
         that.emit('iq/' + payload.namespaceURI + ':' + payload.localName, stanza);
 
-      var id = stanza.DOM.getAttribute('id');
+      var id = el.getAttribute('id');
       if (!(id && id in that.callbacks))
         return;
 
-      var type = stanza.DOM.getAttribute('type');
+      var type = el.getAttribute('type');
       if (type !== 'result' && type !== 'error')
         return; //TODO: warning
 
@@ -233,7 +240,7 @@ Lightstring.Connection.prototype.connect
       delete that.callbacks[id];
     }
 
-    else if (name === 'presence' || name === 'message') {
+    else if (el.localName === 'presence' || el.localName === 'message') {
       that.emit(name, stanza);
     }
   });
@@ -252,17 +259,17 @@ Lightstring.Connection.prototype.send = 
   if (!stanza)
     return;
 
-  if (stanza.DOM.tagName === 'iq') {
-    var type = stanza.DOM.getAttribute('type');
+  if (stanza.el.tagName === 'iq') {
+    var type = stanza.el.getAttribute('type');
     if (type !== 'get' || type !== 'set')
       ; //TODO: error
 
     var callback = {success: aSuccess, error: aError};
 
-    var id = stanza.DOM.getAttribute('id');
+    var id = stanza.el.getAttribute('id');
     if (!id) {
       var id = Lightstring.newId('sendiq:');
-      stanza.DOM.setAttribute('id', id);
+      stanza.el.setAttribute('id', id);
     }
 
     this.callbacks[id] = callback;
@@ -271,12 +278,7 @@ Lightstring.Connection.prototype.send = 
   else if (aSuccess || aError)
     ; //TODO: warning (no callback without iq)
 
-
-  //FIXME this.socket.send(stanza.XML); (need some work on Lightstring.Stanza)
-  var fixme = Lightstring.DOM2XML(stanza.DOM);
-  stanza.XML = fixme;
-  this.connection.send(fixme);
-  this.emit('output', stanza);
+  this.connection.send(stanza.toString());
 };
 /**
  * @function Closes the XMPP stream and the socket.
@@ -285,7 +287,7 @@ Lightstring.Connection.prototype.disconn
   this.emit('disconnecting');
   var stream = Lightstring.stanzas.stream.close();
   this.socket.send(stream);
-  this.emit('XMLOutput', stream);
+  this.emit('out', stream);
   this.socket.close();
 };
 Lightstring.Connection.prototype.load = function() {
old mode 100644
new mode 100755
old mode 100644
new mode 100755
old mode 100644
new mode 100755
--- a/plugins/PLAIN.js
+++ b/plugins/PLAIN.js
@@ -66,7 +66,7 @@ Lightstring.plugins['PLAIN'] = {
         //Success
         function(stanza) {
           //Session http://xmpp.org/rfcs/rfc3921.html#session
-          Conn.jid = new Lightstring.JID(stanza.DOM.textContent);
+          Conn.jid = new Lightstring.JID(stanza.el.textContent);
           Conn.send(
             "<iq type='set' id='"+Lightstring.newId('sendiq:')+"'>" +
               "<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/>" +
old mode 100644
new mode 100755
old mode 100644
new mode 100755
old mode 100644
new mode 100755
--- a/plugins/im.js
+++ b/plugins/im.js
@@ -19,13 +19,47 @@
 //////
 //IM//
 //////
-Lightstring.plugins['message'] = {
+Lightstring.plugins['im'] = {
   stanzas: {
-    normalMessage: function(aTo, aSubject, aText) {
-      return "<message type='normal' to='" + aTo + "'><subject>" + aSubject + "</subject><body>" + aText + "</body></message>";
+    normal: function(aTo, aSubject, aText) {
+      return(
+        "<message type='normal' to='" + aTo + "'>" +
+          "<subject>" + aSubject + "</subject>" +
+          "<body>" + aText + "</body>" +
+        "</message>"
+      );
     },
-    chatMessage: function(aTo, aText) {
-      return "<message type='chat' to='" + aTo + "'><body>" + aText + "</body></message>";
-    }
+    chat: function(aTo, aText, aReceipt) {
+      var message = Lightstring.parse(
+        "<message type='chat' to='" + aTo + "'>" +
+          "<body>" + aText + "</body>" +
+        "</message>"
+      );
+
+      if (aReceipt) {
+        var receipt = document.createElement('request');
+        receipt.setAttribute('xmlns', 'urn:xmpp:receipts');
+        message.appendChild(receipt);
+        message.setAttribute('id', Lightstring.newId());
+      }
+
+      return message;
+    },
+    received: function(aTo, aId) {
+      var message = Lightstring.parse(
+        "<message to='" + aTo + "'>" +
+          "<received xmlns='urn:xmpp:receipts' id='" + aId + "'/>" +
+        "</message>"
+      );
+      return message;
+    },
+    read: function(aTo, aId) {
+      var message = Lightstring.parse(
+        "<message to='" + aTo + "'>" +
+          "<read xmlns='urn:xmpp:receipts' id='" + aId + "'/>" +
+        "</message>"
+      );
+      return message;
+    },
   }
 };
new file mode 100644
--- /dev/null
+++ b/plugins/ping.js
@@ -0,0 +1,51 @@
+'use strict';
+
+/**
+  Copyright (c) 2012, Sonny Piers <sonny at fastmail dot net>
+
+  Permission to use, copy, modify, and/or distribute this software for any
+  purpose with or without fee is hereby granted, provided that the above
+  copyright notice and this permission notice appear in all copies.
+
+  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+/*
+References:
+  http://xmpp.org/extensions/xep-0199.html
+*/
+
+Lightstring.plugins['ping'] = {
+  namespaces: {
+    ping: 'urn:xmpp:ping'
+  },
+  stanzas: {
+    ping: function (aTo){
+      return(
+        "<iq to='" + aTo + "' type='get'>" +
+          "<ping xmlns='" + Lightstring.ns.ping + "'/>" +
+        "</iq>"
+      );
+    },
+    pong: function (aTo){
+      return "<iq to='" + aTo + "' type='result'/>"
+    },
+  },
+  handlers: {
+    'iq': function (stanza) {
+      if (stanza.el.firstChild.localName !== 'ping')
+        return;
+
+      var id = stanza.el.getAttribute('id');
+      var from = stanza.el.getAttribute('from');
+      var stanza = Lightstring.stanzas.pong(from);
+      stanza.setAttribute('id', id);
+    }
+  }
+}
old mode 100644
new mode 100755
--- a/plugins/presence.js
+++ b/plugins/presence.js
@@ -21,7 +21,15 @@
 //Presence// http://xmpp.org/rfcs/rfc6121.html#presence
 ////////////
 (function() {
-  var legal_types = ['error', 'probe', 'subscribe', 'subscribed', 'unavailable', 'unsubscribe', 'unsubscribed'];
+  var legal_types = [
+    'error',
+    'probe',
+    'subscribe',
+    'subscribed',
+    'unavailable',
+    'unsubscribe',
+    'unsubscribed'
+  ];
 
   Lightstring.plugins['presence'] = {
     stanzas: {
old mode 100644
new mode 100755
--- a/plugins/pubsub.js
+++ b/plugins/pubsub.js
@@ -44,7 +44,13 @@
         return stanza + "</items></pubsub></iq>";
       },
       affiliations: function(aTo, aNode) {
-        return "<iq type='get' to='" + aTo + "'><pubsub xmlns='" + Lightstring.ns.pubsub_owner + "'><affiliations node='" + aNode + "'/></pubsub></iq>";
+        return(
+          "<iq type='get' to='" + aTo + "'>" +
+            "<pubsub xmlns='" + Lightstring.ns.pubsub_owner + "'>" +
+              "<affiliations node='" + aNode + "'/>" +
+            "</pubsub>" +
+          "</iq>"
+        );
       },
       publish: function(aTo, aNode, aItem, aId) {
         return  "<iq type='set' to='" + aTo + "'><pubsub xmlns='" + Lightstring.ns.pubsub + "'><publish node='" + aNode + "'><item id='" + aId + "'>" + aItem + "</item></publish></pubsub></iq>";
old mode 100644
new mode 100755
--- a/plugins/roster.js
+++ b/plugins/roster.js
@@ -51,11 +51,11 @@ Lightstring.plugins['roster'] = {
     }
   },
   methods: {
-    'get': function(aSuccess, aError) {
+    'get': function(aOnSuccess, aOnError) {
       this.send(Lightstring.stanzas.roster.get(), function(stanza) {
         var contacts = [];
 
-        var items = stanza.DOM.getElementsByTagName('item');
+        var items = stanza.el.getElementsByTagName('item');
 
         for (var i = 0; i < items.length; i++) {
           var item = items[i];
@@ -87,9 +87,9 @@ Lightstring.plugins['roster'] = {
           contacts: contacts
         };
 
-        if (aSuccess)
-          aSuccess(stanza);
-      }, aError);
+        if (aOnSuccess)
+          aOnSuccess(stanza);
+      }, aOnError);
     }
   }
 };
old mode 100644
new mode 100755
old mode 100644
new mode 100755
--- a/stanza.js
+++ b/stanza.js
@@ -23,16 +23,13 @@
  * @memberOf Lightstring
  */
 Lightstring.Stanza = function(aStanza) {
-  if (typeof aStanza === 'string') {
-    this.XML = aStanza;
-    this.DOM = Lightstring.XML2DOM(this.XML);
-  }
-  else if (aStanza instanceof Element) {
-    this.DOM = aStanza;
-    this.XML = Lightstring.DOM2XML(this.DOM);
-  }
-  //ToDo error ?
-  else {
-    return null;
-  }
+  if (typeof aStanza === 'string')
+    this.el = Lightstring.parse(aStanza);
+  else if (aStanza instanceof Element)
+    this.el = aStanza;
+  else
+    this.el = null;//TODO error
 };
+Lightstring.Stanza.prototype.toString = function() {
+  return Lightstring.serialize(this.el);
+};
\ No newline at end of file
old mode 100644
new mode 100755
--- a/transports/bosh.js
+++ b/transports/bosh.js
@@ -54,20 +54,19 @@
     //   }
     // }
 
-    var body = '<body rid="' + this.rid++ + '"  xmlns="http://jabber.org/protocol/httpbind"/>';
-    var body = Lightstring.XML2DOM(body);
+    var body = new Lightstring.Stanza('<body rid="' + this.rid++ + '"  xmlns="http://jabber.org/protocol/httpbind"/>');
 
     //sid
     if (this.sid)
-      body.setAttribute('sid', this.sid);
+      body.el.setAttribute('sid', this.sid);
 
     //attributes on body
     for (var i in attrs)
-      body.setAttribute(i, attrs[i]);
+      body.el.setAttribute(i, attrs[i]);
 
     //children
     for (var i in children)
-      body.appendChild(children[i]);
+      body.el.appendChild(children[i]);
 
 
 
@@ -99,7 +98,7 @@
 
       var body = this.response;
       that.emit('rawin', body);
-      var bodyEl = Lightstring.XML2DOM(body);
+      var bodyEl = Lightstring.parse(body);
       that.processResponse(bodyEl)
       if (aOnSuccess)
         aOnSuccess(bodyEl);
@@ -117,14 +116,15 @@
     //   }
     // });
     // this.emit('rawout', body.toString());
+    if (body.children) {
+      for(var i = 0; i < body.children.length; i++) {
+        var child = body.children[i];
+        that.emit('out', child);
+      }
+    }
+    this.emit('rawout', body);
 
-    for(var i = 0; i < body.children.length; i++) {
-      var child = body.children[i];
-      that.emit('out', child);
-    }
-    this.emit('rawout', Lightstring.DOM2XML(body))
-
-    req.send(Lightstring.DOM2XML(body));
+    req.send(Lightstring.serialize(body));
     this.currentRequests++;
   };
   Lightstring.BOSHConnection.prototype.send = function(aData) {
@@ -134,7 +134,7 @@
 
     else if(typeof aData == 'string') {
       try {
-        var el = Lightstring.XML2DOM(aData);
+        var el = Lightstring.parse(aData);
       }
       catch(e) {
         console.log(e);
old mode 100644
new mode 100755
--- a/transports/websocket.js
+++ b/transports/websocket.js
@@ -1,24 +1,18 @@
 'use strict';
 
 (function() {
-  Lightstring.WebSocketConnection = function(aService) {
+  Lightstring.WebSocket = WebSocket || MozWebSocket || undefined;
+  Lightstring.WebSocketConnection = function(aService, aJid) {
     this.service = aService;
+    this.jid = aJid;
   };
   Lightstring.WebSocketConnection.prototype = new EventEmitter();
   Lightstring.WebSocketConnection.prototype.open = function() {
-    // Standard
-    if (typeof(WebSocket) === 'function')
-      this.socket = new WebSocket(this.service, 'xmpp');
-    // Safari
-    else if (typeof(WebSocket) === 'object')
-      this.socket = new WebSocket(this.service, 'xmpp');
-    // Old Gecko
-    else if (typeof(MozWebSocket) === 'function')
-      this.socket = new MozWebSocket(this.service, 'xmpp');
-    // No WebSocket support
-    else
+    if(!Lightstring.WebSocket)
       return; //TODO: error
 
+    this.socket = new WebSocket(this.service, 'xmpp');
+
     var that = this;
     this.socket.addEventListener('open', function() {
       //FIXME: Opera/Safari WebSocket implementation doesn't support sub-protocol mechanism.
@@ -27,11 +21,11 @@
       that.emit('open');
 
       var stream = Lightstring.stanzas.stream.open(that.jid.domain);
-      this.socket.send(stream);
-      var stanza = {
-        XML: stream
-      };
-      that.emit('out', stanza);
+      var stanza = new Lightstring.Stanza();
+      stanza.toString = function() {
+        return stream;
+      }
+      that.send(stanza);
     });
     this.socket.addEventListener('error', function(e) {
       that.emit('disconnecting', e.data);
@@ -41,11 +35,12 @@
       that.emit('disconnected', e.data);
     });
     this.socket.addEventListener('message', function(e) {
-      that.emit('in', e.data);
+      var stanza = new Lightstring.Stanza(e.data);
+      that.emit('in', stanza);
     });
   };
   Lightstring.WebSocketConnection.prototype.send = function(aStanza) {
-    this.socket.send(aStanza);
-    that.emit('out', aStanza);
+    this.emit('out', aStanza);
+    this.socket.send(aStanza.toString());
   };
 })();
\ No newline at end of file