Repository: jugglerchris/rust-html2text
Pull request #104 (2023-12-27 22:40:07)

Support CSS colours on non-span elements.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 12db2ee..c818795 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,11 @@ Possible log types:
 - `[fixed]` for any bug fixes.
 - `[security]` to invite users to upgrade in case of vulnerabilities.
 
+### 0.9.1
+
+- [fixed] Various documentation issues (thanks sgtatham)
+- [changed] CSS color rules now work for elements other than span.
+
 ### 0.9.0
 
 - [changed] `Config::add_css` now returns `Result` instead of panicking on
diff --git a/Cargo.toml b/Cargo.toml
index a37ee1f..cf66240 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "html2text"
-version = "0.9.0"
+version = "0.9.1"
 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 a384c39..024c637 100644
--- a/README.md
+++ b/README.md
@@ -99,9 +99,11 @@ read keys from stdin.
 When the `css` feature is enabled, some simple CSS handling is done.
 
 * The contents of \<style\> elements are parsed and some colour rules are extracted
-* Some simplified selector matching is done: currently `<span class="foo">` with
-  CSS rules similar to `.foo { color:#123456; }`.  This will add `Coloured(...)` nodes
-  to the render tree when matching.
+* Basic selector matching (including child and descendents, classes and element
+  types).
+* CSS colors will add `Coloured(...)` nodes to the render tree.
+* Rules with `display: none` will cause matching elements to be removed from the
+  render tree.
 
 The CSS handling is expected to improve in future (PRs welcome), but not to a full-
 blown browser style system, which would be overkill for terminal output.
diff --git a/src/lib.rs b/src/lib.rs
index de2d5cb..3f12ced 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1073,7 +1073,9 @@ fn process_dom_node<'a, 'b, 'c, T: Write>(
                 for style in context.style_data.matching_rules(&handle) {
                     match style {
                         css::Style::Colour(col) => {
-                            css_colour = Some(col);
+                            if let Ok(colour) = Colour::try_from(&col) {
+                                css_colour = Some(colour);
+                            }
                         }
                         css::Style::DisplayNone => {
                             return Ok(Nothing);
@@ -1097,20 +1099,8 @@ fn process_dom_node<'a, 'b, 'c, T: Write>(
                     Nothing
                 }
                 expanded_name!(html "span") => {
-                    #[cfg(not(feature = "css"))]
-                    {
-                        /* process children, but don't add anything */
-                        pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
-                    }
-                    #[cfg(feature = "css")]
-                    {
-                        if let Some(Ok(colour)) = css_colour.as_ref().map(TryFrom::try_from) {
-                            pending(handle, move |_, cs| Ok(Some(RenderNode::new(Coloured(colour, vec![RenderNode::new(Container(cs))])))))
-                        } else {
-                            /* process children, but don't add anything */
-                            pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
-                        }
-                    }
+                    /* process children, but don't add anything */
+                    pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
                 }
                 #[cfg(feature = "css")]
                 expanded_name!(html "font") => {
@@ -1264,7 +1254,7 @@ fn process_dom_node<'a, 'b, 'c, T: Write>(
                 }
             }
 
-            if let Some(fragname) = fragment {
+            let result = if let Some(fragname) = fragment {
                 match result {
                     Finished(node) => {
                         Finished(prepend_marker(RenderNode::new(FragStart(fragname)), node))
@@ -1293,7 +1283,32 @@ fn process_dom_node<'a, 'b, 'c, T: Write>(
                 }
             } else {
                 result
-            }
+            };
+
+            #[cfg(feature = "css")]
+            let result = if let Some(colour) = css_colour {
+                match result {
+                    Finished(node) => Finished(RenderNode::new(Coloured(colour.into(), vec![node]))),
+                    PendingChildren { children, cons, prefn, postfn } => {
+                        PendingChildren {
+                            children,
+                            prefn,
+                            postfn,
+                            cons: Box::new(move |ctx, ch| {
+                                Ok(cons(ctx, ch)?
+                                    .map(|n| {
+                                        RenderNode::new(Coloured(colour.into(), vec![n]))
+                                    }))
+                            }),
+                        }
+                    }
+                    Nothing => Nothing
+                }
+            } else {
+                result
+            };
+
+            result
         }
         markup5ever_rcdom::NodeData::Text { contents: ref tstr } => {
             Finished(RenderNode::new(Text((&*tstr.borrow()).into())))


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index 9877be5..059c1dd 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -29,6 +29,39 @@ fn test_html_css(input: &[u8], expected: &str, width: usize) {
         .string_from_read(input, width).unwrap();
     assert_eq_str!(result, expected);
 }
+#[cfg(feature = "css")]
+fn test_colour_map(annotations: &[RichAnnotation], s: &str) -> String
+{
+    let mut tags = ("", "");
+    for ann in annotations {
+        match ann {
+            RichAnnotation::Colour(c) => {
+                match c {
+                    crate::Colour{
+                        r: 0xff,
+                        g: 0,
+                        b: 0
+                    } => {
+                        tags = ("<R>", "</R>");
+                    }
+                    _ => {
+                        tags = ("<?>", "</?>");
+                    }
+                }
+            }
+            _ => ()
+        }
+    }
+    format!("{}{}{}", tags.0, s, tags.1)
+}
+
+#[cfg(feature = "css")]
+fn test_html_coloured(input: &[u8], expected: &str, width: usize) {
+    let result = config::rich()
+        .use_doc_css()
+        .coloured(input, width, test_colour_map).unwrap();
+    assert_eq_str!(result, expected);
+}
 fn test_html_err(input: &[u8], expected: Error, width: usize) {
     let result = config::plain()
         .string_from_read(input, width);
@@ -1685,7 +1718,7 @@ fn test_issue_93_x() {
 
 #[cfg(feature = "css")]
 mod css_tests {
-    use super::{test_html_css, test_html_style};
+    use super::{test_html_css, test_html_style, test_html_coloured};
 
     #[test]
     fn test_disp_none() {
@@ -1757,6 +1790,51 @@ Span1
 Span1
 
 There
+"#, 20);
+    }
+
+    #[test]
+    fn test_coloured()
+    {
+        test_html_coloured(br##"
+          <style>
+              .red {
+                  color:#FF0000;
+              }
+          </style>
+        <p>Test <span class="red">red</span></p>
+        "##,
+        r#"Test <R>red</R>
+"#, 20);
+    }
+
+    #[test]
+    fn test_coloured_a()
+    {
+        test_html_coloured(br##"
+          <style>
+              .red {
+                  color:#FF0000;
+              }
+          </style>
+        <p>Test <a class="red" href="foo">red</a></p>
+        "##,
+        r#"Test <R>red</R>
+"#, 20);
+    }
+
+    #[test]
+    fn test_coloured_element()
+    {
+        test_html_coloured(br##"
+          <style>
+              .red {
+                  color:#FF0000;
+              }
+          </style>
+        <p>Test <blah class="red" href="foo">red</blah></p>
+        "##,
+        r#"Test <R>red</R>
 "#, 20);
     }
 }