citum-engine 0.79.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;
use crate::render::format::{OutputFormat, PunctuationPosition, realize_punctuation};
use crate::values::ScriptClass;
use citum_schema::options::PunctuationRealization;

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<F>(
    component: &TemplateComponent,
    script: ScriptClass,
    realization: Option<&PunctuationRealization>,
    fmt: &F,
) -> Option<String>
where
    F: OutputFormat<Output = String>,
{
    let r = component.rendering();
    let own_affix = r
        .prefix
        .as_ref()
        .map(|punctuation| {
            let realized = realize_punctuation(
                punctuation,
                script,
                realization,
                PunctuationPosition::Prefix,
            );
            if punctuation.is_semantic() {
                fmt.text(&realized)
            } else {
                realized.into_owned()
            }
        })
        .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(|first| leading_group_affix(first, script, realization, fmt))
            } 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),
    }
}