Skip to main content

atmos/os_lib/js/builtins/
window_api.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5// ===== window.confirm / window.prompt =====
6/// `window.confirm(message)`(丸ごと未対応だった。`window.alert`は`console_
7/// log`で無害に代替されていたが、対になる`confirm`/`prompt`自体がグローバル
8/// にも`window`にも一切登録されておらず、呼び出すと`ReferenceError`/
9/// `TypeError`になっていた。2026-07-16 発見・実装)。この処理系にはブロッキング
10/// モーダルダイアログ機構が無いため、メッセージを`console_log`と同じ経路へ
11/// 出力したうえで、常に「OK」を選択したものとして`true`を返す(`alert`と
12/// 同じ「実際のUIは出さないが呼び出し自体は安全に完了させる」簡略方針)。
13pub(crate) fn window_confirm(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
14    let _ = console_log(it, this, a);
15    Ok(Value::Bool(true))
16}
17/// `window.prompt(message, default)`(丸ごと未対応だった。理由は`window_
18/// confirm`と同じ)。`default`引数があればそれをそのまま返す(ユーザーが
19/// 何も変更せず「OK」を押した相当)。無指定時は仕様どおり`null`。
20pub(crate) fn window_prompt(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
21    let _ = console_log(it, this, a);
22    match a.get(1) {
23        Some(v) if !matches!(v, Value::Undefined) => Ok(v.clone()),
24        _ => Ok(Value::Null),
25    }
26}
27
28// ===== window.open / window.close / window.focus / window.blur =====
29/// `window.open(url, target, features)`(丸ごと未対応だった。`confirm`/
30/// `prompt`と同じ監査で発見。`typeof window.open === 'function'`という
31/// 定番のフィーチャー検出ガードや、ポップアップを試みるコードが
32/// `TypeError`で丸ごと落ちる状態だった。2026-07-16 発見・実装)。この処理系
33/// には複数ウィンドウ/ポップアップを新規に開く仕組みが無いため、実際には
34/// 何も開かず、仕様上「開けなかった(ポップアップブロック等)」場合と
35/// 同じ`null`を返す誠実な簡略実装(ブラウザのポップアップブロッカーが
36/// `window.open()`を拒否した場合の正規の戻り値と同じ形)。
37pub(crate) fn window_open(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
38    Ok(Value::Null)
39}
40/// `window.close()`(丸ごと未対応だった。理由は`window_open`と同じ)。
41/// この処理系にはウィンドウを閉じる操作自体が無いため無害な no-op
42/// (`window.print()`と同方針)。
43pub(crate) fn window_close(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
44    Ok(Value::Undefined)
45}
46/// `window.stop()`(丸ごと未対応だった。ブラウザの「読み込み中止」ボタン
47/// に相当する定番API。2026-07-17 発見)。この処理系はページ全体を同期的に
48/// パースしてから`<script>`を実行するため、途中で中断できる非同期の
49/// 読み込み処理自体が無く、`window.close`/`.print`と同じ無害なno-op。
50pub(crate) fn window_stop(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
51    Ok(Value::Undefined)
52}
53/// `window.focus()`/`.blur()`(丸ごと未対応だった。理由は`window_open`と
54/// 同じ)。この処理系にはウィンドウ/タブという概念自体が無く(
55/// `document.hasFocus()`が常に`true`固定なのと同じ理由)、フォーカス要求
56/// 先も存在しないため無害な no-op。
57pub(crate) fn window_focus(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
58    Ok(Value::Undefined)
59}
60
61// ===== window.scrollY / scrollTo / scrollBy / scrollIntoView =====
62
63/// `window.scrollTo(x, y)`/`window.scroll(x, y)`(丸ごと未対応だった。以前は
64/// `SCROLL_Y` という孤立した `static` へ書き込むだけの実装で、`window.
65/// scrollY`(`fire_scroll`経由でレンダラが更新する別経路)にも実際の描画位置
66/// にも一切反映されない死んだ経路だった。2026-07-16 発見・修正)。
67/// `DomBridge::pending_scroll_abs_y`へ保留し、`render.rs`の`draw()`冒頭で
68/// 消費・`WebEngine::scroll_y`へ適用・`fire_scroll()`呼び出しまで行う。
69pub(crate) fn window_scroll_to(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
70    let y = if let Some(Value::Object(o)) = a.first() {
71        o.borrow()
72            .props
73            .get("top")
74            .map(|v| v.to_number())
75            .unwrap_or(0.0)
76    } else {
77        arg(a, 1).to_number()
78    };
79    it.dom.borrow_mut().pending_scroll_abs_y = Some(y as i32);
80    Ok(Value::Undefined)
81}
82/// `window.scrollBy(x, y)`(丸ごと未対応だった。`dom_noop`のまま何もせず、
83/// 相対スクロールが一切機能しなかった。2026-07-16 発見・実装)。複数回
84/// 呼ばれた場合は次の描画までの分を加算する。
85pub(crate) fn window_scroll_by(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
86    let dy = if let Some(Value::Object(o)) = a.first() {
87        o.borrow()
88            .props
89            .get("top")
90            .map(|v| v.to_number())
91            .unwrap_or(0.0)
92    } else {
93        arg(a, 1).to_number()
94    };
95    let mut dom = it.dom.borrow_mut();
96    dom.pending_scroll_by_y = Some(dom.pending_scroll_by_y.unwrap_or(0) + dy as i32);
97    Ok(Value::Undefined)
98}
99pub(crate) fn element_scroll_into_view(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
100    Ok(Value::Undefined) // ページ内スクロール:AtmOS web_engine が管理するため no-op
101}
102
103// ===== window.matchMedia =====
104pub(crate) fn window_match_media(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
105    let query = arg(a, 0).to_js_string();
106    // `window.matchMedia()` は `max-width`/`min-width` の狭い専用判定のみで、
107    // 同じ `@media` 構文を対象とする CSS 側の `css::media_query_matches`
108    // (`prefers-color-scheme`/`hover`/`pointer`/`resolution`/`monochrome`/
109    // `forced-colors`/`inverted-colors`/`color-gamut`/複合条件/カンマ区切り等、
110    // 多数のバグ修正を経て充実済み)と食い違う非対称なバグだった。同じ判定
111    // ロジックを再利用して一致させる。
112    let matches = crate::os_lib::css::media_query_matches(&query);
113    let mql = Obj::plain();
114    {
115        let mut b = mql.borrow_mut();
116        b.props.insert("matches".into(), Value::Bool(matches));
117        b.props.insert("media".into(), Value::str(query));
118        b.props
119            .insert("addEventListener".into(), nv("addEventListener", dom_noop));
120        b.props
121            .insert("addListener".into(), nv("addListener", dom_noop));
122        b.props
123            .insert("removeListener".into(), nv("removeListener", dom_noop));
124        b.props.insert(
125            "removeEventListener".into(),
126            nv("removeEventListener", dom_noop),
127        );
128    }
129    Ok(Value::Object(mql))
130}
131
132/// window の隠し props 配列(ObjKind::Array)へリスナを追記する。
133/// scroll/popstate など、eval をまたいで永続させたいリスナの保存に使う
134/// (Interp ローカルだと eval 毎に破棄されるため window オブジェクトに退避)。
135/// エントリは `addEventListener` の `{signal}` 対応(2026-07-14 発見・実装)
136/// のため `[cb, signal]` 形式で格納する(`event_target_*` 系の `[cb, once,
137/// signal]` と同系統。`window` 系イベントには `once` 概念が無いためこちら
138/// は2要素)。`window_listeners`(interp.rs)/`_message_listeners`/
139/// `_error_listeners` の発火経路側で `event_target_listener_cb`/
140/// `window_listener_aborted` を使って読み解く。
141pub(crate) fn push_window_listener(it: &mut Interp, key: &str, cb: Value, signal: Option<Value>) {
142    let w = match it.global.borrow().vars.get("window") {
143        Some(Value::Object(w)) => w.clone(),
144        _ => return,
145    };
146    let entry = Value::Object(Obj::array(alloc::vec![cb, signal.unwrap_or(Value::Undefined)]));
147    let existing = w.borrow().props.get(key).cloned();
148    match existing {
149        Some(Value::Object(arr)) if matches!(arr.borrow().kind, ObjKind::Array(_)) => {
150            if let ObjKind::Array(items) = &mut arr.borrow_mut().kind {
151                items.push(entry);
152            }
153        }
154        _ => {
155            let arr = Obj::array(alloc::vec![entry]);
156            w.borrow_mut().props.insert(key.into(), Value::Object(arr));
157        }
158    }
159}
160/// `push_window_listener` の格納エントリ `[cb, signal]` の `signal` が
161/// abort 済みかどうか。
162pub(crate) fn window_listener_aborted(entry: &Value) -> bool {
163    if let Value::Object(o) = entry {
164        if let ObjKind::Array(items) = &o.borrow().kind {
165            if let Some(sig) = items.get(1) {
166                return super::super::dom_bridge::is_signal_aborted(sig);
167            }
168        }
169    }
170    false
171}
172
173/// window.addEventListener('load'|'DOMContentLoaded', cb) → load は即時発火。
174/// 'scroll' は window の隠し props `_scroll_listeners`、'popstate' は `_popstate_listeners`、
175/// 'hashchange' は `_hashchange_listeners`、'resize' は `_resize_listeners`
176/// 配列へ永続記録(該当イベント発生時にホストが発火)。
177pub(crate) fn window_add_event_listener(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
178    let event = arg(a, 0).to_js_string();
179    let cb = arg(a, 1);
180    // `{signal}`(丸ごと未対応だった。`AbortController` でリスナを一括解除する
181    // 定番パターン。2026-07-14 発見・実装)。既に abort 済みなら登録自体を
182    // 行わない(DOM 仕様どおり)。
183    let signal = parse_listener_signal(&arg(a, 2));
184    if signal.as_ref().is_some_and(super::super::dom_bridge::is_signal_aborted) {
185        return Ok(Value::Undefined);
186    }
187    match event.as_str() {
188        // `readystatechange`(丸ごと未対応だった。`document.readyState`は
189        // 常に `"complete"` を返す簡略実装のため、`load`/`DOMContentLoaded`
190        // と同じく登録時点で即時発火する)。
191        "load" | "DOMContentLoaded" | "readystatechange" => {
192            // 例外を捨てるとページ初期化が途中で止まった理由が消える。
193            it.call_listener(&cb, Value::Undefined, &[], "window load listener");
194        }
195        "scroll" => {
196            push_window_listener(it, "_scroll_listeners", cb, signal);
197        }
198        "popstate" => {
199            push_window_listener(it, "_popstate_listeners", cb, signal);
200        }
201        "hashchange" => {
202            push_window_listener(it, "_hashchange_listeners", cb, signal);
203        }
204        "resize" => {
205            push_window_listener(it, "_resize_listeners", cb, signal);
206        }
207        // `online`/`offline`(丸ごと未対応だった。`navigator.onLine` は既に
208        // 実装済みだったが、対応するイベント種別自体が登録経路すら無く
209        // `addEventListener('online', ...)` が黙って何もしないバグだった。
210        // この処理系には実際のネットワーク接続状態変化検知機構が無いため、
211        // 実発火は無い(`window.dispatchEvent()` 経由の手動シミュレーション
212        // のみサポート)。
213        "online" => {
214            push_window_listener(it, "_online_listeners", cb, signal);
215        }
216        "offline" => {
217            push_window_listener(it, "_offline_listeners", cb, signal);
218        }
219        // `beforeunload`/`unload`/`pageshow`/`pagehide`/`visibilitychange`/
220        // `languagechange`(丸ごと未対応だった。`online`/`offline` と同じ
221        // 理由で登録経路自体が無かった)。この処理系にはページ遷移/タブ
222        // 可視性変化/言語設定変更のいずれの検知機構も無いため、`online`/
223        // `offline` と同じく実発火は無く手動シミュレーションのみサポート。
224        "beforeunload" => {
225            push_window_listener(it, "_beforeunload_listeners", cb, signal);
226        }
227        "unload" => {
228            push_window_listener(it, "_unload_listeners", cb, signal);
229        }
230        "pageshow" => {
231            push_window_listener(it, "_pageshow_listeners", cb, signal);
232        }
233        "pagehide" => {
234            push_window_listener(it, "_pagehide_listeners", cb, signal);
235        }
236        "visibilitychange" => {
237            push_window_listener(it, "_visibilitychange_listeners", cb, signal);
238        }
239        "languagechange" => {
240            push_window_listener(it, "_languagechange_listeners", cb, signal);
241        }
242        // `storage`(他タブ/ウィンドウでの `localStorage`/`sessionStorage`
243        // 変更通知。丸ごと未対応だった。`StorageEvent` コンストラクタ自体は
244        // 既に対応済みだったが、対応するイベント種別の登録経路が無かった)。
245        // この処理系には実際の複数タブ間通知機構が無いため、`online`/
246        // `offline` と同じく手動シミュレーションのみサポート。
247        "storage" => {
248            push_window_listener(it, "_storage_listeners", cb, signal);
249        }
250        // `beforeprint`/`afterprint`(`window.print()`の前後に発火する
251        // 印刷ダイアログ連動イベント。丸ごと未対応だった)。この処理系には
252        // 実際の印刷パイプライン自体が無いため、`online`/`offline` と同じく
253        // 手動シミュレーションのみサポート。
254        "beforeprint" => {
255            push_window_listener(it, "_beforeprint_listeners", cb, signal);
256        }
257        "afterprint" => {
258            push_window_listener(it, "_afterprint_listeners", cb, signal);
259        }
260        // `window.postMessage()` の受信側。`_message_listeners` は
261        // `window_post_message` が同期的に配送する。
262        "message" => {
263            push_window_listener(it, "_message_listeners", cb, signal);
264        }
265        // `reportError()`/未捕捉例外の受信側。`_error_listeners` は
266        // `report_error` が同期的に配送する。
267        "error" => {
268            push_window_listener(it, "_error_listeners", cb, signal);
269        }
270        _ => {}
271    }
272    Ok(Value::Undefined)
273}
274
275/// `window.removeEventListener(type, cb)`(丸ごと未対応だった。以前は
276/// 無害な `domNoop` のままで、`window.addEventListener` で登録した
277/// リスナが `AbortSignal` 経由以外では二度と解除できないバグだった)。
278/// `window_add_event_listener` と同じ6種の固定イベント種別に限り対応する
279/// 隠し配列から、参照が一致するエントリを取り除く(`event_target_remove_
280/// event_listener` と同じ `Rc::ptr_eq` 比較パターン)。
281pub(crate) fn window_remove_event_listener(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
282    let event = arg(a, 0).to_js_string();
283    let cb = arg(a, 1);
284    let key = match event.as_str() {
285        "scroll" => "_scroll_listeners",
286        "popstate" => "_popstate_listeners",
287        "hashchange" => "_hashchange_listeners",
288        "resize" => "_resize_listeners",
289        "online" => "_online_listeners",
290        "offline" => "_offline_listeners",
291        "beforeunload" => "_beforeunload_listeners",
292        "unload" => "_unload_listeners",
293        "pageshow" => "_pageshow_listeners",
294        "pagehide" => "_pagehide_listeners",
295        "visibilitychange" => "_visibilitychange_listeners",
296        "languagechange" => "_languagechange_listeners",
297        "storage" => "_storage_listeners",
298        "beforeprint" => "_beforeprint_listeners",
299        "afterprint" => "_afterprint_listeners",
300        "message" => "_message_listeners",
301        "error" => "_error_listeners",
302        _ => return Ok(Value::Undefined),
303    };
304    let w = match it.global.borrow().vars.get("window") {
305        Some(Value::Object(w)) => w.clone(),
306        _ => return Ok(Value::Undefined),
307    };
308    let arr = match w.borrow().props.get(key) {
309        Some(Value::Object(arr)) => arr.clone(),
310        _ => return Ok(Value::Undefined),
311    };
312    if let ObjKind::Array(items) = &mut arr.borrow_mut().kind {
313        items.retain(|entry| {
314            let stored_cb = match entry {
315                Value::Object(o) => match &o.borrow().kind {
316                    ObjKind::Array(pair) => pair.first().cloned().unwrap_or(Value::Undefined),
317                    _ => entry.clone(),
318                },
319                _ => entry.clone(),
320            };
321            match (&stored_cb, &cb) {
322                (Value::Object(x), Value::Object(y)) => !Rc::ptr_eq(x, y),
323                _ => true,
324            }
325        });
326    }
327    Ok(Value::Undefined)
328}
329/// `window.dispatchEvent(event)`(丸ごと未対応だった。`window` オブジェクトに
330/// `addEventListener`/`removeEventListener` はあったが `dispatchEvent` 自体が
331/// 存在せず、`window.dispatchEvent(new HashChangeEvent(...))` のような JS 側
332/// からのイベントシミュレーションが一切できなかった。`window_add_event_
333/// listener` が対応する6種の固定イベント種別(`scroll`/`popstate`/
334/// `hashchange`/`resize`/`message`/`error`)に限り、対応する隠し配列
335/// (`push_window_listener`が書き込む先)からリスナを読み出し、渡された
336/// `event` オブジェクトをそのまま転送して呼び出す(`fire_window_listeners`
337/// のような独自 Event 再構築は行わず、JS 側が構築した実際のオブジェクトを
338/// そのまま渡す)。対応外の種別は無害に無視する(`addEventListener`側の
339/// 未対応種別の扱いと同じ)。`window.onerror`(`report_error`側で既に
340/// 対応済み)と同様に、`window.on<type>`(`window.onhashchange = fn`等の
341/// IDL属性スタイルのハンドラ)が設定されていればそれも合わせて呼び出す。
342pub(crate) fn window_dispatch_event(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
343    let event = arg(a, 0);
344    let event_type = obj_prop(&event, "type").map(|v| v.to_js_string()).unwrap_or_default();
345    let key = match event_type.as_str() {
346        "scroll" => "_scroll_listeners",
347        "popstate" => "_popstate_listeners",
348        "hashchange" => "_hashchange_listeners",
349        "resize" => "_resize_listeners",
350        "online" => "_online_listeners",
351        "offline" => "_offline_listeners",
352        "beforeunload" => "_beforeunload_listeners",
353        "unload" => "_unload_listeners",
354        "pageshow" => "_pageshow_listeners",
355        "pagehide" => "_pagehide_listeners",
356        "visibilitychange" => "_visibilitychange_listeners",
357        "languagechange" => "_languagechange_listeners",
358        "storage" => "_storage_listeners",
359        "beforeprint" => "_beforeprint_listeners",
360        "afterprint" => "_afterprint_listeners",
361        "message" => "_message_listeners",
362        "error" => "_error_listeners",
363        _ => return Ok(Value::Bool(true)),
364    };
365    let w = match it.global.borrow().vars.get("window") {
366        Some(Value::Object(w)) => w.clone(),
367        _ => return Ok(Value::Bool(true)),
368    };
369    // dispatch 対象(`window.dispatchEvent` なら window、`document.dispatchEvent`
370    // なら document)を `event.target`/`srcElement`/`currentTarget` に設定する
371    // (`event_target_dispatch_event` と同じイディオム。以前はこれらを一切
372    // 設定せず、リスナ内で `e.target` を読むと常に `undefined` だった)。
373    // `this` が取れない経路では window にフォールバックする。
374    let target = if matches!(this, Value::Object(_)) {
375        this.clone()
376    } else {
377        Value::Object(w.clone())
378    };
379    if let Value::Object(eo) = &event {
380        let mut e = eo.borrow_mut();
381        e.props.insert("target".into(), target.clone());
382        e.props.insert("srcElement".into(), target.clone());
383        e.props.insert("currentTarget".into(), target);
384        // window/document には DOM ツリーの伝播フェーズが無いため AT_TARGET(2)。
385        e.props.insert("eventPhase".into(), Value::Number(2.0));
386    }
387    let on_handler = w.borrow().props.get(&alloc::format!("on{event_type}")).cloned();
388    if let Some(f) = on_handler.and_then(callable_or_none) {
389        // `window.onerror` は HTML 仕様上 `GlobalEventHandlers.onerror` として
390        // 特別扱いされ、`ErrorEvent` 経由でディスパッチされた場合もレガシー
391        // 5引数シグネチャ `(message, source, lineno, colno, error)` で呼ばれる
392        // (`reportError()`〔`report_error`〕は既にこの形で呼んでいたが、
393        // `window.dispatchEvent(new ErrorEvent(...))`/`document.dispatchEvent
394        // (new ErrorEvent(...))` 経由では Event オブジェクトをそのまま1引数で
395        // 渡してしまい、2経路で `onerror` の呼び出し形が食い違っていた。
396        // 2026-07-16 発見・修正)。
397        if event_type == "error" {
398            let (message, filename, lineno, colno, err) = if let Value::Object(eo) = &event {
399                let b = eo.borrow();
400                (
401                    b.props.get("message").cloned().unwrap_or_else(|| Value::str("")),
402                    b.props.get("filename").cloned().unwrap_or_else(|| Value::str("")),
403                    b.props.get("lineno").cloned().unwrap_or(Value::Number(0.0)),
404                    b.props.get("colno").cloned().unwrap_or(Value::Number(0.0)),
405                    b.props.get("error").cloned().unwrap_or(Value::Null),
406                )
407            } else {
408                (Value::str(""), Value::str(""), Value::Number(0.0), Value::Number(0.0), Value::Null)
409            };
410            it.call_listener(
411                &f,
412                Value::Undefined,
413                &[message, filename, lineno, colno, err],
414                "window.onerror",
415            );
416        } else {
417            it.call_listener(
418                &f,
419                Value::Undefined,
420                core::slice::from_ref(&event),
421                "window.onerror",
422            );
423        }
424    }
425    let entries = match w.borrow().props.get(key) {
426        Some(Value::Object(arr)) => match &arr.borrow().kind {
427            ObjKind::Array(items) => items.clone(),
428            _ => alloc::vec::Vec::new(),
429        },
430        _ => alloc::vec::Vec::new(),
431    };
432    for entry in entries {
433        let (cb, signal) = match &entry {
434            Value::Object(o) => match &o.borrow().kind {
435                ObjKind::Array(items) => (items.first().cloned().unwrap_or(Value::Undefined), items.get(1).cloned()),
436                _ => (entry.clone(), None),
437            },
438            _ => (entry.clone(), None),
439        };
440        if signal.as_ref().is_some_and(super::super::dom_bridge::is_signal_aborted) {
441            continue;
442        }
443        it.call_listener(
444            &cb,
445            Value::Undefined,
446            core::slice::from_ref(&event),
447            "window listener",
448        );
449    }
450    // 発火完了後は仕様どおり `currentTarget: null` / `eventPhase: NONE(0)` へ戻す
451    // (`target` は発火後の `event.target` 参照用にそのまま残す)。
452    if let Value::Object(eo) = &event {
453        let mut e = eo.borrow_mut();
454        e.props.insert("currentTarget".into(), Value::Null);
455        e.props.insert("eventPhase".into(), Value::Number(0.0));
456    }
457    // 仕様どおり `!event.defaultPrevented` を返す。以前は無条件 `true` を返して
458    // おり、リスナが `preventDefault()` を呼んでも `window.dispatchEvent()`/
459    // `document.dispatchEvent()` が常に `true` を返すバグだった(要素用
460    // `dom_dispatch_event`/`event_target_dispatch_event` は正しく実装済みで、
461    // window/document 経路にだけ同型バグが残っていた)。
462    let default_prevented = if let Value::Object(eo) = &event {
463        matches!(eo.borrow().props.get("defaultPrevented"), Some(Value::Bool(true)))
464    } else {
465        false
466    };
467    Ok(Value::Bool(!default_prevented))
468}
469
470/// `document.addEventListener`/`removeEventListener`/`dispatchEvent` が
471/// `window` と共有する固定イベント種別の集合(`window_add_event_listener`の
472/// match armと同じ)。
473fn is_window_shared_event_type(t: &str) -> bool {
474    matches!(
475        t,
476        "load"
477            | "DOMContentLoaded"
478            | "readystatechange"
479            | "scroll"
480            | "popstate"
481            | "hashchange"
482            | "resize"
483            | "online"
484            | "offline"
485            | "beforeunload"
486            | "unload"
487            | "pageshow"
488            | "pagehide"
489            | "visibilitychange"
490            | "languagechange"
491            | "storage"
492            | "beforeprint"
493            | "afterprint"
494            | "message"
495            | "error"
496    )
497}
498
499/// `document.addEventListener(type, fn, options)`。`window` 固有の固定
500/// イベント種別(`scroll`/`hashchange`/`error`等)は従来どおり `window`と
501/// 共有する隠し配列へ登録するが、それ以外の汎用種別(`click`/`keydown`等)は
502/// `document`自身が持つ`EventTarget`風の`_listeners`(`event_target_add_
503/// event_listener`と同じ格納形式)へ登録する。実クリック等の実DOMイベント
504/// バブリングの最終到達点として`dispatch_event_in_interp`
505/// (`interp/calls.rs`)がこちらを読み出す(丸ごと未対応だった。以前は
506/// 固定種別以外を`window_add_event_listener`の`_ => {}`アームで黙って
507/// 破棄しており、`document.addEventListener('click', fn)`という定番の
508/// イベント委譲パターンが登録すら行われず一切機能しなかった。2026-07-16
509/// 発見・実装)。
510pub(crate) fn document_add_event_listener(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
511    let etype = arg(a, 0).to_js_string();
512    if is_window_shared_event_type(&etype) {
513        return window_add_event_listener(it, this, a);
514    }
515    event_target_add_event_listener(it, this, a)
516}
517/// `document.removeEventListener`。`document_add_event_listener`と対になる
518/// 振り分け(丸ごと未対応だった。2026-07-16 発見・実装)。
519pub(crate) fn document_remove_event_listener(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
520    let etype = arg(a, 0).to_js_string();
521    if is_window_shared_event_type(&etype) {
522        return window_remove_event_listener(it, this, a);
523    }
524    event_target_remove_event_listener(it, this, a)
525}
526/// `document.dispatchEvent(event)`。JS側から直接`document.dispatchEvent(new
527/// Event('click'))`のように合成発火する経路(実クリックのDOMバブリング経由の
528/// 到達は`dispatch_event_in_interp`側の別経路)。固定種別以外は`document`
529/// 自身をターゲットとして`event_target_dispatch_event`と同じロジックで
530/// 発火する(丸ごと未対応だった。2026-07-16 発見・実装)。
531pub(crate) fn document_dispatch_event(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
532    let event = arg(a, 0);
533    let etype = obj_prop(&event, "type").map(|v| v.to_js_string()).unwrap_or_default();
534    if is_window_shared_event_type(&etype) {
535        return window_dispatch_event(it, this, a);
536    }
537    event_target_dispatch_event(it, this, a)
538}
539
540/// `element.requestFullscreen()`(Fullscreen API。丸ごと未対応だった)。
541/// 実際の画面占有切り替えは行わず、`document.fullscreenElement` を更新する
542/// だけの簡略実装(`fullscreenchange` イベントは対象外)。CSS `:fullscreen`
543/// 疑似クラス(丸ごと未対応だった。2026-07-18 発見・実装)が対象要素を
544/// 判定できるよう、`:indeterminate`の`_indeterminate`と同じ内部専用属性
545/// `_fullscreen`もあわせて書き込む。
546pub fn dom_request_fullscreen(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
547    if let Some(idx) = this_dom_idx(&this) {
548        if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
549            doc.borrow_mut().props.insert("fullscreenElement".into(), this.clone());
550        }
551        it.dom.borrow_mut().set_attr(idx, "_fullscreen", "1");
552    }
553    Ok(resolved_promise(it, Value::Undefined))
554}
555
556/// `document.exitFullscreen()`(対をなす解除メソッド。丸ごと未対応だった)。
557pub(crate) fn document_exit_fullscreen(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
558    if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
559        let prev = doc.borrow().props.get("fullscreenElement").cloned();
560        if let Some(prev_val) = prev {
561            if let Some(idx) = this_dom_idx(&prev_val) {
562                it.dom.borrow_mut().remove_attr(idx, "_fullscreen");
563            }
564        }
565        doc.borrow_mut().props.insert("fullscreenElement".into(), Value::Null);
566    }
567    Ok(resolved_promise(it, Value::Undefined))
568}
569
570/// `element.requestPointerLock()`(Pointer Lock API。丸ごと未対応だった)。
571/// `requestFullscreen`/`fullscreenElement` と全く同じ「実際のロック/画面占有
572/// までは行わず状態のみ追跡する」簡略方針(`pointerlockchange` イベントは
573/// 対象外)。
574pub fn dom_request_pointer_lock(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
575    if this_dom_idx(&this).is_some() {
576        if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
577            doc.borrow_mut().props.insert("pointerLockElement".into(), this.clone());
578        }
579    }
580    Ok(Value::Undefined)
581}
582/// `document.exitPointerLock()`(対をなす解除メソッド。丸ごと未対応だった)。
583pub(crate) fn document_exit_pointer_lock(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
584    if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
585        doc.borrow_mut().props.insert("pointerLockElement".into(), Value::Null);
586    }
587    Ok(Value::Undefined)
588}
589
590/// `<video>` の `.requestPictureInPicture()`(Picture-in-Picture API。丸ごと
591/// 未対応だった)。`requestFullscreen`/`requestPointerLock` と全く同じ
592/// 「状態のみ追跡する」簡略方針。仕様どおり解決済み Promise(
593/// `PictureInPictureWindow` 相当のダミーオブジェクトで解決)を返す。
594pub fn dom_request_picture_in_picture(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
595    if this_dom_idx(&this).is_some() {
596        if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
597            doc.borrow_mut()
598                .props
599                .insert("pictureInPictureElement".into(), this.clone());
600        }
601    }
602    Ok(resolved_promise(it, Value::Object(Obj::plain())))
603}
604/// `document.exitPictureInPicture()`(対をなす解除メソッド。丸ごと未対応
605/// だった)。
606pub(crate) fn document_exit_picture_in_picture(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
607    if let Some(Value::Object(doc)) = it.global.borrow().vars.get("document").cloned() {
608        doc.borrow_mut()
609            .props
610            .insert("pictureInPictureElement".into(), Value::Null);
611    }
612    Ok(resolved_promise(it, Value::Undefined))
613}
614
615/// `document.createTreeWalker(root, whatToShow?, filter?)`(DOM 標準。
616/// 丸ごと未対応だった。DOM 部分木を走査する定番パターン)。`whatToShow`/
617/// `filter` は保持のみで実際の絞り込みは行わない簡略実装(`:where()` を
618/// `:is()` と同一視するのと同じ「フィルタは常に通過」方針)。
619pub(crate) fn document_create_tree_walker(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
620    let root = arg(a, 0);
621    let root_idx = this_dom_idx(&root).unwrap_or(0);
622    let tw = Obj::plain();
623    {
624        let mut b = tw.borrow_mut();
625        b.props.insert("root".into(), root.clone());
626        b.props.insert("currentNode".into(), root);
627        b.props.insert("_tw_root".into(), Value::Number(root_idx as f64));
628        b.props.insert(
629            "whatToShow".into(),
630            if a.len() > 1 { arg(a, 1) } else { Value::Number(0xFFFF_FFFFu32 as f64) },
631        );
632        b.props.insert("filter".into(), arg(a, 2));
633        b.props.insert("nextNode".into(), nv("nextNode", tree_walker_next_node));
634        b.props.insert("previousNode".into(), nv("previousNode", tree_walker_previous_node));
635        b.props.insert("parentNode".into(), nv("parentNode", tree_walker_parent_node));
636        b.props.insert("firstChild".into(), nv("firstChild", tree_walker_first_child));
637        b.props.insert("lastChild".into(), nv("lastChild", tree_walker_last_child));
638        b.props.insert("nextSibling".into(), nv("nextSibling", tree_walker_next_sibling));
639        b.props
640            .insert("previousSibling".into(), nv("previousSibling", tree_walker_previous_sibling));
641    }
642    Ok(Value::Object(tw))
643}
644pub(crate) fn tw_current_idx(this: &Value) -> Option<usize> {
645    let Value::Object(o) = this else { return None };
646    let cur = o.borrow().props.get("currentNode").cloned();
647    cur.and_then(|v| this_dom_idx(&v))
648}
649pub(crate) fn tw_root_idx(this: &Value) -> Option<usize> {
650    let Value::Object(o) = this else { return None };
651    o.borrow().props.get("_tw_root").map(|v| v.to_number() as usize)
652}
653pub(crate) fn tw_set_current(this: &Value, idx: usize) {
654    if let Value::Object(o) = this {
655        o.borrow_mut().props.insert("currentNode".into(), Value::Object(Obj::dom(idx)));
656    }
657}
658/// 部分木内で最も深い「最後の子孫」(`previousNode` の下降先探索用)。
659pub(crate) fn deepest_last_descendant(dom: &super::super::dom_bridge::DomBridge, mut idx: usize) -> usize {
660    while let Some(&c) = dom.nodes.get(idx).and_then(|n| n.children.last()) {
661        idx = c;
662    }
663    idx
664}
665/// `TreeWalker.prototype.nextNode()`。文書順で次のノードへ移動する(`root`
666/// を越えない)。
667pub(crate) fn tree_walker_next_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
668    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
669    let root = tw_root_idx(&this).unwrap_or(cur);
670    let dom = it.dom.borrow();
671    if let Some(&c) = dom.nodes.get(cur).and_then(|n| n.children.first()) {
672        drop(dom);
673        tw_set_current(&this, c);
674        return Ok(Value::Object(Obj::dom(c)));
675    }
676    let mut node = cur;
677    loop {
678        if node == root {
679            return Ok(Value::Null);
680        }
681        let Some(p) = dom.nodes.get(node).and_then(|n| n.parent) else {
682            return Ok(Value::Null);
683        };
684        let kids = dom.nodes.get(p).map(|n| n.children.clone()).unwrap_or_default();
685        if let Some(pos) = kids.iter().position(|&c| c == node) {
686            if let Some(&next) = kids.get(pos + 1) {
687                drop(dom);
688                tw_set_current(&this, next);
689                return Ok(Value::Object(Obj::dom(next)));
690            }
691        }
692        if p == root {
693            return Ok(Value::Null);
694        }
695        node = p;
696    }
697}
698/// `TreeWalker.prototype.previousNode()`。
699pub(crate) fn tree_walker_previous_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
700    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
701    let root = tw_root_idx(&this).unwrap_or(cur);
702    if cur == root {
703        return Ok(Value::Null);
704    }
705    let dom = it.dom.borrow();
706    let Some(p) = dom.nodes.get(cur).and_then(|n| n.parent) else {
707        return Ok(Value::Null);
708    };
709    let kids = dom.nodes.get(p).map(|n| n.children.clone()).unwrap_or_default();
710    let target = match kids.iter().position(|&c| c == cur) {
711        Some(pos) if pos > 0 => Some(deepest_last_descendant(&dom, kids[pos - 1])),
712        Some(_) => Some(p),
713        None => None,
714    };
715    drop(dom);
716    match target {
717        Some(t) => {
718            tw_set_current(&this, t);
719            Ok(Value::Object(Obj::dom(t)))
720        }
721        None => Ok(Value::Null),
722    }
723}
724/// `TreeWalker.prototype.parentNode()`。
725pub(crate) fn tree_walker_parent_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
726    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
727    let root = tw_root_idx(&this).unwrap_or(cur);
728    if cur == root {
729        return Ok(Value::Null);
730    }
731    let parent = it.dom.borrow().nodes.get(cur).and_then(|n| n.parent);
732    match parent {
733        Some(p) => {
734            tw_set_current(&this, p);
735            Ok(Value::Object(Obj::dom(p)))
736        }
737        None => Ok(Value::Null),
738    }
739}
740/// `TreeWalker.prototype.firstChild()`。
741pub(crate) fn tree_walker_first_child(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
742    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
743    let first = it.dom.borrow().nodes.get(cur).and_then(|n| n.children.first().copied());
744    match first {
745        Some(c) => {
746            tw_set_current(&this, c);
747            Ok(Value::Object(Obj::dom(c)))
748        }
749        None => Ok(Value::Null),
750    }
751}
752/// `TreeWalker.prototype.lastChild()`。
753pub(crate) fn tree_walker_last_child(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
754    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
755    let last = it.dom.borrow().nodes.get(cur).and_then(|n| n.children.last().copied());
756    match last {
757        Some(c) => {
758            tw_set_current(&this, c);
759            Ok(Value::Object(Obj::dom(c)))
760        }
761        None => Ok(Value::Null),
762    }
763}
764/// `TreeWalker.prototype.nextSibling()`。
765pub(crate) fn tree_walker_next_sibling(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
766    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
767    let root = tw_root_idx(&this).unwrap_or(cur);
768    if cur == root {
769        return Ok(Value::Null);
770    }
771    let dom = it.dom.borrow();
772    let Some(p) = dom.nodes.get(cur).and_then(|n| n.parent) else {
773        return Ok(Value::Null);
774    };
775    let kids = dom.nodes.get(p).map(|n| n.children.clone()).unwrap_or_default();
776    let target = kids.iter().position(|&c| c == cur).and_then(|pos| kids.get(pos + 1).copied());
777    drop(dom);
778    match target {
779        Some(t) => {
780            tw_set_current(&this, t);
781            Ok(Value::Object(Obj::dom(t)))
782        }
783        None => Ok(Value::Null),
784    }
785}
786/// `TreeWalker.prototype.previousSibling()`。
787pub(crate) fn tree_walker_previous_sibling(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
788    let Some(cur) = tw_current_idx(&this) else { return Ok(Value::Null) };
789    let root = tw_root_idx(&this).unwrap_or(cur);
790    if cur == root {
791        return Ok(Value::Null);
792    }
793    let dom = it.dom.borrow();
794    let Some(p) = dom.nodes.get(cur).and_then(|n| n.parent) else {
795        return Ok(Value::Null);
796    };
797    let kids = dom.nodes.get(p).map(|n| n.children.clone()).unwrap_or_default();
798    let target = kids
799        .iter()
800        .position(|&c| c == cur)
801        .filter(|&pos| pos > 0)
802        .and_then(|pos| kids.get(pos - 1).copied());
803    drop(dom);
804    match target {
805        Some(t) => {
806            tw_set_current(&this, t);
807            Ok(Value::Object(Obj::dom(t)))
808        }
809        None => Ok(Value::Null),
810    }
811}
812
813/// `document.createNodeIterator(root, whatToShow?, filter?)`。
814pub(crate) fn document_create_node_iterator(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
815    let root = arg(a, 0);
816    let root_idx = this_dom_idx(&root).unwrap_or(0);
817    let ni = Obj::plain();
818    {
819        let mut b = ni.borrow_mut();
820        b.props.insert("root".into(), root.clone());
821        b.props.insert("referenceNode".into(), root);
822        b.props.insert("pointerBeforeReferenceNode".into(), Value::Bool(true));
823        b.props.insert("_ni_root".into(), Value::Number(root_idx as f64));
824        b.props.insert(
825            "whatToShow".into(),
826            if a.len() > 1 { arg(a, 1) } else { Value::Number(0xFFFF_FFFFu32 as f64) },
827        );
828        b.props.insert("filter".into(), arg(a, 2));
829        b.props
830            .insert("nextNode".into(), nv("nextNode", node_iterator_next_node));
831        b.props.insert(
832            "previousNode".into(),
833            nv("previousNode", node_iterator_previous_node),
834        );
835        b.props.insert("detach".into(), nv("detach", dom_noop));
836    }
837    Ok(Value::Object(ni))
838}
839pub(crate) fn ni_ref_idx(this: &Value) -> Option<usize> {
840    let Value::Object(o) = this else { return None };
841    let r = o.borrow().props.get("referenceNode").cloned();
842    r.and_then(|v| this_dom_idx(&v))
843}
844pub(crate) fn ni_root_idx(this: &Value) -> Option<usize> {
845    let Value::Object(o) = this else { return None };
846    o.borrow()
847        .props
848        .get("_ni_root")
849        .map(|v| v.to_number() as usize)
850}
851pub(crate) fn ni_pointer_before(this: &Value) -> bool {
852    let Value::Object(o) = this else { return false };
853    matches!(
854        o.borrow().props.get("pointerBeforeReferenceNode"),
855        Some(Value::Bool(true))
856    )
857}
858pub(crate) fn ni_set_state(this: &Value, idx: usize, before: bool) {
859    if let Value::Object(o) = this {
860        let mut b = o.borrow_mut();
861        b.props
862            .insert("referenceNode".into(), Value::Object(Obj::dom(idx)));
863        b.props
864            .insert("pointerBeforeReferenceNode".into(), Value::Bool(before));
865    }
866}
867/// pre-order で `idx` の次のノード(`root` 部分木の範囲内)。
868pub(crate) fn node_iterator_step_next(
869    dom: &super::super::dom_bridge::DomBridge,
870    root: usize,
871    idx: usize,
872) -> Option<usize> {
873    if let Some(&c) = dom.nodes.get(idx).and_then(|n| n.children.first()) {
874        return Some(c);
875    }
876    let mut node = idx;
877    loop {
878        if node == root {
879            return None;
880        }
881        let p = dom.nodes.get(node).and_then(|n| n.parent)?;
882        let kids = dom
883            .nodes
884            .get(p)
885            .map(|n| n.children.clone())
886            .unwrap_or_default();
887        if let Some(pos) = kids.iter().position(|&c| c == node) {
888            if let Some(&next) = kids.get(pos + 1) {
889                return Some(next);
890            }
891        }
892        if p == root {
893            return None;
894        }
895        node = p;
896    }
897}
898/// `NodeIterator.prototype.nextNode()`。
899pub(crate) fn node_iterator_next_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
900    let Some(cur) = ni_ref_idx(&this) else {
901        return Ok(Value::Null);
902    };
903    let root = ni_root_idx(&this).unwrap_or(cur);
904    if ni_pointer_before(&this) {
905        ni_set_state(&this, cur, false);
906        return Ok(Value::Object(Obj::dom(cur)));
907    }
908    let dom = it.dom.borrow();
909    let next = node_iterator_step_next(&dom, root, cur);
910    drop(dom);
911    match next {
912        Some(n) => {
913            ni_set_state(&this, n, false);
914            Ok(Value::Object(Obj::dom(n)))
915        }
916        None => Ok(Value::Null),
917    }
918}
919/// `NodeIterator.prototype.previousNode()`。
920pub(crate) fn node_iterator_previous_node(
921    it: &mut Interp,
922    this: Value,
923    _a: &[Value],
924) -> Result<Value, Value> {
925    let Some(cur) = ni_ref_idx(&this) else {
926        return Ok(Value::Null);
927    };
928    let root = ni_root_idx(&this).unwrap_or(cur);
929    if !ni_pointer_before(&this) {
930        ni_set_state(&this, cur, true);
931        return Ok(Value::Object(Obj::dom(cur)));
932    }
933    if cur == root {
934        return Ok(Value::Null);
935    }
936    let dom = it.dom.borrow();
937    let Some(p) = dom.nodes.get(cur).and_then(|n| n.parent) else {
938        return Ok(Value::Null);
939    };
940    let kids = dom
941        .nodes
942        .get(p)
943        .map(|n| n.children.clone())
944        .unwrap_or_default();
945    let target = match kids.iter().position(|&c| c == cur) {
946        Some(pos) if pos > 0 => Some(deepest_last_descendant(&dom, kids[pos - 1])),
947        Some(_) => Some(p),
948        None => None,
949    };
950    drop(dom);
951    match target {
952        Some(t) => {
953            ni_set_state(&this, t, true);
954            Ok(Value::Object(Obj::dom(t)))
955        }
956        None => Ok(Value::Null),
957    }
958}
959
960/// `document.hasStorageAccess()`(Storage Access API。丸ごと未対応
961/// だった)。この処理系にはサードパーティ Cookie 分離/ブロック機構自体が
962/// 無くストレージは常にアクセス可能なため、常に `true` を返す簡略実装。
963pub(crate) fn document_has_storage_access(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
964    Ok(resolved_promise(it, Value::Bool(true)))
965}
966/// `document.requestStorageAccess()`(対をなす要求メソッド。丸ごと未対応
967/// だった)。理由は `hasStorageAccess` と同じで、常に成功を返す。
968pub(crate) fn document_request_storage_access(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
969    Ok(resolved_promise(it, Value::Undefined))
970}
971
972/// `screen.orientation.lock(orientation)`(Screen Orientation API。丸ごと
973/// 未対応だった)。実際にディスプレイの向きを固定する機構は無いため、
974/// `fullscreenElement` 等と同じ「要求された値へ `type` を書き換えるだけ」の
975/// 簡略実装。
976pub(crate) fn screen_orientation_lock(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
977    if let Value::Object(o) = &this {
978        o.borrow_mut().props.insert("type".into(), Value::str(arg(a, 0).to_js_string()));
979    }
980    Ok(resolved_promise(it, Value::Undefined))
981}
982/// `screen.orientation.unlock()`(対をなす解除メソッド。丸ごと未対応
983/// だった)。現在のビューポート寸法から求めた自然な向きへ `type` を戻す
984/// (`window.screen` 初期化時の判定ロジックと同じ)。
985pub(crate) fn screen_orientation_unlock(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
986    if let Value::Object(o) = &this {
987        let vw = super::super::super::layout::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
988        let vh = super::super::super::layout::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
989        let otype = if vw >= vh { "landscape-primary" } else { "portrait-primary" };
990        o.borrow_mut().props.insert("type".into(), Value::str(otype));
991    }
992    Ok(Value::Undefined)
993}
994
995/// `window.postMessage(data, targetOrigin)`(丸ごと未対応だった)。詳細は
996/// `window.props.insert("postMessage", ...)` 箇所のコメント参照。
997pub(crate) fn window_post_message(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
998    let data = structured_clone(it, Value::Undefined, a)?;
999    let href = location_href(&it.global).unwrap_or_default();
1000    let origin = location_components(&href)
1001        .into_iter()
1002        .find(|(k, _)| *k == "origin")
1003        .map(|(_, v)| v)
1004        .unwrap_or_default();
1005    let ev = Obj::plain();
1006    ev.borrow_mut().props.insert("type".into(), Value::str("message"));
1007    ev.borrow_mut().props.insert("data".into(), data);
1008    ev.borrow_mut().props.insert("origin".into(), Value::str(origin));
1009    ev.borrow_mut().props.insert("source".into(), Value::Null);
1010    let listeners = match it.global.borrow().vars.get("window") {
1011        Some(Value::Object(w)) => w.borrow().props.get("_message_listeners").cloned(),
1012        _ => None,
1013    };
1014    if let Some(Value::Object(arr)) = listeners {
1015        let entries: Vec<Value> = match &arr.borrow().kind {
1016            ObjKind::Array(items) => items.clone(),
1017            _ => Vec::new(),
1018        };
1019        for entry in entries {
1020            if window_listener_aborted(&entry) {
1021                continue;
1022            }
1023            let cb = event_target_listener_cb(&entry);
1024            it.call_listener(
1025                &cb,
1026                Value::Undefined,
1027                &[Value::Object(ev.clone())],
1028                "window listener",
1029            );
1030        }
1031    }
1032    Ok(Value::Undefined)
1033}
1034
1035/// `reportError(error)`(丸ごと未対応だった。未捕捉例外と同じ経路で
1036/// `window.onerror`(レガシー5引数シグネチャ)/
1037/// `window.addEventListener('error', fn)`(ErrorEvent 風オブジェクト)へ
1038/// 同期的に通知する。仕様上のイベントループ経由の非同期報告・
1039/// `event.preventDefault()` によるコンソール抑制は簡略化し、常に
1040/// `console.error` へも出力する)。
1041pub(crate) fn report_error(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1042    let err = arg(a, 0);
1043    let message = if let Value::Object(o) = &err {
1044        o.borrow()
1045            .props
1046            .get("message")
1047            .map(|v| v.to_js_string())
1048            .unwrap_or_else(|| err.to_js_string())
1049    } else {
1050        err.to_js_string()
1051    };
1052    let _ = console_log(it, Value::Undefined, &[Value::str(&message)]);
1053    let ev = Obj::plain();
1054    {
1055        let mut e = ev.borrow_mut();
1056        e.props.insert("type".into(), Value::str("error"));
1057        e.props.insert("message".into(), Value::str(&message));
1058        e.props.insert("error".into(), err.clone());
1059        e.props.insert("filename".into(), Value::str(""));
1060        e.props.insert("lineno".into(), Value::Number(0.0));
1061        e.props.insert("colno".into(), Value::Number(0.0));
1062    }
1063    let window = match it.global.borrow().vars.get("window") {
1064        Some(Value::Object(w)) => Some(w.clone()),
1065        _ => None,
1066    };
1067    if let Some(w) = &window {
1068        let onerror = w.borrow().props.get("onerror").cloned();
1069        if let Some(f) = onerror.and_then(callable_or_none) {
1070            it.call_listener(
1071                &f,
1072                Value::Undefined,
1073                &[
1074                    Value::str(&message),
1075                    Value::str(""),
1076                    Value::Number(0.0),
1077                    Value::Number(0.0),
1078                    err.clone(),
1079                ],
1080                "window.onerror",
1081            );
1082        }
1083        let listeners = w.borrow().props.get("_error_listeners").cloned();
1084        if let Some(Value::Object(arr)) = listeners {
1085            let entries: Vec<Value> = match &arr.borrow().kind {
1086                ObjKind::Array(items) => items.clone(),
1087                _ => Vec::new(),
1088            };
1089            for entry in entries {
1090                if window_listener_aborted(&entry) {
1091                    continue;
1092                }
1093                let cb = event_target_listener_cb(&entry);
1094                it.call_listener(
1095                &cb,
1096                Value::Undefined,
1097                &[Value::Object(ev.clone())],
1098                "window listener",
1099            );
1100            }
1101        }
1102    }
1103    Ok(Value::Undefined)
1104}
1105
1106/// `AbortSignal` 用の既定の中断理由(`DOMException("AbortError")` の簡易近似)。
1107pub(crate) fn make_abort_error() -> Value {
1108    make_dom_exception("AbortError", "signal is aborted without reason")
1109}
1110/// `new DOMException(message?, name?)`。仕様どおり既定値は `message=""`/
1111/// `name="Error"`。
1112pub(crate) fn dom_exception_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1113    let message = match arg(a, 0) {
1114        Value::Undefined => String::new(),
1115        v => v.to_js_string(),
1116    };
1117    let name = match arg(a, 1) {
1118        Value::Undefined => String::from("Error"),
1119        v => v.to_js_string(),
1120    };
1121    Ok(make_dom_exception(&name, &message))
1122}
1123/// `name` から DOM Level 4 のレガシー数値定数(`err.code`)を引く。未知の
1124/// 名前は仕様どおり `0`。
1125pub(crate) fn dom_exception_legacy_code(name: &str) -> f64 {
1126    match name {
1127        "IndexSizeError" => 1.0,
1128        "HierarchyRequestError" => 3.0,
1129        "WrongDocumentError" => 4.0,
1130        "InvalidCharacterError" => 5.0,
1131        "NoModificationAllowedError" => 7.0,
1132        "NotFoundError" => 8.0,
1133        "NotSupportedError" => 9.0,
1134        "InUseAttributeError" => 10.0,
1135        "InvalidStateError" => 11.0,
1136        "SyntaxError" => 12.0,
1137        "InvalidModificationError" => 13.0,
1138        "NamespaceError" => 14.0,
1139        "InvalidAccessError" => 15.0,
1140        "SecurityError" => 18.0,
1141        "NetworkError" => 19.0,
1142        "AbortError" => 20.0,
1143        "TimeoutError" => 23.0,
1144        "DataCloneError" => 25.0,
1145        _ => 0.0,
1146    }
1147}
1148/// `DOMException` インスタンスを構築する共通ヘルパー。`AbortController.
1149/// abort()` 等、エンジン内部で例外を作る箇所からも再利用する(詳細は
1150/// `dom_exception_ctor` 参照)。
1151pub(crate) fn make_dom_exception(name: &str, message: &str) -> Value {
1152    let o = Obj::plain();
1153    {
1154        let mut b = o.borrow_mut();
1155        b.props.insert("name".into(), Value::str(name));
1156        b.props.insert("message".into(), Value::str(message));
1157        b.props
1158            .insert("code".into(), Value::Number(dom_exception_legacy_code(name)));
1159        b.props.insert("toString".into(), nv("toString", dom_exception_to_string));
1160    }
1161    Value::Object(o)
1162}
1163pub(crate) fn dom_exception_to_string(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1164    if let Value::Object(o) = &this {
1165        let b = o.borrow();
1166        let name = b.props.get("name").map(|v| v.to_js_string()).unwrap_or_default();
1167        let message = b.props.get("message").map(|v| v.to_js_string()).unwrap_or_default();
1168        return Ok(Value::str(if message.is_empty() {
1169            name
1170        } else {
1171            alloc::format!("{}: {}", name, message)
1172        }));
1173    }
1174    Ok(Value::str("DOMException"))
1175}
1176/// `controller.abort(reason?)`。以前は `signal.aborted` を立てるだけで、(1) 既に中断済み
1177/// の場合の二重発火防止、(2) 仕様どおりの `signal.reason`、(3) `signal.addEventListener
1178/// ('abort', fn)`/`signal.onabort` によるコールバック通知、のいずれも欠けていた
1179/// (`addEventListener` 自体が単なる no-op だったため、`abort()` を呼んでも登録した
1180/// リスナが一切発火しないサイレントなバグ)。
1181pub(crate) fn abort_controller_abort(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1182    if let Value::Object(o) = &this {
1183        let sig = o.borrow().props.get("signal").cloned();
1184        if let Some(Value::Object(s)) = sig {
1185            let already_aborted = matches!(s.borrow().props.get("aborted"), Some(Value::Bool(true)));
1186            if already_aborted {
1187                return Ok(Value::Undefined);
1188            }
1189            let reason = match arg(a, 0) {
1190                Value::Undefined => make_abort_error(),
1191                r => r,
1192            };
1193            {
1194                let mut b = s.borrow_mut();
1195                b.props.insert("aborted".into(), Value::Bool(true));
1196                b.props.insert("reason".into(), reason);
1197            }
1198            let cb = s.borrow().props.get("onabort").cloned();
1199            if let Some(f) = cb.and_then(callable_or_none) {
1200                it.call_listener(&f, Value::Object(s.clone()), &[], "AbortSignal onabort");
1201            }
1202        }
1203    }
1204    Ok(Value::Undefined)
1205}
1206/// `signal.addEventListener('abort', fn)`。以前は無条件 no-op で、リスナが一切
1207/// 保持/発火されなかった。他の `EventTarget` 風オブジェクト(XHR 等)と同じ
1208/// 「`on<type>` プロパティへ格納する単一リスナ」簡略方式で対応する。
1209pub(crate) fn abort_signal_add_event_listener(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1210    if let Value::Object(o) = &this {
1211        o.borrow_mut()
1212            .props
1213            .insert(format!("on{}", arg(a, 0).to_js_string()), arg(a, 1));
1214    }
1215    Ok(Value::Undefined)
1216}
1217/// `signal.removeEventListener('abort', fn)`。`addEventListener` は実装済みなのに
1218/// 対をなす削除側が無条件 no-op のままだったバグ(登録したリスナを外すつもりが
1219/// 何も起きず、後から `abort()` すると外したはずのリスナがまだ発火する)。
1220/// 「`on<type>` に単一リスナを格納する」既存の簡略方式に合わせ、渡された関数が
1221/// 現在登録済みのものと同一参照の場合のみ消す(別の関数参照を渡した場合は
1222/// 仕様どおり no-op のまま)。
1223pub(crate) fn abort_signal_remove_event_listener(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1224    if let Value::Object(o) = &this {
1225        let key = format!("on{}", arg(a, 0).to_js_string());
1226        let target = arg(a, 1);
1227        let matches = match (o.borrow().props.get(&key), &target) {
1228            (Some(Value::Object(stored)), Value::Object(t)) => Rc::ptr_eq(stored, t),
1229            _ => false,
1230        };
1231        if matches {
1232            o.borrow_mut().props.shift_remove(&key);
1233        }
1234    }
1235    Ok(Value::Undefined)
1236}
1237pub(crate) fn abort_controller_ctor(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
1238    let signal = Obj::plain();
1239    {
1240        let mut s = signal.borrow_mut();
1241        s.props.insert("aborted".into(), Value::Bool(false));
1242        s.props.insert("reason".into(), Value::Undefined);
1243        s.props.insert(
1244            "addEventListener".into(),
1245            nv("addEventListener", abort_signal_add_event_listener),
1246        );
1247        s.props.insert(
1248            "removeEventListener".into(),
1249            nv("removeEventListener", abort_signal_remove_event_listener),
1250        );
1251        s.props.insert(
1252            "throwIfAborted".into(),
1253            nv("throwIfAborted", abort_signal_throw_if_aborted),
1254        );
1255    }
1256    let ctrl = Obj::plain();
1257    {
1258        let mut c = ctrl.borrow_mut();
1259        c.props.insert("signal".into(), Value::Object(signal));
1260        c.props
1261            .insert("abort".into(), nv("abort", abort_controller_abort));
1262    }
1263    Ok(Value::Object(ctrl))
1264}
1265/// `signal.throwIfAborted()`(ES2022/DOM。既に中断済みなら `reason` を投げ、
1266/// そうでなければ何もしない、fetch 等のループ内チェックで頻出のイディオム)が
1267/// 丸ごと未対応だった。`AbortSignal.timeout()`/`.any()` は内部で
1268/// `new AbortController()` を経由するため、`abort_controller_ctor`/
1269/// `abort_signal_abort_static` の2箇所にさえ追加すれば全生成経路をカバーする。
1270pub(crate) fn abort_signal_throw_if_aborted(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1271    if let Value::Object(o) = &this {
1272        let aborted = matches!(o.borrow().props.get("aborted"), Some(Value::Bool(true)));
1273        if aborted {
1274            let reason = o.borrow().props.get("reason").cloned().unwrap_or(Value::Undefined);
1275            return Err(reason);
1276        }
1277    }
1278    Ok(Value::Undefined)
1279}
1280/// `AbortSignal.abort(reason?)`(ES2022)。既に中断済みの状態で signal を返す
1281/// 静的ファクトリ。`AbortController` を経由せず即座に中断済み signal が欲しい
1282/// 場合(引数バリデーション失敗時の早期中断等)に使われる。
1283pub(crate) fn abort_signal_abort_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1284    let signal = Obj::plain();
1285    {
1286        let mut s = signal.borrow_mut();
1287        s.props.insert("aborted".into(), Value::Bool(true));
1288        let reason = match arg(a, 0) {
1289            Value::Undefined => make_abort_error(),
1290            r => r,
1291        };
1292        s.props.insert("reason".into(), reason);
1293        s.props.insert(
1294            "addEventListener".into(),
1295            nv("addEventListener", abort_signal_add_event_listener),
1296        );
1297        s.props.insert(
1298            "removeEventListener".into(),
1299            nv("removeEventListener", abort_signal_remove_event_listener),
1300        );
1301        s.props.insert(
1302            "throwIfAborted".into(),
1303            nv("throwIfAborted", abort_signal_throw_if_aborted),
1304        );
1305    }
1306    Ok(Value::Object(signal))
1307}
1308/// `AbortSignal.timeout(ms)`(ES2022)。指定ミリ秒後に自動的に abort する
1309/// AbortSignal を返す、fetch のタイムアウト実装で最頻出のイディオム。この
1310/// エンジンの `setTimeout` は実時間を計測せずマクロタスクキューに積むだけの
1311/// 簡略実装のため、正確な実時間待機ではなくキュー順序に従った近似になる
1312/// (既存の `setTimeout` 全体の簡略化と同じ水準)。ネイティブ関数はクロージャで
1313/// `ms` を束縛できないため、`AbortController`/`setTimeout` を組み合わせる
1314/// JS ソースを組み立てて `eval_source` で実行する。
1315/// `AbortSignal.timeout(ms)`。以前は `c.abort()` を引数無しで呼んでおり、
1316/// `signal.reason` が仕様どおりの `TimeoutError`(DOM Level 4 legacy code
1317/// 23)ではなく既定の `AbortError`(code 20)になっていたバグ。
1318/// `abort_signal_any` と同じ「グローバル一時変数経由で Rust 側の値を JS へ
1319/// 渡す」パターンで、事前に構築した `TimeoutError` の `DOMException` を
1320/// 束縛してから `c.abort(reason)` を呼ぶ。
1321pub(crate) fn abort_signal_timeout(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1322    let ms = arg(a, 0).to_number();
1323    let reason = make_dom_exception("TimeoutError", "signal timed out");
1324    it.global
1325        .borrow_mut()
1326        .vars
1327        .insert("__abort_signal_timeout_reason".into(), reason);
1328    let src = alloc::format!(
1329        "(function(){{ var c = new AbortController(); var r = __abort_signal_timeout_reason; \
1330         setTimeout(function(){{ c.abort(r); }}, {}); return c.signal; }})()",
1331        ms
1332    );
1333    let result = it.eval_source(&src);
1334    it.global.borrow_mut().vars.remove("__abort_signal_timeout_reason");
1335    result
1336}
1337/// `AbortSignal.any(iterable)`(ES2024)。渡した signal 群のいずれかが中断された
1338/// 時点で中断される合成 signal を返す(複数の中断条件を1つの signal にまとめる
1339/// 定番パターン)。既存の `addEventListener('abort', fn)` は「`on<type>` に単一
1340/// リスナを格納する」簡略実装のため、入力 signal 側に別のリスナが既に登録済みだと
1341/// 上書きしてしまう制約がある(この処理系の EventTarget 系オブジェクト全般に
1342/// 共通の既知の簡略化)。`abort_signal_timeout` と同じ理由(ネイティブ関数は
1343/// クロージャで値を束縛できない)で、入力配列をグローバルの一時変数経由で
1344/// 渡した上で `eval_source` に組み立てた JS を実行する。
1345pub(crate) fn abort_signal_any(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1346    let signals = it.iter_to_vec(&arg(a, 0));
1347    it.global
1348        .borrow_mut()
1349        .vars
1350        .insert("__abort_signal_any_input".into(), Value::Object(Obj::array(signals)));
1351    let src = "(function(){ \
1352        var c = new AbortController(); \
1353        var arr = __abort_signal_any_input; \
1354        for (var i = 0; i < arr.length; i++) { \
1355            if (arr[i].aborted) { c.abort(arr[i].reason); return c.signal; } \
1356        } \
1357        for (var i = 0; i < arr.length; i++) { \
1358            (function(s){ \
1359                s.addEventListener('abort', function(){ if (!c.signal.aborted) c.abort(s.reason); }); \
1360            })(arr[i]); \
1361        } \
1362        return c.signal; \
1363    })()";
1364    let result = it.eval_source(src);
1365    it.global.borrow_mut().vars.remove("__abort_signal_any_input");
1366    result
1367}
1368