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