1pub mod contributor;
13pub mod date;
15pub mod list;
17pub mod locator;
19pub mod number;
21pub mod range;
23pub mod term;
25pub mod text_case;
27pub mod title;
29pub mod variable;
31
32#[cfg(test)]
33#[allow(
34 clippy::unwrap_used,
35 clippy::expect_used,
36 clippy::panic,
37 clippy::indexing_slicing,
38 clippy::todo,
39 clippy::unimplemented,
40 clippy::unreachable,
41 clippy::get_unwrap,
42 reason = "Panicking is acceptable and often desired in tests."
43)]
44mod tests;
45
46use crate::reference::Reference;
47use citum_schema::locale::Locale;
48use citum_schema::options::{Config, bibliography::BibliographyConfig};
49use citum_schema::reference::types::Title;
50use citum_schema::template::{TemplateComponent, TitleType};
51
52pub use contributor::format_contributors_short;
53pub use date::int_to_letter;
54
55fn resolve_transliteration<'a>(
63 transliterations: &'a std::collections::HashMap<String, String>,
64 preferred_transliteration: Option<&[String]>,
65 preferred_script: Option<&String>,
66) -> Option<&'a str> {
67 if let Some(tags) = preferred_transliteration {
69 for tag in tags {
70 if let Some(v) = transliterations.get(tag) {
71 return Some(v.as_str());
72 }
73 }
74 for tag in tags {
76 for (k, v) in transliterations {
77 if k.contains(tag.as_str()) {
78 return Some(v.as_str());
79 }
80 }
81 }
82 }
83 if let Some(script) = preferred_script {
85 if let Some(v) = transliterations.get(script) {
86 return Some(v.as_str());
87 }
88 for (k, v) in transliterations {
90 if k.contains(script.as_str()) {
91 return Some(v.as_str());
92 }
93 }
94 }
95 None
96}
97
98fn resolve_translation<'a>(
99 translations: &'a std::collections::HashMap<citum_schema::reference::LangID, String>,
100 style_locale: &str,
101) -> Option<&'a str> {
102 translations
103 .get(style_locale)
104 .or_else(|| {
105 style_locale
106 .split(['-', '_'])
107 .next()
108 .and_then(|base| translations.get(base))
109 })
110 .map(String::as_str)
111}
112
113#[must_use]
127pub fn resolve_multilingual_string(
128 string: &citum_schema::reference::types::MultilingualString,
129 mode: Option<&citum_schema::options::MultilingualMode>,
130 preferred_transliteration: Option<&[String]>,
131 preferred_script: Option<&String>,
132 style_locale: &str,
133) -> String {
134 use citum_schema::options::MultilingualMode;
135 use citum_schema::reference::types::MultilingualString;
136
137 match string {
138 MultilingualString::Simple(s) => s.clone(),
139 MultilingualString::Complex(complex) => {
140 let mode = mode.unwrap_or(&MultilingualMode::Primary);
141
142 match mode {
143 MultilingualMode::Primary => complex.original.clone(),
144
145 MultilingualMode::Transliterated => {
146 if let Some(trans) = resolve_transliteration(
147 &complex.transliterations,
148 preferred_transliteration,
149 preferred_script,
150 ) {
151 return trans.to_string();
152 }
153
154 complex
156 .transliterations
157 .values()
158 .next()
159 .cloned()
160 .unwrap_or_else(|| complex.original.clone())
161 }
162
163 MultilingualMode::Translated => {
164 resolve_translation(&complex.translations, style_locale)
166 .map(ToString::to_string)
167 .unwrap_or_else(|| complex.original.clone())
168 }
169
170 MultilingualMode::Combined => {
171 let trans = resolve_transliteration(
173 &complex.transliterations,
174 preferred_transliteration,
175 preferred_script,
176 );
177
178 let translation = resolve_translation(&complex.translations, style_locale);
179
180 match (trans, translation) {
181 (Some(t), Some(tr)) => format!("{t} [{tr}]"),
182 (Some(t), None) => t.to_string(),
183 (None, Some(tr)) => format!("{} [{}]", complex.original, tr),
184 (None, None) => complex.original.clone(),
185 }
186 }
187 }
188 }
189 }
190}
191
192#[must_use]
198pub fn effective_field_language(
199 reference: &Reference,
200 scope: &str,
201 title: Option<&Title>,
202) -> Option<String> {
203 reference
204 .field_languages()
205 .get(scope)
206 .map(ToString::to_string)
207 .or_else(|| match title {
208 Some(Title::Multilingual(multilingual)) => {
209 multilingual.lang.as_ref().map(ToString::to_string)
210 }
211 _ => None,
212 })
213 .or_else(|| reference.language().map(|lang| lang.to_string()))
214}
215
216#[must_use]
218pub fn effective_item_language(reference: &Reference) -> Option<String> {
219 effective_field_language(reference, "title", reference.title().as_ref())
220}
221
222#[must_use]
224pub fn effective_component_language(
225 reference: &Reference,
226 component: &TemplateComponent,
227) -> Option<String> {
228 match component {
229 TemplateComponent::Title(title_component) => {
230 let title = match title_component.title {
231 TitleType::Primary => reference.title(),
232 TitleType::ParentMonograph => reference.container_title(),
233 TitleType::ParentSerial => reference.container_title(),
234 _ => reference.title(),
235 };
236
237 let scope = match title_component.title {
238 TitleType::Primary => "title",
239 TitleType::ParentMonograph => "parent-monograph.title",
240 TitleType::ParentSerial => "parent-serial.title",
241 _ => "title",
242 };
243
244 effective_field_language(reference, scope, title.as_ref())
245 }
246 _ => effective_item_language(reference),
247 }
248}
249
250fn select_by_transliteration<'a>(
252 m: &'a citum_schema::reference::contributor::MultilingualName,
253 preferred_transliteration: Option<&[String]>,
254 preferred_script: Option<&String>,
255) -> &'a citum_schema::reference::contributor::StructuredName {
256 if let Some(tags) = preferred_transliteration {
258 for tag in tags {
259 if let Some(name) = m.transliterations.get(tag) {
260 return name;
261 }
262 }
263 for tag in tags {
265 if let Some((_, name)) = m
266 .transliterations
267 .iter()
268 .find(|(k, _)| k.contains(tag.as_str()))
269 {
270 return name;
271 }
272 }
273 }
274 if let Some(script) = preferred_script {
276 if let Some(name) = m.transliterations.get(script) {
277 return name;
278 }
279 if let Some((_, name)) = m
281 .transliterations
282 .iter()
283 .find(|(tag, _)| tag.contains(script))
284 {
285 return name;
286 }
287 }
288 m.transliterations.values().next().unwrap_or(&m.original)
290}
291
292#[must_use]
304pub fn resolve_multilingual_name(
305 contributor: &citum_schema::reference::contributor::Contributor,
306 mode: Option<&citum_schema::options::MultilingualMode>,
307 preferred_transliteration: Option<&[String]>,
308 preferred_script: Option<&String>,
309 style_locale: &str,
310) -> Vec<crate::reference::FlatName> {
311 use citum_schema::options::MultilingualMode;
312 use citum_schema::reference::contributor::Contributor;
313
314 match contributor {
315 Contributor::SimpleName(_) | Contributor::StructuredName(_) => contributor.to_names_vec(),
317
318 Contributor::Multilingual(m) => {
320 let mode = mode.unwrap_or(&MultilingualMode::Primary);
321
322 let selected_name = match mode {
323 MultilingualMode::Primary => &m.original,
324 MultilingualMode::Transliterated => {
325 select_by_transliteration(m, preferred_transliteration, preferred_script)
326 }
327 MultilingualMode::Translated => {
328 m.translations.get(style_locale).unwrap_or(&m.original)
329 }
330 MultilingualMode::Combined => {
332 select_by_transliteration(m, preferred_transliteration, preferred_script)
333 }
334 };
335
336 vec![crate::reference::FlatName {
338 given: Some(selected_name.given.to_string()),
339 family: Some(selected_name.family.to_string()),
340 suffix: selected_name.suffix.clone(),
341 dropping_particle: selected_name.dropping_particle.clone(),
342 non_dropping_particle: selected_name.non_dropping_particle.clone(),
343 literal: None,
344 short_name: None,
345 }]
346 }
347
348 Contributor::ContributorList(l) => {
349 l.0.iter()
350 .flat_map(|c| {
351 resolve_multilingual_name(
352 c,
353 mode,
354 preferred_transliteration,
355 preferred_script,
356 style_locale,
357 )
358 })
359 .collect()
360 }
361 }
362}
363
364#[must_use]
366pub fn resolve_url(
367 links: &citum_schema::options::LinksConfig,
368 reference: &Reference,
369) -> Option<String> {
370 use citum_schema::options::LinkTarget;
371
372 let target = links.target.as_ref().unwrap_or(&LinkTarget::UrlOrDoi);
373
374 match target {
375 LinkTarget::Url => reference.url().map(|u| u.to_string()),
376 LinkTarget::Doi => reference.doi().map(|d| format!("https://doi.org/{d}")),
377 LinkTarget::UrlOrDoi => reference
378 .url()
379 .map(|u| u.to_string())
380 .or_else(|| reference.doi().map(|d| format!("https://doi.org/{d}"))),
381 LinkTarget::Pubmed => reference
382 .id()
383 .filter(|id| id.starts_with("pmid:"))
384 .map(|id| {
385 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
386 let result = format!("https://pubmed.ncbi.nlm.nih.gov/{}/", &id[5..]);
387 result
388 }),
389 LinkTarget::Pmcid => reference
390 .id()
391 .filter(|id| id.starts_with("pmc:"))
392 .map(|id| {
393 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
394 let result = format!("https://www.ncbi.nlm.nih.gov/pmc/articles/{}/", &id[4..]);
395 result
396 }),
397 }
398}
399
400#[must_use]
402pub fn resolve_effective_url(
403 local_links: Option<&citum_schema::options::LinksConfig>,
404 global_links: Option<&citum_schema::options::LinksConfig>,
405 reference: &Reference,
406 component_anchor: citum_schema::options::LinkAnchor,
407) -> Option<String> {
408 use citum_schema::options::LinkAnchor;
409
410 if let Some(links) = local_links {
412 let anchor = links.anchor.as_ref().unwrap_or(&LinkAnchor::Component);
413 if matches!(anchor, LinkAnchor::Component) || *anchor == component_anchor {
414 return resolve_url(links, reference);
415 }
416 }
417
418 if let Some(links) = global_links
420 && let Some(anchor) = &links.anchor
421 && *anchor == component_anchor
422 {
423 return resolve_url(links, reference);
424 }
425
426 None
427}
428
429#[derive(Debug, Clone, Default)]
431pub struct ProcValues<T = String> {
432 pub value: T,
434 pub prefix: Option<String>,
436 pub suffix: Option<String>,
438 pub url: Option<String>,
440 pub substituted_key: Option<String>,
443 pub pre_formatted: bool,
445}
446
447#[derive(Debug, Clone, Default)]
449pub struct ProcHints {
450 pub disamb_condition: bool,
452 pub group_index: usize,
454 pub group_length: usize,
456 pub group_key: String,
458 pub expand_given_names: bool,
460 pub min_names_to_show: Option<usize>,
462 pub citation_number: Option<usize>,
464 pub citation_sub_label: Option<String>,
466 pub position: Option<citum_schema::citation::Position>,
468 pub integral_name_state: Option<citum_schema::citation::IntegralNameState>,
470 pub org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
472 pub first_reference_note_number: Option<u32>,
475 pub suppress_disambiguation_title: bool,
479}
480
481#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
483pub enum RenderContext {
484 #[default]
485 Citation,
487 Bibliography,
489}
490
491#[derive(Clone)]
493pub struct RenderOptions<'a> {
494 pub config: &'a Config,
496 pub bibliography_config: Option<BibliographyConfig>,
498 pub locale: &'a Locale,
500 pub context: RenderContext,
502 pub mode: citum_schema::citation::CitationMode,
504 pub suppress_author: bool,
507 pub locator_raw: Option<&'a citum_schema::citation::CitationLocator>,
509 pub ref_type: Option<String>,
511 pub show_semantics: bool,
513 pub current_template_index: Option<usize>,
515 pub abbreviation_map: Option<&'a crate::api::AbbreviationMap>,
517}
518
519pub trait ComponentValues {
521 fn values<F: crate::render::format::OutputFormat<Output = String>>(
523 &self,
524 reference: &Reference,
525 hints: &ProcHints,
526 options: &RenderOptions<'_>,
527 ) -> Option<ProcValues<F::Output>>;
528}
529
530impl ComponentValues for TemplateComponent {
531 fn values<F: crate::render::format::OutputFormat<Output = String>>(
532 &self,
533 reference: &Reference,
534 hints: &ProcHints,
535 options: &RenderOptions<'_>,
536 ) -> Option<ProcValues<F::Output>> {
537 match self {
538 TemplateComponent::Contributor(c) => c.values::<F>(reference, hints, options),
539 TemplateComponent::Date(d) => d.values::<F>(reference, hints, options),
540 TemplateComponent::Title(t) => t.values::<F>(reference, hints, options),
541 TemplateComponent::Number(n) => n.values::<F>(reference, hints, options),
542 TemplateComponent::Variable(v) => v.values::<F>(reference, hints, options),
543 TemplateComponent::Group(l) => l.values::<F>(reference, hints, options),
544 TemplateComponent::Term(t) => t.values::<F>(reference, hints, options),
545 _ => None,
546 }
547 }
548}
549
550#[must_use]
557pub fn should_strip_periods(
558 rendering: &citum_schema::template::Rendering,
559 options: &RenderOptions<'_>,
560) -> bool {
561 rendering
562 .strip_periods
563 .or(options.config.strip_periods)
564 .unwrap_or(false)
565}
566
567#[must_use]
572pub fn strip_trailing_periods(s: &str) -> String {
573 s.trim_end_matches('.').to_string()
574}
575
576#[must_use]
580pub fn apply_abbreviation(value: String, map: Option<&crate::api::AbbreviationMap>) -> String {
581 if let Some(abbr) = map.and_then(|m| m.0.get(&value)) {
582 return abbr.clone();
583 }
584 value
585}