Skip to main content

merman_core/
lib.rs

1#![forbid(unsafe_code)]
2//! Mermaid parser + semantic model (headless).
3//!
4//! Design goals:
5//! - 1:1 parity with the repository's pinned upstream Mermaid baseline
6//! - deterministic, testable outputs (semantic snapshot goldens)
7//! - runtime-agnostic async APIs (no specific executor required)
8//!
9//! Default features are empty. Select `diagram-*` features for the required logical families or
10//! `all-diagrams` for the complete parser surface. Family-exclusive public model types and enum
11//! variants are conditional; recognition and custom registries remain available without a family.
12
13pub mod baseline;
14pub mod common;
15pub mod common_db;
16#[cfg(any(
17    feature = "diagram-mindmap",
18    feature = "diagram-state",
19    feature = "diagram-usecase"
20))]
21mod compatibility_json;
22pub mod config;
23pub mod detect;
24pub mod diagram;
25pub mod diagrams;
26pub mod editor;
27pub mod entities;
28pub mod error;
29mod family;
30pub mod generated;
31pub mod geom;
32#[cfg(any(
33    test,
34    feature = "diagram-flowchart",
35    feature = "diagram-swimlane",
36    feature = "diagram-agentflow",
37    feature = "diagram-kanban",
38    feature = "diagram-sequence"
39))]
40mod inline_config;
41pub mod models;
42pub mod operation;
43mod parse_pipeline;
44pub mod preprocess;
45pub mod resources;
46pub mod runtime;
47pub mod sanitize;
48pub mod svg_security;
49#[doc(hidden)]
50pub mod terminal_text;
51mod theme;
52pub mod theme_color;
53pub mod time;
54pub mod utils;
55mod yaml_config;
56
57pub use config::MermaidConfig;
58pub use detect::{Detector, DetectorRegistry};
59pub use diagram::{
60    AGENTFLOW_CONTAINMENT_VIOLATION_WARNING_RULE_ID, AGENTFLOW_SHAPE_REMOVED_WARNING_RULE_ID,
61    AGENTFLOW_SHAPE_UNSUPPORTED_WARNING_RULE_ID, BLOCK_WIDTH_WARNING_RULE_ID,
62    BuiltinRenderSemantic, CapturedPanic, CustomJsonProvenance, CustomJsonRenderModel,
63    CustomJsonRenderParser, DiagramParseOutcome, DiagramParseSnapshot, DiagramRegistry,
64    DiagramSemanticParser, DiagramSnapshotCapture, DiagramWarningFact,
65    FLOWCHART_EXPLICIT_DIRECTION_WARNING_RULE_ID, FLOWCHART_UNKNOWN_STYLE_TARGET_WARNING_RULE_ID,
66    GIT_GRAPH_DUPLICATE_COMMIT_WARNING_RULE_ID, ParsedDiagram, ParsedDiagramRender,
67    ParsedEditorFacts, RenderDiagramRegistry, RenderSemanticModel,
68};
69pub use editor::{
70    EditorExpectedSyntax, EditorExpectedSyntaxKind, EditorFamilySemantics, EditorRenamePolicy,
71    EditorSemanticCompleteness, EditorSemanticDiagnostic, EditorSemanticDiagnosticKind,
72    EditorSemanticFacts, EditorSemanticKind, EditorSemanticRole, EditorSemanticSymbol, SourceSpan,
73};
74pub use error::{Error, ParseDiagnostic, ParseDiagnosticSpanKind, Result};
75pub use family::{
76    BuiltInTypedRenderFamily, DiagramFamilyCapability, DiagramFamilyId, DiagramFamilySelector,
77    DiagramHeaderFact, diagram_type_family_id, diagram_type_family_kind, diagram_type_metadata_id,
78    diagram_type_render_model_kind,
79};
80pub use operation::{
81    CancelReason, OperationCancelled, OperationControl, OperationControlResult,
82    OperationLedgerError, OperationPhase, OperationResourceDomain, OperationResourceLimitExceeded,
83    OperationResourceOverride, OperationResourceProvenance,
84};
85pub use preprocess::{
86    PreprocessResult, PreprocessedSource, preprocess_diagram, preprocess_diagram_with_known_type,
87};
88
89/// Maximum nested diagram/include depth accepted by recursive parsers.
90pub const MAX_DIAGRAM_NESTING_DEPTH: usize = 256;
91
92/// Returns Mermaid theme names supported by the pinned baseline.
93pub fn supported_themes() -> &'static [&'static str] {
94    theme::SUPPORTED_THEME_NAMES
95}
96
97/// Returns metadata names backed by semantic parsers compiled into this build.
98///
99/// Aliases share their catalog-owned metadata name. This does not promise an output adapter;
100/// use the complete [`diagram_family_capabilities`] catalog to distinguish known identities
101/// from available implementations.
102pub fn supported_diagrams() -> &'static [&'static str] {
103    family::supported_diagram_metadata_ids()
104}
105
106/// Returns every known Mermaid diagram id in the pinned baseline, including unavailable ones.
107///
108/// Identity, detector, header, and configuration facts are retained independently of the parser,
109/// editor, and typed render-model callbacks compiled into this build.
110pub fn diagram_family_capabilities() -> &'static [DiagramFamilyCapability] {
111    family::diagram_family_capabilities()
112}
113
114/// Returns the complete catalog-owned mapping from logical families to Cargo selectors.
115///
116/// These declarations are independent of enabled features and exclude infrastructure models.
117/// Selecting a family does not enable an output format or optional layout engine.
118pub fn diagram_family_selectors() -> &'static [DiagramFamilySelector] {
119    family::diagram_family_selectors()
120}
121
122/// Returns each compiled concrete built-in typed render-model family exactly once.
123///
124/// This is independent of output adapters and deduplicates aliases sharing a typed model.
125pub fn built_in_typed_render_families() -> &'static [BuiltInTypedRenderFamily] {
126    family::built_in_typed_render_families()
127}
128
129/// Returns known header facts for Mermaid diagram starters in the pinned baseline.
130///
131/// A header identifies a known family; actionable suggestions must also check that its semantic
132/// parser is available in [`diagram_family_capabilities`].
133pub fn diagram_header_facts() -> &'static [DiagramHeaderFact] {
134    family::diagram_header_facts()
135}
136
137fn build_default_effective_config(
138    site_config: &MermaidConfig,
139) -> std::result::Result<MermaidConfig, theme_color::ColorError> {
140    let mut effective_config = site_config.clone();
141    theme::apply_theme_defaults(&mut effective_config)?;
142    Ok(effective_config)
143}
144
145fn merge_site_config_override(target: &mut MermaidConfig, mut site_config: MermaidConfig) {
146    config::mirror_legacy_font_family_into_theme_variables(&mut site_config);
147    // initialize() normalizes an unknown global theme before it becomes a user layer.
148    // Scoped themes instead fall through during diagram appearance resolution.
149    if site_config.as_value().get("theme").is_some_and(|value| {
150        !value.is_null()
151            && !value
152                .as_str()
153                .is_some_and(|name| name == "null" || theme::SUPPORTED_THEME_NAMES.contains(&name))
154    }) && let Some(theme) = generated::upstream_default_config().as_value().get("theme")
155    {
156        site_config.set_value("theme", theme.clone());
157    }
158    let explicit_secure_policy = site_config
159        .as_value()
160        .get("secure")
161        .filter(|value| value.is_array())
162        .map(config::clone_value_nonrecursive);
163    target.deep_merge(site_config.as_value());
164
165    // Merman adds host-level hardening beyond Mermaid's upstream defaults. An explicit site
166    // policy is host authority, so it replaces that added list after the source-compatible array
167    // merge instead of making the hardening impossible to opt out of.
168    if let Some(secure) = explicit_secure_policy {
169        target.set_value("secure", secure);
170    }
171}
172
173fn generated_default_effective_config()
174-> std::result::Result<MermaidConfig, theme_color::ColorError> {
175    static DEFAULT_EFFECTIVE_CONFIG: std::sync::OnceLock<
176        std::result::Result<MermaidConfig, theme_color::ColorError>,
177    > = std::sync::OnceLock::new();
178    DEFAULT_EFFECTIVE_CONFIG
179        .get_or_init(|| build_default_effective_config(&generated::default_site_config()))
180        .clone()
181}
182
183/// Parser behavior switches for model-producing parse facades.
184#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
185pub struct ParseOptions {
186    /// Return an `error` diagram model from JSON/render facades when diagram parsing fails.
187    pub suppress_errors: bool,
188}
189
190impl ParseOptions {
191    /// Strict parsing (errors are returned).
192    pub fn strict() -> Self {
193        Self {
194            suppress_errors: false,
195        }
196    }
197
198    /// Lenient model parsing: return an `error` diagram from JSON/render facades on failure.
199    pub fn lenient() -> Self {
200        Self {
201            suppress_errors: true,
202        }
203    }
204}
205
206/// Metadata extracted before semantic diagram parsing.
207#[derive(Debug, Clone)]
208pub struct ParseMetadata {
209    /// Mermaid diagram type id selected by detection or supplied by a known-type parse entrypoint.
210    pub diagram_type: String,
211    /// Parsed config overrides extracted from front-matter and directives.
212    /// This mirrors Mermaid's `mermaidAPI.parse()` return shape.
213    pub config: MermaidConfig,
214    /// The effective config used for detection/parsing after applying site defaults.
215    pub effective_config: MermaidConfig,
216    /// Sanitized Mermaid title from front-matter/directives, when present.
217    pub title: Option<String>,
218}
219
220/// Headless Mermaid parser engine.
221///
222/// An engine owns detector/parser registries and a site-level Mermaid configuration. It is cheap
223/// to clone when callers need per-request option variants.
224#[derive(Debug, Clone)]
225pub struct Engine {
226    registry: DetectorRegistry,
227    diagram_registry: DiagramRegistry,
228    render_diagram_registry: RenderDiagramRegistry,
229    site_config: MermaidConfig,
230    site_config_delta: MermaidConfig,
231    default_effective_config: std::result::Result<MermaidConfig, theme_color::ColorError>,
232    runtime_policy: runtime::RuntimePolicy,
233}
234
235impl Default for Engine {
236    fn default() -> Self {
237        let site_config = generated::default_site_config();
238        let default_effective_config = generated_default_effective_config();
239
240        Self {
241            registry: DetectorRegistry::pinned_mermaid_baseline(),
242            diagram_registry: DiagramRegistry::pinned_mermaid_baseline(),
243            render_diagram_registry: RenderDiagramRegistry::pinned_mermaid_baseline(),
244            site_config,
245            site_config_delta: MermaidConfig::empty_object(),
246            default_effective_config,
247            runtime_policy: runtime::RuntimePolicy::deterministic(),
248        }
249    }
250}
251
252impl Engine {
253    /// Creates an engine using the pinned Mermaid baseline registries and default site config.
254    pub fn new() -> Self {
255        Self::default()
256    }
257
258    /// Creates an engine backed by all native system adapters.
259    pub fn try_native() -> std::result::Result<Self, runtime::RuntimePolicyError> {
260        Ok(Self::new().with_runtime_policy(runtime::RuntimePolicy::try_native()?))
261    }
262
263    pub(crate) fn default_effective_config(&self) -> Result<MermaidConfig> {
264        self.default_effective_config.clone().map_err(Error::from)
265    }
266
267    /// Overrides the "today" value used by diagrams that depend on local time (e.g. Gantt).
268    ///
269    /// This exists primarily to make fixture snapshots deterministic. The default runtime policy
270    /// uses the Unix epoch; native callers must explicitly select [`Engine::try_native`].
271    pub fn with_fixed_today(mut self, today: Option<time::CivilDate>) -> Self {
272        self.runtime_policy = self.runtime_policy.with_fixed_today(today);
273        self
274    }
275
276    /// Selects a fixed local UTC offset for diagrams with local-time semantics.
277    pub fn try_with_fixed_local_offset_minutes(
278        mut self,
279        offset_minutes: i32,
280    ) -> std::result::Result<Self, runtime::RuntimePolicyError> {
281        self.runtime_policy = self
282            .runtime_policy
283            .try_with_fixed_local_offset_minutes(offset_minutes)?;
284        Ok(self)
285    }
286
287    /// Installs an already-resolved local timezone without consulting ambient process state.
288    pub fn with_local_time_zone(mut self, time_zone: time::LocalTimeZone) -> Self {
289        self.runtime_policy = self.runtime_policy.with_local_time_zone(time_zone);
290        self
291    }
292
293    /// Replaces the complete runtime policy used to begin future operations.
294    pub fn with_runtime_policy(mut self, policy: runtime::RuntimePolicy) -> Self {
295        self.runtime_policy = policy;
296        self
297    }
298
299    /// Replays a previously frozen runtime context without consulting system state.
300    pub fn with_operation_context(self, context: runtime::OperationContext) -> Self {
301        self.with_runtime_policy(runtime::RuntimePolicy::from_operation_context(context))
302    }
303
304    pub fn runtime_policy(&self) -> &runtime::RuntimePolicy {
305        &self.runtime_policy
306    }
307
308    pub fn begin_operation(
309        &self,
310    ) -> std::result::Result<runtime::OperationContext, runtime::RuntimePolicyError> {
311        self.runtime_policy.begin_operation()
312    }
313
314    /// Returns the fixed local timezone offset configured for this engine.
315    pub fn fixed_local_offset_minutes(&self) -> Option<i32> {
316        self.runtime_policy.fixed_local_offset_minutes()
317    }
318
319    pub fn local_time_zone(&self) -> &time::LocalTimeZone {
320        self.runtime_policy.local_time_zone()
321    }
322
323    /// Applies site-level Mermaid config defaults.
324    pub fn with_site_config(mut self, site_config: MermaidConfig) -> Self {
325        if site_config.is_empty_object() {
326            return self;
327        }
328        // Keep the user's layer separate: an explicit schema-default appearance still
329        // outranks a diagram-specific default.
330        merge_site_config_override(&mut self.site_config_delta, site_config.clone());
331        merge_site_config_override(&mut self.site_config, site_config);
332        self.default_effective_config = build_default_effective_config(&self.site_config);
333        self
334    }
335
336    /// Replaces the complete site-config environment while preserving custom registries.
337    ///
338    /// `None` restores the pinned Mermaid defaults. An explicit config is merged onto those
339    /// defaults without inheriting values from the engine's previous site config.
340    pub fn with_exact_site_config(mut self, site_config: Option<MermaidConfig>) -> Self {
341        self.site_config = generated::default_site_config();
342        self.site_config_delta = MermaidConfig::empty_object();
343        if let Some(site_config) = site_config {
344            merge_site_config_override(&mut self.site_config_delta, site_config.clone());
345            merge_site_config_override(&mut self.site_config, site_config);
346        }
347        self.default_effective_config = build_default_effective_config(&self.site_config);
348        self
349    }
350
351    /// Returns the detector registry used for automatic diagram type detection.
352    pub fn registry(&self) -> &DetectorRegistry {
353        &self.registry
354    }
355
356    /// Returns a mutable detector registry for custom diagram detection.
357    pub fn registry_mut(&mut self) -> &mut DetectorRegistry {
358        &mut self.registry
359    }
360
361    /// Returns the semantic JSON parser registry.
362    pub fn diagram_registry(&self) -> &DiagramRegistry {
363        &self.diagram_registry
364    }
365
366    /// Returns a mutable semantic JSON parser registry for custom diagram adapters.
367    pub fn diagram_registry_mut(&mut self) -> &mut DiagramRegistry {
368        &mut self.diagram_registry
369    }
370
371    /// Returns the typed render-model parser registry.
372    pub fn render_diagram_registry(&self) -> &RenderDiagramRegistry {
373        &self.render_diagram_registry
374    }
375
376    /// Returns a mutable typed render-model parser registry.
377    pub fn render_diagram_registry_mut(&mut self) -> &mut RenderDiagramRegistry {
378        &mut self.render_diagram_registry
379    }
380
381    /// Synchronous variant of [`Engine::parse_metadata`].
382    ///
383    /// This is useful for UI render pipelines that are synchronous (e.g. immediate-mode UI),
384    /// where introducing an async executor would be awkward. The parsing work is CPU-bound and
385    /// does not perform I/O.
386    pub fn parse_metadata_sync(&self, text: &str) -> Result<ParseMetadata> {
387        parse_pipeline::ParsePipeline::detect(self, text, ParseOptions::strict()).metadata()
388    }
389
390    /// Parses metadata for an already-known diagram type (skips type detection).
391    ///
392    /// This is intended for integrations that already know the diagram type, e.g. Markdown fences
393    /// like ````mermaid` / ` ```flowchart` / ` ```sequenceDiagram`.
394    ///
395    /// ## Example (Markdown fence)
396    ///
397    /// ```no_run
398    /// use merman_core::Engine;
399    ///
400    /// let engine = Engine::new();
401    ///
402    /// // Your markdown parser provides the fence info string (e.g. "flowchart", "sequenceDiagram").
403    /// let fence = "sequenceDiagram";
404    /// let diagram = r#"sequenceDiagram
405    ///   Alice->>Bob: Hello
406    /// "#;
407    ///
408    /// // Map fence info strings to merman's internal diagram ids.
409    /// let diagram_type = match fence {
410    ///     "sequenceDiagram" => "sequence",
411    ///     "flowchart" | "graph" => "flowchart-v2",
412    ///     "stateDiagram" | "stateDiagram-v2" => "stateDiagram",
413    ///     other => other,
414    /// };
415    ///
416    /// let meta = engine
417    ///     .parse_metadata_with_type_sync(diagram_type, diagram)?;
418    /// # Ok::<(), merman_core::Error>(())
419    /// ```
420    pub fn parse_metadata_with_type_sync(
421        &self,
422        diagram_type: &str,
423        text: &str,
424    ) -> Result<ParseMetadata> {
425        parse_pipeline::ParsePipeline::known_type(self, diagram_type, text, ParseOptions::strict())
426            .metadata()
427    }
428
429    /// Parses editor-facing semantic facts when a family has a parser-backed implementation.
430    ///
431    /// Returned spans are byte offsets in the `text` supplied to this method.
432    pub fn parse_editor_semantic_facts_with_type_sync(
433        &self,
434        diagram_type: &str,
435        text: &str,
436    ) -> Result<Option<EditorSemanticFacts>> {
437        let Some(snapshot) = self.parse_diagram_snapshot_with_type_sync(diagram_type, text)? else {
438            return Ok(None);
439        };
440        let (_, outcome, editor_facts) = snapshot.into_parts();
441        match editor_facts {
442            ParsedEditorFacts::Available(facts) => Ok(Some(facts)),
443            ParsedEditorFacts::Unavailable => match outcome {
444                DiagramParseOutcome::Failed(error @ Error::UnsupportedDiagram { .. })
445                    if family::is_builtin_diagram_type(diagram_type) =>
446                {
447                    Err(error)
448                }
449                _ => Ok(None),
450            },
451        }
452    }
453
454    /// Async facade for [`Engine::parse_metadata_sync`].
455    ///
456    /// The work is CPU-bound and executes synchronously; this method exists for callers that
457    /// prefer an async-shaped API.
458    pub async fn parse_metadata(&self, text: &str) -> Result<ParseMetadata> {
459        self.parse_metadata_sync(text)
460    }
461
462    /// Async facade for [`Engine::parse_metadata_with_type_sync`].
463    ///
464    /// The work is CPU-bound and executes synchronously.
465    pub async fn parse_metadata_with_type(
466        &self,
467        diagram_type: &str,
468        text: &str,
469    ) -> Result<ParseMetadata> {
470        self.parse_metadata_with_type_sync(diagram_type, text)
471    }
472
473    /// Synchronous variant of [`Engine::parse_diagram`].
474    ///
475    /// Note: callers that want “always returns a diagram” behavior can set
476    /// [`ParseOptions::suppress_errors`] to `true` to get an `error` diagram on parse failures.
477    pub fn parse_diagram_sync(
478        &self,
479        text: &str,
480        options: ParseOptions,
481    ) -> Result<Option<ParsedDiagram>> {
482        parse_pipeline::ParsePipeline::detect(self, text, options)
483            .parse_json(parse_pipeline::ParseTiming::Json)
484    }
485
486    /// Captures semantic JSON or its original error, parser-owned typed warning facts, and
487    /// parser-backed editor facts in one operation.
488    ///
489    /// This is intended for editor integrations that need both diagnostics/facts and the
490    /// Mermaid-compatible model. Once preprocessing and detection succeed, family parse errors and
491    /// panics are retained inside the snapshot alongside metadata and recovery facts. Successful
492    /// snapshots expose warnings through the `warning_facts` field of
493    /// [`DiagramParseOutcome::Parsed`]; consumers should not decode the compatibility model's
494    /// `warningFacts` field. Consumers must project a retained failure state directly rather than
495    /// parsing the source again.
496    /// Error suppression is deliberately absent from this API; suppression remains limited to
497    /// model-producing JSON and render facades.
498    pub fn parse_diagram_snapshot_sync(&self, text: &str) -> Result<Option<DiagramParseSnapshot>> {
499        let control = OperationControl::new();
500        self.parse_diagram_snapshot_controlled_sync(text, &control)
501            .map_err(Error::from)?
502    }
503
504    /// Captures an editor-facing parse operation with cooperative cancellation.
505    ///
506    /// Cancellation is returned through the outer [`OperationControlResult`] and is never converted
507    /// into a Mermaid parse error, failed snapshot, or recovery diagnostic.
508    pub fn parse_diagram_snapshot_controlled_sync(
509        &self,
510        text: &str,
511        control: &OperationControl,
512    ) -> OperationControlResult<Result<Option<DiagramParseSnapshot>>> {
513        parse_pipeline::ParsePipeline::detect(self, text, ParseOptions::strict())
514            .parse_editor_snapshot_controlled(parse_pipeline::ParseTiming::Json, control)
515    }
516
517    /// Captures an editor parse operation while retaining preprocessing source-configuration
518    /// evidence on non-cancellation failures and on panics after preprocessing completes.
519    /// Cooperative cancellation remains the outer error channel and never yields a partial
520    /// capture.
521    pub fn capture_diagram_snapshot_controlled_sync(
522        &self,
523        text: &str,
524        control: &OperationControl,
525    ) -> OperationControlResult<DiagramSnapshotCapture> {
526        parse_pipeline::ParsePipeline::detect(self, text, ParseOptions::strict())
527            .capture_editor_snapshot_controlled(parse_pipeline::ParseTiming::Json, control)
528    }
529
530    /// Captures one editor-facing parse operation when the diagram type is already known.
531    ///
532    /// This has the same closed snapshot contract, including parser-owned typed warning facts, as
533    /// [`Engine::parse_diagram_snapshot_sync`], but skips automatic detection. Family parse
534    /// failures and panics remain inside the returned snapshot.
535    pub fn parse_diagram_snapshot_with_type_sync(
536        &self,
537        diagram_type: &str,
538        text: &str,
539    ) -> Result<Option<DiagramParseSnapshot>> {
540        parse_pipeline::ParsePipeline::known_type(self, diagram_type, text, ParseOptions::strict())
541            .parse_editor_snapshot(parse_pipeline::ParseTiming::Json)
542    }
543
544    /// Async facade for [`Engine::parse_diagram_sync`].
545    ///
546    /// The work is CPU-bound and executes synchronously.
547    pub async fn parse_diagram(
548        &self,
549        text: &str,
550        options: ParseOptions,
551    ) -> Result<Option<ParsedDiagram>> {
552        self.parse_diagram_sync(text, options)
553    }
554
555    /// Parses a diagram into a typed semantic model optimized for headless layout + SVG rendering.
556    ///
557    /// Unlike [`Engine::parse_diagram_sync`], this avoids constructing large
558    /// `serde_json::Value` object trees for high-impact typed-first diagrams and instead returns
559    /// typed semantic structs that the renderer can consume directly.
560    ///
561    /// Callers that need the semantic JSON model should continue using
562    /// [`Engine::parse_diagram_sync`].
563    pub fn parse_diagram_for_render_model_sync(
564        &self,
565        text: &str,
566        options: ParseOptions,
567    ) -> Result<Option<ParsedDiagramRender>> {
568        parse_pipeline::ParsePipeline::detect(self, text, options).parse_render_model()
569    }
570
571    /// Controlled variant of [`Self::parse_diagram_for_render_model_sync`].
572    ///
573    /// The caller owns the cloneable operation control and may cancel it from another thread or
574    /// task. Cancellation is returned through the outer result and is never folded into a parse
575    /// error or a partial render model.
576    pub fn parse_diagram_for_render_model_controlled_sync(
577        &self,
578        text: &str,
579        options: ParseOptions,
580        operation: &OperationControl,
581    ) -> OperationControlResult<Result<Option<ParsedDiagramRender>>> {
582        parse_pipeline::ParsePipeline::detect(self, text, options)
583            .parse_render_model_controlled(operation)
584    }
585
586    /// Parses a typed render model inside a caller-owned runtime context and operation.
587    ///
588    /// Higher-level render facades use this composition seam to keep runtime values and
589    /// cancellation state identical across parsing and target-specific layout/output stages.
590    /// Unlike [`Self::parse_diagram_for_render_model_controlled_sync`], this method does not
591    /// begin a replacement runtime operation.
592    #[doc(hidden)]
593    pub fn parse_diagram_for_render_model_controlled_in_context_sync(
594        &self,
595        text: &str,
596        options: ParseOptions,
597        operation: &OperationControl,
598        operation_context: &runtime::OperationContext,
599    ) -> OperationControlResult<Result<Option<ParsedDiagramRender>>> {
600        parse_pipeline::ParsePipeline::detect(self, text, options)
601            .parse_render_model_controlled_in_context(operation, operation_context)
602    }
603
604    /// Async facade for [`Engine::parse_diagram_for_render_model_sync`].
605    ///
606    /// The work is CPU-bound and executes synchronously.
607    pub async fn parse_diagram_for_render_model(
608        &self,
609        text: &str,
610        options: ParseOptions,
611    ) -> Result<Option<ParsedDiagramRender>> {
612        self.parse_diagram_for_render_model_sync(text, options)
613    }
614
615    /// Parses a diagram into a typed semantic render model when the diagram type is already known
616    /// (skips type detection).
617    ///
618    /// This is the preferred entrypoint for Markdown renderers and editors that already know the
619    /// diagram type from the code fence info string. It avoids the detection pass and can reduce a
620    /// small fixed overhead in tight render loops.
621    pub fn parse_diagram_for_render_model_with_type_sync(
622        &self,
623        diagram_type: &str,
624        text: &str,
625        options: ParseOptions,
626    ) -> Result<Option<ParsedDiagramRender>> {
627        parse_pipeline::ParsePipeline::known_type(self, diagram_type, text, options)
628            .parse_render_model()
629    }
630
631    /// Controlled known-type variant of [`Self::parse_diagram_for_render_model_with_type_sync`].
632    pub fn parse_diagram_for_render_model_with_type_controlled_sync(
633        &self,
634        diagram_type: &str,
635        text: &str,
636        options: ParseOptions,
637        operation: &OperationControl,
638    ) -> OperationControlResult<Result<Option<ParsedDiagramRender>>> {
639        parse_pipeline::ParsePipeline::known_type(self, diagram_type, text, options)
640            .parse_render_model_controlled(operation)
641    }
642
643    /// Known-type composition seam for a caller-owned runtime context and operation.
644    #[doc(hidden)]
645    pub fn parse_diagram_for_render_model_with_type_controlled_in_context_sync(
646        &self,
647        diagram_type: &str,
648        text: &str,
649        options: ParseOptions,
650        operation: &OperationControl,
651        operation_context: &runtime::OperationContext,
652    ) -> OperationControlResult<Result<Option<ParsedDiagramRender>>> {
653        parse_pipeline::ParsePipeline::known_type(self, diagram_type, text, options)
654            .parse_render_model_controlled_in_context(operation, operation_context)
655    }
656
657    /// Async facade for [`Engine::parse_diagram_for_render_model_with_type_sync`].
658    ///
659    /// The work is CPU-bound and executes synchronously.
660    pub async fn parse_diagram_for_render_model_with_type(
661        &self,
662        diagram_type: &str,
663        text: &str,
664        options: ParseOptions,
665    ) -> Result<Option<ParsedDiagramRender>> {
666        self.parse_diagram_for_render_model_with_type_sync(diagram_type, text, options)
667    }
668
669    /// Parses a diagram when the diagram type is already known (skips type detection).
670    ///
671    /// This is the preferred entrypoint for Markdown renderers and editors that already know the
672    /// diagram type from the code fence info string. It avoids the detection pass and can reduce a
673    /// small fixed overhead in tight render loops.
674    ///
675    /// ## Example
676    ///
677    /// ```no_run
678    /// use merman_core::{Engine, ParseOptions};
679    ///
680    /// let engine = Engine::new();
681    /// let input = "flowchart TD; A-->B;";
682    ///
683    /// let parsed = engine
684    ///     .parse_diagram_with_type_sync("flowchart-v2", input, ParseOptions::strict())?
685    ///     .expect("diagram detected");
686    ///
687    /// assert_eq!(parsed.meta.diagram_type, "flowchart-v2");
688    /// # Ok::<(), merman_core::Error>(())
689    /// ```
690    pub fn parse_diagram_with_type_sync(
691        &self,
692        diagram_type: &str,
693        text: &str,
694        options: ParseOptions,
695    ) -> Result<Option<ParsedDiagram>> {
696        parse_pipeline::ParsePipeline::known_type(self, diagram_type, text, options)
697            .parse_json(parse_pipeline::ParseTiming::None)
698    }
699
700    /// Async facade for [`Engine::parse_diagram_with_type_sync`].
701    ///
702    /// The work is CPU-bound and executes synchronously.
703    pub async fn parse_diagram_with_type(
704        &self,
705        diagram_type: &str,
706        text: &str,
707        options: ParseOptions,
708    ) -> Result<Option<ParsedDiagram>> {
709        self.parse_diagram_with_type_sync(diagram_type, text, options)
710    }
711}
712
713#[cfg(test)]
714mod tests;