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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use lazy_static::lazy_static;
use libxml::{
    parser::Parser,
    tree::{node::Node, nodetype::NodeType},
};
use log::warn;
use regex::Regex;
use std::collections::HashSet;
use std::fmt::Write;
use std::str;

lazy_static! {
    static ref IGNORED_ELEMENTS: HashSet<&'static str> = {
        let mut s = HashSet::new();
        s.insert("base");
        s.insert("link");
        s.insert("meta");
        s.insert("head");
        s.insert("script");
        s.insert("style");
        s.insert("template");
        s.insert("img");
        s
    };
    static ref SPACING_ELEMENTS: HashSet<&'static str> = {
        let mut s = HashSet::new();
        s.insert("dt");
        s.insert("dd");
        s.insert("td");
        s.insert("th");
        s
    };
    static ref ALT_TEXT_ELEMENTS: HashSet<&'static str> = {
        let s = HashSet::new();
        s
    };
    static ref BREAKING_ELEMENTS: HashSet<&'static str> = {
        let mut s = HashSet::new();
        s.insert("address");
        s.insert("blockquote");
        s.insert("br");
        s.insert("caption");
        s.insert("center");
        s.insert("div");
        s.insert("dt");
        s.insert("embed");
        s.insert("form");
        s.insert("hr");
        s.insert("iframe");
        s.insert("li");
        s.insert("map");
        s.insert("menu");
        s.insert("tr");
        s.insert("pre");
        s.insert("p");
        s.insert("object");
        s.insert("noscript");
        s.insert("h1");
        s.insert("h2");
        s.insert("h3");
        s.insert("h4");
        s.insert("h5");
        s.insert("h6");
        s
    };
}

pub struct Html2Text;

impl Html2Text {
    pub fn to_summary(plain_text: &str) -> String {
        plain_text.chars().take(300).collect()
    }

    pub fn process(html: &str) -> Option<String> {
        let parser = Parser::default_html();
        if let Ok(doc) = parser.parse_string(html) {
            if let Some(root_node) = doc.get_root_element() {
                let mut text = String::new();
                Self::recurse_html_nodes_for_text(&root_node, &mut text);

                let text = match escaper::decode_html(&text) {
                    Ok(text) => text,
                    Err(e) => {
                        warn!("Error {:?} at character {}", e.kind, e.position);
                        text
                    }
                };
                let text = text.trim();
                let text = str::replace(&text, "\n", " ");
                let text = str::replace(&text, "\r", " ");
                let text = str::replace(&text, "_", " ");

                let compress_whitespace = Regex::new(r#"/\s+/g"#).expect("Failed to create RegEx");
                let text = compress_whitespace.replace_all(&text, " ");

                return Some(text.into_owned());
            }
        }
        None
    }

    fn recurse_html_nodes_for_text(node: &Node, text: &mut String) {
        for n in node.get_child_nodes() {
            let node_type = n.get_type();

            if let Some(NodeType::TextNode) = node_type {
                write!(text, "{}", n.get_content()).unwrap();
            } else if let Some(NodeType::ElementNode) = node_type {
                let name = n.get_name();

                if ALT_TEXT_ELEMENTS.contains::<str>(&name) {
                    if let Some(alt_text) = n.get_property("alt") {
                        write!(text, "{}", alt_text).unwrap();
                    }
                }

                if !IGNORED_ELEMENTS.contains::<str>(&name) {
                    Self::recurse_html_nodes_for_text(&n, text);
                }

                if SPACING_ELEMENTS.contains::<str>(&name) {
                    write!(text, " ").unwrap();
                }

                if BREAKING_ELEMENTS.contains::<str>(&name) {
                    write!(text, "\n").unwrap();
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Html2Text;

    #[test]
    pub fn hardwareluxx() {
        let article = "<p><img src=\"https://www.hardwareluxx.de/images/stories/2017/stadia.jpg\" alt=\"stadia\">Am vergangenen Dienstag präsentierte Google im Rahmen der Game Developers Conference in San Francisco seinen neuen  <a href=\"https://www.hardwareluxx.de/index.php/news/software/spiele/48994-googles-cloud-gaming-plattform-stadia-geht-an-den-start.html\" rel=\"noopener noreferrer\" target=\"_blank\" referrerpolicy=\"no-referrer\">Spiele-Streaming-Dienst Stadia</a> , der noch im Sommer dieses Jahres an den Start gehen soll. Auch einen eigenen  <a href=\"https://www.hardwareluxx.de/index.php/news/hardware/eingabegeraete/49001-googles-stadia-controller-mit-zwei-sondertasten-zum-heimlichen-star.html\" rel=\"noopener noreferrer\" target=\"_blank\" referrerpolicy=\"no-referrer\">Controller mit vielen interessanten Features</a>  hatte der Konzern den anwesenden Journalisten gezeigt.</p><p>Die Vorteile von Stadia liegen klar auf der Hand: Die Hardware im Rechenzentrum ist dank skalierbarer Infrastruktur schneller als jede Heimkonsole und jeder Spiele-PC zu Hause und erlaubt damit theoretisch die höchste Bildqualität. Hinzu kommt, dass langwierige Downloads und Installations-Prozesse entfallen und teure Hardware für die Nutzung des Dienstes nicht benötigt wird. Ein leistungsschwaches Notebook oder gar ein herkömmliches Smartphone sollen laut Google genügen.</p>";
        let needle = "Am vergangenen Dienstag";

        let summary = Html2Text::process(&article).unwrap();

        assert_eq!(needle, &summary[..needle.len()]);
    }
}