static-site-server-rs/src/text_template.rs

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)
}
}