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