merman_render/presentation/
profile.rs1use 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 pub const fn render_policy(&self) -> PresentationRenderPolicy {
103 PresentationRenderPolicy {
104 profile: self.presentation.profile,
105 }
106 }
107}
108
109#[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}