Skip to main content

citum_schema_style/options/
integral_name_memory.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6#[cfg(feature = "schema")]
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10/// Integral citation name-memory policy.
11///
12/// The presence of this block enables full-then-short name memory for narrative
13/// (integral) citations. Absence disables it — there is no on/off field.
14#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
15#[cfg_attr(feature = "schema", derive(JsonSchema))]
16#[serde(rename_all = "kebab-case")]
17pub struct IntegralNameMemoryConfig {
18    /// Where the first-mention memory resets.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub scope: Option<IntegralNameScope>,
21    /// Which document contexts participate in name-memory tracking.
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub contexts: Option<IntegralNameContexts>,
24    /// The contributor form to use after the first mention in scope.
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub subsequent_form: Option<SubsequentNameForm>,
27    /// Forward-compat: captures unknown keys when an older engine reads a
28    /// style produced by a newer schema. Empty by default; treated as a
29    /// SoftDegrade signal. See `docs/specs/FORWARD_COMPATIBILITY.md`.
30    #[serde(
31        flatten,
32        default,
33        skip_serializing_if = "std::collections::BTreeMap::is_empty"
34    )]
35    #[cfg_attr(feature = "schema", schemars(skip))]
36    pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
37}
38
39impl IntegralNameMemoryConfig {
40    /// Merge another integral-name-memory config into this one.
41    pub fn merge(&mut self, other: &IntegralNameMemoryConfig) {
42        if other.scope.is_some() {
43            self.scope = other.scope;
44        }
45        if other.contexts.is_some() {
46            self.contexts = other.contexts;
47        }
48        if other.subsequent_form.is_some() {
49            self.subsequent_form = other.subsequent_form;
50        }
51    }
52
53    /// Resolve the effective integral-name-memory config with defaults filled in.
54    pub fn resolve(&self) -> ResolvedIntegralNameMemoryConfig {
55        ResolvedIntegralNameMemoryConfig {
56            scope: self.scope.unwrap_or_default(),
57            contexts: self.contexts.unwrap_or_default(),
58            subsequent_form: self.subsequent_form.unwrap_or_default(),
59        }
60    }
61}
62
63/// The scope where integral citation name-memory resets.
64#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
65#[cfg_attr(feature = "schema", derive(JsonSchema))]
66#[serde(rename_all = "kebab-case")]
67pub enum IntegralNameScope {
68    /// Keep one name-memory scope for the whole document.
69    #[default]
70    Document,
71    /// Reset name memory at chapter boundaries.
72    Chapter,
73    /// Reset name memory at section boundaries.
74    Section,
75}
76
77/// Which document contexts participate in integral citation name memory.
78#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
79#[cfg_attr(feature = "schema", derive(JsonSchema))]
80#[serde(rename_all = "kebab-case")]
81pub enum IntegralNameContexts {
82    /// Only body-text integral citations participate.
83    #[default]
84    BodyOnly,
85    /// Body text and note citations both participate.
86    BodyAndNotes,
87}
88
89/// The contributor form used after the first integral mention in scope.
90///
91/// `Short` preserves non-dropping particles ("van Beethoven"); `FamilyOnly`
92/// strips them ("Beethoven"). MLA-style narrative memory uses `Short`.
93#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
94#[cfg_attr(feature = "schema", derive(JsonSchema))]
95#[serde(rename_all = "kebab-case")]
96pub enum SubsequentNameForm {
97    /// Use the short contributor form for subsequent mentions.
98    #[default]
99    Short,
100    /// Use family name only (without non-dropping particles) for subsequent mentions.
101    FamilyOnly,
102}
103
104/// How to display a contributor's short name on the first integral mention.
105#[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
106#[cfg_attr(feature = "schema", derive(JsonSchema))]
107#[serde(rename_all = "kebab-case")]
108pub enum ShortNameDisplay {
109    /// "Full Name (Short)" on first mention (default). For narrative/integral context.
110    #[default]
111    FullThenParenthetical,
112    /// `"Full Name [Short]"` on first mention. For parenthetical context (already inside parens).
113    FullThenBracketed,
114    /// "Short (Full Name)" on first mention.
115    ShortThenParenthetical,
116    /// "Short [Full Name]" on first mention.
117    ShortThenBracketed,
118}
119
120#[cfg(test)]
121#[allow(clippy::unwrap_used, reason = "Panicking is acceptable in tests.")]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn captures_unknown_fields_for_forward_compat() {
127        let yaml = r#"
128scope: chapter
129future-key: true
130"#;
131        let cfg: IntegralNameMemoryConfig = serde_yaml::from_str(yaml).unwrap();
132        assert!(cfg.unknown_fields.contains_key("future-key"));
133        assert_eq!(cfg.scope, Some(IntegralNameScope::Chapter));
134    }
135}
136
137/// Integral-name-memory configuration with defaults resolved.
138#[derive(Debug, PartialEq, Eq, Clone, Copy)]
139pub struct ResolvedIntegralNameMemoryConfig {
140    /// The active scope boundary.
141    pub scope: IntegralNameScope,
142    /// The active context participation mode.
143    pub contexts: IntegralNameContexts,
144    /// The contributor form used after the first mention.
145    pub subsequent_form: SubsequentNameForm,
146}
147
148/// Organizational name abbreviation expansion policy.
149///
150/// The presence of this block enables first-mention expansion of org names —
151/// "World Health Organization (WHO)" on first mention, "WHO" thereafter.
152/// Absence disables org abbreviation entirely.
153#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)]
154#[cfg_attr(feature = "schema", derive(JsonSchema))]
155#[serde(rename_all = "kebab-case")]
156pub struct OrgAbbreviationMemoryConfig {
157    /// Where the first-mention memory resets.
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub scope: Option<IntegralNameScope>,
160    /// Which document contexts participate in org-abbreviation tracking.
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub contexts: Option<IntegralNameContexts>,
163    /// How to display an organizational short name on the first integral mention.
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub short_name_display: Option<ShortNameDisplay>,
166    /// Forward-compat: captures unknown keys from newer schema versions.
167    #[serde(
168        flatten,
169        default,
170        skip_serializing_if = "std::collections::BTreeMap::is_empty"
171    )]
172    #[cfg_attr(feature = "schema", schemars(skip))]
173    pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
174}
175
176impl OrgAbbreviationMemoryConfig {
177    /// Merge another org-abbreviation config into this one.
178    pub fn merge(&mut self, other: &OrgAbbreviationMemoryConfig) {
179        if other.scope.is_some() {
180            self.scope = other.scope;
181        }
182        if other.contexts.is_some() {
183            self.contexts = other.contexts;
184        }
185        if other.short_name_display.is_some() {
186            self.short_name_display = other.short_name_display;
187        }
188    }
189
190    /// Resolve the effective org-abbreviation config with defaults filled in.
191    pub fn resolve(&self) -> ResolvedOrgAbbreviationMemoryConfig {
192        ResolvedOrgAbbreviationMemoryConfig {
193            scope: self.scope.unwrap_or_default(),
194            contexts: self.contexts.unwrap_or_default(),
195            short_name_display: self.short_name_display.unwrap_or_default(),
196        }
197    }
198}
199
200/// Org-abbreviation-memory configuration with defaults resolved.
201#[derive(Debug, PartialEq, Eq, Clone, Copy)]
202pub struct ResolvedOrgAbbreviationMemoryConfig {
203    /// The active scope boundary.
204    pub scope: IntegralNameScope,
205    /// The active context participation mode.
206    pub contexts: IntegralNameContexts,
207    /// How to display short names on first mention.
208    pub short_name_display: ShortNameDisplay,
209}