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::rc::Rc;
60
61pub use contributor::format_contributors_short;
62pub use date::int_to_letter;
63
64fn resolve_transliteration<'a>(
72 transliterations: &'a std::collections::HashMap<String, String>,
73 preferred_transliteration: Option<&[String]>,
74 preferred_script: Option<&String>,
75) -> Option<&'a str> {
76 if let Some(tags) = preferred_transliteration {
78 for tag in tags {
79 if let Some(v) = transliterations.get(tag) {
80 return Some(v.as_str());
81 }
82 }
83 for tag in tags {
85 for (k, v) in transliterations {
86 if k.contains(tag.as_str()) {
87 return Some(v.as_str());
88 }
89 }
90 }
91 }
92 if let Some(script) = preferred_script {
94 if let Some(v) = transliterations.get(script) {
95 return Some(v.as_str());
96 }
97 for (k, v) in transliterations {
99 if k.contains(script.as_str()) {
100 return Some(v.as_str());
101 }
102 }
103 }
104 None
105}
106
107fn resolve_translation<'a>(
108 translations: &'a std::collections::HashMap<citum_schema::reference::LangID, String>,
109 style_locale: &str,
110) -> Option<&'a str> {
111 translations
112 .get(style_locale)
113 .or_else(|| {
114 style_locale
115 .split(['-', '_'])
116 .next()
117 .and_then(|base| translations.get(base))
118 })
119 .map(String::as_str)
120}
121
122#[must_use]
136pub fn resolve_multilingual_string(
137 string: &citum_schema::reference::types::MultilingualString,
138 mode: Option<&citum_schema::options::MultilingualMode>,
139 preferred_transliteration: Option<&[String]>,
140 preferred_script: Option<&String>,
141 style_locale: &str,
142) -> String {
143 use citum_schema::options::MultilingualMode;
144 use citum_schema::reference::types::MultilingualString;
145
146 match string {
147 MultilingualString::Simple(s) => s.clone(),
148 MultilingualString::Complex(complex) => {
149 let mode = mode.unwrap_or(&MultilingualMode::Primary);
150
151 match mode {
152 MultilingualMode::Primary => complex.original.clone(),
153
154 MultilingualMode::Transliterated => {
155 if let Some(trans) = resolve_transliteration(
156 &complex.transliterations,
157 preferred_transliteration,
158 preferred_script,
159 ) {
160 return trans.to_string();
161 }
162
163 complex
165 .transliterations
166 .values()
167 .next()
168 .cloned()
169 .unwrap_or_else(|| complex.original.clone())
170 }
171
172 MultilingualMode::Translated => {
173 resolve_translation(&complex.translations, style_locale)
175 .map(ToString::to_string)
176 .unwrap_or_else(|| complex.original.clone())
177 }
178
179 MultilingualMode::Combined => {
180 let trans = resolve_transliteration(
182 &complex.transliterations,
183 preferred_transliteration,
184 preferred_script,
185 );
186
187 let translation = resolve_translation(&complex.translations, style_locale);
188
189 match (trans, translation) {
190 (Some(t), Some(tr)) => format!("{t} [{tr}]"),
191 (Some(t), None) => t.to_string(),
192 (None, Some(tr)) => format!("{} [{}]", complex.original, tr),
193 (None, None) => complex.original.clone(),
194 }
195 }
196
197 MultilingualMode::Pattern(segments) => resolve_multilingual_pattern(
198 segments,
199 &complex.original,
200 &complex.transliterations,
201 &complex.translations,
202 preferred_transliteration,
203 preferred_script,
204 style_locale,
205 ),
206 }
207 }
208 }
209}
210
211fn resolve_multilingual_pattern(
217 segments: &[citum_schema::options::MultilingualSegment],
218 original: &str,
219 transliterations: &std::collections::HashMap<String, String>,
220 translations: &std::collections::HashMap<citum_schema::reference::types::LangID, String>,
221 preferred_transliteration: Option<&[String]>,
222 preferred_script: Option<&String>,
223 style_locale: &str,
224) -> String {
225 use citum_schema::options::{MultilingualView, SegmentWrap};
226 let mut parts: Vec<String> = Vec::with_capacity(segments.len());
227 let mut last_text: Option<String> = None;
228
229 for seg in segments {
230 let text: Option<String> = match &seg.view {
231 MultilingualView::OriginalScript => Some(original.to_string()),
232 MultilingualView::Transliterated => resolve_transliteration(
233 transliterations,
234 preferred_transliteration,
235 preferred_script,
236 )
237 .map(ToString::to_string),
238 MultilingualView::Translated => {
239 resolve_translation(translations, style_locale).map(ToString::to_string)
240 }
241 };
242
243 let Some(text) = text else { continue };
244 if text.is_empty() {
245 continue;
246 }
247 if last_text.as_deref() == Some(text.as_str()) {
250 continue;
251 }
252
253 let wrapped = match &seg.wrap {
254 SegmentWrap::None => text.clone(),
255 other => other.apply(&text),
256 };
257 last_text = Some(text);
258 parts.push(wrapped);
259 }
260
261 parts.join(" ")
262}
263
264#[must_use]
270pub fn effective_field_language(
271 reference: &Reference,
272 scope: &str,
273 title: Option<&Title>,
274) -> Option<String> {
275 reference
276 .field_languages()
277 .get(scope)
278 .map(ToString::to_string)
279 .or_else(|| match title {
280 Some(Title::Multilingual(multilingual)) => {
281 multilingual.lang.as_ref().map(ToString::to_string)
282 }
283 _ => None,
284 })
285 .or_else(|| reference.language().map(|lang| lang.to_string()))
286}
287
288#[must_use]
290pub fn effective_item_language(reference: &Reference) -> Option<String> {
291 effective_field_language(reference, "title", reference.title().as_ref())
292}
293
294#[must_use]
296pub fn effective_component_language(
297 reference: &Reference,
298 component: &TemplateComponent,
299) -> Option<String> {
300 match component {
301 TemplateComponent::Title(title_component) => {
302 let title = match title_component.title {
303 TitleType::Primary => reference.title(),
304 TitleType::ContainerTitle => reference.container_title(),
305 TitleType::ParentMonograph => reference.container_title(),
306 TitleType::ParentSerial => reference.container_title(),
307 TitleType::CollectionTitle => reference.collection_title(),
308 _ => reference.title(),
309 };
310
311 let scope = match title_component.title {
312 TitleType::Primary => "title",
313 TitleType::ContainerTitle => "container-title",
314 TitleType::ParentMonograph => "parent-monograph.title",
315 TitleType::ParentSerial => "parent-serial.title",
316 TitleType::CollectionTitle => "collection-title",
317 _ => "title",
318 };
319
320 effective_field_language(reference, scope, title.as_ref())
321 }
322 _ => effective_item_language(reference),
323 }
324}
325
326fn select_by_transliteration<'a>(
328 m: &'a citum_schema::reference::contributor::MultilingualName,
329 preferred_transliteration: Option<&[String]>,
330 preferred_script: Option<&String>,
331) -> &'a citum_schema::reference::contributor::StructuredName {
332 if let Some(tags) = preferred_transliteration {
334 for tag in tags {
335 if let Some(name) = m.transliterations.get(tag) {
336 return name;
337 }
338 }
339 for tag in tags {
341 if let Some((_, name)) = m
342 .transliterations
343 .iter()
344 .find(|(k, _)| k.contains(tag.as_str()))
345 {
346 return name;
347 }
348 }
349 }
350 if let Some(script) = preferred_script {
352 if let Some(name) = m.transliterations.get(script) {
353 return name;
354 }
355 if let Some((_, name)) = m
357 .transliterations
358 .iter()
359 .find(|(tag, _)| tag.contains(script))
360 {
361 return name;
362 }
363 }
364 m.transliterations.values().next().unwrap_or(&m.original)
366}
367
368fn original_script_display(name: &citum_schema::reference::contributor::StructuredName) -> String {
373 use unicode_script::{Script, UnicodeScript};
374
375 let family = name.family.to_string();
376 let given = name.given.to_string();
377 let is_cjk = family.chars().chain(given.chars()).any(|ch| {
378 matches!(
379 ch.script(),
380 Script::Han | Script::Hiragana | Script::Katakana | Script::Hangul
381 )
382 });
383 if is_cjk || family.is_empty() || given.is_empty() {
384 format!("{family}{given}")
385 } else {
386 format!("{given} {family}")
387 }
388}
389
390#[must_use]
402pub fn resolve_multilingual_name(
403 contributor: &citum_schema::reference::contributor::Contributor,
404 mode: Option<&citum_schema::options::MultilingualMode>,
405 preferred_transliteration: Option<&[String]>,
406 preferred_script: Option<&String>,
407 style_locale: &str,
408) -> Vec<crate::reference::FlatName> {
409 use citum_schema::options::MultilingualMode;
410 use citum_schema::reference::contributor::Contributor;
411
412 match contributor {
413 Contributor::SimpleName(_) | Contributor::StructuredName(_) => contributor.to_names_vec(),
415
416 Contributor::Multilingual(m) => {
418 let mode = mode.unwrap_or(&MultilingualMode::Primary);
419
420 let selected_name = match mode {
421 MultilingualMode::Primary => &m.original,
422 MultilingualMode::Transliterated => {
423 select_by_transliteration(m, preferred_transliteration, preferred_script)
424 }
425 MultilingualMode::Translated => {
426 m.translations.get(style_locale).unwrap_or(&m.original)
427 }
428 MultilingualMode::Combined => {
430 select_by_transliteration(m, preferred_transliteration, preferred_script)
431 }
432 MultilingualMode::Pattern(_) => {
436 select_by_transliteration(m, preferred_transliteration, preferred_script)
437 }
438 };
439
440 let original_script = match mode {
445 MultilingualMode::Pattern(segments) if selected_name != &m.original => segments
446 .iter()
447 .find(|segment| {
448 segment.view == citum_schema::options::MultilingualView::OriginalScript
449 })
450 .map(|segment| segment.wrap.apply(&original_script_display(&m.original))),
451 _ => None,
452 };
453
454 vec![crate::reference::FlatName {
456 given: Some(selected_name.given.to_string()),
457 family: Some(selected_name.family.to_string()),
458 suffix: selected_name.suffix.clone(),
459 dropping_particle: selected_name.dropping_particle.clone(),
460 non_dropping_particle: selected_name.non_dropping_particle.clone(),
461 literal: None,
462 short_name: None,
463 original_script,
464 }]
465 }
466
467 Contributor::ContributorList(l) => {
468 l.0.iter()
469 .flat_map(|c| {
470 resolve_multilingual_name(
471 c,
472 mode,
473 preferred_transliteration,
474 preferred_script,
475 style_locale,
476 )
477 })
478 .collect()
479 }
480 }
481}
482
483#[must_use]
485pub fn resolve_url(
486 links: &citum_schema::options::LinksConfig,
487 reference: &Reference,
488) -> Option<String> {
489 use citum_schema::options::LinkTarget;
490
491 let target = links.target.as_ref().unwrap_or(&LinkTarget::UrlOrDoi);
492
493 match target {
494 LinkTarget::Url => reference.url().map(|u| u.to_string()),
495 LinkTarget::Doi => reference.doi().map(|d| format!("https://doi.org/{d}")),
496 LinkTarget::UrlOrDoi => reference
497 .url()
498 .map(|u| u.to_string())
499 .or_else(|| reference.doi().map(|d| format!("https://doi.org/{d}"))),
500 LinkTarget::Pubmed => reference
501 .id()
502 .filter(|id| id.starts_with("pmid:"))
503 .map(|id| {
504 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
505 let result = format!("https://pubmed.ncbi.nlm.nih.gov/{}/", &id[5..]);
506 result
507 }),
508 LinkTarget::Pmcid => reference
509 .id()
510 .filter(|id| id.starts_with("pmc:"))
511 .map(|id| {
512 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
513 let result = format!("https://www.ncbi.nlm.nih.gov/pmc/articles/{}/", &id[4..]);
514 result
515 }),
516 }
517}
518
519#[must_use]
521pub fn resolve_effective_url(
522 local_links: Option<&citum_schema::options::LinksConfig>,
523 global_links: Option<&citum_schema::options::LinksConfig>,
524 reference: &Reference,
525 component_anchor: citum_schema::options::LinkAnchor,
526) -> Option<String> {
527 use citum_schema::options::LinkAnchor;
528
529 if let Some(links) = local_links {
531 let anchor = links.anchor.as_ref().unwrap_or(&LinkAnchor::Component);
532 if matches!(anchor, LinkAnchor::Component) || *anchor == component_anchor {
533 return resolve_url(links, reference);
534 }
535 }
536
537 if let Some(links) = global_links
539 && let Some(anchor) = &links.anchor
540 && *anchor == component_anchor
541 {
542 return resolve_url(links, reference);
543 }
544
545 None
546}
547
548#[derive(Debug, Clone, Default)]
550pub struct ProcValues<T = String> {
551 pub value: T,
553 pub prefix: Option<String>,
555 pub suffix: Option<String>,
557 pub url: Option<String>,
559 pub substituted_key: Option<String>,
562 pub pre_formatted: bool,
564}
565
566#[derive(Debug, Clone, Default)]
568pub struct ProcHints {
569 pub disamb_condition: bool,
571 pub group_index: usize,
573 pub group_length: usize,
575 pub group_key: String,
577 pub expand_given_names: bool,
579 pub expand_given_names_primary_only: bool,
581 pub min_names_to_show: Option<usize>,
583 pub citation_number: Option<usize>,
585 pub citation_sub_label: Option<String>,
587 pub position: Option<citum_schema::citation::Position>,
589 pub integral_name_state: Option<citum_schema::citation::IntegralNameState>,
591 pub org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
593 pub first_reference_note_number: Option<u32>,
596 pub suppress_disambiguation_title: bool,
600}
601
602#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
604pub enum RenderContext {
605 #[default]
606 Citation,
608 Bibliography,
610}
611
612#[derive(Clone)]
614pub struct RenderOptions<'a> {
615 pub config: Rc<Config>,
617 pub bibliography_config: Option<Rc<BibliographyConfig>>,
619 pub locale: &'a Locale,
621 pub context: RenderContext,
623 pub mode: citum_schema::citation::CitationMode,
625 pub suppress_author: bool,
628 pub locator_raw: Option<&'a citum_schema::citation::CitationLocator>,
630 pub ref_type: Option<String>,
632 pub show_semantics: bool,
634 pub current_template_index: Option<usize>,
636 pub abbreviation_map: Option<&'a crate::api::AbbreviationMap>,
638}
639
640pub trait ComponentValues {
642 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}
650
651impl ComponentValues for TemplateComponent {
652 fn values<F: crate::render::format::OutputFormat<Output = String>>(
653 &self,
654 reference: &Reference,
655 hints: &ProcHints,
656 options: &RenderOptions<'_>,
657 ) -> Option<ProcValues<F::Output>> {
658 match self {
659 TemplateComponent::Contributor(c) => c.values::<F>(reference, hints, options),
660 TemplateComponent::Date(d) => d.values::<F>(reference, hints, options),
661 TemplateComponent::Title(t) => t.values::<F>(reference, hints, options),
662 TemplateComponent::Number(n) => n.values::<F>(reference, hints, options),
663 TemplateComponent::Variable(v) => v.values::<F>(reference, hints, options),
664 TemplateComponent::Message(m) => m.values::<F>(reference, hints, options),
665 TemplateComponent::Group(l) => l.values::<F>(reference, hints, options),
666 TemplateComponent::Term(t) => t.values::<F>(reference, hints, options),
667 TemplateComponent::TypeLabel(t) => t.values::<F>(reference, hints, options),
668 _ => None,
669 }
670 }
671}
672
673#[must_use]
680pub fn should_strip_periods(
681 rendering: &citum_schema::template::Rendering,
682 options: &RenderOptions<'_>,
683) -> bool {
684 rendering
685 .strip_periods
686 .or(options.config.strip_periods)
687 .unwrap_or(false)
688}
689
690#[must_use]
695pub fn strip_trailing_periods(s: &str) -> String {
696 s.trim_end_matches('.').to_string()
697}
698
699#[must_use]
703pub fn apply_abbreviation(value: String, map: Option<&crate::api::AbbreviationMap>) -> String {
704 if let Some(abbr) = map.and_then(|m| m.0.get(&value)) {
705 return abbr.clone();
706 }
707 value
708}