Repository: jugglerchris/rust-html2text
Pull request #102 (2023-12-22 21:29:40)

Prepare for 0.9.0


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2626f3..12db2ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,15 @@ Possible log types:
 - `[fixed]` for any bug fixes.
 - `[security]` to invite users to upgrade in case of vulnerabilities.
 
+### 0.9.0
+
+- [changed] `Config::add_css` now returns `Result` instead of panicking on
+  CSS parse errors.  Errors from parsing document CSS are ignored.
+- [added] Support `<font color=...>` when CSS is enabled.
+- [added] `Config::max_wrap_width()` to wrap text to a norrower width than
+  the overal size available.
+- [added] Add --wrap-width and --css options to html2text example.
+
 ### 0.8.0
 
 - [added] CSS: Support more extensive selectors
diff --git a/Cargo.toml b/Cargo.toml
index 559cfbc..a37ee1f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "html2text"
-version = "0.8.0"
+version = "0.9.0"
 authors = ["Chris Emerson <github@mail.nosreme.org>"]
 description = "Render HTML as plain text."
 repository = "https://github.com/jugglerchris/rust-html2text/"
diff --git a/README.md b/README.md
index 7cfab0f..a384c39 100644
--- a/README.md
+++ b/README.md
@@ -52,8 +52,9 @@ let html = b"
 
 assert_eq!(
     config::plain()
-           .string_from_read(html, 20),
-    Ok("\
+           .string_from_read(&html[..], 20)
+           .unwrap(),
+    "\
 * Item one
 * Item two
 * Item three
diff --git a/examples/html2text.rs b/examples/html2text.rs
index 7821b5f..a9164eb 100644
--- a/examples/html2text.rs
+++ b/examples/html2text.rs
@@ -1,6 +1,8 @@
 extern crate argparse;
 extern crate html2text;
 use argparse::{ArgumentParser, Store, StoreOption, StoreTrue};
+use html2text::config::{Config, self};
+use html2text::render::text_renderer::{TextDecorator, TrivialDecorator};
 use std::io;
 use std::io::Write;
 
@@ -76,31 +78,63 @@ fn default_colour_map(annotations: &[RichAnnotation], s: &str) -> String {
     result
 }
 
-fn translate<R>(input: R, width: usize, literal: bool, _use_colour: bool) -> String
+fn update_config<T: TextDecorator>(mut config: Config<T>, flags: &Flags) -> Config<T> {
+    if let Some(wrap_width) = flags.wrap_width {
+        config = config.max_wrap_width(wrap_width);
+    }
+    #[cfg(feature = "css")]
+    if flags.use_css {
+        config = config.use_doc_css();
+    }
+    config
+}
+
+fn translate<R>(input: R, flags: Flags, literal: bool) -> String
 where
     R: io::Read,
 {
     #[cfg(unix)]
     {
-        if _use_colour {
-            return html2text::from_read_coloured(input, width, default_colour_map).unwrap();
+        if flags.use_colour {
+            let conf = config::rich();
+            let conf = update_config(conf, &flags);
+            return conf.coloured(input, flags.width, default_colour_map)
+                    .unwrap()
         }
     }
     if literal {
-        let decorator = html2text::render::text_renderer::TrivialDecorator::new();
-        html2text::from_read_with_decorator(input, width, decorator)
+        let conf = config::with_decorator(TrivialDecorator::new());
+        let conf = update_config(conf, &flags);
+        conf.string_from_read(input, flags.width)
+            .unwrap()
     } else {
-        html2text::from_read(input, width)
+        let conf = config::plain();
+        let conf = update_config(conf, &flags);
+        conf.string_from_read(input, flags.width)
+            .unwrap()
     }
 }
 
+struct Flags {
+    width: usize,
+    wrap_width: Option<usize>,
+    #[allow(unused)]
+    use_colour: bool,
+    #[cfg(feature = "css")]
+    use_css: bool,
+}
+
 fn main() {
     let mut infile: Option<String> = None;
     let mut outfile: Option<String> = None;
-    let mut width: usize = 80;
+    let mut flags = Flags {
+        width: 80,
+        wrap_width: None,
+        use_colour: false,
+        #[cfg(feature = "css")]
+        use_css: false,
+    };
     let mut literal: bool = false;
-    #[allow(unused)]
-    let mut use_colour = false;
 
     {
         let mut ap = ArgumentParser::new();
@@ -109,11 +143,16 @@ fn main() {
             StoreOption,
             "Input HTML file (default is standard input)",
         );
-        ap.refer(&mut width).add_option(
+        ap.refer(&mut flags.width).add_option(
             &["-w", "--width"],
             Store,
             "Column width to format to (default is 80)",
         );
+        ap.refer(&mut flags.wrap_width).add_option(
+            &["-W", "--wrap-width"],
+            StoreOption,
+            "Maximum text wrap width (default same as width)",
+        );
         ap.refer(&mut outfile).add_option(
             &["-o", "--output"],
             StoreOption,
@@ -125,20 +164,23 @@ fn main() {
             "Output only literal text (no decorations)",
         );
         #[cfg(unix)]
-        ap.refer(&mut use_colour)
+        ap.refer(&mut flags.use_colour)
             .add_option(&["--colour"], StoreTrue, "Use ANSI terminal colours");
+        #[cfg(feature = "css")]
+        ap.refer(&mut flags.use_css)
+            .add_option(&["--css"], StoreTrue, "Enable CSS");
         ap.parse_args_or_exit();
     }
 
     let data = match infile {
         None => {
             let stdin = io::stdin();
-            let data = translate(&mut stdin.lock(), width, literal, use_colour);
+            let data = translate(&mut stdin.lock(), flags, literal);
             data
         }
         Some(name) => {
             let mut file = std::fs::File::open(name).expect("Tried to open file");
-            translate(&mut file, width, literal, use_colour)
+            translate(&mut file, flags, literal)
         }
     };
 
diff --git a/src/css.rs b/src/css.rs
index d0e505c..2963b9d 100644
--- a/src/css.rs
+++ b/src/css.rs
@@ -179,14 +179,9 @@ pub struct StyleData {
 impl StyleData {
     /// Add some CSS source to be included.  The source will be parsed
     /// and the relevant and supported features extracted.
-    pub fn add_css(&mut self, css: &str) {
-        let ss = match StyleSheet::parse(css, ParserOptions::default()) {
-            Ok(ss) => ss,
-            Err(_e) => {
-                html_trace!("failed to parse CSS: {}, [[{}]]", _e, css);
-                return;
-            }
-        };
+    pub fn add_css(&mut self, css: &str) -> Result<()> {
+        let ss = StyleSheet::parse(css, ParserOptions::default())
+            .map_err(|_| crate::Error::CssParseError)?;
 
         for rule in &ss.rules.0 {
             match rule {
@@ -224,6 +219,7 @@ impl StyleData {
                 _ => (),
             }
         }
+        Ok(())
     }
 
     /// Merge style data from other into this one.
@@ -321,7 +317,8 @@ pub fn dom_to_stylesheet<T: Write>(handle: Handle, err_out: &mut T) -> Result<St
     let mut result = StyleData::default();
     if let Some(styles) = styles {
         for css in styles {
-            result.add_css(&css);
+            // Ignore CSS parse errors.
+            let _ = result.add_css(&css);
         }
     }
     Ok(result)
diff --git a/src/lib.rs b/src/lib.rs
index 6b8fed2..290d3e9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -51,6 +51,11 @@
 #![cfg_attr(feature = "clippy", plugin(clippy))]
 #![deny(missing_docs)]
 
+// Check code in README.md
+#[cfg(doctest)]
+#[doc = include_str!("../README.md")]
+struct ReadMe;
+
 #[macro_use]
 extern crate html5ever;
 extern crate unicode_width;
@@ -128,6 +133,10 @@ pub enum Error {
     /// The output width was too narrow to render to.
     #[error("Output width not wide enough.")]
     TooNarrow,
+    #[cfg(feature = "css")]
+    /// CSS parse error
+    #[error("Invalid CSS")]
+    CssParseError,
     /// An general error was encountered.
     #[error("Unknown failure")]
     Fail,
@@ -1786,9 +1795,9 @@ pub mod config {
         #[cfg(feature = "css")]
         /// Add some CSS rules which will be used (if supported) with any
         /// HTML processed.
-        pub fn add_css(mut self, css: &str) -> Self {
-            self.style.add_css(css);
-            self
+        pub fn add_css(mut self, css: &str) -> Result<Self> {
+            self.style.add_css(css)?;
+            Ok(self)
         }
 
         #[cfg(feature = "css")]


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index 47c9d08..9877be5 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -46,6 +46,7 @@ fn test_html_err(input: &[u8], expected: Error, width: usize) {
 fn test_html_style(input: &[u8], style: &str, expected: &str, width: usize) {
     let result = config::plain()
         .add_css(style)
+        .unwrap()
         .string_from_read(input, width).unwrap();
     assert_eq_str!(result, expected);
 }
@@ -937,6 +938,8 @@ Indented again
     );
 }
 
+// Some of the tracing output can overflow the stack when tracing some values.
+#[cfg(not(feature = "html_trace"))]
 #[test]
 fn test_deeply_nested() {
     use ::std::iter::repeat;
@@ -944,6 +947,8 @@ fn test_deeply_nested() {
     test_html(html.as_bytes(), "", 10);
 }
 
+// Some of the tracing output can overflow the stack when tracing some values.
+#[cfg(not(feature = "html_trace"))]
 #[test]
 fn test_deeply_nested_table() {
     use ::std::iter::repeat;