use crate::{config, View, ViewResult};
use soph_config::support::config;
use std::{collections::HashMap, ops::Deref};
use tera::Tera;
impl View {
pub fn new() -> ViewResult<Self> {
let config = config().parse::<config::View>()?;
let mut tera = Tera::new(&config.path)?;
tera.register_function("asset", asset(config.asset.url.to_owned()));
tera.register_function("vite", vite(config.asset.url.to_owned()));
Ok(Self { engine: tera })
}
}
fn asset(url: String) -> impl tera::Function {
Box::new(
move |args: &HashMap<String, serde_json::Value>| -> tera::Result<serde_json::Value> {
let resource = match args.get("resource") {
Some(val) => match serde_json::from_value::<String>(val.clone()) {
Ok(v) => v,
Err(_) => {
let msg =
format!("Function `asset` received resource={val} but `resource` can only be a string",);
tracing::error!(err.msg = msg, "error");
return Err(tera::Error::msg(msg));
}
},
None => {
let msg = "Function `asset` didn't receive a `resource` argument";
tracing::error!(err.msg = msg, "error");
return Err(tera::Error::msg(msg));
}
};
Ok(serde_json::Value::String(format!("{url}/{resource}")))
},
)
}
fn vite(url: String) -> impl tera::Function {
Box::new(
move |args: &HashMap<String, serde_json::Value>| -> tera::Result<serde_json::Value> {
let entry = match args.get("entry") {
Some(val) => match serde_json::from_value::<String>(val.clone()) {
Ok(v) => v,
Err(_) => {
let msg = format!("Function `vite` received entry={val} but `entry` can only be a string",);
tracing::error!(err.msg = msg, "error");
return Err(tera::Error::msg(msg));
}
},
None => {
let msg = "Function `vite` didn't receive a `entry` argument";
tracing::error!(err.msg = msg, "error");
return Err(tera::Error::msg(msg));
}
};
if std::path::Path::new("public/hot").exists() {
let dev = format!(
r#"<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/{}"></script>"#,
&entry
);
return Ok(serde_json::Value::String(dev));
}
let manifest = match std::fs::read_to_string("public/build/manifest.json").ok() {
None => {
let msg = format!("Vite manifest not found at `{}`", "public/build/manifest.json");
tracing::error!(err.msg = msg, "error");
return Err(tera::Error::msg(msg));
}
Some(content) => match serde_json::from_str::<serde_json::Value>(&content)?.get(&entry) {
None => {
let msg = format!("Vite manifest entry not found at `{}`", &entry);
tracing::error!(err.msg = msg, "error");
return Err(tera::Error::msg(msg));
}
Some(val) => {
if let Some(is_entry) = val.get("isEntry") {
if !is_entry
.as_bool()
.ok_or_else(|| tera::Error::msg("Failed to parse `isEntry` as bool"))?
{
let msg = format!("Vite manifest entry `{}` is not an entry", &entry);
tracing::error!(err.msg = msg, "error");
return Err(tera::Error::msg(msg));
}
}
val.clone()
}
},
};
let mut resources = String::new();
if let Some(css) = manifest.get("css") {
for css in css
.as_array()
.ok_or_else(|| tera::Error::msg("Failed to parse `css` as array"))?
{
resources.push_str(&format!(
r#"<link rel="stylesheet" href="{}/build/{}">"#,
url,
css.as_str()
.ok_or_else(|| tera::Error::msg("Failed to parse `css` as string"))?
));
}
}
if let Some(js) = manifest.get("file") {
resources.push_str(&format!(
r#"<script type="module" src="{}/build/{}"></script>"#,
url,
js.as_str()
.ok_or_else(|| tera::Error::msg("Failed to parse `file` as string"))?
));
}
Ok(serde_json::Value::String(resources))
},
)
}
impl Deref for View {
type Target = Tera;
fn deref(&self) -> &Self::Target {
&self.engine
}
}