Repository: jugglerchris/rust-html2text
Pull request #173 (2024-10-11 12:47:30)

Implement CSS priorities
Allow for separate user agent and user CSS layers.
Implement specificity rules.

Update version and CHANGELOG for 0.13.0-alpha.2.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e018dd..c1f4010 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,8 +11,18 @@ Possible log types:
 
 ### Latest
 
+### 0.13.0-alpha.2
+
 - [changed] Updated html5ever and markup5ever crate versions.  This has meant
   updating the MSRV, which is now set to 1.72.
+- [fixed] Add `Config::no_link_wrapping()` (thanks JadeBlueEyes)
+- [fixed] Fix panic with empty table inside a list (thanks sftse)
+- [changed] Top level convenience functions (`from_read` etc.) now return
+  `Result<..>` instead of panicking (thanks sftse)
+- [fixed] Fix panic with very large HTML `colspan` (thanks pycui)
+- [changed] CSS updates:
+  - Separate user agent, author, and user CSS layers
+  - Improve the style precedence between layers and implement specificity.
 
 ### 0.13.0-alpha.1
 
diff --git a/Cargo.toml b/Cargo.toml
index 8f5e8ee..84815ea 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "html2text"
-version = "0.13.0-alpha.1"
+version = "0.13.0-alpha.2"
 authors = ["Chris Emerson <github@mail.nosreme.org>"]
 description = "Render HTML as plain text."
 repository = "https://github.com/jugglerchris/rust-html2text/"
diff --git a/src/css.rs b/src/css.rs
index e8aeff2..db50f9e 100644
--- a/src/css.rs
+++ b/src/css.rs
@@ -13,6 +13,8 @@ use crate::{
     tree_map_reduce, Colour, Result, TreeMapResult,
 };
 
+use self::parser::Importance;
+
 #[derive(Debug, Clone, PartialEq)]
 pub(crate) enum SelectorComponent {
     Class(String),
@@ -23,6 +25,43 @@ pub(crate) enum SelectorComponent {
     CombDescendant,
 }
 
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
+pub(crate) struct Specificity {
+    inline: bool,
+    id: u16,
+    class: u16,
+    typ: u16,
+}
+
+impl Specificity {
+    fn inline() -> Self {
+        Specificity {
+            inline: true,
+            id: 0,
+            class: 0,
+            typ: 0,
+        }
+    }
+}
+
+impl PartialOrd for Specificity {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        match self.inline.partial_cmp(&other.inline) {
+            Some(core::cmp::Ordering::Equal) => {}
+            ord => return ord,
+        }
+        match self.id.partial_cmp(&other.id) {
+            Some(core::cmp::Ordering::Equal) => {}
+            ord => return ord,
+        }
+        match self.class.partial_cmp(&other.class) {
+            Some(core::cmp::Ordering::Equal) => {}
+            ord => return ord,
+        }
+        self.typ.partial_cmp(&other.typ)
+    }
+}
+
 #[derive(Debug, Clone, PartialEq)]
 pub(crate) struct Selector {
     // List of components, right first so we match from the leaf.
@@ -108,6 +147,28 @@ impl Selector {
     fn matches(&self, node: &Handle) -> bool {
         Self::do_matches(&self.components, node)
     }
+    fn specificity(&self) -> Specificity {
+        let mut result: Specificity = Default::default();
+
+        for component in &self.components {
+            match component {
+                SelectorComponent::Class(_) => {
+                    result.class += 1;
+                }
+                SelectorComponent::Element(_) => {
+                    result.typ += 1;
+                }
+                SelectorComponent::Hash(_) => {
+                    result.id += 1;
+                }
+                SelectorComponent::Star => {}
+                SelectorComponent::CombChild => {}
+                SelectorComponent::CombDescendant => {}
+            }
+        }
+
+        result
+    }
 }
 
 #[derive(Debug, Clone)]
@@ -117,28 +178,120 @@ pub(crate) enum Style {
     DisplayNone,
 }
 
+#[derive(Debug, Clone)]
+pub(crate) struct StyleDecl {
+    style: Style,
+    importance: Importance,
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd)]
+pub(crate) enum StyleOrigin {
+    #[default]
+    None,
+    Agent,
+    User,
+    Author,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub(crate) struct WithSpec<T: Copy + Clone> {
+    val: Option<T>,
+    origin: StyleOrigin,
+    specificity: Specificity,
+    important: bool,
+}
+impl<T: Copy + Clone> WithSpec<T> {
+    fn maybe_update(
+        &mut self,
+        important: bool,
+        origin: StyleOrigin,
+        specificity: Specificity,
+        val: T,
+    ) {
+        if self.val.is_some() {
+            // We already have a value, so need to check.
+            if self.important && !important {
+                // important takes priority over not important.
+                return;
+            }
+            // importance is the same.  Next is checking the origin.
+            {
+                use StyleOrigin::*;
+                match (self.origin, origin) {
+                    (Agent, Agent) | (User, User) | (Author, Author) => {
+                        // They're the same so continue the comparison
+                    }
+                    (mine, theirs) => {
+                        if important && theirs > mine {
+                            return;
+                        } else if !important && mine > theirs {
+                            return;
+                        }
+                    }
+                }
+            }
+            // We're now from the same origin an importance
+            if specificity < self.specificity {
+                return;
+            }
+        }
+        self.val = Some(val);
+        self.origin = origin;
+        self.specificity = specificity;
+        self.important = important;
+    }
+
+    pub fn val(&self) -> Option<T> {
+        self.val.clone()
+    }
+}
+
+impl<T: Copy + Clone> Default for WithSpec<T> {
+    fn default() -> Self {
+        WithSpec {
+            val: None,
+            origin: StyleOrigin::None,
+            specificity: Default::default(),
+            important: false,
+        }
+    }
+}
+
+#[derive(Debug, Copy, Clone, Default)]
+pub(crate) struct ComputedStyle {
+    /// The computed foreground colour, if any
+    pub(crate) colour: WithSpec<Colour>,
+    /// The specificity for colour
+    /// The computed background colour, if any
+    pub(crate) bg_colour: WithSpec<Colour>,
+    /// If set, indicates whether `display: none` or something equivalent applies
+    pub(crate) display_none: WithSpec<bool>,
+}
+
 #[derive(Debug, Clone)]
 struct Ruleset {
     selector: Selector,
-    styles: Vec<Style>,
+    styles: Vec<StyleDecl>,
 }
 
 /// Stylesheet data which can be used while building the render tree.
 #[derive(Clone, Default, Debug)]
 pub(crate) struct StyleData {
-    rules: Vec<Ruleset>,
+    agent_rules: Vec<Ruleset>,
+    user_rules: Vec<Ruleset>,
+    author_rules: Vec<Ruleset>,
 }
 
-pub(crate) fn parse_style_attribute(text: &str) -> Result<Vec<Style>> {
+pub(crate) fn parse_style_attribute(text: &str) -> Result<Vec<StyleDecl>> {
     html_trace_quiet!("Parsing inline style: {text}");
     let (_rest, decls) = parse_rules(text).map_err(|_| crate::Error::CssParseError)?;
 
-    let styles = styles_from_properties2(&decls);
+    let styles = styles_from_properties(&decls);
     html_trace_quiet!("Parsed inline style: {:?}", styles);
     Ok(styles)
 }
 
-fn styles_from_properties2(decls: &[parser::Declaration]) -> Vec<Style> {
+fn styles_from_properties(decls: &[parser::Declaration]) -> Vec<StyleDecl> {
     let mut styles = Vec::new();
     html_trace_quiet!("styles:from_properties2: {decls:?}");
     let mut overflow_hidden = false;
@@ -150,20 +303,26 @@ fn styles_from_properties2(decls: &[parser::Declaration]) -> Vec<Style> {
             parser::Decl::Color {
                 value: parser::Colour::Rgb(r, g, b),
             } => {
-                styles.push(Style::Colour(Colour {
-                    r: *r,
-                    g: *g,
-                    b: *b,
-                }));
+                styles.push(StyleDecl {
+                    style: Style::Colour(Colour {
+                        r: *r,
+                        g: *g,
+                        b: *b,
+                    }),
+                    importance: decl.important,
+                });
             }
             parser::Decl::BackgroundColor {
                 value: parser::Colour::Rgb(r, g, b),
             } => {
-                styles.push(Style::BgColour(Colour {
-                    r: *r,
-                    g: *g,
-                    b: *b,
-                }));
+                styles.push(StyleDecl {
+                    style: Style::BgColour(Colour {
+                        r: *r,
+                        g: *g,
+                        b: *b,
+                    }),
+                    importance: decl.important,
+                });
             }
             parser::Decl::Height { value } => match value {
                 parser::Height::Auto => (),
@@ -192,7 +351,10 @@ fn styles_from_properties2(decls: &[parser::Declaration]) -> Vec<Style> {
             parser::Decl::Overflow { .. } | parser::Decl::OverflowY { .. } => {}
             parser::Decl::Display { value } => {
                 if let parser::Display::None = value {
-                    styles.push(Style::DisplayNone);
+                    styles.push(StyleDecl {
+                        style: Style::DisplayNone,
+                        importance: decl.important,
+                    });
                 }
             } /*
               _ => {
@@ -203,7 +365,10 @@ fn styles_from_properties2(decls: &[parser::Declaration]) -> Vec<Style> {
     }
     // If the height is set to zero and overflow hidden, treat as display: none
     if height_zero && overflow_hidden {
-        styles.push(Style::DisplayNone);
+        styles.push(StyleDecl {
+            style: Style::DisplayNone,
+            importance: Importance::Default,
+        });
     }
     styles
 }
@@ -211,11 +376,11 @@ fn styles_from_properties2(decls: &[parser::Declaration]) -> Vec<Style> {
 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) -> Result<()> {
+    fn do_add_css(css: &str, rules: &mut Vec<Ruleset>) -> Result<()> {
         let (_, ss) = parser::parse_stylesheet(css).map_err(|_| crate::Error::CssParseError)?;
 
         for rule in ss {
-            let styles = styles_from_properties2(&rule.declarations);
+            let styles = styles_from_properties(&rule.declarations);
             if !styles.is_empty() {
                 for selector in rule.selectors {
                     let ruleset = Ruleset {
@@ -223,26 +388,63 @@ impl StyleData {
                         styles: styles.clone(),
                     };
                     html_trace_quiet!("Adding ruleset {ruleset:?}");
-                    self.rules.push(ruleset);
+                    rules.push(ruleset);
                 }
             }
         }
         Ok(())
     }
+    /// Add some CSS source to be included as part of the user agent ("browser") CSS rules.
+    pub fn add_agent_css(&mut self, css: &str) -> Result<()> {
+        Self::do_add_css(css, &mut self.agent_rules)
+    }
+
+    /// Add some CSS source to be included as part of the user CSS rules.
+    pub fn add_user_css(&mut self, css: &str) -> Result<()> {
+        Self::do_add_css(css, &mut self.user_rules)
+    }
+
+    /// Add some CSS source to be included as part of the document/author CSS rules.
+    pub fn add_author_css(&mut self, css: &str) -> Result<()> {
+        Self::do_add_css(css, &mut self.author_rules)
+    }
 
     /// Merge style data from other into this one.
     /// Data on other takes precedence.
     pub fn merge(&mut self, other: Self) {
-        self.rules.extend(other.rules);
+        self.agent_rules.extend(other.agent_rules);
+        self.user_rules.extend(other.user_rules);
+        self.author_rules.extend(other.author_rules);
     }
 
-    pub(crate) fn matching_rules(&self, handle: &Handle, use_doc_css: bool) -> Vec<Style> {
-        let mut result = Vec::new();
-        for rule in &self.rules {
-            if rule.selector.matches(handle) {
-                result.extend(rule.styles.iter().cloned());
+    pub(crate) fn computed_style(
+        &self,
+        parent_style: &ComputedStyle,
+        handle: &Handle,
+        use_doc_css: bool,
+    ) -> ComputedStyle {
+        let mut result = *parent_style;
+
+        for (origin, ruleset) in [
+            (StyleOrigin::Agent, &self.agent_rules),
+            (StyleOrigin::User, &self.user_rules),
+            (StyleOrigin::Author, &self.author_rules),
+        ] {
+            for rule in ruleset {
+                if rule.selector.matches(handle) {
+                    for style in rule.styles.iter() {
+                        Self::merge_computed_style(
+                            &mut result,
+                            style.importance == Importance::Important,
+                            origin,
+                            rule.selector.specificity(),
+                            style,
+                        );
+                    }
+                }
             }
         }
+
         if use_doc_css {
             // Now look for a style attribute
             if let Element { attrs, .. } = &handle.data {
@@ -250,14 +452,40 @@ impl StyleData {
                 for attr in borrowed.iter() {
                     if &attr.name.local == "style" {
                         let rules = parse_style_attribute(&attr.value).unwrap_or_default();
-                        result.extend(rules);
+                        for style in rules {
+                            Self::merge_computed_style(
+                                &mut result,
+                                false,
+                                StyleOrigin::Author,
+                                Specificity::inline(),
+                                &style,
+                            );
+                        }
                     } else if &*attr.name.local == "color" {
                         if let Ok(colour) = parser::parse_color_attribute(&attr.value) {
-                            result.push(Style::Colour(colour.into()));
+                            Self::merge_computed_style(
+                                &mut result,
+                                false,
+                                StyleOrigin::Author,
+                                Specificity::inline(),
+                                &StyleDecl {
+                                    style: Style::Colour(colour.into()),
+                                    importance: Importance::Default,
+                                },
+                            );
                         }
                     } else if &*attr.name.local == "bgcolor" {
                         if let Ok(colour) = parser::parse_color_attribute(&attr.value) {
-                            result.push(Style::BgColour(colour.into()));
+                            Self::merge_computed_style(
+                                &mut result,
+                                false,
+                                StyleOrigin::Author,
+                                Specificity::inline(),
+                                &StyleDecl {
+                                    style: Style::BgColour(colour.into()),
+                                    importance: Importance::Default,
+                                },
+                            );
                         }
                     }
                 }
@@ -266,6 +494,43 @@ impl StyleData {
 
         result
     }
+
+    fn merge_computed_style(
+        result: &mut ComputedStyle,
+        important: bool,
+        origin: StyleOrigin,
+        specificity: Specificity,
+        style: &StyleDecl,
+    ) {
+        // The increasing priority is:
+        // * agent
+        // * user
+        // * author
+        // * author !important
+        // * user !important
+        // * agent !important
+        // Since we view in the order agent, user, author, we always want to
+        // replace the value if we haven't yet seen an !important rule, and
+        // never afterwards.
+        match style.style {
+            Style::Colour(col) => {
+                result
+                    .colour
+                    .maybe_update(important, origin, specificity, col);
+            }
+            Style::BgColour(col) => {
+                result
+                    .bg_colour
+                    .maybe_update(important, origin, specificity, col);
+            }
+            Style::DisplayNone => {
+                // We don't have a "not DisplayNone" - we might need to fix this.
+                result
+                    .display_none
+                    .maybe_update(important, origin, specificity, true);
+            }
+        }
+    }
 }
 
 fn pending<'a, F>(handle: Handle, f: F) -> TreeMapResult<'a, (), Handle, Vec<String>>
@@ -338,8 +603,38 @@ pub(crate) fn dom_to_stylesheet<T: Write>(handle: Handle, err_out: &mut T) -> Re
     if let Some(styles) = styles {
         for css in styles {
             // Ignore CSS parse errors.
-            let _ = result.add_css(&css);
+            let _ = result.add_author_css(&css);
         }
     }
     Ok(result)
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::css::Specificity;
+
+    use super::parser::parse_selector;
+
+    #[test]
+    fn test_specificity() {
+        let sel_id1 = parse_selector("#foo").unwrap().1;
+        assert_eq!(
+            sel_id1.specificity(),
+            Specificity {
+                id: 1,
+                ..Default::default()
+            }
+        );
+
+        let sel_cl3 = parse_selector(".foo .bar .baz").unwrap().1;
+        assert_eq!(
+            sel_cl3.specificity(),
+            Specificity {
+                class: 3,
+                ..Default::default()
+            }
+        );
+
+        assert!(sel_id1.specificity() > sel_cl3.specificity());
+    }
+}
diff --git a/src/css/parser.rs b/src/css/parser.rs
index edf39e7..29450c1 100644
--- a/src/css/parser.rs
+++ b/src/css/parser.rs
@@ -733,7 +733,7 @@ fn parse_selector_without_element(text: &str) -> IResult<&str, Vec<SelectorCompo
     many1(parse_simple_selector_component)(text)
 }
 
-fn parse_selector(text: &str) -> IResult<&str, Selector> {
+pub(crate) fn parse_selector(text: &str) -> IResult<&str, Selector> {
     let (rest, mut components) = alt((
         parse_selector_with_element,
         parse_selector_without_element,
diff --git a/src/lib.rs b/src/lib.rs
index e88397f..660739c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -60,10 +60,10 @@ extern crate html5ever;
 #[macro_use]
 mod macros;
 
-#[cfg(feature = "css")]
 pub mod css;
 pub mod render;
 
+use css::ComputedStyle;
 use render::text_renderer::{
     RenderLine, RenderOptions, RichAnnotation, SubRenderer, TaggedLine, TextRenderer,
 };
@@ -82,6 +82,7 @@ use markup5ever_rcdom::{
 use std::cell::Cell;
 use std::cmp::{max, min};
 use std::collections::{BTreeSet, HashMap};
+use std::rc::Rc;
 use unicode_width::UnicodeWidthStr;
 
 use std::io;
@@ -106,7 +107,6 @@ 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,
@@ -196,6 +196,7 @@ struct RenderTableCell {
     content: Vec<RenderNode>,
     size_estimate: Cell<Option<SizeEstimate>>,
     col_width: Option<usize>, // Actual width to use
+    style: ComputedStyle,
 }
 
 impl RenderTableCell {
@@ -218,6 +219,7 @@ impl RenderTableCell {
 struct RenderTableRow {
     cells: Vec<RenderTableCell>,
     col_sizes: Option<Vec<usize>>,
+    style: ComputedStyle,
 }
 
 impl RenderTableRow {
@@ -251,7 +253,11 @@ impl RenderTableRow {
             // Skip any zero-width columns
             if col_width > 0 {
                 cell.col_width = Some(col_width + cell.colspan - 1);
-                result.push(RenderNode::new(RenderNodeInfo::TableCell(cell)));
+                let style = cell.style.clone();
+                result.push(RenderNode::new_styled(
+                    RenderNodeInfo::TableCell(cell),
+                    style,
+                ));
             }
             colno += colspan;
         }
@@ -335,7 +341,8 @@ impl RenderTable {
             .into_iter()
             .map(|mut tr| {
                 tr.col_sizes = Some(col_sizes.clone());
-                RenderNode::new(RenderNodeInfo::TableRow(tr, vert))
+                let style = tr.style.clone();
+                RenderNode::new_styled(RenderNodeInfo::TableRow(tr, vert), style)
             })
             .collect()
     }
@@ -433,12 +440,6 @@ enum RenderNodeInfo {
     TableCell(RenderTableCell),
     /// Start of a named HTML fragment
     FragStart(String),
-    /// A region with a foreground colour
-    #[allow(unused)]
-    Coloured(Colour, Vec<RenderNode>),
-    /// A region with a background colour
-    #[allow(unused)]
-    BgColoured(Colour, Vec<RenderNode>),
     /// A list item
     ListItem(Vec<RenderNode>),
     /// Superscript text
@@ -450,6 +451,7 @@ enum RenderNodeInfo {
 struct RenderNode {
     size_estimate: Cell<Option<SizeEstimate>>,
     info: RenderNodeInfo,
+    style: css::ComputedStyle,
 }
 
 impl RenderNode {
@@ -458,6 +460,16 @@ impl RenderNode {
         RenderNode {
             size_estimate: Cell::new(None),
             info,
+            style: Default::default(),
+        }
+    }
+
+    /// Create a node from the RenderNodeInfo.
+    fn new_styled(info: RenderNodeInfo, style: css::ComputedStyle) -> RenderNode {
+        RenderNode {
+            size_estimate: Cell::new(None),
+            info,
+            style,
         }
     }
 
@@ -583,10 +595,6 @@ impl RenderNode {
             Table(ref t) => t.calc_size_estimate(context),
             TableRow(..) | TableBody(_) | TableCell(_) => unimplemented!(),
             FragStart(_) => Default::default(),
-            BgColoured(_, ref v) | Coloured(_, ref v) => v
-                .iter()
-                .map(recurse)
-                .fold(Default::default(), SizeEstimate::add),
         };
         self.size_estimate.set(Some(estimate));
         estimate
@@ -627,7 +635,6 @@ impl RenderNode {
             Table(ref _t) => false,
             TableRow(..) | TableBody(_) | TableCell(_) => false,
             FragStart(_) => true,
-            BgColoured(_, ref v) | Coloured(_, ref v) => v.is_empty(),
         }
     }
 }
@@ -692,15 +699,6 @@ fn precalc_size_estimate<'a, 'b: 'a, D: TextDecorator>(
             }
         }
         TableRow(..) | TableBody(_) | TableCell(_) => unimplemented!(),
-        BgColoured(_, ref v) | Coloured(_, ref v) => TreeMapResult::PendingChildren {
-            children: v.iter().collect(),
-            cons: Box::new(move |context, _cs| {
-                node.calc_size_estimate(context, decorator);
-                Ok(None)
-            }),
-            prefn: None,
-            postfn: None,
-        },
     })
 }
 
@@ -751,10 +749,11 @@ fn desc_list_children_to_render_nodes<T: Write>(
 
 /// Convert a table into a RenderNode
 fn table_to_render_tree<'a, T: Write>(
-    handle: Handle,
+    input: RenderInput,
+    computed: css::ComputedStyle,
     _err_out: &mut T,
-) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode> {
-    pending(handle, |_, rowset| {
+) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode> {
+    pending(input, move |_, rowset| {
         let mut rows = vec![];
         for bodynode in rowset {
             if let RenderNodeInfo::TableBody(body) = bodynode.info {
@@ -766,19 +765,21 @@ fn table_to_render_tree<'a, T: Write>(
         if rows.is_empty() {
             Ok(None)
         } else {
-            Ok(Some(RenderNode::new(RenderNodeInfo::Table(
-                RenderTable::new(rows),
-            ))))
+            Ok(Some(RenderNode::new_styled(
+                RenderNodeInfo::Table(RenderTable::new(rows)),
+                computed,
+            )))
         }
     })
 }
 
 /// Add rows from a thead or tbody.
 fn tbody_to_render_tree<'a, T: Write>(
-    handle: Handle,
+    input: RenderInput,
+    computed: css::ComputedStyle,
     _err_out: &mut T,
-) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode> {
-    pending_noempty(handle, |_, rowchildren| {
+) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode> {
+    pending_noempty(input, move |_, rowchildren| {
         let mut rows = rowchildren
             .into_iter()
             .flat_map(|rownode| {
@@ -818,16 +819,20 @@ fn tbody_to_render_tree<'a, T: Write>(
             }
         }
 
-        Ok(Some(RenderNode::new(RenderNodeInfo::TableBody(rows))))
+        Ok(Some(RenderNode::new_styled(
+            RenderNodeInfo::TableBody(rows),
+            computed,
+        )))
     })
 }
 
 /// Convert a table row to a RenderTableRow
 fn tr_to_render_tree<'a, T: Write>(
-    handle: Handle,
+    input: RenderInput,
+    computed: css::ComputedStyle,
     _err_out: &mut T,
-) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode> {
-    pending(handle, |_, cellnodes| {
+) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode> {
+    pending(input, move |_, cellnodes| {
         let cells = cellnodes
             .into_iter()
             .flat_map(|cellnode| {
@@ -839,23 +844,28 @@ fn tr_to_render_tree<'a, T: Write>(
                 }
             })
             .collect();
-        Ok(Some(RenderNode::new(RenderNodeInfo::TableRow(
-            RenderTableRow {
-                cells,
-                col_sizes: None,
-            },
-            false,
-        ))))
+        Ok(Some(RenderNode::new_styled(
+            RenderNodeInfo::TableRow(
+                RenderTableRow {
+                    cells,
+                    col_sizes: None,
+                    style: computed.clone(),
+                },
+                false,
+            ),
+            computed,
+        )))
     })
 }
 
 /// Convert a single table cell to a render node.
 fn td_to_render_tree<'a, T: Write>(
-    handle: Handle,
+    input: RenderInput,
+    computed: css::ComputedStyle,
     _err_out: &mut T,
-) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode> {
+) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode> {
     let mut colspan = 1;
-    if let Element { ref attrs, .. } = handle.data {
+    if let Element { ref attrs, .. } = input.handle.data {
         for attr in attrs.borrow().iter() {
             if &attr.name.local == "colspan" {
                 let v: &str = &attr.value;
@@ -863,15 +873,17 @@ fn td_to_render_tree<'a, T: Write>(
             }
         }
     }
-    pending(handle, move |_, children| {
-        Ok(Some(RenderNode::new(RenderNodeInfo::TableCell(
-            RenderTableCell {
+    pending(input, move |_, children| {
+        Ok(Some(RenderNode::new_styled(
+            RenderNodeInfo::TableCell(RenderTableCell {
                 colspan,
                 content: children,
                 size_estimate: Cell::new(None),
                 col_width: None,
-            },
-        ))))
+                style: computed,
+            }),
+            computed,
+        )))
     })
 }
 
@@ -904,65 +916,6 @@ enum TreeMapResult<'a, C, N, R> {
     Nothing,
 }
 
-impl<'a, C: 'a, N> TreeMapResult<'a, C, N, RenderNode> {
-    #[cfg(feature = "css")]
-    fn wrap_render_nodes<F>(self, f: F) -> Self
-    where
-        F: Fn(Vec<RenderNode>) -> RenderNodeInfo + 'a,
-    {
-        use TreeMapResult::*;
-        match self {
-            Finished(node) => Finished(RenderNode::new(f(vec![node]))),
-            PendingChildren {
-                children,
-                cons,
-                prefn,
-                postfn,
-            } => {
-                PendingChildren {
-                    children,
-                    prefn,
-                    postfn,
-                    cons: Box::new(move |ctx, ch| {
-                        Ok(cons(ctx, ch)?.map(|n| {
-                            // Special case: lists need to recognise ListItem nodes as
-                            // direct children.
-                            match n.info {
-                                RenderNodeInfo::ListItem(children) => {
-                                    let wrapped = RenderNode::new(f(children));
-                                    RenderNode::new(RenderNodeInfo::ListItem(vec![wrapped]))
-                                }
-                                RenderNodeInfo::TableCell(mut cellinfo) => {
-                                    let children = cellinfo.content;
-                                    let wrapped = RenderNode::new(f(children));
-                                    cellinfo.content = vec![wrapped];
-                                    RenderNode::new(RenderNodeInfo::TableCell(cellinfo))
-                                }
-                                RenderNodeInfo::TableRow(mut row, vert) => {
-                                    let cells = row.cells;
-                                    let cells = cells
-                                        .into_iter()
-                                        .map(|mut child| {
-                                            let children = child.content;
-                                            let wrapped = RenderNode::new(f(children));
-                                            child.content = vec![wrapped];
-                                            child
-                                        })
-                                        .collect();
-                                    row.cells = cells;
-                                    RenderNode::new(RenderNodeInfo::TableRow(row, vert))
-                                }
-                                ni => RenderNode::new(f(vec![RenderNode::new(ni)])),
-                            }
-                        }))
-                    }),
-                }
-            }
-            Nothing => Nothing,
-        }
-    }
-}
-
 fn tree_map_reduce<'a, C, N, R, M>(
     context: &mut C,
     top: N,
@@ -1064,6 +1017,27 @@ struct HtmlContext {
     wrap_links: bool,
 }
 
+// Input to render tree conversion.
+struct RenderInput {
+    handle: Handle,
+    parent_style: Rc<ComputedStyle>,
+}
+
+impl RenderInput {
+    // Return the children in the right form
+    fn children(&self) -> Vec<RenderInput> {
+        self.handle
+            .children
+            .borrow()
+            .iter()
+            .map(|child| RenderInput {
+                handle: child.clone(),
+                parent_style: Rc::clone(&self.parent_style),
+            })
+            .collect()
+    }
+}
+
 fn dom_to_render_tree_with_context<T: Write>(
     handle: Handle,
     err_out: &mut T,
@@ -1077,21 +1051,30 @@ fn dom_to_render_tree_with_context<T: Write>(
         context.style_data = doc_style_data;
     }
 
-    let result = tree_map_reduce(context, handle, |context, handle| {
-        process_dom_node(handle, err_out, context)
-    });
+    let parent_style = Default::default();
+    let result = tree_map_reduce(
+        context,
+        RenderInput {
+            handle,
+            parent_style,
+        },
+        |context, input| process_dom_node(input, err_out, context),
+    );
 
     html_trace!("### dom_to_render_tree: out= {:#?}", result);
     result
 }
 
-fn pending<'a, F>(handle: Handle, f: F) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode>
+fn pending<'a, F>(
+    input: RenderInput,
+    f: F,
+) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode>
 where
     for<'r> F:
         Fn(&'r mut HtmlContext, std::vec::Vec<RenderNode>) -> Result<Option<RenderNode>> + 'static,
 {
     TreeMapResult::PendingChildren {
-        children: handle.children.borrow().clone(),
+        children: input.children(),
         cons: Box::new(f),
         prefn: None,
         postfn: None,
@@ -1099,15 +1082,25 @@ where
 }
 
 fn pending_noempty<'a, F>(
-    handle: Handle,
+    input: RenderInput,
     f: F,
-) -> TreeMapResult<'a, HtmlContext, Handle, RenderNode>
+) -> TreeMapResult<'a, HtmlContext, RenderInput, RenderNode>
 where
     for<'r> F:
         Fn(&'r mut HtmlContext, std::vec::Vec<RenderNode>) -> Result<Option<RenderNode>> + 'static,
 {
+    let handle = &input.handle;
+    let style = &input.parent_style;
     TreeMapResult::PendingChildren {
-        children: handle.children.borrow().clone(),
+        children: handle
+            .children
+            .borrow()
+            .iter()
+            .map(|child| RenderInput {
+                handle: child.clone(),
+                parent_style: Rc::clone(style),
+            })
+            .collect(),
         cons: Box::new(move |ctx, children| {
             if children.is_empty() {
                 Ok(None)
@@ -1182,16 +1175,16 @@ fn prepend_marker(prefix: RenderNode, mut orig: RenderNode) -> RenderNode {
 }
 
 fn process_dom_node<'a, T: Write>(
-    handle: Handle,
+    input: RenderInput,
     err_out: &mut T,
     #[allow(unused)] // Used with css feature
     context: &mut HtmlContext,
-) -> Result<TreeMapResult<'a, HtmlContext, Handle, RenderNode>> {
+) -> Result<TreeMapResult<'a, HtmlContext, RenderInput, RenderNode>> {
     use RenderNodeInfo::*;
     use TreeMapResult::*;
 
-    Ok(match handle.clone().data {
-        Document => pending(handle, |_context, cs| {
+    Ok(match input.handle.clone().data {
+        Document => pending(input, |_context, cs| {
             Ok(Some(RenderNode::new(Container(cs))))
         }),
         Comment { .. } => Nothing,
@@ -1202,33 +1195,31 @@ fn process_dom_node<'a, T: Write>(
         } => {
             let mut frag_from_name_attr = false;
 
+            let RenderInput {
+                ref handle,
+                ref parent_style,
+            } = input;
+
             #[cfg(feature = "css")]
-            let mut css_colour = None;
-            #[cfg(feature = "css")]
-            let mut css_bgcolour = None;
-            #[cfg(feature = "css")]
-            {
-                for style in context
-                    .style_data
-                    .matching_rules(&handle, context.use_doc_css)
-                {
-                    match style {
-                        css::Style::Colour(colour) => {
-                            css_colour = Some(colour);
-                        }
-                        css::Style::BgColour(colour) => {
-                            css_bgcolour = Some(colour);
-                        }
-                        css::Style::DisplayNone => {
-                            return Ok(Nothing);
-                        }
-                    }
+            let computed = {
+                let computed =
+                    context
+                        .style_data
+                        .computed_style(&parent_style, &handle, context.use_doc_css);
+                if let Some(true) = computed.display_none.val() {
+                    return Ok(Nothing);
                 }
+                computed
             };
+            #[cfg(not(feature = "css"))]
+            let computed = (**parent_style).clone();
+
             let result = match name.expanded() {
                 expanded_name!(html "html") | expanded_name!(html "body") => {
                     /* process children, but don't add anything */
-                    pending(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
+                    pending(input, move |_, cs| {
+                        Ok(Some(RenderNode::new_styled(Container(cs), computed)))
+                    })
                 }
                 expanded_name!(html "link")
                 | expanded_name!(html "meta")
@@ -1241,7 +1232,9 @@ fn process_dom_node<'a, T: Write>(
                 }
                 expanded_name!(html "span") => {
                     /* process children, but don't add anything */
-                    pending_noempty(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
+                    pending_noempty(input, move |_, cs| {
+                        Ok(Some(RenderNode::new_styled(Container(cs), computed)))
+                    })
                 }
                 expanded_name!(html "a") => {
                     let borrowed = attrs.borrow();
@@ -1254,18 +1247,20 @@ fn process_dom_node<'a, T: Write>(
                         }
                     }
                     PendingChildren {
-                        children: handle.children.borrow().clone(),
+                        children: input.children(),
                         cons: if let Some(href) = target {
                             let href: String = href.into();
                             Box::new(move |_, cs: Vec<RenderNode>| {
                                 if cs.iter().any(|c| !c.is_shallow_empty()) {
-                                    Ok(Some(RenderNode::new(Link(href, cs))))
+                                    Ok(Some(RenderNode::new_styled(Link(href, cs), computed)))
                                 } else {
                                     Ok(None)
                                 }
                             })
                         } else {
-                            Box::new(|_, cs| Ok(Some(RenderNode::new(Container(cs)))))
+                            Box::new(move |_, cs| {
+                                Ok(Some(RenderNode::new_styled(Container(cs), computed)))
+                            })
                         },
                         prefn: None,
                         postfn: None,
@@ -1273,18 +1268,20 @@ fn process_dom_node<'a, T: Write>(
                 }
                 expanded_name!(html "em")
                 | expanded_name!(html "i")
-                | expanded_name!(html "ins") => {
-                    pending(handle, |_, cs| Ok(Some(RenderNode::new(Em(cs)))))
-                }
-                expanded_name!(html "strong") => {
-                    pending(handle, |_, cs| Ok(Some(RenderNode::new(Strong(cs)))))
-                }
+                | expanded_name!(html "ins") => pending(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(Em(cs), computed)))
+                }),
+                expanded_name!(html "strong") => pending(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(Strong(cs), computed)))
+                }),
                 expanded_name!(html "s") | expanded_name!(html "del") => {
-                    pending(handle, |_, cs| Ok(Some(RenderNode::new(Strikeout(cs)))))
-                }
-                expanded_name!(html "code") => {
-                    pending(handle, |_, cs| Ok(Some(RenderNode::new(Code(cs)))))
+                    pending(input, move |_, cs| {
+                        Ok(Some(RenderNode::new_styled(Strikeout(cs), computed)))
+                    })
                 }
+                expanded_name!(html "code") => pending(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(Code(cs), computed)))
+                }),
                 expanded_name!(html "img") => {
                     let borrowed = attrs.borrow();
                     let mut title = None;
@@ -1301,7 +1298,10 @@ fn process_dom_node<'a, T: Write>(
                         }
                     }
                     if let (Some(title), Some(src)) = (title, src) {
-                        Finished(RenderNode::new(Img(src.into(), title.into())))
+                        Finished(RenderNode::new_styled(
+                            Img(src.into(), title.into()),
+                            computed,
+                        ))
                     } else {
                         Nothing
                     }
@@ -1311,40 +1311,40 @@ fn process_dom_node<'a, T: Write>(
                 | expanded_name!(html "h3")
                 | expanded_name!(html "h4") => {
                     let level: usize = name.local[1..].parse().unwrap();
-                    pending(handle, move |_, cs| {
-                        Ok(Some(RenderNode::new(Header(level, cs))))
+                    pending(input, move |_, cs| {
+                        Ok(Some(RenderNode::new_styled(Header(level, cs), computed)))
                     })
                 }
-                expanded_name!(html "p") => {
-                    pending_noempty(handle, |_, cs| Ok(Some(RenderNode::new(Block(cs)))))
-                }
-                expanded_name!(html "li") => {
-                    pending(handle, |_, cs| Ok(Some(RenderNode::new(ListItem(cs)))))
-                }
-                expanded_name!(html "sup") => {
-                    pending(handle, |_, cs| Ok(Some(RenderNode::new(Sup(cs)))))
-                }
-                expanded_name!(html "div") => {
-                    pending_noempty(handle, |_, cs| Ok(Some(RenderNode::new(Div(cs)))))
-                }
-                expanded_name!(html "pre") => {
-                    pending(handle, |_, cs| Ok(Some(RenderNode::new(Pre(cs)))))
-                }
-                expanded_name!(html "br") => Finished(RenderNode::new(Break)),
-                expanded_name!(html "table") => table_to_render_tree(handle.clone(), err_out),
+                expanded_name!(html "p") => pending_noempty(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(Block(cs), computed)))
+                }),
+                expanded_name!(html "li") => pending(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(ListItem(cs), computed)))
+                }),
+                expanded_name!(html "sup") => pending(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(Sup(cs), computed)))
+                }),
+                expanded_name!(html "div") => pending_noempty(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(Div(cs), computed)))
+                }),
+                expanded_name!(html "pre") => pending(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(Pre(cs), computed)))
+                }),
+                expanded_name!(html "br") => Finished(RenderNode::new_styled(Break, computed)),
+                expanded_name!(html "table") => table_to_render_tree(input, computed, err_out),
                 expanded_name!(html "thead") | expanded_name!(html "tbody") => {
-                    tbody_to_render_tree(handle.clone(), err_out)
+                    tbody_to_render_tree(input, computed, err_out)
                 }
-                expanded_name!(html "tr") => tr_to_render_tree(handle.clone(), err_out),
+                expanded_name!(html "tr") => tr_to_render_tree(input, computed, err_out),
                 expanded_name!(html "th") | expanded_name!(html "td") => {
-                    td_to_render_tree(handle.clone(), err_out)
-                }
-                expanded_name!(html "blockquote") => {
-                    pending_noempty(handle, |_, cs| Ok(Some(RenderNode::new(BlockQuote(cs)))))
-                }
-                expanded_name!(html "ul") => {
-                    pending_noempty(handle, |_, cs| Ok(Some(RenderNode::new(Ul(cs)))))
+                    td_to_render_tree(input, computed, err_out)
                 }
+                expanded_name!(html "blockquote") => pending_noempty(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(BlockQuote(cs), computed)))
+                }),
+                expanded_name!(html "ul") => pending_noempty(input, move |_, cs| {
+                    Ok(Some(RenderNode::new_styled(Ul(cs), computed)))
+                }),
                 expanded_name!(html "ol") => {
                     let borrowed = attrs.borrow();
                     let mut start = 1;
@@ -1355,20 +1355,27 @@ fn process_dom_node<'a, T: Write>(
                         }
                     }
 
-                    pending_noempty(handle, move |_, cs| {
+                    pending_noempty(input, move |_, cs| {
                         let cs = cs
                             .into_iter()
                             .filter(|n| matches!(n.info, RenderNodeInfo::ListItem(..)))
                             .collect();
-                        Ok(Some(RenderNode::new(Ol(start, cs))))
+                        Ok(Some(RenderNode::new_styled(Ol(start, cs), computed)))
                     })
                 }
-                expanded_name!(html "dl") => Finished(RenderNode::new(Dl(
-                    desc_list_children_to_render_nodes(handle.clone(), err_out, context)?,
-                ))),
+                expanded_name!(html "dl") => Finished(RenderNode::new_styled(
+                    Dl(desc_list_children_to_render_nodes(
+                        handle.clone(),
+                        err_out,
+                        context,
+                    )?),
+                    computed,
+                )),
                 _ => {
                     html_trace!("Unhandled element: {:?}\n", name.local);
-                    pending_noempty(handle, |_, cs| Ok(Some(RenderNode::new(Container(cs)))))
+                    pending_noempty(input, move |_, cs| {
+                        Ok(Some(RenderNode::new_styled(Container(cs), computed)))
+                    })
                     //None
                 }
             };
@@ -1410,19 +1417,6 @@ fn process_dom_node<'a, T: Write>(
                 result
             };
 
-            #[cfg(feature = "css")]
-            let result = if let Some(colour) = css_colour {
-                result.wrap_render_nodes(move |children| Coloured(colour, children))
-            } else {
-                result
-            };
-            #[cfg(feature = "css")]
-            let result = if let Some(colour) = css_bgcolour {
-                result.wrap_render_nodes(move |children| BgColoured(colour, children))
-            } else {
-                result
-            };
-
             result
         }
         markup5ever_rcdom::NodeData::Text { contents: ref tstr } => {
@@ -1465,7 +1459,7 @@ fn render_tree_to_string<T: Write, D: TextDecorator>(
 fn pending2<
     'a,
     D: TextDecorator,
-    F: Fn(
+    F: FnOnce(
             &mut TextRenderer<D>,
             Vec<Option<SubRenderer<D>>>,
         ) -> Result<Option<Option<SubRenderer<D>>>>
@@ -1482,6 +1476,37 @@ fn pending2<
     }
 }
 
+/// Keep track of what style state has been applied to a renderer so that we
+/// can undo it.
+#[derive(Default)]
+struct PushedStyleInfo {
+    colour: bool,
+    bgcolour: bool,
+}
+
+impl PushedStyleInfo {
+    fn apply<D: TextDecorator>(render: &mut TextRenderer<D>, style: &css::ComputedStyle) -> Self {
+        let mut result: PushedStyleInfo = Default::default();
+        if let Some(col) = style.colour.val() {
+            render.push_colour(col);
+            result.colour = true;
+        }
+        if let Some(col) = style.bg_colour.val() {
+            render.push_bgcolour(col);
+            result.bgcolour = true;
+        }
+        result
+    }
+    fn unwind<D: TextDecorator>(self, renderer: &mut TextRenderer<D>) {
+        if self.bgcolour {
+            renderer.pop_bgcolour();
+        }
+        if self.colour {
+            renderer.pop_colour();
+        }
+    }
+}
+
 fn do_render_node<T: Write, D: TextDecorator>(
     renderer: &mut TextRenderer<D>,
     tree: RenderNode,
@@ -1493,16 +1518,23 @@ fn do_render_node<T: Write, D: TextDecorator>(
 
     let size_estimate = tree.size_estimate.get().unwrap_or_default();
 
+    let pushed_style = PushedStyleInfo::apply(renderer, &tree.style);
+
     Ok(match tree.info {
         Text(ref tstr) => {
             renderer.add_inline_text(tstr)?;
+            pushed_style.unwind(renderer);
             Finished(None)
         }
-        Container(children) => pending2(children, |_, _| Ok(Some(None))),
+        Container(children) => pending2(children, |renderer, _| {
+            pushed_style.unwind(renderer);
+            Ok(Some(None))
+        }),
         Link(href, children) => {
             renderer.start_link(&href)?;
-            pending2(children, |renderer: &mut TextRenderer<D>, _| {
+            pending2(children, move |renderer: &mut TextRenderer<D>, _| {
                 renderer.end_link()?;
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1510,6 +1542,7 @@ fn do_render_node<T: Write, D: TextDecorator>(
             renderer.start_emphasis()?;
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 renderer.end_emphasis()?;
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1517,6 +1550,7 @@ fn do_render_node<T: Write, D: TextDecorator>(
             renderer.start_strong()?;
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 renderer.end_strong()?;
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1524,6 +1558,7 @@ fn do_render_node<T: Write, D: TextDecorator>(
             renderer.start_strikeout()?;
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 renderer.end_strikeout()?;
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1531,17 +1566,20 @@ fn do_render_node<T: Write, D: TextDecorator>(
             renderer.start_code()?;
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 renderer.end_code()?;
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
         Img(src, title) => {
             renderer.add_image(&src, &title)?;
+            pushed_style.unwind(renderer);
             Finished(None)
         }
         Block(children) | ListItem(children) => {
             renderer.start_block()?;
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 renderer.end_block();
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1560,6 +1598,7 @@ fn do_render_node<T: Write, D: TextDecorator>(
                 renderer.start_block()?;
                 renderer.append_subrender(sub_builder, repeat(&prefix[..]))?;
                 renderer.end_block();
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1567,6 +1606,7 @@ fn do_render_node<T: Write, D: TextDecorator>(
             renderer.new_line()?;
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 renderer.new_line()?;
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1576,6 +1616,7 @@ fn do_render_node<T: Write, D: TextDecorator>(
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 renderer.new_line()?;
                 renderer.end_pre();
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1592,6 +1633,7 @@ fn do_render_node<T: Write, D: TextDecorator>(
                 renderer.start_block()?;
                 renderer.append_subrender(sub_builder, repeat(&prefix[..]))?;
                 renderer.end_block();
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1603,7 +1645,10 @@ fn do_render_node<T: Write, D: TextDecorator>(
 
             TreeMapResult::PendingChildren {
                 children: items,
-                cons: Box::new(|_, _| Ok(Some(None))),
+                cons: Box::new(|renderer, _| {
+                    pushed_style.unwind(renderer);
+                    Ok(Some(None))
+                }),
                 prefn: Some(Box::new(move |renderer: &mut TextRenderer<D>, _| {
                     let inner_width = size_estimate.min_width - prefix_len;
                     let sub_builder = renderer
@@ -1641,7 +1686,10 @@ fn do_render_node<T: Write, D: TextDecorator>(
 
             TreeMapResult::PendingChildren {
                 children: items,
-                cons: Box::new(|_, _| Ok(Some(None))),
+                cons: Box::new(|renderer, _| {
+                    pushed_style.unwind(renderer);
+                    Ok(Some(None))
+                }),
                 prefn: Some(Box::new(move |renderer: &mut TextRenderer<D>, _| {
                     let inner_min = size_estimate.min_width - size_estimate.prefix_size;
                     let sub_builder = renderer
@@ -1668,7 +1716,10 @@ fn do_render_node<T: Write, D: TextDecorator>(
 
             TreeMapResult::PendingChildren {
                 children: items,
-                cons: Box::new(|_, _| Ok(Some(None))),
+                cons: Box::new(|renderer, _| {
+                    pushed_style.unwind(renderer);
+                    Ok(Some(None))
+                }),
                 prefn: None,
                 postfn: None,
             }
@@ -1678,6 +1729,7 @@ fn do_render_node<T: Write, D: TextDecorator>(
             renderer.start_emphasis()?;
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 renderer.end_emphasis()?;
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
@@ -1688,36 +1740,25 @@ fn do_render_node<T: Write, D: TextDecorator>(
             pending2(children, |renderer: &mut TextRenderer<D>, _| {
                 let sub_builder = renderer.pop();
                 renderer.append_subrender(sub_builder, repeat("  "))?;
+                pushed_style.unwind(renderer);
                 Ok(Some(None))
             })
         }
         Break => {
             renderer.new_line_hard()?;
+            pushed_style.unwind(renderer);
             Finished(None)
         }
         Table(tab) => render_table_tree(renderer, tab, err_out)?,
-        TableRow(row, false) => render_table_row(renderer, row, err_out),
-        TableRow(row, true) => render_table_row_vert(renderer, row, err_out),
+        TableRow(row, false) => render_table_row(renderer, row, pushed_style, err_out),
+        TableRow(row, true) => render_table_row_vert(renderer, row, pushed_style, err_out),
         TableBody(_) => unimplemented!("Unexpected TableBody while rendering"),
-        TableCell(cell) => render_table_cell(renderer, cell, err_out),
+        TableCell(cell) => render_table_cell(renderer, cell, pushed_style, err_out),
         FragStart(fragname) => {
             renderer.record_frag_start(&fragname);
+            pushed_style.unwind(renderer);
             Finished(None)
         }
-        Coloured(colour, children) => {
-            renderer.push_colour(colour);
-            pending2(children, |renderer: &mut TextRenderer<D>, _| {
-                renderer.pop_colour();
-                Ok(Some(None))
-            })
-        }
-        BgColoured(colour, children) => {
-            renderer.push_bgcolour(colour);
-            pending2(children, |renderer: &mut TextRenderer<D>, _| {
-                renderer.pop_bgcolour();
-                Ok(Some(None))
-            })
-        }
         Sup(children) => {
             // Special case for digit-only superscripts - use superscript
             // characters.
@@ -1741,11 +1782,13 @@ fn do_render_node<T: Write, D: TextDecorator>(
             }
             if let Some(digitstr) = sup_digits(&children) {
                 renderer.add_inline_text(&digitstr)?;
+                pushed_style.unwind(renderer);
                 Finished(None)
             } else {
                 renderer.start_superscript()?;
                 pending2(children, |renderer: &mut TextRenderer<D>, _| {
                     renderer.end_superscript()?;
+                    pushed_style.unwind(renderer);
                     Ok(Some(None))
                 })
             }
@@ -1864,6 +1907,7 @@ fn render_table_tree<T: Write, D: TextDecorator>(
 fn render_table_row<T: Write, D: TextDecorator>(
     _renderer: &mut TextRenderer<D>,
     row: RenderTableRow,
+    pushed_style: PushedStyleInfo,
     _err_out: &mut T,
 ) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
     TreeMapResult::PendingChildren {
@@ -1873,6 +1917,7 @@ fn render_table_row<T: Write, D: TextDecorator>(
             if children.iter().any(|c| !c.empty()) {
                 builders.append_columns_with_borders(children, true)?;
             }
+            pushed_style.unwind(builders);
             Ok(Some(None))
         }),
         prefn: Some(Box::new(|renderer: &mut TextRenderer<D>, node| {
@@ -1891,6 +1936,7 @@ fn render_table_row<T: Write, D: TextDecorator>(
 fn render_table_row_vert<T: Write, D: TextDecorator>(
     _renderer: &mut TextRenderer<D>,
     row: RenderTableRow,
+    pushed_style: PushedStyleInfo,
     _err_out: &mut T,
 ) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
     TreeMapResult::PendingChildren {
@@ -1898,6 +1944,7 @@ fn render_table_row_vert<T: Write, D: TextDecorator>(
         cons: Box::new(|builders, children| {
             let children: Vec<_> = children.into_iter().map(Option::unwrap).collect();
             builders.append_vert_row(children)?;
+            pushed_style.unwind(builders);
             Ok(Some(None))
         }),
         prefn: Some(Box::new(|renderer: &mut TextRenderer<D>, node| {
@@ -1916,10 +1963,13 @@ fn render_table_row_vert<T: Write, D: TextDecorator>(
 fn render_table_cell<T: Write, D: TextDecorator>(
     _renderer: &mut TextRenderer<D>,
     cell: RenderTableCell,
+    pushed_style: PushedStyleInfo,
     _err_out: &mut T,
 ) -> TreeMapResult<'static, TextRenderer<D>, RenderNode, Option<SubRenderer<D>>> {
     pending2(cell.content, |renderer: &mut TextRenderer<D>, _| {
+        pushed_style.unwind(renderer);
         let sub_builder = renderer.pop();
+
         Ok(Some(Some(sub_builder)))
     })
 }
@@ -2073,7 +2123,15 @@ pub mod config {
         /// Add some CSS rules which will be used (if supported) with any
         /// HTML processed.
         pub fn add_css(mut self, css: &str) -> Result<Self> {
-            self.style.add_css(css)?;
+            self.style.add_user_css(css)?;
+            Ok(self)
+        }
+
+        #[cfg(feature = "css")]
+        /// Add some agent CSS rules which will be used (if supported) with any
+        /// HTML processed.
+        pub fn add_agent_css(mut self, css: &str) -> Result<Self> {
+            self.style.add_agent_css(css)?;
             Ok(self)
         }
 


Test patch:
diff --git a/src/tests.rs b/src/tests.rs
index bb5fe97..77e7c92 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -2,7 +2,7 @@ use crate::config::Config;
 use crate::render::text_renderer::PlainDecorator;
 use crate::{config, Error};
 
-use super::render::text_renderer::{RichAnnotation, TaggedLine, TrivialDecorator};
+use super::render::text_renderer::{RichAnnotation, RichDecorator, TaggedLine, TrivialDecorator};
 use super::{from_read, from_read_with_decorator, parse, TextDecorator};
 
 /// Like assert_eq!(), but prints out the results normally as well
@@ -124,13 +124,20 @@ fn test_colour_map(annotations: &[RichAnnotation], s: &str) -> String {
 
 #[cfg(feature = "css")]
 #[track_caller]
-fn test_html_coloured(input: &[u8], expected: &str, width: usize) {
-    let result = config::rich()
-        .use_doc_css()
+fn test_html_coloured_conf<F>(input: &[u8], expected: &str, width: usize, conf: F)
+where
+    F: Fn(Config<RichDecorator>) -> Config<RichDecorator>,
+{
+    let result = conf(config::rich().use_doc_css())
         .coloured(input, width, test_colour_map)
         .unwrap();
     assert_eq_str!(result, expected);
 }
+#[cfg(feature = "css")]
+#[track_caller]
+fn test_html_coloured(input: &[u8], expected: &str, width: usize) {
+    test_html_coloured_conf(input, expected, width, |c| c)
+}
 #[track_caller]
 fn test_html_err_conf<F>(input: &[u8], expected: Error, width: usize, conf: F)
 where
@@ -2065,7 +2072,7 @@ foo
 
 #[cfg(feature = "css")]
 mod css_tests {
-    use super::{test_html_coloured, test_html_css, test_html_style};
+    use super::{test_html_coloured, test_html_coloured_conf, test_html_css, test_html_style};
 
     #[test]
     fn test_disp_none() {
@@ -2468,12 +2475,78 @@ Baz
             r#"───┬─────
 Row│One  
 ───┼─────
-<R>Row</R>│<R>Two</R>  
-───┼─────
+<R>Row│Two  </R>
+<R>───┼─────</R>
 Row│Three
 ───┴─────
 "#,
             20,
         );
     }
+
+    #[test]
+    fn test_css_levels() {
+        test_html_coloured_conf(
+            br#"
+        <head><style>
+            .doc_red { color: #f00; }
+            .doc_red_imp { color: #f00 !important; }
+        </style></head>
+        <body>
+            <p class="doc_red">Doc red</p>
+            <p class="agent_green">Agent green</p>
+            <p class="user_blue">User blue</p>
+            <p class="doc_red agent_green">Doc vs agent</p>
+            <p class="agent_green user_blue">Agent vs user</p>
+            <p class="user_blue doc_red">User vs doc</p>
+            <p class="doc_red agent_green_imp">Doc vs agent!</p>
+            <p class="agent_green_imp user_blue">Agent! vs user</p>
+            <p class="user_blue_imp doc_red">User! vs doc</p>
+            <p class="doc_red_imp agent_green_imp">Doc! vs agent!</p>
+            <p class="agent_green_imp user_blue_imp">Agent! vs user!</p>
+            <p class="user_blue_imp doc_red_imp">User! vs doc!</p>
+        </body>"#,
+            r#"<R>Doc red</R>
+
+<G>Agent green</G>
+
+<B>User blue</B>
+
+<R>Doc vs agent</R>
+
+<B>Agent vs user</B>
+
+<R>User vs doc</R>
+
+<G>Doc vs agent!</G>
+
+<G>Agent! vs user</G>
+
+<B>User! vs doc</B>
+
+<G>Doc! vs agent!</G>
+
+<G>Agent! vs user!</G>
+
+<B>User! vs doc!</B>
+"#,
+            80,
+            |conf| {
+                conf.add_agent_css(
+                    r#"
+                .agent_green { color: #0f0; }
+                .agent_green_imp { color: #0f0 !important; }
+                "#,
+                )
+                .unwrap()
+                .add_css(
+                    r#"
+                .user_blue { color: #00f; }
+                .user_blue_imp { color: #00f !important; }
+                "#,
+                )
+                .unwrap()
+            },
+        );
+    }
 }