lssg_lib/renderer/modules/
default_module.rs

1use std::collections::{HashMap, HashSet};
2
3use lazy_static::lazy_static;
4use log::warn;
5
6use serde_extensions::Overwrite;
7
8use crate::{
9    html,
10    html::{to_attributes, DomNodeKind, Html},
11    lmarkdown::Token,
12    lssg_error::LssgError,
13    sitetree::{Page, Relation, SiteNode, SiteNodeKind, SiteTree, Stylesheet},
14    tree::{Node, DFS},
15};
16
17use crate::renderer::{RenderContext, RendererModule, TokenRenderer};
18
19const DEFAULT_STYLESHEET: &[u8] = include_bytes!("./default_stylesheet.css");
20
21lazy_static! {
22    static ref WATERMARK: Html = html!(
23        r#"<footer id="watermark">Generated by <a href="https://github.com/lyr-7D1h/lssg">LSSG</a></footer>"#
24    );
25}
26
27#[derive(Debug, Clone, Overwrite)]
28struct PropegatedOptions {
29    /// Add extra resources
30    pub title: String,
31    /// Translates to meta tags <https://www.w3schools.com/tags/tag_meta.asp>
32    pub meta: HashMap<String, String>,
33    /// Lang attribute ("en") <https://www.w3schools.com/tags/ref_language_codes.asp>
34    pub language: String,
35}
36impl Default for PropegatedOptions {
37    fn default() -> Self {
38        Self {
39            // favicon: None,
40            // stylesheets: vec![],
41            meta: HashMap::new(),
42            title: String::new(),
43            language: "en".into(),
44        }
45    }
46}
47
48#[derive(Debug, Clone, Overwrite)]
49pub struct SinglePageOptions {
50    pub disable_parent_resources: bool,
51}
52impl Default for SinglePageOptions {
53    fn default() -> Self {
54        Self {
55            disable_parent_resources: false,
56        }
57    }
58}
59
60fn create_options_map(
61    module: &DefaultModule,
62    site_tree: &SiteTree,
63) -> Result<HashMap<usize, PropegatedOptions>, LssgError> {
64    let mut options_map: HashMap<usize, PropegatedOptions> = HashMap::new();
65    for id in DFS::new(site_tree) {
66        if let SiteNodeKind::Page(page) = &site_tree[id].kind {
67            if let Some(parent) = site_tree.page_parent(id) {
68                if let Some(parent_options) = options_map.get(&parent) {
69                    let parent_options = parent_options.clone();
70                    let options: PropegatedOptions =
71                        module.options_with_default(page, parent_options);
72                    options_map.insert(id, options.clone());
73                    continue;
74                }
75            }
76
77            let options: PropegatedOptions = module.options(page);
78            options_map.insert(id, options.clone());
79        }
80    }
81    Ok(options_map)
82}
83
84/// Implements all basic default behavior, like rendering all tokens and adding meta tags and title to head
85pub struct DefaultModule {
86    /// Map of all site pages to options
87    options_map: HashMap<usize, PropegatedOptions>,
88}
89
90impl DefaultModule {
91    pub fn new() -> Self {
92        Self {
93            options_map: HashMap::new(),
94        }
95    }
96}
97
98impl RendererModule for DefaultModule {
99    fn id(&self) -> &'static str {
100        return "default";
101    }
102
103    /// Add all resources from ResourceOptions to SiteTree
104    fn init(&mut self, site_tree: &mut SiteTree) -> Result<(), LssgError> {
105        let mut relation_map = HashMap::new();
106
107        let pages: Vec<usize> = DFS::new(site_tree)
108            .filter(|id| site_tree[*id].kind.is_page())
109            .collect();
110
111        let default_stylesheet = site_tree.add(SiteNode::stylesheet(
112            "default.css",
113            site_tree.root(),
114            Stylesheet::from_readable(DEFAULT_STYLESHEET)?,
115        ))?;
116
117        // propegate relations to stylesheets and favicon from parent to child
118        for id in pages {
119            site_tree.add_link(id, default_stylesheet);
120
121            // skip page if disabled
122            if let SiteNodeKind::Page(page) = &site_tree[id].kind {
123                let opts: SinglePageOptions = self.options(page);
124                if opts.disable_parent_resources {
125                    continue;
126                }
127            }
128
129            // get the set of links to favicon and stylesheets
130            let mut set: HashSet<usize> = site_tree
131                .links_from(id)
132                .into_iter()
133                .filter_map(|link| match link.relation {
134                    Relation::External | Relation::Discovered { .. } => {
135                        let node = &site_tree[link.to];
136                        match node.kind {
137                            SiteNodeKind::Stylesheet { .. } => Some(link.to),
138                            SiteNodeKind::Resource { .. } if node.name == "favicon.ico" => {
139                                Some(link.to)
140                            }
141                            _ => None,
142                        }
143                    }
144                    _ => None,
145                })
146                .collect();
147
148            // update set with parent and add any links from parent
149            if let Some(parent) = site_tree.page_parent(id) {
150                if let Some(parent_set) = relation_map.get(&parent) {
151                    // add links from parent_set without the ones it already has
152                    for to in (parent_set - &set).iter() {
153                        site_tree.add_link(id, *to);
154                    }
155                    set = set.union(parent_set).cloned().collect();
156                }
157            }
158            relation_map.insert(id, set);
159        }
160
161        Ok(())
162    }
163
164    fn after_init(&mut self, site_tree: &SiteTree) -> Result<(), LssgError> {
165        // save options map after site tree has been created to get all pages
166        self.options_map = create_options_map(&self, site_tree)?;
167        Ok(())
168    }
169
170    fn render_page<'n>(&mut self, dom: &mut crate::html::DomTree, context: &RenderContext<'n>) {
171        let site_id = context.site_id;
172        let site_tree = context.site_tree;
173
174        let options = self
175            .options_map
176            .get(&site_id)
177            .expect("expected options map to contain all page ids");
178
179        // Add language to html tag
180        let html = dom.get_elements_by_tag_name("html")[0];
181        if let DomNodeKind::Element { attributes, .. } = &mut dom.get_mut(html).kind {
182            attributes.insert("lang".to_owned(), options.language.clone());
183        }
184
185        // fill head
186        let head = dom.get_elements_by_tag_name("head")[0];
187
188        let title = dom.add_element(head, "title");
189        dom.add_text(title, options.title.clone());
190
191        for link in site_tree.links_from(site_id) {
192            match link.relation {
193                Relation::External | Relation::Discovered { .. } => match site_tree[link.to].kind {
194                    SiteNodeKind::Resource { .. } if site_tree[link.to].name == "favicon.ico" => {
195                        dom.add_element_with_attributes(
196                            head,
197                            "link",
198                            to_attributes([
199                                ("rel", "icon"),
200                                ("type", "image/x-icon"),
201                                ("href", &site_tree.path(link.to)),
202                            ]),
203                        );
204                    }
205                    SiteNodeKind::Stylesheet { .. } => {
206                        dom.add_element_with_attributes(
207                            head,
208                            "link",
209                            to_attributes([
210                                ("rel", "stylesheet"),
211                                ("href", &site_tree.path(link.to)),
212                            ]),
213                        );
214                    }
215                    _ => {}
216                },
217                _ => {}
218            }
219        }
220        dom.add_element_with_attributes(
221            head,
222            "meta",
223            to_attributes([
224                ("name", "viewport"),
225                ("content", r#"width=device-width, initial-scale=1"#),
226            ]),
227        );
228        dom.add_element_with_attributes(head, "meta", to_attributes([("charset", "utf-8")]));
229        for (key, value) in &options.meta {
230            dom.add_element_with_attributes(
231                head,
232                "meta",
233                to_attributes([("name", key), ("content", value)]),
234            );
235        }
236    }
237
238    fn render_body<'n>(
239        &mut self,
240        dom: &mut crate::html::DomTree,
241        context: &super::RenderContext<'n>,
242        parent_id: usize,
243        token: &crate::lmarkdown::Token,
244        tr: &mut TokenRenderer,
245    ) -> bool {
246        match token {
247            Token::Attributes { .. } | Token::Comment { .. } | Token::EOF | Token::Space=> {}
248            Token::Break { raw: _ } => {
249                dom.add_element(parent_id, "br");
250            }
251            Token::Heading { depth, tokens } => {
252                let parent_id = dom.add_element(parent_id, format!("h{depth}"));
253                tr.render(dom, context, parent_id, tokens);
254            }
255            Token::Paragraph { tokens } => {
256                let parent_id = dom.add_element(parent_id, "p");
257                tr.render(dom, context, parent_id, tokens);
258            }
259            Token::Bold { text } => {
260                let parent_id = dom.add_element(parent_id, "b");
261                dom.add_text(parent_id, text);
262            }
263            Token::Italic { text } => {
264                let parent_id = dom.add_element(parent_id, "i");
265                dom.add_text(parent_id, text);
266            }
267            Token::Code { code, language: _ } => {
268                let parent_id = dom.add_element(parent_id, "code");
269                dom.add_text(parent_id, code);
270            }
271            Token::Link { tokens: text, href } => {
272                if text.len() == 0 {
273                    return true;
274                }
275
276                // external link
277                if is_href_external(href) {
278                    let a = dom.add_element_with_attributes(
279                        parent_id,
280                        "a",
281                        to_attributes([("href", href)]),
282                    );
283
284                    tr.render(dom, context, a, text);
285
286                    dom.add_html(parent_id, html!(r##"<svg width="1em" height="1em" viewBox="0 0 24 24" style="cursor:pointer"><g stroke-width="2.1" stroke="#666" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 13.5 17 19.5 5 19.5 5 7.5 11 7.5"></polyline><path d="M14,4.5 L20,4.5 L20,10.5 M20,4.5 L11,13.5"></path></g></svg>"##));
287                    return true;
288                }
289
290                if Page::is_href_to_page(href) {
291                    let to_id = context
292                        .site_tree
293                        .links_from(context.site_id)
294                        .into_iter()
295                        .find_map(|l| {
296                            if let Relation::Discovered { raw_path: path } = &l.relation {
297                                if path == href {
298                                    return Some(l.to);
299                                }
300                            }
301                            None
302                        });
303                    if let Some(to_id) = to_id {
304                        let rel_path = context.site_tree.path(to_id);
305                        let parent_id = dom.add_element_with_attributes(
306                            parent_id,
307                            "a",
308                            to_attributes([("href", rel_path)]),
309                        );
310                        tr.render(dom, context, parent_id, text);
311                        return true;
312                    }
313                    warn!("Could not find node where {href:?} points to");
314                }
315
316                let parent_id = dom.add_element_with_attributes(
317                    parent_id,
318                    "a",
319                    to_attributes([("href", href)]),
320                );
321                tr.render(dom, context, parent_id, text);
322            }
323            Token::Text { text } => {
324                dom.add_text(parent_id, text);
325            }
326            Token::Html {
327                tag,
328                attributes,
329                tokens,
330            } => match tag.as_str() {
331                "links" if attributes.contains_key("boxes") => {
332                    let parent_id = dom
333                        .add_html(parent_id, html!(r#"<nav class="links"></nav>"#))
334                        .unwrap();
335                    for t in tokens {
336                        match t {
337                            Token::Link { tokens, href } => {
338                                let href = if Page::is_href_to_page(href) {
339                                    let to_id = context
340                                        .site_tree
341                                        .links_from(context.site_id)
342                                        .into_iter()
343                                        .find_map(|l| {
344                                            if let Relation::Discovered { raw_path: path } =
345                                                &l.relation
346                                            {
347                                                if path == href {
348                                                    return Some(l.to);
349                                                }
350                                            }
351                                            None
352                                        });
353
354                                    match to_id {
355                                        Some(to_id) => context.site_tree.path(to_id),
356                                        None => {
357                                            warn!("Could not find node where {href:?} points to");
358                                            return true;
359                                        }
360                                    }
361                                } else {
362                                    href.into()
363                                };
364
365                                let a = dom
366                                    .add_html(
367                                        parent_id,
368                                        html!(r#"<a href="{href}"><div class="box"></div></a>"#),
369                                    )
370                                    .unwrap();
371                                let div = dom[a].children()[0];
372                                tr.render(dom, context, div, tokens);
373                            }
374                            _ => {}
375                        }
376                    }
377                }
378                _ => {
379                    let parent_id =
380                        dom.add_element_with_attributes(parent_id, tag, attributes.clone());
381                    tr.render(dom, context, parent_id, tokens);
382                }
383            },
384        };
385        true
386    }
387
388    fn after_render<'n>(&mut self, dom: &mut crate::html::DomTree, _: &RenderContext<'n>) {
389        let body = dom.body();
390        // move all dom elements to under #content
391        let children = dom[body].children().clone();
392        let content =
393            dom.add_element_with_attributes(body, "div", to_attributes([("id", "content")]));
394        for child in children.into_iter() {
395            dom.set_parent(child, content);
396        }
397
398        // add watermark
399        dom.add_html(body, WATERMARK.clone());
400    }
401}
402
403pub fn is_href_external(href: &str) -> bool {
404    return href.starts_with("http") || href.starts_with("mailto:");
405}