diff --git a/src/css.rs b/src/css.rs index a7bb8a6..5471fea 100644 --- a/src/css.rs +++ b/src/css.rs @@ -10,7 +10,8 @@ use crate::{ Handle, NodeData::{self, Comment, Document, Element}, }, - tree_map_reduce, Colour, Result, TreeMapResult, + tree_map_reduce, Colour, ComputedStyle, Result, Specificity, StyleOrigin, TreeMapResult, + WhiteSpace, }; use self::parser::Importance; @@ -25,43 +26,6 @@ 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. @@ -176,6 +140,7 @@ pub(crate) enum Style { Colour(Colour), BgColour(Colour), DisplayNone, + WhiteSpace(WhiteSpace), } #[derive(Debug, Clone)] @@ -184,88 +149,6 @@ pub(crate) struct StyleDecl { 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) || (!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 - } -} - -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, @@ -354,6 +237,12 @@ fn styles_from_properties(decls: &[parser::Declaration]) -> Vec<StyleDecl> { importance: decl.important, }); } + } + parser::Decl::WhiteSpace { value } => { + styles.push(StyleDecl { + style: Style::WhiteSpace(*value), + importance: decl.important, + }); } /* _ => { html_trace_quiet!("CSS: Unhandled property {:?}", decl); @@ -386,7 +275,7 @@ impl StyleData { styles: styles.clone(), }; html_trace_quiet!("Adding ruleset {ruleset:?}"); - rules.push(ruleset); + rules.push(dbg!(ruleset)); } } } @@ -527,6 +416,11 @@ impl StyleData { .display_none .maybe_update(important, origin, specificity, true); } + Style::WhiteSpace(ws) => { + result + .white_space + .maybe_update(important, origin, specificity, ws); + } } } } @@ -609,7 +503,7 @@ pub(crate) fn dom_to_stylesheet<T: Write>(handle: Handle, err_out: &mut T) -> Re #[cfg(test)] mod tests { - use crate::css::Specificity; + use crate::Specificity; use super::parser::parse_selector; diff --git a/src/css/parser.rs b/src/css/parser.rs index 29450c1..607c341 100644 --- a/src/css/parser.rs +++ b/src/css/parser.rs @@ -64,6 +64,7 @@ pub enum Display { } #[derive(Debug, PartialEq)] +#[non_exhaustive] pub enum Decl { Color { value: Colour, @@ -86,6 +87,9 @@ pub enum Decl { Display { value: Display, }, + WhiteSpace { + value: WhiteSpace, + }, Unknown { name: PropertyName, // value: Vec<Token>, @@ -161,7 +165,7 @@ pub struct Declaration { pub important: Importance, } -use super::{Selector, SelectorComponent}; +use super::{Selector, SelectorComponent, WhiteSpace}; #[derive(Debug, PartialEq)] pub(crate) struct RuleSet { @@ -427,6 +431,10 @@ pub fn parse_declaration(text: &str) -> IResult<&str, Option<Declaration>> { let value = parse_display(&value)?; Decl::Display { value } } + "white-space" => { + let value = parse_white_space(&value)?; + Decl::WhiteSpace { value } + } _ => Decl::Unknown { name: prop, // value: /*value*/"".into(), @@ -679,6 +687,22 @@ fn parse_display(value: &RawValue) -> Result<Display, nom::Err<nom::error::Error Ok(Display::Other) } +fn parse_white_space( + value: &RawValue, +) -> Result<WhiteSpace, nom::Err<nom::error::Error<&'static str>>> { + for tok in &value.tokens { + if let Token::Ident(word) = tok { + match word.deref() { + "normal" => return Ok(WhiteSpace::Normal), + "pre" => return Ok(WhiteSpace::Pre), + "pre-wrap" => return Ok(WhiteSpace::PreWrap), + _ => (), + } + } + } + Ok(WhiteSpace::Normal) +} + pub fn parse_rules(text: &str) -> IResult<&str, Vec<Declaration>> { separated_list0( tuple((tag(";"), skip_optional_whitespace)), diff --git a/src/lib.rs b/src/lib.rs index abbc164..48ca823 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,13 +64,6 @@ mod macros; pub mod css; pub mod render; -#[cfg(feature = "css")] -use css::ComputedStyle; - -#[cfg(not(feature = "css"))] -#[derive(Copy, Clone, Debug, Default)] -struct ComputedStyle; - use render::text_renderer::{ RenderLine, RenderOptions, RichAnnotation, SubRenderer, TaggedLine, TextRenderer, }; @@ -96,6 +89,27 @@ use std::io; use std::io::Write; use std::iter::{once, repeat}; +#[derive(Debug, Copy, Clone, Default, PartialEq)] +pub(crate) enum WhiteSpace { + #[default] + Normal, + // NoWrap, + Pre, + #[allow(unused)] + PreWrap, + // PreLine, + // BreakSpaces, +} + +impl WhiteSpace { + pub fn preserve_whitespace(&self) -> bool { + match self { + WhiteSpace::Normal => false, + WhiteSpace::Pre | WhiteSpace::PreWrap => true, + } + } +} + /// An RGB colour value #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Colour { @@ -107,6 +121,132 @@ pub struct Colour { pub b: u8, } +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, PartialOrd)] +pub(crate) enum StyleOrigin { + #[default] + None, + Agent, + #[allow(unused)] + User, + #[allow(unused)] + Author, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] +pub(crate) struct Specificity { + inline: bool, + id: u16, + class: u16, + typ: u16, +} + +impl Specificity { + #[cfg(feature = "css")] + 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(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> { + pub(crate) 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) || (!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 + } +} + +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 { + #[cfg(feature = "css")] + /// The computed foreground colour, if any + pub(crate) colour: WithSpec<Colour>, + #[cfg(feature = "css")] + /// The computed background colour, if any + pub(crate) bg_colour: WithSpec<Colour>, + #[cfg(feature = "css")] + /// If set, indicates whether `display: none` or something equivalent applies + pub(crate) display_none: WithSpec<bool>, + /// The CSS white-space property + pub(crate) white_space: WithSpec<WhiteSpace>, +} + /// Errors from reading or rendering HTML #[derive(thiserror::Error, Debug)] #[non_exhaustive] @@ -419,8 +559,6 @@ enum RenderNodeInfo { Header(usize, Vec<RenderNode>), /// A Div element with children Div(Vec<RenderNode>), - /// A preformatted region. - Pre(Vec<RenderNode>), /// A blockquote BlockQuote(Vec<RenderNode>), /// An unordered list @@ -532,8 +670,7 @@ impl RenderNode { } Container(ref v) | Em(ref v) | Strong(ref v) | Strikeout(ref v) | Code(ref v) - | Block(ref v) | Div(ref v) | Pre(ref v) | Dl(ref v) | Dt(ref v) | ListItem(ref v) - | Sup(ref v) => v + | Block(ref v) | Div(ref v) | Dl(ref v) | Dt(ref v) | ListItem(ref v) | Sup(ref v) => v .iter() .map(recurse) .fold(Default::default(), SizeEstimate::add), @@ -629,7 +766,6 @@ impl RenderNode { | Block(ref v) | ListItem(ref v) | Div(ref v) - | Pre(ref v) | BlockQuote(ref v) | Dl(ref v) | Dt(ref v) @@ -670,7 +806,6 @@ fn precalc_size_estimate<'a, 'b: 'a, D: TextDecorator>( | Block(ref v) | ListItem(ref v) | Div(ref v) - | Pre(ref v) | BlockQuote(ref v) | Ul(ref v) | Ol(_, ref v) @@ -1138,7 +1273,6 @@ fn prepend_marker(prefix: RenderNode, mut orig: RenderNode) -> RenderNode { Block(ref mut children) | ListItem(ref mut children) | Div(ref mut children) - | Pre(ref mut children) | BlockQuote(ref mut children) | Container(ref mut children) | TableCell(RenderTableCell { @@ -1335,7 +1469,14 @@ fn process_dom_node<'a, T: Write>( Ok(Some(RenderNode::new_styled(Div(cs), computed))) }), expanded_name!(html "pre") => pending(input, move |_, cs| { - Ok(Some(RenderNode::new_styled(Pre(cs), computed))) + let mut computed = computed; + computed.white_space.maybe_update( + false, + StyleOrigin::Agent, + Default::default(), + WhiteSpace::Pre, + ); + Ok(Some(RenderNode::new_styled(Block(cs), computed))) }), expanded_name!(html "br") => Finished(RenderNode::new_styled(Break, computed)), expanded_name!(html "table") => table_to_render_tree(input, computed, err_out), @@ -1491,22 +1632,33 @@ fn pending2< struct PushedStyleInfo { colour: bool, bgcolour: bool, + white_space: bool, } impl PushedStyleInfo { fn apply<D: TextDecorator>(render: &mut TextRenderer<D>, style: &ComputedStyle) -> Self { #[allow(unused_mut)] let mut result: PushedStyleInfo = Default::default(); - #[cfg(feature = "css")] { + #[cfg(feature = "css")] if let Some(col) = style.colour.val() { render.push_colour(col); result.colour = true; } + #[cfg(feature = "css")] if let Some(col) = style.bg_colour.val() { render.push_bgcolour(col); result.bgcolour = true; } + if let Some(ws) = style.white_space.val() { + match ws { + WhiteSpace::Normal => {} + WhiteSpace::Pre | WhiteSpace::PreWrap => { + render.push_ws(ws); + result.white_space = true; + } + } + } } #[cfg(not(feature = "css"))] { @@ -1522,6 +1674,9 @@ impl PushedStyleInfo { if self.colour { renderer.pop_colour(); } + if self.white_space { + renderer.pop_ws(); + } } } @@ -1628,16 +1783,6 @@ fn do_render_node<T: Write, D: TextDecorator>( Ok(Some(None)) }) } - Pre(children) => { - renderer.new_line()?; - renderer.start_pre(); - pending2(children, |renderer: &mut TextRenderer<D>, _| { - renderer.new_line()?; - renderer.end_pre(); - pushed_style.unwind(renderer); - Ok(Some(None)) - }) - } BlockQuote(children) => { let prefix = renderer.quote_prefix(); debug_assert!(size_estimate.prefix_size == prefix.len()); diff --git a/src/render/mod.rs b/src/render/mod.rs index 574622e..5437920 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -3,6 +3,7 @@ use crate::Colour; use crate::Error; +use crate::WhiteSpace; pub(crate) mod text_renderer; @@ -45,11 +46,11 @@ pub(crate) trait Renderer { } /// Begin a preformatted block. Until the corresponding end, - /// whitespace will used verbatim. Pre regions can nest. - fn start_pre(&mut self); + /// whitespace handlinng will be modified. Can be nested. + fn push_ws(&mut self, ws: WhiteSpace); - /// Finish a preformatted block started with `start_pre`. - fn end_pre(&mut self); + /// Finish a preformatted block started with `push_ws`. + fn pop_ws(&mut self); /// Add some inline text (which should be wrapped at the /// appropriate width) to the current block. diff --git a/src/render/text_renderer.rs b/src/render/text_renderer.rs index dd511e5..5b9a986 100644 --- a/src/render/text_renderer.rs +++ b/src/render/text_renderer.rs @@ -5,6 +5,7 @@ use crate::Colour; use crate::Error; +use crate::WhiteSpace; use super::Renderer; use std::cell::Cell; @@ -332,7 +333,7 @@ impl<T: Clone + Eq + Debug + Default> WrappedBlock<T> { } } - fn flush_word(&mut self) -> Result<(), Error> { + fn flush_word(&mut self, with_space: bool) -> Result<(), Error> { use self::TaggedLineElement::Str; /* Finish the word. */ @@ -343,7 +344,7 @@ impl<T: Clone + Eq + Debug + Default> WrappedBlock<T> { let space_needed = self.wordlen + if self.linelen > 0 { 1 } else { 0 }; // space if space_needed <= space_in_line { html_trace!("Got enough space"); - if self.linelen > 0 { + if self.linelen > 0 && with_space { self.line.push(Str(TaggedString { s: " ".into(), tag: self.spacetag.clone().unwrap_or_else(|| Default::default()), @@ -458,7 +459,7 @@ impl<T: Clone + Eq + Debug + Default> WrappedBlock<T> { } fn flush(&mut self) -> Result<(), Error> { - self.flush_word()?; + self.flush_word(true)?; self.flush_line(); Ok(()) } @@ -492,7 +493,7 @@ impl<T: Clone + Eq + Debug + Default> WrappedBlock<T> { for c in text.chars() { if c.is_whitespace() { /* Whitespace is mostly ignored, except to terminate words. */ - self.flush_word()?; + self.flush_word(true)?; self.spacetag = Some(tag.clone()); } else if let Some(charwidth) = UnicodeWidthChar::width(c) { /* Not whitespace; add to the current word. */ @@ -518,7 +519,7 @@ impl<T: Clone + Eq + Debug + Default> WrappedBlock<T> { ); // Make sure that any previous word has been sent to the line, as we // bypass the word buffer. - self.flush_word()?; + self.flush_word(true)?; for c in text.chars() { if let Some(charwidth) = UnicodeWidthChar::width(c) { @@ -569,6 +570,52 @@ impl<T: Clone + Eq + Debug + Default> WrappedBlock<T> { Ok(()) } + fn add_text_prewrap(&mut self, text: &str, tag: &T) -> Result<(), Error> { + html_trace!("WrappedBlock::add_text_prewrap({}), {:?}", text, tag); + for c in text.chars() { + if c.is_whitespace() { + // Wrap if needed + self.flush_word(false)?; + match c { + '\n' => { + self.force_flush_line(); + } + '\t' => { + let tab_stop = 8; + let mut at_least_one_space = false; + while self.linelen % tab_stop != 0 || !at_least_one_space { + if self.linelen >= self.width { + self.flush_line(); + } else { + self.line.push_char(' ', tag); + self.linelen += 1; + at_least_one_space = true; + } + } + } + _ => { + if let Some(width) = UnicodeWidthChar::width(c) { + if self.width - self.linelen < width { + self.flush_line(); + } + if self.width - self.linelen >= width { + // Check for space again to avoid pathological issues + self.line.push_char(c, tag); + self.linelen += width; + } + } + } + } + } else if let Some(charwidth) = UnicodeWidthChar::width(c) { + /* Not whitespace; add to the current word. */ + self.word.push_char(c, tag); + self.wordlen += charwidth; + } + html_trace_quiet!(" Added char {:?}, wordlen={}", c, self.wordlen); + } + Ok(()) + } + fn add_element(&mut self, elt: TaggedLineElement<T>) { self.word.push(elt); } @@ -900,7 +947,7 @@ pub(crate) struct SubRenderer<D: TextDecorator> { ann_stack: Vec<D::Annotation>, text_filter_stack: Vec<fn(&str) -> Option<String>>, /// The depth of `<pre>` block stacking. - pre_depth: usize, + ws_stack: Vec<WhiteSpace>, } impl<D: TextDecorator> std::fmt::Debug for SubRenderer<D> { @@ -910,7 +957,7 @@ impl<D: TextDecorator> std::fmt::Debug for SubRenderer<D> { .field("lines", &self.lines) //.field("decorator", &self.decorator) .field("ann_stack", &self.ann_stack) - .field("pre_depth", &self.pre_depth) + .field("ws_stack", &self.ws_stack) .field("wrapping", &self.wrapping) .finish() } @@ -993,7 +1040,7 @@ impl<D: TextDecorator> SubRenderer<D> { wrapping: None, decorator, ann_stack: Vec::new(), - pre_depth: 0, + ws_stack: Vec::new(), text_filter_stack: Vec::new(), } } @@ -1104,6 +1151,10 @@ impl<D: TextDecorator> SubRenderer<D> { } Ok(new_width.max(min_width)) } + + fn ws_mode(&self) -> WhiteSpace { + self.ws_stack.last().cloned().unwrap_or(WhiteSpace::Normal) + } } fn filter_text_strikeout(s: &str) -> Option<String> { @@ -1186,16 +1237,12 @@ impl<D: TextDecorator> Renderer for SubRenderer<D> { Ok(()) } - fn start_pre(&mut self) { - self.pre_depth += 1; + fn push_ws(&mut self, ws: WhiteSpace) { + self.ws_stack.push(ws); } - fn end_pre(&mut self) { - if self.pre_depth > 0 { - self.pre_depth -= 1; - } else { - panic!("Attempt to end a preformatted block which wasn't opened."); - } + fn pop_ws(&mut self) { + self.ws_stack.pop(); } fn end_block(&mut self) { @@ -1204,7 +1251,10 @@ impl<D: TextDecorator> Renderer for SubRenderer<D> { fn add_inline_text(&mut self, text: &str) -> crate::Result<()> { html_trace!("add_inline_text({}, {})", self.width, text); - if self.pre_depth == 0 && self.at_block_end && text.chars().all(char::is_whitespace) { + if !self.ws_mode().preserve_whitespace() + && self.at_block_end + && text.chars().all(char::is_whitespace) + { // Ignore whitespace between blocks. return Ok(()); } @@ -1220,16 +1270,23 @@ impl<D: TextDecorator> Renderer for SubRenderer<D> { } } let filtered_text = s.as_deref().unwrap_or(text); - if self.pre_depth == 0 { - get_wrapping_or_insert::<D>(&mut self.wrapping, &self.options, self.width) - .add_text(filtered_text, &self.ann_stack)?; - } else { - let mut tag_first = self.ann_stack.clone(); - let mut tag_cont = self.ann_stack.clone(); - tag_first.push(self.decorator.decorate_preformat_first()); - tag_cont.push(self.decorator.decorate_preformat_cont()); - get_wrapping_or_insert::<D>(&mut self.wrapping, &self.options, self.width) - .add_preformatted_text(filtered_text, &tag_first, &tag_cont)?; + let ws_mode = self.ws_mode(); + let wrapping = get_wrapping_or_insert::<D>(&mut self.wrapping, &self.options, self.width); + match ws_mode { + WhiteSpace::Normal => { + wrapping.add_text(filtered_text, &self.ann_stack)?; + } + WhiteSpace::Pre => { + let mut tag_first = self.ann_stack.clone(); + let mut tag_cont = self.ann_stack.clone(); + tag_first.push(self.decorator.decorate_preformat_first()); + tag_cont.push(self.decorator.decorate_preformat_cont()); + get_wrapping_or_insert::<D>(&mut self.wrapping, &self.options, self.width) + .add_preformatted_text(filtered_text, &tag_first, &tag_cont)?; + } + WhiteSpace::PreWrap => { + wrapping.add_text_prewrap(filtered_text, &self.ann_stack)?; + } } Ok(()) } diff --git a/src/tests.rs b/src/tests.rs index 0092de3..f93e6af 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -2074,7 +2074,9 @@ foo #[cfg(feature = "css")] mod css_tests { - use super::{test_html_coloured, test_html_coloured_conf, test_html_css, test_html_style}; + use super::{ + test_html_coloured, test_html_coloured_conf, test_html_conf, test_html_css, test_html_style, + }; #[test] fn test_disp_none() { @@ -2551,4 +2553,28 @@ Row│Three }, ); } + + #[test] + fn test_pre_wrap() { + test_html_conf( + br#"<p class="prewrap">Hi + a + b + x longword +c d e +</p>"#, + r#"Hi + a + b + x +longword +c d e +"#, + 10, + |conf| { + conf.add_css(r#".prewrap { white-space: pre-wrap; }"#) + .unwrap() + }, + ); + } }