diff options
Diffstat (limited to 'backend/src/render.rs')
-rw-r--r-- | backend/src/render.rs | 346 |
1 files changed, 346 insertions, 0 deletions
diff --git a/backend/src/render.rs b/backend/src/render.rs new file mode 100644 index 0000000..76b5490 --- /dev/null +++ b/backend/src/render.rs @@ -0,0 +1,346 @@ +use regex::Regex; +use chrono::DateTime; +use tzfile::Tz; + +use std::path::Path; +use std::fs::File; +use std::io::Read; +use std::borrow::Cow; + +use crate::monolith::MediaInstance; +use crate::monolith::Post; +use crate::config::Config; + +pub struct Renderer { + paget: String, + pagert: String, + postt: String, + media_contt: String, + media_imgt: String, + errort: String +} + +#[derive(Clone)] +struct SubstitutionContext<'p> { + ps: Option<&'p Vec<Post>>, + p: Option<&'p Post>, + curmedia: Option<usize>, + curpage: Option<usize>, + maxpage: Option<usize>, + single_post: bool, + error: Option<usize>, + error_str: Option<String> +} + +impl Renderer { + pub fn load<P: AsRef<Path>>(template_dir: P) -> Renderer { + let template_dir = template_dir.as_ref(); + let mut f = File::open(template_dir.join("page.template")).unwrap(); + let mut paget = String::from(""); + f.read_to_string(&mut paget).unwrap(); + let mut f = File::open(template_dir.join("pager.template")).unwrap(); + let mut pagert = String::from(""); + f.read_to_string(&mut pagert).unwrap(); + let mut f = File::open(template_dir.join("post.template")).unwrap(); + let mut postt = String::from(""); + f.read_to_string(&mut postt).unwrap(); + let mut f = File::open(template_dir.join("media-container.template")).unwrap(); + let mut media_contt = String::from(""); + f.read_to_string(&mut media_contt).unwrap(); + let mut f = File::open(template_dir.join("media-image.template")).unwrap(); + let mut media_imgt = String::from(""); + f.read_to_string(&mut media_imgt).unwrap(); + let mut f = File::open(template_dir.join("error.template")).unwrap(); + let mut errort = String::from(""); + f.read_to_string(&mut errort).unwrap(); + Renderer{paget, pagert, postt, media_contt, media_imgt, errort} + } + fn resolve_current_page(sub: &str, curpage: Option<usize>, maxpage: Option<usize>) -> String { + if curpage.is_none() || maxpage.is_none() { + return String::from("") + } + let curpage = curpage.unwrap() + 1; + let maxpage = maxpage.unwrap(); + let p: Vec<&str> = sub.split('/').collect(); + if p.len() < 2 { + curpage.to_string() + } else { + let d = isize::from_str_radix(p[1], 10).unwrap_or_default(); + if let Some(v) = curpage.checked_add_signed(d) { + if (v == 0) || (v > maxpage) { + String::from("") + } else { + v.to_string() + } + } else { String::from("") } + } + } + + fn resolve_current_page_url(sub: &str, curpage: Option<usize>, maxpage: Option<usize>) -> String { + if curpage.is_none() || maxpage.is_none() { + return String::from(""); + } + let curpage = curpage.unwrap() + 1; + let maxpage = maxpage.unwrap(); + let mut p = sub.split('/'); + p.next(); + let d = isize::from_str_radix(p.next().unwrap_or_default(), 10).unwrap_or_default(); + let default = p.next().unwrap_or_default(); + if let Some(v) = curpage.checked_add_signed(d) { + if (v == 0) || (v > maxpage) { + String::from(default) + } else { + String::from("?page=") + &v.to_string() + } + } else { String::from(default) } + } + + fn resolve_post_content_plain(sub: &str, p: Option<&Post>) -> String { + if p.is_none() { + return String::from(""); + } + let p = p.unwrap(); + let mut params = sub.split('/'); + params.next(); + let c = usize::from_str_radix(params.next().unwrap_or_default(), 10).unwrap_or_default(); + let re = Regex::new("<[^>]+>").unwrap(); + let plain = re.replace_all(&p.content, ""); + let taken: String = plain.chars().take(c).collect(); + if taken.len() < plain.len() { + taken + " ..." + } else { taken } + } + + fn resolve_img_thumb_url(p: Option<&Post>, curmedia: Option<usize>, conf: &Config) -> String { + if p.is_none() { + return String::from(""); + } + let p = p.unwrap(); + if let Some(curmedia) = curmedia { + if let MediaInstance::Image{thmb, orig: _} = &p.media[curmedia] { + return conf.get_str("SERVED_DATA_ROOT") + "/" + thmb; + } + } + String::from("") + } + + fn resolve_img_orig_url(p: Option<&Post>, curmedia: Option<usize>, conf: &Config) -> String { + if p.is_none() { + return String::from(""); + } + let p = p.unwrap(); + if let Some(curmedia) = curmedia { + if let MediaInstance::Image{thmb: _, orig} = &p.media[curmedia] { + return conf.get_str("SERVED_DATA_ROOT") + "/" + orig; + } + } + String::from("") + } + + fn resolve_max_page(maxpage: Option<usize>) -> String { + if let Some(maxpage) = maxpage { + maxpage.to_string() + } else { + String::from("") + } + } + + fn resolve_post_content(p: Option<&Post>) -> String { + if let Some(p) = p { + p.content.clone() + } else { String::from("") } + } + + fn resolve_post_date_formatted(p: Option<&Post>, conf: &Config) -> String { + if p.is_none() { + return String::from(""); + } + let p = p.unwrap(); + if let Some(dt) = DateTime::from_timestamp(p.date, 0) { + if let Ok(tz) = Tz::named(&conf.get_str("DISPLAY_TIMEZONE")) { + dt.with_timezone(&&tz).to_rfc3339_opts(chrono::SecondsFormat::Secs, true) + } else { dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true) } + } else { String::from("") } + } + + fn resolve_post_date_timestamp(p: Option<&Post>) -> String { + if let Some(p) = p { + p.date.to_string() + } else { String::from("") } + } + + fn resolve_post_tags(p: Option<&Post>) -> String { + if let Some(p) = p { + String::from(p.tags.iter().fold(String::from(""), |s, t| s + "#" + &t + " ").trim_end()) + } else { String::from("") } + } + + fn render_post(&self, sc: &SubstitutionContext, conf: &Config) -> String { + self.substitute(&self.postt, sc, conf) + } + + fn render_posts(&self, sc: &SubstitutionContext, conf: &Config) -> String { + if let Some(ps) = sc.ps { + let s = ps.iter().rev().fold(String::from(""), |r, p| { + let psc = SubstitutionContext { + p: Some(&p), + .. sc.clone() + }; + r + &self.render_post(&psc, conf) + }); + return s; + } + String::from("") + } + + fn render_pager(&self, sc: &SubstitutionContext, conf: &Config) -> String { + if sc.single_post { + String::from("") + } else { + self.substitute(&self.pagert, sc, conf) + } + } + + fn render_media_instance(&self, sc: &SubstitutionContext, conf: &Config) -> String { + if let Some(curmedia) = sc.curmedia { + if let Some(p) = sc.p { + if curmedia < p.media.len() { + if let MediaInstance::Image{thmb: _, orig: _} = p.media[curmedia] { + return self.substitute(&self.media_imgt, sc, conf); + } + } + } + } + String::from("") + } + + fn render_media(&self, sc: &SubstitutionContext, conf: &Config) -> String { + if let Some(p) = sc.p { + let s = (0..p.media.len()).fold(String::from(""), |r, midx| { + let nsc = SubstitutionContext { + curmedia: Some(midx), + .. sc.clone() + }; + r + &self.render_media_instance(&nsc, conf) + }); + return s; + } + String::from("") + } + + fn resolve_media_container(&self, sc: &SubstitutionContext, conf: &Config) -> String { + if let Some(p) = sc.p { + if p.media.len() > 0 { + return self.substitute(&self.media_contt, sc, conf); + } + } + String::from("") + } + + fn resolve_error_status(sc: &SubstitutionContext) -> String { + if let Some(err) = sc.error { + err.to_string() + } else { + String::from("") + } + } + + fn resolve_error_description(sc: &SubstitutionContext) -> String { + if let Some(errs) = &sc.error_str { + String::from(errs) + } else { + String::from("") + } + } + + fn resolve_notekins_version(conf: &Config) -> String { + conf.get_str("VERSION_STRING") + } + + fn resolve_substitution(&self, sub: &str, sc: &SubstitutionContext, conf: &Config) -> String { + if sub.starts_with("CURRENT_PAGE_URL") { + Self::resolve_current_page_url(sub, sc.curpage, sc.maxpage) + } else if sub.starts_with("CURRENT_PAGE") { + Self::resolve_current_page(sub, sc.curpage, sc.maxpage) + } else if sub.starts_with("POST_CONTENT_PLAIN") { + Self::resolve_post_content_plain(sub, sc.p) + } else { + match sub { + "IMG_THUMB_URL" => Self::resolve_img_thumb_url(sc.p, sc.curmedia, conf), + "IMG_ORIG_URL" => Self::resolve_img_orig_url(sc.p, sc.curmedia, conf), + "POSTS" => self.render_posts(sc, conf), + "MAX_PAGE" => Self::resolve_max_page(sc.maxpage), + "PAGER" => self.render_pager(sc, conf), + "MEDIA" => self.render_media(sc, conf), + "MEDIA_CONTAINER" => self.resolve_media_container(sc, conf), + "POST_CONTENT" => Self::resolve_post_content(sc.p), + "POST_DATE_FORMATTED" => Self::resolve_post_date_formatted(sc.p, conf), + "POST_DATE_TIMESTAMP" => Self::resolve_post_date_timestamp(sc.p), + "POST_TAGS" => Self::resolve_post_tags(sc.p), + "ERROR_STATUS" => Self::resolve_error_status(sc), + "ERROR_DESCRIPTION" => Self::resolve_error_description(sc), + "NOTEKINS_VERSION" => Self::resolve_notekins_version(conf), + _ => { + eprintln!("unknown substitution string {}", sub); + String::from("") + } + } + } + } + + fn substitute(&self, template: &str, sc: &SubstitutionContext, conf: &Config) -> String { + let mut sp: Vec<Cow<'_, str>> = template.split('@').map(|x| Cow::Borrowed(x)).collect(); + for sub in sp.iter_mut().skip(1).step_by(2) { + let subbed = self.resolve_substitution(sub, sc, conf); + *sub = Cow::Owned(subbed); + } + sp.iter().fold(String::from(""), |r, s| r + &s) + } + + fn render_page_internal(&self, sc: &SubstitutionContext, conf: &Config) -> String { + self.substitute(&self.paget, sc, conf) + } + + pub fn render_page(&self, posts: Vec<Post>, curpage: usize, maxpage: usize, conf: &Config) -> String { + let sc = SubstitutionContext { + ps: Some(&posts), + p: None, + curmedia: None, + curpage: Some(curpage), + maxpage: Some(maxpage), + single_post: false, + error: None, + error_str: None + }; + self.render_page_internal(&sc, conf) + } + + pub fn render_single_post(&self, post: Post, conf: &Config) -> String { + let ps = vec![post]; + let sc = SubstitutionContext { + ps: Some(&ps), + p: Some(&ps[0]), + curmedia: None, + curpage: None, + maxpage: None, + single_post: true, + error: None, + error_str: None + }; + self.render_page_internal(&sc, conf) + } + + pub fn render_error(&self, err: usize, errs: String, conf: &Config) -> String { + let sc = SubstitutionContext { + ps: None, + p: None, + curmedia: None, + curpage: None, + maxpage: None, + single_post: false, + error: Some(err), + error_str: Some(errs) + }; + self.substitute(&self.errort, &sc, &conf) + } +} |