Support attachements in metadata fields (fixes #77)

Change-Id: Ia5be6ec851ef318d89b115ac814663dc32da3484
diff --git a/dev/demo/matchdemo.js b/dev/demo/matchdemo.js
index f7fd779..4264783 100644
--- a/dev/demo/matchdemo.js
+++ b/dev/demo/matchdemo.js
@@ -593,6 +593,12 @@
         },
         {
           "@type":"koral:field",
+          "key":"link",
+          "type":"type:attachement",
+          "value":"data:application/x.korap-link;title=Wikipedia,https://de.wikipedia.org/wiki/Beispiel"
+        },
+        {
+          "@type":"koral:field",
           "key":"docTitle",
           "type":"type:text",
           "value":"Wikipedia, Artikel mit Anfangszahl 2, Teil 64"
diff --git a/dev/js/spec/matchSpec.js b/dev/js/spec/matchSpec.js
index b27ddbd..04e188e 100644
--- a/dev/js/spec/matchSpec.js
+++ b/dev/js/spec/matchSpec.js
@@ -9,6 +9,7 @@
         'match/treeitem',
         'match/treearc',
         'buttongroup/menu',
+        'match/attachement',
         'hint/foundries/cnx',
         'hint/foundries/mate'], function (
           matchClass,
@@ -19,7 +20,8 @@
           infoClass,
           matchTreeItemClass,
           matchRelClass,
-          matchTreeMenuClass) {
+          matchTreeMenuClass,
+          attachementClass) {
 
   var available = [
     'base/s=spans',
@@ -77,7 +79,14 @@
         "opennlp\/morpho",
         "opennlp\/sentences"
       ]
-    }
+    },
+    {
+      "@type": "koral:field",
+      "key": "xlink",
+      "type": "type:attachement",
+      "value": "data:application/x.korap-link;title=Cool,https://de.wikipedia.org/wiki/Beispiel"
+    },
+
   ];
 
 
@@ -741,7 +750,18 @@
 		  expect(mel.children[2].children[1].children[1].tagName).toEqual('DIV');
 		  expect(mel.children[2].children[1].children[1].firstChild.nodeValue).toEqual('film');		 
 	  }); 
-  
+
+    it('attachements should be formatted', function(){
+		  //type:attachement
+      expect(mel.children[3].children[1].getAttribute('data-type')).toEqual('type:attachement')
+		  expect(mel.children[3].children[1].classList.contains('metakeyvalues')).toBeFalsy;
+		  expect(mel.children[3].children[0].firstChild.nodeValue).toEqual('xlink');
+		  expect(mel.children[3].children[1].firstChild.textContent).toEqual('Cool');
+		  expect(mel.children[3].children[1].firstChild.tagName).toEqual('A');
+      expect(mel.children[3].children[1].firstChild.getAttribute('href')).toEqual('https://de.wikipedia.org/wiki/Beispiel');
+	  }); 
+ 
+    
   
 	  // Meta information should be sorted alphabetically
     it('should be alphabetically sorted', function(){
@@ -750,7 +770,58 @@
   	  var c = mel.children[2].children[0].firstChild.nodeValue;
   	  expect(a.localeCompare(b)).toBe(-1);
   	  expect(b.localeCompare(c)).toBe(-1);
-    });  
+    });
+
+
+    it('should handle attachements', function () {
+      let uri = attachementClass.create("data:text/plain;title=new,Hallo");
+      expect(uri.contentType).toEqual("text/plain");
+
+      expect(uri.payload).toEqual("Hallo");
+      expect(uri.base64).toBeFalsy();
+      expect(uri.isLink).toBeFalsy();
+      expect(uri.param["title"]).toEqual("new");
+
+      uri = attachementClass.create("data:application/x.korap-link,https://de.wikipedia.org/wiki/Beispiel");
+      expect(uri.contentType).toEqual("application/x.korap-link");
+      expect(uri.payload).toEqual("https://de.wikipedia.org/wiki/Beispiel");
+      expect(uri.base64).toBeFalsy();
+      expect(uri.isLink).toBeTruthy();
+      expect(uri.inline().textContent).toEqual("https://de.wikipedia.org/wiki/Beispiel");
+      expect(uri.inline().nodeType).toEqual(1);
+      expect(uri.inline().tagName).toEqual("A");
+      expect(uri.inline().getAttribute("rel")).toEqual("noopener noreferrer");
+
+
+      uri = attachementClass.create("data:application/x.korap-link;title=Das ist ein Titel,https://de.wikipedia.org/wiki/Beispiel");
+      expect(uri.contentType).toEqual("application/x.korap-link");
+      expect(uri.payload).toEqual("https://de.wikipedia.org/wiki/Beispiel");
+      expect(uri.base64).toBeFalsy();
+      expect(uri.isLink).toBeTruthy();
+      expect(uri.inline().textContent).toEqual("Das ist ein Titel");
+      expect(uri.inline().nodeType).toEqual(1);
+      expect(uri.inline().tagName).toEqual("A");
+      expect(uri.inline().getAttribute("rel")).toEqual("noopener noreferrer");
+
+      
+      uri = attachementClass.create("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D");
+      expect(uri.contentType).toEqual("text/plain");
+      expect(uri.payload).toEqual("Hello, World!");
+      expect(uri.base64).toBeTruthy();
+      expect(uri.isLink).toBeFalsy();
+      expect(uri.inline().nodeType).toEqual(3);
+      expect(uri.inline().textContent).toEqual("Hello, World!");
+
+      uri = attachementClass.create("data:text/plain;title= new ; subTitle = old ;base64,SGVsbG8sIFdvcmxkIQ%3D%3D");
+      expect(uri.contentType).toEqual("text/plain");
+      expect(uri.payload).toEqual("Hello, World!");
+      expect(uri.param["title"]).toEqual("new");
+      expect(uri.param["subTitle"]).toEqual("old");
+      expect(uri.base64).toBeTruthy();
+      expect(uri.isLink).toBeFalsy();
+      expect(uri.inline().nodeType).toEqual(3);
+      expect(uri.inline().textContent).toEqual("Hello, World!");
+    });
   });
   // table = view.toTable();
   // table.sortBy('');
diff --git a/dev/js/src/match/attachement.js b/dev/js/src/match/attachement.js
new file mode 100644
index 0000000..4548703
--- /dev/null
+++ b/dev/js/src/match/attachement.js
@@ -0,0 +1,86 @@
+/**
+ * Parse Data URI scheme for attachement fields
+ * Afterwards the object has the parameters
+ * - contentType (defaults to text/plain)
+ * - base64 (if the data was base64 encoded)
+ * - isLink (if the contentType is application/x.korap-link)
+ * - param (as a map of arbitrary parameters)
+ * - payload (the URI decoded data)
+ *
+ * @author Nils Diewald
+ */
+define(function () {
+  const uriRE = new RegExp("^data: *([^;,]+?(?: *; *[^,;]+?)*) *, *(.+)$");
+  const mapRE = new RegExp("^ *([^=]+?) *= *(.+?) *$");
+
+  return {
+
+    /**
+     * Constructor
+     */
+    create : function (url) {
+      return Object.create(this)._init(url);
+    },
+
+    // Parse URI scheme
+    _init : function (url) {
+
+      // Decode
+      url = decodeURIComponent(url);
+
+      if (!uriRE.exec(url))
+        return;
+
+      this.payload = RegExp.$2;
+
+      let map = {};
+      let start = 0;
+      this.base64 = false;
+      this.isLink = false;
+      this.contentType = "text/plain";
+
+      // Split parameter map
+      RegExp.$1.split(/ *; */).map(function (item) {
+
+        // Check first parameter
+        if (!start++ && item.match(/^[-a-z0-9]+?\/.+$/)) {
+          this.contentType = item;
+
+          if (item === "application/x.korap-link")
+            this.isLink = true;
+        }
+       
+        // Decode b64
+        else if (item.toLowerCase() == "base64") {
+          this.base64 = true;
+          this.payload = window.atob(this.payload);
+        }
+
+        // Parse arbitrary metadata
+        else if (mapRE.exec(item)) {
+          map[RegExp.$1] = RegExp.$2;
+        };
+      }.bind(this));
+
+      this.param = map;
+      return this;
+    },
+
+    /**
+     * Inline the attachement
+     * This should optimally be plugin-treatable
+     */ 
+    inline : function () {
+      if (this.isLink) {
+        let title = this.param["title"] || this.payload;
+        let a = document.createElement('a');
+        a.setAttribute('href', this.payload);
+        a.setAttribute('rel', 'noopener noreferrer');
+        a.addT(title);
+        return a;
+      };
+
+      return document.createTextNode(this.payload);
+    }
+  }
+});
diff --git a/dev/js/src/match/corpusByMatch.js b/dev/js/src/match/corpusByMatch.js
index a405206..43048a3 100644
--- a/dev/js/src/match/corpusByMatch.js
+++ b/dev/js/src/match/corpusByMatch.js
@@ -106,7 +106,7 @@
       };
 
       // Ignore stored types
-      if (type === "type:store")
+      if (type === "type:store" || type === "type:attachement")
         return;
 
       type = type || "type:string";
diff --git a/dev/js/src/match/meta.js b/dev/js/src/match/meta.js
index e273a39..c4cd63f 100644
--- a/dev/js/src/match/meta.js
+++ b/dev/js/src/match/meta.js
@@ -1,4 +1,4 @@
-define(['match/corpusByMatch','util'], function (cbmClass) {
+define(['match/corpusByMatch','match/attachement','util'], function (cbmClass, attClass) {
 
   // Localization values
   const loc   = KorAP.Locale;
@@ -83,15 +83,30 @@
           let metaDescr = field["value"];
           metaDD = metaL.addE('dd');
           metaDD.setAttribute('data-type', field["type"]);
-          
+
           if(metaDescr instanceof Array){
         	  metaDD.classList.add("metakeyvalues");  
-        	  for(i = 0; i < metaDescr.length; i++){
-        	    metaDD.addE('div').addT(metaDescr[i]);
+        	  for (i = 0; i < metaDescr.length; i++){
+
+              if (field["type"] === 'type:attachement') {
+                let att = attClass.create(metaDescr[i]);
+                if (att)
+        	        metaDD.addE('div').appendChild(att.inline());
+              }
+              else {
+        	      metaDD.addE('div').addT(metaDescr[i]);
+              }
         	  } 
           }
           else{
-            metaDD.addT(field["value"]);
+            if (field["type"] === 'type:attachement') {
+              let att = attClass.create(field["value"]);
+              if (att)
+              metaDD.appendChild(att.inline());
+            }
+            else {
+              metaDD.addT(field["value"]);
+            };
           }
           
           metaDL.appendChild(metaL);
diff --git a/dev/scss/main/matchinfo.scss b/dev/scss/main/matchinfo.scss
index e0fe6f9..9d3bf50 100644
--- a/dev/scss/main/matchinfo.scss
+++ b/dev/scss/main/matchinfo.scss
@@ -228,11 +228,16 @@
       background-color: $light-orange;
       cursor: pointer;
     }
-    > dd[data-type="type:store"] {
+    > dd[data-type="type:store"],
+    > dd[data-type="type:attachement"]{
       background-color: $middle-orange;
       cursor: default;
-    }
 
+      a {
+        color: inherit;
+      }
+    }
+    
     > dd.metakeyvalues {
       padding:0;
       > div {