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 MultilingualMode::Pattern(segments) => resolve_multilingual_pattern(
189 segments,
190 &complex.original,
191 &complex.transliterations,
192 &complex.translations,
193 preferred_transliteration,
194 preferred_script,
195 style_locale,
196 ),
197 }
198 }
199 }
200}
201
202fn resolve_multilingual_pattern(
208 segments: &[citum_schema::options::MultilingualSegment],
209 original: &str,
210 transliterations: &std::collections::HashMap<String, String>,
211 translations: &std::collections::HashMap<citum_schema::reference::types::LangID, String>,
212 preferred_transliteration: Option<&[String]>,
213 preferred_script: Option<&String>,
214 style_locale: &str,
215) -> String {
216 use citum_schema::options::{MultilingualView, SegmentWrap};
217 let mut parts: Vec<String> = Vec::with_capacity(segments.len());
218 let mut last_text: Option<String> = None;
219
220 for seg in segments {
221 let text: Option<String> = match &seg.view {
222 MultilingualView::Original => Some(original.to_string()),
223 MultilingualView::Transliterated => resolve_transliteration(
224 transliterations,
225 preferred_transliteration,
226 preferred_script,
227 )
228 .map(ToString::to_string),
229 MultilingualView::Translated => {
230 resolve_translation(translations, style_locale).map(ToString::to_string)
231 }
232 };
233
234 let Some(text) = text else { continue };
235 if text.is_empty() {
236 continue;
237 }
238 if last_text.as_deref() == Some(text.as_str()) {
241 continue;
242 }
243
244 let wrapped = match &seg.wrap {
245 SegmentWrap::None => text.clone(),
246 other => other.apply(&text),
247 };
248 last_text = Some(text);
249 parts.push(wrapped);
250 }
251
252 parts.join(" ")
253}
254
255#[must_use]
261pub fn effective_field_language(
262 reference: &Reference,
263 scope: &str,
264 title: Option<&Title>,
265) -> Option<String> {
266 reference
267 .field_languages()
268 .get(scope)
269 .map(ToString::to_string)
270 .or_else(|| match title {
271 Some(Title::Multilingual(multilingual)) => {
272 multilingual.lang.as_ref().map(ToString::to_string)
273 }
274 _ => None,
275 })
276 .or_else(|| reference.language().map(|lang| lang.to_string()))
277}
278
279#[must_use]
281pub fn effective_item_language(reference: &Reference) -> Option<String> {
282 effective_field_language(reference, "title", reference.title().as_ref())
283}
284
285#[must_use]
287pub fn effective_component_language(
288 reference: &Reference,
289 component: &TemplateComponent,
290) -> Option<String> {
291 match component {
292 TemplateComponent::Title(title_component) => {
293 let title = match title_component.title {
294 TitleType::Primary => reference.title(),
295 TitleType::ParentMonograph => reference.container_title(),
296 TitleType::ParentSerial => reference.container_title(),
297 _ => reference.title(),
298 };
299
300 let scope = match title_component.title {
301 TitleType::Primary => "title",
302 TitleType::ParentMonograph => "parent-monograph.title",
303 TitleType::ParentSerial => "parent-serial.title",
304 _ => "title",
305 };
306
307 effective_field_language(reference, scope, title.as_ref())
308 }
309 _ => effective_item_language(reference),
310 }
311}
312
313fn select_by_transliteration<'a>(
315 m: &'a citum_schema::reference::contributor::MultilingualName,
316 preferred_transliteration: Option<&[String]>,
317 preferred_script: Option<&String>,
318) -> &'a citum_schema::reference::contributor::StructuredName {
319 if let Some(tags) = preferred_transliteration {
321 for tag in tags {
322 if let Some(name) = m.transliterations.get(tag) {
323 return name;
324 }
325 }
326 for tag in tags {
328 if let Some((_, name)) = m
329 .transliterations
330 .iter()
331 .find(|(k, _)| k.contains(tag.as_str()))
332 {
333 return name;
334 }
335 }
336 }
337 if let Some(script) = preferred_script {
339 if let Some(name) = m.transliterations.get(script) {
340 return name;
341 }
342 if let Some((_, name)) = m
344 .transliterations
345 .iter()
346 .find(|(tag, _)| tag.contains(script))
347 {
348 return name;
349 }
350 }
351 m.transliterations.values().next().unwrap_or(&m.original)
353}
354
355#[must_use]
367pub fn resolve_multilingual_name(
368 contributor: &citum_schema::reference::contributor::Contributor,
369 mode: Option<&citum_schema::options::MultilingualMode>,
370 preferred_transliteration: Option<&[String]>,
371 preferred_script: Option<&String>,
372 style_locale: &str,
373) -> Vec<crate::reference::FlatName> {
374 use citum_schema::options::MultilingualMode;
375 use citum_schema::reference::contributor::Contributor;
376
377 match contributor {
378 Contributor::SimpleName(_) | Contributor::StructuredName(_) => contributor.to_names_vec(),
380
381 Contributor::Multilingual(m) => {
383 let mode = mode.unwrap_or(&MultilingualMode::Primary);
384
385 let selected_name = match mode {
386 MultilingualMode::Primary => &m.original,
387 MultilingualMode::Transliterated => {
388 select_by_transliteration(m, preferred_transliteration, preferred_script)
389 }
390 MultilingualMode::Translated => {
391 m.translations.get(style_locale).unwrap_or(&m.original)
392 }
393 MultilingualMode::Combined => {
395 select_by_transliteration(m, preferred_transliteration, preferred_script)
396 }
397 MultilingualMode::Pattern(_) => {
401 select_by_transliteration(m, preferred_transliteration, preferred_script)
402 }
403 };
404
405 vec![crate::reference::FlatName {
407 given: Some(selected_name.given.to_string()),
408 family: Some(selected_name.family.to_string()),
409 suffix: selected_name.suffix.clone(),
410 dropping_particle: selected_name.dropping_particle.clone(),
411 non_dropping_particle: selected_name.non_dropping_particle.clone(),
412 literal: None,
413 short_name: None,
414 }]
415 }
416
417 Contributor::ContributorList(l) => {
418 l.0.iter()
419 .flat_map(|c| {
420 resolve_multilingual_name(
421 c,
422 mode,
423 preferred_transliteration,
424 preferred_script,
425 style_locale,
426 )
427 })
428 .collect()
429 }
430 }
431}
432
433#[must_use]
435pub fn resolve_url(
436 links: &citum_schema::options::LinksConfig,
437 reference: &Reference,
438) -> Option<String> {
439 use citum_schema::options::LinkTarget;
440
441 let target = links.target.as_ref().unwrap_or(&LinkTarget::UrlOrDoi);
442
443 match target {
444 LinkTarget::Url => reference.url().map(|u| u.to_string()),
445 LinkTarget::Doi => reference.doi().map(|d| format!("https://doi.org/{d}")),
446 LinkTarget::UrlOrDoi => reference
447 .url()
448 .map(|u| u.to_string())
449 .or_else(|| reference.doi().map(|d| format!("https://doi.org/{d}"))),
450 LinkTarget::Pubmed => reference
451 .id()
452 .filter(|id| id.starts_with("pmid:"))
453 .map(|id| {
454 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
455 let result = format!("https://pubmed.ncbi.nlm.nih.gov/{}/", &id[5..]);
456 result
457 }),
458 LinkTarget::Pmcid => reference
459 .id()
460 .filter(|id| id.starts_with("pmc:"))
461 .map(|id| {
462 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
463 let result = format!("https://www.ncbi.nlm.nih.gov/pmc/articles/{}/", &id[4..]);
464 result
465 }),
466 }
467}
468
469#[must_use]
471pub fn resolve_effective_url(
472 local_links: Option<&citum_schema::options::LinksConfig>,
473 global_links: Option<&citum_schema::options::LinksConfig>,
474 reference: &Reference,
475 component_anchor: citum_schema::options::LinkAnchor,
476) -> Option<String> {
477 use citum_schema::options::LinkAnchor;
478
479 if let Some(links) = local_links {
481 let anchor = links.anchor.as_ref().unwrap_or(&LinkAnchor::Component);
482 if matches!(anchor, LinkAnchor::Component) || *anchor == component_anchor {
483 return resolve_url(links, reference);
484 }
485 }
486
487 if let Some(links) = global_links
489 && let Some(anchor) = &links.anchor
490 && *anchor == component_anchor
491 {
492 return resolve_url(links, reference);
493 }
494
495 None
496}
497
498#[derive(Debug, Clone, Default)]
500pub struct ProcValues<T = String> {
501 pub value: T,
503 pub prefix: Option<String>,
505 pub suffix: Option<String>,
507 pub url: Option<String>,
509 pub substituted_key: Option<String>,
512 pub pre_formatted: bool,
514}
515
516#[derive(Debug, Clone, Default)]
518pub struct ProcHints {
519 pub disamb_condition: bool,
521 pub group_index: usize,
523 pub group_length: usize,
525 pub group_key: String,
527 pub expand_given_names: bool,
529 pub expand_given_names_primary_only: bool,
531 pub min_names_to_show: Option<usize>,
533 pub citation_number: Option<usize>,
535 pub citation_sub_label: Option<String>,
537 pub position: Option<citum_schema::citation::Position>,
539 pub integral_name_state: Option<citum_schema::citation::IntegralNameState>,
541 pub org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
543 pub first_reference_note_number: Option<u32>,
546 pub suppress_disambiguation_title: bool,
550}
551
552#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
554pub enum RenderContext {
555 #[default]
556 Citation,
558 Bibliography,
560}
561
562#[derive(Clone)]
564pub struct RenderOptions<'a> {
565 pub config: &'a Config,
567 pub bibliography_config: Option<BibliographyConfig>,
569 pub locale: &'a Locale,
571 pub context: RenderContext,
573 pub mode: citum_schema::citation::CitationMode,
575 pub suppress_author: bool,
578 pub locator_raw: Option<&'a citum_schema::citation::CitationLocator>,
580 pub ref_type: Option<String>,
582 pub show_semantics: bool,
584 pub current_template_index: Option<usize>,
586 pub abbreviation_map: Option<&'a crate::api::AbbreviationMap>,
588}
589
590pub trait ComponentValues {
592 fn values<F: crate::render::format::OutputFormat<Output = String>>(
594 &self,
595 reference: &Reference,
596 hints: &ProcHints,
597 options: &RenderOptions<'_>,
598 ) -> Option<ProcValues<F::Output>>;
599}
600
601impl ComponentValues for TemplateComponent {
602 fn values<F: crate::render::format::OutputFormat<Output = String>>(
603 &self,
604 reference: &Reference,
605 hints: &ProcHints,
606 options: &RenderOptions<'_>,
607 ) -> Option<ProcValues<F::Output>> {
608 match self {
609 TemplateComponent::Contributor(c) => c.values::<F>(reference, hints, options),
610 TemplateComponent::Date(d) => d.values::<F>(reference, hints, options),
611 TemplateComponent::Title(t) => t.values::<F>(reference, hints, options),
612 TemplateComponent::Number(n) => n.values::<F>(reference, hints, options),
613 TemplateComponent::Variable(v) => v.values::<F>(reference, hints, options),
614 TemplateComponent::Group(l) => l.values::<F>(reference, hints, options),
615 TemplateComponent::Term(t) => t.values::<F>(reference, hints, options),
616 _ => None,
617 }
618 }
619}
620
621#[must_use]
628pub fn should_strip_periods(
629 rendering: &citum_schema::template::Rendering,
630 options: &RenderOptions<'_>,
631) -> bool {
632 rendering
633 .strip_periods
634 .or(options.config.strip_periods)
635 .unwrap_or(false)
636}
637
638#[must_use]
643pub fn strip_trailing_periods(s: &str) -> String {
644 s.trim_end_matches('.').to_string()
645}
646
647#[must_use]
651pub fn apply_abbreviation(value: String, map: Option<&crate::api::AbbreviationMap>) -> String {
652 if let Some(abbr) = map.and_then(|m| m.0.get(&value)) {
653 return abbr.clone();
654 }
655 value
656}