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::OriginalScript => 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::ContainerTitle => reference.container_title(),
296 TitleType::ParentMonograph => reference.container_title(),
297 TitleType::ParentSerial => reference.container_title(),
298 TitleType::CollectionTitle => reference.collection_title(),
299 _ => reference.title(),
300 };
301
302 let scope = match title_component.title {
303 TitleType::Primary => "title",
304 TitleType::ContainerTitle => "container-title",
305 TitleType::ParentMonograph => "parent-monograph.title",
306 TitleType::ParentSerial => "parent-serial.title",
307 TitleType::CollectionTitle => "collection-title",
308 _ => "title",
309 };
310
311 effective_field_language(reference, scope, title.as_ref())
312 }
313 _ => effective_item_language(reference),
314 }
315}
316
317fn select_by_transliteration<'a>(
319 m: &'a citum_schema::reference::contributor::MultilingualName,
320 preferred_transliteration: Option<&[String]>,
321 preferred_script: Option<&String>,
322) -> &'a citum_schema::reference::contributor::StructuredName {
323 if let Some(tags) = preferred_transliteration {
325 for tag in tags {
326 if let Some(name) = m.transliterations.get(tag) {
327 return name;
328 }
329 }
330 for tag in tags {
332 if let Some((_, name)) = m
333 .transliterations
334 .iter()
335 .find(|(k, _)| k.contains(tag.as_str()))
336 {
337 return name;
338 }
339 }
340 }
341 if let Some(script) = preferred_script {
343 if let Some(name) = m.transliterations.get(script) {
344 return name;
345 }
346 if let Some((_, name)) = m
348 .transliterations
349 .iter()
350 .find(|(tag, _)| tag.contains(script))
351 {
352 return name;
353 }
354 }
355 m.transliterations.values().next().unwrap_or(&m.original)
357}
358
359fn original_script_display(name: &citum_schema::reference::contributor::StructuredName) -> String {
364 use unicode_script::{Script, UnicodeScript};
365
366 let family = name.family.to_string();
367 let given = name.given.to_string();
368 let is_cjk = family.chars().chain(given.chars()).any(|ch| {
369 matches!(
370 ch.script(),
371 Script::Han | Script::Hiragana | Script::Katakana | Script::Hangul
372 )
373 });
374 if is_cjk || family.is_empty() || given.is_empty() {
375 format!("{family}{given}")
376 } else {
377 format!("{given} {family}")
378 }
379}
380
381#[must_use]
393pub fn resolve_multilingual_name(
394 contributor: &citum_schema::reference::contributor::Contributor,
395 mode: Option<&citum_schema::options::MultilingualMode>,
396 preferred_transliteration: Option<&[String]>,
397 preferred_script: Option<&String>,
398 style_locale: &str,
399) -> Vec<crate::reference::FlatName> {
400 use citum_schema::options::MultilingualMode;
401 use citum_schema::reference::contributor::Contributor;
402
403 match contributor {
404 Contributor::SimpleName(_) | Contributor::StructuredName(_) => contributor.to_names_vec(),
406
407 Contributor::Multilingual(m) => {
409 let mode = mode.unwrap_or(&MultilingualMode::Primary);
410
411 let selected_name = match mode {
412 MultilingualMode::Primary => &m.original,
413 MultilingualMode::Transliterated => {
414 select_by_transliteration(m, preferred_transliteration, preferred_script)
415 }
416 MultilingualMode::Translated => {
417 m.translations.get(style_locale).unwrap_or(&m.original)
418 }
419 MultilingualMode::Combined => {
421 select_by_transliteration(m, preferred_transliteration, preferred_script)
422 }
423 MultilingualMode::Pattern(_) => {
427 select_by_transliteration(m, preferred_transliteration, preferred_script)
428 }
429 };
430
431 let original_script = match mode {
436 MultilingualMode::Pattern(segments) if selected_name != &m.original => segments
437 .iter()
438 .find(|segment| {
439 segment.view == citum_schema::options::MultilingualView::OriginalScript
440 })
441 .map(|segment| segment.wrap.apply(&original_script_display(&m.original))),
442 _ => None,
443 };
444
445 vec![crate::reference::FlatName {
447 given: Some(selected_name.given.to_string()),
448 family: Some(selected_name.family.to_string()),
449 suffix: selected_name.suffix.clone(),
450 dropping_particle: selected_name.dropping_particle.clone(),
451 non_dropping_particle: selected_name.non_dropping_particle.clone(),
452 literal: None,
453 short_name: None,
454 original_script,
455 }]
456 }
457
458 Contributor::ContributorList(l) => {
459 l.0.iter()
460 .flat_map(|c| {
461 resolve_multilingual_name(
462 c,
463 mode,
464 preferred_transliteration,
465 preferred_script,
466 style_locale,
467 )
468 })
469 .collect()
470 }
471 }
472}
473
474#[must_use]
476pub fn resolve_url(
477 links: &citum_schema::options::LinksConfig,
478 reference: &Reference,
479) -> Option<String> {
480 use citum_schema::options::LinkTarget;
481
482 let target = links.target.as_ref().unwrap_or(&LinkTarget::UrlOrDoi);
483
484 match target {
485 LinkTarget::Url => reference.url().map(|u| u.to_string()),
486 LinkTarget::Doi => reference.doi().map(|d| format!("https://doi.org/{d}")),
487 LinkTarget::UrlOrDoi => reference
488 .url()
489 .map(|u| u.to_string())
490 .or_else(|| reference.doi().map(|d| format!("https://doi.org/{d}"))),
491 LinkTarget::Pubmed => reference
492 .id()
493 .filter(|id| id.starts_with("pmid:"))
494 .map(|id| {
495 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
496 let result = format!("https://pubmed.ncbi.nlm.nih.gov/{}/", &id[5..]);
497 result
498 }),
499 LinkTarget::Pmcid => reference
500 .id()
501 .filter(|id| id.starts_with("pmc:"))
502 .map(|id| {
503 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
504 let result = format!("https://www.ncbi.nlm.nih.gov/pmc/articles/{}/", &id[4..]);
505 result
506 }),
507 }
508}
509
510#[must_use]
512pub fn resolve_effective_url(
513 local_links: Option<&citum_schema::options::LinksConfig>,
514 global_links: Option<&citum_schema::options::LinksConfig>,
515 reference: &Reference,
516 component_anchor: citum_schema::options::LinkAnchor,
517) -> Option<String> {
518 use citum_schema::options::LinkAnchor;
519
520 if let Some(links) = local_links {
522 let anchor = links.anchor.as_ref().unwrap_or(&LinkAnchor::Component);
523 if matches!(anchor, LinkAnchor::Component) || *anchor == component_anchor {
524 return resolve_url(links, reference);
525 }
526 }
527
528 if let Some(links) = global_links
530 && let Some(anchor) = &links.anchor
531 && *anchor == component_anchor
532 {
533 return resolve_url(links, reference);
534 }
535
536 None
537}
538
539#[derive(Debug, Clone, Default)]
541pub struct ProcValues<T = String> {
542 pub value: T,
544 pub prefix: Option<String>,
546 pub suffix: Option<String>,
548 pub url: Option<String>,
550 pub substituted_key: Option<String>,
553 pub pre_formatted: bool,
555}
556
557#[derive(Debug, Clone, Default)]
559pub struct ProcHints {
560 pub disamb_condition: bool,
562 pub group_index: usize,
564 pub group_length: usize,
566 pub group_key: String,
568 pub expand_given_names: bool,
570 pub expand_given_names_primary_only: bool,
572 pub min_names_to_show: Option<usize>,
574 pub citation_number: Option<usize>,
576 pub citation_sub_label: Option<String>,
578 pub position: Option<citum_schema::citation::Position>,
580 pub integral_name_state: Option<citum_schema::citation::IntegralNameState>,
582 pub org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
584 pub first_reference_note_number: Option<u32>,
587 pub suppress_disambiguation_title: bool,
591}
592
593#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
595pub enum RenderContext {
596 #[default]
597 Citation,
599 Bibliography,
601}
602
603#[derive(Clone)]
605pub struct RenderOptions<'a> {
606 pub config: &'a Config,
608 pub bibliography_config: Option<BibliographyConfig>,
610 pub locale: &'a Locale,
612 pub context: RenderContext,
614 pub mode: citum_schema::citation::CitationMode,
616 pub suppress_author: bool,
619 pub locator_raw: Option<&'a citum_schema::citation::CitationLocator>,
621 pub ref_type: Option<String>,
623 pub show_semantics: bool,
625 pub current_template_index: Option<usize>,
627 pub abbreviation_map: Option<&'a crate::api::AbbreviationMap>,
629}
630
631pub trait ComponentValues {
633 fn values<F: crate::render::format::OutputFormat<Output = String>>(
635 &self,
636 reference: &Reference,
637 hints: &ProcHints,
638 options: &RenderOptions<'_>,
639 ) -> Option<ProcValues<F::Output>>;
640}
641
642impl ComponentValues for TemplateComponent {
643 fn values<F: crate::render::format::OutputFormat<Output = String>>(
644 &self,
645 reference: &Reference,
646 hints: &ProcHints,
647 options: &RenderOptions<'_>,
648 ) -> Option<ProcValues<F::Output>> {
649 match self {
650 TemplateComponent::Contributor(c) => c.values::<F>(reference, hints, options),
651 TemplateComponent::Date(d) => d.values::<F>(reference, hints, options),
652 TemplateComponent::Title(t) => t.values::<F>(reference, hints, options),
653 TemplateComponent::Number(n) => n.values::<F>(reference, hints, options),
654 TemplateComponent::Variable(v) => v.values::<F>(reference, hints, options),
655 TemplateComponent::Group(l) => l.values::<F>(reference, hints, options),
656 TemplateComponent::Term(t) => t.values::<F>(reference, hints, options),
657 _ => None,
658 }
659 }
660}
661
662#[must_use]
669pub fn should_strip_periods(
670 rendering: &citum_schema::template::Rendering,
671 options: &RenderOptions<'_>,
672) -> bool {
673 rendering
674 .strip_periods
675 .or(options.config.strip_periods)
676 .unwrap_or(false)
677}
678
679#[must_use]
684pub fn strip_trailing_periods(s: &str) -> String {
685 s.trim_end_matches('.').to_string()
686}
687
688#[must_use]
692pub fn apply_abbreviation(value: String, map: Option<&crate::api::AbbreviationMap>) -> String {
693 if let Some(abbr) = map.and_then(|m| m.0.get(&value)) {
694 return abbr.clone();
695 }
696 value
697}