use std::{
collections::{self, HashMap},
fmt, io,
};
pub mod notactuallysvg;
pub use crate::notactuallysvg as svg;
use crate::svg::HDir;
mod nodes;
pub use crate::nodes::containers::{Choice, Sequence, Stack};
pub use crate::nodes::grids::{HorizontalGrid, VerticalGrid};
pub use crate::nodes::text::{Comment, NonTerminal, Terminal};
pub use crate::nodes::wrappers::{LabeledBox, Link, LinkTarget, Optional, Repeat};
#[cfg(feature = "resvg")]
pub mod render;
#[cfg(feature = "resvg")]
pub use resvg;
#[doc = include_str!("../README.md")]
#[allow(dead_code)]
type _READMETEST = ();
const ARC_RADIUS: i64 = 12;
fn text_width(s: &str) -> usize {
use unicode_width::UnicodeWidthStr;
s.width() + (s.width() / 20)
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
#[non_exhaustive]
pub enum Stylesheet {
#[default]
Light,
Dark,
LightRendersafe,
DarkRendersafe,
}
impl Stylesheet {
#[must_use]
pub const fn to_dark(&self) -> Self {
match self {
Self::Light | Self::Dark => Self::Dark,
Self::LightRendersafe | Self::DarkRendersafe => Self::DarkRendersafe,
}
}
#[must_use]
pub const fn to_light(&self) -> Self {
match self {
Self::Light | Self::Dark => Self::Light,
Self::LightRendersafe | Self::DarkRendersafe => Self::LightRendersafe,
}
}
#[must_use]
pub const fn is_light(&self) -> bool {
matches!(self, Self::Light | Self::LightRendersafe)
}
#[must_use]
pub const fn stylesheet(self) -> &'static str {
match self {
Self::Light => include_str!("stylesheet_light.css"),
Self::Dark => include_str!("stylesheet_dark.css"),
Self::LightRendersafe => include_str!("stylesheet_light_safe.css"),
Self::DarkRendersafe => include_str!("stylesheet_dark_safe.css"),
}
}
}
pub const DEFAULT_CSS: &str = Stylesheet::Light.stylesheet();
#[derive(Debug, Clone)]
pub struct NodeGeometry {
pub entry_height: i64,
pub height: i64,
pub width: i64,
pub children: Vec<NodeGeometry>,
}
impl NodeGeometry {
#[must_use]
pub fn height_below_entry(&self) -> i64 {
self.height - self.entry_height
}
}
pub trait Node {
fn entry_height(&self) -> i64;
fn height(&self) -> i64;
fn width(&self) -> i64;
fn height_below_entry(&self) -> i64 {
self.height() - self.entry_height()
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element;
fn compute_geometry(&self) -> NodeGeometry {
NodeGeometry {
entry_height: self.entry_height(),
height: self.height(),
width: self.width(),
children: vec![],
}
}
fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, _geo: &NodeGeometry) -> svg::Element {
self.draw(x, y, h_dir)
}
fn render(&self, out: &mut svg::Renderer<'_>, x: i64, y: i64, h_dir: HDir) -> fmt::Result {
let geo = self.compute_geometry();
self.render_with_geometry(out, x, y, h_dir, &geo)
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
out.write_display(self.draw_with_geometry(x, y, h_dir, geo))
}
}
impl fmt::Debug for dyn Node {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Node")
.field("entry_height", &self.entry_height())
.field("height", &self.height())
.field("width", &self.width())
.finish()
}
}
macro_rules! deref_impl {
($($sig:tt)+) => {
impl $($sig)+ {
fn entry_height(&self) -> i64 {
(**self).entry_height()
}
fn height(&self) -> i64 {
(**self).height()
}
fn width(&self) -> i64 {
(**self).width()
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
(**self).draw(x, y, h_dir)
}
fn compute_geometry(&self) -> NodeGeometry {
(**self).compute_geometry()
}
fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, geo: &NodeGeometry) -> svg::Element {
(**self).draw_with_geometry(x, y, h_dir, geo)
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
(**self).render_with_geometry(out, x, y, h_dir, geo)
}
}
};
}
deref_impl!(<'a, N> Node for &'a N where N: Node + ?Sized);
deref_impl!(<'a, N> Node for &'a mut N where N: Node + ?Sized);
deref_impl!(<N> Node for Box<N> where N: Node + ?Sized);
deref_impl!(<N> Node for std::rc::Rc<N> where N: Node + ?Sized);
deref_impl!(<N> Node for std::sync::Arc<N> where N: Node + ?Sized);
#[cfg(feature = "visual-debug")]
fn add_debug_attrs(
tag: &mut svg::StartTag<'_, '_>,
name: &str,
x: i64,
y: i64,
geo: &NodeGeometry,
) -> fmt::Result {
tag.attr("railroad:type", name)?;
tag.attr("railroad:x", x)?;
tag.attr("railroad:y", y)?;
tag.attr("railroad:entry_height", geo.entry_height)?;
tag.attr("railroad:height", geo.height)?;
tag.attr("railroad:width", geo.width)
}
#[cfg(not(feature = "visual-debug"))]
fn add_debug_attrs(
_tag: &mut svg::StartTag<'_, '_>,
_name: &str,
_x: i64,
_y: i64,
_geo: &NodeGeometry,
) -> fmt::Result {
Ok(())
}
#[cfg(feature = "visual-debug")]
fn write_debug_overlay(
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
geo: &NodeGeometry,
) -> fmt::Result {
out.path_with_class(
&svg::PathData::new(HDir::LTR)
.move_to(x, y)
.horizontal(geo.width)
.vertical(5)
.move_rel(-geo.width, -5)
.vertical(geo.height)
.horizontal(5)
.move_rel(-5, -geo.height)
.move_rel(0, geo.entry_height)
.horizontal(10),
"debug",
)
}
#[cfg(not(feature = "visual-debug"))]
fn write_debug_overlay(
_out: &mut svg::Renderer<'_>,
_x: i64,
_y: i64,
_geo: &NodeGeometry,
) -> fmt::Result {
Ok(())
}
trait RenderBackend {
fn push_path(&mut self, path: svg::PathData) -> fmt::Result;
fn push_rect(&mut self, x: i64, y: i64, width: i64, height: i64) -> fmt::Result;
fn push_rounded_rect(
&mut self,
x: i64,
y: i64,
width: i64,
height: i64,
radius: i64,
) -> fmt::Result;
fn push_text(&mut self, x: i64, y: i64, text: &str) -> fmt::Result;
fn push_child<N: Node + ?Sized>(
&mut self,
child: &N,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result;
}
#[derive(Default)]
struct ElementBackend {
children: Vec<svg::Element>,
}
impl ElementBackend {
fn finish_group(
self,
attrs: &HashMap<String, String>,
name: &str,
x: i64,
y: i64,
geo: &NodeGeometry,
) -> svg::Element {
let mut group = svg::Element::new("g").set_all(attrs.iter());
for child in self.children {
group.push(child);
}
group.debug_with_geometry(name, x, y, geo)
}
}
impl RenderBackend for ElementBackend {
fn push_path(&mut self, path: svg::PathData) -> fmt::Result {
self.children.push(path.into_path());
Ok(())
}
fn push_rect(&mut self, x: i64, y: i64, width: i64, height: i64) -> fmt::Result {
self.children.push(
svg::Element::new("rect")
.set("x", &x)
.set("y", &y)
.set("height", &height)
.set("width", &width),
);
Ok(())
}
fn push_rounded_rect(
&mut self,
x: i64,
y: i64,
width: i64,
height: i64,
radius: i64,
) -> fmt::Result {
self.children.push(
svg::Element::new("rect")
.set("x", &x)
.set("y", &y)
.set("height", &height)
.set("width", &width)
.set("rx", &radius)
.set("ry", &radius),
);
Ok(())
}
fn push_text(&mut self, x: i64, y: i64, text: &str) -> fmt::Result {
self.children.push(
svg::Element::new("text")
.set("x", &x)
.set("y", &y)
.text(text),
);
Ok(())
}
fn push_child<N: Node + ?Sized>(
&mut self,
child: &N,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
self.children
.push(child.draw_with_geometry(x, y, h_dir, geo));
Ok(())
}
}
struct RendererBackend<'a, 'b> {
out: &'a mut svg::Renderer<'b>,
}
impl RenderBackend for RendererBackend<'_, '_> {
fn push_path(&mut self, path: svg::PathData) -> fmt::Result {
self.out.path(&path)
}
fn push_rect(&mut self, x: i64, y: i64, width: i64, height: i64) -> fmt::Result {
let mut rect = self.out.start_element("rect")?;
rect.attr("x", x)?;
rect.attr("y", y)?;
rect.attr("height", height)?;
rect.attr("width", width)?;
rect.finish_empty()
}
fn push_rounded_rect(
&mut self,
x: i64,
y: i64,
width: i64,
height: i64,
radius: i64,
) -> fmt::Result {
let mut rect = self.out.start_element("rect")?;
rect.attr("x", x)?;
rect.attr("y", y)?;
rect.attr("height", height)?;
rect.attr("width", width)?;
rect.attr("rx", radius)?;
rect.attr("ry", radius)?;
rect.finish_empty()
}
fn push_text(&mut self, x: i64, y: i64, text: &str) -> fmt::Result {
self.out.text_element("text", text, |tag| {
tag.attr("x", x)?;
tag.attr("y", y)
})
}
fn push_child<N: Node + ?Sized>(
&mut self,
child: &N,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
child.render_with_geometry(self.out, x, y, h_dir, geo)
}
}
fn draw_group_with_geometry(
attrs: &HashMap<String, String>,
name: &str,
x: i64,
y: i64,
geo: &NodeGeometry,
emit: impl FnOnce(&mut ElementBackend) -> fmt::Result,
) -> svg::Element {
let mut backend = ElementBackend::default();
emit(&mut backend).expect("element backend is infallible");
backend.finish_group(attrs, name, x, y, geo)
}
fn render_group_with_geometry(
out: &mut svg::Renderer<'_>,
attrs: &HashMap<String, String>,
name: &str,
x: i64,
y: i64,
geo: &NodeGeometry,
emit: impl FnOnce(&mut RendererBackend<'_, '_>) -> fmt::Result,
) -> fmt::Result {
let mut group = out.start_element("g")?;
group.attr_hashmap(attrs)?;
add_debug_attrs(&mut group, name, x, y, geo)?;
group.finish()?;
let mut backend = RendererBackend { out };
emit(&mut backend)?;
write_debug_overlay(backend.out, x, y, geo)?;
backend.out.end_element("g")
}
fn draw_class_group_with_geometry(
class: &str,
name: &str,
x: i64,
y: i64,
geo: &NodeGeometry,
emit: impl FnOnce(&mut ElementBackend) -> fmt::Result,
) -> svg::Element {
let mut backend = ElementBackend::default();
emit(&mut backend).expect("element backend is infallible");
let mut group = svg::Element::new("g").set("class", &class);
for child in backend.children {
group.push(child);
}
group.debug_with_geometry(name, x, y, geo)
}
fn render_class_group_with_geometry(
out: &mut svg::Renderer<'_>,
class: &str,
name: &str,
x: i64,
y: i64,
geo: &NodeGeometry,
emit: impl FnOnce(&mut RendererBackend<'_, '_>) -> fmt::Result,
) -> fmt::Result {
let mut group = out.start_element("g")?;
group.attr("class", class)?;
add_debug_attrs(&mut group, name, x, y, geo)?;
group.finish()?;
let mut backend = RendererBackend { out };
emit(&mut backend)?;
write_debug_overlay(backend.out, x, y, geo)?;
backend.out.end_element("g")
}
fn draw_debug_path(
name: &str,
x: i64,
y: i64,
geo: &NodeGeometry,
path: svg::PathData,
) -> svg::Element {
path.into_path().debug_with_geometry(name, x, y, geo)
}
fn render_debug_path(
out: &mut svg::Renderer<'_>,
name: &str,
x: i64,
y: i64,
geo: &NodeGeometry,
path: svg::PathData,
) -> fmt::Result {
let mut tag = out.start_element("path")?;
tag.attr("d", path)?;
add_debug_attrs(&mut tag, name, x, y, geo)?;
tag.finish_empty()?;
write_debug_overlay(out, x, y, geo)
}
fn emit_text_box<B: RenderBackend>(
backend: &mut B,
x: i64,
y: i64,
geo: &NodeGeometry,
label: &str,
rounded: bool,
) -> fmt::Result {
if rounded {
backend.push_rounded_rect(x, y, geo.width, geo.height, 10)?;
} else {
backend.push_rect(x, y, geo.width, geo.height)?;
}
backend.push_text(x + geo.width / 2, y + geo.entry_height + 5, label)
}
pub trait NodeCollection {
fn max_entry_height(self) -> i64;
fn max_height(self) -> i64;
fn max_height_below_entry(self) -> i64;
fn max_width(self) -> i64;
fn total_width(self) -> i64;
fn total_height(self) -> i64;
}
impl<I, N> NodeCollection for I
where
I: IntoIterator<Item = N>,
N: Node,
{
fn max_height_below_entry(self) -> i64 {
self.into_iter()
.map(|n| n.height_below_entry())
.max()
.unwrap_or_default()
}
fn max_entry_height(self) -> i64 {
self.into_iter()
.map(|n| n.entry_height())
.max()
.unwrap_or_default()
}
fn max_height(self) -> i64 {
self.into_iter()
.map(|n| n.height())
.max()
.unwrap_or_default()
}
fn max_width(self) -> i64 {
self.into_iter()
.map(|n| n.width())
.max()
.unwrap_or_default()
}
fn total_width(self) -> i64 {
self.into_iter().map(|n| n.width()).sum()
}
fn total_height(self) -> i64 {
self.into_iter().map(|n| n.height()).sum()
}
}
#[derive(Debug, Clone, Default)]
pub struct End;
impl Node for End {
fn entry_height(&self) -> i64 {
10
}
fn height(&self) -> i64 {
20
}
fn width(&self) -> i64 {
20
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
draw_debug_path(
"End",
x,
y,
&self.compute_geometry(),
svg::PathData::new(h_dir)
.move_to(x, y + 10)
.horizontal(20)
.move_rel(-10, -10)
.vertical(20)
.move_rel(10, -20)
.vertical(20),
)
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
render_debug_path(
out,
"End",
x,
y,
geo,
svg::PathData::new(h_dir)
.move_to(x, y + 10)
.horizontal(20)
.move_rel(-10, -10)
.vertical(20)
.move_rel(10, -20)
.vertical(20),
)
}
}
#[derive(Debug, Clone, Default)]
pub struct SimpleStart;
impl Node for SimpleStart {
fn entry_height(&self) -> i64 {
5
}
fn height(&self) -> i64 {
10
}
fn width(&self) -> i64 {
15
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
draw_debug_path(
"SimpleStart",
x,
y,
&self.compute_geometry(),
svg::PathData::new(h_dir)
.move_to(x, y + 5)
.arc(5, svg::Arc::SouthToEast)
.arc(5, svg::Arc::WestToSouth)
.arc(5, svg::Arc::NorthToWest)
.arc(5, svg::Arc::EastToNorth)
.move_rel(10, 0)
.horizontal(5),
)
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
render_debug_path(
out,
"SimpleStart",
x,
y,
geo,
svg::PathData::new(h_dir)
.move_to(x, y + 5)
.arc(5, svg::Arc::SouthToEast)
.arc(5, svg::Arc::WestToSouth)
.arc(5, svg::Arc::NorthToWest)
.arc(5, svg::Arc::EastToNorth)
.move_rel(10, 0)
.horizontal(5),
)
}
}
#[derive(Debug, Clone, Default)]
pub struct SimpleEnd;
impl Node for SimpleEnd {
fn entry_height(&self) -> i64 {
5
}
fn height(&self) -> i64 {
10
}
fn width(&self) -> i64 {
15
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
draw_debug_path(
"SimpleEnd",
x,
y,
&self.compute_geometry(),
svg::PathData::new(h_dir)
.move_to(x, y + 5)
.horizontal(5)
.arc(5, svg::Arc::SouthToEast)
.arc(5, svg::Arc::WestToSouth)
.arc(5, svg::Arc::NorthToWest)
.arc(5, svg::Arc::EastToNorth),
)
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
render_debug_path(
out,
"SimpleEnd",
x,
y,
geo,
svg::PathData::new(h_dir)
.move_to(x, y + 5)
.horizontal(5)
.arc(5, svg::Arc::SouthToEast)
.arc(5, svg::Arc::WestToSouth)
.arc(5, svg::Arc::NorthToWest)
.arc(5, svg::Arc::EastToNorth),
)
}
}
#[derive(Debug, Clone, Default)]
pub struct Start;
impl Node for Start {
fn entry_height(&self) -> i64 {
10
}
fn height(&self) -> i64 {
20
}
fn width(&self) -> i64 {
20
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
draw_debug_path(
"Start",
x,
y,
&self.compute_geometry(),
svg::PathData::new(h_dir)
.move_to(x, y)
.vertical(20)
.move_rel(10, -20)
.vertical(20)
.move_rel(-10, -10)
.horizontal(20),
)
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
render_debug_path(
out,
"Start",
x,
y,
geo,
svg::PathData::new(h_dir)
.move_to(x, y)
.vertical(20)
.move_rel(10, -20)
.vertical(20)
.move_rel(-10, -10)
.horizontal(20),
)
}
}
#[derive(Debug)]
#[doc(hidden)]
pub struct Debug {
entry_height: i64,
height: i64,
width: i64,
attributes: HashMap<String, String>,
}
impl Debug {
#[must_use]
pub fn new(entry_height: i64, height: i64, width: i64) -> Self {
assert!(entry_height < height);
let mut d = Self {
entry_height,
height,
width,
attributes: HashMap::default(),
};
d.attributes.insert("class".to_owned(), "debug".to_owned());
d.attributes.insert(
"style".to_owned(),
"fill: hsla(0, 100%, 90%, 0.9); stroke-width: 2; stroke: red".to_owned(),
);
d
}
}
impl Node for Debug {
fn entry_height(&self) -> i64 {
self.entry_height
}
fn height(&self) -> i64 {
self.height
}
fn width(&self) -> i64 {
self.width
}
fn draw(&self, x: i64, y: i64, _: HDir) -> svg::Element {
svg::Element::new("rect")
.set("x", &x)
.set("y", &y)
.set("height", &self.height())
.set("width", &self.width())
.set_all(self.attributes.iter())
.debug("Debug", x, y, self)
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
_h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
let mut rect = out.start_element("rect")?;
rect.attr("x", x)?;
rect.attr("y", y)?;
rect.attr("height", geo.height)?;
rect.attr("width", geo.width)?;
rect.attr_hashmap(&self.attributes)?;
add_debug_attrs(&mut rect, "Debug", x, y, geo)?;
rect.finish_empty()?;
write_debug_overlay(out, x, y, geo)
}
}
#[derive(Debug, Clone, Default)]
pub struct Empty;
impl Node for Empty {
fn entry_height(&self) -> i64 {
0
}
fn height(&self) -> i64 {
0
}
fn width(&self) -> i64 {
0
}
fn draw(&self, x: i64, y: i64, _: HDir) -> svg::Element {
svg::Element::new("g").debug("Empty", x, y, self)
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
_h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
let mut g = out.start_element("g")?;
add_debug_attrs(&mut g, "Empty", x, y, geo)?;
g.finish()?;
write_debug_overlay(out, x, y, geo)?;
out.end_element("g")
}
}
#[derive(Debug, Clone)]
pub struct Diagram<N> {
root: N,
extra_attributes: HashMap<String, String>,
extra_elements: Vec<svg::Element>,
left_padding: i64,
right_padding: i64,
top_padding: i64,
bottom_padding: i64,
}
impl<N: Node> Diagram<N> {
pub fn new(root: N) -> Self {
Self {
root,
extra_attributes: HashMap::default(),
extra_elements: Vec::default(),
left_padding: 10,
right_padding: 10,
top_padding: 10,
bottom_padding: 10,
}
}
pub fn new_with_stylesheet(root: N, style: &Stylesheet) -> Self {
let mut dia = Self::new(root);
dia.add_stylesheet(style);
dia
}
pub fn with_default_css(root: N) -> Self {
let mut dia = Self::new(root);
dia.add_default_css();
dia
}
pub fn add_stylesheet(&mut self, style: &Stylesheet) {
self.add_css(style.stylesheet());
}
pub fn add_default_css(&mut self) {
self.add_css(DEFAULT_CSS);
}
pub fn add_css(&mut self, css: &str) {
self.add_element(
svg::Element::new("style")
.set("type", "text/css")
.raw_text(css),
);
}
pub fn attr(&mut self, key: String) -> collections::hash_map::Entry<'_, String, String> {
self.extra_attributes.entry(key)
}
pub fn add_element(&mut self, e: svg::Element) -> &mut Self {
self.extra_elements.push(e);
self
}
pub fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
writer.write_all(self.to_string().as_bytes())
}
pub fn into_inner(self) -> N {
self.root
}
}
impl<N> Default for Diagram<N>
where
N: Default,
{
fn default() -> Self {
Self {
root: Default::default(),
extra_attributes: HashMap::default(),
extra_elements: Vec::default(),
left_padding: 10,
right_padding: 10,
top_padding: 10,
bottom_padding: 10,
}
}
}
impl<N> Node for Diagram<N>
where
N: Node,
{
fn entry_height(&self) -> i64 {
0
}
fn height(&self) -> i64 {
self.top_padding + self.root.height() + self.bottom_padding
}
fn width(&self) -> i64 {
self.left_padding + self.root.width() + self.right_padding
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
let geo = self.compute_geometry();
self.draw_with_geometry(x, y, h_dir, &geo)
}
fn compute_geometry(&self) -> NodeGeometry {
let root_geo = self.root.compute_geometry();
let height = self.top_padding + root_geo.height + self.bottom_padding;
let width = self.left_padding + root_geo.width + self.right_padding;
NodeGeometry {
entry_height: 0,
height,
width,
children: vec![root_geo],
}
}
fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, geo: &NodeGeometry) -> svg::Element {
let mut e = svg::Element::new("svg")
.set("xmlns", "http://www.w3.org/2000/svg")
.set("xmlns:xlink", "http://www.w3.org/1999/xlink")
.set("class", "railroad")
.set("viewBox", &format!("0 0 {} {}", geo.width, geo.height));
#[cfg(feature = "visual-debug")]
{
e = e.set("xmlns:railroad", "http://www.github.com/lukaslueg/railroad");
}
for (k, v) in &self.extra_attributes {
e = e.set(&k, &v);
}
for extra_ele in self.extra_elements.iter().cloned() {
e = e.add(extra_ele);
}
e.add(
svg::Element::new("rect")
.set("width", "100%")
.set("height", "100%")
.set("class", "railroad_canvas"),
)
.add(self.root.draw_with_geometry(
x + self.left_padding,
y + self.top_padding,
h_dir,
&geo.children[0],
))
}
fn render_with_geometry(
&self,
out: &mut svg::Renderer<'_>,
x: i64,
y: i64,
h_dir: HDir,
geo: &NodeGeometry,
) -> fmt::Result {
let mut svg_tag = out.start_element("svg")?;
svg_tag.attr("xmlns", "http://www.w3.org/2000/svg")?;
svg_tag.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")?;
svg_tag.attr("class", "railroad")?;
svg_tag.attr("viewBox", format_args!("0 0 {} {}", geo.width, geo.height))?;
#[cfg(feature = "visual-debug")]
svg_tag.attr("xmlns:railroad", "http://www.github.com/lukaslueg/railroad")?;
svg_tag.attr_hashmap(&self.extra_attributes)?;
svg_tag.finish()?;
for extra in &self.extra_elements {
out.write_display(extra)?;
}
let mut rect = out.start_element("rect")?;
rect.attr("width", "100%")?;
rect.attr("height", "100%")?;
rect.attr("class", "railroad_canvas")?;
rect.finish_empty()?;
self.root.render_with_geometry(
out,
x + self.left_padding,
y + self.top_padding,
h_dir,
&geo.children[0],
)?;
out.end_element("svg")
}
}
impl<N> fmt::Display for Diagram<N>
where
N: Node,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let geo = self.compute_geometry();
let mut renderer = svg::Renderer::new(f);
self.render_with_geometry(&mut renderer, 0, 0, HDir::LTR, &geo)
}
}
#[cfg(test)]
#[cfg(not(feature = "visual-debug"))]
mod tests_without_visual_debug {
use super::*;
use std::cell::Cell;
struct CountingNode<'a> {
inner: Box<dyn Node>,
calls: &'a Cell<usize>,
}
impl Node for CountingNode<'_> {
fn entry_height(&self) -> i64 {
self.calls.set(self.calls.get() + 1);
self.inner.entry_height()
}
fn height(&self) -> i64 {
self.calls.set(self.calls.get() + 1);
self.inner.height()
}
fn width(&self) -> i64 {
self.calls.set(self.calls.get() + 1);
self.inner.width()
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
self.inner.draw(x, y, h_dir)
}
}
#[test]
fn geometry_cache_linear_calls() {
let calls = Cell::new(0usize);
let leaf = CountingNode {
inner: Box::new(Terminal::new("leaf".to_owned())),
calls: &calls,
};
let inner_seq: Sequence<Box<dyn Node>> =
Sequence::new(vec![Box::new(leaf) as Box<dyn Node>]);
let outer_seq: Sequence<Box<dyn Node>> =
Sequence::new(vec![Box::new(inner_seq) as Box<dyn Node>]);
let geo = outer_seq.compute_geometry();
let _ = outer_seq.draw_with_geometry(0, 0, HDir::LTR, &geo);
assert_eq!(
calls.get(),
3,
"each leaf geometry method must be called exactly once"
);
}
}
#[cfg(test)]
#[cfg(feature = "visual-debug")]
mod tests_with_visual_debug {
use super::*;
use std::cell::Cell;
struct CountingNode<'a> {
inner: Box<dyn Node>,
calls: &'a Cell<usize>,
}
impl Node for CountingNode<'_> {
fn entry_height(&self) -> i64 {
self.calls.set(self.calls.get() + 1);
self.inner.entry_height()
}
fn height(&self) -> i64 {
self.calls.set(self.calls.get() + 1);
self.inner.height()
}
fn width(&self) -> i64 {
self.calls.set(self.calls.get() + 1);
self.inner.width()
}
fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
self.inner.draw(x, y, h_dir)
}
}
#[test]
fn visual_debug_geometry_cache_linear_calls() {
let calls = Cell::new(0usize);
let leaf = CountingNode {
inner: Box::new(Terminal::new("leaf".to_owned())),
calls: &calls,
};
let mut nested: Box<dyn Node> = Box::new(leaf);
for _ in 0..8 {
nested = Box::new(Sequence::new(vec![nested]));
}
let geo = nested.compute_geometry();
let _ = nested.draw_with_geometry(0, 0, HDir::LTR, &geo);
assert_eq!(
calls.get(),
3,
"visual-debug must not trigger extra geometry calls during draw_with_geometry"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn debug_impl() {
let s = Sequence::new(vec![
Box::new(SimpleStart) as Box<dyn Node>,
Box::new(SimpleEnd),
]);
assert_eq!(
"Sequence { children: [Node { entry_height: 5, height: 10, width: 15 }, Node { entry_height: 5, height: 10, width: 15 }], spacing: 10 }",
format!("{:?}", &s)
);
assert_eq!(
"Node { entry_height: 5, height: 10, width: 40 }",
format!("{:?}", &s as &dyn Node)
);
}
fn make_deep_seq(depth: usize, width: usize) -> Box<dyn Node> {
if depth == 0 {
Box::new(Terminal::new("x".to_owned()))
} else {
let children: Vec<Box<dyn Node>> = (0..width)
.map(|_| make_deep_seq(depth - 1, width))
.collect();
Box::new(Sequence::new(children))
}
}
#[test]
fn geometry_cache_regression() {
let root = make_deep_seq(3, 3);
let dia_a = Diagram::new(make_deep_seq(3, 3));
let dia_b = Diagram::new(make_deep_seq(3, 3));
let svg_a = format!("{}", dia_a);
let svg_b = format!("{}", dia_b);
assert_eq!(svg_a, svg_b, "SVG output must be deterministic");
let geo = root.compute_geometry();
assert_eq!(geo.entry_height, root.entry_height());
assert_eq!(geo.height, root.height());
assert_eq!(geo.width, root.width());
}
#[test]
fn diagram_write_matches_display() {
let diagram = Diagram::new(make_deep_seq(2, 3));
let displayed = format!("{}", diagram);
let mut written = Vec::new();
diagram.write(&mut written).unwrap();
assert_eq!(String::from_utf8(written).unwrap(), displayed);
}
const PAYLOADS: &[&str] = &[
r#""><script>alert(1)</script>"#,
r#"' onload='alert(1)"#,
r#"foo & bar"#,
r#"</style><script>bad</script>"#,
r#"foo"bar"#,
];
fn assert_no_payload(svg: &str, payload: &str) {
assert!(
!svg.contains(payload),
"raw payload {payload:?} found in SVG output"
);
}
#[test]
fn terminal_label_no_injection() {
for payload in PAYLOADS {
let svg = format!("{}", Diagram::new(Terminal::new(payload.to_string())));
assert_no_payload(&svg, payload);
let svg = format!("{}", Diagram::new(NonTerminal::new(payload.to_string())));
assert_no_payload(&svg, payload);
}
}
#[test]
fn comment_text_no_injection() {
for payload in PAYLOADS {
let svg = format!("{}", Diagram::new(Comment::new(payload.to_string())));
assert_no_payload(&svg, payload);
}
}
#[test]
fn link_uri_no_injection() {
for payload in PAYLOADS {
let node = Link::new(Empty, payload.to_string());
let svg = format!("{}", Diagram::new(node));
assert_no_payload(&svg, payload);
}
}
#[test]
fn node_attr_no_injection() {
for payload in PAYLOADS {
let mut t = Terminal::new("x".to_owned());
t.attr("data-x".to_owned()).or_insert(payload.to_string());
let svg = format!("{}", Diagram::new(t));
assert_no_payload(&svg, payload);
let mut t = Terminal::new("x".to_owned());
t.attr(payload.to_string()).or_insert("value".to_owned());
let svg = format!("{}", Diagram::new(t));
assert_no_payload(&svg, payload);
}
}
}