lssg_lib/renderer/modules/
blog_module.rs

1use std::{collections::HashSet, str::FromStr};
2
3use chrono::{DateTime, Utc};
4use log::{error, warn};
5use serde_extensions::Overwrite;
6
7use crate::{
8    html::{to_attributes, DomId, DomTree},
9    lmarkdown::Token,
10    lssg_error::LssgError,
11    renderer::RenderContext,
12    sitetree::{Input, SiteNodeKind},
13    tree::DFS,
14};
15
16use super::{RendererModule, TokenRenderer};
17
18pub struct BlogModule {
19    post_site_ids: HashSet<usize>,
20    root_site_ids: HashSet<usize>,
21    /// Local variable to keep track if date has been inserted
22    has_inserted_date: bool,
23}
24
25impl BlogModule {
26    pub fn new() -> Self {
27        Self {
28            post_site_ids: HashSet::new(),
29            root_site_ids: HashSet::new(),
30            has_inserted_date: false,
31        }
32    }
33}
34
35impl RendererModule for BlogModule {
36    fn id(&self) -> &'static str {
37        return "blog";
38    }
39
40    fn after_init(
41        &mut self,
42        site_tree: &crate::sitetree::SiteTree,
43    ) -> Result<(), crate::lssg_error::LssgError> {
44        // if parent contains blog key than all children also belong to blog
45        for id in DFS::new(site_tree) {
46            match &site_tree[id].kind {
47                SiteNodeKind::Page(page) => {
48                    if let Some(attributes) = page.attributes() {
49                        if let Some(_) = attributes.get("blog") {
50                            self.root_site_ids.insert(id);
51                            continue;
52                        }
53                    }
54
55                    if let Some(parent) = site_tree.page_parent(id) {
56                        if self.post_site_ids.contains(&parent)
57                            || self.root_site_ids.contains(&parent)
58                        {
59                            self.post_site_ids.insert(id);
60                        }
61                    }
62                }
63                _ => {}
64            }
65        }
66
67        Ok(())
68    }
69
70    fn render_page<'n>(&mut self, dom: &mut DomTree, context: &RenderContext<'n>) {
71        let site_tree = context.site_tree;
72        let site_id = context.site_id;
73
74        if !self.post_site_ids.contains(&site_id) && !self.root_site_ids.contains(&site_id) {
75            return;
76        }
77
78        // reset state
79        self.has_inserted_date = false;
80
81        // TODO make shorter
82        let body = dom.get_elements_by_tag_name("body")[0];
83
84        // add breacrumbs
85        {
86            let nav = dom.add_element_with_attributes(
87                body,
88                "nav",
89                to_attributes([("class", "breadcrumbs")]),
90            );
91
92            dom.add_text(nav, "/");
93
94            let parents = site_tree.parents(site_id);
95            let parents_length = parents.len();
96            for (i, p) in parents.into_iter().rev().enumerate() {
97                let a = dom.add_element_with_attributes(
98                    nav,
99                    "a",
100                    to_attributes([("href", site_tree.rel_path(site_id, p))]),
101                );
102                if i != parents_length - 1 {
103                    dom.add_text(nav, "/");
104                }
105                dom.add_text(a, site_tree[p].name.clone());
106            }
107            dom.add_text(nav, format!("/{}", site_tree[site_id].name));
108        }
109    }
110
111    fn render_body<'n>(
112        &mut self,
113        dom: &mut DomTree,
114        context: &RenderContext<'n>,
115        parent_id: DomId,
116        token: &Token,
117        tr: &mut TokenRenderer,
118    ) -> bool {
119        let site_id = context.site_id;
120        if !self.post_site_ids.contains(&site_id) {
121            return false;
122        }
123
124        match token {
125            Token::Heading { depth, .. } if *depth == 1 && !self.has_inserted_date => {
126                match get_date(self, context) {
127                    Ok(date) => {
128                        self.has_inserted_date = true;
129                        // render heading
130                        tr.render(dom, context, parent_id, &vec![token.clone()]);
131                        let div = dom.add_element_with_attributes(
132                            parent_id,
133                            "div",
134                            to_attributes([("class", "post-updated-on")]),
135                        );
136                        dom.add_text(div, date);
137
138                        return true;
139                    }
140                    Err(e) => error!("failed to read date from post: {e}"),
141                }
142            }
143            _ => {}
144        }
145        return false;
146    }
147}
148
149#[derive(Overwrite)]
150pub struct PostOptions {
151    modified_on: Option<String>,
152}
153impl Default for PostOptions {
154    fn default() -> Self {
155        Self { modified_on: None }
156    }
157}
158
159/// get the date from input and options
160fn get_date(module: &mut BlogModule, context: &RenderContext) -> Result<String, LssgError> {
161    let po: PostOptions = module.options(context.page);
162
163    if let Some(date) = po.modified_on {
164        match DateTime::<Utc>::from_str(&date) {
165            Ok(date) => {
166                let date = date.format("Updated on %B %d, %Y").to_string();
167                return Ok(date);
168            }
169            Err(e) => warn!("could not parse modified_on to date: {e}"),
170        }
171    }
172
173    match context.input {
174        Some(Input::Local { path }) => {
175            let date: DateTime<Utc> = path.metadata()?.modified()?.into();
176            let date = date.format("Updated on %B %d, %Y").to_string();
177            Ok(date)
178        }
179        Some(Input::External { url }) => {
180            return Err(LssgError::render(
181                "getting modified date from url is not supported",
182            ))
183        }
184        None => return Err(LssgError::render("page does not have an Input")),
185    }
186}