aboutsummaryrefslogtreecommitdiff
path: root/backend/src/render.rs
diff options
context:
space:
mode:
authorGravatar Chris Xiong <chirs241097@gmail.com> 2024-07-24 23:40:11 -0400
committerGravatar Chris Xiong <chirs241097@gmail.com> 2024-07-24 23:40:11 -0400
commitb736068ee7b82e05c2ede8bc48ace7ffa4709e29 (patch)
treeeaa3cce9fdd9973043a7a55613584f90f6598a20 /backend/src/render.rs
downloadnotekins-b736068ee7b82e05c2ede8bc48ace7ffa4709e29.tar.xz
Initial commit.
Diffstat (limited to 'backend/src/render.rs')
-rw-r--r--backend/src/render.rs346
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)
+ }
+}