1use super::*;
2#[cfg(test)]
3use crate::runtime::escape_html_text;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct TrustedHtml(String);
7
8impl TrustedHtml {
9 pub fn new(value: impl Into<String>) -> Result<Self, TemplateModelError> {
10 Ok(Self(require_non_empty("trusted_html", value.into())?))
11 }
12
13 pub fn as_str(&self) -> &str {
14 &self.0
15 }
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum RenderValue {
20 Text(String),
21 TrustedHtml(TrustedHtml),
22 Bool(bool),
23 List(Vec<RenderModel>),
24 Object(RenderModel),
25}
26
27impl RenderValue {
28 pub fn text(value: impl Into<String>) -> Self {
29 Self::Text(value.into())
30 }
31
32 pub fn trusted_html(value: TrustedHtml) -> Self {
33 Self::TrustedHtml(value)
34 }
35
36 pub fn bool(value: bool) -> Self {
37 Self::Bool(value)
38 }
39
40 pub fn list(value: Vec<RenderModel>) -> Self {
41 Self::List(value)
42 }
43
44 pub fn object(value: RenderModel) -> Self {
45 Self::Object(value)
46 }
47
48 pub(crate) fn as_text(&self, key: &str) -> Result<&str, TemplateModelError> {
49 match self {
50 Self::Text(value) => Ok(value),
51 Self::TrustedHtml(_) => Err(TemplateModelError::ValueTypeMismatch {
52 key: key.to_string(),
53 expected: "text",
54 }),
55 Self::Bool(_) => Err(TemplateModelError::ValueTypeMismatch {
56 key: key.to_string(),
57 expected: "text",
58 }),
59 Self::List(_) => Err(TemplateModelError::ValueTypeMismatch {
60 key: key.to_string(),
61 expected: "text",
62 }),
63 Self::Object(_) => Err(TemplateModelError::ValueTypeMismatch {
64 key: key.to_string(),
65 expected: "text",
66 }),
67 }
68 }
69
70 pub(crate) fn as_bool(&self, key: &str) -> Result<bool, TemplateModelError> {
71 match self {
72 Self::Bool(value) => Ok(*value),
73 Self::Text(_) | Self::TrustedHtml(_) | Self::List(_) | Self::Object(_) => {
74 Err(TemplateModelError::ValueTypeMismatch {
75 key: key.to_string(),
76 expected: "bool",
77 })
78 }
79 }
80 }
81
82 pub(crate) fn as_list(&self, key: &str) -> Result<&[RenderModel], TemplateModelError> {
83 match self {
84 Self::List(value) => Ok(value.as_slice()),
85 Self::Text(_) | Self::TrustedHtml(_) | Self::Bool(_) | Self::Object(_) => {
86 Err(TemplateModelError::ValueTypeMismatch {
87 key: key.to_string(),
88 expected: "list",
89 })
90 }
91 }
92 }
93
94 pub(crate) fn as_object(&self, key: &str) -> Result<&RenderModel, TemplateModelError> {
95 match self {
96 Self::Object(value) => Ok(value),
97 Self::Text(_) | Self::TrustedHtml(_) | Self::Bool(_) | Self::List(_) => {
98 Err(TemplateModelError::ValueTypeMismatch {
99 key: key.to_string(),
100 expected: "object",
101 })
102 }
103 }
104 }
105
106 #[cfg(test)]
107 pub(crate) fn render_html(&self) -> String {
108 match self {
109 Self::Text(value) => escape_html_text(value),
110 Self::TrustedHtml(value) => value.as_str().to_string(),
111 Self::Bool(value) => value.to_string(),
112 Self::List(_) => String::new(),
113 Self::Object(_) => String::new(),
114 }
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Default)]
119pub struct RenderModel {
120 values: BTreeMap<String, RenderValue>,
121 asset_paths: BTreeMap<String, String>,
122 translations: BTreeMap<String, String>,
123}
124
125impl RenderModel {
126 pub fn new() -> Self {
127 Self::default()
128 }
129
130 pub fn with_value(
131 mut self,
132 key: impl Into<String>,
133 value: RenderValue,
134 ) -> Result<Self, TemplateModelError> {
135 let key = validate_token("render_key", key.into())?;
136 self.values.insert(key, value);
137 Ok(self)
138 }
139
140 pub fn with_bool(
141 self,
142 key: impl Into<String>,
143 value: bool,
144 ) -> Result<Self, TemplateModelError> {
145 self.with_value(key, RenderValue::bool(value))
146 }
147
148 pub fn with_list(
149 self,
150 key: impl Into<String>,
151 value: Vec<RenderModel>,
152 ) -> Result<Self, TemplateModelError> {
153 self.with_value(key, RenderValue::list(value))
154 }
155
156 pub fn with_object(
157 self,
158 key: impl Into<String>,
159 value: RenderModel,
160 ) -> Result<Self, TemplateModelError> {
161 self.with_value(key, RenderValue::object(value))
162 }
163
164 pub fn with_asset_path(
165 mut self,
166 logical_path: impl Into<String>,
167 public_url: impl Into<String>,
168 ) -> Result<Self, TemplateModelError> {
169 let logical_path = validate_token("asset_logical_path", logical_path.into())?;
170 let public_url = require_non_empty("asset_public_url", public_url.into())?;
171 self.asset_paths.insert(logical_path, public_url);
172 Ok(self)
173 }
174
175 pub fn with_translation(
176 mut self,
177 key: impl Into<String>,
178 value: impl Into<String>,
179 ) -> Result<Self, TemplateModelError> {
180 let key = validate_token("translation_key", key.into())?;
181 let value = require_non_empty("translation_value", value.into())?;
182 self.translations.insert(key, value);
183 Ok(self)
184 }
185
186 pub(crate) fn get(&self, key: &str) -> Option<&RenderValue> {
187 if let Some(value) = self.values.get(key) {
188 return Some(value);
189 }
190
191 let (head, tail) = key.split_once('.')?;
192 let value = self.values.get(head)?;
193 value.as_object(head).ok()?.get(tail)
194 }
195
196 pub(crate) fn get_path(&self, path: &str) -> Option<&RenderValue> {
197 self.get(path)
198 }
199
200 pub(crate) fn get_asset_path(&self, logical_path: &str) -> Option<&str> {
201 self.asset_paths.get(logical_path).map(String::as_str)
202 }
203
204 pub(crate) fn get_translation(&self, key: &str) -> Option<&str> {
205 self.translations.get(key).map(String::as_str)
206 }
207
208 pub(crate) fn merged_with(&self, overlay: &RenderModel) -> RenderModel {
209 let mut values = self.values.clone();
210 values.extend(overlay.values.clone());
211 let mut asset_paths = self.asset_paths.clone();
212 asset_paths.extend(overlay.asset_paths.clone());
213 let mut translations = self.translations.clone();
214 translations.extend(overlay.translations.clone());
215 RenderModel {
216 values,
217 asset_paths,
218 translations,
219 }
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct RenderOutput {
225 pub html: String,
226}