webots_proto_template/template/
mod.rs1use boa_engine::object::ObjectInitializer;
2use boa_engine::property::{Attribute, PropertyKey};
3use boa_engine::{Context, JsString, Source};
4use std::collections::HashMap;
5use thiserror::Error;
6use webots_proto_ast::proto::parser::Parser;
7
8pub mod js_api;
9pub mod parser;
10pub mod types;
11
12#[derive(Error, Debug)]
13pub enum TemplateError {
14 #[error("JS execution error: {0}")]
15 JsError(String), #[error("Template parsing error: {0}")]
17 ParseError(String),
18 #[error("Generated VRML invalid: {0}")]
19 ValidationError(String),
20}
21
22use crate::template::parser::{TemplateChunk, parse};
23use crate::template::types::{
24 TemplateContext, TemplateField, TemplateFieldBinding, TemplateWebotsVersion,
25};
26
27#[derive(Debug, Clone, Default)]
28pub struct TemplateEvaluator {
29 context_data: TemplateContext,
30}
31
32impl TemplateEvaluator {
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 pub fn with_context(context_data: TemplateContext) -> Self {
38 Self { context_data }
39 }
40
41 pub fn evaluate_with_environment(
48 &self,
49 template_content: &str,
50 fields: &HashMap<String, TemplateFieldBinding>,
51 ) -> Result<String, TemplateError> {
52 evaluate_template_with_environment(template_content, fields, &self.context_data)
53 }
54
55 pub fn evaluate_with_fields<I, K, V>(
59 &self,
60 template_content: &str,
61 fields: I,
62 ) -> Result<String, TemplateError>
63 where
64 I: IntoIterator<Item = (K, V)>,
65 K: Into<String>,
66 V: Into<TemplateField>,
67 {
68 let bindings = fields
69 .into_iter()
70 .map(|(field_name, field_value)| {
71 let field_value = field_value.into();
72 (
73 field_name.into(),
74 TemplateFieldBinding::new(field_value.clone(), field_value),
75 )
76 })
77 .collect::<HashMap<_, _>>();
78 self.evaluate_with_environment(template_content, &bindings)
79 }
80
81 pub fn evaluate_with_bindings<I, K>(
83 &self,
84 template_content: &str,
85 fields: I,
86 ) -> Result<String, TemplateError>
87 where
88 I: IntoIterator<Item = (K, TemplateFieldBinding)>,
89 K: Into<String>,
90 {
91 let bindings = fields
92 .into_iter()
93 .map(|(field_name, binding)| (field_name.into(), binding))
94 .collect::<HashMap<_, _>>();
95 self.evaluate_with_environment(template_content, &bindings)
96 }
97}
98
99fn evaluate_template_with_environment(
106 template_content: &str,
107 fields: &HashMap<String, TemplateFieldBinding>,
108 context_data: &TemplateContext,
109) -> Result<String, TemplateError> {
110 let chunks =
112 parse(template_content).map_err(|e| TemplateError::ParseError(format!("{:?}", e)))?;
113
114 let mut context = Context::default();
116
117 let global = context.global_object();
119 global
120 .delete_property_or_throw(PropertyKey::from(JsString::from("Date")), &mut context)
121 .ok();
122
123 if let Ok(math_obj) = global.get(PropertyKey::from(JsString::from("Math")), &mut context)
124 && let Some(math_obj) = math_obj.as_object()
125 {
126 math_obj
127 .delete_property_or_throw(PropertyKey::from(JsString::from("random")), &mut context)
128 .ok();
129 }
130
131 let init_script = r#"
136 (function() {
137 var real_buffer = [];
138
139 // Expose a dummy buffer for users who try to access it
140 var dummy_buffer = [];
141 Object.defineProperty(this, "__webots_output_buffer", {
142 value: dummy_buffer,
143 writable: false,
144 configurable: false,
145 enumerable: false
146 });
147
148 // The write function pushes to the real buffer
149 Object.defineProperty(this, "__webots_write", {
150 value: function(s) {
151 real_buffer.push(String(s));
152 },
153 writable: false,
154 configurable: false,
155 enumerable: false
156 });
157
158 // Internal accessor for Rust to retrieve output
159 Object.defineProperty(this, "__webots_get_output_internal", {
160 value: function() {
161 return real_buffer.join("");
162 },
163 writable: false,
164 configurable: false,
165 enumerable: false
166 });
167 }).call(this);
168 "#;
169 context
170 .eval(Source::from_bytes(init_script.as_bytes()))
171 .map_err(|e| TemplateError::JsError(e.to_string()))?;
172
173 let fields_obj = ObjectInitializer::new(&mut context).build();
175 for (field_name, field_binding) in fields {
176 let field_value = field_binding.value.to_js_value(&mut context);
177 let field_default_value = field_binding.default_value.to_js_value(&mut context);
178 let field_wrapper = ObjectInitializer::new(&mut context)
179 .property(
180 PropertyKey::from(JsString::from("value")),
181 field_value,
182 Attribute::READONLY,
183 )
184 .property(
185 PropertyKey::from(JsString::from("defaultValue")),
186 field_default_value,
187 Attribute::READONLY,
188 )
189 .build();
190
191 fields_obj
192 .set(
193 PropertyKey::from(JsString::from(field_name.as_str())),
194 field_wrapper,
195 false,
196 &mut context,
197 )
198 .map_err(|e| TemplateError::JsError(e.to_string()))?;
199 }
200
201 context
202 .register_global_property(
203 PropertyKey::from(JsString::from("fields")),
204 fields_obj,
205 Attribute::READONLY,
206 )
207 .map_err(|e| TemplateError::JsError(e.to_string()))?;
208
209 let context_object = build_context_object(context_data, &mut context)?;
211 context
212 .register_global_property(
213 PropertyKey::from(JsString::from("context")),
214 context_object,
215 Attribute::READONLY,
216 )
217 .map_err(|e| TemplateError::JsError(e.to_string()))?;
218
219 let mut js_code = String::new();
221 for chunk in chunks {
222 match chunk {
223 TemplateChunk::Text { content, .. } => {
224 match serde_json::to_string(&content) {
226 Ok(s) => js_code.push_str(&format!("__webots_write({});\n", s)),
227 Err(_) => {
228 return Err(TemplateError::ParseError(
229 "Failed to serialize text chunk".into(),
230 ));
231 }
232 }
233 }
234 TemplateChunk::ExpressionBlock { content, .. } => {
235 js_code.push_str(&format!("__webots_write({});\n", content));
236 }
237 TemplateChunk::ExecutionBlock { content, .. } => {
238 js_code.push_str(&content);
239 js_code.push('\n');
240 }
241 }
242 }
243
244 match context.eval(Source::from_bytes(js_code.as_bytes())) {
246 Ok(_) => {
247 let output_script = r#"__webots_get_output_internal()"#;
249 match context.eval(Source::from_bytes(output_script.as_bytes())) {
250 Ok(res) => {
251 match res.as_string() {
252 Some(s) => {
253 let output = s
254 .to_std_string()
255 .map_err(|e| TemplateError::JsError(e.to_string()))?;
256
257 let mut parser = Parser::new(&output);
260 match parser.parse_body() {
261 Ok(_) => Ok(output),
262 Err(e) => Err(TemplateError::ValidationError(format!("{:?}", e))),
263 }
264 }
265 None => Ok(String::new()), }
267 }
268 Err(e) => Err(TemplateError::JsError(e.to_string())),
269 }
270 }
271 Err(e) => {
272 Err(TemplateError::JsError(e.to_string()))
275 }
276 }
277}
278
279fn build_context_object(
280 context_data: &TemplateContext,
281 context: &mut Context,
282) -> Result<boa_engine::JsValue, TemplateError> {
283 let context_object = ObjectInitializer::new(context).build();
284
285 if let Some(world_path) = &context_data.world {
286 context_object
287 .set(
288 PropertyKey::from(JsString::from("world")),
289 JsString::from(world_path.as_str()),
290 false,
291 context,
292 )
293 .map_err(|e| TemplateError::JsError(e.to_string()))?;
294 }
295 if let Some(proto_path) = &context_data.proto {
296 context_object
297 .set(
298 PropertyKey::from(JsString::from("proto")),
299 JsString::from(proto_path.as_str()),
300 false,
301 context,
302 )
303 .map_err(|e| TemplateError::JsError(e.to_string()))?;
304 }
305 if let Some(project_path) = &context_data.project_path {
306 context_object
307 .set(
308 PropertyKey::from(JsString::from("project_path")),
309 JsString::from(project_path.as_str()),
310 false,
311 context,
312 )
313 .map_err(|e| TemplateError::JsError(e.to_string()))?;
314 }
315 if let Some(webots_home) = &context_data.webots_home {
316 context_object
317 .set(
318 PropertyKey::from(JsString::from("webots_home")),
319 JsString::from(webots_home.as_str()),
320 false,
321 context,
322 )
323 .map_err(|e| TemplateError::JsError(e.to_string()))?;
324 }
325 if let Some(temporary_files_path) = &context_data.temporary_files_path {
326 context_object
327 .set(
328 PropertyKey::from(JsString::from("temporary_files_path")),
329 JsString::from(temporary_files_path.as_str()),
330 false,
331 context,
332 )
333 .map_err(|e| TemplateError::JsError(e.to_string()))?;
334 }
335 if let Some(operating_system) = &context_data.os {
336 context_object
337 .set(
338 PropertyKey::from(JsString::from("os")),
339 JsString::from(operating_system.as_str()),
340 false,
341 context,
342 )
343 .map_err(|e| TemplateError::JsError(e.to_string()))?;
344 }
345 if let Some(node_id) = &context_data.id {
346 context_object
347 .set(
348 PropertyKey::from(JsString::from("id")),
349 JsString::from(node_id.as_str()),
350 false,
351 context,
352 )
353 .map_err(|e| TemplateError::JsError(e.to_string()))?;
354 }
355 if let Some(coordinate_system) = &context_data.coordinate_system {
356 context_object
357 .set(
358 PropertyKey::from(JsString::from("coordinate_system")),
359 JsString::from(coordinate_system.as_str()),
360 false,
361 context,
362 )
363 .map_err(|e| TemplateError::JsError(e.to_string()))?;
364 }
365 if let Some(version) = &context_data.webots_version {
366 context_object
367 .set(
368 PropertyKey::from(JsString::from("webots_version")),
369 build_webots_version_object(version, context)?,
370 false,
371 context,
372 )
373 .map_err(|e| TemplateError::JsError(e.to_string()))?;
374 }
375
376 Ok(context_object.into())
377}
378
379fn build_webots_version_object(
380 version: &TemplateWebotsVersion,
381 context: &mut Context,
382) -> Result<boa_engine::JsValue, TemplateError> {
383 let version_object = ObjectInitializer::new(context).build();
384 version_object
385 .set(
386 PropertyKey::from(JsString::from("major")),
387 JsString::from(version.major.as_str()),
388 false,
389 context,
390 )
391 .map_err(|e| TemplateError::JsError(e.to_string()))?;
392 version_object
393 .set(
394 PropertyKey::from(JsString::from("revision")),
395 JsString::from(version.revision.as_str()),
396 false,
397 context,
398 )
399 .map_err(|e| TemplateError::JsError(e.to_string()))?;
400
401 Ok(version_object.into())
402}