citum_schema_style/style/sections/
citation.rs1use std::collections::HashMap;
9
10#[cfg(feature = "schema")]
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13
14use crate::grouping;
15use crate::options::CitationOptions;
16use crate::template::{
17 LocalizedTemplateSpec, Template, TemplateReference, TemplateVariants, locale_matches,
18};
19
20#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
22#[cfg_attr(feature = "schema", derive(JsonSchema))]
23#[serde(rename_all = "kebab-case")]
24pub enum CitationCollapse {
25 CitationNumber,
27}
28
29#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
31#[cfg_attr(feature = "schema", derive(JsonSchema))]
32#[serde(rename_all = "kebab-case")]
33pub enum NoteStartTextCase {
34 CapitalizeFirst,
36 Lowercase,
38}
39
40#[derive(Debug, Deserialize, Serialize, Clone, Default)]
42#[cfg_attr(feature = "schema", derive(JsonSchema))]
43#[serde(rename_all = "kebab-case")]
44pub struct CitationSpec {
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub options: Option<CitationOptions>,
48 #[serde(skip_serializing_if = "Option::is_none")]
52 pub template_ref: Option<TemplateReference>,
53 #[serde(skip_serializing_if = "Option::is_none", default)]
55 pub template: Option<Template>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub locales: Option<Vec<LocalizedTemplateSpec>>,
59 #[serde(skip_serializing_if = "Option::is_none", rename = "type-variants")]
65 pub type_variants: Option<TemplateVariants>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub wrap: Option<crate::template::WrapConfig>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub prefix: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub suffix: Option<String>,
75 #[serde(skip_serializing_if = "Option::is_none")]
78 pub delimiter: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
82 #[serde(rename = "multi-cite-delimiter")]
83 pub multi_cite_delimiter: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub collapse: Option<CitationCollapse>,
87 #[serde(skip_serializing_if = "Option::is_none")]
89 pub sort: Option<grouping::GroupSortEntry>,
90 #[serde(skip_serializing_if = "Option::is_none")]
93 pub integral: Option<Box<CitationSpec>>,
94 #[serde(skip_serializing_if = "Option::is_none")]
97 pub non_integral: Option<Box<CitationSpec>>,
98 #[serde(skip_serializing_if = "Option::is_none")]
103 pub subsequent: Option<Box<CitationSpec>>,
104 #[serde(skip_serializing_if = "Option::is_none")]
109 pub ibid: Option<Box<CitationSpec>>,
110 #[serde(skip_serializing_if = "Option::is_none")]
115 pub note_start_text_case: Option<NoteStartTextCase>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub custom: Option<HashMap<String, serde_json::Value>>,
119 #[serde(
123 flatten,
124 default,
125 skip_serializing_if = "std::collections::BTreeMap::is_empty"
126 )]
127 #[cfg_attr(feature = "schema", schemars(skip))]
128 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
129}
130
131impl CitationSpec {
132 pub fn resolve_template(&self) -> Option<Template> {
137 self.template.clone().or_else(|| {
138 self.template_ref
139 .as_ref()
140 .and_then(TemplateReference::citation_template)
141 })
142 }
143
144 pub fn resolve_template_for_language(&self, language: Option<&str>) -> Option<Template> {
147 if let Some(language) = language
148 && let Some(locales) = &self.locales
149 && let Some(matched) = locales.iter().find(|spec| {
150 spec.locale
151 .as_ref()
152 .is_some_and(|targets| locale_matches(targets, language))
153 })
154 {
155 return Some(matched.template.clone());
156 }
157
158 self.locales
159 .as_ref()
160 .and_then(|locales| {
161 locales
162 .iter()
163 .find(|spec| spec.default.unwrap_or(false))
164 .map(|spec| spec.template.clone())
165 })
166 .or_else(|| self.resolve_template())
167 }
168
169 pub fn resolve_template_for_type(
175 &self,
176 ref_type: &str,
177 language: Option<&str>,
178 ) -> Option<Template> {
179 if let Some(type_variants) = &self.type_variants {
180 for (selector, variant) in type_variants {
181 if selector.matches(ref_type) {
182 return variant.clone().into_template();
183 }
184 }
185 }
186 self.resolve_template_for_language(language)
187 }
188
189 pub fn resolve_for_mode(
194 &self,
195 mode: &crate::citation::CitationMode,
196 ) -> std::borrow::Cow<'_, CitationSpec> {
197 use crate::citation::CitationMode;
198 let mode_spec = match mode {
199 CitationMode::Integral => self.integral.as_ref(),
200 CitationMode::NonIntegral => self.non_integral.as_ref(),
201 };
202
203 match mode_spec {
204 Some(spec) => {
205 let mut merged = self.clone();
207 merged.integral = None;
209 merged.non_integral = None;
210
211 if spec.options.is_some() {
212 merged.options = spec.options.clone();
213 }
214 if spec.template_ref.is_some() {
215 merged.template_ref = spec.template_ref.clone();
216 }
217 if spec.template.is_some() {
218 merged.template = spec.template.clone();
219 }
220 if spec.locales.is_some() {
221 merged.locales = spec.locales.clone();
222 }
223 if spec.type_variants.is_some() {
224 merged.type_variants = spec.type_variants.clone();
225 }
226 if spec.wrap.is_some() {
227 merged.wrap = spec.wrap.clone();
228 }
229 if spec.prefix.is_some() {
230 merged.prefix = spec.prefix.clone();
231 }
232 if spec.suffix.is_some() {
233 merged.suffix = spec.suffix.clone();
234 }
235 if spec.delimiter.is_some() {
236 merged.delimiter = spec.delimiter.clone();
237 }
238 if spec.multi_cite_delimiter.is_some() {
239 merged.multi_cite_delimiter = spec.multi_cite_delimiter.clone();
240 }
241 if spec.collapse.is_some() {
242 merged.collapse = spec.collapse.clone();
243 }
244 if spec.sort.is_some() {
245 merged.sort = spec.sort.clone();
246 }
247 if spec.note_start_text_case.is_some() {
248 merged.note_start_text_case = spec.note_start_text_case;
249 }
250
251 std::borrow::Cow::Owned(merged)
252 }
253 None => std::borrow::Cow::Borrowed(self),
254 }
255 }
256
257 pub fn resolve_for_position(
265 &self,
266 position: Option<&crate::citation::Position>,
267 ) -> std::borrow::Cow<'_, CitationSpec> {
268 use crate::citation::Position;
269
270 let position_spec = match position {
271 Some(Position::Ibid | Position::IbidWithLocator) => {
272 self.ibid.as_ref().or(self.subsequent.as_ref())
273 }
274 Some(Position::Subsequent) => self.subsequent.as_ref(),
275 Some(Position::First) | None => None,
276 };
277
278 match position_spec {
279 Some(spec) => {
280 let mut merged = self.clone();
282 merged.subsequent = None;
284 merged.ibid = None;
285
286 if spec.options.is_some() {
287 merged.options = spec.options.clone();
288 }
289 if spec.template_ref.is_some() {
290 merged.template_ref = spec.template_ref.clone();
291 }
292 if spec.template.is_some() {
293 merged.template = spec.template.clone();
294 if spec.type_variants.is_none() {
299 merged.type_variants = None;
300 }
301 }
302 if spec.locales.is_some() {
303 merged.locales = spec.locales.clone();
304 }
305 if spec.type_variants.is_some() {
306 merged.type_variants = spec.type_variants.clone();
307 }
308 if spec.wrap.is_some() {
309 merged.wrap = spec.wrap.clone();
310 }
311 if spec.prefix.is_some() {
312 merged.prefix = spec.prefix.clone();
313 }
314 if spec.suffix.is_some() {
315 merged.suffix = spec.suffix.clone();
316 }
317 if spec.delimiter.is_some() {
318 merged.delimiter = spec.delimiter.clone();
319 }
320 if spec.multi_cite_delimiter.is_some() {
321 merged.multi_cite_delimiter = spec.multi_cite_delimiter.clone();
322 }
323 if spec.collapse.is_some() {
324 merged.collapse = spec.collapse.clone();
325 }
326 if spec.sort.is_some() {
327 merged.sort = spec.sort.clone();
328 }
329 if spec.note_start_text_case.is_some() {
330 merged.note_start_text_case = spec.note_start_text_case;
331 }
332
333 std::borrow::Cow::Owned(merged)
334 }
335 None => std::borrow::Cow::Borrowed(self),
336 }
337 }
338}