From fc6d1206b471a8bef37780d895ca3a1f6be2c9f8 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Wed, 14 May 2025 21:04:49 +0200 Subject: [PATCH 01/40] start html support --- inputs/html.dot | 13 + inputs/html2.dot | 19 + inputs/html3.dot | 24 + inputs/html_arrow.dot | 28 + inputs/html_complex.dot | 51 + inputs/html_font.dot | 20 + layout/src/backends/svg.rs | 5 +- layout/src/core/geometry.rs | 7 +- layout/src/core/style.rs | 89 ++ layout/src/gv/builder.rs | 85 +- layout/src/gv/html.rs | 1552 +++++++++++++++++++++++++++++++ layout/src/gv/mod.rs | 1 + layout/src/gv/parser/ast.rs | 29 +- layout/src/gv/parser/lexer.rs | 46 +- layout/src/gv/parser/parser.rs | 60 +- layout/src/gv/parser/printer.rs | 4 +- layout/src/std_shapes/render.rs | 323 ++++++- layout/src/std_shapes/shapes.rs | 2 + 18 files changed, 2320 insertions(+), 38 deletions(-) create mode 100644 inputs/html.dot create mode 100644 inputs/html2.dot create mode 100644 inputs/html3.dot create mode 100644 inputs/html_arrow.dot create mode 100644 inputs/html_complex.dot create mode 100644 inputs/html_font.dot create mode 100644 layout/src/gv/html.rs 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..9ed82d9 --- /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_font.dot b/inputs/html_font.dot new file mode 100644 index 0000000..ffe871a --- /dev/null +++ b/inputs/html_font.dot @@ -0,0 +1,20 @@ +digraph structs { + node [shape=plaintext]; + + struct1 [label=< + + + + + + + +
line 1line2line3line4 + + + + + +
Mixedfonts
+
>]; +} \ No newline at end of file diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index a357341..c7781f8 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -213,6 +213,8 @@ impl RenderBackend for SVGWriter { 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 mut content = String::new(); let cnt = 1 + text.lines().count(); @@ -226,10 +228,11 @@ impl RenderBackend for SVGWriter { self.grow_window(xy, Point::new(10., len as f64 * 10.)); let line = format!( "{}", + x=\"{}\" y=\"{}\" class=\"{}\" fill=\"{}\">{}", xy.x, xy.y - size_y / 2., font_class, + font_color.to_web_color(), &content ); diff --git a/layout/src/core/geometry.rs b/layout/src/core/geometry.rs index b39f94c..0473d22 100644 --- a/layout/src/core/geometry.rs +++ b/layout/src/core/geometry.rs @@ -280,11 +280,16 @@ pub fn pad_shape_scalar(size: Point, s: f64) -> Point { Point::new(size.x + s, size.y + s) } +#[inline] +fn get_width_of_line(label: &str) -> usize { + label.chars().count() * 2 +} + /// 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..745980e 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,35 @@ pub enum LineStyleKind { None, } +#[derive(Debug, Copy, Clone)] +pub(crate) enum FontStyle { + Normal, + Italic, + Oblique, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum FontWeight { + Normal, + Bold, + Bolder, + Lighter, + Number(usize), +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum TextDecoration { + None, + Underline, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum BaselineShift { + Sub, + Super, + Normal, +} + #[derive(Clone, Debug)] pub struct StyleAttr { pub line_color: Color, @@ -17,6 +89,14 @@ pub struct StyleAttr { pub fill_color: Option, pub rounded: usize, pub font_size: usize, + 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 +107,21 @@ impl StyleAttr { rounded: usize, font_size: usize, ) -> Self { + let font_color = Color::fast("black"); Self { line_color, line_width, fill_color, + font_color, rounded, font_size, + font_style: FontStyle::Normal, + font_weight: FontWeight::Normal, + text_decoration: TextDecoration::None, + baseline_shift: BaselineShift::Normal, + align: Align::Center, + valign: VAlign::Middle, + balign: BAlign::Center, } } diff --git a/layout/src/gv/builder.rs b/layout/src/gv/builder.rs index 95d7494..aa37047 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 { @@ -301,15 +315,26 @@ impl GraphBuilder { 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()); - 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 = val.clone(); + shape = ShapeKind::Circle(label.clone()); + } + DotString::HtmlString(val) => { + label = val.clone(); + shape = ShapeKind::Html(parse_html_string(val).unwrap()); + } + } } - 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); @@ -326,27 +351,35 @@ impl GraphBuilder { rounded_corder_value = 15; shape = record_builder(&label); } - _ => shape = ShapeKind::Circle(label), + _ => {} } } - 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 +388,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 +403,14 @@ impl GraphBuilder { // grow top down the records grow to the left. let dir = dir.flip(); + match &mut shape { + ShapeKind::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..f001e57 --- /dev/null +++ b/layout/src/gv/html.rs @@ -0,0 +1,1552 @@ +use crate::core::geometry::{get_size_for_str, pad_shape_scalar, Point}; +use std::collections::HashMap; + +use crate::std_shapes::render::BOX_SHAPE_PADDING; + +use crate::core::style::{Align, BAlign, FontStyle, StyleAttr, VAlign}; + +use crate::core::color::Color; + +/// Creates an error from the string \p str. +fn to_error(str: &str) -> Result { + Result::Err(str.to_string()) +} + +#[derive(Debug, Clone)] +pub enum Token { + Colon, + EOF, + Identifier(String), + OpeningTag(TagType), + ClosingTag(TagType), + TagEnd, + TagEndWithSlash, + TagAttr(String, String), + Error(usize), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum TagType { + Table, + Tr, + Td, + Font, + Br, + I, + Img, + B, + U, + O, + Sub, + Sup, + S, + Hr, + Vr, + Unrecognized, +} + +impl TagType { + pub 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, + } + } + pub fn is_single_tag(&self) -> bool { + match self { + TagType::Br | TagType::Hr | TagType::Vr | TagType::Img => true, + _ => false, + } + } +} + +#[derive(Debug, Clone)] +pub enum Scale { + False, + True, + Width, + Height, + Both, +} + +#[derive(Debug, Clone)] +pub struct Font { + pub(crate) color: Option, + pub(crate) face: Option, + pub(crate) point_size: Option, +} + +impl Font { + pub fn new() -> Self { + Self { + color: None, + face: None, + point_size: None, + } + } + + pub 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(), + _ => {} + } + } + + pub 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 + } +} + +#[derive(Debug, Clone)] +pub enum TextTag { + Font(Font), + I, + B, + U, + O, + Sub, + Sup, + S, +} + +impl TextTag { + pub 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), + } + } +} + +pub type Text = Vec; + +#[derive(Debug, Clone)] +pub struct TaggedText { + pub text_items: Text, + pub tag: TextTag, +} + +#[derive(Debug, Clone)] +pub enum TableTag { + None, + Font(Font), + I, + B, + U, + O, +} +impl TableTag { + pub 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 + } + } +} +#[derive(Debug, Clone)] +pub enum LabelOrImg { + Html(Html), + Img(Scale, String), +} +#[derive(Debug, Clone)] +pub struct DotCell { + pub label: LabelOrImg, + pub td_attr: TdAttr, +} + +#[derive(Debug, Clone)] +pub struct DotCellGrid { + pub(crate) i: usize, + pub(crate) j: usize, + pub(crate) width: usize, + pub(crate) height: usize, + pub label_grid: LabelOrImgGrid, + pub td_attr: TdAttr, +} + +impl DotCellGrid { + pub fn from_dot_cell( + i: usize, + j: usize, + width: usize, + height: usize, + dot_cell: &DotCell, + ) -> Self { + let label_grid = match &dot_cell.label { + LabelOrImg::Html(html) => { + LabelOrImgGrid::Html(HtmlGrid::from_html(html)) + } + LabelOrImg::Img(scale, img) => { + LabelOrImgGrid::Img(scale.clone(), img.clone()) + } + }; + Self { + i, + j, + width, + height, + label_grid, + td_attr: dot_cell.td_attr.clone(), + } + } +} + +#[derive(Debug, Clone)] +pub enum LabelOrImgGrid { + Html(HtmlGrid), + Img(Scale, String), +} + +#[derive(Debug, Clone)] +pub 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 +} + +impl TdAttr { + pub 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, + } + } + + pub 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 + } + + pub 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 fn update_style_attr(&self, style_attr: &mut StyleAttr) { + 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(); + } +} + +#[derive(Debug, Clone)] +pub enum ColumnFormat { + Star, + None, +} + +impl ColumnFormat { + pub fn from_str(s: &str) -> Self { + if s.starts_with('*') { + Self::Star + } else { + Self::None + } + } +} + +#[derive(Debug, Clone)] +pub enum RowFormat { + Star, + None, +} + +impl RowFormat { + pub fn from_str(s: &str) -> Self { + if s.starts_with('*') { + Self::Star + } else { + Self::None + } + } +} + +#[derive(Debug, Clone)] +pub struct Sides { + left: bool, + right: bool, + top: bool, + bottom: bool, +} + +impl Sides { + pub fn from_str(s: &str) -> Self { + let mut sides = Sides { + 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 + } +} + +#[derive(Debug, Clone)] +pub 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 +} + +impl TableAttr { + pub 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, + } + } + pub 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 + } + + pub 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)), + _ => {} + } + } + pub fn update_style_attr(&self, style_attr: &mut StyleAttr) { + if let Some(ref color) = self.bgcolor { + style_attr.fill_color = Some(color.clone()); + } + style_attr.valign = self.valign.clone(); + style_attr.align = self.align.clone(); + } +} +#[derive(Debug, Clone)] +pub struct FontTable { + pub rows: Vec<(Row, Option
)>, + pub tag: TableTag, + pub table_attr: TableAttr, +} +#[derive(Debug, Clone)] +pub struct Vr {} + +#[derive(Debug, Clone)] +pub struct Hr {} + +#[derive(Debug, Clone)] +pub struct Row { + pub cells: Vec<(DotCell, Option)>, +} + +#[derive(Debug, Clone)] +pub enum TextItem { + TaggedText(TaggedText), + Br(Align), + PlainText(String), +} + +#[derive(Debug, Clone)] +pub enum Html { + Text(Text), + FontTable(FontTable), +} + +impl Html { + // fn new_text() -> Self { + + // } +} + +#[derive(Debug, Clone)] +pub enum HtmlGrid { + Text(Text), + FontTable(TableGrid), +} + +impl HtmlGrid { + pub fn size(&self, font_size: usize) -> Point { + match self { + HtmlGrid::Text(text) => { + let mut size = Point::new(0.0, 0.0); + for item in text.iter() { + match item { + TextItem::TaggedText(tagged_text) => { + let text_size = + get_tagged_text_size(tagged_text, font_size); + size.x += text_size.x; + size.y = size.y.max(text_size.y); + } + TextItem::Br(_) => { + size.x = 0.0; + size.y += font_size as f64; + } + TextItem::PlainText(text) => { + let text_size = get_size_for_str(text, font_size); + size.x += text_size.x; + size.y = size.y.max(text_size.y); + } + } + } + pad_shape_scalar(size, BOX_SHAPE_PADDING) + } + HtmlGrid::FontTable(table_grid) => table_grid.size(font_size), + } + } + pub fn from_html(html: &Html) -> Self { + match html { + Html::Text(text) => HtmlGrid::Text(text.clone()), + Html::FontTable(table) => { + HtmlGrid::FontTable(TableGrid::from_table(table)) + } + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum HtmlMode { + Html, + // HtmlText, + HtmlTag, +} + +// fn escape_new + +#[derive(Debug, Clone)] +struct HtmlParser { + input: Vec, + pos: usize, + tok: Token, + mode: HtmlMode, + pub ch: char, +} + +impl HtmlParser { + pub fn next_token(&mut self) -> Token { + match self.mode { + HtmlMode::Html => self.read_html(), + // HtmlMode::HtmlText => self.read_html_text(), + HtmlMode::HtmlTag => self.read_tag_inside(), + } + } + pub fn lex(&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.next_token(); + } + } + } + pub fn skip_whitespace(&mut self) -> bool { + let mut changed = false; + while self.ch.is_ascii_whitespace() { + self.read_char(); + changed = true; + } + changed + } + pub fn has_next(&self) -> bool { + self.pos < self.input.len() + } + pub fn read_char(&mut self) { + if !self.has_next() { + self.ch = '\0'; + } else { + self.ch = self.input[self.pos]; + self.pos += 1; + } + } + + pub 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 + } + pub 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) + } + pub 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)) + } + } + pub 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) + } + pub 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); + } + // self.read_char(); + 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 > + pub 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()?)) + } + } + + pub 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) + } + + pub 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) + } + + pub 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(), + ) + } + }) + } + + pub fn parse_table(&mut self) -> Result { + let mut string_before = false; + if let Token::Identifier(_) = self.tok.clone() { + // Consume the text. + self.lex(); + string_before = true; + } + 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 string_before { + return to_error( + "Cannot inlcuding string before table tag", + ); + } + (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, true)?; + } + let table_attr = TableAttr::from_attr_list(table_attr2); + + Ok(FontTable { + rows, + tag: TableTag::from_tag(table_tag1), + table_attr, + }) + } + + pub 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) + } + pub 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(), + ); + } + // TODO: consume the 1st cell so that it gurannets the grammar property that splitter can appear in better + // cells + // The same for splitting of row + 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)?; + Ok(Row { cells }) + } + + pub 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(), ); } - // TODO: consume the 1st cell so that it gurannets the grammar property that splitter can appear in better - // cells - // The same for splitting of row + 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)); + 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 = 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), + }) + } + + pub 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)) + } + + pub fn parse_tag_end( + &mut self, + tag: &TagType, + pass_identifier: bool, + ) -> Result<(), String> { + if let Token::ClosingTag(x) = self.tok.clone() { + // self.lexer.mode = super::lexer::HtmlMode::HtmlText; + 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(()) + } +} + +pub 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)) +} + +#[derive(Debug, Clone)] +struct TableGridInner { + pub(crate) cells: Vec<(TdAttr, DotCellGrid)>, + pub(crate) occupation: HashMap<(usize, usize), usize>, // x, y, cell index +} + +impl TableGridInner { + pub fn width(&self) -> usize { + self.occupation.keys().map(|(x, _)| *x).max().unwrap_or(0) + 1 + } + pub fn height(&self) -> usize { + self.occupation.keys().map(|(_, y)| *y).max().unwrap_or(0) + 1 + } + pub 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!(); + } + } + pub 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)) + } + + pub 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.pretty_print(); + 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; + } +} + +#[derive(Debug, Clone)] +pub struct TableGrid { + pub(crate) cells: Vec<(TdAttr, DotCellGrid)>, + pub(crate) grid: Vec>, + pub(crate) width_arr: Vec, // width in svg units + pub(crate) 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, +} + +impl TableGrid { + pub fn width(&self) -> f64 { + self.width_arr.iter().sum::() + + (self.table_attr.cellspacing as usize * (self.width_in_cell + 1)) + as f64 + + self.table_attr.border as f64 * 2. + } + pub fn height(&self) -> f64 { + self.height_arr.iter().sum::() + + (self.table_attr.cellspacing as usize * (self.height_in_cell + 1)) + as f64 + + self.table_attr.border as f64 * 2. + } + pub 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 fn cell_pos(&self, d: &DotCellGrid) -> Point { + let idx = d.i; + let x = self.width_arr.iter().take(idx).sum::() + + (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::() + + (self.table_attr.cellspacing as usize * (idx + 1)) as f64 + + self.table_attr.border as f64 / 2.0; + + Point::new(x, y) + } + pub 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) { + height += self.height_arr[i]; + } + height += self.table_attr.cellspacing as f64 + * (dot_cell_grid.height as f64 - 1.); + + let mut width = 0f64; + for i in dot_cell_grid.i..(dot_cell_grid.i + dot_cell_grid.width) { + width += self.width_arr[i]; + } + width += self.table_attr.cellspacing as f64 + * (dot_cell_grid.width as f64 - 1.); + + Point::new(width, height) + } + + pub fn from_table(font_table: &FontTable) -> Self { + let table_grid_inner = TableGridInner::from_table(font_table); + let width_in_cell = table_grid_inner.width(); + let height_in_cell = table_grid_inner.height(); + let mut grid = vec![None; width_in_cell * height_in_cell]; + for (idx, (_td_attr, dot_cell)) in + table_grid_inner.cells.iter().enumerate() + { + for i in 0..dot_cell.width { + for j in 0..dot_cell.height { + 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_grid_inner.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(), + } + } + + pub 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 + } + + pub 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 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) => { + let mut size = Point::zero(); + for text_item in text { + let item_size = get_text_item_size( + text_item, font_size, + ); + size = size.add(item_size); + } + size.x + } + HtmlGrid::FontTable(x) => x.width(), + }, + _ => 0.0, + }; + 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 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) => { + let mut size = Point::zero(); + for text_item in text { + let item_size = get_text_item_size( + text_item, font_size, + ); + size = size.add(item_size); + } + size.y as f64 + } + HtmlGrid::FontTable(x) => x.height(), + }, + _ => 0.0, + }; + 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 as f64); + } + } + self.height_arr[y] = max_height; + } + + // update the font size + self.font_size = font_size; + } +} + +pub(crate) fn get_text_item_size(item: &TextItem, font_size: usize) -> Point { + match item { + TextItem::Br(_) => Point::new(1.0, 1.0), + TextItem::PlainText(text) => { + let size = get_size_for_str(text, font_size); + pad_shape_scalar(size, BOX_SHAPE_PADDING) + } + TextItem::TaggedText(tagged_text) => { + get_tagged_text_size(tagged_text, font_size) + } + } +} + +fn get_tagged_text_size(tagged_text: &TaggedText, font_size: usize) -> Point { + let mut size = Point::zero(); + for text_item in &tagged_text.text_items { + let item_size = get_text_item_size(text_item, font_size); + size = size.add(item_size); + } + size +} 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..4aa8401 100644 --- a/layout/src/gv/parser/ast.rs +++ b/layout/src/gv/parser/ast.rs @@ -15,21 +15,42 @@ 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_html(&mut self, from: &str, to: &str) { + self.list + .push((from.to_string(), DotString::HtmlString(to.to_string()))); } - 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..a21c1c8 100644 --- a/layout/src/gv/parser/lexer.rs +++ b/layout/src/gv/parser/lexer.rs @@ -18,16 +18,20 @@ pub enum Token { ArrowLine, OpenBracket, CloseBracket, + HtmlStart, + HtmlEnd, OpenBrace, CloseBrace, Error(usize), } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Lexer { input: Vec, pub pos: usize, pub ch: char, + // pub mode: LexerMode, + // pub html_depth: usize, } impl Lexer { @@ -41,6 +45,8 @@ impl Lexer { input, pos: 0, ch: '\0', + // mode: LexerMode::Normal, + // html_depth: 0, }; l.read_char(); l @@ -140,6 +146,15 @@ impl Lexer { result.push(self.ch); self.read_char(); } + // exception for POINT-SIZE + // if result == "POINT" && self.ch == '-' { + // result.push(self.ch); + // self.read_char(); + // while self.ch.is_ascii_alphanumeric() || self.ch == '_' { + // result.push(self.ch); + // self.read_char(); + // } + // } result } @@ -184,6 +199,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 +253,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..a89af31 100644 --- a/layout/src/gv/parser/parser.rs +++ b/layout/src/gv/parser/parser.rs @@ -2,7 +2,7 @@ use super::ast; use super::lexer::Lexer; use super::lexer::Token; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct DotParser { lexer: Lexer, tok: Token, @@ -40,6 +40,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 '}' @@ -233,12 +247,38 @@ impl DotParser { return to_error("Expected '='"); } - if let Token::Identifier(value) = self.tok.clone() { - lst.add_attr(&prop, &value); + if let Token::HtmlStart = self.tok.clone() { + if prop == "label" { + // self.lexer.mode = super::lexer::LexerMode::Html; + println!("ch before html string is {:?}", self.lexer.ch); + // self.lex(); + println!("ch before html string is {:?}", self.lexer.ch); + let html = self.parse_html_string()?; + println!("html is {:?}", html); + lst.add_attr_html(&prop, &html); + println!("html completed"); + // self.lexer.mode = super::lexer::LexerMode::Normal; + if let Token::HtmlEnd = self.tok.clone() { + self.lex(); + } else { + return to_error( + format!("Expected '>', found {:?}", self.tok) + .as_str(), + ); + } + } + } else if let Token::Identifier(value) = self.tok.clone() { + lst.add_attr_str(&prop, &value); // Consume the value name. self.lex(); } else { - return to_error("Expected value after assignment"); + return to_error( + format!( + "Expected value after assignment, found {:?}", + self.tok + ) + .as_str(), + ); } // Skip semicolon. @@ -257,6 +297,16 @@ impl DotParser { } Result::Ok(lst) } + // Parses a string that is inside a HTML tag. + pub fn parse_html_string(&mut self) -> Result { + 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) @@ -280,7 +330,7 @@ impl DotParser { } if let Token::Identifier(val) = self.tok.clone() { - lst.add_attr(&id.name, &val); + lst.add_attr_str(&id.name, &val); self.lex(); } else { return to_error("Expected identifier."); 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..5edb2b5 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -3,7 +3,13 @@ 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, BaselineShift, FontStyle, FontWeight, LineStyleKind, StyleAttr, + TextDecoration, VAlign, +}; +use crate::gv::html::{ + DotCellGrid, HtmlGrid, LabelOrImgGrid, TableGrid, Text, TextItem, TextTag, +}; use crate::std_shapes::shapes::*; /// Return the height and width of the record, depending on the geometry and @@ -36,7 +42,7 @@ fn get_record_size( } } -const BOX_SHAPE_PADDING: f64 = 10.; +pub(crate) const BOX_SHAPE_PADDING: f64 = 10.; const CIRCLE_SHAPE_PADDING: f64 = 20.; /// Return the size of the shape. If \p make_xy_same is set then make the @@ -72,7 +78,8 @@ pub fn get_shape_size( Point::new(1., 1.) } } - _ => Point::new(1., 1.), + ShapeKind::Html(html_grid) => html_grid.size(font), + ShapeKind::None => Point::new(1., 1.), }; if make_xy_same { res = make_size_square(res); @@ -122,6 +129,71 @@ 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, + // look: &StyleAttr, + 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 +259,227 @@ fn render_record( ); } +fn render_html( + rec: &HtmlGrid, + _dir: Orientation, + loc: Point, + size: Point, + look: &StyleAttr, + canvas: &mut dyn RenderBackend, +) { + match rec { + HtmlGrid::Text(text) => { + render_text( + text, + loc, + size, + look, + canvas, + Option::None, + Option::None, + ); + } + HtmlGrid::FontTable(table) => { + render_font_table( + table, + loc, + look, + canvas, + Option::None, + Option::None, + ); + } + } +} + +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: &Text, + loc: Point, + size: Point, + look: &StyleAttr, + canvas: &mut dyn RenderBackend, + clip_handle: Option, + _clip: Option, +) { + let mut loc = loc; + for item in rec { + match item { + TextItem::Br(_) => { + loc.y += look.font_size as f64; + } + TextItem::PlainText(text) => { + let mut look = look.clone(); + let loc2 = update_location(loc, size, text, &look); + canvas.draw_text(loc2, text.as_str(), &look); + } + TextItem::TaggedText(tagged_text) => { + let mut look = look.clone(); + match &tagged_text.tag { + TextTag::B => { + look.font_weight = FontWeight::Bold; + } + TextTag::I => { + look.font_style = FontStyle::Italic; + } + TextTag::U => { + look.text_decoration = TextDecoration::Underline; + } + TextTag::S => { + look.text_decoration = TextDecoration::None; + } + TextTag::Sub => { + look.baseline_shift = BaselineShift::Sub; + } + TextTag::Font(font) => { + // look.font_size = font.point_size; + if let Some(point_size) = font.point_size { + look.font_size = point_size as usize; + } + if let Some(font_color) = font.color { + look.font_color = font_color; + } + if let Some(font_style) = font.face { + look.font_style = font_style; + } + } + TextTag::O => { + look.font_weight = FontWeight::Normal; + } + TextTag::Sup => { + look.baseline_shift = BaselineShift::Super; + } + } + render_text( + &tagged_text.text_items, + loc, + size, + &look, + canvas, + clip_handle, + Option::None, + ); + } + } + } +} + +fn render_font_table( + rec: &TableGrid, + loc: Point, + look: &StyleAttr, + canvas: &mut dyn RenderBackend, + clip_handle: Option, + _clip: Option, +) { + let mut look = look.clone(); + look.line_width = rec.table_attr.border as usize; + + rec.table_attr.update_style_attr(&mut look); + let table_grid_width = rec.width(); + let table_grid_height = rec.height(); + let loc0 = Point::new( + loc.x - table_grid_width / 2., + loc.y - table_grid_height / 2., + ); + canvas.draw_rect( + loc0, + Point::new( + table_grid_width - rec.table_attr.border as f64, + table_grid_height - rec.table_attr.border as f64, + ), + &look, + Option::None, + clip_handle, + ); + + for (td_attr, c) in rec.cells.iter() { + let cellpadding = rec.cellpadding(c); + let cellborder = rec.cellborder(c); + let mut look = look.clone(); + + td_attr.update_style_attr(&mut look); + + let cell_size = rec.cell_size(c); + let cell_origin = rec.cell_pos(c); + let cell_loc = Point::new( + loc0.x + cell_origin.x + cell_size.x * 0.5, + loc0.y + cell_origin.y + cell_size.y * 0.5, + ); + let mut look_border = look.clone(); + look_border.line_width = cellborder as usize; + + canvas.draw_rect( + Point::new( + loc0.x + cell_origin.x + look_border.line_width as f64 * 0.5, + loc0.y + cell_origin.y + look_border.line_width as f64 * 0.5, + ), + cell_size.sub(Point::splat(cellborder)), + &look_border, + Option::None, + clip_handle, + ); + render_cell( + &c, + cell_loc, + cell_size.sub(Point::splat(cellpadding * 2.0 + cellborder * 2.0)), + &look, + 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, + Orientation::LeftToRight, + loc, + size, + look, + canvas, + ); + } + LabelOrImgGrid::Img(_, _) => {} + } +} + pub trait RecordVisitor { fn handle_box(&mut self, loc: Point, size: Point); fn handle_text( @@ -299,6 +592,14 @@ impl Renderable for Element { canvas, ); } + ShapeKind::Html(rec) => render_html( + rec, + self.orientation, + self.pos.center(), + self.pos.size(false), + &self.look, + canvas, + ), ShapeKind::Box(text) => { canvas.draw_rect( self.pos.bbox(false).0, @@ -413,6 +714,22 @@ impl Renderable for Element { let size = self.pos.size(false); get_connection_point_for_circle(loc, size, from, force) } + ShapeKind::Html(html) => { + let mut loc = self.pos.center(); + let mut size = self.pos.size(false); + if let Some(port_name) = port { + let mut visitor = Locator { + port_name: port_name.to_string(), + loc, + size, + }; + let r = + get_html_port_location(html, loc, size, &mut visitor); + loc = r.0; + size = r.1; + } + get_connection_point_for_box(loc, size, from, force) + } _ => { unreachable!(); } diff --git a/layout/src/std_shapes/shapes.rs b/layout/src/std_shapes/shapes.rs index f159344..807762f 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.; @@ -43,6 +44,7 @@ pub enum ShapeKind { DoubleCircle(String), Record(RecordDef), Connector(Option), + Html(HtmlGrid), } impl ShapeKind { From 3b89ce457bbf3fa378adfa9ffe4ac562d8a008ab Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Wed, 14 May 2025 21:19:42 +0200 Subject: [PATCH 02/40] Fix type error --- layout/src/gv/html.rs | 2 +- layout/src/gv/parser/parser.rs | 6 --- layout/src/std_shapes/render.rs | 3 -- output.svg | 76 +++++++++++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 output.svg diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index f001e57..3b84574 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -87,7 +87,7 @@ pub enum Scale { #[derive(Debug, Clone)] pub struct Font { pub(crate) color: Option, - pub(crate) face: Option, + pub(crate) face: Option, pub(crate) point_size: Option, } diff --git a/layout/src/gv/parser/parser.rs b/layout/src/gv/parser/parser.rs index a89af31..fe5efd9 100644 --- a/layout/src/gv/parser/parser.rs +++ b/layout/src/gv/parser/parser.rs @@ -249,14 +249,8 @@ impl DotParser { if let Token::HtmlStart = self.tok.clone() { if prop == "label" { - // self.lexer.mode = super::lexer::LexerMode::Html; - println!("ch before html string is {:?}", self.lexer.ch); - // self.lex(); - println!("ch before html string is {:?}", self.lexer.ch); let html = self.parse_html_string()?; - println!("html is {:?}", html); lst.add_attr_html(&prop, &html); - println!("html completed"); // self.lexer.mode = super::lexer::LexerMode::Normal; if let Token::HtmlEnd = self.tok.clone() { self.lex(); diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 5edb2b5..6b7b31f 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -368,9 +368,6 @@ fn render_text( if let Some(font_color) = font.color { look.font_color = font_color; } - if let Some(font_style) = font.face { - look.font_style = font_style; - } } TextTag::O => { look.font_weight = FontWeight::Normal; diff --git a/output.svg b/output.svg new file mode 100644 index 0000000..9afaa38 --- /dev/null +++ b/output.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + +line 1 + + + + +line2 + + + + +line3 + + + + +line4 + + + + + + + + + + + + + + +Mixed + + + + +fonts \ No newline at end of file From d00f992b6822fea9b4a637797052cd6e1e2b1ca4 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Wed, 14 May 2025 23:02:20 +0200 Subject: [PATCH 03/40] add fontname support --- inputs/html_font.dot | 2 +- layout/src/backends/svg.rs | 7 +-- layout/src/core/style.rs | 3 ++ layout/src/gv/html.rs | 1 - layout/src/std_shapes/render.rs | 3 ++ output.svg | 76 --------------------------------- 6 files changed, 11 insertions(+), 81 deletions(-) delete mode 100644 output.svg diff --git a/inputs/html_font.dot b/inputs/html_font.dot index ffe871a..5d4ba54 100644 --- a/inputs/html_font.dot +++ b/inputs/html_font.dot @@ -10,7 +10,7 @@ digraph structs { - +
MixedMixed fonts
diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index c7781f8..b9884f5 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -212,9 +212,9 @@ 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 mut content = String::new(); let cnt = 1 + text.lines().count(); @@ -228,10 +228,11 @@ impl RenderBackend for SVGWriter { self.grow_window(xy, Point::new(10., len as f64 * 10.)); let line = format!( "{}", + x=\"{}\" y=\"{}\" font-size=\"{}\" font-family=\"{}\" fill=\"{}\">{}", xy.x, xy.y - size_y / 2., - font_class, + font_size, + font_family, font_color.to_web_color(), &content ); diff --git a/layout/src/core/style.rs b/layout/src/core/style.rs index 745980e..79c7d04 100644 --- a/layout/src/core/style.rs +++ b/layout/src/core/style.rs @@ -89,6 +89,7 @@ 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, @@ -108,6 +109,7 @@ impl StyleAttr { font_size: usize, ) -> Self { let font_color = Color::fast("black"); + let fontname = String::from("Times,serif"); Self { line_color, line_width, @@ -115,6 +117,7 @@ impl StyleAttr { font_color, rounded, font_size, + fontname, font_style: FontStyle::Normal, font_weight: FontWeight::Normal, text_decoration: TextDecoration::None, diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 3b84574..df67d17 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1261,7 +1261,6 @@ impl TableGridInner { } // ending part - table_grid.pretty_print(); table_grid } diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 6b7b31f..062b3d9 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -368,6 +368,9 @@ fn render_text( if let Some(font_color) = font.color { look.font_color = font_color; } + if let Some(ref font_name) = font.face { + look.fontname = font_name.clone(); + } } TextTag::O => { look.font_weight = FontWeight::Normal; diff --git a/output.svg b/output.svg deleted file mode 100644 index 9afaa38..0000000 --- a/output.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -line 1 - - - - -line2 - - - - -line3 - - - - -line4 - - - - - - - - - - - - - - -Mixed - - - - -fonts \ No newline at end of file From 25fa935947e32f252ecfc63179e8555b1de2754a Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 00:53:27 +0200 Subject: [PATCH 04/40] add support for text declaration and font style --- inputs/html_font.dot | 6 +++--- layout/src/backends/svg.rs | 37 +++++++++++++++++++++++++++++++-- layout/src/core/style.rs | 20 +++++++++++++----- layout/src/std_shapes/render.rs | 6 +++--- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/inputs/html_font.dot b/inputs/html_font.dot index 5d4ba54..a9856e3 100644 --- a/inputs/html_font.dot +++ b/inputs/html_font.dot @@ -4,9 +4,9 @@ digraph structs { struct1 [label=< - - - + + + diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index 12cc268..678eb5d 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -5,7 +5,6 @@ use crate::core::format::{ClipHandle, RenderBackend}; use crate::core::geometry::Point; use crate::core::style::{StyleAttr, TextDecoration}; use std::collections::HashMap; -use std::str::FromStr; static SVG_HEADER: &str = r#""#; @@ -253,17 +252,22 @@ impl RenderBackend for SVGWriter { crate::core::style::FontStyle::Italic => "font-style=\"italic\"", crate::core::style::FontStyle::Normal => "", }; + let font_weight_text = match look.font_weight { + crate::core::style::FontWeight::Bold => "font-weight=\"bold\"", + crate::core::style::FontWeight::Normal => "", + }; let text_decoration_str = svg_text_decoration_str(&look.text_decoration); let line = format!( "{}", + x=\"{}\" y=\"{}\" font-size=\"{}\" font-family=\"{}\" {} {} {} fill=\"{}\">{}", xy.x, xy.y - size_y / 2., font_size, font_family, font_style_text, text_decoration_str, + font_weight_text, font_color.to_web_color(), &content ); diff --git a/layout/src/core/style.rs b/layout/src/core/style.rs index cf2e162..bdf3b63 100644 --- a/layout/src/core/style.rs +++ b/layout/src/core/style.rs @@ -63,9 +63,6 @@ pub(crate) enum FontStyle { pub(crate) enum FontWeight { Normal, Bold, - Bolder, - Lighter, - Number(usize), } #[derive(Debug, Copy, Clone)] From 26b88be7a91c034c72cffcb45bb5ec18189ecb73 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 04:07:04 +0200 Subject: [PATCH 07/40] Better layout for cells with multi text items --- inputs/html_font.dot | 2 +- layout/src/backends/svg.rs | 6 ++-- layout/src/gv/html.rs | 50 +++++++++++++++++++++++---------- layout/src/std_shapes/render.rs | 20 ++++++------- 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/inputs/html_font.dot b/inputs/html_font.dot index 37fcbdc..ce6b602 100644 --- a/inputs/html_font.dot +++ b/inputs/html_font.dot @@ -5,7 +5,7 @@ digraph structs { - + + +
line 1line2line3line4line2line3dfsline4 diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index b9884f5..7a4078e 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -3,8 +3,11 @@ use crate::core::color::Color; use crate::core::format::{ClipHandle, RenderBackend}; use crate::core::geometry::Point; -use crate::core::style::StyleAttr; +use crate::core::style::{ + StyleAttr, TextDecoration, +}; use std::collections::HashMap; +use std::str::FromStr; static SVG_HEADER: &str = r#""#; @@ -84,6 +87,29 @@ 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. @@ -226,13 +252,20 @@ 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::Normal => "", + }; + let text_decoration_str = svg_text_decoration_str(&look.text_decoration); let line = format!( "{}", + x=\"{}\" y=\"{}\" font-size=\"{}\" font-family=\"{}\" {} {} fill=\"{}\">{}", xy.x, xy.y - size_y / 2., font_size, font_family, + font_style_text, + text_decoration_str, font_color.to_web_color(), &content ); diff --git a/layout/src/core/style.rs b/layout/src/core/style.rs index 79c7d04..cf2e162 100644 --- a/layout/src/core/style.rs +++ b/layout/src/core/style.rs @@ -57,7 +57,6 @@ pub enum LineStyleKind { pub(crate) enum FontStyle { Normal, Italic, - Oblique, } #[derive(Debug, Copy, Clone)] @@ -70,9 +69,20 @@ pub(crate) enum FontWeight { } #[derive(Debug, Copy, Clone)] -pub(crate) enum TextDecoration { - None, - Underline, +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)] @@ -120,7 +130,7 @@ impl StyleAttr { fontname, font_style: FontStyle::Normal, font_weight: FontWeight::Normal, - text_decoration: TextDecoration::None, + text_decoration: TextDecoration::new(), baseline_shift: BaselineShift::Normal, align: Align::Center, valign: VAlign::Middle, diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 062b3d9..d3afefb 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -352,10 +352,10 @@ fn render_text( look.font_style = FontStyle::Italic; } TextTag::U => { - look.text_decoration = TextDecoration::Underline; + look.text_decoration.underline = true; } TextTag::S => { - look.text_decoration = TextDecoration::None; + look.text_decoration.line_through = true; } TextTag::Sub => { look.baseline_shift = BaselineShift::Sub; @@ -373,7 +373,7 @@ fn render_text( } } TextTag::O => { - look.font_weight = FontWeight::Normal; + look.text_decoration.overline = true; } TextTag::Sup => { look.baseline_shift = BaselineShift::Super; From 3fc31aee0160c1fc2faeacb66bdee0b34a78bb66 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 00:54:46 +0200 Subject: [PATCH 05/40] rustfmt --- layout/src/backends/svg.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index 7a4078e..12cc268 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -3,9 +3,7 @@ use crate::core::color::Color; use crate::core::format::{ClipHandle, RenderBackend}; use crate::core::geometry::Point; -use crate::core::style::{ - StyleAttr, TextDecoration, -}; +use crate::core::style::{StyleAttr, TextDecoration}; use std::collections::HashMap; use std::str::FromStr; @@ -88,10 +86,9 @@ impl Drop for SVGWriter { } #[inline] -fn svg_text_decoration_str( - text_decoration: &TextDecoration, -) -> String { - if !text_decoration.underline && !text_decoration.overline +fn svg_text_decoration_str(text_decoration: &TextDecoration) -> String { + if !text_decoration.underline + && !text_decoration.overline && !text_decoration.line_through { return String::new(); @@ -256,7 +253,8 @@ impl RenderBackend for SVGWriter { crate::core::style::FontStyle::Italic => "font-style=\"italic\"", crate::core::style::FontStyle::Normal => "", }; - let text_decoration_str = svg_text_decoration_str(&look.text_decoration); + let text_decoration_str = + svg_text_decoration_str(&look.text_decoration); let line = format!( "{}", From 53ff59744e976b89e3ea6522cc8b1fa52174b3a8 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 01:00:55 +0200 Subject: [PATCH 06/40] Add support for fontweight --- inputs/html_font.dot | 2 +- layout/src/backends/svg.rs | 8 ++++++-- layout/src/core/style.rs | 3 --- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/inputs/html_font.dot b/inputs/html_font.dot index a9856e3..37fcbdc 100644 --- a/inputs/html_font.dot +++ b/inputs/html_font.dot @@ -11,7 +11,7 @@ digraph structs {
- +
Mixedfontsfonts
line 1 line2line3line3 dfsline4 diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index 678eb5d..ca1209a 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -242,9 +242,9 @@ impl RenderBackend for SVGWriter { 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(&format!("", xy.x)); content.push_str(&escape_string(line)); - content.push_str(""); + // content.push_str(""); } self.grow_window(xy, Point::new(10., len as f64 * 10.)); @@ -262,7 +262,7 @@ impl RenderBackend for SVGWriter { "{}", xy.x, - xy.y - size_y / 2., + xy.y, font_size, font_family, font_style_text, diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index df67d17..2f0a261 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::std_shapes::render::BOX_SHAPE_PADDING; -use crate::core::style::{Align, BAlign, FontStyle, StyleAttr, VAlign}; +use crate::core::style::{Align, BAlign, StyleAttr, VAlign}; use crate::core::color::Color; @@ -210,8 +210,8 @@ pub struct DotCell { pub struct DotCellGrid { pub(crate) i: usize, pub(crate) j: usize, - pub(crate) width: usize, - pub(crate) height: usize, + pub(crate) width_in_cell: usize, + pub(crate) height_in_cell: usize, pub label_grid: LabelOrImgGrid, pub td_attr: TdAttr, } @@ -220,8 +220,8 @@ impl DotCellGrid { pub fn from_dot_cell( i: usize, j: usize, - width: usize, - height: usize, + width_in_cell: usize, + height_in_cell: usize, dot_cell: &DotCell, ) -> Self { let label_grid = match &dot_cell.label { @@ -235,12 +235,19 @@ impl DotCellGrid { Self { i, j, - width, - height, + width_in_cell, + height_in_cell, label_grid, td_attr: dot_cell.td_attr.clone(), } } + + pub fn size(&self, font_size: usize) -> Point { + match &self.label_grid { + LabelOrImgGrid::Html(html) => html.size(font_size), + LabelOrImgGrid::Img(_, _) => Point::new(0.0, 0.0), + } + } } #[derive(Debug, Clone)] @@ -1357,18 +1364,22 @@ impl TableGrid { } pub 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) { + 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 as f64 - 1.); + * (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) { + 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 as f64 - 1.); + * (dot_cell_grid.width_in_cell as f64 - 1.); Point::new(width, height) } @@ -1381,8 +1392,8 @@ impl TableGrid { for (idx, (_td_attr, dot_cell)) in table_grid_inner.cells.iter().enumerate() { - for i in 0..dot_cell.width { - for j in 0..dot_cell.height { + 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); @@ -1485,7 +1496,7 @@ impl TableGrid { let w = w + cellborder * 2.0 + cellpadding * 2.0; - max_width = max_width.max(w / cell.width as f64); + max_width = max_width.max(w / cell.width_in_cell as f64); } } @@ -1517,7 +1528,7 @@ impl TableGrid { let h = h + cellborder * 2.0 + cellpadding * 2.0; - max_height = max_height.max(h / cell.height as f64); + max_height = max_height.max(h / cell.height_in_cell as f64); } } self.height_arr[y] = max_height; @@ -1543,6 +1554,15 @@ pub(crate) fn get_text_item_size(item: &TextItem, font_size: usize) -> Point { fn get_tagged_text_size(tagged_text: &TaggedText, font_size: usize) -> Point { let mut size = Point::zero(); + let mut font_size = font_size; + match &tagged_text.tag { + TextTag::Font(font_tag) => { + if let Some(size_str) = font_tag.point_size { + font_size = size_str as usize; + } + } + _ => {} + } for text_item in &tagged_text.text_items { let item_size = get_text_item_size(text_item, font_size); size = size.add(item_size); diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index d3afefb..7f9b5e2 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -332,15 +332,19 @@ fn render_text( _clip: Option, ) { let mut loc = loc; + loc.x -= size.x / 2.; for item in rec { match item { TextItem::Br(_) => { loc.y += look.font_size as f64; } TextItem::PlainText(text) => { - let mut look = look.clone(); + let size_str = get_size_for_str(text, look.font_size); + let look = look.clone(); + loc.x += size_str.x / 2.; let loc2 = update_location(loc, size, text, &look); canvas.draw_text(loc2, text.as_str(), &look); + loc.x += size_str.x / 2.; } TextItem::TaggedText(tagged_text) => { let mut look = look.clone(); @@ -379,9 +383,11 @@ fn render_text( look.baseline_shift = BaselineShift::Super; } } + let mut loc3 = loc; + loc3.x += size.x / 2.; render_text( &tagged_text.text_items, - loc, + loc3, size, &look, canvas, @@ -423,7 +429,7 @@ fn render_font_table( ); for (td_attr, c) in rec.cells.iter() { - let cellpadding = rec.cellpadding(c); + // let cellpadding = rec.cellpadding(c); let cellborder = rec.cellborder(c); let mut look = look.clone(); @@ -448,13 +454,7 @@ fn render_font_table( Option::None, clip_handle, ); - render_cell( - &c, - cell_loc, - cell_size.sub(Point::splat(cellpadding * 2.0 + cellborder * 2.0)), - &look, - canvas, - ); + render_cell(&c, cell_loc, c.size(look.font_size), &look, canvas); } } From d7f96bf1cbff901311adde211a3c3adce5797d05 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 04:23:09 +0200 Subject: [PATCH 08/40] Remove stale comments --- layout/src/backends/svg.rs | 4 ---- layout/src/std_shapes/render.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index ca1209a..4fcaf60 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -239,12 +239,8 @@ impl RenderBackend for SVGWriter { let font_family = look.fontname.clone(); 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)); - // content.push_str(""); } self.grow_window(xy, Point::new(10., len as f64 * 10.)); diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 7f9b5e2..2781ed5 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -5,7 +5,7 @@ use crate::core::format::{ClipHandle, RenderBackend, Renderable, Visible}; use crate::core::geometry::*; use crate::core::style::{ Align, BaselineShift, FontStyle, FontWeight, LineStyleKind, StyleAttr, - TextDecoration, VAlign, + VAlign, }; use crate::gv::html::{ DotCellGrid, HtmlGrid, LabelOrImgGrid, TableGrid, Text, TextItem, TextTag, From 33b93404e01b60955ad7d81375ea1c835176a26f Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 04:36:58 +0200 Subject: [PATCH 09/40] Revert tspan change --- layout/src/backends/svg.rs | 6 +++++- layout/src/gv/parser/lexer.rs | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index 4fcaf60..678eb5d 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -239,8 +239,12 @@ impl RenderBackend for SVGWriter { let font_family = look.fontname.clone(); 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)); + content.push_str(""); } self.grow_window(xy, Point::new(10., len as f64 * 10.)); @@ -258,7 +262,7 @@ impl RenderBackend for SVGWriter { "{}", xy.x, - xy.y, + xy.y - size_y / 2., font_size, font_family, font_style_text, diff --git a/layout/src/gv/parser/lexer.rs b/layout/src/gv/parser/lexer.rs index a21c1c8..8097f7e 100644 --- a/layout/src/gv/parser/lexer.rs +++ b/layout/src/gv/parser/lexer.rs @@ -178,6 +178,7 @@ impl Lexer { pub fn read_string(&mut self) -> Token { let mut result = String::new(); + println!("Reading string"); self.read_char(); while self.ch != '"' { // Handle escaping @@ -196,6 +197,7 @@ impl Lexer { result.push(self.ch); self.read_char(); } + println!("Finished reading string: {}", result); Token::Identifier(result) } From bfd0051a9c6d621e035e2dd95c4c4842e10078ff Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 11:23:24 +0200 Subject: [PATCH 10/40] Font support for table --- inputs/html_font_table.dot | 27 +++++++ layout/src/gv/html.rs | 8 +- layout/src/std_shapes/render.rs | 32 +++++++- output.svg | 137 ++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 inputs/html_font_table.dot create mode 100644 output.svg diff --git a/inputs/html_font_table.dot b/inputs/html_font_table.dot new file mode 100644 index 0000000..703ae6b --- /dev/null +++ b/inputs/html_font_table.dot @@ -0,0 +1,27 @@ +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/layout/src/gv/html.rs b/layout/src/gv/html.rs index 2f0a261..4f9b10f 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -969,7 +969,11 @@ impl HtmlParser { } if string_before { return to_error( - "Cannot inlcuding string before table tag", + format!( + "cannot have string before table tag: {:?}", + tag1 + ) + .as_str(), ); } (Some((tag1, table_attr1)), tag_attr) @@ -1324,6 +1328,7 @@ pub struct TableGrid { height_in_cell: usize, // height of the table in cells font_size: usize, pub(crate) table_attr: TableAttr, + pub(crate) table_tag: TableTag, } impl TableGrid { @@ -1410,6 +1415,7 @@ impl TableGrid { height_in_cell, font_size: 0, table_attr: font_table.table_attr.clone(), + table_tag: font_table.tag.clone(), } } diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 2781ed5..1e1ab21 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -8,7 +8,8 @@ use crate::core::style::{ VAlign, }; use crate::gv::html::{ - DotCellGrid, HtmlGrid, LabelOrImgGrid, TableGrid, Text, TextItem, TextTag, + DotCellGrid, HtmlGrid, LabelOrImgGrid, TableGrid, TableTag, Text, TextItem, + TextTag, }; use crate::std_shapes::shapes::*; @@ -331,12 +332,14 @@ fn render_text( clip_handle: Option, _clip: Option, ) { + let loc0_x = loc.x; let mut loc = loc; loc.x -= size.x / 2.; for item in rec { match item { TextItem::Br(_) => { loc.y += look.font_size as f64; + loc.x = loc0_x - size.x / 2.; } TextItem::PlainText(text) => { let size_str = get_size_for_str(text, look.font_size); @@ -410,6 +413,33 @@ fn render_font_table( let mut look = look.clone(); look.line_width = rec.table_attr.border as usize; + match &rec.table_tag { + TableTag::B => { + look.font_weight = FontWeight::Bold; + } + TableTag::I => { + look.font_style = FontStyle::Italic; + } + TableTag::U => { + look.text_decoration.underline = true; + } + TableTag::O => { + look.text_decoration.overline = true; + } + TableTag::Font(font) => { + if let Some(point_size) = font.point_size { + look.font_size = point_size as usize; + } + if let Some(font_color) = font.color { + look.font_color = font_color; + } + if let Some(ref font_name) = font.face { + look.fontname = font_name.clone(); + } + } + TableTag::None => {} + } + rec.table_attr.update_style_attr(&mut look); let table_grid_width = rec.width(); let table_grid_height = rec.height(); diff --git a/output.svg b/output.svg new file mode 100644 index 0000000..7f17997 --- /dev/null +++ b/output.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + +left + + + + +mid dle + + + + +right + + + + + + + + + +one + + + + +two + + + + + + + + + +helloworld + + + + +b + + + + +g + + + + +h + + + + +c + + + + +d + + + + +e + + + + +f + + + + + + + + + + \ No newline at end of file From c11f08bcc9e8ab1ad148ec3ed3359f34b19386a8 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 11:23:45 +0200 Subject: [PATCH 11/40] remove output file --- output.svg | 137 ----------------------------------------------------- 1 file changed, 137 deletions(-) delete mode 100644 output.svg diff --git a/output.svg b/output.svg deleted file mode 100644 index 7f17997..0000000 --- a/output.svg +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -left - - - - -mid dle - - - - -right - - - - - - - - - -one - - - - -two - - - - - - - - - -helloworld - - - - -b - - - - -g - - - - -h - - - - -c - - - - -d - - - - -e - - - - -f - - - - - - - - - - \ No newline at end of file From f7c419a5bf51ac1b7e13925ea9ea1d36ad02bf72 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 11:35:38 +0200 Subject: [PATCH 12/40] Handle error for using text after tag wrapping table --- layout/src/gv/html.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 4f9b10f..c10d383 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1010,7 +1010,14 @@ impl HtmlParser { self.parse_tag_end(&TagType::Table, true)?; if let Some(ref tag) = table_tag1 { - self.parse_tag_end(&tag.0, true)?; + self.parse_tag_end(&tag.0, false)?; + + if let Token::Identifier(x) = self.tok.clone() { + return to_error( + format!("No space after font tag wrapping table: {:?}", x) + .as_str(), + ); + } } let table_attr = TableAttr::from_attr_list(table_attr2); @@ -1160,7 +1167,6 @@ impl HtmlParser { pass_identifier: bool, ) -> Result<(), String> { if let Token::ClosingTag(x) = self.tok.clone() { - // self.lexer.mode = super::lexer::HtmlMode::HtmlText; if x == *tag { self.lex(); } else { From 5935984f836ce7f90a2e13612fa71db3b6eb7d85 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 12:10:04 +0200 Subject: [PATCH 13/40] Count spaces for handling table wrapper text, see the note: https://graphviz.org/doc/info/shapes.html --- inputs/html_font_table.dot | 3 ++- layout/src/gv/html.rs | 25 +++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/inputs/html_font_table.dot b/inputs/html_font_table.dot index 703ae6b..222a3f0 100644 --- a/inputs/html_font_table.dot +++ b/inputs/html_font_table.dot @@ -8,7 +8,8 @@ digraph structs {
onetwo
>]; - struct3 [label=< + struct3 [label=< +
diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index c10d383..8576002 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -666,7 +666,14 @@ pub enum HtmlMode { HtmlTag, } -// fn escape_new +fn is_text_table_wrapper_legal(text: &str) -> bool { + for line in text.lines() { + if !line.is_empty() { + return false; + } + } + true +} #[derive(Debug, Clone)] struct HtmlParser { @@ -949,10 +956,11 @@ impl HtmlParser { pub fn parse_table(&mut self) -> Result { let mut string_before = false; - if let Token::Identifier(_) = self.tok.clone() { + if let Token::Identifier(x) = self.tok.clone() { // Consume the text. self.lex(); - string_before = true; + // string_before = true; + string_before = !is_text_table_wrapper_legal(x.as_str()); } let (tag1, table_attr1) = self.parse_tag_start(true)?; let (table_tag1, table_attr2) = match tag1 { @@ -1013,10 +1021,15 @@ impl HtmlParser { self.parse_tag_end(&tag.0, false)?; if let Token::Identifier(x) = self.tok.clone() { - return to_error( - format!("No space after font tag wrapping table: {:?}", x) + if !is_text_table_wrapper_legal(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); From 267a921eb0873c2319b06fb17a2bc1bf38161eb4 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 12:17:12 +0200 Subject: [PATCH 14/40] Remove some stale comments, bettter name for detecting invalid string before table wrapper --- layout/src/gv/html.rs | 18 ++++++++---------- layout/src/std_shapes/render.rs | 4 +--- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 8576002..3ce7118 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -666,13 +666,13 @@ pub enum HtmlMode { HtmlTag, } -fn is_text_table_wrapper_legal(text: &str) -> bool { +fn is_text_table_wrapper_invalid(text: &str) -> bool { for line in text.lines() { if !line.is_empty() { - return false; + return true; } } - true + false } #[derive(Debug, Clone)] @@ -688,7 +688,6 @@ impl HtmlParser { pub fn next_token(&mut self) -> Token { match self.mode { HtmlMode::Html => self.read_html(), - // HtmlMode::HtmlText => self.read_html_text(), HtmlMode::HtmlTag => self.read_tag_inside(), } } @@ -837,7 +836,7 @@ impl HtmlParser { if self.ch != '"' { return Token::Error(self.pos); } - // self.read_char(); + let x = self.read_string(); if let Token::Identifier(s) = x { Token::TagAttr(attr_name.to_lowercase(), s.to_lowercase()) @@ -955,12 +954,11 @@ impl HtmlParser { } pub fn parse_table(&mut self) -> Result { - let mut string_before = false; + let mut invalid_string = false; if let Token::Identifier(x) = self.tok.clone() { // Consume the text. self.lex(); - // string_before = true; - string_before = !is_text_table_wrapper_legal(x.as_str()); + 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 { @@ -975,7 +973,7 @@ impl HtmlParser { format!("Expected
hello
world
b
, found {:?}", tag).as_str(), ); } - if string_before { + if invalid_string { return to_error( format!( "cannot have string before table tag: {:?}", @@ -1021,7 +1019,7 @@ impl HtmlParser { self.parse_tag_end(&tag.0, false)?; if let Token::Identifier(x) = self.tok.clone() { - if !is_text_table_wrapper_legal(x.as_str()) { + if is_text_table_wrapper_invalid(x.as_str()) { return to_error( format!( "No space after font tag wrapping table: {:?}", diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 1e1ab21..9e2e3be 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -139,7 +139,6 @@ fn get_html_port_location( html: &HtmlGrid, loc: Point, size: Point, - // look: &StyleAttr, visitor: &mut Locator, ) -> (Point, Point) { match html { @@ -368,7 +367,6 @@ fn render_text( look.baseline_shift = BaselineShift::Sub; } TextTag::Font(font) => { - // look.font_size = font.point_size; if let Some(point_size) = font.point_size { look.font_size = point_size as usize; } @@ -459,7 +457,7 @@ fn render_font_table( ); for (td_attr, c) in rec.cells.iter() { - // let cellpadding = rec.cellpadding(c); + let cellborder = rec.cellborder(c); let mut look = look.clone(); From 8ff779f07e6efcae36bd5d12164582d22b1ae879 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 12:21:32 +0200 Subject: [PATCH 15/40] rustfmt --- layout/src/gv/html.rs | 1 - layout/src/std_shapes/render.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 3ce7118..a987028 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -662,7 +662,6 @@ impl HtmlGrid { #[derive(Debug, Clone, PartialEq)] pub enum HtmlMode { Html, - // HtmlText, HtmlTag, } diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 9e2e3be..a37c2af 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -457,7 +457,6 @@ fn render_font_table( ); for (td_attr, c) in rec.cells.iter() { - let cellborder = rec.cellborder(c); let mut look = look.clone(); From 94a79b1aebc591e9e370d05af31c59400d047399 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 14:16:50 +0200 Subject: [PATCH 16/40] remove some stale comment and new token error print --- layout/src/gv/html.rs | 4 ++-- layout/src/gv/parser/lexer.rs | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index a987028..6aac4ce 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -692,8 +692,8 @@ impl HtmlParser { } pub fn lex(&mut self) { match self.tok { - Token::Error(_) => { - panic!("can't parse after error"); + Token::Error(pos) => { + panic!("can't parse after error at {}", pos); } Token::EOF => { panic!("can't parse after EOF"); diff --git a/layout/src/gv/parser/lexer.rs b/layout/src/gv/parser/lexer.rs index 8097f7e..7bb3428 100644 --- a/layout/src/gv/parser/lexer.rs +++ b/layout/src/gv/parser/lexer.rs @@ -30,8 +30,6 @@ pub struct Lexer { input: Vec, pub pos: usize, pub ch: char, - // pub mode: LexerMode, - // pub html_depth: usize, } impl Lexer { @@ -45,8 +43,6 @@ impl Lexer { input, pos: 0, ch: '\0', - // mode: LexerMode::Normal, - // html_depth: 0, }; l.read_char(); l From 60d8b5636edfe96223965cd35b56591c74a663c3 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 14:51:08 +0200 Subject: [PATCH 17/40] dont pad html text, not compatible with inner layout algo --- layout/src/core/geometry.rs | 2 +- layout/src/gv/html.rs | 11 ++---- output.svg | 75 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 output.svg diff --git a/layout/src/core/geometry.rs b/layout/src/core/geometry.rs index 0473d22..87b5f73 100644 --- a/layout/src/core/geometry.rs +++ b/layout/src/core/geometry.rs @@ -282,7 +282,7 @@ pub fn pad_shape_scalar(size: Point, s: f64) -> Point { #[inline] fn get_width_of_line(label: &str) -> usize { - label.chars().count() * 2 + label.chars().count() } /// Estimate the bounding box of some rendered text. diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 6aac4ce..a58342c 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1,8 +1,6 @@ -use crate::core::geometry::{get_size_for_str, pad_shape_scalar, Point}; +use crate::core::geometry::{get_size_for_str, Point}; use std::collections::HashMap; -use crate::std_shapes::render::BOX_SHAPE_PADDING; - use crate::core::style::{Align, BAlign, StyleAttr, VAlign}; use crate::core::color::Color; @@ -644,7 +642,7 @@ impl HtmlGrid { } } } - pad_shape_scalar(size, BOX_SHAPE_PADDING) + size } HtmlGrid::FontTable(table_grid) => table_grid.size(font_size), } @@ -1564,10 +1562,7 @@ impl TableGrid { pub(crate) fn get_text_item_size(item: &TextItem, font_size: usize) -> Point { match item { TextItem::Br(_) => Point::new(1.0, 1.0), - TextItem::PlainText(text) => { - let size = get_size_for_str(text, font_size); - pad_shape_scalar(size, BOX_SHAPE_PADDING) - } + TextItem::PlainText(text) => get_size_for_str(text, font_size), TextItem::TaggedText(tagged_text) => { get_tagged_text_size(tagged_text, font_size) } diff --git a/output.svg b/output.svg new file mode 100644 index 0000000..620f679 --- /dev/null +++ b/output.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + +line 1 + + + + +line2 + + + + +line3 + + + + +dfsline4 + + + + + + + + + + + + + + +Mixed + + + + +fonts \ No newline at end of file From f931d3daa8a2a682145e1457c096eba598ec5e97 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 14:51:37 +0200 Subject: [PATCH 18/40] remove output file --- output.svg | 75 ------------------------------------------------------ 1 file changed, 75 deletions(-) delete mode 100644 output.svg diff --git a/output.svg b/output.svg deleted file mode 100644 index 620f679..0000000 --- a/output.svg +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - -line 1 - - - - -line2 - - - - -line3 - - - - -dfsline4 - - - - - - - - - - - - - - -Mixed - - - - -fonts \ No newline at end of file From fc5de1428a5df465ce7766cb2d133968ee468217 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 15:43:15 +0200 Subject: [PATCH 19/40] more precise measurement of line width --- layout/src/core/geometry.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/layout/src/core/geometry.rs b/layout/src/core/geometry.rs index 87b5f73..94233f9 100644 --- a/layout/src/core/geometry.rs +++ b/layout/src/core/geometry.rs @@ -280,9 +280,25 @@ 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 { - label.chars().count() + 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. From 0b910db898633b32bb9c26fe5db83c6b2bec37d5 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 19:21:53 +0200 Subject: [PATCH 20/40] flatten the structure of text in html --- layout/src/gv/html.rs | 203 +++++++++++++++++++++----------- layout/src/std_shapes/render.rs | 108 +++++------------ 2 files changed, 162 insertions(+), 149 deletions(-) diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index a58342c..5def849 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1,7 +1,10 @@ use crate::core::geometry::{get_size_for_str, Point}; use std::collections::HashMap; -use crate::core::style::{Align, BAlign, StyleAttr, VAlign}; +use crate::core::style::{ + Align, BAlign, BaselineShift, FontWeight, StyleAttr, VAlign, + FontStyle, TextDecoration +}; use crate::core::color::Color; @@ -612,9 +615,119 @@ impl Html { // } } +#[derive(Debug, Clone)] +pub 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 PlainTextGrid { + pub(crate) text: String, + pub(crate) text_style: TextStyle, +} +#[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>, + pub(crate) br: Vec, +} + +impl TextGrid { + pub fn new() -> Self { + Self { + text_items: vec![], + br: vec![], + } + } + pub 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 = PlainTextGrid { + 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); + } + } + } + } + } + + pub 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 text_size = get_size_for_str(&item.text, font_size); + line_width += text_size.x; + } + if width < line_width { + width = line_width; + } + } + width + } + pub 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 + if let Some(last_line) = line.last() { + let text_size = get_size_for_str(&last_line.text, font_size); + height += text_size.y; + } + } + height + } +} + #[derive(Debug, Clone)] pub enum HtmlGrid { - Text(Text), + Text(TextGrid), FontTable(TableGrid), } @@ -622,34 +735,26 @@ impl HtmlGrid { pub fn size(&self, font_size: usize) -> Point { match self { HtmlGrid::Text(text) => { - let mut size = Point::new(0.0, 0.0); - for item in text.iter() { - match item { - TextItem::TaggedText(tagged_text) => { - let text_size = - get_tagged_text_size(tagged_text, font_size); - size.x += text_size.x; - size.y = size.y.max(text_size.y); - } - TextItem::Br(_) => { - size.x = 0.0; - size.y += font_size as f64; - } - TextItem::PlainText(text) => { - let text_size = get_size_for_str(text, font_size); - size.x += text_size.x; - size.y = size.y.max(text_size.y); - } - } - } - size + Point::new(text.width(font_size), text.height(font_size)) } HtmlGrid::FontTable(table_grid) => table_grid.size(font_size), } } pub fn from_html(html: &Html) -> Self { match html { - Html::Text(text) => HtmlGrid::Text(text.clone()), + // Html::Text(text) => HtmlGrid::Text(text.clone()), + Html::Text(text) => { + let mut text_grid = TextGrid::new(); + let text_style = TextStyle { + font: Font::new(), + font_style: FontStyle::Normal, + font_weight: FontWeight::Normal, + 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)) } @@ -1497,16 +1602,7 @@ impl TableGrid { if let Some(cell) = self.get_cell(x, y) { let w = match &cell.label_grid { LabelOrImgGrid::Html(html) => match html { - HtmlGrid::Text(text) => { - let mut size = Point::zero(); - for text_item in text { - let item_size = get_text_item_size( - text_item, font_size, - ); - size = size.add(item_size); - } - size.x - } + HtmlGrid::Text(text) => text.width(font_size), HtmlGrid::FontTable(x) => x.width(), }, _ => 0.0, @@ -1529,16 +1625,7 @@ impl TableGrid { if let Some(cell) = self.get_cell(x, y) { let h = match &cell.label_grid { LabelOrImgGrid::Html(html) => match html { - HtmlGrid::Text(text) => { - let mut size = Point::zero(); - for text_item in text { - let item_size = get_text_item_size( - text_item, font_size, - ); - size = size.add(item_size); - } - size.y as f64 - } + HtmlGrid::Text(text) => text.height(font_size), HtmlGrid::FontTable(x) => x.height(), }, _ => 0.0, @@ -1558,31 +1645,3 @@ impl TableGrid { self.font_size = font_size; } } - -pub(crate) fn get_text_item_size(item: &TextItem, font_size: usize) -> Point { - match item { - TextItem::Br(_) => Point::new(1.0, 1.0), - TextItem::PlainText(text) => get_size_for_str(text, font_size), - TextItem::TaggedText(tagged_text) => { - get_tagged_text_size(tagged_text, font_size) - } - } -} - -fn get_tagged_text_size(tagged_text: &TaggedText, font_size: usize) -> Point { - let mut size = Point::zero(); - let mut font_size = font_size; - match &tagged_text.tag { - TextTag::Font(font_tag) => { - if let Some(size_str) = font_tag.point_size { - font_size = size_str as usize; - } - } - _ => {} - } - for text_item in &tagged_text.text_items { - let item_size = get_text_item_size(text_item, font_size); - size = size.add(item_size); - } - size -} diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index a37c2af..24ca07f 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -4,12 +4,10 @@ use crate::core::base::Orientation; use crate::core::format::{ClipHandle, RenderBackend, Renderable, Visible}; use crate::core::geometry::*; use crate::core::style::{ - Align, BaselineShift, FontStyle, FontWeight, LineStyleKind, StyleAttr, - VAlign, + Align, FontStyle, FontWeight, LineStyleKind, StyleAttr, VAlign, }; use crate::gv::html::{ - DotCellGrid, HtmlGrid, LabelOrImgGrid, TableGrid, TableTag, Text, TextItem, - TextTag, + DotCellGrid, HtmlGrid, LabelOrImgGrid, TableGrid, TableTag, TextGrid, }; use crate::std_shapes::shapes::*; @@ -269,15 +267,7 @@ fn render_html( ) { match rec { HtmlGrid::Text(text) => { - render_text( - text, - loc, - size, - look, - canvas, - Option::None, - Option::None, - ); + render_text(text, loc, size, look, canvas); } HtmlGrid::FontTable(table) => { render_font_table( @@ -323,80 +313,44 @@ fn update_location( } fn render_text( - rec: &Text, + rec: &TextGrid, loc: Point, size: Point, look: &StyleAttr, canvas: &mut dyn RenderBackend, - clip_handle: Option, - _clip: Option, ) { let loc0_x = loc.x; let mut loc = loc; - loc.x -= size.x / 2.; - for item in rec { - match item { - TextItem::Br(_) => { - loc.y += look.font_size as f64; - loc.x = loc0_x - size.x / 2.; + + for line in &rec.text_items { + let mut line_width = 0.; + for t in line { + let size_str = get_size_for_str(t.text.as_str(), look.font_size); + line_width += size_str.x; + } + loc.x -= line_width / 2.; + for t in line { + loc.x += get_size_for_str(t.text.as_str(), look.font_size).x / 2.; + let mut look = look.clone(); + if let Some(x) = t.text_style.font.point_size { + look.font_size = x as usize; } - TextItem::PlainText(text) => { - let size_str = get_size_for_str(text, look.font_size); - let look = look.clone(); - loc.x += size_str.x / 2.; - let loc2 = update_location(loc, size, text, &look); - canvas.draw_text(loc2, text.as_str(), &look); - loc.x += size_str.x / 2.; + if let Some(x) = t.text_style.font.color { + look.font_color = x; } - TextItem::TaggedText(tagged_text) => { - let mut look = look.clone(); - match &tagged_text.tag { - TextTag::B => { - look.font_weight = FontWeight::Bold; - } - TextTag::I => { - look.font_style = FontStyle::Italic; - } - TextTag::U => { - look.text_decoration.underline = true; - } - TextTag::S => { - look.text_decoration.line_through = true; - } - TextTag::Sub => { - look.baseline_shift = BaselineShift::Sub; - } - TextTag::Font(font) => { - if let Some(point_size) = font.point_size { - look.font_size = point_size as usize; - } - if let Some(font_color) = font.color { - look.font_color = font_color; - } - if let Some(ref font_name) = font.face { - look.fontname = font_name.clone(); - } - } - TextTag::O => { - look.text_decoration.overline = true; - } - TextTag::Sup => { - look.baseline_shift = BaselineShift::Super; - } - } - let mut loc3 = loc; - loc3.x += size.x / 2.; - render_text( - &tagged_text.text_items, - loc3, - size, - &look, - canvas, - clip_handle, - Option::None, - ); + if let Some(ref x) = t.text_style.font.face { + look.fontname = x.clone(); } - } + look.font_style = t.text_style.font_style; + look.font_weight = t.text_style.font_weight; + look.text_decoration = t.text_style.text_decoration; + look.baseline_shift = t.text_style.baseline_shift; + let loc2 = update_location(loc, size, t.text.as_str(), &look); + canvas.draw_text(loc2, t.text.as_str(), &look); + loc.x += get_size_for_str(t.text.as_str(), look.font_size).x / 2.; + } + loc.y += look.font_size as f64; + loc.x = loc0_x; } } From 8b82624260703eeb2638e465a0549cdb9112d2f5 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 19:28:54 +0200 Subject: [PATCH 21/40] rustfmt --- layout/src/gv/html.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 5def849..a3d9850 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -2,8 +2,8 @@ use crate::core::geometry::{get_size_for_str, Point}; use std::collections::HashMap; use crate::core::style::{ - Align, BAlign, BaselineShift, FontWeight, StyleAttr, VAlign, - FontStyle, TextDecoration + Align, BAlign, BaselineShift, FontStyle, FontWeight, StyleAttr, + TextDecoration, VAlign, }; use crate::core::color::Color; From dfc21ee39da9625c25c2a07d7a16e773d4bcea3e Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 19:48:34 +0200 Subject: [PATCH 22/40] add support for baseline shift --- inputs/html_complex.dot | 2 +- layout/src/backends/svg.rs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/inputs/html_complex.dot b/inputs/html_complex.dot index 9ed82d9..a50e02c 100644 --- a/inputs/html_complex.dot +++ b/inputs/html_complex.dot @@ -4,7 +4,7 @@ digraph G { a [ label=<
- +
class
class
qualifier
> ] diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index 678eb5d..f511279 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -258,9 +258,22 @@ impl RenderBackend for SVGWriter { }; 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=\"middle\"" + } + }; let line = format!( - "{}", + baseline_shift_str, xy.x, xy.y - size_y / 2., font_size, From c942392ad463c606e9489cecb70790f391acc674 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Thu, 15 May 2025 20:18:19 +0200 Subject: [PATCH 23/40] Correct alignment within cell --- layout/src/std_shapes/render.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 24ca07f..d24efe8 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -411,6 +411,7 @@ fn render_font_table( ); for (td_attr, c) in rec.cells.iter() { + let cellpadding = rec.cellpadding(c); let cellborder = rec.cellborder(c); let mut look = look.clone(); @@ -435,7 +436,11 @@ fn render_font_table( Option::None, clip_handle, ); - render_cell(&c, cell_loc, c.size(look.font_size), &look, canvas); + 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, canvas); } } From 38febce9e04c32b0ce492d2e728cb9838e877f02 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Fri, 16 May 2025 17:21:06 +0200 Subject: [PATCH 24/40] rustfmt --- layout/src/std_shapes/render.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index d24efe8..f371faf 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -437,8 +437,8 @@ fn render_font_table( clip_handle, ); let size = Point::new( - cell_size.x - cellborder*2. - cellpadding * 2., - cell_size.y - cellborder*2. - cellpadding * 2., + cell_size.x - cellborder * 2. - cellpadding * 2., + cell_size.y - cellborder * 2. - cellpadding * 2., ); render_cell(&c, cell_loc, size, &look, canvas); } From 6f304441ec620aeb909db6fdad8cc613f23c2222 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Fri, 16 May 2025 17:40:01 +0200 Subject: [PATCH 25/40] remove stale comment and print --- layout/src/gv/parser/lexer.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/layout/src/gv/parser/lexer.rs b/layout/src/gv/parser/lexer.rs index 7bb3428..a32fbe0 100644 --- a/layout/src/gv/parser/lexer.rs +++ b/layout/src/gv/parser/lexer.rs @@ -142,15 +142,6 @@ impl Lexer { result.push(self.ch); self.read_char(); } - // exception for POINT-SIZE - // if result == "POINT" && self.ch == '-' { - // result.push(self.ch); - // self.read_char(); - // while self.ch.is_ascii_alphanumeric() || self.ch == '_' { - // result.push(self.ch); - // self.read_char(); - // } - // } result } @@ -174,7 +165,6 @@ impl Lexer { pub fn read_string(&mut self) -> Token { let mut result = String::new(); - println!("Reading string"); self.read_char(); while self.ch != '"' { // Handle escaping @@ -193,7 +183,6 @@ impl Lexer { result.push(self.ch); self.read_char(); } - println!("Finished reading string: {}", result); Token::Identifier(result) } From 0ef2585e0239164c9cbce303c398b8fe3f65d31c Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Fri, 16 May 2025 22:14:09 +0200 Subject: [PATCH 26/40] better visibility deafult for html --- layout/src/gv/html.rs | 217 +++++++++++++++----------------- layout/src/std_shapes/render.rs | 2 +- 2 files changed, 100 insertions(+), 119 deletions(-) diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index a3d9850..9d6a953 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -14,7 +14,7 @@ fn to_error(str: &str) -> Result { } #[derive(Debug, Clone)] -pub enum Token { +enum Token { Colon, EOF, Identifier(String), @@ -27,7 +27,7 @@ pub enum Token { } #[derive(Debug, Clone, PartialEq)] -pub enum TagType { +enum TagType { Table, Tr, Td, @@ -47,7 +47,7 @@ pub enum TagType { } impl TagType { - pub fn from_str(tag: &str) -> Self { + fn from_str(tag: &str) -> Self { // use capital letter for all letters for patter matching match tag { "table" => TagType::Table, @@ -68,7 +68,7 @@ impl TagType { _ => TagType::Unrecognized, } } - pub fn is_single_tag(&self) -> bool { + fn is_single_tag(&self) -> bool { match self { TagType::Br | TagType::Hr | TagType::Vr | TagType::Img => true, _ => false, @@ -77,7 +77,7 @@ impl TagType { } #[derive(Debug, Clone)] -pub enum Scale { +pub(crate) enum Scale { False, True, Width, @@ -86,14 +86,14 @@ pub enum Scale { } #[derive(Debug, Clone)] -pub struct Font { +pub(crate) struct Font { pub(crate) color: Option, pub(crate) face: Option, pub(crate) point_size: Option, } impl Font { - pub fn new() -> Self { + fn new() -> Self { Self { color: None, face: None, @@ -101,7 +101,7 @@ impl Font { } } - pub fn set_attr(&mut self, attr: &str, value: &str) { + fn set_attr(&mut self, attr: &str, value: &str) { match attr { "color" => { self.color = { @@ -118,7 +118,7 @@ impl Font { } } - pub fn from_tag_attr_list(list: Vec<(String, String)>) -> Self { + 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); @@ -128,7 +128,7 @@ impl Font { } #[derive(Debug, Clone)] -pub enum TextTag { +enum TextTag { Font(Font), I, B, @@ -140,7 +140,7 @@ pub enum TextTag { } impl TextTag { - pub fn new(tag: &TagType, tag_attr_list: Vec<(String, String)>) -> Self { + 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); @@ -158,16 +158,16 @@ impl TextTag { } } -pub type Text = Vec; +type Text = Vec; #[derive(Debug, Clone)] -pub struct TaggedText { - pub text_items: Text, - pub tag: TextTag, +struct TaggedText { + text_items: Text, + tag: TextTag, } #[derive(Debug, Clone)] -pub enum TableTag { +pub(crate) enum TableTag { None, Font(Font), I, @@ -176,9 +176,7 @@ pub enum TableTag { O, } impl TableTag { - pub fn from_tag( - tag_pair: Option<(TagType, Vec<(String, String)>)>, - ) -> Self { + 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, @@ -197,28 +195,28 @@ impl TableTag { } } #[derive(Debug, Clone)] -pub enum LabelOrImg { +enum LabelOrImg { Html(Html), Img(Scale, String), } #[derive(Debug, Clone)] -pub struct DotCell { - pub label: LabelOrImg, - pub td_attr: TdAttr, +struct DotCell { + label: LabelOrImg, + td_attr: TdAttr, } #[derive(Debug, Clone)] -pub struct DotCellGrid { +pub(crate) struct DotCellGrid { pub(crate) i: usize, pub(crate) j: usize, pub(crate) width_in_cell: usize, pub(crate) height_in_cell: usize, - pub label_grid: LabelOrImgGrid, - pub td_attr: TdAttr, + pub(crate) label_grid: LabelOrImgGrid, + td_attr: TdAttr, } impl DotCellGrid { - pub fn from_dot_cell( + fn from_dot_cell( i: usize, j: usize, width_in_cell: usize, @@ -242,23 +240,16 @@ impl DotCellGrid { td_attr: dot_cell.td_attr.clone(), } } - - pub fn size(&self, font_size: usize) -> Point { - match &self.label_grid { - LabelOrImgGrid::Html(html) => html.size(font_size), - LabelOrImgGrid::Img(_, _) => Point::new(0.0, 0.0), - } - } } #[derive(Debug, Clone)] -pub enum LabelOrImgGrid { +pub(crate) enum LabelOrImgGrid { Html(HtmlGrid), Img(Scale, String), } #[derive(Debug, Clone)] -pub struct TdAttr { +pub(crate) struct TdAttr { // No inheritance on align, use the most recent value align: Align, // CENTER|LEFT|RIGHT balign: BAlign, @@ -296,7 +287,7 @@ pub struct TdAttr { } impl TdAttr { - pub fn new() -> Self { + fn new() -> Self { Self { align: Align::Center, balign: BAlign::Center, @@ -323,7 +314,7 @@ impl TdAttr { } } - pub fn from_tag_attr_list(list: Vec<(String, String)>) -> Self { + 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); @@ -331,7 +322,7 @@ impl TdAttr { attr } - pub fn set_attr(&mut self, attr: &str, value: &str) { + fn set_attr(&mut self, attr: &str, value: &str) { match attr { "align" => { self.align = match value { @@ -377,7 +368,7 @@ impl TdAttr { } } - pub fn update_style_attr(&self, style_attr: &mut StyleAttr) { + pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { if let Some(ref color) = self.bgcolor { style_attr.fill_color = Color::from_name(color); } @@ -388,13 +379,13 @@ impl TdAttr { } #[derive(Debug, Clone)] -pub enum ColumnFormat { +enum ColumnFormat { Star, None, } impl ColumnFormat { - pub fn from_str(s: &str) -> Self { + fn from_str(s: &str) -> Self { if s.starts_with('*') { Self::Star } else { @@ -404,13 +395,13 @@ impl ColumnFormat { } #[derive(Debug, Clone)] -pub enum RowFormat { +enum RowFormat { Star, None, } impl RowFormat { - pub fn from_str(s: &str) -> Self { + fn from_str(s: &str) -> Self { if s.starts_with('*') { Self::Star } else { @@ -420,7 +411,7 @@ impl RowFormat { } #[derive(Debug, Clone)] -pub struct Sides { +struct Sides { left: bool, right: bool, top: bool, @@ -428,7 +419,7 @@ pub struct Sides { } impl Sides { - pub fn from_str(s: &str) -> Self { + fn from_str(s: &str) -> Self { let mut sides = Sides { left: false, right: false, @@ -449,7 +440,7 @@ impl Sides { } #[derive(Debug, Clone)] -pub struct TableAttr { +pub(crate) struct TableAttr { // No inheritance on align, use the most recent value align: Align, // CENTER|LEFT|RIGHT valign: VAlign, // MIDDLE|BOTTOM|TOP @@ -481,7 +472,7 @@ pub struct TableAttr { } impl TableAttr { - pub fn new() -> Self { + fn new() -> Self { Self { align: Align::Center, bgcolor: None, @@ -507,7 +498,7 @@ impl TableAttr { width: None, } } - pub fn from_attr_list(list: Vec<(String, String)>) -> Self { + 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); @@ -515,7 +506,7 @@ impl TableAttr { attr } - pub fn set_attr(&mut self, attr: &str, value: &str) { + fn set_attr(&mut self, attr: &str, value: &str) { let attr = attr.to_lowercase(); match attr.as_str() { "align" => { @@ -571,7 +562,7 @@ impl TableAttr { _ => {} } } - pub fn update_style_attr(&self, style_attr: &mut StyleAttr) { + pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { if let Some(ref color) = self.bgcolor { style_attr.fill_color = Some(color.clone()); } @@ -580,43 +571,37 @@ impl TableAttr { } } #[derive(Debug, Clone)] -pub struct FontTable { - pub rows: Vec<(Row, Option
)>, - pub tag: TableTag, - pub table_attr: TableAttr, +struct FontTable { + rows: Vec<(Row, Option
)>, + tag: TableTag, + table_attr: TableAttr, } #[derive(Debug, Clone)] -pub struct Vr {} +struct Vr {} #[derive(Debug, Clone)] -pub struct Hr {} +struct Hr {} #[derive(Debug, Clone)] -pub struct Row { - pub cells: Vec<(DotCell, Option)>, +struct Row { + cells: Vec<(DotCell, Option)>, } #[derive(Debug, Clone)] -pub enum TextItem { +enum TextItem { TaggedText(TaggedText), Br(Align), PlainText(String), } #[derive(Debug, Clone)] -pub enum Html { +enum Html { Text(Text), FontTable(FontTable), } -impl Html { - // fn new_text() -> Self { - - // } -} - #[derive(Debug, Clone)] -pub struct TextStyle { +pub(crate) struct TextStyle { pub(crate) font: Font, pub(crate) font_style: FontStyle, pub(crate) font_weight: FontWeight, @@ -625,12 +610,12 @@ pub struct TextStyle { } #[derive(Debug, Clone)] -pub struct PlainTextGrid { +pub(crate) struct PlainTextGrid { pub(crate) text: String, pub(crate) text_style: TextStyle, } #[derive(Debug, Clone)] -pub struct TextGrid { +pub(crate) struct TextGrid { // each line is a vector of PlainTextGrid // as a whole it represent multiline text pub(crate) text_items: Vec>, @@ -638,13 +623,13 @@ pub struct TextGrid { } impl TextGrid { - pub fn new() -> Self { + fn new() -> Self { Self { text_items: vec![], br: vec![], } } - pub fn collect_from_text(&mut self, text: &Text, text_style: &TextStyle) { + fn collect_from_text(&mut self, text: &Text, text_style: &TextStyle) { for item in text.iter() { match item { TextItem::TaggedText(tagged_text) => { @@ -697,7 +682,7 @@ impl TextGrid { } } - pub fn width(&self, font_size: usize) -> f64 { + 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; @@ -711,7 +696,7 @@ impl TextGrid { } width } - pub fn height(&self, font_size: usize) -> f64 { + 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, @@ -726,13 +711,13 @@ impl TextGrid { } #[derive(Debug, Clone)] -pub enum HtmlGrid { +pub(crate) enum HtmlGrid { Text(TextGrid), FontTable(TableGrid), } impl HtmlGrid { - pub fn size(&self, font_size: usize) -> Point { + pub(crate) fn size(&self, font_size: usize) -> Point { match self { HtmlGrid::Text(text) => { Point::new(text.width(font_size), text.height(font_size)) @@ -740,7 +725,7 @@ impl HtmlGrid { HtmlGrid::FontTable(table_grid) => table_grid.size(font_size), } } - pub fn from_html(html: &Html) -> Self { + fn from_html(html: &Html) -> Self { match html { // Html::Text(text) => HtmlGrid::Text(text.clone()), Html::Text(text) => { @@ -763,7 +748,7 @@ impl HtmlGrid { } #[derive(Debug, Clone, PartialEq)] -pub enum HtmlMode { +enum HtmlMode { Html, HtmlTag, } @@ -787,13 +772,13 @@ struct HtmlParser { } impl HtmlParser { - pub fn next_token(&mut self) -> Token { + fn next_token(&mut self) -> Token { match self.mode { HtmlMode::Html => self.read_html(), HtmlMode::HtmlTag => self.read_tag_inside(), } } - pub fn lex(&mut self) { + fn lex(&mut self) { match self.tok { Token::Error(pos) => { panic!("can't parse after error at {}", pos); @@ -807,7 +792,7 @@ impl HtmlParser { } } } - pub fn skip_whitespace(&mut self) -> bool { + fn skip_whitespace(&mut self) -> bool { let mut changed = false; while self.ch.is_ascii_whitespace() { self.read_char(); @@ -815,10 +800,10 @@ impl HtmlParser { } changed } - pub fn has_next(&self) -> bool { + fn has_next(&self) -> bool { self.pos < self.input.len() } - pub fn read_char(&mut self) { + fn read_char(&mut self) { if !self.has_next() { self.ch = '\0'; } else { @@ -827,7 +812,7 @@ impl HtmlParser { } } - pub fn read_tag_inside(&mut self) -> Token { + fn read_tag_inside(&mut self) -> Token { let tok: Token; while self.skip_whitespace() {} if self.ch == '>' { @@ -849,7 +834,7 @@ impl HtmlParser { self.read_char(); tok } - pub fn read_html_text(&mut self) -> Token { + fn read_html_text(&mut self) -> Token { let mut result = String::new(); while self.ch != '<' && self.ch != '\0' && self.ch != '>' { result.push(self.ch); @@ -858,7 +843,7 @@ impl HtmlParser { } Token::Identifier(result) } - pub fn read_html(&mut self) -> Token { + fn read_html(&mut self) -> Token { let mut tag_name = String::new(); if self.ch == '\0' { return Token::EOF; @@ -898,7 +883,7 @@ impl HtmlParser { Token::OpeningTag(TagType::from_str(&tag_name)) } } - pub fn read_string(&mut self) -> Token { + fn read_string(&mut self) -> Token { let mut result = String::new(); self.read_char(); while self.ch != '"' { @@ -920,7 +905,7 @@ impl HtmlParser { } Token::Identifier(result) } - pub fn read_tag_attr(&mut self) -> Token { + 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' { @@ -947,7 +932,7 @@ impl HtmlParser { } } // Parse HTML-like label content between < and > - pub fn parse_html_label(&mut self) -> Result { + fn parse_html_label(&mut self) -> Result { let is_table = self.is_table()?; if is_table { @@ -957,7 +942,7 @@ impl HtmlParser { } } - pub fn parse_text(&mut self) -> Result { + fn parse_text(&mut self) -> Result { let mut text_items = vec![]; loop { match self.tok { @@ -971,7 +956,7 @@ impl HtmlParser { Ok(text_items) } - pub fn is_table(&self) -> Result { + 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(); @@ -1003,7 +988,7 @@ impl HtmlParser { Ok(false) } - pub fn parse_text_item(&mut self) -> Result { + fn parse_text_item(&mut self) -> Result { Ok(match self.tok.clone() { Token::Identifier(x) => { self.lex(); @@ -1055,7 +1040,7 @@ impl HtmlParser { }) } - pub fn parse_table(&mut self) -> Result { + fn parse_table(&mut self) -> Result { let mut invalid_string = false; if let Token::Identifier(x) = self.tok.clone() { // Consume the text. @@ -1141,7 +1126,7 @@ impl HtmlParser { }) } - pub fn parse_tag_attr_list( + fn parse_tag_attr_list( &mut self, tag_type: TagType, ) -> Result, String> { @@ -1176,7 +1161,7 @@ impl HtmlParser { Ok(lst) } - pub fn parse_row(&mut self) -> Result { + fn parse_row(&mut self) -> Result { let (tag_type, _attr_list) = self.parse_tag_start(true)?; if tag_type != TagType::Tr { return to_error( @@ -1211,7 +1196,7 @@ impl HtmlParser { Ok(Row { cells }) } - pub fn parse_cell(&mut self) -> Result { + fn parse_cell(&mut self) -> Result { let (tag_type, attr_list) = self.parse_tag_start(false)?; if tag_type != TagType::Td { return to_error( @@ -1226,7 +1211,7 @@ impl HtmlParser { }) } - pub fn parse_tag_start( + fn parse_tag_start( &mut self, pass_identifier: bool, ) -> Result<(TagType, Vec<(String, String)>), String> { @@ -1274,7 +1259,7 @@ impl HtmlParser { Ok((tag_type, tag_attr_list)) } - pub fn parse_tag_end( + fn parse_tag_end( &mut self, tag: &TagType, pass_identifier: bool, @@ -1304,7 +1289,7 @@ impl HtmlParser { } } -pub fn parse_html_string(input: &str) -> Result { +pub(crate) fn parse_html_string(input: &str) -> Result { let mut parser = HtmlParser { input: input.chars().collect(), pos: 0, @@ -1325,13 +1310,13 @@ struct TableGridInner { } impl TableGridInner { - pub fn width(&self) -> usize { + fn width(&self) -> usize { self.occupation.keys().map(|(x, _)| *x).max().unwrap_or(0) + 1 } - pub fn height(&self) -> usize { + fn height(&self) -> usize { self.occupation.keys().map(|(_, y)| *y).max().unwrap_or(0) + 1 } - pub fn pretty_print(&self) { + fn pretty_print(&self) { // print in a table format with + indicating occupied and - indicating free let width = self.width(); let height = self.height(); @@ -1346,7 +1331,7 @@ impl TableGridInner { println!(); } } - pub fn add_cell( + fn add_cell( &mut self, x: usize, y: usize, @@ -1373,7 +1358,7 @@ impl TableGridInner { self.occupation.contains_key(&(x, y)) } - pub fn from_table(font_table: &FontTable) -> Self { + fn from_table(font_table: &FontTable) -> Self { let mut width = 0; let mut height = 0; let mut y_current = 0; @@ -1438,7 +1423,7 @@ impl TableGridInner { } #[derive(Debug, Clone)] -pub struct TableGrid { +pub(crate) struct TableGrid { pub(crate) cells: Vec<(TdAttr, DotCellGrid)>, pub(crate) grid: Vec>, pub(crate) width_arr: Vec, // width in svg units @@ -1451,19 +1436,19 @@ pub struct TableGrid { } impl TableGrid { - pub fn width(&self) -> f64 { + pub(crate) fn width(&self) -> f64 { self.width_arr.iter().sum::() + (self.table_attr.cellspacing as usize * (self.width_in_cell + 1)) as f64 + self.table_attr.border as f64 * 2. } - pub fn height(&self) -> f64 { + pub(crate) fn height(&self) -> f64 { self.height_arr.iter().sum::() + (self.table_attr.cellspacing as usize * (self.height_in_cell + 1)) as f64 + self.table_attr.border as f64 * 2. } - pub fn size(&self, font_size: usize) -> Point { + fn size(&self, font_size: usize) -> Point { if font_size != self.font_size { let mut table_grid = self.clone(); table_grid.resize(font_size); @@ -1472,7 +1457,7 @@ impl TableGrid { Point::new(self.width(), self.height()) } } - pub fn cell_pos(&self, d: &DotCellGrid) -> Point { + pub(crate) fn cell_pos(&self, d: &DotCellGrid) -> Point { let idx = d.i; let x = self.width_arr.iter().take(idx).sum::() + (self.table_attr.cellspacing as usize * (idx + 1)) as f64 @@ -1486,7 +1471,7 @@ impl TableGrid { Point::new(x, y) } - pub fn cell_size(&self, dot_cell_grid: &DotCellGrid) -> Point { + 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) @@ -1508,7 +1493,7 @@ impl TableGrid { Point::new(width, height) } - pub fn from_table(font_table: &FontTable) -> Self { + fn from_table(font_table: &FontTable) -> Self { let table_grid_inner = TableGridInner::from_table(font_table); let width_in_cell = table_grid_inner.width(); let height_in_cell = table_grid_inner.height(); @@ -1538,7 +1523,7 @@ impl TableGrid { } } - pub fn get_cell(&self, i: usize, j: usize) -> Option<&DotCellGrid> { + 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 { @@ -1548,11 +1533,7 @@ impl TableGrid { None } - pub fn get_cell_mut( - &mut self, - i: usize, - j: usize, - ) -> Option<&mut DotCellGrid> { + 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 { @@ -1584,7 +1565,7 @@ impl TableGrid { cellborder } - pub fn resize(&mut self, font_size: usize) { + 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; diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index f371faf..246ecdd 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -41,7 +41,7 @@ fn get_record_size( } } -pub(crate) const BOX_SHAPE_PADDING: f64 = 10.; +const BOX_SHAPE_PADDING: f64 = 10.; const CIRCLE_SHAPE_PADDING: f64 = 20.; /// Return the size of the shape. If \p make_xy_same is set then make the From 7910f0628870bede06b43d13a98b18e37418ae39 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Sat, 17 May 2025 02:15:17 +0200 Subject: [PATCH 27/40] add support for embedding image --- docs/sample.png | Bin 0 -> 2335 bytes inputs/html_img.dot | 8 ++++ layout/src/backends/svg.rs | 19 ++++++++ layout/src/core/format.rs | 9 ++++ layout/src/core/utils.rs | 36 +++++++++++++++- layout/src/gv/html.rs | 74 +++++++++++++++++++++++++++----- layout/src/std_shapes/render.rs | 42 +++++++++++++++++- 7 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 docs/sample.png create mode 100644 inputs/html_img.dot diff --git a/docs/sample.png b/docs/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..aa613599201f8ac36246f65c18e760f6f3c82903 GIT binary patch literal 2335 zcmV+)3E=jLP)73W`Ip=h?7hZ#|CUP!NCzCJuJa%@>lvgzW$SnOi5xXtuTQ zbVj0_xJ^HOT{=Py<6n`#7kgk2?14S72ll`o*aLfD5A1Sx9O@S><0@o{QNkk^VV;HbPw(U)R{`f|$#>k{;n$m=F~3|wMov?JE9)o7*+d_#4NujV|T1YQB;?waY-yek$+{{(g=4!hqCOel_$v!wdYwf-;d zjB@|06()jpUo0o~TCI^pJ7YPhW-*gmlQdcQ)^5+V zd7imw9V?&}>S;=UhQQb?hMloG8L6pWN}-FjcD?<&{X(z-d~Me}jxk5X?+DY`7u0NL z26bPo1u1%g^kTJXDf8`mgA@7r^ViRXW?onsF=AFV5rJ6vECJSiv9O}g%PV?OC3xG- z#}#AH=Bv$BG$*wzlqX>Ynv^F6b!V*PteT+w6VvizLAmoYt$_xqQL06w6%`^fx9*LV zFi6Vk*e~2m0j)GurfOxbWHT)|HTz*!NV4w!FCyn5Bq~gF`5LuL(MsA%;{v1M(0OYfP1>D&<;O$%efDlY;aCfmH6i<=8_K zQFH4Bt7#eQtVJ5J6_cV`a3#fh>bqkl4k3ovPxs}%ZIAm26(c#uB*w&4*s7V=32l|a zOkC*^1=gLh$sbbu*{VOBx;(b~_CWmiJ{}oR)rE_il8=ZeXJZ*`{|83&+T$>U^mv?3 zrxjnA@AqZ7KYAbfIAD!=%ry~|r7)?i%wyDXcWj2!D@?rH9r+Zmj$TnVK10ZZgTpxB zlar{j#u&KojrCg=0&tfTsQ2yuHb2(B_TLG;J3I|O-8o+5eglV2wBdo4)UjO!u!QRQ7~{y*^;|qJ3=`jTKe|(tvjD+g~4nj6Bxh;`KSX zxG}u5oz@q17=~!!quzB%>S1;MiacavO&!kQN!rJc!uH^x?Xf~Uj6Tq>2>>|qC%^eN z$xtE#5@qb4jD6|UvWpWgJ{SAdO#NSW&1lDU6bPcbl>7-*CFSA%|7Wh&KuUzMa_wj| z*Sq*3*mQMsz&$8w=Kps9vS1hNf?co+cEK*#1-oDu?1Eje3wFUS*af>_7wm$Stv?@@ zU?~Cs+yPd_kYu$EYZ6_yd=5)Wl1P?P1RDIed#hFhlgCBN=ddzz0Vd(W4h{Otmd{}c zSR@IEb#47R(pGX^wtNnoB_MJKmhZkO@GLB?CpB{S{ji2ludFP*0hY9#Oat&WQ3X)7p`OX*T^OQ6OM$lCqGwP*gD_p^ zU=7VFYvnJ103*!x#uB7S7~iw7h0}HgI0Gi%rtpWBZSM(^;8&rp!G4*t377N$9Qiv- zpN6;b?RoIM3Tt=^cES-*@6Q0omJYR*P)@>hU4mu2{h9^rhTI-o=9JA#i;w^>!OkNq zX_t|P<#=m(L_xrVFuuG7>!d8Q9Bq4|g!#+ZvBhwvL+=3@VOP+)1WUkMmZhP{bt#}a z0fPhVvBl3;7QmtiyRWP(X+RMvOWAh6tqo^Q>uU-F`r4b;F01Bt(>E)?L&OuX z*`uL;TN{reTeVMBD!`h(;a)>F05-kLV^<|*6Y(6Zte#0S0T+6LZJK>qy-PTG(*jJ( zK(X9H$`By|V3#K1IasDF$y(J=Z3b3!jkZFdfU~zB1&~<)n|*G*vk+iXHWAOjLa8F| z5wld8&@KX4qB-ye+h;NKl(TeLRB0lffi;HT&w8!=#cD9y`k^;_S`uJ7D{Qq55zoL9 zdaa^-T`;7@Oi)MOt}*7$o<+bA@f56(%4R4z!n$Q$o_*{cWcN;HiiU@X=TB=AnE>Z3 zFwDQlQ}jdchMf6w5CcxcbFg_Ui9DYYWZd@HASZ7D9eEQl{x(;pz(hO`O9L#b8%w97 z2H2eQnmuV}Z@1oduOxVR+BRW`Nc}#nTESO-MixL?TVP#^Y-Tg}?46YD_flZ>%rBXV zn5B3RO8_v19DX$ja7$wEm&&695IFYkpLUZvyOaSt5p#s|7PfoupUP?5G})bo6;2JG zlXrGmjt4VHom-J&Iv;-vTlnaO;atbhy%jWjZ(ko>dLz?2k3fF&mu~{UsWkMnMUF6- zCn}&n0vlRGCtktN!4|`YPt?JG61Gs{ZxuA|QkWy)at~Mv0ASq$wq5H#lRv)}?1Eje z3wFUS*af>_7wkXn-N66=004u)k>roLM;b+t!KJcVvj +
caption
>]; +} \ No newline at end of file diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index f511279..df0707c 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -378,6 +378,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/utils.rs b/layout/src/core/utils.rs index f62dd64..97065f7 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,37 @@ pub fn save_to_file(filename: &str, content: &str) -> Result<(), Error> { log::info!("Wrote {}", filename); Result::Ok(()) } + +pub fn read_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/html.rs b/layout/src/gv/html.rs index 9d6a953..fcd1c93 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -76,6 +76,46 @@ impl TagType { } } +#[derive(Debug, Clone)] +pub(crate) struct Image { + pub(crate) scale: Scale, + pub(crate) source: String, +} + +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 = crate::core::utils::read_png_size(&self.source).unwrap(); + size.0 as f64 + } + fn height(&self) -> f64 { + let size = crate::core::utils::read_png_size(&self.source).unwrap(); + size.1 as f64 + } +} + #[derive(Debug, Clone)] pub(crate) enum Scale { False, @@ -197,7 +237,7 @@ impl TableTag { #[derive(Debug, Clone)] enum LabelOrImg { Html(Html), - Img(Scale, String), + Img(Image), } #[derive(Debug, Clone)] struct DotCell { @@ -227,9 +267,7 @@ impl DotCellGrid { LabelOrImg::Html(html) => { LabelOrImgGrid::Html(HtmlGrid::from_html(html)) } - LabelOrImg::Img(scale, img) => { - LabelOrImgGrid::Img(scale.clone(), img.clone()) - } + LabelOrImg::Img(image) => LabelOrImgGrid::Img(image.clone()), }; Self { i, @@ -245,7 +283,7 @@ impl DotCellGrid { #[derive(Debug, Clone)] pub(crate) enum LabelOrImgGrid { Html(HtmlGrid), - Img(Scale, String), + Img(Image), } #[derive(Debug, Clone)] @@ -615,7 +653,7 @@ pub(crate) struct PlainTextGrid { pub(crate) text_style: TextStyle, } #[derive(Debug, Clone)] -pub(crate) struct TextGrid { +pub struct TextGrid { // each line is a vector of PlainTextGrid // as a whole it represent multiline text pub(crate) text_items: Vec>, @@ -711,7 +749,7 @@ impl TextGrid { } #[derive(Debug, Clone)] -pub(crate) enum HtmlGrid { +pub enum HtmlGrid { Text(TextGrid), FontTable(TableGrid), } @@ -1203,7 +1241,21 @@ impl HtmlParser { format!("Expected
, found {:?}", tag_type).as_str(), ); } - let label = LabelOrImg::Html(self.parse_html_label()?); + 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, @@ -1423,7 +1475,7 @@ impl TableGridInner { } #[derive(Debug, Clone)] -pub(crate) struct TableGrid { +pub struct TableGrid { pub(crate) cells: Vec<(TdAttr, DotCellGrid)>, pub(crate) grid: Vec>, pub(crate) width_arr: Vec, // width in svg units @@ -1586,7 +1638,7 @@ impl TableGrid { HtmlGrid::Text(text) => text.width(font_size), HtmlGrid::FontTable(x) => x.width(), }, - _ => 0.0, + LabelOrImgGrid::Img(img) => img.width(), }; let cellpadding = self.cellpadding(cell); let cellborder = self.cellborder(cell); @@ -1609,7 +1661,7 @@ impl TableGrid { HtmlGrid::Text(text) => text.height(font_size), HtmlGrid::FontTable(x) => x.height(), }, - _ => 0.0, + LabelOrImgGrid::Img(img) => img.height(), }; let cellpadding = self.cellpadding(cell); let cellborder = self.cellborder(cell); diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 246ecdd..4f53a7d 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -7,7 +7,7 @@ use crate::core::style::{ Align, FontStyle, FontWeight, LineStyleKind, StyleAttr, VAlign, }; use crate::gv::html::{ - DotCellGrid, HtmlGrid, LabelOrImgGrid, TableGrid, TableTag, TextGrid, + DotCellGrid, HtmlGrid, LabelOrImgGrid, Scale, TableGrid, TableTag, TextGrid, }; use crate::std_shapes::shapes::*; @@ -462,7 +462,45 @@ fn render_cell( canvas, ); } - LabelOrImgGrid::Img(_, _) => {} + LabelOrImgGrid::Img(img) => { + let mut look = look.clone(); + look.fill_color = Option::None; + let image_size = + crate::core::utils::read_png_size(&img.source).unwrap(); + let image_size = match &img.scale { + Scale::False => { + Point::new(image_size.0 as f64, image_size.1 as f64) + } + Scale::True => { + let x_scale = size.x / image_size.0 as f64; + let y_scale = size.y / image_size.1 as f64; + let scale = + if x_scale < y_scale { x_scale } else { y_scale }; + Point::new( + image_size.0 as f64 * scale, + image_size.1 as f64 * scale, + ) + } + Scale::Width => { + let scale = size.x / image_size.0 as f64; + Point::new(image_size.0 as f64 * scale, image_size.1 as f64) + } + Scale::Height => { + let scale = size.y / image_size.1 as f64; + Point::new(image_size.0 as f64, image_size.1 as f64 * 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, + ); + } } } From 8d4a4a53f27e7dde02b9c900f246a8f963fc4de2 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Sat, 17 May 2025 04:51:17 +0200 Subject: [PATCH 28/40] Add validation for row and table splitting and some reorganization --- layout/src/core/utils.rs | 2 +- layout/src/gv/html.rs | 1550 ++++++++++++++++--------------- layout/src/std_shapes/render.rs | 24 +- 3 files changed, 804 insertions(+), 772 deletions(-) diff --git a/layout/src/core/utils.rs b/layout/src/core/utils.rs index 97065f7..88f212e 100644 --- a/layout/src/core/utils.rs +++ b/layout/src/core/utils.rs @@ -13,7 +13,7 @@ pub fn save_to_file(filename: &str, content: &str) -> Result<(), Error> { Result::Ok(()) } -pub fn read_png_size(filename: &str) -> Result<(u32, u32), Error> { +pub(crate) fn get_png_size(filename: &str) -> Result<(u32, u32), Error> { let mut f = File::open(filename)?; let mut signature = [0; 8]; diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index fcd1c93..f649bca 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1,18 +1,47 @@ 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_png_size; -use crate::core::color::Color; +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, @@ -77,128 +106,75 @@ impl TagType { } #[derive(Debug, Clone)] -pub(crate) struct Image { - pub(crate) scale: Scale, - pub(crate) source: String, -} - -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 = crate::core::utils::read_png_size(&self.source).unwrap(); - size.0 as f64 - } - fn height(&self) -> f64 { - let size = crate::core::utils::read_png_size(&self.source).unwrap(); - size.1 as f64 - } +enum Html { + Text(Text), + FontTable(FontTable), } -#[derive(Debug, Clone)] -pub(crate) enum Scale { - False, - True, - Width, - Height, - Both, -} +type Text = Vec; #[derive(Debug, Clone)] -pub(crate) struct Font { - pub(crate) color: Option, - pub(crate) face: Option, - pub(crate) point_size: Option, +struct FontTable { + rows: Vec<(Row, Option
)>, + tag: TableTag, + table_attr: TableAttr, } -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 - } - } +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"); } - "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); + } else { + return to_error("Table cannot be empty"); } - font + Ok(Self { + rows, + tag, + table_attr, + }) } } #[derive(Debug, Clone)] -enum TextTag { +pub(crate) enum TableTag { + None, Font(Font), I, B, U, O, - Sub, - Sup, - S, } -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) +#[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"); } - 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), + } else { + return to_error("Row cannot be empty"); } + Ok(Self { cells }) } } -type Text = Vec; +#[derive(Debug, Clone)] +enum TextItem { + TaggedText(TaggedText), + Br(Align), + PlainText(String), +} #[derive(Debug, Clone)] struct TaggedText { @@ -207,38 +183,24 @@ struct TaggedText { } #[derive(Debug, Clone)] -pub(crate) enum TableTag { - None, +enum TextTag { Font(Font), I, B, U, O, + Sub, + Sup, + S, } -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 - } - } -} + #[derive(Debug, Clone)] -enum LabelOrImg { - Html(Html), - Img(Image), +pub(crate) struct Font { + pub(crate) color: Option, + pub(crate) face: Option, + pub(crate) point_size: Option, } + #[derive(Debug, Clone)] struct DotCell { label: LabelOrImg, @@ -246,43 +208,8 @@ struct DotCell { } #[derive(Debug, Clone)] -pub(crate) struct DotCellGrid { - pub(crate) i: usize, - pub(crate) j: usize, - pub(crate) width_in_cell: usize, - pub(crate) height_in_cell: usize, - pub(crate) label_grid: LabelOrImgGrid, - td_attr: TdAttr, -} - -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(), - } - } -} - -#[derive(Debug, Clone)] -pub(crate) enum LabelOrImgGrid { - Html(HtmlGrid), +enum LabelOrImg { + Html(Html), Img(Image), } @@ -324,158 +251,17 @@ pub(crate) struct TdAttr { tooltip: Option, // value } -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 update_style_attr(&self, style_attr: &mut StyleAttr) { - 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(); - } -} - -#[derive(Debug, Clone)] -enum ColumnFormat { - Star, - None, -} - -impl ColumnFormat { - fn from_str(s: &str) -> Self { - if s.starts_with('*') { - Self::Star - } else { - Self::None - } - } -} +#[derive(Debug, Clone)] +pub(crate) struct Image { + pub(crate) scale: Scale, + pub(crate) source: String, +} #[derive(Debug, Clone)] -enum RowFormat { - Star, - None, -} - -impl RowFormat { - fn from_str(s: &str) -> Self { - if s.starts_with('*') { - Self::Star - } else { - Self::None - } - } -} +struct Vr {} #[derive(Debug, Clone)] -struct Sides { - left: bool, - right: bool, - top: bool, - bottom: bool, -} - -impl Sides { - fn from_str(s: &str) -> Self { - let mut sides = Sides { - 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 - } -} +struct Hr {} #[derive(Debug, Clone)] pub(crate) struct TableAttr { @@ -509,304 +295,33 @@ pub(crate) struct TableAttr { tooltip: Option, // value } -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)), - _ => {} - } - } - pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { - if let Some(ref color) = self.bgcolor { - style_attr.fill_color = Some(color.clone()); - } - style_attr.valign = self.valign.clone(); - style_attr.align = self.align.clone(); - } -} #[derive(Debug, Clone)] -struct FontTable { - rows: Vec<(Row, Option
)>, - tag: TableTag, - table_attr: TableAttr, +struct Sides { + left: bool, + right: bool, + top: bool, + bottom: bool, } -#[derive(Debug, Clone)] -struct Vr {} - -#[derive(Debug, Clone)] -struct Hr {} #[derive(Debug, Clone)] -struct Row { - cells: Vec<(DotCell, Option)>, +enum RowFormat { + Star, + None, } #[derive(Debug, Clone)] -enum TextItem { - TaggedText(TaggedText), - Br(Align), - PlainText(String), +enum ColumnFormat { + Star, + None, } #[derive(Debug, Clone)] -enum Html { - Text(Text), - FontTable(FontTable), -} - -#[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(crate) struct PlainTextGrid { - pub(crate) text: String, - pub(crate) text_style: TextStyle, -} -#[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>, - pub(crate) br: Vec, -} - -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 = PlainTextGrid { - 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 text_size = get_size_for_str(&item.text, font_size); - line_width += text_size.x; - } - if width < line_width { - width = line_width; - } - } - width - } - 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 - if let Some(last_line) = line.last() { - let text_size = get_size_for_str(&last_line.text, font_size); - height += text_size.y; - } - } - height - } -} - -#[derive(Debug, Clone)] -pub enum HtmlGrid { - Text(TextGrid), - FontTable(TableGrid), -} - -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) => HtmlGrid::Text(text.clone()), - Html::Text(text) => { - let mut text_grid = TextGrid::new(); - let text_style = TextStyle { - font: Font::new(), - font_style: FontStyle::Normal, - font_weight: FontWeight::Normal, - 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)) - } - } - } -} - -#[derive(Debug, Clone, PartialEq)] -enum HtmlMode { - Html, - HtmlTag, -} - -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 HtmlParser { - input: Vec, - pos: usize, - tok: Token, - mode: HtmlMode, - pub ch: char, +pub(crate) enum Scale { + False, + True, + Width, + Height, + Both, } impl HtmlParser { @@ -1157,11 +672,7 @@ impl HtmlParser { } let table_attr = TableAttr::from_attr_list(table_attr2); - Ok(FontTable { - rows, - tag: TableTag::from_tag(table_tag1), - table_attr, - }) + FontTable::try_new(rows, TableTag::from_tag(table_tag1), table_attr) } fn parse_tag_attr_list( @@ -1206,183 +717,723 @@ impl HtmlParser { 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_png_size(&self.source).unwrap(); + size.0 as f64 + } + fn height(&self) -> f64 { + let size = get_png_size(&self.source).unwrap(); + size.1 as f64 + } + + pub(crate) fn size(&self) -> Point { + let size = get_png_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 update_style_attr(&self, style_attr: &mut StyleAttr) { + 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(); + } +} + +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)), + _ => {} + } + } + pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { + if let Some(ref color) = self.bgcolor { + style_attr.fill_color = Some(color.clone()); + } + style_attr.valign = self.valign.clone(); + style_attr.align = self.align.clone(); + } +} + +#[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(), } - self.parse_tag_end(&TagType::Tr, true)?; - Ok(Row { 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(), - ); +impl TextGrid { + fn new() -> Self { + Self { + text_items: vec![], + br: vec![], } - 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(), + } + 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, ); } - let img = Image::from_tag_attr_list(attr_list)?; - LabelOrImg::Img(img) + 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); + } + } } - _ => 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; + 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 text_size = get_size_for_str(&item.text, font_size); + line_width += text_size.x; } - 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(); - } - } + if width < line_width { + width = line_width; } - TagType::Unrecognized => { - return to_error( - format!("Unrecognized tag type {:?}", tag_type).as_str(), - ); + } + width + } + 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 + if let Some(last_line) = line.last() { + let text_size = get_size_for_str(&last_line.text, font_size); + height += text_size.y; } } - Ok((tag_type, tag_attr_list)) + height } +} - 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(), - ); +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)) } - } else { - return to_error(format!("Expected 'closing tag {:?}' to end HTML label tag, found {:?}", tag, self.tok).as_str()); + HtmlGrid::FontTable(table_grid) => table_grid.size(font_size), } - if pass_identifier { - if let Token::Identifier(_) = self.tok.clone() { - self.lex(); + } + 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::Normal, + font_weight: FontWeight::Normal, + 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)) } } - - Ok(()) } } -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)) +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 TableGridInner { +struct TableHashGrid { pub(crate) cells: Vec<(TdAttr, DotCellGrid)>, pub(crate) occupation: HashMap<(usize, usize), usize>, // x, y, cell index } -impl TableGridInner { +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 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, @@ -1474,19 +1525,6 @@ impl TableGridInner { } } -#[derive(Debug, Clone)] -pub struct TableGrid { - pub(crate) cells: Vec<(TdAttr, DotCellGrid)>, - pub(crate) grid: Vec>, - pub(crate) width_arr: Vec, // width in svg units - pub(crate) 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, -} - impl TableGrid { pub(crate) fn width(&self) -> f64 { self.width_arr.iter().sum::() @@ -1546,12 +1584,12 @@ impl TableGrid { } fn from_table(font_table: &FontTable) -> Self { - let table_grid_inner = TableGridInner::from_table(font_table); - let width_in_cell = table_grid_inner.width(); - let height_in_cell = table_grid_inner.height(); + 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_grid_inner.cells.iter().enumerate() + table_hash_grid.cells.iter().enumerate() { for i in 0..dot_cell.width_in_cell { for j in 0..dot_cell.height_in_cell { @@ -1563,7 +1601,7 @@ impl TableGrid { } Self { - cells: table_grid_inner.cells, + cells: table_hash_grid.cells, grid, width_arr: vec![1.0; width_in_cell], height_arr: vec![1.0; height_in_cell], diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 4f53a7d..46f696a 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -465,29 +465,23 @@ fn render_cell( LabelOrImgGrid::Img(img) => { let mut look = look.clone(); look.fill_color = Option::None; - let image_size = - crate::core::utils::read_png_size(&img.source).unwrap(); + let image_size = img.size(); let image_size = match &img.scale { - Scale::False => { - Point::new(image_size.0 as f64, image_size.1 as f64) - } + Scale::False => Point::new(image_size.x, image_size.y), Scale::True => { - let x_scale = size.x / image_size.0 as f64; - let y_scale = size.y / image_size.1 as f64; + 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.0 as f64 * scale, - image_size.1 as f64 * scale, - ) + Point::new(image_size.x * scale, image_size.y * scale) } Scale::Width => { - let scale = size.x / image_size.0 as f64; - Point::new(image_size.0 as f64 * scale, image_size.1 as f64) + let scale = size.x / image_size.x; + Point::new(image_size.x * scale, image_size.y) } Scale::Height => { - let scale = size.y / image_size.1 as f64; - Point::new(image_size.0 as f64, image_size.1 as f64 * scale) + let scale = size.y / image_size.y; + Point::new(image_size.x, image_size.y * scale) } Scale::Both => size.clone(), }; From 276635df2604eafc107c9cab287b2b83e63e85f7 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Sat, 17 May 2025 20:50:45 +0200 Subject: [PATCH 29/40] Better horizontal layout within cell, fix inheritance bug for font_weight --- inputs/html_complex.dot | 4 +- layout/src/backends/svg.rs | 11 ++-- layout/src/core/style.rs | 4 +- layout/src/gv/html.rs | 95 +++++++++++++++++++++++++++++---- layout/src/std_shapes/render.rs | 59 ++++---------------- 5 files changed, 102 insertions(+), 71 deletions(-) diff --git a/inputs/html_complex.dot b/inputs/html_complex.dot index a50e02c..ee72dd6 100644 --- a/inputs/html_complex.dot +++ b/inputs/html_complex.dot @@ -10,7 +10,7 @@ digraph G { ] b [shape=ellipse style=filled label=< - +
-
elephant 4
> +
> ] c [ label=line 2
line 3
> diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index df0707c..22a44a8 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -2,7 +2,7 @@ use crate::core::color::Color; use crate::core::format::{ClipHandle, RenderBackend}; -use crate::core::geometry::Point; +use crate::core::geometry::{get_size_for_str, Point}; use crate::core::style::{StyleAttr, TextDecoration}; use std::collections::HashMap; @@ -237,10 +237,9 @@ impl RenderBackend for SVGWriter { 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)); @@ -254,7 +253,7 @@ impl RenderBackend for SVGWriter { }; let font_weight_text = match look.font_weight { crate::core::style::FontWeight::Bold => "font-weight=\"bold\"", - crate::core::style::FontWeight::Normal => "", + crate::core::style::FontWeight::None => "", }; let text_decoration_str = svg_text_decoration_str(&look.text_decoration); @@ -267,7 +266,7 @@ impl RenderBackend for SVGWriter { "dominant-baseline=\"text-top\"" } crate::core::style::BaselineShift::Normal => { - "dominant-baseline=\"middle\"" + "dominant-baseline=\"auto\"" } }; let line = format!( @@ -275,7 +274,7 @@ impl RenderBackend for SVGWriter { x=\"{}\" y=\"{}\" font-size=\"{}\" font-family=\"{}\" {} {} {} fill=\"{}\">{}", baseline_shift_str, xy.x, - xy.y - size_y / 2., + xy.y - size.y / 2., font_size, font_family, font_style_text, diff --git a/layout/src/core/style.rs b/layout/src/core/style.rs index bdf3b63..e3f9ee2 100644 --- a/layout/src/core/style.rs +++ b/layout/src/core/style.rs @@ -61,7 +61,7 @@ pub(crate) enum FontStyle { #[derive(Debug, Copy, Clone)] pub(crate) enum FontWeight { - Normal, + None, Bold, } @@ -126,7 +126,7 @@ impl StyleAttr { font_size, fontname, font_style: FontStyle::Normal, - font_weight: FontWeight::Normal, + font_weight: FontWeight::None, text_decoration: TextDecoration::new(), baseline_shift: BaselineShift::Normal, align: Align::Center, diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index f649bca..b91b9d0 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1187,13 +1187,6 @@ impl TableAttr { _ => {} } } - pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { - if let Some(ref color) = self.bgcolor { - style_attr.fill_color = Some(color.clone()); - } - style_attr.valign = self.valign.clone(); - style_attr.align = self.align.clone(); - } } #[derive(Debug, Clone)] @@ -1344,6 +1337,10 @@ impl TextGrid { 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; } @@ -1358,15 +1355,55 @@ impl TextGrid { 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 - if let Some(last_line) = line.last() { - let text_size = get_size_for_str(&last_line.text, font_size); - height += text_size.y; + 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 } } +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 update_style_attr(&self, style_attr: &mut StyleAttr) { + 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(); + } + + style_attr.font_style = self.text_style.font_style.clone(); + 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(); + } +} + impl HtmlGrid { pub(crate) fn size(&self, font_size: usize) -> Point { match self { @@ -1383,7 +1420,7 @@ impl HtmlGrid { let text_style = TextStyle { font: Font::new(), font_style: FontStyle::Normal, - font_weight: FontWeight::Normal, + font_weight: FontWeight::None, text_decoration: TextDecoration::new(), baseline_shift: BaselineShift::Normal, }; @@ -1715,4 +1752,40 @@ impl TableGrid { // update the font size self.font_size = font_size; } + + pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { + 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 => {} + } + } } diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 46f696a..6176eda 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -3,11 +3,9 @@ use crate::core::base::Orientation; use crate::core::format::{ClipHandle, RenderBackend, Renderable, Visible}; use crate::core::geometry::*; -use crate::core::style::{ - Align, FontStyle, FontWeight, LineStyleKind, StyleAttr, VAlign, -}; +use crate::core::style::{Align, LineStyleKind, StyleAttr, VAlign}; use crate::gv::html::{ - DotCellGrid, HtmlGrid, LabelOrImgGrid, Scale, TableGrid, TableTag, TextGrid, + DotCellGrid, HtmlGrid, LabelOrImgGrid, Scale, TableGrid, TextGrid, }; use crate::std_shapes::shapes::*; @@ -325,29 +323,17 @@ fn render_text( for line in &rec.text_items { let mut line_width = 0.; for t in line { - let size_str = get_size_for_str(t.text.as_str(), look.font_size); - line_width += size_str.x; + line_width += t.width(look.font_size); } loc.x -= line_width / 2.; for t in line { - loc.x += get_size_for_str(t.text.as_str(), look.font_size).x / 2.; let mut look = look.clone(); - if let Some(x) = t.text_style.font.point_size { - look.font_size = x as usize; - } - if let Some(x) = t.text_style.font.color { - look.font_color = x; - } - if let Some(ref x) = t.text_style.font.face { - look.fontname = x.clone(); - } - look.font_style = t.text_style.font_style; - look.font_weight = t.text_style.font_weight; - look.text_decoration = t.text_style.text_decoration; - look.baseline_shift = t.text_style.baseline_shift; + t.update_style_attr(&mut look); + let text_size = get_size_for_str(t.text.as_str(), look.font_size); + loc.x += text_size.x / 2.; let loc2 = update_location(loc, size, t.text.as_str(), &look); canvas.draw_text(loc2, t.text.as_str(), &look); - loc.x += get_size_for_str(t.text.as_str(), look.font_size).x / 2.; + loc.x += text_size.x / 2.; } loc.y += look.font_size as f64; loc.x = loc0_x; @@ -363,36 +349,8 @@ fn render_font_table( _clip: Option, ) { let mut look = look.clone(); - look.line_width = rec.table_attr.border as usize; - - match &rec.table_tag { - TableTag::B => { - look.font_weight = FontWeight::Bold; - } - TableTag::I => { - look.font_style = FontStyle::Italic; - } - TableTag::U => { - look.text_decoration.underline = true; - } - TableTag::O => { - look.text_decoration.overline = true; - } - TableTag::Font(font) => { - if let Some(point_size) = font.point_size { - look.font_size = point_size as usize; - } - if let Some(font_color) = font.color { - look.font_color = font_color; - } - if let Some(ref font_name) = font.face { - look.fontname = font_name.clone(); - } - } - TableTag::None => {} - } - rec.table_attr.update_style_attr(&mut look); + rec.update_style_attr(&mut look); let table_grid_width = rec.width(); let table_grid_height = rec.height(); let loc0 = Point::new( @@ -463,6 +421,7 @@ fn render_cell( ); } LabelOrImgGrid::Img(img) => { + // TODO: Need to introduce setting to control file access as specificed by ofifical graphviz source let mut look = look.clone(); look.fill_color = Option::None; let image_size = img.size(); From 9a64cac551d2d50ef30a021dc343fc34a159d471 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Sat, 17 May 2025 20:57:23 +0200 Subject: [PATCH 30/40] Fix inheritance bug for font style --- layout/src/backends/svg.rs | 2 +- layout/src/core/style.rs | 4 ++-- layout/src/gv/html.rs | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/layout/src/backends/svg.rs b/layout/src/backends/svg.rs index 22a44a8..dfd65da 100644 --- a/layout/src/backends/svg.rs +++ b/layout/src/backends/svg.rs @@ -249,7 +249,7 @@ 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::Normal => "", + crate::core::style::FontStyle::None => "", }; let font_weight_text = match look.font_weight { crate::core::style::FontWeight::Bold => "font-weight=\"bold\"", diff --git a/layout/src/core/style.rs b/layout/src/core/style.rs index e3f9ee2..b2a1460 100644 --- a/layout/src/core/style.rs +++ b/layout/src/core/style.rs @@ -55,7 +55,7 @@ pub enum LineStyleKind { #[derive(Debug, Copy, Clone)] pub(crate) enum FontStyle { - Normal, + None, Italic, } @@ -125,7 +125,7 @@ impl StyleAttr { rounded, font_size, fontname, - font_style: FontStyle::Normal, + font_style: FontStyle::None, font_weight: FontWeight::None, text_decoration: TextDecoration::new(), baseline_shift: BaselineShift::Normal, diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index b91b9d0..44e597a 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1394,7 +1394,11 @@ impl PlainText { style_attr.fontname = face.clone(); } - style_attr.font_style = self.text_style.font_style.clone(); + // style_attr.font_style = self.text_style.font_style.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 => {} @@ -1419,7 +1423,7 @@ impl HtmlGrid { let mut text_grid = TextGrid::new(); let text_style = TextStyle { font: Font::new(), - font_style: FontStyle::Normal, + font_style: FontStyle::None, font_weight: FontWeight::None, text_decoration: TextDecoration::new(), baseline_shift: BaselineShift::Normal, From e95b330dc3f9a3fbed5ba610e6fe340b18bc0f3a Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Sat, 17 May 2025 21:03:37 +0200 Subject: [PATCH 31/40] remove stale comment --- layout/src/gv/html.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 44e597a..1008ca7 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1394,7 +1394,6 @@ impl PlainText { style_attr.fontname = face.clone(); } - // style_attr.font_style = self.text_style.font_style.clone(); match self.text_style.font_style { FontStyle::Italic => style_attr.font_style = FontStyle::Italic, FontStyle::None => {} From f00d5b583deba56e34d785a1cea98d0f1143b622 Mon Sep 17 00:00:00 2001 From: mert-kurttutan Date: Sat, 17 May 2025 21:29:00 +0200 Subject: [PATCH 32/40] better layout for height within cell --- inputs/html_densenet.dot | 495 ++++++++++++++++++++++++++++++++ layout/src/gv/html.rs | 17 +- layout/src/std_shapes/render.rs | 7 +- 3 files changed, 516 insertions(+), 3 deletions(-) create mode 100644 inputs/html_densenet.dot 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/layout/src/gv/html.rs b/layout/src/gv/html.rs index 1008ca7..33c4f77 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1350,7 +1350,7 @@ impl TextGrid { } width } - fn height(&self, font_size: usize) -> f64 { + 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, @@ -1372,6 +1372,21 @@ impl TextGrid { } } +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 { diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 6176eda..8e08efa 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -5,7 +5,8 @@ use crate::core::format::{ClipHandle, RenderBackend, Renderable, Visible}; use crate::core::geometry::*; use crate::core::style::{Align, LineStyleKind, StyleAttr, VAlign}; use crate::gv::html::{ - DotCellGrid, HtmlGrid, LabelOrImgGrid, Scale, TableGrid, TextGrid, + get_line_height, DotCellGrid, HtmlGrid, LabelOrImgGrid, Scale, TableGrid, + TextGrid, }; use crate::std_shapes::shapes::*; @@ -320,6 +321,8 @@ fn render_text( let loc0_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 { @@ -335,7 +338,7 @@ fn render_text( canvas.draw_text(loc2, t.text.as_str(), &look); loc.x += text_size.x / 2.; } - loc.y += look.font_size as f64; + loc.y += get_line_height(line, look.font_size); loc.x = loc0_x; } } From 736699b6cd102efce3de82ad36d2708f2ace9249 Mon Sep 17 00:00:00 2001 From: mert-kurttutan <kurttutan.mert@gmail.com> Date: Sat, 17 May 2025 23:07:00 +0200 Subject: [PATCH 33/40] add lr test for html, remove redundant clip argument for html parsing --- inputs/html_lr.dot | 29 ++++++++++++ layout/src/gv/html.rs | 15 +++++-- layout/src/std_shapes/render.rs | 78 ++++++++++++--------------------- 3 files changed, 70 insertions(+), 52 deletions(-) create mode 100644 inputs/html_lr.dot 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=< +<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> + <TR><TD>left</TD><TD PORT="f1">mid dle</TD><TD PORT="f2">right</TD></TR> +</TABLE>>]; + struct2 [label=< +<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"> + <TR><TD PORT="f0">one</TD><TD>two</TD></TR> +</TABLE>>]; + struct3 [label=< +<B><TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="4"> + <TR> + <TD ROWSPAN="3">hello<BR/>world</TD> + <TD COLSPAN="3">b</TD> + <TD ROWSPAN="3">g</TD> + <TD ROWSPAN="3">h</TD> + </TR> + <TR> + <TD>c</TD><TD PORT="here">d</TD><TD>e</TD> + </TR> + <TR> + <TD COLSPAN="3">f</TD> + </TR> +</TABLE></B>>]; + struct1:f1 -> struct2:f0; + struct1:f2 -> struct3:here; +} \ No newline at end of file diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 33c4f77..66ba55a 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -1045,13 +1045,16 @@ impl TdAttr { } } - pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { + 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 } } @@ -1396,7 +1399,8 @@ impl PlainText { get_size_for_str(&self.text, font_size).x } - pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { + 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; } @@ -1419,6 +1423,8 @@ impl PlainText { } style_attr.text_decoration = self.text_style.text_decoration.clone(); style_attr.baseline_shift = self.text_style.baseline_shift.clone(); + + style_attr } } @@ -1771,7 +1777,8 @@ impl TableGrid { self.font_size = font_size; } - pub(crate) fn update_style_attr(&self, style_attr: &mut StyleAttr) { + 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()); } @@ -1805,5 +1812,7 @@ impl TableGrid { } TableTag::None => {} } + + style_attr } } diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 8e08efa..6cb9fb4 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -258,7 +258,6 @@ fn render_record( fn render_html( rec: &HtmlGrid, - _dir: Orientation, loc: Point, size: Point, look: &StyleAttr, @@ -269,14 +268,7 @@ fn render_html( render_text(text, loc, size, look, canvas); } HtmlGrid::FontTable(table) => { - render_font_table( - table, - loc, - look, - canvas, - Option::None, - Option::None, - ); + render_font_table(table, loc, look, canvas); } } } @@ -318,7 +310,7 @@ fn render_text( look: &StyleAttr, canvas: &mut dyn RenderBackend, ) { - let loc0_x = loc.x; + let loc_0_x = loc.x; let mut loc = loc; loc.y -= rec.height(look.font_size) / 2.; @@ -328,18 +320,16 @@ fn render_text( for t in line { line_width += t.width(look.font_size); } - loc.x -= line_width / 2.; + loc.x = loc_0_x - line_width / 2.; for t in line { - let mut look = look.clone(); - t.update_style_attr(&mut look); - let text_size = get_size_for_str(t.text.as_str(), look.font_size); + 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 loc2 = update_location(loc, size, t.text.as_str(), &look); - canvas.draw_text(loc2, t.text.as_str(), &look); + 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); - loc.x = loc0_x; } } @@ -348,60 +338,60 @@ fn render_font_table( loc: Point, look: &StyleAttr, canvas: &mut dyn RenderBackend, - clip_handle: Option<ClipHandle>, - _clip: Option<ClipHandle>, ) { - let mut look = look.clone(); - - rec.update_style_attr(&mut look); + let look = rec.build_style_attr(look); let table_grid_width = rec.width(); let table_grid_height = rec.height(); - let loc0 = Point::new( + + // 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( - loc0, + 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, - clip_handle, + Option::None, ); for (td_attr, c) in rec.cells.iter() { let cellpadding = rec.cellpadding(c); let cellborder = rec.cellborder(c); - let mut look = look.clone(); - - td_attr.update_style_attr(&mut look); - let cell_size = rec.cell_size(c); let cell_origin = rec.cell_pos(c); + + // center of the cell let cell_loc = Point::new( - loc0.x + cell_origin.x + cell_size.x * 0.5, - loc0.y + cell_origin.y + cell_size.y * 0.5, + loc_0.x + cell_origin.x + cell_size.x * 0.5, + loc_0.y + cell_origin.y + cell_size.y * 0.5, ); - let mut look_border = look.clone(); - look_border.line_width = cellborder as usize; + 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( - loc0.x + cell_origin.x + look_border.line_width as f64 * 0.5, - loc0.y + cell_origin.y + look_border.line_width as f64 * 0.5, + 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_border, + &look_cell_border, + Option::None, Option::None, - clip_handle, ); + + // 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, canvas); + render_cell(&c, cell_loc, size, &look_cell, canvas); } } @@ -414,19 +404,10 @@ fn render_cell( ) { match &rec.label_grid { LabelOrImgGrid::Html(html) => { - render_html( - html, - Orientation::LeftToRight, - loc, - size, - look, - canvas, - ); + 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 mut look = look.clone(); - look.fill_color = Option::None; let image_size = img.size(); let image_size = match &img.scale { Scale::False => Point::new(image_size.x, image_size.y), @@ -574,7 +555,6 @@ impl Renderable for Element { } ShapeKind::Html(rec) => render_html( rec, - self.orientation, self.pos.center(), self.pos.size(false), &self.look, From 34aaae033872f9f4f973f6adf8f0bfd65f7202dc Mon Sep 17 00:00:00 2001 From: mert-kurttutan <kurttutan.mert@gmail.com> Date: Sat, 17 May 2025 23:26:08 +0200 Subject: [PATCH 34/40] Better api for handling image size --- layout/src/core/utils.rs | 17 ++++++++++++++++- layout/src/gv/html.rs | 8 ++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/layout/src/core/utils.rs b/layout/src/core/utils.rs index 88f212e..d9daedf 100644 --- a/layout/src/core/utils.rs +++ b/layout/src/core/utils.rs @@ -13,7 +13,22 @@ pub fn save_to_file(filename: &str, content: &str) -> Result<(), Error> { Result::Ok(()) } -pub(crate) fn get_png_size(filename: &str) -> Result<(u32, u32), Error> { +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]; diff --git a/layout/src/gv/html.rs b/layout/src/gv/html.rs index 66ba55a..49caf49 100644 --- a/layout/src/gv/html.rs +++ b/layout/src/gv/html.rs @@ -6,7 +6,7 @@ use crate::core::style::{ Align, BAlign, BaselineShift, FontStyle, FontWeight, StyleAttr, TextDecoration, VAlign, }; -use crate::core::utils::get_png_size; +use crate::core::utils::get_image_size; pub(crate) fn parse_html_string(input: &str) -> Result<HtmlGrid, String> { let mut parser = HtmlParser { @@ -875,16 +875,16 @@ impl Image { } fn width(&self) -> f64 { - let size = get_png_size(&self.source).unwrap(); + let size = get_image_size(&self.source).unwrap(); size.0 as f64 } fn height(&self) -> f64 { - let size = get_png_size(&self.source).unwrap(); + let size = get_image_size(&self.source).unwrap(); size.1 as f64 } pub(crate) fn size(&self) -> Point { - let size = get_png_size(&self.source).unwrap(); + let size = get_image_size(&self.source).unwrap(); Point::new(size.0 as f64, size.1 as f64) } } From 342cb3c1e67b9b90a7c905589af3a1855732a85b Mon Sep 17 00:00:00 2001 From: mert-kurttutan <kurttutan.mert@gmail.com> Date: Sat, 17 May 2025 23:27:01 +0200 Subject: [PATCH 35/40] rustfmt --- layout/src/core/utils.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/layout/src/core/utils.rs b/layout/src/core/utils.rs index d9daedf..628f8a1 100644 --- a/layout/src/core/utils.rs +++ b/layout/src/core/utils.rs @@ -14,7 +14,6 @@ pub fn save_to_file(filename: &str, content: &str) -> Result<(), Error> { } 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); } @@ -27,7 +26,6 @@ pub(crate) fn get_image_size(filename: &str) -> Result<(u32, u32), Error> { )) } - fn get_png_size(filename: &str) -> Result<(u32, u32), Error> { let mut f = File::open(filename)?; let mut signature = [0; 8]; From a91d60b688f92a9e78eca13917c61fe6b0183f29 Mon Sep 17 00:00:00 2001 From: mert-kurttutan <kurttutan.mert@gmail.com> Date: Sun, 8 Jun 2025 14:29:40 +0200 Subject: [PATCH 36/40] make html into variant of shape content --- layout/src/gv/builder.rs | 45 ++++++-- layout/src/std_shapes/render.rs | 187 ++++++++++++++++++++++++-------- layout/src/std_shapes/shapes.rs | 23 ++-- 3 files changed, 194 insertions(+), 61 deletions(-) diff --git a/layout/src/gv/builder.rs b/layout/src/gv/builder.rs index aa37047..a1f4558 100644 --- a/layout/src/gv/builder.rs +++ b/layout/src/gv/builder.rs @@ -308,25 +308,27 @@ 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(x) = lst.get(&"label".to_string()) { // label = val.clone(); match x { DotString::String(val) => { - label = val.clone(); - shape = ShapeKind::Circle(label.clone()); + label = ShapeContent::String(val.clone()); + shape = + ShapeKind::Circle(ShapeContent::String(val.clone())); } DotString::HtmlString(val) => { - label = val.clone(); - shape = ShapeKind::Html(parse_html_string(val).unwrap()); + label = ShapeContent::Html(parse_html_string(val).unwrap()); + shape = ShapeKind::Circle(label.clone()); } } } @@ -345,11 +347,22 @@ 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(_) => {} + } } _ => {} } @@ -403,11 +416,25 @@ impl GraphBuilder { // grow top down the records grow to the left. let dir = dir.flip(); + // match &mut shape { + // ShapeKind::Html(HtmlGrid::FontTable(x)) => { + // x.resize(font_size); + // } + + // _ => {} + // } match &mut shape { - ShapeKind::Html(HtmlGrid::FontTable(x)) => { + 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); } - _ => {} } diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index 6cb9fb4..e621bdc 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -43,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 @@ -54,29 +61,31 @@ 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.) } } - ShapeKind::Html(html_grid) => html_grid.size(font), ShapeKind::None => Point::new(1., 1.), }; if make_xy_same { @@ -526,6 +535,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 { @@ -553,13 +579,6 @@ impl Renderable for Element { canvas, ); } - ShapeKind::Html(rec) => render_html( - rec, - self.pos.center(), - self.pos.size(false), - &self.look, - canvas, - ), ShapeKind::Box(text) => { canvas.draw_rect( self.pos.bbox(false).0, @@ -568,7 +587,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( @@ -577,7 +602,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( @@ -597,7 +629,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 { @@ -618,7 +656,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, + ); } } } @@ -659,36 +704,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) - } - ShapeKind::Html(html) => { - let mut loc = self.pos.center(); - let mut size = self.pos.size(false); - if let Some(port_name) = port { - let mut visitor = Locator { - port_name: port_name.to_string(), - loc, - size, - }; - let r = - get_html_port_location(html, loc, size, &mut visitor); - loc = r.0; - size = r.1; + // 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) + } } - get_connection_point_for_box(loc, size, from, force) } _ => { unreachable!(); diff --git a/layout/src/std_shapes/shapes.rs b/layout/src/std_shapes/shapes.rs index 807762f..e22fdce 100644 --- a/layout/src/std_shapes/shapes.rs +++ b/layout/src/std_shapes/shapes.rs @@ -19,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: @@ -39,23 +45,22 @@ impl RecordDef { #[derive(Debug, Clone)] pub enum ShapeKind { None, - Box(String), - Circle(String), - DoubleCircle(String), + Box(ShapeContent), + Circle(ShapeContent), + DoubleCircle(ShapeContent), Record(RecordDef), - Connector(Option<String>), - Html(HtmlGrid), + 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()) @@ -64,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()))) } } From d0f0b237ed41732d088cb8b42b724c3a02ab0598 Mon Sep 17 00:00:00 2001 From: mert-kurttutan <kurttutan.mert@gmail.com> Date: Sun, 8 Jun 2025 14:32:42 +0200 Subject: [PATCH 37/40] Update benchmark after ShapeContent update --- examples/benchmark.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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() { From 174b2150223f561fe267ec41048af989314e756e Mon Sep 17 00:00:00 2001 From: mert-kurttutan <kurttutan.mert@gmail.com> Date: Sun, 8 Jun 2025 18:55:16 +0200 Subject: [PATCH 38/40] Process html string at the level of graph statements --- layout/src/gv/parser/ast.rs | 5 +- layout/src/gv/parser/parser.rs | 86 ++++++++++++++-------------------- 2 files changed, 37 insertions(+), 54 deletions(-) diff --git a/layout/src/gv/parser/ast.rs b/layout/src/gv/parser/ast.rs index 4aa8401..ff56737 100644 --- a/layout/src/gv/parser/ast.rs +++ b/layout/src/gv/parser/ast.rs @@ -45,9 +45,8 @@ impl AttributeList { .push((from.to_string(), DotString::String(to.to_string()))); } - pub fn add_attr_html(&mut self, from: &str, to: &str) { - self.list - .push((from.to_string(), DotString::HtmlString(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, DotString)> { diff --git a/layout/src/gv/parser/parser.rs b/layout/src/gv/parser/parser.rs index fe5efd9..a3dcd61 100644 --- a/layout/src/gv/parser/parser.rs +++ b/layout/src/gv/parser/parser.rs @@ -1,6 +1,7 @@ use super::ast; use super::lexer::Lexer; use super::lexer::Token; +use crate::gv::parser::ast::DotString; #[derive(Debug, Clone)] pub struct DotParser { @@ -155,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); @@ -246,34 +256,8 @@ impl DotParser { } else { return to_error("Expected '='"); } - - if let Token::HtmlStart = self.tok.clone() { - if prop == "label" { - let html = self.parse_html_string()?; - lst.add_attr_html(&prop, &html); - // self.lexer.mode = super::lexer::LexerMode::Normal; - if let Token::HtmlEnd = self.tok.clone() { - self.lex(); - } else { - return to_error( - format!("Expected '>', found {:?}", self.tok) - .as_str(), - ); - } - } - } else if let Token::Identifier(value) = self.tok.clone() { - lst.add_attr_str(&prop, &value); - // Consume the value name. - self.lex(); - } else { - return to_error( - format!( - "Expected value after assignment, found {:?}", - self.tok - ) - .as_str(), - ); - } + let value = self.parse_attr_id()?; + lst.add_attr(prop, value); // Skip semicolon. if let Token::Semicolon = self.tok.clone() { @@ -307,30 +291,30 @@ impl DotParser { } // 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_str(&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 ] From 030013743e8d9d590f9260032971026e05f37ad7 Mon Sep 17 00:00:00 2001 From: mert-kurttutan <kurttutan.mert@gmail.com> Date: Sun, 8 Jun 2025 19:17:34 +0200 Subject: [PATCH 39/40] Fix the default shape and introduce no shape with text content --- layout/src/gv/builder.rs | 9 ++++++- layout/src/std_shapes/render.rs | 45 ++++++++++++++++++++++++++++++--- layout/src/std_shapes/shapes.rs | 2 +- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/layout/src/gv/builder.rs b/layout/src/gv/builder.rs index a1f4558..d9ec820 100644 --- a/layout/src/gv/builder.rs +++ b/layout/src/gv/builder.rs @@ -328,7 +328,7 @@ impl GraphBuilder { } DotString::HtmlString(val) => { label = ShapeContent::Html(parse_html_string(val).unwrap()); - shape = ShapeKind::Circle(label.clone()); + shape = ShapeKind::None(label.clone()); } } } @@ -364,6 +364,10 @@ impl GraphBuilder { ShapeContent::Html(_) => {} } } + "circle" => { + shape = ShapeKind::Circle(label); + make_xy_same = true; + } _ => {} } } @@ -435,6 +439,9 @@ impl GraphBuilder { )) => { x.resize(font_size); } + ShapeKind::None(ShapeContent::Html(HtmlGrid::FontTable(x))) => { + x.resize(font_size); + } _ => {} } diff --git a/layout/src/std_shapes/render.rs b/layout/src/std_shapes/render.rs index e621bdc..15afae0 100644 --- a/layout/src/std_shapes/render.rs +++ b/layout/src/std_shapes/render.rs @@ -86,7 +86,10 @@ pub fn get_shape_size( Point::new(1., 1.) } } - ShapeKind::None => 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); @@ -568,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, @@ -684,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); diff --git a/layout/src/std_shapes/shapes.rs b/layout/src/std_shapes/shapes.rs index e22fdce..5df8253 100644 --- a/layout/src/std_shapes/shapes.rs +++ b/layout/src/std_shapes/shapes.rs @@ -44,7 +44,7 @@ impl RecordDef { #[derive(Debug, Clone)] pub enum ShapeKind { - None, + None(ShapeContent), Box(ShapeContent), Circle(ShapeContent), DoubleCircle(ShapeContent), From a741bb43c37b431f4b66b9e4f26654b7e95e869c Mon Sep 17 00:00:00 2001 From: mert-kurttutan <kurttutan.mert@gmail.com> Date: Sun, 8 Jun 2025 19:19:57 +0200 Subject: [PATCH 40/40] remove stale comment --- layout/src/gv/builder.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/layout/src/gv/builder.rs b/layout/src/gv/builder.rs index d9ec820..fd772b2 100644 --- a/layout/src/gv/builder.rs +++ b/layout/src/gv/builder.rs @@ -420,13 +420,6 @@ impl GraphBuilder { // grow top down the records grow to the left. let dir = dir.flip(); - // match &mut shape { - // ShapeKind::Html(HtmlGrid::FontTable(x)) => { - // x.resize(font_size); - // } - - // _ => {} - // } match &mut shape { ShapeKind::Circle(ShapeContent::Html(HtmlGrid::FontTable(x))) => { x.resize(font_size);