Skip to main content

merman_render/presentation/
mod.rs

1//! Product presentation inputs that are intentionally separate from Mermaid config and SVG output.
2
3mod compiler;
4mod presets;
5mod profile;
6mod theme;
7
8pub use presets::{HostThemePreset, ThemePresetDescriptor, theme_preset_descriptors};
9#[cfg(any(
10    feature = "diagram-flowchart",
11    feature = "diagram-swimlane",
12    feature = "diagram-agentflow"
13))]
14pub(crate) use profile::FlowchartPresentationPolicy;
15pub use profile::{
16    Presentation, PresentationAspectApplicability, PresentationAspectDescriptor,
17    PresentationAspectResolution, PresentationAspectState, PresentationProfile,
18    PresentationProfileDescriptor, PresentationRenderPolicy, ResolvedPresentation,
19    presentation_profile_descriptors,
20};
21pub use theme::{HostTheme, HostThemeAppearance, ThemeRole};
22
23#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
24#[non_exhaustive]
25pub enum PresentationError {
26    #[error("unknown host theme preset `{0}`")]
27    UnknownThemePreset(String),
28    #[error("unknown presentation profile `{0}`")]
29    UnknownPresentationProfile(String),
30    #[error("unknown theme role `{0}`")]
31    UnknownThemeRole(String),
32    #[error("{field} must be a non-empty single CSS declaration value")]
33    InvalidCssValue { field: String },
34}
35
36fn css_declaration_value(
37    value: impl Into<String>,
38    field: &str,
39) -> Result<String, PresentationError> {
40    let value = value.into();
41    let trimmed = value.trim();
42    let invalid = trimmed.is_empty()
43        || trimmed
44            .chars()
45            .any(|ch| ch.is_control() || matches!(ch, ';' | '"' | '\'' | '<' | '>' | '{' | '}'));
46    if invalid {
47        return Err(PresentationError::InvalidCssValue {
48            field: field.to_string(),
49        });
50    }
51    Ok(trimmed.to_string())
52}
53
54fn css_font_family(value: impl Into<String>, field: &str) -> Result<String, PresentationError> {
55    let value = value.into();
56    let trimmed = value.trim();
57    let invalid = trimmed.is_empty()
58        || trimmed
59            .chars()
60            .any(|ch| ch.is_control() || matches!(ch, ';' | '<' | '>' | '{' | '}'));
61    if invalid {
62        return Err(PresentationError::InvalidCssValue {
63            field: field.to_string(),
64        });
65    }
66    Ok(trimmed.to_string())
67}