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 indexmap::IndexMap;
33use std::borrow::Cow;
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 &'a self,
152 citation: &Citation,
153 effective_spec: &'a citum_schema::CitationSpec,
154 ) -> (Cow<'a, str>, Cow<'a, str>) {
155 let (script, realization) = self.citation_punctuation_context(citation);
156 let intra_delimiter = effective_spec
157 .delimiter
158 .as_ref()
159 .map(|punctuation| {
160 Cow::Owned(
161 crate::render::format::realize_punctuation(
162 punctuation,
163 script,
164 realization.as_deref(),
165 crate::render::format::PunctuationPosition::Separator,
166 )
167 .into_owned(),
168 )
169 })
170 .unwrap_or(Cow::Borrowed(", "));
171 let inter_delimiter = effective_spec
172 .multi_cite_delimiter
173 .as_ref()
174 .map(|punctuation| {
175 Cow::Owned(
176 crate::render::format::realize_punctuation(
177 punctuation,
178 script,
179 realization.as_deref(),
180 crate::render::format::PunctuationPosition::Separator,
181 )
182 .into_owned(),
183 )
184 })
185 .unwrap_or(Cow::Borrowed("; "));
186
187 (intra_delimiter, inter_delimiter)
188 }
189
190 fn resolve_dynamic_group(&self, citation: &Citation, run: &mut RunState) {
201 if self.get_bibliography_options().compound_numeric.is_none() {
202 return;
203 }
204
205 if citation.items.len() < 2 {
206 return;
207 }
208
209 #[allow(clippy::indexing_slicing, reason = "citation.items.len() >= 2")]
210 let head_id = &citation.items[0].id;
211 #[allow(clippy::indexing_slicing, reason = "citation.items.len() >= 2")]
212 let tail_ids: Vec<String> = citation.items[1..].iter().map(|i| i.id.clone()).collect();
213
214 if self.compound_set_by_ref.contains_key(head_id) {
216 return;
217 }
218 for tail in &tail_ids {
219 if self.compound_set_by_ref.contains_key(tail.as_str()) {
220 return;
221 }
222 }
223
224 if run
229 .dynamic_compound_set_by_ref
230 .contains_key(head_id.as_str())
231 || run.cited_ids.contains(head_id.as_str())
232 {
233 return;
234 }
235 for tail in &tail_ids {
236 if run.dynamic_compound_set_by_ref.contains_key(tail.as_str())
237 || run.cited_ids.contains(tail.as_str())
238 {
239 return;
240 }
241 }
242
243 let head_number = {
244 let numbers = run
245 .citation_numbers
246 .read()
247 .unwrap_or_else(std::sync::PoisonError::into_inner);
248 let Some(&n) = numbers.get(head_id.as_str()) else {
249 return;
250 };
251 n
252 };
253
254 {
256 let mut numbers = run
257 .citation_numbers
258 .write()
259 .unwrap_or_else(std::sync::PoisonError::into_inner);
260 for tail in &tail_ids {
261 numbers.insert(tail.clone(), head_number);
262 }
263 }
264
265 let all_members: Vec<String> = std::iter::once(head_id.clone())
267 .chain(tail_ids.iter().cloned())
268 .collect();
269
270 for (idx, member) in all_members.iter().enumerate() {
272 run.dynamic_compound_set_by_ref
273 .insert(member.clone(), head_id.clone());
274 run.dynamic_compound_member_index
275 .insert(member.clone(), idx);
276 }
277
278 {
280 let members = run
281 .compound_groups
282 .entry(head_number)
283 .or_insert_with(|| vec![head_id.clone()]);
284 for tail in &tail_ids {
285 if !members.contains(tail) {
286 members.push(tail.clone());
287 }
288 }
289 }
290
291 run.dynamic_compound_sets
293 .insert(head_id.clone(), all_members);
294 }
295
296 fn citation_scoped_by_cite_hints(
302 &self,
303 items: &[crate::reference::CitationItem],
304 config: &Config,
305 ) -> Option<HashMap<String, ProcHints>> {
306 if !Self::uses_by_cite_givenname(config) {
307 return None;
308 }
309
310 let mut scoped_hints = HashMap::new();
311 let mut scoped_bibliography = IndexMap::new();
312
313 for item in items {
314 let mut hint = self.hints.get(&item.id).cloned().unwrap_or_default();
315 hint.expand_given_names = false;
316 hint.expand_given_names_primary_only = false;
317 hint.min_names_to_show = None;
318 scoped_hints.insert(item.id.clone(), hint);
319
320 if let Some(reference) = self.bibliography.get(&item.id) {
321 scoped_bibliography.insert(item.id.clone(), reference.clone());
322 }
323 }
324
325 if scoped_bibliography.len() < 2 {
326 return Some(scoped_hints);
327 }
328
329 let bibliography_config = self.get_bibliography_config();
330 let mut disambiguator = Disambiguator::new(
331 &scoped_bibliography,
332 config,
333 &bibliography_config,
334 &self.locale,
335 );
336 if let Some(spec) = self.style.citation.as_ref() {
337 disambiguator = disambiguator.with_citation_spec(spec);
338 }
339 let local_hints = disambiguator.calculate_hints();
340
341 for item in items {
342 let Some(local) = local_hints.get(&item.id) else {
343 continue;
344 };
345 let target = scoped_hints.entry(item.id.clone()).or_default();
346 target.expand_given_names = local.expand_given_names;
347 target.expand_given_names_primary_only = local.expand_given_names_primary_only;
348 target.min_names_to_show = local.min_names_to_show;
349 }
350
351 Some(scoped_hints)
352 }
353
354 fn uses_by_cite_givenname(config: &Config) -> bool {
356 let disambiguate = config.effective_processing().config().disambiguate;
357
358 disambiguate
359 .as_ref()
360 .is_some_and(|d| d.add_givenname && matches!(d.givenname_rule, GivennameRule::ByCite))
361 }
362
363 fn merged_compound_data(
369 &self,
370 run: &RunState,
371 ) -> (
372 Option<HashMap<String, String>>,
373 Option<HashMap<String, usize>>,
374 Option<IndexMap<String, Vec<String>>>,
375 ) {
376 if run.dynamic_compound_set_by_ref.is_empty() {
377 return (None, None, None);
378 }
379 let merged_set: HashMap<String, String> = self
380 .compound_set_by_ref
381 .iter()
382 .chain(run.dynamic_compound_set_by_ref.iter())
383 .map(|(k, v)| (k.clone(), v.clone()))
384 .collect();
385 let merged_idx: HashMap<String, usize> = self
386 .compound_member_index
387 .iter()
388 .chain(run.dynamic_compound_member_index.iter())
389 .map(|(k, v)| (k.clone(), *v))
390 .collect();
391 let merged_sets: IndexMap<String, Vec<String>> = self
392 .compound_sets
393 .iter()
394 .chain(run.dynamic_compound_sets.iter())
395 .map(|(k, v)| (k.clone(), v.clone()))
396 .collect();
397 (Some(merged_set), Some(merged_idx), Some(merged_sets))
398 }
399
400 fn render_citation_content<F>(
405 &self,
406 citation: &Citation,
407 effective_spec: &citum_schema::CitationSpec,
408 renderer_delimiter: &str,
409 renderer_inter_delimiter: &str,
410 note_start_text_case: Option<NoteStartTextCase>,
411 run: &RunState,
412 ) -> Result<String, ProcessorError>
413 where
414 F: crate::render::format::OutputFormat<Output = String>,
415 {
416 let sorted_items = if citation.grouped {
419 citation.items.clone()
420 } else {
421 self.sort_citation_items(citation.items.clone(), effective_spec)
422 };
423
424 let (dyn_set_owned, dyn_idx_owned, dyn_sets_owned) = self.merged_compound_data(run);
427 let effective_set_by_ref = dyn_set_owned.as_ref().unwrap_or(&self.compound_set_by_ref);
428 let effective_member_index = dyn_idx_owned
429 .as_ref()
430 .unwrap_or(&self.compound_member_index);
431 let effective_compound_sets = dyn_sets_owned.as_ref().unwrap_or(&self.compound_sets);
432
433 let citation_config = self.get_citation_config();
434 let citation_config = match effective_spec.options.as_ref() {
435 Some(mode_options) => {
436 let mut config = citation_config.into_owned();
437 config.merge(&mode_options.to_config());
438 std::borrow::Cow::Owned(config)
439 }
440 None => citation_config,
441 };
442 let scoped_hints = self.citation_scoped_by_cite_hints(&sorted_items, &citation_config);
443 let renderer_hints = scoped_hints.as_ref().unwrap_or(&self.hints);
444 let citation_config = Arc::new(citation_config.into_owned());
445 let renderer = Renderer::new(
446 RendererResources {
447 style: &self.style,
448 bibliography: &self.bibliography,
449 locale: &self.locale,
450 config: citation_config.clone(),
451 bibliography_config: Some(Arc::new(self.get_bibliography_options().into_owned())),
452 first_note_by_id: Some(&run.first_note_by_id),
453 },
454 renderer_hints,
455 &run.citation_numbers,
456 CompoundRenderData {
457 set_by_ref: effective_set_by_ref,
458 member_index: effective_member_index,
459 sets: effective_compound_sets,
460 },
461 self.show_semantics,
462 self.inject_ast_indices,
463 self.abbreviation_map.as_ref(),
464 );
465 let processing = citation_config.processing.clone().unwrap_or_default();
466 let has_explicit_integral_multi_cite_delimiter = matches!(
467 citation.mode,
468 citum_schema::citation::CitationMode::Integral
469 ) && self
470 .resolve_positioned_citation_spec(citation)
471 .integral
472 .as_ref()
473 .and_then(|spec| spec.multi_cite_delimiter.as_ref())
474 .is_some();
475 let rendered_groups = if matches!(
476 processing,
477 citum_schema::options::Processing::Numeric
478 | citum_schema::options::Processing::Label(_)
479 ) {
480 renderer.render_ungrouped_citation_with_format::<F>(
481 &sorted_items,
482 effective_spec,
483 &citation.mode,
484 renderer_delimiter,
485 citation.suppress_author,
486 citation.position.as_ref(),
487 note_start_text_case,
488 )?
489 } else {
490 renderer.render_grouped_citation_with_format::<F>(
491 &sorted_items,
492 &GroupRenderParams {
493 spec: effective_spec,
494 mode: &citation.mode,
495 intra_delimiter: renderer_delimiter,
496 suppress_author: citation.suppress_author,
497 position: citation.position.as_ref(),
498 note_start_text_case,
499 },
500 )?
501 };
502
503 Ok(
504 if matches!(
505 citation.mode,
506 citum_schema::citation::CitationMode::Integral
507 ) && !has_explicit_integral_multi_cite_delimiter
508 {
509 join_integral_groups(rendered_groups, &self.locale)
510 } else {
511 F::default().join(rendered_groups, renderer_inter_delimiter)
512 },
513 )
514 }
515
516 fn apply_citation_input_affixes<F>(
521 &self,
522 citation: &Citation,
523 content: String,
524 fmt: &F,
525 ) -> String
526 where
527 F: crate::render::format::OutputFormat<Output = String>,
528 {
529 let citation_prefix = citation.prefix.as_deref().unwrap_or("");
530 let citation_suffix = citation.suffix.as_deref().unwrap_or("");
531
532 if citation_prefix.is_empty() && citation_suffix.is_empty() {
533 return content;
534 }
535
536 let formatted_prefix =
537 if !citation_prefix.is_empty() && !citation_prefix.ends_with(char::is_whitespace) {
538 format!("{citation_prefix} ")
539 } else {
540 citation_prefix.to_string()
541 };
542
543 let formatted_suffix =
544 if !citation_suffix.is_empty() && !citation_suffix.starts_with(char::is_whitespace) {
545 format!(" {citation_suffix}")
546 } else {
547 citation_suffix.to_string()
548 };
549
550 fmt.affix(&formatted_prefix, content, &formatted_suffix)
551 }
552
553 fn apply_spec_wrap_and_affixes<F>(
558 &self,
559 citation: &Citation,
560 effective_spec: &citum_schema::CitationSpec,
561 output: String,
562 fmt: &F,
563 ) -> String
564 where
565 F: crate::render::format::OutputFormat<Output = String>,
566 {
567 let (script, realization) = self.citation_punctuation_context(citation);
568 let spec_prefix = effective_spec
569 .prefix
570 .as_ref()
571 .map(|punctuation| {
572 crate::render::format::realize_punctuation(
573 punctuation,
574 script,
575 realization.as_deref(),
576 crate::render::format::PunctuationPosition::Prefix,
577 )
578 })
579 .unwrap_or(Cow::Borrowed(""));
580 let spec_suffix = effective_spec
581 .suffix
582 .as_ref()
583 .map(|punctuation| {
584 crate::render::format::realize_punctuation(
585 punctuation,
586 script,
587 realization.as_deref(),
588 crate::render::format::PunctuationPosition::Suffix,
589 )
590 })
591 .unwrap_or(Cow::Borrowed(""));
592
593 if matches!(
594 citation.mode,
595 citum_schema::citation::CitationMode::Integral
596 ) {
597 if !spec_prefix.is_empty() || !spec_suffix.is_empty() {
598 crate::render::format::apply_punctuation_affixes(
599 fmt,
600 effective_spec
601 .prefix
602 .as_ref()
603 .map(|punctuation| (punctuation, spec_prefix.as_ref())),
604 output,
605 effective_spec
606 .suffix
607 .as_ref()
608 .map(|punctuation| (punctuation, spec_suffix.as_ref())),
609 )
610 } else {
611 output
612 }
613 } else if let Some(wrap) = effective_spec.wrap.as_ref() {
614 let inner_prefix = wrap.inner_prefix.as_deref().unwrap_or("");
615 let inner_suffix = wrap.inner_suffix.as_deref().unwrap_or("");
616 let inner_wrapped = if !inner_prefix.is_empty() || !inner_suffix.is_empty() {
617 fmt.inner_affix(inner_prefix, output, inner_suffix)
618 } else {
619 output
620 };
621 let marks = crate::render::format::QuoteMarks::from(&self.locale.grammar_options);
622 fmt.wrap_punctuation(
623 &wrap.punctuation,
624 inner_wrapped,
625 &marks,
626 script,
627 realization.as_deref(),
628 )
629 } else if !spec_prefix.is_empty() || !spec_suffix.is_empty() {
630 crate::render::format::apply_punctuation_affixes(
631 fmt,
632 effective_spec
633 .prefix
634 .as_ref()
635 .map(|punctuation| (punctuation, spec_prefix.as_ref())),
636 output,
637 effective_spec
638 .suffix
639 .as_ref()
640 .map(|punctuation| (punctuation, spec_suffix.as_ref())),
641 )
642 } else {
643 output
644 }
645 }
646
647 fn wants_latin_punctuation_for_citation(&self, citation: &Citation) -> bool {
659 let configured = self.get_config().multilingual.as_ref().is_some_and(|ml| {
660 ml.scripts.get("latin").is_some_and(|script| {
661 script.punctuation == Some(citum_schema::options::PunctuationStyle::Latin)
662 })
663 });
664
665 configured
666 && citation.items.first().is_some_and(|item| {
667 self.bibliography.get(&item.id).is_some_and(|reference| {
668 crate::values::is_latin_script_language(
669 crate::values::effective_item_language(reference).as_deref(),
670 )
671 })
672 })
673 }
674
675 fn citation_punctuation_context(
678 &self,
679 citation: &Citation,
680 ) -> (
681 crate::values::ScriptClass,
682 Option<Cow<'_, citum_schema::options::PunctuationRealization>>,
683 ) {
684 let lang = citation.items.first().and_then(|item| {
685 self.bibliography
686 .get(&item.id)
687 .and_then(crate::values::effective_item_language)
688 });
689 crate::values::punctuation_realization_context(
690 lang.as_deref(),
691 self.get_config().multilingual.as_ref(),
692 self.locale.punctuation_realization.as_ref(),
693 )
694 }
695
696 pub fn process_citation(&self, citation: &Citation) -> Result<String, ProcessorError> {
709 let mut run = self.begin_run();
710 self.process_citation_with_format::<crate::render::plain::PlainText>(citation, &mut run)
711 }
712
713 pub fn process_citation_with_format<F>(
727 &self,
728 citation: &Citation,
729 run: &mut RunState,
730 ) -> Result<String, ProcessorError>
731 where
732 F: crate::render::format::OutputFormat<Output = String>,
733 {
734 let fmt = F::default();
735
736 if citation.grouped {
740 self.initialize_numeric_citation_numbers(run);
741 self.resolve_dynamic_group(citation, run);
742 }
743
744 self.track_cited_ids_and_init_numbers(citation, run);
745
746 let effective_spec = self.resolve_effective_citation_spec(citation);
747 let note_start_text_case =
748 self.sentence_initial_note_start_text_case(citation, &effective_spec);
749 let (renderer_delimiter, renderer_inter_delimiter) =
750 self.resolve_citation_delimiters(citation, &effective_spec);
751 let renderer_delimiter = if effective_spec
752 .delimiter
753 .as_ref()
754 .is_some_and(citum_schema::template::DelimiterPunctuation::is_semantic)
755 {
756 fmt.text(&renderer_delimiter)
757 } else {
758 renderer_delimiter.into_owned()
759 };
760 let renderer_inter_delimiter = if effective_spec
761 .multi_cite_delimiter
762 .as_ref()
763 .is_some_and(citum_schema::template::DelimiterPunctuation::is_semantic)
764 {
765 fmt.text(&renderer_inter_delimiter)
766 } else {
767 renderer_inter_delimiter.into_owned()
768 };
769 let content = self.render_citation_content::<F>(
770 citation,
771 &effective_spec,
772 &renderer_delimiter,
773 &renderer_inter_delimiter,
774 note_start_text_case,
775 run,
776 )?;
777 let output = self.apply_citation_input_affixes(citation, content, &fmt);
778 let wrapped = self.apply_spec_wrap_and_affixes(citation, &effective_spec, output, &fmt);
779 let wrapped = if self.wants_latin_punctuation_for_citation(citation) {
780 crate::render::component::remap_to_latin_punctuation(wrapped)
781 } else {
782 wrapped
783 };
784
785 let finalized = if citation.sentence_start {
790 let case = crate::values::text_case::resolve_text_case(
791 citum_schema::options::titles::TextCase::CapitalizeFirst,
792 Some(self.locale.locale.as_str()),
793 );
794 crate::values::text_case::apply_text_case_markup_aware_with_language(
795 &wrapped,
796 case,
797 Some(self.locale.locale.as_str()),
798 )
799 } else {
800 wrapped
801 };
802
803 Ok(fmt.finish(finalized))
804 }
805
806 pub fn process_citations(&self, citations: &[Citation]) -> Result<Vec<String>, ProcessorError> {
818 let mut run = self.begin_run();
819 self.process_citations_with_format::<crate::render::plain::PlainText>(citations, &mut run)
820 }
821
822 pub fn process_citations_with_format<F>(
834 &self,
835 citations: &[Citation],
836 run: &mut RunState,
837 ) -> Result<Vec<String>, ProcessorError>
838 where
839 F: crate::render::format::OutputFormat<Output = String>,
840 {
841 let mut normalized = self.normalize_note_context(citations, run);
842 self.annotate_positions(&mut normalized);
843 normalized
844 .iter()
845 .map(|citation| self.process_citation_with_format::<F>(citation, run))
846 .collect()
847 }
848}