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>, p: Option<&'p Post>, curmedia: Option, curpage: Option, maxpage: Option, single_post: bool, error: Option, error_str: Option } impl Renderer { pub fn load>(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, maxpage: Option) -> 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, maxpage: Option) -> 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, 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, 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) -> 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> = 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, 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) } }