1use std::fs;
2use std::path::{Path, PathBuf};
3
4use handlebars::Handlebars;
5
6use log::{debug, info};
7
8use crate::error::GeneratorError;
9use crate::loader::directory::DirectoryLoader;
10use crate::loader::Loader;
11use crate::rules::{Asset, Render, Rule};
12
13use serde::{Deserialize, Serialize};
14use serde_json::{Map, Value};
15
16use std::fs::File;
17
18type Result<T> = std::result::Result<T, GeneratorError>;
19
20use crate::helper::basic::{ConcatHelper, DumpHelper, ExpandHelper, TimesHelper};
21use crate::helper::markdown::MarkdownifyHelper;
22
23use crate::copy;
24use crate::helper::time::TimeHelper;
25use crate::helper::url::{full_url_for, AbsoluteUrlHelper, ActiveHelper, RelativeUrlHelper};
26use relative_path::RelativePath;
27
28use crate::processor::{Processor, ProcessorSession};
29
30use crate::helper::sort::SortedHelper;
31use crate::processor::rss::RssProcessor;
32use crate::processor::sitemap::SitemapProcessor;
33use lazy_static::lazy_static;
34use regex::Regex;
35use std::collections::BTreeMap;
36use std::sync::{Arc, RwLock};
37
38use std::str::FromStr;
39use url::Url;
40
41lazy_static! {
42 static ref RE: Regex = Regex::new(r"/{2,}").unwrap();
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct Output {
48 pub path: String,
50 pub site_url: String,
52 pub template: Option<String>,
54 pub url: String,
56}
57
58impl Output {
59 pub fn new<S1, S2, S3>(site_url: S1, path: S2, template: Option<S3>) -> Result<Self>
60 where
61 S1: Into<String>,
62 S2: Into<String>,
63 S3: Into<String>,
64 {
65 let mut site_url_str = site_url.into();
66 if !site_url_str.ends_with('/') {
67 site_url_str.push('/');
68 }
69 let site_url = Url::from_str(&site_url_str)?;
70 let path = normalize_path(path.into());
71 let mut url = full_url_for(&site_url, &path)?;
72
73 if url.path().ends_with("/index.html") {
75 url.path_segments_mut()
76 .map_err(|_| GeneratorError::Error("Unable to parse path".into()))?
77 .pop()
78 .push("");
79 }
80
81 Ok(Output {
82 path,
83 url: url.into_string(),
84 site_url: site_url_str,
85 template: template.map(|s| s.into()),
86 })
87 }
88}
89
90#[derive(Debug, Clone)]
91pub struct GeneratorConfig {
92 pub root: PathBuf,
93 pub output: PathBuf,
94 pub basename: Url,
95}
96
97#[derive(Debug, Clone)]
98pub struct GeneratorContext {
99 pub config: GeneratorConfig,
100 pub output: Output,
101}
102
103impl GeneratorContext {
104 pub fn new(config: &GeneratorConfig, output: &Output) -> Self {
105 GeneratorContext {
106 config: config.clone(),
107 output: output.clone(),
108 }
109 }
110}
111
112pub struct GeneratorBuilder {
113 root: PathBuf,
114 basename_override: Option<String>,
115 dump: bool,
116}
117
118impl GeneratorBuilder {
119 pub fn new<P: Into<PathBuf>>(root: P) -> Self {
120 return GeneratorBuilder {
121 root: root.into(),
122 basename_override: None,
123 dump: false,
124 };
125 }
126
127 pub fn dump(mut self, dump: bool) -> Self {
128 self.dump = dump;
129 self
130 }
131
132 pub fn override_basename<S: Into<String>>(mut self, basename: Option<S>) -> Self {
133 self.basename_override = basename.map(|s| s.into());
134 self
135 }
136
137 pub fn build<'b>(self) -> Generator<'b> {
138 Generator::new(&self.root, self.basename_override, self.dump)
139 }
140}
141
142pub struct Generator<'a> {
143 root: PathBuf,
144 basename_override: Option<String>,
145 dump: bool,
146
147 handlebars: Handlebars<'a>,
148
149 processors: BTreeMap<String, Box<dyn Processor>>,
150
151 config: Option<Render>,
152 full_content: Value,
153 compact_content: Value,
154
155 context_provider: Arc<RwLock<Option<GeneratorContext>>>,
156}
157
158impl<'a> Generator<'a> {
159 fn output(&self) -> PathBuf {
160 self.root.join("output")
161 }
162
163 pub(crate) fn new(root: &Path, basename_override: Option<String>, dump: bool) -> Self {
164 let mut handlebars = Handlebars::new();
167 handlebars.set_strict_mode(true);
168
169 let mut processors: BTreeMap<String, Box<dyn Processor>> = BTreeMap::new();
172 processors.insert("sitemap".into(), Box::new(SitemapProcessor));
173 processors.insert("rss".into(), Box::new(RssProcessor));
174
175 let root = PathBuf::from(&root);
178
179 let context_provider = Arc::new(RwLock::new(None));
182
183 handlebars.register_helper("dump", Box::new(DumpHelper));
186
187 handlebars.register_helper("times", Box::new(TimesHelper));
188 handlebars.register_helper("expand", Box::new(ExpandHelper));
189 handlebars.register_helper("concat", Box::new(ConcatHelper));
190
191 handlebars.register_helper("sorted", Box::new(SortedHelper));
192
193 handlebars.register_helper("markdownify", Box::new(MarkdownifyHelper));
194
195 handlebars.register_helper("timestamp", Box::new(TimeHelper));
196
197 handlebars.register_helper(
198 "absolute_url",
199 Box::new(AbsoluteUrlHelper {
200 context: context_provider.clone(),
201 }),
202 );
203 handlebars.register_helper(
204 "relative_url",
205 Box::new(RelativeUrlHelper {
206 context: context_provider.clone(),
207 }),
208 );
209 handlebars.register_helper(
210 "active",
211 Box::new(ActiveHelper {
212 context: context_provider.clone(),
213 }),
214 );
215
216 Generator {
219 root,
220 basename_override,
221 dump,
222
223 handlebars,
224
225 processors,
226
227 config: Default::default(),
228 full_content: Default::default(),
229 compact_content: Default::default(),
230 context_provider: context_provider.clone(),
231 }
232 }
233
234 pub fn run(&mut self) -> Result<()> {
235 debug!("Running generator");
236
237 let templates = self.root.join("templates");
238 info!("Loading templates: {:?}", templates);
239 self.handlebars
240 .register_templates_directory(".hbs", templates)?;
241
242 self.clean()?;
244
245 self.load_content()?;
247
248 self.load_config()?;
250
251 self.build()?;
253
254 Ok(())
256 }
257
258 fn load_config(&mut self) -> Result<()> {
259 let path = self.root.join("hagen.yaml");
260 info!("Loading configuration: {:?}", path);
261 self.config = Some(Render::load_from(path)?);
262
263 Ok(())
264 }
265
266 fn load_content(&mut self) -> Result<()> {
267 let content = self.root.join("content");
268
269 info!("Loading content: {:?}", content);
270
271 let content = DirectoryLoader::new(&content, &content).load_from()?;
273
274 self.full_content = content.to_value()?;
276 self.compact_content = Generator::compact_content(&self.full_content).unwrap_or_default();
277
278 if self.dump {
279 info!("Dumping content");
281 let writer = File::create(self.output().join("content.yaml"))?;
282 serde_yaml::to_writer(writer, &self.full_content)?;
283 let writer = File::create(self.output().join("compact.yaml"))?;
284 serde_yaml::to_writer(writer, &self.compact_content)?;
285 }
286
287 Ok(())
289 }
290
291 fn compact_content(v: &Value) -> Option<Value> {
293 match v {
294 Value::Object(m) => match m.get("content") {
295 Some(Value::Object(mc)) => {
296 let mut result = Map::new();
297 for (k, v) in mc {
298 if let Some(x) = Generator::compact_content(v) {
299 result.insert(k.clone(), x);
300 }
301 }
302 Some(Value::Object(result))
303 }
304 Some(x) => Some(x.clone()),
305 _ => None,
306 },
307 _ => Some(v.clone()),
308 }
309 }
310
311 fn build(&mut self) -> Result<()> {
312 let config = self
313 .config
314 .as_ref()
315 .ok_or(GeneratorError::Error("Missing site configuration".into()))?
316 .clone();
317
318 let mut basename = self
319 .basename_override
320 .as_ref()
321 .unwrap_or(&config.site.basename)
322 .to_owned();
323
324 if !basename.ends_with('/') {
325 basename.push('/');
326 }
327
328 let basename = Url::from_str(&basename)?;
329
330 let generator_config = GeneratorConfig {
332 basename: basename.clone(),
333 root: self.root.clone(),
334 output: self.output(),
335 };
336
337 let data = self.data(None, None);
338 let mut processors = ProcessorSession::new(
339 &self.processors,
340 &mut self.handlebars,
341 &data,
342 &generator_config,
343 &config.processors,
344 )?;
345
346 info!("Rendering content");
348 for rule in &config.rules {
349 self.render_rule(&rule, &mut processors, &generator_config)?;
350 }
351
352 info!("Processing assets");
354 for a in &config.assets {
355 self.process_asset(a)?;
356 }
357
358 processors.complete(&mut self.handlebars)?;
359
360 info!("Done");
361 Ok(())
363 }
364
365 fn process_asset(&self, asset: &Asset) -> Result<()> {
366 let from = self.root.join(&asset.dir);
367
368 let mut target = self.root.join("output");
369 if let Some(ref to) = asset.to {
370 target = target.join(to);
371 }
372
373 info!("Copying assets: {:?} -> {:?}", &from, &target);
374
375 fs::create_dir_all(&target)?;
376
377 copy::copy_dir(&from, &target, asset.glob.as_ref())?;
378
379 Ok(())
380 }
381
382 fn render_rule(
383 &mut self,
384 rule: &Rule,
385 processors: &mut ProcessorSession,
386 config: &GeneratorConfig,
387 ) -> Result<()> {
388 info!(
389 "Render rule: {:?}:{:?} -> {:?} -> {}",
390 rule.selector_type, rule.selector, rule.template, rule.output_pattern
391 );
392
393 let result: Vec<_> = rule.processor()?.query(&self.full_content)?;
394 let result: Vec<Value> = result.iter().cloned().cloned().collect();
395
396 info!("Matches {} entries", result.len());
397
398 for entry in &result {
400 debug!("Processing entry: {}", entry);
401 self.process_render(rule, entry, processors, config)?;
402 }
403
404 Ok(())
406 }
407
408 fn process_render(
409 &mut self,
410 rule: &Rule,
411 context: &Value,
412 processors: &mut ProcessorSession,
413 config: &GeneratorConfig,
414 ) -> Result<()> {
415 let path = self
417 .handlebars
418 .render_template(&rule.output_pattern, context)?;
419 let path = normalize_path(path);
420 let template = rule
421 .template
422 .as_ref()
423 .map(|t| self.handlebars.render_template(&t, context))
424 .transpose()?;
425
426 let relative_target = RelativePath::new(&path);
427 let target = relative_target.to_path(self.output());
428
429 if let Some(parent) = target.parent() {
430 fs::create_dir_all(parent)?;
431 }
432
433 let output = Output::new(config.basename.as_str(), &path, template.as_ref())?;
436
437 {
438 let ctx = GeneratorContext::new(config, &output);
439 {
440 *self.context_provider.write().unwrap() = Some(ctx);
441 }
442 let output_value = serde_json::to_value(&output)?;
443
444 info!("Render '{}' with '{:?}'", path, template);
447 info!(" Target: {:?}", target);
448
449 let writer = File::create(target)?;
450
451 let context = Generator::build_context(&rule, &context)?;
452 let data = &self.data(Some(output_value), Some(context.clone()));
453
454 match template {
455 Some(ref t) => self.handlebars.render_to_write(t, data, writer)?,
456 None => {
457 let content = match &context.as_object().and_then(|s| s.get("content")) {
458 Some(Value::String(c)) => Ok(c),
459 _ => Err(GeneratorError::Error("Rule is missing 'template' on rule and '.content' value in context. Either must be set.".into())),
460 }?;
461 self.handlebars
462 .render_template_to_write(content, data, writer)?;
463 }
464 }
465
466 processors.file_created(&output, data, &mut self.handlebars)?;
468
469 {
471 *self.context_provider.write().unwrap() = None;
472 }
473 }
474
475 Ok(())
477 }
478
479 fn build_context(rule: &Rule, context: &Value) -> Result<Value> {
481 if rule.context.is_empty() {
482 return Ok(context.clone());
483 }
484
485 let mut result = Map::new();
486
487 for (k, v) in &rule.context {
488 match v {
489 Value::String(path) => {
490 let obj = jsonpath_lib::select(context, &path)?;
491 let obj = obj.as_slice();
492 let value = match obj {
493 [] => None,
494 [x] => Some((*x).clone()),
495 obj => Some(Value::Array(obj.iter().cloned().cloned().collect())),
496 };
497 debug!("Mapped context - name: {:?} = {:?}", k, value);
498 if let Some(value) = value {
499 result.insert(k.into(), value);
500 }
501 }
502 _ => {
503 return Err(GeneratorError::Error(
504 "Context value must be a string/JSON path".into(),
505 ));
506 }
507 }
508 }
509
510 Ok(Value::Object(result))
511 }
512
513 fn data(&self, output: Option<Value>, context: Option<Value>) -> Value {
514 let mut data = serde_json::value::Map::new();
515
516 if let Some(output) = output {
518 data.insert("output".into(), output);
519 }
520 if let Some(context) = context {
521 data.insert("context".into(), context);
522 }
523 data.insert("full".into(), self.full_content.clone());
525 data.insert("compact".into(), self.compact_content.clone());
527
528 serde_json::value::Value::Object(data)
530 }
531
532 pub fn clean(&self) -> Result<()> {
533 let p = self.output();
534 let p = p.as_path();
535
536 if p.exists() {
537 info!("Cleaning up: {:?}", self.output());
538 fs::remove_dir_all(self.output().as_path())?;
539 }
540
541 fs::create_dir_all(p)?;
542
543 Ok(())
544 }
545}
546
547fn normalize_path<S: AsRef<str>>(path: S) -> String {
549 let s = path.as_ref().replace('\\', "/");
551
552 let s = RE.replace_all(&s, "/");
554
555 s.trim_start_matches('/').into()
556}
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 #[test]
563 fn test_1() {
564 assert_eq!(normalize_path(""), "");
565 }
566
567 #[test]
568 fn test_backslash() {
569 assert_eq!(normalize_path("\\foo/bar/baz"), "foo/bar/baz");
570 }
571
572 #[test]
573 fn test_double() {
574 assert_eq!(normalize_path("//foo/bar/baz"), "foo/bar/baz");
575 }
576
577 #[test]
578 fn test_double_2() {
579 assert_eq!(normalize_path("//foo////bar/baz"), "foo/bar/baz");
580 }
581
582 #[test]
583 fn test_double_back() {
584 assert_eq!(normalize_path("\\\\foo/bar/baz"), "foo/bar/baz");
585 }
586
587 #[test]
588 fn test_double_back_2() {
589 assert_eq!(normalize_path("\\\\foo//bar/baz"), "foo/bar/baz");
590 }
591}