1use super::*;
4
5pub(crate) use alloc::rc::Rc as PromiseRc;
8pub(crate) use core::cell::RefCell as PromiseRefCell;
9
10pub(crate) fn new_pending() -> PromiseRc<PromiseRefCell<PromiseState>> {
11 PromiseRc::new(PromiseRefCell::new(PromiseState::pending()))
12}
13
14pub(crate) fn this_promise_state(this: &Value) -> Option<PromiseRc<PromiseRefCell<PromiseState>>> {
15 if let Value::Object(o) = this {
16 let o = unwrap_proxy_target(o);
17 let b = o.borrow();
18 if let ObjKind::PromiseObj(s) = &b.kind {
19 return Some(s.clone());
20 }
21 }
22 None
23}
24
25pub(crate) fn callable_or_none(v: Value) -> Option<Value> {
26 if let Value::Object(o) = &v {
27 if o.borrow().is_callable() {
28 return Some(v);
29 }
30 }
31 None
32}
33
34pub(crate) fn promise_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
36 let state = new_pending();
37 let executor = arg(a, 0);
38 if let Some(exec) = callable_or_none(executor) {
39 let resolve = Value::Object(Obj::resolver(state.clone(), false));
40 let reject = Value::Object(Obj::resolver(state.clone(), true));
41 if let Err(e) = it.call_value(&exec, Value::Undefined, &[resolve, reject]) {
42 if !it.aborted {
43 it.promise_reject(&state, e);
44 }
45 }
46 }
47 Ok(Value::Object(Obj::promise(state)))
48}
49
50pub(crate) fn promise_resolve_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
51 let v = arg(a, 0);
52 if this_promise_state(&v).is_some() {
54 return Ok(v);
55 }
56 let state = new_pending();
57 it.promise_resolve(&state, v);
58 Ok(Value::Object(Obj::promise(state)))
59}
60
61pub(crate) fn promise_reject_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
62 let state = new_pending();
63 it.promise_reject(&state, arg(a, 0));
64 Ok(Value::Object(Obj::promise(state)))
65}
66
67pub(crate) fn promise_with_resolvers_static(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
69 let state = new_pending();
70 let promise = Value::Object(Obj::promise(state.clone()));
71 let resolve = Value::Object(Obj::resolver(state.clone(), false));
72 let reject = Value::Object(Obj::resolver(state, true));
73 let o = Obj::plain();
74 {
75 let mut b = o.borrow_mut();
76 b.props.insert("promise".into(), promise);
77 b.props.insert("resolve".into(), resolve);
78 b.props.insert("reject".into(), reject);
79 }
80 Ok(Value::Object(o))
81}
82
83pub(crate) fn promise_try_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
87 let state = new_pending();
88 let callback = arg(a, 0);
89 let rest: Vec<Value> = if a.len() > 1 { a[1..].to_vec() } else { Vec::new() };
90 match it.call_value(&callback, Value::Undefined, &rest) {
91 Ok(v) => it.promise_resolve(&state, v),
92 Err(e) => {
93 if !it.aborted {
94 it.promise_reject(&state, e);
95 }
96 }
97 }
98 Ok(Value::Object(Obj::promise(state)))
99}
100
101pub(crate) fn resolved_promise(it: &mut Interp, v: Value) -> Value {
103 let state = new_pending();
104 it.promise_resolve(&state, v);
105 Value::Object(Obj::promise(state))
106}
107pub fn make_empty_time_ranges() -> super::super::value::ObjRef {
110 let o = Obj::plain();
111 {
112 let mut b = o.borrow_mut();
113 b.props.insert("length".into(), Value::Number(0.0));
114 b.props.insert("start".into(), nv("start", time_ranges_out_of_bounds));
115 b.props.insert("end".into(), nv("end", time_ranges_out_of_bounds));
116 }
117 o
118}
119pub(crate) fn time_ranges_out_of_bounds(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
120 Err(it.error(alloc::format!(
121 "Failed to execute on 'TimeRanges': The index provided ({}) is greater than or equal to the maximum bound (0).",
122 arg(a, 0).to_number() as i64
123 )))
124}
125pub fn dom_media_play(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
130 if let Some(idx) = this_dom_idx(&this) {
131 let was_paused = it.dom.borrow().get_attr(idx, "_paused").as_deref() != Some("false");
132 it.dom.borrow_mut().set_attr(idx, "_paused", "false");
133 if was_paused {
137 let _ = it.dispatch_event_in_interp(idx, "play", &[]);
138 let _ = it.dispatch_event_in_interp(idx, "playing", &[]);
139 }
140 }
141 Ok(resolved_promise(it, Value::Undefined))
142}
143pub fn dom_media_pause(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
147 if let Some(idx) = this_dom_idx(&this) {
148 let was_playing = it.dom.borrow().get_attr(idx, "_paused").as_deref() == Some("false");
149 it.dom.borrow_mut().set_attr(idx, "_paused", "true");
150 if was_playing {
151 let _ = it.dispatch_event_in_interp(idx, "pause", &[]);
152 }
153 }
154 Ok(Value::Undefined)
155}
156pub(crate) fn clipboard_write_text(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
160 if let Value::Object(o) = &this {
161 o.borrow_mut().props.insert("_clipboard".into(), Value::str(arg(a, 0).to_js_string()));
162 }
163 Ok(resolved_promise(it, Value::Undefined))
164}
165pub(crate) fn clipboard_read_text(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
167 let text = if let Value::Object(o) = &this {
168 o.borrow().props.get("_clipboard").map(|v| v.to_js_string()).unwrap_or_default()
169 } else {
170 String::new()
171 };
172 Ok(resolved_promise(it, Value::str(text)))
173}
174pub(crate) fn clipboard_item_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
178 let data = arg(a, 0);
179 let types: Vec<Value> = if let Value::Object(d) = &data {
180 d.borrow().props.keys().map(Value::str).collect()
181 } else {
182 Vec::new()
183 };
184 let o = Obj::plain();
185 {
186 let mut b = o.borrow_mut();
187 b.props.insert("_clipboard_item_data".into(), data);
188 b.props.insert("types".into(), Value::Object(Obj::array(types)));
189 b.props
190 .insert("getType".into(), nv("ClipboardItem.getType", clipboard_item_get_type));
191 }
192 Ok(Value::Object(o))
193}
194pub(crate) fn clipboard_item_get_type(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
196 let ty = arg(a, 0).to_js_string();
197 let value = if let Value::Object(o) = &this {
198 match o.borrow().props.get("_clipboard_item_data").cloned() {
199 Some(Value::Object(d)) => d.borrow().props.get(&ty).cloned().unwrap_or(Value::Undefined),
200 _ => Value::Undefined,
201 }
202 } else {
203 Value::Undefined
204 };
205 Ok(resolved_promise(it, value))
206}
207pub(crate) fn blob_bytes_to_string(blob: &Value) -> Option<String> {
211 let Value::Object(o) = blob else { return None };
212 let bytes_obj = o.borrow().props.get("_blob_bytes").cloned();
213 let Some(Value::Object(bytes)) = bytes_obj else { return None };
214 let byte_vals: alloc::vec::Vec<u8> = match &bytes.borrow().kind {
215 ObjKind::Array(items) => items.iter().map(|v| v.to_number() as u8).collect(),
216 _ => return None,
217 };
218 Some(String::from_utf8_lossy(&byte_vals).into_owned())
219}
220pub(crate) fn navigator_clipboard_write(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
224 if let Some(Value::Object(item)) = this_items(&arg(a, 0)).into_iter().next() {
225 let data = item.borrow().props.get("_clipboard_item_data").cloned();
226 if let Some(Value::Object(d)) = data {
227 let plain = d.borrow().props.get("text/plain").cloned();
228 if let Some(v) = plain {
229 let text = blob_bytes_to_string(&v).unwrap_or_else(|| v.to_js_string());
230 if let Value::Object(o) = &this {
231 o.borrow_mut().props.insert("_clipboard".into(), Value::str(text));
232 }
233 }
234 }
235 }
236 Ok(resolved_promise(it, Value::Undefined))
237}
238pub(crate) fn navigator_get_high_entropy_values(
244 it: &mut Interp,
245 this: Value,
246 a: &[Value],
247) -> Result<Value, Value> {
248 let hints: alloc::vec::Vec<String> = this_items(&arg(a, 0)).iter().map(|v| v.to_js_string()).collect();
249 let result = Obj::plain();
250 {
251 let mut r = result.borrow_mut();
252 if let Value::Object(ua) = &this {
253 for key in ["brands", "mobile", "platform"] {
254 if let Some(v) = ua.borrow().props.get(key).cloned() {
255 r.props.insert(key.into(), v);
256 }
257 }
258 }
259 for hint in &hints {
260 match hint.as_str() {
261 "architecture" => {
262 r.props.insert("architecture".into(), Value::str("arm"));
263 }
264 "bitness" => {
265 r.props.insert("bitness".into(), Value::str("64"));
266 }
267 "model" => {
268 r.props.insert("model".into(), Value::str(""));
269 }
270 "platformVersion" => {
271 r.props.insert("platformVersion".into(), Value::str("1.0"));
272 }
273 "uaFullVersion" | "fullVersionList" => {
274 if let Value::Object(ua) = &this {
275 if let Some(v) = ua.borrow().props.get("brands").cloned() {
276 r.props.insert(hint.clone(), v);
277 }
278 }
279 }
280 _ => {}
281 }
282 }
283 }
284 Ok(resolved_promise(it, Value::Object(result)))
285}
286pub(crate) fn navigator_storage_estimate(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
290 let result = Obj::plain();
291 {
292 let mut r = result.borrow_mut();
293 r.props.insert("usage".into(), Value::Number(0.0));
294 r.props.insert("quota".into(), Value::Number(1024.0 * 1024.0 * 1024.0));
295 }
296 Ok(resolved_promise(it, Value::Object(result)))
297}
298pub(crate) fn navigator_storage_persist(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
302 Ok(resolved_promise(it, Value::Bool(true)))
303}
304pub(crate) fn navigator_clipboard_read(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
307 let text = if let Value::Object(o) = &this {
308 o.borrow().props.get("_clipboard").map(|v| v.to_js_string()).unwrap_or_default()
309 } else {
310 String::new()
311 };
312 let blob = make_blob(text.bytes().map(|b| b as f64).collect(), String::from("text/plain"));
313 let data_obj = Obj::plain();
314 data_obj.borrow_mut().props.insert("text/plain".into(), blob);
315 let item = clipboard_item_ctor(it, Value::Undefined, &[Value::Object(data_obj)])?;
316 Ok(resolved_promise(it, Value::Object(Obj::array(alloc::vec![item]))))
317}
318
319pub(crate) fn rejected_promise(it: &mut Interp, reason: Value) -> Value {
321 let state = new_pending();
322 it.promise_reject(&state, reason);
323 Value::Object(Obj::promise(state))
324}
325
326pub(crate) fn split_http_url(url: &str) -> Option<(bool, String, String)> {
329 let (is_https, rest) = if let Some(r) = url.strip_prefix("https://") {
330 (true, r)
331 } else if let Some(r) = url.strip_prefix("http://") {
332 (false, r)
333 } else {
334 return None;
335 };
336 let (host, path) = match rest.split_once('/') {
337 Some((h, p)) => (h.to_string(), format!("/{}", p)),
338 None => (rest.to_string(), String::from("/")),
339 };
340 if host.is_empty() {
341 return None;
342 }
343 Some((is_https, host, path))
344}
345
346pub(crate) fn data_url_body(url: &str) -> Option<String> {
348 let rest = url.strip_prefix("data:")?;
349 let (_meta, body) = rest.split_once(',')?;
350 Some(body.to_string())
351}
352
353pub(crate) fn http_status_text(status: u16) -> &'static str {
360 match status {
361 200 => "OK",
362 201 => "Created",
363 202 => "Accepted",
364 204 => "No Content",
365 301 => "Moved Permanently",
366 302 => "Found",
367 304 => "Not Modified",
368 400 => "Bad Request",
369 401 => "Unauthorized",
370 403 => "Forbidden",
371 404 => "Not Found",
372 405 => "Method Not Allowed",
373 408 => "Request Timeout",
374 409 => "Conflict",
375 410 => "Gone",
376 429 => "Too Many Requests",
377 500 => "Internal Server Error",
378 501 => "Not Implemented",
379 502 => "Bad Gateway",
380 503 => "Service Unavailable",
381 504 => "Gateway Timeout",
382 _ => "",
383 }
384}
385pub(crate) fn make_response(status: u16, body: String) -> Value {
386 make_response_with_url(status, body, String::new())
387}
388pub(crate) fn response_init_status(init: &Value, default: u16) -> u16 {
390 match init {
391 Value::Object(o) => o
392 .borrow()
393 .props
394 .get("status")
395 .map(|v| v.to_number() as u16)
396 .unwrap_or(default),
397 _ => default,
398 }
399}
400pub(crate) fn response_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
402 let body = match arg(a, 0) {
403 Value::Undefined => String::new(),
404 v => v.to_js_string(),
405 };
406 let init = arg(a, 1);
407 let status = response_init_status(&init, 200);
408 let headers_init = match &init {
409 Value::Object(o) => o.borrow().props.get("headers").cloned().unwrap_or(Value::Undefined),
410 _ => Value::Undefined,
411 };
412 let resp = make_response(status, body);
413 if let Value::Object(r) = &resp {
414 r.borrow_mut().props.insert("headers".into(), make_headers(headers_init));
415 }
416 Ok(resp)
417}
418pub(crate) fn response_json_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
421 let body = json_stringify(it, Value::Undefined, &[arg(a, 0)])?.to_js_string();
422 let status = response_init_status(&arg(a, 1), 200);
423 let resp = make_response(status, body);
424 if let Value::Object(r) = &resp {
425 let headers = make_headers(Value::Undefined);
426 if let Value::Object(h) = &headers {
427 h.borrow_mut()
428 .props
429 .insert("content-type".into(), Value::str("application/json"));
430 }
431 r.borrow_mut().props.insert("headers".into(), headers);
432 }
433 Ok(resp)
434}
435pub(crate) fn response_error_static(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
438 let resp = make_response(0, String::new());
439 if let Value::Object(r) = &resp {
440 let mut b = r.borrow_mut();
441 b.props.insert("type".into(), Value::str("error"));
442 b.props.insert("ok".into(), Value::Bool(false));
443 }
444 Ok(resp)
445}
446pub(crate) fn response_redirect_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
449 let url = arg(a, 0).to_js_string();
450 let status = match arg(a, 1) {
451 Value::Undefined => 302,
452 v => v.to_number() as u16,
453 };
454 let resp = make_response(status, String::new());
455 if let Value::Object(r) = &resp {
456 let headers = make_headers(Value::Undefined);
457 if let Value::Object(h) = &headers {
458 h.borrow_mut().props.insert("location".into(), Value::str(url));
459 }
460 r.borrow_mut().props.insert("headers".into(), headers);
461 }
462 Ok(resp)
463}
464
465pub(crate) fn make_response_with_url(status: u16, body: String, url: String) -> Value {
470 let resp = Obj::plain();
471 {
472 let mut r = resp.borrow_mut();
473 r.props
474 .insert("status".into(), Value::Number(status as f64));
475 r.props
476 .insert("ok".into(), Value::Bool((200..300).contains(&status)));
477 r.props
478 .insert("statusText".into(), Value::str(http_status_text(status)));
479 r.props.insert("url".into(), Value::str(url));
480 r.props.insert("type".into(), Value::str("basic"));
484 r.props.insert("redirected".into(), Value::Bool(false));
485 r.props.insert("headers".into(), make_headers(Value::Undefined));
490 r.props.insert("_body".into(), Value::str(body));
491 r.props.insert("text".into(), nv("text", response_text));
492 r.props.insert("json".into(), nv("json", response_json));
493 r.props.insert("blob".into(), nv("blob", response_blob));
496 r.props
499 .insert("arrayBuffer".into(), nv("arrayBuffer", response_array_buffer));
500 r.props.insert("clone".into(), nv("clone", response_clone));
504 }
505 Value::Object(resp)
506}
507
508pub(crate) fn response_clone(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
513 if let Value::Object(o) = &this {
514 let b = o.borrow();
515 let status = b.props.get("status").map(|v| v.to_number() as u16).unwrap_or(200);
516 let body = b.props.get("_body").map(|v| v.to_js_string()).unwrap_or_default();
517 let url = b.props.get("url").map(|v| v.to_js_string()).unwrap_or_default();
518 drop(b);
519 return Ok(make_response_with_url(status, body, url));
520 }
521 Ok(this)
522}
523
524pub(crate) fn response_body_string(this: &Value) -> String {
526 if let Value::Object(o) = this {
527 if let Some(v) = o.borrow().props.get("_body") {
528 return v.to_js_string();
529 }
530 }
531 String::new()
532}
533
534pub(crate) fn response_text(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
536 let body = response_body_string(&this);
537 Ok(resolved_promise(it, Value::str(body)))
538}
539
540pub(crate) fn response_json(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
542 let body = response_body_string(&this);
543 let parsed = json_parse(it, Value::Undefined, &[Value::str(body)])?;
544 Ok(resolved_promise(it, parsed))
545}
546
547pub(crate) fn response_blob(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
549 let body = response_body_string(&this);
550 let bytes: alloc::vec::Vec<f64> = body.as_bytes().iter().map(|b| *b as f64).collect();
551 let blob = make_blob(bytes, String::new());
552 Ok(resolved_promise(it, blob))
553}
554
555pub(crate) fn response_array_buffer(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
558 let body = response_body_string(&this);
559 let bytes: alloc::vec::Vec<f64> = body.as_bytes().iter().map(|b| *b as f64).collect();
560 let buf = make_arraybuffer_with_bytes(bytes);
561 Ok(resolved_promise(it, buf))
562}
563
564pub(crate) fn location_to_string(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
566 if let Value::Object(o) = &this {
567 if let Some(v) = o.borrow().props.get("href") {
568 return Ok(v.clone());
569 }
570 }
571 Ok(Value::str(""))
572}
573
574pub(crate) fn queue_location_nav(this: &Value, url: &str, mode: &str) {
578 if let Value::Object(o) = this {
579 let mut b = o.borrow_mut();
580 b.props
581 .insert(String::from("_pending_location"), Value::str(url));
582 b.props
583 .insert(String::from("_pending_location_mode"), Value::str(mode));
584 }
585}
586
587pub(crate) fn location_assign(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
589 let url = a.first().map(|v| v.to_js_string()).unwrap_or_default();
590 if !url.is_empty() {
591 queue_location_nav(&this, &url, "assign");
592 }
593 Ok(Value::Undefined)
594}
595
596pub(crate) fn location_replace(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
598 let url = a.first().map(|v| v.to_js_string()).unwrap_or_default();
599 if !url.is_empty() {
600 queue_location_nav(&this, &url, "replace");
601 }
602 Ok(Value::Undefined)
603}
604
605pub(crate) fn location_reload(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
607 let cur = if let Value::Object(o) = &this {
609 o.borrow()
610 .props
611 .get("href")
612 .map(|v| v.to_js_string())
613 .unwrap_or_default()
614 } else {
615 String::new()
616 };
617 queue_location_nav(&this, &cur, "reload");
618 Ok(Value::Undefined)
619}
620
621pub(crate) fn location_components(url: &str) -> Vec<(&'static str, String)> {
624 let (protocol, after) = if let Some(a) = url.strip_prefix("https://") {
625 ("https:", a)
626 } else if let Some(a) = url.strip_prefix("http://") {
627 ("http:", a)
628 } else {
629 ("", url)
630 };
631 let (hostport, pathpart) = match after.split_once('/') {
632 Some((h, p)) => (String::from(h), format!("/{}", p)),
633 None => (String::from(after), String::from("/")),
634 };
635 let (userinfo, hostport) = match hostport.split_once('@') {
642 Some((ui, hp)) => (String::from(ui), String::from(hp)),
643 None => (String::new(), hostport),
644 };
645 let (username, password) = match userinfo.split_once(':') {
646 Some((u, p)) => (String::from(u), String::from(p)),
647 None => (userinfo.clone(), String::new()),
648 };
649 let (before_hash, hash) = match pathpart.split_once('#') {
650 Some((b, h)) => (String::from(b), format!("#{}", h)),
651 None => (pathpart.clone(), String::new()),
652 };
653 let (pathname, search) = match before_hash.split_once('?') {
654 Some((p, q)) => (String::from(p), format!("?{}", q)),
655 None => (before_hash.clone(), String::new()),
656 };
657 let (hostname, port) = match hostport.split_once(':') {
658 Some((h, p)) => (String::from(h), String::from(p)),
659 None => (hostport.clone(), String::new()),
660 };
661 let origin = if protocol.is_empty() {
662 String::new()
663 } else {
664 format!("{}//{}", protocol, hostport)
665 };
666 alloc::vec![
667 ("href", String::from(url)),
668 ("protocol", String::from(protocol)),
669 ("username", username),
670 ("password", password),
671 ("host", hostport),
672 ("hostname", hostname),
673 ("port", port),
674 ("origin", origin),
675 ("pathname", pathname),
676 ("search", search),
677 ("hash", hash),
678 ]
679}
680
681pub fn location_href(global: &Rc<RefCell<Scope>>) -> Option<String> {
683 let loc = match global.borrow().vars.get("location") {
684 Some(Value::Object(o)) => o.clone(),
685 _ => return None,
686 };
687 let href = loc
688 .borrow()
689 .props
690 .get("href")
691 .map(|v| v.to_js_string())
692 .unwrap_or_default();
693 if href.is_empty() {
694 None
695 } else {
696 Some(href)
697 }
698}
699
700pub fn location_hostname(global: &Rc<RefCell<Scope>>) -> Option<String> {
703 let loc = match global.borrow().vars.get("location") {
704 Some(Value::Object(o)) => o.clone(),
705 _ => return None,
706 };
707 let hostname = loc.borrow().props.get("hostname").map(|v| v.to_js_string());
708 hostname
709}
710
711pub fn format_last_modified() -> String {
715 let (y, mo, d, hh, mm, ss, ..) = decompose_ms(epoch_ms_now());
716 alloc::format!("{mo:02}/{d:02}/{y:04} {hh:02}:{mm:02}:{ss:02}")
717}
718
719pub(crate) fn history_change_state(
722 it: &mut Interp,
723 this: &Value,
724 a: &[Value],
725 push: bool,
726) -> Result<Value, Value> {
727 let new_state = arg(a, 0);
728 let url_arg = arg(a, 2);
729
730 if let Value::Object(o) = this {
731 if push {
732 let cur_url = location_href(&it.global).unwrap_or_default();
734 let cur_state = o.borrow().props.get("state").cloned().unwrap_or(Value::Null);
735 let stack_val = o.borrow().props.get("_spa_back_stack").cloned();
736 let stack = match stack_val {
737 Some(Value::Object(arr)) => arr,
738 _ => Obj::array(alloc::vec![]),
739 };
740 let entry = Obj::plain();
741 {
742 let mut eb = entry.borrow_mut();
743 eb.props.insert("url".into(), Value::str(cur_url));
744 eb.props.insert("state".into(), cur_state);
745 }
746 if let ObjKind::Array(items) = &mut stack.borrow_mut().kind {
747 items.push(Value::Object(entry));
748 }
749 o.borrow_mut().props.insert("_spa_back_stack".into(), Value::Object(stack));
750 o.borrow_mut().props.shift_remove("_spa_fwd_stack");
752 }
753
754 if !matches!(url_arg, Value::Undefined | Value::Null) {
755 let url = url_arg.to_js_string();
756 if !url.is_empty() {
757 let base = location_href(&it.global).unwrap_or_default();
758 let resolved = resolve_url(&base, &url);
759 update_location(&it.global, &resolved);
760 it.base_url = resolved;
761 }
762 }
763
764 let mut b = o.borrow_mut();
765 b.props.insert("state".into(), new_state);
766 if push {
767 let len = match b.props.get("length") {
768 Some(Value::Number(n)) => *n,
769 _ => 1.0,
770 };
771 b.props.insert("length".into(), Value::Number(len + 1.0));
772 }
773 } else {
774 if !matches!(url_arg, Value::Undefined | Value::Null) {
775 let url = url_arg.to_js_string();
776 if !url.is_empty() {
777 let base = location_href(&it.global).unwrap_or_default();
778 let resolved = resolve_url(&base, &url);
779 update_location(&it.global, &resolved);
780 it.base_url = resolved;
781 }
782 }
783 }
784 Ok(Value::Undefined)
785}
786
787pub(crate) fn history_push_state(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
788 history_change_state(it, &this, a, true)
789}
790
791pub(crate) fn history_replace_state(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
792 history_change_state(it, &this, a, false)
793}
794
795pub(crate) fn set_pending_nav(global: &Rc<RefCell<Scope>>, delta: i64) {
800 if delta == 0 {
801 return;
802 }
803 if let Some(Value::Object(h)) = global.borrow().vars.get("history") {
804 let cur = match h.borrow().props.get("_pending_nav") {
805 Some(Value::Number(n)) => *n as i64,
806 _ => 0,
807 };
808 h.borrow_mut()
809 .props
810 .insert("_pending_nav".into(), Value::Number((cur + delta) as f64));
811 }
812}
813
814pub(crate) fn history_back(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
815 set_pending_nav(&it.global, -1);
816 Ok(Value::Undefined)
817}
818
819pub(crate) fn history_forward(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
820 set_pending_nav(&it.global, 1);
821 Ok(Value::Undefined)
822}
823
824pub(crate) fn history_go(it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
825 let n = arg(a, 0).to_number();
827 if n.is_finite() {
828 set_pending_nav(&it.global, n as i64);
829 }
830 Ok(Value::Undefined)
831}
832