1use crate::Style;
14use crate::embedded::get_embedded_style;
15#[cfg(feature = "schema")]
16use schemars::JsonSchema;
17use serde::{Deserialize, Serialize};
18use std::collections::HashSet;
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[cfg_attr(feature = "schema", derive(JsonSchema))]
28#[serde(rename_all = "kebab-case")]
29#[non_exhaustive]
30pub enum StyleBase {
31 ElsevierHarvardCore,
33 ElsevierWithTitlesCore,
35 ElsevierVancouverCore,
37 SpringerBasicAuthorDateCore,
39 SpringerBasicBracketsCore,
41 SpringerVancouverBracketsCore,
43 TaylorAndFrancisChicagoAuthorDateCore,
45 TaylorAndFrancisCouncilOfScienceEditorsAuthorDateCore,
47 TaylorAndFrancisNationalLibraryOfMedicineCore,
49 ChicagoShortenedNotesBibliographyCore,
51 #[serde(rename = "chicago-notes-18th")]
53 ChicagoNotes18th,
54 #[serde(rename = "chicago-author-date-18th")]
56 ChicagoAuthorDate18th,
57 #[serde(rename = "chicago-shortened-notes-bibliography")]
59 ChicagoShortenedNotesBibliography,
60 #[serde(rename = "apa-7th")]
62 Apa7th,
63 ElsevierHarvard,
65 ElsevierWithTitles,
67 ElsevierVancouver,
69 SpringerBasicAuthorDate,
71 SpringerVancouverBrackets,
73 SpringerBasicBrackets,
75 AmericanMedicalAssociation,
77 Ieee,
79 TaylorAndFrancisChicagoAuthorDate,
81 TaylorAndFrancisCouncilOfScienceEditorsAuthorDate,
83 TaylorAndFrancisNationalLibraryOfMedicine,
85 ModernLanguageAssociation,
87}
88
89impl StyleBase {
90 fn embedded_key(&self) -> &'static str {
92 match self {
93 StyleBase::ElsevierHarvardCore => "elsevier-harvard-core",
94 StyleBase::ElsevierWithTitlesCore => "elsevier-with-titles-core",
95 StyleBase::ElsevierVancouverCore => "elsevier-vancouver-core",
96 StyleBase::SpringerBasicAuthorDateCore => "springer-basic-author-date-core",
97 StyleBase::SpringerBasicBracketsCore => "springer-basic-brackets-core",
98 StyleBase::SpringerVancouverBracketsCore => "springer-vancouver-brackets-core",
99 StyleBase::TaylorAndFrancisChicagoAuthorDateCore => {
100 "taylor-and-francis-chicago-author-date-core"
101 }
102 StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDateCore => {
103 "taylor-and-francis-council-of-science-editors-author-date-core"
104 }
105 StyleBase::TaylorAndFrancisNationalLibraryOfMedicineCore => {
106 "taylor-and-francis-national-library-of-medicine-core"
107 }
108 StyleBase::ChicagoShortenedNotesBibliographyCore => {
109 "chicago-shortened-notes-bibliography-core"
110 }
111 StyleBase::ChicagoNotes18th => "chicago-notes-18th",
112 StyleBase::ChicagoAuthorDate18th => "chicago-author-date-18th",
113 StyleBase::ChicagoShortenedNotesBibliography => "chicago-shortened-notes-bibliography",
114 StyleBase::Apa7th => "apa-7th",
115 StyleBase::ElsevierHarvard => "elsevier-harvard",
116 StyleBase::ElsevierWithTitles => "elsevier-with-titles",
117 StyleBase::ElsevierVancouver => "elsevier-vancouver",
118 StyleBase::SpringerBasicAuthorDate => "springer-basic-author-date",
119 StyleBase::SpringerVancouverBrackets => "springer-vancouver-brackets",
120 StyleBase::SpringerBasicBrackets => "springer-basic-brackets",
121 StyleBase::AmericanMedicalAssociation => "american-medical-association",
122 StyleBase::Ieee => "ieee",
123 StyleBase::TaylorAndFrancisChicagoAuthorDate => {
124 "taylor-and-francis-chicago-author-date"
125 }
126 StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDate => {
127 "taylor-and-francis-council-of-science-editors-author-date"
128 }
129 StyleBase::TaylorAndFrancisNationalLibraryOfMedicine => {
130 "taylor-and-francis-national-library-of-medicine"
131 }
132 StyleBase::ModernLanguageAssociation => "modern-language-association",
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
141#[cfg_attr(feature = "schema", derive(JsonSchema))]
142#[serde(untagged)]
143pub enum StyleReference {
144 Base(StyleBase),
146 Uri(String),
150}
151
152impl StyleReference {
153 pub fn key(&self) -> &str {
155 match self {
156 StyleReference::Base(base) => base.key(),
157 StyleReference::Uri(uri) => uri,
158 }
159 }
160
161 pub fn is_cid(&self) -> bool {
163 matches!(self, StyleReference::Uri(uri) if uri.starts_with("cid:"))
164 }
165}
166
167impl From<StyleBase> for StyleReference {
168 fn from(base: StyleBase) -> Self {
169 StyleReference::Base(base)
170 }
171}
172
173impl StyleBase {
174 #[allow(
179 clippy::panic,
180 reason = "Embedded styles must be valid and present at runtime"
181 )]
182 pub fn base(&self) -> Style {
183 let key = self.embedded_key();
184 get_embedded_style(key)
185 .unwrap_or_else(|| panic!("StyleBase: missing embedded style for key '{key}'"))
186 .unwrap_or_else(|e| panic!("StyleBase: malformed embedded YAML for key '{key}': {e}"))
187 }
188
189 pub fn key(&self) -> &'static str {
191 match self {
192 StyleBase::ElsevierHarvardCore => "elsevier-harvard-core",
193 StyleBase::ElsevierWithTitlesCore => "elsevier-with-titles-core",
194 StyleBase::ElsevierVancouverCore => "elsevier-vancouver-core",
195 StyleBase::SpringerBasicAuthorDateCore => "springer-basic-author-date-core",
196 StyleBase::SpringerBasicBracketsCore => "springer-basic-brackets-core",
197 StyleBase::SpringerVancouverBracketsCore => "springer-vancouver-brackets-core",
198 StyleBase::TaylorAndFrancisChicagoAuthorDateCore => {
199 "taylor-and-francis-chicago-author-date-core"
200 }
201 StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDateCore => {
202 "taylor-and-francis-council-of-science-editors-author-date-core"
203 }
204 StyleBase::TaylorAndFrancisNationalLibraryOfMedicineCore => {
205 "taylor-and-francis-national-library-of-medicine-core"
206 }
207 StyleBase::ChicagoShortenedNotesBibliographyCore => {
208 "chicago-shortened-notes-bibliography-core"
209 }
210 StyleBase::ChicagoNotes18th => "chicago-notes-18th",
211 StyleBase::ChicagoAuthorDate18th => "chicago-author-date-18th",
212 StyleBase::ChicagoShortenedNotesBibliography => "chicago-shortened-notes-bibliography",
213 StyleBase::Apa7th => "apa-7th",
214 StyleBase::ElsevierHarvard => "elsevier-harvard",
215 StyleBase::ElsevierWithTitles => "elsevier-with-titles",
216 StyleBase::ElsevierVancouver => "elsevier-vancouver",
217 StyleBase::SpringerBasicAuthorDate => "springer-basic-author-date",
218 StyleBase::SpringerVancouverBrackets => "springer-vancouver-brackets",
219 StyleBase::SpringerBasicBrackets => "springer-basic-brackets",
220 StyleBase::AmericanMedicalAssociation => "american-medical-association",
221 StyleBase::Ieee => "ieee",
222 StyleBase::TaylorAndFrancisChicagoAuthorDate => {
223 "taylor-and-francis-chicago-author-date"
224 }
225 StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDate => {
226 "taylor-and-francis-council-of-science-editors-author-date"
227 }
228 StyleBase::TaylorAndFrancisNationalLibraryOfMedicine => {
229 "taylor-and-francis-national-library-of-medicine"
230 }
231 StyleBase::ModernLanguageAssociation => "modern-language-association",
232 }
233 }
234
235 pub fn all() -> &'static [StyleBase] {
240 &[
241 StyleBase::ElsevierHarvardCore,
242 StyleBase::ElsevierWithTitlesCore,
243 StyleBase::ElsevierVancouverCore,
244 StyleBase::SpringerBasicAuthorDateCore,
245 StyleBase::SpringerBasicBracketsCore,
246 StyleBase::SpringerVancouverBracketsCore,
247 StyleBase::TaylorAndFrancisChicagoAuthorDateCore,
248 StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDateCore,
249 StyleBase::TaylorAndFrancisNationalLibraryOfMedicineCore,
250 StyleBase::ChicagoShortenedNotesBibliographyCore,
251 StyleBase::ChicagoNotes18th,
252 StyleBase::ChicagoAuthorDate18th,
253 StyleBase::ChicagoShortenedNotesBibliography,
254 StyleBase::Apa7th,
255 StyleBase::ElsevierHarvard,
256 StyleBase::ElsevierWithTitles,
257 StyleBase::ElsevierVancouver,
258 StyleBase::SpringerBasicAuthorDate,
259 StyleBase::SpringerVancouverBrackets,
260 StyleBase::SpringerBasicBrackets,
261 StyleBase::AmericanMedicalAssociation,
262 StyleBase::Ieee,
263 StyleBase::TaylorAndFrancisChicagoAuthorDate,
264 StyleBase::TaylorAndFrancisCouncilOfScienceEditorsAuthorDate,
265 StyleBase::TaylorAndFrancisNationalLibraryOfMedicine,
266 StyleBase::ModernLanguageAssociation,
267 ]
268 }
269
270 pub(crate) fn try_resolve_with_visited(
272 &self,
273 resolver: Option<&crate::StyleResolver>,
274 visited: &mut HashSet<String>,
275 ) -> Result<Style, crate::ResolutionError> {
276 self.base()
277 .try_into_resolved_recursive_with(resolver, visited)
278 }
279}
280
281#[cfg(test)]
282#[allow(
283 clippy::unwrap_used,
284 clippy::expect_used,
285 clippy::panic,
286 clippy::indexing_slicing,
287 clippy::todo,
288 clippy::unimplemented,
289 clippy::unreachable,
290 clippy::get_unwrap,
291 reason = "Panicking is acceptable and often desired in tests."
292)]
293mod tests {
294 use super::*;
295 use crate::options::{Config, PageRangeFormat};
296 use crate::{Style, StyleInfo, TemplateVariant};
297
298 #[test]
299 fn style_base_chicago_notes_base_is_valid() {
300 let style = StyleBase::ChicagoNotes18th.base();
301 let yaml = serde_yaml::to_string(&style).expect("serialization failed");
302 let back: Style = serde_yaml::from_str(&yaml).expect("deserialization failed");
303 assert!(back.info.title.is_some(), "title should be present");
304 assert!(
305 back.citation
306 .as_ref()
307 .and_then(|citation| citation.ibid.as_ref())
308 .is_some()
309 );
310 }
311
312 #[test]
313 fn style_base_chicago_author_date_base_is_valid() {
314 let style = StyleBase::ChicagoAuthorDate18th.base();
315 assert!(style.info.title.is_some(), "title should be present");
316 }
317
318 #[test]
319 fn style_base_apa_7th_base_is_valid() {
320 let style = StyleBase::Apa7th.base();
321 assert!(style.info.title.is_some(), "title should be present");
322 assert!(
323 style.extends.is_none(),
324 "apa-7th is a Tier-1 base and must not extend anything"
325 );
326 let citation = style.citation.as_ref().expect("citation should be present");
327 assert!(
328 citation.template_ref.is_none(),
329 "APA base should carry authored citation templates"
330 );
331 assert!(
332 citation.template.is_none(),
333 "APA base should not define a top-level citation template"
334 );
335 assert!(
336 citation
337 .integral
338 .as_ref()
339 .is_some_and(|i| i.template.is_some()),
340 "APA base should define an authored integral citation template"
341 );
342 assert!(
343 citation
344 .non_integral
345 .as_ref()
346 .is_some_and(|ni| ni.template.is_some()),
347 "APA base should define an authored non-integral citation template"
348 );
349
350 let bibliography = style
351 .bibliography
352 .as_ref()
353 .expect("bibliography should be present");
354 assert!(
355 bibliography.template_ref.is_none(),
356 "APA base should carry authored bibliography templates"
357 );
358 assert!(
359 bibliography.template.is_some(),
360 "APA base should define an authored bibliography template"
361 );
362 assert!(
363 bibliography
364 .type_variants
365 .as_ref()
366 .is_some_and(|variants| !variants.is_empty()),
367 "APA base should define authored bibliography type variants"
368 );
369 }
370
371 #[test]
372 fn style_base_yaml_roundtrip() {
373 let yaml = "chicago-notes-18th";
374 let base: StyleBase = serde_yaml::from_str(yaml).expect("deserialization failed");
375 assert_eq!(base, StyleBase::ChicagoNotes18th);
376
377 let back = serde_yaml::to_string(&base).expect("serialization failed");
378 assert!(back.trim() == "chicago-notes-18th");
379 }
380
381 #[test]
382 fn top_level_null_field_clears_inherited_base_value() {
383 let yaml = r#"
387extends: chicago-notes-18th
388citation:
389 ibid: ~
390"#;
391 let style: Style = Style::from_yaml_str(yaml).expect("style parses");
392 let resolved = style.into_resolved();
393 assert!(
394 resolved
395 .citation
396 .as_ref()
397 .expect("citation present")
398 .ibid
399 .is_none(),
400 "top-level null should clear inherited ibid"
401 );
402 assert!(
403 resolved.citation.as_ref().unwrap().template.is_some(),
404 "top-level override should preserve the inherited template"
405 );
406 }
407
408 #[test]
409 fn local_style_overrides_merge_with_base() {
410 let style = Style {
411 info: StyleInfo {
412 title: Some("Taylor & Francis Test".to_string()),
413 id: Some("tf-test".into()),
414 ..Default::default()
415 },
416 extends: Some(StyleBase::ChicagoAuthorDate18th.into()),
417 options: Some(Config {
418 page_range_format: Some(PageRangeFormat::Expanded),
419 ..Default::default()
420 }),
421 ..Default::default()
422 };
423
424 let resolved = style.into_resolved();
425 let options = resolved
426 .options
427 .expect("resolved options should be present");
428 assert_eq!(options.page_range_format, Some(PageRangeFormat::Expanded));
429 assert!(
430 options.processing.is_some(),
431 "local override should preserve inherited processing"
432 );
433 assert!(
434 resolved.citation.is_some(),
435 "local override should preserve inherited citation spec"
436 );
437 }
438
439 #[test]
440 fn style_base_resolution_materializes_template_v3_variants() {
441 let mut visited = HashSet::new();
442 let resolved = StyleBase::Ieee
443 .try_resolve_with_visited(None, &mut visited)
444 .expect("ieee base resolves");
445 let variants = resolved
446 .bibliography
447 .as_ref()
448 .and_then(|bibliography| bibliography.type_variants.as_ref())
449 .expect("ieee bibliography variants resolve");
450
451 assert!(
452 variants
453 .values()
454 .all(|variant| matches!(variant, TemplateVariant::Full(_)))
455 );
456 }
457
458 #[test]
459 fn style_base_circular_dependency_is_handled() {
460 let mut base = StyleBase::ChicagoNotes18th.base();
461 base.extends = Some(StyleBase::ChicagoNotes18th.into());
462
463 let _ = base.try_into_resolved();
464 }
465
466 #[test]
467 fn all_bases_resolve_cleanly() {
468 for base in StyleBase::all() {
469 let resolved = base.base().into_resolved();
470 assert!(
471 resolved.citation.is_some(),
472 "{} resolved citation missing",
473 base.key()
474 );
475 assert!(
476 resolved.options.is_some(),
477 "{} resolved options missing",
478 base.key()
479 );
480 }
481 }
482
483 #[test]
484 fn tier1_bases_have_no_extends_field() {
485 let tier1 = [
488 StyleBase::Apa7th,
489 StyleBase::ChicagoNotes18th,
490 StyleBase::ChicagoAuthorDate18th,
491 StyleBase::Ieee,
492 StyleBase::AmericanMedicalAssociation,
493 StyleBase::ModernLanguageAssociation,
494 ];
495 for base in &tier1 {
496 assert!(
497 base.base().extends.is_none(),
498 "{} is a Tier-1 base and must not have an extends: field",
499 base.key()
500 );
501 }
502 }
503
504 #[test]
505 fn turabian_pattern_disables_ibid_via_top_level_citation() {
506 let yaml = r#"
509info:
510 title: "Turabian 9th"
511extends: chicago-notes-18th
512citation:
513 ibid: ~
514"#;
515 let style = Style::from_yaml_str(yaml).expect("style parses");
516 let resolved = style.into_resolved();
517 let citation = resolved.citation.expect("citation should be present");
518 assert!(citation.ibid.is_none(), "ibid should be disabled");
519 assert!(
520 citation.template.is_some(),
521 "inherited template should be preserved"
522 );
523 }
524}