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