Skip to main content

mdbook_cat_prep/
render.rs

1//! modul obsahující renderovací funkce tohoto preprocessoru
2//!
3//! V první fázi dojde ke zpracování cat kontextu na seznam
4//! typu [`Vec<RenderSite>`], který obsahuje renderovací příkazy
5//! a jejich cíle.
6//!
7//! Následně je seznam renderů aplikován na knihu.
8//! Vzhledem k tomu, že samotné vytváření renderů
9//! přidává do knihy několik článků, tak obě operace
10//! vyžadují mutabilní přístup ke knize.
11//!
12//! Tvorba renderů je zprostředkována pomocí traity
13//! [`Render`]. Všechny výchozí rendery využívají `tinytemplate`
14//! šablony. Pro `tinytemplate` je nejlepší, když
15//! je šablona statický strig, proto jsou zde všechny
16//! šablony prozatím 'nahardcodované' jako immutabilní
17//! globální stringy.
18//!
19//! V budoucnu by bylo možné využít makra `include_str!()`
20//! k extrakci těchto šablon do vnějších souborů.
21
22use std::fmt;
23use std::path::PathBuf;
24use std::convert::From;
25use std::collections::HashMap;
26
27use mdbook::{
28	BookItem,
29	book::{Book, Chapter},
30};
31use tinytemplate::TinyTemplate;
32use serde::{Serialize, Deserialize};
33
34use crate::cat_context::CatContext;
35use crate::error::CatError;
36use crate::models::*;
37
38/// typ daného renderu (a jeho obsah).
39/// Určuje chování, jakým bude zacházeno
40/// z obsahem článku
41#[derive(Debug, Clone)]
42pub enum RenderType {
43	/// Přidá vyrenderovaný obsah na začátek
44	Prepend(String),
45	/// Přidá vyrenderovaný obsah na konec
46	Append(String),
47	/// Přidá něco na začátek a něco na konec
48	Both(String, String),
49	/// Přepíše stárnku
50	EntirePage(String),
51}
52
53use RenderType::*;
54
55impl fmt::Display for RenderType {
56	/// je zapotřebí, aby nám vypisování RenderTypu v
57	/// chybách nekazilo výstup. Tato implementace
58	/// tedy usekne asociovaná data každé varianty
59	/// a vypíše pouze její název.
60	///
61	/// Pro vypsání nejen názvu, ale i obsahu použijte
62	/// debug formátování ("{:?}" nebo "{:#?}").
63	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64		match self {
65			Prepend(_) => write!(f, "Prepend"),
66			Append(_) => write!(f, "Append"),
67			Both(_, _) => write!(f, "Both"),
68			EntirePage(_) => write!(f, "EntirePage"),
69		}
70	}
71}
72
73/// Uchovává informace o daném renderu
74#[derive(Debug, Clone)]
75pub struct RenderSite {
76	/// soubor, ve kterým má být render proveden
77	pub site:   PathBuf,
78	/// typ a obsah renderu
79	pub render: RenderType,
80}
81
82impl RenderSite {
83	/// vytvoří nový render
84	pub fn new(site: PathBuf, render: RenderType) -> Self {
85		RenderSite { site, render }
86	}
87}
88
89/// Trait umožňující renderování struktury
90/// jako Markdown/HT?ML
91pub trait Render {
92	/// metoda pro renderování daného typu.
93	///
94	/// V případě, že renderování selže by měla
95	/// implementace vracet správný chybový typ
96	fn render(&self, context: &CatContext) -> Result<RenderSite, CatError>;
97}
98
99/// šablona karty učitele
100pub static TEACHER_TEMPLATE: &'static str = r#"
101<h2 id="{card.username}">{card.jmeno}</h2>
102
103- email: <a href="mailto:{card.email}">{card.email}</a>
104- username: {card.username}
105
106### Bio
107{card.bio}
108
109### Předměty
110{{ for p in subjects }} - [{p.card.nazev}](/{p.path})
111{{ endfor }}
112
113### Materiály
114{{ for a in articles }} - [{a.card.nazev}](/{a.path})
115{{ endfor }}
116<hr>
117"#;
118
119impl Render for Teacher {
120	fn render(&self, _: &CatContext) -> Result<RenderSite, CatError> {
121		let render_site = PathBuf::from("teachers.md");
122		let mut tt = TinyTemplate::new();
123
124		tt.add_template("teacher", TEACHER_TEMPLATE)
125			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
126		let res = tt
127			.render("teacher", &self)
128			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
129
130		dbg!("{}", &res);
131
132		Ok(RenderSite::new(render_site, Append(res)))
133	}
134}
135
136/// šablona pro seznam učitelů
137pub static TEACHER_LIST_TEMPLATE: &'static str = r#"
138{{ for t in list }} [{t.jmeno}](#{t.username}) {{ endfor }}
139"#;
140
141/// tato struktura existuje jako způsob obcházení limitací `tinytemplate`
142#[derive(Debug, Serialize, Clone)]
143pub struct TeacherList {
144    /// karty všech učitelů
145    pub list: Vec<TeacherCard>,
146}
147
148impl Render for TeacherList {
149	fn render(&self, _: &CatContext) -> Result<RenderSite, CatError> {
150		let render_site = PathBuf::from("teachers.md");
151		let mut tt = TinyTemplate::new();
152
153		tt.add_template("teacher", TEACHER_LIST_TEMPLATE)
154			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
155		let res = tt
156			.render("teacher", &self)
157			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
158
159		dbg!("{}", &res);
160
161		Ok(RenderSite::new(render_site, Append(res)))
162	}
163}
164
165/// šablona karty předmětu (část před obsahem)
166pub static SUBJECT_PRE_TEMPLATE: &'static str = r#"
167| Název | { card.nazev } |
168| ----- | -------------- |
169{{ if resolved_author }}| Zodpovědná osoba |  [{resolved_author.jmeno}](/teachers.md#{resolved_author.username}) | {{ else }}| Zodpovědná osoba | {card.zodpovedna_osoba} | {{ endif }}
170| Popis | { card.bio }   |
171"#;
172
173/// šablona seznamu materiálů v daném předmětu (část za obsahem)
174pub static SUBJECT_POST_TEMPLATE: &'static str = r#"
175### Seznam materiálů
176{{ for a in articles }} - [{a.card.nazev}](/{a.path})
177{{ endfor }}
178"#;
179
180impl Render for Subject {
181	fn render(&self, _: &CatContext) -> Result<RenderSite, CatError> {
182		let render_site = self.path.clone();
183		let mut tt = TinyTemplate::new();
184
185		tt.add_template("subject_pre", SUBJECT_PRE_TEMPLATE)
186			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
187		tt.add_template("subject_post", SUBJECT_POST_TEMPLATE)
188			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
189
190		let pre = tt
191			.render("subject_pre", &self)
192			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
193
194		let post = tt
195			.render("subject_post", &self)
196			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
197
198		dbg!("{}\n{}", &pre, &post);
199
200		Ok(RenderSite::new(render_site, Both(pre, post)))
201	}
202}
203
204/// šablona karty článku (část před obsahem)
205pub static ARTICLE_PRE_TEMPLATE: &'static str = r#"
206| Název | {card.nazev} |
207| ----- | ------------ |
208{{ if resolved_author }}| Autor |  [{resolved_author.jmeno}](/teachers.md#{resolved_author.username}) | {{ else }}| Autor | {author} | {{ endif }}
209{{ if modified_resolved }}| Naposledy upravil |  [{modified_resolved.jmeno}](/teachers.md#{modified_resolved.username}) | {{ else }}| Naposledy upravil | {modified_by} | {{ endif }}
210| Poslední změna | {last_modified} |
211| Předmět | [{subject_card.nazev}](/{subject_card._resolved_path}) |
212{{ if card.datum }}| Datum | {card.datum} |{{endif}}
213"#;
214
215/// čablona seznamu tagů u článku (část za obsahem)
216///
217/// tato šablona také embedduje Disqus za účelem zprostředkování
218/// komentářů.
219pub static ARTICLE_POST_TEMPLATE: &'static str = r#"
220#### Tagy
221{{ for tag in card.tagy}} [{tag}](/tags.md#{tag}) {{ endfor }}
222
223<div id="disqus_thread"></div>
224<script>var disqus_config = function () \{ this.page.url = window.location.href; this.page.identifier = window.location.href; }; (function() \{ var d = document, s = d.createElement('script'); s.src = 'https://gjk-cat.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })(); </script>
225<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
226"#;
227
228impl Render for Article {
229	fn render(&self, _: &CatContext) -> Result<RenderSite, CatError> {
230		let render_site = self.path.clone();
231		let mut tt = TinyTemplate::new();
232
233		tt.add_template("article_pre", ARTICLE_PRE_TEMPLATE)
234			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
235		tt.add_template("article_post", ARTICLE_POST_TEMPLATE)
236			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
237
238		let pre = tt
239			.render("article_pre", &self)
240			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
241
242		let post = tt
243			.render("article_post", &self)
244			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
245
246		dbg!("{}\n{}", &pre, &post);
247
248		Ok(RenderSite::new(render_site, Both(pre, post)))
249	}
250}
251
252/// struktura obsahující pár tag - články
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct Tag {
255	/// samotný tag jako string
256	pub name:     String,
257	/// seznam článků s tímto tagem
258	pub articles: Vec<ArticleCard>,
259}
260
261/// tagový kontext pro `tinytemplate` šablonu
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct TagContext {
264	/// vektor obsahující prvky typu [`Tag`]
265	pub tags: Vec<Tag>,
266}
267
268/// konverze z tagové hasmapy na šablonový kontext
269impl From<&HashMap<String, Vec<ArticleCard>>> for TagContext {
270	fn from(src: &HashMap<String, Vec<ArticleCard>>) -> Self {
271		let mut tags =
272			src.iter().map(|(k, v)| (k.clone(), v.clone())).collect::<Vec<_>>();
273		tags.sort_by(|a, b| a.0.cmp(&b.0));
274
275		Self {
276			tags: tags
277				.into_iter()
278				.map(|(k, v)| Tag { name: k, articles: v })
279				.collect::<Vec<_>>(),
280		}
281	}
282}
283
284/// šablona pro stránku se seznamem tagů a asociovaných článků
285pub static TAGS_TEMPLATE: &'static str = r#"
286# Tagy
287{{ for tag in tags }} [{tag.name}](#{tag.name}) {{ endfor }}
288
289{{ for tag in tags }}
290<h3 id="{tag.name}">{tag.name}</h3>
291{{ for a in tag.articles }}
292 - [{a.nazev}](/{a._resolved_path}){{ endfor }}
293{{ endfor }}
294"#;
295
296impl Render for TagContext {
297	fn render(&self, _: &CatContext) -> Result<RenderSite, CatError> {
298		let render_site = PathBuf::from("tags.md");
299		let mut tt = TinyTemplate::new();
300
301		tt.add_template("tags", TAGS_TEMPLATE)
302			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
303		let res = tt
304			.render("tags", &self)
305			.map_err(|e| CatError::TinyError { error: e.to_string() })?;
306
307		dbg!("{}", &res);
308
309		Ok(RenderSite::new(render_site, EntirePage(res)))
310	}
311}
312
313/// vytvoří rendery z objektů
314///
315/// zároveň založí stránky `teachers.md`
316/// a `tags.md`.
317pub fn create_renders(
318	context: &CatContext,
319	book: &mut Book,
320) -> Result<Vec<RenderSite>, CatError> {
321	let mut pending_renders: Vec<RenderSite> = vec![];
322	let mut errors: Vec<CatError> = vec![];
323
324	match (TeacherList { list: context.teacher_cards.clone() }).render(context) {
325    	Ok(r) => pending_renders.push(r),
326    	Err(e) => return Err(e),
327	}
328
329	context.teachers.iter().for_each(|t| match t.render(context) {
330		Ok(r) => pending_renders.push(r),
331		Err(e) => errors.push(e),
332	});
333
334	if !errors.is_empty() {
335		errors.iter().for_each(|x| eprintln!("[cat-prep] {}", x));
336
337		return Err(errors[0].clone());
338	}
339
340	context.subjects.iter().for_each(|t| match t.render(context) {
341		Ok(r) => pending_renders.push(r),
342		Err(e) => errors.push(e),
343	});
344
345	if !errors.is_empty() {
346		errors.iter().for_each(|x| eprintln!("[cat-prep] {}", x));
347
348		return Err(errors[0].clone());
349	}
350
351	context.articles.iter().for_each(|t| match t.render(context) {
352		Ok(r) => pending_renders.push(r),
353		Err(e) => errors.push(e),
354	});
355
356	if !errors.is_empty() {
357		errors.iter().for_each(|x| eprintln!("[cat-prep] {}", x));
358
359		return Err(errors[0].clone());
360	}
361
362	match TagContext::from(&context.tags).render(context) {
363		Ok(r) => pending_renders.push(r),
364		Err(e) => return Err(e),
365	}
366
367
368	if !context.teacher_cards.is_empty() {
369    	book.push_item(BookItem::Chapter(Chapter::new(
370    		"Vyučující",
371    		"# Vyučující\n".to_string(),
372    		"teachers.md".to_string(),
373    		vec![],
374    	)));
375	}
376
377	if !context.tags.is_empty() {
378    	book.push_item(BookItem::Chapter(Chapter::new(
379    		"Tagy",
380    		"".to_string(),
381    		"tags.md".to_string(),
382    		vec![],
383    	)));
384	}
385
386	dbg!("[cat prep] prerender: {:#?}", &book);
387
388	Ok(pending_renders)
389}
390
391/// spustí dané Rendery na knize
392///
393/// jelikož nevyužitý render pravdepodobně znamená chybnou syntaxi,
394/// vrací chybu v případě nevyužitých renderů.
395pub fn execute_renders(
396	mut pending_renders: Vec<RenderSite>,
397	book: &mut Book,
398) -> Result<(), CatError> {
399	book.for_each_mut(|c| {
400		if let BookItem::Chapter(c) = c {
401			let path = c.path.clone();
402
403			pending_renders.iter().filter(|x| x.site == path.clone().unwrap()).for_each(|x| {
404				match &x.render {
405					Prepend(s) => c.content = format!("{}\n{}", c.content, s),
406					Both(pre, post) =>
407						c.content = format!("{}\n{}\n{}", pre, c.content, post),
408					Append(s) => c.content = format!("{}\n{}", c.content, s),
409					EntirePage(s) => c.content = s.clone(),
410				}
411			});
412
413			pending_renders.retain(|x| x.site != c.path.clone().unwrap());
414		}
415	});
416
417	if !pending_renders.is_empty() {
418		for RenderSite { site, render } in &pending_renders {
419			println!("[cat-prep] error: oprhan render: {} at {}", render, site.display());
420		}
421		return Err(CatError::OrphanRender {
422			site:   pending_renders[0].site.display().to_string(),
423			render: pending_renders[0].render.clone(),
424		});
425	}
426
427	Ok(())
428}