citum-engine 0.60.0

Citum citation and bibliography processor
Documentation
/*
SPDX-License-Identifier: MIT OR Apache-2.0
SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
*/

//! Matching logic for determining if references share primary contributors.
//!
//! This module implements contributor matching according to substitution rules,
//! allowing comparison of references (particularly for "ibid" tracking) based on author,
//! editor, translator, or title fallback logic.

use crate::reference::Reference;
use citum_schema::Style;
use citum_schema::options::{Config, Substitute, SubstituteKey};

/// Matcher for determining if references share the same primary contributors.
///
/// Uses the style's substitution configuration to determine which contributor
/// (author, editor, translator) should be used for comparison.
pub struct Matcher<'a> {
    /// The active citation style.
    style: &'a Style,
    /// The default configuration used as a fallback.
    default_config: &'a Config,
}

impl<'a> Matcher<'a> {
    /// Build a matcher from the active style and default configuration.
    #[must_use]
    pub fn new(style: &'a Style, default_config: &'a Config) -> Self {
        Self {
            style,
            default_config,
        }
    }

    /// Check if primary contributors (authors/editors) match between two references.
    /// Uses the style's substitution logic to determine the primary contributor.
    #[must_use]
    pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
        let substitute = self.get_substitute_config();
        let prev_contributors = self.get_primary_contributors(prev, &substitute);
        let curr_contributors = self.get_primary_contributors(current, &substitute);

        match (prev_contributors, curr_contributors) {
            (Some(p), Some(c)) => p == c,
            _ => false,
        }
    }

    /// Gets the substitute configuration from the style or falls back to defaults.
    ///
    /// Resolves the substitute template from the style's options if available,
    /// otherwise falls back to the default configuration's substitute settings.
    fn get_substitute_config(&self) -> Substitute {
        self.style
            .options
            .as_ref()
            .and_then(|o| o.substitute.as_ref())
            .map(citum_schema::options::SubstituteConfig::resolve)
            .or_else(|| {
                self.default_config
                    .substitute
                    .as_ref()
                    .map(citum_schema::options::SubstituteConfig::resolve)
            })
            .unwrap_or_default()
    }

    /// Get the primary contributors for a reference based on the style's substitution order.
    /// Follows the substitute template: Author is always first, then the configured fallbacks.
    fn get_primary_contributors(
        &self,
        reference: &Reference,
        substitute: &Substitute,
    ) -> Option<crate::reference::Contributor> {
        // Author is always the primary contributor
        if let Some(author) = reference.author() {
            return Some(author);
        }

        // Fall back through the substitute template order
        for key in &substitute.template {
            let contributor = match key {
                SubstituteKey::Editor => reference.editor(),
                SubstituteKey::Translator => reference.translator(),
                SubstituteKey::Title => None, // Title is not a contributor
            };
            if contributor.is_some() {
                return contributor;
            }
        }

        None
    }
}