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)]
11pub enum Tag<'a> {
14 None,
16
17 Id,
19
20 Key(ResultItem<'a, DataKey>), KeyAndValue(ResultItem<'a, DataKey>), Value(ResultItem<'a, DataKey>),
28}
29
30#[derive(Clone, Debug)]
31pub struct Highlight<'a> {
33 tag: Tag<'a>,
34 style: Option<String>,
35 varname: &'a str,
37 label: Option<&'a str>,
38 hide: bool, }
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 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
99pub 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: bool,
108 output_data_ids: bool,
110 output_key_ids: bool,
112 output_offset: bool,
114 output_data: bool,
116 header: Option<&'a str>,
118 footer: Option<&'a str>,
120 legend: bool,
122 titles: bool,
124 interactive: bool,
126 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 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
471fn 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 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 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 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 if self.previous.is_some()
620 && Some(&resulttextselection) != self.previous.as_ref()
621 {
622 let previous_highlights = std::mem::replace(
624 &mut self.highlight_results,
625 Self::new_highlight_results(self.highlights.len()),
626 );
627
628 let previous_whole_resource = self.whole_resource;
630 let previous_id = self.id;
631
632 self.whole_resource = whole_resource;
634 self.id = id;
635 get_highlights_results(
637 &queryresultitems,
638 &self.highlights,
639 &mut self.highlight_results, );
641
642 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 self.previous = Some(resulttextselection);
656 self.whole_resource = whole_resource;
657 self.id = id;
658 get_highlights_results(
660 &queryresultitems,
661 &self.highlights,
662 &mut self.highlight_results, );
664 }
665 }
666 }
667 } else if let Some(resulttextselection) = self.previous.take() {
668 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 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!(f, "{}", header)?;
692 }
693 if self.interactive {
694 write!(
696 f,
697 "<script>autocollapse = {};</script>",
698 if self.autocollapse { "true" } else { "false" }
699 )?;
700 write!(f, "{}", HTML_SCRIPT)?;
701 }
702 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 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)); classnames.push(format!("hi{}", i + 1));
719 close_annotations.push(Vec::new());
720 }
721
722 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 let results = self.store.query(self.query.clone()).map_err(|e| {
750 eprintln!("{}", e);
751 std::fmt::Error
752 })?;
753
754 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 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 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 if let Some(beginpositionitem) = resource.as_ref().position(segment.begin())
804 {
805 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 openingtags.clear(); 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; }
852 openingtags.push('>');
853 closetags += "</span>";
854 }
855 } else if !text_is_whitespace {
857 openingtags.clear();
859 openingtags += "<span>";
860 closetags.clear();
861 closetags += "</span>";
862 }
863
864 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(" ", " ")
885 .replace('\t', " ")
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 pendingnewlines = subtext.replace("\n", "<br/>");
898 }
899 }
900 BufferType::None => {}
901 }
902 }
903
904 if let Some(endpositionitem) = resource.as_ref().position(segment.end()) {
906 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 if let Some(a_handle) = a_handle {
925 let annotation = self
926 .store
927 .annotation(*a_handle)
928 .expect("annotation must exist");
929 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 for (j, annotations) in close_annotations.iter().enumerate() {
944 let highlight = &self.highlights[j];
945 for annotation in annotations {
946 let tag = highlight.get_tag(&annotation);
949 if !tag.is_empty() {
950 if annotation
952 .textselections()
953 .any(|ts| ts.begin() == ts.end())
954 {
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 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
1004pub struct AnsiWriter<'a> {
1006 store: &'a AnnotationStore,
1007 query: Query<'a>,
1008 selectionvar: Option<&'a str>,
1009 highlights: Vec<Highlight<'a>>,
1010 legend: bool,
1012 titles: bool,
1014}
1015
1016impl<'a> AnsiWriter<'a> {
1017 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 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 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 if let Some(beginpositionitem) = resource.as_ref().position(segment.begin())
1151 {
1152 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 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 if let Some(endpositionitem) = resource.as_ref().position(segment.end()) {
1187 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 if let Some(a_handle) = a_handle {
1206 let annotation = self
1207 .store
1208 .annotation(*a_handle)
1209 .expect("annotation must exist");
1210 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 for (j, annotations) in close_annotations.iter().enumerate() {
1225 let highlight = &self.highlights[j];
1226 for annotation in annotations {
1227 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 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 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 NewLines,
1308 Whitespace,
1310 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 self.curbytepos += c.len_utf8();
1348 } else {
1349 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 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}