Skip to main content

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}