Skip to main content

citum_engine/values/
list.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Rendering logic for list components with configurable delimiters.
7//!
8//! This module handles rendering of lists of template items, with support for
9//! different delimiters between items (commas, semicolons, etc.) and rendering modes.
10
11use crate::reference::Reference;
12use crate::values::{ComponentValues, ProcHints, ProcValues, RenderOptions};
13use citum_schema::template::{DelimiterPunctuation, TemplateGroup};
14
15impl ComponentValues for TemplateGroup {
16    /// Returns the resolved list of component values for rendering.
17    fn values<F: crate::render::format::OutputFormat<Output = String>>(
18        &self,
19        reference: &Reference,
20        hints: &ProcHints,
21        options: &RenderOptions<'_>,
22    ) -> Option<ProcValues<F::Output>> {
23        let mut has_content = false;
24        let fmt = F::default();
25
26        // Collect values from all items, applying their rendering
27        let mut values = Vec::with_capacity(self.group.len());
28        for item in &self.group {
29            let Some(v) = item.values::<F>(reference, hints, options) else {
30                continue;
31            };
32            if v.value.is_empty() {
33                continue;
34            }
35
36            // Track if we have any "meaningful" content (not just a term)
37            if !is_term_based(item) {
38                has_content = true;
39            }
40
41            // Use the central rendering logic to apply global config, local settings, and overrides
42            let proc_item = crate::render::ProcTemplateComponent {
43                template_component: item.clone(),
44                template_index: options.current_template_index,
45                value: v.value,
46                prefix: v.prefix,
47                suffix: v.suffix,
48                url: v.url,
49                ref_type: Some(reference.ref_type().clone()),
50                config: Some(options.config.clone()),
51                bibliography_config: options.bibliography_config.clone(),
52                item_language: crate::values::effective_component_language(reference, item),
53                quote_marks: crate::render::format::QuoteMarks::from(options.locale),
54                sentence_initial: false,
55                pre_formatted: v.pre_formatted,
56            };
57
58            let rendered = crate::render::render_component_with_format_and_renderer::<F>(
59                &proc_item,
60                &fmt,
61                options.show_semantics,
62            );
63            if !rendered.is_empty() {
64                values.push(rendered);
65            }
66        }
67
68        if values.is_empty() || !has_content {
69            return None;
70        }
71
72        // Join with delimiter
73        let default_delimiter = DelimiterPunctuation::Comma;
74        let punctuation = self.delimiter.as_ref().unwrap_or(&default_delimiter);
75        let (script, realization) = crate::values::punctuation_realization_context(
76            crate::values::effective_item_language(reference).as_deref(),
77            options.config.multilingual.as_ref(),
78            options.locale.punctuation_realization.as_ref(),
79        );
80        let delimiter = crate::render::format::realize_punctuation(
81            punctuation,
82            script,
83            realization.as_deref(),
84            crate::render::format::PunctuationPosition::Separator,
85        );
86
87        let delimiter = if punctuation.is_semantic() {
88            fmt.text(&delimiter)
89        } else {
90            delimiter.into_owned()
91        };
92
93        Some(ProcValues {
94            value: fmt.join(values, &delimiter),
95            prefix: None,
96            suffix: None,
97            url: None,
98            substituted_key: None,
99            pre_formatted: true,
100        })
101    }
102}
103
104/// Returns true if the component value is derived from a locale term rather than a data field.
105fn is_term_based(component: &citum_schema::template::TemplateComponent) -> bool {
106    use citum_schema::template::TemplateComponent;
107    match component {
108        TemplateComponent::Term(_) => true,
109        TemplateComponent::Message(message) if message.message.starts_with("term.") => true,
110        TemplateComponent::Group(l) => l.group.iter().all(is_term_based),
111        _ => false,
112    }
113}