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 DelimiterPunctuation, LocalizedTemplateSpec, ResolvedLocalizedTemplate, Template,
18 TemplateReference, TemplateVariants, matched_localized_template,
19};
20
21#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
23#[cfg_attr(feature = "schema", derive(JsonSchema))]
24#[serde(rename_all = "kebab-case")]
25pub enum CitationCollapse {
26 CitationNumber,
28}
29
30#[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)]
32#[cfg_attr(feature = "schema", derive(JsonSchema))]
33#[serde(rename_all = "kebab-case")]
34pub enum NoteStartTextCase {
35 CapitalizeFirst,
37 Lowercase,
39}
40
41#[derive(Debug, Deserialize, Serialize, Clone, Default)]
43#[cfg_attr(feature = "schema", derive(JsonSchema))]
44#[serde(rename_all = "kebab-case")]
45pub struct CitationSpec {
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub options: Option<CitationOptions>,
49 #[serde(skip_serializing_if = "Option::is_none")]
53 pub template_ref: Option<TemplateReference>,
54 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub template: Option<Template>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub locales: Option<Vec<LocalizedTemplateSpec>>,
60 #[serde(skip_serializing_if = "Option::is_none", rename = "type-variants")]
66 pub type_variants: Option<TemplateVariants>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub wrap: Option<crate::template::WrapConfig>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub prefix: Option<DelimiterPunctuation>,
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub suffix: Option<DelimiterPunctuation>,
76 #[serde(skip_serializing_if = "Option::is_none")]
79 pub delimiter: Option<DelimiterPunctuation>,
80 #[serde(skip_serializing_if = "Option::is_none")]
83 #[serde(rename = "multi-cite-delimiter")]
84 pub multi_cite_delimiter: Option<DelimiterPunctuation>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub collapse: Option<CitationCollapse>,
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub sort: Option<grouping::GroupSortEntry>,
91 #[serde(skip_serializing_if = "Option::is_none")]
94 pub integral: Option<Box<CitationSpec>>,
95 #[serde(skip_serializing_if = "Option::is_none")]
98 pub non_integral: Option<Box<CitationSpec>>,
99 #[serde(skip_serializing_if = "Option::is_none")]
104 pub subsequent: Option<Box<CitationSpec>>,
105 #[serde(skip_serializing_if = "Option::is_none")]
110 pub ibid: Option<Box<CitationSpec>>,
111 #[serde(skip_serializing_if = "Option::is_none")]
116 pub note_start_text_case: Option<NoteStartTextCase>,
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub custom: Option<HashMap<String, serde_json::Value>>,
120 #[serde(
124 flatten,
125 default,
126 skip_serializing_if = "std::collections::BTreeMap::is_empty"
127 )]
128 #[cfg_attr(feature = "schema", schemars(skip))]
129 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
130}
131
132impl CitationSpec {
133 pub fn resolve_template(&self) -> Option<Template> {
138 self.template.clone().or_else(|| {
139 self.template_ref
140 .as_ref()
141 .and_then(TemplateReference::citation_template)
142 })
143 }
144
145 pub fn resolve_localized_template(
147 &self,
148 language: Option<&str>,
149 ) -> Option<ResolvedLocalizedTemplate> {
150 if let Some(matched) = language
151 .zip(self.locales.as_deref())
152 .and_then(|(language, locales)| matched_localized_template(locales, language))
153 {
154 return Some(matched);
155 }
156
157 self.locales
158 .as_ref()
159 .and_then(|locales| {
160 locales
161 .iter()
162 .find(|spec| spec.default.unwrap_or(false))
163 .map(|spec| ResolvedLocalizedTemplate {
164 template: spec.template.clone(),
165 locale: None,
166 type_variants: spec.type_variants.clone(),
167 })
168 })
169 .or_else(|| {
170 self.resolve_template()
171 .map(|template| ResolvedLocalizedTemplate {
172 template,
173 locale: None,
174 type_variants: None,
175 })
176 })
177 }
178
179 pub fn resolve_template_for_language(&self, language: Option<&str>) -> Option<Template> {
181 self.resolve_localized_template(language)
182 .map(|resolved| resolved.template)
183 }
184
185 pub fn resolve_template_for_type(
191 &self,
192 ref_type: &str,
193 language: Option<&str>,
194 ) -> Option<Template> {
195 self.resolve_localized_template_for_type(ref_type, language)
196 .map(|resolved| resolved.template)
197 }
198
199 pub fn resolve_localized_template_for_type(
201 &self,
202 ref_type: &str,
203 language: Option<&str>,
204 ) -> Option<ResolvedLocalizedTemplate> {
205 let mut resolved = self.resolve_localized_template(language)?;
206 if let Some(template) = resolved
207 .type_variants
208 .as_ref()
209 .and_then(|variants| {
210 variants.iter().find_map(|(selector, template)| {
211 selector.matches(ref_type).then(|| template.clone())
212 })
213 })
214 .or_else(|| {
215 self.type_variants.as_ref().and_then(|variants| {
216 variants.iter().find_map(|(selector, variant)| {
217 selector
218 .matches(ref_type)
219 .then(|| variant.clone().into_template())
220 .flatten()
221 })
222 })
223 })
224 {
225 resolved.template = template;
226 }
227 Some(resolved)
228 }
229
230 pub fn resolve_for_mode(
235 &self,
236 mode: &crate::citation::CitationMode,
237 ) -> std::borrow::Cow<'_, CitationSpec> {
238 use crate::citation::CitationMode;
239 let mode_spec = match mode {
240 CitationMode::Integral => self.integral.as_ref(),
241 CitationMode::NonIntegral => self.non_integral.as_ref(),
242 };
243
244 match mode_spec {
245 Some(spec) => {
246 let mut merged = self.clone();
248 merged.integral = None;
250 merged.non_integral = None;
251
252 match (&mut merged.options, &spec.options) {
253 (Some(base), Some(mode)) => base.merge(mode),
254 (None, Some(mode)) => merged.options = Some(mode.clone()),
255 _ => {}
256 }
257 if spec.template_ref.is_some() {
258 merged.template_ref = spec.template_ref.clone();
259 }
260 if spec.template.is_some() {
261 merged.template = spec.template.clone();
262 }
263 if spec.locales.is_some() {
264 merged.locales = spec.locales.clone();
265 }
266 if spec.type_variants.is_some() {
267 merged.type_variants = spec.type_variants.clone();
268 }
269 if spec.wrap.is_some() {
270 merged.wrap = spec.wrap.clone();
271 }
272 if spec.prefix.is_some() {
273 merged.prefix = spec.prefix.clone();
274 }
275 if spec.suffix.is_some() {
276 merged.suffix = spec.suffix.clone();
277 }
278 if spec.delimiter.is_some() {
279 merged.delimiter = spec.delimiter.clone();
280 }
281 if spec.multi_cite_delimiter.is_some() {
282 merged.multi_cite_delimiter = spec.multi_cite_delimiter.clone();
283 }
284 if spec.collapse.is_some() {
285 merged.collapse = spec.collapse.clone();
286 }
287 if spec.sort.is_some() {
288 merged.sort = spec.sort.clone();
289 }
290 if spec.note_start_text_case.is_some() {
291 merged.note_start_text_case = spec.note_start_text_case;
292 }
293
294 std::borrow::Cow::Owned(merged)
295 }
296 None => std::borrow::Cow::Borrowed(self),
297 }
298 }
299
300 pub fn resolve_for_position(
308 &self,
309 position: Option<&crate::citation::Position>,
310 ) -> std::borrow::Cow<'_, CitationSpec> {
311 use crate::citation::Position;
312
313 let position_spec = match position {
314 Some(Position::Ibid | Position::IbidWithLocator) => {
315 self.ibid.as_ref().or(self.subsequent.as_ref())
316 }
317 Some(Position::Subsequent) => self.subsequent.as_ref(),
318 Some(Position::First) | None => None,
319 };
320
321 match position_spec {
322 Some(spec) => {
323 let mut merged = self.clone();
325 merged.subsequent = None;
327 merged.ibid = None;
328
329 match (&mut merged.options, &spec.options) {
330 (Some(base), Some(mode)) => base.merge(mode),
331 (None, Some(mode)) => merged.options = Some(mode.clone()),
332 _ => {}
333 }
334 if spec.template_ref.is_some() {
335 merged.template_ref = spec.template_ref.clone();
336 }
337 if spec.template.is_some() {
338 merged.template = spec.template.clone();
339 if spec.type_variants.is_none() {
344 merged.type_variants = None;
345 }
346 }
347 if spec.locales.is_some() {
348 merged.locales = spec.locales.clone();
349 }
350 if spec.type_variants.is_some() {
351 merged.type_variants = spec.type_variants.clone();
352 }
353 if spec.wrap.is_some() {
354 merged.wrap = spec.wrap.clone();
355 }
356 if spec.prefix.is_some() {
357 merged.prefix = spec.prefix.clone();
358 }
359 if spec.suffix.is_some() {
360 merged.suffix = spec.suffix.clone();
361 }
362 if spec.delimiter.is_some() {
363 merged.delimiter = spec.delimiter.clone();
364 }
365 if spec.multi_cite_delimiter.is_some() {
366 merged.multi_cite_delimiter = spec.multi_cite_delimiter.clone();
367 }
368 if spec.collapse.is_some() {
369 merged.collapse = spec.collapse.clone();
370 }
371 if spec.sort.is_some() {
372 merged.sort = spec.sort.clone();
373 }
374 if spec.note_start_text_case.is_some() {
375 merged.note_start_text_case = spec.note_start_text_case;
376 }
377
378 std::borrow::Cow::Owned(merged)
379 }
380 None => std::borrow::Cow::Borrowed(self),
381 }
382 }
383}