1use super::Processor;
23use super::disambiguation::Disambiguator;
24use super::rendering::{CompoundRenderData, GroupRenderParams, Renderer, RendererResources};
25use super::run_state::RunState;
26use crate::error::ProcessorError;
27use crate::reference::Citation;
28use crate::values::ProcHints;
29use citum_schema::NoteStartTextCase;
30use citum_schema::locale::{GeneralTerm, Locale, TermForm};
31use citum_schema::options::{Config, GivennameRule};
32use citum_schema::template::DelimiterPunctuation;
33use indexmap::IndexMap;
34use std::collections::HashMap;
35use std::sync::Arc;
36
37fn join_integral_groups(rendered_groups: Vec<String>, locale: &Locale) -> String {
42 match rendered_groups.len() {
43 0 => String::new(),
44 1 => rendered_groups.into_iter().next().unwrap_or_default(),
45 2 => {
46 let conjunction = locale
47 .resolved_general_term(&GeneralTerm::And, &TermForm::Long, None)
48 .unwrap_or_else(|| locale.and_term(false).to_string());
49 rendered_groups.join(&format!(" {} ", conjunction.trim()))
50 }
51 _ => {
52 let conjunction = locale
53 .resolved_general_term(&GeneralTerm::And, &TermForm::Long, None)
54 .unwrap_or_else(|| locale.and_term(false).to_string());
55 let final_delimiter = if locale.grammar_options.serial_comma {
56 format!(", {} ", conjunction.trim())
57 } else {
58 format!(" {} ", conjunction.trim())
59 };
60
61 let mut rendered_groups = rendered_groups;
62 let last = rendered_groups.pop().unwrap_or_default();
63 format!("{}{}{}", rendered_groups.join(", "), final_delimiter, last)
64 }
65 }
66}
67
68impl Processor {
69 fn sentence_initial_note_start_text_case(
74 &self,
75 citation: &Citation,
76 effective_spec: &citum_schema::CitationSpec,
77 ) -> Option<NoteStartTextCase> {
78 let spec_prefix = effective_spec.prefix.as_deref().unwrap_or("");
79 if self.is_note_style()
80 && matches!(
81 citation.position,
82 Some(
83 citum_schema::citation::Position::Ibid
84 | citum_schema::citation::Position::IbidWithLocator
85 )
86 )
87 && matches!(
88 citation.mode,
89 citum_schema::citation::CitationMode::NonIntegral
90 )
91 && citation.prefix.as_deref().unwrap_or("").is_empty()
92 && spec_prefix.is_empty()
93 {
94 effective_spec.note_start_text_case
95 } else {
96 None
97 }
98 }
99
100 fn resolve_positioned_citation_spec(
105 &self,
106 citation: &Citation,
107 ) -> std::borrow::Cow<'_, citum_schema::CitationSpec> {
108 self.style.citation.as_ref().map_or_else(
109 || std::borrow::Cow::Owned(citum_schema::CitationSpec::default()),
110 |spec| spec.resolve_for_position(citation.position.as_ref()),
111 )
112 }
113
114 pub fn register_nocite_ids(&self, ids: impl IntoIterator<Item = String>, run: &mut RunState) {
125 for id in ids {
126 run.cited_ids.insert(id);
127 }
128 }
129
130 fn track_cited_ids_and_init_numbers(&self, citation: &Citation, run: &mut RunState) {
135 self.initialize_numeric_citation_numbers(run);
136 for item in &citation.items {
137 run.cited_ids.insert(item.id.clone());
138 }
139 }
140
141 fn resolve_effective_citation_spec(&self, citation: &Citation) -> citum_schema::CitationSpec {
143 self.resolve_positioned_citation_spec(citation)
144 .into_owned()
145 .resolve_for_mode(&citation.mode)
146 .into_owned()
147 }
148
149 fn resolve_citation_delimiters<'a>(
151 &self,
152 effective_spec: &'a citum_schema::CitationSpec,
153 ) -> (&'a str, &'a str) {
154 let intra_delimiter = effective_spec.delimiter.as_deref().unwrap_or(", ");
155 let inter_delimiter = effective_spec
156 .multi_cite_delimiter
157 .as_deref()
158 .unwrap_or("; ");
159
160 (
161 if matches!(
162 DelimiterPunctuation::from_csl_string(intra_delimiter),
163 DelimiterPunctuation::None
164 ) {
165 ""
166 } else {
167 intra_delimiter
168 },
169 if matches!(
170 DelimiterPunctuation::from_csl_string(inter_delimiter),
171 DelimiterPunctuation::None
172 ) {
173 ""
174 } else {
175 inter_delimiter
176 },
177 )
178 }
179
180 fn resolve_dynamic_group(&self, citation: &Citation, run: &mut RunState) {
191 if self.get_bibliography_options().compound_numeric.is_none() {
192 return;
193 }
194
195 if citation.items.len() < 2 {
196 return;
197 }
198
199 #[allow(clippy::indexing_slicing, reason = "citation.items.len() >= 2")]
200 let head_id = &citation.items[0].id;
201 #[allow(clippy::indexing_slicing, reason = "citation.items.len() >= 2")]
202 let tail_ids: Vec<String> = citation.items[1..].iter().map(|i| i.id.clone()).collect();
203
204 if self.compound_set_by_ref.contains_key(head_id) {
206 return;
207 }
208 for tail in &tail_ids {
209 if self.compound_set_by_ref.contains_key(tail.as_str()) {
210 return;
211 }
212 }
213
214 if run
219 .dynamic_compound_set_by_ref
220 .contains_key(head_id.as_str())
221 || run.cited_ids.contains(head_id.as_str())
222 {
223 return;
224 }
225 for tail in &tail_ids {
226 if run.dynamic_compound_set_by_ref.contains_key(tail.as_str())
227 || run.cited_ids.contains(tail.as_str())
228 {
229 return;
230 }
231 }
232
233 let head_number = {
234 let numbers = run
235 .citation_numbers
236 .read()
237 .unwrap_or_else(std::sync::PoisonError::into_inner);
238 let Some(&n) = numbers.get(head_id.as_str()) else {
239 return;
240 };
241 n
242 };
243
244 {
246 let mut numbers = run
247 .citation_numbers
248 .write()
249 .unwrap_or_else(std::sync::PoisonError::into_inner);
250 for tail in &tail_ids {
251 numbers.insert(tail.clone(), head_number);
252 }
253 }
254
255 let all_members: Vec<String> = std::iter::once(head_id.clone())
257 .chain(tail_ids.iter().cloned())
258 .collect();
259
260 for (idx, member) in all_members.iter().enumerate() {
262 run.dynamic_compound_set_by_ref
263 .insert(member.clone(), head_id.clone());
264 run.dynamic_compound_member_index
265 .insert(member.clone(), idx);
266 }
267
268 {
270 let members = run
271 .compound_groups
272 .entry(head_number)
273 .or_insert_with(|| vec![head_id.clone()]);
274 for tail in &tail_ids {
275 if !members.contains(tail) {
276 members.push(tail.clone());
277 }
278 }
279 }
280
281 run.dynamic_compound_sets
283 .insert(head_id.clone(), all_members);
284 }
285
286 fn citation_scoped_by_cite_hints(
292 &self,
293 items: &[crate::reference::CitationItem],
294 config: &Config,
295 ) -> Option<HashMap<String, ProcHints>> {
296 if !Self::uses_by_cite_givenname(config) {
297 return None;
298 }
299
300 let mut scoped_hints = HashMap::new();
301 let mut scoped_bibliography = IndexMap::new();
302
303 for item in items {
304 let mut hint = self.hints.get(&item.id).cloned().unwrap_or_default();
305 hint.expand_given_names = false;
306 hint.expand_given_names_primary_only = false;
307 hint.min_names_to_show = None;
308 scoped_hints.insert(item.id.clone(), hint);
309
310 if let Some(reference) = self.bibliography.get(&item.id) {
311 scoped_bibliography.insert(item.id.clone(), reference.clone());
312 }
313 }
314
315 if scoped_bibliography.len() < 2 {
316 return Some(scoped_hints);
317 }
318
319 let bibliography_config = self.get_bibliography_config();
320 let mut disambiguator = Disambiguator::new(
321 &scoped_bibliography,
322 config,
323 &bibliography_config,
324 &self.locale,
325 );
326 if let Some(spec) = self.style.citation.as_ref() {
327 disambiguator = disambiguator.with_citation_spec(spec);
328 }
329 let local_hints = disambiguator.calculate_hints();
330
331 for item in items {
332 let Some(local) = local_hints.get(&item.id) else {
333 continue;
334 };
335 let target = scoped_hints.entry(item.id.clone()).or_default();
336 target.expand_given_names = local.expand_given_names;
337 target.expand_given_names_primary_only = local.expand_given_names_primary_only;
338 target.min_names_to_show = local.min_names_to_show;
339 }
340
341 Some(scoped_hints)
342 }
343
344 fn uses_by_cite_givenname(config: &Config) -> bool {
346 let disambiguate = config.effective_processing().config().disambiguate;
347
348 disambiguate
349 .as_ref()
350 .is_some_and(|d| d.add_givenname && matches!(d.givenname_rule, GivennameRule::ByCite))
351 }
352
353 fn merged_compound_data(
359 &self,
360 run: &RunState,
361 ) -> (
362 Option<HashMap<String, String>>,
363 Option<HashMap<String, usize>>,
364 Option<IndexMap<String, Vec<String>>>,
365 ) {
366 if run.dynamic_compound_set_by_ref.is_empty() {
367 return (None, None, None);
368 }
369 let merged_set: HashMap<String, String> = self
370 .compound_set_by_ref
371 .iter()
372 .chain(run.dynamic_compound_set_by_ref.iter())
373 .map(|(k, v)| (k.clone(), v.clone()))
374 .collect();
375 let merged_idx: HashMap<String, usize> = self
376 .compound_member_index
377 .iter()
378 .chain(run.dynamic_compound_member_index.iter())
379 .map(|(k, v)| (k.clone(), *v))
380 .collect();
381 let merged_sets: IndexMap<String, Vec<String>> = self
382 .compound_sets
383 .iter()
384 .chain(run.dynamic_compound_sets.iter())
385 .map(|(k, v)| (k.clone(), v.clone()))
386 .collect();
387 (Some(merged_set), Some(merged_idx), Some(merged_sets))
388 }
389
390 fn render_citation_content<F>(
395 &self,
396 citation: &Citation,
397 effective_spec: &citum_schema::CitationSpec,
398 renderer_delimiter: &str,
399 renderer_inter_delimiter: &str,
400 note_start_text_case: Option<NoteStartTextCase>,
401 run: &RunState,
402 ) -> Result<String, ProcessorError>
403 where
404 F: crate::render::format::OutputFormat<Output = String>,
405 {
406 let sorted_items = if citation.grouped {
409 citation.items.clone()
410 } else {
411 self.sort_citation_items(citation.items.clone(), effective_spec)
412 };
413
414 let (dyn_set_owned, dyn_idx_owned, dyn_sets_owned) = self.merged_compound_data(run);
417 let effective_set_by_ref = dyn_set_owned.as_ref().unwrap_or(&self.compound_set_by_ref);
418 let effective_member_index = dyn_idx_owned
419 .as_ref()
420 .unwrap_or(&self.compound_member_index);
421 let effective_compound_sets = dyn_sets_owned.as_ref().unwrap_or(&self.compound_sets);
422
423 let citation_config = self.get_citation_config();
424 let citation_config = match effective_spec.options.as_ref() {
425 Some(mode_options) => {
426 let mut config = citation_config.into_owned();
427 config.merge(&mode_options.to_config());
428 std::borrow::Cow::Owned(config)
429 }
430 None => citation_config,
431 };
432 let scoped_hints = self.citation_scoped_by_cite_hints(&sorted_items, &citation_config);
433 let renderer_hints = scoped_hints.as_ref().unwrap_or(&self.hints);
434 let citation_config = Arc::new(citation_config.into_owned());
435 let renderer = Renderer::new(
436 RendererResources {
437 style: &self.style,
438 bibliography: &self.bibliography,
439 locale: &self.locale,
440 config: citation_config.clone(),
441 bibliography_config: Some(Arc::new(self.get_bibliography_options().into_owned())),
442 first_note_by_id: Some(&run.first_note_by_id),
443 },
444 renderer_hints,
445 &run.citation_numbers,
446 CompoundRenderData {
447 set_by_ref: effective_set_by_ref,
448 member_index: effective_member_index,
449 sets: effective_compound_sets,
450 },
451 self.show_semantics,
452 self.inject_ast_indices,
453 self.abbreviation_map.as_ref(),
454 );
455 let processing = citation_config.processing.clone().unwrap_or_default();
456 let has_explicit_integral_multi_cite_delimiter = matches!(
457 citation.mode,
458 citum_schema::citation::CitationMode::Integral
459 ) && self
460 .resolve_positioned_citation_spec(citation)
461 .integral
462 .as_ref()
463 .and_then(|spec| spec.multi_cite_delimiter.as_ref())
464 .is_some();
465 let rendered_groups = if matches!(
466 processing,
467 citum_schema::options::Processing::Numeric
468 | citum_schema::options::Processing::Label(_)
469 ) {
470 renderer.render_ungrouped_citation_with_format::<F>(
471 &sorted_items,
472 effective_spec,
473 &citation.mode,
474 renderer_delimiter,
475 citation.suppress_author,
476 citation.position.as_ref(),
477 note_start_text_case,
478 )?
479 } else {
480 renderer.render_grouped_citation_with_format::<F>(
481 &sorted_items,
482 &GroupRenderParams {
483 spec: effective_spec,
484 mode: &citation.mode,
485 intra_delimiter: renderer_delimiter,
486 suppress_author: citation.suppress_author,
487 position: citation.position.as_ref(),
488 note_start_text_case,
489 },
490 )?
491 };
492
493 Ok(
494 if matches!(
495 citation.mode,
496 citum_schema::citation::CitationMode::Integral
497 ) && !has_explicit_integral_multi_cite_delimiter
498 {
499 join_integral_groups(rendered_groups, &self.locale)
500 } else {
501 F::default().join(rendered_groups, renderer_inter_delimiter)
502 },
503 )
504 }
505
506 fn apply_citation_input_affixes<F>(
511 &self,
512 citation: &Citation,
513 content: String,
514 fmt: &F,
515 ) -> String
516 where
517 F: crate::render::format::OutputFormat<Output = String>,
518 {
519 let citation_prefix = citation.prefix.as_deref().unwrap_or("");
520 let citation_suffix = citation.suffix.as_deref().unwrap_or("");
521
522 if citation_prefix.is_empty() && citation_suffix.is_empty() {
523 return content;
524 }
525
526 let formatted_prefix =
527 if !citation_prefix.is_empty() && !citation_prefix.ends_with(char::is_whitespace) {
528 format!("{citation_prefix} ")
529 } else {
530 citation_prefix.to_string()
531 };
532
533 let formatted_suffix =
534 if !citation_suffix.is_empty() && !citation_suffix.starts_with(char::is_whitespace) {
535 format!(" {citation_suffix}")
536 } else {
537 citation_suffix.to_string()
538 };
539
540 fmt.affix(&formatted_prefix, content, &formatted_suffix)
541 }
542
543 fn apply_spec_wrap_and_affixes<F>(
548 &self,
549 citation: &Citation,
550 effective_spec: &citum_schema::CitationSpec,
551 output: String,
552 fmt: &F,
553 ) -> String
554 where
555 F: crate::render::format::OutputFormat<Output = String>,
556 {
557 let spec_prefix = effective_spec.prefix.as_deref().unwrap_or("");
558 let spec_suffix = effective_spec.suffix.as_deref().unwrap_or("");
559
560 if matches!(
561 citation.mode,
562 citum_schema::citation::CitationMode::Integral
563 ) {
564 if !spec_prefix.is_empty() || !spec_suffix.is_empty() {
565 fmt.affix(spec_prefix, output, spec_suffix)
566 } else {
567 output
568 }
569 } else if let Some(wrap) = effective_spec.wrap.as_ref() {
570 let inner_prefix = wrap.inner_prefix.as_deref().unwrap_or("");
571 let inner_suffix = wrap.inner_suffix.as_deref().unwrap_or("");
572 let inner_wrapped = if !inner_prefix.is_empty() || !inner_suffix.is_empty() {
573 fmt.inner_affix(inner_prefix, output, inner_suffix)
574 } else {
575 output
576 };
577 let marks = crate::render::format::QuoteMarks::from(&self.locale.grammar_options);
578 fmt.wrap_punctuation(&wrap.punctuation, inner_wrapped, &marks)
579 } else if !spec_prefix.is_empty() || !spec_suffix.is_empty() {
580 fmt.affix(spec_prefix, output, spec_suffix)
581 } else {
582 output
583 }
584 }
585
586 pub fn process_citation(&self, citation: &Citation) -> Result<String, ProcessorError> {
599 let mut run = self.begin_run();
600 self.process_citation_with_format::<crate::render::plain::PlainText>(citation, &mut run)
601 }
602
603 pub fn process_citation_with_format<F>(
617 &self,
618 citation: &Citation,
619 run: &mut RunState,
620 ) -> Result<String, ProcessorError>
621 where
622 F: crate::render::format::OutputFormat<Output = String>,
623 {
624 let fmt = F::default();
625
626 if citation.grouped {
630 self.initialize_numeric_citation_numbers(run);
631 self.resolve_dynamic_group(citation, run);
632 }
633
634 self.track_cited_ids_and_init_numbers(citation, run);
635
636 let effective_spec = self.resolve_effective_citation_spec(citation);
637 let note_start_text_case =
638 self.sentence_initial_note_start_text_case(citation, &effective_spec);
639 let (renderer_delimiter, renderer_inter_delimiter) =
640 self.resolve_citation_delimiters(&effective_spec);
641 let content = self.render_citation_content::<F>(
642 citation,
643 &effective_spec,
644 renderer_delimiter,
645 renderer_inter_delimiter,
646 note_start_text_case,
647 run,
648 )?;
649 let output = self.apply_citation_input_affixes(citation, content, &fmt);
650 let wrapped = self.apply_spec_wrap_and_affixes(citation, &effective_spec, output, &fmt);
651
652 let finalized = if citation.sentence_start {
657 let case = crate::values::text_case::resolve_text_case(
658 citum_schema::options::titles::TextCase::CapitalizeFirst,
659 Some(self.locale.locale.as_str()),
660 );
661 crate::values::text_case::apply_text_case_markup_aware(&wrapped, case)
662 } else {
663 wrapped
664 };
665
666 Ok(fmt.finish(finalized))
667 }
668
669 pub fn process_citations(&self, citations: &[Citation]) -> Result<Vec<String>, ProcessorError> {
681 let mut run = self.begin_run();
682 self.process_citations_with_format::<crate::render::plain::PlainText>(citations, &mut run)
683 }
684
685 pub fn process_citations_with_format<F>(
697 &self,
698 citations: &[Citation],
699 run: &mut RunState,
700 ) -> Result<Vec<String>, ProcessorError>
701 where
702 F: crate::render::format::OutputFormat<Output = String>,
703 {
704 let mut normalized = self.normalize_note_context(citations, run);
705 self.annotate_positions(&mut normalized);
706 normalized
707 .iter()
708 .map(|citation| self.process_citation_with_format::<F>(citation, run))
709 .collect()
710 }
711}