diff --git a/docs/sample.png b/docs/sample.png new file mode 100644 index 0000000..aa61359 Binary files /dev/null and b/docs/sample.png differ diff --git a/examples/benchmark.rs b/examples/benchmark.rs index a8e325f..0c975de 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -14,7 +14,7 @@ fn test_main(n_node: usize, _n_edge: usize) { for i in 0..n_node { let elem = Element::create( - ShapeKind::Circle(format!("hi_{}", i)), + ShapeKind::Box(ShapeContent::String(format!("hi_{}", i))), StyleAttr::new(Color::transparent(), 0, None, 0, 0), Orientation::LeftToRight, Point::zero(), @@ -29,7 +29,7 @@ fn test_main(n_node: usize, _n_edge: usize) { println!("Time elapsed in expensive_function() is: {:?}", duration); println!("--------------------------------------"); } -use layout::std_shapes::shapes::{Element, ShapeKind}; +use layout::std_shapes::shapes::{Element, ShapeContent, ShapeKind}; use layout::topo::layout::VisualGraph; fn main() { diff --git a/inputs/html.dot b/inputs/html.dot new file mode 100644 index 0000000..2876f1a --- /dev/null +++ b/inputs/html.dot @@ -0,0 +1,13 @@ +digraph structs { + node [shape=plaintext] + struct3 [label=< + + + + + + + + +
1st col2nd col
3rd col
>]; +} \ No newline at end of file diff --git a/inputs/html2.dot b/inputs/html2.dot new file mode 100644 index 0000000..7f1e541 --- /dev/null +++ b/inputs/html2.dot @@ -0,0 +1,19 @@ +digraph structs { + node [shape=plaintext]; + + struct3 [label=< + + + + + + + + + + + + + +
hello
world
bgh
cde
f
>]; +} \ No newline at end of file diff --git a/inputs/html3.dot b/inputs/html3.dot new file mode 100644 index 0000000..aa6b405 --- /dev/null +++ b/inputs/html3.dot @@ -0,0 +1,24 @@ +digraph structs { + node [shape=plaintext] + struct1 [ label=< + + + + + + + + + + + + +
elephanttwo
+ + + + +
corn
c
f
+
penguin
4
>]; +} \ No newline at end of file diff --git a/inputs/html_arrow.dot b/inputs/html_arrow.dot new file mode 100644 index 0000000..d3981f9 --- /dev/null +++ b/inputs/html_arrow.dot @@ -0,0 +1,28 @@ +digraph structs { + node [shape=plaintext] + struct1 [label=< + + +
leftmid dleright
>]; + struct2 [label=< + + +
onetwo
>]; + struct3 [label=< + + + + + + + + + + + + + +
hello
world
bgh
cde
f
>]; + struct1:f1 -> struct2:f0; + struct1:f2 -> struct3:here; +} \ No newline at end of file diff --git a/inputs/html_complex.dot b/inputs/html_complex.dot new file mode 100644 index 0000000..ee72dd6 --- /dev/null +++ b/inputs/html_complex.dot @@ -0,0 +1,51 @@ +digraph G { + rankdir=LR + node [shape=plaintext] + a [ + label=< + + + +
class
qualifier
> + ] + b [shape=ellipse style=filled + label=< + + + + + + + + + + + + +
elephanttwo
+ + + + +
corn
c
f
+
penguin
4
> + ] + c [ + label=line 2
line 3
> + ] + + subgraph { rank=same b c } + a:here -> b:there [dir=both arrowtail=diamond] + c -> b + d [shape=triangle] + d -> c [label=< + + + + + + +
Edge labels
also
> + ] +} \ No newline at end of file diff --git a/inputs/html_densenet.dot b/inputs/html_densenet.dot new file mode 100644 index 0000000..4bae7a1 --- /dev/null +++ b/inputs/html_densenet.dot @@ -0,0 +1,495 @@ +strict digraph CustomDenseNet { + graph [ordering=in rankdir=TB size="33.0,33.0"] + node [align=left fontname="Linux libertine" fontsize=10 height=0.2 margin=0 ranksep=0.1 shape=plaintext style=filled] + edge [fontsize=10] + 0 [label=< + + +
input-tensor
depth:0
(1, 3, 224, 224)
> fillcolor=lightyellow] + 1 [label=< + + + + + + + + + + +
Conv2d
depth:3
input:(1, 3, 224, 224)
output: (1, 32, 224, 224)
> fillcolor=darkseagreen] + 2 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 32, 224, 224)
output: (1, 24, 224, 224)
> fillcolor=darkseagreen] + 3 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 32, 224, 224), (1, 24, 224, 224)
output: (1, 24, 224, 224)
> fillcolor=darkseagreen] + 4 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 32, 224, 224), 2 x (1, 24, 224, 224)
output: (1, 24, 224, 224)
> fillcolor=darkseagreen] + 5 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 32, 224, 224), 3 x (1, 24, 224, 224)
output: (1, 24, 224, 224)
> fillcolor=darkseagreen] + 6 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 32, 224, 224), 4 x (1, 24, 224, 224)
output: (1, 24, 224, 224)
> fillcolor=darkseagreen] + 7 [label=< + + + + + + + + + + +
cat
depth:3
input:(1, 32, 224, 224), 5 x (1, 24, 224, 224)
output: (1, 152, 224, 224)
> fillcolor=aliceblue] + 8 [label=< + + + + + + + + + + +
BatchNorm2d
depth:3
input:(1, 152, 224, 224)
output: (1, 152, 224, 224)
> fillcolor=darkseagreen] + 9 [label=< + + + + + + + + + + +
relu
depth:3
input:(1, 152, 224, 224)
output: (1, 152, 224, 224)
> fillcolor=aliceblue] + 10 [label=< + + + + + + + + + + +
Conv2d
depth:3
input:(1, 152, 224, 224)
output: (1, 76, 224, 224)
> fillcolor=darkseagreen] + 11 [label=< + + + + + + + + + + +
AvgPool2d
depth:3
input:(1, 76, 224, 224)
output: (1, 76, 112, 112)
> fillcolor=darkseagreen] + 12 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 76, 112, 112)
output: (1, 24, 112, 112)
> fillcolor=darkseagreen] + 13 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 76, 112, 112), (1, 24, 112, 112)
output: (1, 24, 112, 112)
> fillcolor=darkseagreen] + 14 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 76, 112, 112), 2 x (1, 24, 112, 112)
output: (1, 24, 112, 112)
> fillcolor=darkseagreen] + 15 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 76, 112, 112), 3 x (1, 24, 112, 112)
output: (1, 24, 112, 112)
> fillcolor=darkseagreen] + 16 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 76, 112, 112), 4 x (1, 24, 112, 112)
output: (1, 24, 112, 112)
> fillcolor=darkseagreen] + 17 [label=< + + + + + + + + + + +
cat
depth:3
input:(1, 76, 112, 112), 5 x (1, 24, 112, 112)
output: (1, 196, 112, 112)
> fillcolor=aliceblue] + 18 [label=< + + + + + + + + + + +
BatchNorm2d
depth:3
input:(1, 196, 112, 112)
output: (1, 196, 112, 112)
> fillcolor=darkseagreen] + 19 [label=< + + + + + + + + + + +
relu
depth:3
input:(1, 196, 112, 112)
output: (1, 196, 112, 112)
> fillcolor=aliceblue] + 20 [label=< + + + + + + + + + + +
Conv2d
depth:3
input:(1, 196, 112, 112)
output: (1, 98, 112, 112)
> fillcolor=darkseagreen] + 21 [label=< + + + + + + + + + + +
AvgPool2d
depth:3
input:(1, 98, 112, 112)
output: (1, 98, 56, 56)
> fillcolor=darkseagreen] + 22 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 98, 56, 56)
output: (1, 24, 56, 56)
> fillcolor=darkseagreen] + 23 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 98, 56, 56), (1, 24, 56, 56)
output: (1, 24, 56, 56)
> fillcolor=darkseagreen] + 24 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 98, 56, 56), 2 x (1, 24, 56, 56)
output: (1, 24, 56, 56)
> fillcolor=darkseagreen] + 25 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 98, 56, 56), 3 x (1, 24, 56, 56)
output: (1, 24, 56, 56)
> fillcolor=darkseagreen] + 26 [label=< + + + + + + + + + + +
BottleneckUnit
depth:3
input:(1, 98, 56, 56), 4 x (1, 24, 56, 56)
output: (1, 24, 56, 56)
> fillcolor=darkseagreen] + 27 [label=< + + + + + + + + + + +
cat
depth:3
input:(1, 98, 56, 56), 5 x (1, 24, 56, 56)
output: (1, 218, 56, 56)
> fillcolor=aliceblue] + 28 [label=< + + + + + + + + + + +
relu
depth:1
input:(1, 218, 56, 56)
output: (1, 218, 56, 56)
> fillcolor=aliceblue] + 29 [label=< + + + + + + + + + + +
adaptive_avg_pool2d
depth:1
input:(1, 218, 56, 56)
output: (1, 218, 1, 1)
> fillcolor=aliceblue] + 30 [label=< + + + + + + + + + + +
flatten
depth:1
input:(1, 218, 1, 1)
output: (1, 218)
> fillcolor=aliceblue] + 31 [label=< + + + + + + + + + + +
Linear
depth:1
input:(1, 218)
output: (1, 10)
> fillcolor=darkseagreen] + 32 [label=< + + +
output-tensor
depth:0
(1, 10)
> fillcolor=lightyellow] + 0 -> 1 + 1 -> 2 + 1 -> 3 + 1 -> 4 + 1 -> 5 + 1 -> 6 + 1 -> 7 + 2 -> 3 + 2 -> 4 + 2 -> 5 + 2 -> 6 + 2 -> 7 + 3 -> 4 + 3 -> 5 + 3 -> 6 + 3 -> 7 + 4 -> 5 + 4 -> 6 + 4 -> 7 + 5 -> 6 + 5 -> 7 + 6 -> 7 + 7 -> 8 + 8 -> 9 + 9 -> 10 + 10 -> 11 + 11 -> 12 + 11 -> 13 + 11 -> 14 + 11 -> 15 + 11 -> 16 + 11 -> 17 + 12 -> 13 + 12 -> 14 + 12 -> 15 + 12 -> 16 + 12 -> 17 + 13 -> 14 + 13 -> 15 + 13 -> 16 + 13 -> 17 + 14 -> 15 + 14 -> 16 + 14 -> 17 + 15 -> 16 + 15 -> 17 + 16 -> 17 + 17 -> 18 + 18 -> 19 + 19 -> 20 + 20 -> 21 + 21 -> 22 + 21 -> 23 + 21 -> 24 + 21 -> 25 + 21 -> 26 + 21 -> 27 + 22 -> 23 + 22 -> 24 + 22 -> 25 + 22 -> 26 + 22 -> 27 + 23 -> 24 + 23 -> 25 + 23 -> 26 + 23 -> 27 + 24 -> 25 + 24 -> 26 + 24 -> 27 + 25 -> 26 + 25 -> 27 + 26 -> 27 + 27 -> 28 + 28 -> 29 + 29 -> 30 + 30 -> 31 + 31 -> 32 +} \ No newline at end of file diff --git a/inputs/html_font.dot b/inputs/html_font.dot new file mode 100644 index 0000000..ce6b602 --- /dev/null +++ b/inputs/html_font.dot @@ -0,0 +1,20 @@ +digraph structs { + node [shape=plaintext]; + + struct1 [label=< + + + + + + + +
line 1line2line3dfsline4 + + + + + +
Mixedfonts
+
>]; +} \ No newline at end of file diff --git a/inputs/html_font_table.dot b/inputs/html_font_table.dot new file mode 100644 index 0000000..222a3f0 --- /dev/null +++ b/inputs/html_font_table.dot @@ -0,0 +1,28 @@ +digraph structs { + node [shape=plaintext] + struct1 [label=< + + +
leftmid dleright
>]; + struct2 [label=< + + +
onetwo
>]; + struct3 [label=< + + + + + + + + + + + + + +
hello
world
bgh
cde
f
>]; + struct1:f1 -> struct2:f0; + struct1:f2 -> struct3:here; +} \ No newline at end of file diff --git a/inputs/html_img.dot b/inputs/html_img.dot new file mode 100644 index 0000000..a1ca7de --- /dev/null +++ b/inputs/html_img.dot @@ -0,0 +1,8 @@ +digraph structs { + node [shape=plaintext]; + + struct1 [label=< + + +
caption
>]; +} \ No newline at end of file diff --git a/inputs/html_lr.dot b/inputs/html_lr.dot new file mode 100644 index 0000000..4305f72 --- /dev/null +++ b/inputs/html_lr.dot @@ -0,0 +1,29 @@ +digraph structs { + rankdir=LR + node [shape=plaintext] + struct1 [label=< + + +
leftmid dleright
>]; + struct2 [label=< + + +
onetwo
>]; + struct3 [label=< + + + + + + + + + + + + + +
hello
world
bgh
cde
f
>]; + struct1:f1 -> struct2:f0; + struct1:f2 -> struct3:here; +} \ No newline at end of file diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index df4c54d..a2b7af0 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -2,8 +2,8 @@ use crate::core::color::Color; use crate::core::format::{ClipHandle, RenderBackend}; -use crate::core::geometry::Point; -use crate::core::style::StyleAttr; +use crate::core::geometry::{get_size_for_str, Point}; +use crate::core::style::{StyleAttr, TextDecoration}; use std::collections::HashMap; static SVG_HEADER: &str = @@ -84,6 +84,28 @@ impl Drop for SVGWriter { fn drop(&mut self) {} } +#[inline] +fn svg_text_decoration_str(text_decoration: &TextDecoration) -> String { + if !text_decoration.underline + && !text_decoration.overline + && !text_decoration.line_through + { + return String::new(); + } + let mut result = "text-decoration=\"".to_string(); + if text_decoration.underline { + result.push_str("underline "); + } + if text_decoration.overline { + result.push_str("overline "); + } + if text_decoration.line_through { + result.push_str("line-through "); + } + result.push_str("\""); + result +} + impl SVGWriter { // Grow the viewable svg window to include the point \p point plus some // offset \p size. @@ -214,11 +236,12 @@ impl RenderBackend for SVGWriter { fn draw_text(&mut self, xy: Point, text: &str, look: &StyleAttr) { let len = text.len(); - let font_class = self.get_or_create_font_style(look.font_size); + let font_color = look.font_color; + let font_size = look.font_size; + let font_family = look.fontname.clone(); + let size = get_size_for_str(text, font_size); let mut content = String::new(); - let cnt = 1 + text.lines().count(); - let size_y = (cnt * look.font_size) as f64; for line in text.lines() { content.push_str(&format!("", xy.x)); content.push_str(&escape_string(line)); @@ -226,12 +249,40 @@ impl RenderBackend for SVGWriter { } self.grow_window(xy, Point::new(10., len as f64 * 10.)); + let font_style_text = match look.font_style { + crate::core::style::FontStyle::Italic => "font-style=\"italic\"", + crate::core::style::FontStyle::None => "", + }; + let font_weight_text = match look.font_weight { + crate::core::style::FontWeight::Bold => "font-weight=\"bold\"", + crate::core::style::FontWeight::None => "", + }; + let text_decoration_str = + svg_text_decoration_str(&look.text_decoration); + + let baseline_shift_str = match look.baseline_shift { + crate::core::style::BaselineShift::Sub => { + "dominant-baseline=\"text-bottom\"" + } + crate::core::style::BaselineShift::Super => { + "dominant-baseline=\"text-top\"" + } + crate::core::style::BaselineShift::Normal => { + "dominant-baseline=\"auto\"" + } + }; let line = format!( - "{}", + "{}", + baseline_shift_str, xy.x, - xy.y - size_y / 2., - font_class, + xy.y - size.y / 2., + font_size, + font_family, + font_style_text, + text_decoration_str, + font_weight_text, + font_color.to_web_color(), &content ); @@ -328,6 +379,25 @@ impl RenderBackend for SVGWriter { self.counter += 1; } + fn draw_image( + &mut self, + xy: Point, + size: Point, + file_path: &str, + properties: Option, + ) { + self.grow_window(xy, size); + let props = properties.unwrap_or_default(); + let line1 = format!( + "\n + \n + \n", + xy.x, xy.y, size.x, size.y, file_path + ); + self.content.push_str(&line1); + } + fn draw_line( &mut self, start: Point, diff --git a/layout/src/core/format.rs b/layout/src/core/format.rs index 08c3a69..7f5b845 100644 --- a/layout/src/core/format.rs +++ b/layout/src/core/format.rs @@ -102,6 +102,15 @@ pub trait RenderBackend { text: &str, ); + /// Embeds an image in the canvas. + fn draw_image( + &mut self, + xy: Point, + size: Point, + file_path: &str, + properties: Option, + ); + /// Generate a clip region that shapes can use to create complex shapes. fn create_clip( &mut self, diff --git a/layout/src/core/geometry.rs b/layout/src/core/geometry.rs index b39f94c..94233f9 100644 --- a/layout/src/core/geometry.rs +++ b/layout/src/core/geometry.rs @@ -280,11 +280,32 @@ pub fn pad_shape_scalar(size: Point, s: f64) -> Point { Point::new(size.x + s, size.y + s) } +fn char_width(c: char) -> f64 { + match c { + 'a' | 'b' | 'c' | 'd' | 'e' | 'g' | 'h' | 'j' | 'k' | 'n' | 'o' + | 'v' | 'f' | 'r' | 'q' | 'x' | 'y' | 'z' | '2' | '3' | '4' | '5' + | '6' | '7' | '8' | '9' => 0.5, + 't' | 'i' | 'l' | '|' | '!' | '1' => 0.2, + 'm' | 'w' => 0.8, + _ => 1.0, + } +} + +#[inline] +fn get_width_of_line(label: &str) -> usize { + let mut width = 0f64; + for c in label.chars() { + width += char_width(c); + } + // round above + width.ceil() as usize +} + /// Estimate the bounding box of some rendered text. pub fn get_size_for_str(label: &str, font_size: usize) -> Point { // Find the longest line. let max_line_len = if !label.is_empty() { - label.lines().map(|x| x.chars().count()).max().unwrap() + label.lines().map(|x| get_width_of_line(x)).max().unwrap() } else { 0 }; diff --git a/layout/src/core/style.rs b/layout/src/core/style.rs index f917c67..b2a1460 100644 --- a/layout/src/core/style.rs +++ b/layout/src/core/style.rs @@ -2,6 +2,49 @@ use crate::core::color::Color; +#[derive(Debug, Clone)] +pub enum Align { + Center, + Left, + Right, +} + +impl Align { + pub fn from_tag_attr_list(list: Vec<(String, String)>) -> Self { + for (key, value) in list.iter() { + if key == "align" { + return match value.as_str() { + "left" => Align::Left, + "right" => Align::Right, + _ => Align::Center, + }; + } + } + Align::Center + } + + pub fn from_str(s: &str) -> Self { + match s { + "left" => Align::Left, + "right" => Align::Right, + _ => Align::Center, + } + } +} + +#[derive(Debug, Clone)] +pub enum BAlign { + Center, + Left, + Right, +} +#[derive(Debug, Clone)] +pub enum VAlign { + Middle, + Top, + Bottom, +} + #[derive(Debug, Copy, Clone)] pub enum LineStyleKind { Normal, @@ -10,6 +53,42 @@ pub enum LineStyleKind { None, } +#[derive(Debug, Copy, Clone)] +pub(crate) enum FontStyle { + None, + Italic, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum FontWeight { + None, + Bold, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct TextDecoration { + pub(crate) underline: bool, + pub(crate) overline: bool, + pub(crate) line_through: bool, +} + +impl TextDecoration { + pub fn new() -> Self { + Self { + underline: false, + overline: false, + line_through: false, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum BaselineShift { + Sub, + Super, + Normal, +} + #[derive(Clone, Debug)] pub struct StyleAttr { pub line_color: Color, @@ -17,6 +96,15 @@ pub struct StyleAttr { pub fill_color: Option, pub rounded: usize, pub font_size: usize, + pub(crate) fontname: String, + pub(crate) font_color: Color, + pub(crate) font_style: FontStyle, + pub(crate) font_weight: FontWeight, + pub(crate) text_decoration: TextDecoration, + pub(crate) baseline_shift: BaselineShift, + pub(crate) align: Align, + pub(crate) valign: VAlign, + pub(crate) balign: BAlign, } impl StyleAttr { @@ -27,12 +115,23 @@ impl StyleAttr { rounded: usize, font_size: usize, ) -> Self { + let font_color = Color::fast("black"); + let fontname = String::from("Times,serif"); Self { line_color, line_width, fill_color, + font_color, rounded, font_size, + fontname, + font_style: FontStyle::None, + font_weight: FontWeight::None, + text_decoration: TextDecoration::new(), + baseline_shift: BaselineShift::Normal, + align: Align::Center, + valign: VAlign::Middle, + balign: BAlign::Center, } } diff --git a/layout/src/core/utils.rs b/layout/src/core/utils.rs index f62dd64..628f8a1 100644 --- a/layout/src/core/utils.rs +++ b/layout/src/core/utils.rs @@ -3,7 +3,7 @@ #[cfg(feature = "log")] use log; use std::fs::File; -use std::io::{Error, Write}; +use std::io::{Error, Read, Seek, Write}; pub fn save_to_file(filename: &str, content: &str) -> Result<(), Error> { let f = File::create(filename)?; @@ -12,3 +12,50 @@ pub fn save_to_file(filename: &str, content: &str) -> Result<(), Error> { log::info!("Wrote {}", filename); Result::Ok(()) } + +pub(crate) fn get_image_size(filename: &str) -> Result<(u32, u32), Error> { + if let Ok(image_size) = get_png_size(filename) { + return Ok(image_size); + } + + // TODO: Add support for other image formats (e.g., JPEG, SVG) following graphviz specs + + Err(Error::new( + std::io::ErrorKind::InvalidData, + "Unsupported image format", + )) +} + +fn get_png_size(filename: &str) -> Result<(u32, u32), Error> { + let mut f = File::open(filename)?; + let mut signature = [0; 8]; + + f.read_exact(&mut signature)?; + + if signature != [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] { + return Err(Error::new( + std::io::ErrorKind::InvalidData, + "Not a valid PNG file", + )); + } + f.seek(std::io::SeekFrom::Current(4))?; + + let mut chunk_type = [0; 4]; + f.read_exact(&mut chunk_type)?; + if &chunk_type != b"IHDR" { + return Err(Error::new( + std::io::ErrorKind::InvalidData, + "Missing IHDR chunk", + )); + } + + let mut width_bytes = [0; 4]; + f.read_exact(&mut width_bytes)?; + let mut height_bytes = [0; 4]; + f.read_exact(&mut height_bytes)?; + + let width = u32::from_be_bytes(width_bytes); + let height = u32::from_be_bytes(height_bytes); + + Ok((width, height)) +} diff --git a/layout/src/gv/builder.rs b/layout/src/gv/builder.rs index 95d7494..fd772b2 100644 --- a/layout/src/gv/builder.rs +++ b/layout/src/gv/builder.rs @@ -1,5 +1,7 @@ //! A graph builder that converts parsed AST trees to graphs. +use super::html::{parse_html_string, HtmlGrid}; +use super::parser::ast::DotString; use super::record::record_builder; use crate::adt::dag::NodeHandle; use crate::adt::map::ScopedMap; @@ -13,7 +15,7 @@ use crate::std_shapes::shapes::*; use crate::topo::layout::VisualGraph; use std::collections::HashMap; -type PropertyList = HashMap; +type PropertyList = HashMap; // The methods in this file are responsible for converting the parsed Graphviz // AST into the VisualGraph data-structure that we use for layout and rendering @@ -43,9 +45,9 @@ pub struct GraphBuilder { edges: Vec, /// Scopes that maintain the property list that changes as we enter and /// leave different regions of the graph. - global_attr: ScopedMap, - node_attr: ScopedMap, - edge_attr: ScopedMap, + global_attr: ScopedMap, + node_attr: ScopedMap, + edge_attr: ScopedMap, } impl Default for GraphBuilder { fn default() -> Self { @@ -182,7 +184,9 @@ impl GraphBuilder { let mut dir = Orientation::TopToBottom; // Set the graph orientation based on the 'rankdir' property. - if let Option::Some(rd) = self.global_state.get("rankdir") { + if let Option::Some(DotString::String(rd)) = + self.global_state.get("rankdir") + { if rd == "LR" { dir = Orientation::LeftToRight; } @@ -239,22 +243,30 @@ impl GraphBuilder { let mut color = String::from("black"); let mut line_style = LineStyleKind::Normal; - if let Option::Some(val) = lst.get(&"label".to_string()) { + if let Option::Some(DotString::String(val)) = + lst.get(&"label".to_string()) + { label = val.clone(); } - if let Option::Some(stl) = lst.get(&"style".to_string()) { + if let Option::Some(DotString::String(stl)) = + lst.get(&"style".to_string()) + { if stl == "dashed" { line_style = LineStyleKind::Dashed; } } - if let Option::Some(x) = lst.get(&"color".to_string()) { + if let Option::Some(DotString::String(x)) = + lst.get(&"color".to_string()) + { color = x.clone(); color = Self::normalize_color(color); } - if let Option::Some(pw) = lst.get(&"penwidth".to_string()) { + if let Option::Some(DotString::String(pw)) = + lst.get(&"penwidth".to_string()) + { if let Result::Ok(x) = pw.parse::() { line_width = x; } else { @@ -263,7 +275,9 @@ impl GraphBuilder { } } - if let Option::Some(fx) = lst.get(&"fontsize".to_string()) { + if let Option::Some(DotString::String(fx)) = + lst.get(&"fontsize".to_string()) + { if let Result::Ok(x) = fx.parse::() { font_size = x; } else { @@ -294,22 +308,35 @@ impl GraphBuilder { lst: &PropertyList, default_name: &str, ) -> Element { - let mut label = default_name.to_string(); + let mut label = ShapeContent::String(default_name.to_string()); let mut edge_color = String::from("black"); let mut fill_color = String::from("white"); let mut font_size: usize = 14; let mut line_width: usize = 1; let mut make_xy_same = false; let mut rounded_corder_value = 0; + // let mut shape = ShapeKind::Circle(label.clone()); + let mut shape = ShapeKind::Circle(label.clone()); - if let Option::Some(val) = lst.get(&"label".to_string()) { - label = val.clone(); + if let Option::Some(x) = lst.get(&"label".to_string()) { + // label = val.clone(); + match x { + DotString::String(val) => { + label = ShapeContent::String(val.clone()); + shape = + ShapeKind::Circle(ShapeContent::String(val.clone())); + } + DotString::HtmlString(val) => { + label = ShapeContent::Html(parse_html_string(val).unwrap()); + shape = ShapeKind::None(label.clone()); + } + } } - let mut shape = ShapeKind::Circle(label.clone()); - // Set the shape. - if let Option::Some(val) = lst.get(&"shape".to_string()) { + if let Option::Some(DotString::String(val)) = + lst.get(&"shape".to_string()) + { match &val[..] { "box" => { shape = ShapeKind::Box(label); @@ -320,33 +347,56 @@ impl GraphBuilder { make_xy_same = true; } "record" => { - shape = record_builder(&label); + // shape = record_builder(&label); + match label { + ShapeContent::String(s) => { + shape = record_builder(&s); + } + ShapeContent::Html(_) => {} + } } "Mrecord" => { rounded_corder_value = 15; - shape = record_builder(&label); + match label { + ShapeContent::String(s) => { + shape = record_builder(&s); + } + ShapeContent::Html(_) => {} + } } - _ => shape = ShapeKind::Circle(label), + "circle" => { + shape = ShapeKind::Circle(label); + make_xy_same = true; + } + _ => {} } } - if let Option::Some(x) = lst.get(&"color".to_string()) { + if let Option::Some(DotString::String(x)) = + lst.get(&"color".to_string()) + { edge_color = x.clone(); edge_color = Self::normalize_color(edge_color); } - if let Option::Some(style) = lst.get(&"style".to_string()) { + if let Option::Some(DotString::String(style)) = + lst.get(&"style".to_string()) + { if style == "filled" && !lst.contains_key("fillcolor") { fill_color = "lightgray".to_string(); } } - if let Option::Some(x) = lst.get(&"fillcolor".to_string()) { + if let Option::Some(DotString::String(x)) = + lst.get(&"fillcolor".to_string()) + { fill_color = x.clone(); fill_color = Self::normalize_color(fill_color); } - if let Option::Some(fx) = lst.get(&"fontsize".to_string()) { + if let Option::Some(DotString::String(fx)) = + lst.get(&"fontsize".to_string()) + { if let Result::Ok(x) = fx.parse::() { font_size = x; } else { @@ -355,7 +405,9 @@ impl GraphBuilder { } } - if let Option::Some(pw) = lst.get(&"width".to_string()) { + if let Option::Some(DotString::String(pw)) = + lst.get(&"width".to_string()) + { if let Result::Ok(x) = pw.parse::() { line_width = x; } else { @@ -368,6 +420,24 @@ impl GraphBuilder { // grow top down the records grow to the left. let dir = dir.flip(); + match &mut shape { + ShapeKind::Circle(ShapeContent::Html(HtmlGrid::FontTable(x))) => { + x.resize(font_size); + } + ShapeKind::Box(ShapeContent::Html(HtmlGrid::FontTable(x))) => { + x.resize(font_size); + } + ShapeKind::DoubleCircle(ShapeContent::Html( + HtmlGrid::FontTable(x), + )) => { + x.resize(font_size); + } + ShapeKind::None(ShapeContent::Html(HtmlGrid::FontTable(x))) => { + x.resize(font_size); + } + _ => {} + } + let sz = get_shape_size(dir, &shape, font_size, make_xy_same); let look = StyleAttr::new( Color::fast(&edge_color), diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs new file mode 100644 index 0000000..49caf49 --- /dev/null +++ b/layout/src/gv/html.rs @@ -0,0 +1,1818 @@ +use crate::core::geometry::{get_size_for_str, Point}; +use std::collections::HashMap; + +use crate::core::color::Color; +use crate::core::style::{ + Align, BAlign, BaselineShift, FontStyle, FontWeight, StyleAttr, + TextDecoration, VAlign, +}; +use crate::core::utils::get_image_size; + +pub(crate) fn parse_html_string(input: &str) -> Result { + let mut parser = HtmlParser { + input: input.chars().collect(), + pos: 0, + tok: Token::Colon, + mode: HtmlMode::Html, + ch: '\0', + }; + parser.read_char(); + parser.lex(); + let x = parser.parse_html_label()?; + Ok(HtmlGrid::from_html(&x)) +} + +/// Creates an error from the string \p str. +fn to_error(str: &str) -> Result { + Result::Err(str.to_string()) +} + +#[derive(Debug, Clone)] +struct HtmlParser { + input: Vec, + pos: usize, + tok: Token, + mode: HtmlMode, + pub ch: char, +} + +#[derive(Debug, Clone, PartialEq)] +enum HtmlMode { + Html, + HtmlTag, +} + +#[derive(Debug, Clone)] +enum Token { + Colon, + EOF, + Identifier(String), + OpeningTag(TagType), + ClosingTag(TagType), + TagEnd, + TagEndWithSlash, + TagAttr(String, String), + Error(usize), +} + +#[derive(Debug, Clone, PartialEq)] +enum TagType { + Table, + Tr, + Td, + Font, + Br, + I, + Img, + B, + U, + O, + Sub, + Sup, + S, + Hr, + Vr, + Unrecognized, +} + +impl TagType { + fn from_str(tag: &str) -> Self { + // use capital letter for all letters for patter matching + match tag { + "table" => TagType::Table, + "tr" => TagType::Tr, + "td" => TagType::Td, + "font" => TagType::Font, + "br" => TagType::Br, + "i" => TagType::I, + "img" => TagType::Img, + "b" => TagType::B, + "u" => TagType::U, + "o" => TagType::O, + "sub" => TagType::Sub, + "sup" => TagType::Sup, + "s" => TagType::S, + "hr" => TagType::Hr, + "vr" => TagType::Vr, + _ => TagType::Unrecognized, + } + } + fn is_single_tag(&self) -> bool { + match self { + TagType::Br | TagType::Hr | TagType::Vr | TagType::Img => true, + _ => false, + } + } +} + +#[derive(Debug, Clone)] +enum Html { + Text(Text), + FontTable(FontTable), +} + +type Text = Vec; + +#[derive(Debug, Clone)] +struct FontTable { + rows: Vec<(Row, Option
)>, + tag: TableTag, + table_attr: TableAttr, +} + +impl FontTable { + fn try_new( + rows: Vec<(Row, Option
)>, + tag: TableTag, + table_attr: TableAttr, + ) -> Result { + if let Some(last_row) = rows.last() { + if last_row.1.is_some() { + return to_error("Table cannot end with a
tag"); + } + } else { + return to_error("Table cannot be empty"); + } + Ok(Self { + rows, + tag, + table_attr, + }) + } +} + +#[derive(Debug, Clone)] +pub(crate) enum TableTag { + None, + Font(Font), + I, + B, + U, + O, +} + +#[derive(Debug, Clone)] +struct Row { + cells: Vec<(DotCell, Option)>, +} + +impl Row { + fn try_new(cells: Vec<(DotCell, Option)>) -> Result { + if let Some(last_cell) = cells.last() { + if last_cell.1.is_some() { + return to_error("Row cannot end with a tag"); + } + } else { + return to_error("Row cannot be empty"); + } + Ok(Self { cells }) + } +} + +#[derive(Debug, Clone)] +enum TextItem { + TaggedText(TaggedText), + Br(Align), + PlainText(String), +} + +#[derive(Debug, Clone)] +struct TaggedText { + text_items: Text, + tag: TextTag, +} + +#[derive(Debug, Clone)] +enum TextTag { + Font(Font), + I, + B, + U, + O, + Sub, + Sup, + S, +} + +#[derive(Debug, Clone)] +pub(crate) struct Font { + pub(crate) color: Option, + pub(crate) face: Option, + pub(crate) point_size: Option, +} + +#[derive(Debug, Clone)] +struct DotCell { + label: LabelOrImg, + td_attr: TdAttr, +} + +#[derive(Debug, Clone)] +enum LabelOrImg { + Html(Html), + Img(Image), +} + +#[derive(Debug, Clone)] +pub(crate) struct TdAttr { + // No inheritance on align, use the most recent value + align: Align, // CENTER|LEFT|RIGHT + balign: BAlign, + valign: VAlign, // MIDDLE|BOTTOM|TOP + colspan: u16, + rowspan: u16, + height: Option, // value + width: Option, // value + fixedsize: bool, // FALSE|TRUE + sides: Sides, + + // Full inheritance on bgcolor, use only if set + bgcolor: Option, // color + color: Option, // color + + // inheritance only for the first children cell + pub(crate) border: Option, // value + pub(crate) cellpadding: Option, // value + + // this seems not to be used by the official graphviz tools + // nor does it make sense to have it in the cell attributes + // probably wrong documentation + // TODO: report it back to graphviz + cellspacing: Option, // value + + // TODO: to be implemented + gradientangle: Option, // value + href: Option, // value + id: Option, // value + pub(crate) port: Option, // portName + style: Option, // value + target: Option, // value + title: Option, // value + tooltip: Option, // value +} + +#[derive(Debug, Clone)] +pub(crate) struct Image { + pub(crate) scale: Scale, + pub(crate) source: String, +} + +#[derive(Debug, Clone)] +struct Vr {} + +#[derive(Debug, Clone)] +struct Hr {} + +#[derive(Debug, Clone)] +pub(crate) struct TableAttr { + // No inheritance on align, use the most recent value + align: Align, // CENTER|LEFT|RIGHT + valign: VAlign, // MIDDLE|BOTTOM|TOP + sides: Sides, // value + height: Option, // value + width: Option, // value + columns: Option, // value + rows: Option, // value + fixedsize: bool, // FALSE|TRUE + + // Full inheritance on bgcolor, use only if set + color: Option, // color + bgcolor: Option, // color + + // inheritance only for the first children cell + pub(crate) border: u8, // value + pub(crate) cellborder: Option, // value + pub(crate) cellpadding: u8, // value + pub(crate) cellspacing: u8, // value + + gradientangle: Option, // value + href: Option, // value + id: Option, // value + pub(crate) port: Option, // portName + style: Option, // value + target: Option, // value + title: Option, // value + tooltip: Option, // value +} + +#[derive(Debug, Clone)] +struct Sides { + left: bool, + right: bool, + top: bool, + bottom: bool, +} + +#[derive(Debug, Clone)] +enum RowFormat { + Star, + None, +} + +#[derive(Debug, Clone)] +enum ColumnFormat { + Star, + None, +} + +#[derive(Debug, Clone)] +pub(crate) enum Scale { + False, + True, + Width, + Height, + Both, +} + +impl HtmlParser { + fn next_token(&mut self) -> Token { + match self.mode { + HtmlMode::Html => self.read_html(), + HtmlMode::HtmlTag => self.read_tag_inside(), + } + } + fn lex(&mut self) { + match self.tok { + Token::Error(pos) => { + panic!("can't parse after error at {}", pos); + } + Token::EOF => { + panic!("can't parse after EOF"); + } + _ => { + // Lex the next token. + self.tok = self.next_token(); + } + } + } + fn skip_whitespace(&mut self) -> bool { + let mut changed = false; + while self.ch.is_ascii_whitespace() { + self.read_char(); + changed = true; + } + changed + } + fn has_next(&self) -> bool { + self.pos < self.input.len() + } + fn read_char(&mut self) { + if !self.has_next() { + self.ch = '\0'; + } else { + self.ch = self.input[self.pos]; + self.pos += 1; + } + } + + fn read_tag_inside(&mut self) -> Token { + let tok: Token; + while self.skip_whitespace() {} + if self.ch == '>' { + self.read_char(); + self.mode = HtmlMode::Html; + return Token::TagEnd; + } + if self.ch == '/' { + self.read_char(); + if self.ch == '>' { + self.read_char(); + self.mode = HtmlMode::Html; + return Token::TagEndWithSlash; + } else { + return Token::Error(self.pos); + } + } + tok = self.read_tag_attr(); + self.read_char(); + tok + } + fn read_html_text(&mut self) -> Token { + let mut result = String::new(); + while self.ch != '<' && self.ch != '\0' && self.ch != '>' { + result.push(self.ch); + self.read_char(); + // escape new line + } + Token::Identifier(result) + } + fn read_html(&mut self) -> Token { + let mut tag_name = String::new(); + if self.ch == '\0' { + return Token::EOF; + } + + if self.ch != '<' { + return self.read_html_text(); + } + self.read_char(); + + if self.ch == '/' { + self.read_char(); + while self.ch != '>' && self.ch != '\0' { + tag_name.push(self.ch); + self.read_char(); + } + if self.ch == '\0' { + return Token::Error(self.pos); + } + if tag_name.is_empty() { + return Token::Error(self.pos); + } + self.mode = HtmlMode::Html; + self.read_char(); + let tag_name = tag_name.to_lowercase(); + Token::ClosingTag(TagType::from_str(&tag_name)) + } else { + while self.ch.is_alphabetic() { + tag_name.push(self.ch); + self.read_char(); + } + if tag_name.is_empty() { + return Token::Error(self.pos); + } + let tag_name = tag_name.to_lowercase(); + self.mode = HtmlMode::HtmlTag; + Token::OpeningTag(TagType::from_str(&tag_name)) + } + } + fn read_string(&mut self) -> Token { + let mut result = String::new(); + self.read_char(); + while self.ch != '"' { + // Handle escaping + if self.ch == '\\' { + // Consume the escape character. + self.read_char(); + self.ch = match self.ch { + 'n' => '\n', + 'l' => '\n', + _ => self.ch, + } + } else if self.ch == '\0' { + // Reached EOF without completing the string + return Token::Error(self.pos); + } + result.push(self.ch); + self.read_char(); + } + Token::Identifier(result) + } + fn read_tag_attr(&mut self) -> Token { + let mut attr_name = String::new(); + while self.skip_whitespace() {} + while self.ch != '=' && self.ch != '>' && self.ch != '\0' { + // skip over whitespace + if !self.ch.is_ascii_whitespace() { + attr_name.push(self.ch); + } + self.read_char(); + } + if self.ch != '=' { + return Token::Error(self.pos); + } + self.read_char(); + while self.skip_whitespace() {} + if self.ch != '"' { + return Token::Error(self.pos); + } + + let x = self.read_string(); + if let Token::Identifier(s) = x { + Token::TagAttr(attr_name.to_lowercase(), s.to_lowercase()) + } else { + Token::Error(self.pos) + } + } + // Parse HTML-like label content between < and > + fn parse_html_label(&mut self) -> Result { + let is_table = self.is_table()?; + + if is_table { + Ok(Html::FontTable(self.parse_table()?)) + } else { + Ok(Html::Text(self.parse_text()?)) + } + } + + fn parse_text(&mut self) -> Result { + let mut text_items = vec![]; + loop { + match self.tok { + Token::ClosingTag(_) | Token::EOF => { + break; + } + _ => {} + } + text_items.push(self.parse_text_item()?); + } + Ok(text_items) + } + + fn is_table(&self) -> Result { + // check if the current token is a table tag with a look ahead of distance 2 + // check if cloing is necessary + let mut parser = self.clone(); + if let Token::Identifier(_) = parser.tok.clone() { + // Consume the text. + parser.lex(); + } + + if let Token::OpeningTag(TagType::Table) = parser.tok.clone() { + // Consume the opening tag. + return Ok(true); + } + + match parser.tok.clone() { + Token::OpeningTag(_) => { + parser.mode = HtmlMode::HtmlTag; + parser.parse_tag_start(true)?; + } + _ => { + return Ok(false); + } + } + + if let Token::OpeningTag(TagType::Table) = parser.tok.clone() { + // Consume the opening tag. + return Ok(true); + } + + Ok(false) + } + + fn parse_text_item(&mut self) -> Result { + Ok(match self.tok.clone() { + Token::Identifier(x) => { + self.lex(); + TextItem::PlainText(x) + } + Token::OpeningTag(x) => { + self.mode = HtmlMode::HtmlTag; + let (tag, tag_attr) = self.parse_tag_start(false)?; + + if tag.is_single_tag() { + match x { + TagType::Br => { + return Ok(TextItem::Br( + Align::from_tag_attr_list(tag_attr), + )); + } + _ => {} + } + } else { + let text_items = self.parse_text()?; + self.parse_tag_end(&tag, false)?; + match x { + _ => { + return Ok(TextItem::TaggedText(TaggedText { + tag: TextTag::new(&tag, tag_attr), + text_items, + })) + } + } + } + + return to_error( + format!( + "Expected closing tag for {:?}, found {:?}", + x, self.tok + ) + .as_str(), + ); + } + _ => { + return to_error( + format!( + "Expected identifier or tag opener, found {:?}", + self.tok + ) + .as_str(), + ) + } + }) + } + + fn parse_table(&mut self) -> Result { + let mut invalid_string = false; + if let Token::Identifier(x) = self.tok.clone() { + // Consume the text. + self.lex(); + invalid_string = is_text_table_wrapper_invalid(x.as_str()); + } + let (tag1, table_attr1) = self.parse_tag_start(true)?; + let (table_tag1, table_attr2) = match tag1 { + TagType::Font + | TagType::I + | TagType::B + | TagType::U + | TagType::O => { + let (tag, tag_attr) = self.parse_tag_start(true)?; + if tag != TagType::Table { + return to_error( + format!("Expected , found {:?}", tag).as_str(), + ); + } + if invalid_string { + return to_error( + format!( + "cannot have string before table tag: {:?}", + tag1 + ) + .as_str(), + ); + } + (Some((tag1, table_attr1)), tag_attr) + } + TagType::Table => (None, table_attr1), + _ => { + return to_error( + format!("Expected , found {:?}", tag1).as_str(), + ); + } + }; + let mut rows = Vec::new(); + + loop { + if let Token::ClosingTag(_) = self.tok.clone() { + break; + } + let row = self.parse_row()?; + let row_split = if let Token::OpeningTag(TagType::Hr) = + self.tok.clone() + { + let (tag_type, _) = self.parse_tag_start(true)?; + if tag_type != TagType::Hr { + return to_error( + format!("Expected , found {:?}", tag_type).as_str(), + ); + } + Some(Hr {}) + } else { + None + }; + rows.push((row, row_split)); + } + + self.parse_tag_end(&TagType::Table, true)?; + if let Some(ref tag) = table_tag1 { + self.parse_tag_end(&tag.0, false)?; + + if let Token::Identifier(x) = self.tok.clone() { + if is_text_table_wrapper_invalid(x.as_str()) { + return to_error( + format!( + "No space after font tag wrapping table: {:?}", + x + ) + .as_str(), + ); + } + } + } + let table_attr = TableAttr::from_attr_list(table_attr2); + + FontTable::try_new(rows, TableTag::from_tag(table_tag1), table_attr) + } + + fn parse_tag_attr_list( + &mut self, + tag_type: TagType, + ) -> Result, String> { + let mut lst = Vec::new(); + loop { + match self.tok { + Token::TagEnd => { + if tag_type.is_single_tag() { + return to_error(format!("Tag {:?} is a pair attribe and should be closed ending tag", tag_type).as_str()); + } + self.lex(); + break; + } + Token::TagEndWithSlash => { + if !tag_type.is_single_tag() { + return to_error(format!("Tag {:?} is a single tag and should be closed with single tag", tag_type).as_str()); + } + self.lex(); + break; + } + _ => {} + } + let tag_attr = if let Token::TagAttr(attr, value) = self.tok.clone() + { + self.lex(); + (attr, value) + } else { + return to_error("wrong identifi inside able tag"); + }; + lst.push(tag_attr); + } + + Ok(lst) + } + fn parse_row(&mut self) -> Result { + let (tag_type, _attr_list) = self.parse_tag_start(true)?; + if tag_type != TagType::Tr { + return to_error( + format!("Expected , found {:?}", tag_type).as_str(), + ); + } + + let mut cells = Vec::new(); + loop { + if let Token::ClosingTag(_) = self.tok.clone() { + break; + } + let cell = self.parse_cell()?; + let cell_split = if let Token::OpeningTag(TagType::Vr) = + self.tok.clone() + { + let (tag_type, _) = self.parse_tag_start(true)?; + if tag_type != TagType::Vr { + return to_error( + format!("Expected , found {:?}", tag_type).as_str(), + ); + } + Some(Vr {}) + } else { + None + }; + cells.push((cell, cell_split)); + } + self.parse_tag_end(&TagType::Tr, true)?; + Row::try_new(cells) + } + + fn parse_cell(&mut self) -> Result { + let (tag_type, attr_list) = self.parse_tag_start(false)?; + if tag_type != TagType::Td { + return to_error( + format!("Expected
, found {:?}", tag_type).as_str(), + ); + } + let label = match self.tok.clone() { + Token::OpeningTag(TagType::Img) => { + self.mode = HtmlMode::HtmlTag; + let (tag_type, attr_list) = self.parse_tag_start(false)?; + if tag_type != TagType::Img { + return to_error( + format!("Expected , found {:?}", tag_type) + .as_str(), + ); + } + let img = Image::from_tag_attr_list(attr_list)?; + LabelOrImg::Img(img) + } + _ => LabelOrImg::Html(self.parse_html_label()?), + }; + self.parse_tag_end(&TagType::Td, true)?; + Ok(DotCell { + label, + td_attr: TdAttr::from_tag_attr_list(attr_list), + }) + } + + fn parse_tag_start( + &mut self, + pass_identifier: bool, + ) -> Result<(TagType, Vec<(String, String)>), String> { + let tag_type = if let Token::OpeningTag(x) = self.tok.clone() { + self.mode = HtmlMode::HtmlTag; + self.lex(); + x + } else { + return to_error( + format!( + "Expected opening tag to start HTML label tag, found {:?}", + self.tok + ) + .as_str(), + ); + }; + let tag_attr_list = self.parse_tag_attr_list(tag_type.clone())?; + match tag_type { + TagType::Br | TagType::Sub | TagType::Sup | TagType::S => { + // self.lexer.mode = super::lexer::HtmlMode::Html; + } + TagType::Hr + | TagType::Tr + | TagType::Td + | TagType::Table + | TagType::Img + | TagType::Vr + | TagType::Font + | TagType::I + | TagType::B + | TagType::U + | TagType::O => { + if pass_identifier { + if let Token::Identifier(_) = self.tok.clone() { + self.lex(); + } + } + } + TagType::Unrecognized => { + return to_error( + format!("Unrecognized tag type {:?}", tag_type).as_str(), + ); + } + } + Ok((tag_type, tag_attr_list)) + } + + fn parse_tag_end( + &mut self, + tag: &TagType, + pass_identifier: bool, + ) -> Result<(), String> { + if let Token::ClosingTag(x) = self.tok.clone() { + if x == *tag { + self.lex(); + } else { + return to_error( + format!( + "Expected {:?} to end HTML label tag, found {:?}", + tag, x + ) + .as_str(), + ); + } + } else { + return to_error(format!("Expected 'closing tag {:?}' to end HTML label tag, found {:?}", tag, self.tok).as_str()); + } + if pass_identifier { + if let Token::Identifier(_) = self.tok.clone() { + self.lex(); + } + } + + Ok(()) + } +} + +impl Image { + fn from_tag_attr_list( + tag_attr_list: Vec<(String, String)>, + ) -> Result { + let mut scale = Scale::False; + let mut source = String::new(); + for (key, value) in tag_attr_list.iter() { + match key.as_str() { + "scale" => { + scale = match value.as_str() { + "true" => Scale::True, + "width" => Scale::Width, + "height" => Scale::Height, + "both" => Scale::Both, + _ => Scale::False, + } + } + "src" => source = value.clone(), + _ => {} + } + } + Ok(Self { scale, source }) + } + + fn width(&self) -> f64 { + let size = get_image_size(&self.source).unwrap(); + size.0 as f64 + } + fn height(&self) -> f64 { + let size = get_image_size(&self.source).unwrap(); + size.1 as f64 + } + + pub(crate) fn size(&self) -> Point { + let size = get_image_size(&self.source).unwrap(); + Point::new(size.0 as f64, size.1 as f64) + } +} + +impl Font { + fn new() -> Self { + Self { + color: None, + face: None, + point_size: None, + } + } + + fn set_attr(&mut self, attr: &str, value: &str) { + match attr { + "color" => { + self.color = { + if let Some(color) = Color::from_name(value) { + Some(color) + } else { + None + } + } + } + "face" => self.face = Some(value.to_string()), + "point-size" => self.point_size = value.parse().ok(), + _ => {} + } + } + + fn from_tag_attr_list(list: Vec<(String, String)>) -> Self { + let mut font = Self::new(); + for (key, value) in list.iter() { + font.set_attr(key, value); + } + font + } +} + +impl TextTag { + fn new(tag: &TagType, tag_attr_list: Vec<(String, String)>) -> Self { + match tag { + TagType::Font => { + let font = Font::from_tag_attr_list(tag_attr_list); + TextTag::Font(font) + } + TagType::I => TextTag::I, + TagType::B => TextTag::B, + TagType::U => TextTag::U, + TagType::O => TextTag::O, + TagType::Sub => TextTag::Sub, + TagType::Sup => TextTag::Sup, + TagType::S => TextTag::S, + _ => panic!("Invalid tag for text: {:?}", tag), + } + } +} + +impl TableTag { + fn from_tag(tag_pair: Option<(TagType, Vec<(String, String)>)>) -> Self { + if let Some(tag_inner) = tag_pair { + match tag_inner.0 { + TagType::Table => TableTag::None, + TagType::Font => TableTag::Font(Font::from_tag_attr_list( + tag_inner.1.clone(), + )), + TagType::I => TableTag::I, + TagType::B => TableTag::B, + TagType::U => TableTag::U, + TagType::O => TableTag::O, + _ => panic!("Invalid tag for table: {:?}", tag_inner.0), + } + } else { + TableTag::None + } + } +} + +impl TdAttr { + fn new() -> Self { + Self { + align: Align::Center, + balign: BAlign::Center, + bgcolor: None, + border: None, + cellpadding: None, + cellspacing: None, + color: None, + colspan: 1, + fixedsize: false, + gradientangle: None, + height: None, + href: None, + id: None, + port: None, + rowspan: 1, + sides: Sides::from_str(""), + style: None, + target: None, + title: None, + tooltip: None, + valign: VAlign::Middle, + width: None, + } + } + + fn from_tag_attr_list(list: Vec<(String, String)>) -> Self { + let mut attr = Self::new(); + for (key, value) in list.iter() { + attr.set_attr(key, value); + } + attr + } + + fn set_attr(&mut self, attr: &str, value: &str) { + match attr { + "align" => { + self.align = match value { + "left" => Align::Left, + "right" => Align::Right, + _ => Align::Center, + } + } + "balign" => { + self.balign = match value { + "left" => BAlign::Left, + "right" => BAlign::Right, + _ => BAlign::Center, + } + } + "bgcolor" => self.bgcolor = Some(value.to_string()), + "border" => self.border = value.parse().ok(), + "cellpadding" => self.cellpadding = value.parse().ok(), + "cellspacing" => self.cellspacing = value.parse().ok(), + "color" => self.color = Some(value.to_string()), + "colspan" => self.colspan = value.parse().unwrap_or(1), + "fixedsize" => self.fixedsize = value == "true", + "gradientangle" => self.gradientangle = Some(value.to_string()), + "height" => self.height = value.parse().ok(), + "href" => self.href = Some(value.to_string()), + "id" => self.id = Some(value.to_string()), + "port" => self.port = Some(value.to_string()), + "rowspan" => self.rowspan = value.parse().unwrap_or(1), + "sides" => self.sides = Sides::from_str(value), + "style" => self.style = Some(value.to_string()), + "target" => self.target = Some(value.to_string()), + "title" => self.title = Some(value.to_string()), + "tooltip" => self.tooltip = Some(value.to_string()), + "valign" => { + self.valign = match value { + "top" => VAlign::Top, + "bottom" => VAlign::Bottom, + _ => VAlign::Middle, + } + } + "width" => self.width = value.parse().ok(), + _ => {} + } + } + + pub(crate) fn build_style_attr(&self, style_attr: &StyleAttr) -> StyleAttr { + let mut style_attr = style_attr.clone(); + if let Some(ref color) = self.bgcolor { + style_attr.fill_color = Color::from_name(color); + } + style_attr.valign = self.valign.clone(); + style_attr.align = self.align.clone(); + style_attr.balign = self.balign.clone(); + + style_attr + } +} + +impl ColumnFormat { + fn from_str(s: &str) -> Self { + if s.starts_with('*') { + Self::Star + } else { + Self::None + } + } +} + +impl RowFormat { + fn from_str(s: &str) -> Self { + if s.starts_with('*') { + Self::Star + } else { + Self::None + } + } +} + +impl Sides { + fn from_str(s: &str) -> Self { + let mut sides = Self { + left: false, + right: false, + top: false, + bottom: false, + }; + for c in s.chars() { + match c { + 'L' => sides.left = true, + 'R' => sides.right = true, + 'T' => sides.top = true, + 'B' => sides.bottom = true, + _ => {} + } + } + sides + } +} + +impl TableAttr { + fn new() -> Self { + Self { + align: Align::Center, + bgcolor: None, + border: 1, + cellborder: None, + cellpadding: 2, + cellspacing: 2, + color: None, + columns: None, + fixedsize: false, + gradientangle: None, + height: None, + href: None, + id: None, + port: None, + rows: None, + sides: Sides::from_str(""), + style: None, + target: None, + title: None, + tooltip: None, + valign: VAlign::Middle, + width: None, + } + } + fn from_attr_list(list: Vec<(String, String)>) -> Self { + let mut attr = Self::new(); + for (key, value) in list.iter() { + attr.set_attr(key, value); + } + attr + } + + fn set_attr(&mut self, attr: &str, value: &str) { + let attr = attr.to_lowercase(); + match attr.as_str() { + "align" => { + self.align = match value { + "left" => Align::Left, + "right" => Align::Right, + _ => Align::Center, + } + } + "bgcolor" => { + self.bgcolor = { + if let Some(color) = Color::from_name(value) { + Some(color) + } else { + None + } + } + } + "border" => self.border = value.parse().unwrap_or(0), + "cellborder" => self.cellborder = value.parse().ok(), + "cellpadding" => self.cellpadding = value.parse().unwrap_or(0), + "cellspacing" => self.cellspacing = value.parse().unwrap_or(0), + "color" => { + self.color = { + if let Some(color) = Color::from_name(value) { + Some(color) + } else { + None + } + } + } + "fixedsize" => self.fixedsize = value == "true", + "gradientangle" => self.gradientangle = Some(value.to_string()), + "height" => self.height = value.parse().ok(), + "width" => self.width = value.parse().ok(), + "href" => self.href = Some(value.to_string()), + "id" => self.id = Some(value.to_string()), + "port" => self.port = Some(value.to_string()), + "rows" => self.rows = Some(RowFormat::from_str(value)), + "sides" => self.sides = Sides::from_str(value), + "style" => self.style = Some(value.to_string()), + "target" => self.target = Some(value.to_string()), + "title" => self.title = Some(value.to_string()), + "tooltip" => self.tooltip = Some(value.to_string()), + "valign" => { + self.valign = match value { + "top" => VAlign::Top, + "bottom" => VAlign::Bottom, + _ => VAlign::Middle, + } + } + "columns" => self.columns = Some(ColumnFormat::from_str(value)), + _ => {} + } + } +} + +#[derive(Debug, Clone)] +pub enum HtmlGrid { + Text(TextGrid), + FontTable(TableGrid), +} + +#[derive(Debug, Clone)] +pub struct TextGrid { + // each line is a vector of PlainTextGrid + // as a whole it represent multiline text + pub(crate) text_items: Vec>, + br: Vec, +} + +#[derive(Debug, Clone)] +pub(crate) struct PlainText { + pub(crate) text: String, + pub(crate) text_style: TextStyle, +} + +#[derive(Debug, Clone)] +pub(crate) struct TextStyle { + pub(crate) font: Font, + pub(crate) font_style: FontStyle, + pub(crate) font_weight: FontWeight, + pub(crate) text_decoration: TextDecoration, + pub(crate) baseline_shift: BaselineShift, +} + +#[derive(Debug, Clone)] +pub struct TableGrid { + pub(crate) cells: Vec<(TdAttr, DotCellGrid)>, + grid: Vec>, + width_arr: Vec, // width in svg units + height_arr: Vec, // height in svg units + width_in_cell: usize, // width of the table in cells + height_in_cell: usize, // height of the table in cells + font_size: usize, + pub(crate) table_attr: TableAttr, + pub(crate) table_tag: TableTag, +} + +#[derive(Debug, Clone)] +pub(crate) struct DotCellGrid { + i: usize, + j: usize, + width_in_cell: usize, + height_in_cell: usize, + pub(crate) label_grid: LabelOrImgGrid, + td_attr: TdAttr, +} + +#[derive(Debug, Clone)] +pub(crate) enum LabelOrImgGrid { + Html(HtmlGrid), + Img(Image), +} + +impl DotCellGrid { + fn from_dot_cell( + i: usize, + j: usize, + width_in_cell: usize, + height_in_cell: usize, + dot_cell: &DotCell, + ) -> Self { + let label_grid = match &dot_cell.label { + LabelOrImg::Html(html) => { + LabelOrImgGrid::Html(HtmlGrid::from_html(html)) + } + LabelOrImg::Img(image) => LabelOrImgGrid::Img(image.clone()), + }; + Self { + i, + j, + width_in_cell, + height_in_cell, + label_grid, + td_attr: dot_cell.td_attr.clone(), + } + } +} + +impl TextGrid { + fn new() -> Self { + Self { + text_items: vec![], + br: vec![], + } + } + fn collect_from_text(&mut self, text: &Text, text_style: &TextStyle) { + for item in text.iter() { + match item { + TextItem::TaggedText(tagged_text) => { + let mut text_style = text_style.clone(); + match &tagged_text.tag { + TextTag::Font(font) => { + text_style.font = font.clone(); + } + TextTag::I => text_style.font_style = FontStyle::Italic, + TextTag::B => text_style.font_weight = FontWeight::Bold, + TextTag::U => { + text_style.text_decoration.underline = true + } + TextTag::O => { + text_style.text_decoration.overline = true + } + TextTag::Sub => { + text_style.baseline_shift = BaselineShift::Sub + } + TextTag::Sup => { + text_style.baseline_shift = BaselineShift::Super + } + TextTag::S => { + text_style.text_decoration.line_through = true + } + } + self.collect_from_text( + &tagged_text.text_items, + &text_style, + ); + } + TextItem::Br(align) => { + self.text_items.push(vec![]); + self.br.push(align.clone()); + } + TextItem::PlainText(text) => { + let plain_text = PlainText { + text: text.clone(), + text_style: text_style.clone(), + }; + if let Some(last_line) = self.text_items.last_mut() { + last_line.push(plain_text); + } else { + let mut line = vec![]; + line.push(plain_text); + self.text_items.push(line); + } + } + } + } + } + + fn width(&self, font_size: usize) -> f64 { + let mut width = 0.0; + for line in self.text_items.iter() { + let mut line_width = 0.0; + for item in line.iter() { + let font_size = match item.text_style.font.point_size { + Some(size) => size as usize, + None => font_size, + }; + let text_size = get_size_for_str(&item.text, font_size); + line_width += text_size.x; + } + if width < line_width { + width = line_width; + } + } + width + } + pub(crate) fn height(&self, font_size: usize) -> f64 { + let mut height = 0.0; + for line in self.text_items.iter() { + // TODO: we are going with the last with the assumption that heigh is the same for every plaintext, + // which is correct for the current get_size_for_str implementation + let mut line_height = 0.0; + for item in line.iter() { + let font_size = match item.text_style.font.point_size { + Some(size) => size as usize, + None => font_size, + }; + let text_size = get_size_for_str(&item.text, font_size); + if line_height < text_size.y { + line_height = text_size.y; + } + } + height += line_height; + } + height + } +} + +pub(crate) fn get_line_height(line: &Vec, font_size: usize) -> f64 { + let mut line_height = 0.0; + for item in line.iter() { + let font_size = match item.text_style.font.point_size { + Some(size) => size as usize, + None => font_size, + }; + let text_size = get_size_for_str(&item.text, font_size); + if line_height < text_size.y { + line_height = text_size.y; + } + } + line_height +} + +impl PlainText { + pub(crate) fn width(&self, font_size: usize) -> f64 { + let font_size = match self.text_style.font.point_size { + Some(size) => size as usize, + None => font_size, + }; + get_size_for_str(&self.text, font_size).x + } + + pub(crate) fn build_style_attr(&self, style_attr: &StyleAttr) -> StyleAttr { + let mut style_attr = style_attr.clone(); + if let Some(font_size) = self.text_style.font.point_size { + style_attr.font_size = font_size as usize; + } + + if let Some(ref color) = self.text_style.font.color { + style_attr.font_color = color.clone(); + } + + if let Some(ref face) = self.text_style.font.face { + style_attr.fontname = face.clone(); + } + + match self.text_style.font_style { + FontStyle::Italic => style_attr.font_style = FontStyle::Italic, + FontStyle::None => {} + } + match self.text_style.font_weight { + FontWeight::Bold => style_attr.font_weight = FontWeight::Bold, + FontWeight::None => {} + } + style_attr.text_decoration = self.text_style.text_decoration.clone(); + style_attr.baseline_shift = self.text_style.baseline_shift.clone(); + + style_attr + } +} + +impl HtmlGrid { + pub(crate) fn size(&self, font_size: usize) -> Point { + match self { + HtmlGrid::Text(text) => { + Point::new(text.width(font_size), text.height(font_size)) + } + HtmlGrid::FontTable(table_grid) => table_grid.size(font_size), + } + } + fn from_html(html: &Html) -> Self { + match html { + Html::Text(text) => { + let mut text_grid = TextGrid::new(); + let text_style = TextStyle { + font: Font::new(), + font_style: FontStyle::None, + font_weight: FontWeight::None, + text_decoration: TextDecoration::new(), + baseline_shift: BaselineShift::Normal, + }; + text_grid.collect_from_text(text, &text_style); + HtmlGrid::Text(text_grid) + } + Html::FontTable(table) => { + HtmlGrid::FontTable(TableGrid::from_table(table)) + } + } + } +} + +fn is_text_table_wrapper_invalid(text: &str) -> bool { + for line in text.lines() { + if !line.is_empty() { + return true; + } + } + false +} + +#[derive(Debug, Clone)] +struct TableHashGrid { + pub(crate) cells: Vec<(TdAttr, DotCellGrid)>, + pub(crate) occupation: HashMap<(usize, usize), usize>, // x, y, cell index +} + +impl TableHashGrid { + fn width(&self) -> usize { + self.occupation.keys().map(|(x, _)| *x).max().unwrap_or(0) + 1 + } + fn height(&self) -> usize { + self.occupation.keys().map(|(_, y)| *y).max().unwrap_or(0) + 1 + } + // fn pretty_print(&self) { + // // print in a table format with + indicating occupied and - indicating free + // let width = self.width(); + // let height = self.height(); + // let mut table = vec![vec!['-'; width]; height]; + // for (x, y) in self.occupation.keys() { + // table[*y][*x] = '+'; + // } + // for y in 0..height { + // for x in 0..width { + // print!("{}", table[y][x]); + // } + // println!(); + // } + // } + fn add_cell( + &mut self, + x: usize, + y: usize, + width: usize, + height: usize, + td_attr: TdAttr, + dot_cell: DotCell, + ) { + self.cells.push(( + td_attr, + DotCellGrid::from_dot_cell(x, y, width, height, &dot_cell), + )); + // boundaries are desinged with respect to html specs for forming table algo + for i in x..(x + width) { + for j in y..(y + height) { + let x = self.occupation.insert((i, j), self.cells.len() - 1); + if x.is_some() { + panic!("Cell already occupied at ({}, {})", i, j); + } + } + } + } + fn is_occupied(&self, x: usize, y: usize) -> bool { + self.occupation.contains_key(&(x, y)) + } + + fn from_table(font_table: &FontTable) -> Self { + let mut width = 0; + let mut height = 0; + let mut y_current = 0; + let mut table_grid = Self { + cells: Vec::new(), + occupation: HashMap::new(), + }; + for row in &font_table.rows { + table_grid.process_row( + &row.0, + &mut width, + &mut height, + &mut y_current, + ); + } + + // ending part + table_grid + } + + fn process_row( + &mut self, + row: &Row, + width: &mut usize, + height: &mut usize, + y_current: &mut usize, + ) { + if height == y_current { + *height += 1; + } + let mut x_current = 0; + for c in &row.cells { + let cell = &c.0; + let colspan = cell.td_attr.colspan as usize; + let rowspan = cell.td_attr.rowspan as usize; + while x_current < *width && self.is_occupied(x_current, *y_current) + { + x_current += 1; + } + if x_current == *width { + *width += 1; + } + if *width < x_current + colspan { + *width = x_current + colspan; + } + if *height < *y_current + rowspan { + *height = *y_current + rowspan; + } + + self.add_cell( + x_current, + *y_current, + colspan, + rowspan, + cell.td_attr.clone(), + cell.clone(), + ); + x_current += colspan; + } + *y_current += 1; + } +} + +impl TableGrid { + pub(crate) fn width(&self) -> f64 { + self.width_arr.iter().sum::<f64>() + + (self.table_attr.cellspacing as usize * (self.width_in_cell + 1)) + as f64 + + self.table_attr.border as f64 * 2. + } + pub(crate) fn height(&self) -> f64 { + self.height_arr.iter().sum::<f64>() + + (self.table_attr.cellspacing as usize * (self.height_in_cell + 1)) + as f64 + + self.table_attr.border as f64 * 2. + } + fn size(&self, font_size: usize) -> Point { + if font_size != self.font_size { + let mut table_grid = self.clone(); + table_grid.resize(font_size); + Point::new(table_grid.width(), table_grid.height()) + } else { + Point::new(self.width(), self.height()) + } + } + pub(crate) fn cell_pos(&self, d: &DotCellGrid) -> Point { + let idx = d.i; + let x = self.width_arr.iter().take(idx).sum::<f64>() + + (self.table_attr.cellspacing as usize * (idx + 1)) as f64 + + self.table_attr.border as f64 / 2.0; + + let idx = d.j; + + let y = self.height_arr.iter().take(idx).sum::<f64>() + + (self.table_attr.cellspacing as usize * (idx + 1)) as f64 + + self.table_attr.border as f64 / 2.0; + + Point::new(x, y) + } + pub(crate) fn cell_size(&self, dot_cell_grid: &DotCellGrid) -> Point { + let mut height = 0f64; + for i in + dot_cell_grid.j..(dot_cell_grid.j + dot_cell_grid.height_in_cell) + { + height += self.height_arr[i]; + } + height += self.table_attr.cellspacing as f64 + * (dot_cell_grid.height_in_cell as f64 - 1.); + + let mut width = 0f64; + for i in + dot_cell_grid.i..(dot_cell_grid.i + dot_cell_grid.width_in_cell) + { + width += self.width_arr[i]; + } + width += self.table_attr.cellspacing as f64 + * (dot_cell_grid.width_in_cell as f64 - 1.); + + Point::new(width, height) + } + + fn from_table(font_table: &FontTable) -> Self { + let table_hash_grid = TableHashGrid::from_table(font_table); + let width_in_cell = table_hash_grid.width(); + let height_in_cell = table_hash_grid.height(); + let mut grid = vec![None; width_in_cell * height_in_cell]; + for (idx, (_td_attr, dot_cell)) in + table_hash_grid.cells.iter().enumerate() + { + for i in 0..dot_cell.width_in_cell { + for j in 0..dot_cell.height_in_cell { + let x_cur = dot_cell.i + i; + let y_cur = dot_cell.j + j; + grid[(y_cur * width_in_cell) + x_cur] = Some(idx); + } + } + } + + Self { + cells: table_hash_grid.cells, + grid, + width_arr: vec![1.0; width_in_cell], + height_arr: vec![1.0; height_in_cell], + width_in_cell, + height_in_cell, + font_size: 0, + table_attr: font_table.table_attr.clone(), + table_tag: font_table.tag.clone(), + } + } + + fn get_cell(&self, i: usize, j: usize) -> Option<&DotCellGrid> { + if i < self.width_in_cell && j < self.height_in_cell { + let index = self.grid[(j * (self.width_in_cell)) + i]; + if let Some(i) = index { + return Some(&self.cells[i].1); + } + } + None + } + + fn get_cell_mut(&mut self, i: usize, j: usize) -> Option<&mut DotCellGrid> { + if i < self.width_in_cell && j < self.height_in_cell { + let index = self.grid[(j * (self.width_in_cell)) + i]; + if let Some(i) = index { + return Some(&mut self.cells[i].1); + } + } + None + } + + pub(crate) fn cellpadding(&self, d: &DotCellGrid) -> f64 { + let cellpadding = if let Some(td_cellpadding) = d.td_attr.cellpadding { + td_cellpadding + } else { + self.table_attr.cellpadding + } as f64; + + cellpadding + } + + pub(crate) fn cellborder(&self, d: &DotCellGrid) -> f64 { + let cellborder = if let Some(td_cellborder) = d.td_attr.border { + td_cellborder + } else if let Some(td_cellborder) = self.table_attr.cellborder { + td_cellborder + } else { + self.table_attr.border + } as f64; + + cellborder + } + + pub(crate) fn resize(&mut self, font_size: usize) { + // TODO: can check if font size is updated + for x in 0..self.width_in_cell { + let mut max_width = 0f64; + for y in 0..self.height_in_cell { + if let Some(cell) = self.get_cell_mut(x, y) { + match &mut cell.label_grid { + LabelOrImgGrid::Html(HtmlGrid::FontTable(x)) => { + x.resize(font_size); + } + _ => {} + } + } + } + for y in 0..self.height_in_cell { + if let Some(cell) = self.get_cell(x, y) { + let w = match &cell.label_grid { + LabelOrImgGrid::Html(html) => match html { + HtmlGrid::Text(text) => text.width(font_size), + HtmlGrid::FontTable(x) => x.width(), + }, + LabelOrImgGrid::Img(img) => img.width(), + }; + let cellpadding = self.cellpadding(cell); + let cellborder = self.cellborder(cell); + + let w = w + cellborder * 2.0 + cellpadding * 2.0; + + max_width = max_width.max(w / cell.width_in_cell as f64); + } + } + + self.width_arr[x] = max_width; + } + + for y in 0..self.height_in_cell { + let mut max_height = 0f64; + for x in 0..self.width_in_cell { + if let Some(cell) = self.get_cell(x, y) { + let h = match &cell.label_grid { + LabelOrImgGrid::Html(html) => match html { + HtmlGrid::Text(text) => text.height(font_size), + HtmlGrid::FontTable(x) => x.height(), + }, + LabelOrImgGrid::Img(img) => img.height(), + }; + let cellpadding = self.cellpadding(cell); + let cellborder = self.cellborder(cell); + + let h = h + cellborder * 2.0 + cellpadding * 2.0; + + max_height = max_height.max(h / cell.height_in_cell as f64); + } + } + self.height_arr[y] = max_height; + } + + // update the font size + self.font_size = font_size; + } + + pub(crate) fn build_style_attr(&self, style_attr: &StyleAttr) -> StyleAttr { + let mut style_attr = style_attr.clone(); + if let Some(ref color) = self.table_attr.bgcolor { + style_attr.fill_color = Some(color.clone()); + } + style_attr.valign = self.table_attr.valign.clone(); + style_attr.align = self.table_attr.align.clone(); + style_attr.line_width = self.table_attr.border as usize; + + match &self.table_tag { + TableTag::B => { + style_attr.font_weight = FontWeight::Bold; + } + TableTag::I => { + style_attr.font_style = FontStyle::Italic; + } + TableTag::U => { + style_attr.text_decoration.underline = true; + } + TableTag::O => { + style_attr.text_decoration.overline = true; + } + TableTag::Font(font) => { + if let Some(point_size) = font.point_size { + style_attr.font_size = point_size as usize; + } + if let Some(font_color) = font.color { + style_attr.font_color = font_color; + } + if let Some(ref font_name) = font.face { + style_attr.fontname = font_name.clone(); + } + } + TableTag::None => {} + } + + style_attr + } +} diff --git a/layout/src/gv/mod.rs b/layout/src/gv/mod.rs index 2dc4acb..8bc4a48 100644 --- a/layout/src/gv/mod.rs +++ b/layout/src/gv/mod.rs @@ -2,6 +2,7 @@ //! file format (parsing, building a compatible graph, etc.) pub mod builder; +pub(crate) mod html; pub mod parser; pub mod record; diff --git a/layout/src/gv/parser/ast.rs b/layout/src/gv/parser/ast.rs index 8caff9d..ff56737 100644 --- a/layout/src/gv/parser/ast.rs +++ b/layout/src/gv/parser/ast.rs @@ -15,21 +15,41 @@ impl NodeId { } } +#[derive(Debug, Clone)] +pub enum DotString { + String(String), + HtmlString(String), +} + +impl std::fmt::Display for DotString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::String(x) => write!(f, "{}", x), + Self::HtmlString(_) => write!(f, "htmlstring"), + } + } +} + // [a=b; c=d; ... ] #[derive(Debug, Clone)] pub struct AttributeList { - pub list: Vec<(String, String)>, + pub list: Vec<(String, DotString)>, } impl AttributeList { pub fn new() -> Self { Self { list: Vec::new() } } - pub fn add_attr(&mut self, from: &str, to: &str) { - self.list.push((from.to_string(), to.to_string())); + pub fn add_attr_str(&mut self, from: &str, to: &str) { + self.list + .push((from.to_string(), DotString::String(to.to_string()))); + } + + pub fn add_attr(&mut self, from: String, to: DotString) { + self.list.push((from, to)); } - pub fn iter(&self) -> std::slice::Iter<(String, String)> { + pub fn iter(&self) -> std::slice::Iter<(String, DotString)> { self.list.iter() } } diff --git a/layout/src/gv/parser/lexer.rs b/layout/src/gv/parser/lexer.rs index aa894de..a32fbe0 100644 --- a/layout/src/gv/parser/lexer.rs +++ b/layout/src/gv/parser/lexer.rs @@ -18,12 +18,14 @@ pub enum Token { ArrowLine, OpenBracket, CloseBracket, + HtmlStart, + HtmlEnd, OpenBrace, CloseBrace, Error(usize), } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Lexer { input: Vec<char>, pub pos: usize, @@ -184,6 +186,29 @@ impl Lexer { Token::Identifier(result) } + pub fn next_token_html(&mut self) -> Token { + let mut result = String::new(); + let mut bracket_balance = 1; + loop { + // Handle escaping + if self.ch == '\0' { + // Reached EOF without completing the string + return Token::Error(self.pos); + } + if self.ch == '<' { + bracket_balance += 1; + } else if self.ch == '>' { + if bracket_balance == 1 { + break; + } + bracket_balance -= 1; + } + result.push(self.ch); + self.read_char(); + } + Token::Identifier(result) + } + pub fn next_token(&mut self) -> Token { let tok: Token; while self.skip_comment() || self.skip_whitespace() {} @@ -215,6 +240,12 @@ impl Lexer { '"' => { tok = self.read_string(); } + '<' => { + tok = Token::HtmlStart; + } + '>' => { + tok = Token::HtmlEnd; + } '-' => { self.read_char(); match self.ch { diff --git a/layout/src/gv/parser/parser.rs b/layout/src/gv/parser/parser.rs index c61280f..a3dcd61 100644 --- a/layout/src/gv/parser/parser.rs +++ b/layout/src/gv/parser/parser.rs @@ -1,8 +1,9 @@ use super::ast; use super::lexer::Lexer; use super::lexer::Token; +use crate::gv::parser::ast::DotString; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DotParser { lexer: Lexer, tok: Token, @@ -40,6 +41,20 @@ impl DotParser { } } } + pub fn lex_html(&mut self) { + match self.tok { + Token::Error(_) => { + panic!("can't parse after error"); + } + Token::EOF => { + panic!("can't parse after EOF"); + } + _ => { + // Lex the next token. + self.tok = self.lexer.next_token_html(); + } + } + } // graph : [ strict ] (graph | digraph) [ ID ] '{' stmt_list '}' //subgraph : [ subgraph [ ID ] ] '{' stmt_list '}' @@ -141,8 +156,17 @@ impl DotParser { Result::Ok(ast::Stmt::Edge(es)) } Token::Equal => { - let es = self.parse_attribute_stmt(id0)?; - Result::Ok(ast::Stmt::Attribute(es)) + if id0.port.is_some() { + return to_error("Can't assign into a port"); + } + self.lex(); + let es = self.parse_attr_id()?; + let mut list = ast::AttributeList::new(); + list.add_attr(id0.name, es); + Result::Ok(ast::Stmt::Attribute(ast::AttrStmt::new( + ast::AttrStmtTarget::Graph, + list, + ))) } Token::Identifier(_) => { let ns = ast::NodeStmt::new(id0); @@ -232,14 +256,8 @@ impl DotParser { } else { return to_error("Expected '='"); } - - if let Token::Identifier(value) = self.tok.clone() { - lst.add_attr(&prop, &value); - // Consume the value name. - self.lex(); - } else { - return to_error("Expected value after assignment"); - } + let value = self.parse_attr_id()?; + lst.add_attr(prop, value); // Skip semicolon. if let Token::Semicolon = self.tok.clone() { @@ -257,36 +275,46 @@ impl DotParser { } Result::Ok(lst) } + // Parses a string that is inside a HTML tag. + pub fn parse_html_string(&mut self) -> Result<String, String> { + self.lex_html(); + if let Token::Identifier(s) = self.tok.clone() { + self.lex(); + Ok(s) + } else { + to_error("Expected a string") + } + } fn is_edge_token(&self) -> bool { matches!(self.tok, Token::ArrowLine | Token::ArrowRight) } // ID '=' ID - pub fn parse_attribute_stmt( - &mut self, - id: ast::NodeId, - ) -> Result<ast::AttrStmt, String> { - let mut lst = ast::AttributeList::new(); - - if id.port.is_some() { - return to_error("Can't assign into a port"); - } - - if let Token::Equal = self.tok.clone() { - self.lex(); - } else { - return to_error("Expected '='"); - } - - if let Token::Identifier(val) = self.tok.clone() { - lst.add_attr(&id.name, &val); + pub fn parse_attr_id(&mut self) -> Result<DotString, String> { + if let Token::HtmlStart = self.tok.clone() { + let html = self.parse_html_string()?; + if let Token::HtmlEnd = self.tok.clone() { + self.lex(); + } else { + return to_error( + format!("Expected '>', found {:?}", self.tok).as_str(), + ); + } + Result::Ok(DotString::HtmlString(html)) + } else if let Token::Identifier(value) = self.tok.clone() { + // Consume the value name. self.lex(); + Result::Ok(DotString::String(value)) } else { - return to_error("Expected identifier."); + to_error( + format!( + "Expected value after assignment, found {:?}", + self.tok + ) + .as_str(), + ) } - - Result::Ok(ast::AttrStmt::new(ast::AttrStmtTarget::Graph, lst)) } //edge_stmt : (node_id | subgraph) edgeRHS [ attr_list ] diff --git a/layout/src/gv/parser/printer.rs b/layout/src/gv/parser/printer.rs index 75007dd..601c713 100644 --- a/layout/src/gv/parser/printer.rs +++ b/layout/src/gv/parser/printer.rs @@ -1,6 +1,6 @@ //! A collection of methods for printing the AST. -use super::ast; +use super::ast::{self, DotString}; fn print_node_id(n: &ast::NodeId, indent: usize) { print!("{}", " ".repeat(indent)); @@ -21,7 +21,7 @@ fn print_arrow(k: &ast::ArrowKind, indent: usize) { } } } -fn print_attribute(a: &str, b: &str, indent: usize, i: usize) { +fn print_attribute(a: &str, b: &DotString, indent: usize, i: usize) { print!("{}", " ".repeat(indent)); println!("{})\"{}\" = \"{}\"", i, a, b); } diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index b818278..15afae0 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -3,7 +3,11 @@ use crate::core::base::Orientation; use crate::core::format::{ClipHandle, RenderBackend, Renderable, Visible}; use crate::core::geometry::*; -use crate::core::style::{LineStyleKind, StyleAttr}; +use crate::core::style::{Align, LineStyleKind, StyleAttr, VAlign}; +use crate::gv::html::{ + get_line_height, DotCellGrid, HtmlGrid, LabelOrImgGrid, Scale, TableGrid, + TextGrid, +}; use crate::std_shapes::shapes::*; /// Return the height and width of the record, depending on the geometry and @@ -39,6 +43,13 @@ fn get_record_size( const BOX_SHAPE_PADDING: f64 = 10.; const CIRCLE_SHAPE_PADDING: f64 = 20.; +fn get_size_for_content(content: &ShapeContent, font: usize) -> Point { + match content { + ShapeContent::String(s) => get_size_for_str(s, font), + ShapeContent::Html(html) => html.size(font), + } +} + /// Return the size of the shape. If \p make_xy_same is set then make the /// X and the Y of the shape the same. This will turn ellipses into circles and /// rectangles into boxes. The parameter \p dir specifies the direction of the @@ -50,29 +61,35 @@ pub fn get_shape_size( make_xy_same: bool, ) -> Point { let mut res = match s { - ShapeKind::Box(text) => { - pad_shape_scalar(get_size_for_str(text, font), BOX_SHAPE_PADDING) - } - ShapeKind::Circle(text) => { - pad_shape_scalar(get_size_for_str(text, font), CIRCLE_SHAPE_PADDING) - } - ShapeKind::DoubleCircle(text) => { - pad_shape_scalar(get_size_for_str(text, font), CIRCLE_SHAPE_PADDING) - } + ShapeKind::Box(text) => pad_shape_scalar( + get_size_for_content(text, font), + BOX_SHAPE_PADDING, + ), + ShapeKind::Circle(text) => pad_shape_scalar( + get_size_for_content(text, font), + CIRCLE_SHAPE_PADDING, + ), + ShapeKind::DoubleCircle(text) => pad_shape_scalar( + get_size_for_content(text, font), + CIRCLE_SHAPE_PADDING, + ), ShapeKind::Record(sr) => { pad_shape_scalar(get_record_size(sr, dir, font), BOX_SHAPE_PADDING) } ShapeKind::Connector(text) => { if let Option::Some(text) = text { pad_shape_scalar( - get_size_for_str(text, font), + get_size_for_content(text, font), BOX_SHAPE_PADDING, ) } else { Point::new(1., 1.) } } - _ => Point::new(1., 1.), + ShapeKind::None(text) => pad_shape_scalar( + get_size_for_content(text, font), + BOX_SHAPE_PADDING, + ), }; if make_xy_same { res = make_size_square(res); @@ -122,6 +139,70 @@ fn get_record_port_location( visit_record(rec, dir, loc, size, look, &mut visitor); (visitor.loc, visitor.size) } +struct Locator { + port_name: String, + loc: Point, + size: Point, +} +fn get_html_port_location( + html: &HtmlGrid, + loc: Point, + size: Point, + visitor: &mut Locator, +) -> (Point, Point) { + match html { + HtmlGrid::Text(_text) => {} + HtmlGrid::FontTable(table) => { + get_table_port_location(table, loc, size, visitor) + } + } + (visitor.loc, visitor.size) +} + +fn get_table_port_location( + table: &TableGrid, + loc: Point, + _size: Point, + visitor: &mut Locator, +) { + if let Some(ref port_name) = table.table_attr.port { + if port_name == &visitor.port_name { + visitor.loc = loc; + visitor.size = Point::new(table.width(), table.height()); + } + } + let table_width = table.width(); + let table_height = table.height(); + for (td_attr, c) in table.cells.iter() { + let cell_size = table.cell_size(c); + let cell_origin = table.cell_pos(c); + let cell_loc = Point::new( + visitor.loc.x + cell_origin.x + cell_size.x * 0.5 + - table_width * 0.5, + visitor.loc.y + cell_origin.y + cell_size.y * 0.5 + - table_height * 0.5, + ); + if let Option::Some(ref port_name) = td_attr.port { + if port_name == &visitor.port_name { + visitor.loc = cell_loc; + visitor.size = cell_size; + } + } + + get_cell_port_location(&c, cell_loc, cell_size, visitor); + } +} + +fn get_cell_port_location( + rec: &DotCellGrid, + loc: Point, + size: Point, + visitor: &mut Locator, +) { + if let LabelOrImgGrid::Html(html) = &rec.label_grid { + get_html_port_location(html, loc, size, visitor); + } +} fn render_record( rec: &RecordDef, @@ -187,6 +268,191 @@ fn render_record( ); } +fn render_html( + rec: &HtmlGrid, + loc: Point, + size: Point, + look: &StyleAttr, + canvas: &mut dyn RenderBackend, +) { + match rec { + HtmlGrid::Text(text) => { + render_text(text, loc, size, look, canvas); + } + HtmlGrid::FontTable(table) => { + render_font_table(table, loc, look, canvas); + } + } +} + +fn update_location( + loc: Point, + size: Point, + text: &str, + look: &StyleAttr, +) -> Point { + let mut loc = loc; + let text_size = get_size_for_str(text, look.font_size); + let displacement = size.sub(text_size); + match look.align { + Align::Left => { + loc.x -= displacement.x / 2.; + } + Align::Right => { + loc.x += displacement.x / 2.; + } + Align::Center => {} + } + match look.valign { + VAlign::Top => { + loc.y -= displacement.y / 2.; + } + VAlign::Bottom => { + loc.y += displacement.y / 2.; + } + VAlign::Middle => {} + } + loc +} + +fn render_text( + rec: &TextGrid, + loc: Point, + size: Point, + look: &StyleAttr, + canvas: &mut dyn RenderBackend, +) { + let loc_0_x = loc.x; + let mut loc = loc; + + loc.y -= rec.height(look.font_size) / 2.; + + for line in &rec.text_items { + let mut line_width = 0.; + for t in line { + line_width += t.width(look.font_size); + } + loc.x = loc_0_x - line_width / 2.; + for t in line { + let look_text = t.build_style_attr(look); + let text_size = get_size_for_str(&t.text, look_text.font_size); + loc.x += text_size.x / 2.; + let loc_text = update_location(loc, size, &t.text, &look_text); + canvas.draw_text(loc_text, t.text.as_str(), &look_text); + loc.x += text_size.x / 2.; + } + loc.y += get_line_height(line, look.font_size); + } +} + +fn render_font_table( + rec: &TableGrid, + loc: Point, + look: &StyleAttr, + canvas: &mut dyn RenderBackend, +) { + let look = rec.build_style_attr(look); + let table_grid_width = rec.width(); + let table_grid_height = rec.height(); + + // top left origin location of the table + let loc_0 = Point::new( + loc.x - table_grid_width / 2., + loc.y - table_grid_height / 2., + ); + canvas.draw_rect( + loc_0, + Point::new( + table_grid_width - rec.table_attr.border as f64, + table_grid_height - rec.table_attr.border as f64, + ), + &look, + Option::None, + Option::None, + ); + + for (td_attr, c) in rec.cells.iter() { + let cellpadding = rec.cellpadding(c); + let cellborder = rec.cellborder(c); + let cell_size = rec.cell_size(c); + let cell_origin = rec.cell_pos(c); + + // center of the cell + let cell_loc = Point::new( + loc_0.x + cell_origin.x + cell_size.x * 0.5, + loc_0.y + cell_origin.y + cell_size.y * 0.5, + ); + let look_cell = td_attr.build_style_attr(&look); + + let mut look_cell_border = look.clone(); + look_cell_border.line_width = cellborder as usize; + + canvas.draw_rect( + Point::new( + loc_0.x + cell_origin.x + cellborder * 0.5, + loc_0.y + cell_origin.y + cellborder * 0.5, + ), + cell_size.sub(Point::splat(cellborder)), + &look_cell_border, + Option::None, + Option::None, + ); + + // cell inside + let size = Point::new( + cell_size.x - cellborder * 2. - cellpadding * 2., + cell_size.y - cellborder * 2. - cellpadding * 2., + ); + render_cell(&c, cell_loc, size, &look_cell, canvas); + } +} + +fn render_cell( + rec: &DotCellGrid, + loc: Point, + size: Point, + look: &StyleAttr, + canvas: &mut dyn RenderBackend, +) { + match &rec.label_grid { + LabelOrImgGrid::Html(html) => { + render_html(html, loc, size, look, canvas); + } + LabelOrImgGrid::Img(img) => { + // TODO: Need to introduce setting to control file access as specificed by ofifical graphviz source + let image_size = img.size(); + let image_size = match &img.scale { + Scale::False => Point::new(image_size.x, image_size.y), + Scale::True => { + let x_scale = size.x / image_size.x; + let y_scale = size.y / image_size.y; + let scale = + if x_scale < y_scale { x_scale } else { y_scale }; + Point::new(image_size.x * scale, image_size.y * scale) + } + Scale::Width => { + let scale = size.x / image_size.x; + Point::new(image_size.x * scale, image_size.y) + } + Scale::Height => { + let scale = size.y / image_size.y; + Point::new(image_size.x, image_size.y * scale) + } + Scale::Both => size.clone(), + }; + canvas.draw_image( + Point::new( + loc.x - image_size.x / 2., + loc.y - image_size.y / 2., + ), + image_size, + &img.source, + None, + ); + } + } +} + pub trait RecordVisitor { fn handle_box(&mut self, loc: Point, size: Point); fn handle_text( @@ -272,6 +538,23 @@ fn visit_record( } } +fn draw_shape_content( + content: &ShapeContent, + loc: Point, + size: Point, + look: &StyleAttr, + canvas: &mut dyn RenderBackend, +) { + match content { + ShapeContent::String(text) => { + canvas.draw_text(loc, text.as_str(), look); + } + ShapeContent::Html(html) => { + render_html(html, loc, size, look, canvas); + } + } +} + impl Renderable for Element { fn render(&self, debug: bool, canvas: &mut dyn RenderBackend) { if debug { @@ -288,7 +571,15 @@ impl Renderable for Element { } match &self.shape { - ShapeKind::None => {} + ShapeKind::None(text) => { + draw_shape_content( + text, + self.pos.center(), + self.pos.size(false), + &self.look, + canvas, + ); + } ShapeKind::Record(rec) => { render_record( rec, @@ -307,7 +598,13 @@ impl Renderable for Element { self.properties.clone(), Option::None, ); - canvas.draw_text(self.pos.center(), text.as_str(), &self.look); + draw_shape_content( + text, + self.pos.center(), + self.pos.size(false), + &self.look, + canvas, + ); } ShapeKind::Circle(text) => { canvas.draw_circle( @@ -316,7 +613,14 @@ impl Renderable for Element { &self.look, self.properties.clone(), ); - canvas.draw_text(self.pos.center(), text.as_str(), &self.look); + // canvas.draw_text(self.pos.center(), text.as_str(), &self.look); + draw_shape_content( + text, + self.pos.center(), + self.pos.size(false), + &self.look, + canvas, + ); } ShapeKind::DoubleCircle(text) => { canvas.draw_circle( @@ -336,7 +640,13 @@ impl Renderable for Element { &outer_circle_style, None, ); - canvas.draw_text(self.pos.center(), text.as_str(), &self.look); + draw_shape_content( + text, + self.pos.center(), + self.pos.size(false), + &self.look, + canvas, + ); } ShapeKind::Connector(label) => { if debug { @@ -357,7 +667,14 @@ impl Renderable for Element { ); } if let Option::Some(label) = label { - canvas.draw_text(self.pos.middle(), label, &self.look); + // canvas.draw_text(self.pos.middle(), label, &self.look); + draw_shape_content( + label, + self.pos.middle(), + self.pos.size(false), + &self.look, + canvas, + ); } } } @@ -378,7 +695,35 @@ impl Renderable for Element { port: &Option<String>, ) -> (Point, Point) { match &self.shape { - ShapeKind::None => (Point::zero(), Point::zero()), + ShapeKind::None(x) => { + let loc = self.pos.center(); + let size = self.pos.size(false); + // get_connection_point_for_box(loc, size, from, force) + match x { + ShapeContent::String(_) => { + get_connection_point_for_box(loc, size, from, force) + } + ShapeContent::Html(html) => { + let mut loc = self.pos.center(); + let mut size = self.pos.size(false); + if let Option::Some(port_name) = port { + let r = get_html_port_location( + html, + loc, + size, + &mut Locator { + port_name: port_name.to_string(), + loc, + size, + }, + ); + loc = r.0; + size = r.1; + } + get_connection_point_for_box(loc, size, from, force) + } + } + } ShapeKind::Record(rec) => { let mut loc = self.pos.center(); let mut size = self.pos.size(false); @@ -398,20 +743,92 @@ impl Renderable for Element { get_connection_point_for_box(loc, size, from, force) } - ShapeKind::Box(_) => { + ShapeKind::Box(x) => { let loc = self.pos.center(); let size = self.pos.size(false); - get_connection_point_for_box(loc, size, from, force) + // get_connection_point_for_box(loc, size, from, force) + match x { + ShapeContent::String(_) => { + get_connection_point_for_box(loc, size, from, force) + } + ShapeContent::Html(html) => { + let mut loc = self.pos.center(); + let mut size = self.pos.size(false); + if let Option::Some(port_name) = port { + let r = get_html_port_location( + html, + loc, + size, + &mut Locator { + port_name: port_name.to_string(), + loc, + size, + }, + ); + loc = r.0; + size = r.1; + } + get_connection_point_for_box(loc, size, from, force) + } + } } - ShapeKind::Circle(_) => { + ShapeKind::Circle(x) => { let loc = self.pos.center(); let size = self.pos.size(false); - get_connection_point_for_circle(loc, size, from, force) + // get_connection_point_for_circle(loc, size, from, force) + match x { + ShapeContent::String(_) => { + get_connection_point_for_circle(loc, size, from, force) + } + ShapeContent::Html(html) => { + let mut loc = self.pos.center(); + let mut size = self.pos.size(false); + if let Option::Some(port_name) = port { + let r = get_html_port_location( + html, + loc, + size, + &mut Locator { + port_name: port_name.to_string(), + loc, + size, + }, + ); + loc = r.0; + size = r.1; + } + get_connection_point_for_circle(loc, size, from, force) + } + } } - ShapeKind::DoubleCircle(_) => { + ShapeKind::DoubleCircle(x) => { let loc = self.pos.center(); let size = self.pos.size(false); - get_connection_point_for_circle(loc, size, from, force) + // get_connection_point_for_circle(loc, size, from, force) + match x { + ShapeContent::String(_) => { + get_connection_point_for_circle(loc, size, from, force) + } + ShapeContent::Html(html) => { + let mut loc = self.pos.center(); + let mut size = self.pos.size(false); + if let Option::Some(port_name) = port { + let r = get_html_port_location( + html, + loc, + size, + &mut Locator { + port_name: port_name.to_string(), + loc, + size, + }, + ); + loc = r.0; + size = r.1; + } + get_connection_point_for_circle(loc, size, from, force) + } + } } _ => { unreachable!(); diff --git a/layout/src/std_shapes/shapes.rs b/layout/src/std_shapes/shapes.rs index f159344..5df8253 100644 --- a/layout/src/std_shapes/shapes.rs +++ b/layout/src/std_shapes/shapes.rs @@ -7,6 +7,7 @@ use crate::core::base::Orientation; use crate::core::format::Visible; use crate::core::geometry::{Point, Position}; use crate::core::style::{LineStyleKind, StyleAttr}; +use crate::gv::html::HtmlGrid; use crate::std_shapes::render::get_shape_size; const PADDING: f64 = 60.; @@ -18,6 +19,12 @@ pub enum LineEndKind { Arrow, } +#[derive(Debug, Clone)] +pub enum ShapeContent { + String(String), + Html(HtmlGrid), +} + #[derive(Debug, Clone)] pub enum RecordDef { // Label, port: @@ -37,23 +44,23 @@ impl RecordDef { #[derive(Debug, Clone)] pub enum ShapeKind { - None, - Box(String), - Circle(String), - DoubleCircle(String), + None(ShapeContent), + Box(ShapeContent), + Circle(ShapeContent), + DoubleCircle(ShapeContent), Record(RecordDef), - Connector(Option<String>), + Connector(Option<ShapeContent>), } impl ShapeKind { pub fn new_box(s: &str) -> Self { - ShapeKind::Box(s.to_string()) + ShapeKind::Box(ShapeContent::String(s.to_string())) } pub fn new_circle(s: &str) -> Self { - ShapeKind::Circle(s.to_string()) + ShapeKind::Circle(ShapeContent::String(s.to_string())) } pub fn new_double_circle(s: &str) -> Self { - ShapeKind::DoubleCircle(s.to_string()) + ShapeKind::DoubleCircle(ShapeContent::String(s.to_string())) } pub fn new_record(r: &RecordDef) -> Self { ShapeKind::Record(r.clone()) @@ -62,7 +69,7 @@ impl ShapeKind { if s.is_empty() { return ShapeKind::Connector(None); } - ShapeKind::Connector(Some(s.to_string())) + ShapeKind::Connector(Some(ShapeContent::String(s.to_string()))) } }