Skip to main content

merman_render/presentation/
profile.rs

1use merman_core::{Engine, MermaidConfig};
2use serde_json::Value;
3
4use super::{HostTheme, PresentationError};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7#[non_exhaustive]
8pub enum PresentationProfile {
9    MermanModern,
10}
11
12impl PresentationProfile {
13    pub const ALL: [Self; 1] = [Self::MermanModern];
14
15    pub const fn id(self) -> &'static str {
16        match self {
17            Self::MermanModern => "merman-modern",
18        }
19    }
20
21    pub fn from_id(id: &str) -> Result<Self, PresentationError> {
22        Self::ALL
23            .into_iter()
24            .find(|profile| profile.id() == id)
25            .ok_or_else(|| PresentationError::UnknownPresentationProfile(id.to_string()))
26    }
27
28    #[cfg(any(
29        feature = "diagram-flowchart",
30        feature = "diagram-swimlane",
31        feature = "diagram-agentflow"
32    ))]
33    const fn flowchart_policy(self) -> FlowchartPresentationPolicy {
34        match self {
35            Self::MermanModern => FlowchartPresentationPolicy {
36                edge_corner_radius: None,
37                edge_label_padding: 4.0,
38                compact_edge_corners: true,
39            },
40        }
41    }
42}
43
44#[derive(Debug, Clone, Default, PartialEq, Eq)]
45pub struct Presentation {
46    profile: Option<PresentationProfile>,
47    theme: Option<HostTheme>,
48}
49
50impl Presentation {
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    pub const fn profile(&self) -> Option<PresentationProfile> {
56        self.profile
57    }
58
59    pub fn theme(&self) -> Option<&HostTheme> {
60        self.theme.as_ref()
61    }
62
63    pub fn with_profile(mut self, profile: PresentationProfile) -> Self {
64        self.profile = Some(profile);
65        self
66    }
67
68    pub fn with_theme(mut self, theme: HostTheme) -> Self {
69        self.theme = Some(theme);
70        self
71    }
72
73    pub fn resolve(self) -> ResolvedPresentation {
74        let mut mermaid_config = self.profile.map(profile_defaults).unwrap_or_default();
75        if let Some(theme) = &self.theme {
76            let theme = theme.mermaid_config_patch();
77            mermaid_config.deep_merge(theme.as_value());
78        }
79        ResolvedPresentation {
80            presentation: self,
81            mermaid_config,
82        }
83    }
84}
85
86#[derive(Debug, Clone)]
87pub struct ResolvedPresentation {
88    presentation: Presentation,
89    mermaid_config: MermaidConfig,
90}
91
92impl ResolvedPresentation {
93    pub fn presentation(&self) -> &Presentation {
94        &self.presentation
95    }
96
97    pub fn materialize_engine(&self, engine: Engine) -> Engine {
98        engine.with_site_config(self.mermaid_config.clone())
99    }
100
101    /// Returns the small renderer-owned policy that accompanies the materialized Mermaid config.
102    pub const fn render_policy(&self) -> PresentationRenderPolicy {
103        PresentationRenderPolicy {
104            profile: self.presentation.profile,
105        }
106    }
107}
108
109/// Opaque renderer policy derived from a resolved product presentation.
110///
111/// The policy intentionally excludes Mermaid configuration and presentation theme data. Callers should
112/// first materialize the resolved presentation into the parsing engine, then carry this compact
113/// value alongside the resulting typed render model.
114#[derive(Debug, Clone, Copy, Default, PartialEq)]
115pub struct PresentationRenderPolicy {
116    profile: Option<PresentationProfile>,
117}
118
119impl PresentationRenderPolicy {
120    pub const fn profile(self) -> Option<PresentationProfile> {
121        self.profile
122    }
123
124    #[cfg(any(
125        feature = "diagram-flowchart",
126        feature = "diagram-swimlane",
127        feature = "diagram-agentflow"
128    ))]
129    pub(crate) const fn flowchart(self) -> Option<FlowchartPresentationPolicy> {
130        match self.profile {
131            Some(profile) => Some(profile.flowchart_policy()),
132            None => None,
133        }
134    }
135
136    pub(crate) fn resolve_aspects(
137        self,
138        flowchart_svg_applicable: bool,
139        uses_elk: bool,
140        elk_available: bool,
141    ) -> Vec<PresentationAspectResolution> {
142        match self.profile {
143            None => Vec::new(),
144            Some(PresentationProfile::MermanModern) => {
145                let [global_defaults, flowchart_svg, flowchart_elk_default] = MERMAN_MODERN_ASPECTS;
146                vec![
147                    PresentationAspectResolution::new(
148                        global_defaults,
149                        PresentationAspectState::Active,
150                    ),
151                    PresentationAspectResolution::new(
152                        flowchart_svg,
153                        if flowchart_svg_applicable {
154                            PresentationAspectState::Active
155                        } else {
156                            PresentationAspectState::Inactive
157                        },
158                    ),
159                    PresentationAspectResolution::new(
160                        flowchart_elk_default,
161                        if !flowchart_svg_applicable || !uses_elk {
162                            PresentationAspectState::Inactive
163                        } else if elk_available {
164                            PresentationAspectState::Active
165                        } else {
166                            PresentationAspectState::Blocked
167                        },
168                    ),
169                ]
170            }
171        }
172    }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176#[non_exhaustive]
177pub enum PresentationAspectState {
178    Active,
179    Inactive,
180    Blocked,
181}
182
183impl PresentationAspectState {
184    pub const fn as_str(self) -> &'static str {
185        match self {
186            Self::Active => "active",
187            Self::Inactive => "inactive",
188            Self::Blocked => "blocked",
189        }
190    }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct PresentationAspectResolution {
195    descriptor: PresentationAspectDescriptor,
196    state: PresentationAspectState,
197}
198
199impl PresentationAspectResolution {
200    const fn new(descriptor: PresentationAspectDescriptor, state: PresentationAspectState) -> Self {
201        Self { descriptor, state }
202    }
203
204    pub const fn id(self) -> &'static str {
205        self.descriptor.id()
206    }
207
208    pub const fn state(self) -> PresentationAspectState {
209        self.state
210    }
211
212    pub const fn required_capability_id(self) -> Option<&'static str> {
213        self.descriptor.required_capability_id()
214    }
215}
216
217#[derive(Debug, Clone, Copy, Default, PartialEq)]
218#[cfg(any(
219    feature = "diagram-flowchart",
220    feature = "diagram-swimlane",
221    feature = "diagram-agentflow"
222))]
223pub(crate) struct FlowchartPresentationPolicy {
224    pub(crate) edge_corner_radius: Option<f64>,
225    pub(crate) edge_label_padding: f64,
226    pub(crate) compact_edge_corners: bool,
227}
228
229fn profile_defaults(profile: PresentationProfile) -> MermaidConfig {
230    match profile {
231        PresentationProfile::MermanModern => {
232            let theme_variables = [
233                ("mainBkg", "#F8FAFC"),
234                ("nodeBorder", "#64748B"),
235                ("nodeTextColor", "#1E293B"),
236                ("primaryColor", "#F8FAFC"),
237                ("primaryBorderColor", "#64748B"),
238                ("primaryTextColor", "#1E293B"),
239                ("lineColor", "#64748B"),
240                ("arrowheadColor", "#64748B"),
241                ("edgeLabelBackground", "#FFFFFF"),
242                ("clusterBkg", "#F1F5F9"),
243                ("clusterBorder", "#CBD5E1"),
244            ]
245            .into_iter()
246            .map(|(key, value)| (key.to_string(), Value::String(value.to_string())))
247            .collect();
248            MermaidConfig::from_value(serde_json::json!({
249                "theme": "redux",
250                "look": "neo",
251                "flowchart": { "layout": "elk" },
252                "themeVariables": Value::Object(theme_variables),
253            }))
254        }
255    }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259#[non_exhaustive]
260pub enum PresentationAspectApplicability {
261    AllDiagrams,
262    Family(&'static str),
263}
264
265impl PresentationAspectApplicability {
266    pub const fn kind_id(self) -> &'static str {
267        match self {
268            Self::AllDiagrams => "all-diagrams",
269            Self::Family(_) => "family",
270        }
271    }
272
273    pub const fn family_id(self) -> Option<&'static str> {
274        match self {
275            Self::AllDiagrams => None,
276            Self::Family(family_id) => Some(family_id),
277        }
278    }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct PresentationAspectDescriptor {
283    id: &'static str,
284    applicability: PresentationAspectApplicability,
285    required_capability_id: Option<&'static str>,
286}
287
288impl PresentationAspectDescriptor {
289    pub const fn id(&self) -> &'static str {
290        self.id
291    }
292
293    pub const fn applicability(&self) -> PresentationAspectApplicability {
294        self.applicability
295    }
296
297    pub const fn required_capability_id(&self) -> Option<&'static str> {
298        self.required_capability_id
299    }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub struct PresentationProfileDescriptor {
304    profile: PresentationProfile,
305    aspects: &'static [PresentationAspectDescriptor],
306}
307
308impl PresentationProfileDescriptor {
309    pub const fn id(&self) -> &'static str {
310        self.profile.id()
311    }
312
313    pub const fn aspects(&self) -> &'static [PresentationAspectDescriptor] {
314        self.aspects
315    }
316}
317
318const MERMAN_MODERN_ASPECTS: [PresentationAspectDescriptor; 3] = [
319    PresentationAspectDescriptor {
320        id: "global-defaults",
321        applicability: PresentationAspectApplicability::AllDiagrams,
322        required_capability_id: None,
323    },
324    PresentationAspectDescriptor {
325        id: "flowchart-svg",
326        applicability: PresentationAspectApplicability::Family("flowchart"),
327        required_capability_id: None,
328    },
329    PresentationAspectDescriptor {
330        id: "flowchart-elk-default",
331        applicability: PresentationAspectApplicability::Family("flowchart"),
332        required_capability_id: Some("layout-elk"),
333    },
334];
335
336const PRESENTATION_PROFILE_DESCRIPTORS: [PresentationProfileDescriptor; 1] =
337    [PresentationProfileDescriptor {
338        profile: PresentationProfile::MermanModern,
339        aspects: &MERMAN_MODERN_ASPECTS,
340    }];
341
342pub const fn presentation_profile_descriptors() -> &'static [PresentationProfileDescriptor] {
343    &PRESENTATION_PROFILE_DESCRIPTORS
344}