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::locale::Locale;
15use citum_schema::options::{Config, Substitute};
16use std::borrow::Cow;
17
18/// Matcher for determining if references share the same primary contributors.
19///
20/// Uses the style's substitution configuration to determine which contributor
21/// (author, editor, translator) should be used for comparison.
22pub struct Matcher<'a> {
23 /// The active citation style.
24 style: &'a Style,
25 /// The effective style configuration used for name resolution and fallbacks.
26 config: &'a Config,
27 /// The locale used to resolve multilingual and merged-list names.
28 locale: &'a Locale,
29}
30
31impl<'a> Matcher<'a> {
32 /// Build a matcher from the active style, effective configuration, and locale.
33 #[must_use]
34 pub fn new(style: &'a Style, config: &'a Config, locale: &'a Locale) -> Self {
35 Self {
36 style,
37 config,
38 locale,
39 }
40 }
41
42 /// Check if primary contributors (authors/editors) match between two references.
43 ///
44 /// Delegates to the shared effective-primary resolver
45 /// ([`crate::values::contributor::substitute::effective_primary_names`]) so
46 /// matching honors type overrides and merged-role candidates identically to
47 /// rendering, sorting, and disambiguation. Two references match only when
48 /// both resolve non-empty, equal name lists; a title-substitute result
49 /// (empty names) never matches.
50 #[must_use]
51 pub fn contributors_match(&self, prev: &Reference, current: &Reference) -> bool {
52 let substitute = self.get_substitute_config();
53 let prev_names = crate::values::contributor::substitute::effective_primary_names(
54 prev,
55 substitute.as_ref(),
56 self.config,
57 self.locale,
58 );
59 let curr_names = crate::values::contributor::substitute::effective_primary_names(
60 current,
61 substitute.as_ref(),
62 self.config,
63 self.locale,
64 );
65 !prev_names.is_empty() && !curr_names.is_empty() && prev_names == curr_names
66 }
67
68 /// Gets the substitute configuration from the style or falls back to defaults.
69 ///
70 /// Resolves the substitute template from the style's options if available,
71 /// otherwise falls back to the default configuration's substitute settings.
72 fn get_substitute_config(&self) -> Cow<'_, Substitute> {
73 if let Some(config) = self
74 .style
75 .options
76 .as_ref()
77 .and_then(|o| o.substitute.as_ref())
78 {
79 return config.resolve_ref();
80 }
81 self.config.substitute.as_ref().map_or_else(
82 || Cow::Owned(Substitute::default()),
83 citum_schema::options::SubstituteConfig::resolve_ref,
84 )
85 }
86}