Skip to main content

sim_codec_doc/
backend.rs

1//! Backend registry and fidelity contracts for markup codecs.
2
3use std::collections::BTreeMap;
4use std::error::Error as StdError;
5use std::fmt;
6use std::sync::Arc;
7
8use sim_kernel::CodecId;
9
10use crate::asciidoc::AsciiDocBackend;
11use crate::latex::LatexBackend;
12use crate::markdown::MarkdownBackend;
13use crate::markup::{BackendId, MarkupDoc};
14use crate::typst_backend::TypstBackend;
15
16/// Decode options shared by markup backends.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct MarkupDecodeOptions {
19    /// Preserve backend source text when the backend can do so.
20    pub preserve_source: bool,
21    /// Preserve backend-specific raw fragments when possible.
22    pub preserve_raw: bool,
23}
24
25impl Default for MarkupDecodeOptions {
26    fn default() -> Self {
27        Self {
28            preserve_source: true,
29            preserve_raw: true,
30        }
31    }
32}
33
34/// Encode options shared by markup backends.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct MarkupEncodeOptions {
37    /// Treat any reported loss as an encode error.
38    pub fail_on_loss: bool,
39    /// Preserve backend-specific raw nodes when possible.
40    pub preserve_raw: bool,
41}
42
43impl Default for MarkupEncodeOptions {
44    fn default() -> Self {
45        Self {
46            fail_on_loss: true,
47            preserve_raw: true,
48        }
49    }
50}
51
52/// A single lossy conversion note.
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct MarkupLoss {
55    /// Stable path to the affected document part.
56    pub path: String,
57    /// Human-readable loss reason.
58    pub reason: String,
59}
60
61/// Fidelity report returned by markup backends.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct MarkupFidelity {
64    /// Backend that produced the report.
65    pub backend: BackendId,
66    /// Raw backend fragments preserved in the semantic document.
67    pub preserved_raw: Vec<String>,
68    /// Semantic parts dropped during conversion.
69    pub dropped: Vec<MarkupLoss>,
70    /// Non-fatal warnings, such as ambiguous source constructs.
71    pub warnings: Vec<String>,
72}
73
74impl MarkupFidelity {
75    /// Create an exact, warning-free report for `backend`.
76    pub fn exact(backend: BackendId) -> Self {
77        Self {
78            backend,
79            preserved_raw: Vec::new(),
80            dropped: Vec::new(),
81            warnings: Vec::new(),
82        }
83    }
84}
85
86/// Error returned by markup backend and registry operations.
87#[derive(Clone, Debug, PartialEq, Eq)]
88pub enum MarkupError {
89    /// No backend is registered for the requested id.
90    UnknownBackend(BackendId),
91    /// Backend decoding failed.
92    Decode(String),
93    /// Backend encoding failed.
94    Encode(String),
95    /// The input expression is not a markup document value.
96    InvalidDocument(String),
97}
98
99impl MarkupError {
100    pub(crate) fn into_kernel_error(self, codec: CodecId) -> sim_kernel::Error {
101        sim_kernel::Error::CodecError {
102            codec,
103            message: self.to_string(),
104        }
105    }
106}
107
108impl fmt::Display for MarkupError {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        match self {
111            Self::UnknownBackend(id) => write!(f, "unknown markup backend {id}"),
112            Self::Decode(message) => write!(f, "markup decode failed: {message}"),
113            Self::Encode(message) => write!(f, "markup encode failed: {message}"),
114            Self::InvalidDocument(message) => write!(f, "invalid markup document: {message}"),
115        }
116    }
117}
118
119impl StdError for MarkupError {}
120
121/// A concrete markup reader/writer behind a runtime codec id.
122pub trait MarkupBackend: Send + Sync {
123    /// Stable backend id, such as `markdown`, `typst`, `asciidoc`, or `latex`.
124    fn id(&self) -> BackendId;
125
126    /// Decode backend source text into the shared markup document IR.
127    fn decode(
128        &self,
129        input: &str,
130        opts: &MarkupDecodeOptions,
131    ) -> Result<(MarkupDoc, MarkupFidelity), MarkupError>;
132
133    /// Encode the shared markup document IR into backend source text.
134    fn encode(
135        &self,
136        doc: &MarkupDoc,
137        opts: &MarkupEncodeOptions,
138    ) -> Result<(String, MarkupFidelity), MarkupError>;
139}
140
141/// Deterministic registry of markup backends.
142#[derive(Clone, Default)]
143pub struct BackendRegistry {
144    backends: BTreeMap<BackendId, Arc<dyn MarkupBackend>>,
145}
146
147impl BackendRegistry {
148    /// Create an empty backend registry.
149    pub fn new() -> Self {
150        Self {
151            backends: BTreeMap::new(),
152        }
153    }
154
155    /// Register `backend`, returning any backend previously registered with
156    /// the same id.
157    pub fn register<B: MarkupBackend + 'static>(
158        &mut self,
159        backend: B,
160    ) -> Option<Arc<dyn MarkupBackend>> {
161        self.register_arc(Arc::new(backend))
162    }
163
164    /// Register an already shared backend handle.
165    pub fn register_arc(
166        &mut self,
167        backend: Arc<dyn MarkupBackend>,
168    ) -> Option<Arc<dyn MarkupBackend>> {
169        self.backends.insert(backend.id(), backend)
170    }
171
172    /// Return a backend handle by id.
173    pub fn backend(&self, id: &BackendId) -> Result<Arc<dyn MarkupBackend>, MarkupError> {
174        self.backends
175            .get(id)
176            .cloned()
177            .ok_or_else(|| MarkupError::UnknownBackend(id.clone()))
178    }
179
180    /// Return backend ids in deterministic registry order.
181    pub fn ids(&self) -> Vec<BackendId> {
182        self.backends.keys().cloned().collect()
183    }
184
185    /// Iterate over backends in deterministic registry order.
186    pub fn iter(&self) -> impl Iterator<Item = (&BackendId, &Arc<dyn MarkupBackend>)> {
187        self.backends.iter()
188    }
189
190    /// Whether this registry contains no backends.
191    pub fn is_empty(&self) -> bool {
192        self.backends.is_empty()
193    }
194}
195
196/// Compatibility name for the default Markdown backend.
197#[derive(Clone, Debug, Default)]
198pub struct BasicMarkdownBackend;
199
200impl MarkupBackend for BasicMarkdownBackend {
201    fn id(&self) -> BackendId {
202        BackendId::new("markdown")
203    }
204
205    fn decode(
206        &self,
207        input: &str,
208        opts: &MarkupDecodeOptions,
209    ) -> Result<(MarkupDoc, MarkupFidelity), MarkupError> {
210        MarkdownBackend.decode(input, opts)
211    }
212
213    fn encode(
214        &self,
215        doc: &MarkupDoc,
216        opts: &MarkupEncodeOptions,
217    ) -> Result<(String, MarkupFidelity), MarkupError> {
218        MarkdownBackend.encode(doc, opts)
219    }
220}
221
222/// Build the default registry installed by [`install_doc_codec`](crate::install_doc_codec).
223///
224/// Only implemented backends are installed here. Tracked catalog entries remain
225/// absent from this registry so runtime decode fails closed for those formats.
226pub fn default_backend_registry() -> BackendRegistry {
227    let mut registry = BackendRegistry::new();
228    registry.register(AsciiDocBackend);
229    registry.register(LatexBackend);
230    registry.register(MarkdownBackend);
231    registry.register(TypstBackend);
232    registry
233}