futhorc/
build.rs

1//! Exports the [`build_site`] function which stitches together the high-level
2//! steps of building the output static site: parsing the posts
3//! ([`crate::post`]), rendering index and post pages ([`crate::write`]),
4//! copying the static source directory into the static output directory, and
5//! generating the Atom feed.
6
7use crate::config::Config;
8use crate::feed::{Error as FeedError, *};
9use crate::parser::{Error as ParseError, Parser as PostParser};
10use crate::write::{Error as WriteError, *};
11use gtmpl::Template;
12use std::fmt;
13use std::fs::File;
14use std::path::{Path, PathBuf};
15
16/// Builds the site from a [`Config`] object. This calls into
17/// [`PostParser::parse_posts`], [`Writer::write_posts`], and
18/// [`write_feed`] which do the heavy-lifting. This function also copies
19/// the static assets from source directory to the output directory.
20pub fn build_site(config: Config) -> Result<()> {
21    let post_parser = PostParser::new(
22        &config.index_url,
23        &config.posts_url,
24        &config.posts_output_directory,
25    );
26
27    // collect all posts
28    let posts = post_parser.parse_posts(&config.posts_source_directory)?;
29
30    // Parse the template files.
31    let index_template = parse_template(config.index_template.iter())?;
32    let posts_template = parse_template(config.posts_template.iter())?;
33
34    // Blow away the old output directories so we don't have any collisions. We
35    // probably don't want to naively delete the whole root output directory in
36    // case the user accidentally passes the wrong directory. In the future, we
37    // could refuse to build in a directory that already exists unless it was
38    // created by `futhorc`, in which case we would then delete and rebuild
39    // that directory. In order to tell that the output directory was
40    // created by futhorc, we could leave a `.futhorc` watermark file,
41    // possibly with the identifier of the specific futhorc project.
42    rmdir(&config.posts_output_directory)?;
43    rmdir(&config.index_output_directory)?;
44    rmdir(&config.static_output_directory)?;
45
46    // write the post and index pages
47    let writer = Writer {
48        posts_template: &posts_template,
49        index_template: &index_template,
50        index_page_size: config.index_page_size,
51        index_base_url: &config.index_url,
52        index_output_directory: &config.index_output_directory,
53        home_page: &config.home_page,
54        static_url: &config.static_url,
55        atom_url: &config.atom_url,
56    };
57    writer.write_posts(&posts)?;
58
59    // copy static directory
60    copy_dir(
61        &config.static_source_directory,
62        &config.static_output_directory,
63    )?;
64
65    // copy /pages/index.html to /index.html
66    let _ = std::fs::copy(
67        config.index_output_directory.join("index.html"),
68        config.root_output_directory.join("index.html"),
69    )?;
70
71    // create the atom feed
72    write_feed(
73        FeedConfig {
74            title: config.title,
75            id: config.home_page.to_string(),
76            author: config.author,
77            home_page: config.home_page,
78        },
79        &posts,
80        File::create(config.root_output_directory.join("feed.atom"))?,
81    )?;
82
83    Ok(())
84}
85
86fn copy_dir(src: &Path, dst: &Path) -> Result<()> {
87    std::fs::create_dir(dst)?;
88    for entry in std::fs::read_dir(src)? {
89        let entry = entry?;
90        if entry.file_type()?.is_dir() {
91            copy_dir(src, &dst.join(entry.file_name()))?;
92        } else {
93            std::fs::copy(
94                src.join(entry.file_name()),
95                dst.join(entry.file_name()),
96            )?;
97        }
98    }
99
100    Ok(())
101}
102
103// Loads the template file contents, appends them to `base_template`, and
104// parses the result into a template.
105fn parse_template<P: AsRef<Path>>(
106    template_files: impl Iterator<Item = P>,
107) -> Result<Template> {
108    let mut contents = String::new();
109    for template_file in template_files {
110        use std::io::Read;
111        let template_file = template_file.as_ref();
112        File::open(template_file)
113            .map_err(|e| Error::OpenTemplateFile {
114                path: template_file.to_owned(),
115                err: e,
116            })?
117            .read_to_string(&mut contents)?;
118        contents.push(' ');
119    }
120
121    let mut template = Template::default();
122    template.parse(&contents).map_err(Error::ParseTemplate)?;
123    Ok(template)
124}
125
126type Result<T> = std::result::Result<T, Error>;
127
128/// The error type for building a site. Errors can be during parsing, writing,
129/// cleaning output directories, parsing template files, and other I/O.
130#[derive(Debug)]
131pub enum Error {
132    /// Returned for errors during parsing.
133    Parse(ParseError),
134
135    /// Returned for errors writing [`crate::post::Post`]s to disk as HTML
136    /// files.
137    Write(WriteError),
138
139    /// Returned for I/O problems while cleaning output directories.
140    Clean { path: PathBuf, err: std::io::Error },
141
142    /// Returned for I/O problems while opening template files.
143    OpenTemplateFile { path: PathBuf, err: std::io::Error },
144
145    /// Returned for errors parsing template files.
146    ParseTemplate(String),
147
148    /// Returned for errors writing the feed.
149    Feed(FeedError),
150
151    /// Returned for other I/O errors.
152    Io(std::io::Error),
153}
154
155impl fmt::Display for Error {
156    /// Implements [`fmt::Display`] for [`Error`].
157    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
158        match self {
159            Error::Parse(err) => err.fmt(f),
160            Error::Write(err) => err.fmt(f),
161            Error::Clean { path, err } => {
162                write!(f, "Cleaning directory '{}': {}", path.display(), err)
163            }
164            Error::OpenTemplateFile { path, err } => {
165                write!(
166                    f,
167                    "Opening template file '{}': {}",
168                    path.display(),
169                    err
170                )
171            }
172            Error::ParseTemplate(err) => err.fmt(f),
173            Error::Feed(err) => err.fmt(f),
174            Error::Io(err) => err.fmt(f),
175        }
176    }
177}
178
179impl std::error::Error for Error {
180    /// Implements [`std::error::Error`] for [`Error`].
181    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
182        match self {
183            Error::Parse(err) => Some(err),
184            Error::Write(err) => Some(err),
185            Error::Clean { path: _, err } => Some(err),
186            Error::OpenTemplateFile { path: _, err } => Some(err),
187            Error::ParseTemplate(_) => None,
188            Error::Feed(err) => Some(err),
189            Error::Io(err) => Some(err),
190        }
191    }
192}
193
194impl From<std::io::Error> for Error {
195    /// Converts [`std::io::Error`]s into [`Error`]. This allows us to use the
196    /// `?` operator.
197    fn from(err: std::io::Error) -> Error {
198        Error::Io(err)
199    }
200}
201
202impl From<ParseError> for Error {
203    /// Converts [`ParseError`]s into [`Error`]. This allows us to use the `?`
204    /// operator.
205    fn from(err: ParseError) -> Error {
206        Error::Parse(err)
207    }
208}
209
210impl From<WriteError> for Error {
211    /// Converts [`WriteError`]s into [`Error`]. This allows us to use the `?`
212    /// operator.
213    fn from(err: WriteError) -> Error {
214        Error::Write(err)
215    }
216}
217
218impl From<FeedError> for Error {
219    /// Converts [`FeedError`]s into [`Error`]. This allows us to use the `?`
220    /// operator.
221    fn from(err: FeedError) -> Error {
222        Error::Feed(err)
223    }
224}
225
226fn rmdir(dir: &Path) -> Result<()> {
227    match std::fs::remove_dir_all(dir) {
228        Ok(x) => Ok(x),
229        Err(e) => match e.kind() {
230            std::io::ErrorKind::NotFound => Ok(()),
231            _ => Err(Error::Clean {
232                path: dir.to_owned(),
233                err: e,
234            }),
235        },
236    }
237}