mrml/
lib.rs

1#![allow(clippy::useless_conversion)]
2
3use std::borrow::Cow;
4use std::collections::{HashMap, HashSet};
5use std::path::PathBuf;
6
7use mrml::prelude::parser::http_loader::{HttpIncludeLoader, UreqFetcher};
8use mrml::prelude::parser::loader::IncludeLoader;
9use mrml::prelude::parser::local_loader::LocalIncludeLoader;
10use mrml::prelude::parser::memory_loader::MemoryIncludeLoader;
11use mrml::prelude::parser::noop_loader::NoopIncludeLoader;
12use pyo3::exceptions::PyIOError;
13use pyo3::prelude::*;
14
15#[pyclass(frozen)]
16#[derive(Clone, Debug, Default)]
17pub struct NoopIncludeLoaderOptions;
18
19#[pyclass(frozen)]
20#[derive(Clone, Debug, Default)]
21pub struct MemoryIncludeLoaderOptions(HashMap<String, String>);
22
23#[pyclass(frozen)]
24#[derive(Clone, Debug, Default)]
25pub struct LocalIncludeLoaderOptions(PathBuf);
26
27#[pyclass(frozen, eq, eq_int)]
28#[derive(Clone, Debug, PartialEq, Eq, Default)]
29pub enum HttpIncludeLoaderOptionsMode {
30    #[default]
31    Allow,
32    Deny,
33}
34
35#[pyclass(frozen)]
36#[derive(Clone, Debug, Default)]
37pub struct HttpIncludeLoaderOptions {
38    mode: HttpIncludeLoaderOptionsMode,
39    list: HashSet<String>,
40}
41
42#[pyclass(frozen)]
43#[derive(Clone, Debug)]
44pub enum ParserIncludeLoaderOptions {
45    Noop(NoopIncludeLoaderOptions),
46    Memory(MemoryIncludeLoaderOptions),
47    Local(LocalIncludeLoaderOptions),
48    Http(HttpIncludeLoaderOptions),
49}
50
51impl Default for ParserIncludeLoaderOptions {
52    fn default() -> Self {
53        Self::Noop(NoopIncludeLoaderOptions)
54    }
55}
56
57impl ParserIncludeLoaderOptions {
58    fn build(self) -> Box<dyn IncludeLoader + Send + Sync> {
59        match self {
60            Self::Noop(_) => Box::<NoopIncludeLoader>::default(),
61            Self::Memory(MemoryIncludeLoaderOptions(inner)) => {
62                Box::new(MemoryIncludeLoader::from(inner))
63            }
64            Self::Local(LocalIncludeLoaderOptions(inner)) => {
65                Box::new(LocalIncludeLoader::new(inner))
66            }
67            Self::Http(HttpIncludeLoaderOptions { mode, list }) => match mode {
68                HttpIncludeLoaderOptionsMode::Allow => {
69                    Box::new(HttpIncludeLoader::<UreqFetcher>::new_allow(list))
70                }
71                HttpIncludeLoaderOptionsMode::Deny => {
72                    Box::new(HttpIncludeLoader::<UreqFetcher>::new_deny(list))
73                }
74            },
75        }
76    }
77}
78
79#[pyfunction]
80#[pyo3(name = "noop_loader")]
81pub fn noop_loader() -> ParserIncludeLoaderOptions {
82    ParserIncludeLoaderOptions::Noop(NoopIncludeLoaderOptions)
83}
84
85#[pyfunction]
86#[pyo3(name = "memory_loader", signature = (data = None))]
87pub fn memory_loader(data: Option<HashMap<String, String>>) -> ParserIncludeLoaderOptions {
88    ParserIncludeLoaderOptions::Memory(MemoryIncludeLoaderOptions(data.unwrap_or_default()))
89}
90
91#[pyfunction]
92#[pyo3(name = "local_loader", signature = (data = None))]
93pub fn local_loader(data: Option<String>) -> PyResult<ParserIncludeLoaderOptions> {
94    let path = match data.map(PathBuf::from) {
95        Some(path) if path.is_absolute() => path,
96        Some(path) => std::env::current_dir()
97            .map_err(|err| PyErr::new::<pyo3::exceptions::PyException, String>(err.to_string()))?
98            .join(path),
99        None => std::env::current_dir()
100            .map_err(|err| PyErr::new::<pyo3::exceptions::PyException, String>(err.to_string()))?,
101    };
102    Ok(ParserIncludeLoaderOptions::Local(
103        LocalIncludeLoaderOptions(path),
104    ))
105}
106
107#[pyfunction]
108#[pyo3(name = "http_loader", signature = (mode = None, list = None))]
109pub fn http_loader(
110    mode: Option<HttpIncludeLoaderOptionsMode>,
111    list: Option<HashSet<String>>,
112) -> ParserIncludeLoaderOptions {
113    ParserIncludeLoaderOptions::Http(HttpIncludeLoaderOptions {
114        mode: mode.unwrap_or(HttpIncludeLoaderOptionsMode::Allow),
115        list: list.unwrap_or_default(),
116    })
117}
118
119#[pyclass(frozen)]
120#[derive(Clone, Debug, Default)]
121pub struct ParserOptions {
122    #[pyo3(get)]
123    pub include_loader: ParserIncludeLoaderOptions,
124}
125
126#[pymethods]
127impl ParserOptions {
128    #[new]
129    #[pyo3(signature = (include_loader=None))]
130    pub fn new(include_loader: Option<ParserIncludeLoaderOptions>) -> Self {
131        Self {
132            include_loader: include_loader.unwrap_or_default(),
133        }
134    }
135}
136
137impl From<ParserOptions> for mrml::prelude::parser::ParserOptions {
138    fn from(value: ParserOptions) -> Self {
139        let include_loader = value.include_loader.build();
140        mrml::prelude::parser::ParserOptions { include_loader }
141    }
142}
143
144#[pyclass(frozen)]
145#[derive(Clone, Debug, Default)]
146pub struct RenderOptions {
147    #[pyo3(get)]
148    pub disable_comments: bool,
149    #[pyo3(get)]
150    pub social_icon_origin: Option<String>,
151    #[pyo3(get)]
152    pub fonts: Option<HashMap<String, String>>,
153}
154
155#[pymethods]
156impl RenderOptions {
157    #[new]
158    #[pyo3(signature = (disable_comments=false, social_icon_origin=None, fonts=None))]
159    pub fn new(
160        disable_comments: bool,
161        social_icon_origin: Option<String>,
162        fonts: Option<HashMap<String, String>>,
163    ) -> Self {
164        Self {
165            disable_comments,
166            social_icon_origin,
167            fonts,
168        }
169    }
170}
171
172impl From<RenderOptions> for mrml::prelude::render::RenderOptions {
173    fn from(value: RenderOptions) -> Self {
174        let mut opts = mrml::prelude::render::RenderOptions {
175            disable_comments: value.disable_comments,
176            ..Default::default()
177        };
178        if let Some(social) = value.social_icon_origin {
179            opts.social_icon_origin = Some(Cow::Owned(social));
180        }
181        if let Some(fonts) = value.fonts {
182            opts.fonts = fonts
183                .into_iter()
184                .map(|(name, value)| (name, Cow::Owned(value)))
185                .collect();
186        }
187        opts
188    }
189}
190
191#[pyclass(frozen)]
192#[derive(Clone, Debug, Default)]
193pub struct Warning {
194    #[pyo3(get)]
195    pub origin: Option<String>,
196    #[pyo3(get)]
197    pub kind: &'static str,
198    #[pyo3(get)]
199    pub start: usize,
200    #[pyo3(get)]
201    pub end: usize,
202}
203
204impl Warning {
205    fn from_vec(input: Vec<mrml::prelude::parser::Warning>) -> Vec<Self> {
206        input.into_iter().map(Self::from).collect()
207    }
208}
209
210impl From<mrml::prelude::parser::Warning> for Warning {
211    fn from(value: mrml::prelude::parser::Warning) -> Self {
212        Self {
213            origin: match value.origin {
214                mrml::prelude::parser::Origin::Root => None,
215                mrml::prelude::parser::Origin::Include { path } => Some(path),
216            },
217            kind: value.kind.as_str(),
218            start: value.span.start,
219            end: value.span.end,
220        }
221    }
222}
223
224#[pyclass(frozen)]
225#[derive(Clone, Debug, Default)]
226pub struct Output {
227    #[pyo3(get)]
228    pub content: String,
229    #[pyo3(get)]
230    pub title: Option<String>,
231    #[pyo3(get)]
232    pub preview: Option<String>,
233    #[pyo3(get)]
234    pub warnings: Vec<Warning>,
235}
236
237#[pyfunction]
238#[pyo3(name = "to_html", signature = (input, parser_options=None, render_options=None))]
239fn to_html(
240    input: String,
241    parser_options: Option<ParserOptions>,
242    render_options: Option<RenderOptions>,
243) -> PyResult<Output> {
244    let parser_options = parser_options.unwrap_or_default().into();
245    let parsed = mrml::parse_with_options(input, &parser_options)
246        .map_err(|err| PyIOError::new_err(err.to_string()))?;
247
248    let render_options = render_options.unwrap_or_default().into();
249    let content = parsed
250        .element
251        .render(&render_options)
252        .map_err(|err| PyIOError::new_err(err.to_string()))?;
253    let title = parsed.element.get_title();
254    let preview = parsed.element.get_preview();
255    Ok(Output {
256        content,
257        title,
258        preview,
259        warnings: Warning::from_vec(parsed.warnings),
260    })
261}
262
263#[pymodule]
264#[pyo3(name = "mrml")]
265fn register(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
266    m.add_class::<NoopIncludeLoaderOptions>()?;
267    m.add_class::<MemoryIncludeLoaderOptions>()?;
268    m.add_class::<LocalIncludeLoaderOptions>()?;
269    m.add_class::<HttpIncludeLoaderOptions>()?;
270    m.add_class::<HttpIncludeLoaderOptionsMode>()?;
271    m.add_class::<ParserOptions>()?;
272    m.add_class::<RenderOptions>()?;
273    m.add_class::<Output>()?;
274    m.add_class::<Warning>()?;
275    m.add_function(wrap_pyfunction!(to_html, m)?)?;
276    m.add_function(wrap_pyfunction!(noop_loader, m)?)?;
277    m.add_function(wrap_pyfunction!(local_loader, m)?)?;
278    m.add_function(wrap_pyfunction!(http_loader, m)?)?;
279    m.add_function(wrap_pyfunction!(memory_loader, m)?)?;
280    m.gil_used(false)?;
281    Ok(())
282}