citum-engine 0.76.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
*/

use super::TemplateComponent;

pub fn strip_author_component(component: &TemplateComponent) -> Option<TemplateComponent> {
    match component {
        TemplateComponent::Contributor(c)
            if c.contributor == citum_schema::template::ContributorRole::Author =>
        {
            None
        }
        TemplateComponent::Group(list) => {
            let filtered_items: Vec<TemplateComponent> = list
                .group
                .iter()
                .filter_map(strip_author_component)
                .collect();

            if filtered_items.is_empty() {
                None
            } else {
                let mut filtered_list = list.clone();
                filtered_list.group = filtered_items;
                Some(TemplateComponent::Group(filtered_list))
            }
        }
        _ => Some(component.clone()),
    }
}

/// Extract the leading affix used to separate grouped authors from item details.
pub fn leading_group_affix(component: &TemplateComponent) -> Option<String> {
    let r = component.rendering();
    let own_affix = r
        .prefix
        .clone()
        .or_else(|| r.wrap.as_ref().and_then(|w| w.inner_prefix.clone()))
        .or_else(|| {
            if let TemplateComponent::Group(inner) = component {
                inner.group.first().and_then(leading_group_affix)
            } else {
                None
            }
        });

    own_affix.filter(|value| !value.is_empty())
}

/// Remove leading affixes from the first surviving grouped-citation component.
///
/// When the author component is stripped from an author-date template, the next
/// component often carries a prefix like `", "` that only makes sense when the
/// author is still present. Grouped citation assembly adds the author/date
/// delimiter separately, so the first surviving component must start "clean".
pub fn strip_leading_group_affixes(component: &mut TemplateComponent) {
    let r = component.rendering_mut();
    r.prefix = None;
    if let Some(ref mut wrap_config) = r.wrap {
        wrap_config.inner_prefix = None;
    }
    if let TemplateComponent::Group(inner) = component
        && let Some(first) = inner.group.first_mut()
    {
        strip_leading_group_affixes(first);
    }
}

/// Finds a grouping component (contributor or title) within a template.
///
/// Descends into lists to find the first semantically relevant component
/// for grouping citations by author or title.
pub fn find_grouping_component(component: &TemplateComponent) -> Option<&TemplateComponent> {
    match component {
        TemplateComponent::Contributor(_) | TemplateComponent::Title(_) => Some(component),
        TemplateComponent::Group(list) => list.group.iter().find_map(find_grouping_component),
        _ => None,
    }
}

pub fn has_contributor_component(component: &TemplateComponent) -> bool {
    match component {
        TemplateComponent::Contributor(_) => true,
        TemplateComponent::Group(list) => list.group.iter().any(has_contributor_component),
        _ => false,
    }
}

/// Remove the first contributor component with `role` from `component`,
/// descending into groups the same way [`find_grouping_component`] does.
///
/// Returns the component with the match removed (`None` when nothing
/// remains) and whether a match was removed. Used to keep grouped-citation
/// item parts symmetric with the author part when a citation template
/// leads with a non-author contributor (the grouping component).
pub fn remove_first_contributor_with_role(
    component: TemplateComponent,
    role: &citum_schema::template::ContributorRoles,
) -> (Option<TemplateComponent>, bool) {
    match component {
        TemplateComponent::Contributor(ref c) if &c.contributor == role => (None, true),
        TemplateComponent::Group(mut list) => {
            let mut removed = false;
            let mut kept = Vec::with_capacity(list.group.len());
            for child in list.group.drain(..) {
                if removed {
                    kept.push(child);
                    continue;
                }
                let (remaining, child_removed) = remove_first_contributor_with_role(child, role);
                removed = child_removed;
                if let Some(remaining) = remaining {
                    kept.push(remaining);
                }
            }
            if kept.is_empty() {
                (None, removed)
            } else {
                list.group = kept;
                (Some(TemplateComponent::Group(list)), removed)
            }
        }
        other => (Some(other), false),
    }
}