1use super::Processor;
13use super::rendering::{CompoundRenderData, GroupRenderParams, Renderer, RendererResources};
14use crate::error::ProcessorError;
15use crate::reference::Citation;
16use citum_schema::NoteStartTextCase;
17use citum_schema::locale::{GeneralTerm, Locale, TermForm};
18use citum_schema::template::DelimiterPunctuation;
19use indexmap::IndexMap;
20use std::collections::HashMap;
21
22fn join_integral_groups(rendered_groups: Vec<String>, locale: &Locale) -> String {
23 match rendered_groups.len() {
24 0 => String::new(),
25 1 => rendered_groups.into_iter().next().unwrap_or_default(),
26 2 => {
27 let conjunction = locale
28 .resolved_general_term(&GeneralTerm::And, &TermForm::Long, None)
29 .unwrap_or_else(|| locale.and_term(false).to_string());
30 rendered_groups.join(&format!(" {} ", conjunction.trim()))
31 }
32 _ => {
33 let conjunction = locale
34 .resolved_general_term(&GeneralTerm::And, &TermForm::Long, None)
35 .unwrap_or_else(|| locale.and_term(false).to_string());
36 let final_delimiter = if locale.grammar_options.serial_comma {
37 format!(", {} ", conjunction.trim())
38 } else {
39 format!(" {} ", conjunction.trim())
40 };
41
42 let mut rendered_groups = rendered_groups;
43 let last = rendered_groups.pop().unwrap_or_default();
44 format!("{}{}{}", rendered_groups.join(", "), final_delimiter, last)
45 }
46 }
47}
48
49impl Processor {
50 fn sentence_initial_note_start_text_case(
51 &self,
52 citation: &Citation,
53 effective_spec: &citum_schema::CitationSpec,
54 ) -> Option<NoteStartTextCase> {
55 let spec_prefix = effective_spec.prefix.as_deref().unwrap_or("");
56 if self.is_note_style()
57 && matches!(
58 citation.position,
59 Some(
60 citum_schema::citation::Position::Ibid
61 | citum_schema::citation::Position::IbidWithLocator
62 )
63 )
64 && matches!(
65 citation.mode,
66 citum_schema::citation::CitationMode::NonIntegral
67 )
68 && citation.prefix.as_deref().unwrap_or("").is_empty()
69 && spec_prefix.is_empty()
70 {
71 effective_spec.note_start_text_case
72 } else {
73 None
74 }
75 }
76
77 fn resolve_positioned_citation_spec(
78 &self,
79 citation: &Citation,
80 ) -> std::borrow::Cow<'_, citum_schema::CitationSpec> {
81 self.style.citation.as_ref().map_or_else(
82 || std::borrow::Cow::Owned(citum_schema::CitationSpec::default()),
83 |spec| spec.resolve_for_position(citation.position.as_ref()),
84 )
85 }
86
87 fn track_cited_ids_and_init_numbers(&self, citation: &Citation) {
88 self.initialize_numeric_citation_numbers();
89 let mut cited_ids = self.cited_ids.borrow_mut();
90 for item in &citation.items {
91 cited_ids.insert(item.id.clone());
92 }
93 }
94
95 fn resolve_effective_citation_spec(&self, citation: &Citation) -> citum_schema::CitationSpec {
96 self.resolve_positioned_citation_spec(citation)
97 .into_owned()
98 .resolve_for_mode(&citation.mode)
99 .into_owned()
100 }
101
102 fn resolve_citation_delimiters<'a>(
103 &self,
104 effective_spec: &'a citum_schema::CitationSpec,
105 ) -> (&'a str, &'a str) {
106 let intra_delimiter = effective_spec.delimiter.as_deref().unwrap_or(", ");
107 let inter_delimiter = effective_spec
108 .multi_cite_delimiter
109 .as_deref()
110 .unwrap_or("; ");
111
112 (
113 if matches!(
114 DelimiterPunctuation::from_csl_string(intra_delimiter),
115 DelimiterPunctuation::None
116 ) {
117 ""
118 } else {
119 intra_delimiter
120 },
121 if matches!(
122 DelimiterPunctuation::from_csl_string(inter_delimiter),
123 DelimiterPunctuation::None
124 ) {
125 ""
126 } else {
127 inter_delimiter
128 },
129 )
130 }
131
132 fn resolve_dynamic_group(&self, citation: &Citation) {
143 if self.get_bibliography_options().compound_numeric.is_none() {
144 return;
145 }
146
147 if citation.items.len() < 2 {
148 return;
149 }
150
151 #[allow(clippy::indexing_slicing, reason = "citation.items.len() >= 2")]
152 let head_id = &citation.items[0].id;
153 #[allow(clippy::indexing_slicing, reason = "citation.items.len() >= 2")]
154 let tail_ids: Vec<String> = citation.items[1..].iter().map(|i| i.id.clone()).collect();
155
156 if self.compound_set_by_ref.contains_key(head_id) {
158 return;
159 }
160 for tail in &tail_ids {
161 if self.compound_set_by_ref.contains_key(tail.as_str()) {
162 return;
163 }
164 }
165
166 {
171 let dyn_set = self.dynamic_compound_set_by_ref.borrow();
172 let cited = self.cited_ids.borrow();
173
174 if dyn_set.contains_key(head_id.as_str()) || cited.contains(head_id.as_str()) {
175 return;
176 }
177 for tail in &tail_ids {
178 if dyn_set.contains_key(tail.as_str()) || cited.contains(tail.as_str()) {
179 return;
180 }
181 }
182 }
183
184 let head_number = {
185 let numbers = self.citation_numbers.borrow();
186 let Some(&n) = numbers.get(head_id.as_str()) else {
187 return;
188 };
189 n
190 };
191
192 {
194 let mut numbers = self.citation_numbers.borrow_mut();
195 for tail in &tail_ids {
196 numbers.insert(tail.clone(), head_number);
197 }
198 }
199
200 let all_members: Vec<String> = std::iter::once(head_id.clone())
202 .chain(tail_ids.iter().cloned())
203 .collect();
204
205 {
207 let mut dyn_set = self.dynamic_compound_set_by_ref.borrow_mut();
208 let mut dyn_idx = self.dynamic_compound_member_index.borrow_mut();
209 for (idx, member) in all_members.iter().enumerate() {
210 dyn_set.insert(member.clone(), head_id.clone());
211 dyn_idx.insert(member.clone(), idx);
212 }
213 }
214
215 {
217 let mut groups = self.compound_groups.borrow_mut();
218 let members = groups
219 .entry(head_number)
220 .or_insert_with(|| vec![head_id.clone()]);
221 for tail in &tail_ids {
222 if !members.contains(tail) {
223 members.push(tail.clone());
224 }
225 }
226 }
227
228 self.dynamic_compound_sets
230 .borrow_mut()
231 .insert(head_id.clone(), all_members);
232 }
233
234 fn merged_compound_data(
240 &self,
241 ) -> (
242 Option<HashMap<String, String>>,
243 Option<HashMap<String, usize>>,
244 Option<IndexMap<String, Vec<String>>>,
245 ) {
246 if self.dynamic_compound_set_by_ref.borrow().is_empty() {
247 return (None, None, None);
248 }
249 let merged_set: HashMap<String, String> = self
250 .compound_set_by_ref
251 .iter()
252 .chain(self.dynamic_compound_set_by_ref.borrow().iter())
253 .map(|(k, v)| (k.clone(), v.clone()))
254 .collect();
255 let merged_idx: HashMap<String, usize> = self
256 .compound_member_index
257 .iter()
258 .chain(self.dynamic_compound_member_index.borrow().iter())
259 .map(|(k, v)| (k.clone(), *v))
260 .collect();
261 let merged_sets: IndexMap<String, Vec<String>> = self
262 .compound_sets
263 .iter()
264 .chain(self.dynamic_compound_sets.borrow().iter())
265 .map(|(k, v)| (k.clone(), v.clone()))
266 .collect();
267 (Some(merged_set), Some(merged_idx), Some(merged_sets))
268 }
269
270 fn render_citation_content<F>(
271 &self,
272 citation: &Citation,
273 effective_spec: &citum_schema::CitationSpec,
274 renderer_delimiter: &str,
275 renderer_inter_delimiter: &str,
276 note_start_text_case: Option<NoteStartTextCase>,
277 ) -> Result<String, ProcessorError>
278 where
279 F: crate::render::format::OutputFormat<Output = String>,
280 {
281 let sorted_items = if citation.grouped {
284 citation.items.clone()
285 } else {
286 self.sort_citation_items(citation.items.clone(), effective_spec)
287 };
288
289 let (dyn_set_owned, dyn_idx_owned, dyn_sets_owned) = self.merged_compound_data();
292 let effective_set_by_ref = dyn_set_owned.as_ref().unwrap_or(&self.compound_set_by_ref);
293 let effective_member_index = dyn_idx_owned
294 .as_ref()
295 .unwrap_or(&self.compound_member_index);
296 let effective_compound_sets = dyn_sets_owned.as_ref().unwrap_or(&self.compound_sets);
297
298 let citation_config = self.get_citation_config();
299 let renderer = Renderer::new(
300 RendererResources {
301 style: &self.style,
302 bibliography: &self.bibliography,
303 locale: &self.locale,
304 config: &citation_config,
305 bibliography_config: Some(self.get_bibliography_options().into_owned()),
306 },
307 &self.hints,
308 &self.citation_numbers,
309 CompoundRenderData {
310 set_by_ref: effective_set_by_ref,
311 member_index: effective_member_index,
312 sets: effective_compound_sets,
313 },
314 self.show_semantics,
315 self.inject_ast_indices,
316 self.abbreviation_map.as_ref(),
317 );
318 let processing = citation_config.processing.clone().unwrap_or_default();
319 let has_explicit_integral_multi_cite_delimiter = matches!(
320 citation.mode,
321 citum_schema::citation::CitationMode::Integral
322 ) && self
323 .resolve_positioned_citation_spec(citation)
324 .integral
325 .as_ref()
326 .and_then(|spec| spec.multi_cite_delimiter.as_ref())
327 .is_some();
328 let rendered_groups = if matches!(
329 processing,
330 citum_schema::options::Processing::Numeric
331 | citum_schema::options::Processing::Label(_)
332 ) {
333 renderer.render_ungrouped_citation_with_format::<F>(
334 &sorted_items,
335 effective_spec,
336 &citation.mode,
337 renderer_delimiter,
338 citation.suppress_author,
339 citation.position.as_ref(),
340 note_start_text_case,
341 )?
342 } else {
343 renderer.render_grouped_citation_with_format::<F>(
344 &sorted_items,
345 &GroupRenderParams {
346 spec: effective_spec,
347 mode: &citation.mode,
348 intra_delimiter: renderer_delimiter,
349 suppress_author: citation.suppress_author,
350 position: citation.position.as_ref(),
351 note_start_text_case,
352 },
353 )?
354 };
355
356 Ok(
357 if matches!(
358 citation.mode,
359 citum_schema::citation::CitationMode::Integral
360 ) && !has_explicit_integral_multi_cite_delimiter
361 {
362 join_integral_groups(rendered_groups, &self.locale)
363 } else {
364 F::default().join(rendered_groups, renderer_inter_delimiter)
365 },
366 )
367 }
368
369 fn apply_citation_input_affixes<F>(
370 &self,
371 citation: &Citation,
372 content: String,
373 fmt: &F,
374 ) -> String
375 where
376 F: crate::render::format::OutputFormat<Output = String>,
377 {
378 let citation_prefix = citation.prefix.as_deref().unwrap_or("");
379 let citation_suffix = citation.suffix.as_deref().unwrap_or("");
380
381 if citation_prefix.is_empty() && citation_suffix.is_empty() {
382 return content;
383 }
384
385 let formatted_prefix =
386 if !citation_prefix.is_empty() && !citation_prefix.ends_with(char::is_whitespace) {
387 format!("{citation_prefix} ")
388 } else {
389 citation_prefix.to_string()
390 };
391
392 let formatted_suffix =
393 if !citation_suffix.is_empty() && !citation_suffix.starts_with(char::is_whitespace) {
394 format!(" {citation_suffix}")
395 } else {
396 citation_suffix.to_string()
397 };
398
399 fmt.affix(&formatted_prefix, content, &formatted_suffix)
400 }
401
402 fn apply_spec_wrap_and_affixes<F>(
403 &self,
404 citation: &Citation,
405 effective_spec: &citum_schema::CitationSpec,
406 output: String,
407 fmt: &F,
408 ) -> String
409 where
410 F: crate::render::format::OutputFormat<Output = String>,
411 {
412 let spec_prefix = effective_spec.prefix.as_deref().unwrap_or("");
413 let spec_suffix = effective_spec.suffix.as_deref().unwrap_or("");
414
415 if matches!(
416 citation.mode,
417 citum_schema::citation::CitationMode::Integral
418 ) {
419 if !spec_prefix.is_empty() || !spec_suffix.is_empty() {
420 fmt.affix(spec_prefix, output, spec_suffix)
421 } else {
422 output
423 }
424 } else if let Some(wrap) = effective_spec.wrap.as_ref() {
425 let inner_prefix = wrap.inner_prefix.as_deref().unwrap_or("");
426 let inner_suffix = wrap.inner_suffix.as_deref().unwrap_or("");
427 let inner_wrapped = if !inner_prefix.is_empty() || !inner_suffix.is_empty() {
428 fmt.inner_affix(inner_prefix, output, inner_suffix)
429 } else {
430 output
431 };
432 fmt.wrap_punctuation(&wrap.punctuation, inner_wrapped)
433 } else if !spec_prefix.is_empty() || !spec_suffix.is_empty() {
434 fmt.affix(spec_prefix, output, spec_suffix)
435 } else {
436 output
437 }
438 }
439
440 pub fn process_citation(&self, citation: &Citation) -> Result<String, ProcessorError> {
454 self.process_citation_with_format::<crate::render::plain::PlainText>(citation)
455 }
456
457 pub fn process_citation_with_format<F>(
466 &self,
467 citation: &Citation,
468 ) -> Result<String, ProcessorError>
469 where
470 F: crate::render::format::OutputFormat<Output = String>,
471 {
472 let fmt = F::default();
473
474 if citation.grouped {
478 self.initialize_numeric_citation_numbers();
479 self.resolve_dynamic_group(citation);
480 }
481
482 self.track_cited_ids_and_init_numbers(citation);
483
484 let effective_spec = self.resolve_effective_citation_spec(citation);
485 let note_start_text_case =
486 self.sentence_initial_note_start_text_case(citation, &effective_spec);
487 let (renderer_delimiter, renderer_inter_delimiter) =
488 self.resolve_citation_delimiters(&effective_spec);
489 let content = self.render_citation_content::<F>(
490 citation,
491 &effective_spec,
492 renderer_delimiter,
493 renderer_inter_delimiter,
494 note_start_text_case,
495 )?;
496 let output = self.apply_citation_input_affixes(citation, content, &fmt);
497 let wrapped = self.apply_spec_wrap_and_affixes(citation, &effective_spec, output, &fmt);
498
499 Ok(fmt.finish(wrapped))
500 }
501
502 pub fn process_citations(&self, citations: &[Citation]) -> Result<Vec<String>, ProcessorError> {
510 self.process_citations_with_format::<crate::render::plain::PlainText>(citations)
511 }
512
513 pub fn process_citations_with_format<F>(
519 &self,
520 citations: &[Citation],
521 ) -> Result<Vec<String>, ProcessorError>
522 where
523 F: crate::render::format::OutputFormat<Output = String>,
524 {
525 let mut normalized = self.normalize_note_context(citations);
526 self.annotate_positions(&mut normalized);
527 normalized
528 .iter()
529 .map(|citation| self.process_citation_with_format::<F>(citation))
530 .collect()
531 }
532}