lssg_lib/renderer/modules/
blog_module.rs1use 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 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 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 self.has_inserted_date = false;
80
81 let body = dom.get_elements_by_tag_name("body")[0];
83
84 {
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 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
159fn 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}