1pub mod contributor;
13pub mod date;
15pub mod list;
17pub mod locator;
19pub mod message;
21pub mod number;
23pub mod range;
25pub mod term;
27pub mod text_case;
29pub mod title;
31pub(crate) mod type_class;
35pub mod type_label;
37pub mod variable;
39
40#[cfg(test)]
41#[allow(
42 clippy::unwrap_used,
43 clippy::expect_used,
44 clippy::panic,
45 clippy::indexing_slicing,
46 clippy::todo,
47 clippy::unimplemented,
48 clippy::unreachable,
49 clippy::get_unwrap,
50 reason = "Panicking is acceptable and often desired in tests."
51)]
52mod tests;
53
54use crate::reference::Reference;
55use citum_schema::locale::Locale;
56use citum_schema::options::{Config, bibliography::BibliographyConfig};
57use citum_schema::reference::types::Title;
58use citum_schema::template::{TemplateComponent, TitleType};
59use std::sync::Arc;
60
61pub use contributor::format_contributors_short;
62pub use date::int_to_letter;
63
64pub(crate) fn resolve_preferred_variant<'a, T>(
76 variants: &'a std::collections::HashMap<String, T>,
77 preferred_transliteration: Option<&[String]>,
78 preferred_script: Option<&String>,
79) -> Option<&'a T> {
80 let substring_match = |needle: &str| {
81 variants
82 .iter()
83 .filter(|(key, _)| key.contains(needle))
84 .min_by(|(a, _), (b, _)| a.cmp(b))
85 .map(|(_, value)| value)
86 };
87
88 if let Some(tags) = preferred_transliteration {
89 for tag in tags {
90 if let Some(value) = variants.get(tag) {
91 return Some(value);
92 }
93 }
94 for tag in tags {
95 if let Some(value) = substring_match(tag) {
96 return Some(value);
97 }
98 }
99 }
100
101 if let Some(script) = preferred_script {
102 if let Some(value) = variants.get(script) {
103 return Some(value);
104 }
105 if let Some(value) = substring_match(script) {
106 return Some(value);
107 }
108 }
109
110 None
111}
112
113fn resolve_transliteration<'a>(
115 transliterations: &'a std::collections::HashMap<String, String>,
116 preferred_transliteration: Option<&[String]>,
117 preferred_script: Option<&String>,
118) -> Option<&'a str> {
119 resolve_preferred_variant(
120 transliterations,
121 preferred_transliteration,
122 preferred_script,
123 )
124 .map(String::as_str)
125}
126
127fn resolve_translation<'a>(
128 translations: &'a std::collections::HashMap<citum_schema::reference::LangID, String>,
129 style_locale: &str,
130) -> Option<&'a str> {
131 translations
132 .get(style_locale)
133 .or_else(|| {
134 style_locale
135 .split(['-', '_'])
136 .next()
137 .and_then(|base| translations.get(base))
138 })
139 .map(String::as_str)
140}
141
142#[must_use]
156pub fn resolve_multilingual_string(
157 string: &citum_schema::reference::types::MultilingualString,
158 mode: Option<&citum_schema::options::MultilingualMode>,
159 preferred_transliteration: Option<&[String]>,
160 preferred_script: Option<&String>,
161 style_locale: &str,
162) -> String {
163 use citum_schema::options::MultilingualMode;
164 use citum_schema::reference::types::MultilingualString;
165
166 match string {
167 MultilingualString::Simple(s) => s.clone(),
168 MultilingualString::Complex(complex) => {
169 let mode = mode.unwrap_or(&MultilingualMode::Primary);
170
171 match mode {
172 MultilingualMode::Primary => complex.original.clone(),
173
174 MultilingualMode::Transliterated => {
175 if let Some(trans) = resolve_transliteration(
176 &complex.transliterations,
177 preferred_transliteration,
178 preferred_script,
179 ) {
180 return trans.to_string();
181 }
182
183 complex
185 .transliterations
186 .values()
187 .next()
188 .cloned()
189 .unwrap_or_else(|| complex.original.clone())
190 }
191
192 MultilingualMode::Translated => {
193 resolve_translation(&complex.translations, style_locale)
195 .map(ToString::to_string)
196 .unwrap_or_else(|| complex.original.clone())
197 }
198
199 MultilingualMode::Combined => {
200 let trans = resolve_transliteration(
202 &complex.transliterations,
203 preferred_transliteration,
204 preferred_script,
205 );
206
207 let translation = resolve_translation(&complex.translations, style_locale);
208
209 match (trans, translation) {
210 (Some(t), Some(tr)) => format!("{t} [{tr}]"),
211 (Some(t), None) => t.to_string(),
212 (None, Some(tr)) => format!("{} [{}]", complex.original, tr),
213 (None, None) => complex.original.clone(),
214 }
215 }
216
217 MultilingualMode::Pattern(segments) => resolve_multilingual_pattern(
218 segments,
219 &complex.original,
220 &complex.transliterations,
221 &complex.translations,
222 preferred_transliteration,
223 preferred_script,
224 style_locale,
225 ),
226 }
227 }
228 }
229}
230
231fn resolve_multilingual_pattern(
237 segments: &[citum_schema::options::MultilingualSegment],
238 original: &str,
239 transliterations: &std::collections::HashMap<String, String>,
240 translations: &std::collections::HashMap<citum_schema::reference::types::LangID, String>,
241 preferred_transliteration: Option<&[String]>,
242 preferred_script: Option<&String>,
243 style_locale: &str,
244) -> String {
245 use citum_schema::options::{MultilingualView, SegmentWrap};
246 let mut parts: Vec<String> = Vec::with_capacity(segments.len());
247 let mut last_text: Option<String> = None;
248
249 for seg in segments {
250 let text: Option<String> = match &seg.view {
251 MultilingualView::OriginalScript => Some(original.to_string()),
252 MultilingualView::Transliterated => resolve_transliteration(
253 transliterations,
254 preferred_transliteration,
255 preferred_script,
256 )
257 .map(ToString::to_string),
258 MultilingualView::Translated => {
259 resolve_translation(translations, style_locale).map(ToString::to_string)
260 }
261 };
262
263 let Some(text) = text else { continue };
264 if text.is_empty() {
265 continue;
266 }
267 if last_text.as_deref() == Some(text.as_str()) {
270 continue;
271 }
272
273 let wrapped = match &seg.wrap {
274 SegmentWrap::None => text.clone(),
275 other => other.apply(&text),
276 };
277 last_text = Some(text);
278 parts.push(wrapped);
279 }
280
281 parts.join(" ")
282}
283
284#[must_use]
290pub fn effective_field_language(
291 reference: &Reference,
292 scope: &str,
293 title: Option<&Title>,
294) -> Option<String> {
295 reference
296 .field_languages()
297 .get(scope)
298 .map(ToString::to_string)
299 .or_else(|| match title {
300 Some(Title::Multilingual(multilingual)) => {
301 multilingual.lang.as_ref().map(ToString::to_string)
302 }
303 _ => None,
304 })
305 .or_else(|| reference.language().map(|lang| lang.to_string()))
306}
307
308#[must_use]
310pub fn effective_item_language(reference: &Reference) -> Option<String> {
311 effective_field_language(reference, "title", reference.title().as_ref())
312}
313
314#[must_use]
316pub fn effective_component_language(
317 reference: &Reference,
318 component: &TemplateComponent,
319) -> Option<String> {
320 match component {
321 TemplateComponent::Title(title_component) => {
322 let title = match title_component.title {
323 TitleType::Primary => reference.title(),
324 TitleType::ContainerTitle => reference.container_title(),
325 TitleType::ParentMonograph => reference.container_title(),
326 TitleType::ParentSerial => reference.container_title(),
327 TitleType::CollectionTitle => reference.collection_title(),
328 _ => reference.title(),
329 };
330
331 let scope = match title_component.title {
332 TitleType::Primary => "title",
333 TitleType::ContainerTitle => "container-title",
334 TitleType::ParentMonograph => "parent-monograph.title",
335 TitleType::ParentSerial => "parent-serial.title",
336 TitleType::CollectionTitle => "collection-title",
337 _ => "title",
338 };
339
340 effective_field_language(reference, scope, title.as_ref())
341 }
342 _ => effective_item_language(reference),
343 }
344}
345
346fn select_by_transliteration<'a>(
348 m: &'a citum_schema::reference::contributor::MultilingualName,
349 preferred_transliteration: Option<&[String]>,
350 preferred_script: Option<&String>,
351) -> &'a citum_schema::reference::contributor::StructuredName {
352 if let Some(tags) = preferred_transliteration {
354 for tag in tags {
355 if let Some(name) = m.transliterations.get(tag) {
356 return name;
357 }
358 }
359 for tag in tags {
361 if let Some((_, name)) = m
362 .transliterations
363 .iter()
364 .find(|(k, _)| k.contains(tag.as_str()))
365 {
366 return name;
367 }
368 }
369 }
370 if let Some(script) = preferred_script {
372 if let Some(name) = m.transliterations.get(script) {
373 return name;
374 }
375 if let Some((_, name)) = m
377 .transliterations
378 .iter()
379 .find(|(tag, _)| tag.contains(script))
380 {
381 return name;
382 }
383 }
384 m.transliterations.values().next().unwrap_or(&m.original)
386}
387
388fn original_script_display(name: &citum_schema::reference::contributor::StructuredName) -> String {
393 use unicode_script::{Script, UnicodeScript};
394
395 let family = name.family.to_string();
396 let given = name.given.to_string();
397 let is_cjk = family.chars().chain(given.chars()).any(|ch| {
398 matches!(
399 ch.script(),
400 Script::Han | Script::Hiragana | Script::Katakana | Script::Hangul
401 )
402 });
403 if is_cjk || family.is_empty() || given.is_empty() {
404 format!("{family}{given}")
405 } else {
406 format!("{given} {family}")
407 }
408}
409
410#[must_use]
422pub fn resolve_multilingual_name(
423 contributor: &citum_schema::reference::contributor::Contributor,
424 mode: Option<&citum_schema::options::MultilingualMode>,
425 preferred_transliteration: Option<&[String]>,
426 preferred_script: Option<&String>,
427 style_locale: &str,
428) -> Vec<crate::reference::FlatName> {
429 use citum_schema::options::MultilingualMode;
430 use citum_schema::reference::contributor::Contributor;
431
432 match contributor {
433 Contributor::SimpleName(_) | Contributor::StructuredName(_) => contributor.to_names_vec(),
435
436 Contributor::Multilingual(m) => {
438 let mode = mode.unwrap_or(&MultilingualMode::Primary);
439
440 let selected_name = match mode {
441 MultilingualMode::Primary => &m.original,
442 MultilingualMode::Transliterated => {
443 select_by_transliteration(m, preferred_transliteration, preferred_script)
444 }
445 MultilingualMode::Translated => {
446 m.translations.get(style_locale).unwrap_or(&m.original)
447 }
448 MultilingualMode::Combined => {
450 select_by_transliteration(m, preferred_transliteration, preferred_script)
451 }
452 MultilingualMode::Pattern(_) => {
456 select_by_transliteration(m, preferred_transliteration, preferred_script)
457 }
458 };
459
460 let original_script = match mode {
465 MultilingualMode::Pattern(segments) if selected_name != &m.original => segments
466 .iter()
467 .find(|segment| {
468 segment.view == citum_schema::options::MultilingualView::OriginalScript
469 })
470 .map(|segment| segment.wrap.apply(&original_script_display(&m.original))),
471 _ => None,
472 };
473
474 vec![crate::reference::FlatName {
476 given: Some(selected_name.given.to_string()),
477 family: Some(selected_name.family.to_string()),
478 suffix: selected_name.suffix.clone(),
479 dropping_particle: selected_name.dropping_particle.clone(),
480 non_dropping_particle: selected_name.non_dropping_particle.clone(),
481 literal: None,
482 short_name: None,
483 original_script,
484 }]
485 }
486
487 Contributor::ContributorList(l) => {
488 l.0.iter()
489 .flat_map(|c| {
490 resolve_multilingual_name(
491 c,
492 mode,
493 preferred_transliteration,
494 preferred_script,
495 style_locale,
496 )
497 })
498 .collect()
499 }
500 }
501}
502
503#[must_use]
505pub fn resolve_url(
506 links: &citum_schema::options::LinksConfig,
507 reference: &Reference,
508) -> Option<String> {
509 use citum_schema::options::LinkTarget;
510
511 let target = links.target.as_ref().unwrap_or(&LinkTarget::UrlOrDoi);
512
513 let url = match target {
514 LinkTarget::Url => reference.url().map(|u| u.to_string()),
515 LinkTarget::Doi => reference.doi().map(|d| format!("https://doi.org/{d}")),
516 LinkTarget::UrlOrDoi => reference
517 .url()
518 .map(|u| u.to_string())
519 .or_else(|| reference.doi().map(|d| format!("https://doi.org/{d}"))),
520 LinkTarget::Pubmed => reference
521 .id()
522 .filter(|id| id.starts_with("pmid:"))
523 .map(|id| {
524 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
525 let result = format!("https://pubmed.ncbi.nlm.nih.gov/{}/", &id[5..]);
526 result
527 }),
528 LinkTarget::Pmcid => reference
529 .id()
530 .filter(|id| id.starts_with("pmc:"))
531 .map(|id| {
532 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
533 let result = format!("https://www.ncbi.nlm.nih.gov/pmc/articles/{}/", &id[4..]);
534 result
535 }),
536 };
537
538 if links.strip_protocol == Some(true) {
539 url.map(|u| {
540 u.strip_prefix("https://")
541 .or_else(|| u.strip_prefix("http://"))
542 .map_or_else(|| u.clone(), ToString::to_string)
543 })
544 } else {
545 url
546 }
547}
548
549#[must_use]
551pub fn resolve_effective_url(
552 local_links: Option<&citum_schema::options::LinksConfig>,
553 global_links: Option<&citum_schema::options::LinksConfig>,
554 reference: &Reference,
555 component_anchor: citum_schema::options::LinkAnchor,
556) -> Option<String> {
557 use citum_schema::options::LinkAnchor;
558
559 if let Some(links) = local_links {
561 let anchor = links.anchor.as_ref().unwrap_or(&LinkAnchor::Component);
562 if matches!(anchor, LinkAnchor::Component) || *anchor == component_anchor {
563 return resolve_url(links, reference);
564 }
565 }
566
567 if let Some(links) = global_links
569 && let Some(anchor) = &links.anchor
570 && *anchor == component_anchor
571 {
572 return resolve_url(links, reference);
573 }
574
575 None
576}
577
578#[derive(Debug, Clone, Default)]
580pub struct ProcValues<T = String> {
581 pub value: T,
583 pub prefix: Option<String>,
585 pub suffix: Option<String>,
587 pub url: Option<String>,
589 pub substituted_key: Option<String>,
592 pub pre_formatted: bool,
594}
595
596#[derive(Debug, Clone, Default)]
598pub struct ProcHints {
599 pub disamb_condition: bool,
601 pub group_index: usize,
603 pub group_length: usize,
605 pub group_key: String,
607 pub expand_given_names: bool,
609 pub expand_given_names_primary_only: bool,
611 pub min_names_to_show: Option<usize>,
613 pub citation_number: Option<usize>,
615 pub citation_sub_label: Option<String>,
617 pub position: Option<citum_schema::citation::Position>,
619 pub integral_name_state: Option<citum_schema::citation::IntegralNameState>,
621 pub org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
623 pub first_reference_note_number: Option<u32>,
626 pub suppress_disambiguation_title: bool,
630}
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
634pub enum RenderContext {
635 #[default]
636 Citation,
638 Bibliography,
640}
641
642#[derive(Clone)]
644pub struct RenderOptions<'a> {
645 pub config: Arc<Config>,
647 pub bibliography_config: Option<Arc<BibliographyConfig>>,
649 pub locale: &'a Locale,
651 pub context: RenderContext,
653 pub mode: citum_schema::citation::CitationMode,
655 pub suppress_author: bool,
658 pub locator_raw: Option<&'a citum_schema::citation::CitationLocator>,
660 pub ref_type: Option<String>,
662 pub show_semantics: bool,
664 pub current_template_index: Option<usize>,
666 pub abbreviation_map: Option<&'a crate::api::AbbreviationMap>,
668}
669
670pub trait ComponentValues {
672 fn values<F: crate::render::format::OutputFormat<Output = String>>(
674 &self,
675 reference: &Reference,
676 hints: &ProcHints,
677 options: &RenderOptions<'_>,
678 ) -> Option<ProcValues<F::Output>>;
679}
680
681impl ComponentValues for TemplateComponent {
682 fn values<F: crate::render::format::OutputFormat<Output = String>>(
683 &self,
684 reference: &Reference,
685 hints: &ProcHints,
686 options: &RenderOptions<'_>,
687 ) -> Option<ProcValues<F::Output>> {
688 match self {
689 TemplateComponent::Contributor(c) => c.values::<F>(reference, hints, options),
690 TemplateComponent::Date(d) => d.values::<F>(reference, hints, options),
691 TemplateComponent::Title(t) => t.values::<F>(reference, hints, options),
692 TemplateComponent::Number(n) => n.values::<F>(reference, hints, options),
693 TemplateComponent::Variable(v) => v.values::<F>(reference, hints, options),
694 TemplateComponent::Message(m) => m.values::<F>(reference, hints, options),
695 TemplateComponent::Group(l) => l.values::<F>(reference, hints, options),
696 TemplateComponent::Term(t) => t.values::<F>(reference, hints, options),
697 TemplateComponent::TypeLabel(t) => t.values::<F>(reference, hints, options),
698 _ => None,
699 }
700 }
701}
702
703#[must_use]
710pub fn should_strip_periods(
711 rendering: &citum_schema::template::Rendering,
712 options: &RenderOptions<'_>,
713) -> bool {
714 rendering
715 .strip_periods
716 .or(options.config.strip_periods)
717 .unwrap_or(false)
718}
719
720#[must_use]
725pub fn strip_trailing_periods(s: &str) -> String {
726 s.trim_end_matches('.').to_string()
727}
728
729#[must_use]
733pub fn apply_abbreviation(value: String, map: Option<&crate::api::AbbreviationMap>) -> String {
734 if let Some(abbr) = map.and_then(|m| m.0.get(&value)) {
735 return abbr.clone();
736 }
737 value
738}