Index: frontend/node_modules/dom-converter/lib/domConverter.js
===================================================================
--- frontend/node_modules/dom-converter/lib/domConverter.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/dom-converter/lib/domConverter.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,33 @@
+// Generated by CoffeeScript 1.12.7
+var domToMarkup, object, objectToSaneObject, saneObjectToDom, self;
+
+objectToSaneObject = require('./objectToSaneObject');
+
+saneObjectToDom = require('./saneObjectToDom');
+
+domToMarkup = require('./domToMarkup');
+
+object = require('utila').object;
+
+module.exports = self = {
+  objectToDom: function(o) {
+    o = self._object2SaneObject(o);
+    return saneObjectToDom.convert(o);
+  },
+  object2markup: function(o) {
+    var dom;
+    dom = self.objectToDom(o);
+    return domToMarkup.convert(dom);
+  },
+  domToMarkup: function(dom) {
+    return domToMarkup.convert(dom);
+  },
+  _object2SaneObject: function(o) {
+    if (!Array.isArray(o)) {
+      if (!object.isBareObject(o)) {
+        throw Error("toDom() only accepts arrays and bare objects as input");
+      }
+    }
+    return objectToSaneObject.sanitize(o);
+  }
+};
Index: frontend/node_modules/dom-converter/lib/domToMarkup.js
===================================================================
--- frontend/node_modules/dom-converter/lib/domToMarkup.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/dom-converter/lib/domToMarkup.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,2 @@
+// Generated by CoffeeScript 1.12.7
+
Index: frontend/node_modules/dom-converter/lib/objectToSaneObject.js
===================================================================
--- frontend/node_modules/dom-converter/lib/objectToSaneObject.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/dom-converter/lib/objectToSaneObject.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,63 @@
+// Generated by CoffeeScript 1.12.7
+var object, self,
+  hasProp = {}.hasOwnProperty;
+
+object = require('utila').object;
+
+module.exports = self = {
+  sanitize: function(val) {
+    return self._toChildren(val);
+  },
+  _toChildren: function(val) {
+    var ref;
+    if (object.isBareObject(val)) {
+      return self._objectToChildren(val);
+    } else if (Array.isArray(val)) {
+      return self._arrayToChildren(val);
+    } else if (val === null || typeof val === 'undefined') {
+      return [];
+    } else if ((ref = typeof val) === 'string' || ref === 'number') {
+      return [String(val)];
+    } else {
+      throw Error("not a valid child node: `" + val);
+    }
+  },
+  _objectToChildren: function(o) {
+    var a, cur, key, val;
+    a = [];
+    for (key in o) {
+      if (!hasProp.call(o, key)) continue;
+      val = o[key];
+      cur = {};
+      cur[key] = self.sanitize(val);
+      a.push(cur);
+    }
+    return a;
+  },
+  _arrayToChildren: function(a) {
+    var i, len, ret, v;
+    ret = [];
+    for (i = 0, len = a.length; i < len; i++) {
+      v = a[i];
+      ret.push(self._toNode(v));
+    }
+    return ret;
+  },
+  _toNode: function(o) {
+    var key, keys, obj, ref;
+    if ((ref = typeof o) === 'string' || ref === 'number') {
+      return String(o);
+    } else if (object.isBareObject(o)) {
+      keys = Object.keys(o);
+      if (keys.length !== 1) {
+        throw Error("a node must only have one key as tag name");
+      }
+      key = keys[0];
+      obj = {};
+      obj[key] = self._toChildren(o[key]);
+      return obj;
+    } else {
+      throw Error("not a valid node: `" + o + "`");
+    }
+  }
+};
Index: frontend/node_modules/dom-converter/lib/saneObjectToDom.js
===================================================================
--- frontend/node_modules/dom-converter/lib/saneObjectToDom.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/dom-converter/lib/saneObjectToDom.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,114 @@
+// Generated by CoffeeScript 1.12.7
+var self,
+  hasProp = {}.hasOwnProperty;
+
+module.exports = self = {
+  convert: function(obj) {
+    return self._arrayToChildren(obj);
+  },
+  _arrayToChildren: function(a, parent) {
+    var children, j, len, node, prev, v;
+    if (parent == null) {
+      parent = null;
+    }
+    children = [];
+    prev = null;
+    for (j = 0, len = a.length; j < len; j++) {
+      v = a[j];
+      if (typeof v === 'string') {
+        node = self._getTextNodeFor(v);
+      } else {
+        node = self._objectToNode(v, parent);
+        node.prev = null;
+        node.next = null;
+        node.parent = parent;
+        if (prev != null) {
+          node.prev = prev;
+          prev.next = node;
+        }
+        prev = node;
+      }
+      children.push(node);
+    }
+    return children;
+  },
+  _objectToNode: function(o) {
+    var attribs, children, i, k, key, name, node, ref, v, val;
+    i = 0;
+    for (k in o) {
+      if (!hasProp.call(o, k)) continue;
+      v = o[k];
+      if (i > 0) {
+        throw Error("_objectToNode() only accepts an object with one key/value");
+      }
+      key = k;
+      val = v;
+      i++;
+    }
+    node = {};
+    if (typeof key !== 'string') {
+      throw Error("_objectToNode()'s key must be a string of tag name and classes");
+    }
+    if (typeof val === 'string') {
+      children = [self._getTextNodeFor(val)];
+    } else if (Array.isArray(val)) {
+      children = self._arrayToChildren(val, node);
+    } else {
+      inspect(o);
+      throw Error("_objectToNode()'s key's value must only be a string or an array");
+    }
+    node.type = 'tag';
+    ref = self._parseTag(key), name = ref.name, attribs = ref.attribs;
+    node.name = name;
+    node.attribs = attribs;
+    node.children = children;
+    return node;
+  },
+  _getTextNodeFor: function(s) {
+    return {
+      type: 'text',
+      data: s
+    };
+  },
+  _nameRx: /^[a-zA-Z\-\_]{1}[a-zA-Z0-9\-\_]*$/,
+  _parseTag: function(k) {
+    var attribs, classes, cls, id, m, name, parts;
+    if (!k.match(/^[a-zA-Z0-9\#\-\_\.\[\]\"\'\=\,\s]+$/) || k.match(/^[0-9]+/)) {
+      throw Error("cannot parse tag `" + k + "`");
+    }
+    attribs = {};
+    parts = {
+      name: '',
+      attribs: attribs
+    };
+    if (m = k.match(/^([^\.#]+)/)) {
+      name = m[1];
+      if (!name.match(self._nameRx)) {
+        throw Error("tag name `" + name + "` is not valid");
+      }
+      parts.name = name;
+      k = k.substr(name.length, k.length);
+    }
+    if (m = k.match(/^#([a-zA-Z0-9\-]+)/)) {
+      id = m[1];
+      if (!id.match(self._nameRx)) {
+        throw Error("tag id `" + id + "` is not valid");
+      }
+      attribs.id = id;
+      k = k.substr(id.length + 1, k.length);
+    }
+    classes = [];
+    while (m = k.match(/\.([a-zA-Z0-9\-\_]+)/)) {
+      cls = m[1];
+      if (!cls.match(self._nameRx)) {
+        throw Error("tag class `" + cls + "` is not valid");
+      }
+      classes.push(cls);
+      k = k.replace('.' + cls, '');
+    }
+    if (classes.length) {
+      attribs["class"] = classes.join(" ");
+    }
+    return parts;
+  }
+};
