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, TemplateVariant, 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")]
54 pub template_ref: Option<TemplateReference>,
55 #[serde(skip_serializing_if = "Option::is_none", default)]
58 pub template: Option<TemplateVariant>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub locales: Option<Vec<LocalizedTemplateSpec>>,
62 #[serde(
68 default,
69 skip_serializing_if = "Option::is_none",
70 rename = "type-variants"
71 )]
72 #[cfg_attr(
73 feature = "schema",
74 schemars(
75 schema_with = "crate::template::type_keyed_map_schema::<crate::template::TemplateVariant>"
76 )
77 )]
78 pub type_variants: Option<TemplateVariants>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub wrap: Option<crate::template::WrapConfig>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub prefix: Option<DelimiterPunctuation>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub suffix: Option<DelimiterPunctuation>,
88 #[serde(skip_serializing_if = "Option::is_none")]
91 pub delimiter: Option<DelimiterPunctuation>,
92 #[serde(skip_serializing_if = "Option::is_none")]
95 #[serde(rename = "multi-cite-delimiter")]
96 pub multi_cite_delimiter: Option<DelimiterPunctuation>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub collapse: Option<CitationCollapse>,
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub sort: Option<grouping::GroupSortEntry>,
103 #[serde(skip_serializing_if = "Option::is_none")]
106 pub integral: Option<Box<CitationSpec>>,
107 #[serde(skip_serializing_if = "Option::is_none")]
110 pub non_integral: Option<Box<CitationSpec>>,
111 #[serde(skip_serializing_if = "Option::is_none")]
116 pub subsequent: Option<Box<CitationSpec>>,
117 #[serde(skip_serializing_if = "Option::is_none")]
122 pub ibid: Option<Box<CitationSpec>>,
123 #[serde(skip_serializing_if = "Option::is_none")]
128 pub note_start_text_case: Option<NoteStartTextCase>,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub custom: Option<HashMap<String, serde_json::Value>>,
132 #[serde(
136 flatten,
137 default,
138 skip_serializing_if = "std::collections::BTreeMap::is_empty"
139 )]
140 #[cfg_attr(feature = "schema", schemars(skip))]
141 pub unknown_fields: std::collections::BTreeMap<String, serde_yaml::Value>,
142}
143
144impl CitationSpec {
145 pub fn resolve_template(&self) -> Option<Template> {
150 self.template
151 .as_ref()
152 .and_then(TemplateVariant::as_template)
153 .map(<[_]>::to_vec)
154 .or_else(|| {
155 self.template_ref
156 .as_ref()
157 .and_then(TemplateReference::citation_template)
158 })
159 }
160
161 pub fn resolve_localized_template(
163 &self,
164 language: Option<&str>,
165 ) -> Option<ResolvedLocalizedTemplate> {
166 if let Some(matched) = language
167 .zip(self.locales.as_deref())
168 .and_then(|(language, locales)| matched_localized_template(locales, language))
169 {
170 return Some(matched);
171 }
172
173 self.locales
174 .as_ref()
175 .and_then(|locales| {
176 locales
177 .iter()
178 .find(|spec| spec.default.unwrap_or(false))
179 .map(|spec| ResolvedLocalizedTemplate {
180 template: spec.template.clone(),
181 locale: None,
182 type_variants: spec.type_variants.clone(),
183 })
184 })
185 .or_else(|| {
186 self.resolve_template()
187 .map(|template| ResolvedLocalizedTemplate {
188 template,
189 locale: None,
190 type_variants: None,
191 })
192 })
193 }
194
195 pub fn resolve_template_for_language(&self, language: Option<&str>) -> Option<Template> {
197 self.resolve_localized_template(language)
198 .map(|resolved| resolved.template)
199 }
200
201 pub fn resolve_template_for_type(
207 &self,
208 ref_type: &str,
209 language: Option<&str>,
210 ) -> Option<Template> {
211 self.resolve_localized_template_for_type(ref_type, language)
212 .map(|resolved| resolved.template)
213 }
214
215 pub fn resolve_localized_template_for_type(
217 &self,
218 ref_type: &str,
219 language: Option<&str>,
220 ) -> Option<ResolvedLocalizedTemplate> {
221 let mut resolved = self.resolve_localized_template(language)?;
222 if let Some(template) = resolved
223 .type_variants
224 .as_ref()
225 .and_then(|variants| {
226 variants.iter().find_map(|(selector, template)| {
227 selector.matches(ref_type).then(|| template.clone())
228 })
229 })
230 .or_else(|| {
231 self.type_variants.as_ref().and_then(|variants| {
232 variants.iter().find_map(|(selector, variant)| {
233 selector
234 .matches(ref_type)
235 .then(|| variant.clone().into_template())
236 .flatten()
237 })
238 })
239 })
240 {
241 resolved.template = template;
242 }
243 Some(resolved)
244 }
245
246 pub fn resolve_for_mode(
251 &self,
252 mode: &crate::citation::CitationMode,
253 ) -> std::borrow::Cow<'_, CitationSpec> {
254 use crate::citation::CitationMode;
255 let mode_spec = match mode {
256 CitationMode::Integral => self.integral.as_ref(),
257 CitationMode::NonIntegral => self.non_integral.as_ref(),
258 };
259
260 match mode_spec {
261 Some(spec) => {
262 let mut merged = self.clone();
264 merged.integral = None;
266 merged.non_integral = None;
267
268 match (&mut merged.options, &spec.options) {
269 (Some(base), Some(mode)) => base.merge(mode),
270 (None, Some(mode)) => merged.options = Some(mode.clone()),
271 _ => {}
272 }
273 if spec.template_ref.is_some() {
274 merged.template_ref = spec.template_ref.clone();
275 }
276 if spec.template.is_some() {
277 merged.template = spec.template.clone();
278 }
279 if spec.locales.is_some() {
280 merged.locales = spec.locales.clone();
281 }
282 if spec.type_variants.is_some() {
283 merged.type_variants = spec.type_variants.clone();
284 }
285 if spec.wrap.is_some() {
286 merged.wrap = spec.wrap.clone();
287 }
288 if spec.prefix.is_some() {
289 merged.prefix = spec.prefix.clone();
290 }
291 if spec.suffix.is_some() {
292 merged.suffix = spec.suffix.clone();
293 }
294 if spec.delimiter.is_some() {
295 merged.delimiter = spec.delimiter.clone();
296 }
297 if spec.multi_cite_delimiter.is_some() {
298 merged.multi_cite_delimiter = spec.multi_cite_delimiter.clone();
299 }
300 if spec.collapse.is_some() {
301 merged.collapse = spec.collapse.clone();
302 }
303 if spec.sort.is_some() {
304 merged.sort = spec.sort.clone();
305 }
306 if spec.note_start_text_case.is_some() {
307 merged.note_start_text_case = spec.note_start_text_case;
308 }
309
310 std::borrow::Cow::Owned(merged)
311 }
312 None => std::borrow::Cow::Borrowed(self),
313 }
314 }
315
316 pub fn resolve_for_position(
324 &self,
325 position: Option<&crate::citation::Position>,
326 ) -> std::borrow::Cow<'_, CitationSpec> {
327 use crate::citation::Position;
328
329 let position_spec = match position {
330 Some(Position::Ibid | Position::IbidWithLocator) => {
331 self.ibid.as_ref().or(self.subsequent.as_ref())
332 }
333 Some(Position::Subsequent) => self.subsequent.as_ref(),
334 Some(Position::First) | None => None,
335 };
336
337 match position_spec {
338 Some(spec) => {
339 let mut merged = self.clone();
341 merged.subsequent = None;
343 merged.ibid = None;
344
345 match (&mut merged.options, &spec.options) {
346 (Some(base), Some(mode)) => base.merge(mode),
347 (None, Some(mode)) => merged.options = Some(mode.clone()),
348 _ => {}
349 }
350 if spec.template_ref.is_some() {
351 merged.template_ref = spec.template_ref.clone();
352 }
353 if spec.template.is_some() {
354 merged.template = spec.template.clone();
355 if spec.type_variants.is_none() {
360 merged.type_variants = None;
361 }
362 }
363 if spec.locales.is_some() {
364 merged.locales = spec.locales.clone();
365 }
366 if spec.type_variants.is_some() {
367 merged.type_variants = spec.type_variants.clone();
368 }
369 if spec.wrap.is_some() {
370 merged.wrap = spec.wrap.clone();
371 }
372 if spec.prefix.is_some() {
373 merged.prefix = spec.prefix.clone();
374 }
375 if spec.suffix.is_some() {
376 merged.suffix = spec.suffix.clone();
377 }
378 if spec.delimiter.is_some() {
379 merged.delimiter = spec.delimiter.clone();
380 }
381 if spec.multi_cite_delimiter.is_some() {
382 merged.multi_cite_delimiter = spec.multi_cite_delimiter.clone();
383 }
384 if spec.collapse.is_some() {
385 merged.collapse = spec.collapse.clone();
386 }
387 if spec.sort.is_some() {
388 merged.sort = spec.sort.clone();
389 }
390 if spec.note_start_text_case.is_some() {
391 merged.note_start_text_case = spec.note_start_text_case;
392 }
393
394 std::borrow::Cow::Owned(merged)
395 }
396 None => std::borrow::Cow::Borrowed(self),
397 }
398 }
399}