Skip to main content

citum_engine/processor/
matching.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus
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}