Use requirejs for clientside scripting
diff --git a/dev/js/src/api.js b/dev/js/src/api.js
index ed33cc2..f9058bc 100644
--- a/dev/js/src/api.js
+++ b/dev/js/src/api.js
@@ -1,15 +1,4 @@
-var KorAP = KorAP || {};
-
-(function (KorAP) {
-  "use strict";
-
-  // Default log message
-  KorAP.log = KorAP.log || function (type, msg) {
-    console.log(type + ": " + msg);
-  };
-
-  KorAP.URL = KorAP.URL || 'http://korap.ids-mannheim.de/kalamar';
-
+define(['util'], function () {
   // TODO: https://github.com/honza/140medley/blob/master/140medley.js
   // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
   // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
@@ -17,6 +6,8 @@
   // http://www.javascriptkit.com/javatutors/loadjavascriptcss.shtml
   // http://stackoverflow.com/questions/6112744/load-javascript-on-demand
 
+  KorAP.URL = KorAP.URL || 'http://korap.ids-mannheim.de/kalamar';
+
   KorAP.API = {
     getMatchInfo : function (match, param, cb) {
       // match is a KorAP.Match object
@@ -44,14 +35,11 @@
 
       this.getJSON(url, cb);
     },
+
     getJSON : function (url, onload) {
       var req = new XMLHttpRequest();
 
-      console.log('Request url: ' + url);
-
       req.open("GET", url, true);
-
-
       req.setRequestHeader("Accept", "application/json");
       req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 
       req.onreadystatechange = function () {
@@ -62,7 +50,7 @@
 	  2 - headers received
 	  3 - loading (responseText has partial data)
 	  4 - done
-	 */
+	*/
 	if (this.readyState == 4) {
 	  if (this.status === 200)
 	    onload(JSON.parse(this.responseText));
@@ -76,5 +64,4 @@
       req.send();
     }
   };
-
-}(this.KorAP));
+});
diff --git a/dev/js/src/hint.js b/dev/js/src/hint.js
index 11c3e1e..6aed544 100644
--- a/dev/js/src/hint.js
+++ b/dev/js/src/hint.js
@@ -3,23 +3,20 @@
  *
  * @author Nils Diewald
  */
-
-// requires menu.js
-
-var KorAP = KorAP || {};
-
-(function (KorAP) {
+define([
+  'hint/input',
+  'hint/menu',
+  'hint/contextanalyzer',
+  'util'
+], function (inputClass, 
+	     menuClass, 
+	     analyzerClass) {
   "use strict";
 
-  // Default log message
-  KorAP.log = KorAP.log || function (type, msg) {
-    console.log(type + ": " + msg);
-  };
-
   /**
    * @define {regex} Regular expression for context
    */
-  KorAP.context =
+  KorAP.context = KorAP.context ||
     "(?:^|[^-_a-zA-Z0-9])" +   // Anchor
     "((?:[-_a-zA-Z0-9]+?)\/" + // Foundry
     "(?:" +
@@ -27,10 +24,18 @@
     "(?:(?:[^:=\/ ]+?):)?" +   // Key
     ")?" +
     ")$";
-
-  // Initialize hint array
   KorAP.hintArray = KorAP.hintArray || {};
 
+  /**
+   * Return keycode based on event
+   */
+  function _codeFromEvent (e) {
+    if ((e.charCode) && (e.keyCode==0))
+      return e.charCode
+    return e.keyCode;
+  };
+
+  // Initialize hint array
 
   /**
    * KorAP.Hint.create({
@@ -38,16 +43,20 @@
    *   context : context regex
    * });
    */
-  KorAP.Hint = {
+  return {
 
     // Some variables
     // _firstTry : true,
     active : false,
 
+    /**
+     * Create new hint helper.
+     */
     create : function (param) {
-      return Object.create(KorAP.Hint)._init(param);
+      return Object.create(this)._init(param);
     },
 
+    // Initialize hint helper
     _init : function (param) {
       param = param || {};
 
@@ -55,9 +64,11 @@
       this._menu = {};
 
       // Get input field
-      this._inputField = KorAP.InputField.create(
-	param["inputField"] || document.getElementById("q-field")
-      );
+      var qfield = param["inputField"] || document.getElementById("q-field");
+      if (!qfield)
+	return null;
+
+      this._inputField = inputClass.create(qfield);
 
       var inputFieldElement = this._inputField.element();
 
@@ -83,7 +94,7 @@
       );
 
       // Set Analyzer for context
-      this._analyzer = KorAP.ContextAnalyzer.create(
+      this._analyzer = analyzerClass.create(
 	param["context"] || KorAP.context
       );
       return this;
@@ -124,10 +135,9 @@
 	  return;
 
 	// Create matching hint menu
-	this._menu[action] = KorAP.HintMenu.create(
+	this._menu[action] = menuClass.create(
 	  this, action, KorAP.hintArray[action]
 	);
-
       };
 
       // Return matching hint menu
@@ -189,278 +199,4 @@
       };
     }
   };
-
-
-  // Input field for queries
-  KorAP.InputField = {
-    create : function (element) {
-      return Object.create(KorAP.InputField)._init(element);
-    },
-
-    _init : function (element) {
-      this._element = element;
-
-      // Create mirror for searchField
-      if ((this._mirror = document.getElementById("searchMirror")) === null) {
-	this._mirror = document.createElement("div");
-	this._mirror.setAttribute("id", "searchMirror");
-	this._mirror.appendChild(document.createElement("span"));
-	this._container = this._mirror.appendChild(document.createElement("div"));
-	this._mirror.style.height = "0px";
-	document.getElementsByTagName("body")[0].appendChild(this._mirror);
-      };
-
-      // Update position of the mirror
-      var that = this;
-      var repos = function () {
-	that.reposition();
-      };
-      window.addEventListener('resize', repos);
-      this._element.addEventListener('onfocus', repos);
-      that.reposition();
-
-      return this;
-    },
-
-    rightPos : function () {
-      var box = this._mirror.firstChild.getBoundingClientRect();
-      return box.right - box.left;
-    },
-
-    mirror : function () {
-      return this._mirror;
-    },
-
-    container : function () {
-      return this._container;
-    },
-
-    element : function () {
-      return this._element;
-    },
-
-    value : function () {
-      return this._element.value;
-    },
-
-    update : function () {
-      this._mirror.firstChild.textContent = this.split()[0];
-      this._container.style.left = this.rightPos() + 'px';
-    },
-
-    insert : function (text) {
-      var splittedText = this.split();
-      var s = this._element;
-      s.value = splittedText[0] + text + splittedText[1];
-      s.selectionStart = (splittedText[0] + text).length;
-      s.selectionEnd = s.selectionStart;
-      this._mirror.firstChild.textContent = splittedText[0] + text;
-    },
-
-    // Return two substrings, splitted at current cursor position
-    split : function () {
-      var s = this._element;
-      var value = s.value;
-      var start = s.selectionStart;
-      return new Array(
-	value.substring(0, start),
-	value.substring(start, value.length)
-      );
-    },
-
-    // Position the input mirror directly below the input box
-    reposition : function () {
-      var inputClientRect = this._element.getBoundingClientRect();
-      var inputStyle = window.getComputedStyle(this._element, null);
-
-      var bodyClientRect = 
-	document.getElementsByTagName('body')[0].getBoundingClientRect();
-
-      // Reset position
-      var mirrorStyle = this._mirror.style;
-      mirrorStyle.left = inputClientRect.left + "px";
-      mirrorStyle.top  = (inputClientRect.bottom - bodyClientRect.top) + "px";
-      mirrorStyle.width = inputStyle.getPropertyValue("width");
-
-      // These may be relevant in case of media depending css
-      mirrorStyle.paddingLeft     = inputStyle.getPropertyValue("padding-left");
-      mirrorStyle.marginLeft      = inputStyle.getPropertyValue("margin-left");
-      mirrorStyle.borderLeftWidth = inputStyle.getPropertyValue("border-left-width");
-      mirrorStyle.borderLeftStyle = inputStyle.getPropertyValue("border-left-style");
-      mirrorStyle.fontSize        = inputStyle.getPropertyValue("font-size");
-      mirrorStyle.fontFamily      = inputStyle.getPropertyValue("font-family");
-    },
-    context : function () {
-      return this.split()[0];
-    }
-  };
-
-
-  /**
-   * Regex object for checking the context of the hint
-   */
-  KorAP.ContextAnalyzer = {
-    create : function (regex) {
-      return Object.create(KorAP.ContextAnalyzer)._init(regex);
-    },
-    _init : function (regex) {
-      try {
-	this._regex = new RegExp(regex);
-      }
-      catch (e) {
-	KorAP.log(0, e);
-	return;
-      };
-      return this;
-    },
-    test : function (text) {
-      if (!this._regex.exec(text))
-	return;
-      return RegExp.$1;
-    }
-  };
-
-
-  /**
-   * Hint menu
-   */
-  KorAP.HintMenu = {
-    create : function (hint, context, params) {
-      var obj = Object.create(KorAP.Menu)
-	.upgradeTo(KorAP.HintMenu)
-	._init(KorAP.HintMenuItem, KorAP.HintMenuPrefix, params);
-      obj._context = context;
-      obj._element.classList.add('hint');
-      obj._hint = hint;
-
-      // This is only domspecific
-      obj.element().addEventListener('blur', function (e) {
-	this.menu.hide();
-      });
-
-      // Focus on input field on hide
-      obj.onHide = function () {
-	var input = this._hint.inputField();
-	input.container().classList.remove('active');
-	input.element().focus();
-      };
-
-      return obj;
-    },
-    // Todo: Is this necessary?
-    context : function () {
-      return this._context;
-    },
-    hint : function () {
-      return this._hint;
-    }
-  };
-
-
-  /**
-   * Hint menu item based on MenuItem
-   */
-  KorAP.HintMenuItem = {
-    create : function (params) {
-      return Object.create(KorAP.MenuItem)
-	.upgradeTo(KorAP.HintMenuItem)
-	._init(params);
-    },
-    _init : function (params) {
-      if (params[0] === undefined ||
-	  params[1] === undefined)
-	throw new Error("Missing parameters");
-      
-      this._name   = params[0];
-      this._action = params[1];
-      this._lcField = ' ' + this._name.toLowerCase();
-      
-      if (params.length > 2) {
-	this._desc = params[2];
-	this._lcField += " " + this._desc.toLowerCase();
-      };
-
-      return this;
-    },
-    content : function (content) {
-      if (arguments.length === 1) {
-	this._content = content;
-      };
-      return this._content;
-    },
-    onclick : function () {
-      var m = this.menu();
-      var h = m.hint();
-      m.hide();
-
-      // Update input field
-      var input = h.inputField();
-      input.insert(this._action);
-      input.update();
-
-      h.active = false;
-      h.show(true);
-    },
-    name : function () {
-      return this._name;
-    },
-    action : function () {
-      return this._action;
-    },
-    desc : function () {
-      return this._desc;
-    },
-    element : function () {
-      // already defined
-      if (this._element !== undefined)
-	return this._element;
-
-      // Create list item
-      var li = document.createElement("li");
-
-      if (this.onclick !== undefined) {
-	li["onclick"] = this.onclick.bind(this);
-      };
-
-      // Create title
-      var name =  document.createElement("span");
-      name.appendChild(document.createTextNode(this._name));
-      
-      li.appendChild(name);
-
-      // Create description
-      if (this._desc !== undefined) {
-	var desc = document.createElement("span");
-	desc.classList.add('desc');
-	desc.appendChild(document.createTextNode(this._desc));
-	li.appendChild(desc);
-      };
-      return this._element = li;
-    }
-  };
-
-  KorAP.HintMenuPrefix = {
-    create : function (params) {
-      return Object.create(KorAP.MenuPrefix).upgradeTo(KorAP.HintMenuPrefix)._init(params);
-    },
-    onclick : function () {
-      var m = this.menu();
-      var h = m.hint();
-      m.hide();
-
-      h.inputField().insert(this.value());
-      h.active = false;
-    }
-  };
-
-
-  /**
-   * Return keycode based on event
-   */
-  function _codeFromEvent (e) {
-    if ((e.charCode) && (e.keyCode==0))
-      return e.charCode
-    return e.keyCode;
-  };
-
-}(this.KorAP));
+});
diff --git a/dev/js/src/hint/contextanalyzer.js b/dev/js/src/hint/contextanalyzer.js
new file mode 100644
index 0000000..bdbf885
--- /dev/null
+++ b/dev/js/src/hint/contextanalyzer.js
@@ -0,0 +1,23 @@
+/**
+ * Regex object for checking the context of the hint
+ */
+define({
+  create : function (regex) {
+    return Object.create(this)._init(regex);
+  },
+  _init : function (regex) {
+    try {
+      this._regex = new RegExp(regex);
+    }
+    catch (e) {
+      KorAP.log(0, e);
+      return;
+    };
+    return this;
+  },
+  test : function (text) {
+    if (!this._regex.exec(text))
+      return;
+    return RegExp.$1;
+  }
+});
diff --git a/dev/js/src/hint/input.js b/dev/js/src/hint/input.js
new file mode 100644
index 0000000..b1c6632
--- /dev/null
+++ b/dev/js/src/hint/input.js
@@ -0,0 +1,103 @@
+// Input field for queries
+define({
+  create : function (element) {
+    return Object.create(this)._init(element);
+  },
+    
+  _init : function (element) {
+    this._element = element;
+
+    // Create mirror for searchField
+    if ((this._mirror = document.getElementById("searchMirror")) === null) {
+      this._mirror = document.createElement("div");
+      this._mirror.setAttribute("id", "searchMirror");
+      this._mirror.appendChild(document.createElement("span"));
+      this._container = this._mirror.appendChild(document.createElement("div"));
+      this._mirror.style.height = "0px";
+      document.getElementsByTagName("body")[0].appendChild(this._mirror);
+    };
+
+    // Update position of the mirror
+    var that = this;
+    var repos = function () {
+      that.reposition();
+    };
+    window.addEventListener('resize', repos);
+    this._element.addEventListener('onfocus', repos);
+    that.reposition();
+
+    return this;
+  },
+
+  rightPos : function () {
+    var box = this._mirror.firstChild.getBoundingClientRect();
+    return box.right - box.left;
+  },
+
+  mirror : function () {
+    return this._mirror;
+  },
+
+  container : function () {
+    return this._container;
+  },
+
+  element : function () {
+    return this._element;
+  },
+
+  value : function () {
+    return this._element.value;
+  },
+
+  update : function () {
+    this._mirror.firstChild.textContent = this.split()[0];
+    this._container.style.left = this.rightPos() + 'px';
+  },
+
+  insert : function (text) {
+    var splittedText = this.split();
+    var s = this._element;
+    s.value = splittedText[0] + text + splittedText[1];
+    s.selectionStart = (splittedText[0] + text).length;
+    s.selectionEnd = s.selectionStart;
+    this._mirror.firstChild.textContent = splittedText[0] + text;
+  },
+  
+  // Return two substrings, splitted at current cursor position
+  split : function () {
+    var s = this._element;
+    var value = s.value;
+    var start = s.selectionStart;
+    return new Array(
+      value.substring(0, start),
+      value.substring(start, value.length)
+    );
+  },
+
+  // Position the input mirror directly below the input box
+  reposition : function () {
+    var inputClientRect = this._element.getBoundingClientRect();
+    var inputStyle = window.getComputedStyle(this._element, null);
+
+    var bodyClientRect = 
+      document.getElementsByTagName('body')[0].getBoundingClientRect();
+
+    // Reset position
+    var mirrorStyle = this._mirror.style;
+    mirrorStyle.left = inputClientRect.left + "px";
+    mirrorStyle.top  = (inputClientRect.bottom - bodyClientRect.top) + "px";
+    mirrorStyle.width = inputStyle.getPropertyValue("width");
+    
+    // These may be relevant in case of media depending css
+    mirrorStyle.paddingLeft     = inputStyle.getPropertyValue("padding-left");
+    mirrorStyle.marginLeft      = inputStyle.getPropertyValue("margin-left");
+    mirrorStyle.borderLeftWidth = inputStyle.getPropertyValue("border-left-width");
+    mirrorStyle.borderLeftStyle = inputStyle.getPropertyValue("border-left-style");
+    mirrorStyle.fontSize        = inputStyle.getPropertyValue("font-size");
+    mirrorStyle.fontFamily      = inputStyle.getPropertyValue("font-family");
+  },
+  context : function () {
+    return this.split()[0];
+  }
+});
diff --git a/dev/js/src/hint/item.js b/dev/js/src/hint/item.js
new file mode 100644
index 0000000..f23cabc
--- /dev/null
+++ b/dev/js/src/hint/item.js
@@ -0,0 +1,85 @@
+/**
+ * Hint menu item based on MenuItem
+ */
+define(['menu/item'], function (itemClass) {
+  return {
+    create : function (params) {
+      return Object.create(itemClass)
+	.upgradeTo(this)
+	._init(params);
+    },
+    _init : function (params) {
+      if (params[0] === undefined ||
+	  params[1] === undefined)
+	throw new Error("Missing parameters");
+      
+      this._name   = params[0];
+      this._action = params[1];
+      this._lcField = ' ' + this._name.toLowerCase();
+      
+      if (params.length > 2) {
+	this._desc = params[2];
+	this._lcField += " " + this._desc.toLowerCase();
+      };
+
+      return this;
+    },
+
+    content : function (content) {
+      if (arguments.length === 1) {
+	this._content = content;
+      };
+      return this._content;
+    },
+    
+    onclick : function () {
+      var m = this.menu();
+      var h = m.hint();
+      m.hide();
+
+      // Update input field
+      var input = h.inputField();
+      input.insert(this._action);
+      input.update();
+
+      h.active = false;
+      h.show(true);
+    },
+    name : function () {
+      return this._name;
+    },
+    action : function () {
+      return this._action;
+    },
+    desc : function () {
+      return this._desc;
+    },
+    element : function () {
+      // already defined
+      if (this._element !== undefined)
+	return this._element;
+
+      // Create list item
+      var li = document.createElement("li");
+
+      if (this.onclick !== undefined) {
+	li["onclick"] = this.onclick.bind(this);
+      };
+
+      // Create title
+      var name =  document.createElement("span");
+      name.appendChild(document.createTextNode(this._name));
+      
+      li.appendChild(name);
+
+      // Create description
+      if (this._desc !== undefined) {
+	var desc = document.createElement("span");
+	desc.classList.add('desc');
+	desc.appendChild(document.createTextNode(this._desc));
+	li.appendChild(desc);
+      };
+      return this._element = li;
+    }
+  };
+});
diff --git a/dev/js/src/hint/menu.js b/dev/js/src/hint/menu.js
new file mode 100644
index 0000000..c579945
--- /dev/null
+++ b/dev/js/src/hint/menu.js
@@ -0,0 +1,36 @@
+/**
+ * Hint menu
+ */
+define(['menu', 'hint/item', 'hint/prefix'], function (menuClass, itemClass, prefixClass) {
+  return {
+    create : function (hint, context, params) {
+      var obj = Object.create(menuClass)
+	.upgradeTo(this)
+	._init(itemClass, prefixClass, params);
+      obj._context = context;
+      obj._element.classList.add('hint');
+      obj._hint = hint;
+
+      // This is only domspecific
+      obj.element().addEventListener('blur', function (e) {
+	this.menu.hide();
+      });
+
+      // Focus on input field on hide
+      obj.onHide = function () {
+	var input = this._hint.inputField();
+	input.container().classList.remove('active');
+	input.element().focus();
+      };
+
+      return obj;
+    },
+    // Todo: Is this necessary?
+    context : function () {
+      return this._context;
+    },
+    hint : function () {
+      return this._hint;
+    }
+  };
+});
diff --git a/dev/js/src/hint/prefix.js b/dev/js/src/hint/prefix.js
new file mode 100644
index 0000000..84f785a
--- /dev/null
+++ b/dev/js/src/hint/prefix.js
@@ -0,0 +1,15 @@
+define(['menu/prefix'], function (prefixClass) {
+  return {
+    create : function (params) {
+      return Object.create(prefixClass).upgradeTo(this)._init(params);
+    },
+    onclick : function () {
+      var m = this.menu();
+      var h = m.hint();
+      m.hide();
+
+      h.inputField().insert(this.value());
+      h.active = false;
+    }
+  };
+});
diff --git a/dev/js/src/init.js b/dev/js/src/init.js
index ecaad6f..66bc081 100644
--- a/dev/js/src/init.js
+++ b/dev/js/src/init.js
@@ -1,42 +1,17 @@
-/**
- * These are utility functions for the frontend
- */
-
-// Add toggleClass method similar to jquery
-HTMLElement.prototype.toggleClass = function (c1, c2) {
-  var cl = this.classList;
-  if (cl.contains(c1)) {
-    cl.add(c2);
-    cl.remove(c1);
-  }
-  else {
-    cl.remove(c2);
-    cl.add(c1);
-  };
-};
-
-
-// Don't let events bubble up
-if (Event.halt === undefined) {
-  // Don't let events bubble up
-  Event.prototype.halt = function () {
-    this.stopPropagation();
-    this.preventDefault();
-  };
-};
-
-var KorAP = KorAP || {};
-
-
-(function (KorAP) {
-  "use strict";
-
-
-  /**
-   * Initialize user interface elements
-   */
-  KorAP.init = function () {
-    var obj = Object.create(KorAP.init);
+define([
+  'match',
+  'hint',
+  'vc',
+  'tutorial',
+  'lib/domReady',
+  'util'
+], function (matchClass,
+	     hintClass,
+	     vcClass,
+	     tutClass,
+	     domReady) {
+  domReady(function (event) {
+    var obj = {};
 
     /**
      * Add actions to match entries
@@ -49,13 +24,13 @@
       inactiveLi[i].addEventListener('click', function (e) {
 	if (this._match !== undefined)
 	  this._match.open();
-	else
-	  KorAP.Match.create(this).open();
+	else {
+	  matchClass.create(this).open();
+	};
 	e.halt();
       });
     };
 
-
     /**
      * Toggle the alignment (left <=> right)
      */
@@ -97,28 +72,29 @@
       input.parentNode.insertBefore(vcname, input);
       
       vcname.onclick = function () {
-	var vc = KorAP.VirtualCollection.render(vcExample);
+	var vc = vcClass.render(vcExample);
 	var view = document.getElementById('vc-view');
 	view.appendChild(vc.element());
       };
     };
 
+  
     /**
      * Init Tutorial view
      */
-    obj.tutorial = KorAP.Tutorial.create(
+    obj.tutorial = tutClass.create(
       document.getElementById('view-tutorial')
     );
 
+  
     /**
      * Init hint helper
      * has to be final because of
      * reposition
      */
-// Todo: Pass an element, so this works with
-// tutorial pages as well!
-    obj.hint = KorAP.Hint.create();
+    // Todo: Pass an element, so this works with
+    // tutorial pages as well!
+    obj.hint = hintClass.create();
     return obj;
-  };
-
-}(this.KorAP));
+  });
+});
diff --git a/dev/js/src/match.js b/dev/js/src/match.js
index 4e3786b..beb4bca 100644
--- a/dev/js/src/match.js
+++ b/dev/js/src/match.js
@@ -4,46 +4,31 @@
  *
  * @author Nils Diewald
  */
-// require menu.js, dagre
 /*
  * - Highlight (at least mark as bold) the match
  * - Scroll to match vertically per default
  */
-var KorAP = KorAP || {};
-
-(function (KorAP) {
-  "use strict";
-
-  var svgXmlns = "http://www.w3.org/2000/svg";
-
-  // Default log message
-  KorAP.log = KorAP.log || function (type, msg) {
-    console.log(type + ": " + msg);
-  };
+define([
+  'match/infolayer',
+  'match/info',
+  'util'
+], function (infoLayerClass,
+	     infoClass) {
 
   // Localization values
-  var loc   = (KorAP.Locale = KorAP.Locale || {} );
+  var loc   = KorAP.Locale;
   loc.ADDTREE  = loc.ADDTREE  || 'Add tree view';
   loc.SHOWINFO = loc.SHOWINFO || 'Show information';
   loc.CLOSE    = loc.CLOSE    || 'Close';
-
-  KorAP._AvailableRE = new RegExp("^([^\/]+?)\/([^=]+?)(?:=(spans|rels|tokens))?$");
-  KorAP._TermRE      = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
-  KorAP._matchTerms  = ['corpusID', 'docID', 'textID', 'matchID', 'available'];
-
-  // API requests
-  KorAP.API = KorAP.API || {};
-
-  // TODO: Make this async
-  KorAP.API.getMatchInfo = KorAP.API.getMatchInfo || function () {
-    KorAP.log(0, 'KorAP.API.getMatchInfo() not implemented')
-    return {};
-  };
+  
+  // KorAP._AvailableRE = new RegExp("^([^\/]+?)\/([^=]+?)(?:=(spans|rels|tokens))?$");
+  // KorAP._TermRE      = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
+  var _matchTerms  = ['corpusID', 'docID', 'textID', 'matchID', 'available'];
 
   /**
    * Match object
    */
-  KorAP.Match = {
+  return {
 
     /**
      * Create a new annotation object.
@@ -51,7 +36,7 @@
      * Supported types are 'spans', 'tokens' and 'rels'.
      */
     create : function (match) {
-      return Object.create(KorAP.Match)._init(match);
+      return Object.create(this)._init(match);
     },
 
     /**
@@ -87,8 +72,8 @@
       else {
 
 	// Iterate over allowed match terms
-	for (var i in KorAP._matchTerms) {
-	  var term = KorAP._matchTerms[i];
+	for (var i in _matchTerms) {
+	  var term = _matchTerms[i];
 	  if (match[term] !== undefined) {
 	    this[term] = match[term];
 	  }
@@ -97,7 +82,7 @@
 	  }
 	};
       };
-
+      
       this._available = {
 	tokens : [],
 	spans  : [],
@@ -110,7 +95,7 @@
 
 	// Create info layer objects
 	try {
-	  var layer = KorAP.InfoLayer.create(term);
+	  var layer = infoLayerClass.create(term);
 	  this._available[layer.type].push(layer);
 	}
 	catch (e) {
@@ -178,7 +163,7 @@
 	.appendChild(document.createTextNode(loc.CLOSE));
       close.classList.add('close');
       close.setAttribute('title', loc.CLOSE);
-
+      
       // Add info button
       var info = document.createElement('li');
       info.appendChild(document.createElement('span'))
@@ -227,7 +212,7 @@
 
       // Create match info
       if (this._info === undefined)
-	this._info = KorAP.MatchInfo.create(this);
+	this._info = infoClass.create(this);
 
       // There is an element to append
       if (this._element === undefined ||
@@ -251,812 +236,4 @@
       return this._element;
     }
   };
-
-
-
-  /**
-   * Information about a match.
-   */
-  KorAP.MatchInfo = {
-
-    /**
-     * Create new object
-     */
-    create : function (match) {
-      return Object.create(KorAP.MatchInfo)._init(match);
-    },
-
-    /**
-     * Initialize object
-     */
-    _init : function (match) {
-      this._match = match;
-      this.opened = false;
-      return this;
-    },
-
-    /**
-     * Get match object
-     */
-    match : function () {
-      return this._match;
-    },
-
-    toggle : function () {
-      if (this.opened == true) {
-	this._match.element().children[0].removeChild(
-	  this.element()
-	);
-	this.opened = false;
-      }
-      else {
-	// Append element to match
-	this._match.element().children[0].appendChild(
-	  this.element()
-	);
-	this.opened = true;
-      };
-
-      return this.opened;
-    },
-
-
-    /**
-     * Retrieve and parse snippet for table representation
-     */
-    getTable : function (tokens, cb) {
-      var focus = [];
-
-      // Get all tokens
-      if (tokens === undefined) {
-	focus = this._match.getTokens();
-      } 
-
-      // Get only some tokens
-      else {
-
-	// Push newly to focus array
-	for (var i = 0; i < tokens.length; i++) {
-	  var term = tokens[i];
-	  try {
-	    // Create info layer objects
-	    var layer = KorAP.InfoLayer.create(term);
-	    layer.type = "tokens";
-	    focus.push(layer);
-	  }
-	  catch (e) {
-	    continue;
-	  };
-	};
-      };
-
-      // No tokens chosen
-      if (focus.length == 0)
-	cb(null);
-
-      // Get info (may be cached)
-      // TODO: Async
-      KorAP.API.getMatchInfo(
-	this._match,
-	{ 'spans' : false, 'layer' : focus },
-
-	// Callback for retrieval
-	function (matchResponse) {
-	  // Get snippet from match info
-	  if (matchResponse["snippet"] !== undefined) {
-	    this._table = KorAP.MatchTable.create(matchResponse["snippet"]);
-	    cb(this._table);
-	  };
-	}.bind(this)
-      );
-
-/*
-      // Todo: Store the table as a hash of the focus
-      return null;
-*/
-    },
-
-
-    /**
-     * Retrieve and parse snippet for tree representation
-     */
-    getTree : function (foundry, layer, cb) {
-      var focus = [];
-
-      // TODO: Support and cache multiple trees
-
-      KorAP.API.getMatchInfo(
-	this._match, {
-	  'spans' : true,
-	  'foundry' : foundry,
-	  'layer' : layer
-	},
-	function (matchResponse) {
-	  // Get snippet from match info
-	  if (matchResponse["snippet"] !== undefined) {
-	    // Todo: This should be cached somehow
-	    cb(KorAP.MatchTree.create(matchResponse["snippet"]));
-	  }
-	  else {
-	    cb(null);
-	  };
-	}.bind(this)
-      );
-    },
-
-    /**
-     * Destroy this match information view.
-     */
-    destroy : function () {
-
-      // Remove circular reference
-      if (this._treeMenu !== undefined)
-	delete this._treeMenu["info"];
-
-      this._treeMenu.destroy();
-      this._treeMenu = undefined;
-      this._match = undefined;
-
-      // Element destroy
-    },
-
-
-    /**
-     * Add a new tree view to the list
-     */
-    addTree : function (foundry, layer, cb) {
-      var matchtree = document.createElement('div');
-      matchtree.classList.add('matchtree');
-
-      var h6 = matchtree.appendChild(document.createElement('h6'));
-      h6.appendChild(document.createElement('span'))
-	.appendChild(document.createTextNode(foundry));
-      h6.appendChild(document.createElement('span'))
-	.appendChild(document.createTextNode(layer));
-
-      var tree = matchtree.appendChild(
-	document.createElement('div')
-      );
-
-      this._element.insertBefore(matchtree, this._element.lastChild);
-
-      var close = tree.appendChild(document.createElement('em'));
-      close.addEventListener(
-	'click', function (e) {
-	  matchtree.parentNode.removeChild(matchtree);
-	  e.halt();
-	}
-      );
-
-      // Get tree data async
-      this.getTree(foundry, layer, function (treeObj) {
-	// Something went wrong - probably log!!!
-	if (treeObj === null) {
-	  tree.appendChild(document.createTextNode('No data available.'));
-	}
-	else {
-	  tree.appendChild(treeObj.element());
-	  // Reposition the view to the center
-	  // (This may in a future release be a reposition
-	  // to move the root into the center or the actual
-	  // match)
-	  treeObj.center();
-	}
-
-	if (cb !== undefined)
-	  cb(treeObj);
-      });
-    },
-
-    /**
-     * Create match information view.
-     */
-    element : function () {
-
-      if (this._element !== undefined)
-	return this._element;
-
-      // Create info table
-      var info = document.createElement('div');
-      info.classList.add('matchinfo');
-
-      // Append default table
-      var matchtable = document.createElement('div');
-      matchtable.classList.add('matchtable');
-      info.appendChild(matchtable);
-
-      // Create the table asynchronous
-      this.getTable(undefined, function (table) {
-	if (table !== null) {
-	  matchtable.appendChild(table.element());
-	};
-      });
-
-      // Get spans
-      var spanLayers = this._match.getSpans().sort(
-	function (a, b) {
-	  if (a.foundry < b.foundry) {
-	    return -1;
-	  }
-	  else if (a.foundry > b.foundry) {
-	    return 1;
-	  }
-	  else if (a.layer < b.layer) {
-	    return -1;
-	  }
-	  else if (a.layer > b.layer) {
-	    return 1;
-	  };
-	  return 0;
-	});
-
-      var menuList = [];
-
-      // Show tree views
-      for (var i = 0; i < spanLayers.length; i++) {
-	var span = spanLayers[i];
-	
-	// Add foundry/layer to menu list
-	menuList.push([
-	  span.foundry + '/' + span.layer,
-	  span.foundry,
-	  span.layer
-	]);
-      };
-
-      // Create tree menu
-      var treemenu = this.treeMenu(menuList);
-      var span = info.appendChild(document.createElement('p'));
-      span.classList.add('addtree');
-      span.appendChild(document.createTextNode(loc.ADDTREE));
-
-      var treeElement = treemenu.element();
-      span.appendChild(treeElement);
-
-      span.addEventListener('click', function (e) {
-	treemenu.show('');
-	treemenu.focus();
-      });
-      
-      this._element = info;
-
-      return info;
-
-    },
-
-
-    /**
-     * Get tree menu.
-     * There is only one menu rendered
-     * - no matter how many trees exist
-     */
-    treeMenu : function (list) {
-      if (this._treeMenu !== undefined)
-	return this._treeMenu;
-
-      return this._treeMenu = KorAP.MatchTreeMenu.create(this, list);
-    }
-  };
-
-
-
-  /**
-   *
-   * Alternatively pass a string as <tt>base/s=span</tt>
-   *
-   * @param foundry
-   */
-  KorAP.InfoLayer = {
-    create : function (foundry, layer, type) {
-      return Object.create(KorAP.InfoLayer)._init(foundry, layer, type);
-    },
-    _init : function (foundry, layer, type) {
-      if (foundry === undefined)
-	throw new Error("Missing parameters");
-
-      if (layer === undefined) {
-	if (KorAP._AvailableRE.exec(foundry)) {
-	  this.foundry = RegExp.$1;
-	  this.layer = RegExp.$2;
-	  this.type = RegExp.$3;
-	}
-	else {
-	  throw new Error("Missing parameters");
-	};
-      }
-      else {
-	this.foundry = foundry;
-	this.layer = layer;
-	this.type = type;
-      };
-
-      if (this.type === undefined)
-	this.type = 'tokens';
-
-      return this;
-    }
-  };
-
-
-  KorAP.MatchTable = {
-    create : function (snippet) {
-      return Object.create(KorAP.MatchTable)._init(snippet);
-    },
-    _init : function (snippet) {
-      // Create html for traversal
-      var html = document.createElement("div");
-      html.innerHTML = snippet;
-
-      this._pos = 0;
-      this._token = [];
-      this._info = [];
-      this._foundry = {};
-      this._layer = {};
-
-      // Parse the snippet
-      this._parse(html.childNodes);      
-
-      html.innerHTML = '';
-      return this;
-    },
-
-    length : function () {
-      return this._pos;
-    },
-
-    getToken : function (pos) {
-      if (pos === undefined)
-	return this._token;
-      return this._token[pos];
-    },
-
-    getValue : function (pos, foundry, layer) {
-      return this._info[pos][foundry + '/' + layer]
-    },
-
-    getLayerPerFoundry : function (foundry) {
-      return this._foundry[foundry]
-    },
-
-    getFoundryPerLayer : function (layer) {
-      return this._layer[layer];
-    },
-
-    // Parse the snippet
-    _parse : function (children) {
-
-      // Get all children
-      for (var i in children) {
-	var c = children[i];
-
-	// Create object on position unless it exists
-	if (this._info[this._pos] === undefined)
-	  this._info[this._pos] = {};
-
-	// Store at position in foundry/layer as array
-	var found = this._info[this._pos];
-
-	// Element with title
-	if (c.nodeType === 1) {
-	  if (c.getAttribute("title") &&
-	      KorAP._TermRE.exec(c.getAttribute("title"))) {
-
-	    // Fill position with info
-	    var foundry, layer, value;
-	    if (RegExp.$2) {
-	      foundry = RegExp.$1;
-	      layer   = RegExp.$2;
-	    }
-	    else {
-	      foundry = "base";
-	      layer   = RegExp.$1
-	    };
-
-	    value = RegExp.$3;
-
-	    if (found[foundry + "/" + layer] === undefined)
-	      found[foundry + "/" + layer] = [];
-
-	    // Push value to foundry/layer at correct position
-	    found[foundry + "/" + layer].push(RegExp.$3);
-
-	    // Set foundry
-	    if (this._foundry[foundry] === undefined)
-	      this._foundry[foundry] = {};
-	    this._foundry[foundry][layer] = 1;
-
-	    // Set layer
-	    if (this._layer[layer] === undefined)
-	      this._layer[layer] = {};
-	    this._layer[layer][foundry] = 1;
-	  };
-
-	  // depth search
-	  if (c.hasChildNodes())
-	    this._parse(c.childNodes);
-	}
-
-	// Leaf node
-	// store string on position and go to next string
-	else if (c.nodeType === 3) {
-	  if (c.nodeValue.match(/[a-z0-9]/i))
-	    this._token[this._pos++] = c.nodeValue;
-	};
-      };
-
-      delete this._info[this._pos];
-    },
-
-
-    /**
-     * Get HTML table view of annotations.
-     */
-    element : function () {
-      if (this._element !== undefined)
-	return this._element;
-
-      // First the legend table
-      var d = document;
-      var table = d.createElement('table');
-
-      // Single row in head
-      var tr = table.appendChild(d.createElement('thead'))
-	.appendChild(d.createElement('tr'));
-
-      // Add cell to row
-      var addCell = function (type, name) {
-	var c = this.appendChild(d.createElement(type))
-	if (name === undefined)
-	  return c;
-
-	if (name instanceof Array) {
-	  for (var n = 0; n < name.length; n++) {
-	    c.appendChild(d.createTextNode(name[n]));
-	    if (n !== name.length - 1) {
-	      c.appendChild(d.createElement('br'));
-	    };
-	  };
-	}
-	else {
-	  c.appendChild(d.createTextNode(name));
-	};
-      };
-
-      tr.addCell = addCell;
-
-      // Add header information
-      tr.addCell('th', 'Foundry');
-      tr.addCell('th', 'Layer');
-
-      // Add tokens
-      for (var i in this._token) {
-	tr.addCell('th', this.getToken(i));
-      };
-
-      var tbody = table.appendChild(
-	d.createElement('tbody')
-      );
-
-      var foundryList = Object.keys(this._foundry).sort();
-
-      for (var f = 0; f < foundryList.length; f++) {
-	var foundry = foundryList[f];
-	var layerList =
-	  Object.keys(this._foundry[foundry]).sort();
-
-	for (var l = 0; l < layerList.length; l++) {
-	  var layer = layerList[l];
-	  tr = tbody.appendChild(
-	    d.createElement('tr')
-	  );
-	  tr.setAttribute('tabindex', 0);
-	  tr.addCell = addCell;
-
-	  tr.addCell('th', foundry);
-	  tr.addCell('th', layer);
-
-	  for (var v = 0; v < this.length(); v++) {
-	    tr.addCell(
-	      'td',
-	      this.getValue(v, foundry, layer) 
-	    );
-	  };
-	};
-      };
-
-      return this._element = table;
-    }
-  };
-
-
-  /**
-   * Visualize span annotations as a tree using Dagre.
-   */
-  KorAP.MatchTree = {
-
-    create : function (snippet) {
-      return Object.create(KorAP.MatchTree)._init(snippet);
-    },
-
-    nodes : function () {
-      return this._next;
-    },
-
-    _addNode : function (id, obj) {
-      obj["width"] = 55;
-      obj["height"] = 20;
-      this._graph.setNode(id, obj)
-    },
-
-    _addEdge : function (src, target) {
-      this._graph.setEdge(src, target);
-    },
-
-    _init : function (snippet) {
-      this._next = new Number(0);
-
-      // Create html for traversal
-      var html = document.createElement("div");
-      html.innerHTML = snippet;
-      var g = new dagre.graphlib.Graph({
-	"directed" : true	
-      });
-      g.setGraph({
-	"nodesep" : 35,
-	"ranksep" : 15,
-	"marginx" : 40,
-	"marginy" : 10
-      });
-      g.setDefaultEdgeLabel({});
-
-      this._graph = g;
-
-      // This is a new root
-      this._addNode(
-	this._next++,
-	{ "class" : "root" }
-      );
-
-      // Parse nodes from root
-      this._parse(0, html.childNodes);
-
-      // Root node has only one child - remove
-      if (g.outEdges(0).length === 1)
-	g.removeNode(0);
-
-      html = undefined;
-      return this;
-    },
-
-    // Remove foundry and layer for labels
-    _clean : function (title) {
-      return title.replace(KorAP._TermRE, "$3");
-    },
-
-    // Parse the snippet
-    _parse : function (parent, children) {
-      for (var i in children) {
-	var c = children[i];
-
-	// Element node
-	if (c.nodeType == 1) {
-
-	  // Get title from html
-	  if (c.getAttribute("title")) {
-	    var title = this._clean(c.getAttribute("title"));
-
-	    // Add child node
-	    var id = this._next++;
-
-	    this._addNode(id, {
-	      "class" : "middle",
-	      "label" : title
-	    });
-	    this._addEdge(parent, id);
-
-	    // Check for next level
-	    if (c.hasChildNodes())
-	      this._parse(id, c.childNodes);
-	  }
-
-	  // Step further
-	  else if (c.hasChildNodes())
-	    this._parse(parent, c.childNodes);
-	}
-
-	// Text node
-	else if (c.nodeType == 3)
-
-	  if (c.nodeValue.match(/[-a-z0-9]/i)) {
-
-	    // Add child node
-	    var id = this._next++;
-	    this._addNode(id, {
-	      "class" : "leaf",
-	      "label" : c.nodeValue
-	    });
-
-	    this._addEdge(parent, id);
-	  };
-      };
-      return this;
-    },
-
-    /**
-     * Center the viewport of the canvas
-     */
-    center : function () {
-      if (this._element === undefined)
-	return;
-
-      var treeDiv = this._element.parentNode;
-
-      var cWidth = parseFloat(window.getComputedStyle(this._element).width);
-      var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
-      // Reposition:
-      if (cWidth > treeWidth) {
-	var scrollValue = (cWidth - treeWidth) / 2;
-	treeDiv.scrollLeft = scrollValue;
-      };
-    },
-
-    // Get element
-    element : function () {
-      if (this._element !== undefined)
-	return this._element;
-
-      var g = this._graph;
-
-      dagre.layout(g);
-
-      var canvas = document.createElementNS(svgXmlns, 'svg');
-      this._element = canvas;
-
-      canvas.setAttribute('height', g.graph().height);
-      canvas.setAttribute('width', g.graph().width);
-
-      // Create edges
-      g.edges().forEach(
-	function (e) {
-	  var src = g.node(e.v);
-	  var target = g.node(e.w);
-	  var p = document.createElementNS(svgXmlns, 'path');
-	  p.setAttributeNS(null, "d", _line(src, target));
-	  p.classList.add('edge');
-	  canvas.appendChild(p);
-	});
-
-      // Create nodes
-      g.nodes().forEach(
-	function (v) {
-	  v = g.node(v);
-	  var group = document.createElementNS(svgXmlns, 'g');
-	  group.classList.add(v.class);
-
-	  // Add node box
-	  var rect = group.appendChild(document.createElementNS(svgXmlns, 'rect'));
-	  rect.setAttributeNS(null, 'x', v.x - v.width / 2);
-	  rect.setAttributeNS(null, 'y', v.y - v.height / 2);
-	  rect.setAttributeNS(null, 'rx', 5);
-	  rect.setAttributeNS(null, 'ry', 5);
-	  rect.setAttributeNS(null, 'width', v.width);
-	  rect.setAttributeNS(null, 'height', v.height);
-
-	  if (v.class === 'root' && v.label === undefined) {
-	    rect.setAttributeNS(null, 'width', v.height);
-	    rect.setAttributeNS(null, 'x', v.x - v.height / 2);
-	    rect.setAttributeNS(null, 'class', 'empty');
-	  };
-
-	  // Add label
-	  if (v.label !== undefined) {
-	    var text = group.appendChild(document.createElementNS(svgXmlns, 'text'));
-	    text.setAttributeNS(null, 'x', v.x - v.width / 2);
-	    text.setAttributeNS(null, 'y', v.y - v.height / 2);
-	    text.setAttributeNS(
-	      null,
-	      'transform',
-	      'translate(' + v.width/2 + ',' + ((v.height / 2) + 5) + ')'
-	    );
-
-	    var tspan = document.createElementNS(svgXmlns, 'tspan');
-	    tspan.appendChild(document.createTextNode(v.label));
-	    text.appendChild(tspan);
-	  };
-
-	  canvas.appendChild(group);
-	}
-      );
-
-      return this._element;
-    }
-  };
-
-  /**
-   * Menu item for tree view choice.
-   */
-  KorAP.MatchTreeItem = {
-    create : function (params) {
-      return Object.create(KorAP.MenuItem)
-	.upgradeTo(KorAP.MatchTreeItem)._init(params);
-    },
-    content : function (content) {
-      if (arguments.length === 1) {
-	this._content = content;
-      };
-      return this._content;
-    },
-
-    // The foundry attribute
-    foundry : function () {
-      return this._foundry;
-    },
-
-    // The layer attribute
-    layer : function () {
-      return this._layer;
-    },
-
-    // enter or click
-    onclick : function (e) {
-      var menu = this.menu();
-      menu.hide();
-      e.halt();
-      if (menu.info() !== undefined)
-	menu.info().addTree(this._foundry, this._layer);
-    },
-    
-    _init : function (params) {
-      if (params[0] === undefined)
-	throw new Error("Missing parameters");
-
-      this._name    = params[0];
-      this._foundry = params[1];
-      this._layer   = params[2];
-      this._content = document.createTextNode(this._name);
-      this._lcField = ' ' + this.content().textContent.toLowerCase();
-      return this;
-    }
-  };
-
-
-  /**
-   * Menu to choose from for tree views.
-   */
-  KorAP.MatchTreeMenu = {
-    create : function (info, params) {
-      var obj = Object.create(KorAP.Menu)
-	.upgradeTo(KorAP.MatchTreeMenu)
-	._init(KorAP.MatchTreeItem, undefined, params);
-      obj.limit(6);
-
-      obj._info = info;
-
-      // This is only domspecific
-      obj.element().addEventListener('blur', function (e) {
-	this.menu.hide();
-      });
-
-      return obj;
-    },
-    info :function () {
-      return this._info;
-    }
-  };
-
-
-  // Create path for node connections 
-  function _line (src, target) {
-    var x1 = src.x,
-        y1 = src.y,
-        x2 = target.x,
-        y2 = target.y - target.height / 2;
-
-    // c 0,0 -10,0
-    return 'M ' + x1 + ',' + y1 + ' ' + 
-      'C ' + x1 + ',' + y1 + ' ' + 
-      x2 + ',' + (y2 - (y2 - y1) / 2)  + ' ' + 
-      x2 + ',' + y2;
-  };
-
-}(this.KorAP));
+});
diff --git a/dev/js/src/match/info.js b/dev/js/src/match/info.js
new file mode 100644
index 0000000..b24bed5
--- /dev/null
+++ b/dev/js/src/match/info.js
@@ -0,0 +1,290 @@
+  /**
+   * Information about a match.
+   */
+define(['match/infolayer','match/table','match/tree', 'match/treemenu', 'util'], function (infoLayerClass, matchTableClass, matchTreeClass, matchTreeMenuClass) {
+
+  // TODO: Make this async
+  KorAP.API.getMatchInfo = KorAP.API.getMatchInfo || function () {
+    KorAP.log(0, 'KorAP.API.getMatchInfo() not implemented')
+    return {};
+  };
+
+  var loc = KorAP.Locale;
+
+  /**
+   * Create new object
+   */
+  return {
+    create : function (match) {
+      return Object.create(this)._init(match);
+    },
+
+    /**
+     * Initialize object
+     */
+    _init : function (match) {
+      this._match = match;
+      this.opened = false;
+      return this;
+    },
+
+    /**
+     * Get match object
+     */
+    match : function () {
+      return this._match;
+    },
+
+    toggle : function () {
+      if (this.opened == true) {
+	this._match.element().children[0].removeChild(
+	  this.element()
+	);
+	this.opened = false;
+      }
+      else {
+	// Append element to match
+	this._match.element().children[0].appendChild(
+	  this.element()
+	);
+	this.opened = true;
+      };
+      
+      return this.opened;
+    },
+
+
+    /**
+     * Retrieve and parse snippet for table representation
+     */
+    getTable : function (tokens, cb) {
+      var focus = [];
+
+      // Get all tokens
+      if (tokens === undefined) {
+	focus = this._match.getTokens();
+      } 
+
+      // Get only some tokens
+      else {
+
+	// Push newly to focus array
+	for (var i = 0; i < tokens.length; i++) {
+	  var term = tokens[i];
+	  try {
+	    // Create info layer objects
+	    var layer = infoLayerClass.create(term);
+	    layer.type = "tokens";
+	    focus.push(layer);
+	  }
+	  catch (e) {
+	    continue;
+	  };
+	};
+      };
+      
+      // No tokens chosen
+      if (focus.length == 0)
+	cb(null);
+
+      // Get info (may be cached)
+      // TODO: Async
+      KorAP.API.getMatchInfo(
+	this._match,
+	{ 'spans' : false, 'layer' : focus },
+
+	// Callback for retrieval
+	function (matchResponse) {
+	  // Get snippet from match info
+	  if (matchResponse["snippet"] !== undefined) {
+	    this._table = matchTableClass.create(matchResponse["snippet"]);
+	    cb(this._table);
+	  };
+	}.bind(this)
+      );
+
+      /*
+      // Todo: Store the table as a hash of the focus
+      return null;
+      */
+    },
+    
+
+    /**
+     * Retrieve and parse snippet for tree representation
+     */
+    getTree : function (foundry, layer, cb) {
+      var focus = [];
+      
+      // TODO: Support and cache multiple trees
+      KorAP.API.getMatchInfo(
+	this._match, {
+	  'spans' : true,
+	  'foundry' : foundry,
+	  'layer' : layer
+	},
+	function (matchResponse) {
+	  // Get snippet from match info
+	  if (matchResponse["snippet"] !== undefined) {
+	    // Todo: This should be cached somehow
+	    cb(matchTreeClass.create(matchResponse["snippet"]));
+	  }
+	  else {
+	    cb(null);
+	  };
+	}.bind(this)
+      );
+    },
+
+    /**
+     * Destroy this match information view.
+     */
+    destroy : function () {
+
+      // Remove circular reference
+      if (this._treeMenu !== undefined)
+	delete this._treeMenu["info"];
+      
+      this._treeMenu.destroy();
+      this._treeMenu = undefined;
+      this._match = undefined;
+      
+      // Element destroy
+    },
+
+    /**
+     * Add a new tree view to the list
+     */
+    addTree : function (foundry, layer, cb) {
+      var matchtree = document.createElement('div');
+      matchtree.classList.add('matchtree');
+      
+      var h6 = matchtree.appendChild(document.createElement('h6'));
+      h6.appendChild(document.createElement('span'))
+	.appendChild(document.createTextNode(foundry));
+      h6.appendChild(document.createElement('span'))
+	.appendChild(document.createTextNode(layer));
+      
+      var tree = matchtree.appendChild(
+	document.createElement('div')
+      );
+      
+      this._element.insertBefore(matchtree, this._element.lastChild);
+
+      var close = tree.appendChild(document.createElement('em'));
+      close.addEventListener(
+	'click', function (e) {
+	  matchtree.parentNode.removeChild(matchtree);
+	  e.halt();
+	}
+      );
+
+      // Get tree data async
+      this.getTree(foundry, layer, function (treeObj) {
+	// Something went wrong - probably log!!!
+	if (treeObj === null) {
+	  tree.appendChild(document.createTextNode('No data available.'));
+	}
+	else {
+	  tree.appendChild(treeObj.element());
+	  // Reposition the view to the center
+	  // (This may in a future release be a reposition
+	  // to move the root into the center or the actual
+	  // match)
+	  treeObj.center();
+	}
+	
+	if (cb !== undefined)
+	  cb(treeObj);
+      });
+    },
+    
+    /**
+     * Create match information view.
+     */
+    element : function () {
+      
+      if (this._element !== undefined)
+	return this._element;
+      
+      // Create info table
+      var info = document.createElement('div');
+      info.classList.add('matchinfo');
+
+      // Append default table
+      var matchtable = document.createElement('div');
+      matchtable.classList.add('matchtable');
+      info.appendChild(matchtable);
+
+      // Create the table asynchronous
+      this.getTable(undefined, function (table) {
+	if (table !== null) {
+	  matchtable.appendChild(table.element());
+	};
+      });
+
+      // Get spans
+      var spanLayers = this._match.getSpans().sort(
+	function (a, b) {
+	  if (a.foundry < b.foundry) {
+	    return -1;
+	  }
+	  else if (a.foundry > b.foundry) {
+	    return 1;
+	  }
+	  else if (a.layer < b.layer) {
+	    return -1;
+	  }
+	  else if (a.layer > b.layer) {
+	    return 1;
+	  };
+	  return 0;
+	});
+      
+      var menuList = [];
+      
+      // Show tree views
+      for (var i = 0; i < spanLayers.length; i++) {
+	var span = spanLayers[i];
+	
+	// Add foundry/layer to menu list
+	menuList.push([
+	  span.foundry + '/' + span.layer,
+	  span.foundry,
+	  span.layer
+	]);
+      };
+
+      // Create tree menu
+      var treemenu = this.treeMenu(menuList);
+      var span = info.appendChild(document.createElement('p'));
+      span.classList.add('addtree');
+      span.appendChild(document.createTextNode(loc.ADDTREE));
+
+      var treeElement = treemenu.element();
+      span.appendChild(treeElement);
+
+      span.addEventListener('click', function (e) {
+	treemenu.show('');
+	treemenu.focus();
+      });
+      
+      this._element = info;
+
+      return info;
+    },
+
+    
+    /**
+     * Get tree menu.
+     * There is only one menu rendered
+     * - no matter how many trees exist
+     */
+    treeMenu : function (list) {
+      if (this._treeMenu !== undefined)
+	return this._treeMenu;
+      
+      return this._treeMenu = matchTreeMenuClass.create(this, list);
+    }
+  };
+});
diff --git a/dev/js/src/match/infolayer.js b/dev/js/src/match/infolayer.js
new file mode 100644
index 0000000..dbd93f9
--- /dev/null
+++ b/dev/js/src/match/infolayer.js
@@ -0,0 +1,41 @@
+/**
+ *
+ * Alternatively pass a string as <tt>base/s=span</tt>
+ *
+ * @param foundry
+ */
+define(function () {
+  var _AvailableRE = new RegExp("^([^\/]+?)\/([^=]+?)(?:=(spans|rels|tokens))?$");
+
+  return {
+    create : function (foundry, layer, type) {
+      return Object.create(this)._init(foundry, layer, type);
+    },
+    _init : function (foundry, layer, type) {
+      if (foundry === undefined)
+	throw new Error("Missing parameters");
+      
+      if (layer === undefined) {
+	if (_AvailableRE.exec(foundry)) {
+	  this.foundry = RegExp.$1;
+	  this.layer = RegExp.$2;
+	  this.type = RegExp.$3;
+	}
+	else {
+	  throw new Error("Missing parameters");
+	};
+      }
+      else {
+	this.foundry = foundry;
+	this.layer = layer;
+	this.type = type;
+      };
+      
+      if (this.type === undefined)
+	this.type = 'tokens';
+
+      return this;
+    }
+  };
+});
+
diff --git a/dev/js/src/match/table.js b/dev/js/src/match/table.js
new file mode 100644
index 0000000..7eba8a0
--- /dev/null
+++ b/dev/js/src/match/table.js
@@ -0,0 +1,193 @@
+define(function () {
+  var _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
+  
+  return {
+    create : function (snippet) {
+      return Object.create(this)._init(snippet);
+    },
+    _init : function (snippet) {
+      // Create html for traversal
+      var html = document.createElement("div");
+      html.innerHTML = snippet;
+      
+      this._pos = 0;
+      this._token = [];
+      this._info = [];
+      this._foundry = {};
+      this._layer = {};
+    
+      // Parse the snippet
+      this._parse(html.childNodes);      
+    
+      html.innerHTML = '';
+      return this;
+    },
+    
+    length : function () {
+      return this._pos;
+    },
+
+    getToken : function (pos) {
+      if (pos === undefined)
+	return this._token;
+      return this._token[pos];
+    },
+    
+    getValue : function (pos, foundry, layer) {
+      return this._info[pos][foundry + '/' + layer]
+    },
+    
+    getLayerPerFoundry : function (foundry) {
+      return this._foundry[foundry]
+    },
+    
+    getFoundryPerLayer : function (layer) {
+      return this._layer[layer];
+    },
+
+    // Parse the snippet
+    _parse : function (children) {
+
+      // Get all children
+      for (var i in children) {
+	var c = children[i];
+
+	// Create object on position unless it exists
+	if (this._info[this._pos] === undefined)
+	  this._info[this._pos] = {};
+
+	// Store at position in foundry/layer as array
+	var found = this._info[this._pos];
+
+	// Element with title
+	if (c.nodeType === 1) {
+	  if (c.getAttribute("title") &&
+	      _TermRE.exec(c.getAttribute("title"))) {
+
+	    // Fill position with info
+	    var foundry, layer, value;
+	    if (RegExp.$2) {
+	      foundry = RegExp.$1;
+	      layer   = RegExp.$2;
+	    }
+	    else {
+	      foundry = "base";
+	      layer   = RegExp.$1
+	    };
+
+	    value = RegExp.$3;
+	    
+	    if (found[foundry + "/" + layer] === undefined)
+	      found[foundry + "/" + layer] = [];
+
+	    // Push value to foundry/layer at correct position
+	    found[foundry + "/" + layer].push(RegExp.$3);
+
+	    // Set foundry
+	    if (this._foundry[foundry] === undefined)
+	      this._foundry[foundry] = {};
+	    this._foundry[foundry][layer] = 1;
+
+	    // Set layer
+	    if (this._layer[layer] === undefined)
+	      this._layer[layer] = {};
+	    this._layer[layer][foundry] = 1;
+	  };
+
+	  // depth search
+	  if (c.hasChildNodes())
+	    this._parse(c.childNodes);
+	}
+
+	// Leaf node
+	// store string on position and go to next string
+	else if (c.nodeType === 3) {
+	  if (c.nodeValue.match(/[a-z0-9]/i))
+	    this._token[this._pos++] = c.nodeValue;
+	};
+      };
+
+      delete this._info[this._pos];
+    },
+
+
+    /**
+     * Get HTML table view of annotations.
+     */
+    element : function () {
+      if (this._element !== undefined)
+	return this._element;
+
+      // First the legend table
+      var d = document;
+      var table = d.createElement('table');
+
+      // Single row in head
+      var tr = table.appendChild(d.createElement('thead'))
+	.appendChild(d.createElement('tr'));
+
+      // Add cell to row
+      var addCell = function (type, name) {
+	var c = this.appendChild(d.createElement(type))
+	if (name === undefined)
+	  return c;
+
+	if (name instanceof Array) {
+	  for (var n = 0; n < name.length; n++) {
+	    c.appendChild(d.createTextNode(name[n]));
+	    if (n !== name.length - 1) {
+	      c.appendChild(d.createElement('br'));
+	    };
+	  };
+	}
+	else {
+	  c.appendChild(d.createTextNode(name));
+	};
+      };
+
+      tr.addCell = addCell;
+
+      // Add header information
+      tr.addCell('th', 'Foundry');
+      tr.addCell('th', 'Layer');
+
+      // Add tokens
+      for (var i in this._token) {
+	tr.addCell('th', this.getToken(i));
+      };
+
+      var tbody = table.appendChild(
+	d.createElement('tbody')
+      );
+
+      var foundryList = Object.keys(this._foundry).sort();
+
+      for (var f = 0; f < foundryList.length; f++) {
+	var foundry = foundryList[f];
+	var layerList =
+	  Object.keys(this._foundry[foundry]).sort();
+
+	for (var l = 0; l < layerList.length; l++) {
+	  var layer = layerList[l];
+	  tr = tbody.appendChild(
+	    d.createElement('tr')
+	  );
+	  tr.setAttribute('tabindex', 0);
+	  tr.addCell = addCell;
+
+	  tr.addCell('th', foundry);
+	  tr.addCell('th', layer);
+
+	  for (var v = 0; v < this.length(); v++) {
+	    tr.addCell(
+	      'td',
+	      this.getValue(v, foundry, layer) 
+	    );
+	  };
+	};
+      };
+
+      return this._element = table;
+    }
+  };
+});
diff --git a/dev/js/src/match/tree.js b/dev/js/src/match/tree.js
new file mode 100644
index 0000000..b379b37
--- /dev/null
+++ b/dev/js/src/match/tree.js
@@ -0,0 +1,222 @@
+/**
+ * Visualize span annotations as a tree using Dagre.
+ */
+define(['lib/dagre'], function (dagre) {
+  "use strict";
+
+  var svgXmlns = "http://www.w3.org/2000/svg";
+  var _TermRE = new RegExp("^(?:([^\/]+?)\/)?([^:]+?):(.+?)$");
+
+  // Create path for node connections 
+  function _line (src, target) {
+    var x1 = src.x,
+        y1 = src.y,
+        x2 = target.x,
+        y2 = target.y - target.height / 2;
+
+    // c 0,0 -10,0
+    return 'M ' + x1 + ',' + y1 + ' ' + 
+      'C ' + x1 + ',' + y1 + ' ' + 
+      x2 + ',' + (y2 - (y2 - y1) / 2)  + ' ' + 
+      x2 + ',' + y2;
+  };
+
+  return {
+    create : function (snippet) {
+      return Object.create(this)._init(snippet);
+    },
+
+    nodes : function () {
+      return this._next;
+    },
+
+    _addNode : function (id, obj) {
+      obj["width"] = 55;
+      obj["height"] = 20;
+      this._graph.setNode(id, obj)
+    },
+    
+    _addEdge : function (src, target) {
+      this._graph.setEdge(src, target);
+    },
+    
+    _init : function (snippet) {
+      this._next = new Number(0);
+
+      // Create html for traversal
+      var html = document.createElement("div");
+      html.innerHTML = snippet;
+      var g = new dagre.graphlib.Graph({
+	"directed" : true	
+      });
+      g.setGraph({
+	"nodesep" : 35,
+	"ranksep" : 15,
+	"marginx" : 40,
+	"marginy" : 10
+      });
+      g.setDefaultEdgeLabel({});
+
+      this._graph = g;
+      
+      // This is a new root
+      this._addNode(
+	this._next++,
+	{ "class" : "root" }
+      );
+      
+      // Parse nodes from root
+      this._parse(0, html.childNodes);
+
+      // Root node has only one child - remove
+      if (g.outEdges(0).length === 1)
+	g.removeNode(0);
+
+      html = undefined;
+      return this;
+    },
+
+    // Remove foundry and layer for labels
+    _clean : function (title) {
+      return title.replace(_TermRE, "$3");
+    },
+
+    // Parse the snippet
+    _parse : function (parent, children) {
+      for (var i in children) {
+	var c = children[i];
+
+	// Element node
+	if (c.nodeType == 1) {
+
+	  // Get title from html
+	  if (c.getAttribute("title")) {
+	    var title = this._clean(c.getAttribute("title"));
+
+	    // Add child node
+	    var id = this._next++;
+
+	    this._addNode(id, {
+	      "class" : "middle",
+	      "label" : title
+	    });
+	    this._addEdge(parent, id);
+
+	    // Check for next level
+	    if (c.hasChildNodes())
+	      this._parse(id, c.childNodes);
+	  }
+
+	  // Step further
+	  else if (c.hasChildNodes())
+	    this._parse(parent, c.childNodes);
+	}
+
+	// Text node
+	else if (c.nodeType == 3)
+
+	  if (c.nodeValue.match(/[-a-z0-9]/i)) {
+
+	    // Add child node
+	    var id = this._next++;
+	    this._addNode(id, {
+	      "class" : "leaf",
+	      "label" : c.nodeValue
+	    });
+
+	    this._addEdge(parent, id);
+	  };
+      };
+      return this;
+    },
+
+    /**
+     * Center the viewport of the canvas
+     */
+    center : function () {
+      if (this._element === undefined)
+	return;
+
+      var treeDiv = this._element.parentNode;
+
+      var cWidth = parseFloat(window.getComputedStyle(this._element).width);
+      var treeWidth = parseFloat(window.getComputedStyle(treeDiv).width);
+      // Reposition:
+      if (cWidth > treeWidth) {
+	var scrollValue = (cWidth - treeWidth) / 2;
+	treeDiv.scrollLeft = scrollValue;
+      };
+    },
+
+    // Get element
+    element : function () {
+      if (this._element !== undefined)
+	return this._element;
+
+      var g = this._graph;
+
+      dagre.layout(g);
+
+      var canvas = document.createElementNS(svgXmlns, 'svg');
+      this._element = canvas;
+
+      canvas.setAttribute('height', g.graph().height);
+      canvas.setAttribute('width', g.graph().width);
+
+      // Create edges
+      g.edges().forEach(
+	function (e) {
+	  var src = g.node(e.v);
+	  var target = g.node(e.w);
+	  var p = document.createElementNS(svgXmlns, 'path');
+	  p.setAttributeNS(null, "d", _line(src, target));
+	  p.classList.add('edge');
+	  canvas.appendChild(p);
+	});
+
+      // Create nodes
+      g.nodes().forEach(
+	function (v) {
+	  v = g.node(v);
+	  var group = document.createElementNS(svgXmlns, 'g');
+	  group.classList.add(v.class);
+
+	  // Add node box
+	  var rect = group.appendChild(document.createElementNS(svgXmlns, 'rect'));
+	  rect.setAttributeNS(null, 'x', v.x - v.width / 2);
+	  rect.setAttributeNS(null, 'y', v.y - v.height / 2);
+	  rect.setAttributeNS(null, 'rx', 5);
+	  rect.setAttributeNS(null, 'ry', 5);
+	  rect.setAttributeNS(null, 'width', v.width);
+	  rect.setAttributeNS(null, 'height', v.height);
+
+	  if (v.class === 'root' && v.label === undefined) {
+	    rect.setAttributeNS(null, 'width', v.height);
+	    rect.setAttributeNS(null, 'x', v.x - v.height / 2);
+	    rect.setAttributeNS(null, 'class', 'empty');
+	  };
+
+	  // Add label
+	  if (v.label !== undefined) {
+	    var text = group.appendChild(document.createElementNS(svgXmlns, 'text'));
+	    text.setAttributeNS(null, 'x', v.x - v.width / 2);
+	    text.setAttributeNS(null, 'y', v.y - v.height / 2);
+	    text.setAttributeNS(
+	      null,
+	      'transform',
+	      'translate(' + v.width/2 + ',' + ((v.height / 2) + 5) + ')'
+	    );
+
+	    var tspan = document.createElementNS(svgXmlns, 'tspan');
+	    tspan.appendChild(document.createTextNode(v.label));
+	    text.appendChild(tspan);
+	  };
+
+	  canvas.appendChild(group);
+	}
+      );
+
+      return this._element;
+    }
+  };
+});
diff --git a/dev/js/src/match/treeitem.js b/dev/js/src/match/treeitem.js
new file mode 100644
index 0000000..f096861
--- /dev/null
+++ b/dev/js/src/match/treeitem.js
@@ -0,0 +1,49 @@
+define(['menu/item'], function (itemClass) {
+  /**
+   * Menu item for tree view choice.
+   */
+
+  return {
+    create : function (params) {
+      return Object.create(itemClass)
+	.upgradeTo(this)._init(params);
+    },
+    content : function (content) {
+      if (arguments.length === 1) {
+	this._content = content;
+      };
+      return this._content;
+    },
+    
+    // The foundry attribute
+    foundry : function () {
+      return this._foundry;
+    },
+
+    // The layer attribute
+    layer : function () {
+      return this._layer;
+    },
+
+    // enter or click
+    onclick : function (e) {
+      var menu = this.menu();
+      menu.hide();
+      e.halt();
+      if (menu.info() !== undefined)
+	menu.info().addTree(this._foundry, this._layer);
+    },
+    
+    _init : function (params) {
+      if (params[0] === undefined)
+	throw new Error("Missing parameters");
+
+      this._name    = params[0];
+      this._foundry = params[1];
+      this._layer   = params[2];
+      this._content = document.createTextNode(this._name);
+      this._lcField = ' ' + this.content().textContent.toLowerCase();
+      return this;
+    }
+  };
+});
diff --git a/dev/js/src/match/treemenu.js b/dev/js/src/match/treemenu.js
new file mode 100644
index 0000000..23341a4
--- /dev/null
+++ b/dev/js/src/match/treemenu.js
@@ -0,0 +1,26 @@
+  /**
+   * Menu to choose from for tree views.
+   */
+define(['menu', 'match/treeitem'], function (menuClass, itemClass) {
+  "use strict";
+
+  return {
+    create : function (info, params) {
+      var obj = Object.create(menuClass)
+	.upgradeTo(this)
+	._init(itemClass, undefined, params);
+      obj.limit(6);
+      obj._info = info;
+
+      // This is only domspecific
+      obj.element().addEventListener('blur', function (e) {
+	this.menu.hide();
+      });
+      
+      return obj;
+    },
+    info :function () {
+      return this._info;
+    }
+  };
+});
diff --git a/dev/js/src/menu.js b/dev/js/src/menu.js
index 78f9ea7..0d5e6ac 100644
--- a/dev/js/src/menu.js
+++ b/dev/js/src/menu.js
@@ -1,35 +1,35 @@
-var KorAP = KorAP || {};
-
 /**
  * Create scrollable drop-down menus.
  *
  * @author Nils Diewald
  */
-
 /*
-  * TODO: space is not a valid prefix!
+ * TODO: space is not a valid prefix!
  */
-(function (KorAP) {
-  "use strict";
+define([
+  'menu/item',
+  'menu/prefix',
+  'util'
+], function (defaultItemClass,
+	     defaultPrefixClass) {
 
-  // Don't let events bubble up
-  if (Event.halt === undefined) {
-    // Don't let events bubble up
-    Event.prototype.halt = function () {
-      this.stopPropagation();
-      this.preventDefault();
-    };
+  // Todo: This may not be necessary
+  // Default maximum number of menu items
+  var menuLimit = 8;
+
+  function _codeFromEvent (e) {
+    if (e.charCode && (e.keyCode == 0))
+      return e.charCode
+    return e.keyCode;
   };
 
-  // Default maximum number of menu items
-  KorAP.menuLimit = 8;
 
   /**
    * List of items for drop down menu (complete).
    * Only a sublist of the menu is filtered (live).
    * Only a sublist of the filtered menu is visible (shown).
    */
-  KorAP.Menu = {
+  return {
     /**
      * Create new Menu based on the action prefix
      * and a list of menu items.
@@ -40,7 +40,7 @@
      * @param {Array.<Array.<string>>} List of menu items
      */
     create : function (params) {
-      return Object.create(KorAP.Menu)._init(params);
+      return Object.create(this)._init(params);
     },
 
     /**
@@ -143,12 +143,12 @@
     // Initialize list
     _init : function (itemClass, prefixClass, params) {
       var that = this;
-      this._itemClass = itemClass;
+      this._itemClass = itemClass || defaultItemClass;
 
       if (prefixClass !== undefined)
 	this._prefix = prefixClass.create();
       else
-	this._prefix = KorAP.MenuPrefix.create();
+	this._prefix = defaultPrefixClass.create();
 
       this._prefix._menu = this;
 
@@ -188,14 +188,14 @@
 
       // Initialize item list based on parameters
       for (i in params) {
-	var obj = itemClass.create(params[i]);
+	var obj = this._itemClass.create(params[i]);
 
 	// This may become circular
 	obj["_menu"] = this;
 
 	this._items.push(obj);
       };
-      this._limit    = KorAP.menuLimit;
+      this._limit    = menuLimit;
       this._position = 0;  // position in the active list
       this._active   = -1; // active item in the item list
       this._reset();
@@ -654,318 +654,4 @@
       this._element.removeChild(this._element.lastChild);
     }
   };
-
-
-  /**
-   * Item in the Dropdown menu
-   */
-  KorAP.MenuItem = {
-
-    /**
-     * Create a new MenuItem object.
-     *
-     * @constructor
-     * @this {MenuItem}
-     * @param {Array.<string>} An array object of name, action and
-     *   optionally a description
-     */
-    create : function (params) {
-      return Object.create(KorAP.MenuItem)._init(params);
-    },
-
-    /**
-     * Upgrade this object to another object,
-     * while private data stays intact.
-     *
-     * @param {Object] An object with properties.
-     */
-    upgradeTo : function (props) {
-      for (var prop in props) {
-	this[prop] = props[prop];
-      };
-      return this;
-    },
-
-    content : function (content) {
-      if (arguments.length === 1)
-	this._content = document.createTextNode(content);
-      return this._content;
-    },
-
-    lcField : function () {
-      return this._lcField;
-    },
-
-    action : function (action) {
-      if (arguments.length === 1)
-	this._action = action;
-      return this._action;
-    },
-
-    /**
-     * Check or set if the item is active
-     *
-     * @param {boolean|null} State of activity
-     */
-    active : function (bool) {
-      var cl = this.element().classList;
-      if (bool === undefined)
-	return cl.contains("active");
-      else if (bool)
-	cl.add("active");
-      else
-	cl.remove("active");
-    },
-
-    /**
-     * Check or set if the item is
-     * at the boundary of the menu
-     * list
-     *
-     * @param {boolean|null} State of activity
-     */
-    noMore : function (bool) {
-      var cl = this.element().classList;
-      if (bool === undefined)
-	return cl.contains("no-more");
-      else if (bool)
-	cl.add("no-more");
-      else
-	cl.remove("no-more");
-    },
-
-    /**
-     * Get the document element of the menu item
-     */
-    element : function () {
-      // already defined
-      if (this._element !== undefined)
-	return this._element;
-
-      // Create list item
-      var li = document.createElement("li");
-
-      // Connect action
-      if (this["onclick"] !== undefined) {
-	li["onclick"] = this.onclick.bind(this);
-      };
-
-      // Append template
-      li.appendChild(this.content());
-
-      return this._element = li;
-    },
-
-    /**
-     * Highlight parts of the item
-     *
-     * @param {string} Prefix string for highlights
-     */
-    highlight : function (prefix) {
-      var children = this.element().childNodes;
-      for (var i = children.length -1; i >= 0; i--) {
-	this._highlight(children[i], prefix);
-      };
-    },
-
-    // Highlight a certain substring of the menu item
-    _highlight : function (elem, prefix) {
-
-      if (elem.nodeType === 3) {
-
-	var text   = elem.nodeValue;
-	var textlc = text.toLowerCase();
-	var pos    = textlc.indexOf(prefix);
-	if (pos >= 0) {
-
-	  // First element
-	  if (pos > 0) {
-	    elem.parentNode.insertBefore(
-	      document.createTextNode(text.substr(0, pos)),
-	      elem
-	    );
-	  };
-
-	  // Second element
-	  var hl = document.createElement("mark");
-	  hl.appendChild(
-	    document.createTextNode(text.substr(pos, prefix.length))
-	  );
-	  elem.parentNode.insertBefore(hl, elem);
-
-	  // Third element
-	  var third = text.substr(pos + prefix.length);
-	  if (third.length > 0) {
-	    var thirdE = document.createTextNode(third);
-	    elem.parentNode.insertBefore(
-	      thirdE,
-	      elem
-	    );
-	    this._highlight(thirdE, prefix);
-	  };
-
-	  var p = elem.parentNode;
-	  p.removeChild(elem);
-	};
-      }
-      else {
-	var children = elem.childNodes;
-	for (var i = children.length -1; i >= 0; i--) {
-	  this._highlight(children[i], prefix);
-	};
-      };
-    },
-
-
-    /**
-     * Remove highlight of the menu item
-     */
-    lowlight : function () {
-      var e = this.element();
-
-      var marks = e.getElementsByTagName("mark");
-      for (var i = marks.length - 1; i >= 0; i--) {
-	// Create text node clone
-	var x = document.createTextNode(
-	  marks[i].firstChild.nodeValue
-	);
-
-	// Replace with content
-	marks[i].parentNode.replaceChild(
-	  x,
-	  marks[i]
-	);
-      };
-
-      // Remove consecutive textnodes
-      e.normalize();
-    },
-
-    // Initialize menu item
-    _init : function (params) {
-
-      if (params[0] === undefined)
-	throw new Error("Missing parameters");
-
-      this.content(params[0]);
-
-      if (params.length === 2)
-	this._action = params[1];
-
-      this._lcField = ' ' + this.content().textContent.toLowerCase();
-
-      return this;
-    },
-
-    /**
-     * Return menu list.
-     */
-    menu : function () {
-      return this._menu;
-    }
-  };
-
-  KorAP.MenuPrefix = {
-    create : function (params) {
-      return Object.create(KorAP.MenuPrefix)._init();
-    },
-    _init : function () {
-      this._string = '';
-
-      // Add prefix span
-      this._element = document.createElement('span');
-      this._element.classList.add('pref');
-      // Connect action
-
-      if (this["onclick"] !== undefined)
-	this._element["onclick"] = this.onclick.bind(this);
-
-      return this;
-    },
-    _update : function () {
-      this._element.innerHTML
-      = this._string;
-    },
-
-    /**
-     * Upgrade this object to another object,
-     * while private data stays intact.
-     *
-     * @param {Object} An object with properties.
-     */
-    upgradeTo : function (props) {
-      for (var prop in props) {
-	this[prop] = props[prop];
-      };
-      return this;
-    },
-
-    active : function (bool) {
-      var cl = this.element().classList;
-      if (bool === undefined)
-	return cl.contains("active");
-      else if (bool)
-	cl.add("active");
-      else
-	cl.remove("active");
-    },
-
-    element : function () {
-      return this._element;
-    },
-
-    isSet : function () {
-      return this._string.length > 0 ?
-	true : false;
-    },
-
-    value : function (string) {
-      if (arguments.length === 1) {
-	this._string = string;
-	this._update();
-      };
-      return this._string;
-    },
-
-    add : function (string) {
-      this._string += string;
-      this._update();
-    },
-
-    onclick : function () {},
-
-    backspace : function () {
-      if (this._string.length > 1) {
-	this._string = this._string.substring(
-	  0, this._string.length - 1
-	);
-      }
-      else {
-	this._string = '';
-      };
-
-      this._update();
-    },
-
-    /**
-     * Return menu list.
-     */
-    menu : function () {
-      return this._menu;
-    }
-  };
-
-  function _codeFromEvent (e) {
-    if (e.charCode && (e.keyCode == 0))
-      return e.charCode
-    return e.keyCode;
-  };
-
-}(this.KorAP));
-
-/**
- * MenuItems may define:
- *
- * onclick: action happen on click and enter.
- * further: action happen on right arrow
- */
+});
diff --git a/dev/js/src/menu/item.js b/dev/js/src/menu/item.js
new file mode 100644
index 0000000..b80aa2a
--- /dev/null
+++ b/dev/js/src/menu/item.js
@@ -0,0 +1,213 @@
+/*
+ * MenuItems may define:
+ *
+ * onclick: action happen on click and enter.
+ * further: action happen on right arrow
+ */
+
+/**
+ * Item in the Dropdown menu
+ */
+define({
+  /**
+   * Create a new MenuItem object.
+   *
+   * @constructor
+   * @this {MenuItem}
+   * @param {Array.<string>} An array object of name, action and
+   *   optionally a description
+   */
+  create : function (params) {
+    return Object.create(this)._init(params);
+  },
+
+  /**
+   * Upgrade this object to another object,
+   * while private data stays intact.
+   *
+   * @param {Object] An object with properties.
+   */
+  upgradeTo : function (props) {
+    for (var prop in props) {
+      this[prop] = props[prop];
+    };
+    return this;
+  },
+
+  content : function (content) {
+    if (arguments.length === 1)
+      this._content = document.createTextNode(content);
+    return this._content;
+  },
+
+  lcField : function () {
+    return this._lcField;
+  },
+
+  action : function (action) {
+    if (arguments.length === 1)
+      this._action = action;
+    return this._action;
+  },
+
+  /**
+   * Check or set if the item is active
+   *
+   * @param {boolean|null} State of activity
+   */
+  active : function (bool) {
+    var cl = this.element().classList;
+    if (bool === undefined)
+      return cl.contains("active");
+    else if (bool)
+      cl.add("active");
+    else
+      cl.remove("active");
+  },
+
+  /**
+   * Check or set if the item is
+   * at the boundary of the menu
+   * list
+   *
+   * @param {boolean|null} State of activity
+   */
+  noMore : function (bool) {
+    var cl = this.element().classList;
+    if (bool === undefined)
+      return cl.contains("no-more");
+    else if (bool)
+      cl.add("no-more");
+    else
+      cl.remove("no-more");
+  },
+  
+  /**
+   * Get the document element of the menu item
+   */
+  element : function () {
+    // already defined
+    if (this._element !== undefined)
+      return this._element;
+    
+    // Create list item
+    var li = document.createElement("li");
+
+    // Connect action
+    if (this["onclick"] !== undefined) {
+      li["onclick"] = this.onclick.bind(this);
+    };
+
+    // Append template
+    li.appendChild(this.content());
+    
+    return this._element = li;
+  },
+
+  /**
+   * Highlight parts of the item
+   *
+   * @param {string} Prefix string for highlights
+   */
+  highlight : function (prefix) {
+    var children = this.element().childNodes;
+    for (var i = children.length -1; i >= 0; i--) {
+      this._highlight(children[i], prefix);
+    };
+  },
+
+  // Highlight a certain substring of the menu item
+  _highlight : function (elem, prefix) {
+    
+    if (elem.nodeType === 3) {
+      
+      var text   = elem.nodeValue;
+      var textlc = text.toLowerCase();
+      var pos    = textlc.indexOf(prefix);
+      if (pos >= 0) {
+	
+	// First element
+	if (pos > 0) {
+	  elem.parentNode.insertBefore(
+	    document.createTextNode(text.substr(0, pos)),
+	    elem
+	  );
+	};
+	
+	// Second element
+	var hl = document.createElement("mark");
+	hl.appendChild(
+	  document.createTextNode(text.substr(pos, prefix.length))
+	);
+	elem.parentNode.insertBefore(hl, elem);
+	
+	// Third element
+	var third = text.substr(pos + prefix.length);
+	if (third.length > 0) {
+	  var thirdE = document.createTextNode(third);
+	  elem.parentNode.insertBefore(
+	    thirdE,
+	    elem
+	  );
+	  this._highlight(thirdE, prefix);
+	};
+	
+	var p = elem.parentNode;
+	p.removeChild(elem);
+      };
+    }
+    else {
+      var children = elem.childNodes;
+      for (var i = children.length -1; i >= 0; i--) {
+	this._highlight(children[i], prefix);
+      };
+    };
+  },
+
+  /**
+   * Remove highlight of the menu item
+   */
+  lowlight : function () {
+    var e = this.element();
+    
+    var marks = e.getElementsByTagName("mark");
+    for (var i = marks.length - 1; i >= 0; i--) {
+      // Create text node clone
+      var x = document.createTextNode(
+	marks[i].firstChild.nodeValue
+      );
+      
+      // Replace with content
+      marks[i].parentNode.replaceChild(
+	x,
+	marks[i]
+      );
+    };
+
+    // Remove consecutive textnodes
+    e.normalize();
+  },
+
+  // Initialize menu item
+  _init : function (params) {
+    
+    if (params[0] === undefined)
+      throw new Error("Missing parameters");
+
+    this.content(params[0]);
+    
+    if (params.length === 2)
+      this._action = params[1];
+
+    this._lcField = ' ' + this.content().textContent.toLowerCase();
+
+    return this;
+  },
+
+  /**
+   * Return menu list.
+   */
+  menu : function () {
+    return this._menu;
+  }
+});
diff --git a/dev/js/src/menu/prefix.js b/dev/js/src/menu/prefix.js
new file mode 100644
index 0000000..a4e428f
--- /dev/null
+++ b/dev/js/src/menu/prefix.js
@@ -0,0 +1,89 @@
+define({
+  create : function (params) {
+    return Object.create(this)._init();
+  },
+  _init : function () {
+    this._string = '';
+
+    // Add prefix span
+    this._element = document.createElement('span');
+    this._element.classList.add('pref');
+    // Connect action
+
+    if (this["onclick"] !== undefined)
+      this._element["onclick"] = this.onclick.bind(this);
+    
+    return this;
+  },
+  _update : function () {
+    this._element.innerHTML
+      = this._string;
+  },
+
+  /**
+   * Upgrade this object to another object,
+   * while private data stays intact.
+   *
+   * @param {Object} An object with properties.
+   */
+  upgradeTo : function (props) {
+    for (var prop in props) {
+      this[prop] = props[prop];
+    };
+    return this;
+  },
+
+  active : function (bool) {
+    var cl = this.element().classList;
+    if (bool === undefined)
+      return cl.contains("active");
+    else if (bool)
+      cl.add("active");
+    else
+      cl.remove("active");
+  },
+
+  element : function () {
+    return this._element;
+  },
+
+  isSet : function () {
+    return this._string.length > 0 ?
+      true : false;
+  },
+
+  value : function (string) {
+    if (arguments.length === 1) {
+      this._string = string;
+      this._update();
+    };
+    return this._string;
+  },
+  
+  add : function (string) {
+    this._string += string;
+    this._update();
+  },
+
+  onclick : function () {},
+
+  backspace : function () {
+    if (this._string.length > 1) {
+      this._string = this._string.substring(
+	0, this._string.length - 1
+      );
+    }
+    else {
+      this._string = '';
+    };
+    
+    this._update();
+  },
+
+  /**
+   * Return menu list.
+   */
+  menu : function () {
+    return this._menu;
+  }
+});
diff --git a/dev/js/src/session.js b/dev/js/src/session.js
index a10ce81..f81eb8f 100644
--- a/dev/js/src/session.js
+++ b/dev/js/src/session.js
@@ -4,77 +4,68 @@
  *
  * @author Nils Diewald
  */
-var KorAP = KorAP || {};
+define({
+  /**
+   * Create a new session.
+   * Expects a name or defaults to 'korap'
+   */
+  create : function (name) {
+    var obj = Object.create(this);
+    if (name === undefined)
+      name = 'korap';
+    obj._name = name.toLowerCase();
+    obj._hash = {};
+    obj._parse();
+    return obj;
+  },
 
-(function (KorAP) {
-  "use strict";
+  /**
+   * Get a value based on a key.
+   * The value can be complex, as the value is stored as JSON.
+   */
+  get : function (key) {
+    return this._hash[key.toLowerCase()];
+  },
 
-  
-  KorAP.Session = {
+  /**
+   * Set a value based on a key.
+   * The value can be complex, as the value is stored as JSON.
+   */
+  set : function (key, value) {
+    this._hash[key] = value;
+    this._store();
+  },
 
-    /**
-     * Create a new session.
-     * Expects a name or defaults to 'korap'
-     */
-    create : function (name) {
-      var obj = Object.create(KorAP.Session);
-      if (name === undefined)
-	name = 'korap';
-      obj._name = name.toLowerCase();
-      obj._hash = {};
-      obj._parse();
-      return obj;
-    },
+  /**
+   * Clears the session by removing the cookie
+   */
+  clear : function () {
+    document.cookie = this._name + '=; expires=-1';
+  },
 
-    /**
-     * Get a value based on a key.
-     * The value can be complex, as the value is stored as JSON.
-     */
-    get : function (key) {
-      return this._hash[key.toLowerCase()];
-    },
+  /* Store cookie */
+  _store : function () {
+    /*
+      var date = new Date();
+      date.setYear(date.getFullYear() + 1);
+    */
+    document.cookie =
+      this._name + '=' + encodeURIComponent(JSON.stringify(this._hash)) + ';';
+  },
 
-    /**
-     * Set a value based on a key.
-     * The value can be complex, as the value is stored as JSON.
-     */
-    set : function (key, value) {
-      this._hash[key] = value;
-      this._store();
-    },
-
-    /**
-     * Clears the session by removing the cookie
-     */
-    clear : function () {
-      document.cookie = this._name + '=; expires=-1';
-    },
-
-    /* Store cookie */
-    _store : function () {
-      /*
-	var date = new Date();
-	date.setYear(date.getFullYear() + 1);
-      */
-      document.cookie =
-	this._name + '=' + encodeURIComponent(JSON.stringify(this._hash)) + ';';
-    },
-
-    /* Parse cookie */
-    _parse : function () {
-      var c = document.cookie;
-      var part = document.cookie.split(';');
-      for(var i = 0; i < part.length; i++) {
-        var pair = part[i].split('=');
-        var name = pair[0].trim().toLowerCase();
-	if (name === this._name) {
-	  if (pair.length === 1 || pair[1].length === 0)
-	    return;
-          this._hash = JSON.parse(decodeURIComponent(pair[1]));
+  /* Parse cookie */
+  _parse : function () {
+    var c = document.cookie;
+    var part = document.cookie.split(';');
+    for(var i = 0; i < part.length; i++) {
+      var pair = part[i].split('=');
+      var name = pair[0].trim().toLowerCase();
+      if (name === this._name) {
+	if (pair.length === 1 || pair[1].length === 0)
 	  return;
-	};
+        this._hash = JSON.parse(decodeURIComponent(pair[1]));
+	return;
       };
-    }
+    };
   }
-
-}(this.KorAP));
+});
diff --git a/dev/js/src/tutorial.js b/dev/js/src/tutorial.js
index 03cf903..dae5fbf 100644
--- a/dev/js/src/tutorial.js
+++ b/dev/js/src/tutorial.js
@@ -2,31 +2,30 @@
  * Open and close a tutorial page.
  * The current page is stored and retrieved in a session cookie.
  */
-// Requires session.js
-var KorAP = KorAP || {};
-
 // Todo: add query mechanism!
 
-(function (KorAP) {
+define(['session', 'util'], function (sessionClass) {
   "use strict";
 
   // Localization values
-  var loc   = (KorAP.Locale = KorAP.Locale || {} );
+  var loc   = KorAP.Locale;
   loc.CLOSE = loc.CLOSE || 'Close';
 
-  KorAP.Tutorial = {
-
+  return {
     /**
      * Create new tutorial object.
      * Accepts an element to bind the tutorial window to.
      */
     create : function (obj) {
-      return Object.create(KorAP.Tutorial)._init(obj);
+      if (!obj)
+	return null;
+      return Object.create(this)._init(obj);
     },
 
     // Initialize Tutorial object
     _init : function (obj) {
-      this._session = KorAP.Session.create();
+
+      this._session = sessionClass.create();
       this._show = obj;
       this.start = obj.getAttribute('href');
       obj.removeAttribute('href');
@@ -58,9 +57,6 @@
 	var ul = document.createElement('ul');
 	ul.classList.add('action', 'right');
 
-	// Use localization
-	var loc = KorAP.Locale;
-
 	// Add close button
 	var close = document.createElement('li');
 	close.appendChild(document.createElement('span'))
@@ -80,7 +76,7 @@
 	  info.classList.add('info');
 	  info.setAttribute('title', loc.SHOWINFO);
 	*/
-
+	
 	ul.appendChild(close);
 
 	element.appendChild(ul);
@@ -129,5 +125,5 @@
     getPage : function () {
       this._session.get('tutpage');
     },
-  }
-}(this.KorAP));
+  };
+});
diff --git a/dev/js/src/util.js b/dev/js/src/util.js
new file mode 100644
index 0000000..d183c36
--- /dev/null
+++ b/dev/js/src/util.js
@@ -0,0 +1,45 @@
+var KorAP = KorAP || {};
+
+// TODO: Make this part of util!
+// Don't let events bubble up
+if (Event.halt === undefined) {
+  // Don't let events bubble up
+  Event.prototype.halt = function () {
+    this.stopPropagation();
+    this.preventDefault();
+  };
+};
+
+// Add toggleClass method similar to jquery
+HTMLElement.prototype.toggleClass = function (c1, c2) {
+  var cl = this.classList;
+  if (cl.contains(c1)) {
+    cl.add(c2);
+    cl.remove(c1);
+  }
+  else {
+    cl.remove(c2);
+    cl.add(c1);
+  };
+};
+
+
+// Utility for removing all children of a node
+function _removeChildren (node) {
+  // Remove everything underneath
+  while (node.firstChild)
+    node.removeChild(node.firstChild);
+};
+
+
+define(function () {
+  KorAP.API = KorAP.API || {};
+  KorAP.Locale = KorAP.Locale || {};
+
+  // Default log message
+  KorAP.log = KorAP.log || function (type, msg) {
+    console.log(type + ": " + msg);
+  };
+
+  return KorAP;
+});
diff --git a/dev/js/src/vc.js b/dev/js/src/vc.js
index 2c8ad01..48c5694 100644
--- a/dev/js/src/vc.js
+++ b/dev/js/src/vc.js
@@ -6,9 +6,6 @@
 /*
  * Replaces a previous version written by Mengfei Zhou
  */
-var KorAP = KorAP || {};
-
-// Requires menu.js
 
 /*
   TODO: Implement a working localization solution!
@@ -33,131 +30,60 @@
   815: "Rewrite expects source"
 */
 
-(function (KorAP) {
+define([
+  'vc/unspecified',
+  'vc/doc',
+  'vc/docgroup',
+  'util'
+], function (unspecDocClass, docClass, docGroupClass) {
   "use strict";
 
-  // Default log message
-  KorAP.log = KorAP.log || function (type, msg) {
-    console.log(type + ": " + msg);
-  };
-
   KorAP._validStringMatchRE = new RegExp("^(?:eq|ne|contains|excludes)$");
-  KorAP._validRegexMatchRE  = new RegExp("^(?:eq|ne)$");
+  // KorAP._validRegexMatchRE  = new RegExp("^(?:eq|ne)$");
   KorAP._validDateMatchRE   = new RegExp("^[lg]?eq$");
   KorAP._validDateRE        = new RegExp("^(?:\\d{4})(?:-\\d\\d(?:-\\d\\d)?)?$");
-  KorAP._validGroupOpRE     = new RegExp("^(?:and|or)$");
-  KorAP._validRewriteOpRE   = new RegExp("^(?:injec|modifica)tion$");
-  KorAP._quote              = new RegExp("([\"\\\\])", 'g');
+  // KorAP._validGroupOpRE     = new RegExp("^(?:and|or)$");
+  // KorAP._quote              = new RegExp("([\"\\\\])", 'g');
 
   // Localization values
   var loc   = (KorAP.Locale = KorAP.Locale || {} );
-  loc.AND   = loc.AND   || 'and';
-  loc.OR    = loc.OR    || 'or';
-  loc.DEL   = loc.DEL   || '×';
-  loc.EMPTY = loc.EMPTY || '⋯'
-
-  // Utility for analysing boolean values
-  function _bool (bool) {
-    return (bool === undefined || bool === null || bool === false) ? false : true;
-  };
-
-
-  // Utility for removing all children of a node
-  function _removeChildren (node) {
-    // Remove everything underneath
-    while (node.firstChild)
-      node.removeChild(node.firstChild);
-  };
-
-
-  // Add new unspecified document
-  KorAP._add = function (obj, type) {
-    var ref = obj.parentNode.refTo;
-    var parent = ref.parent();
-
-    if (ref.ldType() === 'docGroup') {
-
-      // Check that the action differs from the type
-      if (ref.operation() === type)
-	return;
-
-      if (parent.ldType() !== null) {
-	return parent.newAfter(ref);
-      }
-      else {
-	// The group is on root - wrap
-	return ref.wrapOnRoot();
-      };
-    }
-    else if (ref.ldType() === 'doc') {
-
-      if (parent.ldType() === null) {
-	return ref.wrapOnRoot(type);
-      }
-      else if (parent.operation() === type) {
-	return parent.newAfter(ref);
-      }
-      else {
-	return ref.wrap(type);
-      };
-    };
-  };
-
-
-  // Add doc with 'and' relation
-  KorAP._and = function () {
-    return KorAP._add(this, 'and');
-  };
-
-
-  // Add doc with 'or' relation
-  KorAP._or = function () {
-    return KorAP._add(this, 'or');
-  };
-
-
-  // Remove doc or docGroup
-  KorAP._delete = function () {
-    var ref = this.parentNode.refTo;
-    if (ref.parent().ldType() !== null) {
-      return ref.parent().delOperand(ref).update();
-    }
-    else {
-      ref.parent().clean();
-    };
-  };
-
+  /*
+    loc.AND   = loc.AND   || 'and';
+    loc.OR    = loc.OR    || 'or';
+    loc.DEL   = loc.DEL   || '×';
+    loc.EMPTY = loc.EMPTY || '⋯'
+  */
 
   /**
    * Virtual Collection
    */
-  KorAP.VirtualCollection = {
+  return {
     ldType : function () {
       return null;
     },
 
     create : function () {
-      return Object.create(KorAP.VirtualCollection);
+      return Object.create(this);
     },
 
     clean : function () {
       if (this._root.ldType() !== "non") {
 	this._root.destroy();
-	this.root(KorAP.UnspecifiedDoc.create(this));
+	this.root(unspecDocClass.create(this));
       };
       return this;
     },
 
     render : function (json) {
-      var obj = Object.create(KorAP.VirtualCollection);
+      var obj = Object.create(this);
 
       if (json !== undefined) {
 	// Root object
 	if (json['@type'] == 'koral:doc') {
-	  obj._root = KorAP.Doc.create(obj, json);
+	  obj._root = docClass.create(obj, json);
 	}
 	else if (json['@type'] == 'koral:docGroup') {
-	  obj._root = KorAP.DocGroup.create(obj, json);
+	  obj._root = docGroupClass.create(obj, json);
 	}
 	else {
 	  KorAP.log(813, "Collection type is not supported");
@@ -225,1174 +151,4 @@
       return this._root.toQuery();
     }
   };
-
-
-  /**
-   * Operators for criteria
-   */
-  KorAP.Operators = {
-    create : function (and, or, del) {
-      var op = Object.create(KorAP.Operators);
-      op.and(and);
-      op.or(or);
-      op.del(del);
-      return op;
-    },
-
-    update : function () {
-      // Init the element
-      if (this._element === undefined)
-	return this.element();
-
-      var op = this._element;
-
-      op.refTo = this.parent();
-
-      // Remove everything underneath
-      _removeChildren(op);
-
-      // Add and button
-      if (this._and === true) {
-	var andE = document.createElement('span');
-	andE.setAttribute('class', 'and');
-	andE.addEventListener('click', KorAP._and, false);
-	andE.appendChild(
-	  document.createTextNode(KorAP.Locale.AND)
-	);
-	op.appendChild(andE);
-      };
-
-      // Add or button
-      if (this._or === true) {
-	var orE = document.createElement('span');
-	orE.setAttribute('class', 'or');
-	orE.addEventListener('click', KorAP._or, false);
-	orE.appendChild(document.createTextNode(KorAP.Locale.OR));
-	op.appendChild(orE);
-      };
-
-      // Add delete button
-      if (this._del === true) {
-	var delE = document.createElement('span');
-	delE.setAttribute('class', 'delete');
-	delE.appendChild(document.createTextNode(KorAP.Locale.DEL));
-	delE.addEventListener('click', KorAP._delete, false);
-	op.appendChild(delE);
-      };
-
-      return op;
-    },
-
-    // Be aware! This may be cyclic
-    parent : function (obj) {
-      if (arguments.length === 1)
-	this._parent = obj;
-      return this._parent;
-    },
-
-    element : function () {
-
-      // Return existing element
-      if (this._element !== undefined)
-	return this._element;
-
-      this._element = document.createElement('div');
-      this._element.setAttribute('class', 'operators');
-
-      // Init elements
-      this.update();
-      return this._element;
-    },
-
-    and : function (bool) {
-      if (arguments.length === 1)
-	this._and = _bool(bool);
-      return this._and;
-    },
-
-    or : function (bool) {
-      if (arguments.length === 1)
-	this._or = _bool(bool);
-      return this._or;
-    },
-
-    del : function (bool) {
-      if (arguments.length === 1)
-	this._del = _bool(bool);
-      return this._del;
-    }
-  };
-
-
-  /**
-   * Unspecified criterion
-   */
-  KorAP.UnspecifiedDoc = {
-    _ldType : "non",
-    create : function (parent) {
-      var obj = Object.create(KorAP.JsonLD).
-	upgradeTo(KorAP.UnspecifiedDoc);
-
-      if (parent !== undefined)
-	obj._parent = parent;
-
-      return obj;
-    },
-
-    // Set key - replace
-    key : function (v) {
-
-      // Not replaceable
-      if (this._parent === undefined)
-	return null;
-
-      // Set JSON-LD type
-      var newDoc = KorAP.Doc.create(this._parent, {
-	"@type" : "koral:doc",
-	"value" : "",
-	"key"   : v
-      });
-
-      // Unspecified document on root
-      if (this._parent.ldType() === null) {
-	this._parent.root(newDoc);
-	this.destroy();
-      }
-
-      // Unspecified document in group
-      else {
-	this._parent.replaceOperand(this, newDoc);
-      };
-      this._parent.update();
-      return newDoc;
-    },
-
-    update : function () {
-
-      if (this._element === undefined)
-	return this.element();
-
-      // Remove element content
-      _removeChildren(this._element);
-
-      var ellipsis = document.createElement('span');
-      ellipsis.appendChild(document.createTextNode(loc.EMPTY));
-      this._element.appendChild(ellipsis);
-
-      // Set ref - TODO: Cleanup!
-      this._element.refTo = this;
-
-      // Set operators
-      if (this._parent !== undefined && this.parent().ldType() !== null) {
-	var op = this.operators(
-	  false,
-	  false,
-	  true
-	);
-
-	this._element.appendChild(
-	  op.element()
-	);
-      };
-
-      return this.element();
-    },
-
-    element : function () {
-      if (this._element !== undefined)
-	return this._element;
-      this._element = document.createElement('div');
-      this._element.setAttribute('class', 'doc unspecified');
-      this.update();
-      return this._element;
-    },
-
-    
-  };
-
-
-  /**
-   * Document criterion
-   */
-  KorAP.Doc = {
-    _ldType : "doc",
-    _obj : function () { return KorAP.Doc },
-
-    create : function (parent, json) {
-      var obj = Object(KorAP.JsonLD).
-	create().
-	upgradeTo(KorAP.Doc).
-	fromJson(json);
-
-      if (parent !== undefined)
-	obj._parent = parent;
-
-      obj.__changed = true;
-      return obj;
-    },
-
-    update : function () {
-      if (this._element === undefined)
-	return this.element();
-
-      // Get element
-      var e = this._element;
-
-      // Set ref - TODO: Cleanup!
-      e.refTo = this;
-
-      // Check if there is a change
-      if (this.__changed) {
-
-	// Was rewritten
-	if (this.rewrites() !== undefined) {
-	  e.classList.add("rewritten");
-	};
-
-	// Added key
-	var key = document.createElement('span');
-	key.setAttribute('class', 'key');
-
-	// Change key
-	key.addEventListener('click', KorAP._changeKey, false);
-
-	if (this.key())
-	  key.appendChild(document.createTextNode(this.key()));
-
-	// Added match operator
-	var matchop = document.createElement('span');
-	matchop.setAttribute('data-type', this.type());
-	matchop.setAttribute('class', 'match');
-	matchop.appendChild(
-	  document.createTextNode(this.matchop())
-	);
-
-	// Added match operator
-	var value = document.createElement('span');
-	value.setAttribute('data-type', this.type());
-	value.setAttribute('class', 'value');
-	if (this.value())
-	  value.appendChild(
-	    document.createTextNode(this.value())
-	  );
-
-	// Remove all element children
-	_removeChildren(e);
-
-	// Add spans
-	e.appendChild(key);
-	e.appendChild(matchop);
-	e.appendChild(value);
-
-	this.__changed = false;
-      };
-
-      if (this._rewrites !== undefined) {
-	e.appendChild(this._rewrites.element());
-      };
-
-      if (this._parent !== undefined) {
-	// Set operators
-	var op = this.operators(
-	  true,
-	  true,
-	  true
-	);
-
-	// Append new operators
-	e.appendChild(op.element());
-      };
-
-      return e;
-    },
-
-    element : function () {
-      if (this._element !== undefined)
-	return this._element;
-
-      this._element = document.createElement('div');
-      this._element.setAttribute('class', 'doc');
-
-      this.update();
-      return this._element;
-    },
-
-    // Wrap a new operation around the doc element
-    wrap : function (op) {
-      var parent = this.parent();
-      var group = KorAP.DocGroup.create(parent);
-      group.operation(op);
-      group.append(this);
-      group.append();
-      return parent.replaceOperand(this, group).update();
-    },
-
-    // Deserialize from json
-    fromJson : function (json) {
-      if (json === undefined)
-	return this;
-
-      if (json["@type"] === undefined) {
-	KorAP.log(701, "JSON-LD group has no @type attribute");
-	return;
-      };
-
-      if (json["value"] === undefined ||
-	  typeof json["value"] != 'string') {
-	KorAP.log(805, "Value is invalid");
-	return;
-      };
-
-      // There is a defined key
-      if (json["key"] !== undefined &&
-	  typeof json["key"] === 'string') {
-
-	// Set key
-	this.key(json["key"]);
-
-	// Set match operation
-	if (json["match"] !== undefined) {
-	  if (typeof json["match"] === 'string') {
-	    this.matchop(json["match"]);
-	  }
-	  else {
-	    KorAP.log(802, "Match type is not supported by value type");
-	    return;
-	  };
-	};
-
-	// Key is a string
-	if (json["type"] === undefined ||
-	    json["type"] == "type:string") {
-	  this.type("string");
-
-	  // Check match type
-	  if (!KorAP._validStringMatchRE.test(this.matchop())) {
-	    KorAP.log(802, "Match type is not supported by value type");
-	    return;
-	  };
-
-	  // Set string value
-	  this.value(json["value"]);
-	}
-
-	// Key is a date
-	else if (json["type"] === "type:date") {
-	  this.type("date");
-
-	  if (json["value"] !== undefined &&
-	      KorAP._validDateRE.test(json["value"])) {
-
-	    if (!KorAP._validDateMatchRE.test(this.matchop())) {
-	      KorAP.log(802, "Match type is not supported by value type");
-	      return;
-	    };
-
-	    // Set value
-	    this.value(json["value"]);
-	  }
-	  else {
-	    KorAP.log(806, "Value is not a valid date string");
-	    return;
-	  };
-	}
-
-	// Key is a regular expression
-	else if (json["type"] === "type:regex") {
-	  this.type("regex");
-
-	  try {
-
-	    // Try to create a regular expression
-	    var check = new RegExp(json["value"]);
-
-	    if (!KorAP._validRegexMatchRE.test(this.matchop())) {
-	      KorAP.log(802, "Match type is not supported by value type");
-	      return;
-	    };
-
-	    this.value(json["value"]);
-	  }
-
-	  catch (e) {
-	    KorAP.log(807, "Value is not a valid regular expression");
-	    return;
-	  };
-	  this.type("regex");
-	}
-
-	else {
-	  KorAP.log(804, "Unknown value type");
-	  return;
-	};
-
-      };
-
-      if (json["rewrites"] !== undefined) {
-	this._rewrites = KorAP.RewriteList.create(json["rewrites"]);
-      };
-	
-      return this;
-    },
-
-    key : function (value) {
-      if (arguments.length === 1) {
-	this._key = value;
-	this._changed();
-	return this;
-      };
-      return this._key;
-    },
-
-    matchop : function (match) {
-      if (arguments.length === 1) {
-	this._matchop = match.replace(/^match:/, '');
-	this._changed();
-	return this;
-      };
-      return this._matchop || "eq";
-    },
-
-    type : function (type) {
-      if (arguments.length === 1) {
-	this._type = type;
-	this._changed();
-	return this;
-      };
-      return this._type || "string";
-    },
-
-    value : function (value) {
-      if (arguments.length === 1) {
-	this._value = value;
-	this._changed();
-	return this;
-      };
-      return this._value;
-    },
-
-    rewrites : function () {
-      return this._rewrites;
-    },
-
-    _changed : function () {
-      this.__changed = true;
-
-      if (this._rewrites === undefined)
-	return;
-      delete this["_rewrites"];
-      if (this._element === undefined)
-	return;
-      this._element.classList.remove("rewritten");
-    },
-
-    toJson : function () {
-      if (!this.matchop() || !this.key())
-	return {};
-      
-      return {
-	"@type" : "koral:" + this.ldType(),
-	"key"   : this.key(),
-	"match" : "match:" + this.matchop(),
-	"value" : this.value() || '',
-	"type"  : "type:" + this.type()
-      };
-    },
-
-    toQuery : function () {
-      if (!this.matchop() || !this.key())
-	return "";
-
-      // Build doc string based on key
-      var string = this.key() + ' ';
-
-      // Add match operator
-      switch (this.matchop()) {
-      case "ne":
-	string += '!=';
-	break;
-      case "contains":
-	string += '~';
-	break;
-      case "excludes":
-	string += '!~';
-	break;
-      case "geq":
-	string += 'since';
-	break;
-      case "leq":
-	string += 'until';
-	break;
-      default:
-	string += (this.type() == 'date') ? 'in' : '=';
-	break;
-      };
-
-      string += ' ';
-
-      // Add value
-      switch (this.type()) {
-      case "date":
-	return string + this.value();
-	break;
-      case "regex":
-	return string + '/' + this.value() + '/';
-	break;
-      case "string":
-	return string + '"' + this.value().replace(KorAP._quote, '\\$1') + '"';
-	break;
-      };
-
-      return "";
-    }
-  };
-
-
-  /**
-   * Document group criterion
-   */
-  KorAP.DocGroup = {
-    _ldType : "docGroup",
-
-    create : function (parent, json) {
-      var obj = Object.create(KorAP.JsonLD).upgradeTo(KorAP.DocGroup);
-      obj._operands = [];
-      obj.fromJson(json);
-      if (parent !== undefined)
-	obj._parent = parent;
-      return obj;
-    },
-
-    newAfter : function (obj) {
-      for (var i = 0; i < this._operands.length; i++) {
-	if (this._operands[i] === obj) {
-	  var operand = KorAP.UnspecifiedDoc.create(this);
-	  this._operands.splice(i + 1, 0, operand);
-	  return this.update();
-	};
-      };
-    },
-
-    // The doc is already set in the group
-    _duplicate : function (operand) {
-      if (operand.ldType() !== 'doc')
-	return null;
-
-      for (var i = 0; i < this._operands.length; i++) {
-	var op = this.getOperand(i);
-	if (op.ldType() === 'doc'
-	    && operand.key() === op.key()
-	    && operand.matchop() === op.matchop()
-	    && operand.value() === op.value()) {
-	  return op;
-	};
-      };
-      return null;
-    },
-
-    append : function (operand) {
-
-      // Append unspecified object
-      if (operand === undefined) {
-
-	// Be aware of cyclic structures!
-	operand = KorAP.UnspecifiedDoc.create(this);
-	this._operands.push(operand);
-	return operand;
-      };
-
-      switch (operand["@type"]) {
-
-      case undefined:
-	// No @type defined
-	if (operand["ldType"] !== undefined) {
-	  if (operand.ldType() !== 'doc' &&
-	      operand.ldType() !== 'docGroup') {
-	    KorAP.log(812, "Operand not supported in document group");
-	    return;
-	  };
-	  // Be aware of cyclic structures!
-	  operand.parent(this);
-
-	  var dupl = this._duplicate(operand);
-	  if (dupl === null) {
-	    this._operands.push(operand);
-	    return operand;
-	  };
-	  return dupl;
-	};
-
-	KorAP.log(701, "JSON-LD group has no @type attribute");
-	return;
-
-      case "koral:doc":
-	// Be aware of cyclic structures!
-	var doc = KorAP.Doc.create(this, operand);
-	if (doc === undefined)
-	  return;
-	var dupl = this._duplicate(doc);
-	if (dupl === null) {
-	  this._operands.push(doc);
-	  return doc;
-	};
-	return dupl;
-
-      case "koral:docGroup":
-	// Be aware of cyclic structures!
-	var docGroup = KorAP.DocGroup.create(this, operand);
-	if (docGroup === undefined)
-	  return;
-
-	// Flatten group
-	if (docGroup.operation() === this.operation()) {
-	  for (var op in docGroup.operands()) {
-	    op = docGroup.getOperand(op);
-	    var dupl = this._duplicate(op);
-	    if (dupl === null) {
-	      this._operands.push(op);
-	      op.parent(this);
-	    };
-	  };
-	  docGroup._operands = [];
-	  docGroup.destroy();
-	  return this;
-	};
-	this._operands.push(docGroup);
-	return docGroup;
-
-      default:
-	KorAP.log(812, "Operand not supported in document group");
-	return;
-      };
-    },
-
-    update : function () {
-      // There is only one operand in group
-
-      if (this._operands.length === 1) {
-
-	var parent = this.parent();
-	var op = this.getOperand(0);
-
-	// This will prevent destruction of
-	// the operand
-	this._operands = [];
-
-	// Parent is a group
-	if (parent.ldType() !== null)
-	  return parent.replaceOperand(this, op).update();
-
-	// Parent is vc
-	else {
-	  this.destroy();
-	  // Cyclic madness
-	  parent.root(op);
-	  op.parent(parent);
-	  return parent.root();
-	};
-      };
-
-      if (this._element === undefined)
-	return this;
-
-      var group = this._element;
-      group.setAttribute('data-operation', this.operation());
-
-      _removeChildren(group);
-
-      // Append operands
-      for (var i = 0; i < this._operands.length; i++) {
-	group.appendChild(
-	  this.getOperand(i).element()
-	);
-      };
-
-      // Set operators
-      var op = this.operators(
-	this.operation() == 'and' ? false : true,
-	this.operation() == 'or'  ? false : true,
-	true
-      );
-
-      group.appendChild(
-	op.element()
-      );
-
-      return this;
-    },
-
-    element : function () {
-      if (this._element !== undefined)
-	return this._element;
-
-      this._element = document.createElement('div');
-      this._element.setAttribute('class', 'docGroup');
-
-      // Update the object - including optimization
-      this.update();
-
-      return this._element;
-    },
-
-    operation : function (op) {
-      if (arguments.length === 1) {
-	if (KorAP._validGroupOpRE.test(op)) {
-	  this._op = op;
-	}
-	else {
-	  KorAP.log(810, "Unknown operation type");
-	  return;
-	};
-      };
-      return this._op || 'and';
-    },
-
-    operands : function () {
-      return this._operands;
-    },
-
-    getOperand : function (index) {
-      return this._operands[index];
-    },
-
-    // Replace operand
-    replaceOperand : function (oldOp, newOp) {
-
-      for (var i = 0; i < this._operands.length; i++) {
-	if (this._operands[i] === oldOp) {
-
-	  // Just insert a doc or ...
-	  if (newOp.ldType() === "doc" ||
-	      newOp.ldType() === "non" ||
-	      // ... insert a group of a different operation
-	      // (i.e. "and" in "or"/"or" in "and")
-	      newOp.operation() != this.operation()) {
-	    this._operands[i] = newOp;
-	    newOp.parent(this);
-	  }
-
-	  // Flatten group
-	  else {
-	    // Remove old group
-	    this._operands.splice(i, 1);
-
-	    // Inject new operands
-	    for (var op in newOp.operands().reverse()) {
-	      op = newOp.getOperand(op);
-	      this._operands.splice(i, 0, op);
-	      op.parent(this);
-	    };
-	    // Prevent destruction of operands
-	    newOp._operands = [];
-	    newOp.destroy();
-	  };
-	  oldOp.destroy();
-	  return this;
-	}
-      };
-      return false;
-    },
-
-    // Delete operand from group
-    delOperand : function (obj) {
-      for (var i = 0; i < this._operands.length; i++) {
-	if (this._operands[i] === obj) {
-
-	  // Delete identified operand
-	  this._operands.splice(i,1);
-
-	  // Destroy object for cyclic references
-	  obj.destroy();
-
-	  return this;
-	};
-      };
-
-      // Operand not found
-      return undefined;
-    },
-
-    // Deserialize from json
-    fromJson : function (json) {
-      if (json === undefined)
-	return this;
-
-      if (json["@type"] === undefined) {
-	KorAP.log(701, "JSON-LD group has no @type attribute");
-	return;
-      };
-
-      if (json["operation"] === undefined ||
-	  typeof json["operation"] !== 'string') {
-	KorAP.log(811, "Document group expects operation");
-	return;
-      };
-
-      var operation = json["operation"];
-
-      this.operation(operation.replace(/^operation:/,''));
-
-      if (json["operands"] === undefined ||
-	  !(json["operands"] instanceof Array)) {
-	KorAP.log(704, "Operation needs operand list")
-	return;
-      };
-
-      // Add all documents
-      for (var i in json["operands"]) {
-	var operand = json["operands"][i];
-	this.append(operand);
-      };
-    
-      return this;
-    },
-
-    toJson : function () {
-      var opArray = new Array();
-      for (var i = 0; i < this._operands.length; i++) {
-	if (this._operands[i].ldType() !== 'non')
-	  opArray.push(this._operands[i].toJson());
-      };
-      return {
-	"@type"     : "koral:" + this.ldType(),
-	"operation" : "operation:" + this.operation(),
-	"operands"  : opArray
-      };
-    },
-
-    toQuery : function (brackets) {
-      var list = this._operands
-	.filter(function (op) {
-	  return op.ldType() !== 'non';
-	})
-	.map(function (op) {
-	  return (op.ldType() === 'docGroup') ?
-	    op.toQuery(true) :
-	    op.toQuery();
-	});
-
-      if (list.length === 1)
-	return list.join('');
-      else {
-	var str = list.join(this.operation() === 'or' ? ' | ' : ' & ');
-	return brackets ? '(' + str + ')' : str;
-      };
-    }
-  };
-
-
-  KorAP.RewriteList = {
-    // Construction method
-    create : function (json) {
-      var obj = Object(KorAP.JsonLD).
-	create().
-	upgradeTo(KorAP.RewriteList).
-	fromJson(json);
-      return obj;
-    },
-    fromJson : function (json) {
-      this._list = new Array();
-      for (var i = 0; i < json.length; i++) {
-	this._list.push(
-	  KorAP.Rewrite.create(json[i])
-	);
-      };
-      return this;
-    },
-    element : function () {
-      if (this._element !== undefined)
-	return this._element;
-
-      this._element = document.createElement('div');
-      this._element.setAttribute('class', 'rewrite');
-      for (var x in this._list) {
-	var rewrite = this._list[x];
-	var span = document.createElement('span');
-
-	// Set class attribute
-	span.setAttribute('class', rewrite.operation());
-
-	// Append source information
-	span.appendChild(document.createTextNode(rewrite.src()));
-
-	// Append scope information
-	if (rewrite.scope() !== undefined) {
-	  span.appendChild(
-	    document.createTextNode(
-	      ': ' + rewrite.scope()
-	    )
-	  );
-	};
-	this._element.appendChild(span);
-      };
-      return this._element;
-    }
-  };
-
-
-  /**
-   * Implementation of rewrite objects.
-   */
-  KorAP.Rewrite = {
-
-    // Construction method
-    create : function (json) {
-      var obj = Object(KorAP.JsonLD).
-	create().
-	upgradeTo(KorAP.Rewrite).
-	fromJson(json);
-      return obj;
-    },
-
-    // Get or set source
-    src : function (string) {
-      if (arguments.length === 1)
-	this._src = string;
-      return this._src;
-    },
-    
-    // Get or set operation
-    operation : function (op) {
-      if (arguments.length === 1) {
-	if (KorAP._validRewriteOpRE.test(op)) {
-	  this._op = op;
-	}
-	else {
-	  KorAP.log(814, "Unknown rewrite operation");
-	  return;
-	};
-      };
-      return this._op || 'injection';
-    },
-
-    // Get or set scope
-    scope : function (attr) {
-      if (arguments.length === 1)
-	this._scope = attr;
-      return this._scope;
-    },
-
-    // Serialize from Json
-    fromJson : function (json) {
-      if (json === undefined)
-	return this;
-
-      // Missing @type
-      if (json["@type"] === undefined) {
-	KorAP.log(701, "JSON-LD group has no @type attribute");
-	return;
-      };
-
-      // Missing source
-      if (json["src"] === undefined ||
-	 typeof json["src"] !== 'string') {
-	KorAP.log(815, "Rewrite expects source");
-	return;
-      };
-
-      // Set source
-      this.src(json["src"]);
-
-      // Set operation
-      if (json["operation"] !== undefined) {
-	var operation = json["operation"];
-	this.operation(operation.replace(/^operation:/,''));
-      };
-
-      // Set scope
-      if (json["scope"] !== undefined &&
-	  typeof json["scope"] === 'string')
-	this.scope(json["scope"]);
-
-      return this;
-    },
-
-    toString : function () {
-      var str = '';
-      var op = this.operation();
-      str += op.charAt(0).toUpperCase() + op.slice(1);
-      str += ' of ' + (
-	this._scope === null ?
-	  'object' :
-	  '"' +
-	  this.scope().replace(KorAP._quote, '\\$1') +
-	  '"'
-      );
-      str += ' by ' +
-	'"' +
-	this.src().replace(KorAP._quote, '\\$1') +
-	'"';
-      return str;
-    }
-  };
-
-
-  /**
-   * Abstract JsonLD criterion object
-   */
-  KorAP.JsonLD = {
-    __changed : false,
-
-    create : function () {
-      return Object.create(KorAP.JsonLD);
-    },
-
-    /**
-     * Upgrade this object to another object
-     * while private data stays intact
-     */
-    upgradeTo : function (props) {
-      for (var prop in props) {
-	this[prop] = props[prop];
-      };
-      return this;
-    },
-
-    ldType : function (type) {
-      if (arguments.length === 1)
-	this._ldType = type;
-      return this._ldType;
-    },
-
-    parent : function (obj) {
-      if (arguments.length === 1) {
-	this._parent = obj;
-	this.__changed = true;
-      };
-      return this._parent;
-    },
-
-    // Destroy object - especially for
-    // acyclic structures!
-    // I'm paranoid!
-    destroy : function () {
-      if (this._ops != undefined) {
-	this._ops._parent = undefined;
-	if (this._ops._element !== undefined)
-	  this._ops._element.refTo = undefined;
-	this._ops = undefined;
-      };
-      if (this._element !== undefined)
-	this._element = undefined;
-
-      // In case of a group, destroy all operands
-      if (this._operands !== undefined) {
-      for (var i = 0; i < this._operands.length; i++)
-	  this.getOperand(i).destroy();
-	this._operands = [];
-      };
-    },
-
-    // Wrap a new operation around the root group element 
-    wrapOnRoot : function (op) {
-      var parent = this.parent();
-
-      var group = KorAP.DocGroup.create(parent);
-      if (arguments.length === 1)
-	group.operation(op);
-      else
-	group.operation(
-	  this.operation() === 'and' ? 'or' : 'and'
-	);
-      group.append(this);
-      this.parent(group);
-      group.append();
-      group.element(); // Init (seems to be necessary)
-      parent.root(group);
-      return this.parent();
-    },
-
-    // Be aware! This may be cyclic
-    operators : function (and, or, del) {
-      if (arguments === 0)
-	return this._ops;
-      this._ops = KorAP.Operators.create(
-	and, or, del
-      );
-      this._ops.parent(this);
-      return this._ops;
-    },
-
-    toJson : function () {
-      return {
-	// Unspecified object
-	"@type" : "koral:" + this.ldType()
-      };
-    },
-
-    toQuery : function () {
-      return '';
-    }
-  };
-
-
-  /**
-   * Criterion in a KorAP.Doc
-   */
-  KorAP._changeKey = function () {
-    var doc = this.parentNode.refTo;
-    var key = doc.element().firstChild;
-    key.appendChild(KorAP.FieldChooser.element());
-    KorAP.FieldChooser.show();
-    KorAP.FieldChooser.focus();
-    // key, matchop, type, value
-  };
-  
-  // Field menu
-  KorAP.FieldMenu = {
-    create : function (params) {
-      return Object.create(KorAP.Menu)
-	.upgradeTo(KorAP.FieldMenu)
-	._init(KorAP.FieldMenuItem, undefined, params)
-    }
-  };
-
-
-  // Field menu item
-  KorAP.FieldMenuItem = {
-    create : function (params) {
-      return Object.create(KorAP.MenuItem)
-	.upgradeTo(KorAP.FieldMenuItem)
-	._init(params);
-    },
-    _init : function (params) {
-      if (params[0] === undefined)
-	throw new Error("Missing parameters");
-
-      this._name  = params[0];
-      this._value = params[1];
-      this._type  = params[2];
-
-      this._lcField = ' ' + this._name.toLowerCase();
-
-      return this;
-    },
-    name : function () {
-      return this._name;
-    },
-    type : function () {
-      return this._type;
-    },
-    element : function () {
-      // already defined
-      if (this._element !== undefined)
-	return this._element;
-
-      // Create list item
-      var li = document.createElement("li");
-      li.setAttribute("data-type", this._type);
-      li.setAttribute("data-value", this._value);
-      li.appendChild(document.createTextNode(this._name));
-      return this._element = li;
-    }
-  };
-
-  KorAP.FieldChooser = KorAP.FieldMenu.create([
-    ['Titel', 'title', 'string'],
-    ['Untertitel', 'subTitle', 'string'],
-    ['Veröffentlichungsdatum', 'pubDate', 'date'],
-    ['Autor', 'author', 'string']
-  ]);
-  KorAP.FieldChooser.limit(5);
- 
-}(this.KorAP));
+});
diff --git a/dev/js/src/vc/doc.js b/dev/js/src/vc/doc.js
new file mode 100644
index 0000000..61c6439
--- /dev/null
+++ b/dev/js/src/vc/doc.js
@@ -0,0 +1,363 @@
+/**
+ * Document criterion
+ */
+
+/**
+ * Criterion in a KorAP.Doc
+ */
+function _changeKey () {
+  var doc = this.parentNode.refTo;
+  var key = doc.element().firstChild;
+  key.appendChild(fieldMenu.element());
+  fieldMenu.show();
+  fieldMenu.focus();
+  // key, matchop, type, value
+};
+
+define([
+  'vc/jsonld', 'vc/menu', 'vc/rewritelist'], function (jsonldClass, menuClass, rewriteListClass) {
+  var fieldMenu = menuClass.create([
+    ['Titel', 'title', 'string'],
+    ['Untertitel', 'subTitle', 'string'],
+    ['Veröffentlichungsdatum', 'pubDate', 'date'],
+    ['Autor', 'author', 'string']
+  ]);
+
+  fieldMenu.limit(5);
+
+  _validRegexMatchRE  = new RegExp("^(?:eq|ne)$");
+
+  return {
+    _ldType : "doc",
+    _obj : function () { return '???'; /*KorAP.Doc*/ },
+    
+    create : function (parent, json) {
+      var obj = Object(jsonldClass).
+	create().
+	upgradeTo(this).
+	fromJson(json);
+      
+      if (parent !== undefined)
+	obj._parent = parent;
+      
+      obj.__changed = true;
+      return obj;
+    },
+
+    update : function () {
+      if (this._element === undefined)
+	return this.element();
+      
+      // Get element
+      var e = this._element;
+
+      // Set ref - TODO: Cleanup!
+      e.refTo = this;
+
+      // Check if there is a change
+      if (this.__changed) {
+
+	// Was rewritten
+	if (this.rewrites() !== undefined) {
+	  e.classList.add("rewritten");
+	};
+
+	// Added key
+	var key = document.createElement('span');
+	key.setAttribute('class', 'key');
+
+	// Change key
+	key.addEventListener('click', _changeKey, false);
+
+	if (this.key())
+	  key.appendChild(document.createTextNode(this.key()));
+	
+	// Added match operator
+	var matchop = document.createElement('span');
+	matchop.setAttribute('data-type', this.type());
+	matchop.setAttribute('class', 'match');
+	matchop.appendChild(
+	  document.createTextNode(this.matchop())
+	);
+
+	// Added match operator
+	var value = document.createElement('span');
+	value.setAttribute('data-type', this.type());
+	value.setAttribute('class', 'value');
+	if (this.value())
+	  value.appendChild(
+	    document.createTextNode(this.value())
+	  );
+
+	// Remove all element children
+	_removeChildren(e);
+
+	// Add spans
+	e.appendChild(key);
+	e.appendChild(matchop);
+	e.appendChild(value);
+
+	this.__changed = false;
+      };
+
+      if (this._rewrites !== undefined) {
+	e.appendChild(this._rewrites.element());
+      };
+
+      if (this._parent !== undefined) {
+	// Set operators
+	var op = this.operators(
+	  true,
+	  true,
+	  true
+	);
+
+	// Append new operators
+	e.appendChild(op.element());
+      };
+      
+      return e;
+    },
+
+    element : function () {
+      if (this._element !== undefined)
+	return this._element;
+
+      this._element = document.createElement('div');
+      this._element.setAttribute('class', 'doc');
+
+      this.update();
+      return this._element;
+    },
+
+    // Wrap a new operation around the doc element
+    wrap : function (op) {
+      var parent = this.parent();
+      var group = KorAP.DocGroup.create(parent);
+      group.operation(op);
+      group.append(this);
+      group.append();
+      return parent.replaceOperand(this, group).update();
+    },
+
+    // Deserialize from json
+    fromJson : function (json) {
+      if (json === undefined)
+	return this;
+
+      if (json["@type"] === undefined) {
+	KorAP.log(701, "JSON-LD group has no @type attribute");
+	return;
+      };
+
+      if (json["value"] === undefined ||
+	  typeof json["value"] != 'string') {
+	KorAP.log(805, "Value is invalid");
+	return;
+      };
+
+      // There is a defined key
+      if (json["key"] !== undefined &&
+	  typeof json["key"] === 'string') {
+
+	// Set key
+	this.key(json["key"]);
+
+	// Set match operation
+	if (json["match"] !== undefined) {
+	  if (typeof json["match"] === 'string') {
+	    this.matchop(json["match"]);
+	  }
+	  else {
+	    KorAP.log(802, "Match type is not supported by value type");
+	    return;
+	  };
+	};
+
+	// Key is a string
+	if (json["type"] === undefined ||
+	    json["type"] == "type:string") {
+	  this.type("string");
+
+	  // Check match type
+	  if (!KorAP._validStringMatchRE.test(this.matchop())) {
+	    KorAP.log(802, "Match type is not supported by value type");
+	    return;
+	  };
+
+	  // Set string value
+	  this.value(json["value"]);
+	}
+
+	// Key is a date
+	else if (json["type"] === "type:date") {
+	  this.type("date");
+
+	  if (json["value"] !== undefined &&
+	      KorAP._validDateRE.test(json["value"])) {
+
+	    if (!KorAP._validDateMatchRE.test(this.matchop())) {
+	      KorAP.log(802, "Match type is not supported by value type");
+	      return;
+	    };
+
+	    // Set value
+	    this.value(json["value"]);
+	  }
+	  else {
+	    KorAP.log(806, "Value is not a valid date string");
+	    return;
+	  };
+	}
+
+	// Key is a regular expression
+	else if (json["type"] === "type:regex") {
+	  this.type("regex");
+
+	  try {
+
+	    // Try to create a regular expression
+	    var check = new RegExp(json["value"]);
+
+	    if (!_validRegexMatchRE.test(this.matchop())) {
+	      KorAP.log(802, "Match type is not supported by value type");
+	      return;
+	    };
+
+	    this.value(json["value"]);
+	  }
+
+	  catch (e) {
+	    KorAP.log(807, "Value is not a valid regular expression");
+	    return;
+	  };
+	  this.type("regex");
+	}
+
+	else {
+	  KorAP.log(804, "Unknown value type");
+	  return;
+	};
+      };
+
+      if (json["rewrites"] !== undefined) {
+	this._rewrites = rewriteListClass.create(json["rewrites"]);
+      };
+      
+      return this;
+    },
+
+    key : function (value) {
+      if (arguments.length === 1) {
+	this._key = value;
+	this._changed();
+	return this;
+      };
+      return this._key;
+    },
+
+    matchop : function (match) {
+      if (arguments.length === 1) {
+	this._matchop = match.replace(/^match:/, '');
+	this._changed();
+	return this;
+      };
+      return this._matchop || "eq";
+    },
+
+    type : function (type) {
+      if (arguments.length === 1) {
+	this._type = type;
+	this._changed();
+	return this;
+      };
+      return this._type || "string";
+    },
+
+    value : function (value) {
+      if (arguments.length === 1) {
+	this._value = value;
+	this._changed();
+	return this;
+      };
+      return this._value;
+    },
+
+    rewrites : function () {
+      return this._rewrites;
+    },
+
+    _changed : function () {
+      this.__changed = true;
+      
+      if (this._rewrites === undefined)
+	return;
+
+      delete this["_rewrites"];
+
+      if (this._element === undefined)
+	return;
+      this._element.classList.remove("rewritten");
+    },
+
+    toJson : function () {
+      if (!this.matchop() || !this.key())
+	return {};
+      
+      return {
+	"@type" : "koral:" + this.ldType(),
+	"key"   : this.key(),
+	"match" : "match:" + this.matchop(),
+	"value" : this.value() || '',
+	"type"  : "type:" + this.type()
+      };
+    },
+
+    toQuery : function () {
+      if (!this.matchop() || !this.key())
+	return "";
+
+      // Build doc string based on key
+      var string = this.key() + ' ';
+
+      // Add match operator
+      switch (this.matchop()) {
+      case "ne":
+	string += '!=';
+	break;
+      case "contains":
+	string += '~';
+	break;
+      case "excludes":
+	string += '!~';
+	break;
+      case "geq":
+	string += 'since';
+	break;
+      case "leq":
+	string += 'until';
+	break;
+      default:
+	string += (this.type() == 'date') ? 'in' : '=';
+	break;
+      };
+
+      string += ' ';
+
+      // Add value
+      switch (this.type()) {
+      case "date":
+	return string + this.value();
+	break;
+      case "regex":
+	return string + '/' + this.value() + '/';
+	break;
+      case "string":
+	return string + '"' + this.value().replace(KorAP._quote, '\\$1') + '"';
+	break;
+      };
+
+      return "";
+    }
+  };
+});
diff --git a/dev/js/src/vc/docgroup.js b/dev/js/src/vc/docgroup.js
new file mode 100644
index 0000000..5e5f010
--- /dev/null
+++ b/dev/js/src/vc/docgroup.js
@@ -0,0 +1,343 @@
+/**
+ * Document group criterion
+ */
+define([
+  'vc/jsonld',
+  'vc/unspecified',
+  'vc/doc',
+  'util'
+], function (jsonldClass,
+	     unspecClass,
+	     docClass) {
+
+  var _validGroupOpRE = new RegExp("^(?:and|or)$");
+
+  var docGroupClass = {
+    _ldType : "docGroup",
+
+    create : function (parent, json) {
+      var obj = Object.create(jsonldClass).upgradeTo(this);
+      obj._operands = [];
+      obj.fromJson(json);
+      if (parent !== undefined)
+	obj._parent = parent;
+      return obj;
+    },
+    
+    newAfter : function (obj) {
+      for (var i = 0; i < this._operands.length; i++) {
+	if (this._operands[i] === obj) {
+	  var operand = unspecClass.create(this);
+	  this._operands.splice(i + 1, 0, operand);
+	  return this.update();
+	};
+      };
+    },
+
+    // The doc is already set in the group
+    _duplicate : function (operand) {
+      if (operand.ldType() !== 'doc')
+	return null;
+
+      for (var i = 0; i < this._operands.length; i++) {
+	var op = this.getOperand(i);
+	if (op.ldType() === 'doc'
+	    && operand.key() === op.key()
+	    && operand.matchop() === op.matchop()
+	    && operand.value() === op.value()) {
+	  return op;
+	};
+      };
+      return null;
+    },
+    
+    append : function (operand) {
+      
+      // Append unspecified object
+      if (operand === undefined) {
+
+	// Be aware of cyclic structures!
+	operand = unspecClass.create(this);
+	this._operands.push(operand);
+	return operand;
+      };
+      
+      switch (operand["@type"]) {
+	
+      case undefined:
+	// No @type defined
+	if (operand["ldType"] !== undefined) {
+	  if (operand.ldType() !== 'doc' &&
+	      operand.ldType() !== 'docGroup') {
+	    KorAP.log(812, "Operand not supported in document group");
+	    return;
+	  };
+
+	  // Be aware of cyclic structures!
+	  operand.parent(this);
+
+	  var dupl = this._duplicate(operand);
+	  if (dupl === null) {
+	    this._operands.push(operand);
+	    return operand;
+	  };
+	  return dupl;
+	};
+
+	KorAP.log(701, "JSON-LD group has no @type attribute");
+	return;
+
+      case "koral:doc":
+	// Be aware of cyclic structures!
+	var doc = docClass.create(this, operand);
+	if (doc === undefined)
+	  return;
+	var dupl = this._duplicate(doc);
+	if (dupl === null) {
+	  this._operands.push(doc);
+	  return doc;
+	};
+	return dupl;
+
+      case "koral:docGroup":
+	// Be aware of cyclic structures!
+	var docGroup = docGroupClass.create(this, operand);
+	if (docGroup === undefined)
+	  return;
+
+	// Flatten group
+	if (docGroup.operation() === this.operation()) {
+	  for (var op in docGroup.operands()) {
+	    op = docGroup.getOperand(op);
+	    var dupl = this._duplicate(op);
+	    if (dupl === null) {
+	      this._operands.push(op);
+	      op.parent(this);
+	    };
+	  };
+	  docGroup._operands = [];
+	  docGroup.destroy();
+	  return this;
+	};
+	this._operands.push(docGroup);
+	return docGroup;
+
+      default:
+	KorAP.log(812, "Operand not supported in document group");
+	return;
+      };
+    },
+
+    update : function () {
+      // There is only one operand in group
+      
+      if (this._operands.length === 1) {
+	
+	var parent = this.parent();
+	var op = this.getOperand(0);
+	
+	// This will prevent destruction of
+	// the operand
+	this._operands = [];
+
+	// Parent is a group
+	if (parent.ldType() !== null)
+	  return parent.replaceOperand(this, op).update();
+
+	// Parent is vc
+	else {
+	  this.destroy();
+	  // Cyclic madness
+	  parent.root(op);
+	  op.parent(parent);
+	  return parent.root();
+	};
+      };
+
+      if (this._element === undefined)
+	return this;
+
+      var group = this._element;
+      group.setAttribute('data-operation', this.operation());
+
+      _removeChildren(group);
+
+      // Append operands
+      for (var i = 0; i < this._operands.length; i++) {
+	group.appendChild(
+	  this.getOperand(i).element()
+	);
+      };
+
+      // Set operators
+      var op = this.operators(
+	this.operation() == 'and' ? false : true,
+	this.operation() == 'or'  ? false : true,
+	true
+      );
+
+      group.appendChild(op.element());
+
+      return this;
+    },
+
+    element : function () {
+      if (this._element !== undefined)
+	return this._element;
+
+      this._element = document.createElement('div');
+      this._element.setAttribute('class', 'docGroup');
+
+      // Update the object - including optimization
+      this.update();
+
+      return this._element;
+    },
+
+    operation : function (op) {
+      if (arguments.length === 1) {
+	if (_validGroupOpRE.test(op)) {
+	  this._op = op;
+	}
+	else {
+	  KorAP.log(810, "Unknown operation type");
+	  return;
+	};
+      };
+      return this._op || 'and';
+    },
+
+    operands : function () {
+      return this._operands;
+    },
+
+    getOperand : function (index) {
+      return this._operands[index];
+    },
+
+    // Replace operand
+    replaceOperand : function (oldOp, newOp) {
+      
+      for (var i = 0; i < this._operands.length; i++) {
+	if (this._operands[i] === oldOp) {
+	  
+	  // Just insert a doc or ...
+	  if (newOp.ldType() === "doc" ||
+	      newOp.ldType() === "non" ||
+	      // ... insert a group of a different operation
+	      // (i.e. "and" in "or"/"or" in "and")
+	      newOp.operation() != this.operation()) {
+	    this._operands[i] = newOp;
+	    newOp.parent(this);
+	  }
+
+	  // Flatten group
+	  else {
+	    // Remove old group
+	    this._operands.splice(i, 1);
+
+	    // Inject new operands
+	    for (var op in newOp.operands().reverse()) {
+	      op = newOp.getOperand(op);
+	      this._operands.splice(i, 0, op);
+	      op.parent(this);
+	    };
+	    // Prevent destruction of operands
+	    newOp._operands = [];
+	    newOp.destroy();
+	  };
+	  oldOp.destroy();
+	  return this;
+	}
+      };
+      return false;
+    },
+
+    // Delete operand from group
+    delOperand : function (obj) {
+      for (var i = 0; i < this._operands.length; i++) {
+	if (this._operands[i] === obj) {
+	  
+	  // Delete identified operand
+	  this._operands.splice(i,1);
+
+	  // Destroy object for cyclic references
+	  obj.destroy();
+
+	  return this;
+	};
+      };
+
+      // Operand not found
+      return undefined;
+    },
+    
+    // Deserialize from json
+    fromJson : function (json) {
+      if (json === undefined)
+	return this;
+
+      if (json["@type"] === undefined) {
+	KorAP.log(701, "JSON-LD group has no @type attribute");
+	return;
+      };
+
+      if (json["operation"] === undefined ||
+	  typeof json["operation"] !== 'string') {
+	KorAP.log(811, "Document group expects operation");
+	return;
+      };
+
+      var operation = json["operation"];
+
+      this.operation(operation.replace(/^operation:/,''));
+
+      if (json["operands"] === undefined ||
+	  !(json["operands"] instanceof Array)) {
+	KorAP.log(704, "Operation needs operand list")
+	return;
+      };
+
+      // Add all documents
+      for (var i in json["operands"]) {
+	var operand = json["operands"][i];
+	this.append(operand);
+      };
+      
+      return this;
+    },
+
+    toJson : function () {
+      var opArray = new Array();
+      for (var i = 0; i < this._operands.length; i++) {
+	if (this._operands[i].ldType() !== 'non')
+	  opArray.push(this._operands[i].toJson());
+      };
+      return {
+	"@type"     : "koral:" + this.ldType(),
+	"operation" : "operation:" + this.operation(),
+	"operands"  : opArray
+      };
+    },
+
+    toQuery : function (brackets) {
+      var list = this._operands
+	.filter(function (op) {
+	  return op.ldType() !== 'non';
+	})
+	.map(function (op) {
+	  return (op.ldType() === 'docGroup') ?
+	    op.toQuery(true) :
+	    op.toQuery();
+	});
+      
+      if (list.length === 1)
+	return list.join('');
+      else {
+	var str = list.join(this.operation() === 'or' ? ' | ' : ' & ');
+	return brackets ? '(' + str + ')' : str;
+      };
+    }
+  };
+  return docGroupClass;
+});
diff --git a/dev/js/src/vc/item.js b/dev/js/src/vc/item.js
new file mode 100644
index 0000000..bfefe38
--- /dev/null
+++ b/dev/js/src/vc/item.js
@@ -0,0 +1,44 @@
+// Field menu item
+define(['menu/item'], function (itemClass) {
+  return {
+    create : function (params) {
+      return Object.create(itemClass)
+	.upgradeTo(this)
+	._init(params);
+    },
+
+    _init : function (params) {
+      if (params[0] === undefined)
+	throw new Error("Missing parameters");
+
+      this._name  = params[0];
+      this._value = params[1];
+      this._type  = params[2];
+
+      this._lcField = ' ' + this._name.toLowerCase();
+
+      return this;
+    },
+
+    name : function () {
+      return this._name;
+    },
+
+    type : function () {
+      return this._type;
+    },
+
+    element : function () {
+      // already defined
+      if (this._element !== undefined)
+	return this._element;
+
+      // Create list item
+      var li = document.createElement("li");
+      li.setAttribute("data-type", this._type);
+      li.setAttribute("data-value", this._value);
+      li.appendChild(document.createTextNode(this._name));
+      return this._element = li;
+    }
+  };
+});
diff --git a/dev/js/src/vc/jsonld.js b/dev/js/src/vc/jsonld.js
new file mode 100644
index 0000000..324e7db
--- /dev/null
+++ b/dev/js/src/vc/jsonld.js
@@ -0,0 +1,99 @@
+/**
+ * Abstract JsonLD criterion object
+ */
+define(['vc/operators'], function (operatorsClass) {
+  return {
+    __changed : false,
+    
+    create : function () {
+      return Object.create(this);
+    },
+
+    /**
+     * Upgrade this object to another object
+     * while private data stays intact
+     */
+    upgradeTo : function (props) {
+      for (var prop in props) {
+	this[prop] = props[prop];
+      };
+      return this;
+    },
+    
+    ldType : function (type) {
+      if (arguments.length === 1)
+	this._ldType = type;
+      return this._ldType;
+    },
+    
+    parent : function (obj) {
+      if (arguments.length === 1) {
+	this._parent = obj;
+	this.__changed = true;
+      };
+      return this._parent;
+    },
+
+    // Destroy object - especially for
+    // acyclic structures!
+    // I'm paranoid!
+    destroy : function () {
+      if (this._ops != undefined) {
+	this._ops._parent = undefined;
+	if (this._ops._element !== undefined)
+	  this._ops._element.refTo = undefined;
+	this._ops = undefined;
+      };
+      if (this._element !== undefined)
+	this._element = undefined;
+      
+      // In case of a group, destroy all operands
+      if (this._operands !== undefined) {
+	for (var i = 0; i < this._operands.length; i++)
+	  this.getOperand(i).destroy();
+	this._operands = [];
+      };
+    },
+    
+    // Wrap a new operation around the root group element 
+    wrapOnRoot : function (op) {
+      var parent = this.parent();
+      
+      var group = require('vc/docgroup').create(parent);
+      if (arguments.length === 1)
+	group.operation(op);
+      else
+	group.operation(
+	  this.operation() === 'and' ? 'or' : 'and'
+	);
+      group.append(this);
+      this.parent(group);
+      group.append();
+      group.element(); // Init (seems to be necessary)
+      parent.root(group);
+      return this.parent();
+    },
+
+    // Be aware! This may be cyclic
+    operators : function (and, or, del) {
+      if (arguments === 0)
+	return this._ops;
+      this._ops = operatorsClass.create(
+	and, or, del
+      );
+      this._ops.parent(this);
+      return this._ops;
+    },
+
+    toJson : function () {
+      return {
+	// Unspecified object
+	"@type" : "koral:" + this.ldType()
+      };
+    },
+
+    toQuery : function () {
+      return '';
+    }
+  };
+});
diff --git a/dev/js/src/vc/menu.js b/dev/js/src/vc/menu.js
new file mode 100644
index 0000000..6f6d2bd
--- /dev/null
+++ b/dev/js/src/vc/menu.js
@@ -0,0 +1,10 @@
+// Field menu
+define(['menu', 'menu/item'], function (menuClass, itemClass) {
+  return {
+    create : function (params) {
+      return Object.create(menuClass)
+	.upgradeTo(this)
+	._init(itemClass, undefined, params)
+    }
+  };
+});
diff --git a/dev/js/src/vc/operators.js b/dev/js/src/vc/operators.js
new file mode 100644
index 0000000..312f75d
--- /dev/null
+++ b/dev/js/src/vc/operators.js
@@ -0,0 +1,168 @@
+/**
+ * Operators for criteria
+ */
+define(['util'], function () {
+
+  var loc = KorAP.Locale;
+  loc.AND   = loc.AND   || 'and';
+  loc.OR    = loc.OR    || 'or';
+  loc.DEL   = loc.DEL   || '×';
+
+
+  // Utility for analysing boolean values
+  function _bool (bool) {
+    return (bool === undefined || bool === null || bool === false) ? false : true;
+  };
+
+
+  // Add new unspecified document
+  function _add (obj, type) {
+    var ref = obj.parentNode.refTo;
+    var parent = ref.parent();
+
+    if (ref.ldType() === 'docGroup') {
+
+      // Check that the action differs from the type
+      if (ref.operation() === type)
+	return;
+
+      if (parent.ldType() !== null) {
+	return parent.newAfter(ref);
+      }
+      else {
+	// The group is on root - wrap
+	return ref.wrapOnRoot();
+      };
+    }
+    else if (ref.ldType() === 'doc') {
+
+      if (parent.ldType() === null) {
+	return ref.wrapOnRoot(type);
+      }
+      else if (parent.operation() === type) {
+	return parent.newAfter(ref);
+      }
+      else {
+	return ref.wrap(type);
+      };
+    };
+  };
+
+
+  // Add doc with 'and' relation
+  var _and = function () {
+    return _add(this, 'and');
+  };
+
+
+  // Add doc with 'or' relation
+  var _or = function () {
+    return _add(this, 'or');
+  };
+
+
+  // Remove doc or docGroup
+  var _delete = function () {
+    var ref = this.parentNode.refTo;
+    if (ref.parent().ldType() !== null) {
+      return ref.parent().delOperand(ref).update();
+    }
+    else {
+      ref.parent().clean();
+    };
+  };
+
+
+  return {
+    create : function (and, or, del) {
+      var op = Object.create(this);
+      op.and(and);
+      op.or(or);
+      op.del(del);
+      return op;
+    },
+
+    update : function () {
+      // Init the element
+      if (this._element === undefined)
+	return this.element();
+
+      var op = this._element;
+
+      op.refTo = this.parent();
+
+      // Remove everything underneath
+      _removeChildren(op);
+      
+      // Add and button
+      if (this._and === true) {
+	var andE = document.createElement('span');
+	andE.setAttribute('class', 'and');
+	andE.addEventListener('click', _and, false);
+	andE.appendChild(
+	  document.createTextNode(loc.AND)
+	);
+	op.appendChild(andE);
+      };
+
+      // Add or button
+      if (this._or === true) {
+	var orE = document.createElement('span');
+	orE.setAttribute('class', 'or');
+	orE.addEventListener('click', _or, false);
+	orE.appendChild(document.createTextNode(loc.OR));
+	op.appendChild(orE);
+      };
+
+      // Add delete button
+      if (this._del === true) {
+	var delE = document.createElement('span');
+	delE.setAttribute('class', 'delete');
+	delE.appendChild(document.createTextNode(loc.DEL));
+	delE.addEventListener('click', _delete, false);
+	op.appendChild(delE);
+      };
+
+      return op;
+    },
+
+    // Be aware! This may be cyclic
+    parent : function (obj) {
+      if (arguments.length === 1)
+	this._parent = obj;
+      return this._parent;
+    },
+
+    element : function () {
+
+      // Return existing element
+      if (this._element !== undefined)
+	return this._element;
+
+      this._element = document.createElement('div');
+      this._element.setAttribute('class', 'operators');
+
+      // Init elements
+      this.update();
+      return this._element;
+    },
+
+    and : function (bool) {
+      if (arguments.length === 1)
+	this._and = _bool(bool);
+      return this._and;
+    },
+
+    or : function (bool) {
+      if (arguments.length === 1)
+	this._or = _bool(bool);
+      return this._or;
+    },
+
+    del : function (bool) {
+      if (arguments.length === 1)
+	this._del = _bool(bool);
+      return this._del;
+    }
+  };
+});
diff --git a/dev/js/src/vc/rewrite.js b/dev/js/src/vc/rewrite.js
new file mode 100644
index 0000000..79ba4b3
--- /dev/null
+++ b/dev/js/src/vc/rewrite.js
@@ -0,0 +1,100 @@
+/**
+ * Implementation of rewrite objects.
+ */
+define(['vc/jsonld', 'util'], function (jsonldClass) {
+
+  var _validRewriteOpRE   = new RegExp("^(?:injec|modifica)tion$");
+  var _quote              = new RegExp("([\"\\\\])", 'g');
+
+  return {
+    // Construction method
+    create : function (json) {
+      var obj = Object(jsonldClass).
+	create().
+	upgradeTo(this).
+	fromJson(json);
+      return obj;
+    },
+
+    // Get or set source
+    src : function (string) {
+      if (arguments.length === 1)
+	this._src = string;
+      return this._src;
+    },
+    
+    // Get or set operation
+    operation : function (op) {
+      if (arguments.length === 1) {
+	if (_validRewriteOpRE.test(op)) {
+	  this._op = op;
+	}
+	else {
+	  KorAP.log(814, "Unknown rewrite operation");
+	  return;
+	};
+      };
+      return this._op || 'injection';
+    },
+
+    // Get or set scope
+    scope : function (attr) {
+      if (arguments.length === 1)
+	this._scope = attr;
+      return this._scope;
+    },
+
+    // Serialize from Json
+    fromJson : function (json) {
+      if (json === undefined)
+	return this;
+
+      // Missing @type
+      if (json["@type"] === undefined) {
+	KorAP.log(701, "JSON-LD group has no @type attribute");
+	return;
+      };
+      
+      // Missing source
+      if (json["src"] === undefined ||
+	  typeof json["src"] !== 'string') {
+	KorAP.log(815, "Rewrite expects source");
+	return;
+      };
+
+      // Set source
+      this.src(json["src"]);
+
+      // Set operation
+      if (json["operation"] !== undefined) {
+	var operation = json["operation"];
+	this.operation(operation.replace(/^operation:/,''));
+      };
+
+      // Set scope
+      if (json["scope"] !== undefined &&
+	  typeof json["scope"] === 'string')
+	this.scope(json["scope"]);
+
+      return this;
+    },
+    
+    toString : function () {
+      var str = '';
+      var op = this.operation();
+      str += op.charAt(0).toUpperCase() + op.slice(1);
+      str += ' of ' + (
+	this._scope === null ?
+	  'object' :
+	  '"' +
+	  this.scope().replace(_quote, '\\$1') +
+	  '"'
+      );
+      str += ' by ' +
+	'"' +
+	this.src().replace(_quote, '\\$1') +
+	'"';
+      return str;
+    }
+  };
+});
diff --git a/dev/js/src/vc/rewritelist.js b/dev/js/src/vc/rewritelist.js
new file mode 100644
index 0000000..af3c05a
--- /dev/null
+++ b/dev/js/src/vc/rewritelist.js
@@ -0,0 +1,49 @@
+define(['vc/jsonld', 'vc/rewrite'], function (jsonldClass, rewriteClass) {
+  return {
+    // Construction method
+    create : function (json) {
+      var obj = Object(jsonldClass).
+	create().
+	upgradeTo(this).
+	fromJson(json);
+      return obj;
+    },
+    fromJson : function (json) {
+      this._list = new Array();
+      for (var i = 0; i < json.length; i++) {
+	this._list.push(
+	  rewriteClass.create(json[i])
+	);
+      };
+      return this;
+    },
+    element : function () {
+      if (this._element !== undefined)
+	return this._element;
+
+      this._element = document.createElement('div');
+      this._element.setAttribute('class', 'rewrite');
+      for (var x in this._list) {
+	var rewrite = this._list[x];
+	var span = document.createElement('span');
+
+	// Set class attribute
+	span.setAttribute('class', rewrite.operation());
+
+	// Append source information
+	span.appendChild(document.createTextNode(rewrite.src()));
+
+	// Append scope information
+	if (rewrite.scope() !== undefined) {
+	  span.appendChild(
+	    document.createTextNode(
+	      ': ' + rewrite.scope()
+	    )
+	  );
+	};
+	this._element.appendChild(span);
+      };
+      return this._element;
+    }
+  };
+});
diff --git a/dev/js/src/vc/unspecified.js b/dev/js/src/vc/unspecified.js
new file mode 100644
index 0000000..9efd007
--- /dev/null
+++ b/dev/js/src/vc/unspecified.js
@@ -0,0 +1,89 @@
+/**
+ * Unspecified criterion
+ */
+define(['vc/jsonld', 'vc/doc', 'util'], function (jsonldClass, docClass) {
+
+  var loc = KorAP.Locale;
+  loc.EMPTY = loc.EMPTY || '⋯';
+
+  return {
+    _ldType : "non",
+    create : function (parent) {
+      var obj = Object.create(jsonldClass).
+	upgradeTo(this);
+
+      if (parent !== undefined)
+	obj._parent = parent;
+
+      return obj;
+    },
+
+    // Set key - replace
+    key : function (v) {
+
+      // Not replaceable
+      if (this._parent === undefined)
+	return null;
+
+      // Set JSON-LD type
+      var newDoc = docClass.create(this._parent, {
+	"@type" : "koral:doc",
+	"value" : "",
+	"key"   : v
+      });
+
+      // Unspecified document on root
+      if (this._parent.ldType() === null) {
+	this._parent.root(newDoc);
+	this.destroy();
+      }
+
+      // Unspecified document in group
+      else {
+	this._parent.replaceOperand(this, newDoc);
+      };
+      this._parent.update();
+      return newDoc;
+    },
+
+    update : function () {
+
+      if (this._element === undefined)
+	return this.element();
+
+      // Remove element content
+      _removeChildren(this._element);
+
+      var ellipsis = document.createElement('span');
+      ellipsis.appendChild(document.createTextNode(loc.EMPTY));
+      this._element.appendChild(ellipsis);
+
+      // Set ref - TODO: Cleanup!
+      this._element.refTo = this;
+
+      // Set operators
+      if (this._parent !== undefined && this.parent().ldType() !== null) {
+	var op = this.operators(
+	  false,
+	  false,
+	  true
+	);
+	
+	this._element.appendChild(
+	  op.element()
+	);
+      };
+
+      return this.element();
+    },
+
+    element : function () {
+      if (this._element !== undefined)
+	return this._element;
+      this._element = document.createElement('div');
+      this._element.setAttribute('class', 'doc unspecified');
+      this.update();
+      return this._element;
+    },
+  };
+});