Add pipe mechanism

Change-Id: I4207b33b1e4bab06ee632fd17be68542891caee3
diff --git a/Changes b/Changes
index 2ae4191..628728f 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.37 2019-12-05
+0.37 2019-12-09
         - Removed deprecated 'kalamar_test_port' helper.
         - Separated KalamarHelpers and KalamarPages.
         - Renamed 'doc_link_to' to 'embedded_link_to'
@@ -19,6 +19,7 @@
         - Added result panel for plugin registration (hebasta).
         - Added state object.
         - Added toggle button to buttongroup.
+        - Added pipe object to implement KoralPipes.
 
 0.36 2019-09-19
         - Rename all cookies to be independent
diff --git a/dev/js/runner/all.html b/dev/js/runner/all.html
index 1d3349c..48e7353 100644
--- a/dev/js/runner/all.html
+++ b/dev/js/runner/all.html
@@ -49,7 +49,8 @@
         'spec/vcSpec',
         'spec/tourSpec',
         'spec/utilSpec',
-        'spec/stateSpec'
+        'spec/stateSpec',
+        'spec/pipeSpec'
       ],
       function () {
         window.onload();
diff --git a/dev/js/spec/pipeSpec.js b/dev/js/spec/pipeSpec.js
new file mode 100644
index 0000000..46cd5b9
--- /dev/null
+++ b/dev/js/spec/pipeSpec.js
@@ -0,0 +1,92 @@
+define(['pipe'], function (pipeClass) {
+
+  describe('KorAP.Pipe', function () {
+    it('should be initializable', function () {
+      let p = pipeClass.create();
+      expect(p.size()).toEqual(0);
+    });
+
+    it('should be appendable', function () {
+      let p = pipeClass.create();
+      expect(p.size()).toEqual(0);
+      expect(p.toString()).toEqual('');
+      p.append('service1');
+      expect(p.size()).toEqual(1);
+      expect(p.toString()).toEqual('service1');
+      p.append('service2');
+      expect(p.size()).toEqual(2);
+      expect(p.toString()).toEqual('service1,service2');
+
+      p.append('');
+      expect(p.size()).toEqual(2);
+      expect(p.toString()).toEqual('service1,service2');
+    });
+    
+    it('should be prependable', function () {
+      let p = pipeClass.create();
+      expect(p.size()).toEqual(0);
+      expect(p.toString()).toEqual('');
+      p.prepend('service1');
+      expect(p.size()).toEqual(1);
+      expect(p.toString()).toEqual('service1');
+      p.prepend('service2');
+      expect(p.size()).toEqual(2);
+      expect(p.toString()).toEqual('service2,service1');
+
+      p.prepend('');
+      expect(p.size()).toEqual(2);
+      expect(p.toString()).toEqual('service2,service1');
+    });
+
+    it('should be trimmed', function () {
+      let p = pipeClass.create();
+      expect(p.size()).toEqual(0);
+      expect(p.toString()).toEqual('');
+      p.prepend('  service1  ');
+      expect(p.size()).toEqual(1);
+      expect(p.toString()).toEqual('service1');
+
+      p.prepend("\t service2 \t");
+      expect(p.size()).toEqual(2);
+      expect(p.toString()).toEqual('service2,service1');
+
+      p.append('      ');
+      expect(p.size()).toEqual(2);
+      expect(p.toString()).toEqual('service2,service1');
+    });
+    
+    it('should be deletable', function () {
+      let p = pipeClass.create();
+      p.append('service1');
+      p.append('service2');
+      p.append('service3');
+      expect(p.toString()).toEqual('service1,service2,service3');
+      p.remove('service2')
+      expect(p.toString()).toEqual('service1,service3');
+
+      p = pipeClass.create();
+      p.append('service1');
+      p.append('service2');
+      p.append('service3');
+      expect(p.toString()).toEqual('service1,service2,service3');
+      p.remove('service1')
+      expect(p.toString()).toEqual('service2,service3');
+
+      p = pipeClass.create();
+      p.append('service1');
+      p.append('service2');
+      p.append('service3');
+      expect(p.toString()).toEqual('service1,service2,service3');
+      p.remove('service3')
+      expect(p.toString()).toEqual('service1,service2');      
+
+      p = pipeClass.create();
+      p.append('service1');
+      p.append('service2');
+      p.append('service3');
+      expect(p.toString()).toEqual('service1,service2,service3');
+      p.remove('service0')
+      expect(p.toString()).toEqual('service1,service2,service3');      
+    });
+  });
+});
diff --git a/dev/js/src/init.js b/dev/js/src/init.js
index 982929c..109fd48 100644
--- a/dev/js/src/init.js
+++ b/dev/js/src/init.js
@@ -355,9 +355,12 @@
 
         if (vc !== undefined) {
           input.value = vc.toQuery();
+          if (input.value == '')
+            input.removeAttribute('name');
         }
         else {
-          delete input['value'];
+          input.removeAttribute('value');
+          input.removeAttribute('name');
         };
       });
     };
diff --git a/dev/js/src/pipe.js b/dev/js/src/pipe.js
new file mode 100644
index 0000000..7b9afee
--- /dev/null
+++ b/dev/js/src/pipe.js
@@ -0,0 +1,76 @@
+/**
+ * Create a pipe object, that holds a list of services
+ * meant to transform the KQ passed before it's finally
+ * passed to the search engine.
+ *
+ * @author Nils Diewald
+ */
+define(function () {
+
+  "use strict";
+
+  const notNullRe = new RegExp("[a-zA-Z0-9]");
+
+  // Trim and check
+  function _notNull (service) {
+    service = service.trim();
+    if (service.match(notNullRe)) {
+      return service;
+    };
+    return null;
+  }
+  
+  return {
+
+    /**
+     * Constructor
+     */
+    create : function () {
+      let obj = Object.create(this);
+      obj._pipe = [];
+      return obj;
+    },
+
+    /**
+     * Append service to pipe.
+     */
+    append : function (service) {
+      service = _notNull(service);
+      if (service)
+        this._pipe.push(service);
+    },
+
+    /**
+     * Prepend service to pipe.
+     */
+    prepend : function (service) {
+      service = _notNull(service);
+      if (service)
+        this._pipe.unshift(service);
+    },
+
+    /**
+     * Remove service from the pipe.
+     */
+    remove : function (service) {
+      let i = this._pipe.indexOf(service);
+      if (i != -1) {
+        this._pipe.splice(i, 1);
+      };
+    },
+
+    /**
+     * The number of services in the pipe.
+     */
+    size : function () {
+      return this._pipe.length;
+    },
+
+    /**
+     * Return the pipe as a string.
+     */
+    toString : function () {
+      return this._pipe.join(',');
+    }
+  };
+});