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()]); } }