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};
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 resolve_archive_name(reference: &Reference, options: &RenderOptions<'_>) -> Option<String> {
32 let archive_name = reference.archive_name()?;
33 let multilingual = options.config.multilingual.as_ref();
34
35 Some(crate::values::resolve_multilingual_string(
36 &archive_name,
37 multilingual.and_then(|ml| ml.name_mode.as_ref()),
38 multilingual.and_then(|ml| ml.preferred_transliteration.as_deref()),
39 multilingual.and_then(|ml| ml.preferred_script.as_ref()),
40 options.locale.locale.as_str(),
41 ))
42}
43
44fn assemble_archive_hierarchy(
45 reference: &Reference,
46 options: &RenderOptions<'_>,
47) -> Option<String> {
48 let locale = options.locale;
49 let mut parts: Vec<String> = Vec::new();
50
51 if let Some(collection) = reference.archive_collection() {
53 let label = locale
54 .resolved_archive_term(ArchiveHierarchyField::Collection)
55 .map(|l| format!("{l} "))
56 .unwrap_or_default();
57 if let Some(cid) = reference.archive_collection_id() {
58 parts.push(format!("{label}{collection} ({cid})"));
59 } else {
60 parts.push(format!("{label}{collection}"));
61 }
62 }
63
64 if let Some(series) = reference.archive_series() {
66 let label = locale
67 .resolved_archive_term(ArchiveHierarchyField::Series)
68 .map(|l| format!("{l} "))
69 .unwrap_or_default();
70 parts.push(format!("{label}{series}"));
71 }
72
73 if let Some(b) = reference.archive_box() {
75 let label = locale
76 .resolved_archive_term(ArchiveHierarchyField::Box)
77 .map(|l| format!("{l} "))
78 .unwrap_or_default();
79 parts.push(format!("{label}{b}"));
80 }
81
82 if let Some(folder) = reference.archive_folder() {
84 let label = locale
85 .resolved_archive_term(ArchiveHierarchyField::Folder)
86 .map(|l| format!("{l} "))
87 .unwrap_or_default();
88 parts.push(format!("{label}{folder}"));
89 }
90
91 if let Some(item) = reference.archive_item() {
93 let label = locale
94 .resolved_archive_term(ArchiveHierarchyField::Item)
95 .map(|l| format!("{l} "))
96 .unwrap_or_default();
97 parts.push(format!("{label}{item}"));
98 }
99
100 if parts.is_empty() {
101 None
102 } else {
103 Some(parts.join(", "))
104 }
105}
106
107fn make_rich_text_case_transform(case: TextCase) -> impl FnMut(&str) -> String {
108 let mut seen_alpha = false;
109 move |text: &str| match case {
110 TextCase::Sentence | TextCase::SentenceApa | TextCase::SentenceNlm => {
111 let lowered = text.to_lowercase();
112 if seen_alpha {
113 lowered
114 } else {
115 let result = crate::values::text_case::capitalize_first_word(&lowered);
116 if result.chars().any(char::is_alphabetic) {
117 seen_alpha = true;
118 }
119 result
120 }
121 }
122 _ => crate::values::text_case::apply_text_case(text, case),
123 }
124}
125
126fn resolve_variable_value(
128 variable: &SimpleVariable,
129 reference: &Reference,
130 options: &RenderOptions<'_>,
131) -> Option<String> {
132 match variable {
133 SimpleVariable::Doi => reference.doi(),
134 SimpleVariable::Url => reference.url().map(|u| u.to_string()).or_else(|| {
135 (reference.ref_type() == "dataset")
136 .then(|| reference.doi().map(|doi| format!("https://doi.org/{doi}")))
137 .flatten()
138 }),
139 SimpleVariable::Isbn => reference.isbn(),
140 SimpleVariable::Issn => reference.issn(),
141 SimpleVariable::Publisher => reference.publisher_str(),
142 SimpleVariable::PublisherPlace => reference.publisher_place(),
143 SimpleVariable::OriginalPublisher => reference.original_publisher_str(),
144 SimpleVariable::OriginalPublisherPlace => reference.original_publisher_place(),
145 SimpleVariable::Genre => reference
151 .genre()
152 .filter(|genre| *genre != reference.ref_type())
153 .map(|k| options.locale.lookup_genre(&k)),
154 SimpleVariable::Medium => reference.medium().map(|k| options.locale.lookup_medium(&k)),
155 SimpleVariable::Status => reference.status(),
156 SimpleVariable::Abstract | SimpleVariable::Note => None,
157 SimpleVariable::Archive => reference.archive(),
158 SimpleVariable::ArchiveLocation => reference
159 .archive_location()
160 .or_else(|| assemble_archive_hierarchy(reference, options)),
161 SimpleVariable::ArchiveName => resolve_archive_name(reference, options),
162 SimpleVariable::ArchivePlace => reference.archive_place(),
163 SimpleVariable::ArchiveCollection => reference.archive_collection(),
164 SimpleVariable::ArchiveCollectionId => reference.archive_collection_id(),
165 SimpleVariable::ArchiveSeries => reference.archive_series(),
166 SimpleVariable::ArchiveBox => reference.archive_box(),
167 SimpleVariable::ArchiveFolder => reference.archive_folder(),
168 SimpleVariable::ArchiveItem => reference.archive_item(),
169 SimpleVariable::ArchiveUrl => reference.archive_url().map(|url| url.to_string()),
170 SimpleVariable::EprintId => reference.eprint_id(),
171 SimpleVariable::EprintServer => reference.eprint_server(),
172 SimpleVariable::EprintClass => reference.eprint_class(),
173 SimpleVariable::Authority => reference.authority(),
174 SimpleVariable::Code => reference.code(),
175 SimpleVariable::Reporter => reference.reporter(),
176 SimpleVariable::Page => reference.pages().map(|v| v.to_string()),
177 SimpleVariable::Section => reference.section(),
178 SimpleVariable::Volume => reference.volume().map(|v| v.to_string()),
179 SimpleVariable::Number => reference.number(),
180 SimpleVariable::DocketNumber => match reference.extension() {
181 ClassExtension::Brief(r) => r.docket_number.clone(),
182 _ => None,
183 },
184 SimpleVariable::PatentNumber => match reference.extension() {
185 ClassExtension::Patent(r) => Some(r.patent_number.clone()),
186 _ => None,
187 },
188 SimpleVariable::StandardNumber => match reference.extension() {
189 ClassExtension::Standard(r) => Some(r.standard_number.clone()),
190 _ => None,
191 },
192 SimpleVariable::AdsBibcode => reference.ads_bibcode(),
193 SimpleVariable::ReportNumber => reference.report_number(),
194 SimpleVariable::Version => reference.version(),
195 SimpleVariable::ContainerTitleShort => container_title_short(reference),
196 SimpleVariable::Locator => options.locator_raw.map(|loc| {
197 let derived;
200 let cfg = if let Some(c) = options.config.locators.as_ref() {
201 c
202 } else {
203 derived = if matches!(
204 options.config.processing,
205 Some(citum_schema::options::Processing::Note)
206 ) {
207 citum_schema::options::LocatorPreset::Note.config()
208 } else {
209 citum_schema::options::LocatorConfig::default()
210 };
211 &derived
212 };
213 let ref_type = options.ref_type.as_deref().unwrap_or("");
214 crate::values::locator::render_locator(loc, ref_type, cfg, options.locale)
215 }),
216 _ => None,
217 }
218}
219
220impl ComponentValues for TemplateVariable {
221 fn values<F: crate::render::format::OutputFormat<Output = String>>(
222 &self,
223 reference: &Reference,
224 _hints: &ProcHints,
225 options: &RenderOptions<'_>,
226 ) -> Option<ProcValues<F::Output>> {
227 let rich_text: Option<RichText> = match self.variable {
229 SimpleVariable::Note => reference.note(),
230 SimpleVariable::Abstract => reference.abstract_text(),
231 _ => None,
232 };
233
234 if let Some(rt) = rich_text {
235 if rt.is_empty() {
236 return None;
237 }
238 let fmt = F::default();
239 let (value, pre_formatted) = match (rt, self.rendering.text_case) {
240 (RichText::Plain(s), Some(tc)) => {
241 (crate::values::text_case::apply_text_case(&s, tc), false)
242 }
243 (RichText::Plain(s), None) => (s, false),
244 (RichText::Djot { djot }, Some(tc)) => (
245 crate::render::rich_text::render_djot_inline_with_transform(
246 &djot,
247 &fmt,
248 make_rich_text_case_transform(tc),
249 )
250 .0,
251 true,
252 ),
253 (RichText::Djot { djot }, None) => {
254 (crate::render::render_djot_inline(&djot, &fmt), true)
255 }
256 };
257 return Some(ProcValues {
258 value,
259 prefix: None,
260 suffix: None,
261 url: None,
262 substituted_key: None,
263 pre_formatted,
264 });
265 }
266
267 let value = resolve_variable_value(&self.variable, reference, options);
269
270 value.filter(|s: &String| !s.is_empty()).map(|value| {
271 let value = if let Some(tc) = self.rendering.text_case {
272 crate::values::text_case::apply_text_case(&value, tc)
273 } else {
274 value
275 };
276 let value = crate::values::apply_abbreviation(value, options.abbreviation_map);
277 use citum_schema::options::{LinkAnchor, LinkTarget};
278 let component_anchor = match self.variable {
279 SimpleVariable::Url => LinkAnchor::Url,
280 SimpleVariable::Doi => LinkAnchor::Doi,
281 _ => LinkAnchor::Component,
282 };
283
284 let mut url = crate::values::resolve_effective_url(
285 self.links.as_ref(),
286 options.config.links.as_ref(),
287 reference,
288 component_anchor,
289 );
290
291 if url.is_none()
293 && let Some(links) = &self.links
294 {
295 if self.variable == SimpleVariable::Url
296 && (links.url == Some(true)
297 || matches!(links.target, Some(LinkTarget::Url | LinkTarget::UrlOrDoi)))
298 {
299 url = reference.url().map(|u| u.to_string());
300 } else if self.variable == SimpleVariable::Doi
301 && (links.doi == Some(true)
302 || matches!(links.target, Some(LinkTarget::Doi | LinkTarget::UrlOrDoi)))
303 {
304 url = reference.doi().map(|d| format!("https://doi.org/{d}"));
305 }
306 }
307
308 ProcValues {
309 value,
310 prefix: None,
311 suffix: None,
312 url,
313 substituted_key: None,
314 pre_formatted: false,
315 }
316 })
317 }
318}