1use boa_engine::property::Attribute;
2use boa_engine::NativeFunction;
3use boa_engine::{Context, JsArgs, JsValue, Source};
4use std::cell::RefCell;
5use std::rc::Rc;
6
7use crate::scene::*;
8
9#[derive(Debug, Clone)]
10pub enum DomMutation {
11 SetTextContent(String, String),
12 SetStyle(String, String, String),
13 AddClass(String, String),
14 RemoveClass(String, String),
15}
16
17#[derive(Debug, Clone, Default)]
19pub struct LocalStorage {
20 data: std::collections::HashMap<String, String>,
21 file_path: Option<std::path::PathBuf>,
22}
23
24impl LocalStorage {
25 pub fn new(file_path: Option<std::path::PathBuf>) -> Self {
26 let mut storage = Self {
27 data: std::collections::HashMap::new(),
28 file_path: file_path.clone(),
29 };
30 if let Some(path) = &file_path {
31 if let Ok(content) = std::fs::read_to_string(path) {
32 if let Ok(map) =
33 serde_json::from_str::<std::collections::HashMap<String, String>>(&content)
34 {
35 storage.data = map;
36 }
37 }
38 }
39 storage
40 }
41
42 pub fn get_item(&self, key: &str) -> Option<&String> {
43 self.data.get(key)
44 }
45
46 pub fn set_item(&mut self, key: String, value: String) {
47 self.data.insert(key, value);
48 self.persist();
49 }
50
51 pub fn remove_item(&mut self, key: &str) {
52 self.data.remove(key);
53 self.persist();
54 }
55
56 pub fn clear(&mut self) {
57 self.data.clear();
58 self.persist();
59 }
60
61 pub fn length(&self) -> usize {
62 self.data.len()
63 }
64
65 fn persist(&self) {
66 if let Some(path) = &self.file_path {
67 if let Ok(json) = serde_json::to_string_pretty(&self.data) {
68 let tmp = path.with_extension("json.tmp");
71 if std::fs::write(&tmp, &json).is_ok() {
72 let _ = std::fs::rename(&tmp, path);
73 }
74 }
75 }
76 }
77}
78
79pub struct JsEngine {
80 context: Context,
81 mutations: Rc<RefCell<Vec<DomMutation>>>,
82 storage: Rc<RefCell<LocalStorage>>,
83}
84
85impl Default for JsEngine {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl JsEngine {
92 pub fn new() -> Self {
93 Self::with_storage_path(None)
94 }
95
96 pub fn with_storage_path(storage_path: Option<std::path::PathBuf>) -> Self {
97 let context = Context::default();
98 let mutations = Rc::new(RefCell::new(Vec::new()));
99 let storage = Rc::new(RefCell::new(LocalStorage::new(storage_path)));
100
101 let mut engine = Self {
102 context,
103 mutations,
104 storage,
105 };
106 engine.register_dom_api();
107 engine.register_storage_api();
108 engine.register_fetch_api();
109 engine
110 }
111
112 fn register_dom_api(&mut self) {
113 let mutations = self.mutations.clone();
114
115 let get_element = unsafe {
119 NativeFunction::from_closure(move |_, args, ctx| {
120 let id = args
121 .get_or_undefined(0)
122 .to_string(ctx)?
123 .to_std_string_escaped();
124
125 let m = mutations.clone();
126 let element_id = id.clone();
127
128 let obj = boa_engine::JsObject::with_null_proto();
129
130 {
132 let m = m.clone();
133 let eid = element_id.clone();
134 let func = NativeFunction::from_closure(move |_, args, ctx| {
135 let text = args
136 .get_or_undefined(0)
137 .to_string(ctx)?
138 .to_std_string_escaped();
139 m.borrow_mut()
140 .push(DomMutation::SetTextContent(eid.clone(), text));
141 Ok(JsValue::undefined())
142 });
143 let js_func = func.to_js_function(ctx.realm());
144 obj.set(
145 boa_engine::js_string!("setTextContent"),
146 js_func,
147 false,
148 ctx,
149 )?;
150 }
151
152 {
154 let m = m.clone();
155 let eid = element_id.clone();
156 let func = NativeFunction::from_closure(move |_, args, ctx| {
157 let prop = args
158 .get_or_undefined(0)
159 .to_string(ctx)?
160 .to_std_string_escaped();
161 let val = args
162 .get_or_undefined(1)
163 .to_string(ctx)?
164 .to_std_string_escaped();
165 m.borrow_mut()
166 .push(DomMutation::SetStyle(eid.clone(), prop, val));
167 Ok(JsValue::undefined())
168 });
169 let js_func = func.to_js_function(ctx.realm());
170 obj.set(boa_engine::js_string!("setStyle"), js_func, false, ctx)?;
171 }
172
173 {
175 let m = m.clone();
176 let eid = element_id.clone();
177 let func = NativeFunction::from_closure(move |_, args, ctx| {
178 let class = args
179 .get_or_undefined(0)
180 .to_string(ctx)?
181 .to_std_string_escaped();
182 m.borrow_mut()
183 .push(DomMutation::AddClass(eid.clone(), class));
184 Ok(JsValue::undefined())
185 });
186 let js_func = func.to_js_function(ctx.realm());
187 obj.set(boa_engine::js_string!("addClass"), js_func, false, ctx)?;
188 }
189
190 {
192 let m = m.clone();
193 let eid = element_id.clone();
194 let func = NativeFunction::from_closure(move |_, args, ctx| {
195 let class = args
196 .get_or_undefined(0)
197 .to_string(ctx)?
198 .to_std_string_escaped();
199 m.borrow_mut()
200 .push(DomMutation::RemoveClass(eid.clone(), class));
201 Ok(JsValue::undefined())
202 });
203 let js_func = func.to_js_function(ctx.realm());
204 obj.set(boa_engine::js_string!("removeClass"), js_func, false, ctx)?;
205 }
206
207 obj.set(
208 boa_engine::js_string!("id"),
209 JsValue::from(boa_engine::js_string!(element_id.as_str())),
210 false,
211 ctx,
212 )?;
213
214 Ok(JsValue::from(obj))
215 })
216 };
217
218 let document = boa_engine::JsObject::with_null_proto();
219 let get_elem_func = get_element.to_js_function(self.context.realm());
220 document
221 .set(
222 boa_engine::js_string!("getElementById"),
223 get_elem_func,
224 false,
225 &mut self.context,
226 )
227 .ok();
228
229 self.context
230 .register_global_property(
231 boa_engine::js_string!("document"),
232 JsValue::from(document),
233 Attribute::all(),
234 )
235 .ok();
236
237 let console = boa_engine::JsObject::with_null_proto();
239 let log_fn = NativeFunction::from_fn_ptr(|_, args, ctx| {
240 let msg = args
241 .get_or_undefined(0)
242 .to_string(ctx)?
243 .to_std_string_escaped();
244 println!("[foundry:js] {}", msg);
245 Ok(JsValue::undefined())
246 });
247 let log_js_func = log_fn.to_js_function(self.context.realm());
248 console
249 .set(
250 boa_engine::js_string!("log"),
251 log_js_func,
252 false,
253 &mut self.context,
254 )
255 .ok();
256
257 self.context
258 .register_global_property(
259 boa_engine::js_string!("console"),
260 JsValue::from(console),
261 Attribute::all(),
262 )
263 .ok();
264 }
265
266 fn register_storage_api(&mut self) {
267 let storage = self.storage.clone();
268 let ls = boa_engine::JsObject::with_null_proto();
269
270 {
272 let s = storage.clone();
273 let func = unsafe {
275 NativeFunction::from_closure(move |_, args, ctx| {
276 let key = args
277 .get_or_undefined(0)
278 .to_string(ctx)?
279 .to_std_string_escaped();
280 let val = s.borrow().get_item(&key).cloned();
281 match val {
282 Some(v) => Ok(JsValue::from(boa_engine::js_string!(v.as_str()))),
283 None => Ok(JsValue::null()),
284 }
285 })
286 };
287 let js_func = func.to_js_function(self.context.realm());
288 ls.set(
289 boa_engine::js_string!("getItem"),
290 js_func,
291 false,
292 &mut self.context,
293 )
294 .ok();
295 }
296
297 {
299 let s = storage.clone();
300 let func = unsafe {
302 NativeFunction::from_closure(move |_, args, ctx| {
303 let key = args
304 .get_or_undefined(0)
305 .to_string(ctx)?
306 .to_std_string_escaped();
307 let val = args
308 .get_or_undefined(1)
309 .to_string(ctx)?
310 .to_std_string_escaped();
311 s.borrow_mut().set_item(key, val);
312 Ok(JsValue::undefined())
313 })
314 };
315 let js_func = func.to_js_function(self.context.realm());
316 ls.set(
317 boa_engine::js_string!("setItem"),
318 js_func,
319 false,
320 &mut self.context,
321 )
322 .ok();
323 }
324
325 {
327 let s = storage.clone();
328 let func = unsafe {
330 NativeFunction::from_closure(move |_, args, ctx| {
331 let key = args
332 .get_or_undefined(0)
333 .to_string(ctx)?
334 .to_std_string_escaped();
335 s.borrow_mut().remove_item(&key);
336 Ok(JsValue::undefined())
337 })
338 };
339 let js_func = func.to_js_function(self.context.realm());
340 ls.set(
341 boa_engine::js_string!("removeItem"),
342 js_func,
343 false,
344 &mut self.context,
345 )
346 .ok();
347 }
348
349 {
351 let s = storage.clone();
352 let func = unsafe {
354 NativeFunction::from_closure(move |_, _args, _ctx| {
355 s.borrow_mut().clear();
356 Ok(JsValue::undefined())
357 })
358 };
359 let js_func = func.to_js_function(self.context.realm());
360 ls.set(
361 boa_engine::js_string!("clear"),
362 js_func,
363 false,
364 &mut self.context,
365 )
366 .ok();
367 }
368
369 self.context
370 .register_global_property(
371 boa_engine::js_string!("localStorage"),
372 JsValue::from(ls),
373 Attribute::all(),
374 )
375 .ok();
376 }
377
378 fn register_fetch_api(&mut self) {
379 let func = NativeFunction::from_fn_ptr(|_, args, ctx| {
383 let url = args
384 .get_or_undefined(0)
385 .to_string(ctx)?
386 .to_std_string_escaped();
387
388 let result = boa_engine::JsObject::with_null_proto();
389
390 let agent = ureq::Agent::new_with_config(
391 ureq::config::Config::builder()
392 .timeout_connect(Some(std::time::Duration::from_secs(5)))
393 .timeout_recv_body(Some(std::time::Duration::from_secs(10)))
394 .build(),
395 );
396
397 match agent.get(&url).call() {
398 Ok(response) => {
399 let status = response.status().as_u16();
400 let body = response
402 .into_body()
403 .with_config()
404 .limit(4 * 1024 * 1024)
405 .read_to_string()
406 .unwrap_or_default();
407 result
408 .set(
409 boa_engine::js_string!("ok"),
410 JsValue::from((200..300).contains(&status)),
411 false,
412 ctx,
413 )
414 .ok();
415 result
416 .set(
417 boa_engine::js_string!("status"),
418 JsValue::from(status as i32),
419 false,
420 ctx,
421 )
422 .ok();
423 result
424 .set(
425 boa_engine::js_string!("text"),
426 JsValue::from(boa_engine::js_string!(body.as_str())),
427 false,
428 ctx,
429 )
430 .ok();
431 }
432 Err(e) => {
433 result
434 .set(
435 boa_engine::js_string!("ok"),
436 JsValue::from(false),
437 false,
438 ctx,
439 )
440 .ok();
441 result
442 .set(
443 boa_engine::js_string!("status"),
444 JsValue::from(0),
445 false,
446 ctx,
447 )
448 .ok();
449 let err_msg = e.to_string();
450 result
451 .set(
452 boa_engine::js_string!("text"),
453 JsValue::from(boa_engine::js_string!(err_msg.as_str())),
454 false,
455 ctx,
456 )
457 .ok();
458 }
459 }
460
461 Ok(JsValue::from(result))
462 });
463
464 let js_func = func.to_js_function(self.context.realm());
465 self.context
466 .register_global_property(
467 boa_engine::js_string!("fetch"),
468 JsValue::from(js_func),
469 Attribute::all(),
470 )
471 .ok();
472 }
473
474 pub fn execute(&mut self, code: &str) -> Result<(), String> {
475 self.mutations.borrow_mut().clear();
476 self.context
478 .eval(Source::from_bytes(code))
479 .map_err(|e| format!("JS error: {}", e))?;
480 Ok(())
481 }
482
483 pub fn take_mutations(&self) -> Vec<DomMutation> {
484 self.mutations.borrow_mut().drain(..).collect()
485 }
486
487 pub fn apply_mutations(&self, scene: &mut SceneGraph, mutations: &[DomMutation]) {
488 for mutation in mutations {
489 match mutation {
490 DomMutation::SetTextContent(element_id, text) => {
491 if let Some(node_id) = scene.find_by_element_id(element_id) {
492 let children: Vec<NodeId> = scene.get(node_id).children.clone();
493 let text_child = children
494 .iter()
495 .find(|&&c| scene.get(c).kind == ElementKind::Text);
496 if let Some(&child_id) = text_child {
497 scene.get_mut(child_id).text_content = Some(text.clone());
498 scene.get_mut(child_id).dirty = true;
499 } else {
500 let child = scene.add_node(ElementKind::Text, "text".to_string());
501 scene.get_mut(child).text_content = Some(text.clone());
502 scene.add_child(node_id, child);
503 }
504 scene.mark_dirty_recursive(node_id);
505 }
506 }
507 DomMutation::SetStyle(element_id, prop, val) => {
508 if let Some(node_id) = scene.find_by_element_id(element_id) {
509 crate::css::apply_property(&mut scene.get_mut(node_id).style, prop, val);
510 scene.mark_dirty_recursive(node_id);
511 }
512 }
513 DomMutation::AddClass(element_id, class) => {
514 if let Some(node_id) = scene.find_by_element_id(element_id) {
515 let node = scene.get_mut(node_id);
516 if !node.classes.contains(class) {
517 node.classes.push(class.clone());
518 node.dirty = true;
519 }
520 }
521 }
522 DomMutation::RemoveClass(element_id, class) => {
523 if let Some(node_id) = scene.find_by_element_id(element_id) {
524 let node = scene.get_mut(node_id);
525 node.classes.retain(|c| c != class);
526 node.dirty = true;
527 }
528 }
529 }
530 }
531 }
532}