use crate::Style;
use crate::embedded::get_embedded_style;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum StyleBase {
ElsevierHarvardCore,
ElsevierWithTitlesCore,
ElsevierVancouverCore,
SpringerBasicAuthorDateCore,
SpringerBasicBracketsCore,
SpringerVancouverBracketsCore,
TaylorAndFrancisChicagoAuthorDateCore,
TaylorAndFrancisCouncilOfScienceEditorsAuthorDateCore,
TaylorAndFrancisNationalLibraryOfMedicineCore,
ChicagoShortenedNotesBibliographyCore,
#[serde(rename = "chicago-notes-18th")]
ChicagoNotes18th,
#[serde(rename = "chicago-author-date-18th")]
ChicagoAuthorDate18th,
#[serde(rename = "chicago-shortened-notes-bibliography")]
ChicagoShortenedNotesBibliography,
#[serde(rename = "apa-7th")]
Apa7th,
ElsevierHarvard,
ElsevierWithTitles,
ElsevierVancouver,
SpringerBasicAuthorDate,
SpringerVancouverBrackets,
SpringerBasicBrackets,
AmericanMedicalAssociation,
Ieee,
TaylorAndFrancisChicagoAuthorDate,
TaylorAndFrancisCouncilOfScienceEditorsAuthorDate,
TaylorAndFrancisNationalLibraryOfMedicine,
ModernLanguageAssociation,
}
impl StyleBase {
fn embedded_key(&self) -> &'static str {
match self {
StyleBase::ElsevierHarvardCore => "elsevier-harvard-core",
StyleBase::ElsevierWithTitlesCore => "elsevier-with-titles-core",
StyleBase::ElsevierVancouverCore => "elsevier-vancouver-core",
StyleBase::SpringerBasicAuthorDateCore => "springer-basic-author-date-core",
StyleBase::SpringerBasicBracketsCore => "springer-basic-brackets-core",
StyleBase::SpringerVancouverBracketsCore => "springer-vancouver-brackets-core",
StyleBase::TaylorAndFrancisChicagoAuthorDateCore => {
"taylor-and-francis-chicago-author-date-core"
}
StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDateCore => {
"taylor-and-francis-council-of-science-editors-author-date-core"
}
StyleBase::TaylorAndFrancisNationalLibraryOfMedicineCore => {
"taylor-and-francis-national-library-of-medicine-core"
}
StyleBase::ChicagoShortenedNotesBibliographyCore => {
"chicago-shortened-notes-bibliography-core"
}
StyleBase::ChicagoNotes18th => "chicago-notes-18th",
StyleBase::ChicagoAuthorDate18th => "chicago-author-date-18th",
StyleBase::ChicagoShortenedNotesBibliography => "chicago-shortened-notes-bibliography",
StyleBase::Apa7th => "apa-7th",
StyleBase::ElsevierHarvard => "elsevier-harvard",
StyleBase::ElsevierWithTitles => "elsevier-with-titles",
StyleBase::ElsevierVancouver => "elsevier-vancouver",
StyleBase::SpringerBasicAuthorDate => "springer-basic-author-date",
StyleBase::SpringerVancouverBrackets => "springer-vancouver-brackets",
StyleBase::SpringerBasicBrackets => "springer-basic-brackets",
StyleBase::AmericanMedicalAssociation => "american-medical-association",
StyleBase::Ieee => "ieee",
StyleBase::TaylorAndFrancisChicagoAuthorDate => {
"taylor-and-francis-chicago-author-date"
}
StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDate => {
"taylor-and-francis-council-of-science-editors-author-date"
}
StyleBase::TaylorAndFrancisNationalLibraryOfMedicine => {
"taylor-and-francis-national-library-of-medicine"
}
StyleBase::ModernLanguageAssociation => "modern-language-association",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum StyleReference {
Base(StyleBase),
Uri(String),
}
impl StyleReference {
pub fn key(&self) -> &str {
match self {
StyleReference::Base(base) => base.key(),
StyleReference::Uri(uri) => uri,
}
}
pub fn is_cid(&self) -> bool {
matches!(self, StyleReference::Uri(uri) if uri.starts_with("cid:"))
}
}
impl From<StyleBase> for StyleReference {
fn from(base: StyleBase) -> Self {
StyleReference::Base(base)
}
}
impl StyleBase {
#[allow(
clippy::panic,
reason = "Embedded styles must be valid and present at runtime"
)]
pub fn base(&self) -> Style {
let key = self.embedded_key();
get_embedded_style(key)
.unwrap_or_else(|| panic!("StyleBase: missing embedded style for key '{key}'"))
.unwrap_or_else(|e| panic!("StyleBase: malformed embedded YAML for key '{key}': {e}"))
}
pub fn key(&self) -> &'static str {
match self {
StyleBase::ElsevierHarvardCore => "elsevier-harvard-core",
StyleBase::ElsevierWithTitlesCore => "elsevier-with-titles-core",
StyleBase::ElsevierVancouverCore => "elsevier-vancouver-core",
StyleBase::SpringerBasicAuthorDateCore => "springer-basic-author-date-core",
StyleBase::SpringerBasicBracketsCore => "springer-basic-brackets-core",
StyleBase::SpringerVancouverBracketsCore => "springer-vancouver-brackets-core",
StyleBase::TaylorAndFrancisChicagoAuthorDateCore => {
"taylor-and-francis-chicago-author-date-core"
}
StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDateCore => {
"taylor-and-francis-council-of-science-editors-author-date-core"
}
StyleBase::TaylorAndFrancisNationalLibraryOfMedicineCore => {
"taylor-and-francis-national-library-of-medicine-core"
}
StyleBase::ChicagoShortenedNotesBibliographyCore => {
"chicago-shortened-notes-bibliography-core"
}
StyleBase::ChicagoNotes18th => "chicago-notes-18th",
StyleBase::ChicagoAuthorDate18th => "chicago-author-date-18th",
StyleBase::ChicagoShortenedNotesBibliography => "chicago-shortened-notes-bibliography",
StyleBase::Apa7th => "apa-7th",
StyleBase::ElsevierHarvard => "elsevier-harvard",
StyleBase::ElsevierWithTitles => "elsevier-with-titles",
StyleBase::ElsevierVancouver => "elsevier-vancouver",
StyleBase::SpringerBasicAuthorDate => "springer-basic-author-date",
StyleBase::SpringerVancouverBrackets => "springer-vancouver-brackets",
StyleBase::SpringerBasicBrackets => "springer-basic-brackets",
StyleBase::AmericanMedicalAssociation => "american-medical-association",
StyleBase::Ieee => "ieee",
StyleBase::TaylorAndFrancisChicagoAuthorDate => {
"taylor-and-francis-chicago-author-date"
}
StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDate => {
"taylor-and-francis-council-of-science-editors-author-date"
}
StyleBase::TaylorAndFrancisNationalLibraryOfMedicine => {
"taylor-and-francis-national-library-of-medicine"
}
StyleBase::ModernLanguageAssociation => "modern-language-association",
}
}
pub fn all() -> &'static [StyleBase] {
&[
StyleBase::ElsevierHarvardCore,
StyleBase::ElsevierWithTitlesCore,
StyleBase::ElsevierVancouverCore,
StyleBase::SpringerBasicAuthorDateCore,
StyleBase::SpringerBasicBracketsCore,
StyleBase::SpringerVancouverBracketsCore,
StyleBase::TaylorAndFrancisChicagoAuthorDateCore,
StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDateCore,
StyleBase::TaylorAndFrancisNationalLibraryOfMedicineCore,
StyleBase::ChicagoShortenedNotesBibliographyCore,
StyleBase::ChicagoNotes18th,
StyleBase::ChicagoAuthorDate18th,
StyleBase::ChicagoShortenedNotesBibliography,
StyleBase::Apa7th,
StyleBase::ElsevierHarvard,
StyleBase::ElsevierWithTitles,
StyleBase::ElsevierVancouver,
StyleBase::SpringerBasicAuthorDate,
StyleBase::SpringerVancouverBrackets,
StyleBase::SpringerBasicBrackets,
StyleBase::AmericanMedicalAssociation,
StyleBase::Ieee,
StyleBase::TaylorAndFrancisChicagoAuthorDate,
StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDate,
StyleBase::TaylorAndFrancisNationalLibraryOfMedicine,
StyleBase::ModernLanguageAssociation,
]
}
pub(crate) fn try_resolve_with_visited(
&self,
resolver: Option<&crate::StyleResolver>,
visited: &mut HashSet<String>,
) -> Result<Style, crate::ResolutionError> {
self.base()
.try_into_resolved_recursive_with(resolver, visited)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing,
clippy::todo,
clippy::unimplemented,
clippy::unreachable,
clippy::get_unwrap,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
use crate::options::{Config, PageRangeFormat};
use crate::{Style, StyleInfo, TemplateVariant};
#[test]
fn style_base_chicago_notes_base_is_valid() {
let style = StyleBase::ChicagoNotes18th.base();
let yaml = serde_yaml::to_string(&style).expect("serialization failed");
let back: Style = serde_yaml::from_str(&yaml).expect("deserialization failed");
assert!(back.info.title.is_some(), "title should be present");
assert!(
back.citation
.as_ref()
.and_then(|citation| citation.ibid.as_ref())
.is_some()
);
}
#[test]
fn style_base_chicago_author_date_base_is_valid() {
let style = StyleBase::ChicagoAuthorDate18th.base();
assert!(style.info.title.is_some(), "title should be present");
}
#[test]
fn style_base_apa_7th_base_is_valid() {
let style = StyleBase::Apa7th.base();
assert!(style.info.title.is_some(), "title should be present");
assert!(
style.extends.is_none(),
"apa-7th is a Tier-1 base and must not extend anything"
);
let citation = style.citation.as_ref().expect("citation should be present");
assert!(
citation.template_ref.is_none(),
"APA base should carry authored citation templates"
);
assert!(
citation.template.is_none(),
"APA base should not define a top-level citation template"
);
assert!(
citation
.integral
.as_ref()
.is_some_and(|i| i.template.is_some()),
"APA base should define an authored integral citation template"
);
assert!(
citation
.non_integral
.as_ref()
.is_some_and(|ni| ni.template.is_some()),
"APA base should define an authored non-integral citation template"
);
let bibliography = style
.bibliography
.as_ref()
.expect("bibliography should be present");
assert!(
bibliography.template_ref.is_none(),
"APA base should carry authored bibliography templates"
);
assert!(
bibliography.template.is_some(),
"APA base should define an authored bibliography template"
);
assert!(
bibliography
.type_variants
.as_ref()
.is_some_and(|variants| !variants.is_empty()),
"APA base should define authored bibliography type variants"
);
}
#[test]
fn style_base_yaml_roundtrip() {
let yaml = "chicago-notes-18th";
let base: StyleBase = serde_yaml::from_str(yaml).expect("deserialization failed");
assert_eq!(base, StyleBase::ChicagoNotes18th);
let back = serde_yaml::to_string(&base).expect("serialization failed");
assert!(back.trim() == "chicago-notes-18th");
}
#[test]
fn top_level_null_field_clears_inherited_base_value() {
let yaml = r#"
extends: chicago-notes-18th
citation:
ibid: ~
"#;
let style: Style = Style::from_yaml_str(yaml).expect("style parses");
let resolved = style.into_resolved();
assert!(
resolved
.citation
.as_ref()
.expect("citation present")
.ibid
.is_none(),
"top-level null should clear inherited ibid"
);
assert!(
resolved.citation.as_ref().unwrap().template.is_some(),
"top-level override should preserve the inherited template"
);
}
#[test]
fn local_style_overrides_merge_with_base() {
let style = Style {
info: StyleInfo {
title: Some("Taylor & Francis Test".to_string()),
id: Some("tf-test".into()),
..Default::default()
},
extends: Some(StyleBase::ChicagoAuthorDate18th.into()),
options: Some(Config {
page_range_format: Some(PageRangeFormat::Expanded),
..Default::default()
}),
..Default::default()
};
let resolved = style.into_resolved();
let options = resolved
.options
.expect("resolved options should be present");
assert_eq!(options.page_range_format, Some(PageRangeFormat::Expanded));
assert!(
options.processing.is_some(),
"local override should preserve inherited processing"
);
assert!(
resolved.citation.is_some(),
"local override should preserve inherited citation spec"
);
}
#[test]
fn style_base_resolution_materializes_template_v3_variants() {
let mut visited = HashSet::new();
let resolved = StyleBase::Ieee
.try_resolve_with_visited(None, &mut visited)
.expect("ieee base resolves");
let variants = resolved
.bibliography
.as_ref()
.and_then(|bibliography| bibliography.type_variants.as_ref())
.expect("ieee bibliography variants resolve");
assert!(
variants
.values()
.all(|variant| matches!(variant, TemplateVariant::Full(_)))
);
}
#[test]
fn style_base_circular_dependency_is_handled() {
let mut base = StyleBase::ChicagoNotes18th.base();
base.extends = Some(StyleBase::ChicagoNotes18th.into());
let _ = base.try_into_resolved();
}
#[test]
fn all_bases_resolve_cleanly() {
for base in StyleBase::all() {
let resolved = base.base().into_resolved();
assert!(
resolved.citation.is_some(),
"{} resolved citation missing",
base.key()
);
assert!(
resolved.options.is_some(),
"{} resolved options missing",
base.key()
);
}
}
#[test]
fn tier1_bases_have_no_extends_field() {
let tier1 = [
StyleBase::Apa7th,
StyleBase::ChicagoNotes18th,
StyleBase::ChicagoAuthorDate18th,
StyleBase::Ieee,
StyleBase::AmericanMedicalAssociation,
StyleBase::ModernLanguageAssociation,
];
for base in &tier1 {
assert!(
base.base().extends.is_none(),
"{} is a Tier-1 base and must not have an extends: field",
base.key()
);
}
}
#[test]
fn turabian_pattern_disables_ibid_via_top_level_citation() {
let yaml = r#"
info:
title: "Turabian 9th"
extends: chicago-notes-18th
citation:
ibid: ~
"#;
let style = Style::from_yaml_str(yaml).expect("style parses");
let resolved = style.into_resolved();
let citation = resolved.citation.expect("citation should be present");
assert!(citation.ibid.is_none(), "ibid should be disabled");
assert!(
citation.template.is_some(),
"inherited template should be preserved"
);
}
}