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(case: TextCase) -> impl FnMut(&str) -> String {
227 let mut seen_alpha = false;
228 move |text: &str| match case {
229 TextCase::Sentence | TextCase::SentenceApa | TextCase::SentenceNlm => {
230 let lowered = text.to_lowercase();
231 if seen_alpha {
232 lowered
233 } else {
234 let result = crate::values::text_case::capitalize_first_word(&lowered);
235 if result.chars().any(char::is_alphabetic) {
236 seen_alpha = true;
237 }
238 result
239 }
240 }
241 _ => crate::values::text_case::apply_text_case(text, case),
242 }
243}
244
245fn resolve_variable_value(
247 variable: &SimpleVariable,
248 reference: &Reference,
249 options: &RenderOptions<'_>,
250) -> Option<String> {
251 match variable {
252 SimpleVariable::Doi => reference.doi(),
253 SimpleVariable::Url => reference.url().map(|u| u.to_string()).or_else(|| {
254 crate::values::type_class::synthesizes_doi_url(&reference.ref_type())
255 .then(|| reference.doi().map(|doi| format!("https://doi.org/{doi}")))
256 .flatten()
257 }),
258 SimpleVariable::Isbn => reference.isbn(),
259 SimpleVariable::Issn => reference.issn(),
260 SimpleVariable::Publisher => reference.publisher_str(),
261 SimpleVariable::PublisherPlace => reference.publisher_place(),
262 SimpleVariable::OriginalPublisher => reference.original_publisher_str(),
263 SimpleVariable::OriginalPublisherPlace => reference.original_publisher_place(),
264 SimpleVariable::EventTitle => event_title(reference),
265 SimpleVariable::EventPlace => event_place(reference),
266 SimpleVariable::Dimensions => dimensions(reference),
267 SimpleVariable::References => references(reference),
268 SimpleVariable::Scale => reference.scale(),
269 SimpleVariable::Genre => reference
275 .genre()
276 .filter(|genre| *genre != reference.ref_type())
277 .map(|k| options.locale.lookup_genre(&k)),
278 SimpleVariable::RawGenre => raw_genre(reference),
279 SimpleVariable::Medium => reference.medium().map(|k| options.locale.lookup_medium(&k)),
280 SimpleVariable::RawMedium => raw_medium(reference),
281 SimpleVariable::Status => reference.status(),
282 SimpleVariable::Abstract | SimpleVariable::Note => None,
283 SimpleVariable::Archive => reference.archive(),
284 SimpleVariable::ArchiveLocation => reference
285 .archive_location()
286 .or_else(|| assemble_archive_hierarchy(reference, options)),
287 SimpleVariable::ArchiveName => resolve_archive_name(reference, options),
288 SimpleVariable::ArchivePlace => reference.archive_place(),
289 SimpleVariable::ArchiveCollection => reference.archive_collection(),
290 SimpleVariable::ArchiveCollectionId => reference.archive_collection_id(),
291 SimpleVariable::ArchiveSeries => reference.archive_series(),
292 SimpleVariable::ArchiveBox => reference.archive_box(),
293 SimpleVariable::ArchiveFolder => reference.archive_folder(),
294 SimpleVariable::ArchiveItem => reference.archive_item(),
295 SimpleVariable::ArchiveUrl => reference.archive_url().map(|url| url.to_string()),
296 SimpleVariable::EprintId => reference.eprint_id(),
297 SimpleVariable::EprintServer => reference.eprint_server(),
298 SimpleVariable::EprintClass => reference.eprint_class(),
299 SimpleVariable::Authority => reference.authority(),
300 SimpleVariable::Code => reference.code(),
301 SimpleVariable::Reporter => reference.reporter(),
302 SimpleVariable::Page => reference.pages().map(|v| v.to_string()),
303 SimpleVariable::Section => reference.section(),
304 SimpleVariable::Volume => reference.volume().map(|v| v.to_string()),
305 SimpleVariable::Number => reference.number(),
306 SimpleVariable::DocketNumber => match reference.extension() {
307 ClassExtension::Brief(r) => r.docket_number.clone(),
308 _ => None,
309 },
310 SimpleVariable::PatentNumber => match reference.extension() {
311 ClassExtension::Patent(r) => Some(r.patent_number.clone()),
312 _ => None,
313 },
314 SimpleVariable::StandardNumber => match reference.extension() {
315 ClassExtension::Standard(r) => Some(r.standard_number.clone()),
316 _ => None,
317 },
318 SimpleVariable::AdsBibcode => reference.ads_bibcode(),
319 SimpleVariable::ReportNumber => reference.report_number(),
320 SimpleVariable::Version => reference.version(),
321 SimpleVariable::VolumeTitle => reference.volume_title(),
322 SimpleVariable::ContainerTitleShort => container_title_short(reference),
323 SimpleVariable::Locator => options.locator_raw.map(|loc| {
324 let derived;
327 let cfg = if let Some(c) = options.config.locators.as_ref() {
328 c
329 } else {
330 derived = if matches!(
331 options.config.processing,
332 Some(citum_schema::options::Processing::Note)
333 ) {
334 citum_schema::options::LocatorPreset::Note.config()
335 } else {
336 citum_schema::options::LocatorConfig::default()
337 };
338 &derived
339 };
340 let ref_type = options.ref_type.as_deref().unwrap_or("");
341 crate::values::locator::render_locator(loc, ref_type, cfg, options.locale)
342 }),
343 _ => None,
344 }
345}
346
347impl ComponentValues for TemplateVariable {
348 fn values<F: crate::render::format::OutputFormat<Output = String>>(
349 &self,
350 reference: &Reference,
351 _hints: &ProcHints,
352 options: &RenderOptions<'_>,
353 ) -> Option<ProcValues<F::Output>> {
354 let rich_text: Option<RichText> = match self.variable {
356 SimpleVariable::Note => reference.note(),
357 SimpleVariable::Abstract => reference.abstract_text(),
358 _ => None,
359 };
360
361 if let Some(rt) = rich_text {
362 if rt.is_empty() {
363 return None;
364 }
365 let fmt = F::default();
366 let (value, pre_formatted) = match (rt, self.rendering.text_case) {
367 (RichText::Plain(s), Some(tc)) => {
368 (crate::values::text_case::apply_text_case(&s, tc), false)
369 }
370 (RichText::Plain(s), None) => (s, false),
371 (RichText::Djot { djot }, Some(tc)) => (
372 crate::render::rich_text::render_djot_inline_with_transform(
373 &djot,
374 &fmt,
375 make_rich_text_case_transform(tc),
376 )
377 .0,
378 true,
379 ),
380 (RichText::Djot { djot }, None) => {
381 (crate::render::render_djot_inline(&djot, &fmt), true)
382 }
383 };
384 return Some(ProcValues {
385 value,
386 prefix: None,
387 suffix: None,
388 url: None,
389 substituted_key: None,
390 pre_formatted,
391 });
392 }
393
394 let value = resolve_variable_value(&self.variable, reference, options);
396
397 value.filter(|s: &String| !s.is_empty()).map(|value| {
398 let value = if let Some(tc) = self.rendering.text_case {
399 crate::values::text_case::apply_text_case(&value, tc)
400 } else {
401 value
402 };
403 let value = crate::values::apply_abbreviation(value, options.abbreviation_map);
404 use citum_schema::options::{LinkAnchor, LinkTarget};
405 let component_anchor = match self.variable {
406 SimpleVariable::Url => LinkAnchor::Url,
407 SimpleVariable::Doi => LinkAnchor::Doi,
408 _ => LinkAnchor::Component,
409 };
410
411 let mut url = crate::values::resolve_effective_url(
412 self.links.as_ref(),
413 options.config.links.as_ref(),
414 reference,
415 component_anchor,
416 );
417
418 if url.is_none()
420 && let Some(links) = &self.links
421 {
422 if self.variable == SimpleVariable::Url
423 && (links.url == Some(true)
424 || matches!(links.target, Some(LinkTarget::Url | LinkTarget::UrlOrDoi)))
425 {
426 url = reference.url().map(|u| u.to_string());
427 } else if self.variable == SimpleVariable::Doi
428 && (links.doi == Some(true)
429 || matches!(links.target, Some(LinkTarget::Doi | LinkTarget::UrlOrDoi)))
430 {
431 url = reference.doi().map(|d| format!("https://doi.org/{d}"));
432 }
433 }
434
435 ProcValues {
436 value,
437 prefix: None,
438 suffix: None,
439 url,
440 substituted_key: None,
441 pre_formatted: false,
442 }
443 })
444 }
445}