Make unexpanded KWIC metadata fields configurable by request URL
Resolves #275
Change-Id: I432ee8b2d6091012c8fa7138a113527644398e26
diff --git a/Changes b/Changes
index ccdc780..a6e8049 100644
--- a/Changes
+++ b/Changes
@@ -15,6 +15,7 @@
- Add data-testid to glimpse (diewald)
- Update dependencies (diewald)
- Support combined toggle+widget button for plugins (diewald)
+ - Make KWIC metadata fields configurable by request URL (kupietz)
0.64 2026-02-14
- Improve 'Plugins' mounting (diewald)
diff --git a/dev/scss/main/kwic.scss b/dev/scss/main/kwic.scss
index f6313b0..4427a79 100644
--- a/dev/scss/main/kwic.scss
+++ b/dev/scss/main/kwic.scss
@@ -83,6 +83,8 @@
}
div.meta {
+ display: flex;
+ align-items: center;
flex: 1 0;
min-width: 12em;
max-width: 15em;
@@ -98,6 +100,17 @@
font-size: 75%;
padding: 0 5pt;
+ > span, > ul.meta-list {
+ min-width: 0;
+ width: 100%;
+ }
+
+ ul.meta-list {
+ list-style-type: disc;
+ margin: 0;
+ padding: 0 0 0 1em;
+ }
+
border: {
color: colors.$dark-grey;
style: solid;
@@ -119,6 +132,11 @@
&.flip {
background-color: color.adjust(colors.$middle-grey, $lightness: 17%, $space: hsl);
}
+
+ &.type-text, &.type-keywords {
+ white-space: normal;
+ word-wrap: break-word;
+ }
}
&:first-of-type div.meta {
diff --git a/lib/Kalamar/Controller/Search.pm b/lib/Kalamar/Controller/Search.pm
index 487b1f5..70fceac 100644
--- a/lib/Kalamar/Controller/Search.pm
+++ b/lib/Kalamar/Controller/Search.pm
@@ -5,6 +5,7 @@
use Mojo::Util qw/quote/;
use Mojo::JSON;
use POSIX 'ceil';
+use List::Util qw/uniq/;
our @search_fields = qw!ID UID textSigle layerInfos title subTitle pubDate author availability snippet!;
our $query_placeholder = 'NOQUERY';
@@ -45,6 +46,7 @@
$v->optional('context');
$v->optional('pipe', 'trim');
$v->optional('response-pipe', 'trim');
+ $v->optional('vfields', 'comma_separated', 'trim');
# $v->optional('action'); # action 'inspect' is no longer valid
# $v->optional('snippet');
@@ -139,6 +141,13 @@
$c->stash(items_per_page => $items_per_page);
+ # Meta fields to display in KWIC
+ my @vfields_fields = ('textSigle');
+ if (my $m = $v->param('vfields')) {
+ @vfields_fields = @{$v->every_param('vfields')};
+ }
+ $c->stash(vfields_fields => \@vfields_fields);
+
# TODO:
# if ($v->param('action') eq 'inspect') use trace!
@@ -148,7 +157,7 @@
# Add requested fields
- $query{fields} = join ',', @search_fields;
+ $query{fields} = join ',', uniq(@search_fields, @vfields_fields);
# Create remote request URL
my $url = Mojo::URL->new($c->korap->api);
diff --git a/lib/Kalamar/Plugin/KalamarHelpers.pm b/lib/Kalamar/Plugin/KalamarHelpers.pm
index 74816b5..54a284f 100644
--- a/lib/Kalamar/Plugin/KalamarHelpers.pm
+++ b/lib/Kalamar/Plugin/KalamarHelpers.pm
@@ -204,6 +204,32 @@
)
}
);
+ $mojo->helper(
+ match_field => sub {
+ my ($c, $match, $field) = @_;
+ my $v = '';
+ my $type = 'string';
+ if ($match->{fields} && ref $match->{fields}->[0] eq 'HASH') {
+ foreach (@{$match->{fields}}) {
+ if ($_->{key} && $_->{key} eq $field) {
+ $v = $_->{value};
+ $type = $_->{type} || 'string';
+ last;
+ }
+ }
+ } else {
+ $v = $match->{$field};
+ }
+
+ $type =~ s/^type://;
+
+ my $html = ref $v eq 'ARRAY'
+ ? b('<ul class="meta-list"><li>' . join('</li><li>', map { b($_ // '')->xml_escape } @$v) . '</li></ul>')
+ : b($v // '')->xml_escape;
+
+ return { html => $html, type => $type, raw => $v };
+ }
+ );
};
diff --git a/t/query.t b/t/query.t
index 2ac2ac6..a46b8d6 100644
--- a/t/query.t
+++ b/t/query.t
@@ -70,7 +70,7 @@
'[id="GOE/AGI/00000#p2030-2031"]' .
'[data-available-info^="base/s=spans"]' .
'[data-info^="{"]')
- ->text_is('li:nth-of-type(1) div.meta', 'GOE/AGI/00000')
+ ->text_is('li:nth-of-type(1) div.meta > span', 'GOE/AGI/00000')
->element_exists('li:nth-of-type(1) div.match-main div.match-wrap div.snippet')
->element_exists('li:nth-of-type(1) div.snippet.startMore.endMore')
->text_like('li:nth-of-type(1) div.snippet span.context-left',qr!sie etwas bedeuten!)
@@ -385,5 +385,39 @@
is($f->{corpusSigle}, 'GOE');
is($f->{corpusEditor}, 'Trunz, Erich');
+# Test vfields query parameter
+# vfields=textSigle should behave like default
+$t->get_ok('/?q=baum&vfields=textSigle')
+ ->status_is(200)
+ ->text_is('li:nth-of-type(1) div.meta > span', 'GOE/AGI/00000')
+ ->text_is('li:nth-of-type(2) div.meta > span', 'GOE/AGI/00001') # Value repeats in actual behavior but texts are different
+ ;
+
+# Test vfields query parameter
+# vfields=textSigle should behave like default
+$t->get_ok('/?q=der&p=1&count=2&vfields=textSigle')
+ ->status_is(200)
+ ->text_is('li:nth-of-type(1) div.meta > span', 'GOE/AGI/00000')
+ ->element_exists_not('li:nth-of-type(2) div.meta > span')
+ ->text_is('li:nth-of-type(2) div.meta', '') # Should be completely empty
+ ;
+
+
+# vfields=textSigle,title should show both fields in separate divs
+$t->get_ok('/?q=baum&vfields=textSigle,title')
+ ->status_is(200)
+ ->text_is('li:nth-of-type(1) div.meta:nth-of-type(1) > span', 'GOE/AGI/00000')
+ ->text_is('li:nth-of-type(1) div.meta:nth-of-type(2) > span', 'Italienische Reise')
+ ->text_is('li:nth-of-type(2) div.meta:nth-of-type(1) > span', 'GOE/AGI/00001') # Texts are different
+ ->text_is('li:nth-of-type(2) div.meta:nth-of-type(2) > span', 'Italienische Reise')
+ ;
+
+# vfields=author should show only author
+$t->get_ok('/?q=baum&vfields=author')
+ ->status_is(200)
+ ->text_is('li:nth-of-type(1) div.meta > span', 'Goethe, Johann Wolfgang von')
+ ->text_is('li:nth-of-type(2) div.meta > span', 'Goethe, Johann Wolfgang von') # Printed again because it's a new document
+ ;
+
done_testing;
__END__
diff --git a/templates/match.html.ep b/templates/match.html.ep
index 775008c..4d3cacf 100644
--- a/templates/match.html.ep
+++ b/templates/match.html.ep
@@ -12,14 +12,19 @@
data-info="<%== b(encode_json(\%match_data))->decode->xml_escape %>"
id="<%= $id %>"<% if (current_route eq 'match') { %> class="active"<% } =%>>
%# This should be done using JavaScript
-% my ($show_sigle, $flip) = ('', stash('flip') // 'flip');
-% if ($text_sigle ne (stash('last_sigle') // '')) {
-% $show_sigle = $text_sigle;
-% stash(last_sigle => $text_sigle);
+% my @vfields_fields = @{stash('vfields_fields')};
+% my $is_new = ($text_sigle ne (stash('last_text_sigle') // ''));
+% my $flip = stash('flip') // 'flip';
+% if ($is_new) {
+% stash(last_text_sigle => $text_sigle);
% $flip = $flip eq 'flip' ? 'flop' : 'flip';
% stash(flip => $flip);
% }
- <div class="meta <%= $flip %>"><%= $show_sigle %></div>
+% foreach my $field (@vfields_fields) {
+% my $info = match_field($match, $field);
+% my $val = ($is_new && defined $info->{raw} && $info->{raw} ne '') ? $info->{html} : '';
+ <div class="meta <%= $flip %><%= $info->{type} ? " type-" . $info->{type} : '' %>"><% if ($val ne '') { %><span><%== $val %></span><% } %></div>
+% }
<div class="match-main">
<div class="match-wrap">
%# --- Snippet
diff --git a/templates/partial/header.html.ep b/templates/partial/header.html.ep
index 5a2189a..da00042 100644
--- a/templates/partial/header.html.ep
+++ b/templates/partial/header.html.ep
@@ -36,6 +36,9 @@
%= select_field 'ql', [[loc('QL_poliqarp') => 'poliqarp'], [loc('QL_cosmas2') => 'cosmas2'], [loc('QL_annis') => 'annis'], [loc('QL_cqp') => 'cqp'], [loc('QL_cql') => 'cql'], [loc('QL_fcsql') => 'fcsql']], id => 'ql-field'
</span>
<div class="button right">
+ % if (my @vfields = @{$c->req->every_param('vfields') || []}) {
+ %= hidden_field 'vfields' => join(',', @vfields)
+ % }
% param(cutoff => 1) unless param 'q';
%= check_box cutoff => 1, id => 'q-cutoff-field', class => 'checkbox'
<label for="q-cutoff-field" title="<%= loc('glimpse_desc') %>"><span id="glimpse"></span><%= loc('glimpse') %></label>