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 match (&mut merged.options, &spec.options) {
212 (Some(base), Some(mode)) => base.merge(mode),
213 (None, Some(mode)) => merged.options = Some(mode.clone()),
214 _ => {}
215 }
216 if spec.template_ref.is_some() {
217 merged.template_ref = spec.template_ref.clone();
218 }
219 if spec.template.is_some() {
220 merged.template = spec.template.clone();
221 }
222 if spec.locales.is_some() {
223 merged.locales = spec.locales.clone();
224 }
225 if spec.type_variants.is_some() {
226 merged.type_variants = spec.type_variants.clone();
227 }
228 if spec.wrap.is_some() {
229 merged.wrap = spec.wrap.clone();
230 }
231 if spec.prefix.is_some() {
232 merged.prefix = spec.prefix.clone();
233 }
234 if spec.suffix.is_some() {
235 merged.suffix = spec.suffix.clone();
236 }
237 if spec.delimiter.is_some() {
238 merged.delimiter = spec.delimiter.clone();
239 }
240 if spec.multi_cite_delimiter.is_some() {
241 merged.multi_cite_delimiter = spec.multi_cite_delimiter.clone();
242 }
243 if spec.collapse.is_some() {
244 merged.collapse = spec.collapse.clone();
245 }
246 if spec.sort.is_some() {
247 merged.sort = spec.sort.clone();
248 }
249 if spec.note_start_text_case.is_some() {
250 merged.note_start_text_case = spec.note_start_text_case;
251 }
252
253 std::borrow::Cow::Owned(merged)
254 }
255 None => std::borrow::Cow::Borrowed(self),
256 }
257 }
258
259 pub fn resolve_for_position(
267 &self,
268 position: Option<&crate::citation::Position>,
269 ) -> std::borrow::Cow<'_, CitationSpec> {
270 use crate::citation::Position;
271
272 let position_spec = match position {
273 Some(Position::Ibid | Position::IbidWithLocator) => {
274 self.ibid.as_ref().or(self.subsequent.as_ref())
275 }
276 Some(Position::Subsequent) => self.subsequent.as_ref(),
277 Some(Position::First) | None => None,
278 };
279
280 match position_spec {
281 Some(spec) => {
282 let mut merged = self.clone();
284 merged.subsequent = None;
286 merged.ibid = None;
287
288 match (&mut merged.options, &spec.options) {
289 (Some(base), Some(mode)) => base.merge(mode),
290 (None, Some(mode)) => merged.options = Some(mode.clone()),
291 _ => {}
292 }
293 if spec.template_ref.is_some() {
294 merged.template_ref = spec.template_ref.clone();
295 }
296 if spec.template.is_some() {
297 merged.template = spec.template.clone();
298 if spec.type_variants.is_none() {
303 merged.type_variants = None;
304 }
305 }
306 if spec.locales.is_some() {
307 merged.locales = spec.locales.clone();
308 }
309 if spec.type_variants.is_some() {
310 merged.type_variants = spec.type_variants.clone();
311 }
312 if spec.wrap.is_some() {
313 merged.wrap = spec.wrap.clone();
314 }
315 if spec.prefix.is_some() {
316 merged.prefix = spec.prefix.clone();
317 }
318 if spec.suffix.is_some() {
319 merged.suffix = spec.suffix.clone();
320 }
321 if spec.delimiter.is_some() {
322 merged.delimiter = spec.delimiter.clone();
323 }
324 if spec.multi_cite_delimiter.is_some() {
325 merged.multi_cite_delimiter = spec.multi_cite_delimiter.clone();
326 }
327 if spec.collapse.is_some() {
328 merged.collapse = spec.collapse.clone();
329 }
330 if spec.sort.is_some() {
331 merged.sort = spec.sort.clone();
332 }
333 if spec.note_start_text_case.is_some() {
334 merged.note_start_text_case = spec.note_start_text_case;
335 }
336
337 std::borrow::Cow::Owned(merged)
338 }
339 None => std::borrow::Cow::Borrowed(self),
340 }
341 }
342}