⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
js_integration.rs

1//! JavaScript Integration
2//!
3//! Ties together the JS lexer, parser, compiler, VM, GC, DOM bindings,
4//! and event system into a cohesive script engine. Handles `<script>`
5//! tag processing, event loop ticking, and callback dispatch.
6
7#![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
20// ---------------------------------------------------------------------------
21// Script engine
22// ---------------------------------------------------------------------------
23
24/// The script engine integrating all JS components
25pub struct ScriptEngine {
26    /// JavaScript virtual machine
27    pub vm: JsVm,
28    /// Garbage collector heap
29    pub gc: GcHeap,
30    /// DOM API bridge
31    pub dom_api: DomApi,
32    /// Total scripts executed
33    scripts_executed: usize,
34    /// Total ticks processed
35    ticks_processed: u64,
36    /// Pending microtasks (callback IDs)
37    microtasks: Vec<usize>,
38    /// Last error message
39    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    /// Execute a JavaScript source string.
62    /// Returns Ok(()) on success, Err(message) on failure.
63    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                // Collect garbage if needed
84                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    /// Extract inline JavaScript from `<script>` tags and execute each.
98    /// Returns the number of scripts successfully executed.
99    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            // Find end of open tag
105            let tag_end = match html[open_tag..].find('>') {
106                Some(pos) => open_tag + pos + 1,
107                None => break,
108            };
109
110            // Find </script>
111            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; // len("</script>")
122        }
123
124        count
125    }
126
127    /// Process a single tick of the event loop:
128    /// 1. Process expired timers
129    /// 2. Execute microtasks
130    /// 3. Run GC if needed
131    pub fn tick(&mut self) {
132        self.ticks_processed += 1;
133
134        // Process timers
135        let expired = self.dom_api.timer_queue.tick();
136        for callback_id in expired {
137            self.invoke_callback(callback_id);
138        }
139
140        // Process microtasks
141        let tasks = core::mem::take(&mut self.microtasks);
142        for callback_id in tasks {
143            self.invoke_callback(callback_id);
144        }
145
146        // GC check
147        if self.gc.should_collect() {
148            self.gc.collect(&self.vm);
149        }
150    }
151
152    /// Process a DOM event: dispatch it through the event system,
153    /// then invoke any JS callbacks that were triggered.
154    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    /// Process a click at pixel coordinates
165    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    /// Invoke a JS callback by function ID.
175    /// In a full implementation this would look up the function in the VM
176    /// and execute it. Here we use a simplified approach.
177    fn invoke_callback(&mut self, _callback_id: usize) {
178        // Callbacks would be stored in a table mapping callback_id -> JS
179        // function. For now this is a stub that will be connected when
180        // the JS VM has a function call-by-id mechanism.
181    }
182
183    /// Schedule a microtask
184    pub fn queue_microtask(&mut self, callback_id: usize) {
185        self.microtasks.push(callback_id);
186    }
187
188    /// Get the console output from both JS VM and DOM API
189    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    /// Clear console output
196    pub fn clear_console(&mut self) {
197        self.vm.output.clear();
198        self.dom_api.console_output.clear();
199    }
200
201    /// Number of scripts executed
202    pub fn scripts_executed(&self) -> usize {
203        self.scripts_executed
204    }
205
206    /// Number of ticks processed
207    pub fn ticks_processed(&self) -> u64 {
208        self.ticks_processed
209    }
210
211    /// Set a global variable in the JS VM
212    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    /// Get a global variable from the JS VM
217    pub fn get_global(&self, name: &str) -> Option<&super::js_vm::JsValue> {
218        self.vm.globals.get(name)
219    }
220}
221
222// ---------------------------------------------------------------------------
223// Helpers
224// ---------------------------------------------------------------------------
225
226/// Case-insensitive find in a string
227fn 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// ---------------------------------------------------------------------------
245// Tests
246// ---------------------------------------------------------------------------
247
248#[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        // Infinite loop should hit execution limit
288        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(); // 1
342        engine.tick(); // 2
343        engine.tick(); // 3 -- timer fires
344        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(); // processes microtasks
369        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        // Callback was invoked (stub), no crash
379    }
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        // No crash, event dispatched
392    }
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        // Direct DOM API usage (script bridge not fully wired)
435        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        // b depends on a from first script
447        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        // Allocate objects and trigger GC
455        for i in 0..100 {
456            engine.gc.allocate(super::super::js_vm::JsObject::new());
457        }
458        engine.gc.collect(&engine.vm);
459        // All unreferenced objects should be collected
460        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}