1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// Copyright 2019 Robert Sayre
// Licensed under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

extern crate pest;
use pest::Parser;
use pest::iterators::Pairs;
use twitter_text_parser::highlighter::HighlightParser;
use twitter_text_parser::highlighter::Rule;

type Hit = (usize, usize);
const DEFAULT_HIGHLIGHT_TAG: &str = "em";

pub struct HitHighlighter<'a> {
    highlight_tag: &'a str,
}

impl<'a> HitHighlighter<'a> {
    pub fn new() -> HitHighlighter<'a> {
        HitHighlighter {
            highlight_tag: DEFAULT_HIGHLIGHT_TAG,
        }
    }

    pub fn highlight(&self, text: &str, hits: Vec<Hit>) -> String {
        if hits.is_empty() {
            return String::from(text);
        }

        let mut builder = HighlightBuilder::new(text, self.highlight_tag, &hits);
        match HighlightParser::parse(Rule::hit_text, text) {
            Ok(pairs) => {
                self.walk(pairs, &hits[..], &mut builder);
            }
            Err(e) => {
                println!("Error, {}", e);
            }
        }

        builder.buffer()
    }

    fn walk(&self, pairs: Pairs<Rule>, hits: &[(Hit)], builder: &mut HighlightBuilder) -> usize {
        let mut start = 0;
        let mut tag_open = false;

        for pair in pairs {
            let rule = pair.as_rule();
            match rule {
                Rule::element => start += self.walk(pair.into_inner(), &hits[start..], builder),
                Rule::start_tag => builder.append_tag(pair.into_span().as_str()),
                Rule::end_tag => builder.append_tag(pair.into_span().as_str()),
                Rule::text => {
                    let span = pair.into_span();
                    for c in span.as_str().chars() {
                        if builder.count() == hits.get(start).unwrap_or(&(0, 0)).0 {
                            builder.append_open();
                            tag_open = true;
                            start += 1;
                        }
                        builder.append_char(c);
                        if tag_open && builder.count() == hits[start - 1].1 {
                            builder.append_close();
                            tag_open = false;
                        }
                    }
                }
                Rule::EOI => {
                    if tag_open && builder.count() == hits[start - 1].1 {
                        builder.append_close();
                    }
                }
                _ => unreachable!("Should only match silent rules.")
            }
        }

        start
    }
}

struct HighlightBuilder {
    buffer: String,
    char_count: usize,
    open: String,
    close: String
}

impl HighlightBuilder {
    fn new(text: &str, tag: &str, hits: &Vec<(Hit)>) -> HighlightBuilder {
        let capacity = text.len() + (hits.len() * (tag.len() + 2 + tag.len() + 3));
        HighlightBuilder {
            buffer: String::with_capacity(capacity),
            char_count: 0,
            open: ["<", tag, ">"].join(""),
            close: ["</", tag, ">"].join(""),
        }
    }

    fn append_open(&mut self) {
        self.buffer.push_str(self.open.as_str());
    }

    fn append_close(&mut self) {
        self.buffer.push_str(self.close.as_str());
    }

    fn append_tag(&mut self, text: &str) {
        self.buffer.push_str(text);
    }

    fn append_char(&mut self, c: char) {
        self.buffer.push(c);
        self.char_count += 1;
    }

    fn count(&self) -> usize {
        self.char_count
    }

    fn buffer(self) -> String {
        self.buffer
    }
}