aboutsummaryrefslogblamecommitdiff
path: root/backend/src/render.rs
blob: 76b5490f21b7f81da1accb0fc0c00f15ca2fa1c7 (plain) (tree)

























































































































































































































































































































































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