1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
use std::ffi::OsStr;
use std::fs;
use std::path;

use anyhow::Context as _;
use cobalt_config::DateTime;
use liquid;
use log::debug;
use serde::{Deserialize, Serialize};
use serde_json;
use serde_yaml;
use toml;

use crate::error::*;

use super::files;

#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Site {
    pub title: Option<liquid::model::KString>,
    pub description: Option<liquid::model::KString>,
    pub base_url: Option<liquid::model::KString>,
    pub sitemap: Option<cobalt_config::RelPath>,
    pub data: Option<liquid::Object>,
    pub data_dir: &'static str,
    /// The time at which the `cobalt` binary built the site
    pub time: DateTime,
}

impl Site {
    pub fn from_config(config: cobalt_config::Site) -> Self {
        let cobalt_config::Site {
            title,
            description,
            base_url,
            sitemap,
            data,
            data_dir,
        } = config;

        let base_url = base_url.map(|mut l| {
            if l.ends_with('/') {
                let mut other = String::from(l.as_str());
                other.pop();
                l = liquid::model::KString::from_string(other);
            }
            l
        });

        Self {
            title,
            description,
            base_url,
            sitemap,
            data,
            data_dir,
            time: DateTime::now(),
        }
    }

    pub fn load(&self, source: &std::path::Path) -> Result<liquid::Object> {
        let mut attributes = liquid::Object::new();
        if let Some(title) = self.title.as_ref() {
            attributes.insert(
                "title".into(),
                liquid::model::Value::scalar(liquid::model::KString::from_ref(title)),
            );
        }
        if let Some(description) = self.description.as_ref() {
            attributes.insert(
                "description".into(),
                liquid::model::Value::scalar(liquid::model::KString::from_ref(description)),
            );
        }
        if let Some(base_url) = self.base_url.as_ref() {
            attributes.insert(
                "base_url".into(),
                liquid::model::Value::scalar(liquid::model::KString::from_ref(base_url)),
            );
        }
        attributes.insert("time".into(), liquid::model::Value::scalar(self.time));

        let mut data = self.data.clone().unwrap_or_default();
        let data_path = source.join(&self.data_dir);
        insert_data_dir(&mut data, &data_path)?;
        if !data.is_empty() {
            attributes.insert("data".into(), liquid::model::Value::Object(data));
        }

        Ok(attributes)
    }
}

fn deep_insert(
    data_map: &mut liquid::Object,
    file_path: &path::Path,
    target_key: String,
    data: liquid::model::Value,
) -> Result<()> {
    // now find the nested map it is supposed to be in
    let target_map = if let Some(path) = file_path.parent() {
        let mut map = data_map;
        for part in path.iter() {
            let key = part.to_str().ok_or_else(|| {
                anyhow::format_err!(
                    "The data from {:?} can't be loaded as it contains non utf-8 characters",
                    path
                )
            })?;
            let cur_map = map;
            let key = liquid::model::KString::from_ref(key);
            map = cur_map
                .entry(key)
                .or_insert_with(|| liquid::model::Value::Object(liquid::Object::new()))
                .as_object_mut()
                .ok_or_else(|| {
                    anyhow::format_err!(
                        "Aborting: Duplicate in data tree. Would overwrite {:?} ",
                        path
                    )
                })?;
        }
        map
    } else {
        data_map
    };

    match target_map.insert(target_key.into(), data) {
        None => Ok(()),
        _ => Err(anyhow::format_err!(
            "The data from {:?} can't be loaded: the key already exists",
            file_path
        )),
    }
}

fn load_data(data_path: &path::Path) -> Result<liquid::model::Value> {
    let ext = data_path.extension().unwrap_or_else(|| OsStr::new(""));

    let data: liquid::model::Value;

    if ext == OsStr::new("yml") || ext == OsStr::new("yaml") {
        let reader = fs::File::open(data_path)?;
        data = serde_yaml::from_reader(reader)?;
    } else if ext == OsStr::new("json") {
        let reader = fs::File::open(data_path)?;
        data = serde_json::from_reader(reader)?;
    } else if ext == OsStr::new("toml") {
        let text = files::read_file(data_path)?;
        data = toml::from_str(&text)?;
    } else {
        anyhow::bail!(
            "Failed to load of data {:?}: unknown file type '{:?}'.\n\
             Supported data files extensions are: yml, yaml, json and toml.",
            data_path,
            ext
        );
    }

    Ok(data)
}

fn insert_data_dir(data: &mut liquid::Object, data_root: &path::Path) -> Result<()> {
    debug!("Loading data from `{}`", data_root.display());

    let data_files_builder = files::FilesBuilder::new(data_root)?;
    let data_files = data_files_builder.build()?;
    for full_path in data_files.files() {
        let rel_path = full_path
            .strip_prefix(data_root)
            .expect("file was found under the root");

        let file_stem = full_path
            .file_stem()
            .expect("Files will always return with a stem");
        let file_stem = String::from(file_stem.to_str().unwrap());
        let data_fragment = load_data(&full_path)
            .with_context(|| format!("Loading data from `{}` failed", full_path.display()))?;

        deep_insert(data, rel_path, file_stem, data_fragment)
            .with_context(|| format!("Merging data into `{}` failed", rel_path.display()))?;
    }

    Ok(())
}