Skip to main content

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}