use super::*;
pub(crate) fn parse_args() -> Result<Args, String> {
let mut positional: Vec<PathBuf> = Vec::new();
let mut out_dir: Option<PathBuf> = None;
let mut name: Option<String> = None;
let mut index_html: Option<PathBuf> = None;
let mut locale: Option<String> = None;
let mut release: bool = true;
let mut iter: Skip<env::Args> = env::args().skip(1);
while let Some(arg) = iter.next() {
match arg.as_str() {
HELP_FLAG_SHORT | HELP_FLAG_LONG => {
print_usage();
exit(0);
}
VERSION_FLAG_SHORT | VERSION_FLAG_LONG => {
println!("euv-docs {}", env!("CARGO_PKG_VERSION"));
exit(0);
}
OUT_FLAG => {
let value: String = iter
.next()
.ok_or_else(|| format!("{OUT_FLAG} {MSG_FLAG_REQUIRES_VALUE}"))?;
out_dir = Some(PathBuf::from(value));
}
NAME_FLAG => {
let value: String = iter
.next()
.ok_or_else(|| format!("{NAME_FLAG} {MSG_FLAG_REQUIRES_VALUE}"))?;
name = Some(value);
}
INDEX_HTML_FLAG => {
let value: String = iter
.next()
.ok_or_else(|| format!("{INDEX_HTML_FLAG} {MSG_FLAG_REQUIRES_VALUE}"))?;
index_html = Some(PathBuf::from(value));
}
LOCALE_FLAG => {
let value: String = iter
.next()
.ok_or_else(|| format!("{LOCALE_FLAG} {MSG_FLAG_REQUIRES_VALUE}"))?;
locale = Some(value);
}
DEBUG_FLAG => {
release = false;
}
RELEASE_FLAG => {
release = true;
}
flag if flag.starts_with(PREFIX_OUT) => {
out_dir = Some(PathBuf::from(&flag[PREFIX_OUT.len()..]));
}
flag if flag.starts_with(PREFIX_NAME) => {
name = Some(flag[PREFIX_NAME.len()..].to_string());
}
flag if flag.starts_with(PREFIX_INDEX_HTML) => {
index_html = Some(PathBuf::from(&flag[PREFIX_INDEX_HTML.len()..]));
}
flag if flag.starts_with(PREFIX_LOCALE) => {
locale = Some(flag[PREFIX_LOCALE.len()..].to_string());
}
flag if flag.starts_with('-') => {
return Err(format!("{MSG_UNKNOWN_FLAG}: {flag}"));
}
_ => {
positional.push(PathBuf::from(arg));
}
}
}
let src_dir: PathBuf = positional
.into_iter()
.next()
.ok_or_else(|| MSG_MISSING_SRC_DIR.to_string())?;
let out_dir: PathBuf = out_dir.unwrap_or_else(|| PathBuf::from(DEFAULT_OUT_DIR));
let name: &'static str = match name {
Some(value) => Box::leak(value.into_boxed_str()),
None => DEFAULT_NAME,
};
let locale: Option<&'static str> =
locale.map(|value: String| &*Box::leak(value.into_boxed_str()));
Ok(Args::new(
src_dir, out_dir, name, release, index_html, locale,
))
}
pub(crate) fn run(args: &Args) -> Result<(), String> {
let user_cwd: PathBuf = env::current_dir().map_err(|e: io::Error| e.to_string())?;
let out_dir: PathBuf = {
let out_dir: &Path = args.get_out_dir().as_path();
if out_dir.is_absolute() {
out_dir.to_path_buf()
} else {
user_cwd.join(out_dir)
}
};
if args.try_get_locale().is_some() {
return build_one(args, &out_dir);
}
build_one(args, &out_dir)?;
for entry in read_locale_manifest(&out_dir)? {
if entry.dir.is_empty() {
continue;
}
let mut next: Args = args.clone();
next.set_locale(Some(&*Box::leak(entry.prefix.into_boxed_str())));
build_one(&next, &out_dir.join(entry.dir.trim_end_matches('/')))?;
}
Ok(())
}
fn read_locale_manifest(out_dir: &Path) -> Result<Vec<LocaleManifestEntry>, String> {
let path: PathBuf = out_dir
.join(DEPLOY_DIR_NAME)
.join(LOCALE_MANIFEST_FILE_NAME);
let raw: String = fs::read_to_string(&path).map_err(|e: io::Error| {
format!(
"locale manifest missing at {} after the default-locale build: {e}",
path.display()
)
})?;
let mut entries: Vec<LocaleManifestEntry> = Vec::new();
for line in raw.lines().skip(1) {
if line.trim().is_empty() {
continue;
}
let mut columns = line.split('\t');
let (Some(prefix), Some(dir), Some(_label)) =
(columns.next(), columns.next(), columns.next())
else {
return Err(format!("malformed locale manifest row: {line}"));
};
entries.push(LocaleManifestEntry {
prefix: prefix.to_string(),
dir: dir.to_string(),
});
}
Ok(entries)
}
fn build_one(args: &Args, out_dir: &Path) -> Result<(), String> {
let manifest_dir: PathBuf = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let src_dir: &Path = args.get_src_dir().as_path();
if !src_dir.is_dir() {
return Err(format!(
"source directory does not exist: {}",
src_dir.display()
));
}
if !src_dir.join(CONFIG_FILE_NAME).is_file() {
eprintln!(
"euv-docs: note: {} not found at {}, falling back to parent README.md frontmatter",
CONFIG_FILE_NAME,
src_dir.display()
);
}
fs::create_dir_all(out_dir).map_err(|e: io::Error| {
format!(
"failed to create output directory {}: {e}",
out_dir.display()
)
})?;
let user_template: Option<&PathBuf> = args.try_get_index_html().as_ref();
let user_cwd: PathBuf = env::current_dir().unwrap_or_else(|_| manifest_dir.clone());
let template_path: PathBuf = match user_template {
Some(path) if path.is_absolute() => path.clone(),
Some(path) => user_cwd.join(path),
None => manifest_dir.join(TEMPLATE_FILE_NAME),
};
let mut command: Command = Command::new(EUV_BIN);
if let Some(path) = env::var_os(PATH_ENV) {
command.env(PATH_ENV, path);
}
command.current_dir(&manifest_dir);
command
.arg(EUV_BUILD_SUBCMD)
.arg(if *args.get_release() {
EUV_RELEASE_FLAG
} else {
EUV_DEBUG_FLAG
})
.arg(EUV_INDEX_HTML_FLAG)
.arg(&template_path);
command.env(EUV_DOCS_SRC_DIR_ENV, src_dir);
command.env(EUV_DOCS_OUT_DIR_ENV, out_dir);
if let Some(locale) = args.try_get_locale() {
command.env(EUV_DOCS_LOCALE_ENV, locale);
}
let pkg_dir: PathBuf = out_dir.join(PKG_DIR_NAME);
command.arg(WASM_PACK_DELIMITER);
command
.arg(EUV_TARGET_FLAG)
.arg(WASM_TARGET_WEB)
.arg(EUV_OUT_DIR_FLAG)
.arg(&pkg_dir)
.arg(EUV_OUT_NAME_FLAG)
.arg(*args.get_name())
.arg(EUV_NO_TYPESCRIPT_FLAG)
.arg(EUV_NO_PACK_FLAG)
.arg(EUV_NO_GITIGNORE_FLAG);
println!(
"euv-docs: building {} -> {}",
src_dir.display(),
out_dir.display()
);
let status: ExitStatus = command
.status()
.map_err(|e: io::Error| format!("failed to invoke `{EUV_BIN}` build: {e}"))?;
if !status.success() {
return Err(format!("{EUV_BIN} build exited with status {status}"));
}
let index_html: PathBuf = out_dir.join(INDEX_HTML_FILE_NAME);
let not_found_html: PathBuf = out_dir.join(NOT_FOUND_HTML_FILE_NAME);
if index_html.is_file() && !not_found_html.exists() {
fs::copy(&index_html, ¬_found_html)
.map_err(|e: io::Error| format!("copy 404.html: {e}"))?;
}
println!("euv-docs: build complete -> {}", out_dir.display());
Ok(())
}
pub(crate) fn print_usage() {
println!(
"euv-docs {} — build a markdown directory into a static site.\n\n\
USAGE:\n \
euv-docs <SRC_DIR> [--out <OUT_DIR>] [--name <NAME>] [--index-html <FILE>] [--debug]\n\n\
ARGS:\n \
<SRC_DIR> Directory containing *.md files (site config\n is read from the parent README.md frontmatter)\n\n\
OPTIONS:\n \
--out <DIR> Output directory (default: ./dist)\n \
--name <NAME> Wasm package name (default: euv_docs)\n \
--index-html <FILE> Custom index.html template (default: CLI-bundled)\n \
--locale <LOCALE> Build a single locale bundle (prefix `/en/`, dir `en`,\n \
or label); default builds every locale, one bundle\n \
per locale under <OUT_DIR>/<locale>/\n \
--release Use release profile (default)\n \
--debug Use dev profile (faster build, slower runtime)\n \
-h, --help Print this help\n \
-v, --version Print version\n",
env!("CARGO_PKG_VERSION")
);
}