Skip to main content

sim_codec_doc/
codec.rs

1//! The markup and `codec:doc` decoder/encoder host libs.
2//!
3//! Markup backends install as strict `codec:markup/<id>` runtime codecs.
4//! `codec:doc` remains as the compatibility document codec and chunk-function
5//! host lib.
6
7use std::sync::Arc;
8
9use sim_codec::{
10    CodecDefaultDecode, CodecRuntime, DecodeBudget, Decoder, Encoder, Input, Output, ReadCx,
11    codec_value,
12};
13use sim_kernel::{
14    AbiVersion, Cx, DefaultFactory, Dependency, Error, Export, Lib, LibManifest, LibTarget, Linker,
15    Result, Symbol, Version, WriteCx,
16};
17
18use crate::backend::{
19    BackendRegistry, MarkupBackend, MarkupDecodeOptions, MarkupEncodeOptions, MarkupError,
20    default_backend_registry,
21};
22use crate::document::DocValue;
23use crate::functions::{CHUNK_FUNCTIONS, DocChunkFunction};
24use crate::markdown::MarkdownBackend;
25use crate::markup::MarkupDoc;
26
27/// Textual prefix for rendered markup codec symbols.
28pub const MARKUP_CODEC_PREFIX: &str = "codec/markup/";
29
30/// Return the runtime codec symbol for a markup backend id.
31pub fn markup_codec_symbol(id: &crate::BackendId) -> Symbol {
32    Symbol::qualified("codec", format!("markup/{}", id.as_str()))
33}
34
35/// Strict runtime codec for one markup backend.
36pub struct MarkupCodec {
37    backend: Arc<dyn MarkupBackend>,
38}
39
40impl MarkupCodec {
41    /// Create a codec backed by `backend`.
42    pub fn new(backend: Arc<dyn MarkupBackend>) -> Self {
43        Self { backend }
44    }
45
46    /// Return this codec's backend id.
47    pub fn backend_id(&self) -> crate::BackendId {
48        self.backend.id()
49    }
50}
51
52impl Decoder for MarkupCodec {
53    fn decode(&self, cx: &mut ReadCx<'_>, input: Input) -> Result<sim_kernel::Expr> {
54        let source = input.into_string()?;
55        let budget = DecodeBudget::new(cx.limits);
56        budget.check_input_bytes(cx.codec, source.len())?;
57        let (doc, _fidelity) = self
58            .backend
59            .decode(&source, &MarkupDecodeOptions::default())
60            .map_err(|err| err.into_kernel_error(cx.codec))?;
61        Ok(doc.as_expr())
62    }
63}
64
65impl Encoder for MarkupCodec {
66    fn encode(&self, cx: &mut WriteCx<'_>, expr: &sim_kernel::Expr) -> Result<Output> {
67        let doc = MarkupDoc::from_expr(expr).map_err(|err| {
68            MarkupError::InvalidDocument(err.to_string()).into_kernel_error(cx.codec)
69        })?;
70        let (source, fidelity) = self
71            .backend
72            .encode(&doc, &MarkupEncodeOptions::default())
73            .map_err(|err| err.into_kernel_error(cx.codec))?;
74        if !fidelity.dropped.is_empty() {
75            return Err(MarkupError::Encode(format!(
76                "backend {} reported {} dropped part(s)",
77                fidelity.backend,
78                fidelity.dropped.len()
79            ))
80            .into_kernel_error(cx.codec));
81        }
82        Ok(Output::Text(source))
83    }
84}
85
86/// The `codec:doc` decoder/encoder.
87///
88/// As a [`Decoder`] it turns document text into a markup document `Expr`; as an
89/// [`Encoder`] it writes a markup document, compatibility document value, or raw
90/// string back to text and fails closed on any other expression.
91pub struct DocCodec;
92
93impl Decoder for DocCodec {
94    fn decode(&self, cx: &mut ReadCx<'_>, input: Input) -> Result<sim_kernel::Expr> {
95        let source = input.into_string()?;
96        let budget = DecodeBudget::new(cx.limits);
97        budget.check_input_bytes(cx.codec, source.len())?;
98        let (doc, _fidelity) = MarkdownBackend
99            .decode(&source, &MarkupDecodeOptions::default())
100            .map_err(|err| err.into_kernel_error(cx.codec))?;
101        Ok(doc.as_expr())
102    }
103}
104
105impl Encoder for DocCodec {
106    fn encode(&self, cx: &mut WriteCx<'_>, expr: &sim_kernel::Expr) -> Result<Output> {
107        match expr {
108            sim_kernel::Expr::String(text) => Ok(Output::Text(text.clone())),
109            sim_kernel::Expr::Map(_) => Ok(Output::Text(encode_doc_expr(expr).map_err(|err| {
110                Error::CodecError {
111                    codec: cx.codec,
112                    message: err.to_string(),
113                }
114            })?)),
115            _ => Err(Error::CodecError {
116                codec: cx.codec,
117                message: "codec:doc encodes document values or strings".to_owned(),
118            }),
119        }
120    }
121}
122
123fn encode_doc_expr(expr: &sim_kernel::Expr) -> Result<String> {
124    match MarkupDoc::from_expr(expr) {
125        Ok(doc) => Ok(doc.to_source_text()),
126        Err(markup_error) => match DocValue::from_expr(expr) {
127            Ok(doc) => Ok(doc.text),
128            Err(_) => Err(markup_error),
129        },
130    }
131}
132
133struct RegisteredBackend {
134    backend: Arc<dyn MarkupBackend>,
135    codec_id: sim_kernel::CodecId,
136}
137
138struct MarkupCodecLib {
139    backends: Vec<RegisteredBackend>,
140}
141
142impl MarkupCodecLib {
143    fn new(cx: &mut Cx, registry: BackendRegistry) -> Self {
144        let backends = registry
145            .iter()
146            .map(|(_, backend)| RegisteredBackend {
147                backend: backend.clone(),
148                codec_id: cx.registry_mut().fresh_codec_id(),
149            })
150            .collect();
151        Self { backends }
152    }
153}
154
155impl Lib for MarkupCodecLib {
156    fn manifest(&self) -> LibManifest {
157        LibManifest {
158            id: Symbol::qualified("codec", "markup"),
159            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
160            abi: AbiVersion { major: 0, minor: 1 },
161            target: LibTarget::HostRegistered,
162            requires: Vec::<Dependency>::new(),
163            capabilities: Vec::new(),
164            exports: self
165                .backends
166                .iter()
167                .map(|registered| Export::Codec {
168                    symbol: markup_codec_symbol(&registered.backend.id()),
169                    codec_id: Some(registered.codec_id),
170                })
171                .collect(),
172        }
173    }
174
175    fn load(&self, _cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
176        let expr_shape =
177            sim_codec::resolve_expr_shape(linker, &Symbol::qualified("codec", "Document"))?;
178        let options_shape = sim_codec::resolve_options_shape(linker)?;
179
180        for registered in &self.backends {
181            let symbol = markup_codec_symbol(&registered.backend.id());
182            linker.codec_value(
183                symbol.clone(),
184                codec_value(CodecRuntime {
185                    id: registered.codec_id,
186                    symbol,
187                    decoder: Some(Arc::new(MarkupCodec::new(registered.backend.clone()))),
188                    located_decoder: None,
189                    tree_decoder: None,
190                    encoder: Some(Arc::new(MarkupCodec::new(registered.backend.clone()))),
191                    located_encoder: None,
192                    tree_encoder: None,
193                    expr_shape: expr_shape.clone(),
194                    options_shape: options_shape.clone(),
195                    default_decode: CodecDefaultDecode::Datum,
196                }),
197            )?;
198        }
199        Ok(())
200    }
201}
202
203/// The host-registered [`Lib`] that installs [`DocCodec`] as `codec:doc` and
204/// the `doc/chunk-*` chunking functions.
205pub struct DocCodecLib {
206    symbol: Symbol,
207    codec_id: sim_kernel::CodecId,
208}
209
210impl DocCodecLib {
211    /// Create the lib bound to the given codec id (obtained from
212    /// [`Registry::fresh_codec_id`](sim_kernel::Registry::fresh_codec_id)).
213    pub fn new(id: sim_kernel::CodecId) -> Self {
214        Self {
215            symbol: Symbol::qualified("codec", "doc"),
216            codec_id: id,
217        }
218    }
219}
220
221impl Lib for DocCodecLib {
222    fn manifest(&self) -> LibManifest {
223        let mut exports = vec![Export::Codec {
224            symbol: self.symbol.clone(),
225            codec_id: Some(self.codec_id),
226        }];
227        exports.extend(CHUNK_FUNCTIONS.iter().map(|kind| Export::Function {
228            symbol: kind.symbol(),
229            function_id: None,
230        }));
231        LibManifest {
232            id: self.symbol.clone(),
233            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
234            abi: AbiVersion { major: 0, minor: 1 },
235            target: LibTarget::HostRegistered,
236            requires: Vec::<Dependency>::new(),
237            capabilities: Vec::new(),
238            exports,
239        }
240    }
241
242    fn load(&self, cx: &mut sim_kernel::LoadCx, linker: &mut Linker<'_>) -> Result<()> {
243        let _factory = DefaultFactory;
244        let expr_shape =
245            sim_codec::resolve_expr_shape(linker, &Symbol::qualified("codec", "Document"))?;
246        let options_shape = sim_codec::resolve_options_shape(linker)?;
247
248        linker.codec_value(
249            self.symbol.clone(),
250            codec_value(CodecRuntime {
251                id: self.codec_id,
252                symbol: self.symbol.clone(),
253                decoder: Some(Arc::new(DocCodec)),
254                located_decoder: None,
255                tree_decoder: None,
256                encoder: Some(Arc::new(DocCodec)),
257                located_encoder: None,
258                tree_encoder: None,
259                expr_shape,
260                options_shape,
261                default_decode: CodecDefaultDecode::Datum,
262            }),
263        )?;
264
265        for kind in CHUNK_FUNCTIONS {
266            linker.function_value(
267                kind.symbol(),
268                cx.factory()
269                    .opaque(Arc::new(DocChunkFunction::new(*kind)))?,
270            )?;
271        }
272        Ok(())
273    }
274}
275
276/// Install [`DocCodecLib`] into `cx`, registering `codec:doc` and the
277/// `doc/chunk-*` functions with a freshly allocated codec id.
278pub fn install_doc_codec(cx: &mut Cx) -> Result<()> {
279    install_markup_codecs(cx, default_backend_registry())?;
280    let lib = DocCodecLib::new(cx.registry_mut().fresh_codec_id());
281    cx.load_lib(&lib)?;
282    Ok(())
283}
284
285/// Install each registered markup backend as `codec:markup/<id>`.
286pub fn install_markup_codecs(cx: &mut Cx, registry: BackendRegistry) -> Result<()> {
287    if registry.is_empty() {
288        return Err(Error::Lib("markup backend registry is empty".to_owned()));
289    }
290    let lib = MarkupCodecLib::new(cx, registry);
291    cx.load_lib(&lib)?;
292    Ok(())
293}