Skip to main content

citum_engine/values/
message.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 locale-authored message components.
7//!
8//! A message component resolves each named argument through the normal template
9//! component renderer, then asks the active locale to evaluate the selected
10//! message ID.
11
12use crate::reference::Reference;
13use crate::values::{ComponentValues, ProcHints, ProcValues, RenderOptions};
14use citum_schema::locale::{MessageArgs, MessageEvaluator, Mf2MessageEvaluator};
15use citum_schema::template::{MessageArgSource, TemplateMessage};
16use std::collections::HashMap;
17
18/// Resolve a zero-argument template message through the active config and locale.
19///
20/// Shared with date-slot disambiguation so message candidates are skipped when
21/// unresolved and use the same form, strip-periods, and text-case semantics as
22/// rendering.
23pub(crate) fn resolve_template_message_value(
24    component: &TemplateMessage,
25    config: &citum_schema::options::Config,
26    locale: &citum_schema::locale::Locale,
27) -> Option<String> {
28    let args = MessageArgs::default();
29    let mut value = if let Some(pattern) = config.messages.get(&component.message) {
30        Mf2MessageEvaluator.evaluate(pattern, &args)?
31    } else {
32        locale.resolve_template_message(
33            &component.message,
34            &args,
35            component.form.as_ref(),
36            component.gender.clone(),
37        )?
38    };
39
40    if component
41        .rendering
42        .strip_periods
43        .or(config.strip_periods)
44        .unwrap_or(false)
45    {
46        value = crate::values::strip_trailing_periods(&value);
47    }
48
49    if let Some(text_case) = component.rendering.text_case {
50        value = crate::values::text_case::apply_text_case_with_language(
51            &value,
52            text_case,
53            Some(locale.locale.as_str()),
54        );
55    }
56
57    (!value.trim().is_empty()).then_some(value)
58}
59
60impl ComponentValues for TemplateMessage {
61    fn values<F: crate::render::format::OutputFormat<Output = String>>(
62        &self,
63        reference: &Reference,
64        hints: &ProcHints,
65        options: &RenderOptions<'_>,
66    ) -> Option<ProcValues<F::Output>> {
67        if self.args.is_empty() {
68            let value =
69                resolve_template_message_value(self, options.config.as_ref(), options.locale)?;
70            let term_backed = self.message.starts_with("term.");
71            return Some(ProcValues {
72                value,
73                pre_formatted: !term_backed,
74                ..Default::default()
75            });
76        }
77
78        let mut named = HashMap::with_capacity(self.args.len());
79
80        for (name, source) in &self.args {
81            let value = render_message_arg::<F>(source, reference, hints, options)?;
82            if value.trim().is_empty() {
83                return None;
84            }
85            named.insert(name.clone(), value);
86        }
87
88        let args = MessageArgs {
89            named,
90            ..MessageArgs::default()
91        };
92        let mut value = if let Some(pattern) = options.config.messages.get(&self.message) {
93            Mf2MessageEvaluator.evaluate(pattern, &args)?
94        } else {
95            options.locale.resolve_template_message(
96                &self.message,
97                &args,
98                self.form.as_ref(),
99                self.gender.clone(),
100            )?
101        };
102
103        if crate::values::should_strip_periods(&self.rendering, options) {
104            value = crate::values::strip_trailing_periods(&value);
105        }
106
107        if let Some(tc) = self.rendering.text_case {
108            value = crate::values::text_case::apply_text_case_with_language(
109                &value,
110                tc,
111                Some(options.locale.locale.as_str()),
112            );
113        }
114
115        if value.trim().is_empty() {
116            return None;
117        }
118        let term_backed = self.message.starts_with("term.");
119
120        Some(ProcValues {
121            value,
122            pre_formatted: !term_backed,
123            ..Default::default()
124        })
125    }
126}
127
128fn render_message_arg<F: crate::render::format::OutputFormat<Output = String>>(
129    source: &MessageArgSource,
130    reference: &Reference,
131    hints: &ProcHints,
132    options: &RenderOptions<'_>,
133) -> Option<String> {
134    if let MessageArgSource::Literal { literal } = source {
135        return Some(literal.clone());
136    }
137    if let MessageArgSource::ReferenceType { .. } = source {
138        return Some(reference.ref_type());
139    }
140    if let MessageArgSource::Carrier { carrier } = source {
141        return Some(reference.medium().unwrap_or_else(|| {
142            let online = reference.url().is_some()
143                || reference.doi().is_some()
144                || reference.identifier("cstr").is_some();
145            if online {
146                carrier.online.clone()
147            } else {
148                carrier.absent.clone()
149            }
150        }));
151    }
152
153    let component = source.as_template_component()?;
154    if component.rendering().suppress == Some(true) {
155        return None;
156    }
157
158    let values = component.values::<F>(reference, hints, options)?;
159    if values.value.trim().is_empty() {
160        return None;
161    }
162
163    let fmt = F::default();
164    let proc_item = crate::render::ProcTemplateComponent {
165        template_component: component.clone(),
166        template_index: options.current_template_index,
167        value: values.value,
168        prefix: values.prefix,
169        suffix: values.suffix,
170        url: values.url,
171        ref_type: Some(reference.ref_type().clone()),
172        config: Some(options.config.clone()),
173        bibliography_config: options.bibliography_config.clone(),
174        item_language: crate::values::effective_component_language(reference, &component),
175        quote_marks: crate::render::format::QuoteMarks::from(options.locale),
176        sentence_initial: false,
177        pre_formatted: values.pre_formatted,
178    };
179
180    let rendered = crate::render::render_component_with_format_and_renderer::<F>(
181        &proc_item,
182        &fmt,
183        options.show_semantics,
184    );
185    if rendered.trim().is_empty() {
186        None
187    } else {
188        Some(rendered)
189    }
190}