Added rewrite capabilities and separated menu
diff --git a/public/js/demo/vc.html b/public/js/demo/vc.html
index 79f7d0e..9254d68 100644
--- a/public/js/demo/vc.html
+++ b/public/js/demo/vc.html
@@ -52,7 +52,14 @@
                   "@type":"korap:doc",
                   "key":"Veröffentlichungsort",
                   "value":"hihi",
-                  "match":"match:eq"
+                  "match":"match:eq",
+                  "rewrites" : [
+                    {
+                      "@type": "korap:rewrite",
+                      "src" : "policy",
+                      "operation" : "operation:injection",
+                    }
+                  ]
                 }
               ]
             }
diff --git a/public/js/spec/menuSpec.js b/public/js/spec/menuSpec.js
index 0477442..98f6e72 100644
--- a/public/js/spec/menuSpec.js
+++ b/public/js/spec/menuSpec.js
@@ -49,6 +49,88 @@
   }
 };
 
+KorAP.OwnMenu = {
+  create : function (params) {
+    return Object.create(KorAP.Menu)
+      .upgradeTo(KorAP.OwnMenu)
+      ._init(KorAP.OwnMenuItem, params);
+  }
+};
+
+
+// Support for hint
+KorAP.HintMenu = {
+  create : function (context, params) {
+    var obj = Object.create(KorAP.Menu)
+      .upgradeTo(KorAP.HintMenu)
+      ._init(KorAP.HintMenuItem, params);
+    obj._context = context;
+    return obj;
+  }
+};
+
+KorAP.HintMenuItem = {
+  create : function (params) {
+   return Object.create(KorAP.MenuItem)
+      .upgradeTo(KorAP.HintMenuItem)
+      ._init(params);
+  },
+  content : function (content) {
+    if (arguments.length === 1) {
+      this._content = content;
+    };
+    return this._content;
+  },
+  _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;
+  },
+  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");
+    li.setAttribute("data-action", this._action);
+
+    // Create title
+    var name =  document.createElement("strong");
+    name.appendChild(document.createTextNode(this._name));
+    
+    li.appendChild(name);
+
+    // Create description
+    if (this._desc !== undefined) {
+      var desc = document.createElement("span");
+      desc.appendChild(document.createTextNode(this._desc));
+      li.appendChild(desc);
+    };
+    return this._element = li;
+  }
+};
+
+
 
 describe('KorAP.MenuItem', function () {
   it('should be initializable', function () {
@@ -219,36 +301,29 @@
     menuItem.lowlight();
     expect(menuItem.element().innerHTML).toEqual(plain);
   });
-
-});
-
-
-/*
-
-describe('KorAP.MenuItem', function () {
-
 });
 
 
 describe('KorAP.Menu', function () {
-
-  var list = [
-    ["Constituency", "c=", "Example 1"],
-    ["Lemma", "l="],
-    ["Morphology", "m=", "Example 2"],
-    ["Part-of-Speech", "p="],
-    ["Syntax", "syn="]
-  ];
-
-
   it('should be initializable', function () {
+    var list = [
+      ["Constituency"],
+      ["Lemma"],
+      ["Morphology"],
+      ["Part-of-Speech"],
+      ["Syntax"]
+    ];
 
-    var menu = KorAP.Menu.create("cnx/", list);
-    expect(menu.context).toEqual('cnx/');
-    expect(menu.element.nodeName).toEqual('UL');
-    expect(menu.element.style.opacity).toEqual("0");
+    var menu = KorAP.OwnMenu.create(list);
+    expect(menu.itemClass()).toEqual(KorAP.OwnMenuItem);
+    expect(menu.element().nodeName).toEqual('UL');
+    expect(menu.element().style.opacity).toEqual("0");
+    expect(menu.limit()).toEqual(8);
 
-    KorAP.limit = 8;
+    menu.limit(9);
+    expect(menu.limit()).toEqual(9);
+
+    menu.limit(8);
 
     // view
     menu.show();
@@ -256,109 +331,155 @@
     // First element in list
     expect(menu.item(0).active()).toBe(true);
     expect(menu.item(0).noMore()).toBe(true);
-    
+
     // Middle element in list
     expect(menu.item(2).active()).toBe(false);
     expect(menu.item(2).noMore()).toBe(false);
 
     // Last element in list
-    expect(menu.item(menu.length - 1).active()).toBe(false);
-    expect(menu.item(menu.length - 1).noMore()).toBe(true);
+    expect(menu.item(menu.length() - 1).active()).toBe(false);
+    expect(menu.item(menu.length() - 1).noMore()).toBe(true);
   });
 
   it('should be visible', function () {
-    var menu = KorAP.Menu.create("cnx/", list);
+    var list = [
+      ["Constituency", "c=", "Example 1"],
+      ["Lemma", "l="],
+      ["Morphology", "m=", "Example 2"],
+      ["Part-of-Speech", "p="],
+      ["Syntax", "syn="]
+    ];
+    var menu = KorAP.HintMenu.create("cnx/", list);
     expect(menu.delete()).toBe(undefined);
-
-    KorAP.limit = 3;
+    menu.limit(3);
 
     expect(menu.show()).toBe(undefined);
-    expect(menu.element.firstChild.innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
-    expect(menu.element.childNodes[1].innerHTML).toEqual("<strong>Lemma</strong>");
-    expect(menu.element.childNodes[1].getAttribute("data-action")).toEqual("l=");
-    expect(menu.element.childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
-    expect(menu.element.childNodes[3]).toBe(undefined);
+    expect(menu.element().firstChild.innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Lemma</strong>");
+    expect(menu.element().childNodes[1].getAttribute("data-action")).toEqual("l=");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+    expect(menu.element().childNodes[3]).toBe(undefined);
 
     // Check boundaries
-    expect(menu.element.childNodes[0].classList.contains("no-more")).toBe(true);
-    expect(menu.element.childNodes[1].classList.contains("no-more")).toBe(false);
-    expect(menu.element.childNodes[2].classList.contains("no-more")).toBe(false);
+    expect(menu.element().childNodes[0].classList.contains("no-more")).toBe(true);
+    expect(menu.element().childNodes[1].classList.contains("no-more")).toBe(false);
+    expect(menu.element().childNodes[2].classList.contains("no-more")).toBe(false);
   });
 
-
   it('should be filterable', function () {
-    var menu = KorAP.Menu.create("cnx/", list);
+    var list = [
+      ["Constituency", "c=", "Example 1"],
+      ["Lemma", "l="],
+      ["Morphology", "m=", "Example 2"],
+      ["Part-of-Speech", "p="],
+      ["Syntax", "syn="]
+    ];
 
-    KorAP.limit = 3;
+    var menu = KorAP.HintMenu.create("cnx/", list);
+    menu.limit(3);
 
     expect(menu.show("o")).toBe(undefined);
-    expect(menu.element.childNodes[0].innerHTML).toEqual("<strong>C<em>o</em>nstituency</strong><span>Example 1</span>");
-    expect(menu.element.childNodes[1].innerHTML).toEqual("<strong>M<em>o</em>rphology</strong><span>Example 2</span>");
-    expect(menu.element.childNodes[2].innerHTML).toEqual("<strong>Part-<em>o</em>f-Speech</strong>");
-    expect(menu.element.childNodes[3]).toBe(undefined);
+    expect(menu.element().childNodes[0].innerHTML).toEqual("<strong>C<mark>o</mark>nstituency</strong><span>Example 1</span>");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Part-<mark>o</mark>f-Speech</strong>");
+    expect(menu.element().childNodes[3]).toBe(undefined);
 
     // Check boundaries
-    expect(menu.element.childNodes[0].classList.contains("no-more")).toBe(true);
-    expect(menu.element.childNodes[1].classList.contains("no-more")).toBe(false);
-    expect(menu.element.childNodes[2].classList.contains("no-more")).toBe(true);
+    expect(menu.element().childNodes[0].classList.contains("no-more")).toBe(true);
+    expect(menu.element().childNodes[1].classList.contains("no-more")).toBe(false);
+    expect(menu.element().childNodes[2].classList.contains("no-more")).toBe(true);
 
-
-    KorAP.limit = 2;
+    menu.limit(2);
 
     expect(menu.show("o")).toBe(undefined);
-    expect(menu.element.childNodes[0].innerHTML).toEqual("<strong>C<em>o</em>nstituency</strong><span>Example 1</span>");
-    expect(menu.element.childNodes[1].innerHTML).toEqual("<strong>M<em>o</em>rphology</strong><span>Example 2</span>");
-    expect(menu.element.childNodes[2]).toBe(undefined);
+    expect(menu.element().childNodes[0].innerHTML).toEqual("<strong>C<mark>o</mark>nstituency</strong><span>Example 1</span>");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>M<mark>o</mark>rph<mark>o</mark>l<mark>o</mark>gy</strong><span>Example 2</span>");
+    expect(menu.element().childNodes[2]).toBe(undefined);
 
     // Check boundaries
-    expect(menu.element.childNodes[0].classList.contains("no-more")).toBe(true);
-    expect(menu.element.childNodes[1].classList.contains("no-more")).toBe(false);
-    expect(menu.element.childNodes[2]).toBe(undefined);
+    expect(menu.element().childNodes[0].classList.contains("no-more")).toBe(true);
+    expect(menu.element().childNodes[1].classList.contains("no-more")).toBe(false);
+    expect(menu.element().childNodes[2]).toBe(undefined);
+
+    expect(menu.show("e")).toBe(undefined);
+    expect(menu.element().childNodes[0].innerHTML).toEqual("<strong>Constitu<mark>e</mark>ncy</strong><span><mark>E</mark>xampl<mark>e</mark> 1</span>");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Morphology</strong><span><mark>E</mark>xampl<mark>e</mark> 2</span>");
+    expect(menu.element().childNodes[2]).toBe(undefined);
+
+    menu.limit(5);
+
+    expect(menu.show("a")).toBe(undefined);
+    expect(menu.element().childNodes[0].innerHTML).toEqual("<strong>Constituency</strong><span>Ex<mark>a</mark>mple 1</span>");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Lemm<mark>a</mark></strong>");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Ex<mark>a</mark>mple 2</span>");
+    expect(menu.element().childNodes[3].innerHTML).toEqual("<strong>P<mark>a</mark>rt-of-Speech</strong>");
+    expect(menu.element().childNodes[4].innerHTML).toEqual("<strong>Synt<mark>a</mark>x</strong>");
+    expect(menu.element().childNodes[5]).toBe(undefined);
+
   });
 
+
   it('should be nextable', function () {
-    var menu = KorAP.Menu.create("cnx/", list);
+    var list = [
+      ["Constituency", "c=", "Example 1"],
+      ["Lemma", "l="],
+      ["Morphology", "m=", "Example 2"],
+      ["Part-of-Speech", "p="],
+      ["Syntax", "syn="]
+    ];
 
-    KorAP.limit = 3;
+    var menu = KorAP.HintMenu.create("cnx/", list);
+
+    // Show only 3 items
+    menu.limit(3);
+
     expect(menu.show()).toBe(undefined);
-
-    expect(menu.element.childNodes[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+    expect(menu.element().childNodes[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
     expect(menu.shownItem(0).active()).toBe(true);
-    expect(menu.element.childNodes[1].innerHTML).toEqual("<strong>Lemma</strong>");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Lemma</strong>");
     expect(menu.shownItem(1).active()).toBe(false);
-    expect(menu.element.childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
     expect(menu.shownItem(2).active()).toBe(false);
-    expect(menu.element.childNodes[3]).toBe(undefined);
+    expect(menu.element().childNodes[3]).toBe(undefined);
 
     // Activate next (1)
     menu.next();
-    expect(menu.element.childNodes[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+    expect(menu.element().childNodes[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
     expect(menu.shownItem(0).active()).toBe(false);
-    expect(menu.element.childNodes[1].innerHTML).toEqual("<strong>Lemma</strong>");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Lemma</strong>");
     expect(menu.shownItem(1).active()).toBe(true);
-    expect(menu.element.childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
     expect(menu.shownItem(2).active()).toBe(false);
-    expect(menu.element.childNodes[3]).toBe(undefined);
+    expect(menu.element().childNodes[3]).toBe(undefined);
 
     // Activate next (2)
     menu.next();
-    expect(menu.element.childNodes[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
+    expect(menu.element().childNodes[0].innerHTML).toEqual("<strong>Constituency</strong><span>Example 1</span>");
     expect(menu.shownItem(0).active()).toBe(false);
-    expect(menu.element.childNodes[1].innerHTML).toEqual("<strong>Lemma</strong>");
+    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Lemma</strong>");
     expect(menu.shownItem(1).active()).toBe(false);
-    expect(menu.element.childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
     expect(menu.shownItem(2).active()).toBe(true);
-    expect(menu.element.childNodes[3]).toBe(undefined);
+    expect(menu.element().childNodes[3]).toBe(undefined);
 
     // Activate next (3)
     menu.next();
-    expect(menu.element.childNodes[0].innerHTML).toEqual("<strong>Lemma</strong>");
+//    expect(menu.element().childNodes[0].innerHTML).toEqual("<strong>Lemma</strong>");
     expect(menu.shownItem(0).active()).toBe(false);
-    expect(menu.element.childNodes[1].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
+//    expect(menu.element().childNodes[1].innerHTML).toEqual("<strong>Morphology</strong><span>Example 2</span>");
     expect(menu.shownItem(1).active()).toBe(false);
-    expect(menu.element.childNodes[2].innerHTML).toEqual("<strong>Part-of-Speech</strong>");
+//    expect(menu.element().childNodes[2].innerHTML).toEqual("<strong>Part-of-Speech</strong>");
     expect(menu.shownItem(2).active()).toBe(true);
-    expect(menu.element.childNodes[3]).toBe(undefined);
+    expect(menu.element().childNodes[3]).toBe(undefined);
+  });
+});
+
+
+/*
+describe('KorAP.Menu', function () {
+
+
 
     // Activate next (4)
     menu.next();
diff --git a/public/js/spec/vcSpec.js b/public/js/spec/vcSpec.js
index 11ada63..49e9275 100644
--- a/public/js/spec/vcSpec.js
+++ b/public/js/spec/vcSpec.js
@@ -1,6 +1,5 @@
 /*
 Todo: In demoSpec: Create "and" on the last element of the top "or"-Group
-
 */
 
 
@@ -1113,6 +1112,63 @@
     vc.clean();
     expect(vc.toQuery()).toEqual('');
   });
+
+  it('should flatten on import', function () {
+    var vc = KorAP.VirtualCollection.create().render({
+      "@type":"korap:docGroup",
+      "operation":"operation:or",
+      "operands":[
+	{
+	  "@type":"korap:docGroup",
+	  "operation":"operation:or",
+	  "operands":[
+            {
+              "@type":"korap:doc",
+              "key":"Titel",
+              "value":"Baum",
+              "match":"match:eq"
+            },
+            {
+              "@type":"korap:doc",
+              "key":"Veröffentlichungsort",
+              "value":"hihi",
+              "match":"match:eq"
+            },
+            {
+              "@type":"korap:docGroup",
+              "operation":"operation:or",
+              "operands":[
+		{
+		  "@type":"korap:doc",
+		  "key":"Titel",
+		  "value":"Baum",
+		  "match":"match:eq"
+		},
+		{
+		  "@type":"korap:doc",
+		  "key":"Veröffentlichungsort",
+		  "value":"hihi",
+		  "match":"match:eq"
+		}
+              ]
+            }
+	  ]
+	},
+	{
+	  "@type":"korap:doc",
+	  "key":"Untertitel",
+	  "value":"huhu",
+	  "match":"match:eq"
+	}
+      ]
+    });
+
+    expect(vc.toQuery()).toEqual(
+      'Titel = "Baum" | Veröffentlichungsort = "hihi" | Untertitel = "huhu"'
+    );
+
+
+  });
 });
 
 describe('KorAP.Operators', function () {
@@ -1784,3 +1840,51 @@
   });
 });
 
+describe('KorAP.Rewrite', function () {
+
+  it('should be initializable', function () {
+    var rewrite = KorAP.Rewrite.create({
+      "@type" : "korap:rewrite",
+      "operation" : "operation:modification",
+      "src" : "querySerializer",
+      "scope" : "tree"
+    });
+    expect(rewrite.toString()).toEqual('Modification of "tree" by "querySerializer"');
+  });
+
+  it('should be deserialized by docs', function () {
+    var doc = KorAP.Doc.create(undefined,
+      {
+        "@type":"korap:doc",
+        "key":"Titel",
+        "value":"Baum",
+        "match":"match:eq"
+      });
+
+    expect(doc.element().classList.contains('doc')).toBeTruthy();
+    expect(doc.element().classList.contains('rewritten')).toBe(false);
+
+    doc = KorAP.Doc.create(undefined,
+      {
+        "@type":"korap:doc",
+        "key":"Titel",
+        "value":"Baum",
+        "match":"match:eq",
+	"rewrites" : [
+	  {
+	    "@type" : "korap:rewrite",
+	    "operation" : "operation:modification",
+	    "src" : "querySerializer",
+	    "scope" : "tree"
+	  }
+	]
+      });
+
+    expect(doc.element().classList.contains('doc')).toBeTruthy();
+    expect(doc.element().classList.contains('rewritten')).toBeTruthy();
+  });
+/*
+  it('should be deserialized by docGroups', function () {
+  });
+*/
+});
diff --git a/public/js/src/hint.js b/public/js/src/hint.js
index 4f5a8b5..0b696d1 100644
--- a/public/js/src/hint.js
+++ b/public/js/src/hint.js
@@ -564,6 +564,9 @@
       this.item(this._list[this._list.length - 1]).noMore(bool);
     },
     _initList : function () {
+
+console.log("..." + this._items.length);
+
       if (this._list === undefined)
 	this._list = [];
       else if (this._list.length != 0) {
@@ -573,7 +576,7 @@
 
       this._offset = 0;
 
-      if (this.prefix.length <= 0) {
+      if (this.prefix().length <= 0) {
 	for (var i = 0; i < this._items.length; i++)
 	  this._list.push(i);
 	return true;
diff --git a/public/js/src/menu.js b/public/js/src/menu.js
index 3b79bdd..b251000 100644
--- a/public/js/src/menu.js
+++ b/public/js/src/menu.js
@@ -3,6 +3,388 @@
 (function (KorAP) {
   "use strict";
 
+  // 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 = {
+    /**
+     * Create new Menu based on the action prefix
+     * and a list of menu items.
+     *
+     * @this {Menu}
+     * @constructor
+     * @param {string} Context prefix
+     * @param {Array.<Array.<string>>} List of menu items
+     */
+    create : function (params) {
+      return Object.create(KorAP.Menu)._init(params);
+    },
+
+    _init : function (itemClass, params) {
+      // this._element.addEventListener("click", chooseHint, false);
+      this._itemClass = itemClass;
+      this._element = document.createElement("ul");
+      this._element.style.opacity = 0;
+
+      this.active = false;
+      this._items = new Array();
+      var i;
+      for (i in params) {
+	var obj = itemClass.create(params[i]);
+	this._items.push(
+	  obj
+	);
+      };
+      this._limit    = KorAP.menuLimit;
+      this._position = 0;  // position in the active list
+      this._active   = -1; // active item in the item list
+
+      this._reset();
+      return this;
+    },
+
+    element : function () {
+      return this._element;
+    },
+
+    itemClass : function () {
+      return this._itemClass;
+    },
+
+    /**
+     * Get and set numerical value for limit
+     */
+    limit : function (limit) {
+      if (arguments.length === 1)
+	this._limit = limit;
+      return this._limit;
+    },
+
+    /**
+     * 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;
+    },
+
+    _reset : function () {
+      this._offset = 0;
+      this._pos    = 0;
+      this._prefix = undefined;
+    },
+
+    /**
+     * Filter the list and make it visible
+     *
+     * @param {string} Prefix for filtering the list
+     */
+    show : function (prefix) {
+      this._prefix = prefix;
+
+      // Initialize the list
+      if (!this._initList())
+	return;
+
+      // show based on offset
+      this._showItems(0);
+
+      // Set the first element to active
+      this.liveItem(0).active(true);
+
+      this._position = 0;
+      this._active = this._list[0];
+
+      // Add classes for rolling menus
+      this._boundary(true);
+    },
+
+    /**
+     * Get a specific item from the complete list
+     *
+     * @param {number} index of the list item
+     */
+    item : function (index) {
+      return this._items[index]
+    },
+
+    _initList : function () {
+
+      if (this._list === undefined) {
+	this._list = [];
+      }
+      else if (this._list.length != 0) {
+	this._boundary(false);
+	this._list.length = 0;
+      };
+
+      // Offset is initially zero
+      this._offset = 0;
+
+      if (this.prefix().length <= 0) {
+	for (var i = 0; i < this._items.length; i++)
+	  this._list.push(i);
+	return true;
+      };
+
+      var pos;
+      var paddedPrefix = " " + this.prefix();
+
+      for (pos = 0; pos < this._items.length; pos++) {
+	if ((this.item(pos).lcField().indexOf(paddedPrefix)) >= 0)
+	  this._list.push(pos);
+      };
+
+      if (this._list.length == 0) {
+	for (pos = 0; pos < this._items.length; pos++) {
+	  if ((this.item(pos).lcField().indexOf(this.prefix())) >= 0)
+	    this._list.push(pos);
+	};
+      };
+
+      // Filter was successful
+      return this._list.length > 0 ? true : false;
+    },
+
+    // Set boundary for viewport
+    _boundary : function (bool) {
+      this.item(this._list[0]).noMore(bool);
+      this.item(this._list[this._list.length - 1]).noMore(bool);
+    },
+
+    /**
+     * Get the prefix for filtering,
+     * e.g. &quot;ve"&quot; for &quot;verb&quot;
+     */
+    prefix : function () {
+      return this._prefix || '';
+    },
+
+    _showItems : function (offset) {
+      this.delete();
+
+      // Use list
+      var shown = 0;
+      var i;
+      for (i in this._list) {
+
+	// Don't show - it's before offset
+	if (shown++ < offset)
+	  continue;
+
+	this._append(this._list[i]);
+
+	if (shown >= (this.limit() + this._offset))
+	  break;
+      };
+    },
+
+    /**
+     * Delete all visible items from the menu element
+     */
+    delete : function () {
+      var child;
+      for (var i = 0; i <= this.limit(); i++) {
+
+	if (child = this.shownItem(i))
+	  child.lowlight();
+      };
+
+      while (child = this._element.firstChild)
+	this._element.removeChild(child);
+    },
+
+
+    // Append item to the shown list based on index
+    _append : function (i) {
+      var item = this.item(i);
+
+      // Highlight based on prefix
+      if (this.prefix().length > 0)
+	item.highlight(this.prefix());
+
+      // Append element
+      this.element().appendChild(item.element());
+    },
+
+
+    /**
+     * Get a specific item from the filtered list
+     *
+     * @param {number} index of the list item
+     */
+    liveItem : function (index) {
+      if (this._list === undefined)
+	if (!this._initList())
+	  return;
+
+      return this._items[this._list[index]];
+    },
+
+    length : function () {
+      return this._items.length;
+    },
+
+
+    /**
+     * Get a specific item from the visible list
+     *
+     * @param {number} index of the list item
+     */
+    shownItem : function (index) {
+      if (index >= this.limit())
+	return;
+      return this.liveItem(this._offset + index);
+    },
+
+
+    /*
+     * Make the next item in the menu active
+     */
+    next : function () {
+      // No active element set
+      if (this._position == -1)
+	return;
+
+      // Set new live item
+      var oldItem = this.liveItem(this._position++);
+      oldItem.active(false);
+      var newItem = this.liveItem(this._position);
+
+      // The next element is undefined - roll to top
+      if (newItem === undefined) {
+	this._offset = 0;
+	this._position = 0;
+	newItem = this.liveItem(0);
+	this._showItems(0);
+      }
+
+      // The next element is outside the view - roll down
+      else if (this._position >= (this.limit + this._offset)) {
+	this._removeFirst();
+	this._offset++;
+	this._append(this._list[this._position]);
+      };
+      newItem.active(true);
+    },
+
+
+    /*
+     * Make the previous item in the menu active
+     */
+/*
+    prev : function () {
+      if (this._position == -1)
+	return;
+
+      // Set new live item
+      var oldItem = this.liveItem(this._position--);
+      oldItem.active(false);
+      var newItem = this.liveItem(this._position);
+
+      // The previous element is undefined - roll to bottom
+      if (newItem === undefined) {
+	this._position = this.liveLength - 1;
+	newItem = this.liveItem(this._position);
+	this._offset = this.liveLength - this.limit;
+	this._showItems(this._offset);
+      }
+
+      // The previous element is outside the view - roll up
+      else if (this._position < this._offset) {
+	this._removeLast();
+	this._offset--;
+	this._prepend(this._list[this._position]);
+      };
+      newItem.active(true);
+    },
+*/
+
+
+    /**
+     * Get the context of the menue,
+     * e.g. &quot;tt/&quot; for the tree tagger menu
+     */
+/*
+    get context () {
+      return this._context;
+    },
+*/
+/*
+    get liveLength () {
+      if (this._list === undefined)
+	this._initList();
+      return this._list.length;
+    },
+*/
+/*
+    chooseHint : function (e) {
+      var element = e.target;
+      while (element.nodeName == "STRONG" || element.nodeName == "SPAN")
+	element = element.parentNode;
+
+      if (element === undefined || element.nodeName != "LI")
+	return;
+
+      var action = element.getAttribute('data-action');
+      hint.insertText(action);
+      var menu = hint.menu();
+      menu.hide();
+
+      // Fill this with the correct value
+      var show;
+      if ((show = hint.analyzeContext()) != "-") {
+	menu.show(show);
+	menu.update(
+	  hint._search.getBoundingClientRect().right
+	);
+      };
+
+      hint._search.focus();
+    },
+
+    _removeFirst : function () {
+      this.item(this._list[this._offset]).lowlight();
+      this._element.removeChild(this._element.firstChild);
+    },
+
+    _removeLast : function () {
+      this.item(this._list[this._offset + this.limit - 1]).lowlight();
+      this._element.removeChild(this._element.lastChild);
+    },
+
+
+    // Prepend item to the shown list based on index
+    _prepend : function (i) {
+      var item = this.item(i);
+
+      // Highlight based on prefix
+      if (this.prefix.length > 0)
+	item.highlight(this.prefix);
+
+      // Append element
+      this.element.insertBefore(
+	item.element,
+	this.element.firstChild
+      );
+    },
+*/
+
+  };
+
+
+
+
   /**
    * Item in the Dropdown menu
    */
@@ -107,7 +489,10 @@
      * @param {string} Prefix string for highlights
      */
     highlight : function (prefix) {
-      this._highlight(this.element().firstChild, 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
@@ -185,6 +570,7 @@
 
     // Initialize menu item
     _init : function (params) {
+
       if (params[0] === undefined)
 	throw new Error("Missing parameters");
 
diff --git a/public/js/src/vc.js b/public/js/src/vc.js
index b68f0aa..d226fc4 100644
--- a/public/js/src/vc.js
+++ b/public/js/src/vc.js
@@ -1,11 +1,16 @@
+/**
+ * Create virtual collections with a visual user interface.
+ *
+ * @author Nils Diewald
+ */
+
 var KorAP = KorAP || {};
 
-// TODO: Implement a working localization solution!
-// TODO: Remove "and" or "or" in case it's followed
-//       by an unspecified document
-// TODO: Add 'or' or 'and' on root
-
 /*
+  TODO: Implement a working localization solution!
+  TODO: Disable "and" or "or" in case it's followed
+        by an unspecified document
+
   Error codes:
   701: "JSON-LD group has no @type attribute" 
   704: "Operation needs operand list"
@@ -18,6 +23,8 @@
   811: "Document group expects operation" (like 703) 
   812: "Operand not supported in document group" (like 744)
   813: "Collection type is not supported" (like 713)
+  814: "Unknown rewrite operation"
+  815: "Rewrite expects source"
 */
 
 (function (KorAP) {
@@ -28,11 +35,12 @@
     console.log(type + ": " + msg);
   };
 
-  KorAP._validStringMatchRE = new RegExp("^(?:eq|ne|contains)$");
+  KorAP._validStringMatchRE = new RegExp("^(?:eq|ne|contains|excludes)$");
   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');
 
   // Localization values
@@ -60,7 +68,6 @@
   // Add new unspecified document
   KorAP._add = function (obj, type) {
     var ref = obj.parentNode.refTo;
-    console.log("DEBUG: " + type + " on " + ref.toQuery());
     var parent = ref.parent();
 
     if (ref.ldType() === 'docGroup') {
@@ -107,7 +114,6 @@
   // Remove doc or docGroup
   KorAP._delete = function () {
     var ref = this.parentNode.refTo;
-console.log("DEBUG: delete " + ref.toQuery());
     if (ref.parent().ldType() !== null) {
       return ref.parent().delOperand(ref).update();
     }
@@ -413,7 +419,7 @@
       if (parent !== undefined)
 	obj._parent = parent;
 
-      obj._changed = true;
+      obj.__changed = true;
       return obj;
     },
 
@@ -425,7 +431,12 @@
       var e = this._element;
 
       // Check if there is a change
-      if (this._changed) {
+      if (this.__changed) {
+
+	// Was rewritten
+	if (this.rewrites() !== undefined) {
+	  e.classList.add("rewritten");
+	};
 
 	// Added key
 	var key = document.createElement('span');
@@ -458,7 +469,11 @@
 	e.appendChild(matchop);
 	e.appendChild(value);
 
-	this._changed = false;
+	this.__changed = false;
+      };
+
+      if (this._rewrites !== undefined) {
+	e.appendChild(this._rewrites.element());
       };
 
       if (this._parent !== undefined) {
@@ -502,7 +517,7 @@
       if (json === undefined)
 	return this;
 
-      if (json["@type"] !== "korap:doc") {
+      if (json["@type"] === undefined) {
 	KorAP.log(701, "JSON-LD group has no @type attribute");
 	return;
       };
@@ -595,15 +610,20 @@
 	  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 = true;
+	this._changed();
 	return this;
       };
       return this._key;
@@ -612,7 +632,7 @@
     matchop : function (match) {
       if (arguments.length === 1) {
 	this._matchop = match.replace(/^match:/, '');
-	this._changed = true;
+	this._changed();
 	return this;
       };
       return this._matchop || "eq";
@@ -621,7 +641,7 @@
     type : function (type) {
       if (arguments.length === 1) {
 	this._type = type;
-	this._changed = true;
+	this._changed();
 	return this;
       };
       return this._type || "string";
@@ -630,12 +650,27 @@
     value : function (value) {
       if (arguments.length === 1) {
 	this._value = value;
-	this._changed = true;
+	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 {};
@@ -664,6 +699,9 @@
       case "contains":
 	string += '~';
 	break;
+      case "excludes":
+	string += '!~';
+	break;
       case "geq":
 	string += 'since';
 	break;
@@ -720,6 +758,23 @@
       };
     },
 
+    // 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
@@ -732,7 +787,9 @@
       };
 
       switch (operand["@type"]) {
+
       case undefined:
+	// No @type defined
 	if (operand["ldType"] !== undefined) {
 	  if (operand.ldType() !== 'doc' &&
 	      operand.ldType() !== 'docGroup') {
@@ -741,8 +798,13 @@
 	  };
 	  // Be aware of cyclic structures!
 	  operand.parent(this);
-	  this._operands.push(operand);
-	  return operand;
+
+	  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");
@@ -753,14 +815,33 @@
 	var doc = KorAP.Doc.create(this, operand);
 	if (doc === undefined)
 	  return;
-	this._operands.push(doc);
-	return doc;
+	var dupl = this._duplicate(doc);
+	if (dupl === null) {
+	  this._operands.push(doc);
+	  return doc;
+	};
+	return dupl;
 
       case "korap: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;
 
@@ -875,15 +956,16 @@
 	    newOp.parent(this);
 	  }
 
-	  // Flatten the group
+	  // Flatten group
 	  else {
 	    // Remove old group
 	    this._operands.splice(i, 1);
 
 	    // Inject new operands
 	    for (var op in newOp.operands().reverse()) {
-	      this._operands.splice(i, 0, newOp.getOperand(op));
-	      newOp.getOperand(0).parent(this);
+	      op = newOp.getOperand(op);
+	      this._operands.splice(i, 0, op);
+	      op.parent(this);
 	    };
 	    // Prevent destruction of operands
 	    newOp._operands = [];
@@ -920,7 +1002,7 @@
       if (json === undefined)
 	return this;
 
-      if (json["@type"] !== "korap:docGroup") {
+      if (json["@type"] === undefined) {
 	KorAP.log(701, "JSON-LD group has no @type attribute");
 	return;
       };
@@ -984,11 +1066,157 @@
   };
 
 
+  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,
+    __changed : false,
 
     create : function () {
       return Object.create(KorAP.JsonLD);
@@ -1014,14 +1242,14 @@
     parent : function (obj) {
       if (arguments.length === 1) {
 	this._parent = obj;
-	this._changed = true;
+	this.__changed = true;
       };
       return this._parent;
     },
 
     // Destroy object - especially for
     // acyclic structures!
-    // I'm a paranoid chicken!
+    // I'm paranoid!
     destroy : function () {
       if (this._ops != undefined) {
 	this._ops._parent = undefined;