changeset 5:03ef53b969bd

Add XMPP support.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 31 Jan 2012 01:09:41 +0100
parents 95c6fa5f4715
children 24aa8dccb170
files config.js index.xhtml record.js script2.js sxe-document.js
diffstat 5 files changed, 620 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/config.js
@@ -0,0 +1,5 @@
+'use strict';
+
+const SERVICE = 'ws://plugsbee.com:5280/';
+const JID = 'test@linkmauve.fr';
+const PASSWORD = 'test';
--- a/index.xhtml
+++ b/index.xhtml
@@ -1,10 +1,20 @@
 <!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
 <head>
   <meta charset="UTF-8" />
   <title> barbecue </title>
   <link type="text/css" href="style.css" rel="stylesheet" />
   <script type="text/javascript" src="script.js"></script>
+  <script type="text/javascript" src="lightstring/md5.js"></script>
+  <script type="text/javascript" src="lightstring/lightstring.js"></script>
+  <script type="text/javascript" src="lightstring/stanza.js"></script>
+  <script type="text/javascript" src="lightstring/jid.js"></script>
+  <script type="text/javascript" src="lightstring/plugins/presence.js"></script>
+  <script type="text/javascript" src="lightstring/plugins/disco.js"></script>
+  <script type="text/javascript" src="lightstring/plugins/feature-not-implemented.js"></script>
+  <script type="text/javascript" src="record.js"></script>
+  <script type="text/javascript" src="sxe-document.js"></script>
+  <script type="text/javascript" src="config.js"></script>
 </head>
 <body>
   <form id="toolbar">
@@ -92,7 +102,7 @@
     <ol><li> and now a list </li><li> with only      </li><li> three items    </li></ol>
   </section>
 
-  <footer contenteditable="true">
+  <footer>
     <p>
       This HTML content editor is not intended to compete with <a
       href="http://ckeditor.com/">CKEditor</a> or <a
@@ -112,5 +122,6 @@
       <a href="http://www.browserscope.org/richtext2/test">Browserscope.org</a>.
     </p>
   </footer>
+  <script type="text/javascript" src="script2.js"></script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/record.js
@@ -0,0 +1,91 @@
+'use strict';
+
+var Record = function(jid, child) {
+  this.creator = jid;
+  this.creationDate = new Date; //FIXME: non-standard?
+  this.update(jid, child);
+}
+
+Record.prototype = {
+  update: function(jid, child) {
+    this.rid = child.getAttributeNS(null, 'rid') || this.rid;
+    this.type = child.getAttributeNS(null, 'type') || this.type;
+    this.version = child.getAttributeNS(null, 'version') || this.version || 0;
+    this.parent = child.getAttributeNS(null, 'parent') || this.parent;
+    this.primaryWeight = child.getAttributeNS(null, 'primary-weight') || this.primaryWeight || 0;
+
+    this.lastModifiedDate = new Date; //FIXME: non-standard?
+    this.lastModifiedBy = jid; // only in 10.5.1
+
+    if (this.type === 'element' || this.type === 'attr') {
+      this.ns = child.getAttributeNS(null, 'ns') || this.ns;
+      this.name = child.getAttributeNS(null, 'name') || this.name;
+    }
+
+    if (this.type === 'text' || this.type === 'attr' || this.type === 'comment')
+      this.chdata = child.getAttributeNS(null, 'chdata') || this.chdata;
+
+    if (this.type === 'processinginstruction') {
+      this.pitarget = child.getAttributeNS(null, 'pitarget') || this.pitarget;
+      this.pidata = child.getAttributeNS(null, 'pidata') || this.pidata;
+    }
+  },
+  toDOM: function(records, dom) {
+    var element;
+    if (this.parent)
+      dom = records[this.parent].dom;
+
+    switch (this.type) {
+      case 'processinginstruction':
+        element = document.createProcessingInstruction(this.pitarget, this.pidata);
+        break;
+      case 'element':
+        if (this.ns)
+          element = document.createElementNS(this.ns, this.name);
+        else
+          element = document.createElement(this.name);
+        break;
+      case 'attr':
+        if (this.ns === 'xml') //FIXME: it’s ugly.
+          this.dom = document.createAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:' + this.name);
+        else if (this.ns)
+          this.dom = document.createAttributeNS(this.ns, this.name);
+        else
+          this.dom = document.createAttribute(this.name);
+        this.dom.value = this.chdata;
+        dom.setAttributeNodeNS(this.dom);
+        return;
+      case 'text':
+        element = document.createTextNode(this.chdata);
+        break;
+      case 'comment':
+        element = document.createComment(this.chdata);
+        break;
+    }
+
+    dom.appendChild(element);
+    this.dom = element;
+  },
+  remove: function(records) {
+    if (this.parent)
+      var parentNode = records[this.parent].dom;
+
+    switch (this.type) {
+      case 'processinginstruction':
+        //TODO
+        break;
+      case 'element':
+        if (this.dom.children.length)
+          return; //TODO: use a better error reporting system.
+        parentNode.removeChild(this.dom);
+        break;
+      case 'attr':
+        parentNode.removeAttributeNS(this.ns, this.name);
+        break;
+      case 'text':
+      case 'comment':
+        parentNode.removeChild(this.node);
+        break;
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/script2.js
@@ -0,0 +1,354 @@
+'use strict';
+
+/** Copyright (c) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+  *
+  * Permission is hereby granted, free of charge, to any person obtaining a copy
+  * of this software and associated documentation files (the "Software"), to
+  * deal in the Software without restriction, including without limitation the
+  * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+  * sell copies of the Software, and to permit persons to whom the Software is
+  * furnished to do so, subject to the following conditions:
+  *
+  * The above copyright notice and this permission notice shall be included in
+  * all copies or substantial portions of the Software.
+  *
+  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+  * IN THE SOFTWARE.
+  */
+
+var conn = new Lightstring.Connection(SERVICE);
+var roster = {};
+var documents = {};
+
+Lightstring.NS.sxe = 'urn:xmpp:sxe:0';
+Lightstring.NS.jingle = {
+  main: 'urn:xmpp:jingle:1',
+  transports: {
+    sxe: 'urn:xmpp:jingle:transports:sxe'
+  },
+  apps: {
+    xhtml: 'urn:xmpp:jingle:apps:xhtml'
+  }
+};
+
+conn.on('iq/' + Lightstring.NS['disco#info'] + ':query', function(stanza) {
+  if (stanza.DOM.getAttributeNS(null, 'type') !== 'get')
+    return;
+
+  var query = stanza.DOM.firstChild;
+  if (query.getAttributeNS(null, 'node')) {
+    var response = "<iq to='" + stanza.DOM.getAttributeNS(null, 'from') + "'" +
+                      " id='" + stanza.DOM.getAttributeNS(null, 'id') + "'" +
+                      " type='error'/>"; //TODO: precise the error.
+    conn.send(response);
+    return;
+  }
+
+  var features = [Lightstring.NS.sxe, Lightstring.NS.jingle.transports.sxe]; //TODO: put that elsewhere.
+
+  var response = "<iq to='" + stanza.DOM.getAttributeNS(null, 'from') + "'" +
+                    " id='" + stanza.DOM.getAttributeNS(null, 'id') + "'" +
+                    " type='result'>" +
+                   "<query xmlns='" + Lightstring.NS['disco#info'] + "'>" +
+                     "<identity category='client' type='browser'/>";
+  features.forEach(function(f) {
+    response += "<feature var='" + f + "'/>";
+  });
+  response += "</query>" +
+            "</iq>";
+
+  conn.send(response);
+});
+
+conn.on('presence', function(stanza) {
+  var from = new Lightstring.JID(stanza.DOM.getAttributeNS(null, 'from'));
+  if (!from.equals(conn.jid)) {
+    var type = stanza.DOM.getAttributeNS(null, 'type');
+    if (!type)
+      Lightstring.discoInfo(conn, from, undefined, function(aData) {
+        roster[from.full] = aData;
+      });
+    else if (type === 'unavailable')
+      delete roster[from.full];
+  }
+});
+
+var host = function(sid, name) {
+  if (!sid)
+    sid = String(Math.random());
+
+  documents[sid] = new Document(conn.jid.full, name, conn.jid.full, 'document');
+
+  for (var jid in roster) {
+    var contact = roster[jid];
+    if ((contact.features.indexOf(Lightstring.NS.sxe) !== -1) && (contact.features.indexOf(Lightstring.NS.jingle.transports.sxe) !== -1))
+      initiate(jid, sid);
+  }
+}
+
+var initiate = function(jid, sid) {
+  var initiate = "<iq to='" + jid + "' type='set'>" +
+                   "<jingle xmlns='" + Lightstring.NS.jingle.main + "'" +
+                          " action='session-initiate'" +
+                          " initiator='" + documents[sid].initiator + "'" +
+                          " sid='" + sid + "'>" +
+                     "<content creator='initiator' name='" + documents[sid].name + "'>" +
+                       "<description xmlns='" + Lightstring.NS.jingle.apps.xhtml + "'/>" +
+                       "<transport xmlns='" + Lightstring.NS.jingle.transports.sxe + "'>" +
+                         "<host>" + documents[sid].host + "</host>" +
+                       "</transport>" +
+                     "</content>" +
+                   "</jingle>" +
+                 "</iq>";
+  conn.send(initiate);
+};
+
+var accept = function(sid) {
+  var accept = "<iq to='" + documents[sid].initiator + "' type='set'>" +
+                 "<jingle xmlns='" + Lightstring.NS.jingle.main + "'" +
+                        " action='session-accept'" +
+                        " initiator='" + documents[sid].initiator + "'" +
+                        " sid='" + sid + "'>" +
+                   "<content creator='initiator' name='" + documents[sid].name + "'>" +
+                     "<description xmlns='" + Lightstring.NS.jingle.apps.xhtml + "'/>" +
+                     "<transport xmlns='" + Lightstring.NS.jingle.transports.sxe + "'>" +
+                       "<host>" + documents[sid].host + "</host>" +
+                     "</transport>" +
+                   "</content>" +
+                 "</jingle>" +
+               "</iq>";
+  conn.send(accept, function(stanza) {
+    if (stanza.DOM.getAttributeNS(null, 'type') === 'result')
+      connect(sid);
+    else
+      terminate('TODO'); //XXX
+  });
+};
+
+var terminate = function(sid, reason) {
+  if (!(sid in documents))
+    return console.log('BIG WARNING!!!');
+
+  var terminate = "<iq to='" + documents[sid].initiator + "' type='set'>" +
+                    "<jingle xmlns='" + Lightstring.NS.jingle.main + "'" +
+                           " action='session-terminate'" +
+                           " host='" + documents[sid].host + "'" +
+                           " initiator='" + documents[sid].initiator + "'" +
+                           " sid='" + sid + "'>" +
+                      "<reason>" +
+                        "<" + reason + "/>" +
+                      "</reason>" +
+                    "</jingle>" +
+                  "</iq>";
+  delete documents[sid];
+  conn.send(terminate);
+};
+
+conn.on('iq/' + Lightstring.NS.jingle.main + ':jingle', function(stanza) {
+  conn.send("<iq to='" + stanza.DOM.getAttributeNS(null, 'from') + "'" +
+               " id='" + stanza.DOM.getAttributeNS(null, 'id') + "'" +
+               " type='result'/>");
+
+  var jingle = stanza.DOM.firstChild;
+  var action = jingle.getAttributeNS(null, 'action');
+  var initiator = jingle.getAttributeNS(null, 'initiator');
+  var sid = jingle.getAttributeNS(null, 'sid');
+
+  var payload = jingle.firstChild; //TODO: what if they are multiple?
+
+  //TODO: verify that the payload is really what we want.
+  //TODO: use the reason even with content as payload.
+  if (action === 'session-initiate') {
+    var content = payload;
+    var creator = content.getAttributeNS(null, 'creator');
+    var name = content.getAttributeNS(null, 'name');
+
+    if (documents[sid])
+      return terminate(sid, 'alternative-session'); //TODO: The XEP: “and wishes to use that [previous] session instead”
+
+    var description = content.getElementsByTagName('description')[0]; //TODO: supporte multiple applications.
+    if (description.namespaceURI !== Lightstring.NS.jingle.apps.xhtml)
+      return terminate(sid, 'unsupported-applications');
+
+    var transport = content.getElementsByTagName('transport')[0]; //TODO: supporte multiple transports.
+    if (transport.namespaceURI !== Lightstring.NS.jingle.transports.sxe)
+      return terminate(sid, 'unsupported-transports');
+
+    var host = transport.textContent; //TODO: verify the presence of the host element.
+
+    documents[sid] = new Document(initiator, name, host, 'document');
+
+    if (confirm('Do you accept?'))
+      accept(sid);
+    else
+      terminate(sid, 'decline');
+
+  } else if (action === 'session-accept') {
+    var content = payload;
+    var creator = content.getAttributeNS(null, 'creator');
+    var name = content.getAttributeNS(null, 'name');
+
+    alert('Accepted! \\o/');
+
+    var description = content.getElementsByTagName('description')[0]; //TODO: supporte multiple applications.
+    if (description.namespaceURI !== Lightstring.NS.jingle.apps.xhtml)
+      return terminate(sid, 'unsupported-applications');
+
+    var transport = content.getElementsByTagName('transport')[0]; //TODO: supporte multiple transports.
+    if (transport.namespaceURI !== Lightstring.NS.jingle.transports.sxe)
+      return terminate(sid, 'unsupported-transports');
+
+    var host = transport.textContent; //TODO: verify the presence of the host element.
+
+    var doc = documents[sid];
+    if (doc.initiator !== initiator ||
+        doc.name !== name ||
+        doc.host !== host)
+      return terminate(sid, 'unknown-error'); //XXX
+
+  } else if (action === 'session-terminate') {
+    var reason = payload;
+
+    //TODO: verify they are what the should be.
+    var element = reason.firstChild;
+    var text = element.nextSibling;
+
+    alert('Terminated, reason: ' + element.localName + (text? ' (' + text.textContent + ')': ''));
+
+    delete documents[sid];
+  }
+});
+
+var connect = function(sid) {
+  var host = documents[sid].host;
+  var identities = roster[host].identities;
+  var type = 'chat';
+  identities.forEach(function(identity) {
+    if (identity.category === 'conference')
+      type = 'groupchat';
+  });
+  var message = "<message to='" + host + "'" +
+                        " type='" + type + "'>" +
+                  "<sxe xmlns='" + Lightstring.NS.sxe + "'" +
+                      " id='" + Lightstring.newId('sxe') + "'" +
+                      " session='" + sid + "'>" +
+                    "<connect/>" +
+                  "</sxe>" +
+                "</message>";
+  conn.send(message);
+};
+
+conn.on('message/' + Lightstring.NS.sxe + ':sxe', function(stanza) {
+  var from = stanza.DOM.getAttributeNS(null, 'from');
+  var type = 'chat'; //TODO: always?
+  var sxe = stanza.DOM.firstChild; //TODO: there can be multiple payloads.
+  var id = sxe.getAttributeNS(null, 'id');
+  var sid = sxe.getAttributeNS(null, 'session');
+
+  var doc = documents[sid];
+  if (!doc) {
+    conn.send("<message to='" + from + "' type='error'/>"); //XXX
+    return;
+  }
+
+  var payload = sxe.firstChild; //TODO: really?
+  switch (payload.localName) {
+    case 'connect':
+      var message = "<message to='" + from + "'" +
+                            " type='" + type + "'>" +
+                      "<sxe xmlns='" + Lightstring.NS.sxe + "'" +
+                          " id='" + Lightstring.newId('sxe') + "'" +
+                          " session='" + sid + "'>" +
+                        "<state-offer>" +
+                          "<description xmlns='" + Lightstring.NS.jingle.apps.xhtml + "'/>" +
+                        "</state-offer>" +
+                      "</sxe>" +
+                    "</message>";
+      conn.send(message);
+      break;
+    case 'state-offer':
+      var description = payload.firstChild;
+      if (description.namespaceURI !== Lightstring.NS.jingle.apps.xhtml)
+        return terminate(sid, 'unsupported-applications');
+
+      var accept = false;
+      if (from === doc.host || from === doc.initiator)
+        accept = true;
+
+      //TODO: refuse if proposed multiple times.
+
+      var message = "<message to='" + from + "'" +
+                            " type='" + type + "'>" +
+                      "<sxe xmlns='" + Lightstring.NS.sxe + "'" +
+                          " id='" + Lightstring.newId('sxe') + "'" +
+                          " session='" + sid + "'>" +
+                        "<" + (accept? 'accept': 'refuse') + "-state/>" +
+                      "</sxe>" +
+                    "</message>";
+      conn.send(message);
+      break;
+    case 'accept-state':
+      var message = "<message to='" + from + "'" +
+                            " type='" + type + "'>" +
+                      "<sxe xmlns='" + Lightstring.NS.sxe + "'" +
+                          " id='" + Lightstring.newId('sxe') + "'" +
+                          " session='" + sid + "'>" +
+                        "<state>" +
+                          "<document-begin prolog='" + doc.prolog + "'/>" +
+                          //TODO: support non-empty documents.
+                          "<document-end last-sender='' last-id=''/>" +
+                        "</state>" +
+                      "</sxe>" +
+                    "</message>";
+      conn.send(message);
+      break;
+    case 'refuse-state':
+      terminate(sid, 'decline');
+      break;
+    case 'state':
+      var elements = payload.children;
+      doc.processState(from, elements);
+      break;
+    default:
+      var elements = sxe.children;
+      doc.processState(from, elements);
+  }
+});
+
+(function() {
+  var events = ['connected', 'conn-error', 'connecting', 'disconnected', 'disconnecting'];
+  var state = document.getElementById('state');
+  events.forEach(function(e) {
+    conn.on(e, function() {
+      state.innerHTML = e;
+    });
+  });
+})();
+
+conn.on('error', function(a) {
+  alert(a);
+});
+
+document.getElementById('host').addEventListener('click', function() {
+  host(Lightstring.newId('sess'), 'My First XHTML Document');
+}, false);
+
+conn.on('connected', function() {
+  conn.on('output', function(stanza) {
+    console.log('out:', stanza.DOM);
+  });
+  conn.on('input', function(stanza) {
+    console.log('in:', stanza.DOM);
+  });
+
+  Lightstring.presence(conn);
+});
+
+conn.connect(JID + '/' + prompt('Resource?'), PASSWORD);
+
+register_feature_not_implemented.call(conn);
new file mode 100644
--- /dev/null
+++ b/sxe-document.js
@@ -0,0 +1,157 @@
+'use strict';
+
+var Document = function(initiator, name, host, domId, prolog) {
+  this.initiator = initiator;
+  this.name = name;
+  this.host = host;
+
+  this.prolog = prolog || 'data:application/xhtml+xml,%3C%3Fxml%20version%3D%271.0%27%3F%3E%0A%3C%21DOCTYPE%20html%3E%0A';
+  this.state = 'not-started';
+  this.records = {};
+  this.dom = document.getElementById(domId);
+};
+
+Document.prototype = {
+  add: function(jid, child) {
+    var record = new Record(jid, child);
+
+    if (record.rid in this.records)
+      console.log('duplicate new');
+
+    this.records[record.rid] = record;
+    record.toDOM(this.records, this.dom);
+  },
+  update: function(jid, child) {
+    var target = child.getAttributeNS(null, 'target');
+
+    if (!(target in this.records))
+      return; // Ignore it.
+
+    var record = this.records[target];
+    console.log(record, child);
+    child.setAttributeNS(null, 'rid', target);
+
+    var version = +child.getAttributeNS(null, 'version');
+    if (this.state === 'getting-state')
+      record.version = version;
+    else
+      record.version++;
+
+    if (record.version === version) {
+      var type = child.getAttributeNS(null, 'type');
+      if (type === 'text' || type === 'attr' || type === 'comment') {
+        var chdata = child.getAttributeNS(null, 'chdata');
+        var replacefrom = +child.getAttributeNS(null, 'replacefrom');
+        var replacen = +child.getAttributeNS(null, 'replacen');
+        if (chdata && replacefrom && replacen) {
+          var string = record.chdata.substr(0, replacefrom);
+          string += chdata;
+          string += record.chdata.substr(replacefrom + replacen);
+          child.removeAttributeNS(null, 'replacefrom');
+          child.removeAttributeNS(null, 'replacen');
+          child.setAttributeNS(null, 'chdata', string);
+        }
+      }
+      record.update(jid, child);
+      record.toDOM(this.records, this.dom);
+    } else
+      ; // Not sure I understand correctly.
+  },
+  remove: function(jid, child) {
+    var rid = child.getAttributeNS(null, 'target');
+    this.records[rid].remove(this.records);
+    delete this.records[rid];
+  },
+  processState: function(jid, elements) {
+    var i = 0;
+    var first = elements[0];
+    if (first.localName === 'document-begin') {
+      if (this.state !== 'not-started')
+        return; //TODO: the session has already started.
+      i = 1;
+      this.prolog = first.getAttributeNS(null, 'prolog');
+      this.state = 'getting-session';
+    }
+
+    //TODO: if the session isn’t started, should ignore changes?
+
+    for (; i < elements.length; i++) {
+      var child = elements[i];
+      var change = child.localName;
+
+      switch (change) {
+        case 'new':
+          doc.add(jid, child);
+          break;
+        case 'set':
+          doc.update(jid, child);
+          break;
+        case 'remove':
+          doc.remove(jid, child);
+          break;
+        case 'document-end':
+          this.state = 'started';
+          break;
+      }
+    }
+  },
+  createState: function(root, parent, state) {
+    var children = root.childNodes;
+    for (var i = 0; i < children.length; i++) {
+      var child = children[i];
+      var element = document.createElementNS(Lightstring.NS.sxe, 'new');
+      var rid = Lightstring.newId('GUID');
+      element.setAttributeNS(null, 'rid', rid);
+
+      if (parent)
+        element.setAttributeNS(null, 'parent', parent);
+
+      switch (child.nodeType) {
+        case 1:
+          element.setAttributeNS(null, 'type', 'element');
+          element.setAttributeNS(null, 'ns', child.namespaceURI);
+          element.setAttributeNS(null, 'name', child.localName);
+          state.push(element);
+
+          //TODO: move that elsewhere, or make it prettier.
+          var convertAttr = function(attr) {
+            var element = document.createElementNS(Lightstring.NS.sxe, 'new');
+            var arid = Lightstring.newId('GUID');
+            element.setAttributeNS(null, 'rid', arid);
+            element.setAttributeNS(null, 'parent', rid);
+            if (attr.namespaceURI)
+              element.setAttributeNS(null, 'ns', attr.namespaceURI);
+            element.setAttributeNS(null, 'name', attr.localName);
+            element.setAttributeNS(null, 'chdata', attr.textContent);
+            state.push(element);
+          };
+
+          for (var j = 0; j < child.attributes.length; j++)
+            convertAttr(child.attributes[j]);
+
+          state = this.createState(child, rid, state);
+          break;
+
+        case 3:
+          element.setAttributeNS(null, 'type', 'text');
+          element.setAttributeNS(null, 'chdata', child.textContent);
+          state.push(element);
+          break;
+
+        case 7:
+          element.setAttributeNS(null, 'type', 'processinginstruction');
+          element.setAttributeNS(null, 'pitarget', child.target);
+          element.setAttributeNS(null, 'pidata', child.data);
+          state.push(element);
+          break;
+
+        case 8:
+          element.setAttributeNS(null, 'type', 'comment');
+          element.setAttributeNS(null, 'chdata', child.textContent);
+          state.push(element);
+          break;
+      }
+    }
+    return state;
+  }
+};