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
98#[must_use]
112pub fn resolve_multilingual_string(
113 string: &citum_schema::reference::types::MultilingualString,
114 mode: Option<&citum_schema::options::MultilingualMode>,
115 preferred_transliteration: Option<&[String]>,
116 preferred_script: Option<&String>,
117 style_locale: &str,
118) -> String {
119 use citum_schema::options::MultilingualMode;
120 use citum_schema::reference::types::MultilingualString;
121
122 match string {
123 MultilingualString::Simple(s) => s.clone(),
124 MultilingualString::Complex(complex) => {
125 let mode = mode.unwrap_or(&MultilingualMode::Primary);
126
127 match mode {
128 MultilingualMode::Primary => complex.original.clone(),
129
130 MultilingualMode::Transliterated => {
131 if let Some(trans) = resolve_transliteration(
132 &complex.transliterations,
133 preferred_transliteration,
134 preferred_script,
135 ) {
136 return trans.to_string();
137 }
138
139 complex
141 .transliterations
142 .values()
143 .next()
144 .cloned()
145 .unwrap_or_else(|| complex.original.clone())
146 }
147
148 MultilingualMode::Translated => {
149 complex
151 .translations
152 .get(style_locale)
153 .cloned()
154 .unwrap_or_else(|| complex.original.clone())
155 }
156
157 MultilingualMode::Combined => {
158 let trans = resolve_transliteration(
160 &complex.transliterations,
161 preferred_transliteration,
162 preferred_script,
163 );
164
165 let translation = complex.translations.get(style_locale);
166
167 match (trans, translation) {
168 (Some(t), Some(tr)) => format!("{t} [{tr}]"),
169 (Some(t), None) => t.to_string(),
170 (None, Some(tr)) => format!("{} [{}]", complex.original, tr),
171 (None, None) => complex.original.clone(),
172 }
173 }
174 }
175 }
176 }
177}
178
179#[must_use]
185pub fn effective_field_language(
186 reference: &Reference,
187 scope: &str,
188 title: Option<&Title>,
189) -> Option<String> {
190 reference
191 .field_languages()
192 .get(scope)
193 .map(ToString::to_string)
194 .or_else(|| match title {
195 Some(Title::Multilingual(multilingual)) => {
196 multilingual.lang.as_ref().map(ToString::to_string)
197 }
198 _ => None,
199 })
200 .or_else(|| reference.language().map(|lang| lang.to_string()))
201}
202
203#[must_use]
205pub fn effective_item_language(reference: &Reference) -> Option<String> {
206 effective_field_language(reference, "title", reference.title().as_ref())
207}
208
209#[must_use]
211pub fn effective_component_language(
212 reference: &Reference,
213 component: &TemplateComponent,
214) -> Option<String> {
215 match component {
216 TemplateComponent::Title(title_component) => {
217 let title = match title_component.title {
218 TitleType::Primary => reference.title(),
219 TitleType::ParentMonograph => reference.container_title(),
220 TitleType::ParentSerial => reference.container_title(),
221 _ => reference.title(),
222 };
223
224 let scope = match title_component.title {
225 TitleType::Primary => "title",
226 TitleType::ParentMonograph => "parent-monograph.title",
227 TitleType::ParentSerial => "parent-serial.title",
228 _ => "title",
229 };
230
231 effective_field_language(reference, scope, title.as_ref())
232 }
233 _ => effective_item_language(reference),
234 }
235}
236
237fn select_by_transliteration<'a>(
239 m: &'a citum_schema::reference::contributor::MultilingualName,
240 preferred_transliteration: Option<&[String]>,
241 preferred_script: Option<&String>,
242) -> &'a citum_schema::reference::contributor::StructuredName {
243 if let Some(tags) = preferred_transliteration {
245 for tag in tags {
246 if let Some(name) = m.transliterations.get(tag) {
247 return name;
248 }
249 }
250 for tag in tags {
252 if let Some((_, name)) = m
253 .transliterations
254 .iter()
255 .find(|(k, _)| k.contains(tag.as_str()))
256 {
257 return name;
258 }
259 }
260 }
261 if let Some(script) = preferred_script {
263 if let Some(name) = m.transliterations.get(script) {
264 return name;
265 }
266 if let Some((_, name)) = m
268 .transliterations
269 .iter()
270 .find(|(tag, _)| tag.contains(script))
271 {
272 return name;
273 }
274 }
275 m.transliterations.values().next().unwrap_or(&m.original)
277}
278
279#[must_use]
291pub fn resolve_multilingual_name(
292 contributor: &citum_schema::reference::contributor::Contributor,
293 mode: Option<&citum_schema::options::MultilingualMode>,
294 preferred_transliteration: Option<&[String]>,
295 preferred_script: Option<&String>,
296 style_locale: &str,
297) -> Vec<crate::reference::FlatName> {
298 use citum_schema::options::MultilingualMode;
299 use citum_schema::reference::contributor::Contributor;
300
301 match contributor {
302 Contributor::SimpleName(_) | Contributor::StructuredName(_) => contributor.to_names_vec(),
304
305 Contributor::Multilingual(m) => {
307 let mode = mode.unwrap_or(&MultilingualMode::Primary);
308
309 let selected_name = match mode {
310 MultilingualMode::Primary => &m.original,
311 MultilingualMode::Transliterated => {
312 select_by_transliteration(m, preferred_transliteration, preferred_script)
313 }
314 MultilingualMode::Translated => {
315 m.translations.get(style_locale).unwrap_or(&m.original)
316 }
317 MultilingualMode::Combined => {
319 select_by_transliteration(m, preferred_transliteration, preferred_script)
320 }
321 };
322
323 vec![crate::reference::FlatName {
325 given: Some(selected_name.given.to_string()),
326 family: Some(selected_name.family.to_string()),
327 suffix: selected_name.suffix.clone(),
328 dropping_particle: selected_name.dropping_particle.clone(),
329 non_dropping_particle: selected_name.non_dropping_particle.clone(),
330 literal: None,
331 short_name: None,
332 }]
333 }
334
335 Contributor::ContributorList(l) => {
336 l.0.iter()
337 .flat_map(|c| {
338 resolve_multilingual_name(
339 c,
340 mode,
341 preferred_transliteration,
342 preferred_script,
343 style_locale,
344 )
345 })
346 .collect()
347 }
348 }
349}
350
351#[must_use]
353pub fn resolve_url(
354 links: &citum_schema::options::LinksConfig,
355 reference: &Reference,
356) -> Option<String> {
357 use citum_schema::options::LinkTarget;
358
359 let target = links.target.as_ref().unwrap_or(&LinkTarget::UrlOrDoi);
360
361 match target {
362 LinkTarget::Url => reference.url().map(|u| u.to_string()),
363 LinkTarget::Doi => reference.doi().map(|d| format!("https://doi.org/{d}")),
364 LinkTarget::UrlOrDoi => reference
365 .url()
366 .map(|u| u.to_string())
367 .or_else(|| reference.doi().map(|d| format!("https://doi.org/{d}"))),
368 LinkTarget::Pubmed => reference
369 .id()
370 .filter(|id| id.starts_with("pmid:"))
371 .map(|id| {
372 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
373 let result = format!("https://pubmed.ncbi.nlm.nih.gov/{}/", &id[5..]);
374 result
375 }),
376 LinkTarget::Pmcid => reference
377 .id()
378 .filter(|id| id.starts_with("pmc:"))
379 .map(|id| {
380 #[allow(clippy::string_slice, reason = "known ASCII prefix")]
381 let result = format!("https://www.ncbi.nlm.nih.gov/pmc/articles/{}/", &id[4..]);
382 result
383 }),
384 }
385}
386
387#[must_use]
389pub fn resolve_effective_url(
390 local_links: Option<&citum_schema::options::LinksConfig>,
391 global_links: Option<&citum_schema::options::LinksConfig>,
392 reference: &Reference,
393 component_anchor: citum_schema::options::LinkAnchor,
394) -> Option<String> {
395 use citum_schema::options::LinkAnchor;
396
397 if let Some(links) = local_links {
399 let anchor = links.anchor.as_ref().unwrap_or(&LinkAnchor::Component);
400 if matches!(anchor, LinkAnchor::Component) || *anchor == component_anchor {
401 return resolve_url(links, reference);
402 }
403 }
404
405 if let Some(links) = global_links
407 && let Some(anchor) = &links.anchor
408 && *anchor == component_anchor
409 {
410 return resolve_url(links, reference);
411 }
412
413 None
414}
415
416#[derive(Debug, Clone, Default)]
418pub struct ProcValues<T = String> {
419 pub value: T,
421 pub prefix: Option<String>,
423 pub suffix: Option<String>,
425 pub url: Option<String>,
427 pub substituted_key: Option<String>,
430 pub pre_formatted: bool,
432}
433
434#[derive(Debug, Clone, Default)]
436pub struct ProcHints {
437 pub disamb_condition: bool,
439 pub group_index: usize,
441 pub group_length: usize,
443 pub group_key: String,
445 pub expand_given_names: bool,
447 pub min_names_to_show: Option<usize>,
449 pub citation_number: Option<usize>,
451 pub citation_sub_label: Option<String>,
453 pub position: Option<citum_schema::citation::Position>,
455 pub integral_name_state: Option<citum_schema::citation::IntegralNameState>,
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
461pub enum RenderContext {
462 #[default]
463 Citation,
465 Bibliography,
467}
468
469#[derive(Clone)]
471pub struct RenderOptions<'a> {
472 pub config: &'a Config,
474 pub bibliography_config: Option<BibliographyConfig>,
476 pub locale: &'a Locale,
478 pub context: RenderContext,
480 pub mode: citum_schema::citation::CitationMode,
482 pub suppress_author: bool,
485 pub locator_raw: Option<&'a citum_schema::citation::CitationLocator>,
487 pub ref_type: Option<String>,
489 pub show_semantics: bool,
491 pub current_template_index: Option<usize>,
493 pub abbreviation_map: Option<&'a crate::api::AbbreviationMap>,
495}
496
497pub trait ComponentValues {
499 fn values<F: crate::render::format::OutputFormat<Output = String>>(
501 &self,
502 reference: &Reference,
503 hints: &ProcHints,
504 options: &RenderOptions<'_>,
505 ) -> Option<ProcValues<F::Output>>;
506}
507
508impl ComponentValues for TemplateComponent {
509 fn values<F: crate::render::format::OutputFormat<Output = String>>(
510 &self,
511 reference: &Reference,
512 hints: &ProcHints,
513 options: &RenderOptions<'_>,
514 ) -> Option<ProcValues<F::Output>> {
515 match self {
516 TemplateComponent::Contributor(c) => c.values::<F>(reference, hints, options),
517 TemplateComponent::Date(d) => d.values::<F>(reference, hints, options),
518 TemplateComponent::Title(t) => t.values::<F>(reference, hints, options),
519 TemplateComponent::Number(n) => n.values::<F>(reference, hints, options),
520 TemplateComponent::Variable(v) => v.values::<F>(reference, hints, options),
521 TemplateComponent::Group(l) => l.values::<F>(reference, hints, options),
522 TemplateComponent::Term(t) => t.values::<F>(reference, hints, options),
523 _ => None,
524 }
525 }
526}
527
528#[must_use]
535pub fn should_strip_periods(
536 rendering: &citum_schema::template::Rendering,
537 options: &RenderOptions<'_>,
538) -> bool {
539 rendering
540 .strip_periods
541 .or(options.config.strip_periods)
542 .unwrap_or(false)
543}
544
545#[must_use]
550pub fn strip_trailing_periods(s: &str) -> String {
551 s.trim_end_matches('.').to_string()
552}
553
554#[must_use]
558pub fn apply_abbreviation(value: String, map: Option<&crate::api::AbbreviationMap>) -> String {
559 if let Some(abbr) = map.and_then(|m| m.0.get(&value)) {
560 return abbr.clone();
561 }
562 value
563}