hagen_core/
generator.rs

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    // path of the output file
49    pub path: String,
50    // the site base name
51    pub site_url: String,
52    // the name of template
53    pub template: Option<String>,
54    // the output URL
55    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        // remove last element "index.html"
74        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        // create instance
165
166        let mut handlebars = Handlebars::new();
167        handlebars.set_strict_mode(true);
168
169        // register processors
170
171        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        // eval root
176
177        let root = PathBuf::from(&root);
178
179        // context
180
181        let context_provider = Arc::new(RwLock::new(None));
182
183        // register helpers
184
185        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        // create generator
217
218        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        // clean output
243        self.clean()?;
244
245        // load data
246        self.load_content()?;
247
248        // load config
249        self.load_config()?;
250
251        // build
252        self.build()?;
253
254        // done
255        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        // load content
272        let content = DirectoryLoader::new(&content, &content).load_from()?;
273
274        // convert to value
275        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            // dump content
280            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        // done
288        Ok(())
289    }
290
291    // Compact the content tree to contain only "content" sections.
292    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        // context
331        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        // render all rules
347        info!("Rendering content");
348        for rule in &config.rules {
349            self.render_rule(&rule, &mut processors, &generator_config)?;
350        }
351
352        // process assets
353        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        // done
362        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        // process selected entries
399        for entry in &result {
400            debug!("Processing entry: {}", entry);
401            self.process_render(rule, entry, processors, config)?;
402        }
403
404        // done
405        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        // eval
416        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        // page data
434
435        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            // render
445
446            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            // call processors
467            processors.file_created(&output, data, &mut self.handlebars)?;
468
469            // reset current context
470            {
471                *self.context_provider.write().unwrap() = None;
472            }
473        }
474
475        // done
476        Ok(())
477    }
478
479    /// Build the render content context object from the rules context mappings
480    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        // add the output context
517        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        // add the full content tree
524        data.insert("full".into(), self.full_content.clone());
525        // add the compact content tree
526        data.insert("compact".into(), self.compact_content.clone());
527
528        // convert to json object
529        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
547/// Normalize a path.
548fn normalize_path<S: AsRef<str>>(path: S) -> String {
549    // translate backslashes into forward slashes
550    let s = path.as_ref().replace('\\', "/");
551
552    // convert multiple slashes into a single one
553    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}