use anyhow::{Context, Result, bail};
use std::fs;
use std::path::Path;
const TEMPLATE_LIB: &str = include_str!("../docs/fy-spec/lib.typ");
const STARTER_MAIN: &str = r#"#import "fy-spec/lib.typ": *
#show: project_book.with(
title: "{{NAME}} Specification",
subtitle: none,
version: "{{VERSION}}",
author: "{{AUTHOR}}",
date: datetime.today().display("[year]-[month]-[day]"),
lang: "en", // change to "zh" or "zh-CN" for Chinese documents
)
= Overview
This document was initialized by `cargo fy-docs init`. Add your module specifications in `docs/modules/` and `#include` them here.
// #include "modules/example.typ"
"#;
pub fn init(cwd: &Path) -> Result<()> {
let docs_dir = cwd.join("docs");
let entry = docs_dir.join("main.typ");
if entry.is_file() {
bail!(
"`{}` already exists — remove it first if you want to re-initialize",
crate::state::display_path(&entry)
);
}
let meta = crate::project::cargo_package_meta(cwd);
let name = meta.name.unwrap_or_else(|| {
cwd.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "project".to_owned())
});
let version = meta.version.unwrap_or_else(|| "0.1.0".to_owned());
let author = meta.author.unwrap_or_else(|| "TODO".to_owned());
fs::create_dir_all(docs_dir.join("modules")).context("could not create docs/modules/")?;
fs::create_dir_all(docs_dir.join("fy-spec")).context("could not create docs/fy-spec/")?;
fs::write(docs_dir.join("fy-spec").join("lib.typ"), TEMPLATE_LIB)
.context("could not write docs/fy-spec/lib.typ")?;
let main_content = STARTER_MAIN
.replace("{{NAME}}", &name)
.replace("{{VERSION}}", &version)
.replace("{{AUTHOR}}", &author);
fs::write(&entry, main_content).context("could not write docs/main.typ")?;
crate::project::ensure_gitignore(cwd, &["/docs/target/", "/docs/release/"]);
crate::state::log(&format!(
"[fy-docs] initialized {}",
crate::state::display_path(&docs_dir)
));
crate::state::log("[fy-docs] created docs/main.typ");
crate::state::log("[fy-docs] created docs/fy-spec/lib.typ");
crate::state::log("[fy-docs] created docs/modules/");
crate::state::log("[fy-docs] → run `cargo fy-docs` to preview");
Ok(())
}
pub fn vendor(cwd: &Path, check: bool) -> Result<()> {
let docs_dir = cwd.join("docs");
if !docs_dir.is_dir() {
bail!(
"`{}` has no docs/ directory — run `cargo fy-docs init` first",
crate::state::display_path(&docs_dir)
);
}
let lib = docs_dir.join("fy-spec").join("lib.typ");
if check {
let current = fs::read_to_string(&lib).with_context(|| {
format!(
"{} is missing — run `cargo fy-docs vendor` to sync it",
crate::state::display_path(&lib)
)
})?;
if current != TEMPLATE_LIB {
bail!(
"{} differs from the embedded template — run `cargo fy-docs vendor` to sync it",
crate::state::display_path(&lib)
);
}
crate::state::log(&format!(
"[fy-docs] {} matches the embedded template",
crate::state::display_path(&lib)
));
return Ok(());
}
fs::create_dir_all(docs_dir.join("fy-spec")).context("could not create docs/fy-spec/")?;
fs::write(&lib, TEMPLATE_LIB).context("could not write docs/fy-spec/lib.typ")?;
crate::state::log(&format!(
"[fy-docs] vendored fy-spec into {}",
crate::state::display_path(&lib)
));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scaffolds_docs_directory() {
let temp = std::env::temp_dir().join(format!("fy-docs-init-{}", std::process::id()));
std::fs::create_dir_all(&temp).unwrap();
std::fs::write(
temp.join("Cargo.toml"),
"[package]\nname = \"test-project\"\nversion = \"1.0.0\"\nauthors = [\"Tester <t@example.com>\"]\n",
)
.unwrap();
init(&temp).unwrap();
assert!(temp.join("docs/main.typ").is_file());
assert!(temp.join("docs/fy-spec/lib.typ").is_file());
assert!(temp.join("docs/modules").is_dir());
let main = std::fs::read_to_string(temp.join("docs/main.typ")).unwrap();
assert!(main.contains("test-project"));
assert!(main.contains("1.0.0"));
assert!(main.contains("Tester"));
assert!(!main.contains("fengyangsi"));
std::fs::remove_dir_all(temp).unwrap();
}
#[test]
fn refuses_to_overwrite_existing_main() {
let temp = std::env::temp_dir().join(format!("fy-docs-init-dup-{}", std::process::id()));
std::fs::create_dir_all(temp.join("docs")).unwrap();
std::fs::write(temp.join("docs/main.typ"), "existing").unwrap();
let result = init(&temp);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
std::fs::remove_dir_all(temp).unwrap();
}
#[test]
fn falls_back_without_cargo_toml() {
let temp =
std::env::temp_dir().join(format!("fy-docs-init-no-cargo-{}", std::process::id()));
std::fs::create_dir_all(&temp).unwrap();
init(&temp).unwrap();
let main = std::fs::read_to_string(temp.join("docs/main.typ")).unwrap();
assert!(main.contains("0.1.0"));
assert!(main.contains(r#"author: "TODO""#));
std::fs::remove_dir_all(temp).unwrap();
}
#[test]
fn vendor_syncs_and_checks_the_template() {
let temp = std::env::temp_dir().join(format!("fy-docs-vendor-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&temp);
std::fs::create_dir_all(&temp).unwrap();
assert!(vendor(&temp, false).is_err());
init(&temp).unwrap();
assert!(vendor(&temp, true).is_ok());
std::fs::write(temp.join("docs/fy-spec/lib.typ"), "drifted").unwrap();
assert!(vendor(&temp, true).is_err());
vendor(&temp, false).unwrap();
assert!(vendor(&temp, true).is_ok());
std::fs::remove_file(temp.join("docs/fy-spec/lib.typ")).unwrap();
assert!(vendor(&temp, true).is_err());
vendor(&temp, false).unwrap();
assert_eq!(
std::fs::read_to_string(temp.join("docs/fy-spec/lib.typ")).unwrap(),
TEMPLATE_LIB
);
std::fs::remove_dir_all(temp).unwrap();
}
}