diff --git a/.editorconfig b/.editorconfig
index 5696cb0f..d1d29f14 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -12,6 +12,10 @@ insert_final_newline = true
indent_style = space
indent_size = 4
+[*.py]
+indent_style = space
+indent_size = 4
+
# Tab indentation (no size specified)
[Makefile]
indent_style = tab
diff --git a/packages/mrml-python/Cargo.toml b/packages/mrml-python/Cargo.toml
index 029e19c0..382616f2 100644
--- a/packages/mrml-python/Cargo.toml
+++ b/packages/mrml-python/Cargo.toml
@@ -16,5 +16,8 @@ name = "mrml"
crate-type = ["cdylib"]
[dependencies]
-mrml = { version = "3.0.3", path = "../mrml-core" }
+mrml = { version = "3.0.3", path = "../mrml-core", features = [
+ "http-loader-ureq",
+ "local-loader",
+] }
pyo3 = { version = "0.20.0", features = ["extension-module"] }
diff --git a/packages/mrml-python/resources/partials/hello-world.mjml b/packages/mrml-python/resources/partials/hello-world.mjml
new file mode 100644
index 00000000..ff87f274
--- /dev/null
+++ b/packages/mrml-python/resources/partials/hello-world.mjml
@@ -0,0 +1 @@
+Hello World
diff --git a/packages/mrml-python/resources/with-http-include.mjml b/packages/mrml-python/resources/with-http-include.mjml
new file mode 100644
index 00000000..d5bb8cce
--- /dev/null
+++ b/packages/mrml-python/resources/with-http-include.mjml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/packages/mrml-python/resources/with-local-include.mjml b/packages/mrml-python/resources/with-local-include.mjml
new file mode 100644
index 00000000..1a8d76ee
--- /dev/null
+++ b/packages/mrml-python/resources/with-local-include.mjml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/packages/mrml-python/resources/with-multi-include.mjml b/packages/mrml-python/resources/with-multi-include.mjml
new file mode 100644
index 00000000..f71ac19d
--- /dev/null
+++ b/packages/mrml-python/resources/with-multi-include.mjml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/packages/mrml-python/src/lib.rs b/packages/mrml-python/src/lib.rs
index 71c65bf8..e5051297 100644
--- a/packages/mrml-python/src/lib.rs
+++ b/packages/mrml-python/src/lib.rs
@@ -1,7 +1,10 @@
use std::borrow::Cow;
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
+use std::path::PathBuf;
+use mrml::prelude::parser::http_loader::{HttpIncludeLoader, UreqFetcher};
use mrml::prelude::parser::loader::IncludeLoader;
+use mrml::prelude::parser::local_loader::LocalIncludeLoader;
use mrml::prelude::parser::memory_loader::MemoryIncludeLoader;
use mrml::prelude::parser::noop_loader::NoopIncludeLoader;
use pyo3::exceptions::PyOSError;
@@ -15,11 +18,37 @@ pub struct NoopIncludeLoaderOptions;
#[derive(Clone, Debug, Default)]
pub struct MemoryIncludeLoaderOptions(HashMap);
+#[pyclass]
+#[derive(Clone, Debug, Default)]
+pub struct LocalIncludeLoaderOptions(PathBuf);
+
+#[pyclass]
+#[derive(Clone, Debug)]
+pub enum HttpIncludeLoaderOptionsMode {
+ Allow,
+ Deny,
+}
+
+impl Default for HttpIncludeLoaderOptionsMode {
+ fn default() -> Self {
+ Self::Allow
+ }
+}
+
+#[pyclass]
+#[derive(Clone, Debug, Default)]
+pub struct HttpIncludeLoaderOptions {
+ mode: HttpIncludeLoaderOptionsMode,
+ list: HashSet,
+}
+
// #[pyclass]
#[derive(FromPyObject, Clone, Debug)]
pub enum ParserIncludeLoaderOptions {
Noop(NoopIncludeLoaderOptions),
Memory(MemoryIncludeLoaderOptions),
+ Local(LocalIncludeLoaderOptions),
+ Http(HttpIncludeLoaderOptions),
}
impl Default for ParserIncludeLoaderOptions {
@@ -35,6 +64,17 @@ impl ParserIncludeLoaderOptions {
Self::Memory(MemoryIncludeLoaderOptions(inner)) => {
Box::new(MemoryIncludeLoader::from(inner))
}
+ Self::Local(LocalIncludeLoaderOptions(inner)) => {
+ Box::new(LocalIncludeLoader::new(inner))
+ }
+ Self::Http(HttpIncludeLoaderOptions { mode, list }) => match mode {
+ HttpIncludeLoaderOptionsMode::Allow => {
+ Box::new(HttpIncludeLoader::::new_allow(list))
+ }
+ HttpIncludeLoaderOptionsMode::Deny => {
+ Box::new(HttpIncludeLoader::::new_deny(list))
+ }
+ },
}
}
}
@@ -44,6 +84,8 @@ impl IntoPy for ParserIncludeLoaderOptions {
match self {
Self::Noop(inner) => inner.into_py(py),
Self::Memory(inner) => inner.into_py(py),
+ Self::Local(inner) => inner.into_py(py),
+ Self::Http(inner) => inner.into_py(py),
}
}
}
@@ -60,6 +102,34 @@ pub fn memory_loader(data: Option>) -> ParserIncludeLoad
ParserIncludeLoaderOptions::Memory(MemoryIncludeLoaderOptions(data.unwrap_or_default()))
}
+#[pyfunction]
+#[pyo3(name = "local_loader", signature = (data = None))]
+pub fn local_loader(data: Option) -> PyResult {
+ let path = match data.map(PathBuf::from) {
+ Some(path) if path.is_absolute() => path,
+ Some(path) => std::env::current_dir()
+ .map_err(|err| PyErr::new::(err.to_string()))?
+ .join(path),
+ None => std::env::current_dir()
+ .map_err(|err| PyErr::new::(err.to_string()))?,
+ };
+ Ok(ParserIncludeLoaderOptions::Local(
+ LocalIncludeLoaderOptions(path),
+ ))
+}
+
+#[pyfunction]
+#[pyo3(name = "http_loader", signature = (mode = None, list = None))]
+pub fn http_loader(
+ mode: Option,
+ list: Option>,
+) -> ParserIncludeLoaderOptions {
+ ParserIncludeLoaderOptions::Http(HttpIncludeLoaderOptions {
+ mode: mode.unwrap_or(HttpIncludeLoaderOptionsMode::Allow),
+ list: list.unwrap_or_default(),
+ })
+}
+
#[pyclass]
#[derive(Clone, Debug, Default)]
pub struct ParserOptions {
@@ -147,10 +217,15 @@ fn to_html(
fn register(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::()?;
m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
+ m.add_class::()?;
m.add_class::()?;
m.add_class::()?;
m.add_function(wrap_pyfunction!(to_html, m)?)?;
m.add_function(wrap_pyfunction!(noop_loader, m)?)?;
+ m.add_function(wrap_pyfunction!(local_loader, m)?)?;
+ m.add_function(wrap_pyfunction!(http_loader, m)?)?;
m.add_function(wrap_pyfunction!(memory_loader, m)?)?;
Ok(())
}
diff --git a/packages/mrml-python/tests/test_loader.py b/packages/mrml-python/tests/test_loader.py
new file mode 100644
index 00000000..82386c2e
--- /dev/null
+++ b/packages/mrml-python/tests/test_loader.py
@@ -0,0 +1,123 @@
+import mrml
+
+
+def test_memory_loader():
+ parser_options = mrml.ParserOptions(
+ include_loader=mrml.memory_loader(
+ {
+ "hello-world.mjml": "Hello World!",
+ }
+ )
+ )
+ result = mrml.to_html(
+ '',
+ parser_options=parser_options,
+ )
+ assert result.startswith("")
+
+
+def test_local_loader_success():
+ parser_options = mrml.ParserOptions(
+ include_loader=mrml.local_loader("./resources/partials")
+ )
+ result = mrml.to_html(
+ '',
+ parser_options=parser_options,
+ )
+ assert result.startswith("")
+
+
+def test_local_loader_missing():
+ parser_options = mrml.ParserOptions(
+ include_loader=mrml.local_loader("./resources/partials")
+ )
+ try:
+ mrml.to_html(
+ '',
+ parser_options=parser_options,
+ )
+ assert False
+ except Exception as err:
+ assert err
+
+
+def test_http_loader_success():
+ parser_options = mrml.ParserOptions(
+ include_loader=mrml.http_loader(
+ mode=mrml.HttpIncludeLoaderOptionsMode.Allow,
+ list=set(["https://gist.githubusercontent.com"]),
+ )
+ )
+ result = mrml.to_html(
+ """
+
+
+
+""",
+ parser_options=parser_options,
+ )
+ assert result.startswith("")
+
+
+def test_http_loader_failed_not_in_allow_list():
+ parser_options = mrml.ParserOptions(
+ include_loader=mrml.http_loader(
+ mode=mrml.HttpIncludeLoaderOptionsMode.Allow,
+ list=set([]),
+ )
+ )
+ try:
+ mrml.to_html(
+ """
+
+
+
+""",
+ parser_options=parser_options,
+ )
+ assert False
+ except Exception as err:
+ assert err
+
+
+def test_http_loader_success_allow_everything():
+ parser_options = mrml.ParserOptions(
+ include_loader=mrml.http_loader(mode=mrml.HttpIncludeLoaderOptionsMode.Deny)
+ )
+ mrml.to_html(
+ """
+
+
+
+""",
+ parser_options=parser_options,
+ )
+
+
+def test_http_loader_failed_deny_github():
+ parser_options = mrml.ParserOptions(
+ include_loader=mrml.http_loader(
+ mode=mrml.HttpIncludeLoaderOptionsMode.Deny,
+ list=set(["https://gist.githubusercontent.com"]),
+ )
+ )
+ try:
+ mrml.to_html(
+ """
+
+
+
+""",
+ parser_options=parser_options,
+ )
+ assert False
+ except Exception as err:
+ assert err