Merge "Make doc navigation extensible"
diff --git a/.gitignore b/.gitignore
index 3f541a0..07d17ee 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,4 +36,5 @@
.*
t/kalamar_user_client.t
!.gitignore
-npm-debug.log
\ No newline at end of file
+npm-debug.log
+package-lock.json
\ No newline at end of file
diff --git a/Changes b/Changes
index 466e752..8d88e0a 100755
--- a/Changes
+++ b/Changes
@@ -1,3 +1,7 @@
+0.32 2018-12-19
+ - Support attachements in metadata fields (#77).
+ - Added ping request option to Piwik.
+
0.31 2018-11-30
- Update to Mojolicious >= 8.06.
- Made Authentication/Authorization a separated Kalamar::Plugin::Auth
diff --git a/Makefile.PL b/Makefile.PL
index 155b0c5..3768bdd 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -20,13 +20,14 @@
'Mojolicious::Plugin::ClosedRedirect' => 0.14,
'Mojolicious::Plugin::Notifications' => 1.01,
'Mojolicious::Plugin::MailException' => 0.20,
+ 'Mojolicious::Plugin::Util::RandomString' => 0.08,
'Mojolicious::Plugin::CHI' => 0.20,
'Mojolicious::Plugin::ClientIP' => 0.01,
'Cache::FastMmap' => 0,
'Mojo::JWT' => 0.05,
# Required for bundled plugins
- 'Mojolicious::Plugin::Piwik' => 0.24,
+ 'Mojolicious::Plugin::Piwik' => 0.25,
# Currently on GitHub only (github.com/akron)
'Mojolicious::Plugin::Localize' => 0.20,
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 {
diff --git a/lib/Kalamar/Plugin/Piwik.pm b/lib/Kalamar/Plugin/Piwik.pm
index bffbbdb..9b0c909 100644
--- a/lib/Kalamar/Plugin/Piwik.pm
+++ b/lib/Kalamar/Plugin/Piwik.pm
@@ -4,11 +4,29 @@
sub register {
my ($plugin, $mojo, $param) = @_;
+ # Load parameter from Config file
+ if (my $config_param = $mojo->config('Kalamar')) {
+ if ($config_param->{Piwik}) {
+ $param = {
+ %$param,
+ %{$config_param->{Piwik}}
+ };
+ };
+ };
+
# Load Piwik if not yet loaded
unless (exists $mojo->renderer->helpers->{piwik_tag}) {
$mojo->plugin('Piwik');
};
+ # Add random string plugin
+ $mojo->plugin('Util::RandomString' => {
+ piwik_rand_id => {
+ alphabet => '0123456789abcdef',
+ length => 16
+ }
+ });
+
# Add opt-out to FAQ
$mojo->content_block(
'faq' => {
@@ -39,6 +57,39 @@
% }
SCRIPT
});
+
+
+ # If all requests should be pinged,
+ # establish this hook
+ if ($param->{ping_requests}) {
+ $mojo->hook(
+ after_render => sub {
+ my $c = shift;
+
+ # Only track valid routes
+ my $route = $c->current_route or return;
+
+ # This won't forward personalized information
+ my $hash = {
+ action_url => $c->url_for->to_abs,
+ action_name => $route,
+ ua => '',
+ urlref => '',
+ send_image => 0,
+ dnt => 0,
+ uid => $c->random_string('piwik_rand_id')
+ };
+
+ # Overrid ping site id
+ if ($param->{ping_site_id}) {
+ $hash->{idsite} = $param->{ping_site_id}
+ };
+
+ # Send track
+ $c->piwik->api_p(Track => $hash)->wait;
+ }
+ );
+ };
};
@@ -46,3 +97,13 @@
__END__
+
+# Parameters can be loaded from
+# {
+# Kalamar => {
+# Piwik => {
+# ping_requests => 1,
+# ping_site_id => 12
+# }
+# }
+# }
diff --git a/templates/doc/ql.html.ep b/templates/doc/ql.html.ep
index 4b57ac1..58aff58 100644
--- a/templates/doc/ql.html.ep
+++ b/templates/doc/ql.html.ep
@@ -10,7 +10,7 @@
<section id="intro">
<h3>Frontend Features</h3>
- <p>This frontend differs to the <%= doc_ext_link_to 'official frontend', 'http://korap.ids-mannheim.de/app/', target => '_blank', rel => 'noopener noreferrer' %> by providing a serialization view, an integrated tutorial, a comparison view for morphological annotations, and an autocompletion for closed annotations (type in <%= doc_link_to 'foundry prefixes', 'data', 'annotation' %> like <code>cnx/</code>).</p>
+ <p>This frontend differs to the <%= doc_ext_link_to 'official frontend', 'http://korap.ids-mannheim.de/app/', target => '_blank', rel => 'noopener noreferrer' %> by providing a serialization view, an integrated tutorial, a comparison view for morphological annotations, and an autocompletion for closed annotations (type in <%= doc_link_to 'foundry prefixes', 'data', 'annotation' %> like <code>tt/</code>).</p>
</section>
<section id="examples">
@@ -29,11 +29,11 @@
<p><em>Be aware</em>: Minor incompatibilities with implemented languages may be announced with warnings.</p>
%= doc_query cosmas2 => 'd* MORPH(mate/p=ADJA) $Baum #IN #ELEM(s)'
- <p><strong><%= doc_link_to 'Poliqarp+', 'ql', 'poliqarp-plus' %></strong>: Find all nominal phrases as annotated using Connexor, that contain an adverb as annotated by OpenNLP, that is annotated as something starting with an "A" using regular expressions in Treetagger.</p>
- %= doc_query poliqarp => 'contains(<cnx/c=np>,{[opennlp/p=ADV & tt/p="A.*"]})', cutoff => 1
+ <p><strong><%= doc_link_to 'Poliqarp+', 'ql', 'poliqarp-plus' %></strong>: Find all nominal phrases as annotated using CoreNLP, that contain an adverb as annotated by OpenNLP, that is annotated as something starting with an "A" using regular expressions in Treetagger.</p>
+ %= doc_query poliqarp => 'contains(<corenlp/c=NP>,{[opennlp/p=ADV & tt/p="A.*"]})', cutoff => 1
- <p><strong><%= doc_link_to 'Poliqarp+', 'ql', 'poliqarp-plus' %></strong>: Find all sentences as annotated by the base foundry that start with a sequence of one token in present tense as annotated by Connexor and the lemma "die" annotated by the <%= doc_link_to 'default foundry', 'data', 'annotation' %>. Highlight both terms of the sequence.</p>
- %= doc_query poliqarp => 'startswith(<base/s=s>, {1:[cnx/m=PRES]}{2:[base=die]})', cutoff => 1
+ <p><strong><%= doc_link_to 'Poliqarp+', 'ql', 'poliqarp-plus' %></strong>: Find all sentences as annotated by the base foundry that start with a sequence of one token in present tense as annotated by Marmot and the lemma "die" annotated by the <%= doc_link_to 'default foundry', 'data', 'annotation' %>. Highlight both terms of the sequence.</p>
+ %= doc_query poliqarp => 'startswith(<base/s=s>, {1:[marmot/m=tense:pres]}{2:[base=die]})', cutoff => 1
<p><strong><%= doc_link_to 'Poliqarp+', 'ql', 'poliqarp-plus' %></strong>: Find all sequences of an article, followed by three to four adjectives and a noun as annotated by the Treetagger foundry, that finish a sentence. Highlight all parts of the sequence.</p>
%= doc_query poliqarp => 'focus(3:endswith(<base/s=s>,{3:[tt/p=ART]{1:{2:[tt/p=ADJA]{3,4}}[tt/p=NN]}}))', cutoff => 1