1#![allow(dead_code)]
8
9use alloc::{
10 format,
11 string::{String, ToString},
12 vec::Vec,
13};
14
15use super::{
16 dom_bindings::DomApi, events::EventType, js_compiler::Compiler, js_gc::GcHeap,
17 js_parser::JsParser, js_vm::JsVm,
18};
19
20pub struct ScriptEngine {
26 pub vm: JsVm,
28 pub gc: GcHeap,
30 pub dom_api: DomApi,
32 scripts_executed: usize,
34 ticks_processed: u64,
36 microtasks: Vec<usize>,
38 pub last_error: Option<String>,
40}
41
42impl Default for ScriptEngine {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl ScriptEngine {
49 pub fn new() -> Self {
50 Self {
51 vm: JsVm::new(),
52 gc: GcHeap::new(),
53 dom_api: DomApi::new(),
54 scripts_executed: 0,
55 ticks_processed: 0,
56 microtasks: Vec::new(),
57 last_error: None,
58 }
59 }
60
61 pub fn execute_script(&mut self, source: &str) -> Result<(), String> {
64 if source.trim().is_empty() {
65 return Ok(());
66 }
67
68 let mut parser = JsParser::from_source(source);
69 let root = parser.parse();
70
71 if !parser.errors.is_empty() {
72 let err = parser.errors.join("; ");
73 self.last_error = Some(err.clone());
74 return Err(err);
75 }
76
77 let mut compiler = Compiler::new();
78 let chunk = compiler.compile(&parser.arena, root);
79
80 match self.vm.run_chunk(&chunk) {
81 Ok(_) => {
82 self.scripts_executed += 1;
83 if self.gc.should_collect() {
85 self.gc.collect(&self.vm);
86 }
87 Ok(())
88 }
89 Err(e) => {
90 let msg = format!("{}", e);
91 self.last_error = Some(msg.clone());
92 Err(msg)
93 }
94 }
95 }
96
97 pub fn process_script_tags(&mut self, html: &str) -> usize {
100 let mut count = 0;
101 let mut search_from = 0;
102
103 while let Some(open_tag) = find_ci(html, "<script", search_from) {
104 let tag_end = match html[open_tag..].find('>') {
106 Some(pos) => open_tag + pos + 1,
107 None => break,
108 };
109
110 let close_tag = match find_ci(html, "</script>", tag_end) {
112 Some(pos) => pos,
113 None => break,
114 };
115
116 let script_src = &html[tag_end..close_tag];
117 if self.execute_script(script_src).is_ok() {
118 count += 1;
119 }
120
121 search_from = close_tag + 9; }
123
124 count
125 }
126
127 pub fn tick(&mut self) {
132 self.ticks_processed += 1;
133
134 let expired = self.dom_api.timer_queue.tick();
136 for callback_id in expired {
137 self.invoke_callback(callback_id);
138 }
139
140 let tasks = core::mem::take(&mut self.microtasks);
142 for callback_id in tasks {
143 self.invoke_callback(callback_id);
144 }
145
146 if self.gc.should_collect() {
148 self.gc.collect(&self.vm);
149 }
150 }
151
152 pub fn process_event(&mut self, event_type: EventType, target_node: super::events::NodeId) {
155 let mut event = super::events::Event::new(event_type, target_node);
156 self.dom_api.event_dispatcher.dispatch(&mut event);
157
158 let invoked = self.dom_api.event_dispatcher.take_invoked();
159 for (callback_id, _event_type) in invoked {
160 self.invoke_callback(callback_id);
161 }
162 }
163
164 pub fn process_click(&mut self, x: i32, y: i32) {
166 if let Some((_target, _prevented)) = self.dom_api.event_dispatcher.dispatch_click(x, y, 0) {
167 let invoked = self.dom_api.event_dispatcher.take_invoked();
168 for (callback_id, _) in invoked {
169 self.invoke_callback(callback_id);
170 }
171 }
172 }
173
174 fn invoke_callback(&mut self, _callback_id: usize) {
178 }
182
183 pub fn queue_microtask(&mut self, callback_id: usize) {
185 self.microtasks.push(callback_id);
186 }
187
188 pub fn console_output(&self) -> Vec<String> {
190 let mut output = self.vm.output.clone();
191 output.extend(self.dom_api.console_output.iter().cloned());
192 output
193 }
194
195 pub fn clear_console(&mut self) {
197 self.vm.output.clear();
198 self.dom_api.console_output.clear();
199 }
200
201 pub fn scripts_executed(&self) -> usize {
203 self.scripts_executed
204 }
205
206 pub fn ticks_processed(&self) -> u64 {
208 self.ticks_processed
209 }
210
211 pub fn set_global(&mut self, name: &str, value: super::js_vm::JsValue) {
213 self.vm.globals.insert(name.to_string(), value);
214 }
215
216 pub fn get_global(&self, name: &str) -> Option<&super::js_vm::JsValue> {
218 self.vm.globals.get(name)
219 }
220}
221
222fn find_ci(haystack: &str, needle: &str, start: usize) -> Option<usize> {
228 let h = haystack.as_bytes();
229 let n = needle.as_bytes();
230 if n.is_empty() || start + n.len() > h.len() {
231 return None;
232 }
233 'outer: for i in start..=(h.len() - n.len()) {
234 for j in 0..n.len() {
235 if !h[i + j].eq_ignore_ascii_case(&n[j]) {
236 continue 'outer;
237 }
238 }
239 return Some(i);
240 }
241 None
242}
243
244#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::browser::{js_lexer::js_int, js_vm::JsValue};
252
253 #[test]
254 fn test_engine_new() {
255 let engine = ScriptEngine::new();
256 assert_eq!(engine.scripts_executed(), 0);
257 assert_eq!(engine.ticks_processed(), 0);
258 }
259
260 #[test]
261 fn test_execute_empty() {
262 let mut engine = ScriptEngine::new();
263 assert!(engine.execute_script("").is_ok());
264 assert!(engine.execute_script(" ").is_ok());
265 }
266
267 #[test]
268 fn test_execute_simple() {
269 let mut engine = ScriptEngine::new();
270 assert!(engine.execute_script("let x = 42;").is_ok());
271 assert_eq!(engine.scripts_executed(), 1);
272 let val = engine.get_global("x");
273 assert!(matches!(val, Some(JsValue::Number(n)) if *n == js_int(42)));
274 }
275
276 #[test]
277 fn test_execute_multiple() {
278 let mut engine = ScriptEngine::new();
279 engine.execute_script("let a = 1;").unwrap();
280 engine.execute_script("let b = 2;").unwrap();
281 assert_eq!(engine.scripts_executed(), 2);
282 }
283
284 #[test]
285 fn test_execute_error() {
286 let mut engine = ScriptEngine::new();
287 let result = engine.execute_script("while(true){}");
289 assert!(result.is_err());
290 assert!(engine.last_error.is_some());
291 }
292
293 #[test]
294 fn test_set_get_global() {
295 let mut engine = ScriptEngine::new();
296 engine.set_global("myVar", JsValue::String("hello".to_string()));
297 let val = engine.get_global("myVar");
298 assert!(matches!(val, Some(JsValue::String(s)) if s == "hello"));
299 }
300
301 #[test]
302 fn test_process_script_tags() {
303 let mut engine = ScriptEngine::new();
304 let html = "<html><body><script>let x = 1;</script><p>Hello</p><script>let y = \
305 2;</script></body></html>";
306 let count = engine.process_script_tags(html);
307 assert_eq!(count, 2);
308 assert!(engine.get_global("x").is_some());
309 assert!(engine.get_global("y").is_some());
310 }
311
312 #[test]
313 fn test_process_script_tags_case_insensitive() {
314 let mut engine = ScriptEngine::new();
315 let html = "<SCRIPT>let z = 3;</SCRIPT>";
316 let count = engine.process_script_tags(html);
317 assert_eq!(count, 1);
318 }
319
320 #[test]
321 fn test_process_script_tags_empty() {
322 let mut engine = ScriptEngine::new();
323 let html = "<p>No scripts here</p>";
324 let count = engine.process_script_tags(html);
325 assert_eq!(count, 0);
326 }
327
328 #[test]
329 fn test_tick() {
330 let mut engine = ScriptEngine::new();
331 engine.tick();
332 assert_eq!(engine.ticks_processed(), 1);
333 engine.tick();
334 assert_eq!(engine.ticks_processed(), 2);
335 }
336
337 #[test]
338 fn test_timer_via_engine() {
339 let mut engine = ScriptEngine::new();
340 engine.dom_api.timer_queue.set_timeout(42, 3);
341 engine.tick(); engine.tick(); engine.tick(); assert_eq!(engine.ticks_processed(), 3);
345 }
346
347 #[test]
348 fn test_console_output() {
349 let mut engine = ScriptEngine::new();
350 engine.dom_api.console_log("from dom");
351 let output = engine.console_output();
352 assert!(output.contains(&"from dom".to_string()));
353 }
354
355 #[test]
356 fn test_clear_console() {
357 let mut engine = ScriptEngine::new();
358 engine.dom_api.console_log("test");
359 engine.clear_console();
360 assert!(engine.console_output().is_empty());
361 }
362
363 #[test]
364 fn test_queue_microtask() {
365 let mut engine = ScriptEngine::new();
366 engine.queue_microtask(99);
367 assert_eq!(engine.microtasks.len(), 1);
368 engine.tick(); assert!(engine.microtasks.is_empty());
370 }
371
372 #[test]
373 fn test_process_event() {
374 let mut engine = ScriptEngine::new();
375 let div = engine.dom_api.create_element("div");
376 engine.dom_api.add_event_listener(div, EventType::Click, 77);
377 engine.process_event(EventType::Click, div);
378 }
380
381 #[test]
382 fn test_process_click() {
383 let mut engine = ScriptEngine::new();
384 let div = engine.dom_api.create_element("div");
385 engine
386 .dom_api
387 .event_dispatcher
388 .add_hit_box(super::super::events::HitRect::new(0, 0, 100, 100, div));
389 engine.dom_api.add_event_listener(div, EventType::Click, 50);
390 engine.process_click(50, 50);
391 }
393
394 #[test]
395 fn test_find_ci() {
396 assert_eq!(find_ci("Hello World", "hello", 0), Some(0));
397 assert_eq!(find_ci("Hello World", "WORLD", 0), Some(6));
398 assert_eq!(find_ci("Hello World", "xyz", 0), None);
399 assert_eq!(find_ci("aabb", "bb", 0), Some(2));
400 }
401
402 #[test]
403 fn test_find_ci_empty() {
404 assert_eq!(find_ci("test", "", 0), None);
405 }
406
407 #[test]
408 fn test_engine_default() {
409 let engine = ScriptEngine::default();
410 assert_eq!(engine.scripts_executed(), 0);
411 }
412
413 #[test]
414 fn test_script_with_function() {
415 let mut engine = ScriptEngine::new();
416 let result = engine.execute_script("function add(a,b) { return a+b; } let r = add(3,4);");
417 assert!(result.is_ok());
418 let val = engine.get_global("r");
419 assert!(matches!(val, Some(JsValue::Number(n)) if *n == js_int(7)));
420 }
421
422 #[test]
423 fn test_script_variables_persist() {
424 let mut engine = ScriptEngine::new();
425 engine.execute_script("let counter = 0;").unwrap();
426 engine.execute_script("counter = counter + 1;").unwrap();
427 let val = engine.get_global("counter");
428 assert!(matches!(val, Some(JsValue::Number(n)) if *n == js_int(1)));
429 }
430
431 #[test]
432 fn test_dom_create_via_script() {
433 let mut engine = ScriptEngine::new();
434 let div = engine.dom_api.create_element("div");
436 engine.dom_api.set_attribute(div, "id", "test");
437 assert_eq!(engine.dom_api.get_element_by_id("test"), Some(div));
438 }
439
440 #[test]
441 fn test_nested_script_tags() {
442 let mut engine = ScriptEngine::new();
443 let html = "<script>let a = 1;</script><script>let b = a + 1;</script>";
444 let count = engine.process_script_tags(html);
445 assert_eq!(count, 2);
446 let val = engine.get_global("b");
448 assert!(matches!(val, Some(JsValue::Number(n)) if *n == js_int(2)));
449 }
450
451 #[test]
452 fn test_gc_integration() {
453 let mut engine = ScriptEngine::new();
454 for i in 0..100 {
456 engine.gc.allocate(super::super::js_vm::JsObject::new());
457 }
458 engine.gc.collect(&engine.vm);
459 assert_eq!(engine.gc.arena.live_count(), 0);
461 }
462
463 #[test]
464 fn test_multiple_ticks_with_timer() {
465 let mut engine = ScriptEngine::new();
466 engine.dom_api.timer_queue.set_interval(10, 2);
467 for _ in 0..6 {
468 engine.tick();
469 }
470 assert_eq!(engine.ticks_processed(), 6);
471 }
472}