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