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
18impl ComponentValues for TemplateMessage {
19    fn values<F: crate::render::format::OutputFormat<Output = String>>(
20        &self,
21        reference: &Reference,
22        hints: &ProcHints,
23        options: &RenderOptions<'_>,
24    ) -> Option<ProcValues<F::Output>> {
25        let mut named = HashMap::with_capacity(self.args.len());
26
27        for (name, source) in &self.args {
28            let value = render_message_arg::<F>(source, reference, hints, options)?;
29            if value.trim().is_empty() {
30                return None;
31            }
32            named.insert(name.clone(), value);
33        }
34
35        let args = MessageArgs {
36            named,
37            ..MessageArgs::default()
38        };
39        let mut value = if let Some(pattern) = options.config.messages.get(&self.message) {
40            Mf2MessageEvaluator.evaluate(pattern, &args)?
41        } else {
42            options.locale.resolve_template_message(
43                &self.message,
44                &args,
45                self.form.as_ref(),
46                self.gender.clone(),
47            )?
48        };
49
50        if crate::values::should_strip_periods(&self.rendering, options) {
51            value = crate::values::strip_trailing_periods(&value);
52        }
53
54        if let Some(tc) = self.rendering.text_case {
55            value = crate::values::text_case::apply_text_case_with_language(
56                &value,
57                tc,
58                Some(options.locale.locale.as_str()),
59            );
60        }
61
62        if value.trim().is_empty() {
63            return None;
64        }
65        let term_backed = self.message.starts_with("term.");
66
67        Some(ProcValues {
68            value,
69            pre_formatted: !term_backed,
70            ..Default::default()
71        })
72    }
73}
74
75fn render_message_arg<F: crate::render::format::OutputFormat<Output = String>>(
76    source: &MessageArgSource,
77    reference: &Reference,
78    hints: &ProcHints,
79    options: &RenderOptions<'_>,
80) -> Option<String> {
81    if let MessageArgSource::Literal { literal } = source {
82        return Some(literal.clone());
83    }
84    if let MessageArgSource::ReferenceType { .. } = source {
85        return Some(reference.ref_type());
86    }
87    if let MessageArgSource::Carrier { carrier } = source {
88        return Some(reference.medium().unwrap_or_else(|| {
89            let online = reference.url().is_some()
90                || reference.doi().is_some()
91                || reference.identifier("cstr").is_some();
92            if online {
93                carrier.online.clone()
94            } else {
95                carrier.absent.clone()
96            }
97        }));
98    }
99
100    let component = source.as_template_component()?;
101    if component.rendering().suppress == Some(true) {
102        return None;
103    }
104
105    let values = component.values::<F>(reference, hints, options)?;
106    if values.value.trim().is_empty() {
107        return None;
108    }
109
110    let fmt = F::default();
111    let proc_item = crate::render::ProcTemplateComponent {
112        template_component: component.clone(),
113        template_index: options.current_template_index,
114        value: values.value,
115        prefix: values.prefix,
116        suffix: values.suffix,
117        url: values.url,
118        ref_type: Some(reference.ref_type().clone()),
119        config: Some(options.config.clone()),
120        bibliography_config: options.bibliography_config.clone(),
121        item_language: crate::values::effective_component_language(reference, &component),
122        quote_marks: crate::render::format::QuoteMarks::from(options.locale),
123        sentence_initial: false,
124        pre_formatted: values.pre_formatted,
125        label_only: false,
126    };
127
128    let rendered = crate::render::render_component_with_format_and_renderer::<F>(
129        &proc_item,
130        &fmt,
131        options.show_semantics,
132    );
133    if rendered.trim().is_empty() {
134        None
135    } else {
136        Some(rendered)
137    }
138}