1use crate::reference::Reference;
12use crate::values::{ComponentValues, ProcHints, ProcValues, RenderOptions};
13use citum_schema::locale::ArchiveHierarchyField;
14use citum_schema::options::titles::TextCase;
15use citum_schema::reference::{ClassExtension, RichText, WorkRelation};
16use citum_schema::template::{SimpleVariable, TemplateVariable};
17
18fn container_title_short(reference: &Reference) -> Option<String> {
24 reference.container_title().and_then(|t| match t {
25 citum_schema::reference::types::Title::Shorthand(short, _) => Some(short),
26 citum_schema::reference::types::Title::Single(s) => Some(s),
27 _ => None,
28 })
29}
30
31fn event_place(reference: &Reference) -> Option<String> {
32 match reference.extension() {
33 ClassExtension::Event(event) => event.location.clone(),
34 ClassExtension::Monograph(monograph) => embedded_event_place(monograph.event.as_ref()?),
35 ClassExtension::SerialComponent(component) => {
36 embedded_event_place(component.event.as_ref()?)
37 }
38 ClassExtension::AudioVisual(audio_visual) => {
39 embedded_event_place(audio_visual.event.as_ref()?)
40 }
41 ClassExtension::CollectionComponent(component) => {
42 embedded_container_event_place(component.container.as_ref()?)
43 }
44 _ => None,
45 }
46}
47
48fn event_title(reference: &Reference) -> Option<String> {
49 match reference.extension() {
50 ClassExtension::Event(event) => event.title.as_ref().map(ToString::to_string),
51 ClassExtension::Monograph(monograph) => embedded_event_title(monograph.event.as_ref()?),
52 ClassExtension::SerialComponent(component) => {
53 embedded_event_title(component.event.as_ref()?)
54 }
55 ClassExtension::AudioVisual(audio_visual) => {
56 embedded_event_title(audio_visual.event.as_ref()?)
57 }
58 ClassExtension::CollectionComponent(component) => {
59 embedded_container_event_title(component.container.as_ref()?)
60 }
61 _ => None,
62 }
63}
64
65fn embedded_event_title(relation: &WorkRelation) -> Option<String> {
66 let WorkRelation::Embedded(reference) = relation else {
67 return None;
68 };
69 let ClassExtension::Event(event) = reference.extension() else {
70 return None;
71 };
72 event.title.as_ref().map(ToString::to_string)
73}
74
75fn embedded_event_place(relation: &WorkRelation) -> Option<String> {
76 let WorkRelation::Embedded(reference) = relation else {
77 return None;
78 };
79 let ClassExtension::Event(event) = reference.extension() else {
80 return None;
81 };
82 event.location.clone()
83}
84
85fn embedded_container_event_title(relation: &WorkRelation) -> Option<String> {
89 let WorkRelation::Embedded(reference) = relation else {
90 return None;
91 };
92 let ClassExtension::Collection(collection) = reference.extension() else {
93 return None;
94 };
95 embedded_event_title(collection.event.as_ref()?)
96}
97
98fn embedded_container_event_place(relation: &WorkRelation) -> Option<String> {
101 let WorkRelation::Embedded(reference) = relation else {
102 return None;
103 };
104 let ClassExtension::Collection(collection) = reference.extension() else {
105 return None;
106 };
107 embedded_event_place(collection.event.as_ref()?)
108}
109
110fn dimensions(reference: &Reference) -> Option<String> {
111 match reference.extension() {
112 ClassExtension::Monograph(monograph) => {
113 monograph.duration.clone().or(monograph.size.clone())
114 }
115 ClassExtension::SerialComponent(component) => component.duration.clone(),
116 ClassExtension::AudioVisual(audio_visual) => audio_visual.dimensions.clone(),
117 _ => None,
118 }
119}
120
121fn raw_medium(reference: &Reference) -> Option<String> {
122 match reference.extension() {
123 ClassExtension::Monograph(monograph) => monograph.medium.clone(),
124 ClassExtension::CollectionComponent(component) => component.medium.clone(),
125 ClassExtension::SerialComponent(component) => component.medium.clone(),
126 ClassExtension::AudioVisual(audio_visual) => audio_visual.medium.clone(),
127 ClassExtension::Software(software) => software.platform.clone(),
128 _ => None,
129 }
130}
131
132fn raw_genre(reference: &Reference) -> Option<String> {
133 match reference.extension() {
134 ClassExtension::Monograph(monograph) => monograph.genre.clone(),
135 ClassExtension::CollectionComponent(component) => component.genre.clone(),
136 ClassExtension::SerialComponent(component) => component.genre.clone(),
137 ClassExtension::Event(event) => event.genre.clone(),
138 ClassExtension::AudioVisual(audio_visual) => audio_visual.core.genre.clone(),
139 _ => None,
140 }
141}
142
143fn references(reference: &Reference) -> Option<String> {
144 match reference.extension() {
145 ClassExtension::Monograph(monograph) => monograph.references.clone(),
146 _ => None,
147 }
148}
149
150fn resolve_archive_name(reference: &Reference, options: &RenderOptions<'_>) -> Option<String> {
151 let archive_name = reference.archive_name()?;
152 let multilingual = options.config.multilingual.as_ref();
153
154 Some(crate::values::resolve_multilingual_string(
155 &archive_name,
156 multilingual.and_then(|ml| ml.name_mode.as_ref()),
157 multilingual.and_then(|ml| ml.preferred_transliteration.as_deref()),
158 multilingual.and_then(|ml| ml.preferred_script.as_ref()),
159 options.locale.locale.as_str(),
160 ))
161}
162
163fn assemble_archive_hierarchy(
164 reference: &Reference,
165 options: &RenderOptions<'_>,
166) -> Option<String> {
167 let locale = options.locale;
168 let mut parts: Vec<String> = Vec::new();
169
170 if let Some(collection) = reference.archive_collection() {
172 let label = locale
173 .resolved_archive_term(ArchiveHierarchyField::Collection)
174 .map(|l| format!("{l} "))
175 .unwrap_or_default();
176 if let Some(cid) = reference.archive_collection_id() {
177 parts.push(format!("{label}{collection} ({cid})"));
178 } else {
179 parts.push(format!("{label}{collection}"));
180 }
181 }
182
183 if let Some(series) = reference.archive_series() {
185 let label = locale
186 .resolved_archive_term(ArchiveHierarchyField::Series)
187 .map(|l| format!("{l} "))
188 .unwrap_or_default();
189 parts.push(format!("{label}{series}"));
190 }
191
192 if let Some(b) = reference.archive_box() {
194 let label = locale
195 .resolved_archive_term(ArchiveHierarchyField::Box)
196 .map(|l| format!("{l} "))
197 .unwrap_or_default();
198 parts.push(format!("{label}{b}"));
199 }
200
201 if let Some(folder) = reference.archive_folder() {
203 let label = locale
204 .resolved_archive_term(ArchiveHierarchyField::Folder)
205 .map(|l| format!("{l} "))
206 .unwrap_or_default();
207 parts.push(format!("{label}{folder}"));
208 }
209
210 if let Some(item) = reference.archive_item() {
212 let label = locale
213 .resolved_archive_term(ArchiveHierarchyField::Item)
214 .map(|l| format!("{l} "))
215 .unwrap_or_default();
216 parts.push(format!("{label}{item}"));
217 }
218
219 if parts.is_empty() {
220 None
221 } else {
222 Some(parts.join(", "))
223 }
224}
225
226fn make_rich_text_case_transform(
227 case: TextCase,
228 language: Option<&str>,
229) -> impl FnMut(&str) -> String {
230 let mut seen_alpha = false;
231 let language = crate::values::text_case::language_identifier_for_tag(language);
232 move |text: &str| match case {
233 TextCase::Sentence | TextCase::SentenceApa | TextCase::SentenceNlm => {
234 let lowered = crate::values::text_case::apply_text_case_with_language_id(
235 text,
236 TextCase::Lowercase,
237 &language,
238 );
239 if seen_alpha {
240 lowered
241 } else {
242 let result = crate::values::text_case::apply_text_case_with_language_id(
243 &lowered,
244 TextCase::CapitalizeFirst,
245 &language,
246 );
247 if result.chars().any(char::is_alphabetic) {
248 seen_alpha = true;
249 }
250 result
251 }
252 }
253 _ => crate::values::text_case::apply_text_case_with_language_id(text, case, &language),
254 }
255}
256
257fn resolve_variable_value(
259 variable: &SimpleVariable,
260 reference: &Reference,
261 options: &RenderOptions<'_>,
262) -> Option<String> {
263 match variable {
264 SimpleVariable::Doi => reference.doi(),
265 SimpleVariable::Url => reference.url().map(|u| u.to_string()).or_else(|| {
266 crate::values::type_class::synthesizes_doi_url(&reference.ref_type())
267 .then(|| reference.doi().map(|doi| format!("https://doi.org/{doi}")))
268 .flatten()
269 }),
270 SimpleVariable::Isbn => reference.isbn(),
271 SimpleVariable::Issn => reference.issn(),
272 SimpleVariable::Publisher => reference.publisher_str(),
273 SimpleVariable::PublisherPlace => reference.publisher_place(),
274 SimpleVariable::OriginalPublisher => reference.original_publisher_str(),
275 SimpleVariable::OriginalPublisherPlace => reference.original_publisher_place(),
276 SimpleVariable::EventTitle => event_title(reference),
277 SimpleVariable::EventPlace => event_place(reference),
278 SimpleVariable::Dimensions => dimensions(reference),
279 SimpleVariable::References => references(reference),
280 SimpleVariable::Scale => reference.scale(),
281 SimpleVariable::Genre => reference
287 .genre()
288 .filter(|genre| *genre != reference.ref_type())
289 .map(|k| options.locale.lookup_genre(&k)),
290 SimpleVariable::RawGenre => raw_genre(reference),
291 SimpleVariable::Medium => reference.medium().map(|k| options.locale.lookup_medium(&k)),
292 SimpleVariable::RawMedium => raw_medium(reference),
293 SimpleVariable::Status => reference.status(),
294 SimpleVariable::Abstract | SimpleVariable::Note => None,
295 SimpleVariable::Archive => reference.archive(),
296 SimpleVariable::ArchiveLocation => reference
297 .archive_location()
298 .or_else(|| assemble_archive_hierarchy(reference, options)),
299 SimpleVariable::ArchiveName => resolve_archive_name(reference, options),
300 SimpleVariable::ArchivePlace => reference.archive_place(),
301 SimpleVariable::ArchiveCollection => reference.archive_collection(),
302 SimpleVariable::ArchiveCollectionId => reference.archive_collection_id(),
303 SimpleVariable::ArchiveSeries => reference.archive_series(),
304 SimpleVariable::ArchiveBox => reference.archive_box(),
305 SimpleVariable::ArchiveFolder => reference.archive_folder(),
306 SimpleVariable::ArchiveItem => reference.archive_item(),
307 SimpleVariable::ArchiveUrl => reference.archive_url().map(|url| url.to_string()),
308 SimpleVariable::EprintId => reference.eprint_id(),
309 SimpleVariable::EprintServer => reference.eprint_server(),
310 SimpleVariable::EprintClass => reference.eprint_class(),
311 SimpleVariable::Authority => reference.authority(),
312 SimpleVariable::Code => reference.code(),
313 SimpleVariable::Reporter => reference.reporter(),
314 SimpleVariable::Page => reference.pages().map(|v| v.to_string()),
315 SimpleVariable::Section => reference.section(),
316 SimpleVariable::Volume => reference.volume().map(|v| v.to_string()),
317 SimpleVariable::Number => reference.number(),
318 SimpleVariable::DocketNumber => match reference.extension() {
319 ClassExtension::Brief(r) => r.docket_number.clone(),
320 _ => None,
321 },
322 SimpleVariable::PatentNumber => match reference.extension() {
323 ClassExtension::Patent(r) => Some(r.patent_number.clone()),
324 _ => None,
325 },
326 SimpleVariable::StandardNumber => match reference.extension() {
327 ClassExtension::Standard(r) => Some(r.standard_number.clone()),
328 _ => None,
329 },
330 SimpleVariable::AdsBibcode => reference.ads_bibcode(),
331 SimpleVariable::ReportNumber => reference.report_number(),
332 SimpleVariable::Version => reference.version(),
333 SimpleVariable::VolumeTitle => reference.volume_title(),
334 SimpleVariable::ContainerTitleShort => container_title_short(reference),
335 SimpleVariable::Locator => options.locator_raw.map(|loc| {
336 let derived;
339 let cfg = if let Some(c) = options.config.locators.as_ref() {
340 c
341 } else {
342 derived = if matches!(
343 options.config.processing,
344 Some(citum_schema::options::Processing::Note)
345 ) {
346 citum_schema::options::LocatorPreset::Note.config()
347 } else {
348 citum_schema::options::LocatorConfig::default()
349 };
350 &derived
351 };
352 let ref_type = options.ref_type.as_deref().unwrap_or("");
353 crate::values::locator::render_locator(loc, ref_type, cfg, options.locale)
354 }),
355 _ => None,
356 }
357}
358
359impl ComponentValues for TemplateVariable {
360 fn values<F: crate::render::format::OutputFormat<Output = String>>(
361 &self,
362 reference: &Reference,
363 _hints: &ProcHints,
364 options: &RenderOptions<'_>,
365 ) -> Option<ProcValues<F::Output>> {
366 let language = reference.language();
367 let rich_text: Option<RichText> = match self.variable {
369 SimpleVariable::Note => reference.note(),
370 SimpleVariable::Abstract => reference.abstract_text(),
371 _ => None,
372 };
373
374 if let Some(rt) = rich_text {
375 if rt.is_empty() {
376 return None;
377 }
378 let fmt = F::default();
379 let (value, pre_formatted) = match (rt, self.rendering.text_case) {
380 (RichText::Plain(s), Some(tc)) => (
381 crate::values::text_case::apply_text_case_with_language(
382 &s,
383 tc,
384 language.as_deref(),
385 ),
386 false,
387 ),
388 (RichText::Plain(s), None) => (s, false),
389 (RichText::Djot { djot }, Some(tc)) => (
390 crate::render::rich_text::render_djot_inline_with_transform(
391 &djot,
392 &fmt,
393 make_rich_text_case_transform(tc, language.as_deref()),
394 )
395 .0,
396 true,
397 ),
398 (RichText::Djot { djot }, None) => {
399 (crate::render::render_djot_inline(&djot, &fmt), true)
400 }
401 };
402 return Some(ProcValues {
403 value,
404 prefix: None,
405 suffix: None,
406 url: None,
407 substituted_key: None,
408 pre_formatted,
409 });
410 }
411
412 let value = resolve_variable_value(&self.variable, reference, options);
414
415 value.filter(|s: &String| !s.is_empty()).map(|value| {
416 let value = if let Some(tc) = self.rendering.text_case {
417 crate::values::text_case::apply_text_case_with_language(
418 &value,
419 tc,
420 language.as_deref(),
421 )
422 } else {
423 value
424 };
425 let value = crate::values::apply_abbreviation(value, options.abbreviation_map);
426 use citum_schema::options::{LinkAnchor, LinkTarget};
427 let component_anchor = match self.variable {
428 SimpleVariable::Url => LinkAnchor::Url,
429 SimpleVariable::Doi => LinkAnchor::Doi,
430 _ => LinkAnchor::Component,
431 };
432
433 let mut url = crate::values::resolve_effective_url(
434 self.links.as_ref(),
435 options.config.links.as_ref(),
436 reference,
437 component_anchor,
438 );
439
440 if url.is_none()
442 && let Some(links) = &self.links
443 {
444 if self.variable == SimpleVariable::Url
445 && (links.url == Some(true)
446 || matches!(links.target, Some(LinkTarget::Url | LinkTarget::UrlOrDoi)))
447 {
448 url = reference.url().map(|u| u.to_string());
449 } else if self.variable == SimpleVariable::Doi
450 && (links.doi == Some(true)
451 || matches!(links.target, Some(LinkTarget::Doi | LinkTarget::UrlOrDoi)))
452 {
453 url = reference.doi().map(|d| format!("https://doi.org/{d}"));
454 }
455 }
456
457 ProcValues {
458 value,
459 prefix: None,
460 suffix: None,
461 url,
462 substituted_key: None,
463 pre_formatted: false,
464 }
465 })
466 }
467}