bard/
default_project.rs

1use std::fs;
2use std::path::MAIN_SEPARATOR;
3
4use crate::prelude::*;
5use crate::util::PathBufExt as _;
6
7/// A filesystem node, either a file (with content), or a directory.
8#[derive(Debug)]
9enum Node {
10    File {
11        path: &'static str,
12        content: &'static [u8],
13    },
14    Dir {
15        path: &'static str,
16    },
17}
18
19impl Node {
20    const fn dir(path: &'static str) -> Self {
21        Self::Dir { path }
22    }
23
24    fn path_buf(&self) -> PathBuf {
25        match self {
26            Self::File { path, .. } => path,
27            Self::Dir { path } => path,
28        }
29        .replace('/', &String::from(MAIN_SEPARATOR))
30        .into() // MAIN_SEPARATOR_STR isn't stable :-|
31    }
32
33    fn resolve(&self, base: &Path) -> NodeResolved {
34        let mut path = self.path_buf();
35        path.resolve(base);
36        match self {
37            Self::File { content, .. } => NodeResolved::File { path, content },
38            Self::Dir { .. } => NodeResolved::Dir { path },
39        }
40    }
41}
42
43enum NodeResolved {
44    File {
45        path: PathBuf,
46        content: &'static [u8],
47    },
48    Dir {
49        path: PathBuf,
50    },
51}
52
53impl NodeResolved {
54    fn path(&self) -> &Path {
55        match self {
56            Self::File { path, .. } => path.as_ref(),
57            Self::Dir { path } => path.as_ref(),
58        }
59    }
60
61    fn create(&self) -> Result<()> {
62        let dir_path = match self {
63            Self::File { path, .. } => path.parent(),
64            Self::Dir { path } => Some(path.as_ref()),
65        };
66        if let Some(dir_path) = dir_path {
67            fs::create_dir_all(dir_path)
68                .with_context(|| format!("Could not create directory {:?}", dir_path))?;
69        }
70
71        if let Self::File { path, content } = self {
72            fs::write(path, content)
73                .with_context(|| format!("Could not initialize file {:?}", path))?;
74        }
75
76        Ok(())
77    }
78}
79
80macro_rules! node_file {
81    ($path:literal) => {
82        Node::File {
83            path: $path,
84            content: include_bytes!(concat!("../default/", $path)),
85        }
86    };
87}
88
89#[derive(Debug)]
90pub struct DefaultProject {
91    nodes: &'static [Node],
92}
93
94impl DefaultProject {
95    pub fn resolve(&self, project_dir: &Path) -> DefaultProjectResolved {
96        let nodes = self.nodes.iter().map(|n| n.resolve(project_dir)).collect();
97        DefaultProjectResolved { nodes }
98    }
99}
100
101pub const DEFAULT_PROJECT: DefaultProject = DefaultProject {
102    nodes: &[
103        // Project file:
104        node_file!("bard.toml"),
105        // Song:
106        node_file!("songs/yippie.md"),
107        // Output dir:
108        Node::dir("output"),
109        // Fonts:
110        Node::dir("output/fonts"),
111        node_file!("output/fonts/BardSerif-Regular.ttf"),
112        node_file!("output/fonts/BardSerif-BoldItalic.ttf"),
113        node_file!("output/fonts/BardSerif-Bold.ttf"),
114        node_file!("output/fonts/BardSerif-Italic.ttf"),
115        node_file!("output/fonts/BardSerif-Regular.ttf"),
116        node_file!("output/fonts/BardSans-BoldItalic.ttf"),
117        node_file!("output/fonts/BardSans-Bold.ttf"),
118        node_file!("output/fonts/BardSans-Italic.ttf"),
119        node_file!("output/fonts/BardSans-Regular.ttf"),
120        node_file!("output/fonts/fonts.css"),
121        node_file!("output/fonts/fonts.tex"),
122    ],
123};
124
125pub struct DefaultProjectResolved {
126    nodes: Vec<NodeResolved>,
127}
128
129impl DefaultProjectResolved {
130    pub fn create(self) -> Result<()> {
131        let existing = self.nodes.iter().find(|n| n.path().exists());
132        if let Some(existing) = existing {
133            bail!("File already exists: {:?}", existing.path());
134        }
135
136        for node in &self.nodes[..] {
137            node.create()?;
138        }
139
140        Ok(())
141    }
142
143    pub fn files(&self) -> impl Iterator<Item = &Path> {
144        self.nodes.iter().filter_map(|node| match node {
145            NodeResolved::File { path, .. } => Some(path.as_path()),
146            NodeResolved::Dir { .. } => None,
147        })
148    }
149
150    pub fn dirs(&self) -> impl Iterator<Item = &Path> {
151        self.nodes.iter().filter_map(|node| match node {
152            NodeResolved::Dir { path } => Some(path.as_path()),
153            NodeResolved::File { .. } => None,
154        })
155    }
156}