diff options
Diffstat (limited to 'packages/svgbob/src/buffer/cell_buffer.rs')
-rw-r--r-- | packages/svgbob/src/buffer/cell_buffer.rs | 803 |
1 files changed, 803 insertions, 0 deletions
diff --git a/packages/svgbob/src/buffer/cell_buffer.rs b/packages/svgbob/src/buffer/cell_buffer.rs new file mode 100644 index 0000000..c1f4421 --- /dev/null +++ b/packages/svgbob/src/buffer/cell_buffer.rs @@ -0,0 +1,803 @@ +use crate::fragment::CellText; +use crate::{ + buffer::{fragment_buffer::FragmentTree, Fragment, StringBuffer}, + util::parser, +}; +pub use cell::{Cell, CellGrid}; +pub use contacts::Contacts; +use itertools::Itertools; +use sauron::{ + html, + html::{attributes::*, *}, + svg::{attributes::*, *}, + Node, +}; +pub use settings::Settings; +pub use span::Span; +use std::{ + collections::BTreeMap, + fmt, + ops::{Deref, DerefMut}, +}; +use unicode_width::UnicodeWidthStr; + +mod cell; +mod contacts; +mod endorse; +mod settings; +mod span; + +/// The simplest buffer. +/// This is maps which char belong to which cell skipping the whitespaces +#[derive(Debug)] +pub struct CellBuffer { + map: BTreeMap<Cell, char>, + /// class, <style> + /// assemble into + /// + /// ```css + /// .class { styles } + /// ``` + css_styles: Vec<(String, String)>, + escaped_text: Vec<(Cell, String)>, +} + +impl Deref for CellBuffer { + type Target = BTreeMap<Cell, char>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for CellBuffer { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +impl CellBuffer { + pub fn new() -> Self { + CellBuffer { + map: BTreeMap::new(), + css_styles: vec![], + escaped_text: vec![], + } + } + + pub fn add_css_styles(&mut self, css_styles: Vec<(String, String)>) { + self.css_styles.extend(css_styles); + } + + /// Groups cell that are adjacents (cells that are next to each other, horizontally or + /// vertically) + /// Note: using .rev() since this has a high change that the last cell is adjacent with the + /// current cell tested + pub fn group_adjacents(&self) -> Vec<Span> { + let mut adjacents: Vec<Span> = vec![]; + for (cell, ch) in self.iter() { + let belongs_to_adjacents = + adjacents.iter_mut().rev().any(|contacts| { + if contacts.is_adjacent(cell) { + contacts.push((*cell, *ch)); + true + } else { + false + } + }); + if !belongs_to_adjacents { + adjacents.push(Span::new(*cell, *ch)); + } + } + Self::merge_recursive(adjacents) + } + + /// merge span recursively until it hasn't changed the number of spans + fn merge_recursive(adjacents: Vec<Span>) -> Vec<Span> { + let original_len = adjacents.len(); + let merged = Self::second_pass_merge(adjacents); + // if has merged continue merging until nothing can be merged + if merged.len() < original_len { + Self::merge_recursive(merged) + } else { + merged + } + } + + /// second pass merge is operating on span comparing to other spans + fn second_pass_merge(adjacents: Vec<Span>) -> Vec<Span> { + let mut new_groups: Vec<Span> = vec![]; + for span in adjacents.into_iter() { + let is_merged = new_groups.iter_mut().rev().any(|new_group| { + if new_group.can_merge(&span) { + new_group.merge(&span); + true + } else { + false + } + }); + if !is_merged { + new_groups.push(span); + } + } + new_groups + } + + pub fn bounds(&self) -> Option<(Cell, Cell)> { + let xlimits = + self.iter().map(|(cell, _)| cell.x).minmax().into_option(); + let ylimits = + self.iter().map(|(cell, _)| cell.y).minmax().into_option(); + match (xlimits, ylimits) { + (Some((min_x, max_x)), Some((min_y, max_y))) => { + Some((Cell::new(min_x, min_y), Cell::new(max_x, max_y))) + } + _ => None, + } + } + + /// get the svg node of this cell buffer, using the default settings for the sizes + pub fn get_node<MSG>(&self) -> Node<MSG> { + let (node, _w, _h) = self.get_node_with_size(&Settings::default()); + node + } + + /// calculate the appropriate size (w,h) in pixels for the whole cell buffer to fit + /// appropriately + pub(crate) fn get_size(&self, settings: &Settings) -> (f32, f32) { + let (_top_left, bottom_right) = + self.bounds().unwrap_or((Cell::new(0, 0), Cell::new(0, 0))); + let w = settings.scale * (bottom_right.x + 2) as f32 * Cell::width(); + let h = settings.scale * (bottom_right.y + 2) as f32 * Cell::height(); + (w, h) + } + + /// get all nodes of this cell buffer + pub fn get_node_with_size<MSG>( + &self, + settings: &Settings, + ) -> (Node<MSG>, f32, f32) { + let (w, h) = self.get_size(&settings); + + let (group_nodes, fragments) = self.group_nodes_and_fragments(settings); + + let svg_node = Self::fragments_to_node( + fragments, + self.legend_css(), + settings, + w, + h, + ) + .add_children(group_nodes); + (svg_node, w, h) + } + + /// get all nodes and use the size supplied + pub fn get_node_override_size<MSG>( + &self, + settings: &Settings, + w: f32, + h: f32, + ) -> Node<MSG> { + let (group_nodes, fragments) = self.group_nodes_and_fragments(settings); + + let svg_node = Self::fragments_to_node( + fragments, + self.legend_css(), + settings, + w, + h, + ) + .add_children(group_nodes); + + svg_node + } + + /// return fragments that are Rect, Circle, + pub fn get_shapes_fragment(&self, settings: &Settings) -> Vec<Fragment> { + let (single_member, _, endorsed_fragments) = + self.group_single_members_from_other_fragments(settings); + endorsed_fragments + .into_iter() + .chain( + single_member + .into_iter() + .filter(|frag| frag.is_rect() || frag.is_circle()), + ) + .collect() + } + + /// returns (single_member, grouped, rest of the fragments + fn group_single_members_from_other_fragments( + &self, + settings: &Settings, + ) -> (Vec<Fragment>, Vec<Vec<Fragment>>, Vec<Fragment>) { + // endorsed_fragments are the fragment result of successful endorsement + // + // vec_groups are not endorsed, but are still touching, these will be grouped together in + // the svg node + let (endorsed_fragments, vec_contacts): ( + Vec<Vec<Fragment>>, + Vec<Vec<Contacts>>, + ) = self + .group_adjacents() + .into_iter() + .map(|span| span.endorse(settings)) + .unzip(); + + // partition the vec_groups into groups that is alone and the group + // that is contacting their parts + let (single_member, vec_groups): (Vec<Contacts>, Vec<Contacts>) = + vec_contacts + .into_iter() + .flatten() + .partition(move |contacts| contacts.0.len() == 1); + + let single_member_fragments: Vec<Fragment> = single_member + .into_iter() + .flat_map(|contact| contact.0) + .collect(); + + let vec_groups: Vec<Vec<Fragment>> = + vec_groups.into_iter().map(|contact| contact.0).collect(); + + let endorsed_fragments: Vec<Fragment> = + endorsed_fragments.into_iter().flatten().collect(); + + (single_member_fragments, vec_groups, endorsed_fragments) + } + + /// group nodes that can be group and the rest will be fragments + /// Note: The grouped fragments is scaled here + fn group_nodes_and_fragments<MSG>( + &self, + settings: &Settings, + ) -> (Vec<Node<MSG>>, Vec<Fragment>) { + let (single_member_fragments, vec_group_fragments, vec_fragments) = + self.group_single_members_from_other_fragments(settings); + + // grouped fragments will be rendered as svg groups + let group_nodes: Vec<Node<MSG>> = vec_group_fragments + .into_iter() + .map(move |fragments| { + let group_members = fragments + .iter() + .map(move |gfrag| { + let scaled = gfrag.scale(settings.scale); + let node: Node<MSG> = scaled.into(); + node + }) + .collect::<Vec<Node<MSG>>>(); + g([], group_members) + }) + .collect(); + + let mut fragments: Vec<Fragment> = vec_fragments; + + fragments.extend(single_member_fragments); + fragments.extend(self.escaped_text_nodes()); + + (group_nodes, fragments) + } + + fn escaped_text_nodes(&self) -> Vec<Fragment> { + let mut fragments = vec![]; + for (cell, text) in &self.escaped_text { + let cell_text = CellText::new(*cell, text.to_string()); + fragments.push(cell_text.into()); + } + fragments + } + + /// construct the css from the # Legend: of the diagram + fn legend_css(&self) -> String { + let classes: Vec<String> = self + .css_styles + .iter() + .map(|(class, styles)| format!(".{}{{ {} }}", class, styles)) + .collect(); + classes.join("\n") + } + + fn get_style<MSG>(settings: &Settings, legend_css: String) -> Node<MSG> { + use sauron::html::units::px; + + let stroke_color = settings.stroke_color.to_owned(); + let stroke_width = settings.stroke_width.to_owned(); + let background = settings.background.to_owned(); + let fill_color = settings.fill_color.to_owned(); + let font_family = settings.font_family.to_owned(); + let font_size = settings.font_size.to_owned(); + + let element_styles = sauron::jss::jss! { + "line, path, circle, rect, polygon": { + stroke: stroke_color.clone(), + stroke_width: stroke_width.clone(), + stroke_opacity: 1, + fill_opacity: 1, + stroke_linecap: "round", + stroke_linejoin: "miter", + }, + + "text": { + /* This fix the spacing bug in svg text*/ + white_space: "pre", + fill: stroke_color.clone(), + }, + + "rect.backdrop":{ + stroke: "none", + fill: background.clone(), + }, + + ".broken":{ + stroke_dasharray: 8, + }, + + ".filled":{ + fill: fill_color.clone(), + }, + + ".bg_filled":{ + fill: background.clone(), + }, + + ".nofill":{ + fill: background.clone(), + }, + + "text": { + font_family: font_family.clone(), + font_size: px(font_size.clone()), + }, + + ".end_marked_arrow":{ + marker_end: "url(#arrow)", + }, + + ".start_marked_arrow":{ + marker_start: "url(#arrow)", + }, + + ".end_marked_diamond":{ + marker_end: "url(#diamond)", + }, + ".start_marked_diamond":{ + marker_start: "url(#diamond)", + }, + + ".end_marked_circle":{ + marker_end: "url(#circle)", + }, + + ".start_marked_circle":{ + marker_start: "url(#circle)", + }, + + ".end_marked_open_circle":{ + marker_end: "url(#open_circle)", + }, + + ".start_marked_open_circle":{ + marker_start: "url(#open_circle)", + }, + + ".end_marked_big_open_circle":{ + marker_end: "url(#big_open_circle)", + }, + + ".start_marked_big_open_circle": { + marker_start: "url(#big_open_circle)", + } + }; + html::tags::style([], [text(element_styles), text(legend_css)]) + } + + /// convert the fragments into svg nodes using the supplied settings, with size for the + /// dimension + pub fn fragments_to_node<MSG>( + fragments: Vec<Fragment>, + legend_css: String, + settings: &Settings, + w: f32, + h: f32, + ) -> Node<MSG> { + let fragments_scaled: Vec<Fragment> = fragments + .into_iter() + .map(|frag| frag.scale(settings.scale)) + .collect(); + let fragment_nodes: Vec<Node<MSG>> = + FragmentTree::fragments_to_node(fragments_scaled); + + let mut children = vec![]; + if settings.include_styles { + children.push(Self::get_style(settings, legend_css)); + } + if settings.include_defs { + children.push(Self::get_defs()); + } + + // backdrop needs to appear first before the fragment nodes + // otherwise it will cover the other elements + // in accordance to how z-index works + if settings.include_backdrop { + children.push(rect( + [class("backdrop"), x(0), y(0), width(w), height(h)], + [], + )); + } + + children.extend(fragment_nodes); + + svg( + [xmlns("http://www.w3.org/2000/svg"), width(w), height(h)], + children, + ) + } + + fn get_defs<MSG>() -> Node<MSG> { + defs( + [], + [ + Self::arrow_marker(), + Self::diamond_marker(), + Self::circle_marker(), + Self::open_circle_marker(), + Self::big_open_circle_marker(), + ], + ) + } + + fn arrow_marker<MSG>() -> Node<MSG> { + marker( + [ + id("arrow"), + viewBox("-2 -2 8 8"), + refX(4), + refY(2), + markerWidth(7), + markerHeight(7), + orient("auto-start-reverse"), + ], + [polygon([points("0,0 0,4 4,2 0,0")], [])], + ) + } + + fn diamond_marker<MSG>() -> Node<MSG> { + marker( + [ + id("diamond"), + viewBox("-2 -2 8 8"), + refX(4), + refY(2), + markerWidth(7), + markerHeight(7), + orient("auto-start-reverse"), + ], + [polygon([points("0,2 2,0 4,2 2,4 0,2")], [])], + ) + } + + fn open_circle_marker<MSG>() -> Node<MSG> { + marker( + [ + id("open_circle"), + viewBox("0 0 8 8"), + refX(4), + refY(4), + markerWidth(7), + markerHeight(7), + orient("auto-start-reverse"), + ], + [circle( + [cx(4), cy(4), r(2), html::attributes::class("bg_filled")], + [], + )], + ) + } + + fn circle_marker<MSG>() -> Node<MSG> { + marker( + [ + id("circle"), + viewBox("0 0 8 8"), + refX(4), + refY(4), + markerWidth(7), + markerHeight(7), + orient("auto-start-reverse"), + ], + [circle( + [cx(4), cy(4), r(2), html::attributes::class("filled")], + [], + )], + ) + } + + fn big_open_circle_marker<MSG>() -> Node<MSG> { + marker( + [ + id("big_open_circle"), + viewBox("0 0 8 8"), + refX(4), + refY(4), + markerWidth(7), + markerHeight(7), + orient("auto-start-reverse"), + ], + [circle( + [cx(4), cy(4), r(3), html::attributes::class("bg_filled")], + [], + )], + ) + } + + /// returns a (Cell, escaped string), and the strings that are not part of the escape string + fn escape_line(line: usize, raw: &str) -> (Vec<(Cell, String)>, String) { + let mut no_escaped_text = String::new(); + + let mut index = 0; + let mut escaped_text = vec![]; + let input_chars: Vec<char> = raw.chars().collect(); + let char_locs = parser::line_parse() + .parse(&input_chars) + .expect("should parse"); + if char_locs.is_empty() { + no_escaped_text = raw.to_string(); + } else { + for (start, end) in char_locs.iter() { + let escaped = input_chars[*start + 1..*end].iter().fold( + String::new(), + |mut acc, c| { + acc.push(*c); + acc + }, + ); + let escaped_unicode_width = escaped.width(); + let cell = Cell::new(*start as i32, line as i32); + escaped_text.push((cell, escaped)); + no_escaped_text += &input_chars[index..*start].iter().fold( + String::new(), + |mut acc, c| { + acc.push(*c); + acc + }, + ); + + // we add 2 to account for the double quotes on end of the escaped string + no_escaped_text += &" ".repeat(escaped_unicode_width + 2); + index = end + 1; + } + // include the rest of the text + no_escaped_text += &input_chars[index..].iter().fold( + String::new(), + |mut acc, c| { + acc.push(*c); + acc + }, + ); + } + (escaped_text, no_escaped_text) + } +} + +impl fmt::Display for CellBuffer { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "CellBuffer dump..")?; + for (cell, ch) in self.iter() { + writeln!(f, "{} {}", cell, ch)?; + } + Ok(()) + } +} + +impl From<&str> for CellBuffer { + fn from(input: &str) -> Self { + let css_styles = if let Some(loc) = input.find("# Legend:") { + if let Ok(css_styles) = parser::parse_css_legend(&input[loc..]) { + Some((loc, css_styles)) + } else { + None + } + } else { + None + }; + if let Some((loc, css_styles)) = css_styles { + let mut cell_buffer = + CellBuffer::from(StringBuffer::from(&input[..loc])); + cell_buffer.add_css_styles(css_styles); + cell_buffer + } else { + CellBuffer::from(StringBuffer::from(input)) + } + } +} + +impl From<StringBuffer> for CellBuffer { + fn from(sb: StringBuffer) -> Self { + use std::iter::FromIterator; + + let mut buffer = CellBuffer::new(); + for (y, line) in sb.iter().enumerate() { + let line_str = String::from_iter(line); + let (escaped_text, unescaped) = Self::escape_line(y, &line_str); + buffer.escaped_text.extend(escaped_text); + + for (x, ch) in unescaped.chars().enumerate() { + if ch != '\0' && !ch.is_whitespace() { + let cell = Cell::new(x as i32, y as i32); + buffer.insert(cell, ch); + } + } + } + buffer + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_escape_line() { + let raw = r#"The "qu/i/ck" brown "fox\"s" jumps over the lazy "do|g""#; + let ex2 = r#"The brown jumps over the lazy "#; + let (escaped, unescaped) = CellBuffer::escape_line(0, raw); + println!("escaped: {:#?}", escaped); + println!("unescaped: {}", unescaped); + assert_eq!( + vec![ + (Cell::new(4, 0), "qu/i/ck".to_string()), + (Cell::new(20, 0), r#"fox\"s"#.to_string()), + (Cell::new(49, 0), "do|g".to_string()) + ], + escaped + ); + assert_eq!(ex2, unescaped); + } + + #[test] + fn test_escape_line2() { + let raw = r#"The quick brown fox jumps over the "lazy" dog"#; + let ex2 = r#"The quick brown fox jumps over the dog"#; + let (escaped, unescaped) = CellBuffer::escape_line(0, raw); + println!("escaped: {:#?}", escaped); + println!("unescaped: {}", unescaped); + assert_eq!(vec![(Cell::new(35, 0), "lazy".to_string())], escaped); + assert_eq!(ex2, unescaped); + } + + #[test] + fn test_escape_line3() { + let raw = r#" in between "| |" these "#; + let ex2 = r#" in between these "#; + let (escaped, unescaped) = CellBuffer::escape_line(0, raw); + println!("escaped: {:#?}", escaped); + println!("unescaped: {}", unescaped); + assert_eq!(vec![(Cell::new(12, 0), "| |".to_string())], escaped); + assert_eq!(ex2, unescaped); + } + + #[test] + fn test_issue38_escaped_german_umlauts() { + let raw = r#"This is some german character "ÖÄÜ" and was escaped"#; + let ex2 = r#"This is some german character and was escaped"#; + let (escaped, unescaped) = CellBuffer::escape_line(0, raw); + println!("escaped: {:#?}", escaped); + println!("unescaped: {}", unescaped); + assert_eq!(vec![(Cell::new(30, 0), "ÖÄÜ".to_string())], escaped); + assert_eq!(ex2, unescaped); + } + + #[test] + fn test_issue38_escape_cjk() { + let raw = r#"This is some CJK "一" and was escaped"#; + let ex2 = r#"This is some CJK and was escaped"#; + let (escaped, unescaped) = CellBuffer::escape_line(0, raw); + println!("escaped: {:#?}", escaped); + println!("unescaped: {}", unescaped); + assert_eq!(vec![(Cell::new(17, 0), r#"一"#.to_string())], escaped); + assert_eq!(ex2, unescaped); + } + + #[test] + fn test_simple_adjacents() { + let art = r#" +.. ._. +'' ( ) + `-' + +.--------. +|________| + "#; + let buffer = CellBuffer::from(art); + let adjacents = buffer.group_adjacents(); + for (i, span) in adjacents.iter().enumerate() { + println!("span: {}", i); + println!("{}\n\n", span); + } + assert_eq!(adjacents.len(), 3); + } + + #[test] + fn test_shapes_fragment() { + let art = r#" + + +-------+ + *-----> | | + +-------+ + +This is a text + .-. + ( ) + `-' + + "#; + let buffer = CellBuffer::from(art); + let shapes = buffer.get_shapes_fragment(&Settings::default()); + println!("shapes: {:#?}", shapes); + assert_eq!(2, shapes.len()); + } + + #[test] + fn test_shapes_fragment_intersecting() { + let art = r#" + +-------+ + | | + +-------+ + + "#; + let buffer = CellBuffer::from(art); + let shapes = buffer.get_shapes_fragment(&Settings::default()); + println!("shapes: {:#?}", shapes); + assert_eq!(1, shapes.len()); + assert!(shapes[0].hit(Cell::new(15, 1).a(), Cell::new(15, 1).y())); + } + + /// The . in .-/ + /// will create a new contacts even since it is not adjacent to / + /// so it needs a second_pass to merge it + #[test] + fn test_one_big() { + let art = r#" + + .---. + /-o-/-- + .-/ / /-> + ( * \/ + '-. \ + \ / + ' + "#; + let buffer = CellBuffer::from(art); + let adjacents = buffer.group_adjacents(); + for (i, span) in adjacents.iter().enumerate() { + println!("span: {}", i); + println!("{}\n\n", span); + } + assert_eq!(adjacents.len(), 1); + } + + #[test] + fn test_one_bigger() { + let art = r#" + + .------------>---------------. + ┌-------------┐ .-. .-. | ┌------┐ .-. ┌-----┐ | .-. ┌------┐ + O____| struct_name |_( : )_( | )_◞__| name |_( : )__| tpe |___◟___( | )__| body |______O + ◝ └-------------┘ `-' `-' ◜ └------┘ `-' └-----┘ ◝ `-' └------┘ ◜ + | | .-. | | + | `------------<------( , )--' | + | `-' | + `--------------------------------------------------------------------------------' + + "#; + let buffer = CellBuffer::from(art); + let adjacents = buffer.group_adjacents(); + for (i, span) in adjacents.iter().enumerate() { + println!("span: {}", i); + println!("{}\n\n", span); + } + assert_eq!(adjacents.len(), 1); + } +} |