citum_engine/processor/matching.rs
1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Matching logic for determining if references share primary contributors.
7//!
8//! This module implements contributor matching according to substitution rules,
9//! allowing comparison of references (particularly for "ibid" tracking) based on author,
10//! editor, translator, or title fallback logic.
11
12use crate::reference::Reference;
13use citum_schema::Style;
14use citum_schema::options::{Config, Substitute, SubstituteKey};
15
16/// Matcher for determining if references share the same primary contributors.
17///
18/// Uses the style's substitution configuration to determine which contributor
19/// (author, editor, translator) should be used for comparison.
20pub struct Matcher<'a> {
21 /// The active citation style.
22 style: &'a Style,
23 /// The default configuration used as a fallback.
24 default_config: &'a Config,
25}
26
27impl<'a> Matcher<'a> {
28 /// Build a matcher from the active style and default configuration.
29 #[must_use]
30 pub fn new(style: &'a Style, default_config: &'a Config) -> Self {
31 Self {
32 style,
33 default_config,
34 }
35 }
36
37 /// Check if primary contributors (authors/editors) match between two references.
38 /// Uses the style's substitution logic to determine the primary contributor.
39 #[must_use]
40 pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
41 let substitute = self.get_substitute_config();
42 let prev_contributors = self.get_primary_contributors(prev, &substitute);
43 let curr_contributors = self.get_primary_contributors(current, &substitute);
44
45 match (prev_contributors, curr_contributors) {
46 (Some(p), Some(c)) => p == c,
47 _ => false,
48 }
49 }
50
51 /// Gets the substitute configuration from the style or falls back to defaults.
52 ///
53 /// Resolves the substitute template from the style's options if available,
54 /// otherwise falls back to the default configuration's substitute settings.
55 fn get_substitute_config(&self) -> Substitute {
56 self.style
57 .options
58 .as_ref()
59 .and_then(|o| o.substitute.as_ref())
60 .map(citum_schema::options::SubstituteConfig::resolve)
61 .or_else(|| {
62 self.default_config
63 .substitute
64 .as_ref()
65 .map(citum_schema::options::SubstituteConfig::resolve)
66 })
67 .unwrap_or_default()
68 }
69
70 /// Get the primary contributors for a reference based on the style's substitution order.
71 /// Follows the substitute template: Author is always first, then the configured fallbacks.
72 fn get_primary_contributors(
73 &self,
74 reference: &Reference,
75 substitute: &Substitute,
76 ) -> Option<crate::reference::Contributor> {
77 // Author is always the primary contributor
78 if let Some(author) = reference.author() {
79 return Some(author);
80 }
81
82 // Fall back through the substitute template order
83 for key in &substitute.template {
84 let contributor = match key {
85 SubstituteKey::Editor => reference.editor(),
86 SubstituteKey::Translator => reference.translator(),
87 SubstituteKey::Title => None, // Title is not a contributor
88 };
89 if contributor.is_some() {
90 return contributor;
91 }
92 }
93
94 None
95 }
96}