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.genre().map(|k| options.locale.lookup_genre(&k)),
146 SimpleVariable::Medium => reference.medium().map(|k| options.locale.lookup_medium(&k)),
147 SimpleVariable::Status => reference.status(),
148 SimpleVariable::Abstract | SimpleVariable::Note => None,
149 SimpleVariable::Archive => reference.archive(),
150 SimpleVariable::ArchiveLocation => reference
151 .archive_location()
152 .or_else(|| assemble_archive_hierarchy(reference, options)),
153 SimpleVariable::ArchiveName => resolve_archive_name(reference, options),
154 SimpleVariable::ArchivePlace => reference.archive_place(),
155 SimpleVariable::ArchiveCollection => reference.archive_collection(),
156 SimpleVariable::ArchiveCollectionId => reference.archive_collection_id(),
157 SimpleVariable::ArchiveSeries => reference.archive_series(),
158 SimpleVariable::ArchiveBox => reference.archive_box(),
159 SimpleVariable::ArchiveFolder => reference.archive_folder(),
160 SimpleVariable::ArchiveItem => reference.archive_item(),
161 SimpleVariable::ArchiveUrl => reference.archive_url().map(|url| url.to_string()),
162 SimpleVariable::EprintId => reference.eprint_id(),
163 SimpleVariable::EprintServer => reference.eprint_server(),
164 SimpleVariable::EprintClass => reference.eprint_class(),
165 SimpleVariable::Authority => reference.authority(),
166 SimpleVariable::Code => reference.code(),
167 SimpleVariable::Reporter => reference.reporter(),
168 SimpleVariable::Page => reference.pages().map(|v| v.to_string()),
169 SimpleVariable::Section => reference.section(),
170 SimpleVariable::Volume => reference.volume().map(|v| v.to_string()),
171 SimpleVariable::Number => reference.number(),
172 SimpleVariable::DocketNumber => match reference.extension() {
173 ClassExtension::Brief(r) => r.docket_number.clone(),
174 _ => None,
175 },
176 SimpleVariable::PatentNumber => match reference.extension() {
177 ClassExtension::Patent(r) => Some(r.patent_number.clone()),
178 _ => None,
179 },
180 SimpleVariable::StandardNumber => match reference.extension() {
181 ClassExtension::Standard(r) => Some(r.standard_number.clone()),
182 _ => None,
183 },
184 SimpleVariable::AdsBibcode => reference.ads_bibcode(),
185 SimpleVariable::ReportNumber => reference.report_number(),
186 SimpleVariable::Version => reference.version(),
187 SimpleVariable::ContainerTitleShort => container_title_short(reference),
188 SimpleVariable::Locator => options.locator_raw.map(|loc| {
189 let derived;
192 let cfg = if let Some(c) = options.config.locators.as_ref() {
193 c
194 } else {
195 derived = if matches!(
196 options.config.processing,
197 Some(citum_schema::options::Processing::Note)
198 ) {
199 citum_schema::options::LocatorPreset::Note.config()
200 } else {
201 citum_schema::options::LocatorConfig::default()
202 };
203 &derived
204 };
205 let ref_type = options.ref_type.as_deref().unwrap_or("");
206 crate::values::locator::render_locator(loc, ref_type, cfg, options.locale)
207 }),
208 _ => None,
209 }
210}
211
212impl ComponentValues for TemplateVariable {
213 fn values<F: crate::render::format::OutputFormat<Output = String>>(
214 &self,
215 reference: &Reference,
216 _hints: &ProcHints,
217 options: &RenderOptions<'_>,
218 ) -> Option<ProcValues<F::Output>> {
219 let rich_text: Option<RichText> = match self.variable {
221 SimpleVariable::Note => reference.note(),
222 SimpleVariable::Abstract => reference.abstract_text(),
223 _ => None,
224 };
225
226 if let Some(rt) = rich_text {
227 if rt.is_empty() {
228 return None;
229 }
230 let fmt = F::default();
231 let (value, pre_formatted) = match (rt, self.rendering.text_case) {
232 (RichText::Plain(s), Some(tc)) => {
233 (crate::values::text_case::apply_text_case(&s, tc), false)
234 }
235 (RichText::Plain(s), None) => (s, false),
236 (RichText::Djot { djot }, Some(tc)) => (
237 crate::render::rich_text::render_djot_inline_with_transform(
238 &djot,
239 &fmt,
240 make_rich_text_case_transform(tc),
241 )
242 .0,
243 true,
244 ),
245 (RichText::Djot { djot }, None) => {
246 (crate::render::render_djot_inline(&djot, &fmt), true)
247 }
248 };
249 return Some(ProcValues {
250 value,
251 prefix: None,
252 suffix: None,
253 url: None,
254 substituted_key: None,
255 pre_formatted,
256 });
257 }
258
259 let value = resolve_variable_value(&self.variable, reference, options);
261
262 value.filter(|s: &String| !s.is_empty()).map(|value| {
263 let value = if let Some(tc) = self.rendering.text_case {
264 crate::values::text_case::apply_text_case(&value, tc)
265 } else {
266 value
267 };
268 let value = crate::values::apply_abbreviation(value, options.abbreviation_map);
269 use citum_schema::options::{LinkAnchor, LinkTarget};
270 let component_anchor = match self.variable {
271 SimpleVariable::Url => LinkAnchor::Url,
272 SimpleVariable::Doi => LinkAnchor::Doi,
273 _ => LinkAnchor::Component,
274 };
275
276 let mut url = crate::values::resolve_effective_url(
277 self.links.as_ref(),
278 options.config.links.as_ref(),
279 reference,
280 component_anchor,
281 );
282
283 if url.is_none()
285 && let Some(links) = &self.links
286 {
287 if self.variable == SimpleVariable::Url
288 && (links.url == Some(true)
289 || matches!(links.target, Some(LinkTarget::Url | LinkTarget::UrlOrDoi)))
290 {
291 url = reference.url().map(|u| u.to_string());
292 } else if self.variable == SimpleVariable::Doi
293 && (links.doi == Some(true)
294 || matches!(links.target, Some(LinkTarget::Doi | LinkTarget::UrlOrDoi)))
295 {
296 url = reference.doi().map(|d| format!("https://doi.org/{d}"));
297 }
298 }
299
300 ProcValues {
301 value,
302 prefix: None,
303 suffix: None,
304 url,
305 substituted_key: None,
306 pre_formatted: false,
307 }
308 })
309 }
310}