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