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                label_only: false,
57            };
58
59            let rendered = crate::render::render_component_with_format_and_renderer::<F>(
60                &proc_item,
61                &fmt,
62                options.show_semantics,
63            );
64            if !rendered.is_empty() {
65                values.push(rendered);
66            }
67        }
68
69        if values.is_empty() || !has_content {
70            return None;
71        }
72
73        // Join with delimiter
74        let default_delimiter = DelimiterPunctuation::Comma;
75        let punctuation = self.delimiter.as_ref().unwrap_or(&default_delimiter);
76        let (script, realization) = crate::values::punctuation_realization_context(
77            crate::values::effective_item_language(reference).as_deref(),
78            options.config.multilingual.as_ref(),
79            options.locale.punctuation_realization.as_ref(),
80        );
81        let delimiter = crate::render::format::realize_punctuation(
82            punctuation,
83            script,
84            realization.as_deref(),
85            crate::render::format::PunctuationPosition::Separator,
86        );
87
88        let delimiter = if punctuation.is_semantic() {
89            fmt.text(&delimiter)
90        } else {
91            delimiter.into_owned()
92        };
93        // `fmt.join` may apply format-specific escaping to the delimiter itself
94        // (e.g. LaTeX special characters); joining two empty strings surfaces
95        // that transform on `delimiter` alone, so the boundary-aware join below
96        // sees the same delimiter text `fmt.join` would have inserted.
97        let escaped_delimiter = fmt.join(vec![String::new(), String::new()], &delimiter);
98        let escaped_delimiter =
99            crate::render::format::RealizedPunctuation::new(escaped_delimiter.into());
100
101        let close_quote = crate::render::format::QuoteMarks::from(options.locale).close;
102        let joined = crate::render::punctuation::join_with_quote_movement(
103            values,
104            &escaped_delimiter,
105            options.config.punctuation_in_quote,
106            &close_quote,
107        );
108
109        Some(ProcValues {
110            value: joined,
111            prefix: None,
112            suffix: None,
113            url: None,
114            substituted_key: None,
115            pre_formatted: true,
116        })
117    }
118}
119
120/// Returns true if the component value is derived from a locale term rather than a data field.
121fn is_term_based(component: &citum_schema::template::TemplateComponent) -> bool {
122    use citum_schema::template::TemplateComponent;
123    match component {
124        TemplateComponent::Term(_) => true,
125        TemplateComponent::Message(message) if message.message.starts_with("term.") => true,
126        TemplateComponent::Group(l) => l.group.iter().all(is_term_based),
127        _ => false,
128    }
129}