Skip to main content

stamtools/
view.rs

1use stam::*;
2use std::borrow::Cow;
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::{Display, Formatter};
5
6use std::str::Chars;
7
8use crate::query::textselection_from_queryresult;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11/// Determines whether to display a Tag when highlighting annotations,
12/// and what information to show in it.
13pub enum Tag<'a> {
14    ///Highlight only, no tag
15    None,
16
17    //Show tag with public identifier
18    Id,
19
20    ///Show tag with key
21    Key(ResultItem<'a, DataKey>), //or label if set
22
23    ///Show tag with key and value
24    KeyAndValue(ResultItem<'a, DataKey>), //or label if set
25
26    ///Show tag with value (for a given key)
27    Value(ResultItem<'a, DataKey>),
28}
29
30#[derive(Clone, Debug)]
31/// Represent a highlight action, represented by a query and a tag to show how to visualize it.
32pub struct Highlight<'a> {
33    tag: Tag<'a>,
34    style: Option<String>,
35    /// The variable this highlight is bound to
36    varname: &'a str,
37    label: Option<&'a str>,
38    hide: bool, //hide the highlight? useful when you only want to assign a custom style and nothing else
39}
40
41impl<'a> Default for Highlight<'a> {
42    fn default() -> Self {
43        Self {
44            label: None,
45            varname: "",
46            tag: Tag::None,
47            style: None,
48            hide: false,
49        }
50    }
51}
52
53impl<'a> Highlight<'a> {
54    pub fn with_tag(mut self, tag: Tag<'a>) -> Self {
55        self.tag = tag;
56        self
57    }
58
59    pub fn new(var: &'a str) -> Self {
60        Self {
61            varname: var,
62            ..Self::default()
63        }
64    }
65
66    pub fn with_label(mut self, label: &'a str) -> Self {
67        self.label = Some(label);
68        self
69    }
70
71    /// Serializes the tag to string, given an annotation
72    pub fn get_tag(&self, annotation: &ResultItem<'a, Annotation>) -> Cow<'a, str> {
73        match &self.tag {
74            Tag::Key(key) => Cow::Borrowed(self.label.unwrap_or(key.as_str())),
75            Tag::KeyAndValue(key) => {
76                if let Some(data) = annotation.data().filter_key(key).next() {
77                    Cow::Owned(format!(
78                        "{}: {}",
79                        self.label.unwrap_or(key.as_str()),
80                        data.value()
81                    ))
82                } else {
83                    Cow::Borrowed(self.label.unwrap_or(key.as_str()))
84                }
85            }
86            Tag::Value(key) => {
87                if let Some(data) = annotation.data().filter_key(key).next() {
88                    Cow::Owned(data.value().to_string())
89                } else {
90                    Cow::Borrowed(self.label.unwrap_or(key.as_str()))
91                }
92            }
93            Tag::Id => Cow::Borrowed(annotation.id().unwrap_or("")),
94            Tag::None => Cow::Borrowed(""),
95        }
96    }
97}
98
99/// Holds all information necessary to visualize annotations as HTML.
100/// The writer can be run (= output HTML) via the [`Display`] trait.
101pub struct HtmlWriter<'a> {
102    store: &'a AnnotationStore,
103    query: Query<'a>,
104    selectionvar: Option<&'a str>,
105    highlights: Vec<Highlight<'a>>,
106    /// Output annotation IDs in the data-annotations attribute
107    output_annotation_ids: bool,
108    /// Output annotation data IDs in data-annotationdata attribute
109    output_data_ids: bool,
110    /// Output key IDs in data-keys attribute
111    output_key_ids: bool,
112    /// Output position in data-pos attribute
113    output_offset: bool,
114    /// Output annotations and data in a `<script>` block (javascript)
115    output_data: bool,
116    /// html header
117    header: Option<&'a str>,
118    /// html footer
119    footer: Option<&'a str>,
120    /// Output legend?
121    legend: bool,
122    /// Output titles (identifiers) for the primary selection?
123    titles: bool,
124    /// Use javascript for interactive elements
125    interactive: bool,
126    /// Auto-collapse all tags on document load
127    autocollapse: bool,
128}
129
130const HTML_HEADER: &str = "<!DOCTYPE html>
131<html>
132<head>
133    <meta charset=\"UTF-8\" />
134    <meta name=\"generator\" content=\"stam view\" />
135    <style type=\"text/css\">
136div.resource, div.textselection {
137    color: black;
138    background: white;
139    font-family: monospace;
140    border: 1px solid black;
141    padding: 10px;
142    margin: 10px;
143    margin-right: 10%;
144    line-height: 2em;
145    max-width: 1200px;
146    margin-left: auto;
147    margin-right: auto;
148}
149:root {
150    --hi1: #00aa00; /* green */
151    --hi2: #aa0000; /* red */
152    --hi3: #0000aa; /* blue */
153    --hi4: #aaaa00; /* yellow */
154    --hi5: #00aaaa; /* ayan */
155    --hi6: #aa00aa; /* magenta */
156    --hiX: #666666; /* gray */
157}
158body {
159    background: #b7c8c7;
160}
161.a { /* annotation */
162    /* background: #dedede;  light gray */
163    vertical-align: top;
164}
165label {
166    font-weight: bold;
167}
168label em {
169    color: white;
170    font-size: 70%;
171    padding-left: 5px;
172    padding-right: 5px;
173    vertical-align: bottom;
174}
175/* highlights for labels/tags */
176label.tag1 {
177    background: var(--hi1);
178}
179label.tag2 {
180    background: var(--hi2);
181}
182label.tag3 {
183    background: var(--hi3);
184}
185label.tag4 {
186    background: var(--hi4);
187}
188label.tag5 {
189    background: var(--hi5);
190}
191label.tag6 {
192    background: var(--hi6);
193}
194label.tag7, label.tag8, label.tag9, label.tag10, label.tag11, label.tag12, label.tag13, label.tag14, label.tag15, label.tag16 {
195    background: var(--hiX);
196}
197span.hi1, span.hi2, span.hi3, span.hi4, span.hi5, span.hi6, span.hi7, span.hi8, span.hi9, span.hi10, span.hi11, span.hi12, span.hi13, span.hi14 {
198    position: relative;
199    line-height: 2em;
200}
201span.hi1::after, span.hi2::after, span.hi3::after, span.hi4::after, span.hi5::after, span.hi6::after, span.hi7::after, span.hi8::after, span.hi9::after, span.hi10::after, span.hi11::after, span.hi12::after, span.hi13::after, span.hi14::after {
202    content: \"\";
203    position: absolute;
204    width: calc(100%);
205    height: 2px;
206    left: 0px;
207}
208span.hi1::after {
209    background-color: var(--hi1);
210    position: absolute;
211    bottom: 0px;
212}
213span.hi2::after {
214    background-color: var(--hi2);
215    position: absolute;
216    bottom: -2px;
217}
218span.hi3::after {
219    background-color: var(--hi3);
220    position: absolute;
221    bottom: -4px;
222}
223span.hi4::after {
224    background-color: var(--hi4);
225    position: absolute;
226    bottom: -6px;
227}
228span.hi5::after {
229    background-color: var(--hi5);
230    position: absolute;
231    bottom: -8px;
232}
233span.hi6::after {
234    background-color: var(--hi6);
235    position: absolute;
236    bottom: -10px;
237}
238span.hi7::after {
239    background-color: var(--hi7);
240    position: absolute;
241    bottom: -12px;
242}
243
244div#legend {
245    background: white;
246    color: black;
247    width: 40%;
248    min-width: 320px;
249    margin-left: auto;
250    margin-right: auto;
251    font-family: sans-serif;
252    padding: 5px;
253    border-radius: 20px;
254}
255div#legend ul {
256    list-style: none;
257}
258div#legend ul li span {
259    display: inline-block;
260    width: 15px;
261    border-radius: 15px;
262    border: 1px #555 solid;
263    font-weight: bold;
264    min-height: 15px;
265}
266div#legend li:hover {
267    font-weight: bold;
268}
269div#legend li.hidetags {
270    text-decoration: none;
271    font-style: normal;
272    color: #333;
273}
274div#legend span.legendhi1 {
275    background: var(--hi1);
276}
277div#legend span.legendhi2 {
278    background: var(--hi2);
279}
280div#legend span.legendhi3 {
281    background: var(--hi3);
282}
283div#legend span.legendhi4 {
284    background: var(--hi4);
285}
286div#legend span.legendhi5 {
287    background: var(--hi5);
288}
289div#legend span.legendhi6 {
290    background: var(--hi6);
291}
292div#legend span.legendhi7 {
293    background: var(--hi7);
294}
295div#legend li {
296    cursor: pointer;
297    text-decoration: underline;
298    font-style: italic;
299}
300body>h2 {
301    color: black;
302    font-size: 1.1em;
303    font-family: sans-serif;
304}
305label.h em {
306    display: none;
307}
308span:hover + label.h em {
309    position: absolute;
310    display: block;
311    padding: 2px;
312    background: black;
313    color: white;
314}
315label.h em, {
316    font-weight: bold;
317}
318span:hover + label + label.h em {
319    position: absolute;
320    margin-top: 20px;
321    display: block;
322    padding: 2px;
323    background: black;
324}
325span:hover + label + label + label.h em {
326    position: absolute;
327    margin-top: 40px;
328    display: block;
329    padding: 2px;
330    background: black;
331}
332/* generic style classes */
333.italic, .italics { font-style: italic; }
334.bold { font-weight: bold; }
335.normal { font-weight: normal; font-style: normal; }
336.red { color: #ff0000; }
337.green { color: #00ff00; }
338.blue { color: #0000ff; }
339.yellow { color: #ffff00; }
340.super, .small { vertical-align: top; font-size: 60%; };
341    </style>
342</head>
343<body>
344";
345
346const HTML_SCRIPT: &str = r###"<script>
347document.addEventListener('DOMContentLoaded', function() {
348    for (let i = 1; i <= 8; i++) {
349        let e = document.getElementById("legend" + i);
350        if (e) {
351            e.addEventListener('click', () => {
352                if (e.classList.contains("hidetags")) {
353                    document.querySelectorAll('label.tag' + i).forEach((tag,i) => { tag.classList.remove("h")} );
354                    e.classList.remove("hidetags");
355                } else {
356                    document.querySelectorAll('label.tag' + i).forEach((tag,i) => { tag.classList.add("h")} );
357                    e.classList.add("hidetags");
358                }
359            });
360            if (autocollapse) {
361                e.click();
362            }
363        }
364    }
365});
366</script>"###;
367
368const HTML_FOOTER: &str = "
369</body></html>";
370
371impl<'a> HtmlWriter<'a> {
372    /// Instantiates an HtmlWriter, uses a builder pattern via the ``with*()`` methods
373    /// to assign data.
374    pub fn new(
375        store: &'a AnnotationStore,
376        query: Query<'a>,
377        selectionvar: Option<&'a str>,
378    ) -> Result<Self, String> {
379        Ok(Self {
380            store,
381            highlights: highlights_from_query(&query, store, selectionvar)?,
382            query,
383            selectionvar,
384            output_annotation_ids: false,
385            output_data_ids: false,
386            output_key_ids: false,
387            output_offset: true,
388            output_data: false,
389            header: Some(HTML_HEADER),
390            footer: Some(HTML_FOOTER),
391            legend: true,
392            titles: true,
393            interactive: true,
394            autocollapse: true,
395        })
396    }
397
398    pub fn with_legend(mut self, value: bool) -> Self {
399        self.legend = value;
400        self
401    }
402    pub fn with_titles(mut self, value: bool) -> Self {
403        self.titles = value;
404        self
405    }
406
407    pub fn with_annotation_ids(mut self, value: bool) -> Self {
408        self.output_annotation_ids = value;
409        self
410    }
411    pub fn with_data_ids(mut self, value: bool) -> Self {
412        self.output_data_ids = value;
413        self
414    }
415    pub fn with_key_ids(mut self, value: bool) -> Self {
416        self.output_key_ids = value;
417        self
418    }
419    pub fn with_pos(mut self, value: bool) -> Self {
420        self.output_offset = value;
421        self
422    }
423    pub fn with_header(mut self, html: Option<&'a str>) -> Self {
424        self.header = html;
425        self
426    }
427    pub fn with_footer(mut self, html: Option<&'a str>) -> Self {
428        self.footer = html;
429        self
430    }
431    pub fn with_interactive(mut self, value: bool) -> Self {
432        self.interactive = value;
433        self
434    }
435    pub fn with_autocollapse(mut self, value: bool) -> Self {
436        self.autocollapse = value;
437        self
438    }
439    pub fn with_data_script(mut self, value: bool) -> Self {
440        self.output_data = value;
441        self
442    }
443
444    pub fn with_selectionvar(mut self, var: &'a str) -> Self {
445        self.selectionvar = Some(var);
446        self
447    }
448
449    fn output_error(&self, f: &mut Formatter, msg: &str) -> std::fmt::Result {
450        write!(f, "<span class=\"error\">{}</span>", msg)?;
451        if let Some(footer) = self.footer {
452            write!(f, "{}", footer)?;
453        }
454        return Ok(());
455    }
456}
457
458fn get_key_from_constraint<'a>(
459    constraint: &Constraint<'a>,
460    store: &'a AnnotationStore,
461) -> Option<ResultItem<'a, DataKey>> {
462    if let Constraint::DataKey { set, key, .. } | Constraint::KeyValue { set, key, .. } = constraint
463    {
464        if let Some(key) = store.key(*set, *key) {
465            return Some(key);
466        }
467    }
468    None
469}
470
471/// Parse highlight information from the query's attributes
472fn highlights_from_query<'a>(
473    query: &Query<'a>,
474    store: &'a AnnotationStore,
475    selectionvar: Option<&'a str>,
476) -> Result<Vec<Highlight<'a>>, String> {
477    let mut highlights = Vec::new();
478    helper_highlights_from_query(&mut highlights, query, store, selectionvar)?;
479    Ok(highlights)
480}
481
482fn helper_highlights_from_query<'a>(
483    highlights: &mut Vec<Highlight<'a>>,
484    query: &Query<'a>,
485    store: &'a AnnotationStore,
486    selectionvar: Option<&'a str>,
487) -> Result<(), String> {
488    for subquery in query.subqueries() {
489        if let Some(varname) = subquery.name() {
490            let mut highlight = Highlight::new(varname);
491            for attrib in subquery.attributes() {
492                let (attribname, attribvalue) = if let Some(pos) = attrib.find('=') {
493                    (&attrib[..pos], &attrib[pos + 1..])
494                } else {
495                    (*attrib, "")
496                };
497                match attribname {
498                    "@HIDE" => highlight.hide = true,
499                    "@IDTAG" | "@ID" => highlight.tag = Tag::Id,
500                    "@STYLE" | "@CLASS" => highlight.style = Some(attribvalue.to_string()),
501                    "@KEYTAG" | "@VALUETAG" | "@KEYVALUETAG" => {
502                        //old-style tags ahead of the whole query (backward compatibility)
503                        for constraint in subquery.constraints() {
504                            if let Some(key) = get_key_from_constraint(&constraint, store) {
505                                match attribname {
506                                    "@KEYTAG" => highlight.tag = Tag::Key(key),
507                                    "@VALUETAG" => highlight.tag = Tag::Value(key),
508                                    "@KEYVALUETAG" => highlight.tag = Tag::Value(key),
509                                    _ => unreachable!("invalid tag"),
510                                }
511                            }
512                        }
513                    }
514                    other => {
515                        return Err(format!("Query syntax error - Unknown attribute: {}", other));
516                    }
517                }
518            }
519            for (constraint, attributes) in subquery.constraints_with_attributes() {
520                for attrib in attributes {
521                    /*
522                    let (attribname, attribvalue) = if let Some(pos) = attrib.find('=') {
523                        (&attrib[..pos], &attrib[pos + 1..])
524                    } else {
525                        (*attrib, "")
526                    };
527                    */
528                    match *attrib {
529                        "@KEYTAG" => {
530                            if let Some(key) = get_key_from_constraint(&constraint, store) {
531                                highlight.tag = Tag::Key(key);
532                            }
533                        }
534                        "@VALUETAG" => {
535                            if let Some(key) = get_key_from_constraint(&constraint, store) {
536                                highlight.tag = Tag::Value(key);
537                            }
538                        }
539                        "@KEYVALUETAG" => {
540                            if let Some(key) = get_key_from_constraint(&constraint, store) {
541                                highlight.tag = Tag::KeyAndValue(key);
542                            }
543                        }
544                        other => {
545                            return Err(format!(
546                                "Query syntax error - Unknown constraint attribute: {}",
547                                other
548                            ));
549                        }
550                    }
551                }
552            }
553            highlights.push(highlight);
554        }
555        helper_highlights_from_query(highlights, subquery, store, selectionvar)?;
556    }
557    Ok(())
558}
559
560pub(crate) struct SelectionWithHighlightsIterator<'a> {
561    iter: QueryIter<'a>,
562    selectionvar: Option<&'a str>,
563    highlights: &'a Vec<Highlight<'a>>,
564
565    //the following are buffers:
566    highlight_results: Vec<HighlightResults>,
567    previous: Option<ResultTextSelection<'a>>,
568    whole_resource: bool,
569    id: Option<&'a str>,
570}
571
572impl<'a> SelectionWithHighlightsIterator<'a> {
573    pub fn new(
574        iter: QueryIter<'a>,
575        selectionvar: Option<&'a str>,
576        highlights: &'a Vec<Highlight<'a>>,
577    ) -> Self {
578        Self {
579            iter,
580            selectionvar,
581            highlights,
582            highlight_results: Self::new_highlight_results(highlights.len()),
583            previous: None,
584            whole_resource: false,
585            id: None,
586        }
587    }
588
589    fn new_highlight_results(len: usize) -> Vec<HighlightResults> {
590        let mut highlight_results: Vec<HighlightResults> = Vec::with_capacity(len);
591        for _ in 0..len {
592            highlight_results.push(HighlightResults::new());
593        }
594        highlight_results
595    }
596}
597
598type HighlightResults = BTreeMap<TextSelection, Option<AnnotationHandle>>;
599
600#[derive(Debug, Clone)]
601pub(crate) struct SelectionWithHighlightResult<'a> {
602    textselection: ResultTextSelection<'a>,
603    highlights: Vec<HighlightResults>,
604    whole_resource: bool,
605    id: Option<&'a str>,
606}
607
608impl<'a> Iterator for SelectionWithHighlightsIterator<'a> {
609    type Item = Result<SelectionWithHighlightResult<'a>, &'a str>;
610
611    fn next(&mut self) -> Option<Self::Item> {
612        loop {
613            if let Some(queryresultitems) = self.iter.next() {
614                match textselection_from_queryresult(&queryresultitems, self.selectionvar) {
615                    Err(msg) => return Some(Err(msg)),
616                    Ok((resulttextselection, whole_resource, id)) => {
617                        //resulttextselection may match the same item multiple times with other highlights (subqueries)
618                        //if so, we need to aggregate these highlights
619                        if self.previous.is_some()
620                            && Some(&resulttextselection) != self.previous.as_ref()
621                        {
622                            //we start a new resultselection, so prepare to return the previous one:
623                            let previous_highlights = std::mem::replace(
624                                &mut self.highlight_results,
625                                Self::new_highlight_results(self.highlights.len()),
626                            );
627
628                            //get the previous results
629                            let previous_whole_resource = self.whole_resource;
630                            let previous_id = self.id;
631
632                            //store the new metadata for next iteration
633                            self.whole_resource = whole_resource;
634                            self.id = id;
635                            //mark highlights in buffer for new results:
636                            get_highlights_results(
637                                &queryresultitems,
638                                &self.highlights,
639                                &mut self.highlight_results, //will be appended to
640                            );
641
642                            //return the previous one:
643                            return Some(Ok(SelectionWithHighlightResult {
644                                textselection: std::mem::replace(
645                                    &mut self.previous,
646                                    Some(resulttextselection),
647                                )
648                                .unwrap(),
649                                highlights: previous_highlights,
650                                whole_resource: previous_whole_resource,
651                                id: previous_id,
652                            }));
653                        } else {
654                            //buffer metadata
655                            self.previous = Some(resulttextselection);
656                            self.whole_resource = whole_resource;
657                            self.id = id;
658                            //same text selection result, mark highlights in buffer:
659                            get_highlights_results(
660                                &queryresultitems,
661                                &self.highlights,
662                                &mut self.highlight_results, //will be appended to
663                            );
664                        }
665                    }
666                }
667            } else if let Some(resulttextselection) = self.previous.take() {
668                //don't forget the last item
669                let return_highlight_results = std::mem::replace(
670                    &mut self.highlight_results,
671                    Self::new_highlight_results(self.highlights.len()),
672                );
673                return Some(Ok(SelectionWithHighlightResult {
674                    textselection: resulttextselection,
675                    highlights: return_highlight_results,
676                    whole_resource: self.whole_resource,
677                    id: self.id,
678                }));
679            } else {
680                //iterator done
681                return None;
682            }
683        }
684    }
685}
686
687impl<'a> Display for HtmlWriter<'a> {
688    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
689        if let Some(header) = self.header {
690            // write the HTML header
691            write!(f, "{}", header)?;
692        }
693        if self.interactive {
694            // in interactive mode, we can '(un)collapse' the tags using a toggle in the legend
695            write!(
696                f,
697                "<script>autocollapse = {};</script>",
698                if self.autocollapse { "true" } else { "false" }
699            )?;
700            write!(f, "{}", HTML_SCRIPT)?;
701        }
702        // output the STAMQL query as a comment, just for reference
703        write!(
704            f,
705            "<!-- Query:\n\n{}\n\n-->\n",
706            self.query
707                .to_string()
708                .unwrap_or_else(|err| format!("{}", err))
709        )?;
710
711        // pre-assign class names and layer opening tags so we can borrow later
712        let mut classnames: Vec<String> = Vec::with_capacity(self.highlights.len());
713        let mut hitags: Vec<String> = Vec::with_capacity(self.highlights.len());
714        let mut close_annotations: Vec<Vec<ResultItem<Annotation>>> = Vec::new();
715
716        for (i, _highlight) in self.highlights.iter().enumerate() {
717            hitags.push(format!("<span class=\"hi{}\"", i + 1)); //note, no closing >!
718            classnames.push(format!("hi{}", i + 1));
719            close_annotations.push(Vec::new());
720        }
721
722        // output the legend
723        if self.legend && !self.highlights.is_empty() {
724            write!(f, "<div id=\"legend\" title=\"Click the items in this legend to toggle visibility of tags (if any)\"><ul>")?;
725            for (i, highlight) in self.highlights.iter().enumerate() {
726                if !highlight.hide {
727                    write!(
728                        f,
729                        "<li id=\"legend{}\"{}><span class=\"legendhi{}\"></span> {}</li>",
730                        i + 1,
731                        if self.interactive {
732                            " title=\"Click to toggle visibility of tags (if any)\""
733                        } else {
734                            ""
735                        },
736                        i + 1,
737                        if let Some(label) = highlight.label {
738                            label.to_string()
739                        } else {
740                            highlight.varname.replace("_", " ")
741                        }
742                    )?;
743                }
744            }
745            write!(f, "</ul></div>")?;
746        }
747
748        //run the query, bail out on error (this is lazy, doesn't return yet until we iterate over the results)
749        let results = self.store.query(self.query.clone()).map_err(|e| {
750            eprintln!("{}", e);
751            std::fmt::Error
752        })?;
753
754        // pre-allocate buffers that we will reuse
755        let mut openingtags = String::new();
756        let mut closetags = String::new();
757        let mut pendingnewlines: String = String::new();
758        let mut classes: Vec<&str> = vec![];
759
760        let mut active_highlights: BTreeSet<usize> = BTreeSet::new();
761        let mut close_highlights: BTreeSet<usize> = BTreeSet::new();
762
763        for (resultnr, result) in
764            SelectionWithHighlightsIterator::new(results, self.selectionvar, &self.highlights)
765                .enumerate()
766        {
767            // obtain the text selection from the query result
768            match result {
769                Err(msg) => return self.output_error(f, msg),
770                Ok(result) => {
771                    active_highlights.clear();
772                    close_highlights.clear();
773                    classes.clear();
774                    let resource = result.textselection.resource();
775
776                    // write title and per-result container span (either for a full resource or any textselection)
777                    if self.titles {
778                        if let Some(id) = result.id {
779                            write!(f, "<h2>{}. <span>{}</span></h2>\n", resultnr + 1, id,)?;
780                        }
781                    }
782                    if result.whole_resource {
783                        write!(
784                            f,
785                            "<div class=\"resource\" data-resource=\"{}\">\n",
786                            resource.id().unwrap_or("undefined"),
787                        )?;
788                    } else {
789                        write!(
790                            f,
791                            "<div class=\"textselection\" data-resource=\"{}\" data-begin=\"{}\" data-end=\"{}\">\n",
792                            resource.id().unwrap_or("undefined"),
793                            result.textselection.begin(),
794                            result.textselection.end(),
795                        )?;
796                    }
797
798                    for segment in result.textselection.segmentation() {
799                        if self.output_offset {
800                            write!(f, "<span data-offset=\"{}\">", segment.begin())?;
801                        }
802                        // Gather position info for the begin point of our segment
803                        if let Some(beginpositionitem) = resource.as_ref().position(segment.begin())
804                        {
805                            // what highlights start at this segment?
806                            for (_, textselectionhandle) in beginpositionitem.iter_begin2end() {
807                                let textselection: &TextSelection = resource
808                                    .as_ref()
809                                    .get(*textselectionhandle)
810                                    .expect("text selection must exist");
811                                for (j, highlighted_selections) in
812                                    result.highlights.iter().enumerate()
813                                {
814                                    if highlighted_selections.contains_key(textselection)
815                                        && textselection.end() != textselection.begin()
816                                    {
817                                        active_highlights.insert(j);
818                                    }
819                                }
820                            }
821                        }
822
823                        let text = segment.text();
824                        let text_is_whitespace =
825                            !segment.text().chars().any(|c| !c.is_whitespace());
826
827                        if !active_highlights.is_empty()
828                            && segment.end() <= result.textselection.end()
829                        {
830                            // output the opening <span> layer tags for the current segment
831                            // this covers all the highlighted text selections we are spanning
832                            // not just those pertaining to annotations that start here
833                            openingtags.clear(); //this is a buffer that may be referenced later (for newline processing), start it anew
834                            closetags.clear();
835                            for j in active_highlights.iter() {
836                                if let Some(style) = self.highlights[*j].style.as_ref() {
837                                    if self.highlights[*j].hide {
838                                        openingtags +=
839                                            format!("<span class=\"{}\"", style).as_str();
840                                    } else {
841                                        openingtags += format!(
842                                            "<span class=\"{} {}\"",
843                                            &classnames[*j], style
844                                        )
845                                        .as_str();
846                                    }
847                                } else if !self.highlights[*j].hide {
848                                    openingtags += &hitags[*j];
849                                } else {
850                                    continue; //skip the remainder
851                                }
852                                openingtags.push('>');
853                                closetags += "</span>";
854                            }
855                            // output buffer
856                        } else if !text_is_whitespace {
857                            //opening if there are no active highlights
858                            openingtags.clear();
859                            openingtags += "<span>";
860                            closetags.clear();
861                            closetags += "</span>";
862                        }
863
864                        // Linebreaks require special handling in rendering, we can't nest
865                        // them in the various <span> layers we have but have to pull them out
866                        // to the top level. Even if they are in the middle of some text!
867                        for (subtext, texttype, done) in LinebreakIter::new(text) {
868                            match texttype {
869                                BufferType::Text => {
870                                    write!(
871                                        f,
872                                        "{}{}{}<wbr/>",
873                                        openingtags.as_str(),
874                                        html_escape::encode_text(subtext),
875                                        closetags.as_str()
876                                    )?;
877                                }
878                                BufferType::Whitespace => {
879                                    write!(
880                                        f,
881                                        "{}{}{}",
882                                        openingtags.as_str(),
883                                        html_escape::encode_text(subtext)
884                                            .replace(" ", "&ensp;")
885                                            .replace('\t', "&nbsp;&nbsp;&nbsp;&nbsp;")
886                                            .as_str(),
887                                        closetags.as_str()
888                                    )?;
889                                }
890                                BufferType::NewLines => {
891                                    if !done {
892                                        write!(f, "{}", subtext.replace("\n", "<br/>").as_str())?;
893                                    } else {
894                                        // we already handled the </span> closure here, prevent doing it again later
895                                        //needclosure = true;
896                                        //set pending newlines, we don't output immediately because there might be a tag to output first
897                                        pendingnewlines = subtext.replace("\n", "<br/>");
898                                    }
899                                }
900                                BufferType::None => {}
901                            }
902                        }
903
904                        // Gather position info for the end point of our segment
905                        if let Some(endpositionitem) = resource.as_ref().position(segment.end()) {
906                            // what highlights stop at this segment?
907                            close_highlights.clear();
908                            for annotations in close_annotations.iter_mut() {
909                                annotations.clear();
910                            }
911                            for (_, textselectionhandle) in endpositionitem.iter_end2begin() {
912                                let textselection: &TextSelection = resource
913                                    .as_ref()
914                                    .get(*textselectionhandle)
915                                    .expect("text selection must exist");
916                                for (j, highlighted_selections) in
917                                    result.highlights.iter().enumerate()
918                                {
919                                    if let Some(a_handle) =
920                                        highlighted_selections.get(textselection)
921                                    {
922                                        close_highlights.insert(j);
923                                        // Identify which highlighted annotations are being closed
924                                        if let Some(a_handle) = a_handle {
925                                            let annotation = self
926                                                .store
927                                                .annotation(*a_handle)
928                                                .expect("annotation must exist");
929                                            //confirm that the annotation really closes here (or at least one of its text selections does if it's non-contingent):
930                                            //MAYBE TODO: this may not be performant enough for large MultiSelectors!
931                                            if annotation
932                                                .textselections()
933                                                .any(|ts| ts.end() == segment.end())
934                                            {
935                                                close_annotations[j].push(annotation);
936                                            }
937                                        }
938                                    }
939                                }
940                            }
941
942                            // output tags for annotations that close here (if requested)
943                            for (j, annotations) in close_annotations.iter().enumerate() {
944                                let highlight = &self.highlights[j];
945                                for annotation in annotations {
946                                    // Get the appropriate tag representation
947                                    // and output the tag for this highlight
948                                    let tag = highlight.get_tag(&annotation);
949                                    if !tag.is_empty() {
950                                        //check for zero-width
951                                        if annotation
952                                            .textselections()
953                                            .any(|ts| ts.begin() == ts.end())
954                                        //MAYBE TODO: potentially expensive on large MultiSelectors!
955                                        {
956                                            write!(
957                                                f,
958                                                "{}<label class=\"zw tag{}\">",
959                                                openingtags,
960                                                j + 1,
961                                            )
962                                            .ok();
963                                            close_highlights.insert(j);
964                                        } else {
965                                            write!(
966                                                f,
967                                                "{}<label class=\"tag{}\">",
968                                                openingtags,
969                                                j + 1,
970                                            )
971                                            .ok();
972                                        }
973                                        write!(f, "<em>{}</em>", tag,).ok();
974                                        write!(f, "</label>{}", closetags).ok();
975                                    }
976                                }
977                            }
978
979                            if !pendingnewlines.is_empty() {
980                                write!(f, "{}", pendingnewlines)?;
981                                pendingnewlines.clear();
982                            }
983
984                            //process the closing highlights
985                            active_highlights.retain(|hl| !close_highlights.contains(hl));
986                        }
987
988                        if self.output_offset {
989                            write!(f, "</span>")?;
990                        }
991                    }
992                    writeln!(f, "\n</div>")?;
993                }
994            }
995        }
996
997        if let Some(footer) = self.footer {
998            write!(f, "{}", footer)?;
999        }
1000        Ok(())
1001    }
1002}
1003
1004/// Holds all information necessary to visualize annotations as text with ANSI escape codes, for terminal output
1005pub struct AnsiWriter<'a> {
1006    store: &'a AnnotationStore,
1007    query: Query<'a>,
1008    selectionvar: Option<&'a str>,
1009    highlights: Vec<Highlight<'a>>,
1010    /// Output legend?
1011    legend: bool,
1012    /// Output titles (identifiers) for the primary selection?
1013    titles: bool,
1014}
1015
1016impl<'a> AnsiWriter<'a> {
1017    /// Instantiates an AnsiWriter, uses a builder pattern via the ``with*()`` methods
1018    /// to assign data.
1019    pub fn new(
1020        store: &'a AnnotationStore,
1021        query: Query<'a>,
1022        selectionvar: Option<&'a str>,
1023    ) -> Result<Self, String> {
1024        Ok(Self {
1025            store,
1026            selectionvar: None,
1027            highlights: highlights_from_query(&query, store, selectionvar)?,
1028            query,
1029            legend: true,
1030            titles: true,
1031        })
1032    }
1033
1034    pub fn with_highlight(mut self, highlight: Highlight<'a>) -> Self {
1035        self.highlights.push(highlight);
1036        self
1037    }
1038    pub fn with_legend(mut self, value: bool) -> Self {
1039        self.legend = value;
1040        self
1041    }
1042    pub fn with_titles(mut self, value: bool) -> Self {
1043        self.titles = value;
1044        self
1045    }
1046
1047    pub fn with_selectionvar(mut self, var: &'a str) -> Self {
1048        self.selectionvar = Some(var);
1049        self
1050    }
1051
1052    fn output_error(&self, msg: &str) {
1053        eprintln!("ERROR: {}", msg);
1054    }
1055
1056    fn writeansicol<W: std::io::Write>(
1057        &self,
1058        writer: &mut W,
1059        i: usize,
1060        s: &str,
1061    ) -> Result<(), std::io::Error> {
1062        let color = if i > 6 { 30 } else { 30 + i };
1063        writer.write(b"\x1b[")?;
1064        writer.write(&format!("{}", color).into_bytes())?;
1065        writer.write(b"m")?;
1066        writer.flush()?;
1067        write!(writer, "{}", s)?;
1068        writer.write(b"\x1b[m")?;
1069        writer.flush()
1070    }
1071
1072    fn writeansicol_bold<W: std::io::Write>(
1073        &self,
1074        writer: &mut W,
1075        i: usize,
1076        s: &str,
1077    ) -> Result<(), std::io::Error> {
1078        let color = if i > 6 { 30 } else { 30 + i };
1079        writer.write(b"\x1b[")?;
1080        writer.write(&format!("{}", color).into_bytes())?;
1081        writer.write(b";1m")?;
1082        writer.flush()?;
1083        write!(writer, "{}", s)?;
1084        writer.write(b"\x1b[m")?;
1085        writer.flush()
1086    }
1087
1088    fn writeheader<W: std::io::Write>(
1089        &self,
1090        writer: &mut W,
1091        s: &str,
1092    ) -> Result<(), std::io::Error> {
1093        writer.write(b"\x1b[37;1m")?;
1094        writer.flush()?;
1095        write!(writer, "{}", s)?;
1096        writer.write(b"\x1b[m")?;
1097        writer.flush()
1098    }
1099
1100    /// Write ANSI output
1101    pub fn write<W: std::io::Write>(&self, writer: &mut W) -> Result<(), std::io::Error> {
1102        if self.legend && !self.highlights.is_empty() {
1103            writeln!(writer, "Legend:")?;
1104            for (i, highlight) in self.highlights.iter().enumerate() {
1105                if !highlight.hide {
1106                    let s = format!(
1107                        "       {}. {}\n",
1108                        i + 1,
1109                        highlight.varname.replace("_", " ")
1110                    );
1111                    self.writeansicol_bold(writer, i + 1, s.as_str())?;
1112                }
1113            }
1114            writeln!(writer)?;
1115        }
1116
1117        let results = self.store.query(self.query.clone()).map_err(|e| {
1118            eprintln!("{}", e);
1119            std::io::Error::new(std::io::ErrorKind::Other, "STAM query error")
1120        })?;
1121
1122        let mut close_annotations: Vec<Vec<ResultItem<Annotation>>> = Vec::new();
1123        for _ in 0..self.highlights.len() {
1124            close_annotations.push(Vec::new());
1125        }
1126
1127        let mut pendingnewlines = String::new();
1128
1129        let mut close_highlights: BTreeSet<usize> = BTreeSet::new();
1130
1131        for (resultnr, result) in
1132            SelectionWithHighlightsIterator::new(results, self.selectionvar, &self.highlights)
1133                .enumerate()
1134        {
1135            // obtain the text selection from the query result
1136            match result {
1137                Err(msg) => return Ok(self.output_error(msg)),
1138                Ok(result) => {
1139                    let resource = result.textselection.resource();
1140
1141                    if self.titles {
1142                        if let Some(id) = result.id {
1143                            let s = format!("----------------------------------- {}. {} -----------------------------------\n", resultnr + 1, id,);
1144                            self.writeheader(writer, s.as_str())?;
1145                        }
1146                    }
1147
1148                    for segment in result.textselection.segmentation() {
1149                        // Gather position info for the begin point of our segment
1150                        if let Some(beginpositionitem) = resource.as_ref().position(segment.begin())
1151                        {
1152                            // what highlights start at this segment?
1153                            for (_, textselectionhandle) in beginpositionitem.iter_begin2end() {
1154                                let textselection: &TextSelection = resource
1155                                    .as_ref()
1156                                    .get(*textselectionhandle)
1157                                    .expect("text selection must exist");
1158                                for (j, highlighted_selections) in
1159                                    result.highlights.iter().enumerate().rev()
1160                                {
1161                                    if highlighted_selections.contains_key(textselection) {
1162                                        if segment.end() <= result.textselection.end() {
1163                                            self.writeansicol_bold(writer, j + 1, "[")?;
1164                                        }
1165                                    }
1166                                }
1167                            }
1168                        }
1169
1170                        //output text
1171                        pendingnewlines.clear();
1172                        if segment.text().ends_with("\n") {
1173                            for c in segment.text().chars().rev() {
1174                                if c == '\n' {
1175                                    pendingnewlines.push(c);
1176                                } else {
1177                                    break;
1178                                }
1179                            }
1180                            write!(writer, "{}", segment.text().trim_end_matches('\n'))?;
1181                        } else {
1182                            write!(writer, "{}", segment.text())?;
1183                        }
1184
1185                        // Gather position info for the end point of our segment
1186                        if let Some(endpositionitem) = resource.as_ref().position(segment.end()) {
1187                            // what highlights stop at this segment?
1188                            close_highlights.clear();
1189                            for annotations in close_annotations.iter_mut() {
1190                                annotations.clear();
1191                            }
1192                            for (_, textselectionhandle) in endpositionitem.iter_end2begin() {
1193                                let textselection: &TextSelection = resource
1194                                    .as_ref()
1195                                    .get(*textselectionhandle)
1196                                    .expect("text selection must exist");
1197                                for (j, highlighted_selections) in
1198                                    result.highlights.iter().enumerate()
1199                                {
1200                                    if let Some(a_handle) =
1201                                        highlighted_selections.get(textselection)
1202                                    {
1203                                        close_highlights.insert(j);
1204                                        // Identify which highlighted annotations are being closed
1205                                        if let Some(a_handle) = a_handle {
1206                                            let annotation = self
1207                                                .store
1208                                                .annotation(*a_handle)
1209                                                .expect("annotation must exist");
1210                                            //confirm that the annotation really closes here (or at least one of its text selections does if it's non-contingent):
1211                                            //MAYBE TODO: this may not be performant enough for large MultiSelectors!
1212                                            if annotation
1213                                                .textselections()
1214                                                .any(|ts| ts.end() == segment.end())
1215                                            {
1216                                                close_annotations[j].push(annotation);
1217                                            }
1218                                        }
1219                                    }
1220                                }
1221                            }
1222
1223                            // output tags for annotations that close here (if requested)
1224                            for (j, annotations) in close_annotations.iter().enumerate() {
1225                                let highlight = &self.highlights[j];
1226                                for annotation in annotations {
1227                                    //closing highlight after adding tag if needed
1228                                    let tag = highlight.get_tag(&annotation);
1229                                    if !tag.is_empty() {
1230                                        if annotation
1231                                            .textselections()
1232                                            .any(|ts| ts.begin() == ts.end())
1233                                        {
1234                                            //MAYBE TODO: potentially expensive on large MultiSelectors!
1235                                            self.writeansicol_bold(writer, j + 1, "[").unwrap();
1236                                            self.writeansicol(
1237                                                writer,
1238                                                j + 1,
1239                                                format!("{}", &tag).as_str(),
1240                                            )
1241                                            .unwrap();
1242                                        } else {
1243                                            self.writeansicol(
1244                                                writer,
1245                                                j + 1,
1246                                                format!("|{}", &tag).as_str(),
1247                                            )
1248                                            .unwrap();
1249                                        }
1250                                    }
1251                                    self.writeansicol_bold(writer, j + 1, "]").unwrap();
1252                                }
1253                            }
1254                        }
1255
1256                        if !pendingnewlines.is_empty() {
1257                            write!(writer, "{}", pendingnewlines)?;
1258                        }
1259                    }
1260                }
1261            }
1262            writeln!(writer)?;
1263        }
1264        Ok(())
1265    }
1266}
1267
1268#[inline]
1269fn get_highlights_results<'a>(
1270    resultitems: &QueryResultItems<'a>,
1271    highlights: &[Highlight],
1272    highlight_results: &mut Vec<HighlightResults>,
1273) {
1274    //gather annotations for the textselection under consideration
1275    for (j, highlight) in highlights.iter().enumerate() {
1276        if highlight_results.len() <= j {
1277            highlight_results.push(HighlightResults::new());
1278        }
1279
1280        if let Some(highlight_results) = highlight_results.get_mut(j) {
1281            if let Ok(result) = resultitems.get_by_name(highlight.varname) {
1282                match result {
1283                    &QueryResultItem::Annotation(ref annotation) => {
1284                        for ts in annotation.textselections() {
1285                            highlight_results.insert(ts.inner().clone(), Some(annotation.handle()));
1286                        }
1287                    }
1288                    &QueryResultItem::TextSelection(ref ts) => {
1289                        highlight_results.insert(ts.inner().clone(), None);
1290                    }
1291                    _ => {
1292                        eprintln!(
1293                            "WARNING: query for highlight {} has invalid resulttype",
1294                            j + 1
1295                        )
1296                    }
1297                }
1298            }
1299        }
1300    }
1301}
1302
1303#[derive(Copy, PartialEq, Clone, Debug)]
1304enum BufferType {
1305    None,
1306    /// Buffer contains only newlines
1307    NewLines,
1308    /// Buffer contains only whitespace
1309    Whitespace,
1310    /// Buffer contains text without newlines
1311    Text,
1312}
1313
1314struct LinebreakIter<'a> {
1315    iter: Chars<'a>,
1316    text: &'a str,
1317    curbytepos: usize,
1318    beginbytepos: usize,
1319    buffertype: BufferType,
1320    done: bool,
1321}
1322
1323impl<'a> LinebreakIter<'a> {
1324    fn new(text: &'a str) -> Self {
1325        Self {
1326            iter: text.chars(),
1327            text,
1328            curbytepos: 0,
1329            beginbytepos: 0,
1330            buffertype: BufferType::None,
1331            done: false,
1332        }
1333    }
1334}
1335
1336impl<'a> Iterator for LinebreakIter<'a> {
1337    type Item = (&'a str, BufferType, bool);
1338    fn next(&mut self) -> Option<Self::Item> {
1339        while !self.done {
1340            if let Some(c) = self.iter.next() {
1341                if (c == '\n' && self.buffertype == BufferType::NewLines)
1342                    || ((c.is_whitespace() && c != '\n')
1343                        && self.buffertype == BufferType::Whitespace)
1344                    || (c != '\n' && !c.is_whitespace() && self.buffertype == BufferType::Text)
1345                {
1346                    //same type as buffer, carry on
1347                    self.curbytepos += c.len_utf8();
1348                } else {
1349                    //switching buffers, yield result
1350                    let resultbuffertype = self.buffertype;
1351                    if c == '\n' {
1352                        self.buffertype = BufferType::NewLines;
1353                    } else if c.is_whitespace() {
1354                        self.buffertype = BufferType::Whitespace;
1355                    } else {
1356                        self.buffertype = BufferType::Text;
1357                    }
1358                    if self.curbytepos > self.beginbytepos {
1359                        let result = &self.text[self.beginbytepos..self.curbytepos];
1360                        self.beginbytepos = self.curbytepos;
1361                        self.curbytepos += c.len_utf8();
1362                        return Some((result, resultbuffertype, self.done));
1363                    } else {
1364                        self.curbytepos += c.len_utf8();
1365                    }
1366                }
1367            } else {
1368                //return last buffer (if any)
1369                if self.curbytepos > self.beginbytepos && !self.done {
1370                    let result = &self.text[self.beginbytepos..];
1371                    self.done = true;
1372                    return Some((result, self.buffertype, self.done));
1373                } else {
1374                    return None;
1375                }
1376            }
1377        }
1378        None
1379    }
1380}