railroad/lib.rs
1// MIT License
2//
3// Copyright (c) Lukas Lueg (lukas.lueg@gmail.com)
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22//
23//! A library to create syntax ("railroad") diagrams as Scalable Vector Graphics (SVG).
24//!
25//! Railroad diagrams are a graphical way to represent context-free grammar.
26//! Every diagram has exactly one starting- and one end-point; everything that
27//! belongs to the described language is represented by one of the possible paths
28//! between those points.
29//!
30//! Using this library, diagrams are created by primitives which implemented `Node`.
31//! Primitives are combined into more complex strctures by wrapping simple elements into more
32//! complex ones.
33//!
34//! ```rust
35//! use railroad::*;
36//!
37//! // This diagram will be a (horizontal) sequence of simple elements
38//! let mut seq: Sequence<Box<dyn Node>> = Sequence::default();
39//! seq.push(Box::new(Start))
40//! .push(Box::new(Terminal::new("BEGIN".to_owned())))
41//! .push(Box::new(NonTerminal::new("syntax".to_owned())))
42//! .push(Box::new(End));
43//!
44//! // The library only computes the diagram's geometry; we use CSS for layout.
45//! let mut dia = Diagram::new_with_stylesheet(seq, &Stylesheet::Light);
46//!
47//! // A `Node`'s `fmt::Display` is its SVG.
48//! println!("<html>{}</html>", dia);
49//!
50//! // For direct streaming, render into `svg::Renderer`.
51//! let mut streamed = String::new();
52//! let mut renderer = svg::Renderer::new(&mut streamed);
53//! dia.render(&mut renderer, 0, 0, svg::HDir::LTR).unwrap();
54//! assert!(streamed.starts_with("<svg"));
55//! ```
56//!
57//! ## Implementing custom nodes
58//!
59//! Downstream crates can implement [`Node`] directly for custom primitives.
60//! The main rule is simple: a node must only draw within the geometry it
61//! advertises. If a node reports `width()`, `height()`, and `entry_height()`,
62//! its drawing must stay inside that box and keep its connecting path at
63//! `y + entry_height()`.
64//!
65//! For simple leaf nodes, implementing `entry_height()`, `height()`, `width()`,
66//! and [`Node::draw`] is usually enough; the provided geometry-aware methods are
67//! correct by default. For composite nodes that position child nodes, override
68//! [`Node::compute_geometry`] and usually also [`Node::draw_with_geometry`] and
69//! [`Node::render_with_geometry`] so child geometry is computed once and reused
70//! during rendering.
71
72use std::{
73 collections::{self, HashMap},
74 fmt, io,
75};
76
77pub mod notactuallysvg;
78pub use crate::notactuallysvg as svg;
79use crate::svg::HDir;
80mod nodes;
81pub use crate::nodes::containers::{Choice, MultiChoice, Sequence, Stack};
82pub use crate::nodes::grids::{HorizontalGrid, VerticalGrid};
83pub use crate::nodes::text::{Comment, NonTerminal, Terminal};
84pub use crate::nodes::wrappers::{LabeledBox, Link, LinkTarget, Optional, Repeat};
85
86#[cfg(feature = "resvg")]
87pub mod render;
88
89#[cfg(feature = "resvg")]
90pub use resvg;
91
92#[doc = include_str!("../README.md")]
93#[allow(dead_code)]
94type _READMETEST = ();
95
96/// Used as a form of scale throughout geometry calculations. Smaller values result in more compact
97/// diagrams.
98const ARC_RADIUS: i64 = 12;
99
100/// Determine the width some text will have when rendered.
101///
102/// The geometry of some primitives depends on this, which is hacky in the first place.
103fn text_width(s: &str) -> usize {
104 use unicode_width::UnicodeWidthStr;
105 // Use a fudge-factor of 1.05
106 s.width() + (s.width() / 20)
107}
108
109/// Pre-defined stylesheets
110/// ```rust
111/// use railroad::*;
112///
113/// let mut seq: Sequence::<Box<dyn Node>> = Sequence::default();
114/// seq.push(Box::new(Start))
115/// .push(Box::new(Terminal::new("Foobar".to_owned())))
116/// .push(Box::new(End));
117///
118/// let dia = Diagram::new_with_stylesheet(seq, &Stylesheet::Light);
119/// println!("{}", dia);
120/// ```
121#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
122#[non_exhaustive]
123pub enum Stylesheet {
124 /// The default stylesheet
125 #[default]
126 Light,
127 Dark,
128 /// Variation of the `Light`-theme, compatible with what can be rendered when using `resvg`.
129 LightRendersafe,
130 /// Variation of the `Dark`-theme, compatible with what can be rendered when using `resvg`.
131 DarkRendersafe,
132}
133
134impl Stylesheet {
135 /// Switch this stylesheet to it's "dark" variant, preserving render-safety.
136 #[must_use]
137 pub const fn to_dark(&self) -> Self {
138 match self {
139 Self::Light | Self::Dark => Self::Dark,
140 Self::LightRendersafe | Self::DarkRendersafe => Self::DarkRendersafe,
141 }
142 }
143
144 /// Switch this stylesheet to it's "light" variant, preserving render-safety.
145 #[must_use]
146 pub const fn to_light(&self) -> Self {
147 match self {
148 Self::Light | Self::Dark => Self::Light,
149 Self::LightRendersafe | Self::DarkRendersafe => Self::LightRendersafe,
150 }
151 }
152
153 /// Returns `True` if this stylesheet is of a "light" variant.
154 #[must_use]
155 pub const fn is_light(&self) -> bool {
156 matches!(self, Self::Light | Self::LightRendersafe)
157 }
158
159 /// The CSS for this stylesheet.
160 #[must_use]
161 pub const fn stylesheet(self) -> &'static str {
162 match self {
163 Self::Light => include_str!("stylesheet_light.css"),
164 Self::Dark => include_str!("stylesheet_dark.css"),
165 Self::LightRendersafe => include_str!("stylesheet_light_safe.css"),
166 Self::DarkRendersafe => include_str!("stylesheet_dark_safe.css"),
167 }
168 }
169}
170
171/// Default Cascading Style Sheets for the resuling SVG.
172pub const DEFAULT_CSS: &str = Stylesheet::Light.stylesheet();
173
174/// Pre-computed geometry for a node and its entire subtree.
175///
176/// This is a transient value created by [`Node::compute_geometry`] and passed into
177/// [`Node::draw_with_geometry`]. It is never stored inside a node struct; it exists
178/// only on the call stack during the draw phase and is dropped when drawing completes.
179///
180/// The `children` vec mirrors the order in which each composite node iterates its
181/// children during drawing, so `children[i]` corresponds to the i-th child drawn.
182/// For single-child wrappers (`Optional`, `Link`) `children[0]` is the inner node.
183/// For `LabeledBox`, `children[0]` is the inner node and `children[1]` is the label.
184/// For `Repeat`, `children[0]` is the inner node and `children[1]` is the repeat node.
185/// Leaf nodes have an empty `children` vec.
186#[derive(Debug, Clone)]
187pub struct NodeGeometry {
188 /// The vertical distance from this node's top edge to its connecting path.
189 pub entry_height: i64,
190 /// The total height of this node's bounding box.
191 pub height: i64,
192 /// The total width of this node's bounding box.
193 pub width: i64,
194 /// Pre-computed geometry for each child, in draw order.
195 pub children: Vec<NodeGeometry>,
196}
197
198impl NodeGeometry {
199 /// The vertical distance from the connecting path to the bottom of this node.
200 ///
201 /// Equivalent to `height - entry_height`.
202 #[must_use]
203 pub fn height_below_entry(&self) -> i64 {
204 self.height - self.entry_height
205 }
206}
207
208/// A diagram primitive that participates in layout and SVG generation.
209///
210/// Every `Node` advertises a rectangular geometry and a single horizontal entry
211/// line inside that rectangle. Parent nodes use that geometry to position child
212/// nodes, so correctness depends on each implementation keeping its drawing
213/// inside the geometry it reports:
214///
215/// - `width()` and `height()` define the full bounding box,
216/// - `entry_height()` defines the vertical offset of the connecting path,
217/// - drawing at `(x, y)` must stay inside `x..x + width()` and `y..y + height()`,
218/// - and the path that enters or leaves the node must be aligned with
219/// `y + entry_height()`.
220///
221/// For simple leaf nodes, implementing [`Node::entry_height`], [`Node::height`],
222/// [`Node::width`], and [`Node::draw`] is usually enough. The default
223/// implementations of the geometry-aware methods are correct, just not always
224/// optimal.
225///
226/// Composite nodes that contain child nodes should usually override
227/// [`Node::compute_geometry`] so child geometry is computed once in a bottom-up
228/// pass, then override [`Node::draw_with_geometry`] and often
229/// [`Node::render_with_geometry`] to reuse that cached geometry during drawing.
230pub trait Node {
231 /// The vertical distance from this element's top to where the entering,
232 /// connecting path is drawn.
233 ///
234 /// By convention, the path connecting primitives enters from the left.
235 /// Parent nodes align children by placing their connecting path at
236 /// `y + entry_height()`, so this value must match where the node actually
237 /// expects its incoming and outgoing path segments.
238 fn entry_height(&self) -> i64;
239
240 /// This primitive's total height.
241 ///
242 /// Together with [`Node::width`], this defines the full bounding box the
243 /// node may occupy when drawn.
244 fn height(&self) -> i64;
245
246 /// This primitive's total width.
247 ///
248 /// The node must not draw outside the horizontal range implied by this
249 /// value when positioned by a parent node.
250 fn width(&self) -> i64;
251
252 /// The vertical distance from the height of the connecting path to the bottom.
253 ///
254 /// Equivalent to `height() - entry_height()`.
255 ///
256 /// This is a convenience method for parent nodes that need to align child
257 /// nodes relative to the connecting path. Implementors normally should not
258 /// override it unless they also change the meaning of the basic geometry
259 /// methods.
260 fn height_below_entry(&self) -> i64 {
261 self.height() - self.entry_height()
262 }
263
264 /// Draw this element as an `svg::Element` at the given position and direction.
265 ///
266 /// The element must fit entirely within the bounding box defined by `(x, y)`,
267 /// `width()`, and `height()`, with the connecting path at `y + entry_height()`.
268 ///
269 /// For many downstream leaf nodes, this is the only drawing method that must
270 /// be implemented directly. The default geometry-aware methods delegate back
271 /// to it.
272 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element;
273
274 /// Compute geometry for this node and its entire subtree in a single bottom-up pass.
275 ///
276 /// The returned [`NodeGeometry`] is a transient value intended to be passed to
277 /// [`Node::draw_with_geometry`]; it is not stored inside the node.
278 ///
279 /// The default implementation is correct for leaf nodes because it simply
280 /// records this node's advertised geometry and assumes there are no children.
281 ///
282 /// Composite nodes should override this to recurse into their children and
283 /// store child geometry in [`NodeGeometry::children`]. That lets parent and
284 /// child rendering share one cached geometry pass instead of repeatedly
285 /// calling `entry_height()`, `height()`, and `width()` throughout the tree.
286 fn compute_geometry(&self) -> NodeGeometry {
287 NodeGeometry {
288 entry_height: self.entry_height(),
289 height: self.height(),
290 width: self.width(),
291 children: vec![],
292 }
293 }
294
295 /// Draw this element using pre-computed geometry, avoiding redundant geometry
296 /// recomputation for deeply nested structures.
297 ///
298 /// `geo` holds the cached dimensions for *this* node. Composite nodes should
299 /// read child geometry from `geo.children[i]` and pass it to each child's
300 /// `draw_with_geometry` call, rather than recomputing child geometry through
301 /// repeated calls to `entry_height()`, `height()`, and `width()`.
302 ///
303 /// The default implementation falls back to [`Node::draw`], which is correct
304 /// for all nodes. Leaf nodes usually do not need to override this. Composite
305 /// nodes should usually override it so the cached geometry from
306 /// [`Node::compute_geometry`] is actually used.
307 fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, _geo: &NodeGeometry) -> svg::Element {
308 self.draw(x, y, h_dir)
309 }
310
311 /// Render this element directly into an SVG renderer.
312 ///
313 /// This is the streaming counterpart to [`Node::draw`]. The default
314 /// implementation computes geometry once and forwards to
315 /// [`Node::render_with_geometry`].
316 ///
317 /// Implementors typically do not override this method directly. Instead,
318 /// override [`Node::render_with_geometry`] if a custom streaming
319 /// implementation is worthwhile.
320 ///
321 /// # Example
322 /// ```rust
323 /// use railroad::*;
324 ///
325 /// let node = Terminal::new("item".to_owned());
326 /// let mut out = String::new();
327 /// let mut renderer = svg::Renderer::new(&mut out);
328 /// node.render(&mut renderer, 0, 0, svg::HDir::LTR).unwrap();
329 /// assert!(out.contains("<text"));
330 /// assert!(out.contains("item"));
331 /// ```
332 fn render(&self, out: &mut svg::Renderer<'_>, x: i64, y: i64, h_dir: HDir) -> fmt::Result {
333 let geo = self.compute_geometry();
334 self.render_with_geometry(out, x, y, h_dir, &geo)
335 }
336
337 /// Render this element using pre-computed geometry.
338 ///
339 /// Override this for node implementations that want to stream SVG directly
340 /// without first materializing an intermediate [`svg::Element`] tree. The
341 /// default implementation preserves compatibility by serializing the result of
342 /// [`Node::draw_with_geometry`].
343 ///
344 /// Leaf nodes can often keep the default implementation. Composite nodes or
345 /// performance-sensitive nodes should usually override this together with
346 /// [`Node::draw_with_geometry`] so both rendering paths consume the same
347 /// cached geometry instead of rebuilding equivalent intermediate structures.
348 fn render_with_geometry(
349 &self,
350 out: &mut svg::Renderer<'_>,
351 x: i64,
352 y: i64,
353 h_dir: HDir,
354 geo: &NodeGeometry,
355 ) -> fmt::Result {
356 out.write_display(self.draw_with_geometry(x, y, h_dir, geo))
357 }
358}
359
360impl fmt::Debug for dyn Node {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 f.debug_struct("Node")
363 .field("entry_height", &self.entry_height())
364 .field("height", &self.height())
365 .field("width", &self.width())
366 .finish()
367 }
368}
369
370macro_rules! deref_impl {
371 ($($sig:tt)+) => {
372 impl $($sig)+ {
373 fn entry_height(&self) -> i64 {
374 (**self).entry_height()
375 }
376
377 fn height(&self) -> i64 {
378 (**self).height()
379 }
380
381 fn width(&self) -> i64 {
382 (**self).width()
383 }
384
385 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
386 (**self).draw(x, y, h_dir)
387 }
388
389 fn compute_geometry(&self) -> NodeGeometry {
390 (**self).compute_geometry()
391 }
392
393 fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, geo: &NodeGeometry) -> svg::Element {
394 (**self).draw_with_geometry(x, y, h_dir, geo)
395 }
396
397 fn render_with_geometry(
398 &self,
399 out: &mut svg::Renderer<'_>,
400 x: i64,
401 y: i64,
402 h_dir: HDir,
403 geo: &NodeGeometry,
404 ) -> fmt::Result {
405 (**self).render_with_geometry(out, x, y, h_dir, geo)
406 }
407 }
408 };
409}
410deref_impl!(<'a, N> Node for &'a N where N: Node + ?Sized);
411deref_impl!(<'a, N> Node for &'a mut N where N: Node + ?Sized);
412deref_impl!(<N> Node for Box<N> where N: Node + ?Sized);
413deref_impl!(<N> Node for std::rc::Rc<N> where N: Node + ?Sized);
414deref_impl!(<N> Node for std::sync::Arc<N> where N: Node + ?Sized);
415
416#[cfg(feature = "visual-debug")]
417fn add_debug_attrs(
418 tag: &mut svg::StartTag<'_, '_>,
419 name: &str,
420 x: i64,
421 y: i64,
422 geo: &NodeGeometry,
423) -> fmt::Result {
424 tag.attr("railroad:type", name)?;
425 tag.attr("railroad:x", x)?;
426 tag.attr("railroad:y", y)?;
427 tag.attr("railroad:entry_height", geo.entry_height)?;
428 tag.attr("railroad:height", geo.height)?;
429 tag.attr("railroad:width", geo.width)
430}
431
432#[cfg(not(feature = "visual-debug"))]
433fn add_debug_attrs(
434 _tag: &mut svg::StartTag<'_, '_>,
435 _name: &str,
436 _x: i64,
437 _y: i64,
438 _geo: &NodeGeometry,
439) -> fmt::Result {
440 Ok(())
441}
442
443#[cfg(feature = "visual-debug")]
444fn write_debug_overlay(
445 out: &mut svg::Renderer<'_>,
446 x: i64,
447 y: i64,
448 geo: &NodeGeometry,
449) -> fmt::Result {
450 out.path_with_class(
451 &svg::PathData::new(HDir::LTR)
452 .move_to(x, y)
453 .horizontal(geo.width)
454 .vertical(5)
455 .move_rel(-geo.width, -5)
456 .vertical(geo.height)
457 .horizontal(5)
458 .move_rel(-5, -geo.height)
459 .move_rel(0, geo.entry_height)
460 .horizontal(10),
461 "debug",
462 )
463}
464
465#[cfg(not(feature = "visual-debug"))]
466fn write_debug_overlay(
467 _out: &mut svg::Renderer<'_>,
468 _x: i64,
469 _y: i64,
470 _geo: &NodeGeometry,
471) -> fmt::Result {
472 Ok(())
473}
474
475/// Internal rendering surface shared by the `svg::Element` and streaming backends.
476///
477/// Built-in nodes express their geometry-aware draw order in terms of this trait
478/// so the crate can keep `draw_with_geometry()` and `render_with_geometry()`
479/// behavior in sync without duplicating traversal logic.
480trait RenderBackend {
481 /// Append a path element to the current output.
482 fn push_path(&mut self, path: svg::PathData) -> fmt::Result;
483
484 /// Append an axis-aligned rectangle to the current output.
485 fn push_rect(&mut self, x: i64, y: i64, width: i64, height: i64) -> fmt::Result;
486
487 /// Append a rounded rectangle to the current output.
488 fn push_rounded_rect(
489 &mut self,
490 x: i64,
491 y: i64,
492 width: i64,
493 height: i64,
494 radius: i64,
495 ) -> fmt::Result;
496
497 /// Append a centered text element at the given coordinates.
498 fn push_text(&mut self, x: i64, y: i64, text: &str) -> fmt::Result;
499
500 /// Append a child node using cached geometry.
501 fn push_child<N: Node + ?Sized>(
502 &mut self,
503 child: &N,
504 x: i64,
505 y: i64,
506 h_dir: HDir,
507 geo: &NodeGeometry,
508 ) -> fmt::Result;
509}
510
511/// `RenderBackend` implementation that accumulates child `svg::Element`s.
512///
513/// This powers the compatibility `draw_with_geometry()` path.
514#[derive(Default)]
515struct ElementBackend {
516 children: Vec<svg::Element>,
517}
518
519impl ElementBackend {
520 /// Wrap the accumulated children in a `<g>` element with debug metadata.
521 ///
522 /// ```ignore
523 /// # use std::collections::HashMap;
524 /// # use railroad::{NodeGeometry, notactuallysvg as svg};
525 /// # use railroad::HDir;
526 /// let mut backend = ElementBackend::default();
527 /// backend.push_path(svg::PathData::new(HDir::LTR).move_to(0, 0).horizontal(10)).unwrap();
528 /// let group = backend.finish_group(
529 /// &HashMap::new(),
530 /// "demo",
531 /// 0,
532 /// 0,
533 /// &NodeGeometry { entry_height: 0, height: 0, width: 0, children: vec![] },
534 /// );
535 /// assert!(group.to_string().starts_with("<g"));
536 /// ```
537 fn finish_group(
538 self,
539 attrs: &HashMap<String, String>,
540 name: &str,
541 x: i64,
542 y: i64,
543 geo: &NodeGeometry,
544 ) -> svg::Element {
545 let mut group = svg::Element::new("g").set_all(attrs.iter());
546 for child in self.children {
547 group.push(child);
548 }
549 group.debug_with_geometry(name, x, y, geo)
550 }
551}
552
553impl RenderBackend for ElementBackend {
554 fn push_path(&mut self, path: svg::PathData) -> fmt::Result {
555 self.children.push(path.into_path());
556 Ok(())
557 }
558
559 fn push_rect(&mut self, x: i64, y: i64, width: i64, height: i64) -> fmt::Result {
560 self.children.push(
561 svg::Element::new("rect")
562 .set("x", &x)
563 .set("y", &y)
564 .set("height", &height)
565 .set("width", &width),
566 );
567 Ok(())
568 }
569
570 fn push_rounded_rect(
571 &mut self,
572 x: i64,
573 y: i64,
574 width: i64,
575 height: i64,
576 radius: i64,
577 ) -> fmt::Result {
578 self.children.push(
579 svg::Element::new("rect")
580 .set("x", &x)
581 .set("y", &y)
582 .set("height", &height)
583 .set("width", &width)
584 .set("rx", &radius)
585 .set("ry", &radius),
586 );
587 Ok(())
588 }
589
590 fn push_text(&mut self, x: i64, y: i64, text: &str) -> fmt::Result {
591 self.children.push(
592 svg::Element::new("text")
593 .set("x", &x)
594 .set("y", &y)
595 .text(text),
596 );
597 Ok(())
598 }
599
600 fn push_child<N: Node + ?Sized>(
601 &mut self,
602 child: &N,
603 x: i64,
604 y: i64,
605 h_dir: HDir,
606 geo: &NodeGeometry,
607 ) -> fmt::Result {
608 self.children
609 .push(child.draw_with_geometry(x, y, h_dir, geo));
610 Ok(())
611 }
612}
613
614/// `RenderBackend` implementation that streams directly into `svg::Renderer`.
615struct RendererBackend<'a, 'b> {
616 out: &'a mut svg::Renderer<'b>,
617}
618
619impl RenderBackend for RendererBackend<'_, '_> {
620 fn push_path(&mut self, path: svg::PathData) -> fmt::Result {
621 self.out.path(&path)
622 }
623
624 fn push_rect(&mut self, x: i64, y: i64, width: i64, height: i64) -> fmt::Result {
625 let mut rect = self.out.start_element("rect")?;
626 rect.attr("x", x)?;
627 rect.attr("y", y)?;
628 rect.attr("height", height)?;
629 rect.attr("width", width)?;
630 rect.finish_empty()
631 }
632
633 fn push_rounded_rect(
634 &mut self,
635 x: i64,
636 y: i64,
637 width: i64,
638 height: i64,
639 radius: i64,
640 ) -> fmt::Result {
641 let mut rect = self.out.start_element("rect")?;
642 rect.attr("x", x)?;
643 rect.attr("y", y)?;
644 rect.attr("height", height)?;
645 rect.attr("width", width)?;
646 rect.attr("rx", radius)?;
647 rect.attr("ry", radius)?;
648 rect.finish_empty()
649 }
650
651 fn push_text(&mut self, x: i64, y: i64, text: &str) -> fmt::Result {
652 self.out.text_element("text", text, |tag| {
653 tag.attr("x", x)?;
654 tag.attr("y", y)
655 })
656 }
657
658 fn push_child<N: Node + ?Sized>(
659 &mut self,
660 child: &N,
661 x: i64,
662 y: i64,
663 h_dir: HDir,
664 geo: &NodeGeometry,
665 ) -> fmt::Result {
666 child.render_with_geometry(self.out, x, y, h_dir, geo)
667 }
668}
669
670/// Build a debug-aware `<g>` element from a shared emit closure.
671///
672/// This is the `svg::Element` counterpart to `render_group_with_geometry`.
673///
674/// ```ignore
675/// # use std::collections::HashMap;
676/// # use railroad::{NodeGeometry, notactuallysvg as svg, HDir};
677/// let group = draw_group_with_geometry(
678/// &HashMap::new(),
679/// "demo",
680/// 0,
681/// 0,
682/// &NodeGeometry { entry_height: 0, height: 0, width: 10, children: vec![] },
683/// |backend| backend.push_path(svg::PathData::new(HDir::LTR).move_to(0, 0).horizontal(10)),
684/// );
685/// assert!(group.to_string().contains("<path"));
686/// ```
687fn draw_group_with_geometry(
688 attrs: &HashMap<String, String>,
689 name: &str,
690 x: i64,
691 y: i64,
692 geo: &NodeGeometry,
693 emit: impl FnOnce(&mut ElementBackend) -> fmt::Result,
694) -> svg::Element {
695 let mut backend = ElementBackend::default();
696 emit(&mut backend).expect("element backend is infallible");
697 backend.finish_group(attrs, name, x, y, geo)
698}
699
700/// Stream a debug-aware `<g>` element from a shared emit closure.
701///
702/// This helper keeps the streaming wrapper logic identical across nodes.
703///
704/// ```ignore
705/// # use std::{collections::HashMap, fmt};
706/// # use railroad::{NodeGeometry, notactuallysvg as svg, HDir};
707/// let mut out = String::new();
708/// let mut renderer = svg::Renderer::new(&mut out);
709/// render_group_with_geometry(
710/// &mut renderer,
711/// &HashMap::new(),
712/// "demo",
713/// 0,
714/// 0,
715/// &NodeGeometry { entry_height: 0, height: 0, width: 10, children: vec![] },
716/// |backend| backend.push_path(svg::PathData::new(HDir::LTR).move_to(0, 0).horizontal(10)),
717/// ).unwrap();
718/// assert!(out.contains("<g"));
719/// ```
720fn render_group_with_geometry(
721 out: &mut svg::Renderer<'_>,
722 attrs: &HashMap<String, String>,
723 name: &str,
724 x: i64,
725 y: i64,
726 geo: &NodeGeometry,
727 emit: impl FnOnce(&mut RendererBackend<'_, '_>) -> fmt::Result,
728) -> fmt::Result {
729 let mut group = out.start_element("g")?;
730 group.attr_hashmap(attrs)?;
731 add_debug_attrs(&mut group, name, x, y, geo)?;
732 group.finish()?;
733
734 let mut backend = RendererBackend { out };
735 emit(&mut backend)?;
736 write_debug_overlay(backend.out, x, y, geo)?;
737 backend.out.end_element("g")
738}
739
740/// Build a debug-aware `<g class="...">` wrapper from a shared emit closure.
741///
742/// ```ignore
743/// # use railroad::{NodeGeometry, notactuallysvg as svg, HDir};
744/// let group = draw_class_group_with_geometry(
745/// "demo",
746/// "Demo",
747/// 0,
748/// 0,
749/// &NodeGeometry { entry_height: 0, height: 0, width: 10, children: vec![] },
750/// |backend| backend.push_path(svg::PathData::new(HDir::LTR).move_to(0, 0).horizontal(10)),
751/// );
752/// assert!(group.to_string().contains("class=\"demo\""));
753/// ```
754fn draw_class_group_with_geometry(
755 class: &str,
756 name: &str,
757 x: i64,
758 y: i64,
759 geo: &NodeGeometry,
760 emit: impl FnOnce(&mut ElementBackend) -> fmt::Result,
761) -> svg::Element {
762 let mut backend = ElementBackend::default();
763 emit(&mut backend).expect("element backend is infallible");
764
765 let mut group = svg::Element::new("g").set("class", &class);
766 for child in backend.children {
767 group.push(child);
768 }
769 group.debug_with_geometry(name, x, y, geo)
770}
771
772/// Stream a debug-aware `<g class="...">` wrapper from a shared emit closure.
773///
774/// ```ignore
775/// # use railroad::{NodeGeometry, notactuallysvg as svg, HDir};
776/// let mut out = String::new();
777/// let mut renderer = svg::Renderer::new(&mut out);
778/// render_class_group_with_geometry(
779/// &mut renderer,
780/// "demo",
781/// "Demo",
782/// 0,
783/// 0,
784/// &NodeGeometry { entry_height: 0, height: 0, width: 10, children: vec![] },
785/// |backend| backend.push_path(svg::PathData::new(HDir::LTR).move_to(0, 0).horizontal(10)),
786/// ).unwrap();
787/// assert!(out.contains("class=\"demo\""));
788/// ```
789fn render_class_group_with_geometry(
790 out: &mut svg::Renderer<'_>,
791 class: &str,
792 name: &str,
793 x: i64,
794 y: i64,
795 geo: &NodeGeometry,
796 emit: impl FnOnce(&mut RendererBackend<'_, '_>) -> fmt::Result,
797) -> fmt::Result {
798 let mut group = out.start_element("g")?;
799 group.attr("class", class)?;
800 add_debug_attrs(&mut group, name, x, y, geo)?;
801 group.finish()?;
802
803 let mut backend = RendererBackend { out };
804 emit(&mut backend)?;
805 write_debug_overlay(backend.out, x, y, geo)?;
806 backend.out.end_element("g")
807}
808
809/// Attach cached-geometry debug metadata to a leaf path in the element backend.
810///
811/// ```ignore
812/// # use railroad::{NodeGeometry, notactuallysvg as svg, HDir};
813/// let geo = NodeGeometry { entry_height: 10, height: 20, width: 20, children: vec![] };
814/// let path = draw_debug_path(
815/// "Start",
816/// 0,
817/// 0,
818/// &geo,
819/// svg::PathData::new(HDir::LTR).move_to(0, 0).horizontal(20),
820/// );
821/// assert!(path.to_string().contains("railroad:type=\"Start\""));
822/// ```
823fn draw_debug_path(
824 name: &str,
825 x: i64,
826 y: i64,
827 geo: &NodeGeometry,
828 path: svg::PathData,
829) -> svg::Element {
830 path.into_path().debug_with_geometry(name, x, y, geo)
831}
832
833/// Attach cached-geometry debug metadata to a leaf path in the streaming backend.
834///
835/// ```ignore
836/// # use railroad::{NodeGeometry, notactuallysvg as svg, HDir};
837/// let geo = NodeGeometry { entry_height: 10, height: 20, width: 20, children: vec![] };
838/// let mut out = String::new();
839/// let mut renderer = svg::Renderer::new(&mut out);
840/// render_debug_path(
841/// &mut renderer,
842/// "Start",
843/// 0,
844/// 0,
845/// &geo,
846/// svg::PathData::new(HDir::LTR).move_to(0, 0).horizontal(20),
847/// ).unwrap();
848/// assert!(out.contains("railroad:type=\"Start\""));
849/// ```
850fn render_debug_path(
851 out: &mut svg::Renderer<'_>,
852 name: &str,
853 x: i64,
854 y: i64,
855 geo: &NodeGeometry,
856 path: svg::PathData,
857) -> fmt::Result {
858 let mut tag = out.start_element("path")?;
859 tag.attr("d", path)?;
860 add_debug_attrs(&mut tag, name, x, y, geo)?;
861 tag.finish_empty()?;
862 write_debug_overlay(out, x, y, geo)
863}
864
865/// Emit a boxed text node shared by `Terminal` and `NonTerminal`.
866///
867/// `rounded` selects between the rounded terminal box and the square
868/// non-terminal box.
869///
870/// ```ignore
871/// # use railroad::{NodeGeometry, notactuallysvg as svg};
872/// let mut backend = ElementBackend::default();
873/// emit_text_box(
874/// &mut backend,
875/// 0,
876/// 0,
877/// &NodeGeometry { entry_height: 11, height: 22, width: 60, children: vec![] },
878/// "item",
879/// true,
880/// ).unwrap();
881/// assert_eq!(backend.children.len(), 2);
882/// ```
883fn emit_text_box<B: RenderBackend>(
884 backend: &mut B,
885 x: i64,
886 y: i64,
887 geo: &NodeGeometry,
888 label: &str,
889 rounded: bool,
890) -> fmt::Result {
891 if rounded {
892 backend.push_rounded_rect(x, y, geo.width, geo.height, 10)?;
893 } else {
894 backend.push_rect(x, y, geo.width, geo.height)?;
895 }
896 backend.push_text(x + geo.width / 2, y + geo.entry_height + 5, label)
897}
898
899/// Convenience aggregation helpers for iterators and collections of [`Node`]s.
900///
901/// `NodeCollection` is implemented for any `IntoIterator<Item = N>` where `N`
902/// implements [`Node`]. It is mainly a small ergonomic helper for container
903/// nodes that need to aggregate child geometry without spelling out the same
904/// iterator expressions repeatedly.
905///
906/// The methods consume `self`, so they work naturally on iterators as well as on
907/// owned collections. When called on borrowed collections such as `slice.iter()`
908/// or `vec.iter()`, the iterator items are references, and the blanket `Node`
909/// implementations for references keep the methods usable.
910///
911/// # Example
912/// ```rust
913/// use railroad::{End, NodeCollection, SimpleStart, Start};
914///
915/// let nodes = [Start, Start];
916/// assert!(nodes.iter().max_entry_height() > 0);
917///
918/// let widths = vec![SimpleStart, SimpleStart];
919/// assert!(widths.iter().total_width() > 0);
920///
921/// let exits: Vec<Box<dyn railroad::Node>> = vec![Box::new(End), Box::new(SimpleStart)];
922/// assert!(exits.iter().max_height_below_entry() > 0);
923/// ```
924pub trait NodeCollection {
925 /// Return the maximum [`Node::entry_height`] in the collection.
926 ///
927 /// This is commonly used by horizontal container nodes that align several
928 /// children to the same connecting path.
929 ///
930 /// # Example
931 /// ```rust
932 /// use railroad::{Comment, NodeCollection, Start};
933 ///
934 /// let nodes: Vec<Box<dyn railroad::Node>> =
935 /// vec![Box::new(Comment::new("note".to_owned())), Box::new(Start)];
936 /// assert!(nodes.iter().max_entry_height() > 0);
937 /// ```
938 fn max_entry_height(self) -> i64;
939
940 /// Return the maximum [`Node::height`] in the collection.
941 fn max_height(self) -> i64;
942
943 /// Return the maximum [`Node::height_below_entry`] in the collection.
944 ///
945 /// This is useful when children are aligned by their connecting path and the
946 /// parent needs enough space below that path for the deepest child.
947 ///
948 /// # Example
949 /// ```rust
950 /// use railroad::{Comment, NodeCollection, Terminal};
951 ///
952 /// let nodes: Vec<Box<dyn railroad::Node>> = vec![
953 /// Box::new(Comment::new("note".to_owned())),
954 /// Box::new(Terminal::new("token".to_owned())),
955 /// ];
956 /// assert!(nodes.iter().max_height_below_entry() > 0);
957 /// ```
958 fn max_height_below_entry(self) -> i64;
959
960 /// Return the maximum [`Node::width`] in the collection.
961 fn max_width(self) -> i64;
962
963 /// Return the sum of all [`Node::width`] values in the collection.
964 ///
965 /// This is typically used by horizontal container nodes before adding their
966 /// own inter-child spacing.
967 ///
968 /// # Example
969 /// ```rust
970 /// use railroad::{NodeCollection, SimpleStart, Start};
971 ///
972 /// let nodes = [Start, Start];
973 /// assert!(nodes.iter().total_width() > 0);
974 ///
975 /// let mixed = [SimpleStart, SimpleStart];
976 /// assert!(mixed.iter().total_width() > 0);
977 /// ```
978 fn total_width(self) -> i64;
979
980 /// Return the sum of all [`Node::height`] values in the collection.
981 fn total_height(self) -> i64;
982}
983
984impl<I, N> NodeCollection for I
985where
986 I: IntoIterator<Item = N>,
987 N: Node,
988{
989 fn max_height_below_entry(self) -> i64 {
990 self.into_iter()
991 .map(|n| n.height_below_entry())
992 .max()
993 .unwrap_or_default()
994 }
995
996 fn max_entry_height(self) -> i64 {
997 self.into_iter()
998 .map(|n| n.entry_height())
999 .max()
1000 .unwrap_or_default()
1001 }
1002
1003 fn max_height(self) -> i64 {
1004 self.into_iter()
1005 .map(|n| n.height())
1006 .max()
1007 .unwrap_or_default()
1008 }
1009
1010 fn max_width(self) -> i64 {
1011 self.into_iter()
1012 .map(|n| n.width())
1013 .max()
1014 .unwrap_or_default()
1015 }
1016
1017 fn total_width(self) -> i64 {
1018 self.into_iter().map(|n| n.width()).sum()
1019 }
1020
1021 fn total_height(self) -> i64 {
1022 self.into_iter().map(|n| n.height()).sum()
1023 }
1024}
1025
1026/// A symbol indicating the logical end of a syntax-diagram via two vertical bars.
1027#[derive(Debug, Clone, Default)]
1028pub struct End;
1029
1030impl Node for End {
1031 fn entry_height(&self) -> i64 {
1032 10
1033 }
1034 fn height(&self) -> i64 {
1035 20
1036 }
1037 fn width(&self) -> i64 {
1038 20
1039 }
1040
1041 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
1042 draw_debug_path(
1043 "End",
1044 x,
1045 y,
1046 &self.compute_geometry(),
1047 svg::PathData::new(h_dir)
1048 .move_to(x, y + 10)
1049 .horizontal(20)
1050 .move_rel(-10, -10)
1051 .vertical(20)
1052 .move_rel(10, -20)
1053 .vertical(20),
1054 )
1055 }
1056
1057 fn render_with_geometry(
1058 &self,
1059 out: &mut svg::Renderer<'_>,
1060 x: i64,
1061 y: i64,
1062 h_dir: HDir,
1063 geo: &NodeGeometry,
1064 ) -> fmt::Result {
1065 render_debug_path(
1066 out,
1067 "End",
1068 x,
1069 y,
1070 geo,
1071 svg::PathData::new(h_dir)
1072 .move_to(x, y + 10)
1073 .horizontal(20)
1074 .move_rel(-10, -10)
1075 .vertical(20)
1076 .move_rel(10, -20)
1077 .vertical(20),
1078 )
1079 }
1080}
1081
1082/// A symbol indicating the logical start of a syntax-diagram via a circle
1083#[derive(Debug, Clone, Default)]
1084pub struct SimpleStart;
1085
1086impl Node for SimpleStart {
1087 fn entry_height(&self) -> i64 {
1088 5
1089 }
1090 fn height(&self) -> i64 {
1091 10
1092 }
1093 fn width(&self) -> i64 {
1094 15
1095 }
1096
1097 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
1098 draw_debug_path(
1099 "SimpleStart",
1100 x,
1101 y,
1102 &self.compute_geometry(),
1103 svg::PathData::new(h_dir)
1104 .move_to(x, y + 5)
1105 .arc(5, svg::Arc::SouthToEast)
1106 .arc(5, svg::Arc::WestToSouth)
1107 .arc(5, svg::Arc::NorthToWest)
1108 .arc(5, svg::Arc::EastToNorth)
1109 .move_rel(10, 0)
1110 .horizontal(5),
1111 )
1112 }
1113
1114 fn render_with_geometry(
1115 &self,
1116 out: &mut svg::Renderer<'_>,
1117 x: i64,
1118 y: i64,
1119 h_dir: HDir,
1120 geo: &NodeGeometry,
1121 ) -> fmt::Result {
1122 render_debug_path(
1123 out,
1124 "SimpleStart",
1125 x,
1126 y,
1127 geo,
1128 svg::PathData::new(h_dir)
1129 .move_to(x, y + 5)
1130 .arc(5, svg::Arc::SouthToEast)
1131 .arc(5, svg::Arc::WestToSouth)
1132 .arc(5, svg::Arc::NorthToWest)
1133 .arc(5, svg::Arc::EastToNorth)
1134 .move_rel(10, 0)
1135 .horizontal(5),
1136 )
1137 }
1138}
1139
1140/// A symbol indicating the logical end of a syntax-diagram via a circle
1141#[derive(Debug, Clone, Default)]
1142pub struct SimpleEnd;
1143
1144impl Node for SimpleEnd {
1145 fn entry_height(&self) -> i64 {
1146 5
1147 }
1148 fn height(&self) -> i64 {
1149 10
1150 }
1151 fn width(&self) -> i64 {
1152 15
1153 }
1154
1155 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
1156 draw_debug_path(
1157 "SimpleEnd",
1158 x,
1159 y,
1160 &self.compute_geometry(),
1161 svg::PathData::new(h_dir)
1162 .move_to(x, y + 5)
1163 .horizontal(5)
1164 .arc(5, svg::Arc::SouthToEast)
1165 .arc(5, svg::Arc::WestToSouth)
1166 .arc(5, svg::Arc::NorthToWest)
1167 .arc(5, svg::Arc::EastToNorth),
1168 )
1169 }
1170
1171 fn render_with_geometry(
1172 &self,
1173 out: &mut svg::Renderer<'_>,
1174 x: i64,
1175 y: i64,
1176 h_dir: HDir,
1177 geo: &NodeGeometry,
1178 ) -> fmt::Result {
1179 render_debug_path(
1180 out,
1181 "SimpleEnd",
1182 x,
1183 y,
1184 geo,
1185 svg::PathData::new(h_dir)
1186 .move_to(x, y + 5)
1187 .horizontal(5)
1188 .arc(5, svg::Arc::SouthToEast)
1189 .arc(5, svg::Arc::WestToSouth)
1190 .arc(5, svg::Arc::NorthToWest)
1191 .arc(5, svg::Arc::EastToNorth),
1192 )
1193 }
1194}
1195
1196/// A symbol indicating the logical start of a syntax-diagram via two vertical bars.
1197#[derive(Debug, Clone, Default)]
1198pub struct Start;
1199
1200impl Node for Start {
1201 fn entry_height(&self) -> i64 {
1202 10
1203 }
1204 fn height(&self) -> i64 {
1205 20
1206 }
1207 fn width(&self) -> i64 {
1208 20
1209 }
1210
1211 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
1212 draw_debug_path(
1213 "Start",
1214 x,
1215 y,
1216 &self.compute_geometry(),
1217 svg::PathData::new(h_dir)
1218 .move_to(x, y)
1219 .vertical(20)
1220 .move_rel(10, -20)
1221 .vertical(20)
1222 .move_rel(-10, -10)
1223 .horizontal(20),
1224 )
1225 }
1226
1227 fn render_with_geometry(
1228 &self,
1229 out: &mut svg::Renderer<'_>,
1230 x: i64,
1231 y: i64,
1232 h_dir: HDir,
1233 geo: &NodeGeometry,
1234 ) -> fmt::Result {
1235 render_debug_path(
1236 out,
1237 "Start",
1238 x,
1239 y,
1240 geo,
1241 svg::PathData::new(h_dir)
1242 .move_to(x, y)
1243 .vertical(20)
1244 .move_rel(10, -20)
1245 .vertical(20)
1246 .move_rel(-10, -10)
1247 .horizontal(20),
1248 )
1249 }
1250}
1251
1252/// A rectangle drawn with the given dimensions, used for visual debugging
1253#[derive(Debug)]
1254#[doc(hidden)]
1255pub struct Debug {
1256 entry_height: i64,
1257 height: i64,
1258 width: i64,
1259 attributes: HashMap<String, String>,
1260}
1261
1262impl Debug {
1263 #[must_use]
1264 /// # Panics
1265 /// If `entry_height` is not smaller than `height`
1266 pub fn new(entry_height: i64, height: i64, width: i64) -> Self {
1267 assert!(entry_height < height);
1268 let mut d = Self {
1269 entry_height,
1270 height,
1271 width,
1272 attributes: HashMap::default(),
1273 };
1274
1275 d.attributes.insert("class".to_owned(), "debug".to_owned());
1276 d.attributes.insert(
1277 "style".to_owned(),
1278 "fill: hsla(0, 100%, 90%, 0.9); stroke-width: 2; stroke: red".to_owned(),
1279 );
1280 d
1281 }
1282}
1283
1284impl Node for Debug {
1285 fn entry_height(&self) -> i64 {
1286 self.entry_height
1287 }
1288 fn height(&self) -> i64 {
1289 self.height
1290 }
1291 fn width(&self) -> i64 {
1292 self.width
1293 }
1294
1295 fn draw(&self, x: i64, y: i64, _: HDir) -> svg::Element {
1296 svg::Element::new("rect")
1297 .set("x", &x)
1298 .set("y", &y)
1299 .set("height", &self.height())
1300 .set("width", &self.width())
1301 .set_all(self.attributes.iter())
1302 .debug("Debug", x, y, self)
1303 }
1304
1305 fn render_with_geometry(
1306 &self,
1307 out: &mut svg::Renderer<'_>,
1308 x: i64,
1309 y: i64,
1310 _h_dir: HDir,
1311 geo: &NodeGeometry,
1312 ) -> fmt::Result {
1313 let mut rect = out.start_element("rect")?;
1314 rect.attr("x", x)?;
1315 rect.attr("y", y)?;
1316 rect.attr("height", geo.height)?;
1317 rect.attr("width", geo.width)?;
1318 rect.attr_hashmap(&self.attributes)?;
1319 add_debug_attrs(&mut rect, "Debug", x, y, geo)?;
1320 rect.finish_empty()?;
1321 write_debug_overlay(out, x, y, geo)
1322 }
1323}
1324
1325/// A dummy-element which has no size and draws nothing.
1326///
1327/// This can be used in conjunction with `Choice` (to indicate that one of the options
1328/// is blank, a shorthand for an `Optional(Choice)`), `Repeat` (if there are
1329/// zero-or-more repetitions or if there is no joining element), or `LabeledBox`
1330/// (if the label should be empty).
1331#[derive(Debug, Clone, Default)]
1332pub struct Empty;
1333
1334impl Node for Empty {
1335 fn entry_height(&self) -> i64 {
1336 0
1337 }
1338 fn height(&self) -> i64 {
1339 0
1340 }
1341 fn width(&self) -> i64 {
1342 0
1343 }
1344
1345 fn draw(&self, x: i64, y: i64, _: HDir) -> svg::Element {
1346 svg::Element::new("g").debug("Empty", x, y, self)
1347 }
1348
1349 fn render_with_geometry(
1350 &self,
1351 out: &mut svg::Renderer<'_>,
1352 x: i64,
1353 y: i64,
1354 _h_dir: HDir,
1355 geo: &NodeGeometry,
1356 ) -> fmt::Result {
1357 let mut g = out.start_element("g")?;
1358 add_debug_attrs(&mut g, "Empty", x, y, geo)?;
1359 g.finish()?;
1360 write_debug_overlay(out, x, y, geo)?;
1361 out.end_element("g")
1362 }
1363}
1364
1365/// The top-level container that renders a node tree as a complete SVG document.
1366///
1367/// `Diagram` wraps a root [`Node`], computes its geometry, and emits a
1368/// self-contained `<svg>` element. CSS stylesheets and arbitrary extra SVG
1369/// elements can be injected before drawing.
1370///
1371/// The `fmt::Display` implementation (and [`Diagram::write`]) both use the
1372/// two-phase geometry caching pipeline internally, so rendering is O(n) in
1373/// the number of nodes.
1374///
1375/// # Example
1376/// ```rust
1377/// use railroad::*;
1378///
1379/// let dia = Diagram::new_with_stylesheet(
1380/// Sequence::new(vec![Box::new(Start) as Box<dyn Node>, Box::new(End)]),
1381/// &Stylesheet::Light,
1382/// );
1383/// let svg = dia.to_string();
1384/// assert!(svg.starts_with("<svg"));
1385/// ```
1386#[derive(Debug, Clone)]
1387pub struct Diagram<N> {
1388 root: N,
1389 extra_attributes: HashMap<String, String>,
1390 extra_elements: Vec<svg::Element>,
1391 left_padding: i64,
1392 right_padding: i64,
1393 top_padding: i64,
1394 bottom_padding: i64,
1395}
1396
1397impl<N: Node> Diagram<N> {
1398 /// Create a diagram using the given root-element.
1399 ///
1400 /// ```
1401 /// use railroad::*;
1402 ///
1403 /// let mut seq: Sequence::<Box<dyn Node>> = Sequence::default();
1404 /// seq.push(Box::new(Start))
1405 /// .push(Box::new(Terminal::new("Foobar".to_owned())))
1406 /// .push(Box::new(End));
1407 ///
1408 /// let mut dia = Diagram::new(seq);
1409 /// println!("{}", dia);
1410 /// ```
1411 pub fn new(root: N) -> Self {
1412 Self {
1413 root,
1414 extra_attributes: HashMap::default(),
1415 extra_elements: Vec::default(),
1416 left_padding: 10,
1417 right_padding: 10,
1418 top_padding: 10,
1419 bottom_padding: 10,
1420 }
1421 }
1422
1423 /// Create a diagram using the given root-element, adding the given stylesheet.
1424 ///
1425 /// ```rust
1426 /// use railroad::*;
1427 ///
1428 /// let mut seq: Sequence::<Box<dyn Node>> = Sequence::default();
1429 /// seq.push(Box::new(Start))
1430 /// .push(Box::new(Terminal::new("Foobar".to_owned())))
1431 /// .push(Box::new(End));
1432 ///
1433 /// let dia = Diagram::new_with_stylesheet(seq, &Stylesheet::Light);
1434 /// println!("{}", dia);
1435 /// ```
1436 pub fn new_with_stylesheet(root: N, style: &Stylesheet) -> Self {
1437 let mut dia = Self::new(root);
1438 dia.add_stylesheet(style);
1439 dia
1440 }
1441
1442 /// Create a diagram which has this library's default CSS style included.
1443 pub fn with_default_css(root: N) -> Self {
1444 let mut dia = Self::new(root);
1445 dia.add_default_css();
1446 dia
1447 }
1448
1449 /// Add the CSS for `style` as an additional `<style>` element.
1450 pub fn add_stylesheet(&mut self, style: &Stylesheet) {
1451 self.add_css(style.stylesheet());
1452 }
1453
1454 /// Add the default CSS as an additional `<style>` element.
1455 pub fn add_default_css(&mut self) {
1456 self.add_css(DEFAULT_CSS);
1457 }
1458
1459 /// Add the given CSS as an additional `<style>` element.
1460 pub fn add_css(&mut self, css: &str) {
1461 self.add_element(
1462 svg::Element::new("style")
1463 .set("type", "text/css")
1464 .raw_text(css),
1465 );
1466 }
1467
1468 /// Set an attribute on the `<svg>`-tag.
1469 pub fn attr(&mut self, key: String) -> collections::hash_map::Entry<'_, String, String> {
1470 self.extra_attributes.entry(key)
1471 }
1472
1473 /// Add an additional `svg::Element` which is written before the root-element
1474 pub fn add_element(&mut self, e: svg::Element) -> &mut Self {
1475 self.extra_elements.push(e);
1476 self
1477 }
1478
1479 /// Write this diagram's SVG-code to the given writer.
1480 ///
1481 /// # Errors
1482 /// Returns errors in the underlying writer.
1483 pub fn write(&self, writer: &mut impl io::Write) -> io::Result<()> {
1484 writer.write_all(self.to_string().as_bytes())
1485 }
1486
1487 /// Unwrap this diagram, returning the root node.
1488 pub fn into_inner(self) -> N {
1489 self.root
1490 }
1491}
1492
1493impl<N> Default for Diagram<N>
1494where
1495 N: Default,
1496{
1497 fn default() -> Self {
1498 Self {
1499 root: Default::default(),
1500 extra_attributes: HashMap::default(),
1501 extra_elements: Vec::default(),
1502 left_padding: 10,
1503 right_padding: 10,
1504 top_padding: 10,
1505 bottom_padding: 10,
1506 }
1507 }
1508}
1509
1510impl<N> Node for Diagram<N>
1511where
1512 N: Node,
1513{
1514 fn entry_height(&self) -> i64 {
1515 0
1516 }
1517
1518 fn height(&self) -> i64 {
1519 self.top_padding + self.root.height() + self.bottom_padding
1520 }
1521
1522 fn width(&self) -> i64 {
1523 self.left_padding + self.root.width() + self.right_padding
1524 }
1525
1526 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
1527 let geo = self.compute_geometry();
1528 self.draw_with_geometry(x, y, h_dir, &geo)
1529 }
1530
1531 fn compute_geometry(&self) -> NodeGeometry {
1532 let root_geo = self.root.compute_geometry();
1533 let height = self.top_padding + root_geo.height + self.bottom_padding;
1534 let width = self.left_padding + root_geo.width + self.right_padding;
1535 NodeGeometry {
1536 entry_height: 0,
1537 height,
1538 width,
1539 children: vec![root_geo],
1540 }
1541 }
1542
1543 fn draw_with_geometry(&self, x: i64, y: i64, h_dir: HDir, geo: &NodeGeometry) -> svg::Element {
1544 let mut e = svg::Element::new("svg")
1545 .set("xmlns", "http://www.w3.org/2000/svg")
1546 .set("xmlns:xlink", "http://www.w3.org/1999/xlink")
1547 .set("class", "railroad")
1548 .set("viewBox", &format!("0 0 {} {}", geo.width, geo.height));
1549
1550 #[cfg(feature = "visual-debug")]
1551 {
1552 e = e.set("xmlns:railroad", "http://www.github.com/lukaslueg/railroad");
1553 }
1554 for (k, v) in &self.extra_attributes {
1555 e = e.set(&k, &v);
1556 }
1557 for extra_ele in self.extra_elements.iter().cloned() {
1558 e = e.add(extra_ele);
1559 }
1560 e.add(
1561 svg::Element::new("rect")
1562 .set("width", "100%")
1563 .set("height", "100%")
1564 .set("class", "railroad_canvas"),
1565 )
1566 .add(self.root.draw_with_geometry(
1567 x + self.left_padding,
1568 y + self.top_padding,
1569 h_dir,
1570 &geo.children[0],
1571 ))
1572 }
1573
1574 fn render_with_geometry(
1575 &self,
1576 out: &mut svg::Renderer<'_>,
1577 x: i64,
1578 y: i64,
1579 h_dir: HDir,
1580 geo: &NodeGeometry,
1581 ) -> fmt::Result {
1582 let mut svg_tag = out.start_element("svg")?;
1583 svg_tag.attr("xmlns", "http://www.w3.org/2000/svg")?;
1584 svg_tag.attr("xmlns:xlink", "http://www.w3.org/1999/xlink")?;
1585 svg_tag.attr("class", "railroad")?;
1586 svg_tag.attr("viewBox", format_args!("0 0 {} {}", geo.width, geo.height))?;
1587 #[cfg(feature = "visual-debug")]
1588 svg_tag.attr("xmlns:railroad", "http://www.github.com/lukaslueg/railroad")?;
1589 svg_tag.attr_hashmap(&self.extra_attributes)?;
1590 svg_tag.finish()?;
1591
1592 for extra in &self.extra_elements {
1593 out.write_display(extra)?;
1594 }
1595
1596 let mut rect = out.start_element("rect")?;
1597 rect.attr("width", "100%")?;
1598 rect.attr("height", "100%")?;
1599 rect.attr("class", "railroad_canvas")?;
1600 rect.finish_empty()?;
1601
1602 self.root.render_with_geometry(
1603 out,
1604 x + self.left_padding,
1605 y + self.top_padding,
1606 h_dir,
1607 &geo.children[0],
1608 )?;
1609 out.end_element("svg")
1610 }
1611}
1612
1613impl<N> fmt::Display for Diagram<N>
1614where
1615 N: Node,
1616{
1617 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
1618 let geo = self.compute_geometry();
1619 let mut renderer = svg::Renderer::new(f);
1620 self.render_with_geometry(&mut renderer, 0, 0, HDir::LTR, &geo)
1621 }
1622}
1623
1624#[cfg(test)]
1625#[cfg(not(feature = "visual-debug"))]
1626mod tests_without_visual_debug {
1627 use super::*;
1628 use std::cell::Cell;
1629
1630 /// A counting wrapper that increments a counter on every geometry call.
1631 struct CountingNode<'a> {
1632 inner: Box<dyn Node>,
1633 calls: &'a Cell<usize>,
1634 }
1635
1636 impl Node for CountingNode<'_> {
1637 fn entry_height(&self) -> i64 {
1638 self.calls.set(self.calls.get() + 1);
1639 self.inner.entry_height()
1640 }
1641 fn height(&self) -> i64 {
1642 self.calls.set(self.calls.get() + 1);
1643 self.inner.height()
1644 }
1645 fn width(&self) -> i64 {
1646 self.calls.set(self.calls.get() + 1);
1647 self.inner.width()
1648 }
1649 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
1650 self.inner.draw(x, y, h_dir)
1651 }
1652 // compute_geometry / draw_with_geometry use the default impls, which
1653 // call entry_height/height/width exactly once each → O(n) total.
1654 }
1655
1656 /// Verify that drawing via compute_geometry + draw_with_geometry calls each
1657 /// leaf node's geometry methods exactly 3 times (once per entry_height /
1658 /// height / width) regardless of tree depth.
1659 #[test]
1660 fn geometry_cache_linear_calls() {
1661 let calls = Cell::new(0usize);
1662 let leaf = CountingNode {
1663 inner: Box::new(Terminal::new("leaf".to_owned())),
1664 calls: &calls,
1665 };
1666
1667 // Wrap in two levels of Sequence: [[leaf]]
1668 let inner_seq: Sequence<Box<dyn Node>> =
1669 Sequence::new(vec![Box::new(leaf) as Box<dyn Node>]);
1670 let outer_seq: Sequence<Box<dyn Node>> =
1671 Sequence::new(vec![Box::new(inner_seq) as Box<dyn Node>]);
1672
1673 let geo = outer_seq.compute_geometry();
1674 let _ = outer_seq.draw_with_geometry(0, 0, HDir::LTR, &geo);
1675
1676 // entry_height + height + width = 3 calls, regardless of nesting depth
1677 assert_eq!(
1678 calls.get(),
1679 3,
1680 "each leaf geometry method must be called exactly once"
1681 );
1682 }
1683}
1684
1685#[cfg(test)]
1686#[cfg(feature = "visual-debug")]
1687mod tests_with_visual_debug {
1688 use super::*;
1689 use std::cell::Cell;
1690
1691 /// A counting wrapper that increments a counter on every geometry call.
1692 struct CountingNode<'a> {
1693 inner: Box<dyn Node>,
1694 calls: &'a Cell<usize>,
1695 }
1696
1697 impl Node for CountingNode<'_> {
1698 fn entry_height(&self) -> i64 {
1699 self.calls.set(self.calls.get() + 1);
1700 self.inner.entry_height()
1701 }
1702 fn height(&self) -> i64 {
1703 self.calls.set(self.calls.get() + 1);
1704 self.inner.height()
1705 }
1706 fn width(&self) -> i64 {
1707 self.calls.set(self.calls.get() + 1);
1708 self.inner.width()
1709 }
1710 fn draw(&self, x: i64, y: i64, h_dir: HDir) -> svg::Element {
1711 self.inner.draw(x, y, h_dir)
1712 }
1713 // compute_geometry / draw_with_geometry use the default impls, which
1714 // call entry_height/height/width exactly once each when cached geometry
1715 // is threaded through the visual-debug path correctly.
1716 }
1717
1718 #[test]
1719 fn visual_debug_geometry_cache_linear_calls() {
1720 let calls = Cell::new(0usize);
1721 let leaf = CountingNode {
1722 inner: Box::new(Terminal::new("leaf".to_owned())),
1723 calls: &calls,
1724 };
1725
1726 let mut nested: Box<dyn Node> = Box::new(leaf);
1727 for _ in 0..8 {
1728 nested = Box::new(Sequence::new(vec![nested]));
1729 }
1730
1731 let geo = nested.compute_geometry();
1732 let _ = nested.draw_with_geometry(0, 0, HDir::LTR, &geo);
1733
1734 assert_eq!(
1735 calls.get(),
1736 3,
1737 "visual-debug must not trigger extra geometry calls during draw_with_geometry"
1738 );
1739 }
1740}
1741
1742#[cfg(test)]
1743mod tests {
1744 use super::*;
1745
1746 #[test]
1747 fn debug_impl() {
1748 let s = Sequence::new(vec![
1749 Box::new(SimpleStart) as Box<dyn Node>,
1750 Box::new(SimpleEnd),
1751 ]);
1752 assert_eq!(
1753 "Sequence { children: [Node { entry_height: 5, height: 10, width: 15 }, Node { entry_height: 5, height: 10, width: 15 }], spacing: 10 }",
1754 format!("{:?}", &s)
1755 );
1756 assert_eq!(
1757 "Node { entry_height: 5, height: 10, width: 40 }",
1758 format!("{:?}", &s as &dyn Node)
1759 );
1760 }
1761
1762 /// Helper: build a nested Sequence tree of the given depth and width.
1763 fn make_deep_seq(depth: usize, width: usize) -> Box<dyn Node> {
1764 if depth == 0 {
1765 Box::new(Terminal::new("x".to_owned()))
1766 } else {
1767 let children: Vec<Box<dyn Node>> = (0..width)
1768 .map(|_| make_deep_seq(depth - 1, width))
1769 .collect();
1770 Box::new(Sequence::new(children))
1771 }
1772 }
1773
1774 /// draw_with_geometry produces the same SVG as the original draw path.
1775 #[test]
1776 fn geometry_cache_regression() {
1777 let root = make_deep_seq(3, 3);
1778 let dia_a = Diagram::new(make_deep_seq(3, 3));
1779 let dia_b = Diagram::new(make_deep_seq(3, 3));
1780
1781 // draw() uses the two-phase path internally; draw_with_geometry is its
1782 // implementation. We verify that `format!` output (which calls draw)
1783 // is stable across two calls (no hidden mutable state).
1784 let svg_a = format!("{}", dia_a);
1785 let svg_b = format!("{}", dia_b);
1786 assert_eq!(svg_a, svg_b, "SVG output must be deterministic");
1787
1788 // Also check that width/height/entry_height are consistent with the
1789 // geometry cache.
1790 let geo = root.compute_geometry();
1791 assert_eq!(geo.entry_height, root.entry_height());
1792 assert_eq!(geo.height, root.height());
1793 assert_eq!(geo.width, root.width());
1794 }
1795
1796 #[test]
1797 fn diagram_write_matches_display() {
1798 let diagram = Diagram::new(make_deep_seq(2, 3));
1799 let displayed = format!("{}", diagram);
1800
1801 let mut written = Vec::new();
1802 diagram.write(&mut written).unwrap();
1803
1804 assert_eq!(String::from_utf8(written).unwrap(), displayed);
1805 }
1806
1807 const PAYLOADS: &[&str] = &[
1808 r#""><script>alert(1)</script>"#,
1809 r#"' onload='alert(1)"#,
1810 r#"foo & bar"#,
1811 r#"</style><script>bad</script>"#,
1812 r#"foo"bar"#,
1813 ];
1814
1815 fn assert_no_payload(svg: &str, payload: &str) {
1816 assert!(
1817 !svg.contains(payload),
1818 "raw payload {payload:?} found in SVG output"
1819 );
1820 }
1821
1822 /// Terminal and NonTerminal labels are rendered as SVG text content.
1823 #[test]
1824 fn terminal_label_no_injection() {
1825 for payload in PAYLOADS {
1826 let svg = format!("{}", Diagram::new(Terminal::new(payload.to_string())));
1827 assert_no_payload(&svg, payload);
1828 let svg = format!("{}", Diagram::new(NonTerminal::new(payload.to_string())));
1829 assert_no_payload(&svg, payload);
1830 }
1831 }
1832
1833 /// Comment text is rendered as SVG text content.
1834 #[test]
1835 fn comment_text_no_injection() {
1836 for payload in PAYLOADS {
1837 let svg = format!("{}", Diagram::new(Comment::new(payload.to_string())));
1838 assert_no_payload(&svg, payload);
1839 }
1840 }
1841
1842 /// Link URIs end up in an xlink:href attribute.
1843 #[test]
1844 fn link_uri_no_injection() {
1845 for payload in PAYLOADS {
1846 let node = Link::new(Empty, payload.to_string());
1847 let svg = format!("{}", Diagram::new(node));
1848 assert_no_payload(&svg, payload);
1849 }
1850 }
1851
1852 /// User-supplied attribute keys and values (via `.attr()`) must be escaped.
1853 #[test]
1854 fn node_attr_no_injection() {
1855 for payload in PAYLOADS {
1856 // Dangerous value
1857 let mut t = Terminal::new("x".to_owned());
1858 t.attr("data-x".to_owned()).or_insert(payload.to_string());
1859 let svg = format!("{}", Diagram::new(t));
1860 assert_no_payload(&svg, payload);
1861
1862 // Dangerous key
1863 let mut t = Terminal::new("x".to_owned());
1864 t.attr(payload.to_string()).or_insert("value".to_owned());
1865 let svg = format!("{}", Diagram::new(t));
1866 assert_no_payload(&svg, payload);
1867 }
1868 }
1869}