use std::fs;
use std::path::{Path, PathBuf};
use crate::error::OrbitError;
struct ScaffoldFile {
relative_path: &'static str,
contents: &'static str,
}
const SCAFFOLD_FILES: &[ScaffoldFile] = &[
ScaffoldFile {
relative_path: "orbit.yaml",
contents: include_str!("../../scaffold/orbit.yaml"),
},
ScaffoldFile {
relative_path: ".gitignore",
contents: include_str!("../../scaffold/.gitignore"),
},
ScaffoldFile {
relative_path: "content/index.md",
contents: include_str!("../../scaffold/content/index.md"),
},
ScaffoldFile {
relative_path: "content/docs/getting-started.md",
contents: include_str!("../../scaffold/content/docs/getting-started.md"),
},
ScaffoldFile {
relative_path: "components/Alert.hbs",
contents: include_str!("../../scaffold/components/Alert.hbs"),
},
ScaffoldFile {
relative_path: "components/Button.hbs",
contents: include_str!("../../scaffold/components/Button.hbs"),
},
ScaffoldFile {
relative_path: "components/Card.hbs",
contents: include_str!("../../scaffold/components/Card.hbs"),
},
ScaffoldFile {
relative_path: "templates/base.hbs",
contents: include_str!("../../scaffold/templates/base.hbs"),
},
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitOptions {
pub path: PathBuf,
pub title: String,
}
impl InitOptions {
pub fn from_path(path: PathBuf) -> Self {
let title = path
.file_name()
.and_then(|n| n.to_str())
.map(title_from_slug)
.unwrap_or_else(|| "My Site".to_owned());
Self { path, title }
}
}
pub fn init_project(options: &InitOptions) -> Result<(), OrbitError> {
let dest = &options.path;
if dest.exists() {
let mut entries = fs::read_dir(dest).map_err(|source| OrbitError::Io {
path: dest.clone(),
source,
})?;
if entries.next().is_some() {
return Err(OrbitError::Config(format!(
"directory {} is not empty — choose an empty folder or a new name",
dest.display()
)));
}
} else {
fs::create_dir_all(dest).map_err(|source| OrbitError::Io {
path: dest.clone(),
source,
})?;
}
for file in SCAFFOLD_FILES {
let target = dest.join(file.relative_path);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(|source| OrbitError::Io {
path: parent.to_path_buf(),
source,
})?;
}
let contents = substitute_placeholders(file.contents, &options.title);
fs::write(&target, contents).map_err(|source| OrbitError::Io {
path: target,
source,
})?;
}
Ok(())
}
pub fn new_page(content_root: &Path, relative: &Path, title: &str) -> Result<PathBuf, OrbitError> {
let normalized = normalize_page_path(relative);
let target = content_root.join(&normalized);
if target.exists() {
return Err(OrbitError::Config(format!(
"page already exists at {}",
target.display()
)));
}
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(|source| OrbitError::Io {
path: parent.to_path_buf(),
source,
})?;
}
let stem = normalized
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Untitled");
let body = format!(
"---\ntitle: {title}\n---\n\n# {heading}\n\nWrite your content here.\n",
title = title,
heading = stem
);
fs::write(&target, body).map_err(|source| OrbitError::Io {
path: target.clone(),
source,
})?;
Ok(target)
}
fn substitute_placeholders(input: &str, title: &str) -> String {
input.replace("{{title}}", title)
}
pub fn title_from_slug(slug: &str) -> String {
slug.split(['-', '_'])
.filter(|part| !part.is_empty())
.map(|part| {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn normalize_page_path(relative: &Path) -> PathBuf {
let mut path = relative.to_path_buf();
if path.extension().is_none() {
path.set_extension("md");
}
path
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn title_from_slug_formats_name() {
assert_eq!(title_from_slug("my-blog"), "My Blog");
assert_eq!(title_from_slug("docs_site"), "Docs Site");
}
#[test]
fn init_creates_project_tree() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("demo-site");
init_project(&InitOptions {
path: path.clone(),
title: "Demo Site".into(),
})
.unwrap();
assert!(path.join("orbit.yaml").exists());
assert!(path.join("content/index.md").exists());
assert!(path.join("components/Alert.hbs").exists());
assert!(path.join("templates/base.hbs").exists());
let config = std::fs::read_to_string(path.join("orbit.yaml")).unwrap();
assert!(config.contains("title: Demo Site"));
}
#[test]
fn init_rejects_nonempty_directory() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("existing.txt"), "data").unwrap();
let result = init_project(&InitOptions {
path: dir.path().to_path_buf(),
title: "X".into(),
});
assert!(result.is_err());
}
#[test]
fn new_page_adds_md_extension() {
let dir = tempfile::tempdir().unwrap();
let content = dir.path().join("content");
std::fs::create_dir_all(&content).unwrap();
let created = new_page(&content, Path::new("blog/post"), "My Post").unwrap();
assert_eq!(created, content.join("blog/post.md"));
assert!(created.exists());
}
}