citum-engine 0.55.0

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

//! Sentence-initial context handling for bibliography entries and note-style
//! citation prefixes. Capitalizes the first word (or role label) of a
//! component when it appears at the start of an entry or note prefix.

use super::super::Renderer;
use crate::render::ProcTemplateComponent;
use crate::render::bibliography::{append_rendered_component, component_starts_new_sentence};
use crate::render::component::render_component_with_format;
use crate::values::RenderContext;
use citum_schema::NoteStartTextCase;
use citum_schema::locale::GeneralTerm;
use citum_schema::options::titles::TextCase;
use citum_schema::template::TemplateComponent;

impl Renderer<'_> {
    pub(super) fn apply_sentence_initial_context<F>(
        &self,
        components: &mut [ProcTemplateComponent],
        context: RenderContext,
        note_start_text_case: Option<NoteStartTextCase>,
    ) where
        F: crate::render::format::OutputFormat<Output = String>,
    {
        match context {
            RenderContext::Bibliography => {
                self.apply_bibliography_sentence_initial_context::<F>(components);
            }
            RenderContext::Citation => {
                self.apply_note_start_sentence_initial_context(components, note_start_text_case);
            }
        }
    }

    fn apply_bibliography_sentence_initial_context<F>(
        &self,
        components: &mut [ProcTemplateComponent],
    ) where
        F: crate::render::format::OutputFormat<Output = String>,
    {
        let punctuation_in_quote = components
            .first()
            .and_then(|component| component.config.as_ref())
            .is_some_and(|config| config.punctuation_in_quote);
        let default_separator = components
            .first()
            .and_then(|component| component.bibliography_config.as_ref())
            .and_then(|bib| bib.separator.as_deref())
            .unwrap_or(". ")
            .to_string();

        let mut entry_output = String::new();
        for component in components.iter_mut() {
            let rendered = render_component_with_format::<F>(component);
            if rendered.is_empty() {
                continue;
            }

            if component_starts_new_sentence(
                &entry_output,
                &rendered,
                &default_separator,
                punctuation_in_quote,
            ) {
                component.sentence_initial = true;
                self.apply_sentence_initial_transform(component, None);
            }

            let rendered = render_component_with_format::<F>(component);
            append_rendered_component(
                &mut entry_output,
                &rendered,
                &default_separator,
                punctuation_in_quote,
            );
        }
    }

    fn apply_note_start_sentence_initial_context(
        &self,
        components: &mut [ProcTemplateComponent],
        note_start_text_case: Option<NoteStartTextCase>,
    ) {
        let Some(text_case) = note_start_text_case else {
            return;
        };

        for component in components.iter_mut() {
            if !self.is_note_start_term_component(component) {
                continue;
            }

            component.sentence_initial = true;
            self.apply_sentence_initial_transform(component, Some(text_case));
            break;
        }
    }

    fn apply_sentence_initial_transform(
        &self,
        component: &mut ProcTemplateComponent,
        note_start_text_case: Option<NoteStartTextCase>,
    ) {
        if !component.sentence_initial {
            return;
        }

        let locale = Some(self.locale.locale.as_str());
        match &component.template_component {
            TemplateComponent::Contributor(_) => {
                let case =
                    crate::values::text_case::resolve_text_case(TextCase::CapitalizeFirst, locale);
                if let Some(prefix) = component.prefix.as_mut() {
                    // Explicit template prefix (e.g. ". Translated by ") — capitalize it.
                    *prefix = crate::values::text_case::apply_text_case(prefix, case);
                } else {
                    // No explicit prefix: the role label (e.g. "edited by ") is baked
                    // into the rendered value.  Capitalize the first word so that
                    // sentence-initial contributors read "Edited by …" not "edited by …".
                    component.value =
                        crate::values::text_case::apply_text_case(&component.value, case);
                }
            }
            // Pre-formatted group components — capitalize the first word of the
            // rendered group value (e.g. "edited by Smith" as first child).
            TemplateComponent::Group(_) => {
                let case =
                    crate::values::text_case::resolve_text_case(TextCase::CapitalizeFirst, locale);
                component.value = crate::values::text_case::apply_text_case(&component.value, case);
            }
            TemplateComponent::Term(_) if self.is_note_start_term_component(component) => {
                if let Some(case) = note_start_text_case {
                    component.value = crate::values::text_case::apply_note_start_text_case(
                        &component.value,
                        case,
                        locale,
                    );
                }
            }
            _ => {}
        }
    }

    fn is_note_start_term_component(&self, component: &ProcTemplateComponent) -> bool {
        matches!(
            &component.template_component,
            TemplateComponent::Term(term) if term.term == GeneralTerm::Ibid
        )
    }
}