diff --git a/src/lib.rs b/src/lib.rs index 175df73..d746b49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,7 @@ use markup5ever_rcdom::{ RcDom, }; use std::cell::Cell; -use std::cmp::{min,max}; +use std::cmp::{max, min}; use std::io; use std::io::Write; use std::iter::{once, repeat}; @@ -285,9 +285,8 @@ impl RenderTable { } let size = sizes.iter().map(|s| s.size).sum(); // Include borders? let min_width = sizes.iter().map(|s| s.min_width).sum::() + self.num_columns - 1; - self.size_estimate.set(Some(SizeEstimate { - size, min_width, - })); + self.size_estimate + .set(Some(SizeEstimate { size, min_width })); } /// Calculate and store (or return stored value) of estimated size @@ -370,7 +369,7 @@ impl RenderNode { } } - /// Get a size estimate (~characters) + /// Get a size estimate pub fn get_size_estimate(&self) -> SizeEstimate { // If it's already calculated, then just return the answer. if let Some(s) = self.size_estimate.get() { @@ -382,7 +381,8 @@ impl RenderNode { // Otherwise, make an estimate. let estimate = match self.info { Text(ref t) | Img(ref t) => { - let mut len = t.trim().len(); + use unicode_width::UnicodeWidthStr; + let mut len = t.trim().width(); // Add one for preceding whitespace. if let Some(true) = t.chars().next().map(|c| c.is_whitespace()) { len += 1; @@ -405,12 +405,26 @@ impl RenderNode { | BlockQuote(ref v) | Dl(ref v) | Dt(ref v) - | Dd(ref v) - | Ul(ref v) - | Ol(_, ref v) => v + | Dd(ref v) => v .iter() .map(RenderNode::get_size_estimate) .fold(Default::default(), SizeEstimate::add), + Ul(ref v) => v + .iter() + .map(RenderNode::get_size_estimate) + .fold(Default::default(), SizeEstimate::add) + .add(SizeEstimate { + size: 2, + min_width: 2, + }), + Ol(i, ref v) => v + .iter() + .map(RenderNode::get_size_estimate) + .fold(Default::default(), SizeEstimate::add) + .add(SizeEstimate { + size: i.to_string().len() + 2, + min_width: i.to_string().len() + 2, + }), Header(level, ref v) => v .iter() .map(RenderNode::get_size_estimate) @@ -646,10 +660,13 @@ fn tr_to_render_tree<'a, 'b, T: Write>( } }) .collect(); - Some(RenderNode::new(RenderNodeInfo::TableRow(RenderTableRow { - cells, - col_sizes: None, - }, false))) + Some(RenderNode::new(RenderNodeInfo::TableRow( + RenderTableRow { + cells, + col_sizes: None, + }, + false, + ))) }) } @@ -930,9 +947,7 @@ fn process_dom_node<'a, 'b, T: Write>( // it on use. let href: String = href.into(); Box::new(move |_, cs: Vec| { - if cs - .iter() - .any(|c| !c.is_shallow_empty()) { + if cs.iter().any(|c| !c.is_shallow_empty()) { Some(RenderNode::new(Link(href.clone(), cs))) } else { None @@ -1082,6 +1097,7 @@ fn process_dom_node<'a, 'b, T: Write>( /// Context to use during tree parsing. /// This mainly gives access to a Renderer, but needs to be able to push /// new ones on for nested structures. +#[derive(Clone, Debug)] struct BuilderStack { builders: Vec, } @@ -1132,7 +1148,6 @@ fn render_tree_to_string( ) -> R { /* Phase 1: get size estimates. */ tree_map_reduce(&mut (), &tree, |_, node| precalc_size_estimate(&node)); - /* Phase 2: actually render. */ let mut bs = BuilderStack::new(builder); tree_map_reduce(&mut bs, tree, |builders, node| { @@ -1391,8 +1406,8 @@ fn render_table_tree( } // TODO: remove empty columns let tot_size: usize = col_sizes.iter().map(|est| est.size).sum(); - let min_size: usize = col_sizes.iter().map(|est| est.min_width).sum::() + - col_sizes.len().saturating_sub(1); + let min_size: usize = col_sizes.iter().map(|est| est.min_width).sum::() + + col_sizes.len().saturating_sub(1); let width = builder.width(); let vert_row = min_size > width; @@ -1404,22 +1419,21 @@ fn render_table_tree( if sz.size == 0 { 0 } else { - min(sz.size, - if usize::MAX/width <= sz.size { + min( + sz.size, + if usize::MAX / width <= sz.size { // The provided width is too large to multiply by width, // so do it the other way around. max((width / tot_size) * sz.size, sz.min_width) } else { max(sz.size * width / tot_size, sz.min_width) - }) + }, + ) } }) .collect() } else { - col_sizes - .iter() - .map(|_| width) - .collect() + col_sizes.iter().map(|_| width).collect() }; if !vert_row { @@ -1449,8 +1463,12 @@ fn render_table_tree( let table_width = if vert_row { width } else { - col_widths.iter().cloned().sum::() + - col_widths.iter().filter(|&w| w > &0).count().saturating_sub(1) + col_widths.iter().cloned().sum::() + + col_widths + .iter() + .filter(|&w| w > &0) + .count() + .saturating_sub(1) }; builder.add_horizontal_border_width(table_width); @@ -1533,7 +1551,11 @@ pub struct RenderTree(RenderNode); impl RenderTree { /// Render this document using the given `decorator` and wrap it to `width` columns. - pub fn render(self, width: usize, decorator: D) -> RenderedText { + pub fn render( + self, + width: usize, + decorator: D, + ) -> RenderedText { let builder = TextRenderer::new(width, decorator); let builder = render_tree_to_string(builder, self.0, &mut Discard {}); RenderedText(builder) @@ -1595,7 +1617,11 @@ pub fn parse(mut input: impl io::Read) -> RenderTree { /// Reads HTML from `input`, decorates it using `decorator`, and /// returns a `String` with text wrapped to `width` columns. -pub fn from_read_with_decorator(input: R, width: usize, decorator: D) -> String +pub fn from_read_with_decorator( + input: R, + width: usize, + decorator: D, +) -> String where R: io::Read, D: TextDecorator, diff --git a/src/render/text_renderer.rs b/src/render/text_renderer.rs index 46b533d..08c603e 100644 --- a/src/render/text_renderer.rs +++ b/src/render/text_renderer.rs @@ -4,15 +4,15 @@ //! into different text formats. use super::Renderer; -use std::{fmt::Debug, collections::LinkedList}; use std::mem; use std::ops::Deref; use std::vec; +use std::{collections::LinkedList, fmt::Debug}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; /// A wrapper around a String with extra metadata. -#[derive(Debug, PartialEq)] -pub struct TaggedString { +#[derive(Debug, Clone, PartialEq)] +pub struct TaggedString { /// The wrapped text. pub s: String, @@ -33,8 +33,8 @@ impl TaggedString { /// An element of a line of tagged text: either a TaggedString or a /// marker appearing in between document characters. -#[derive(Debug, PartialEq)] -pub enum TaggedLineElement { +#[derive(Clone, Debug, PartialEq)] +pub enum TaggedLineElement { /// A string with tag information attached. Str(TaggedString), @@ -43,8 +43,8 @@ pub enum TaggedLineElement { } /// A line of tagged text (composed of a set of `TaggedString`s). -#[derive(Debug, PartialEq)] -pub struct TaggedLine { +#[derive(Debug, Clone, PartialEq)] +pub struct TaggedLine { v: Vec>, } @@ -209,8 +209,8 @@ impl TaggedLine { /// A type to build up wrapped text, allowing extra metadata for /// spans. -#[derive(Debug)] -struct WrappedBlock { +#[derive(Debug, Clone)] +struct WrappedBlock { width: usize, text: Vec>, textlen: usize, @@ -584,7 +584,7 @@ impl BorderHoriz { /// Make a join to a line above at the xth cell pub fn join_above(&mut self, x: usize) { use self::BorderSegHoriz::*; - self.stretch_to(x+1); + self.stretch_to(x + 1); let prev = self.segments[x]; self.segments[x] = match prev { Straight | JoinAbove => JoinAbove, @@ -596,7 +596,7 @@ impl BorderHoriz { /// Make a join to a line below at the xth cell pub fn join_below(&mut self, x: usize) { use self::BorderSegHoriz::*; - self.stretch_to(x+1); + self.stretch_to(x + 1); let prev = self.segments[x]; self.segments[x] = match prev { Straight | JoinBelow => JoinBelow, @@ -665,7 +665,7 @@ impl BorderHoriz { } /// A line, which can either be text or a line. -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum RenderLine { /// Some rendered text Text(TaggedLine), @@ -712,6 +712,7 @@ impl RenderLine { /// A renderer which just outputs plain text with /// annotations depending on a decorator. +#[derive(Clone)] pub struct TextRenderer { width: usize, lines: LinkedList>>, @@ -726,6 +727,18 @@ pub struct TextRenderer { pre_depth: usize, } +impl std::fmt::Debug for TextRenderer { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.debug_struct("TextRenderer") + .field("width", &self.width) + .field("lines", &self.lines) + .field("decorator", &self.decorator) + .field("ann_stack", &self.ann_stack) + .field("pre_depth", &self.pre_depth) + .finish() + } +} + impl TextRenderer { /// Construct a new empty TextRenderer. pub fn new(width: usize, decorator: D) -> TextRenderer { @@ -808,7 +821,6 @@ impl TextRenderer { } result } - /// Returns a `Vec` of `TaggedLine`s with therendered text. pub fn into_lines(mut self) -> LinkedList>> { @@ -1047,7 +1059,7 @@ impl Renderer for TextRenderer { fn append_columns_with_borders(&mut self, cols: I, collapse: bool) where I: IntoIterator, - Self: Sized + Self: Sized, { use self::TaggedLineElement::Str; html_trace!("append_columns_with_borders(collapse={})", collapse); @@ -1133,8 +1145,12 @@ impl Renderer for TextRenderer { self.lines.back_mut().expect("No previous line") { if let RenderLine::Line(line) = sublines.remove(0) { - html_trace!("prev border:\n{}\n, pos={}, line:\n{}", - prev_border.to_string(), pos, line.to_string()); + html_trace!( + "prev border:\n{}\n, pos={}, line:\n{}", + prev_border.to_string(), + pos, + line.to_string() + ); prev_border.merge_from_below(&line, pos); } } else { @@ -1211,7 +1227,7 @@ impl Renderer for TextRenderer { fn append_vert_row(&mut self, cols: I) where I: IntoIterator, - Self: Sized + Self: Sized, { html_trace!("append_vert_row()"); html_trace!("self=\n{}", self.to_string()); @@ -1233,13 +1249,13 @@ impl Renderer for TextRenderer { self.add_horizontal_border(); } - fn empty(&self) -> bool { - self.lines.is_empty() && if let Some(wrapping) = &self.wrapping { - wrapping.is_empty() - } else { - true - } + self.lines.is_empty() + && if let Some(wrapping) = &self.wrapping { + wrapping.is_empty() + } else { + true + } } fn text_len(&self) -> usize { @@ -1379,7 +1395,7 @@ impl Renderer for TextRenderer { /// A decorator for use with `TextRenderer` which outputs plain UTF-8 text /// with no annotations. Markup is rendered as text characters or footnotes. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct PlainDecorator { links: Vec, } @@ -1478,7 +1494,7 @@ impl TextDecorator for PlainDecorator { /// A decorator for use with `TextRenderer` which outputs plain UTF-8 text /// with no annotations or markup, emitting only the literal text. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct TrivialDecorator {} impl TrivialDecorator { @@ -1571,7 +1587,7 @@ impl TextDecorator for TrivialDecorator { /// A decorator to generate rich text (styled) rather than /// pure text output. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct RichDecorator {} /// Annotation type for "rich" text. Text is associated with a set of diff --git a/src/tests.rs b/src/tests.rs index 64f2149..b271616 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -784,13 +784,15 @@ fn test_deeply_nested_table() { .collect::>() .concat(); - let result = repeat(r#"────────── + let result = repeat( + r#"────────── hi ////////// -"#) - .take(rpt - 3) - .collect::>() - .concat() +"#, + ) + .take(rpt - 3) + .collect::>() + .concat() + &r#"──┬──── hi│hi │//// @@ -798,12 +800,8 @@ hi│hi │hi │── ──┴──── -"# + &repeat("──────────\n").take(rpt-3).collect::(); - test_html( - html.as_bytes(), - &result, - 10, - ); +"# + &repeat("──────────\n").take(rpt - 3).collect::(); + test_html(html.as_bytes(), &result, 10); } #[test] @@ -1086,6 +1084,7 @@ fn test_pre_rich() { fn test_finalise() { use crate::render::text_renderer::{TaggedLine, TextDecorator}; + #[derive(Clone, Debug)] struct TestDecorator; impl TextDecorator for TestDecorator { @@ -1275,12 +1274,15 @@ fn test_issue_54_oob() { -"##, r#"─┬────────┬ +"##, + r#"─┬────────┬ │Blah │ │blah │ │blah │ ─┴────────┴ -"#, 10); +"#, + 10, + ); } #[test] @@ -1294,14 +1296,70 @@ fn test_table_vertical_rows() { der -"##, "───── +"##, + "───── wid ///// kin ///// der ───── -", 5); +", + 5, + ); +} + +#[test] +fn test_unicode() { + test_html( + " + + + +
နတမစနတမစaaa
" + .as_bytes(), + "────┬────┬─── +နတမစ│နတမစ│aaa +────┴────┴─── +", + 15, + ); +} + +#[test] +fn test_list_in_table() { + test_html( + b" + +
    +
  1. 0
  2. +
  3. 1
  4. +
  5. 2
  6. +
  7. 3
  8. +
  9. 4
  10. +
  11. 5
  12. +
  13. 6
  14. +
  15. 7
  16. +
  17. 8
  18. +
  19. 9
  20. +
  21. 10
  22. +
", + "────── +1. 0 +2. 1 +3. 2 +4. 3 +5. 4 +6. 5 +7. 6 +8. 7 +9. 8 +10. 9 +11. 10 +────── +", + 6, + ); } #[test]