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