diff options
Diffstat (limited to 'svgbob/src/buffer/fragment_buffer/fragment/text.rs')
-rw-r--r-- | svgbob/src/buffer/fragment_buffer/fragment/text.rs | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/svgbob/src/buffer/fragment_buffer/fragment/text.rs b/svgbob/src/buffer/fragment_buffer/fragment/text.rs new file mode 100644 index 0000000..e8a9b20 --- /dev/null +++ b/svgbob/src/buffer/fragment_buffer/fragment/text.rs @@ -0,0 +1,275 @@ +use crate::{ + buffer::{Cell, CellGrid}, + fragment::Bounds, + Point, +}; +use sauron::{html::*, svg, svg::attributes::*, Node}; +use std::{borrow::Cow, cmp::Ordering, fmt}; + +/// A horizontal cell text +/// Operated based on cell +/// Text are threated differently +/// since scaling them losses the sense +/// of which text it adjacent to without keeping +/// track of the scale. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +pub struct CellText { + pub(crate) start: Cell, + pub(crate) content: String, +} + +impl CellText { + pub fn new(start: Cell, content: String) -> Self { + CellText { start, content } + } + + fn end_cell(&self) -> Cell { + Cell::new(self.start.x + self.content.len() as i32, self.start.y) + } + + /// get the cells of this text + /// TODO: use iterator + fn cells(&'_ self) -> impl IntoIterator<Item = Cell> + '_ { + let range = self.start.x..(self.start.x + self.content.len() as i32); + range.map(move |x| Cell::new(x, self.start.y)) + } + + fn is_adjacent_cell(&self, other_cell: Cell) -> bool { + self.cells().into_iter().any(|cell| cell.y == other_cell.y && cell.is_adjacent(&other_cell)) + } + + /// cell text is groupable when they are adjacent + pub(crate) fn is_contacting(&self, other: &Self) -> bool { + self.cells().into_iter().any(|cell| other.is_adjacent_cell(cell)) + } + + /// text can merge if they are next to each other and at the same line + pub(in crate) fn can_merge(&self, other: &Self) -> bool { + self.start.y == other.start.y + && (self.start.x + self.content.len() as i32 == other.start.x + || other.start.x + other.content.len() as i32 == self.start.x) + } + + pub(in crate) fn merge(&self, other: &Self) -> Option<Self> { + if self.can_merge(other) { + if self.start.x < other.start.x { + Some(CellText::new(self.start, format!("{}{}", self.content, other.content))) + } else { + Some(CellText::new(other.start, format!("{}{}", other.content, self.content))) + } + } else { + None + } + } + + pub(in crate) fn absolute_position(&self, cell: Cell) -> Self { + CellText { start: Cell::new(self.start.x + cell.x, self.start.y + cell.y), ..self.clone() } + } +} + +impl Bounds for CellText { + fn bounds(&self) -> (Point, Point) { + (self.start.top_left_most(), self.end_cell().bottom_right_most()) + } +} + +impl Into<Text> for CellText { + fn into(self) -> Text { + Text::new(self.start.q(), self.content) + } +} + +impl Into<Node<()>> for CellText { + fn into(self) -> Node<()> { + let text: Text = self.into(); + text.into() + } +} + +impl fmt::Display for CellText { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "CT {} {}", self.start, self.content) + } +} + +/// This is ready to be scaled and drawn into the svg file +#[derive(Debug, Clone)] +pub struct Text { + pub start: Point, + pub text: String, +} + +impl Text { + pub fn new(start: Point, text: String) -> Self { + Text { start, text } + } + + /// get the textwidth in terms of cell grid points + fn text_width(&self) -> f32 { + self.text.len() as f32 * CellGrid::width() + } + + pub(in crate) fn absolute_position(&self, cell: Cell) -> Self { + Text { start: cell.absolute_position(self.start), ..self.clone() } + } + + pub(in crate) fn scale(&self, scale: f32) -> Self { + Text { start: self.start.scale(scale), ..self.clone() } + } +} + +fn replace_html_char<'a>(ch: char) -> Cow<'a, str> { + match ch { + '>' => Cow::from(">"), + '<' => Cow::from("<"), + '&' => Cow::from("&"), + '\'' => Cow::from("'"), + '"' => Cow::from("""), + '\0' => Cow::from(""), + _ => Cow::from(ch.to_string()), + } +} + +fn escape_html_text(s: &str) -> String { + s.chars().map(|ch| replace_html_char(ch)).collect() +} + + +impl fmt::Display for Text { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "T {} {}", self.start, self.text) + } +} + +impl Into<Node<()>> for Text { + fn into(self) -> Node<()> { + svg::tags::text(vec![x(self.start.x), y(self.start.y)], vec![text(escape_html_text( + &self.text, + ))]) + } +} + +impl Bounds for Text { + fn bounds(&self) -> (Point, Point) { + (self.start, Point::new(self.start.x + self.text_width(), self.start.y)) + } +} + +impl Eq for Text {} + +/// This is needed since this struct contains f32 which rust doesn't provide Eq implementation +impl Ord for Text { + fn cmp(&self, other: &Self) -> Ordering { + self.start.cmp(&other.start).then(self.text.cmp(&other.text)) + } +} + +impl PartialOrd for Text { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl PartialEq for Text { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +#[cfg(test)] +mod tests { + use crate::{ + buffer::{Cell, CellBuffer, Contacts, Span}, + fragment::CellText, + }; + + #[test] + fn test_cell_text_absolute_position() { + let t1 = CellText::new(Cell::new(0, 0), "c".to_string()); + let t1_abs = t1.absolute_position(Cell::new(1, 1)); + assert_eq!(t1_abs.start, Cell::new(1, 1)); + + let t2 = CellText::new(Cell::new(4, 3), "c".to_string()); + let t2_abs = t2.absolute_position(Cell::new(5, 7)); + assert_eq!(t2_abs.start, Cell::new(9, 10)); + } + + #[test] + fn text_merge_point_base() { + let c1 = Cell::new(1, 1); + let c2 = Cell::new(3, 1); + let t1 = CellText::new(c1, "He".to_string()); + let t2 = CellText::new(c2, "llo".to_string()); + assert!(t1.can_merge(&t2)); + assert_eq!(t2.merge(&t1), t1.merge(&t2)); + assert_eq!(t1.merge(&t2), Some(CellText::new(c1, "Hello".to_string()))); + } + + #[test] + fn test_paragraph() { + let art = r#" + This is some sort of a paragraph + with long words such as supercalifragilisticexpialiducios + and pneumonoultramicrospocisilicovulcanoconiosis + "#; + let cell_buffer = CellBuffer::from(art); + let mut spans: Vec<Span> = cell_buffer.group_adjacents(); + assert_eq!(spans.len(), 1); + let span1 = spans.remove(0); + let groups = span1.get_contacts(); + for (i, group) in groups.iter().enumerate() { + println!("group{}\n{}", i, group); + } + assert_eq!(15, groups.len()); + assert_eq!( + groups[0], + Contacts::new(CellText::new(Cell::new(0, 0), "This".to_string()).into()) + ); + assert_eq!( + groups[5], + Contacts::new(CellText::new(Cell::new(21, 0), "a".to_string()).into()) + ); + assert_eq!( + groups[14], + Contacts::new( + CellText::new( + Cell::new(4, 2), + "pneumonoultramicrospocisilicovulcanoconiosis".to_string() + ) + .into() + ) + ); + } + + #[test] + fn test_2paragraph() { + let art = r#" + This is some sort of a paragraph + with long words such as supercalifragilisticexpialiducios + and pneumonoultramicrospocisilicovulcanoconiosis + + Loren ipsum is a second paragrapah. + This is in one span since all the letters + are adjacent since they are right next to + each other taking account the top and bottom + neighbor cell + "#; + let cell_buffer = CellBuffer::from(art); + let mut spans: Vec<Span> = cell_buffer.group_adjacents(); + assert_eq!(spans.len(), 2); + let groups2 = spans.remove(1).get_contacts(); + let groups1 = spans.remove(0).get_contacts(); + println!("span1 groups:"); + for (i, group1) in groups1.iter().enumerate() { + println!("\tgroup {} {}", i, group1); + } + println!("span2 groups:"); + for (i, group2) in groups2.iter().enumerate() { + println!("\tgroup {} {}", i, group2); + } + assert_eq!(groups1.len(), 15); + assert_eq!(groups2.len(), 33); + assert_eq!(groups1[0].as_ref()[0].as_cell_text().unwrap().start, Cell::new(0, 0)); + assert_eq!(groups2[0].as_ref()[0].as_cell_text().unwrap().start, Cell::new(0, 0)); + } +} |