use crate::styling::{
DocumentConfig, ResolveError, ResolvedStyle, merge::resolve_with_overrides,
};
use std::fs;
use std::path::Path;
#[derive(Debug, Clone)]
pub enum ConfigSource<'a> {
Default,
Theme(&'a str),
File(&'a str),
Embedded(&'a str),
}
pub fn load_config_strict(
source: ConfigSource,
theme_override: Option<&str>,
) -> Result<ResolvedStyle, ResolveError> {
load_config_strict_with_overrides(source, theme_override, None)
}
pub fn load_config_strict_with_overrides(
source: ConfigSource,
theme_override: Option<&str>,
overrides_toml: Option<&str>,
) -> Result<ResolvedStyle, ResolveError> {
let overrides: Option<DocumentConfig> = match overrides_toml {
Some(text) if !text.trim().is_empty() => {
let parsed = toml::from_str(text).map_err(|source| {
let suggestion =
crate::styling::error::unknown_field_suggestion(source.message());
ResolveError::BadToml {
source,
input: text.to_string(),
file: None,
suggestion,
}
})?;
Some(parsed)
}
_ => None,
};
let (toml_text, file_for_errors) = match source {
ConfigSource::Default => {
return resolve_with_overrides(
DocumentConfig::default(),
theme_override,
overrides,
);
}
ConfigSource::Theme(name) => {
let theme = theme_override.or(Some(name));
return resolve_with_overrides(DocumentConfig::default(), theme, overrides);
}
ConfigSource::File(path) => {
let p = Path::new(path).to_path_buf();
let text = fs::read_to_string(&p).map_err(|source| ResolveError::Io {
path: p.clone(),
source,
})?;
(text, Some(p))
}
ConfigSource::Embedded(s) => (s.to_string(), None),
};
let user: DocumentConfig = toml::from_str(&toml_text).map_err(|source| {
let suggestion = crate::styling::error::unknown_field_suggestion(source.message());
ResolveError::BadToml {
source,
input: toml_text.clone(),
file: file_for_errors,
suggestion,
}
})?;
resolve_with_overrides(user, theme_override, overrides)
}
pub fn load_config_from_source(source: ConfigSource) -> ResolvedStyle {
match load_config_strict(source, None) {
Ok(style) => style,
Err(e) => {
log::warn!(
"could not load config; falling back to built-in default theme: {}",
e
);
ResolvedStyle::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_source_loads_built_in_theme() {
let style = load_config_from_source(ConfigSource::Default);
assert_eq!(style.paragraph.font_size_pt, 8.0);
assert_eq!(style.headings[0].font_size_pt, 14.0);
}
#[test]
fn nonexistent_file_falls_back_to_default() {
let style = load_config_from_source(ConfigSource::File("nonexistent.toml"));
assert_eq!(style.paragraph.font_size_pt, 8.0);
}
#[test]
fn embedded_config_overrides_paragraph_font_size() {
let style = load_config_from_source(ConfigSource::Embedded(
r#"
[paragraph]
font_size_pt = 11.0
"#,
));
assert_eq!(style.paragraph.font_size_pt, 11.0);
}
#[test]
fn theme_preset_override() {
let style = load_config_strict(ConfigSource::Default, Some("github")).unwrap();
assert_eq!(style.paragraph.font_size_pt, 10.0);
}
#[test]
fn theme_source_picks_named_preset() {
let style = load_config_strict(ConfigSource::Theme("github"), None).unwrap();
assert_eq!(style.paragraph.font_size_pt, 10.0);
}
#[test]
fn theme_source_unknown_returns_typed_error() {
let err = load_config_strict(ConfigSource::Theme("doesnotexist"), None);
match err {
Err(ResolveError::UnknownTheme { name, .. }) => assert_eq!(name, "doesnotexist"),
other => panic!("expected UnknownTheme, got {:?}", other),
}
}
#[test]
fn theme_source_falls_back_via_load_from_source() {
let style = load_config_from_source(ConfigSource::Theme("doesnotexist"));
assert_eq!(style.paragraph.font_size_pt, 8.0);
}
#[test]
fn unknown_theme_returns_typed_error() {
let err = load_config_strict(ConfigSource::Default, Some("doesnotexist"));
match err {
Err(ResolveError::UnknownTheme { name, .. }) => assert_eq!(name, "doesnotexist"),
other => panic!("expected UnknownTheme, got {:?}", other),
}
}
#[test]
fn invalid_toml_returns_typed_error() {
let err = load_config_strict(ConfigSource::Embedded("not valid toml {{{"), None);
match err {
Err(ResolveError::BadToml { .. }) => {}
other => panic!("expected BadToml, got {:?}", other),
}
}
#[test]
fn unknown_field_returns_typed_error() {
let err = load_config_strict(
ConfigSource::Embedded(
r##"
[paragraph]
texcolor = "#000000"
"##,
),
None,
);
assert!(matches!(err, Err(ResolveError::BadToml { .. })));
}
#[test]
fn overrides_none_equals_no_overrides() {
let a = load_config_strict(ConfigSource::Default, None).unwrap();
let b =
load_config_strict_with_overrides(ConfigSource::Default, None, None).unwrap();
assert_eq!(a.paragraph.font_size_pt, b.paragraph.font_size_pt);
assert_eq!(a.headings[0].font_size_pt, b.headings[0].font_size_pt);
}
#[test]
fn override_beats_embedded_config_file() {
let style = load_config_strict_with_overrides(
ConfigSource::Embedded("[paragraph]\nfont_size_pt = 9.0\n"),
None,
Some("paragraph.font_size_pt = 11.0"),
)
.unwrap();
assert_eq!(style.paragraph.font_size_pt, 11.0);
}
#[test]
fn override_beats_theme_preset() {
let style = load_config_strict_with_overrides(
ConfigSource::Theme("github"),
None,
Some("defaults.font_size_pt = 13.0"),
)
.unwrap();
assert_eq!(style.paragraph.font_size_pt, 13.0);
}
#[test]
fn override_dotted_heading_key() {
let style = load_config_strict_with_overrides(
ConfigSource::Default,
None,
Some("headings.h1.font_size_pt = 28.0"),
)
.unwrap();
assert_eq!(style.headings[0].font_size_pt, 28.0);
}
#[test]
fn override_color_value() {
let style = load_config_strict_with_overrides(
ConfigSource::Default,
None,
Some("blockquote.text_color = \"#888888\""),
)
.unwrap();
assert_eq!(
style.blockquote.text_color,
crate::styling::Color::rgb(0x88, 0x88, 0x88)
);
}
#[test]
fn override_uniform_margins_scalar() {
let style = load_config_strict_with_overrides(
ConfigSource::Default,
None,
Some("page.margins = 25.0"),
)
.unwrap();
assert_eq!(style.page.margins_mm.top, 25.0);
assert_eq!(style.page.margins_mm.left, 25.0);
}
#[test]
fn override_metadata_and_footer() {
let style = load_config_strict_with_overrides(
ConfigSource::Default,
None,
Some(
"metadata.title = \"My Report\"\n\
footer.center = \"{page} / {total_pages}\"",
),
)
.unwrap();
assert_eq!(style.metadata.title.as_deref(), Some("My Report"));
assert_eq!(
style.footer.as_ref().and_then(|f| f.center.clone()),
Some("{page} / {total_pages}".to_string())
);
}
#[test]
fn invalid_override_key_is_typed_error_not_panic() {
let err = load_config_strict_with_overrides(
ConfigSource::Default,
None,
Some("bogus.key = 1"),
);
assert!(matches!(err, Err(ResolveError::BadToml { .. })));
}
#[test]
fn empty_override_fragment_is_noop() {
let a = load_config_strict(ConfigSource::Default, None).unwrap();
let b = load_config_strict_with_overrides(
ConfigSource::Default,
None,
Some(" \n "),
)
.unwrap();
assert_eq!(a.paragraph.font_size_pt, b.paragraph.font_size_pt);
}
#[test]
fn override_layers_on_top_of_theme_override_arg() {
let style = load_config_strict_with_overrides(
ConfigSource::Default,
Some("github"),
Some("defaults.font_size_pt = 7.5"),
)
.unwrap();
assert_eq!(style.paragraph.font_size_pt, 7.5);
}
}