Skip to content

Commit 3aba640

Browse files
committed
web annotation export: implemented extra_target_template
This adds an extra target during serialisation of web annotations that resolves the TextPositionSelectors to a single IRI/URI (according to the template). The backend system must support this (stamd does).
1 parent 44f4d2f commit 3aba640

File tree

1 file changed

+96
-16
lines changed

1 file changed

+96
-16
lines changed

src/api/webanno.rs

Lines changed: 96 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ pub struct WebAnnoConfig {
124124

125125
/// Automatically generate a JSON-LD context alias for all URIs in keys, maps URI prefixes to namespace prefixes
126126
pub context_namespaces: Vec<(String, String)>,
127+
128+
/// Adds an extra target alongside the usual target with TextPositionSelector. This can
129+
/// be used for provide a direct URL to fetch the exact textselection (if the backend system supports it).
130+
/// In the template, you should use the variables {resource} (which is the resource IRI), {begin}, and {end} , they will be substituted accordingly.
131+
/// A common value is {resource}/{begin}/{end} .
132+
pub extra_target_template: Option<String>,
127133
}
128134

129135
impl Default for WebAnnoConfig {
@@ -137,6 +143,7 @@ impl Default for WebAnnoConfig {
137143
auto_generated: true,
138144
auto_generator: true,
139145
context_namespaces: Vec::new(),
146+
extra_target_template: None,
140147
}
141148
}
142149
}
@@ -312,10 +319,33 @@ impl<'store> ResultItem<'store, Annotation> {
312319
ann_out += "},";
313320
}
314321

315-
ann_out += &format!(
316-
" \"target\": {}",
317-
&output_selector(self.as_ref().target(), self.store(), config, false)
322+
// a second pass may be needed if we have an extra_target_template AND nested targets
323+
let mut need_second_pass = false;
324+
let output_selector_out = &output_selector(
325+
self.as_ref().target(),
326+
self.store(),
327+
config,
328+
false,
329+
&mut need_second_pass,
330+
false, //first pass
318331
);
332+
if need_second_pass {
333+
let second_pass_out = &output_selector(
334+
self.as_ref().target(),
335+
self.store(),
336+
config,
337+
false,
338+
&mut need_second_pass,
339+
true, //second pass
340+
);
341+
ann_out += &format!(
342+
" \"target\": [ {}, {} ]",
343+
output_selector_out, &second_pass_out
344+
);
345+
} else {
346+
//normal situation
347+
ann_out += &format!(" \"target\": {}", &output_selector_out);
348+
}
319349

320350
ann_out += "}";
321351
ann_out
@@ -354,6 +384,8 @@ fn output_selector(
354384
store: &AnnotationStore,
355385
config: &WebAnnoConfig,
356386
nested: bool,
387+
need_second_pass: &mut bool,
388+
second_pass: bool,
357389
) -> String {
358390
let mut ann_out = String::new();
359391
match selector {
@@ -364,15 +396,44 @@ fn output_selector(
364396
.as_ref()
365397
.get(*tsel_handle)
366398
.expect("text selection must exist");
367-
ann_out += &format!(
368-
"{{ \"source\": \"{}\", \"selector\": {{ \"type\": \"TextPositionSelector\", \"start\": {}, \"end\": {} }} }}",
369-
into_iri(
370-
resource.id().expect("resource must have ID"),
371-
&config.default_resource_iri
372-
),
373-
textselection.begin(),
374-
textselection.end(),
375-
);
399+
if !second_pass {
400+
if config.extra_target_template.is_some() && !nested {
401+
ann_out += "[";
402+
}
403+
ann_out += &format!(
404+
"{{ \"source\": \"{}\", \"selector\": {{ \"type\": \"TextPositionSelector\", \"start\": {}, \"end\": {} }} }}",
405+
into_iri(
406+
resource.id().expect("resource must have ID"),
407+
&config.default_resource_iri
408+
),
409+
textselection.begin(),
410+
textselection.end(),
411+
);
412+
}
413+
if (!nested && !second_pass) || (nested && second_pass) {
414+
if let Some(extra_target_template) = config.extra_target_template.as_ref() {
415+
let mut template = extra_target_template.clone();
416+
template = template.replace(
417+
"{resource}",
418+
&into_iri(
419+
resource.id().expect("resource must have ID"),
420+
&config.default_resource_iri,
421+
),
422+
);
423+
template = template.replace("{begin}", &format!("{}", textselection.begin()));
424+
template = template.replace("{end}", &format!("{}", textselection.end()));
425+
if !ann_out.is_empty() {
426+
ann_out.push(',');
427+
}
428+
ann_out += &format!("\"{}\"", &template);
429+
if !nested && !second_pass {
430+
ann_out += " ]";
431+
}
432+
}
433+
} else if config.extra_target_template.is_some() && !second_pass {
434+
//we need a second pass to serialize the items using extra_target_template
435+
*need_second_pass = true;
436+
}
376437
}
377438
Selector::AnnotationSelector(a_handle, None) => {
378439
let annotation = store.annotation(*a_handle).expect("annotation must exist");
@@ -406,7 +467,10 @@ fn output_selector(
406467
Selector::CompositeSelector(selectors) => {
407468
ann_out += "{ \"type\": \"http://www.w3.org/ns/oa#Composite\", \"items\": [";
408469
for (i, selector) in selectors.iter().enumerate() {
409-
ann_out += &format!("{}", &output_selector(selector, store, config, true));
470+
ann_out += &format!(
471+
"{}",
472+
&output_selector(selector, store, config, true, need_second_pass, second_pass)
473+
);
410474
if i != selectors.len() - 1 {
411475
ann_out += ",";
412476
}
@@ -416,7 +480,10 @@ fn output_selector(
416480
Selector::MultiSelector(selectors) => {
417481
ann_out += "{ \"type\": \"http://www.w3.org/ns/oa#Independents\", \"items\": [";
418482
for (i, selector) in selectors.iter().enumerate() {
419-
ann_out += &format!("{}", &output_selector(selector, store, config, true));
483+
ann_out += &format!(
484+
"{}",
485+
&output_selector(selector, store, config, true, need_second_pass, second_pass)
486+
);
420487
if i != selectors.len() - 1 {
421488
ann_out += ",";
422489
}
@@ -426,7 +493,10 @@ fn output_selector(
426493
Selector::DirectionalSelector(selectors) => {
427494
ann_out += "{ \"type\": \"http://www.w3.org/ns/oa#List\", \"items\": [";
428495
for (i, selector) in selectors.iter().enumerate() {
429-
ann_out += &format!("{}", &output_selector(selector, store, config, true));
496+
ann_out += &format!(
497+
"{}",
498+
&output_selector(selector, store, config, true, need_second_pass, second_pass)
499+
);
430500
if i != selectors.len() - 1 {
431501
ann_out += ",";
432502
}
@@ -444,7 +514,17 @@ fn output_selector(
444514
if nested {
445515
let subselectors: Vec<_> = selector.iter(store, false).collect();
446516
for (i, subselector) in subselectors.iter().enumerate() {
447-
ann_out += &format!("{}", &output_selector(&subselector, store, config, false));
517+
ann_out += &format!(
518+
"{}",
519+
&output_selector(
520+
&subselector,
521+
store,
522+
config,
523+
true,
524+
need_second_pass,
525+
second_pass
526+
)
527+
);
448528
if i != subselectors.len() - 1 {
449529
ann_out += ",";
450530
}

0 commit comments

Comments
 (0)