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 pub title: String,
31 pub meta: HashMap<String, String>,
33 pub language: String,
35}
36impl Default for PropegatedOptions {
37 fn default() -> Self {
38 Self {
39 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
84pub struct DefaultModule {
86 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 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 for id in pages {
119 site_tree.add_link(id, default_stylesheet);
120
121 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 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 if let Some(parent) = site_tree.page_parent(id) {
150 if let Some(parent_set) = relation_map.get(&parent) {
151 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 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 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 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 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 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 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}