use citum_engine::Processor;
use citum_io::LoadedBibliography;
use citum_schema::{Locale, Style, locale::types::LocaleOverride};
use citum_store::platform_config_dir;
use serde::Deserialize;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Deserialize)]
struct RegistrySourceRecord {
name: String,
}
fn registry_sources_path() -> Option<PathBuf> {
platform_config_dir().map(|dir| dir.join("registry-sources.json"))
}
fn configured_registry_path(name: &str) -> Option<PathBuf> {
platform_config_dir().map(|dir| dir.join("registries").join(format!("{name}.yaml")))
}
fn configured_registry_names() -> Vec<String> {
let Some(path) = registry_sources_path() else {
return Vec::new();
};
let Ok(bytes) = fs::read(path) else {
return Vec::new();
};
let Ok(records) = serde_json::from_slice::<Vec<RegistrySourceRecord>>(&bytes) else {
return Vec::new();
};
records.into_iter().map(|record| record.name).collect()
}
fn registry_candidate_names() -> Vec<String> {
let mut candidates = Vec::new();
let local_path = Path::new("citum-registry.yaml");
if local_path.exists()
&& let Ok(registry) = citum_schema::StyleRegistry::load_from_file(local_path)
{
candidates.extend(registry.styles.iter().flat_map(|entry| {
std::iter::once(entry.id.clone()).chain(entry.aliases.iter().cloned())
}));
}
for name in configured_registry_names() {
let Some(path) = configured_registry_path(&name) else {
continue;
};
let Ok(registry) = citum_schema::StyleRegistry::load_from_file(&path) else {
continue;
};
candidates.extend(registry.styles.iter().flat_map(|entry| {
std::iter::once(entry.id.clone()).chain(entry.aliases.iter().cloned())
}));
}
candidates.extend(
citum_schema::embedded::default_registry()
.styles
.iter()
.flat_map(|entry| {
std::iter::once(entry.id.clone()).chain(entry.aliases.iter().cloned())
}),
);
candidates
}
pub(crate) fn load_locale_file(path: &Path) -> Result<Locale, Box<dyn Error>> {
Locale::from_file(path)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err).into())
}
pub(crate) fn create_processor(
style: Style,
loaded: LoadedBibliography,
style_input: &str,
no_semantics: bool,
locale_override: Option<&str>,
) -> Result<Processor, Box<dyn Error>> {
let LoadedBibliography { references, sets } = loaded;
let compound_sets = sets.unwrap_or_default();
let effective_locale = locale_override
.map(str::to_owned)
.or_else(|| style.info.default_locale.clone());
if let Some(ref locale_id) = effective_locale {
let path = Path::new(style_input);
let mut locale = if path.exists() && path.is_file() {
let locales_dir = find_locales_dir(style_input);
let disk_locale = Locale::load(locale_id, &locales_dir);
if disk_locale.locale == *locale_id || locale_id == "en-US" {
disk_locale
} else {
load_locale_builtin(locale_id)
}
} else {
load_locale_builtin(locale_id)
};
if locale_override.is_some() && locale.locale != *locale_id {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("locale not found: '{locale_id}'"),
)
.into());
}
if let Some(override_id) = locale_override
.is_none()
.then(|| {
style
.options
.as_ref()
.and_then(|options| options.locale_override.as_deref())
})
.flatten()
{
let locale_override = if path.exists() && path.is_file() {
load_locale_override_for_file_style(override_id, style_input)?
.or_else(|| load_locale_override_builtin(override_id))
} else {
load_locale_override_builtin(override_id)
};
let locale_override = locale_override.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"locale override not found: '{override_id}' (expected under locales/overrides/)"
),
)
})?;
locale.apply_override(&locale_override);
}
let mut processor =
Processor::try_with_locale_and_compound_sets(style, references, locale, compound_sets)?;
processor.show_semantics = !no_semantics;
Ok(processor)
} else {
let mut processor = Processor::try_with_compound_sets(style, references, compound_sets)?;
processor.show_semantics = !no_semantics;
Ok(processor)
}
}
pub(crate) fn load_any_style(
style_input: &str,
_no_semantics: bool,
) -> Result<Style, Box<dyn Error>> {
use citum_store::resolver::{ResolverError, StyleResolver};
let chain = citum_store::build_standard_chain()
.map_err(|e| -> Box<dyn Error> { Box::new(std::io::Error::other(e.to_string())) })?;
match chain.resolve_style(style_input) {
Ok(style) => {
let mut resolved = style.try_into_resolved_with(Some(&chain))?;
resolved.extends = None;
Ok(resolved)
}
Err(ResolverError::StyleNotFound(_)) => {
let candidates = registry_candidate_names();
let suggestions: Vec<_> = candidates
.iter()
.filter(|name| strsim::jaro_winkler(style_input, name) > 0.8)
.collect();
let mut msg = format!("style not found: '{style_input}'");
if suggestions.is_empty() {
msg.push_str(
"\n\nUse `citum style list` to browse catalog styles, \
or pass a local path/direct URL.",
);
} else {
msg.push_str("\n\nDid you mean one of these?");
for s in suggestions {
msg.push_str("\n - ");
msg.push_str(s);
}
}
Err(msg.into())
}
Err(err) => Err(err.into()),
}
}
pub(crate) fn find_locales_dir(style_path: &str) -> PathBuf {
let style_dir = Path::new(style_path).parent().unwrap_or(Path::new("."));
let candidates = [
style_dir.join("locales"),
style_dir.join("../locales"),
style_dir.join("../../locales"),
PathBuf::from("locales"),
];
for candidate in &candidates {
if candidate.exists() && candidate.is_dir() {
return candidate.clone();
}
}
PathBuf::from(".")
}
pub(crate) fn load_locale_override_for_file_style(
override_id: &str,
style_path: &str,
) -> Result<Option<LocaleOverride>, Box<dyn Error>> {
let overrides_dir = find_locales_dir(style_path).join("overrides");
load_locale_override_from_dir(override_id, &overrides_dir)
}
pub(crate) fn load_locale_builtin(locale_id: &str) -> Locale {
if let Some(bytes) = citum_schema::embedded::get_locale_bytes(locale_id) {
let content = String::from_utf8_lossy(bytes);
Locale::from_yaml_str(&content).unwrap_or_else(|_| Locale::en_us())
} else {
Locale::en_us()
}
}
pub(crate) fn load_locale_override_from_dir(
override_id: &str,
overrides_dir: &Path,
) -> Result<Option<LocaleOverride>, Box<dyn Error>> {
for ext in ["yaml", "yml", "json", "cbor"] {
let path = overrides_dir.join(format!("{override_id}.{ext}"));
if path.exists() && path.is_file() {
return load_locale_override_file(&path).map(Some);
}
}
Ok(None)
}
pub(crate) fn load_locale_override_file(path: &Path) -> Result<LocaleOverride, Box<dyn Error>> {
let bytes = fs::read(path)?;
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("yaml");
parse_locale_override_bytes(&bytes, ext)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err).into())
}
pub(crate) fn load_locale_override_builtin(override_id: &str) -> Option<LocaleOverride> {
let bytes = citum_schema::embedded::get_locale_override_bytes(override_id)?;
parse_locale_override_bytes(bytes, "yaml").ok()
}
pub(crate) fn parse_locale_override_bytes(
bytes: &[u8],
ext: &str,
) -> Result<LocaleOverride, String> {
use citum_schema::locale::raw::RawLocaleOverride;
match ext {
"cbor" => ciborium::de::from_reader::<RawLocaleOverride, _>(std::io::Cursor::new(bytes))
.map(Into::into)
.map_err(|e| format!("Failed to parse CBOR locale override: {e}")),
"json" => serde_json::from_slice::<RawLocaleOverride>(bytes)
.map(Into::into)
.map_err(|e| format!("Failed to parse JSON locale override: {e}")),
_ => serde_yaml::from_slice::<RawLocaleOverride>(bytes)
.map(Into::into)
.map_err(|e| format!("Failed to parse YAML locale override: {e}")),
}
}