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 locale_grammar_is_authoritative(&self) -> bool {
57 !self.locale.resolved_by_fallback
58 }
59
60 fn raw_mapping_has_key(value: &serde_yaml::Value, key: &str) -> bool {
62 value
63 .as_mapping()
64 .is_some_and(|m| m.get(serde_yaml::Value::String(key.to_string())).is_some())
65 }
66
67 fn punctuation_in_quote_explicitly_authored(
88 &self,
89 scoped_raw: Option<&serde_yaml::Value>,
90 ) -> bool {
91 let leaf_authored = self
92 .style
93 .raw_yaml
94 .as_ref()
95 .and_then(|doc| doc.get("options"))
96 .is_some_and(|options| Self::raw_mapping_has_key(options, "punctuation-in-quote"));
97
98 let scope_authored =
99 scoped_raw.is_some_and(|raw| Self::raw_mapping_has_key(raw, "punctuation-in-quote"));
100
101 leaf_authored || scope_authored
102 }
103
104 fn resolve_punctuation_defaults(
106 &self,
107 config: &mut Config,
108 scoped_raw: Option<&serde_yaml::Value>,
109 ) {
110 if !self.locale_grammar_is_authoritative() {
111 return;
112 }
113
114 if !config.punctuation_in_quote
115 && !self.punctuation_in_quote_explicitly_authored(scoped_raw)
116 {
117 config.punctuation_in_quote = self.locale.grammar_options.punctuation_in_quote;
118 }
119
120 let punctuation = config
121 .punctuation
122 .get_or_insert_with(PunctuationConfig::default);
123 punctuation
124 .strong_terminal_comma_policy
125 .get_or_insert(self.locale.grammar_options.strong_terminal_comma_policy);
126 punctuation
127 .delimiter_suppressing_terminal_marks
128 .get_or_insert_with(|| {
129 self.locale
130 .grammar_options
131 .delimiter_suppressing_terminal_marks
132 .clone()
133 });
134 }
135
136 fn punctuation_defaults_require_resolution(
138 &self,
139 config: &Config,
140 scoped_raw: Option<&serde_yaml::Value>,
141 ) -> bool {
142 if !self.locale_grammar_is_authoritative() {
143 return false;
144 }
145
146 let punctuation_in_quote_is_unset = !config.punctuation_in_quote
147 && self.locale.grammar_options.punctuation_in_quote
148 && !self.punctuation_in_quote_explicitly_authored(scoped_raw);
149
150 let punctuation = config.punctuation.as_ref();
151 let policy_is_unset = punctuation
152 .and_then(|options| options.strong_terminal_comma_policy)
153 .is_none();
154 let marks_are_unset = punctuation
155 .and_then(|options| options.delimiter_suppressing_terminal_marks.as_ref())
156 .is_none();
157
158 punctuation_in_quote_is_unset
159 || (policy_is_unset
160 && self.locale.grammar_options.strong_terminal_comma_policy
161 != citum_schema::options::StrongTerminalCommaPolicy::default())
162 || (marks_are_unset
163 && self
164 .locale
165 .grammar_options
166 .delimiter_suppressing_terminal_marks
167 != "?!…")
168 }
169
170 fn with_punctuation_defaults<'a>(
172 &self,
173 config: std::borrow::Cow<'a, Config>,
174 scoped_raw: Option<&serde_yaml::Value>,
175 ) -> std::borrow::Cow<'a, Config> {
176 if !self.punctuation_defaults_require_resolution(&config, scoped_raw) {
177 return config;
178 }
179
180 let mut config = config.into_owned();
181 self.resolve_punctuation_defaults(&mut config, scoped_raw);
182 std::borrow::Cow::Owned(config)
183 }
184
185 fn build_processor(
189 style: Style,
190 bibliography: Bibliography,
191 locale: Locale,
192 compound_sets: IndexMap<String, Vec<String>>,
193 ) -> Self {
194 let style = style.into_resolved();
195 Self::build_processor_pre_resolved(style, bibliography, locale, compound_sets)
196 }
197
198 pub(super) fn build_processor_pre_resolved(
204 style: Style,
205 bibliography: Bibliography,
206 locale: Locale,
207 compound_sets: IndexMap<String, Vec<String>>,
208 ) -> Self {
209 let (compound_set_by_ref, compound_member_index) =
210 Self::build_compound_set_indexes(&compound_sets);
211 let mut processor = Processor {
212 style,
213 bibliography,
214 locale,
215 default_config: Config::default(),
216 hints: HashMap::new(),
217 compound_sets,
218 compound_set_by_ref,
219 compound_member_index,
220 show_semantics: true,
221 inject_ast_indices: false,
222 abbreviation_map: None,
223 };
224
225 processor.hints = processor.calculate_hints();
227 processor
228 }
229
230 fn try_validate_compound_sets(
232 bibliography: &Bibliography,
233 compound_sets: IndexMap<String, Vec<String>>,
234 ) -> Result<IndexMap<String, Vec<String>>, ProcessorError> {
235 super::validate_compound_sets(Some(compound_sets), bibliography)
236 .map(Option::unwrap_or_default)
237 }
238
239 fn validate_compound_sets_or_default(
241 bibliography: &Bibliography,
242 compound_sets: IndexMap<String, Vec<String>>,
243 ) -> IndexMap<String, Vec<String>> {
244 Self::try_validate_compound_sets(bibliography, compound_sets).unwrap_or_default()
245 }
246
247 fn build_compound_set_indexes(
252 sets: &IndexMap<String, Vec<String>>,
253 ) -> (HashMap<String, String>, HashMap<String, usize>) {
254 let mut by_ref = HashMap::new();
255 let mut member_index = HashMap::new();
256 for (set_id, members) in sets {
257 for (idx, member) in members.iter().enumerate() {
258 by_ref.insert(member.clone(), set_id.clone());
259 member_index.insert(member.clone(), idx);
260 }
261 }
262 (by_ref, member_index)
263 }
264
265 pub(crate) fn is_note_style(&self) -> bool {
267 self.get_config()
268 .processing
269 .as_ref()
270 .is_some_and(|processing| matches!(processing, citum_schema::options::Processing::Note))
271 }
272
273 fn is_numeric_style(&self) -> bool {
275 self.get_config()
276 .processing
277 .as_ref()
278 .is_some_and(|processing| {
279 matches!(processing, citum_schema::options::Processing::Numeric)
280 })
281 }
282
283 fn is_numeric_bibliography_style(&self) -> bool {
285 self.get_bibliography_config()
286 .processing
287 .as_ref()
288 .is_some_and(|processing| {
289 matches!(processing, citum_schema::options::Processing::Numeric)
290 })
291 }
292
293 fn resolved_bibliography_sort(&self) -> Option<(citum_schema::grouping::GroupSort, bool)> {
306 if let Some(sort_spec) = self
307 .style
308 .bibliography
309 .as_ref()
310 .and_then(|bibliography| bibliography.sort.as_ref())
311 {
312 return Some((sort_spec.resolve(), false));
313 }
314
315 let bibliography_config = self.get_bibliography_config();
316
317 if let Some(preset) = bibliography_config
318 .processing
319 .as_ref()
320 .and_then(citum_schema::options::Processing::default_bibliography_sort)
321 {
322 return Some((preset.group_sort(), false));
323 }
324
325 bibliography_config
326 .processing
327 .clone()
328 .unwrap_or_default()
329 .config()
330 .sort
331 .map(|sort_entry| (sort_entry.resolve().group_sort(), true))
332 }
333
334 pub(crate) fn initialize_numeric_citation_numbers(&self, run: &mut RunState) {
344 if !self.is_numeric_style() {
345 return;
346 }
347
348 self.initialize_numeric_numbers(run, self.sort_citation_number_order());
349 }
350
351 pub(crate) fn initialize_numeric_bibliography_numbers(&self, run: &mut RunState) {
353 if !self.is_numeric_bibliography_style() {
354 return;
355 }
356
357 self.initialize_numeric_numbers(run, self.sort_bibliography_number_order());
358 }
359
360 fn initialize_numeric_numbers(&self, run: &mut RunState, ordered_ids: Vec<String>) {
362 if !run
363 .citation_numbers
364 .read()
365 .unwrap_or_else(std::sync::PoisonError::into_inner)
366 .is_empty()
367 {
368 return;
369 }
370
371 self.initialize_numeric_citation_numbers_from_ordered_ids(run, ordered_ids);
372 }
373
374 fn sort_citation_number_order(&self) -> Vec<String> {
376 self.sort_references(self.bibliography.values().collect())
377 .into_iter()
378 .filter_map(citum_schema::reference::InputReference::id)
379 .map(String::from)
380 .collect()
381 }
382
383 fn sort_bibliography_number_order(&self) -> Vec<String> {
385 self.sort_references(self.bibliography.values().collect())
386 .into_iter()
387 .filter_map(citum_schema::reference::InputReference::id)
388 .map(String::from)
389 .collect()
390 }
391
392 fn initialize_numeric_citation_numbers_from_ordered_ids(
397 &self,
398 run: &mut RunState,
399 ordered_ids: Vec<String>,
400 ) {
401 let mut numbers = run
402 .citation_numbers
403 .write()
404 .unwrap_or_else(std::sync::PoisonError::into_inner);
405 if !numbers.is_empty() {
406 return;
407 }
408
409 let compound_config = self.get_bibliography_options().compound_numeric.clone();
410
411 if compound_config.is_some() {
412 let mut set_first_seen: IndexMap<String, usize> = IndexMap::new();
413 let mut current_number = 1usize;
414 run.compound_groups.clear();
415
416 for ref_id in &ordered_ids {
417 if let Some(set_id) = self.compound_set_by_ref.get(ref_id) {
418 if let Some(&number) = set_first_seen.get(set_id) {
419 numbers.insert(ref_id.clone(), number);
420 } else {
421 set_first_seen.insert(set_id.clone(), current_number);
422 if let Some(members) = self.compound_sets.get(set_id) {
423 let present_members: Vec<String> = members
424 .iter()
425 .filter(|id| self.bibliography.contains_key(*id))
426 .cloned()
427 .collect();
428 for member in &present_members {
429 numbers.insert(member.clone(), current_number);
430 }
431 if present_members.len() > 1 {
432 run.compound_groups.insert(current_number, present_members);
433 }
434 } else {
435 numbers.insert(ref_id.clone(), current_number);
436 }
437 current_number += 1;
438 }
439 } else if !numbers.contains_key(ref_id) {
440 numbers.insert(ref_id.clone(), current_number);
441 current_number += 1;
442 }
443 }
444 } else {
445 for (index, ref_id) in ordered_ids.into_iter().enumerate() {
446 numbers.insert(ref_id, index + 1);
447 }
448 }
449 }
450
451 #[must_use]
461 pub fn begin_run(&self) -> RunState {
462 let mut run = RunState::default();
463 self.initialize_numeric_citation_numbers(&mut run);
464 self.initialize_numeric_bibliography_numbers(&mut run);
465 run
466 }
467
468 #[must_use]
470 pub fn new(style: Style, bibliography: Bibliography) -> Self {
471 Self::with_compound_sets(style, bibliography, IndexMap::new())
472 }
473
474 pub fn try_with_compound_sets(
481 style: Style,
482 bibliography: Bibliography,
483 compound_sets: IndexMap<String, Vec<String>>,
484 ) -> Result<Self, ProcessorError> {
485 Self::try_with_locale_and_compound_sets(style, bibliography, Locale::en_us(), compound_sets)
486 }
487
488 #[must_use]
493 pub fn with_compound_sets(
494 style: Style,
495 bibliography: Bibliography,
496 compound_sets: IndexMap<String, Vec<String>>,
497 ) -> Self {
498 let validated_sets = Self::validate_compound_sets_or_default(&bibliography, compound_sets);
499 Self::build_processor(style, bibliography, Locale::en_us(), validated_sets)
500 }
501
502 #[must_use]
506 pub fn with_locale(style: Style, bibliography: Bibliography, locale: Locale) -> Self {
507 Self::with_locale_and_compound_sets(style, bibliography, locale, IndexMap::new())
508 }
509
510 pub fn try_with_locale_and_compound_sets(
518 style: Style,
519 bibliography: Bibliography,
520 locale: Locale,
521 compound_sets: IndexMap<String, Vec<String>>,
522 ) -> Result<Self, ProcessorError> {
523 let validated_sets = Self::try_validate_compound_sets(&bibliography, compound_sets)?;
524 Ok(Self::build_processor(
525 style,
526 bibliography,
527 locale,
528 validated_sets,
529 ))
530 }
531
532 #[must_use]
539 pub fn with_locale_and_compound_sets(
540 style: Style,
541 bibliography: Bibliography,
542 locale: Locale,
543 compound_sets: IndexMap<String, Vec<String>>,
544 ) -> Self {
545 let validated_sets = Self::validate_compound_sets_or_default(&bibliography, compound_sets);
546 Self::build_processor(style, bibliography, locale, validated_sets)
547 }
548
549 #[must_use]
554 pub fn with_style_locale(
555 style: Style,
556 bibliography: Bibliography,
557 locales_dir: &std::path::Path,
558 ) -> Self {
559 let style = style.into_resolved();
560 let locale = if let Some(ref locale_id) = style.info.default_locale {
561 Locale::load(locale_id, locales_dir)
562 } else {
563 Locale::en_us()
564 };
565 Self::with_locale_and_compound_sets(style, bibliography, locale, IndexMap::new())
566 }
567
568 #[must_use]
570 pub fn with_inject_ast_indices(mut self, inject_ast_indices: bool) -> Self {
571 self.inject_ast_indices = inject_ast_indices;
572 self
573 }
574
575 pub fn set_inject_ast_indices(&mut self, inject_ast_indices: bool) {
577 self.inject_ast_indices = inject_ast_indices;
578 }
579
580 pub fn get_config(&self) -> &Config {
582 self.style.options.as_ref().unwrap_or(&self.default_config)
583 }
584
585 pub fn get_citation_config(&self) -> std::borrow::Cow<'_, Config> {
590 let base = self.get_config();
591 let config = match self
592 .style
593 .citation
594 .as_ref()
595 .and_then(|citation| citation.options.as_ref())
596 {
597 Some(citation_options) => std::borrow::Cow::Owned(
598 citation_options
599 .merged_with_raw(base, self.style.scoped_raw_options.citation.as_ref()),
600 ),
601 None => std::borrow::Cow::Borrowed(base),
602 };
603 self.with_punctuation_defaults(config, self.style.scoped_raw_options.citation.as_ref())
604 }
605
606 pub fn get_bibliography_config(&self) -> std::borrow::Cow<'_, Config> {
611 let base = self.get_config();
612 let config = match self
613 .style
614 .bibliography
615 .as_ref()
616 .and_then(|bibliography| bibliography.options.as_ref())
617 {
618 Some(bibliography_options) => std::borrow::Cow::Owned(
619 bibliography_options
620 .merged_with_raw(base, self.style.scoped_raw_options.bibliography.as_ref()),
621 ),
622 None => std::borrow::Cow::Borrowed(base),
623 };
624 self.with_punctuation_defaults(config, self.style.scoped_raw_options.bibliography.as_ref())
625 }
626
627 pub fn get_bibliography_options(&self) -> std::borrow::Cow<'_, BibliographyConfig> {
629 match self
630 .style
631 .bibliography
632 .as_ref()
633 .and_then(|bibliography| bibliography.options.as_ref())
634 {
635 Some(bibliography_options) => {
636 std::borrow::Cow::Owned(bibliography_options.to_bibliography_config())
637 }
638 None => std::borrow::Cow::Owned(BibliographyConfig::default()),
639 }
640 }
641
642 pub fn sort_references<'a>(&self, references: Vec<&'a Reference>) -> Vec<&'a Reference> {
646 let bibliography_config = self.get_bibliography_config();
647 let mut sorted_refs = match self.resolved_bibliography_sort() {
648 Some((sort_spec, true)) => {
649 let mut sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
650 &self.locale,
651 &bibliography_config,
652 );
653 if let Some(spec) = self.style.bibliography.as_ref() {
654 sorter = sorter.with_bibliography_spec(spec);
655 }
656 sorter.sort_references_with_id_tiebreak(references, &sort_spec)
657 }
658 Some((sort_spec, false)) => {
659 let mut sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
660 &self.locale,
661 &bibliography_config,
662 );
663 if let Some(spec) = self.style.bibliography.as_ref() {
664 sorter = sorter.with_bibliography_spec(spec);
665 }
666 sorter.sort_references(references, &sort_spec)
667 }
668 None => references,
669 };
670
671 let bibliography_options = self.get_bibliography_options();
672 if let Some(partitioning) =
673 effective_sort_partitioning(&bibliography_options, &bibliography_config).as_ref()
674 && crate::sort_partitioning::should_sort_flat(partitioning)
675 {
676 crate::sort_partitioning::sort_by_partition(
677 sorted_refs.as_mut_slice(),
678 &self.locale,
679 partitioning,
680 );
681 }
682
683 sorted_refs
684 }
685
686 pub fn sort_citation_items(
688 &self,
689 items: Vec<CitationItem>,
690 spec: &citum_schema::CitationSpec,
691 ) -> Vec<CitationItem> {
692 if let Some(sort_spec) = &spec.sort {
693 let items_with_refs: Vec<(CitationItem, Option<&Reference>)> = items
694 .into_iter()
695 .map(|item| {
696 let reference = self.bibliography.get(&item.id);
697 (item, reference)
698 })
699 .collect();
700
701 let resolved_sort = sort_spec.resolve();
702 let citation_config = self.get_citation_config();
703 let sorter = crate::sorting::ReferenceSorter::with_bibliography_config(
704 &self.locale,
705 &citation_config,
706 )
707 .with_citation_spec(spec);
708 let sorted =
709 sorter.sort_by_keys(items_with_refs, &resolved_sort.template, |item| item.1);
710
711 return sorted.into_iter().map(|(item, _reference)| item).collect();
712 }
713
714 items
715 }
716
717 pub fn calculate_hints(&self) -> HashMap<String, ProcHints> {
722 let citation_config = self.get_citation_config();
723 let config = citation_config.as_ref();
724 let bibliography_config = self.get_bibliography_config();
725 let bibliography_sort = self.resolved_bibliography_sort();
726
727 let mut disambiguator = if let Some((resolved_sort, id_tiebreak)) = &bibliography_sort {
728 Disambiguator::with_group_sort(
729 &self.bibliography,
730 config,
731 &bibliography_config,
732 &self.locale,
733 resolved_sort,
734 )
735 .with_id_tiebreak(*id_tiebreak)
736 } else {
737 Disambiguator::new(
738 &self.bibliography,
739 config,
740 &bibliography_config,
741 &self.locale,
742 )
743 };
744
745 if let Some(citation_spec) = self.style.citation.as_ref() {
746 disambiguator = disambiguator.with_citation_spec(citation_spec);
747 }
748 if let Some(bibliography_spec) = self.style.bibliography.as_ref() {
749 disambiguator = disambiguator.with_bibliography_spec(bibliography_spec);
750 }
751
752 disambiguator.calculate_hints()
753 }
754}
755
756fn effective_sort_partitioning(
757 bibliography_options: &BibliographyConfig,
758 bibliography_config: &Config,
759) -> Option<BibliographySortPartitioning> {
760 if let Some(partitioning) = &bibliography_options.sort_partitioning {
761 return Some(partitioning.clone());
762 }
763
764 bibliography_config
765 .sorting
766 .as_ref()
767 .is_some_and(|sorting| {
768 sorting.effective_multilingual() == SortingMultilingualMode::PerScript
769 })
770 .then(|| BibliographySortPartitioning {
771 by: BibliographyPartitionKind::Script,
772 mode: BibliographyPartitionMode::SortOnly,
773 order: Vec::new(),
774 headings: HashMap::new(),
775 unknown_fields: Default::default(),
776 })
777}