citum_schema_style/style/sections/bibliography.rs
1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Bibliography section specification.
7
8use std::collections::HashMap;
9
10#[cfg(feature = "schema")]
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::grouping;
15use crate::options::BibliographyOptions;
16use crate::template::{
17 LocalizedTemplateSpec, Template, TemplateReference, TemplateVariants, locale_matches,
18};
19
20fn default_true() -> bool {
21 true
22}
23
24/// Bibliography specification.
25#[derive(Debug, Deserialize, Serialize, Clone)]
26#[cfg_attr(feature = "schema", derive(JsonSchema))]
27#[serde(rename_all = "kebab-case")]
28pub struct BibliographySpec {
29 /// Bibliography-specific option overrides merged over the style config.
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub options: Option<BibliographyOptions>,
32 /// Reference to an embedded template preset or external template.
33 ///
34 /// If both `template-ref` and `template` are present, `template` takes precedence.
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub template_ref: Option<TemplateReference>,
37 /// The default template for bibliography entries.
38 /// Default template for entries when no localized override is selected.
39 #[serde(skip_serializing_if = "Option::is_none", default)]
40 pub template: Option<Template>,
41 /// Locale-specific template overrides checked before the default template.
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub locales: Option<Vec<LocalizedTemplateSpec>>,
44 /// Type-specific template overrides. When present, replaces the default
45 /// template for entries of the specified types. Keys are reference type
46 /// names (e.g., "chapter", "article-journal").
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub type_variants: Option<TemplateVariants>,
49 /// Optional global bibliography sorting specification.
50 ///
51 /// When present, used for sorting the flat bibliography or as default
52 /// for groups that don't specify their own sort.
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub sort: Option<grouping::GroupSortEntry>,
55 /// Whether to apply manual `groups:` bibliography grouping.
56 ///
57 /// Defaults to `true`. Set to `false` to disable the `groups:` configuration
58 /// and render a flat bibliography instead. Automatic sort-partition sections
59 /// are unaffected by this toggle.
60 // TODO: consider defaulting to false once grouping matures for publishing workflows
61 #[serde(default = "default_true")]
62 pub groups_enabled: bool,
63 /// Optional bibliography grouping specification.
64 ///
65 /// When present, divides the bibliography into labeled sections with
66 /// optional per-group sorting. Items match the first group whose selector
67 /// evaluates to true (first-match semantics). Omit for flat bibliography.
68 ///
69 /// See `BibliographyGroup` for examples.
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub groups: Option<Vec<grouping::BibliographyGroup>>,
72 /// Custom user-defined fields for extensions.
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub custom: Option<HashMap<String, serde_json::Value>>,
75 /// Forward-compat: captures unknown keys when an older engine reads a
76 /// style produced by a newer schema. Empty by default; treated as a
77 /// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
78 #[serde(
79 flatten,
80 default,
81 skip_serializing_if = "std::collections::BTreeMap::is_empty"
82 )]
83 #[cfg_attr(feature = "schema", schemars(skip))]
84 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
85}
86
87impl Default for BibliographySpec {
88 fn default() -> Self {
89 Self {
90 options: None,
91 template_ref: None,
92 template: None,
93 locales: None,
94 type_variants: None,
95 sort: None,
96 groups_enabled: true,
97 groups: None,
98 custom: None,
99 unknown_fields: std::collections::BTreeMap::new(),
100 }
101 }
102}
103
104impl BibliographySpec {
105 /// Resolve the effective template for this bibliography.
106 ///
107 /// Returns the explicit `template` if present, otherwise resolves `template-ref`.
108 /// Returns `None` if neither is specified.
109 pub fn resolve_template(&self) -> Option<Template> {
110 self.template.clone().or_else(|| {
111 self.template_ref
112 .as_ref()
113 .and_then(TemplateReference::bibliography_template)
114 })
115 }
116
117 /// Resolve the template for a language by checking localized overrides,
118 /// then the localized default, then the base template or preset.
119 pub fn resolve_template_for_language(&self, language: Option<&str>) -> Option<Template> {
120 if let Some(language) = language
121 && let Some(locales) = &self.locales
122 && let Some(matched) = locales.iter().find(|spec| {
123 spec.locale
124 .as_ref()
125 .is_some_and(|targets| locale_matches(targets, language))
126 })
127 {
128 return Some(matched.template.clone());
129 }
130
131 self.locales
132 .as_ref()
133 .and_then(|locales| {
134 locales
135 .iter()
136 .find(|spec| spec.default.unwrap_or(false))
137 .map(|spec| spec.template.clone())
138 })
139 .or_else(|| self.resolve_template())
140 }
141}