897 lines
33 KiB
Rust
897 lines
33 KiB
Rust
use std::collections::HashMap;
|
|
use std::convert::TryFrom;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::http_response::HttpResponse;
|
|
use crate::server_config::DatumExtensionParser;
|
|
use crate::subdomain_info::SubdomainInfo;
|
|
use crate::text_template_result::TextTemplateResult;
|
|
use crate::utils::{escape_html, gen_true};
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
|
#[serde(tag = "type")]
|
|
pub enum TextTemplateStep {
|
|
#[serde(rename = "static")]
|
|
Static {
|
|
content: String,
|
|
#[serde(default = "gen_true")]
|
|
escaped: bool,
|
|
},
|
|
#[serde(rename = "variable")]
|
|
Variable {
|
|
name: String,
|
|
#[serde(default = "bool::default")]
|
|
escaped: bool,
|
|
},
|
|
#[serde(rename = "json")]
|
|
Json { name: String },
|
|
#[serde(rename = "group")]
|
|
Group {
|
|
#[serde(default = "String::default")]
|
|
label: String,
|
|
#[serde(default = "Vec::default")]
|
|
steps: Vec<TextTemplateStep>,
|
|
},
|
|
#[serde(rename = "list")]
|
|
List {
|
|
name: String,
|
|
item: String,
|
|
#[serde(default = "bool::default")]
|
|
absent_as_empty: bool,
|
|
#[serde(default = "String::default")]
|
|
index0: String,
|
|
#[serde(default = "String::default")]
|
|
index1: String,
|
|
#[serde(default = "Vec::default")]
|
|
steps: Vec<TextTemplateStep>,
|
|
#[serde(default = "Vec::default")]
|
|
steps_empty: Vec<TextTemplateStep>,
|
|
},
|
|
#[serde(rename = "if")]
|
|
If {
|
|
name: String,
|
|
#[serde(default = "bool::default")]
|
|
absent_as_false: bool,
|
|
#[serde(default = "gen_true")]
|
|
trim: bool,
|
|
#[serde(default = "Vec::default")]
|
|
steps: Vec<TextTemplateStep>,
|
|
#[serde(default = "Vec::default")]
|
|
falsy_steps: Vec<TextTemplateStep>,
|
|
},
|
|
#[serde(rename = "ifvar")]
|
|
IfVar {
|
|
name: String,
|
|
other: String,
|
|
#[serde(default = "gen_true")]
|
|
trim: bool,
|
|
#[serde(default = "bool::default")]
|
|
case_insensitive: bool,
|
|
#[serde(default = "Vec::default")]
|
|
equal: Vec<TextTemplateStep>,
|
|
#[serde(default = "Vec::default")]
|
|
different: Vec<TextTemplateStep>,
|
|
},
|
|
#[serde(rename = "render_then_redirect")]
|
|
RenderThenRedirect {
|
|
#[serde(default = "String::default")]
|
|
label: String,
|
|
#[serde(default = "Vec::default")]
|
|
steps: Vec<TextTemplateStep>,
|
|
},
|
|
#[serde(rename = "render_404")]
|
|
Render404 {
|
|
#[serde(default = "String::default")]
|
|
label: String,
|
|
},
|
|
}
|
|
|
|
impl TextTemplateStep {
|
|
pub fn reinflate(&self) -> String {
|
|
match self {
|
|
TextTemplateStep::Static { content, escaped } => {
|
|
if *escaped {
|
|
content.to_owned()
|
|
} else {
|
|
escape_html(content)
|
|
}
|
|
}
|
|
TextTemplateStep::Variable { name, escaped } => {
|
|
format!("{{{{ {}{} }}}}", if *escaped { '!' } else { '@' }, name)
|
|
}
|
|
TextTemplateStep::Json { name } => format!("{{{{ <{name} }}}}"),
|
|
TextTemplateStep::Group { steps, .. } => steps.iter().map(|x| x.reinflate()).collect(),
|
|
TextTemplateStep::List {
|
|
name,
|
|
item,
|
|
absent_as_empty,
|
|
index0,
|
|
index1,
|
|
steps,
|
|
steps_empty,
|
|
} => format!(
|
|
"{{% :: @{} <= {}{} #0@{} #1@{} %}}{}{}{}{{% .. %}}",
|
|
item,
|
|
if *absent_as_empty { '!' } else { '@' },
|
|
name,
|
|
index0,
|
|
index1,
|
|
steps.iter().map(|x| x.reinflate()).collect::<String>(),
|
|
if !steps_empty.is_empty() {
|
|
"{% :. %}"
|
|
} else {
|
|
""
|
|
},
|
|
steps_empty
|
|
.iter()
|
|
.map(|x| x.reinflate())
|
|
.collect::<String>()
|
|
),
|
|
TextTemplateStep::If {
|
|
name,
|
|
absent_as_false,
|
|
trim,
|
|
steps,
|
|
falsy_steps,
|
|
} => format!(
|
|
"{{% ?{} {}{} %}}{}{}{}{{% ?? %}}",
|
|
if *trim { '_' } else { '-' },
|
|
if *absent_as_false { '!' } else { '@' },
|
|
name,
|
|
steps.iter().map(|x| x.reinflate()).collect::<String>(),
|
|
if !falsy_steps.is_empty() {
|
|
"{% ?. %}"
|
|
} else {
|
|
""
|
|
},
|
|
falsy_steps
|
|
.iter()
|
|
.map(|x| x.reinflate())
|
|
.collect::<String>()
|
|
),
|
|
TextTemplateStep::IfVar {
|
|
name,
|
|
other,
|
|
trim,
|
|
case_insensitive,
|
|
equal,
|
|
different,
|
|
} => format!(
|
|
"{{% ?$ {}{} @{} == @{} %}}{}{}{}{{% ?? %}}",
|
|
if *case_insensitive { 's' } else { 'i' },
|
|
if *trim { 't' } else { 'u' },
|
|
name,
|
|
other,
|
|
equal.iter().map(|x| x.reinflate()).collect::<String>(),
|
|
if !different.is_empty() {
|
|
"{% ?. %}"
|
|
} else {
|
|
""
|
|
},
|
|
different.iter().map(|x| x.reinflate()).collect::<String>(),
|
|
),
|
|
TextTemplateStep::RenderThenRedirect { steps, .. } => {
|
|
format!(
|
|
"{{% 302 %}}{}{{% .. %}}",
|
|
steps.iter().map(|x| x.reinflate()).collect::<String>(),
|
|
)
|
|
}
|
|
TextTemplateStep::Render404 { .. } => "{% 404 %}".to_owned(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
|
#[serde(untagged)]
|
|
pub enum TextTemplateData {
|
|
Map(HashMap<String, TextTemplateData>),
|
|
List(Vec<TextTemplateData>),
|
|
Text(String),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TextTemplateRuntime<'a> {
|
|
pub parent: Option<&'a TextTemplateRuntime<'a>>,
|
|
pub current: &'a Vec<TextTemplateStep>,
|
|
pub variables: Option<HashMap<String, TextTemplateData>>,
|
|
}
|
|
|
|
fn deref_dict_multiple<'a>(
|
|
map: &'a HashMap<String, TextTemplateData>,
|
|
path: &[&str],
|
|
i: usize,
|
|
) -> Option<&'a TextTemplateData> {
|
|
if (i + 1) < path.len() {
|
|
if let Some(TextTemplateData::Map(nextmap)) = map.get(path[i]) {
|
|
deref_dict_multiple(nextmap, path, i + 1)
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
map.get(path[i])
|
|
}
|
|
}
|
|
|
|
impl<'a> TextTemplateRuntime<'a> {
|
|
pub fn new(
|
|
parent: Option<&'a TextTemplateRuntime<'a>>,
|
|
current: &'a Vec<TextTemplateStep>,
|
|
variables: Option<HashMap<String, TextTemplateData>>,
|
|
) -> Self {
|
|
TextTemplateRuntime {
|
|
parent,
|
|
current,
|
|
variables,
|
|
}
|
|
}
|
|
|
|
fn _get_vars(&self, names: &[&str]) -> Option<&TextTemplateData> {
|
|
self.variables
|
|
.as_ref()
|
|
.and_then(|vars| deref_dict_multiple(vars, names, 0))
|
|
.or_else(|| self.parent.and_then(|rt| rt._get_vars(names)))
|
|
}
|
|
|
|
fn get_var(&self, name: &str) -> Option<&TextTemplateData> {
|
|
self._get_vars(&name.split('>').collect::<Vec<&str>>())
|
|
}
|
|
|
|
pub fn run<'f: 'a>(
|
|
&'f mut self,
|
|
s: &mut Vec<String>,
|
|
) -> Result<(), TextTemplateRenderingError> {
|
|
for current in self.current {
|
|
match current {
|
|
TextTemplateStep::Static { .. }
|
|
| TextTemplateStep::Variable { .. }
|
|
| TextTemplateStep::Json { .. }
|
|
if s.len() + 16 > s.capacity() =>
|
|
{
|
|
s.reserve(128);
|
|
}
|
|
_ => {}
|
|
};
|
|
match current {
|
|
TextTemplateStep::Static { content, escaped } => {
|
|
s.push(if *escaped {
|
|
content.to_owned()
|
|
} else {
|
|
escape_html(content)
|
|
});
|
|
}
|
|
TextTemplateStep::Variable { name, escaped } => {
|
|
let vr = self.get_var(name);
|
|
match vr {
|
|
Some(ttd) => match ttd {
|
|
TextTemplateData::Text(content) => {
|
|
s.push(if *escaped {
|
|
content.to_owned()
|
|
} else {
|
|
escape_html(content)
|
|
});
|
|
}
|
|
_ => Err(TextTemplateRenderingError::VariableNotATextInstead(
|
|
name.to_string(),
|
|
ttd.clone(),
|
|
))?,
|
|
},
|
|
None => Err(TextTemplateRenderingError::VariableNotFound(
|
|
name.to_string(),
|
|
))?,
|
|
}
|
|
}
|
|
TextTemplateStep::Json { name } => {
|
|
s.push(serde_json::to_string(&self.get_var(name)).map_err(|err| {
|
|
TextTemplateRenderingError::HydrationSerialization(format!(
|
|
"Could not hydrate content: {:?}",
|
|
err
|
|
))
|
|
})?);
|
|
}
|
|
TextTemplateStep::Group { label: _, steps } => {
|
|
Self::new(Some(self), steps, self.variables.clone()).run(s)?;
|
|
}
|
|
TextTemplateStep::List {
|
|
name,
|
|
item,
|
|
absent_as_empty,
|
|
index0,
|
|
index1,
|
|
steps,
|
|
steps_empty,
|
|
} => {
|
|
let def: Vec<TextTemplateData> = vec![];
|
|
let def2: TextTemplateData = TextTemplateData::List(def);
|
|
if let Some(TextTemplateData::List(data)) = self
|
|
.get_var(name)
|
|
.or_else(|| absent_as_empty.then_some(&def2))
|
|
{
|
|
if !data.is_empty() {
|
|
for (index, datum) in data.iter().enumerate() {
|
|
let mut vars = HashMap::new();
|
|
vars.insert(item.to_owned(), datum.clone());
|
|
vars.insert(
|
|
index0.to_owned(),
|
|
TextTemplateData::Text(format!("{}", index)),
|
|
);
|
|
vars.insert(
|
|
index1.to_owned(),
|
|
TextTemplateData::Text(format!("{}", index + 1)),
|
|
);
|
|
Self::new(Some(self), steps, Some(vars)).run(s)?;
|
|
}
|
|
} else {
|
|
Self::new(Some(self), steps_empty, self.variables.clone()).run(s)?;
|
|
}
|
|
} else {
|
|
Err(TextTemplateRenderingError::VariableNotAList(
|
|
name.to_string(),
|
|
))?;
|
|
}
|
|
}
|
|
TextTemplateStep::If {
|
|
name,
|
|
absent_as_false,
|
|
trim,
|
|
steps,
|
|
falsy_steps,
|
|
} => {
|
|
let def: Vec<TextTemplateData> = vec![];
|
|
let def2: TextTemplateData = TextTemplateData::List(def);
|
|
let var = self
|
|
.get_var(name)
|
|
.or_else(|| absent_as_false.then_some(&def2))
|
|
.ok_or_else(|| {
|
|
TextTemplateRenderingError::VariableNotFound(name.to_string())
|
|
})?;
|
|
let truthyness = match var {
|
|
TextTemplateData::Map(_) => Err(
|
|
TextTemplateRenderingError::VariableNotATextNorAList(name.to_string()),
|
|
)?,
|
|
TextTemplateData::List(a) => !a.is_empty(),
|
|
TextTemplateData::Text(a) => {
|
|
!trim.then(|| a.trim()).unwrap_or(a).is_empty()
|
|
}
|
|
};
|
|
if truthyness {
|
|
Self::new(Some(self), steps, None).run(s)?;
|
|
} else {
|
|
Self::new(Some(self), falsy_steps, None).run(s)?;
|
|
}
|
|
}
|
|
TextTemplateStep::IfVar {
|
|
name,
|
|
other,
|
|
trim,
|
|
case_insensitive,
|
|
equal,
|
|
different,
|
|
} => {
|
|
let var1 = self
|
|
.get_var(name)
|
|
.and_then(|x| match x {
|
|
TextTemplateData::Text(t) => Some(t),
|
|
_ => None,
|
|
})
|
|
.map(|x| if *trim { x.trim() } else { x })
|
|
.map(|x| {
|
|
if *case_insensitive {
|
|
x.to_lowercase()
|
|
} else {
|
|
x.to_owned()
|
|
}
|
|
})
|
|
.ok_or_else(|| {
|
|
TextTemplateRenderingError::VariableNotFound(name.to_string())
|
|
})?;
|
|
let var2 = self
|
|
.get_var(other)
|
|
.and_then(|x| match x {
|
|
TextTemplateData::Text(t) => Some(t),
|
|
_ => None,
|
|
})
|
|
.map(|x| if *trim { x.trim() } else { x })
|
|
.map(|x| {
|
|
if *case_insensitive {
|
|
x.to_lowercase()
|
|
} else {
|
|
x.to_owned()
|
|
}
|
|
})
|
|
.ok_or_else(|| {
|
|
TextTemplateRenderingError::VariableNotFound(other.to_string())
|
|
})?;
|
|
Self::new(
|
|
Some(self),
|
|
if var1 == var2 { equal } else { different },
|
|
None,
|
|
)
|
|
.run(s)?;
|
|
}
|
|
TextTemplateStep::RenderThenRedirect { label: _, steps } => {
|
|
let mut vs: Vec<String> = Vec::default();
|
|
Self::new(Some(self), steps, None).run(&mut vs)?;
|
|
Err(TextTemplateRenderingError::InterruptedRenderingWith(
|
|
InterruptDetermination::RenderedResponse(HttpResponse::generate_302(
|
|
vs.into_iter().collect::<String>(),
|
|
)),
|
|
))?
|
|
}
|
|
TextTemplateStep::Render404 { label: _ } => {
|
|
Err(TextTemplateRenderingError::InterruptedRenderingWith(
|
|
InterruptDetermination::Return404,
|
|
))?
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
|
pub struct TextTemplate {
|
|
#[serde(default = "Option::default")]
|
|
pub mime: Option<String>,
|
|
#[serde(default = "Vec::default")]
|
|
pub steps: Vec<TextTemplateStep>,
|
|
}
|
|
|
|
impl TextTemplate {
|
|
pub fn render(
|
|
self,
|
|
d: &SubdomainInfo,
|
|
q: &HashMap<String, String>,
|
|
h: &HashMap<String, String>,
|
|
p: DatumExtensionParser,
|
|
s: &str,
|
|
) -> Result<TextTemplateResult, TextTemplateRenderingError> {
|
|
let template_data = {
|
|
let mut td = p
|
|
.parse_str::<TextTemplateData>(s)
|
|
.map_err(TextTemplateRenderingError::DataDeserialization)?;
|
|
Self::add_str_ttd_map_if_not_exists(&mut td, "subdomain", d.sub_domain);
|
|
Self::add_str_ttd_map_if_not_exists(&mut td, "basedomain", Some(d.base_domain));
|
|
Self::add_str_ttd_map_if_not_exists(&mut td, "host", Some(d.host));
|
|
Self::add_str_ttd_map_if_not_exists(
|
|
&mut td,
|
|
"domain_after_first_dot",
|
|
Some(d.after_first_dot),
|
|
);
|
|
Self::add_str_ttd_map_if_not_exists(
|
|
&mut td,
|
|
"domain_before_first_dot",
|
|
Some(d.before_first_dot),
|
|
);
|
|
Self::add_strmap_ttd_map_if_not_exists(&mut td, "query", Some(q));
|
|
Self::add_strmap_ttd_map_if_not_exists(&mut td, "headers", Some(h));
|
|
td
|
|
};
|
|
let mut vs: Vec<String> = Vec::with_capacity(128);
|
|
match template_data {
|
|
TextTemplateData::Map(hm) => {
|
|
TextTemplateRuntime::new(None, &self.steps, Some(hm)).run(&mut vs)
|
|
}
|
|
_ => Err(TextTemplateRenderingError::RootDataNotObject),
|
|
}?;
|
|
Ok(TextTemplateResult::new(self.mime, vs.concat(), None))
|
|
}
|
|
|
|
pub fn reinflate(&self) -> String {
|
|
self.steps.iter().map(|x| x.reinflate()).collect()
|
|
}
|
|
|
|
fn add_str_ttd_map_if_not_exists(td: &mut TextTemplateData, kw: &str, vlopt: Option<&str>) {
|
|
if let TextTemplateData::Map(ref mut mp) = td {
|
|
if let Some(vl) = vlopt {
|
|
if !mp.contains_key(kw) {
|
|
mp.insert(kw.to_string(), TextTemplateData::Text(vl.to_owned()));
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
fn add_strmap_ttd_map_if_not_exists(
|
|
td: &mut TextTemplateData,
|
|
kw: &str,
|
|
vlopt: Option<&HashMap<String, String>>,
|
|
) {
|
|
if let TextTemplateData::Map(ref mut mp) = td {
|
|
if let Some(vl) = vlopt {
|
|
if !mp.contains_key(kw) {
|
|
mp.insert(
|
|
kw.to_string(),
|
|
TextTemplateData::Map(
|
|
vl.iter()
|
|
.map(|(kpk, kpv)| {
|
|
(kpk.to_owned(), TextTemplateData::Text(kpv.to_string()))
|
|
})
|
|
.collect(),
|
|
),
|
|
);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
fn _push_thing(
|
|
toadd: TextTemplateStep,
|
|
steps: &mut Vec<TextTemplateStep>,
|
|
is_else: &[bool],
|
|
) -> Result<(), TemplateParsingError> {
|
|
let is_elsability_empty = is_else.is_empty();
|
|
match steps.last_mut() {
|
|
_ if is_elsability_empty => {
|
|
steps.push(toadd);
|
|
Ok(())
|
|
}
|
|
Some(TextTemplateStep::Group { ref mut steps, .. }) => {
|
|
Self::_push_thing(toadd, steps, &is_else[1..])
|
|
}
|
|
Some(TextTemplateStep::List {
|
|
ref mut steps,
|
|
ref mut steps_empty,
|
|
..
|
|
}) => Self::_push_thing(
|
|
toadd,
|
|
if is_else.first().copied() != Some(true) {
|
|
steps
|
|
} else {
|
|
steps_empty
|
|
},
|
|
&is_else[1..],
|
|
),
|
|
Some(TextTemplateStep::If {
|
|
ref mut steps,
|
|
ref mut falsy_steps,
|
|
..
|
|
}) => Self::_push_thing(
|
|
toadd,
|
|
if is_else.first().copied() != Some(true) {
|
|
steps
|
|
} else {
|
|
falsy_steps
|
|
},
|
|
&is_else[1..],
|
|
),
|
|
Some(TextTemplateStep::IfVar {
|
|
ref mut equal,
|
|
ref mut different,
|
|
..
|
|
}) => Self::_push_thing(
|
|
toadd,
|
|
if is_else.first().copied() != Some(true) {
|
|
equal
|
|
} else {
|
|
different
|
|
},
|
|
&is_else[1..],
|
|
),
|
|
_ => Err(TemplateParsingError::CannotNavigateInStack),
|
|
}
|
|
}
|
|
|
|
fn _push_static(
|
|
source_vec: &[char],
|
|
s: &mut usize,
|
|
e: &usize,
|
|
steps: &mut Vec<TextTemplateStep>,
|
|
is_else: &[bool],
|
|
) -> Result<(), TemplateParsingError> {
|
|
if s != e {
|
|
let content_prev = &source_vec[*s..*e].iter().collect::<String>();
|
|
if !content_prev.is_empty() {
|
|
let toadd = TextTemplateStep::Static {
|
|
content: content_prev.to_owned(),
|
|
escaped: true,
|
|
};
|
|
*s = *e;
|
|
Self::_push_thing(toadd, steps, is_else)?;
|
|
}
|
|
}
|
|
Ok::<(), TemplateParsingError>(())
|
|
}
|
|
|
|
pub fn generate(mime: Option<&str>, source: &str) -> Result<Self, TemplateParsingError> {
|
|
let source_vec = source.chars().collect::<Vec<char>>();
|
|
let mut steps: Vec<TextTemplateStep> = vec![TextTemplateStep::Group {
|
|
label: "".to_owned(),
|
|
steps: vec![],
|
|
}];
|
|
let mut is_else: Vec<bool> = vec![true];
|
|
let mut s = 0usize;
|
|
let mut e = 0usize;
|
|
loop {
|
|
if e >= source_vec.len() {
|
|
Self::_push_static(&source_vec, &mut s, &e, &mut steps, &is_else)?;
|
|
break;
|
|
} else if source_vec.get(e).copied() == Some('{')
|
|
&& source_vec.get(e + 1).copied() == Some('{')
|
|
&& source_vec.len() - e > 2
|
|
{
|
|
Self::_push_static(&source_vec, &mut s, &e, &mut steps, &is_else)?;
|
|
s = e + 2;
|
|
e = s;
|
|
while e < source_vec.len()
|
|
&& !(source_vec.get(e).copied() == Some('}')
|
|
&& source_vec.get(e + 1).copied() == Some('}'))
|
|
{
|
|
e += 1;
|
|
}
|
|
if e >= source_vec.len() {
|
|
Err(TemplateParsingError::CannotFindClosingTokenForVariable)?
|
|
}
|
|
let var = source_vec[s..e]
|
|
.iter()
|
|
.collect::<String>()
|
|
.trim()
|
|
.to_owned();
|
|
if var.is_empty() {
|
|
Err(TemplateParsingError::VariableUnamed)?
|
|
}
|
|
match var.split_at(1) {
|
|
("@", varname) => Self::_push_thing(
|
|
TextTemplateStep::Variable {
|
|
name: varname.to_owned(),
|
|
escaped: false,
|
|
},
|
|
&mut steps,
|
|
&is_else,
|
|
)?,
|
|
("!", varname) => Self::_push_thing(
|
|
TextTemplateStep::Variable {
|
|
name: varname.to_owned(),
|
|
escaped: true,
|
|
},
|
|
&mut steps,
|
|
&is_else,
|
|
)?,
|
|
("<", varname) => Self::_push_thing(
|
|
TextTemplateStep::Json {
|
|
name: varname.to_owned(),
|
|
},
|
|
&mut steps,
|
|
&is_else,
|
|
)?,
|
|
_ => Err(TemplateParsingError::VariableWrongPrefix)?,
|
|
}
|
|
s = e + 2;
|
|
e = s;
|
|
} else if source_vec.get(e).copied() == Some('{')
|
|
&& source_vec.get(e + 1).copied() == Some('%')
|
|
&& source_vec.len() - e > 2
|
|
{
|
|
Self::_push_static(&source_vec, &mut s, &e, &mut steps, &is_else)?;
|
|
s = e + 2;
|
|
e = s;
|
|
while e < source_vec.len()
|
|
&& !(source_vec.get(e).copied() == Some('%')
|
|
&& source_vec.get(e + 1).copied() == Some('}'))
|
|
{
|
|
e += 1;
|
|
}
|
|
if e >= source_vec.len() {
|
|
Err(TemplateParsingError::CannotFindClosingTokenForDirective)?
|
|
}
|
|
let var = source_vec[s..e]
|
|
.iter()
|
|
.collect::<String>()
|
|
.trim()
|
|
.to_owned();
|
|
if var.len() < 2 {
|
|
Err(TemplateParsingError::DirectiveTooShort)?
|
|
}
|
|
let is_else_len_minus_1 = is_else.len() - 1;
|
|
match var.trim() {
|
|
"404" => Self::_push_thing(
|
|
TextTemplateStep::Render404 {
|
|
label: String::default(),
|
|
},
|
|
&mut steps,
|
|
&is_else,
|
|
)?,
|
|
"302" => {
|
|
Self::_push_thing(
|
|
TextTemplateStep::RenderThenRedirect {
|
|
label: String::default(),
|
|
steps: Vec::default(),
|
|
},
|
|
&mut steps,
|
|
&is_else,
|
|
)?;
|
|
is_else.push(false);
|
|
}
|
|
var => match var.split_at(2) {
|
|
("?-", var) => match var.trim().split_at(1) {
|
|
("@", var) => {
|
|
let thing = TextTemplateStep::If {
|
|
name: var.to_owned(),
|
|
absent_as_false: false,
|
|
trim: false,
|
|
steps: vec![],
|
|
falsy_steps: vec![],
|
|
};
|
|
Self::_push_thing(thing, &mut steps, &is_else)?;
|
|
is_else.push(false);
|
|
}
|
|
("!", var) => {
|
|
let thing = TextTemplateStep::If {
|
|
name: var.to_owned(),
|
|
absent_as_false: true,
|
|
trim: false,
|
|
steps: vec![],
|
|
falsy_steps: vec![],
|
|
};
|
|
Self::_push_thing(thing, &mut steps, &is_else)?;
|
|
is_else.push(false);
|
|
}
|
|
_ => Err(TemplateParsingError::VariableWrongPrefix)?,
|
|
},
|
|
("?_", var) => match var.trim().split_at(1) {
|
|
("@", var) => {
|
|
let thing = TextTemplateStep::If {
|
|
name: var.to_owned(),
|
|
absent_as_false: false,
|
|
trim: true,
|
|
steps: vec![],
|
|
falsy_steps: vec![],
|
|
};
|
|
Self::_push_thing(thing, &mut steps, &is_else)?;
|
|
is_else.push(false);
|
|
}
|
|
("!", var) => {
|
|
let thing = TextTemplateStep::If {
|
|
name: var.to_owned(),
|
|
absent_as_false: true,
|
|
trim: true,
|
|
steps: vec![],
|
|
falsy_steps: vec![],
|
|
};
|
|
Self::_push_thing(thing, &mut steps, &is_else)?;
|
|
is_else.push(false);
|
|
}
|
|
_ => Err(TemplateParsingError::VariableWrongPrefix)?,
|
|
},
|
|
("?$", composite) => {
|
|
let (case_insensitive, trim, composite) =
|
|
match composite.trim().split_at(2) {
|
|
("it" | "ti", rest) => (true, true, rest.trim()),
|
|
("iu" | "ui", rest) => (true, false, rest.trim()),
|
|
("st" | "ts", rest) => (false, true, rest.trim()),
|
|
("su" | "us", rest) => (false, false, rest.trim()),
|
|
_ => Err(TemplateParsingError::VariableWrongPrefix)?,
|
|
};
|
|
let (name1, other1) = composite.split_once("==").unwrap();
|
|
let thing = TextTemplateStep::IfVar {
|
|
name: name1.trim()[1..].to_owned(),
|
|
other: other1.trim()[1..].to_owned(),
|
|
trim,
|
|
case_insensitive,
|
|
equal: Vec::default(),
|
|
different: Vec::default(),
|
|
};
|
|
Self::_push_thing(thing, &mut steps, &is_else)?;
|
|
is_else.push(false);
|
|
}
|
|
("::", composite) => {
|
|
let (itemvar, rest) = composite.trim().split_once("<=").unwrap();
|
|
let rest = rest
|
|
.split(' ')
|
|
.filter(|x| !x.trim().is_empty())
|
|
.map(|x| x.to_owned())
|
|
.collect::<Vec<String>>();
|
|
let (varname, parts) = rest.split_first().unwrap();
|
|
let thing = TextTemplateStep::List {
|
|
name: varname.trim()[1..].to_owned(),
|
|
item: itemvar.trim()[1..].to_owned(),
|
|
absent_as_empty: varname.trim().starts_with('!'),
|
|
index0: parts
|
|
.iter()
|
|
.find(|x| x.starts_with("#0"))
|
|
.map(|x| x[3..].to_owned())
|
|
.unwrap_or_else(String::default),
|
|
index1: parts
|
|
.iter()
|
|
.find(|x| x.starts_with("#1"))
|
|
.map(|x| x[3..].to_owned())
|
|
.unwrap_or_else(String::default),
|
|
steps: Vec::default(),
|
|
steps_empty: Vec::default(),
|
|
};
|
|
Self::_push_thing(thing, &mut steps, &is_else)?;
|
|
is_else.push(false);
|
|
}
|
|
("?.", ..) => is_else[is_else_len_minus_1] = true,
|
|
(":.", ..) => is_else[is_else_len_minus_1] = true,
|
|
("??", ..) => {
|
|
is_else.pop();
|
|
}
|
|
("..", ..) => {
|
|
is_else.pop();
|
|
}
|
|
_ => Err(TemplateParsingError::DirectiveWrongPrefix)?,
|
|
},
|
|
};
|
|
s = e + 2;
|
|
e = s;
|
|
} else {
|
|
e += 1;
|
|
}
|
|
}
|
|
|
|
if is_else.len() != 1 {
|
|
Err(TemplateParsingError::StackNotEmptyOnFinish)
|
|
} else if let Some(TextTemplateStep::Group { steps, .. }) = steps.first() {
|
|
Ok(Self {
|
|
mime: mime.map(|x| x.to_owned()),
|
|
steps: steps.to_owned(),
|
|
})
|
|
} else {
|
|
Err(TemplateParsingError::StackNotEmptyOnFinish)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&str> for TextTemplate {
|
|
type Error = TemplateLoadingError;
|
|
|
|
fn try_from(value_borrowed: &str) -> Result<Self, Self::Error> {
|
|
DatumExtensionParser::Yaml
|
|
.parse_str(value_borrowed)
|
|
.map_err(TemplateLoadingError::TemplateDeserialization)
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Deserialize, Serialize, Debug)]
|
|
pub enum TemplateParsingError {
|
|
StackNotEmptyOnFinish,
|
|
CannotNavigateInStack,
|
|
CannotFindClosingTokenForVariable,
|
|
VariableUnamed,
|
|
VariableWrongPrefix,
|
|
CannotFindClosingTokenForDirective,
|
|
DirectiveTooShort,
|
|
DirectiveWrongPrefix,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Deserialize, Serialize, Debug)]
|
|
pub enum TextTemplateEvaluationError {
|
|
DataDeserialization,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
|
pub enum TemplateLoadingError {
|
|
TemplateDeserialization(String),
|
|
}
|
|
|
|
impl From<TemplateLoadingError> for String {
|
|
fn from(ttle: TemplateLoadingError) -> String {
|
|
format!("{:?}", ttle)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
|
pub enum TextTemplateRenderingError {
|
|
DataDeserialization(String),
|
|
RootDataNotObject,
|
|
VariableNotAText(String),
|
|
VariableNotATextInstead(String, TextTemplateData),
|
|
VariableNotAList(String),
|
|
VariableNotFound(String),
|
|
VariableNotATextNorAList(String),
|
|
HydrationSerialization(String),
|
|
InterruptedRenderingWith(InterruptDetermination),
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, Serialize, Debug)]
|
|
pub enum InterruptDetermination {
|
|
RenderedResponse(HttpResponse),
|
|
Return404,
|
|
}
|
|
|
|
impl From<TextTemplateRenderingError> for String {
|
|
fn from(ttre: TextTemplateRenderingError) -> String {
|
|
format!("{:?}", ttre)
|
|
}
|
|
}
|