Skip to main content

jsdet_browser/
window.rs

1/// Window API simulation.
2///
3/// Handles: location (href, hostname, pathname, search, hash, assign, replace),
4/// open(), history (pushState, replaceState, back, forward),
5/// postMessage(), alert/confirm/prompt.
6///
7use jsdet_core::observation::Value;
8
9/// Simulated window.location state.
10#[derive(Debug, Clone, Default, serde::Serialize)]
11pub struct Location {
12    pub href: String,
13    pub protocol: String,
14    pub hostname: String,
15    pub port: String,
16    pub pathname: String,
17    pub search: String,
18    pub hash: String,
19    pub origin: String,
20}
21
22impl Location {
23    pub fn from_url(url: &str) -> Self {
24        let (protocol, rest) = url.split_once("://").unwrap_or(("https", url));
25        let (authority, path_and_query) = rest.split_once('/').unwrap_or((rest, ""));
26        let (host, port) = if authority.contains(':') {
27            let (h, p) = authority.rsplit_once(':').unwrap();
28            (h.to_string(), p.to_string())
29        } else {
30            (authority.to_string(), String::new())
31        };
32        let (path_with_query, hash) = path_and_query
33            .split_once('#')
34            .unwrap_or((path_and_query, ""));
35        let (pathname, search) = path_with_query
36            .split_once('?')
37            .unwrap_or((path_with_query, ""));
38
39        let origin = if port.is_empty() {
40            format!("{protocol}://{host}")
41        } else {
42            format!("{protocol}://{host}:{port}")
43        };
44
45        Self {
46            href: url.to_string(),
47            protocol: format!("{protocol}:"),
48            hostname: host,
49            port,
50            pathname: format!("/{pathname}"),
51            search: if search.is_empty() {
52                String::new()
53            } else {
54                format!("?{search}")
55            },
56            hash: if hash.is_empty() {
57                String::new()
58            } else {
59                format!("#{hash}")
60            },
61            origin,
62        }
63    }
64
65    pub fn get_property(&self, prop: &str) -> Value {
66        match prop {
67            "href" => Value::string(self.href.clone()),
68            "protocol" => Value::string(self.protocol.clone()),
69            "hostname" | "host" => Value::string(self.hostname.clone()),
70            "port" => Value::string(self.port.clone()),
71            "pathname" => Value::string(self.pathname.clone()),
72            "search" => Value::string(self.search.clone()),
73            "hash" => Value::string(self.hash.clone()),
74            "origin" => Value::string(self.origin.clone()),
75            _ => Value::Undefined,
76        }
77    }
78}
79
80/// Simulated history state.
81#[derive(Debug, Clone, Default)]
82pub struct History {
83    entries: Vec<String>,
84    index: usize,
85}
86
87impl History {
88    pub fn new(initial_url: &str) -> Self {
89        Self {
90            entries: vec![initial_url.to_string()],
91            index: 0,
92        }
93    }
94
95    pub fn push_state(&mut self, url: &str) {
96        self.entries.truncate(self.index + 1);
97        self.entries.push(url.to_string());
98        self.index = self.entries.len() - 1;
99    }
100
101    pub fn replace_state(&mut self, url: &str) {
102        if let Some(entry) = self.entries.get_mut(self.index) {
103            *entry = url.to_string();
104        }
105    }
106
107    pub fn back(&mut self) -> Option<&str> {
108        if self.index > 0 {
109            self.index -= 1;
110            self.entries.get(self.index).map(|s| s.as_str())
111        } else {
112            None
113        }
114    }
115
116    pub fn forward(&mut self) -> Option<&str> {
117        if self.index + 1 < self.entries.len() {
118            self.index += 1;
119            self.entries.get(self.index).map(|s| s.as_str())
120        } else {
121            None
122        }
123    }
124
125    pub fn length(&self) -> usize {
126        self.entries.len()
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn location_parses_full_url() {
136        let loc = Location::from_url("https://example.com:8080/path?q=1#frag");
137        assert_eq!(loc.protocol, "https:");
138        assert_eq!(loc.hostname, "example.com");
139        assert_eq!(loc.port, "8080");
140        assert_eq!(loc.pathname, "/path");
141        assert_eq!(loc.search, "?q=1");
142        assert_eq!(loc.hash, "#frag");
143    }
144
145    #[test]
146    fn history_push_and_back() {
147        let mut h = History::new("https://a.com");
148        h.push_state("https://b.com");
149        h.push_state("https://c.com");
150        assert_eq!(h.length(), 3);
151        assert_eq!(h.back(), Some("https://b.com"));
152        assert_eq!(h.back(), Some("https://a.com"));
153        assert_eq!(h.forward(), Some("https://b.com"));
154    }
155}