use anyhow::{Result, bail};
use fs_err::{File, OpenOptions, create_dir_all, read_dir, remove_file};
use ordinary_types::{ContentField, ContentObject};
use regex::RegexBuilder;
use serde_json::Value;
use std::io::{Seek, SeekFrom, Write};
use std::path::Path;
use std::{env, io::Read};
use yaml_rust2::YamlLoader;
fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
if let Some(operation) = args.get(1) {
let operation = operation.as_str();
match operation {
"before_build" => {
if let Some(posts_dir) = args.get(2) {
let posts_dir = Path::new(posts_dir);
let mut posts = vec![];
if posts_dir.exists() {
for entry in read_dir(posts_dir)? {
let entry = entry?;
let path = entry.path();
let mut file = File::open(&path)?;
let mut md = String::new();
file.read_to_string(&mut md)?;
let post = extract_post(&md)?;
let Some(slug) = path
.file_name()
.and_then(|s| s.to_str())
.and_then(|s| s.strip_suffix(".md"))
else {
bail!("bad slug");
};
posts.push(ContentObject {
uuid: post.uuid,
instance_of: "post".to_string(),
fields: vec![
ContentField {
name: "slug".to_string(),
value: Value::String(slug.to_string()),
},
ContentField {
name: "title".to_string(),
value: Value::String(post.title),
},
ContentField {
name: "date".to_string(),
value: Value::Number(post.date.into()),
},
ContentField {
name: "body".to_string(),
value: Value::String(post.body),
},
],
});
}
}
let mut content_file = OpenOptions::new()
.read(true)
.write(true)
.open("content.json")?;
let mut initial_content_json = String::new();
content_file.read_to_string(&mut initial_content_json)?;
let initial_content: Vec<ContentObject> =
serde_json::from_str(&initial_content_json)?;
let mut filtered_content = initial_content
.iter()
.filter(|obj| obj.instance_of != "post")
.cloned()
.collect::<Vec<ContentObject>>();
filtered_content.extend(posts);
let updated_content = serde_json::to_string_pretty(&filtered_content)?;
content_file.seek(SeekFrom::Start(0))?;
content_file.write_all(updated_content.as_bytes())?;
content_file.flush()?;
}
}
"after_add" | "after_edit" | "after_delete" => {
if let Some(posts_dir) = args.get(2)
&& let Some(post_json) = args.get(3)
{
let posts_dir = Path::new(posts_dir);
if !posts_dir.exists() {
create_dir_all(posts_dir)?;
}
let post: ContentObject = serde_json::from_str(post_json)?;
let (slug, md) = to_slug_and_md(&post)?;
let mut post_path = posts_dir.join(slug);
post_path.add_extension("md");
match operation {
"after_add" => {
let mut post_file = File::create(&post_path)?;
post_file.write_all(md.as_bytes())?;
post_file.flush()?;
}
"after_edit" => {
for entry in read_dir(posts_dir)? {
let entry = entry?;
let mut file = File::open(entry.path())?;
let mut curr_md = String::new();
file.read_to_string(&mut curr_md)?;
if extract_post(&curr_md)?.uuid == post.uuid {
remove_file(entry.path())?;
let mut post_file = File::create(&post_path)?;
post_file.write_all(md.as_bytes())?;
post_file.flush()?;
break;
}
}
}
"after_delete" => {
remove_file(&post_path)?;
}
_ => unreachable!(),
}
} else {
bail!("no content object arg provided")
}
}
_ => bail!("invalid operation: {operation}"),
}
} else {
bail!("no args provided")
}
Ok(())
}
fn to_slug_and_md(post: &ContentObject) -> Result<(String, String)> {
let mut slug = String::new();
let mut title = String::new();
let mut date = 0i64;
let mut body = String::new();
for field in &post.fields {
match field.name.as_str() {
"slug" => {
if let Some(value) = field.value.as_str() {
slug = value.to_string();
}
}
"title" => {
if let Some(value) = field.value.as_str() {
title = value.to_string();
}
}
"date" => {
if let Some(value) = field.value.as_i64() {
date = value
}
}
"body" => {
if let Some(value) = field.value.as_str() {
body = value.to_string();
}
}
_ => bail!("invalid field name"),
}
}
let md = format!(
"---
uuid: {}
title: {title}
date: {date}
---
{body}
",
post.uuid
);
Ok((slug, md))
}
struct Post {
uuid: String,
title: String,
date: i64,
body: String,
}
fn extract_post(md: &str) -> anyhow::Result<Post> {
let re = RegexBuilder::new(r"^---$(?<frontmatter>[\s\S]*?)^---$")
.unicode(true)
.multi_line(true)
.build()?;
let Some(caps) = re.captures(md) else {
bail!("no frontmatter");
};
let frontmatter = &caps["frontmatter"];
let Some(body) = md.strip_prefix(&format!("---{frontmatter}---\n\n")) else {
bail!("newline needs to exist between frontmatter and body");
};
let yaml = &YamlLoader::load_from_str(frontmatter)?[0];
if let Some(uuid) = yaml["uuid"].as_str()
&& let Some(title) = yaml["title"].as_str()
&& let Some(date) = yaml["date"].as_i64()
{
Ok(Post {
uuid: uuid.to_string(),
title: title.to_string(),
date,
body: body.to_string(),
})
} else {
bail!("bad frontmatter");
}
}