1use super::Processor;
14use super::disambiguation::Disambiguator;
15use super::run_state::RunState;
16use crate::error::ProcessorError;
17use crate::reference::{Bibliography, CitationItem, Reference};
18use crate::values::ProcHints;
19use citum_schema::Style;
20use citum_schema::locale::Locale;
21use citum_schema::options::{
22 BibliographyPartitionKind, BibliographyPartitionMode, BibliographySortPartitioning, Config,
23 PunctuationConfig, SortingMultilingualMode, bibliography::BibliographyConfig,
24};
25use indexmap::IndexMap;
26use std::collections::HashMap;
27
28impl Default for Processor {
29 fn default() -> Self {
30 let compound_sets = IndexMap::new();
31 let (compound_set_by_ref, compound_member_index) =
32 Self::build_compound_set_indexes(&compound_sets);
33 Self {
34 style: Style::default(),
35 bibliography: Bibliography::default(),
36 locale: Locale::en_us(),
37 default_config: Config::default(),
38 hints: HashMap::new(),
39 compound_sets,
40 compound_set_by_ref,
41 compound_member_index,
42 show_semantics: true,
43 inject_ast_indices: false,
44 abbreviation_map: None,
45 }
46 }
47}
48
49impl Processor {
50 fn resolve_punctuation_defaults(&self, config: &mut Config) {
52 let punctuation = config
53 .punctuation
54 .get_or_insert_with(PunctuationConfig::default);
55 punctuation
56 .strong_terminal_comma_policy
57 .get_or_insert(self.locale.grammar_options.strong_terminal_comma_policy);
58 punctuation
59 .delimiter_suppressing_terminal_marks
60 .get_or_insert_with(|| {
61 self.locale
62 .grammar_options
63 .delimiter_suppressing_terminal_marks
64 .clone()
65 });
66 }
67
68 fn punctuation_defaults_require_resolution(&self, config: &Config) -> bool {
70 let punctuation = config.punctuation.as_ref();
71 let policy_is_unset = punctuation
72 .and_then(|options| options.strong_terminal_comma_policy)
73 .is_none();
74 let marks_are_unset = punctuation
75 .and_then(|options| options.delimiter_suppressing_terminal_marks.as_ref())
76 .is_none();
77
78 (policy_is_unset
79 && self.locale.grammar_options.strong_terminal_comma_policy
80 != citum_schema::options::StrongTerminalCommaPolicy::default())
81 || (marks_are_unset
82 && self
83 .locale
84 .grammar_options
85 .delimiter_suppressing_terminal_marks
86 != "?!…")
87 }
88
89 fn with_punctuation_defaults<'a>(
91 &self,
92 config: std::borrow::Cow<'a, Config>,
93 ) -> std::borrow::Cow<'a, Config> {
94 if !self.punctuation_defaults_require_resolution(&config) {
95 return config;
96 }
97
98 let mut config = config.into_owned();
99 self.resolve_punctuation_defaults(&mut config);
100 std::borrow::Cow::Owned(config)
101 }
102
103 fn build_processor(
107 style: Style,
108 bibliography: Bibliography,
109 locale: Locale,
110 compound_sets: IndexMap<String, Vec<String>>,
111 ) -> Self {
112 let style = style.into_resolved();
113 Self::build_processor_pre_resolved(style, bibliography, locale, compound_sets)
114 }
115
116 pub(super) fn build_processor_pre_resolved(
122 style: Style,
123 bibliography: Bibliography,
124 locale: Locale,
125 compound_sets: IndexMap<String, Vec<String>>,
126 ) -> Self {
127 let (compound_set_by_ref, compound_member_index) =
128 Self::build_compound_set_indexes(&compound_sets);
129 let mut processor = Processor {
130 style,
131 bibliography,
132 locale,
133 default_config: Config::default(),
134 hints: HashMap::new(),
135 compound_sets,
136 compound_set_by_ref,
137 compound_member_index,
138 show_semantics: true,
139 inject_ast_indices: false,
140 abbreviation_map: None,
141 };
142
143 processor.hints = processor.calculate_hints();
145 processor
146 }
147
148 fn try_validate_compound_sets(
150 bibliography: &Bibliography,
151 compound_sets: IndexMap<String, Vec<String>>,
152 ) -> Result<IndexMap<String, Vec<String>>, ProcessorError> {
153 super::validate_compound_sets(Some(compound_sets), bibliography)
154 .map(Option::unwrap_or_default)
155 }
156
157 fn validate_compound_sets_or_default(
159 bibliography: &Bibliography,
160 compound_sets: IndexMap<String, Vec<String>>,
161 ) -> IndexMap<String, Vec<String>> {
162 Self::try_validate_compound_sets(bibliography, compound_sets).unwrap_or_default()
163 }
164
165 fn build_compound_set_indexes(
170 sets: &IndexMap<String, Vec<String>>,
171 ) -> (HashMap<String, String>, HashMap<String, usize>) {
172 let mut by_ref = HashMap::new();
173 let mut member_index = HashMap::new();
174 for (set_id, members) in sets {
175 for (idx, member) in members.iter().enumerate() {
176 by_ref.insert(member.clone(), set_id.clone());
177 member_index.insert(member.clone(), idx);
178 }
179 }
180 (by_ref, member_index)
181 }
182
183 pub(crate) fn is_note_style(&self) -> bool {
185 self.get_config()
186 .processing
187 .as_ref()
188 .is_some_and(|processing| matches!(processing, citum_schema::options::Processing::Note))
189 }
190
191 fn is_numeric_style(&self) -> bool {
193 self.get_config()
194 .processing
195 .as_ref()
196 .is_some_and(|processing| {
197 matches!(processing, citum_schema::options::Processing::Numeric)
198 })
199 }
200
201 fn is_numeric_bibliography_style(&self) -> bool {
203 self.get_bibliography_config()
204 .processing
205 .as_ref()
206 .is_some_and(|processing| {
207 matches!(processing, citum_schema::options::Processing::Numeric)
208 })
209 }
210
211 fn resolved_bibliography_sort(&self) -> Option<(citum_schema::grouping::GroupSort, bool)> {
224 if let Some(sort_spec) = self
225 .style
226 .bibliography
227 .as_ref()
228 .and_then(|bibliography| bibliography.sort.as_ref())
229 {
230 return Some((sort_spec.resolve(), false));
231 }
232
233 let bibliography_config = self.get_bibliography_config();
234
235 if let Some(preset) = bibliography_config
236 .processing
237 .as_ref()
238 .and_then(citum_schema::options::Processing::default_bibliography_sort)
239 {
240 return Some((preset.group_sort(), false));
241 }
242
243 bibliography_config
244 .processing
245 .clone()
246 .unwrap_or_default()
247 .config()
248 .sort
249 .map(|sort_entry| (sort_entry.resolve().group_sort(), true))
250 }
251
252 pub(crate) fn initialize_numeric_citation_numbers(&self, run: &mut RunState) {
262 if !self.is_numeric_style() {
263 return;
264 }
265
266 self.initialize_numeric_numbers(run, self.sort_citation_number_order());
267 }
268
269 pub(crate) fn initialize_numeric_bibliography_numbers(&self, run: &mut RunState) {
271 if !self.is_numeric_bibliography_style() {
272 return;
273 }
274
275 self.initialize_numeric_numbers(run, self.sort_bibliography_number_order());
276 }
277
278 fn initialize_numeric_numbers(&self, run: &mut RunState, ordered_ids: Vec<String>) {
280 if !run
281 .citation_numbers
282 .read()
283 .unwrap_or_else(std::sync::PoisonError::into_inner)
284 .is_empty()
285 {
286 return;
287 }
288
289 self.initialize_numeric_citation_numbers_from_ordered_ids(run, ordered_ids);
290 }
291
292 fn sort_citation_number_order(&self) -> Vec<String> {
294 self.sort_references(self.bibliography.values().collect())
295 .into_iter()
296 .filter_map(citum_schema::reference::InputReference::id)
297 .map(String::from)
298 .collect()
299 }
300
301 fn sort_bibliography_number_order(&self) -> Vec<String> {
303 self.sort_references(self.bibliography.values().collect())
304 .into_iter()
305 .filter_map(citum_schema::reference::InputReference::id)
306 .map(String::from)
307 .collect()
308 }
309
310 fn initialize_numeric_citation_numbers_from_ordered_ids(
315 &self,
316 run: &mut RunState,
317 ordered_ids: Vec<String>,
318 ) {
319 let mut numbers = run
320 .citation_numbers
321 .write()
322 .unwrap_or_else(std::sync::PoisonError::into_inner);
323 if !numbers.is_empty() {
324 return;
325 }
326
327 let compound_config = self.get_bibliography_options().compound_numeric.clone();
328
329 if compound_config.is_some() {
330 let mut set_first_seen: IndexMap<String, usize> = IndexMap::new();
331 let mut current_number = 1usize;
332 run.compound_groups.clear();
333
334 for ref_id in &ordered_ids {
335 if let Some(set_id) = self.compound_set_by_ref.get(ref_id) {
336 if let Some(&number) = set_first_seen.get(set_id) {
337 numbers.insert(ref_id.clone(), number);
338 } else {
339 set_first_seen.insert(set_id.clone(), current_number);
340 if let Some(members) = self.compound_sets.get(set_id) {
341 let present_members: Vec<String> = members
342 .iter()
343 .filter(|id| self.bibliography.contains_key(*id))
344 .cloned()
345 .collect();
346 for member in &present_members {
347 numbers.insert(member.clone(), current_number);
348 }
349 if present_members.len() > 1 {
350 run.compound_groups.insert(current_number, present_members);
351 }
352 } else {
353 numbers.insert(ref_id.clone(), current_number);
354 }
355 current_number += 1;
356 }
357 } else if !numbers.contains_key(ref_id) {
358 numbers.insert(ref_id.clone(), current_number);
359 current_number += 1;
360 }
361 }
362 } else {
363 for (index, ref_id) in ordered_ids.into_iter().enumerate() {
364 numbers.insert(ref_id, index + 1);
365 }
366 }
367 }
368
369 #[must_use]
379 pub fn begin_run(&self) -> RunState {
380 let mut run = RunState::default();
381 self.initialize_numeric_citation_numbers(&mut run);
382 self.initialize_numeric_bibliography_numbers(&mut run);
383 run
384 }
385
386 #[must_use]
388 pub fn new(style: Style, bibliography: Bibliography) -> Self {
389 Self::with_compound_sets(style, bibliography, IndexMap::new())
390 }
391
392 pub fn try_with_compound_sets(
399 style: Style,
400 bibliography: Bibliography,
401 compound_sets: IndexMap<String, Vec<String>>,
402 ) -> Result<Self, ProcessorError> {
403 Self::try_with_locale_and_compound_sets(style, bibliography, Locale::en_us(), compound_sets)
404 }
405
406 #[must_use]
411 pub fn with_compound_sets(
412 style: Style,
413 bibliography: Bibliography,
414 compound_sets: IndexMap<String, Vec<String>>,
415 ) -> Self {
416 let validated_sets = Self::validate_compound_sets_or_default(&bibliography, compound_sets);
417 Self::build_processor(style, bibliography, Locale::en_us(), validated_sets)
418 }
419
420 #[must_use]
424 pub fn with_locale(style: Style, bibliography: Bibliography, locale: Locale) -> Self {
425 Self::with_locale_and_compound_sets(style, bibliography, locale, IndexMap::new())
426 }
427
428 pub fn try_with_locale_and_compound_sets(
436 style: Style,
437 bibliography: Bibliography,
438 locale: Locale,
439 compound_sets: IndexMap<String, Vec<String>>,
440 ) -> Result<Self, ProcessorError> {
441 let validated_sets = Self::try_validate_compound_sets(&bibliography, compound_sets)?;
442 Ok(Self::build_processor(
443 style,
444 bibliography,
445 locale,
446 validated_sets,
447 ))
448 }
449
450 #[must_use]
457 pub fn with_locale_and_compound_sets(
458 style: Style,
459 bibliography: Bibliography,
460 locale: Locale,
461 compound_sets: IndexMap<String, Vec<String>>,
462 ) -> Self {
463 let validated_sets = Self::validate_compound_sets_or_default(&bibliography, compound_sets);
464 Self::build_processor(style, bibliography, locale, validated_sets)
465 }
466
467 #[must_use]
472 pub fn with_style_locale(
473 style: Style,
474 bibliography: Bibliography,
475 locales_dir: &std::path::Path,
476 ) -> Self {
477 let style = style.into_resolved();
478 let locale = if let Some(ref locale_id) = style.info.default_locale {
479 Locale::load(locale_id, locales_dir)
480 } else {
481 Locale::en_us()
482 };
483 Self::with_locale_and_compound_sets(style, bibliography, locale, IndexMap::new())
484 }
485
486 #[must_use]
488 pub fn with_inject_ast_indices(mut self, inject_ast_indices: bool) -> Self {
489 self.inject_ast_indices = inject_ast_indices;
490 self
491 }
492
493 pub fn set_inject_ast_indices(&mut self, inject_ast_indices: bool) {
495 self.inject_ast_indices = inject_ast_indices;
496 }
497
498 pub fn get_config(&self) -> &Config {
500 self.style.options.as_ref().unwrap_or(&self.default_config)
501 }
502
503 pub fn get_citation_config(&self) -> std::borrow::Cow<'_, Config> {
508 let base = self.get_config();
509 let config = match self
510 .style
511 .citation
512 .as_ref()
513 .and_then(|citation| citation.options.as_ref())
514 {
515 Some(citation_options) => std::borrow::Cow::Owned(citation_options.merged_with(base)),
516 None => std::borrow::Cow::Borrowed(base),
517 };
518 self.with_punctuation_defaults(config)
519 }
520
521 pub fn get_bibliography_config(&self) -> std::borrow::Cow<'_, Config> {
526 let base = self.get_config();
527 let config = match self
528 .style
529 .bibliography
530 .as_ref()
531 .and_then(|bibliography| bibliography.options.as_ref())
532 {
533 Some(bibliography_options) => {
534 std::borrow::Cow::Owned(bibliography_options.merged_with(base))
535 }
536 None => std::borrow::Cow::Borrowed(base),
537 };
538 self.with_punctuation_defaults(config)
539 }
540
541 pub fn get_bibliography_options(&self) -> std::borrow::Cow<'_, BibliographyConfig> {
543 match self
544 .style
545 .bibliography
546 .as_ref()
547 .and_then(|bibliography| bibliography.options.as_ref())
548 {
549 Some(bibliography_options) => {
550 std::borrow::Cow::Owned(bibliography_options.to_bibliography_config())
551 }
552 None => std::borrow::Cow::Owned(BibliographyConfig::default()),
553 }
554 }
555
556 pub fn sort_references<'a>(&self, references: Vec<&'a Reference>) -> Vec<&'a Reference> {
560 let bibliography_config = self.get_bibliography_config();
561 let mut sorted_refs = match self.resolved_bibliography_sort() {
562 Some((sort_spec, true)) => {
563 let mut sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
564 &self.locale,
565 &bibliography_config,
566 );
567 if let Some(spec) = self.style.bibliography.as_ref() {
568 sorter = sorter.with_bibliography_spec(spec);
569 }
570 sorter.sort_references_with_id_tiebreak(references, &sort_spec)
571 }
572 Some((sort_spec, false)) => {
573 let mut sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
574 &self.locale,
575 &bibliography_config,
576 );
577 if let Some(spec) = self.style.bibliography.as_ref() {
578 sorter = sorter.with_bibliography_spec(spec);
579 }
580 sorter.sort_references(references, &sort_spec)
581 }
582 None => references,
583 };
584
585 let bibliography_options = self.get_bibliography_options();
586 if let Some(partitioning) =
587 effective_sort_partitioning(&bibliography_options, &bibliography_config).as_ref()
588 && crate::sort_partitioning::should_sort_flat(partitioning)
589 {
590 crate::sort_partitioning::sort_by_partition(
591 sorted_refs.as_mut_slice(),
592 &self.locale,
593 partitioning,
594 );
595 }
596
597 sorted_refs
598 }
599
600 pub fn sort_citation_items(
602 &self,
603 items: Vec<CitationItem>,
604 spec: &citum_schema::CitationSpec,
605 ) -> Vec<CitationItem> {
606 if let Some(sort_spec) = &spec.sort {
607 let items_with_refs: Vec<(CitationItem, Option<&Reference>)> = items
608 .into_iter()
609 .map(|item| {
610 let reference = self.bibliography.get(&item.id);
611 (item, reference)
612 })
613 .collect();
614
615 let resolved_sort = sort_spec.resolve();
616 let citation_config = self.get_citation_config();
617 let sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
618 &self.locale,
619 &citation_config,
620 )
621 .with_citation_spec(spec);
622 let sorted =
623 sorter.sort_by_keys(items_with_refs, &resolved_sort.template, |item| item.1);
624
625 return sorted.into_iter().map(|(item, _reference)| item).collect();
626 }
627
628 items
629 }
630
631 pub fn calculate_hints(&self) -> HashMap<String, ProcHints> {
636 let citation_config = self.get_citation_config();
637 let config = citation_config.as_ref();
638 let bibliography_config = self.get_bibliography_config();
639 let bibliography_sort = self.resolved_bibliography_sort();
640
641 let mut disambiguator = if let Some((resolved_sort, _id_tiebreak)) = &bibliography_sort {
642 Disambiguator::with_group_sort(
643 &self.bibliography,
644 config,
645 &bibliography_config,
646 &self.locale,
647 resolved_sort,
648 )
649 } else {
650 Disambiguator::new(
651 &self.bibliography,
652 config,
653 &bibliography_config,
654 &self.locale,
655 )
656 };
657
658 if let Some(citation_spec) = self.style.citation.as_ref() {
659 disambiguator = disambiguator.with_citation_spec(citation_spec);
660 }
661 if let Some(bibliography_spec) = self.style.bibliography.as_ref() {
662 disambiguator = disambiguator.with_bibliography_spec(bibliography_spec);
663 }
664
665 disambiguator.calculate_hints()
666 }
667}
668
669fn effective_sort_partitioning(
670 bibliography_options: &BibliographyConfig,
671 bibliography_config: &Config,
672) -> Option<BibliographySortPartitioning> {
673 if let Some(partitioning) = &bibliography_options.sort_partitioning {
674 return Some(partitioning.clone());
675 }
676
677 bibliography_config
678 .sorting
679 .as_ref()
680 .is_some_and(|sorting| {
681 sorting.effective_multilingual() == SortingMultilingualMode::PerScript
682 })
683 .then(|| BibliographySortPartitioning {
684 by: BibliographyPartitionKind::Script,
685 mode: BibliographyPartitionMode::SortOnly,
686 order: Vec::new(),
687 headings: HashMap::new(),
688 unknown_fields: Default::default(),
689 })
690}