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
355fn original_script_display(name: &citum_schema::reference::contributor::StructuredName) -> String {
360 use unicode_script::{Script, UnicodeScript};
361
362 let family = name.family.to_string();
363 let given = name.given.to_string();
364 let is_cjk = family.chars().chain(given.chars()).any(|ch| {
365 matches!(
366 ch.script(),
367 Script::Han | Script::Hiragana | Script::Katakana | Script::Hangul
368 )
369 });
370 if is_cjk || family.is_empty() || given.is_empty() {
371 format!("{family}{given}")
372 } else {
373 format!("{given} {family}")
374 }
375}
376
377#[must_use]
389pub fn resolve_multilingual_name(
390 contributor: &citum_schema::reference::contributor::Contributor,
391 mode: Option<&citum_schema::options::MultilingualMode>,
392 preferred_transliteration: Option<&[String]>,
393 preferred_script: Option<&String>,
394 style_locale: &str,
395) -> Vec<crate::reference::FlatName> {
396 use citum_schema::options::MultilingualMode;
397 use citum_schema::reference::contributor::Contributor;
398
399 match contributor {
400 Contributor::SimpleName(_) | Contributor::StructuredName(_) => contributor.to_names_vec(),
402
403 Contributor::Multilingual(m) => {
405 let mode = mode.unwrap_or(&MultilingualMode::Primary);
406
407 let selected_name = match mode {
408 MultilingualMode::Primary => &m.original,
409 MultilingualMode::Transliterated => {
410 select_by_transliteration(m, preferred_transliteration, preferred_script)
411 }
412 MultilingualMode::Translated => {
413 m.translations.get(style_locale).unwrap_or(&m.original)
414 }
415 MultilingualMode::Combined => {
417 select_by_transliteration(m, preferred_transliteration, preferred_script)
418 }
419 MultilingualMode::Pattern(_) => {
423 select_by_transliteration(m, preferred_transliteration, preferred_script)
424 }
425 };
426
427 let original_script = match mode {
432 MultilingualMode::Pattern(segments) if selected_name != &m.original => segments
433 .iter()
434 .find(|segment| {
435 segment.view == citum_schema::options::MultilingualView::Original
436 })
437 .map(|segment| segment.wrap.apply(&original_script_display(&m.original))),
438 _ => None,
439 };
440
441 vec![crate::reference::FlatName {
443 given: Some(selected_name.given.to_string()),
444 family: Some(selected_name.family.to_string()),
445 suffix: selected_name.suffix.clone(),
446 dropping_particle: selected_name.dropping_particle.clone(),
447 non_dropping_particle: selected_name.non_dropping_particle.clone(),
448 literal: None,
449 short_name: None,
450 original_script,
451 }]
452 }
453
454 Contributor::ContributorList(l) => {
455 l.0.iter()
456 .flat_map(|c| {
457 resolve_multilingual_name(
458 c,
459 mode,
460 preferred_transliteration,
461 preferred_script,
462 style_locale,
463 )
464 })
465 .collect()
466 }
467 }
468}
469
470#[must_use]
472pub fn resolve_url(
473 links: &citum_schema::options::LinksConfig,
474 reference: &Reference,
475) -> Option<String> {
476 use citum_schema::options::LinkTarget;
477
478 let target = links.target.as_ref().unwrap_or(&LinkTarget::UrlOrDoi);
479
480 match target {
481 LinkTarget::Url => reference.url().map(|u| u.to_string()),
482 LinkTarget::Doi => reference.doi().map(|d| format!("https://doi.org/{d}")),
483 LinkTarget::UrlOrDoi => reference
484 .url()
485 .map(|u| u.to_string())
486 .or_else(|| reference.doi().map(|d| format!("https://doi.org/{d}"))),
487 LinkTarget::Pubmed => reference
488 .id()
489 .filter(|id| id.starts_with("pmid:"))
490 .map(|id| {
491 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
492 let result = format!("https://pubmed.ncbi.nlm.nih.gov/{}/", &id[5..]);
493 result
494 }),
495 LinkTarget::Pmcid => reference
496 .id()
497 .filter(|id| id.starts_with("pmc:"))
498 .map(|id| {
499 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
500 let result = format!("https://www.ncbi.nlm.nih.gov/pmc/articles/{}/", &id[4..]);
501 result
502 }),
503 }
504}
505
506#[must_use]
508pub fn resolve_effective_url(
509 local_links: Option<&citum_schema::options::LinksConfig>,
510 global_links: Option<&citum_schema::options::LinksConfig>,
511 reference: &Reference,
512 component_anchor: citum_schema::options::LinkAnchor,
513) -> Option<String> {
514 use citum_schema::options::LinkAnchor;
515
516 if let Some(links) = local_links {
518 let anchor = links.anchor.as_ref().unwrap_or(&LinkAnchor::Component);
519 if matches!(anchor, LinkAnchor::Component) || *anchor == component_anchor {
520 return resolve_url(links, reference);
521 }
522 }
523
524 if let Some(links) = global_links
526 && let Some(anchor) = &links.anchor
527 && *anchor == component_anchor
528 {
529 return resolve_url(links, reference);
530 }
531
532 None
533}
534
535#[derive(Debug, Clone, Default)]
537pub struct ProcValues<T = String> {
538 pub value: T,
540 pub prefix: Option<String>,
542 pub suffix: Option<String>,
544 pub url: Option<String>,
546 pub substituted_key: Option<String>,
549 pub pre_formatted: bool,
551}
552
553#[derive(Debug, Clone, Default)]
555pub struct ProcHints {
556 pub disamb_condition: bool,
558 pub group_index: usize,
560 pub group_length: usize,
562 pub group_key: String,
564 pub expand_given_names: bool,
566 pub expand_given_names_primary_only: bool,
568 pub min_names_to_show: Option<usize>,
570 pub citation_number: Option<usize>,
572 pub citation_sub_label: Option<String>,
574 pub position: Option<citum_schema::citation::Position>,
576 pub integral_name_state: Option<citum_schema::citation::IntegralNameState>,
578 pub org_abbreviation_state: Option<citum_schema::citation::IntegralNameState>,
580 pub first_reference_note_number: Option<u32>,
583 pub suppress_disambiguation_title: bool,
587}
588
589#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
591pub enum RenderContext {
592 #[default]
593 Citation,
595 Bibliography,
597}
598
599#[derive(Clone)]
601pub struct RenderOptions<'a> {
602 pub config: &'a Config,
604 pub bibliography_config: Option<BibliographyConfig>,
606 pub locale: &'a Locale,
608 pub context: RenderContext,
610 pub mode: citum_schema::citation::CitationMode,
612 pub suppress_author: bool,
615 pub locator_raw: Option<&'a citum_schema::citation::CitationLocator>,
617 pub ref_type: Option<String>,
619 pub show_semantics: bool,
621 pub current_template_index: Option<usize>,
623 pub abbreviation_map: Option<&'a crate::api::AbbreviationMap>,
625}
626
627pub trait ComponentValues {
629 fn values<F: crate::render::format::OutputFormat<Output = String>>(
631 &self,
632 reference: &Reference,
633 hints: &ProcHints,
634 options: &RenderOptions<'_>,
635 ) -> Option<ProcValues<F::Output>>;
636}
637
638impl ComponentValues for TemplateComponent {
639 fn values<F: crate::render::format::OutputFormat<Output = String>>(
640 &self,
641 reference: &Reference,
642 hints: &ProcHints,
643 options: &RenderOptions<'_>,
644 ) -> Option<ProcValues<F::Output>> {
645 match self {
646 TemplateComponent::Contributor(c) => c.values::<F>(reference, hints, options),
647 TemplateComponent::Date(d) => d.values::<F>(reference, hints, options),
648 TemplateComponent::Title(t) => t.values::<F>(reference, hints, options),
649 TemplateComponent::Number(n) => n.values::<F>(reference, hints, options),
650 TemplateComponent::Variable(v) => v.values::<F>(reference, hints, options),
651 TemplateComponent::Group(l) => l.values::<F>(reference, hints, options),
652 TemplateComponent::Term(t) => t.values::<F>(reference, hints, options),
653 _ => None,
654 }
655 }
656}
657
658#[must_use]
665pub fn should_strip_periods(
666 rendering: &citum_schema::template::Rendering,
667 options: &RenderOptions<'_>,
668) -> bool {
669 rendering
670 .strip_periods
671 .or(options.config.strip_periods)
672 .unwrap_or(false)
673}
674
675#[must_use]
680pub fn strip_trailing_periods(s: &str) -> String {
681 s.trim_end_matches('.').to_string()
682}
683
684#[must_use]
688pub fn apply_abbreviation(value: String, map: Option<&crate::api::AbbreviationMap>) -> String {
689 if let Some(abbr) = map.and_then(|m| m.0.get(&value)) {
690 return abbr.clone();
691 }
692 value
693}