use std::path::{Path, PathBuf};
use clap::{Parser, Subcommand};
use include_dir::{Dir, include_dir};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use url::Url;
use webdog::{
PageMetadata, Site, SiteConfig,
frontmatter::FrontMatter,
resource::{ResourceBuilderConfig, ResourceMetadata, ResourceRSSBuilderConfig},
};
static DEFAULT_PROJECT: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/embedded/default_site");
static DEFAULT_RESOURCE_TEMPLATES: Dir =
include_dir!("$CARGO_MANIFEST_DIR/src/embedded/resource-template");
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(global = true, long, default_value = ".")]
site: PathBuf,
}
#[derive(Debug, Subcommand)]
enum Commands {
Create {
base_url: Url,
title: String,
#[arg(long)]
cdn_url: Option<Url>,
},
Build {},
#[cfg(feature = "serve")]
Serve {
#[arg(long, default_value = "127.0.0.1")]
ip: String,
#[arg(short, long, default_value = "8080")]
port: u16,
},
Now,
Resource {
#[clap(subcommand)]
command: ResourceCommands,
},
Page {
#[clap(subcommand)]
command: PageCommands,
},
New {
resource_type: String,
id: String,
title: String,
#[arg(short, long = "tag")]
tags: Vec<String>,
#[arg(short, long)]
description: Option<String>,
#[arg(long, default_value = "false")]
skip_draft: bool,
},
}
#[derive(Debug, Subcommand)]
enum ResourceCommands {
Create {
id: String,
name: String,
plural: String,
#[arg(long, default_value = "false")]
no_rss: bool,
},
}
#[derive(Debug, Subcommand)]
enum PageCommands {
New {
id: String,
title: Option<String>,
#[arg(long)]
template: Option<String>,
},
}
fn main() -> eyre::Result<()> {
#[cfg(feature = "color-eyre")]
color_eyre::install()?;
let cli = Cli::parse();
let site = || -> eyre::Result<Site> { Site::new(&Path::new(&cli.site).canonicalize()?) };
match cli.command {
Commands::Create {
base_url,
cdn_url,
title,
} => {
if cli.site.exists() {
eprintln!("content exists in the given path! canceling!");
return Ok(());
}
std::fs::create_dir_all(&cli.site)?;
let config = SiteConfig::new(base_url.clone(), cdn_url.unwrap_or(base_url), title);
std::fs::write(
cli.site.join(SiteConfig::FILENAME),
serde_yaml_ng::to_string(&config)?,
)?;
DEFAULT_PROJECT.extract(&cli.site)?;
std::fs::create_dir(cli.site.join(webdog::ROOT_PATH))?;
println!(
"Base site created at {:?}! Ready for editing, woof!",
cli.site
);
Ok(())
}
Commands::Build {} => {
println!("Building site...");
let now = std::time::Instant::now();
site()?.build_once()?;
println!("Build completed in {:?}", now.elapsed());
Ok(())
}
#[cfg(feature = "serve")]
Commands::Serve { ip, port } => {
let site = site()?;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async move { site.serve(&format!("{}:{}", ip, port)).await })
}
Commands::Now => {
let time = OffsetDateTime::now_utc();
println!("{}", time.format(&Rfc3339)?);
Ok(())
}
Commands::Resource { command } => match command {
ResourceCommands::Create {
id,
name,
plural,
no_rss,
} => {
let config_path = cli.site.join(SiteConfig::FILENAME);
let mut config = SiteConfig::read(&cli.site)?;
if config.resources.contains_key(&id) {
eprintln!("resource type {id} already exists, canceling");
return Ok(());
}
let resource_template_path = cli.site.join(webdog::TEMPLATES_PATH).join(&id);
if resource_template_path.exists() {
eprintln!(
"path for resource already exists at {resource_template_path:?}, canceling"
);
return Ok(());
}
std::fs::create_dir_all(&resource_template_path)?;
for file in DEFAULT_RESOURCE_TEMPLATES.files() {
let resource_path = resource_template_path.join(file.path());
if let Some(contents) = file.contents_utf8() {
let mut contents = contents.to_owned();
contents = contents.replace("!!RESOURCE_TYPE!!", &id);
contents = contents.replace("!!RESOURCE_NAME!!", &name);
contents =
contents.replace("!!RESOURCE_NAME_LOWERCASE!!", &name.to_lowercase());
contents = contents.replace("!!RESOURCE_NAME_PLURAL!!", &plural);
contents = contents
.replace("!!RESOURCE_NAME_PLURAL_LOWERCASE!!", &plural.to_lowercase());
std::fs::write(resource_path, contents)?;
} else {
std::fs::write(resource_path, file.contents())?;
}
}
let rss = (!no_rss).then(|| ResourceRSSBuilderConfig {
template: format!("{id}/rss.tera"),
title: id.clone(),
description: Default::default(),
});
let resource_config = ResourceBuilderConfig {
source_path: id.clone(),
output_path_resources: id.clone(),
output_path_lists: id.clone(),
resource_template: format!("{id}/resource.tera"),
resource_list_template: format!("{id}/list.tera"),
tag_list_template: "basic-link-list.tera".to_string(),
rss,
list_title: name.clone(),
tag_list_title: format!("{name} tags"),
resource_name_plural: plural,
resources_per_page: 3,
timestamp_format: "[weekday], [month repr:long] [day], [year]".to_string(),
};
config.resources.insert(id.clone(), resource_config);
std::fs::write(config_path, serde_yaml_ng::to_string(&config)?)?;
let resource_path = cli.site.join(webdog::RESOURCES_PATH).join(&id);
std::fs::create_dir_all(&resource_path)?;
create_resource(
&resource_path.join("first.md"),
&ResourceMetadata {
title: format!("First {name}"),
timestamp: OffsetDateTime::now_utc(),
tags: vec!["first".to_string()],
cdn_file: None,
desc: Some(format!("This is the first {name} :)")),
inner: serde_yaml_ng::Value::Null,
draft: true,
},
)?;
println!(
"Created the new resource type {id}! The first resource of this time is available at {:?}.",
resource_path
);
Ok(())
}
},
Commands::Page { command } => match command {
PageCommands::New {
id,
title,
template,
} => {
let page_path = cli
.site
.join(webdog::PAGES_PATH)
.join(&id)
.with_extension("md");
let dir = page_path.parent().expect("should never fail");
if page_path.exists() {
eprintln!("page already exists!");
return Ok(());
}
let fm = FrontMatter {
content: "new page :)".to_string(),
data: Some(PageMetadata {
title,
template,
..Default::default()
}),
};
std::fs::create_dir_all(dir)?;
std::fs::write(&page_path, fm.format()?)?;
println!("Page created! Edit at {:?}.", page_path);
Ok(())
}
},
Commands::New {
resource_type,
id,
title,
tags,
description,
skip_draft,
} => {
let config = SiteConfig::read(&cli.site)?;
if let Some(resource) = config.resources.get(&resource_type) {
let resource_path = cli
.site
.join(webdog::RESOURCES_PATH)
.join(&resource.source_path)
.join(&id)
.with_extension("md");
if resource_path.exists() {
eprintln!(
"A {resource_type} resource of the ID {id} already exists, canceling!"
);
return Ok(());
}
create_resource(
&resource_path,
&ResourceMetadata {
title,
timestamp: OffsetDateTime::now_utc(),
tags,
cdn_file: None,
desc: description,
inner: serde_yaml_ng::Value::Null,
draft: !skip_draft,
},
)?;
println!(
"Created the new {resource_type} resource {id}! Available at {:?}",
resource_path
);
} else {
eprintln!("no resource of type {resource_type}, canceling");
}
Ok(())
}
}
}
fn create_resource(resource_path: &Path, metadata: &ResourceMetadata) -> eyre::Result<()> {
std::fs::write(
resource_path,
FrontMatter {
content: "hello world :)".to_string(),
data: Some(metadata),
}
.format()?,
)?;
Ok(())
}