1use super::Processor;
13use super::disambiguation::Disambiguator;
14use super::rendering::{CompoundRenderData, GroupRenderParams, Renderer, RendererResources};
15use crate::error::ProcessorError;
16use crate::reference::Citation;
17use crate::values::ProcHints;
18use citum_schema::NoteStartTextCase;
19use citum_schema::locale::{GeneralTerm, Locale, TermForm};
20use citum_schema::options::{Config, GivennameRule};
21use citum_schema::template::DelimiterPunctuation;
22use indexmap::IndexMap;
23use std::collections::HashMap;
24
25fn join_integral_groups(rendered_groups: Vec<String>, locale: &Locale) -> String {
30 match rendered_groups.len() {
31 0 => String::new(),
32 1 => rendered_groups.into_iter().next().unwrap_or_default(),
33 2 => {
34 let conjunction = locale
35 .resolved_general_term(&GeneralTerm::And, &TermForm::Long, None)
36 .unwrap_or_else(|| locale.and_term(false).to_string());
37 rendered_groups.join(&format!(" {} ", conjunction.trim()))
38 }
39 _ => {
40 let conjunction = locale
41 .resolved_general_term(&GeneralTerm::And, &TermForm::Long, None)
42 .unwrap_or_else(|| locale.and_term(false).to_string());
43 let final_delimiter = if locale.grammar_options.serial_comma {
44 format!(", {} ", conjunction.trim())
45 } else {
46 format!(" {} ", conjunction.trim())
47 };
48
49 let mut rendered_groups = rendered_groups;
50 let last = rendered_groups.pop().unwrap_or_default();
51 format!("{}{}{}", rendered_groups.join(", "), final_delimiter, last)
52 }
53 }
54}
55
56impl Processor {
57 fn sentence_initial_note_start_text_case(
62 &self,
63 citation: &Citation,
64 effective_spec: &citum_schema::CitationSpec,
65 ) -> Option<NoteStartTextCase> {
66 let spec_prefix = effective_spec.prefix.as_deref().unwrap_or("");
67 if self.is_note_style()
68 && matches!(
69 citation.position,
70 Some(
71 citum_schema::citation::Position::Ibid
72 | citum_schema::citation::Position::IbidWithLocator
73 )
74 )
75 && matches!(
76 citation.mode,
77 citum_schema::citation::CitationMode::NonIntegral
78 )
79 && citation.prefix.as_deref().unwrap_or("").is_empty()
80 && spec_prefix.is_empty()
81 {
82 effective_spec.note_start_text_case
83 } else {
84 None
85 }
86 }
87
88 fn resolve_positioned_citation_spec(
93 &self,
94 citation: &Citation,
95 ) -> std::borrow::Cow<'_, citum_schema::CitationSpec> {
96 self.style.citation.as_ref().map_or_else(
97 || std::borrow::Cow::Owned(citum_schema::CitationSpec::default()),
98 |spec| spec.resolve_for_position(citation.position.as_ref()),
99 )
100 }
101
102 pub fn register_nocite_ids(&self, ids: impl IntoIterator<Item = String>) {
113 let mut cited_ids = self.cited_ids.borrow_mut();
114 for id in ids {
115 cited_ids.insert(id);
116 }
117 }
118
119 fn track_cited_ids_and_init_numbers(&self, citation: &Citation) {
124 self.initialize_numeric_citation_numbers();
125 let mut cited_ids = self.cited_ids.borrow_mut();
126 for item in &citation.items {
127 cited_ids.insert(item.id.clone());
128 }
129 }
130
131 fn resolve_effective_citation_spec(&self, citation: &Citation) -> citum_schema::CitationSpec {
133 self.resolve_positioned_citation_spec(citation)
134 .into_owned()
135 .resolve_for_mode(&citation.mode)
136 .into_owned()
137 }
138
139 fn resolve_citation_delimiters<'a>(
141 &self,
142 effective_spec: &'a citum_schema::CitationSpec,
143 ) -> (&'a str, &'a str) {
144 let intra_delimiter = effective_spec.delimiter.as_deref().unwrap_or(", ");
145 let inter_delimiter = effective_spec
146 .multi_cite_delimiter
147 .as_deref()
148 .unwrap_or("; ");
149
150 (
151 if matches!(
152 DelimiterPunctuation::from_csl_string(intra_delimiter),
153 DelimiterPunctuation::None
154 ) {
155 ""
156 } else {
157 intra_delimiter
158 },
159 if matches!(
160 DelimiterPunctuation::from_csl_string(inter_delimiter),
161 DelimiterPunctuation::None
162 ) {
163 ""
164 } else {
165 inter_delimiter
166 },
167 )
168 }
169
170 fn resolve_dynamic_group(&self, citation: &Citation) {
181 if self.get_bibliography_options().compound_numeric.is_none() {
182 return;
183 }
184
185 if citation.items.len() < 2 {
186 return;
187 }
188
189 #[allow(clippy::indexing_slicing, reason = "citation.items.len() >= 2")]
190 let head_id = &citation.items[0].id;
191 #[allow(clippy::indexing_slicing, reason = "citation.items.len() >= 2")]
192 let tail_ids: Vec<String> = citation.items[1..].iter().map(|i| i.id.clone()).collect();
193
194 if self.compound_set_by_ref.contains_key(head_id) {
196 return;
197 }
198 for tail in &tail_ids {
199 if self.compound_set_by_ref.contains_key(tail.as_str()) {
200 return;
201 }
202 }
203
204 {
209 let dyn_set = self.dynamic_compound_set_by_ref.borrow();
210 let cited = self.cited_ids.borrow();
211
212 if dyn_set.contains_key(head_id.as_str()) || cited.contains(head_id.as_str()) {
213 return;
214 }
215 for tail in &tail_ids {
216 if dyn_set.contains_key(tail.as_str()) || cited.contains(tail.as_str()) {
217 return;
218 }
219 }
220 }
221
222 let head_number = {
223 let numbers = self.citation_numbers.borrow();
224 let Some(&n) = numbers.get(head_id.as_str()) else {
225 return;
226 };
227 n
228 };
229
230 {
232 let mut numbers = self.citation_numbers.borrow_mut();
233 for tail in &tail_ids {
234 numbers.insert(tail.clone(), head_number);
235 }
236 }
237
238 let all_members: Vec<String> = std::iter::once(head_id.clone())
240 .chain(tail_ids.iter().cloned())
241 .collect();
242
243 {
245 let mut dyn_set = self.dynamic_compound_set_by_ref.borrow_mut();
246 let mut dyn_idx = self.dynamic_compound_member_index.borrow_mut();
247 for (idx, member) in all_members.iter().enumerate() {
248 dyn_set.insert(member.clone(), head_id.clone());
249 dyn_idx.insert(member.clone(), idx);
250 }
251 }
252
253 {
255 let mut groups = self.compound_groups.borrow_mut();
256 let members = groups
257 .entry(head_number)
258 .or_insert_with(|| vec![head_id.clone()]);
259 for tail in &tail_ids {
260 if !members.contains(tail) {
261 members.push(tail.clone());
262 }
263 }
264 }
265
266 self.dynamic_compound_sets
268 .borrow_mut()
269 .insert(head_id.clone(), all_members);
270 }
271
272 fn citation_scoped_by_cite_hints(
278 &self,
279 items: &[crate::reference::CitationItem],
280 config: &Config,
281 ) -> Option<HashMap<String, ProcHints>> {
282 if !Self::uses_by_cite_givenname(config) {
283 return None;
284 }
285
286 let mut scoped_hints = HashMap::new();
287 let mut scoped_bibliography = IndexMap::new();
288
289 for item in items {
290 let mut hint = self.hints.get(&item.id).cloned().unwrap_or_default();
291 hint.expand_given_names = false;
292 hint.expand_given_names_primary_only = false;
293 hint.min_names_to_show = None;
294 scoped_hints.insert(item.id.clone(), hint);
295
296 if let Some(reference) = self.bibliography.get(&item.id) {
297 scoped_bibliography.insert(item.id.clone(), reference.clone());
298 }
299 }
300
301 if scoped_bibliography.len() < 2 {
302 return Some(scoped_hints);
303 }
304
305 let local_hints =
306 Disambiguator::new(&scoped_bibliography, config, &self.locale).calculate_hints();
307
308 for item in items {
309 let Some(local) = local_hints.get(&item.id) else {
310 continue;
311 };
312 let target = scoped_hints.entry(item.id.clone()).or_default();
313 target.expand_given_names = local.expand_given_names;
314 target.expand_given_names_primary_only = local.expand_given_names_primary_only;
315 target.min_names_to_show = local.min_names_to_show;
316 }
317
318 Some(scoped_hints)
319 }
320
321 fn uses_by_cite_givenname(config: &Config) -> bool {
323 let disambiguate = match config.processing.as_ref() {
324 Some(processing) => processing.config().disambiguate,
325 None => {
326 citum_schema::options::Processing::AuthorDate
327 .config()
328 .disambiguate
329 }
330 };
331
332 disambiguate
333 .as_ref()
334 .is_some_and(|d| d.add_givenname && matches!(d.givenname_rule, GivennameRule::ByCite))
335 }
336
337 fn merged_compound_data(
343 &self,
344 ) -> (
345 Option<HashMap<String, String>>,
346 Option<HashMap<String, usize>>,
347 Option<IndexMap<String, Vec<String>>>,
348 ) {
349 if self.dynamic_compound_set_by_ref.borrow().is_empty() {
350 return (None, None, None);
351 }
352 let merged_set: HashMap<String, String> = self
353 .compound_set_by_ref
354 .iter()
355 .chain(self.dynamic_compound_set_by_ref.borrow().iter())
356 .map(|(k, v)| (k.clone(), v.clone()))
357 .collect();
358 let merged_idx: HashMap<String, usize> = self
359 .compound_member_index
360 .iter()
361 .chain(self.dynamic_compound_member_index.borrow().iter())
362 .map(|(k, v)| (k.clone(), *v))
363 .collect();
364 let merged_sets: IndexMap<String, Vec<String>> = self
365 .compound_sets
366 .iter()
367 .chain(self.dynamic_compound_sets.borrow().iter())
368 .map(|(k, v)| (k.clone(), v.clone()))
369 .collect();
370 (Some(merged_set), Some(merged_idx), Some(merged_sets))
371 }
372
373 fn render_citation_content<F>(
378 &self,
379 citation: &Citation,
380 effective_spec: &citum_schema::CitationSpec,
381 renderer_delimiter: &str,
382 renderer_inter_delimiter: &str,
383 note_start_text_case: Option<NoteStartTextCase>,
384 ) -> Result<String, ProcessorError>
385 where
386 F: crate::render::format::OutputFormat<Output = String>,
387 {
388 let sorted_items = if citation.grouped {
391 citation.items.clone()
392 } else {
393 self.sort_citation_items(citation.items.clone(), effective_spec)
394 };
395
396 let (dyn_set_owned, dyn_idx_owned, dyn_sets_owned) = self.merged_compound_data();
399 let effective_set_by_ref = dyn_set_owned.as_ref().unwrap_or(&self.compound_set_by_ref);
400 let effective_member_index = dyn_idx_owned
401 .as_ref()
402 .unwrap_or(&self.compound_member_index);
403 let effective_compound_sets = dyn_sets_owned.as_ref().unwrap_or(&self.compound_sets);
404
405 let citation_config = self.get_citation_config();
406 let citation_config = match effective_spec.options.as_ref() {
407 Some(mode_options) => {
408 let mut config = citation_config.into_owned();
409 config.merge(&mode_options.to_config());
410 std::borrow::Cow::Owned(config)
411 }
412 None => citation_config,
413 };
414 let scoped_hints = self.citation_scoped_by_cite_hints(&sorted_items, &citation_config);
415 let renderer_hints = scoped_hints.as_ref().unwrap_or(&self.hints);
416 let renderer = Renderer::new(
417 RendererResources {
418 style: &self.style,
419 bibliography: &self.bibliography,
420 locale: &self.locale,
421 config: &citation_config,
422 bibliography_config: Some(self.get_bibliography_options().into_owned()),
423 first_note_by_id: Some(&self.first_note_by_id),
424 },
425 renderer_hints,
426 &self.citation_numbers,
427 CompoundRenderData {
428 set_by_ref: effective_set_by_ref,
429 member_index: effective_member_index,
430 sets: effective_compound_sets,
431 },
432 self.show_semantics,
433 self.inject_ast_indices,
434 self.abbreviation_map.as_ref(),
435 );
436 let processing = citation_config.processing.clone().unwrap_or_default();
437 let has_explicit_integral_multi_cite_delimiter = matches!(
438 citation.mode,
439 citum_schema::citation::CitationMode::Integral
440 ) && self
441 .resolve_positioned_citation_spec(citation)
442 .integral
443 .as_ref()
444 .and_then(|spec| spec.multi_cite_delimiter.as_ref())
445 .is_some();
446 let rendered_groups = if matches!(
447 processing,
448 citum_schema::options::Processing::Numeric
449 | citum_schema::options::Processing::Label(_)
450 ) {
451 renderer.render_ungrouped_citation_with_format::<F>(
452 &sorted_items,
453 effective_spec,
454 &citation.mode,
455 renderer_delimiter,
456 citation.suppress_author,
457 citation.position.as_ref(),
458 note_start_text_case,
459 )?
460 } else {
461 renderer.render_grouped_citation_with_format::<F>(
462 &sorted_items,
463 &GroupRenderParams {
464 spec: effective_spec,
465 mode: &citation.mode,
466 intra_delimiter: renderer_delimiter,
467 suppress_author: citation.suppress_author,
468 position: citation.position.as_ref(),
469 note_start_text_case,
470 },
471 )?
472 };
473
474 Ok(
475 if matches!(
476 citation.mode,
477 citum_schema::citation::CitationMode::Integral
478 ) && !has_explicit_integral_multi_cite_delimiter
479 {
480 join_integral_groups(rendered_groups, &self.locale)
481 } else {
482 F::default().join(rendered_groups, renderer_inter_delimiter)
483 },
484 )
485 }
486
487 fn apply_citation_input_affixes<F>(
492 &self,
493 citation: &Citation,
494 content: String,
495 fmt: &F,
496 ) -> String
497 where
498 F: crate::render::format::OutputFormat<Output = String>,
499 {
500 let citation_prefix = citation.prefix.as_deref().unwrap_or("");
501 let citation_suffix = citation.suffix.as_deref().unwrap_or("");
502
503 if citation_prefix.is_empty() && citation_suffix.is_empty() {
504 return content;
505 }
506
507 let formatted_prefix =
508 if !citation_prefix.is_empty() && !citation_prefix.ends_with(char::is_whitespace) {
509 format!("{citation_prefix} ")
510 } else {
511 citation_prefix.to_string()
512 };
513
514 let formatted_suffix =
515 if !citation_suffix.is_empty() && !citation_suffix.starts_with(char::is_whitespace) {
516 format!(" {citation_suffix}")
517 } else {
518 citation_suffix.to_string()
519 };
520
521 fmt.affix(&formatted_prefix, content, &formatted_suffix)
522 }
523
524 fn apply_spec_wrap_and_affixes<F>(
529 &self,
530 citation: &Citation,
531 effective_spec: &citum_schema::CitationSpec,
532 output: String,
533 fmt: &F,
534 ) -> String
535 where
536 F: crate::render::format::OutputFormat<Output = String>,
537 {
538 let spec_prefix = effective_spec.prefix.as_deref().unwrap_or("");
539 let spec_suffix = effective_spec.suffix.as_deref().unwrap_or("");
540
541 if matches!(
542 citation.mode,
543 citum_schema::citation::CitationMode::Integral
544 ) {
545 if !spec_prefix.is_empty() || !spec_suffix.is_empty() {
546 fmt.affix(spec_prefix, output, spec_suffix)
547 } else {
548 output
549 }
550 } else if let Some(wrap) = effective_spec.wrap.as_ref() {
551 let inner_prefix = wrap.inner_prefix.as_deref().unwrap_or("");
552 let inner_suffix = wrap.inner_suffix.as_deref().unwrap_or("");
553 let inner_wrapped = if !inner_prefix.is_empty() || !inner_suffix.is_empty() {
554 fmt.inner_affix(inner_prefix, output, inner_suffix)
555 } else {
556 output
557 };
558 fmt.wrap_punctuation(&wrap.punctuation, inner_wrapped)
559 } else if !spec_prefix.is_empty() || !spec_suffix.is_empty() {
560 fmt.affix(spec_prefix, output, spec_suffix)
561 } else {
562 output
563 }
564 }
565
566 pub fn process_citation(&self, citation: &Citation) -> Result<String, ProcessorError> {
580 self.process_citation_with_format::<crate::render::plain::PlainText>(citation)
581 }
582
583 pub fn process_citation_with_format<F>(
592 &self,
593 citation: &Citation,
594 ) -> Result<String, ProcessorError>
595 where
596 F: crate::render::format::OutputFormat<Output = String>,
597 {
598 let fmt = F::default();
599
600 if citation.grouped {
604 self.initialize_numeric_citation_numbers();
605 self.resolve_dynamic_group(citation);
606 }
607
608 self.track_cited_ids_and_init_numbers(citation);
609
610 let effective_spec = self.resolve_effective_citation_spec(citation);
611 let note_start_text_case =
612 self.sentence_initial_note_start_text_case(citation, &effective_spec);
613 let (renderer_delimiter, renderer_inter_delimiter) =
614 self.resolve_citation_delimiters(&effective_spec);
615 let content = self.render_citation_content::<F>(
616 citation,
617 &effective_spec,
618 renderer_delimiter,
619 renderer_inter_delimiter,
620 note_start_text_case,
621 )?;
622 let output = self.apply_citation_input_affixes(citation, content, &fmt);
623 let wrapped = self.apply_spec_wrap_and_affixes(citation, &effective_spec, output, &fmt);
624
625 let finalized = if citation.sentence_start {
630 let case = crate::values::text_case::resolve_text_case(
631 citum_schema::options::titles::TextCase::CapitalizeFirst,
632 Some(self.locale.locale.as_str()),
633 );
634 crate::values::text_case::apply_text_case_markup_aware(&wrapped, case)
635 } else {
636 wrapped
637 };
638
639 Ok(fmt.finish(finalized))
640 }
641
642 pub fn process_citations(&self, citations: &[Citation]) -> Result<Vec<String>, ProcessorError> {
650 self.process_citations_with_format::<crate::render::plain::PlainText>(citations)
651 }
652
653 pub fn process_citations_with_format<F>(
659 &self,
660 citations: &[Citation],
661 ) -> Result<Vec<String>, ProcessorError>
662 where
663 F: crate::render::format::OutputFormat<Output = String>,
664 {
665 let mut normalized = self.normalize_note_context(citations);
666 self.annotate_positions(&mut normalized);
667 normalized
668 .iter()
669 .map(|citation| self.process_citation_with_format::<F>(citation))
670 .collect()
671 }
672}