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