Skip to main content

atmos/os_lib/js/builtins/
dom.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5// ============ DOM ネイティブ ============
6
7/// 無害な no-op(未対応 API 用)。
8pub fn dom_noop(_: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
9    Ok(Value::Undefined)
10}
11
12/// `new XMLSerializer()`(丸ごと未対応だった。`.serializeToString(node)`
13/// という定番パターン)。
14pub(crate) fn xml_serializer_ctor(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
15    let o = Obj::plain();
16    o.borrow_mut().props.insert(
17        "serializeToString".into(),
18        nv("serializeToString", xml_serializer_serialize_to_string),
19    );
20    Ok(Value::Object(o))
21}
22/// `serializer.serializeToString(node)`。既存の `element.outerHTML` ゲッター
23/// (`interp.rs`)と同じ `DomBridge::get_outer_html` を再利用する(DOM 要素
24/// ハンドルでない値/未接続ノードは仕様の詳細な例外分岐を省略し空文字列を返す
25/// 簡略実装)。
26pub(crate) fn xml_serializer_serialize_to_string(it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
27    let node = arg(a, 0);
28    match this_dom_idx(&node) {
29        Some(idx) => Ok(Value::str(it.dom.borrow().get_outer_html(idx))),
30        None => Ok(Value::str("")),
31    }
32}
33
34/// `this`(DOM 要素ハンドル)からノード index を取り出す。
35pub(crate) fn this_dom_idx(this: &Value) -> Option<usize> {
36    if let Value::Object(o) = this {
37        if let ObjKind::DomElement(i) = o.borrow().kind {
38            return Some(i);
39        }
40    }
41    None
42}
43
44/// `this`(Host("classList:N") など)から prefix を剥がして index を取り出す。
45/// `element.style.setProperty(name, value)`/`getPropertyValue(name)`/`removeProperty(name)`
46/// が丸ごと欠落していた(直接のプロパティ代入 `el.style.color = 'red'` と、
47/// `getComputedStyle().getPropertyValue()` はあったが、インラインスタイルの
48/// メソッド経由の読み書きが無かった)。特に CSS カスタムプロパティ(`--foo`)は
49/// camelCase のプロパティアクセサ経由では設定できないため、`setProperty` の欠落は
50/// カスタムプロパティを使う実用コードで特に影響が大きかった。
51pub fn dom_style_set_property(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
52    if let Some(idx) = this_host_idx(&this, "style:") {
53        let name = arg(a, 0).to_js_string();
54        let value = arg(a, 1).to_js_string();
55        // `setProperty(name, value, priority)` の第3引数(丸ごと無視されて
56        // いた。2026-07-16 発見)。生の値文字列末尾に `!important` を
57        // 付加して保存すれば、`serialize_inline_style` 経由で再構築される
58        // `style` 属性文字列を通じて既存のカスケード側 `!important` パーサ
59        // (`css.rs`)にもそのまま伝わる。
60        let priority = arg(a, 2).to_js_string();
61        let stored = if !value.is_empty() && priority.eq_ignore_ascii_case("important") {
62            alloc::format!("{} !important", value)
63        } else {
64            value
65        };
66        it.dom.borrow_mut().set_style(idx, &name, &stored);
67    }
68    Ok(Value::Undefined)
69}
70pub fn dom_style_get_property_priority(
71    it: &mut Interp,
72    this: Value,
73    a: &[Value],
74) -> Result<Value, Value> {
75    if let Some(idx) = this_host_idx(&this, "style:") {
76        let name = arg(a, 0).to_js_string();
77        return Ok(Value::str(it.dom.borrow().get_style_priority(idx, &name)));
78    }
79    Ok(Value::str(""))
80}
81pub fn dom_style_get_property_value(
82    it: &mut Interp,
83    this: Value,
84    a: &[Value],
85) -> Result<Value, Value> {
86    if let Some(idx) = this_host_idx(&this, "style:") {
87        let name = arg(a, 0).to_js_string();
88        return Ok(Value::str(it.dom.borrow().get_style(idx, &name)));
89    }
90    Ok(Value::str(""))
91}
92pub fn dom_style_remove_property(
93    it: &mut Interp,
94    this: Value,
95    a: &[Value],
96) -> Result<Value, Value> {
97    if let Some(idx) = this_host_idx(&this, "style:") {
98        let name = arg(a, 0).to_js_string();
99        let prev = it.dom.borrow().get_style(idx, &name);
100        it.dom.borrow_mut().set_style(idx, &name, "");
101        return Ok(Value::str(prev));
102    }
103    Ok(Value::str(""))
104}
105/// `style.item(index)`(丸ごと未対応だった。`style.length`と対)。
106/// `get_css_text`の直列化文字列を`;`で分割し直す簡略実装(`dom_props.rs`
107/// の`length`ゲッターと同じロジック)。
108pub fn dom_style_item(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
109    if let Some(idx) = this_host_idx(&this, "style:") {
110        let names: alloc::vec::Vec<String> = it
111            .dom
112            .borrow()
113            .get_css_text(idx)
114            .split(';')
115            .filter_map(|d| d.split_once(':').map(|(k, _)| k.trim().to_string()))
116            .filter(|k| !k.is_empty())
117            .collect();
118        let i = arg(a, 0).to_number();
119        if i.is_finite() && i >= 0.0 {
120            if let Some(name) = names.get(i as usize) {
121                return Ok(Value::str(name.clone()));
122            }
123        }
124    }
125    Ok(Value::str(""))
126}
127pub(crate) fn this_host_idx(this: &Value, prefix: &str) -> Option<usize> {
128    if let Value::Object(o) = this {
129        if let ObjKind::Host(t) = &o.borrow().kind {
130            if let Some(rest) = t.strip_prefix(prefix) {
131                return rest.parse().ok();
132            }
133        }
134    }
135    None
136}
137
138/// 要素ハンドルを Value にして返す。idx が無ければ null。
139pub(crate) fn dom_handle(idx: Option<usize>) -> Value {
140    match idx {
141        Some(i) => Value::Object(Obj::dom(i)),
142        None => Value::Null,
143    }
144}
145
146pub(crate) fn document_get_element_by_id(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
147    let id = arg(a, 0).to_js_string();
148    let idx = it.dom.borrow().get_element_by_id(&id);
149    Ok(dom_handle(idx))
150}
151
152pub(crate) fn document_query_selector(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
153    let sel = arg(a, 0).to_js_string();
154    let idx = it.dom.borrow().query(&sel);
155    Ok(dom_handle(idx))
156}
157
158pub(crate) fn document_query_selector_all(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
159    let sel = arg(a, 0).to_js_string();
160    let indices = it.dom.borrow().query_all(&sel);
161    let items: Vec<Value> = indices
162        .into_iter()
163        .map(|i| Value::Object(Obj::dom(i)))
164        .collect();
165    Ok(Value::Object(Obj::array(items)))
166}
167/// 座標 `(x, y)` を含む矩形を持つ要素を、面積が小さい(=最も深くネストした)
168/// 順に列挙する共通ヘルパー。`elementFromPoint`/`elementsFromPoint` で共有。
169pub(crate) fn dom_hit_test(it: &Interp, x: f64, y: f64) -> alloc::vec::Vec<(usize, i64)> {
170    let dom = it.dom.borrow();
171    let mut matches: alloc::vec::Vec<(usize, i64)> = alloc::vec::Vec::new();
172    for (&idx, &(l, t, w, h)) in dom.rects.iter() {
173        let (lf, tf, wf, hf) = (l as f64, t as f64, w as f64, h as f64);
174        if x >= lf && x < lf + wf && y >= tf && y < tf + hf {
175            matches.push((idx, (w as i64) * (h as i64)));
176        }
177    }
178    matches.sort_by_key(|&(_, area)| area);
179    matches
180}
181pub(crate) fn document_element_from_point(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
182    let x = arg(a, 0).to_number();
183    let y = arg(a, 1).to_number();
184    let matches = dom_hit_test(it, x, y);
185    Ok(match matches.first() {
186        Some(&(idx, _)) => Value::Object(Obj::dom(idx)),
187        None => Value::Null,
188    })
189}
190pub(crate) fn document_elements_from_point(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
191    let x = arg(a, 0).to_number();
192    let y = arg(a, 1).to_number();
193    let items: alloc::vec::Vec<Value> = dom_hit_test(it, x, y)
194        .into_iter()
195        .map(|(idx, _)| Value::Object(Obj::dom(idx)))
196        .collect();
197    Ok(Value::Object(Obj::array(items)))
198}
199
200pub fn dom_add_event_listener(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
201    if let Some(idx) = this_dom_idx(&this) {
202        let event = arg(a, 0).to_js_string();
203        let func = arg(a, 1);
204        let (capture, once) = parse_listener_opts(&arg(a, 2));
205        let signal = parse_listener_signal(&arg(a, 2));
206        if matches!(func, Value::Object(_)) {
207            it.dom
208                .borrow_mut()
209                .add_listener_opts_signal(idx, &event, func, capture, once, signal);
210        }
211    }
212    Ok(Value::Undefined)
213}
214
215/// addEventListener / removeEventListener の第3引数を解釈する。
216/// boolean なら capture フラグ、オブジェクトなら {capture, once} を読む。
217pub(crate) fn parse_listener_opts(opt: &Value) -> (bool, bool) {
218    match opt {
219        Value::Bool(b) => (*b, false),
220        Value::Object(o) => {
221            let b = o.borrow();
222            let capture = b.props.get("capture").map(|v| v.truthy()).unwrap_or(false);
223            let once = b.props.get("once").map(|v| v.truthy()).unwrap_or(false);
224            (capture, once)
225        }
226        _ => (false, false),
227    }
228}
229/// `addEventListener(type, fn, {signal})`(丸ごと未対応だった)の `signal`
230/// オプションを取り出す。`AbortSignal` らしきオブジェクト(`aborted` プロパティ
231/// を持つ)以外は無視する。
232pub(crate) fn parse_listener_signal(opt: &Value) -> Option<Value> {
233    if let Value::Object(o) = opt {
234        let s = o.borrow().props.get("signal").cloned();
235        if let Some(sig @ Value::Object(so)) = &s {
236            if so.borrow().props.contains_key("aborted") {
237                return Some(sig.clone());
238            }
239        }
240    }
241    None
242}
243
244/// element.removeEventListener(type, fn, opts): 一致するリスナを削除する。
245pub fn dom_remove_event_listener(
246    it: &mut Interp,
247    this: Value,
248    a: &[Value],
249) -> Result<Value, Value> {
250    if let Some(idx) = this_dom_idx(&this) {
251        let event = arg(a, 0).to_js_string();
252        let func = arg(a, 1);
253        let (capture, _once) = parse_listener_opts(&arg(a, 2));
254        if matches!(func, Value::Object(_)) {
255            it.dom
256                .borrow_mut()
257                .remove_listener(idx, &event, &func, capture);
258        }
259    }
260    Ok(Value::Undefined)
261}
262
263/// element.dispatchEvent(ev): ev.type のイベントを当該要素から発火する。
264/// 戻り値は !defaultPrevented(DOM 仕様)。
265pub fn dom_dispatch_event(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
266    let idx = match this_dom_idx(&this) {
267        Some(i) => i,
268        None => return Ok(Value::Bool(true)),
269    };
270    let ev = arg(a, 0);
271    // ev.type を取得。bubbles など追加プロパティはそのまま渡す。
272    let (etype, extra) = if let Value::Object(o) = &ev {
273        let b = o.borrow();
274        let t = b
275            .props
276            .get("type")
277            .map(|v| v.to_js_string())
278            .unwrap_or_default();
279        // 既知の組み込みプロパティ以外を extra として転送(detail など)。`bubbles`
280        // は以前ここで除外されており、コメント「bubbles など追加プロパティは
281        // そのまま渡す」と矛盾していた(実際には常に除外され `dispatch_event_*`
282        // 側で無条件 `true` 決め打ちに繋がっていた)。非バブリングイベントを
283        // 正しく伝播させるため、`bubbles` は除外せず転送する。
284        let mut extra: Vec<(String, Value)> = Vec::new();
285        for (k, v) in b.props.iter() {
286            if matches!(
287                k.as_str(),
288                "type"
289                    | "target"
290                    | "currentTarget"
291                    | "eventPhase"
292                    | "defaultPrevented"
293                    | "preventDefault"
294                    | "stopPropagation"
295                    | "stopImmediatePropagation"
296                    | "composedPath"
297                    | "_composedPath"
298                    | "_stop"
299                    | "_stopImmediate"
300            ) {
301                continue;
302            }
303            extra.push((k.clone(), v.clone()));
304        }
305        (t, extra)
306    } else {
307        (ev.to_js_string(), Vec::new())
308    };
309    if etype.is_empty() {
310        return Ok(Value::Bool(true));
311    }
312    let (_fired, prevented) = it.dispatch_event_in_interp(idx, &etype, &extra);
313    Ok(Value::Bool(!prevented))
314}
315
316/// `node.hasChildNodes()`(丸ごと未対応だった。`node.childNodes.length > 0`
317/// の定番の手間を省くメソッド)。
318pub fn dom_has_child_nodes(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
319    if let Some(idx) = this_dom_idx(&this) {
320        let has_kids = it
321            .dom
322            .borrow()
323            .nodes
324            .get(idx)
325            .map(|n| !n.children.is_empty())
326            .unwrap_or(false);
327        return Ok(Value::Bool(has_kids));
328    }
329    Ok(Value::Bool(false))
330}
331pub fn dom_get_attribute(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
332    if let Some(idx) = this_dom_idx(&this) {
333        let name = arg(a, 0).to_js_string();
334        return Ok(match it.dom.borrow().get_attr(idx, &name) {
335            Some(v) => Value::str(v),
336            None => Value::Null,
337        });
338    }
339    Ok(Value::Null)
340}
341
342pub fn dom_has_attribute(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
343    if let Some(idx) = this_dom_idx(&this) {
344        let name = arg(a, 0).to_js_string();
345        return Ok(Value::Bool(it.dom.borrow().has_attr(idx, &name)));
346    }
347    Ok(Value::Bool(false))
348}
349
350/// `element.attributes`(`NamedNodeMap`。`{name,value}` の簡略表現)と同じ
351/// 形で `Attr` ノード相当のオブジェクトを作る。`ownerElement` も含める。
352pub(crate) fn make_attr_node(name: &str, value: &str, owner: &Value) -> Value {
353    let o = Obj::plain();
354    let mut b = o.borrow_mut();
355    b.props.insert("name".into(), Value::str(name));
356    b.props.insert("value".into(), Value::str(value));
357    b.props.insert("ownerElement".into(), owner.clone());
358    drop(b);
359    Value::Object(o)
360}
361/// `document.createAttribute(name)`(丸ごと未対応だった。`getAttributeNode`
362/// と対になる、`Attr` ノードを新規に作ってから `setAttributeNode()` で
363/// 要素へ取り付ける定番パターン)。HTML 文書のため仕様どおり名前を
364/// 小文字化し、値は空文字列・`ownerElement` は `null`(未取り付け)で返す。
365pub fn document_create_attribute(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
366    let name = arg(a, 0).to_js_string().to_lowercase();
367    Ok(make_attr_node(&name, "", &Value::Null))
368}
369/// `element.getAttributeNode(name)`(丸ごと未対応だった。`getAttribute`
370/// より古い/低レベルな `Attr` ノード経由の定番イディオム)。この処理系には
371/// `Attr` 専用の内部表現が無いため、`element.attributes` と同じ
372/// `{name, value, ownerElement}` の簡略オブジェクトを返す。
373pub fn dom_get_attribute_node(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
374    if let Some(idx) = this_dom_idx(&this) {
375        let name = arg(a, 0).to_js_string();
376        if let Some(v) = it.dom.borrow().get_attr(idx, &name) {
377            return Ok(make_attr_node(&name, &v, &this));
378        }
379    }
380    Ok(Value::Null)
381}
382/// `element.setAttributeNode(attr)`。`attr.name`/`attr.value` を読み取って
383/// 通常の `setAttribute` と同じストアへ反映する。戻り値は仕様上「置換された
384/// 旧 Attr ノード(無ければ null)」。
385pub fn dom_set_attribute_node(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
386    if let Some(idx) = this_dom_idx(&this) {
387        let attr = arg(a, 0);
388        if matches!(attr, Value::Object(_)) {
389            let name = obj_prop(&attr, "name").map(|v| v.to_js_string()).unwrap_or_default();
390            let value = obj_prop(&attr, "value").map(|v| v.to_js_string()).unwrap_or_default();
391            let old = it.dom.borrow().get_attr(idx, &name);
392            it.dom.borrow_mut().set_attr(idx, &name, &value);
393            if let Some(old_v) = old {
394                return Ok(make_attr_node(&name, &old_v, &this));
395            }
396        }
397    }
398    Ok(Value::Null)
399}
400/// `element.getAttributeNodeNS(namespace, localName)`(丸ごと未対応だった)。
401/// 他の `*AttributeNS` と同じく `namespace` 引数は無視し `localName` で検索する。
402pub fn dom_get_attribute_node_ns(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
403    if let Some(idx) = this_dom_idx(&this) {
404        let name = if a.len() >= 2 {
405            arg(a, 1).to_js_string()
406        } else {
407            arg(a, 0).to_js_string()
408        };
409        if let Some(v) = it.dom.borrow().get_attr(idx, &name) {
410            return Ok(make_attr_node(&name, &v, &this));
411        }
412    }
413    Ok(Value::Null)
414}
415/// `element.removeAttributeNode(attr)`。`attr.name` の属性を削除し、削除した
416/// `Attr` ノードを返す(仕様上、未存在なら `NotFoundError` だがこの処理系は
417/// 例外機構が限定的なため簡略化し `null` を返す)。
418pub fn dom_remove_attribute_node(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
419    if let Some(idx) = this_dom_idx(&this) {
420        let name = obj_prop(&arg(a, 0), "name").map(|v| v.to_js_string()).unwrap_or_default();
421        // `it.dom.borrow()` の一時 `Ref` を `if let` ブロック内で保持したまま
422        // `borrow_mut()` すると panic する既知のバグ族(`select_options_remove`
423        // で実際に発生した既知パターン。2026-07-14 の RefCell 二重借用横断監査で
424        // この形自体は洗い出し済みだったが、同日の新規コードでまた作り込んで
425        // しまった)。owned な値へ一度 materialize してから使う。
426        let old = it.dom.borrow().get_attr(idx, &name);
427        if let Some(v) = old {
428            it.dom.borrow_mut().remove_attr(idx, &name);
429            return Ok(make_attr_node(&name, &v, &this));
430        }
431    }
432    Ok(Value::Null)
433}
434
435pub fn dom_remove_attribute(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
436    if let Some(idx) = this_dom_idx(&this) {
437        let name = arg(a, 0).to_js_string();
438        it.dom.borrow_mut().remove_attr(idx, &name);
439    }
440    Ok(Value::Undefined)
441}
442
443/// `getAttributeNS`/`setAttributeNS`/`hasAttributeNS`/`removeAttributeNS`
444/// (丸ごと未対応だった。SVG 等で `setAttributeNS(null, 'viewBox', ...)` のように
445/// 使われる定番イディオム)。この処理系は XML 名前空間を一切モデル化していないため、
446/// 第1引数(namespace)は無視して非 NS 版と同じ属性ストアへ委譲する簡略実装
447/// (多くの実用コードは namespace に `null` を渡すため実害は小さい)。
448pub fn dom_get_attribute_ns(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
449    dom_get_attribute(it, this, &a[1.min(a.len())..])
450}
451pub fn dom_set_attribute_ns(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
452    dom_set_attribute(it, this, &a[1.min(a.len())..])
453}
454pub fn dom_has_attribute_ns(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
455    dom_has_attribute(it, this, &a[1.min(a.len())..])
456}
457pub fn dom_remove_attribute_ns(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
458    dom_remove_attribute(it, this, &a[1.min(a.len())..])
459}
460
461pub fn dom_get_attribute_names(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
462    if let Some(idx) = this_dom_idx(&this) {
463        let names: Vec<Value> = it
464            .dom
465            .borrow()
466            .attr_names(idx)
467            .into_iter()
468            .map(Value::str)
469            .collect();
470        return Ok(Value::Object(Obj::array(names)));
471    }
472    Ok(Value::Object(Obj::array(Vec::new())))
473}
474/// `element.hasAttributes()`(丸ごと未対応だった。単一属性の有無を見る `hasAttribute(name)`
475/// は既に実装済みだったが、「1つでも属性を持つか」を見るこちらが漏れていた)。
476pub fn dom_has_attributes(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
477    if let Some(idx) = this_dom_idx(&this) {
478        return Ok(Value::Bool(!it.dom.borrow().attr_names(idx).is_empty()));
479    }
480    Ok(Value::Bool(false))
481}
482/// `node.normalize()`(丸ごと未対応だった。連続する隣接テキストノードを1つに連結し、
483/// 空のテキストノードを取り除く DOM 標準メソッド)。
484pub fn dom_normalize(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
485    if let Some(idx) = this_dom_idx(&this) {
486        it.dom.borrow_mut().normalize_node(idx);
487    }
488    Ok(Value::Undefined)
489}
490/// `CharacterData.appendData`/`.deleteData`/`.insertData`/`.replaceData`/
491/// `.substringData`(丸ごと未対応だった。2026-07-16 発見)。テキストノード
492/// のみに意味を持つ(呼び出し元の interp.rs 側で `is_text` ガード済み)ため
493/// ここでは無条件にテキスト内容を読み書きする。他の文字列系メソッド
494/// (`str_slice`/`str_substring`)と同じ char 単位オフセットの簡略実装
495/// (UTF-16 コード単位ではない)。
496fn char_data_chars(it: &Interp, idx: usize) -> alloc::vec::Vec<char> {
497    it.dom.borrow().get_text_content(idx).chars().collect()
498}
499pub fn dom_char_data_append(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
500    if let Some(idx) = this_dom_idx(&this) {
501        let mut s = it.dom.borrow().get_text_content(idx);
502        s.push_str(&arg(a, 0).to_js_string());
503        it.dom.borrow_mut().set_text_content(idx, &s);
504    }
505    Ok(Value::Undefined)
506}
507/// `CharacterData`編集メソッド群(`deleteData`/`insertData`/`replaceData`/
508/// `substringData`)は仕様上、`offset`が`length`を超える場合`IndexSizeError`を
509/// 投げるべき(`count`側は超過分を自動的にクランプするだけで例外にはならない)
510/// が、以前は`offset`も`len`へ黙ってクランプしてしまい、範囲外呼び出しが
511/// 常に無害な no-op(`deleteData`/`substringData`)や末尾への追記
512/// (`insertData`)として成立していた。`table.insertRow`等と同じ「範囲外
513/// インデックスの黙殺」バグ族の一つ。
514fn char_data_check_offset(offset_arg: Value, len: usize) -> Result<usize, Value> {
515    let raw = offset_arg.to_number();
516    let offset = if raw.is_finite() { raw.max(0.0) as usize } else { 0 };
517    if offset > len {
518        return Err(make_dom_exception(
519            "IndexSizeError",
520            "The offset is greater than the length of the data",
521        ));
522    }
523    Ok(offset)
524}
525pub fn dom_char_data_delete(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
526    if let Some(idx) = this_dom_idx(&this) {
527        let chars = char_data_chars(it, idx);
528        let len = chars.len();
529        let offset = char_data_check_offset(arg(a, 0), len)?;
530        let count = (arg(a, 1).to_number().max(0.0) as usize).min(len - offset);
531        let out: String = chars[..offset].iter().chain(chars[offset + count..].iter()).collect();
532        it.dom.borrow_mut().set_text_content(idx, &out);
533    }
534    Ok(Value::Undefined)
535}
536pub fn dom_char_data_insert(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
537    if let Some(idx) = this_dom_idx(&this) {
538        let chars = char_data_chars(it, idx);
539        let len = chars.len();
540        let offset = char_data_check_offset(arg(a, 0), len)?;
541        let insert = arg(a, 1).to_js_string();
542        let out: String = chars[..offset]
543            .iter()
544            .collect::<String>()
545            + &insert
546            + &chars[offset..].iter().collect::<String>();
547        it.dom.borrow_mut().set_text_content(idx, &out);
548    }
549    Ok(Value::Undefined)
550}
551pub fn dom_char_data_replace(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
552    if let Some(idx) = this_dom_idx(&this) {
553        let chars = char_data_chars(it, idx);
554        let len = chars.len();
555        let offset = char_data_check_offset(arg(a, 0), len)?;
556        let count = (arg(a, 1).to_number().max(0.0) as usize).min(len - offset);
557        let replacement = arg(a, 2).to_js_string();
558        let out: String = chars[..offset]
559            .iter()
560            .collect::<String>()
561            + &replacement
562            + &chars[offset + count..].iter().collect::<String>();
563        it.dom.borrow_mut().set_text_content(idx, &out);
564    }
565    Ok(Value::Undefined)
566}
567pub fn dom_char_data_substring(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
568    if let Some(idx) = this_dom_idx(&this) {
569        let chars = char_data_chars(it, idx);
570        let len = chars.len();
571        let offset = char_data_check_offset(arg(a, 0), len)?;
572        let count = (arg(a, 1).to_number().max(0.0) as usize).min(len - offset);
573        return Ok(Value::str(chars[offset..offset + count].iter().collect::<String>()));
574    }
575    Ok(Value::str(""))
576}
577/// `Text.splitText(offset)`(丸ごと未対応だった。2026-07-16 発見。
578/// `CharacterData`編集メソッド群の隣で発見。テキストノードを`offset`位置で
579/// 2つに分割し、後半を新しいテキストノードとして直後の兄弟に挿入して返す
580/// 仕様どおりの動作。`char_data_chars`と同じ char 単位オフセットの簡略実装)。
581pub fn dom_split_text(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
582    let Some(idx) = this_dom_idx(&this) else {
583        return Ok(Value::Undefined);
584    };
585    let chars = char_data_chars(it, idx);
586    let len = chars.len();
587    // `deleteData`等と同じ仕様: `offset > length`は`IndexSizeError`(詳細は
588    // `char_data_check_offset`のコメント参照)。
589    let offset = char_data_check_offset(arg(a, 0), len)?;
590    let before: String = chars[..offset].iter().collect();
591    let after: String = chars[offset..].iter().collect();
592    it.dom.borrow_mut().set_text_content(idx, &before);
593    let new_idx = it.dom.borrow_mut().create_text_node(&after);
594    let (parent, next_sibling) = {
595        let dom = it.dom.borrow();
596        match dom.nodes.get(idx).and_then(|n| n.parent) {
597            Some(p) => {
598                let pos = dom.nodes[p].children.iter().position(|&c| c == idx);
599                let next = pos.and_then(|i| dom.nodes[p].children.get(i + 1)).copied();
600                (Some(p), next)
601            }
602            None => (None, None),
603        }
604    };
605    if let Some(p) = parent {
606        it.dom.borrow_mut().insert_before(p, new_idx, next_sibling);
607    }
608    Ok(Value::Object(Obj::dom(new_idx)))
609}
610
611/// HTML5 Table DOM(`table.insertRow`/`.deleteRow`/`.rows`、`tr.insertCell`/
612/// `.deleteCell`/`.cells`)が丸ごと未対応だった(2026-07-17 発見。`select.
613/// options.add`/`.remove` と同じ「動的に構築する」定番イディオム)。`thead`/
614/// `tbody`/`tfoot` を明示的にモデル化せず、常に文書順の `descendants_by_tags`
615/// で `tr`/`td`/`th` を集約する簡略実装(新規行は「参照行の親」=既存の
616/// `tbody`があればその中、無ければ`table`直下へ挿入する)。
617pub fn dom_table_insert_row(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
618    let Some(table_idx) = this_dom_idx(&this) else {
619        return Ok(Value::Undefined);
620    };
621    let rows = it.dom.borrow().descendants_by_tags(table_idx, &["tr"]);
622    let len = rows.len();
623    let pos = arg(a, 0).to_number();
624    let pos = if pos.is_nan() { -1 } else { pos as i64 };
625    // 仕様上 `insertRow(index)` の有効範囲は `-1 <= index <= rows.length`
626    // (`-1`/`length`は末尾への追加を意味する)で、範囲外は`IndexSizeError`を
627    // 投げるべきだが、以前は範囲外の値をすべて「末尾へ追加」として黙って
628    // 受け入れてしまっていた(`table.insertRow(9999)`が例外にならず末尾追加
629    // として成功していた)。`insertCell`/`deleteRow`/`deleteCell`にも同じ
630    // パターンのバグがあり、まとめて修正した。
631    if pos < -1 || pos > len as i64 {
632        return Err(make_dom_exception(
633            "IndexSizeError",
634            "The index provided is outside the range of rows represented by this table",
635        ));
636    }
637    let (parent, ref_child) = if pos >= 0 && (pos as usize) < len {
638        let r = rows[pos as usize];
639        (it.dom.borrow().nodes.get(r).and_then(|n| n.parent).unwrap_or(table_idx), Some(r))
640    } else if let Some(&last) = rows.last() {
641        (it.dom.borrow().nodes.get(last).and_then(|n| n.parent).unwrap_or(table_idx), None)
642    } else {
643        (table_idx, None)
644    };
645    let new_row = it.dom.borrow_mut().create_element("tr");
646    it.dom.borrow_mut().insert_before(parent, new_row, ref_child);
647    Ok(Value::Object(Obj::dom(new_row)))
648}
649pub fn dom_table_delete_row(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
650    let Some(table_idx) = this_dom_idx(&this) else {
651        return Ok(Value::Undefined);
652    };
653    let rows = it.dom.borrow().descendants_by_tags(table_idx, &["tr"]);
654    let pos = arg(a, 0).to_number();
655    let pos = if pos.is_nan() { -1 } else { pos as i64 };
656    // 仕様上 `deleteRow(index)` の有効範囲は `-1 <= index < rows.length`
657    // (`-1`は末尾行の削除を意味する)で、範囲外は`IndexSizeError`を投げる
658    // べきだが、以前は範囲外の値をすべて「末尾行を削除」として黙って受け
659    // 入れてしまっていた(詳細は`insertRow`側の同種コメント参照)。
660    if pos < -1 || pos >= rows.len() as i64 {
661        return Err(make_dom_exception(
662            "IndexSizeError",
663            "The index provided is outside the range of rows represented by this table",
664        ));
665    }
666    let target = if pos >= 0 {
667        Some(rows[pos as usize])
668    } else {
669        rows.last().copied()
670    };
671    if let Some(r) = target {
672        it.dom.borrow_mut().remove_node(r);
673    }
674    Ok(Value::Undefined)
675}
676pub fn dom_table_insert_cell(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
677    let Some(row_idx) = this_dom_idx(&this) else {
678        return Ok(Value::Undefined);
679    };
680    let cells = it.dom.borrow().descendants_by_tags(row_idx, &["td", "th"]);
681    let len = cells.len();
682    let pos = arg(a, 0).to_number();
683    let pos = if pos.is_nan() { -1 } else { pos as i64 };
684    // `insertRow`と同じ仕様上の有効範囲チェック(`-1 <= index <= cells.length`)。
685    // 詳細はそちらの同種コメント参照。
686    if pos < -1 || pos > len as i64 {
687        return Err(make_dom_exception(
688            "IndexSizeError",
689            "The index provided is outside the range of cells represented by this row",
690        ));
691    }
692    let ref_child = if pos >= 0 && (pos as usize) < len { Some(cells[pos as usize]) } else { None };
693    let new_cell = it.dom.borrow_mut().create_element("td");
694    it.dom.borrow_mut().insert_before(row_idx, new_cell, ref_child);
695    Ok(Value::Object(Obj::dom(new_cell)))
696}
697pub fn dom_table_delete_cell(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
698    let Some(row_idx) = this_dom_idx(&this) else {
699        return Ok(Value::Undefined);
700    };
701    let cells = it.dom.borrow().descendants_by_tags(row_idx, &["td", "th"]);
702    let pos = arg(a, 0).to_number();
703    let pos = if pos.is_nan() { -1 } else { pos as i64 };
704    // `deleteRow`と同じ仕様上の有効範囲チェック(`-1 <= index < cells.length`)。
705    // 詳細はそちらの同種コメント参照。
706    if pos < -1 || pos >= cells.len() as i64 {
707        return Err(make_dom_exception(
708            "IndexSizeError",
709            "The index provided is outside the range of cells represented by this row",
710        ));
711    }
712    let target = if pos >= 0 {
713        Some(cells[pos as usize])
714    } else {
715        cells.last().copied()
716    };
717    if let Some(c) = target {
718        it.dom.borrow_mut().remove_node(c);
719    }
720    Ok(Value::Undefined)
721}
722
723/// `table.createTHead()`/`.createTFoot()`/`.createCaption()`(丸ごと未対応
724/// だった。2026-07-17 発見。仕様上「既存の該当要素があればそれを返す、
725/// 無ければ新規作成して挿入する」冪等な動作)。直接の子のみを対象とする
726/// (`tHead`/`tFoot`/`caption`ゲッターと同じ制約)。`caption`は最初の子、
727/// `thead`は最初の子(`caption`の後)、`tfoot`は最後の子として挿入する
728/// 簡略実装(`colgroup`との相対位置関係は厳密には対象外)。
729fn table_create_section(it: &mut Interp, table_idx: usize, tag: &str, append: bool) -> Value {
730    let existing = {
731        let dom = it.dom.borrow();
732        dom.nodes[table_idx].children.iter().find(|&&c| dom.nodes[c].tag == tag).copied()
733    };
734    if let Some(i) = existing {
735        return Value::Object(Obj::dom(i));
736    }
737    let new_idx = it.dom.borrow_mut().create_element(tag);
738    let ref_child = if append {
739        None
740    } else {
741        it.dom.borrow().nodes[table_idx].children.first().copied()
742    };
743    it.dom.borrow_mut().insert_before(table_idx, new_idx, ref_child);
744    Value::Object(Obj::dom(new_idx))
745}
746fn table_delete_section(it: &mut Interp, table_idx: usize, tag: &str) {
747    let existing = {
748        let dom = it.dom.borrow();
749        dom.nodes[table_idx].children.iter().find(|&&c| dom.nodes[c].tag == tag).copied()
750    };
751    if let Some(i) = existing {
752        it.dom.borrow_mut().remove_node(i);
753    }
754}
755pub fn dom_table_create_thead(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
756    Ok(match this_dom_idx(&this) {
757        Some(idx) => table_create_section(it, idx, "thead", false),
758        None => Value::Undefined,
759    })
760}
761pub fn dom_table_create_tfoot(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
762    Ok(match this_dom_idx(&this) {
763        Some(idx) => table_create_section(it, idx, "tfoot", true),
764        None => Value::Undefined,
765    })
766}
767pub fn dom_table_create_caption(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
768    Ok(match this_dom_idx(&this) {
769        Some(idx) => table_create_section(it, idx, "caption", false),
770        None => Value::Undefined,
771    })
772}
773pub fn dom_table_delete_thead(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
774    if let Some(idx) = this_dom_idx(&this) {
775        table_delete_section(it, idx, "thead");
776    }
777    Ok(Value::Undefined)
778}
779pub fn dom_table_delete_tfoot(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
780    if let Some(idx) = this_dom_idx(&this) {
781        table_delete_section(it, idx, "tfoot");
782    }
783    Ok(Value::Undefined)
784}
785pub fn dom_table_delete_caption(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
786    if let Some(idx) = this_dom_idx(&this) {
787        table_delete_section(it, idx, "caption");
788    }
789    Ok(Value::Undefined)
790}
791
792/// `img.decode()`(丸ごと未対応だった。2026-07-17 発見)。実デコード成否を
793/// 追跡する経路が無いため、`complete`ゲッターと同じ「`src`があれば
794/// ロード試行済み」前提で、`src`があれば解決・無ければ`DOMException
795/// ("EncodingError")`で拒否するPromiseを返す簡略実装。
796pub fn dom_img_decode(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
797    let has_src = this_dom_idx(&this)
798        .map(|idx| it.dom.borrow().get_attr(idx, "src").map(|s| !s.is_empty()).unwrap_or(false))
799        .unwrap_or(false);
800    if has_src {
801        Ok(resolved_promise(it, Value::Undefined))
802    } else {
803        let reason = make_dom_exception("EncodingError", "The source image cannot be decoded");
804        Ok(rejected_promise(it, reason))
805    }
806}
807
808/// 現在の`.value`文字列を読む(`textarea`は`value`属性優先、無ければ
809/// `initial_text`にフォールバック。`interp/dom_props.rs`の`value`
810/// ゲッターと同じロジック)。
811fn current_field_value(it: &Interp, idx: usize) -> String {
812    let dom = it.dom.borrow();
813    let tag = dom.nodes.get(idx).map(|n| n.tag.as_str()).unwrap_or("");
814    if tag == "textarea" {
815        dom.get_attr(idx, "_live_value")
816            .or_else(|| dom.get_attr(idx, "value"))
817            .unwrap_or_else(|| dom.nodes.get(idx).map(|n| n.initial_text.clone()).unwrap_or_default())
818    } else {
819        dom.get_attr(idx, "_live_value")
820            .or_else(|| dom.get_attr(idx, "value"))
821            .unwrap_or_default()
822    }
823}
824/// `input`/`textarea.setSelectionRange(start, end, direction?)`(丸ごと
825/// 未対応だった。`selectionStart`/`.selectionEnd`/`.selectionDirection`
826/// と対になる一括設定メソッド。2026-07-17 発見・実装)。3プロパティへの
827/// 個別代入と同じ内部専用属性へまとめて保存する簡略実装。
828pub fn dom_set_selection_range(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
829    if let Some(idx) = this_dom_idx(&this) {
830        let start = arg(a, 0).to_number();
831        let end = arg(a, 1).to_number();
832        let direction = if matches!(arg(a, 2), Value::Undefined) {
833            String::from("none")
834        } else {
835            arg(a, 2).to_js_string()
836        };
837        let mut dom = it.dom.borrow_mut();
838        dom.set_attr(idx, "_selection_start", &Value::Number(start).to_js_string());
839        dom.set_attr(idx, "_selection_end", &Value::Number(end).to_js_string());
840        dom.set_attr(idx, "_selection_direction", &direction);
841    }
842    Ok(Value::Undefined)
843}
844/// `input`/`textarea.setRangeText(replacement, start?, end?, selectMode?)`
845/// (丸ごと未対応だった。2026-07-17 発見・実装)。`start`/`end`省略時は
846/// 現在の`selectionStart`/`.selectionEnd`を使う(仕様どおり)。
847/// `selectMode`は`"select"`(置換後の範囲を選択)/`"start"`(キャレットを
848/// 置換範囲の先頭へ)/`"end"`(末尾へ、既定)/`"preserve"`(元の選択位置を
849/// 維持しようと試みる)に対応する簡略実装。
850pub fn dom_set_range_text(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
851    if let Some(idx) = this_dom_idx(&this) {
852        let replacement = arg(a, 0).to_js_string();
853        let chars: alloc::vec::Vec<char> = current_field_value(it, idx).chars().collect();
854        let len = chars.len();
855        let cur_start = it
856            .dom
857            .borrow()
858            .get_attr(idx, "_selection_start")
859            .and_then(|v| v.parse::<f64>().ok())
860            .unwrap_or(0.0);
861        let cur_end = it
862            .dom
863            .borrow()
864            .get_attr(idx, "_selection_end")
865            .and_then(|v| v.parse::<f64>().ok())
866            .unwrap_or(0.0);
867        let start = if matches!(arg(a, 1), Value::Undefined) { cur_start } else { arg(a, 1).to_number() };
868        let end = if matches!(arg(a, 2), Value::Undefined) { cur_end } else { arg(a, 2).to_number() };
869        let start = (start.max(0.0) as usize).min(len);
870        let end = (end.max(0.0) as usize).min(len).max(start);
871        let select_mode = if matches!(arg(a, 3), Value::Undefined) { String::from("preserve") } else { arg(a, 3).to_js_string() };
872        let new_value: String =
873            chars[..start].iter().chain(replacement.chars().collect::<alloc::vec::Vec<_>>().iter()).chain(chars[end..].iter()).collect();
874        let new_end = start + replacement.chars().count();
875        let (sel_start, sel_end) = match select_mode.as_str() {
876            "select" => (start, new_end),
877            "start" => (start, start),
878            "end" => (new_end, new_end),
879            _ => (start, new_end), // "preserve" は簡略実装として置換範囲を返す
880        };
881        let mut dom = it.dom.borrow_mut();
882        dom.set_attr(idx, "_live_value", &new_value);
883        dom.set_attr(idx, "_selection_start", &Value::Number(sel_start as f64).to_js_string());
884        dom.set_attr(idx, "_selection_end", &Value::Number(sel_end as f64).to_js_string());
885    }
886    Ok(Value::Undefined)
887}
888/// `node.isSameNode(other)`(丸ごと未対応だった。DOM 標準の参照同一性判定。`===` と
889/// 同義だが、DOM ノードを扱うライブラリで明示的に使われる定番メソッド)。
890pub fn dom_is_same_node(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
891    let self_idx = this_dom_idx(&this);
892    let other_idx = this_dom_idx(&arg(a, 0));
893    Ok(Value::Bool(self_idx.is_some() && self_idx == other_idx))
894}
895/// `node.isEqualNode(other)`(丸ごと未対応だった。参照ではなく構造的な等価性
896/// — タグ名/属性/子要素が全て一致するか — を見る DOM 標準メソッド)。既存の
897/// `outerHTML` シリアライズを再利用し、文字列として一致するかで近似する
898/// (属性は `BTreeMap` 由来で常に同じ順序にシリアライズされるため、挿入順序に
899/// 依存しない安定した比較になる)。
900pub fn dom_is_equal_node(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
901    let self_idx = this_dom_idx(&this);
902    let other_idx = this_dom_idx(&arg(a, 0));
903    match (self_idx, other_idx) {
904        (Some(ai), Some(bi)) => {
905            let dom = it.dom.borrow();
906            Ok(Value::Bool(dom.get_outer_html(ai) == dom.get_outer_html(bi)))
907        }
908        _ => Ok(Value::Bool(false)),
909    }
910}
911
912/// element.getBoundingClientRect(): レイアウト後の矩形(DomBridge.rects、ページ座標)から
913/// DOMRect 風オブジェクト {x,left,y,top,width,height,right,bottom} を返す。未登録なら 0。
914/// `element.setPointerCapture(pointerId)`(Pointer Events。丸ごと未対応
915/// だった。ドラッグ操作で `el.setPointerCapture(e.pointerId)` する定番
916/// パターン)。
917pub fn dom_set_pointer_capture(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
918    if let Some(idx) = this_dom_idx(&this) {
919        it.dom.borrow_mut().set_pointer_capture(idx, arg(a, 0).to_number() as i32);
920    }
921    Ok(Value::Undefined)
922}
923/// `element.releasePointerCapture(pointerId)`。
924pub fn dom_release_pointer_capture(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
925    if let Some(idx) = this_dom_idx(&this) {
926        it.dom
927            .borrow_mut()
928            .release_pointer_capture(idx, arg(a, 0).to_number() as i32);
929    }
930    Ok(Value::Undefined)
931}
932/// `element.hasPointerCapture(pointerId)`。
933pub fn dom_has_pointer_capture(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
934    let has = this_dom_idx(&this)
935        .map(|idx| it.dom.borrow().has_pointer_capture(idx, arg(a, 0).to_number() as i32))
936        .unwrap_or(false);
937    Ok(Value::Bool(has))
938}
939
940/// `(x,y,w,h)` から `DOMRect` 相当のプレーンオブジェクトを組み立てる。
941/// `getBoundingClientRect`/`getClientRects` で共有する。
942/// `new DOMRect(x, y, width, height)`(丸ごと未対応だった。手動で矩形を
943/// 構築する定番パターン。2026-07-17 発見)。`getBoundingClientRect()`が
944/// 返す`make_dom_rect`と同じプロパティ形状(`x`/`y`/`left`/`top`/
945/// `width`/`height`/`right`/`bottom`)だが、引数は整数丸め無しの浮動小数
946/// 点のまま保持する(手動構築ではサブピクセル値も仕様上有効なため)。
947/// `DOMRectReadOnly`も同じ形状のため区別せず同一コンストラクタで代用する
948/// 簡略実装(`fromRect`静的メソッド・書き込み可能プロパティの違いは
949/// 対象外)。
950pub(crate) fn dom_rect_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
951    let x = arg(a, 0).to_number();
952    let y = arg(a, 1).to_number();
953    let w = arg(a, 2).to_number();
954    let h = arg(a, 3).to_number();
955    let x = if x.is_nan() { 0.0 } else { x };
956    let y = if y.is_nan() { 0.0 } else { y };
957    let w = if w.is_nan() { 0.0 } else { w };
958    let h = if h.is_nan() { 0.0 } else { h };
959    let o = Obj::plain();
960    {
961        let mut b = o.borrow_mut();
962        b.props.insert("x".into(), Value::Number(x));
963        b.props.insert("left".into(), Value::Number(x));
964        b.props.insert("y".into(), Value::Number(y));
965        b.props.insert("top".into(), Value::Number(y));
966        b.props.insert("width".into(), Value::Number(w));
967        b.props.insert("height".into(), Value::Number(h));
968        b.props.insert("right".into(), Value::Number(x + w));
969        b.props.insert("bottom".into(), Value::Number(y + h));
970        b.props.insert(
971            "toJSON".into(),
972            nv("toJSON", |_: &mut Interp, this: Value, _: &[Value]| {
973                if let Value::Object(o) = &this {
974                    let clone = Obj::plain();
975                    for (k, v) in o.borrow().props.iter() {
976                        if k != "toJSON" {
977                            clone.borrow_mut().props.insert(k.clone(), v.clone());
978                        }
979                    }
980                    return Ok(Value::Object(clone));
981                }
982                Ok(Value::Undefined)
983            }),
984        );
985    }
986    Ok(Value::Object(o))
987}
988fn get_num_prop(it: &mut Interp, val: &Value, name: &str, default: f64) -> f64 {
989    it.get_property(val, name).map(|v| v.to_number()).unwrap_or(default)
990}
991
992pub fn dom_point_matrix_transform(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
993    let px = get_num_prop(it, &this, "x", 0.0);
994    let py = get_num_prop(it, &this, "y", 0.0);
995    let pz = get_num_prop(it, &this, "z", 0.0);
996    let pw = get_num_prop(it, &this, "w", 1.0);
997    let m = arg(a, 0);
998
999    let def_a = get_num_prop(it, &m, "a", 1.0);
1000    let m11 = get_num_prop(it, &m, "m11", def_a);
1001    let def_b = get_num_prop(it, &m, "b", 0.0);
1002    let m12 = get_num_prop(it, &m, "m12", def_b);
1003    let m13 = get_num_prop(it, &m, "m13", 0.0);
1004    let m14 = get_num_prop(it, &m, "m14", 0.0);
1005
1006    let def_c = get_num_prop(it, &m, "c", 0.0);
1007    let m21 = get_num_prop(it, &m, "m21", def_c);
1008    let def_d = get_num_prop(it, &m, "d", 1.0);
1009    let m22 = get_num_prop(it, &m, "m22", def_d);
1010    let m23 = get_num_prop(it, &m, "m23", 0.0);
1011    let m24 = get_num_prop(it, &m, "m24", 0.0);
1012
1013    let m31 = get_num_prop(it, &m, "m31", 0.0);
1014    let m32 = get_num_prop(it, &m, "m32", 0.0);
1015    let m33 = get_num_prop(it, &m, "m33", 1.0);
1016    let m34 = get_num_prop(it, &m, "m34", 0.0);
1017
1018    let def_e = get_num_prop(it, &m, "e", 0.0);
1019    let m41 = get_num_prop(it, &m, "m41", def_e);
1020    let def_f = get_num_prop(it, &m, "f", 0.0);
1021    let m42 = get_num_prop(it, &m, "m42", def_f);
1022    let m43 = get_num_prop(it, &m, "m43", 0.0);
1023    let m44 = get_num_prop(it, &m, "m44", 1.0);
1024
1025    let nx = m11 * px + m21 * py + m31 * pz + m41 * pw;
1026    let ny = m12 * px + m22 * py + m32 * pz + m42 * pw;
1027    let nz = m13 * px + m23 * py + m33 * pz + m43 * pw;
1028    let nw = m14 * px + m24 * py + m34 * pz + m44 * pw;
1029    dom_point_ctor(it, Value::Undefined, &[Value::Number(nx), Value::Number(ny), Value::Number(nz), Value::Number(nw)])
1030}
1031
1032/// `new DOMPoint(x, y, z, w)`(幾何形体プリミティブ。2026-07-17 発見、2026-07-21 拡張)。
1033/// 仕様どおり `z`/`w` 省略時の既定値はそれぞれ `0`/`1`。`DOMPointReadOnly` も同形状で代用。
1034/// `matrixTransform(matrix)` で `DOMMatrix` による座標変換に対応。
1035pub(crate) fn dom_point_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1036    let x = arg(a, 0).to_number();
1037    let y = arg(a, 1).to_number();
1038    let z = arg(a, 2).to_number();
1039    let w = arg(a, 3).to_number();
1040    let x = if x.is_nan() { 0.0 } else { x };
1041    let y = if y.is_nan() { 0.0 } else { y };
1042    let z = if z.is_nan() { 0.0 } else { z };
1043    let w = if matches!(arg(a, 3), Value::Undefined) || w.is_nan() { 1.0 } else { w };
1044    let o = Obj::plain();
1045    {
1046        let mut b = o.borrow_mut();
1047        b.props.insert("x".into(), Value::Number(x));
1048        b.props.insert("y".into(), Value::Number(y));
1049        b.props.insert("z".into(), Value::Number(z));
1050        b.props.insert("w".into(), Value::Number(w));
1051        b.props.insert(
1052            "matrixTransform".into(),
1053            nv("matrixTransform", dom_point_matrix_transform),
1054        );
1055        b.props.insert(
1056            "toJSON".into(),
1057            nv("toJSON", |_: &mut Interp, this: Value, _: &[Value]| {
1058                if let Value::Object(o) = &this {
1059                    let clone = Obj::plain();
1060                    for (k, v) in o.borrow().props.iter() {
1061                        if k != "toJSON" && k != "matrixTransform" {
1062                            clone.borrow_mut().props.insert(k.clone(), v.clone());
1063                        }
1064                    }
1065                    return Ok(Value::Object(clone));
1066                }
1067                Ok(Value::Undefined)
1068            }),
1069        );
1070    }
1071    Ok(Value::Object(o))
1072}
1073
1074pub(crate) fn dom_rect_from_rect(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1075    let r = arg(a, 0);
1076    let x = get_num_prop(it, &r, "x", 0.0);
1077    let y = get_num_prop(it, &r, "y", 0.0);
1078    let w = get_num_prop(it, &r, "width", 0.0);
1079    let h = get_num_prop(it, &r, "height", 0.0);
1080    dom_rect_ctor(it, Value::Undefined, &[Value::Number(x), Value::Number(y), Value::Number(w), Value::Number(h)])
1081}
1082
1083pub(crate) fn dom_point_from_point(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1084    let p = arg(a, 0);
1085    let x = get_num_prop(it, &p, "x", 0.0);
1086    let y = get_num_prop(it, &p, "y", 0.0);
1087    let z = get_num_prop(it, &p, "z", 0.0);
1088    let w = get_num_prop(it, &p, "w", 1.0);
1089    dom_point_ctor(it, Value::Undefined, &[Value::Number(x), Value::Number(y), Value::Number(z), Value::Number(w)])
1090}
1091
1092pub fn dom_matrix_to_string(it: &mut Interp, this: Value, _: &[Value]) -> Result<Value, Value> {
1093    let is_2d = it.get_property(&this, "is2D").map(|v| v.truthy()).unwrap_or(true);
1094    let m11 = get_num_prop(it, &this, "m11", 1.0);
1095    let m12 = get_num_prop(it, &this, "m12", 0.0);
1096    let m13 = get_num_prop(it, &this, "m13", 0.0);
1097    let m14 = get_num_prop(it, &this, "m14", 0.0);
1098    let m21 = get_num_prop(it, &this, "m21", 0.0);
1099    let m22 = get_num_prop(it, &this, "m22", 1.0);
1100    let m23 = get_num_prop(it, &this, "m23", 0.0);
1101    let m24 = get_num_prop(it, &this, "m24", 0.0);
1102    let m31 = get_num_prop(it, &this, "m31", 0.0);
1103    let m32 = get_num_prop(it, &this, "m32", 0.0);
1104    let m33 = get_num_prop(it, &this, "m33", 1.0);
1105    let m34 = get_num_prop(it, &this, "m34", 0.0);
1106    let m41 = get_num_prop(it, &this, "m41", 0.0);
1107    let m42 = get_num_prop(it, &this, "m42", 0.0);
1108    let m43 = get_num_prop(it, &this, "m43", 0.0);
1109    let m44 = get_num_prop(it, &this, "m44", 1.0);
1110    if is_2d {
1111        Ok(Value::str(alloc::format!("matrix({}, {}, {}, {}, {}, {})", m11, m12, m21, m22, m41, m42)))
1112    } else {
1113        Ok(Value::str(alloc::format!(
1114            "matrix3d({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {})",
1115            m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44
1116        )))
1117    }
1118}
1119
1120pub fn dom_matrix_multiply(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1121    let m11 = get_num_prop(it, &this, "m11", 1.0);
1122    let m12 = get_num_prop(it, &this, "m12", 0.0);
1123    let m21 = get_num_prop(it, &this, "m21", 0.0);
1124    let m22 = get_num_prop(it, &this, "m22", 1.0);
1125    let m41 = get_num_prop(it, &this, "m41", 0.0);
1126    let m42 = get_num_prop(it, &this, "m42", 0.0);
1127
1128    let other = arg(a, 0);
1129    let om11 = get_num_prop(it, &other, "m11", 1.0);
1130    let om12 = get_num_prop(it, &other, "m12", 0.0);
1131    let om21 = get_num_prop(it, &other, "m21", 0.0);
1132    let om22 = get_num_prop(it, &other, "m22", 1.0);
1133    let om41 = get_num_prop(it, &other, "m41", 0.0);
1134    let om42 = get_num_prop(it, &other, "m42", 0.0);
1135
1136    let res_a = m11 * om11 + m21 * om12;
1137    let res_b = m12 * om11 + m22 * om12;
1138    let res_c = m11 * om21 + m21 * om22;
1139    let res_d = m12 * om21 + m22 * om22;
1140    let res_e = m11 * om41 + m21 * om42 + m41;
1141    let res_f = m12 * om41 + m22 * om42 + m42;
1142
1143    let arr = Value::Object(Obj::array(alloc::vec![
1144        Value::Number(res_a), Value::Number(res_b),
1145        Value::Number(res_c), Value::Number(res_d),
1146        Value::Number(res_e), Value::Number(res_f)
1147    ]));
1148    dom_matrix_ctor(it, Value::Undefined, &[arr])
1149}
1150
1151pub fn dom_matrix_translate(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1152    let m11 = get_num_prop(it, &this, "m11", 1.0);
1153    let m12 = get_num_prop(it, &this, "m12", 0.0);
1154    let m21 = get_num_prop(it, &this, "m21", 0.0);
1155    let m22 = get_num_prop(it, &this, "m22", 1.0);
1156    let m41 = get_num_prop(it, &this, "m41", 0.0);
1157    let m42 = get_num_prop(it, &this, "m42", 0.0);
1158
1159    let tx = arg(a, 0).to_number();
1160    let ty = arg(a, 1).to_number();
1161    let tx = if tx.is_nan() { 0.0 } else { tx };
1162    let ty = if ty.is_nan() { 0.0 } else { ty };
1163
1164    let res_e = m11 * tx + m21 * ty + m41;
1165    let res_f = m12 * tx + m22 * ty + m42;
1166
1167    let arr = Value::Object(Obj::array(alloc::vec![
1168        Value::Number(m11), Value::Number(m12),
1169        Value::Number(m21), Value::Number(m22),
1170        Value::Number(res_e), Value::Number(res_f)
1171    ]));
1172    dom_matrix_ctor(it, Value::Undefined, &[arr])
1173}
1174
1175pub fn dom_matrix_scale(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1176    let m11 = get_num_prop(it, &this, "m11", 1.0);
1177    let m12 = get_num_prop(it, &this, "m12", 0.0);
1178    let m21 = get_num_prop(it, &this, "m21", 0.0);
1179    let m22 = get_num_prop(it, &this, "m22", 1.0);
1180    let m41 = get_num_prop(it, &this, "m41", 0.0);
1181    let m42 = get_num_prop(it, &this, "m42", 0.0);
1182
1183    let sx = arg(a, 0).to_number();
1184    let sy = arg(a, 1).to_number();
1185    let sx = if sx.is_nan() { 1.0 } else { sx };
1186    let sy = if matches!(arg(a, 1), Value::Undefined) || sy.is_nan() { sx } else { sy };
1187
1188    let arr = Value::Object(Obj::array(alloc::vec![
1189        Value::Number(m11 * sx), Value::Number(m12 * sx),
1190        Value::Number(m21 * sy), Value::Number(m22 * sy),
1191        Value::Number(m41), Value::Number(m42)
1192    ]));
1193    dom_matrix_ctor(it, Value::Undefined, &[arr])
1194}
1195
1196pub fn dom_matrix_rotate(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1197    let m11 = get_num_prop(it, &this, "m11", 1.0);
1198    let m12 = get_num_prop(it, &this, "m12", 0.0);
1199    let m21 = get_num_prop(it, &this, "m21", 0.0);
1200    let m22 = get_num_prop(it, &this, "m22", 1.0);
1201    let m41 = get_num_prop(it, &this, "m41", 0.0);
1202    let m42 = get_num_prop(it, &this, "m42", 0.0);
1203
1204    let angle_deg = arg(a, 0).to_number();
1205    let angle_deg = if angle_deg.is_nan() { 0.0 } else { angle_deg };
1206    let rad = angle_deg * core::f64::consts::PI / 180.0;
1207    let cos_a = libm::cos(rad);
1208    let sin_a = libm::sin(rad);
1209
1210    let res_a = m11 * cos_a + m21 * sin_a;
1211    let res_b = m12 * cos_a + m22 * sin_a;
1212    let res_c = m11 * (-sin_a) + m21 * cos_a;
1213    let res_d = m12 * (-sin_a) + m22 * cos_a;
1214
1215    let arr = Value::Object(Obj::array(alloc::vec![
1216        Value::Number(res_a), Value::Number(res_b),
1217        Value::Number(res_c), Value::Number(res_d),
1218        Value::Number(m41), Value::Number(m42)
1219    ]));
1220    dom_matrix_ctor(it, Value::Undefined, &[arr])
1221}
1222
1223pub fn dom_matrix_invert(it: &mut Interp, this: Value, _: &[Value]) -> Result<Value, Value> {
1224    let a = get_num_prop(it, &this, "m11", 1.0);
1225    let b = get_num_prop(it, &this, "m12", 0.0);
1226    let c = get_num_prop(it, &this, "m21", 0.0);
1227    let d = get_num_prop(it, &this, "m22", 1.0);
1228    let e = get_num_prop(it, &this, "m41", 0.0);
1229    let f = get_num_prop(it, &this, "m42", 0.0);
1230
1231    let det = a * d - b * c;
1232    if det == 0.0 || det.is_nan() {
1233        let arr = Value::Object(Obj::array(alloc::vec![
1234            Value::Number(f64::NAN), Value::Number(f64::NAN),
1235            Value::Number(f64::NAN), Value::Number(f64::NAN),
1236            Value::Number(f64::NAN), Value::Number(f64::NAN)
1237        ]));
1238        return dom_matrix_ctor(it, Value::Undefined, &[arr]);
1239    }
1240    let inv_a = d / det;
1241    let inv_b = -b / det;
1242    let inv_c = -c / det;
1243    let inv_d = a / det;
1244    let inv_e = (c * f - d * e) / det;
1245    let inv_f = (b * e - a * f) / det;
1246
1247    let arr = Value::Object(Obj::array(alloc::vec![
1248        Value::Number(inv_a), Value::Number(inv_b),
1249        Value::Number(inv_c), Value::Number(inv_d),
1250        Value::Number(inv_e), Value::Number(inv_f)
1251    ]));
1252    dom_matrix_ctor(it, Value::Undefined, &[arr])
1253}
1254
1255fn update_matrix_props(target: &Value, new_m: Value) {
1256    if let (Value::Object(to), Value::Object(from)) = (target, &new_m) {
1257        let from_props = from.borrow().props.clone();
1258        let mut b = to.borrow_mut();
1259        for (k, v) in from_props {
1260            if k != "toJSON" && k != "toString" && k != "multiply" && k != "translate" && k != "scale" && k != "rotate" && k != "invert"
1261                && k != "multiplySelf" && k != "translateSelf" && k != "scaleSelf" && k != "rotateSelf" && k != "invertSelf" {
1262                b.props.insert(k, v);
1263            }
1264        }
1265    }
1266}
1267
1268pub fn dom_matrix_translate_self(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1269    let new_m = dom_matrix_translate(it, this.clone(), a)?;
1270    update_matrix_props(&this, new_m);
1271    Ok(this)
1272}
1273
1274pub fn dom_matrix_scale_self(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1275    let new_m = dom_matrix_scale(it, this.clone(), a)?;
1276    update_matrix_props(&this, new_m);
1277    Ok(this)
1278}
1279
1280pub fn dom_matrix_rotate_self(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1281    let new_m = dom_matrix_rotate(it, this.clone(), a)?;
1282    update_matrix_props(&this, new_m);
1283    Ok(this)
1284}
1285
1286pub fn dom_matrix_multiply_self(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1287    let new_m = dom_matrix_multiply(it, this.clone(), a)?;
1288    update_matrix_props(&this, new_m);
1289    Ok(this)
1290}
1291
1292pub fn dom_matrix_invert_self(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1293    let new_m = dom_matrix_invert(it, this.clone(), a)?;
1294    update_matrix_props(&this, new_m);
1295    Ok(this)
1296}
1297
1298pub fn dom_matrix_skew_x(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1299    let deg = arg(a, 0).to_number();
1300    let rad = deg * core::f64::consts::PI / 180.0;
1301    let tan_val = libm::tan(rad);
1302    let skew_m = dom_matrix_ctor(
1303        it,
1304        Value::Undefined,
1305        &[
1306            Value::Number(1.0),
1307            Value::Number(0.0),
1308            Value::Number(tan_val),
1309            Value::Number(1.0),
1310            Value::Number(0.0),
1311            Value::Number(0.0),
1312        ],
1313    )?;
1314    dom_matrix_multiply(it, this, &[skew_m])
1315}
1316
1317pub fn dom_matrix_skew_y(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1318    let deg = arg(a, 0).to_number();
1319    let rad = deg * core::f64::consts::PI / 180.0;
1320    let tan_val = libm::tan(rad);
1321    let skew_m = dom_matrix_ctor(
1322        it,
1323        Value::Undefined,
1324        &[
1325            Value::Number(1.0),
1326            Value::Number(tan_val),
1327            Value::Number(0.0),
1328            Value::Number(1.0),
1329            Value::Number(0.0),
1330            Value::Number(0.0),
1331        ],
1332    )?;
1333    dom_matrix_multiply(it, this, &[skew_m])
1334}
1335
1336pub fn dom_matrix_skew_x_self(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1337    let new_m = dom_matrix_skew_x(it, this.clone(), a)?;
1338    update_matrix_props(&this, new_m);
1339    Ok(this)
1340}
1341
1342pub fn dom_matrix_skew_y_self(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1343    let new_m = dom_matrix_skew_y(it, this.clone(), a)?;
1344    update_matrix_props(&this, new_m);
1345    Ok(this)
1346}
1347
1348pub fn dom_matrix_transform_point(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1349    let pt = arg(a, 0);
1350    dom_point_matrix_transform(it, pt, &[this])
1351}
1352
1353/// `new DOMMatrix([init])` / `DOMMatrixReadOnly` (Geometry Interfaces Module Level 1)
1354/// 2D / 3D 変換行列コンストラクタ。
1355pub(crate) fn dom_matrix_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1356    let mut m11 = 1.0; let mut m12 = 0.0; let mut m13 = 0.0; let mut m14 = 0.0;
1357    let mut m21 = 0.0; let mut m22 = 1.0; let mut m23 = 0.0; let mut m24 = 0.0;
1358    let mut m31 = 0.0; let mut m32 = 0.0; let mut m33 = 1.0; let mut m34 = 0.0;
1359    let mut m41 = 0.0; let mut m42 = 0.0; let mut m43 = 0.0; let mut m44 = 1.0;
1360
1361    let init = arg(a, 0);
1362    if let Value::Object(o) = &init {
1363        // `DOMMatrixInit`は仕様上「数値6/16個のシーケンス(配列/TypedArray、この
1364        // エンジンではどちらも`ObjKind::Array`)」か「`{a,b,c,d,e,f,...}`形式の
1365        // 平オブジェクト」のいずれか。以前は非配列オブジェクトも`it.iterate_values`
1366        // へ通していたが、この関数はプレーンオブジェクトに対しても(`Symbol.
1367        // iterator`を持たないにも関わらず)自身の列挙可能プロパティ値をそのまま
1368        // 返す簡略実装のため、`{a:2,d:3,e:5,f:10}`(4値)のような非配列dictでも
1369        // 空でないVecが返ってしまい、`arr.len()>=16`/`>=6`のどちらにも該当せず
1370        // 何も設定されないまま、本来読むべき`.a`/`.b`/`.c`/`.d`/`.e`/`.f`
1371        // プロパティ読み取りのelseブランチへ一切到達できないバグだった
1372        // (`DOMMatrix.fromMatrix({...})`が常に単位行列のまま返る)。
1373        // `ObjKind::Array`(TypedArrayもこの表現を流用)のときのみ数値シーケンス
1374        // として扱い、それ以外は無条件でプロパティ読み取り側へ回す。
1375        // 【2026-07-23続報】この修正自体だけでは`m2.a`/`m2.e`が期待値にならない
1376        // ことが判明しており、原因はこの関数の別の箇所(`.a`/`.e`プロパティの
1377        // 最終露出ロジック)に残っている可能性が高い。ロジックとしては正しい
1378        // 改善のため残すが、根本解決は未完了として記録する。
1379        let vals_opt = if let crate::os_lib::js::value::ObjKind::Array(ref arr) = o.borrow().kind {
1380            Some(arr.clone())
1381        } else {
1382            None
1383        };
1384        if let Some(arr) = vals_opt {
1385            if arr.len() >= 16 {
1386                m11 = arr[0].to_number(); m12 = arr[1].to_number(); m13 = arr[2].to_number(); m14 = arr[3].to_number();
1387                m21 = arr[4].to_number(); m22 = arr[5].to_number(); m23 = arr[6].to_number(); m24 = arr[7].to_number();
1388                m31 = arr[8].to_number(); m32 = arr[9].to_number(); m33 = arr[10].to_number(); m34 = arr[11].to_number();
1389                m41 = arr[12].to_number(); m42 = arr[13].to_number(); m43 = arr[14].to_number(); m44 = arr[15].to_number();
1390            } else if arr.len() >= 6 {
1391                m11 = arr[0].to_number(); m12 = arr[1].to_number();
1392                m21 = arr[2].to_number(); m22 = arr[3].to_number();
1393                m41 = arr[4].to_number(); m42 = arr[5].to_number();
1394            }
1395        } else {
1396            if let Ok(v) = it.get_property(&init, "a") { if !matches!(v, Value::Undefined) { m11 = v.to_number(); } }
1397            if let Ok(v) = it.get_property(&init, "b") { if !matches!(v, Value::Undefined) { m12 = v.to_number(); } }
1398            if let Ok(v) = it.get_property(&init, "c") { if !matches!(v, Value::Undefined) { m21 = v.to_number(); } }
1399            if let Ok(v) = it.get_property(&init, "d") { if !matches!(v, Value::Undefined) { m22 = v.to_number(); } }
1400            if let Ok(v) = it.get_property(&init, "e") { if !matches!(v, Value::Undefined) { m41 = v.to_number(); } }
1401            if let Ok(v) = it.get_property(&init, "f") { if !matches!(v, Value::Undefined) { m42 = v.to_number(); } }
1402
1403            if let Ok(v) = it.get_property(&init, "m11") { if !matches!(v, Value::Undefined) { m11 = v.to_number(); } }
1404            if let Ok(v) = it.get_property(&init, "m12") { if !matches!(v, Value::Undefined) { m12 = v.to_number(); } }
1405            if let Ok(v) = it.get_property(&init, "m13") { if !matches!(v, Value::Undefined) { m13 = v.to_number(); } }
1406            if let Ok(v) = it.get_property(&init, "m14") { if !matches!(v, Value::Undefined) { m14 = v.to_number(); } }
1407            if let Ok(v) = it.get_property(&init, "m21") { if !matches!(v, Value::Undefined) { m21 = v.to_number(); } }
1408            if let Ok(v) = it.get_property(&init, "m22") { if !matches!(v, Value::Undefined) { m22 = v.to_number(); } }
1409            if let Ok(v) = it.get_property(&init, "m23") { if !matches!(v, Value::Undefined) { m23 = v.to_number(); } }
1410            if let Ok(v) = it.get_property(&init, "m24") { if !matches!(v, Value::Undefined) { m24 = v.to_number(); } }
1411            if let Ok(v) = it.get_property(&init, "m31") { if !matches!(v, Value::Undefined) { m31 = v.to_number(); } }
1412            if let Ok(v) = it.get_property(&init, "m32") { if !matches!(v, Value::Undefined) { m32 = v.to_number(); } }
1413            if let Ok(v) = it.get_property(&init, "m33") { if !matches!(v, Value::Undefined) { m33 = v.to_number(); } }
1414            if let Ok(v) = it.get_property(&init, "m34") { if !matches!(v, Value::Undefined) { m34 = v.to_number(); } }
1415            if let Ok(v) = it.get_property(&init, "m41") { if !matches!(v, Value::Undefined) { m41 = v.to_number(); } }
1416            if let Ok(v) = it.get_property(&init, "m42") { if !matches!(v, Value::Undefined) { m42 = v.to_number(); } }
1417            if let Ok(v) = it.get_property(&init, "m43") { if !matches!(v, Value::Undefined) { m43 = v.to_number(); } }
1418            if let Ok(v) = it.get_property(&init, "m44") { if !matches!(v, Value::Undefined) { m44 = v.to_number(); } }
1419        }
1420    }
1421
1422    let is_2d = m13 == 0.0 && m14 == 0.0 && m23 == 0.0 && m24 == 0.0 && m31 == 0.0 && m32 == 0.0 && m33 == 1.0 && m34 == 0.0 && m43 == 0.0;
1423    let is_identity = m11 == 1.0 && m12 == 0.0 && m13 == 0.0 && m14 == 0.0
1424        && m21 == 0.0 && m22 == 1.0 && m23 == 0.0 && m24 == 0.0
1425        && m31 == 0.0 && m32 == 0.0 && m33 == 1.0 && m34 == 0.0
1426        && m41 == 0.0 && m42 == 0.0 && m43 == 0.0 && m44 == 1.0;
1427
1428    let o = Obj::plain();
1429    {
1430        let mut b = o.borrow_mut();
1431        b.props.insert("a".into(), Value::Number(m11));
1432        b.props.insert("b".into(), Value::Number(m12));
1433        b.props.insert("c".into(), Value::Number(m21));
1434        b.props.insert("d".into(), Value::Number(m22));
1435        b.props.insert("e".into(), Value::Number(m41));
1436        b.props.insert("f".into(), Value::Number(m42));
1437
1438        b.props.insert("m11".into(), Value::Number(m11));
1439        b.props.insert("m12".into(), Value::Number(m12));
1440        b.props.insert("m13".into(), Value::Number(m13));
1441        b.props.insert("m14".into(), Value::Number(m14));
1442        b.props.insert("m21".into(), Value::Number(m21));
1443        b.props.insert("m22".into(), Value::Number(m22));
1444        b.props.insert("m23".into(), Value::Number(m23));
1445        b.props.insert("m24".into(), Value::Number(m24));
1446        b.props.insert("m31".into(), Value::Number(m31));
1447        b.props.insert("m32".into(), Value::Number(m32));
1448        b.props.insert("m33".into(), Value::Number(m33));
1449        b.props.insert("m34".into(), Value::Number(m34));
1450        b.props.insert("m41".into(), Value::Number(m41));
1451        b.props.insert("m42".into(), Value::Number(m42));
1452        b.props.insert("m43".into(), Value::Number(m43));
1453        b.props.insert("m44".into(), Value::Number(m44));
1454
1455        b.props.insert("is2D".into(), Value::Bool(is_2d));
1456        b.props.insert("isIdentity".into(), Value::Bool(is_identity));
1457
1458        b.props.insert("toString".into(), nv("toString", dom_matrix_to_string));
1459        b.props.insert("multiply".into(), nv("multiply", dom_matrix_multiply));
1460        b.props.insert("translate".into(), nv("translate", dom_matrix_translate));
1461        b.props.insert("scale".into(), nv("scale", dom_matrix_scale));
1462        b.props.insert("rotate".into(), nv("rotate", dom_matrix_rotate));
1463        b.props.insert("skewX".into(), nv("skewX", dom_matrix_skew_x));
1464        b.props.insert("skewY".into(), nv("skewY", dom_matrix_skew_y));
1465        b.props.insert("invert".into(), nv("invert", dom_matrix_invert));
1466
1467        b.props.insert("multiplySelf".into(), nv("multiplySelf", dom_matrix_multiply_self));
1468        b.props.insert("translateSelf".into(), nv("translateSelf", dom_matrix_translate_self));
1469        b.props.insert("scaleSelf".into(), nv("scaleSelf", dom_matrix_scale_self));
1470        b.props.insert("rotateSelf".into(), nv("rotateSelf", dom_matrix_rotate_self));
1471        b.props.insert("skewXSelf".into(), nv("skewXSelf", dom_matrix_skew_x_self));
1472        b.props.insert("skewYSelf".into(), nv("skewYSelf", dom_matrix_skew_y_self));
1473        b.props.insert("invertSelf".into(), nv("invertSelf", dom_matrix_invert_self));
1474
1475        b.props.insert("transformPoint".into(), nv("transformPoint", dom_matrix_transform_point));
1476        b.props.insert(
1477            "toJSON".into(),
1478            nv("toJSON", |_: &mut Interp, this: Value, _: &[Value]| {
1479                if let Value::Object(o) = &this {
1480                    let clone = Obj::plain();
1481                    for (k, v) in o.borrow().props.iter() {
1482                        if k != "toJSON" && k != "toString" && k != "multiply" && k != "translate" && k != "scale" && k != "rotate" && k != "invert"
1483                            && k != "multiplySelf" && k != "translateSelf" && k != "scaleSelf" && k != "rotateSelf" && k != "invertSelf"
1484                            && k != "skewX" && k != "skewY" && k != "skewXSelf" && k != "skewYSelf" && k != "transformPoint" {
1485                            clone.borrow_mut().props.insert(k.clone(), v.clone());
1486                        }
1487                    }
1488                    return Ok(Value::Object(clone));
1489                }
1490                Ok(Value::Undefined)
1491            }),
1492        );
1493    }
1494    Ok(Value::Object(o))
1495}
1496
1497pub fn dom_quad_get_bounds(it: &mut Interp, this: Value, _: &[Value]) -> Result<Value, Value> {
1498    let p1 = it.get_property(&this, "p1").unwrap_or(Value::Undefined);
1499    let p2 = it.get_property(&this, "p2").unwrap_or(Value::Undefined);
1500    let p3 = it.get_property(&this, "p3").unwrap_or(Value::Undefined);
1501    let p4 = it.get_property(&this, "p4").unwrap_or(Value::Undefined);
1502
1503    let x1 = get_num_prop(it, &p1, "x", 0.0);
1504    let y1 = get_num_prop(it, &p1, "y", 0.0);
1505    let x2 = get_num_prop(it, &p2, "x", 0.0);
1506    let y2 = get_num_prop(it, &p2, "y", 0.0);
1507    let x3 = get_num_prop(it, &p3, "x", 0.0);
1508    let y3 = get_num_prop(it, &p3, "y", 0.0);
1509    let x4 = get_num_prop(it, &p4, "x", 0.0);
1510    let y4 = get_num_prop(it, &p4, "y", 0.0);
1511
1512    let min_x = x1.min(x2).min(x3).min(x4);
1513    let max_x = x1.max(x2).max(x3).max(x4);
1514    let min_y = y1.min(y2).min(y3).min(y4);
1515    let max_y = y1.max(y2).max(y3).max(y4);
1516
1517    let w = (max_x - min_x).max(0.0) as i32;
1518    let h = (max_y - min_y).max(0.0) as i32;
1519    Ok(Value::Object(make_dom_rect(min_x as i32, min_y as i32, w, h)))
1520}
1521
1522
1523
1524/// `new DOMQuad(p1, p2, p3, p4)` (Geometry Interfaces Module Level 1)
1525pub(crate) fn dom_quad_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1526    let p1 = if matches!(arg(a, 0), Value::Undefined) { dom_point_ctor(it, Value::Undefined, &[])? } else { arg(a, 0) };
1527    let p2 = if matches!(arg(a, 1), Value::Undefined) { dom_point_ctor(it, Value::Undefined, &[])? } else { arg(a, 1) };
1528    let p3 = if matches!(arg(a, 2), Value::Undefined) { dom_point_ctor(it, Value::Undefined, &[])? } else { arg(a, 2) };
1529    let p4 = if matches!(arg(a, 3), Value::Undefined) { dom_point_ctor(it, Value::Undefined, &[])? } else { arg(a, 3) };
1530
1531    let o = Obj::plain();
1532    {
1533        let mut b = o.borrow_mut();
1534        b.props.insert("p1".into(), p1.clone());
1535        b.props.insert("p2".into(), p2.clone());
1536        b.props.insert("p3".into(), p3.clone());
1537        b.props.insert("p4".into(), p4.clone());
1538
1539        b.props.insert("getBounds".into(), nv("getBounds", dom_quad_get_bounds));
1540        b.props.insert(
1541            "toJSON".into(),
1542            nv("toJSON", |_: &mut Interp, this: Value, _: &[Value]| {
1543                if let Value::Object(o) = &this {
1544                    let clone = Obj::plain();
1545                    for (k, v) in o.borrow().props.iter() {
1546                        if k != "toJSON" && k != "getBounds" {
1547                            clone.borrow_mut().props.insert(k.clone(), v.clone());
1548                        }
1549                    }
1550                    return Ok(Value::Object(clone));
1551                }
1552                Ok(Value::Undefined)
1553            }),
1554        );
1555    }
1556    Ok(Value::Object(o))
1557}
1558pub(crate) fn make_dom_rect(x: i32, y: i32, w: i32, h: i32) -> ObjRef {
1559    let o = Obj::plain();
1560    {
1561        let mut b = o.borrow_mut();
1562        b.props.insert("x".into(), Value::Number(x as f64));
1563        b.props.insert("left".into(), Value::Number(x as f64));
1564        b.props.insert("y".into(), Value::Number(y as f64));
1565        b.props.insert("top".into(), Value::Number(y as f64));
1566        b.props.insert("width".into(), Value::Number(w as f64));
1567        b.props.insert("height".into(), Value::Number(h as f64));
1568        b.props
1569            .insert("right".into(), Value::Number((x + w) as f64));
1570        b.props
1571            .insert("bottom".into(), Value::Number((y + h) as f64));
1572    }
1573    o
1574}
1575
1576pub fn dom_get_bounding_client_rect(
1577    it: &mut Interp,
1578    this: Value,
1579    _a: &[Value],
1580) -> Result<Value, Value> {
1581    let (x, y, w, h) = this_dom_idx(&this)
1582        .and_then(|idx| it.dom.borrow().get_rect(idx))
1583        .unwrap_or((0, 0, 0, 0));
1584    Ok(Value::Object(make_dom_rect(x, y, w, h)))
1585}
1586
1587/// `element.getClientRects()`(DOM 標準。丸ごと未対応だった。本来は行ボックス
1588/// 単位で複数の `DOMRect` を返すが、この処理系にはテキストの行フラグメント
1589/// モデル自体が無い(`text-overflow`/インライン折返し等と同じ既存の簡略化と
1590/// 同方針)ため、要素1つにつき `getBoundingClientRect()` と同じ矩形を0個
1591/// (未レンダリング)か1個だけ持つ配列として返す)。
1592pub fn dom_get_client_rects(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1593    let rect = this_dom_idx(&this).and_then(|idx| it.dom.borrow().get_rect(idx));
1594    let items = match rect {
1595        Some((x, y, w, h)) => alloc::vec![Value::Object(make_dom_rect(x, y, w, h))],
1596        None => Vec::new(),
1597    };
1598    Ok(Value::Object(Obj::array(items)))
1599}
1600
1601/// getComputedStyle(el): 算出スタイルを読む host プロキシ `computed:idx` を返す。
1602pub(crate) fn get_computed_style(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1603    match this_dom_idx(&arg(a, 0)) {
1604        Some(idx) => Ok(Value::Object(Obj::host(&format!("computed:{}", idx)))),
1605        None => Ok(Value::Object(Obj::host("computed:18446744073709551615"))), // 無効idx
1606    }
1607}
1608
1609/// computedStyle.getPropertyValue(name): kebab-case のプロパティ値を返す。
1610pub fn computed_get_property_value(
1611    it: &mut Interp,
1612    this: Value,
1613    a: &[Value],
1614) -> Result<Value, Value> {
1615    if let Some(idx) = this_host_idx(&this, "computed:") {
1616        let prop = arg(a, 0).to_js_string();
1617        return Ok(Value::str(it.dom.borrow().get_computed(idx, &prop)));
1618    }
1619    Ok(Value::str(""))
1620}
1621
1622pub fn dom_matches(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1623    if let Some(idx) = this_dom_idx(&this) {
1624        let sel = arg(a, 0).to_js_string();
1625        // `:scope`(丸ごと未対応だった)。`el.matches()` における `:scope` は
1626        // 呼び出し元の要素自身を指す。
1627        return Ok(Value::Bool(it.dom.borrow().node_matches_scoped(idx, &sel, Some(idx))));
1628    }
1629    Ok(Value::Bool(false))
1630}
1631
1632/// closest(selector): 自身を含め祖先方向へ最初にマッチする要素。無ければ null。
1633pub fn dom_closest(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1634    if let Some(start) = this_dom_idx(&this) {
1635        let sel = arg(a, 0).to_js_string();
1636        let dom = it.dom.borrow();
1637        let mut cur = Some(start);
1638        let mut guard = 0;
1639        while let Some(c) = cur {
1640            // `:scope`(丸ごと未対応だった)。`el.closest()` における `:scope` は
1641            // 探索中の祖先ではなく、呼び出し元の起点要素(`start`)を指す。
1642            if dom.node_matches_scoped(c, &sel, Some(start)) {
1643                return Ok(dom_handle(Some(c)));
1644            }
1645            cur = dom.nodes.get(c).and_then(|n| n.parent);
1646            guard += 1;
1647            if guard > dom.nodes.len() {
1648                break;
1649            }
1650        }
1651    }
1652    Ok(Value::Null)
1653}
1654
1655/// element.querySelector: 子孫に限定して最初にマッチする要素。`:scope`
1656/// (Selectors Level 4。丸ごと未対応だった)は呼び出し元の要素自身を指す。
1657pub fn dom_element_query_selector(
1658    it: &mut Interp,
1659    this: Value,
1660    a: &[Value],
1661) -> Result<Value, Value> {
1662    if let Some(idx) = this_dom_idx(&this) {
1663        let sel = arg(a, 0).to_js_string();
1664        let dom = it.dom.borrow();
1665        for c in dom.query_all_scoped(&sel, Some(idx)) {
1666            if dom.is_ancestor_of(idx, c) {
1667                return Ok(dom_handle(Some(c)));
1668            }
1669        }
1670    }
1671    Ok(Value::Null)
1672}
1673
1674/// element.querySelectorAll: 子孫に限定してマッチする要素配列。`:scope`
1675/// (Selectors Level 4。丸ごと未対応だった)は呼び出し元の要素自身を指す。
1676pub fn dom_element_query_selector_all(
1677    it: &mut Interp,
1678    this: Value,
1679    a: &[Value],
1680) -> Result<Value, Value> {
1681    let mut items: Vec<Value> = Vec::new();
1682    if let Some(idx) = this_dom_idx(&this) {
1683        let sel = arg(a, 0).to_js_string();
1684        let dom = it.dom.borrow();
1685        for c in dom.query_all_scoped(&sel, Some(idx)) {
1686            if dom.is_ancestor_of(idx, c) {
1687                items.push(Value::Object(Obj::dom(c)));
1688            }
1689        }
1690    }
1691    Ok(Value::Object(Obj::array(items)))
1692}
1693
1694/// `element.getElementsByClassName(name)`(`document.getElementsByClassName` は
1695/// 対応済みだったが、要素インスタンス版が丸ごと未対応だった。`querySelectorAll` と
1696/// 同じ「自身の子孫のみに絞り込む」ロジックを再利用する)。
1697pub fn dom_element_get_elements_by_class_name(
1698    it: &mut Interp,
1699    this: Value,
1700    a: &[Value],
1701) -> Result<Value, Value> {
1702    let mut items: Vec<Value> = Vec::new();
1703    if let Some(idx) = this_dom_idx(&this) {
1704        let raw = arg(a, 0).to_js_string();
1705        let sel_classes: Vec<String> = raw
1706            .split_whitespace()
1707            .map(|c| alloc::format!(".{}", c))
1708            .collect();
1709        let sel = if sel_classes.is_empty() {
1710            alloc::format!(".__invalid_class__")
1711        } else {
1712            sel_classes.concat()
1713        };
1714        let dom = it.dom.borrow();
1715        for c in dom.query_all(&sel) {
1716            if dom.is_ancestor_of(idx, c) {
1717                items.push(Value::Object(Obj::dom(c)));
1718            }
1719        }
1720    }
1721    Ok(Value::Object(Obj::array(items)))
1722}
1723/// `element.getElementsByTagName(tag)`(上記と同じ理由で丸ごと未対応だった)。
1724pub fn dom_element_get_elements_by_tag_name(
1725    it: &mut Interp,
1726    this: Value,
1727    a: &[Value],
1728) -> Result<Value, Value> {
1729    let mut items: Vec<Value> = Vec::new();
1730    if let Some(idx) = this_dom_idx(&this) {
1731        let tag = arg(a, 0).to_js_string();
1732        let dom = it.dom.borrow();
1733        // 【2026-09-05 バグ修正】以前は文書全体を `query_all` で引いてから
1734        // 先祖関係で絞っていた。`query_all` は**文書に繋がっていないノードを
1735        // 除外する**ので、切り離した要素の子孫が一切返らなかった。
1736        // 仕様では接続の有無に関係なく自分の子孫を返す。詳細は
1737        // `dom_bridge::descendants_by_tag_name` の説明。
1738        for c in dom.descendants_by_tag_name(idx, &tag) {
1739            items.push(Value::Object(Obj::dom(c)));
1740        }
1741    }
1742    Ok(Value::Object(Obj::array(items)))
1743}
1744/// `element.getElementsByTagNameNS(namespace, localName)`(丸ごと未対応だった)。
1745pub fn dom_element_get_elements_by_tag_name_ns(
1746    it: &mut Interp,
1747    this: Value,
1748    a: &[Value],
1749) -> Result<Value, Value> {
1750    let mut items: Vec<Value> = Vec::new();
1751    if let Some(idx) = this_dom_idx(&this) {
1752        let tag = if a.len() >= 2 {
1753            arg(a, 1).to_js_string()
1754        } else {
1755            arg(a, 0).to_js_string()
1756        };
1757        let dom = it.dom.borrow();
1758        for c in dom.query_all(&tag) {
1759            if dom.is_ancestor_of(idx, c) {
1760                items.push(Value::Object(Obj::dom(c)));
1761            }
1762        }
1763    }
1764    Ok(Value::Object(Obj::array(items)))
1765}
1766
1767pub fn dom_set_attribute(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1768    if let Some(idx) = this_dom_idx(&this) {
1769        let name = arg(a, 0).to_js_string();
1770        let val = arg(a, 1).to_js_string();
1771        it.dom.borrow_mut().set_attr(idx, &name, &val);
1772        if name == "src" && val.starts_with("blob:") {
1773            let is_media = it
1774                .dom
1775                .borrow()
1776                .nodes
1777                .get(idx)
1778                .map(|n| n.tag == "video" || n.tag == "audio")
1779                .unwrap_or(false);
1780            if is_media {
1781                let ms_val = match it.global.borrow().vars.get("__media_source_urls") {
1782                    Some(Value::Object(r)) => r.borrow().props.get(&val).cloned(),
1783                    _ => None,
1784                };
1785                if let Some(ms) = ms_val {
1786                    super::media_source_open_internal(it, &ms);
1787                }
1788            }
1789        }
1790    }
1791    Ok(Value::Undefined)
1792}
1793
1794/// element.checkValidity() / reportValidity(): 現在値で制約バリデーションを実行し、
1795/// 妥当なら true、違反があれば false を返す。
1796/// (invalid イベントはフォーム送信経路 submit_form 側で WebEngine が発火する。
1797///  Interp からは DOM イベント発火経路が無いため、ここでは bool 判定のみ。)
1798/// **重要**: `<form>` 自身に対する `checkValidity()` が、フォームの子孫コントロール
1799/// を一切見ず `validate_field(form_idx, ...)` を直接呼んでいたため、`<form>` タグは
1800/// `type` が email/url/number のいずれにも一致せず常に無条件で `true`(妥当)を
1801/// 返してしまうバグだった(`WebEngine::submit_form` 側は `descendants_by_tags` で
1802/// 子孫を正しく走査していたのに、JS から呼ぶ `checkValidity()` だけが非対称だった)。
1803pub fn dom_check_validity(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1804    if let Some(idx) = this_dom_idx(&this) {
1805        let tag = it.dom.borrow().nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
1806        if tag == "form" {
1807            let dom = it.dom.borrow();
1808            // `form_associated_controls`は子孫に加え、`form="このformのid"`属性で
1809            // フォーム外から明示的に関連付けられたコントロール(HTML5仕様の
1810            // 「form owner」)も含む。以前は`descendants_by_tags`(子孫のみ)
1811            // だったため、`<form>`の外に置かれた`<input form="...">`が
1812            // `checkValidity()`の対象から漏れる兄弟ギャップだった。
1813            let fields = dom.form_associated_controls(idx, &["input", "textarea", "select"]);
1814            let all_valid = fields.iter().all(|&node| {
1815                let value = dom.effective_form_value(node);
1816                dom.validate_field(node, &value).is_none()
1817            });
1818            return Ok(Value::Bool(all_valid));
1819        }
1820        let value = it.dom.borrow().effective_form_value(idx);
1821        let valid = it.dom.borrow().validate_field(idx, &value).is_none();
1822        return Ok(Value::Bool(valid));
1823    }
1824    Ok(Value::Bool(true))
1825}
1826
1827/// element.setCustomValidity(msg): カスタム検証メッセージを設定。空文字でクリア。
1828/// 内部的には隠し属性 `_custom_validity` に保存し、validate_field が最優先で参照する。
1829pub fn dom_set_custom_validity(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1830    if let Some(idx) = this_dom_idx(&this) {
1831        let msg = arg(a, 0).to_js_string();
1832        if msg.is_empty() {
1833            it.dom.borrow_mut().remove_attr(idx, "_custom_validity");
1834        } else {
1835            it.dom.borrow_mut().set_attr(idx, "_custom_validity", &msg);
1836        }
1837    }
1838    Ok(Value::Undefined)
1839}
1840
1841/// `classList.forEach(fn)`(丸ごと未対応だった。`DOMTokenList` は仕様上イテラブル/
1842/// `forEach` を持つ配列風のオブジェクトだが、この処理系の `classList` は専用の
1843/// `Host("classList:N")` プロキシで表現されており、`Symbol.iterator`/`forEach` が
1844/// 一切配線されていなかった。`for (const c of el.classList)` も同じ理由で常に
1845/// 何も反復しない黙殺バグだった — `iterate_values()` 側の修正と対)。
1846pub fn dom_classlist_for_each(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1847    if let Some(idx) = this_host_idx(&this, "classList:") {
1848        let classes = it.dom.borrow().nodes.get(idx).map(|n| n.classes.clone()).unwrap_or_default();
1849        let cb = arg(a, 0);
1850        for (i, c) in classes.into_iter().enumerate() {
1851            it.call_value(&cb, Value::Undefined, &[Value::str(c), Value::Number(i as f64)])?;
1852        }
1853    }
1854    Ok(Value::Undefined)
1855}
1856pub fn dom_classlist_add(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1857    if let Some(idx) = this_host_idx(&this, "classList:") {
1858        for v in a {
1859            it.dom
1860                .borrow_mut()
1861                .class_add(idx, &v.to_js_string());
1862        }
1863    }
1864    Ok(Value::Undefined)
1865}
1866
1867pub fn dom_classlist_remove(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1868    if let Some(idx) = this_host_idx(&this, "classList:") {
1869        for v in a {
1870            it.dom
1871                .borrow_mut()
1872                .class_remove(idx, &v.to_js_string());
1873        }
1874    }
1875    Ok(Value::Undefined)
1876}
1877
1878/// `classList.supports(token)`(DOM Standard §4.3)。
1879pub fn dom_classlist_supports(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
1880    Ok(Value::Bool(false))
1881}
1882
1883
1884/// `classList.toggle(name, force)`。以前は第2引数 `force`(真偽値を渡すと現在の状態に
1885/// 関係なく強制的に付与/除去する)を完全に無視し、常に単純なトグルだけを行っていた
1886/// (`el.classList.toggle('active', isActive)` という定番パターンが動かなかった)。
1887pub fn dom_classlist_toggle(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1888    if let Some(idx) = this_host_idx(&this, "classList:") {
1889        let cls = arg(a, 0).to_js_string();
1890        if matches!(arg(a, 1), Value::Undefined) {
1891            let on = it.dom.borrow_mut().class_toggle(idx, &cls);
1892            return Ok(Value::Bool(on));
1893        }
1894        let force = arg(a, 1).truthy();
1895        if force {
1896            it.dom.borrow_mut().class_add(idx, &cls);
1897        } else {
1898            it.dom.borrow_mut().class_remove(idx, &cls);
1899        }
1900        return Ok(Value::Bool(force));
1901    }
1902    Ok(Value::Bool(false))
1903}
1904
1905pub fn dom_classlist_contains(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1906    if let Some(idx) = this_host_idx(&this, "classList:") {
1907        return Ok(Value::Bool(
1908            it.dom
1909                .borrow()
1910                .class_contains(idx, &arg(a, 0).to_js_string()),
1911        ));
1912    }
1913    Ok(Value::Bool(false))
1914}
1915
1916/// `classList.entries()`/`.keys()`/`.values()`(丸ごと未対応だった。`DOMTokenList`
1917/// は仕様上 `forEach`/`Symbol.iterator` に加えこの3メソッドも持つが、`forEach`と
1918/// for-of 反復(`iterable_values`)のみ配線され、これらの明示イテレータ取得
1919/// メソッドは未配線だった。`Array.prototype` の同名メソッドと同じく既存の
1920/// `make_iterator` ヘルパーへ委譲する薄い実装。
1921pub fn dom_classlist_entries(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1922    if let Some(idx) = this_host_idx(&this, "classList:") {
1923        let classes = it.dom.borrow().nodes.get(idx).map(|n| n.classes.clone()).unwrap_or_default();
1924        let out: Vec<Value> = classes
1925            .into_iter()
1926            .enumerate()
1927            .map(|(i, c)| Value::Object(Obj::array(alloc::vec![Value::Number(i as f64), Value::str(c)])))
1928            .collect();
1929        return Ok(Value::Object(make_iterator(out)));
1930    }
1931    Ok(Value::Object(make_iterator(alloc::vec![])))
1932}
1933pub fn dom_classlist_keys(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1934    if let Some(idx) = this_host_idx(&this, "classList:") {
1935        let n = it.dom.borrow().nodes.get(idx).map(|n| n.classes.len()).unwrap_or(0);
1936        return Ok(Value::Object(make_iterator(
1937            (0..n).map(|i| Value::Number(i as f64)).collect(),
1938        )));
1939    }
1940    Ok(Value::Object(make_iterator(alloc::vec![])))
1941}
1942pub fn dom_classlist_values(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1943    if let Some(idx) = this_host_idx(&this, "classList:") {
1944        let classes = it.dom.borrow().nodes.get(idx).map(|n| n.classes.clone()).unwrap_or_default();
1945        return Ok(Value::Object(make_iterator(
1946            classes.into_iter().map(Value::str).collect(),
1947        )));
1948    }
1949    Ok(Value::Object(make_iterator(alloc::vec![])))
1950}
1951
1952pub(crate) fn document_create_element(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1953    let tag = arg(a, 0).to_js_string();
1954    let idx = it.dom.borrow_mut().create_element(&tag);
1955    // 定義済みのカスタム要素ならその場で構築する(仕様の「同期カスタム要素フラグ」)。
1956    if tag.contains('-') {
1957        it.ce_try_upgrade(idx, false);
1958    }
1959    Ok(Value::Object(Obj::dom(idx)))
1960}
1961/// `document.createElementNS(namespaceURI, qualifiedName)`(丸ごと未対応
1962/// だった。SVG 要素(`document.createElementNS('http://www.w3.org/2000/svg',
1963/// 'circle')` 等)を JS から動的生成する標準的な定番パターン)。この処理系は
1964/// `*AttributeNS` 系と同じく XML 名前空間を一切モデル化していないため、
1965/// 第1引数(namespace)は無視して非 NS 版の `createElement` へ委譲する
1966/// 簡略実装(多くの実用コードは SVG 名前空間しか渡さないため実害は小さい)。
1967pub(crate) fn document_create_element_ns(it: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
1968    document_create_element(it, t, &[arg(a, 1)])
1969}
1970
1971/// `new Option(text?, value?, defaultSelected?, selected?)`(HTML5。丸ごと
1972/// 未対応だった)。`document.createElement('option')` を土台に組み立てる。
1973pub(crate) fn option_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1974    let idx = it.dom.borrow_mut().create_element("option");
1975    let text = arg(a, 0);
1976    if !matches!(text, Value::Undefined) {
1977        it.dom.borrow_mut().set_text_content(idx, &text.to_js_string());
1978    }
1979    let value = arg(a, 1);
1980    if !matches!(value, Value::Undefined) {
1981        it.dom.borrow_mut().set_attr(idx, "value", &value.to_js_string());
1982    }
1983    if arg(a, 2).truthy() || arg(a, 3).truthy() {
1984        it.dom.borrow_mut().set_attr(idx, "selected", "selected");
1985    }
1986    Ok(Value::Object(Obj::dom(idx)))
1987}
1988
1989pub(crate) fn document_create_text_node(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1990    let text = arg(a, 0).to_js_string();
1991    let idx = it.dom.borrow_mut().create_text_node(&text);
1992    Ok(Value::Object(Obj::dom(idx)))
1993}
1994
1995pub(crate) fn document_create_comment(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1996    let text = arg(a, 0).to_js_string();
1997    let idx = it.dom.borrow_mut().create_comment(&text);
1998    Ok(Value::Object(Obj::dom(idx)))
1999}
2000
2001pub(crate) fn document_create_document_fragment(it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
2002    let idx = it.dom.borrow_mut().create_document_fragment();
2003    Ok(Value::Object(Obj::dom(idx)))
2004}
2005
2006pub(crate) fn split_css_rules(css: &str) -> alloc::vec::Vec<String> {
2007    let mut rules = alloc::vec![];
2008    let mut current_rule = String::new();
2009    let mut depth = 0;
2010    for c in css.chars() {
2011        current_rule.push(c);
2012        if c == '{' {
2013            depth += 1;
2014        } else if c == '}' {
2015            if depth > 0 {
2016                depth -= 1;
2017            }
2018            if depth == 0 {
2019                let trimmed = current_rule.trim();
2020                if !trimmed.is_empty() {
2021                    rules.push(trimmed.to_string());
2022                }
2023                current_rule.clear();
2024            }
2025        }
2026    }
2027    let trimmed = current_rule.trim();
2028    if !trimmed.is_empty() {
2029        rules.push(trimmed.to_string());
2030    }
2031    rules
2032}
2033
2034pub(crate) fn dom_stylesheet_insert_rule(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2035    let rule_text = arg(a, 0).to_js_string();
2036    let insert_index = arg(a, 1).to_number() as usize;
2037    if let Value::Object(o) = this {
2038        if let ObjKind::Host(t) = &o.borrow().kind {
2039            if let Some(rest) = t.strip_prefix("stylesheet:") {
2040                let idx: usize = rest.parse().unwrap_or(usize::MAX);
2041                let text = it.dom.borrow().get_text_content(idx);
2042                let mut rules = split_css_rules(&text);
2043                let actual_index = insert_index.min(rules.len());
2044                rules.insert(actual_index, rule_text);
2045                let new_css = rules.join("\n");
2046                it.dom.borrow_mut().set_text_content(idx, &new_css);
2047                return Ok(Value::Number(actual_index as f64));
2048            }
2049        }
2050    }
2051    Ok(Value::Number(0.0))
2052}
2053
2054pub(crate) fn dom_stylesheet_delete_rule(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2055    let delete_index = arg(a, 0).to_number() as usize;
2056    if let Value::Object(o) = this {
2057        if let ObjKind::Host(t) = &o.borrow().kind {
2058            if let Some(rest) = t.strip_prefix("stylesheet:") {
2059                let idx: usize = rest.parse().unwrap_or(usize::MAX);
2060                let text = it.dom.borrow().get_text_content(idx);
2061                let mut rules = split_css_rules(&text);
2062                if delete_index < rules.len() {
2063                    rules.remove(delete_index);
2064                    let new_css = rules.join("\n");
2065                    it.dom.borrow_mut().set_text_content(idx, &new_css);
2066                }
2067            }
2068        }
2069    }
2070    Ok(Value::Undefined)
2071}
2072
2073pub fn dom_append_child(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2074    let child = arg(a, 0);
2075    if let (Some(p), Some(c)) = (this_dom_idx(&this), this_dom_idx(&child)) {
2076        it.dom.borrow_mut().append_child(p, c);
2077    }
2078    Ok(child) // appendChild は追加した子を返す。
2079}
2080
2081/// parent.insertBefore(newChild, refChild)。ref が null/undefined なら末尾。
2082pub fn dom_insert_before(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2083    let new_child = arg(a, 0);
2084    if let (Some(p), Some(nc)) = (this_dom_idx(&this), this_dom_idx(&new_child)) {
2085        let rc = this_dom_idx(&arg(a, 1)); // null/undefined → None
2086        if let Some(r) = rc {
2087            let is_child =
2088                it.dom.borrow().nodes.get(r).and_then(|n| n.parent) == Some(p);
2089            if !is_child {
2090                return Err(make_dom_exception(
2091                    "NotFoundError",
2092                    "The node before which the new node is to be inserted is not a child of this node",
2093                ));
2094            }
2095        }
2096        it.dom.borrow_mut().insert_before(p, nc, rc);
2097    }
2098    Ok(new_child)
2099}
2100
2101/// `parent.moveBefore(movedNode, referenceNode)`(DOM。2025年にChromeへ
2102/// 実装が入った比較的新しいAPI。丸ごと未対応だった)。仕様上の主眼は
2103/// 「`remove()`してから`insertBefore()`する」場合と異なり、`<iframe>`の
2104/// 読み込み状態・`<video>`の再生状態・カスタム要素のライフサイクル
2105/// コールバック発火等を保持したまま木構造内を移動できる点にあるが、
2106/// この処理系にはそれらの「保持すべき生きた状態」自体が実装されて
2107/// いない(`<iframe>`は実際のナビゲーション状態を持たず、`<video>`は
2108/// 実再生をしない等)ため、実務上は`insertBefore`と同じ「detachしてから
2109/// 指定位置へ挿入」の`insert_before`(`dom_bridge.rs`。既に自動で元の
2110/// 親から外す実装)をそのまま再利用する誠実な簡略実装で十分に仕様どおり
2111/// 動作する。`referenceNode`(`null`可)が`this`の子でない場合は
2112/// `insertBefore`と同じ`NotFoundError`規則を踏襲。2026-07-18 発見・実装。
2113pub fn dom_move_before(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2114    let moved_node = arg(a, 0);
2115    if let (Some(p), Some(mn)) = (this_dom_idx(&this), this_dom_idx(&moved_node)) {
2116        let rc = this_dom_idx(&arg(a, 1));
2117        if let Some(r) = rc {
2118            let is_child = it.dom.borrow().nodes.get(r).and_then(|n| n.parent) == Some(p);
2119            if !is_child {
2120                return Err(make_dom_exception(
2121                    "NotFoundError",
2122                    "The node before which the moved node is to be inserted is not a child of this node",
2123                ));
2124            }
2125        }
2126        it.dom.borrow_mut().insert_before(p, mn, rc);
2127    }
2128    Ok(moved_node)
2129}
2130
2131/// `select.options.add(option, before?)`(HTML5 `HTMLOptionsCollection`。
2132/// 動的にドロップダウンを構築する `select.options.add(new Option(...))` という
2133/// 定番イディオムが、`options` が素の配列(メソッドを一切持たない)を返す
2134/// だけだったため丸ごと未対応(`TypeError: not a function`)だった。`options`
2135/// 取得時に配列オブジェクト自身へ `_select_idx`(隠しプロパティ。既存の
2136/// `_is_headers`/`_ta_kind` 等と同じ内部タグ付けパターン)を仕込んでおき、
2137/// この配列を経由して元の `<select>` を辿れるようにする。`before` は仕様どおり
2138/// 省略時は末尾追加、数値ならその位置の既存 option の前、`<option>` 要素
2139/// そのものでもよい。
2140pub fn select_options_add(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2141    let select_idx = match &this {
2142        Value::Object(o) => o.borrow().props.get("_select_idx").map(|v| v.to_number() as usize),
2143        _ => None,
2144    };
2145    let (Some(select_idx), Some(opt_idx)) = (select_idx, this_dom_idx(&arg(a, 0))) else {
2146        return Ok(Value::Undefined);
2147    };
2148    let before = arg(a, 1);
2149    let ref_idx = match &before {
2150        Value::Number(n) => it.dom.borrow().select_options(select_idx).get(*n as usize).copied(),
2151        Value::Object(_) => this_dom_idx(&before),
2152        _ => None,
2153    };
2154    it.dom.borrow_mut().insert_before(select_idx, opt_idx, ref_idx);
2155    Ok(Value::Undefined)
2156}
2157/// `select.add(element, before)`(`HTMLSelectElement`直下の便利メソッド。
2158/// `select.options.add`と全く同じ意味だが、`this`が`options`コレクション
2159/// 配列ではなく`<select>`要素自身になる点だけが違う。丸ごと未対応
2160/// だった。`select_options_add`とほぼ同じロジックだが、`_select_idx`
2161/// 隠しプロパティ経由ではなく`this_dom_idx`で直接`<select>`のインデックス
2162/// を取る。2026-07-18 発見・実装)。
2163pub fn dom_select_add(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2164    let (Some(select_idx), Some(opt_idx)) = (this_dom_idx(&this), this_dom_idx(&arg(a, 0))) else {
2165        return Ok(Value::Undefined);
2166    };
2167    let before = arg(a, 1);
2168    let ref_idx = match &before {
2169        Value::Number(n) => it.dom.borrow().select_options(select_idx).get(*n as usize).copied(),
2170        Value::Object(_) => this_dom_idx(&before),
2171        _ => None,
2172    };
2173    it.dom.borrow_mut().insert_before(select_idx, opt_idx, ref_idx);
2174    Ok(Value::Undefined)
2175}
2176/// `select.options.remove(index)`(`HTMLOptionsCollection`。上記 `add` と対に
2177/// なる、指定インデックスの `<option>` を取り除く定番メソッド)。
2178pub fn select_options_remove(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2179    let select_idx = match &this {
2180        Value::Object(o) => o.borrow().props.get("_select_idx").map(|v| v.to_number() as usize),
2181        _ => None,
2182    };
2183    let Some(select_idx) = select_idx else {
2184        return Ok(Value::Undefined);
2185    };
2186    let n = arg(a, 0).to_number() as usize;
2187    // `if let Some(x) = obj.borrow()...` は scrutinee の一時 `Ref` が if-let ブロック
2188    // 終端まで生き続けるため、ブロック内で `borrow_mut()` すると「RefCell already
2189    // borrowed」で panic する(このファイルの `report_error` 修正と同型のバグ。
2190    // 一度 owned な値へ materialize してから `if let` することで回避する)。
2191    let opt_idx = it.dom.borrow().select_options(select_idx).get(n).copied();
2192    if let Some(opt_idx) = opt_idx {
2193        it.dom.borrow_mut().remove_child(select_idx, opt_idx);
2194    }
2195    Ok(Value::Undefined)
2196}
2197/// `select.options.namedItem(name)`(`HTMLOptionsCollection`。`id`/`name`
2198/// 属性が一致する `<option>` を探す定番メソッド。見つからなければ仕様どおり
2199/// `null`)。
2200pub fn select_options_named_item(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2201    let select_idx = match &this {
2202        Value::Object(o) => o.borrow().props.get("_select_idx").map(|v| v.to_number() as usize),
2203        _ => None,
2204    };
2205    let Some(select_idx) = select_idx else {
2206        return Ok(Value::Null);
2207    };
2208    let name = arg(a, 0).to_js_string();
2209    let dom = it.dom.borrow();
2210    for opt_idx in dom.select_options(select_idx) {
2211        if dom.get_attr(opt_idx, "id").as_deref() == Some(name.as_str())
2212            || dom.get_attr(opt_idx, "name").as_deref() == Some(name.as_str())
2213        {
2214            return Ok(Value::Object(Obj::dom(opt_idx)));
2215        }
2216    }
2217    Ok(Value::Null)
2218}
2219
2220/// parent.replaceChild(newChild, oldChild)。置換された old を返す。
2221pub fn dom_replace_child(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2222    let old_child = arg(a, 1);
2223    if let (Some(p), Some(nc), Some(oc)) = (
2224        this_dom_idx(&this),
2225        this_dom_idx(&arg(a, 0)),
2226        this_dom_idx(&old_child),
2227    ) {
2228        // `insertBefore`と同じ理由(詳細はそちらの同種コメント参照)で、
2229        // `oldChild`が`this`の子でない場合は仕様上`NotFoundError`を投げる
2230        // べきだが、以前は`replace_child`(`dom_bridge.rs`)が黙って何もせず
2231        // 成功したかのように振る舞っていた。
2232        let is_child = it.dom.borrow().nodes.get(oc).and_then(|n| n.parent) == Some(p);
2233        if !is_child {
2234            return Err(make_dom_exception(
2235                "NotFoundError",
2236                "The node to be replaced is not a child of this node",
2237            ));
2238        }
2239        it.dom.borrow_mut().replace_child(p, nc, oc);
2240    }
2241    Ok(old_child)
2242}
2243
2244/// 引数を DOM ノード idx に解決(要素はそのまま、文字列はテキストノード生成)。
2245pub(crate) fn arg_to_node(it: &mut Interp, v: &Value) -> Option<usize> {
2246    match this_dom_idx(v) {
2247        Some(i) => Some(i),
2248        None => match v {
2249            Value::Undefined | Value::Null => None,
2250            other => Some(it.dom.borrow_mut().create_text_node(&other.to_js_string())),
2251        },
2252    }
2253}
2254
2255/// element.insertAdjacentHTML(position, html)。
2256/// `element.getHTML(options?)`(DOM。`innerHTML`ゲッターの明示的メソッド版
2257/// として2024年頃に標準化。丸ごと未対応だった)。仕様上の`options`
2258/// (`{serializableShadowRoots, shadowRoots}`)はShadow DOM自体が
2259/// この処理系に存在しないため意味を持たず無視する。`innerHTML`と全く同じ
2260/// `get_inner_html`をそのまま呼ぶだけの簡略実装。2026-07-18 発見・実装。
2261pub fn dom_get_html(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2262    if let Some(idx) = this_dom_idx(&this) {
2263        return Ok(Value::str(it.dom.borrow().get_inner_html(idx)));
2264    }
2265    Ok(Value::str(""))
2266}
2267
2268/// `element.setHTMLUnsafe(html)`(DOM。`innerHTML`セッターのTrusted
2269/// Types検証を明示的にバイパスする版として標準化。丸ごと未対応だった)。
2270/// この処理系にはTrusted Typesの強制自体が実装されていないため、
2271/// `innerHTML`セッターと全く同じ`set_inner_html`をそのまま呼ぶだけの
2272/// 簡略実装で仕様上の可視的な違いは無い。2026-07-18 発見・実装。
2273pub fn dom_set_html_unsafe(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2274    if let Some(idx) = this_dom_idx(&this) {
2275        let html = arg(a, 0).to_js_string();
2276        it.dom.borrow_mut().set_inner_html(idx, &html);
2277    }
2278    Ok(Value::Undefined)
2279}
2280
2281pub fn dom_insert_adjacent_html(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2282    if let Some(idx) = this_dom_idx(&this) {
2283        // `position` は仕様上 ASCII 大文字小文字を無視して照合すべきだが
2284        // (`"beforeBegin"`/`"BEFOREEND"` 等も有効)、以前は完全一致のみで
2285        // 小文字以外を渡すと静かに no-op になっていた。
2286        let pos = arg(a, 0).to_js_string().to_lowercase();
2287        let html = arg(a, 1).to_js_string();
2288        it.dom.borrow_mut().insert_adjacent_html(idx, &pos, &html);
2289    }
2290    Ok(Value::Undefined)
2291}
2292/// element.insertAdjacentElement(position, element)。
2293pub fn dom_insert_adjacent_element(
2294    it: &mut Interp,
2295    this: Value,
2296    a: &[Value],
2297) -> Result<Value, Value> {
2298    let el = arg(a, 1);
2299    if let (Some(idx), Some(node)) = (this_dom_idx(&this), this_dom_idx(&el)) {
2300        let pos = arg(a, 0).to_js_string().to_lowercase();
2301        it.dom.borrow_mut().insert_adjacent_node(idx, &pos, node);
2302    }
2303    Ok(el)
2304}
2305/// element.insertAdjacentText(position, text)。
2306pub fn dom_insert_adjacent_text(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2307    if let Some(idx) = this_dom_idx(&this) {
2308        let pos = arg(a, 0).to_js_string().to_lowercase();
2309        let node = it
2310            .dom
2311            .borrow_mut()
2312            .create_text_node(&arg(a, 1).to_js_string());
2313        it.dom.borrow_mut().insert_adjacent_node(idx, &pos, node);
2314    }
2315    Ok(Value::Undefined)
2316}
2317/// element.toggleAttribute(name[, force]) → 操作後の存在を返す。
2318pub fn dom_toggle_attribute(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2319    if let Some(idx) = this_dom_idx(&this) {
2320        let name = arg(a, 0).to_js_string();
2321        let has = it.dom.borrow().has_attr(idx, &name);
2322        let new_state = if a.len() > 1 && !matches!(arg(a, 1), Value::Undefined) {
2323            arg(a, 1).truthy()
2324        } else {
2325            !has
2326        };
2327        if new_state {
2328            it.dom.borrow_mut().set_attr(idx, &name, "");
2329        } else {
2330            it.dom.borrow_mut().remove_attr(idx, &name);
2331        }
2332        return Ok(Value::Bool(new_state));
2333    }
2334    Ok(Value::Bool(false))
2335}
2336
2337/// `dialog.show()`(`<dialog>` が丸ごと未対応だった)。`open` 属性を付ける
2338/// だけの簡略実装。
2339pub fn dom_dialog_show(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2340    if let Some(idx) = this_dom_idx(&this) {
2341        it.dom.borrow_mut().set_attr(idx, "open", "open");
2342    }
2343    Ok(Value::Undefined)
2344}
2345
2346/// `dialog.showModal()`。以前は`.show()`と全く同じ`dom_dialog_show`を共有して
2347/// おり、モーダル/非モーダルの区別自体が存在しなかった(`showModal()`本来の
2348/// モーダルフォーカストラップ・`::backdrop`描画は依然対象外だが対象外)。CSS
2349/// `:modal`疑似クラス(丸ごと未対応だった。2026-07-18 発見・実装)が判定
2350/// できるよう、`open`属性に加え`:fullscreen`と同じ内部専用属性パターンで
2351/// `_modal`を書き込む点だけが`.show()`と異なる。
2352pub fn dom_dialog_show_modal(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2353    if let Some(idx) = this_dom_idx(&this) {
2354        let mut dom = it.dom.borrow_mut();
2355        dom.set_attr(idx, "open", "open");
2356        dom.set_attr(idx, "_modal", "1");
2357    }
2358    Ok(Value::Undefined)
2359}
2360
2361/// `dialog.close(returnValue?)`。`open` 属性を外し、`returnValue` を設定してから
2362/// `close` イベントを発火する(仕様どおり)。DOM 要素は汎用の `Obj.props` ではなく
2363/// `interp.rs` 側の明示的なプロパティ一覧を介してアクセスされるため、`checked`/
2364/// `indeterminate` と同じ「内部専用の `_` プレフィックス属性キー」パターン
2365/// (`_return_value`)で持たせる(`this.props` へ直接書いても読み出し経路が
2366/// 無く反映されない)。
2367pub fn dom_dialog_close(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2368    if let Some(idx) = this_dom_idx(&this) {
2369        {
2370            let mut dom = it.dom.borrow_mut();
2371            dom.remove_attr(idx, "open");
2372            dom.remove_attr(idx, "_modal");
2373        }
2374        if !a.is_empty() {
2375            it.dom
2376                .borrow_mut()
2377                .set_attr(idx, "_return_value", &arg(a, 0).to_js_string());
2378        }
2379        let _ = it.dispatch_event_in_interp(idx, "close", &[]);
2380    }
2381    Ok(Value::Undefined)
2382}
2383
2384// `dialog.requestClose(returnValue?)`(HTML Living Standard。`.close()`と
2385// 違い、閉じる直前に取消可能な`cancel`イベントを発火し、リスナが
2386// `preventDefault()`すればダイアログを開いたままにできる。丸ごと
2387// 未対応だった。`.close()`自体は無条件クローズのため対になるこちらの
2388// 「ユーザーの閉じる操作を横取り可能にする」経路が漏れていた。
2389// `popover_toggle_extra`と同じ「取消可能イベント→未阻止なら実処理」
2390// パターンを再利用する。2026-07-18 発見・実装)。
2391pub fn dom_dialog_request_close(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2392    if let Some(idx) = this_dom_idx(&this) {
2393        let (_, prevented) = it.dispatch_event_in_interp(idx, "cancel", &[]);
2394        if !prevented {
2395            {
2396                let mut dom = it.dom.borrow_mut();
2397                dom.remove_attr(idx, "open");
2398                dom.remove_attr(idx, "_modal");
2399            }
2400            if !a.is_empty() {
2401                it.dom
2402                    .borrow_mut()
2403                    .set_attr(idx, "_return_value", &arg(a, 0).to_js_string());
2404            }
2405            let _ = it.dispatch_event_in_interp(idx, "close", &[]);
2406        }
2407    }
2408    Ok(Value::Undefined)
2409}
2410
2411/// `element.showPopover()`/`.hidePopover()`/`.togglePopover(force?)`(Popover
2412/// API。丸ごと未対応だった)。`<dialog>` の `open` 属性と同じく、内部専用属性
2413/// (`_popover_open`)を付け外しするだけの簡略実装(`css.rs` 側の UA 既定
2414/// display 切り替えと対)。`beforetoggle`/`toggle` イベント発火・
2415/// `:popover-open` 疑似クラス・ライトディスミスは対象外。
2416// `showPopover`/`hidePopover`/`togglePopover`が状態変化を`toggle`イベント
2417// として発火していなかった(丸ごと未対応だった。`<dialog>.close()`は既に
2418// `"close"`イベントを発火していたのに、同じ「開閉状態変化を通知する」
2419// 役目のPopover側だけ漏れていた非対称ギャップ。2026-07-17 発見)。仕様上は
2420// `oldState`/`newState`を持つ`ToggleEvent`だが、この処理系に`ToggleEvent`
2421// コンストラクタ自体が無いため、`<details>`の`toggle`イベント修正と同じ
2422// 簡略方針で`dispatch_event_in_interp`にプレーンな`Event`へ同名プロパティ
2423// を追加する形(`extra`引数)で代用する。対になる取消可能な`beforetoggle`
2424// (状態変更「前」に発火し`preventDefault()`で開閉を中止できる。同じく
2425// 丸ごと未対応だった)も同時に実装。
2426fn popover_toggle_extra(old_open: bool, new_open: bool) -> alloc::vec::Vec<(String, Value)> {
2427    alloc::vec![
2428        (String::from("oldState"), Value::str(if old_open { "open" } else { "closed" })),
2429        (String::from("newState"), Value::str(if new_open { "open" } else { "closed" })),
2430    ]
2431}
2432pub fn dom_show_popover(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2433    if let Some(idx) = this_dom_idx(&this) {
2434        let was_open = it.dom.borrow().has_attr(idx, "_popover_open");
2435        // `beforetoggle`(状態変更「前」に発火する取消可能なイベント。
2436        // `preventDefault()`で開閉自体を中止できる。丸ごと未対応だった。
2437        // 2026-07-17 発見・実装)。
2438        let (_, prevented) =
2439            it.dispatch_event_in_interp(idx, "beforetoggle", &popover_toggle_extra(was_open, true));
2440        if prevented {
2441            return Ok(Value::Undefined);
2442        }
2443        it.dom.borrow_mut().set_attr(idx, "_popover_open", "");
2444        let _ = it.dispatch_event_in_interp(idx, "toggle", &popover_toggle_extra(was_open, true));
2445    }
2446    Ok(Value::Undefined)
2447}
2448pub fn dom_hide_popover(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2449    if let Some(idx) = this_dom_idx(&this) {
2450        let was_open = it.dom.borrow().has_attr(idx, "_popover_open");
2451        let (_, prevented) =
2452            it.dispatch_event_in_interp(idx, "beforetoggle", &popover_toggle_extra(was_open, false));
2453        if prevented {
2454            return Ok(Value::Undefined);
2455        }
2456        it.dom.borrow_mut().remove_attr(idx, "_popover_open");
2457        let _ = it.dispatch_event_in_interp(idx, "toggle", &popover_toggle_extra(was_open, false));
2458    }
2459    Ok(Value::Undefined)
2460}
2461pub fn dom_toggle_popover(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2462    if let Some(idx) = this_dom_idx(&this) {
2463        let was_open = it.dom.borrow().has_attr(idx, "_popover_open");
2464        let force = if matches!(arg(a, 0), Value::Undefined) {
2465            !was_open
2466        } else {
2467            arg(a, 0).truthy()
2468        };
2469        let (_, prevented) =
2470            it.dispatch_event_in_interp(idx, "beforetoggle", &popover_toggle_extra(was_open, force));
2471        if prevented {
2472            return Ok(Value::Bool(was_open));
2473        }
2474        if force {
2475            it.dom.borrow_mut().set_attr(idx, "_popover_open", "");
2476        } else {
2477            it.dom.borrow_mut().remove_attr(idx, "_popover_open");
2478        }
2479        let _ = it.dispatch_event_in_interp(idx, "toggle", &popover_toggle_extra(was_open, force));
2480        return Ok(Value::Bool(force));
2481    }
2482    Ok(Value::Bool(false))
2483}
2484
2485/// element.append(...nodes):末尾に複数追加(文字列はテキスト)。
2486/// `element.replaceChildren(...nodes)`(DOM 標準。`el.innerHTML=''; el.append(...)`
2487/// の1行版として使われる定番イディオム)が丸ごと未対応だった。既存の
2488/// `set_inner_html(idx, "")`(子を丸ごとクリア)と `append`/`arg_to_node` と同じ
2489/// 「文字列は text node、DOM ノードはそのまま」変換を組み合わせて実装する。
2490pub fn dom_replace_children(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2491    if let Some(p) = this_dom_idx(&this) {
2492        it.dom.borrow_mut().set_inner_html(p, "");
2493        for v in a {
2494            if let Some(c) = arg_to_node(it, v) {
2495                it.dom.borrow_mut().append_child(p, c);
2496            }
2497        }
2498    }
2499    Ok(Value::Undefined)
2500}
2501pub fn dom_append(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2502    if let Some(p) = this_dom_idx(&this) {
2503        for v in a {
2504            if let Some(c) = arg_to_node(it, v) {
2505                it.dom.borrow_mut().append_child(p, c);
2506            }
2507        }
2508    }
2509    Ok(Value::Undefined)
2510}
2511/// element.prepend(...nodes):先頭に複数追加(順序保持)。
2512pub fn dom_prepend(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2513    if let Some(p) = this_dom_idx(&this) {
2514        let refnode = it
2515            .dom
2516            .borrow()
2517            .nodes
2518            .get(p)
2519            .and_then(|n| n.children.first().copied());
2520        for v in a {
2521            if let Some(c) = arg_to_node(it, v) {
2522                it.dom.borrow_mut().insert_before(p, c, refnode);
2523            }
2524        }
2525    }
2526    Ok(Value::Undefined)
2527}
2528/// element.before(...nodes):自身の前に兄弟として挿入。
2529pub fn dom_before(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2530    if let Some(s) = this_dom_idx(&this) {
2531        let parent = it.dom.borrow().nodes.get(s).and_then(|n| n.parent);
2532        if let Some(p) = parent {
2533            for v in a {
2534                if let Some(c) = arg_to_node(it, v) {
2535                    it.dom.borrow_mut().insert_before(p, c, Some(s));
2536                }
2537            }
2538        }
2539    }
2540    Ok(Value::Undefined)
2541}
2542/// element.after(...nodes):自身の後に兄弟として挿入。
2543pub fn dom_after(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2544    if let Some(s) = this_dom_idx(&this) {
2545        let (parent, next) = {
2546            let dom = it.dom.borrow();
2547            let parent = dom.nodes.get(s).and_then(|n| n.parent);
2548            let next = parent.and_then(|p| dom.nodes.get(p)).and_then(|n| {
2549                n.children
2550                    .iter()
2551                    .position(|&c| c == s)
2552                    .and_then(|i| n.children.get(i + 1).copied())
2553            });
2554            (parent, next)
2555        };
2556        if let Some(p) = parent {
2557            for v in a {
2558                if let Some(c) = arg_to_node(it, v) {
2559                    it.dom.borrow_mut().insert_before(p, c, next);
2560                }
2561            }
2562        }
2563    }
2564    Ok(Value::Undefined)
2565}
2566/// element.replaceWith(...nodes):自身を nodes で置換。
2567pub fn dom_replace_with(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2568    if let Some(s) = this_dom_idx(&this) {
2569        let parent = it.dom.borrow().nodes.get(s).and_then(|n| n.parent);
2570        if let Some(p) = parent {
2571            for v in a {
2572                if let Some(c) = arg_to_node(it, v) {
2573                    it.dom.borrow_mut().insert_before(p, c, Some(s));
2574                }
2575            }
2576            it.dom.borrow_mut().remove_node(s);
2577        }
2578    }
2579    Ok(Value::Undefined)
2580}
2581
2582/// node.cloneNode(deep)。複製ノード(親なし)を返す。
2583pub fn dom_clone_node(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2584    if let Some(idx) = this_dom_idx(&this) {
2585        let deep = arg(a, 0).truthy();
2586        let new_idx = it.dom.borrow_mut().clone_node(idx, deep);
2587        return Ok(dom_handle(Some(new_idx)));
2588    }
2589    Ok(Value::Null)
2590}
2591/// `document.importNode(externalNode, deep)`(丸ごと未対応だった。
2592/// `<template>` の中身を複製して自文書へ取り込む定番パターン
2593/// `document.importNode(template.content, true)` で使われる)。この処理系は
2594/// 単一文書(複数 `Document` オブジェクトという概念自体が無い)のため、
2595/// 「別文書から取り込む」という仕様上の意味は無く、実質 `node.cloneNode(deep)`
2596/// と同じ結果になる簡略実装。仕様上 `deep` は省略時 `false` だが、実務では
2597/// ほぼ常に明示的に `true` が渡される。
2598pub fn document_import_node(it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
2599    dom_clone_node(it, arg(a, 0), a.get(1..).unwrap_or(&[]))
2600}
2601/// `document.adoptNode(node)`(丸ごと未対応だった)。単一文書のこの処理系
2602/// では「別文書から養子縁組する」という意味は無いため、仕様の副作用
2603/// (現在の親から取り外す)のみを再現し、渡されたノードをそのまま返す。
2604pub fn document_adopt_node(it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
2605    let node = arg(a, 0);
2606    if let Some(idx) = this_dom_idx(&node) {
2607        it.dom.borrow_mut().remove_node(idx);
2608    }
2609    Ok(node)
2610}
2611
2612/// `element.click()`。以前は単なる no-op で、実際のクリック
2613/// (`onclick`/`addEventListener('click', ...)` リスナの発火)を一切引き起こさな
2614/// かった。実マウス操作からも呼ばれる既存の `dispatch_click()` を再利用する。
2615/// `checkbox`/`radio` は仕様上クリックすると(リスナの実行前に)自身の選択状態が
2616/// 切り替わる。以前は `element.checked` という JS プロパティ自体が丸ごと未実装
2617/// だったため、この状態切替もできていなかった。**重要**: 同じ `name` を持つ
2618/// ラジオボタン間の排他選択(1つを選ぶと他が自動で解除される、ラジオボタンの
2619/// 最も基本的な仕様上の挙動)も以前は非対応の簡略化として明示的に見送られていたが、
2620/// `uncheck_radio_group_siblings` を新設し配線した。
2621/// `element.scrollTo(x, y)`/`.scrollTo({top, left, behavior})` が丸ごと未対応
2622/// `element.scrollTop`/`element.scrollLeft` および `scrollTo`/`scrollBy`/`scroll`
2623/// の対象座標を引数から抽出する。オブジェクト引数 `{top, left}` または
2624/// 位置引数 `(x, y)` の両方に対応する。
2625pub(crate) fn scroll_top_target(a: &[Value]) -> Option<i32> {
2626    match arg(a, 0) {
2627        Value::Object(_) => obj_prop(&arg(a, 0), "top").map(|v| v.to_number() as i32),
2628        Value::Undefined => None,
2629        _ => Some(arg(a, 1).to_number() as i32),
2630    }
2631}
2632
2633pub(crate) fn scroll_left_target(a: &[Value]) -> Option<i32> {
2634    match arg(a, 0) {
2635        Value::Object(_) => obj_prop(&arg(a, 0), "left").map(|v| v.to_number() as i32),
2636        Value::Undefined => None,
2637        _ => Some(arg(a, 0).to_number() as i32),
2638    }
2639}
2640
2641pub fn dom_scroll_to(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2642    if let Some(idx) = this_dom_idx(&this) {
2643        let top = scroll_top_target(a);
2644        let left = scroll_left_target(a);
2645        if top.is_some() || left.is_some() {
2646            let mut dom = it.dom.borrow_mut();
2647            if let Some(t) = top {
2648                dom.scroll_tops.insert(idx, t.max(0));
2649            }
2650            if let Some(l) = left {
2651                dom.scroll_lefts.insert(idx, l.max(0));
2652            }
2653            dom.dirty = true;
2654        }
2655    }
2656    Ok(Value::Undefined)
2657}
2658
2659/// `element.scrollBy(x, y)`/`.scrollBy({top, left})`。現在値からの相対移動である点
2660/// のみ `scrollTo` と異なる。
2661pub fn dom_scroll_by(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2662    if let Some(idx) = this_dom_idx(&this) {
2663        let delta_top = scroll_top_target(a);
2664        let delta_left = scroll_left_target(a);
2665        if delta_top.is_some() || delta_left.is_some() {
2666            let mut dom = it.dom.borrow_mut();
2667            if let Some(dt) = delta_top {
2668                let cur = dom.scroll_tops.get(&idx).copied().unwrap_or(0);
2669                dom.scroll_tops.insert(idx, (cur + dt).max(0));
2670            }
2671            if let Some(dl) = delta_left {
2672                let cur = dom.scroll_lefts.get(&idx).copied().unwrap_or(0);
2673                dom.scroll_lefts.insert(idx, (cur + dl).max(0));
2674            }
2675            dom.dirty = true;
2676        }
2677    }
2678    Ok(Value::Undefined)
2679}
2680/// `element.scrollIntoView()`/`.scrollIntoView({behavior, block})`が丸ごと
2681/// no-opだった(HTML Standard §"scroll into view"の簡略実装が丸ごと欠落。
2682/// 実サイト www.sugi-lab.net のナビゲーションリンク(`target.scrollIntoView(
2683/// {behavior:'smooth', block:'start'})`)が完全に無効化されており、クリック
2684/// しても一切スクロールしないという実害があった。2026-07-22発見・修正)。
2685/// 既存の`window.scrollTo`と同じ`pending_scroll_abs_y`機構(`render.rs`の
2686/// `draw()`冒頭で消費・`fire_scroll()`呼び出しまで行う、既に動作確認済みの
2687/// 経路)を再利用する。対象要素の現在のビューポート相対Y座標
2688/// (`get_rect`。`getBoundingClientRect`と同じ値)に現在のスクロール量
2689/// (`window.scrollY`)を加算し、文書内の絶対Y座標を求めて要素の上端が
2690/// ビューポート上端に来るようにスクロールする(`block:'start'`相当。
2691/// `center`/`end`/`nearest`は簡略化しすべて`start`と同一視する。`behavior`
2692/// は既存の`scrollTo`と同様、実時間アニメーションの概念が無いため即座に
2693/// 反映する)。
2694pub fn dom_scroll_into_view(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2695    let Some(idx) = this_dom_idx(&this) else {
2696        return Ok(Value::Undefined);
2697    };
2698    let Some((_, y, _, _)) = it.dom.borrow().get_rect(idx) else {
2699        return Ok(Value::Undefined);
2700    };
2701    let current_scroll_y = it
2702        .global
2703        .borrow()
2704        .vars
2705        .get("scrollY")
2706        .map(|v| v.to_number())
2707        .unwrap_or(0.0);
2708    let target_abs_y = (current_scroll_y + y as f64).max(0.0);
2709    it.dom.borrow_mut().pending_scroll_abs_y = Some(target_abs_y as i32);
2710    Ok(Value::Undefined)
2711}
2712pub fn dom_click(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2713    if let Some(idx) = this_dom_idx(&this) {
2714        let ty = it.dom.borrow().get_attr(idx, "type").unwrap_or_default();
2715        match ty.as_str() {
2716            "checkbox" => {
2717                let checked = {
2718                    let dom = it.dom.borrow();
2719                    if let Some(lc) = dom.get_attr(idx, "_live_checked") {
2720                        lc == "true"
2721                    } else {
2722                        dom.has_attr(idx, "checked")
2723                    }
2724                };
2725                let mut dom = it.dom.borrow_mut();
2726                if checked {
2727                    dom.set_attr(idx, "_live_checked", "false");
2728                } else {
2729                    dom.set_attr(idx, "_live_checked", "true");
2730                }
2731            }
2732            "radio" => {
2733                let mut dom = it.dom.borrow_mut();
2734                dom.set_attr(idx, "_live_checked", "true");
2735                dom.uncheck_radio_group_siblings(idx);
2736            }
2737            _ => {}
2738        }
2739        it.dispatch_event_in_interp(idx, "click", &[]);
2740    }
2741    Ok(Value::Undefined)
2742}
2743
2744/// `element.focus()`(HTML5。丸ごと未対応だった。`document.activeElement`
2745/// と対になる書き込み側。以前は `scrollIntoView` と同じ no-op で
2746/// `document.activeElement` が常に `null` 固定になっていた)。`DomBridge::
2747/// focused_idx` を直接更新し、仕様どおりの順序(旧要素の `blur`/
2748/// `focusout` → 新要素の `focus`/`focusin`)でイベントを発火する。レンダラ側
2749/// の `WebEngine::focused_id`(実クリック/`autofocus` 用の文字列キー版状態)
2750/// との同期は対象外(詳細は `DomBridge::focused_idx` のコメント参照)。
2751pub fn dom_focus(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2752    if let Some(idx) = this_dom_idx(&this) {
2753        let prev = it.dom.borrow().focused_idx;
2754        if prev == Some(idx) {
2755            return Ok(Value::Undefined);
2756        }
2757        if let Some(p) = prev {
2758            it.dom.borrow_mut().focused_idx = None;
2759            let _ = it.dispatch_event_in_interp(p, "blur", &[]);
2760            let _ = it.dispatch_event_in_interp(p, "focusout", &[]);
2761        }
2762        it.dom.borrow_mut().focused_idx = Some(idx);
2763        let _ = it.dispatch_event_in_interp(idx, "focus", &[]);
2764        let _ = it.dispatch_event_in_interp(idx, "focusin", &[]);
2765    }
2766    Ok(Value::Undefined)
2767}
2768/// `element.blur()`(HTML5。丸ごと未対応だった。`dom_focus` の対)。自身が
2769/// 現在のフォーカス対象である場合のみ解除して `blur`/`focusout` を発火する
2770/// (仕様どおり、フォーカスされていない要素での `.blur()` は no-op)。
2771pub fn dom_blur(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2772    if let Some(idx) = this_dom_idx(&this) {
2773        let is_focused = it.dom.borrow().focused_idx == Some(idx);
2774        if is_focused {
2775            it.dom.borrow_mut().focused_idx = None;
2776            let _ = it.dispatch_event_in_interp(idx, "blur", &[]);
2777            let _ = it.dispatch_event_in_interp(idx, "focusout", &[]);
2778        }
2779    }
2780    Ok(Value::Undefined)
2781}
2782
2783/// `input.stepUp(n?)`/`.stepDown(n?)`(HTML5。`<input type="number"/"range">`
2784/// で値を `step` 刻みで増減する定番メソッド)が丸ごと未対応だった。`value`/
2785/// `step`/`min`/`max` 属性を読み、`min`/`max` があればクランプしてから
2786/// `value` 属性へ書き戻す(`step="any"`等パース不能な場合は既定値 1 に
2787/// フォールバックする、この処理系の他の数値属性パースと同じ寛容な簡略化)。
2788pub(crate) fn dom_step_by(it: &mut Interp, this: Value, a: &[Value], sign: f64) -> Result<Value, Value> {
2789    if let Some(idx) = this_dom_idx(&this) {
2790        let (cur, step, min, max) = {
2791            let dom = it.dom.borrow();
2792            let raw_val = dom.get_attr(idx, "_live_value")
2793                .or_else(|| dom.get_attr(idx, "value"));
2794            (
2795                raw_val.and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0),
2796                dom.get_attr(idx, "step").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0),
2797                dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()),
2798                dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()),
2799            )
2800        };
2801        let multiplier = match arg(a, 0) {
2802            Value::Undefined => 1.0,
2803            n => n.to_number(),
2804        };
2805        let mut new_val = cur + sign * step * multiplier;
2806        if let Some(mn) = min {
2807            if new_val < mn {
2808                new_val = mn;
2809            }
2810        }
2811        if let Some(mx) = max {
2812            if new_val > mx {
2813                new_val = mx;
2814            }
2815        }
2816        let str_val = Value::Number(new_val).to_js_string();
2817        it.dom.borrow_mut().set_attr(idx, "_live_value", &str_val);
2818    }
2819    Ok(Value::Undefined)
2820}
2821pub fn dom_step_up(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2822    dom_step_by(it, this, a, 1.0)
2823}
2824pub fn dom_step_down(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2825    dom_step_by(it, this, a, -1.0)
2826}
2827
2828/// `form.requestSubmit(submitter?)`(HTML5)が丸ごと未対応だった。既存の
2829/// `dom_click` と同じ `dispatch_event_in_interp` 経由で `submit` イベントを
2830/// 発火する(`preventDefault()` で中止可能。`submitter` 引数は現状無視)。
2831pub fn dom_request_submit(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2832    if let Some(idx) = this_dom_idx(&this) {
2833        it.dispatch_event_in_interp(idx, "submit", &[]);
2834    }
2835    Ok(Value::Undefined)
2836}
2837
2838/// `node.contains(other)`(DOM 標準。`other` が `node` 自身または子孫かどうか。
2839/// click-outside 判定などで広く使われる)が丸ごと未対応だった。既存の
2840/// `is_ancestor_of()` は「`idx` 自身は含まない」仕様のため、仕様どおり自身も
2841/// 含めるにはここで等価チェックを補う。
2842pub fn dom_contains(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2843    let other = arg(a, 0);
2844    if let (Some(anc), Some(desc)) = (this_dom_idx(&this), this_dom_idx(&other)) {
2845        let contains = anc == desc || it.dom.borrow().is_ancestor_of(anc, desc);
2846        return Ok(Value::Bool(contains));
2847    }
2848    Ok(Value::Bool(false))
2849}
2850
2851/// `node.getRootNode()`(DOM標準。`el.getRootNode() === document` という
2852/// 「接続済みか」の定番判定や、shadow DOM 対応コードの分岐で使われる)が丸ごと
2853/// 未対応だった。この処理系は shadow DOM を持たないため、親を辿れる限り辿った
2854/// 最上位ノードを返す単純な実装で仕様の意図(最も遠い祖先)を満たす。
2855pub fn dom_get_root_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2856    if let Some(idx) = this_dom_idx(&this) {
2857        let mut cur = idx;
2858        let mut guard = 0;
2859        loop {
2860            let parent = it.dom.borrow().nodes.get(cur).and_then(|n| n.parent);
2861            match parent {
2862                Some(p) => {
2863                    cur = p;
2864                    guard += 1;
2865                    if guard > it.dom.borrow().nodes.len() {
2866                        break;
2867                    }
2868                }
2869                None => break,
2870            }
2871        }
2872        if cur == 0 {
2873            if let Some(doc) = it.global.borrow().vars.get("document").cloned() {
2874                return Ok(doc);
2875            }
2876        }
2877        return Ok(Value::Object(Obj::dom(cur)));
2878    }
2879    if matches!(this, Value::Object(_)) {
2880        if let Ok(parent) = it.get_property(&this, "parentNode") {
2881            if !matches!(parent, Value::Null | Value::Undefined) {
2882                return dom_get_root_node(it, parent, _a);
2883            }
2884        }
2885        return Ok(this);
2886    }
2887    Ok(Value::Undefined)
2888}
2889
2890/// `element.checkVisibility()`(DOM 標準。丸ごと未対応だった)。オプション引数
2891/// (`checkOpacity`/`checkVisibilityCSS`/`contentVisibilityAuto` 等)は未対応で
2892/// 常に無視する簡略実装。自身の算出 `visibility` が `hidden`/`collapse` の場合、
2893/// および自身または祖先のいずれかの算出 `display` が `none` の場合に `false` を返す。
2894/// `node.lookupNamespaceURI(prefix)`/`.lookupPrefix(namespace)`/
2895/// `.isDefaultNamespace(namespace)`(丸ごと未対応だった。2026-07-16
2896/// 発見)。この処理系は `*AttributeNS`/`createElementNS` と同じく XML
2897/// 名前空間を一切モデル化していない(`document_create_element_ns` は
2898/// namespace 引数を無視して委譲するのみ)ため、`xmlns`/`xmlns:*` 属性を
2899/// 祖先チェーンから探索する誠実な実装は行わず、常に「名前空間なし」を表す
2900/// 値(`lookupNamespaceURI`/`lookupPrefix` は `null`)を返す簡略実装。
2901/// `isDefaultNamespace(namespace)` は仕様上 `lookupNamespaceURI(null) ===
2902/// namespace` と等価なので、`lookupNamespaceURI` が常に `null` を返す
2903/// このモデルでは `namespace` が `null`/`undefined` の時のみ `true` になる。
2904pub fn dom_lookup_namespace_uri(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
2905    Ok(Value::Null)
2906}
2907
2908pub fn dom_lookup_prefix(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
2909    Ok(Value::Null)
2910}
2911
2912pub fn dom_is_default_namespace(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
2913    Ok(Value::Bool(matches!(arg(a, 0), Value::Null | Value::Undefined)))
2914}
2915
2916pub fn dom_check_visibility(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2917    if let Some(idx) = this_dom_idx(&this) {
2918        let visibility = it.dom.borrow().get_computed(idx, "visibility");
2919        if visibility == "hidden" || visibility == "collapse" {
2920            return Ok(Value::Bool(false));
2921        }
2922        let mut cur = idx;
2923        let mut guard = 0;
2924        loop {
2925            if it.dom.borrow().get_computed(cur, "display") == "none" {
2926                return Ok(Value::Bool(false));
2927            }
2928            let parent = it.dom.borrow().nodes.get(cur).and_then(|n| n.parent);
2929            match parent {
2930                Some(p) => {
2931                    cur = p;
2932                    guard += 1;
2933                    if guard > it.dom.borrow().nodes.len() {
2934                        break;
2935                    }
2936                }
2937                None => break,
2938            }
2939        }
2940        return Ok(Value::Bool(true));
2941    }
2942    Ok(Value::Bool(false))
2943}
2944
2945/// `node.compareDocumentPosition(other)`(DOM標準)が丸ごと未対応だった。
2946/// `DomBridge::document_position` にビットマスク計算を委譲する。`other` が DOM ノードで
2947/// なければ仕様上 `TypeError` だが、この処理系では簡略化して disconnected 扱いにする。
2948pub fn dom_compare_document_position(
2949    it: &mut Interp,
2950    this: Value,
2951    a: &[Value],
2952) -> Result<Value, Value> {
2953    let other = arg(a, 0);
2954    if let (Some(this_idx), Some(other_idx)) = (this_dom_idx(&this), this_dom_idx(&other)) {
2955        let mask = it.dom.borrow().document_position(this_idx, other_idx);
2956        return Ok(Value::Number(mask as f64));
2957    }
2958    // DOM ノードでない引数(この処理系では TypeError の代わりに DISCONNECTED 扱い)。
2959    Ok(Value::Number(1.0))
2960}
2961
2962/// classList.replace(oldClass, newClass)。old があれば new に置換し true、無ければ false。
2963pub fn dom_classlist_replace(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2964    if let Some(idx) = this_host_idx(&this, "classList:") {
2965        let old = arg(a, 0).to_js_string();
2966        let new = arg(a, 1).to_js_string();
2967        let mut dom = it.dom.borrow_mut();
2968        let had = dom.class_contains(idx, &old);
2969        if had {
2970            dom.class_remove(idx, &old);
2971            dom.class_add(idx, &new);
2972        }
2973        return Ok(Value::Bool(had));
2974    }
2975    Ok(Value::Bool(false))
2976}
2977
2978pub fn dom_remove_child(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2979    let child = arg(a, 0);
2980    if let (Some(p), Some(c)) = (this_dom_idx(&this), this_dom_idx(&child)) {
2981        // `insertBefore`/`replaceChild`と同じ理由(詳細はそちらの同種
2982        // コメント参照)で、`child`が`this`の子でない場合は仕様上
2983        // `NotFoundError`を投げるべきだが、以前は黙って何もせず成功した
2984        // かのように振る舞っていた。
2985        let is_child = it.dom.borrow().nodes.get(c).and_then(|n| n.parent) == Some(p);
2986        if !is_child {
2987            return Err(make_dom_exception(
2988                "NotFoundError",
2989                "The node to be removed is not a child of this node",
2990            ));
2991        }
2992        it.dom.borrow_mut().remove_child(p, c);
2993    }
2994    Ok(child)
2995}
2996
2997/// `element.remove()`(`ChildNode.remove()`。引数無しで自身を親から取り外す)。
2998/// ただし`<select>`だけは`HTMLSelectElement.remove(index)`という別物の
2999/// オーバーロードを持ち、数値引数が渡された場合は「自身を削除」ではなく
3000/// 「指定インデックスのoptionを削除」という全く異なる意味になる(仕様上の
3001/// 別メソッドがたまたま同名なだけ)。以前はこの区別が無く、`select.remove(1)`
3002/// が常にselect自身を親から削除してしまう(`options`側の削除ではなく)
3003/// バグだった。
3004pub fn dom_remove(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3005    if let Some(idx) = this_dom_idx(&this) {
3006        let is_select = it.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false);
3007        if is_select {
3008            if let Some(v) = a.first() {
3009                let n = v.to_number();
3010                if n.is_finite() {
3011                    // 負のインデックスは仕様上 no-op(`as usize` は負の f64 を 0 へ
3012                    // 飽和させてしまい、`remove(-1)` が誤って先頭optionを削除する
3013                    // バグになるため明示的に弾く)。
3014                    if n >= 0.0 {
3015                        let options = it.dom.borrow().select_options(idx);
3016                        if let Some(&opt) = options.get(n as usize) {
3017                            it.dom.borrow_mut().remove_child(idx, opt);
3018                        }
3019                    }
3020                    return Ok(Value::Undefined);
3021                }
3022            }
3023        }
3024        it.dom.borrow_mut().remove_node(idx);
3025    }
3026    Ok(Value::Undefined)
3027}
3028
3029/// ネイティブ関数値を作るショートカット。
3030pub(crate) fn nv(name: &str, f: NativeFn) -> Value {
3031    Value::Object(Obj::native(name, f))
3032}
3033pub(crate) fn arg(args: &[Value], i: usize) -> Value {
3034    args.get(i).cloned().unwrap_or(Value::Undefined)
3035}
3036
3037/// `HTMLCollection.prototype.item(index)`(DOM Standard §4.2.1)。
3038pub fn html_collection_item(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3039    if let Value::Object(o) = &this {
3040        let idx_num = arg(a, 0).to_number();
3041        if idx_num >= 0.0 && idx_num.is_finite() {
3042            let i = idx_num as usize;
3043            let borrow = o.borrow();
3044            if let ObjKind::Array(ref vec) = borrow.kind {
3045                if let Some(val) = vec.get(i) {
3046                    return Ok(val.clone());
3047                }
3048            }
3049        }
3050    }
3051    Ok(Value::Null)
3052}
3053
3054/// `HTMLCollection.prototype.namedItem(name)`(DOM Standard §4.2.1 / HTML Standard §4.10.21)。
3055/// 同名/同IDのコントロールが複数ある場合は `RadioNodeList` を返す。
3056pub fn html_collection_named_item(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3057    if let Value::Object(o) = &this {
3058        let target = arg(a, 0).to_js_string();
3059        if !target.is_empty() {
3060            let borrow = o.borrow();
3061            if let ObjKind::Array(ref vec) = borrow.kind {
3062                let dom = it.dom.borrow();
3063                let mut matches: Vec<Value> = Vec::new();
3064                let mut match_indices: Vec<usize> = Vec::new();
3065                for val in vec {
3066                    if let Value::Object(item_obj) = val {
3067                        if let ObjKind::DomElement(elem_idx) = item_obj.borrow().kind {
3068                            let id_match = dom.get_attr(elem_idx, "id").map(|id| id == target).unwrap_or(false);
3069                            let name_match = dom.get_attr(elem_idx, "name").map(|name| name == target).unwrap_or(false);
3070                            if id_match || name_match {
3071                                matches.push(val.clone());
3072                                match_indices.push(elem_idx);
3073                            }
3074                        }
3075                    }
3076                }
3077                if matches.len() == 1 {
3078                    return Ok(matches[0].clone());
3079                } else if matches.len() > 1 {
3080                    return Ok(make_radio_node_list(it, match_indices));
3081                }
3082            }
3083        }
3084    }
3085    Ok(Value::Null)
3086}
3087
3088/// `RadioNodeList` オブジェクトを生成するヘルパー(HTML Standard §4.10.21.1)。
3089/// 配列構造に加え `.value` (getter/setter) アクセスおよび `item(index)` メソッドをサポートする。
3090pub fn make_radio_node_list(_it: &Interp, elements: Vec<usize>) -> Value {
3091    let mut arr: Vec<Value> = Vec::with_capacity(elements.len());
3092    for &idx in &elements {
3093        arr.push(Value::Object(Obj::dom(idx)));
3094    }
3095    let obj = Obj::array(arr);
3096    obj.borrow_mut().props.insert("_is_radio_nodelist".into(), Value::Bool(true));
3097    obj.borrow_mut().props.insert("item".into(), nv("item", html_collection_item));
3098    Value::Object(obj)
3099}
3100
3101/// `HTMLCollection` / `HTMLFormControlsCollection` オブジェクトを生成するヘルパー。
3102/// 配列機能に加え、DOM Standard §4.2.1 に準拠して `item(index)` および
3103/// `namedItem(name)` メソッドを持つ。
3104pub fn make_html_collection(_it: &Interp, elements: Vec<usize>) -> Value {
3105    let mut arr: Vec<Value> = Vec::with_capacity(elements.len());
3106    for &idx in &elements {
3107        arr.push(Value::Object(Obj::dom(idx)));
3108    }
3109    let obj = Obj::array(arr);
3110    obj.borrow_mut().props.insert("item".into(), nv("item", html_collection_item));
3111    obj.borrow_mut().props.insert("namedItem".into(), nv("namedItem", html_collection_named_item));
3112    Value::Object(obj)
3113}
3114
3115fn parsed_doc_get_root_idx(this: &Value) -> Option<usize> {
3116    if let Value::Object(o) = this {
3117        if let Some(Value::Number(n)) = o.borrow().props.get("_root_idx") {
3118            return Some(*n as usize);
3119        }
3120    }
3121    None
3122}
3123
3124pub(crate) fn parsed_doc_get_element_by_id(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3125    if let Some(root_idx) = parsed_doc_get_root_idx(&this) {
3126        let id = arg(a, 0).to_js_string();
3127        let dom = it.dom.borrow();
3128        if let Some(n) = dom.nodes.get(root_idx) {
3129            if !n.is_text && n.id == id {
3130                return Ok(dom_handle(Some(root_idx)));
3131            }
3132        }
3133        for desc in dom.descendants_by_tag_name(root_idx, "*") {
3134            if let Some(n) = dom.nodes.get(desc) {
3135                if !n.is_text && n.id == id {
3136                    return Ok(dom_handle(Some(desc)));
3137                }
3138            }
3139        }
3140        return Ok(Value::Null);
3141    }
3142    document_get_element_by_id(it, this, a)
3143}
3144
3145pub(crate) fn parsed_doc_query_selector(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3146    if let Some(root_idx) = parsed_doc_get_root_idx(&this) {
3147        let sel = arg(a, 0).to_js_string();
3148        let dom = it.dom.borrow();
3149        for c in dom.query_all_scoped(&sel, Some(root_idx)) {
3150            if c == root_idx || dom.is_ancestor_of(root_idx, c) {
3151                return Ok(dom_handle(Some(c)));
3152            }
3153        }
3154        return Ok(Value::Null);
3155    }
3156    document_query_selector(it, this, a)
3157}
3158
3159pub(crate) fn parsed_doc_query_selector_all(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3160    if let Some(root_idx) = parsed_doc_get_root_idx(&this) {
3161        let sel = arg(a, 0).to_js_string();
3162        let dom = it.dom.borrow();
3163        let mut items = Vec::new();
3164        for c in dom.query_all_scoped(&sel, Some(root_idx)) {
3165            if c == root_idx || dom.is_ancestor_of(root_idx, c) {
3166                items.push(Value::Object(Obj::dom(c)));
3167            }
3168        }
3169        return Ok(Value::Object(Obj::array(items)));
3170    }
3171    document_query_selector_all(it, this, a)
3172}
3173
3174pub(crate) fn parsed_doc_get_elements_by_tag_name(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3175    if let Some(root_idx) = parsed_doc_get_root_idx(&this) {
3176        let tag = arg(a, 0).to_js_string();
3177        let dom = it.dom.borrow();
3178        let mut items = Vec::new();
3179        let want_all = tag == "*";
3180        if let Some(n) = dom.nodes.get(root_idx) {
3181            if !n.is_text && (want_all || n.tag.eq_ignore_ascii_case(&tag)) {
3182                items.push(Value::Object(Obj::dom(root_idx)));
3183            }
3184        }
3185        for desc in dom.descendants_by_tag_name(root_idx, &tag) {
3186            items.push(Value::Object(Obj::dom(desc)));
3187        }
3188        return Ok(Value::Object(Obj::array(items)));
3189    }
3190    document_get_elements_by_tag_name(it, this, a)
3191}
3192
3193/// `DOMParser.prototype.parseFromString(str, type)` (DOM Parsing and Serialization §4.1)。
3194/// 入力された文字列を指定された MIME タイプ(text/html, text/xml, application/xml, image/svg+xml 等)でパースし、
3195/// 独立した Document オブジェクト(および XML エラー時は <parsererror> 要素)を返却する。
3196pub fn dom_parser_parse_from_string(it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3197    let raw_input = args.first().map(|v| v.to_js_string()).unwrap_or_default();
3198    let mime = args.get(1).map(|v| v.to_js_string()).unwrap_or_default();
3199    let mime_type = crate::os_lib::xml_parser::parse_mime_type(&mime);
3200    if mime_type == crate::os_lib::xml_parser::SupportedMimeType::Unsupported {
3201        return Err(Value::str("TypeError: Invalid or unsupported MIME type"));
3202    }
3203
3204    let root_node = match mime_type {
3205        crate::os_lib::xml_parser::SupportedMimeType::Html => {
3206            crate::os_lib::dom::parse_html(&raw_input)
3207        }
3208        crate::os_lib::xml_parser::SupportedMimeType::Xml
3209        | crate::os_lib::xml_parser::SupportedMimeType::Svg
3210        | crate::os_lib::xml_parser::SupportedMimeType::Xhtml => {
3211            match crate::os_lib::xml_parser::parse_xml(&raw_input) {
3212                Ok(node) => node,
3213                Err(e) => crate::os_lib::xml_parser::create_parsererror_node(&e),
3214            }
3215        }
3216        crate::os_lib::xml_parser::SupportedMimeType::Unsupported => unreachable!(),
3217    };
3218
3219    let root_idx = it.dom.borrow_mut().import_node(&root_node);
3220    if mime_type != crate::os_lib::xml_parser::SupportedMimeType::Html {
3221        it.dom.borrow_mut().mark_subtree_xml(root_idx);
3222    }
3223    let doc = Obj::host("document");
3224    {
3225        let mut d = doc.borrow_mut();
3226        d.props.insert("_root_idx".into(), Value::Number(root_idx as f64));
3227        d.props.insert("nodeType".into(), Value::Number(9.0));
3228        d.props.insert("nodeName".into(), Value::str("#document"));
3229        d.props.insert("contentType".into(), Value::str(&mime));
3230
3231        let doc_elem_idx = {
3232            let dom = it.dom.borrow();
3233            if let Some(n) = dom.nodes.get(root_idx) {
3234                if !n.is_text && !n.tag.starts_with('#') {
3235                    Some(root_idx)
3236                } else {
3237                    n.children.iter().copied().find(|&c| {
3238                        dom.nodes.get(c).map(|cn| !cn.is_text).unwrap_or(false)
3239                    })
3240                }
3241            } else {
3242                None
3243            }
3244        };
3245
3246        if let Some(de) = doc_elem_idx {
3247            d.props.insert("documentElement".into(), Value::Object(Obj::dom(de)));
3248        } else {
3249            d.props.insert("documentElement".into(), Value::Null);
3250        }
3251
3252        d.props.insert(
3253            "getElementById".into(),
3254            nv("getElementById", parsed_doc_get_element_by_id),
3255        );
3256        d.props.insert(
3257            "querySelector".into(),
3258            nv("querySelector", parsed_doc_query_selector),
3259        );
3260        d.props.insert(
3261            "querySelectorAll".into(),
3262            nv("querySelectorAll", parsed_doc_query_selector_all),
3263        );
3264        d.props.insert(
3265            "getElementsByTagName".into(),
3266            nv("getElementsByTagName", parsed_doc_get_elements_by_tag_name),
3267        );
3268    }
3269    Ok(Value::Object(doc))
3270}
3271
3272/// `DOMParser` コンストラクタ。
3273pub fn dom_parser_ctor(_it: &mut Interp, _this: Value, _args: &[Value]) -> Result<Value, Value> {
3274    let obj = Obj::plain();
3275    obj.borrow_mut().props.insert(
3276        "parseFromString".into(),
3277        nv("parseFromString", dom_parser_parse_from_string),
3278    );
3279    Ok(Value::Object(obj))
3280}
3281
3282/// `form.reset()`(HTML5)の実装。
3283/// フォーム要素配下のすべての `<input>`, `<select>`, `<textarea>` のフォームライブ値を初期状態にリセットする。
3284pub fn dom_form_reset(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
3285    let Some(form_idx) = this_dom_idx(&this) else {
3286        return Ok(Value::Undefined);
3287    };
3288    let child_indices: alloc::vec::Vec<usize> = {
3289        let dom = it.dom.borrow();
3290        let mut list = alloc::vec::Vec::new();
3291        fn collect_descendants(dom: &crate::os_lib::js::dom_bridge::DomBridge, node_idx: usize, out: &mut alloc::vec::Vec<usize>) {
3292            if let Some(node) = dom.nodes.get(node_idx) {
3293                for &child in &node.children {
3294                    out.push(child);
3295                    collect_descendants(dom, child, out);
3296                }
3297            }
3298        }
3299        collect_descendants(&dom, form_idx, &mut list);
3300        list
3301    };
3302
3303    let mut dom = it.dom.borrow_mut();
3304    for idx in child_indices {
3305        let tag = dom.nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
3306        match tag.as_str() {
3307            "input" => {
3308                let ty = dom.get_attr(idx, "type").unwrap_or_default().to_lowercase();
3309                if ty == "checkbox" || ty == "radio" {
3310                    let default_checked = dom.has_attr(idx, "checked");
3311                    dom.set_attr(idx, "_live_checked", if default_checked { "true" } else { "false" });
3312                } else {
3313                    let default_val = dom.get_attr(idx, "value").unwrap_or_default();
3314                    dom.set_attr(idx, "_live_value", &default_val);
3315                }
3316            }
3317            "textarea" => {
3318                let default_val = dom.get_text_content(idx);
3319                dom.set_attr(idx, "_live_value", &default_val);
3320            }
3321            "select" => {
3322                dom.remove_attr(idx, "_live_value");
3323            }
3324            _ => {}
3325        }
3326    }
3327    Ok(Value::Undefined)
3328}
3329
3330/// `<canvas>` 2D context の `measureText(text)` メソッドの実装。
3331pub fn dom_canvas_measure_text(_it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3332    let text = args.first().map(|v| v.to_js_string()).unwrap_or_default();
3333    let width = (text.len() * 8) as f64;
3334    let obj = Obj::plain();
3335    {
3336        let mut b = obj.borrow_mut();
3337        b.props.insert("width".into(), Value::Number(width));
3338        b.props.insert("actualBoundingBoxAscent".into(), Value::Number(10.0));
3339        b.props.insert("actualBoundingBoxDescent".into(), Value::Number(2.0));
3340        b.props.insert("fontBoundingBoxAscent".into(), Value::Number(10.0));
3341        b.props.insert("fontBoundingBoxDescent".into(), Value::Number(2.0));
3342        b.props.insert("actualBoundingBoxLeft".into(), Value::Number(0.0));
3343        b.props.insert("actualBoundingBoxRight".into(), Value::Number(width));
3344    }
3345    Ok(Value::Object(obj))
3346}
3347
3348/// ImageDataSettings オブジェクトから colorSpace ("srgb" | "display-p3") を取り出す。
3349pub fn parse_image_data_color_space(val: Option<&Value>) -> &'static str {
3350    if let Some(Value::Object(obj)) = val {
3351        let b = obj.borrow();
3352        if let Some(Value::Str(s)) = b.props.get("colorSpace") {
3353            if s.eq_ignore_ascii_case("display-p3") {
3354                return "display-p3";
3355            }
3356        }
3357    }
3358    "srgb"
3359}
3360
3361/// width, height, data, colorSpace を持つ標準 ImageData オブジェクトを構築する。
3362pub fn make_image_data_object(
3363    w: usize,
3364    h: usize,
3365    data_items: alloc::vec::Vec<Value>,
3366    color_space: &str,
3367) -> Value {
3368    let img = Obj::plain();
3369    {
3370        let mut b = img.borrow_mut();
3371        b.props.insert("width".into(), Value::Number(w as f64));
3372        b.props.insert("height".into(), Value::Number(h as f64));
3373        b.props.insert("data".into(), Value::Object(Obj::array(data_items)));
3374        b.props.insert("colorSpace".into(), Value::Str(alloc::rc::Rc::new(color_space.into())));
3375    }
3376    Value::Object(img)
3377}
3378
3379/// `createImageData` は透明(全 0)の新しい `ImageData` を返す(仕様)。
3380///
3381/// 呼出形式:
3382/// 1. `ctx.createImageData(anotherImageData)`
3383/// 2. `ctx.createImageData(width, height, [settings])`
3384pub fn dom_canvas_create_image_data(
3385    _it: &mut Interp,
3386    _this: Value,
3387    args: &[Value],
3388) -> Result<Value, Value> {
3389    // パターン1: 別の ImageData オブジェクトが渡された場合
3390    if let Some(Value::Object(other)) = args.first() {
3391        let b = other.borrow();
3392        if let (Some(w_val), Some(h_val)) = (b.props.get("width"), b.props.get("height")) {
3393            let w = (w_val.to_number().abs() as usize).max(1);
3394            let h = (h_val.to_number().abs() as usize).max(1);
3395            let cs = b
3396                .props
3397                .get("colorSpace")
3398                .map(|v| v.to_js_string())
3399                .unwrap_or_else(|| "srgb".into());
3400            let items = alloc::vec![Value::Number(0.0); w * h * 4];
3401            return Ok(make_image_data_object(w, h, items, &cs));
3402        }
3403    }
3404
3405    // パターン2: width, height, [settings]
3406    let w = args.first().map(|v| v.to_number().abs() as usize).unwrap_or(1).max(1);
3407    let h = args.get(1).map(|v| v.to_number().abs() as usize).unwrap_or(1).max(1);
3408    let cs = parse_image_data_color_space(args.get(2));
3409    let items = alloc::vec![Value::Number(0.0); w * h * 4];
3410    Ok(make_image_data_object(w, h, items, cs))
3411}
3412
3413/// グローバル `ImageData` コンストラクタ (WHATWG HTML ImageData API)。
3414///
3415/// 呼出形式:
3416/// 1. `new ImageData(width, height, [settings])`
3417/// 2. `new ImageData(dataArray, width, [height], [settings])`
3418pub fn dom_image_data_ctor(
3419    _it: &mut Interp,
3420    _this: Value,
3421    args: &[Value],
3422) -> Result<Value, Value> {
3423    if let Some(Value::Object(arr_obj)) = args.first() {
3424        let b = arr_obj.borrow();
3425        if let ObjKind::Array(items) = &b.kind {
3426            let w = args.get(1).map(|v| v.to_number().abs() as usize).unwrap_or(1).max(1);
3427            let h = if let Some(hv) = args.get(2) {
3428                if let Value::Number(n) = hv {
3429                    (*n).abs() as usize
3430                } else {
3431                    items.len() / (w * 4)
3432                }
3433            } else {
3434                items.len() / (w * 4)
3435            }.max(1);
3436            let settings_arg = if args.len() > 3 { args.get(3) } else { args.get(2) };
3437            let cs = parse_image_data_color_space(settings_arg);
3438            return Ok(make_image_data_object(w, h, items.clone(), cs));
3439        }
3440    }
3441
3442    // width, height, [settings]
3443    let w = args.first().map(|v| v.to_number().abs() as usize).unwrap_or(1).max(1);
3444    let h = args.get(1).map(|v| v.to_number().abs() as usize).unwrap_or(1).max(1);
3445    let cs = parse_image_data_color_space(args.get(2));
3446    let items = alloc::vec![Value::Number(0.0); w * h * 4];
3447    Ok(make_image_data_object(w, h, items, cs))
3448}
3449
3450pub fn dom_canvas_is_point_in_path(_it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3451    let (subpaths, x, y, rule_str) = if let Some(pid) = args.first().and_then(path2d_id) {
3452        let subpaths = crate::os_lib::canvas2d::get_path2d_subpaths(pid).unwrap_or_default();
3453        let x = canvas_arg_f32(args, 1);
3454        let y = canvas_arg_f32(args, 2);
3455        let rule_str = args.get(3).map(|v| v.to_js_string());
3456        (subpaths, x, y, rule_str)
3457    } else {
3458        let ctx_id = canvas_ctx_id(&this);
3459        let subpaths = ctx_id
3460            .and_then(|id| crate::os_lib::canvas2d::with_context(id, |c| c.path.finish()))
3461            .unwrap_or_default();
3462        let x = canvas_arg_f32(args, 0);
3463        let y = canvas_arg_f32(args, 1);
3464        let rule_str = args.get(2).map(|v| v.to_js_string());
3465        (subpaths, x, y, rule_str)
3466    };
3467
3468    let rule = if rule_str
3469        .as_deref()
3470        .map(|s| s.eq_ignore_ascii_case("evenodd"))
3471        .unwrap_or(false)
3472    {
3473        crate::os_lib::canvas2d::FillRule::EvenOdd
3474    } else {
3475        crate::os_lib::canvas2d::FillRule::NonZero
3476    };
3477
3478    let inside = crate::os_lib::canvas2d::is_point_in_path(&subpaths, x, y, rule);
3479    Ok(Value::Bool(inside))
3480}
3481
3482pub fn dom_canvas_is_point_in_stroke(_it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3483    let ctx_id = canvas_ctx_id(&this);
3484
3485    let (subpaths, x, y) = if let Some(pid) = args.first().and_then(path2d_id) {
3486        let subpaths = crate::os_lib::canvas2d::get_path2d_subpaths(pid).unwrap_or_default();
3487        let x = canvas_arg_f32(args, 1);
3488        let y = canvas_arg_f32(args, 2);
3489        (subpaths, x, y)
3490    } else {
3491        let subpaths = ctx_id
3492            .and_then(|id| crate::os_lib::canvas2d::with_context(id, |c| c.path.finish()))
3493            .unwrap_or_default();
3494        let x = canvas_arg_f32(args, 0);
3495        let y = canvas_arg_f32(args, 1);
3496        (subpaths, x, y)
3497    };
3498
3499    let inside = if let Some(id) = ctx_id {
3500        crate::os_lib::canvas2d::with_context(id, |c| {
3501            canvas_sync_state(&this, c);
3502            c.is_point_in_stroke(&subpaths, x, y)
3503        })
3504        .unwrap_or(false)
3505    } else {
3506        crate::os_lib::canvas2d::is_point_in_stroke(&subpaths, x, y, 1.0)
3507    };
3508    Ok(Value::Bool(inside))
3509}
3510
3511pub fn dom_canvas_get_transform(it: &mut Interp, this: Value, _args: &[Value]) -> Result<Value, Value> {
3512    let (a, b, c, d, e, f) = canvas_ctx_id(&this)
3513        .and_then(|id| {
3514            crate::os_lib::canvas2d::with_context(id, |ctx| {
3515                (
3516                    ctx.state.transform.a,
3517                    ctx.state.transform.b,
3518                    ctx.state.transform.c,
3519                    ctx.state.transform.d,
3520                    ctx.state.transform.e,
3521                    ctx.state.transform.f,
3522                )
3523            })
3524        })
3525        .unwrap_or((1.0, 0.0, 0.0, 1.0, 0.0, 0.0));
3526
3527    let nums = alloc::vec![
3528        Value::Number(a as f64),
3529        Value::Number(b as f64),
3530        Value::Number(c as f64),
3531        Value::Number(d as f64),
3532        Value::Number(e as f64),
3533        Value::Number(f as f64),
3534    ];
3535    let arr = Value::Object(Obj::array(nums));
3536    dom_matrix_ctor(it, Value::Undefined, &[arr])
3537}
3538
3539pub fn dom_canvas_set_line_dash(_it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3540    if let Value::Object(o) = &this {
3541        let segs = args.first().cloned().unwrap_or(Value::Undefined);
3542        let items = this_items(&segs);
3543        let mut nums: alloc::vec::Vec<Value> = items.into_iter().map(|v| Value::Number(v.to_number())).collect();
3544        if nums.len() % 2 != 0 {
3545            let dup = nums.clone();
3546            nums.extend(dup);
3547        }
3548        o.borrow_mut().props.insert("_line_dash".into(), Value::Object(Obj::array(nums)));
3549    }
3550    Ok(Value::Undefined)
3551}
3552
3553pub fn dom_canvas_get_line_dash(_it: &mut Interp, this: Value, _args: &[Value]) -> Result<Value, Value> {
3554    if let Value::Object(o) = &this {
3555        if let Some(Value::Object(arr)) = o.borrow().props.get("_line_dash") {
3556            let items = this_items(&Value::Object(arr.clone()));
3557            return Ok(Value::Object(Obj::array(items)));
3558        }
3559    }
3560    Ok(Value::Object(Obj::array(alloc::vec![])))
3561}
3562
3563pub fn dom_canvas_round_rect(it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3564    canvas_round_rect(it, this, args)
3565}
3566
3567/// `<canvas>` 2D context の `reset()` メソッド (WHATWG HTML Standard)。
3568pub fn dom_canvas_reset(_it: &mut Interp, this: Value, _args: &[Value]) -> Result<Value, Value> {
3569    if let Value::Object(ref ctx) = this {
3570        let mut b = ctx.borrow_mut();
3571        b.props.insert("fillStyle".into(), Value::str("#000000"));
3572        b.props.insert("strokeStyle".into(), Value::str("#000000"));
3573        b.props.insert("lineWidth".into(), Value::Number(1.0));
3574        b.props.insert("lineCap".into(), Value::str("butt"));
3575        b.props.insert("lineJoin".into(), Value::str("miter"));
3576        b.props.insert("miterLimit".into(), Value::Number(10.0));
3577        b.props.insert("lineDashOffset".into(), Value::Number(0.0));
3578        b.props.insert("font".into(), Value::str("10px sans-serif"));
3579        b.props.insert("textAlign".into(), Value::str("start"));
3580        b.props.insert("textBaseline".into(), Value::str("alphabetic"));
3581        b.props.insert("globalAlpha".into(), Value::Number(1.0));
3582        b.props.insert("globalCompositeOperation".into(), Value::str("source-over"));
3583        b.props.insert("shadowBlur".into(), Value::Number(0.0));
3584        b.props.insert("shadowColor".into(), Value::str("rgba(0, 0, 0, 0)"));
3585        b.props.insert("shadowOffsetX".into(), Value::Number(0.0));
3586        b.props.insert("shadowOffsetY".into(), Value::Number(0.0));
3587        b.props.insert("imageSmoothingEnabled".into(), Value::Bool(true));
3588        b.props.insert("direction".into(), Value::str("inherit"));
3589        b.props.insert("filter".into(), Value::str("none"));
3590        b.props.shift_remove("_line_dash");
3591    }
3592    Ok(Value::Undefined)
3593}
3594
3595fn dom_matrix_arg_to_matrix(it: &mut Interp, val: &Value) -> Option<crate::os_lib::canvas2d::Matrix> {
3596    if let Value::Object(_) = val {
3597        let a = it.get_property(val, "a").ok().map(|v| v.to_number()).unwrap_or(1.0);
3598        let b = it.get_property(val, "b").ok().map(|v| v.to_number()).unwrap_or(0.0);
3599        let c = it.get_property(val, "c").ok().map(|v| v.to_number()).unwrap_or(0.0);
3600        let d = it.get_property(val, "d").ok().map(|v| v.to_number()).unwrap_or(1.0);
3601        let e = it.get_property(val, "e").ok().map(|v| v.to_number()).unwrap_or(0.0);
3602        let f = it.get_property(val, "f").ok().map(|v| v.to_number()).unwrap_or(0.0);
3603
3604        if a.is_finite() && b.is_finite() && c.is_finite() && d.is_finite() && e.is_finite() && f.is_finite() {
3605            Some(crate::os_lib::canvas2d::Matrix {
3606                a: a as f32,
3607                b: b as f32,
3608                c: c as f32,
3609                d: d as f32,
3610                e: e as f32,
3611                f: f as f32,
3612            })
3613        } else {
3614            None
3615        }
3616    } else {
3617        None
3618    }
3619}
3620
3621pub fn dom_path2d_add_path(it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3622    if let Some(id) = path2d_id(&this) {
3623        if let Some(src_arg) = args.first() {
3624            if let Some(src_id) = path2d_id(src_arg) {
3625                let transform = args.get(1).and_then(|v| dom_matrix_arg_to_matrix(it, v));
3626                crate::os_lib::canvas2d::add_path_to_path2d(id, src_id, transform.as_ref());
3627            }
3628        }
3629    }
3630    Ok(Value::Undefined)
3631}
3632
3633pub fn dom_path2d_close_path(_it: &mut Interp, this: Value, _args: &[Value]) -> Result<Value, Value> {
3634    if let Some(id) = path2d_id(&this) {
3635        crate::os_lib::canvas2d::with_path2d(id, |p| p.close_path());
3636    }
3637    Ok(Value::Undefined)
3638}
3639
3640pub fn dom_path2d_move_to(_it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3641    if let Some(id) = path2d_id(&this) {
3642        let (x, y) = (canvas_arg_f32(args, 0), canvas_arg_f32(args, 1));
3643        crate::os_lib::canvas2d::with_path2d(id, |p| p.move_to(&crate::os_lib::canvas2d::Matrix::identity(), x, y));
3644    }
3645    Ok(Value::Undefined)
3646}
3647
3648pub fn dom_path2d_line_to(_it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3649    if let Some(id) = path2d_id(&this) {
3650        let (x, y) = (canvas_arg_f32(args, 0), canvas_arg_f32(args, 1));
3651        crate::os_lib::canvas2d::with_path2d(id, |p| p.line_to(&crate::os_lib::canvas2d::Matrix::identity(), x, y));
3652    }
3653    Ok(Value::Undefined)
3654}
3655
3656pub fn dom_path2d_arc(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3657    if let Some(id) = path2d_id(&this) {
3658        let (cx, cy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3659        let r = canvas_arg_f32(a, 2);
3660        let start = canvas_arg_f32(a, 3);
3661        let end = canvas_arg_f32(a, 4);
3662        let ccw = a.get(5).map(|v| v.truthy()).unwrap_or(false);
3663        crate::os_lib::canvas2d::with_path2d(id, |p| {
3664            p.arc(&crate::os_lib::canvas2d::Matrix::identity(), cx, cy, r, start, end, ccw);
3665        });
3666    }
3667    Ok(Value::Undefined)
3668}
3669
3670pub fn dom_path2d_arc_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3671    if let Some(id) = path2d_id(&this) {
3672        let (x1, y1) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3673        let (x2, y2) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3674        let radius = canvas_arg_f32(a, 4);
3675        crate::os_lib::canvas2d::with_path2d(id, |p| {
3676            p.arc_to(&crate::os_lib::canvas2d::Matrix::identity(), x1, y1, x2, y2, radius);
3677        });
3678    }
3679    Ok(Value::Undefined)
3680}
3681
3682pub fn dom_path2d_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3683    if let Some(id) = path2d_id(&this) {
3684        let (x, y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3685        let (w, h) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3686        crate::os_lib::canvas2d::with_path2d(id, |p| {
3687            p.rect(&crate::os_lib::canvas2d::Matrix::identity(), x, y, w, h);
3688        });
3689    }
3690    Ok(Value::Undefined)
3691}
3692
3693/// `roundRect` の `radii` 引数をパース・検証する。
3694/// W3C HTML Canvas 2D Context 仕様準拠:
3695/// - 負の半径指定時は `RangeError`
3696/// - 配列長が 0 または 5 以上の時は `RangeError`
3697/// - 1〜4 個の半径は CSS `border-radius` と同様に 4 隅へ補完
3698fn parse_round_rect_radii(it: &Interp, radii_val: Option<&Value>) -> Result<[f32; 4], Value> {
3699    match radii_val {
3700        None | Some(Value::Undefined) => Ok([0.0; 4]),
3701        Some(Value::Object(o)) => {
3702            let b = o.borrow();
3703            if let ObjKind::Array(items) = &b.kind {
3704                if items.is_empty() || items.len() > 4 {
3705                    return Err(set_error_props(
3706                        it,
3707                        Value::Undefined,
3708                        alloc::format!(
3709                            "Failed to execute 'roundRect': 1 to 4 radii expected, got {}",
3710                            items.len()
3711                        ),
3712                        "RangeError",
3713                    ));
3714                }
3715                let mut vals = alloc::vec::Vec::with_capacity(items.len());
3716                for item in items {
3717                    let n = item.to_number();
3718                    let f = if n.is_finite() { n as f32 } else { 0.0 };
3719                    if f < 0.0 {
3720                        return Err(set_error_props(
3721                            it,
3722                            Value::Undefined,
3723                            alloc::format!(
3724                                "Failed to execute 'roundRect': radius must be non-negative, got {f}"
3725                            ),
3726                            "RangeError",
3727                        ));
3728                    }
3729                    vals.push(f);
3730                }
3731                let radii = match vals.len() {
3732                    1 => [vals[0]; 4],
3733                    2 => [vals[0], vals[1], vals[0], vals[1]],
3734                    3 => [vals[0], vals[1], vals[2], vals[1]],
3735                    4 => [vals[0], vals[1], vals[2], vals[3]],
3736                    _ => [0.0; 4],
3737                };
3738                Ok(radii)
3739            } else {
3740                let n = radii_val.map_or(f64::NAN, |v| v.to_number());
3741                let f = if n.is_finite() { n as f32 } else { 0.0 };
3742                if f < 0.0 {
3743                    return Err(set_error_props(
3744                        it,
3745                        Value::Undefined,
3746                        alloc::format!(
3747                            "Failed to execute 'roundRect': radius must be non-negative, got {f}"
3748                        ),
3749                        "RangeError",
3750                    ));
3751                }
3752                Ok([f; 4])
3753            }
3754        }
3755        Some(v) => {
3756            let n = v.to_number();
3757            let f = if n.is_finite() { n as f32 } else { 0.0 };
3758            if f < 0.0 {
3759                return Err(set_error_props(
3760                    it,
3761                    Value::Undefined,
3762                    alloc::format!(
3763                        "Failed to execute 'roundRect': radius must be non-negative, got {f}"
3764                    ),
3765                    "RangeError",
3766                ));
3767            }
3768            Ok([f; 4])
3769        }
3770    }
3771}
3772
3773pub fn dom_path2d_round_rect(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3774    if let Some(id) = path2d_id(&this) {
3775        let (x, y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3776        let (w, h) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3777        let radii = parse_round_rect_radii(it, a.get(4))?;
3778        crate::os_lib::canvas2d::with_path2d(id, |p| {
3779            p.round_rect(&crate::os_lib::canvas2d::Matrix::identity(), x, y, w, h, radii);
3780        });
3781    }
3782    Ok(Value::Undefined)
3783}
3784
3785pub fn dom_path2d_ellipse(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3786    if let Some(id) = path2d_id(&this) {
3787        let (cx, cy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3788        let (rx, ry) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3789        let rot = canvas_arg_f32(a, 4);
3790        let start = canvas_arg_f32(a, 5);
3791        let end = canvas_arg_f32(a, 6);
3792        let ccw = a.get(7).map(|v| v.truthy()).unwrap_or(false);
3793        crate::os_lib::canvas2d::with_path2d(id, |p| {
3794            p.ellipse(&crate::os_lib::canvas2d::Matrix::identity(), cx, cy, rx, ry, rot, start, end, ccw);
3795        });
3796    }
3797    Ok(Value::Undefined)
3798}
3799
3800pub fn dom_path2d_bezier_curve_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3801    if let Some(id) = path2d_id(&this) {
3802        let (c1x, c1y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3803        let (c2x, c2y) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3804        let (x, y) = (canvas_arg_f32(a, 4), canvas_arg_f32(a, 5));
3805        crate::os_lib::canvas2d::with_path2d(id, |p| {
3806            p.bezier_curve_to(&crate::os_lib::canvas2d::Matrix::identity(), c1x, c1y, c2x, c2y, x, y);
3807        });
3808    }
3809    Ok(Value::Undefined)
3810}
3811
3812pub fn dom_path2d_quadratic_curve_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3813    if let Some(id) = path2d_id(&this) {
3814        let (cx, cy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3815        let (x, y) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3816        crate::os_lib::canvas2d::with_path2d(id, |p| {
3817            p.quadratic_curve_to(&crate::os_lib::canvas2d::Matrix::identity(), cx, cy, x, y);
3818        });
3819    }
3820    Ok(Value::Undefined)
3821}
3822
3823/// `Path2D` コンストラクタ (WHATWG HTML Canvas 2D Path2D API)。
3824pub fn dom_path2d_ctor(_it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3825    let p = Obj::plain();
3826    let id = if let Some(arg) = args.first() {
3827        if let Some(src_id) = path2d_id(arg) {
3828            crate::os_lib::canvas2d::clone_path2d(src_id)
3829        } else if let Value::Str(s) = arg {
3830            crate::os_lib::canvas2d::create_path2d(Some(s))
3831        } else if !matches!(arg, Value::Undefined | Value::Null) {
3832            let s = arg.to_js_string();
3833            crate::os_lib::canvas2d::create_path2d(Some(&s))
3834        } else {
3835            crate::os_lib::canvas2d::create_path2d(None)
3836        }
3837    } else {
3838        crate::os_lib::canvas2d::create_path2d(None)
3839    };
3840    {
3841        let mut b = p.borrow_mut();
3842        b.props.insert("_path2d_id".into(), Value::Number(id as f64));
3843        b.props.insert("addPath".into(), nv("addPath", dom_path2d_add_path));
3844        b.props.insert("closePath".into(), nv("closePath", dom_path2d_close_path));
3845        b.props.insert("moveTo".into(), nv("moveTo", dom_path2d_move_to));
3846        b.props.insert("lineTo".into(), nv("lineTo", dom_path2d_line_to));
3847        b.props.insert("arc".into(), nv("arc", dom_path2d_arc));
3848        b.props.insert("arcTo".into(), nv("arcTo", dom_path2d_arc_to));
3849        b.props.insert("rect".into(), nv("rect", dom_path2d_rect));
3850        b.props.insert("roundRect".into(), nv("roundRect", dom_path2d_round_rect));
3851        b.props.insert("ellipse".into(), nv("ellipse", dom_path2d_ellipse));
3852        b.props.insert("bezierCurveTo".into(), nv("bezierCurveTo", dom_path2d_bezier_curve_to));
3853        b.props.insert("quadraticCurveTo".into(), nv("quadraticCurveTo", dom_path2d_quadratic_curve_to));
3854    }
3855    Ok(Value::Object(p))
3856}
3857
3858/// `<canvas>` 2D context の `createLinearGradient(x0, y0, x1, y1)`。
3859pub fn dom_canvas_create_linear_gradient(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
3860    let x0 = canvas_arg_f32(a, 0);
3861    let y0 = canvas_arg_f32(a, 1);
3862    let x1 = canvas_arg_f32(a, 2);
3863    let y1 = canvas_arg_f32(a, 3);
3864    let id = crate::os_lib::canvas2d::create_linear_gradient(x0, y0, x1, y1);
3865    let grad = Obj::plain();
3866    {
3867        let mut b = grad.borrow_mut();
3868        b.props.insert("_gradient_id".into(), Value::Number(id as f64));
3869        b.props.insert("addColorStop".into(), nv("addColorStop", dom_gradient_add_color_stop));
3870    }
3871    Ok(Value::Object(grad))
3872}
3873
3874/// `<canvas>` 2D context の `createRadialGradient(x0, y0, r0, x1, y1, r1)`。
3875pub fn dom_canvas_create_radial_gradient(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
3876    let x0 = canvas_arg_f32(a, 0);
3877    let y0 = canvas_arg_f32(a, 1);
3878    let r0 = canvas_arg_f32(a, 2);
3879    let x1 = canvas_arg_f32(a, 3);
3880    let y1 = canvas_arg_f32(a, 4);
3881    let r1 = canvas_arg_f32(a, 5);
3882    let id = crate::os_lib::canvas2d::create_radial_gradient(x0, y0, r0, x1, y1, r1);
3883    let grad = Obj::plain();
3884    {
3885        let mut b = grad.borrow_mut();
3886        b.props.insert("_gradient_id".into(), Value::Number(id as f64));
3887        b.props.insert("addColorStop".into(), nv("addColorStop", dom_gradient_add_color_stop));
3888    }
3889    Ok(Value::Object(grad))
3890}
3891
3892/// `CanvasGradient.prototype.addColorStop(offset, color)`。
3893pub fn dom_gradient_add_color_stop(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3894    let id = match &this {
3895        Value::Object(o) => {
3896            let b = o.borrow();
3897            b.props.get("_gradient_id").and_then(|v| match v {
3898                Value::Number(n) => Some(*n as u32),
3899                _ => None,
3900            })
3901        }
3902        _ => None,
3903    };
3904    let Some(id) = id else {
3905        return Ok(Value::Undefined);
3906    };
3907    let offset = canvas_arg_f32(a, 0);
3908    let color_str = a.get(1).map(|v| v.to_js_string()).unwrap_or_default();
3909    let color = crate::os_lib::css::parse_color(&color_str).unwrap_or(0xFF00_0000);
3910    crate::os_lib::canvas2d::add_color_stop_to_gradient(id, offset, color);
3911    Ok(Value::Undefined)
3912}
3913
3914/// `<canvas>` 2D context の `createConicGradient(startAngle, x, y)`。
3915pub fn dom_canvas_create_conic_gradient(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
3916    let start_angle = canvas_arg_f32(a, 0);
3917    let cx = canvas_arg_f32(a, 1);
3918    let cy = canvas_arg_f32(a, 2);
3919    let id = crate::os_lib::canvas2d::create_conic_gradient(start_angle, cx, cy);
3920    let grad = Obj::plain();
3921    {
3922        let mut b = grad.borrow_mut();
3923        b.props.insert("_gradient_id".into(), Value::Number(id as f64));
3924        b.props.insert("addColorStop".into(), nv("addColorStop", dom_gradient_add_color_stop));
3925    }
3926    Ok(Value::Object(grad))
3927}
3928
3929/// `createPattern` の引数(ImageData, HTMLCanvasElement, HTMLImageElement)からピクセルデータを抽出する。
3930fn extract_pattern_pixels(it: &mut Interp, v: &Value) -> Option<(u32, u32, alloc::vec::Vec<u32>)> {
3931    let Value::Object(o) = v else {
3932        return None;
3933    };
3934
3935    // 1. ImageData か? (width, height, data を持つ)
3936    {
3937        let b = o.borrow();
3938        if let (Some(Value::Number(w_num)), Some(Value::Number(h_num)), Some(Value::Object(data_arr))) =
3939            (b.props.get("width"), b.props.get("height"), b.props.get("data"))
3940        {
3941            let w = *w_num as u32;
3942            let h = *h_num as u32;
3943            if w > 0 && h > 0 {
3944                let ab = data_arr.borrow();
3945                // Uint8ClampedArray(記憶域)も受け付ける。
3946                let items_owned: alloc::vec::Vec<Value> = match &ab.kind {
3947                    ObjKind::Bytes(st) => st.values(),
3948                    _ => alloc::vec::Vec::new(),
3949                };
3950                let items_ref = match &ab.kind {
3951                    ObjKind::Array(items) => Some(items),
3952                    ObjKind::Bytes(_) => Some(&items_owned),
3953                    _ => None,
3954                };
3955                if let Some(items) = items_ref {
3956                    let total = (w * h) as usize;
3957                    let mut pixels = alloc::vec::Vec::with_capacity(total);
3958                    for chunk in items.chunks(4) {
3959                        if chunk.len() == 4 {
3960                            let r = chunk[0].to_number().clamp(0.0, 255.0) as u32;
3961                            let g = chunk[1].to_number().clamp(0.0, 255.0) as u32;
3962                            let b = chunk[2].to_number().clamp(0.0, 255.0) as u32;
3963                            let a = chunk[3].to_number().clamp(0.0, 255.0) as u32;
3964                            pixels.push((a << 24) | (r << 16) | (g << 8) | b);
3965                        } else {
3966                            pixels.push(0);
3967                        }
3968                    }
3969                    if pixels.len() == total {
3970                        return Some((w, h, pixels));
3971                    }
3972                }
3973            }
3974        }
3975    }
3976
3977    // 2. DOM 要素(<img> または <canvas>)か?
3978    let idx = match o.borrow().kind {
3979        ObjKind::DomElement(i) => Some(i),
3980        _ => None,
3981    }?;
3982    let (tag, src, cid) = {
3983        let dom = it.dom.borrow();
3984        let tag = dom.nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
3985        let src = dom.get_attr(idx, "src").unwrap_or_default();
3986        let cid = dom
3987            .get_attr(idx, "_c2d_id")
3988            .and_then(|s| s.parse::<u32>().ok());
3989        (tag, src, cid)
3990    };
3991
3992    if tag == "canvas" {
3993        let id = cid?;
3994        return crate::os_lib::canvas2d::with_context(id, |c| {
3995            let (w, h) = (c.surface.width, c.surface.height);
3996            if w == 0 || h == 0 {
3997                None
3998            } else {
3999                Some((w, h, c.surface.pixels.clone()))
4000            }
4001        }).flatten();
4002    }
4003
4004    if let Some(img) = crate::os_lib::web_engine::shared_image(&src) {
4005        let (w, h) = (img.width, img.height);
4006        if w > 0 && h > 0 {
4007            let mut pixels = alloc::vec::Vec::with_capacity((w * h) as usize);
4008            for chunk in img.rgba.chunks_exact(4) {
4009                let r = chunk[0] as u32;
4010                let g = chunk[1] as u32;
4011                let b = chunk[2] as u32;
4012                let a = chunk[3] as u32;
4013                pixels.push((a << 24) | (r << 16) | (g << 8) | b);
4014            }
4015            return Some((w, h, pixels));
4016        }
4017    }
4018
4019    None
4020}
4021
4022/// `pattern.setTransform(matrix)`。
4023pub fn dom_pattern_set_transform(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4024    let Value::Object(o) = &this else {
4025        return Ok(Value::Undefined);
4026    };
4027    let pid = match o.borrow().props.get("_pattern_id") {
4028        Some(Value::Number(n)) if *n >= 0.0 => *n as u32,
4029        _ => return Ok(Value::Undefined),
4030    };
4031    let matrix = if let Some(arg) = a.first() {
4032        dom_matrix_arg_to_matrix(it, arg).unwrap_or_else(crate::os_lib::canvas2d::Matrix::identity)
4033    } else {
4034        crate::os_lib::canvas2d::Matrix::identity()
4035    };
4036    crate::os_lib::canvas2d::set_pattern_transform(pid, matrix);
4037    Ok(Value::Undefined)
4038}
4039
4040/// `<canvas>` 2D context の `createPattern(image, repetition)`。
4041pub fn dom_canvas_create_pattern(it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
4042    let Some(image_arg) = a.first() else {
4043        return Ok(Value::Null);
4044    };
4045    let Some((w, h, pixels)) = extract_pattern_pixels(it, image_arg) else {
4046        return Ok(Value::Null);
4047    };
4048    let rep_str = a.get(1).map(|v| v.to_js_string()).unwrap_or_else(|| String::from("repeat"));
4049    let repetition = crate::os_lib::canvas2d::parse_repetition(&rep_str);
4050    let id = crate::os_lib::canvas2d::create_pattern(w, h, pixels, repetition);
4051
4052    let pat = Obj::plain();
4053    {
4054        let mut b = pat.borrow_mut();
4055        b.props.insert("_pattern_id".into(), Value::Number(id as f64));
4056        b.props.insert("setTransform".into(), nv("setTransform", dom_pattern_set_transform));
4057    }
4058    Ok(Value::Object(pat))
4059}
4060
4061/// `<canvas>` 要素の `getContext(contextId, options?)` (HTML5 Canvas 2D Context API)。
4062pub fn dom_canvas_get_context(_it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
4063    let ctx_type = args.first().map(|v| v.to_js_string()).unwrap_or_default();
4064    if ctx_type.eq_ignore_ascii_case("2d") {
4065        // 面のサイズは `<canvas width height>` 属性から取る。
4066        // 属性が無い場合の既定は仕様どおり 300x150。
4067        // `this` はこの後 props へ move されるので、その前に読む。
4068        let (cw, ch) = match &this {
4069            Value::Object(o) => {
4070                let kind = o.borrow().kind.clone();
4071                if let ObjKind::DomElement(idx) = kind {
4072                    let d = _it.dom.borrow();
4073                    let w = d
4074                        .get_attr(idx, "width")
4075                        .and_then(|s| s.trim().parse::<u32>().ok())
4076                        .unwrap_or(300);
4077                    let h = d
4078                        .get_attr(idx, "height")
4079                        .and_then(|s| s.trim().parse::<u32>().ok())
4080                        .unwrap_or(150);
4081                    (w, h)
4082                } else {
4083                    (300, 150)
4084                }
4085            }
4086            _ => (300, 150),
4087        };
4088        let c2d_id = crate::os_lib::canvas2d::create_context(cw, ch);
4089        // レイアウト側(`web_engine/layout.rs`)は `StyledNode` しか見ないので、
4090        // id を DOM 属性へ書いて橋渡しする。JS のオブジェクトを直接引かせると
4091        // 層をまたぐ依存になる。
4092        if let (Some(id), Value::Object(o)) = (c2d_id, &this) {
4093            let kind = o.borrow().kind.clone();
4094            if let ObjKind::DomElement(idx) = kind {
4095                _it.dom
4096                    .borrow_mut()
4097                    .set_attr(idx, "_c2d_id", &alloc::format!("{}", id));
4098            }
4099        }
4100        let ctx = Obj::plain();
4101        {
4102            let mut b = ctx.borrow_mut();
4103            b.props.insert("canvas".into(), this);
4104            b.props.insert("fillStyle".into(), Value::str("#000000"));
4105            b.props.insert("strokeStyle".into(), Value::str("#000000"));
4106            b.props.insert("lineWidth".into(), Value::Number(1.0));
4107            b.props.insert("lineCap".into(), Value::str("butt"));
4108            b.props.insert("lineJoin".into(), Value::str("miter"));
4109            b.props.insert("miterLimit".into(), Value::Number(10.0));
4110            b.props.insert("lineDashOffset".into(), Value::Number(0.0));
4111            b.props.insert("font".into(), Value::str("10px sans-serif"));
4112            b.props.insert("textAlign".into(), Value::str("start"));
4113            b.props.insert("textBaseline".into(), Value::str("alphabetic"));
4114            b.props.insert("globalAlpha".into(), Value::Number(1.0));
4115            b.props.insert("globalCompositeOperation".into(), Value::str("source-over"));
4116            b.props.insert("shadowBlur".into(), Value::Number(0.0));
4117            b.props.insert("shadowColor".into(), Value::str("rgba(0, 0, 0, 0)"));
4118            b.props.insert("shadowOffsetX".into(), Value::Number(0.0));
4119            b.props.insert("shadowOffsetY".into(), Value::Number(0.0));
4120            b.props.insert("imageSmoothingEnabled".into(), Value::Bool(true));
4121            b.props.insert("direction".into(), Value::str("inherit"));
4122            b.props.insert("filter".into(), Value::str("none"));
4123            // 【2026-09-03】実描画へ接続する。従来これらは全て `dom_noop` で、
4124            // API 表面だけが存在し canvas は一切描画されなかった。
4125            // 幾何・ラスタライズは `os_lib/canvas2d.rs`(試験済み)にあり、
4126            // ここは registry の id を持たせて各メソッドから引くだけにする。
4127            if let Some(id) = c2d_id {
4128                b.props.insert("_c2d_id".into(), Value::Number(id as f64));
4129            }
4130
4131            // 【2026-09-03】実描画へ接続する。
4132            //
4133            // 従来これらは全て `dom_noop` で、API 表面だけが存在し
4134            // canvas は一切描画されなかった。`os_lib/canvas2d.rs` の
4135            // `Canvas2dContext`(試験済み)を registry へ作り、その id を
4136            // `_c2d_id` として持たせて各メソッドから引く。
4137            //
4138            b.props.insert("fillRect".into(), nv("fillRect", canvas_fill_rect));
4139            b.props.insert("strokeRect".into(), nv("strokeRect", canvas_stroke_rect));
4140            b.props.insert("clearRect".into(), nv("clearRect", canvas_clear_rect));
4141
4142            b.props.insert("beginPath".into(), nv("beginPath", canvas_begin_path));
4143            b.props.insert("closePath".into(), nv("closePath", canvas_close_path));
4144            b.props.insert("moveTo".into(), nv("moveTo", canvas_move_to));
4145            b.props.insert("lineTo".into(), nv("lineTo", canvas_line_to));
4146            b.props.insert("arc".into(), nv("arc", canvas_arc));
4147            b.props.insert("arcTo".into(), nv("arcTo", canvas_arc_to));
4148            b.props.insert("bezierCurveTo".into(), nv("bezierCurveTo", canvas_bezier_curve_to));
4149            b.props.insert("quadraticCurveTo".into(), nv("quadraticCurveTo", canvas_quadratic_curve_to));
4150            b.props.insert("ellipse".into(), nv("ellipse", canvas_ellipse));
4151            b.props.insert("rect".into(), nv("rect", canvas_rect));
4152            b.props.insert("roundRect".into(), nv("roundRect", canvas_round_rect));
4153
4154            b.props.insert("fill".into(), nv("fill", canvas_fill));
4155            b.props.insert("stroke".into(), nv("stroke", canvas_stroke));
4156            b.props.insert("clip".into(), nv("clip", canvas_clip));
4157
4158            b.props.insert("fillText".into(), nv("fillText", canvas_fill_text));
4159            b.props.insert("strokeText".into(), nv("strokeText", canvas_stroke_text));
4160            b.props.insert("measureText".into(), nv("measureText", canvas_measure_text));
4161
4162            b.props.insert("drawImage".into(), nv("drawImage", canvas_draw_image));
4163            b.props.insert("getImageData".into(), nv("getImageData", canvas_get_image_data));
4164            b.props.insert("createImageData".into(), nv("createImageData", dom_canvas_create_image_data));
4165            b.props.insert("putImageData".into(), nv("putImageData", canvas_put_image_data));
4166            b.props.insert("isPointInPath".into(), nv("isPointInPath", dom_canvas_is_point_in_path));
4167            b.props.insert("isPointInStroke".into(), nv("isPointInStroke", dom_canvas_is_point_in_stroke));
4168            b.props.insert("getTransform".into(), nv("getTransform", dom_canvas_get_transform));
4169            b.props.insert("setLineDash".into(), nv("setLineDash", dom_canvas_set_line_dash));
4170            b.props.insert("getLineDash".into(), nv("getLineDash", dom_canvas_get_line_dash));
4171            b.props.insert("reset".into(), nv("reset", dom_canvas_reset));
4172
4173            b.props.insert("createLinearGradient".into(), nv("createLinearGradient", dom_canvas_create_linear_gradient));
4174            b.props.insert("createRadialGradient".into(), nv("createRadialGradient", dom_canvas_create_radial_gradient));
4175            b.props.insert("createConicGradient".into(), nv("createConicGradient", dom_canvas_create_conic_gradient));
4176            b.props.insert("createPattern".into(), nv("createPattern", dom_canvas_create_pattern));
4177
4178            b.props.insert("save".into(), nv("save", canvas_save));
4179            b.props.insert("restore".into(), nv("restore", canvas_restore));
4180            b.props.insert("scale".into(), nv("scale", canvas_scale));
4181            b.props.insert("rotate".into(), nv("rotate", canvas_rotate));
4182            b.props.insert("translate".into(), nv("translate", canvas_translate));
4183            b.props.insert("transform".into(), nv("transform", canvas_transform));
4184            b.props.insert("setTransform".into(), nv("setTransform", canvas_set_transform));
4185            b.props.insert("resetTransform".into(), nv("resetTransform", canvas_reset_transform));
4186        }
4187        return Ok(Value::Object(ctx));
4188    }
4189    // webgl/webgl2/bitmaprenderer は未実装なので、ブラウザが対応しない場合と同じく null を返す。
4190    // 【2026-10-17】従来はメソッドの無い空オブジェクトを返しており、機能判定(`ctx && ctx.getExtension(...)`)
4191    // が「対応している」と誤認して落ちていた(YouTube player の `getDebugText`)。
4192    Ok(Value::Null)
4193}
4194
4195/// `<canvas>` 要素の `toDataURL(type, quality)` (HTML5 Canvas)。
4196pub fn dom_canvas_to_data_url(_it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
4197    let mime = args.first().map(|v| v.to_js_string()).unwrap_or_else(|| String::from("image/png"));
4198    let header = if mime.eq_ignore_ascii_case("image/jpeg") {
4199        "data:image/jpeg;base64,"
4200    } else if mime.eq_ignore_ascii_case("image/webp") {
4201        "data:image/webp;base64,"
4202    } else {
4203        "data:image/png;base64,"
4204    };
4205    // 1x1 透明 PNG の最小限データ URL
4206    let dummy_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
4207    Ok(Value::str(alloc::format!("{}{}", header, dummy_base64)))
4208}
4209
4210/// `<canvas>` 要素の `toBlob(callback, type, quality)` (HTML5 Canvas)。
4211pub fn dom_canvas_to_blob(it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
4212    let cb = args.first().cloned().unwrap_or(Value::Undefined);
4213    let mime = args.get(1).map(|v| v.to_js_string()).unwrap_or_else(|| String::from("image/png"));
4214    if matches!(cb, Value::Object(_)) {
4215        let blob = Obj::plain();
4216        {
4217            let mut b = blob.borrow_mut();
4218            b.props.insert("size".into(), Value::Number(68.0));
4219            b.props.insert("type".into(), Value::str(mime));
4220        }
4221        it.call_value(&cb, Value::Undefined, &[Value::Object(blob)])?;
4222    }
4223    Ok(Value::Undefined)
4224}
4225
4226
4227// ───────────── Canvas 2D の実描画(2026-09-03 追加) ─────────────
4228//
4229// これらは以前すべて `dom_noop` に接続されており、API 表面だけが存在して
4230// 一切描画されなかった。幾何とラスタライズは `os_lib/canvas2d.rs`
4231// (純粋モジュール・試験済み)にあり、ここは「JS の値を取り出して
4232// コンテキストへ渡す」薄い層に徹する。
4233
4234/// Path2D オブジェクトから registry の id を取り出す。
4235fn path2d_id(this: &Value) -> Option<u32> {
4236    match this {
4237        Value::Object(o) => match o.borrow().props.get("_path2d_id") {
4238            Some(Value::Number(n)) if *n >= 0.0 => Some(*n as u32),
4239            _ => None,
4240        },
4241        _ => None,
4242    }
4243}
4244
4245/// コンテキストオブジェクトから registry の id を取り出す。
4246fn canvas_ctx_id(this: &Value) -> Option<u32> {
4247    match this {
4248        Value::Object(o) => match o.borrow().props.get("_c2d_id") {
4249            Some(Value::Number(n)) if *n >= 0.0 => Some(*n as u32),
4250            _ => None,
4251        },
4252        _ => None,
4253    }
4254}
4255
4256/// 引数を f32 で取る。非有限値は 0 とみなす(描画系でパニックさせない)。
4257fn canvas_arg_f32(a: &[Value], i: usize) -> f32 {
4258    let v = a.get(i).map(|v| v.to_number()).unwrap_or(0.0);
4259    if v.is_finite() {
4260        v as f32
4261    } else {
4262        0.0
4263    }
4264}
4265
4266/// JS 側のスタイル文字列を ARGB へ解決する。
4267///
4268/// CSS の色解析(`css::parse_color`)をそのまま使う。解釈できない値は
4269/// 仕様上「無視して直前の値を保つ」だが、ここでは不透明な黒へ倒す
4270/// (描かれないより、既定色で描かれた方が原因に気づきやすい)。
4271fn canvas_style_argb(this: &Value, key: &str) -> u32 {
4272    let s = match this {
4273        Value::Object(o) => o
4274            .borrow()
4275            .props
4276            .get(key)
4277            .map(|v| v.to_js_string())
4278            .unwrap_or_default(),
4279        _ => String::new(),
4280    };
4281    crate::os_lib::css::parse_color(&s).unwrap_or(0xFF00_0000)
4282}
4283
4284/// 描画前に、JS 側で書き換えられた状態をコンテキストへ反映する。
4285///
4286/// `fillStyle` 等は JS の素のプロパティなので、代入は検知できない。
4287/// 各描画メソッドの入口で毎回読み直すのが確実。
4288fn canvas_sync_state(this: &Value, ctx: &mut crate::os_lib::canvas2d::Canvas2dContext) {
4289    ctx.state.fill = canvas_style_argb(this, "fillStyle");
4290    ctx.state.stroke = canvas_style_argb(this, "strokeStyle");
4291    if let Value::Object(o) = this {
4292        let b = o.borrow();
4293        if let Some(Value::Number(n)) = b.props.get("globalAlpha") {
4294            if n.is_finite() {
4295                ctx.state.global_alpha = (*n as f32).clamp(0.0, 1.0);
4296            }
4297        }
4298        if let Some(Value::Number(n)) = b.props.get("lineWidth") {
4299            if n.is_finite() && *n > 0.0 {
4300                ctx.state.line_width = *n as f32;
4301            }
4302        }
4303        // 未知の値は無視して既定を保つ(仕様どおり代入を捨てる)。
4304        if let Some(Value::Str(s)) = b.props.get("lineCap") {
4305            if let Some(v) = crate::os_lib::canvas2d::parse_line_cap(s) {
4306                ctx.state.line_cap = v;
4307            }
4308        }
4309        if let Some(Value::Str(s)) = b.props.get("lineJoin") {
4310            if let Some(v) = crate::os_lib::canvas2d::parse_line_join(s) {
4311                ctx.state.line_join = v;
4312            }
4313        }
4314        if let Some(Value::Number(n)) = b.props.get("miterLimit") {
4315            if n.is_finite() && *n > 0.0 {
4316                ctx.state.miter_limit = *n as f32;
4317            }
4318        }
4319        // `setLineDash` が入れた配列。値の検査は `normalize_dash` に任せる
4320        // (不正なパターンはまるごと捨てるのが仕様)。
4321        ctx.state.line_dash.clear();
4322        if let Some(Value::Object(arr)) = b.props.get("_line_dash") {
4323            let ab = arr.borrow();
4324            if let ObjKind::Array(items) = &ab.kind {
4325                for v in items {
4326                    ctx.state.line_dash.push(v.to_number() as f32);
4327                }
4328            }
4329        }
4330        if let Some(Value::Number(n)) = b.props.get("lineDashOffset") {
4331            if n.is_finite() {
4332                ctx.state.line_dash_offset = *n as f32;
4333            }
4334        }
4335        if let Some(Value::Str(s)) = b.props.get("fillStyle") {
4336            ctx.state.fill_str = (**s).clone();
4337            ctx.state.fill_gradient = None;
4338            ctx.state.fill_pattern = None;
4339        } else if let Some(Value::Object(style_obj)) = b.props.get("fillStyle") {
4340            let sb = style_obj.borrow();
4341            if let Some(Value::Number(gid)) = sb.props.get("_gradient_id") {
4342                ctx.state.fill_gradient = crate::os_lib::canvas2d::get_gradient(*gid as u32);
4343                ctx.state.fill_pattern = None;
4344            } else if let Some(Value::Number(pid)) = sb.props.get("_pattern_id") {
4345                ctx.state.fill_pattern = crate::os_lib::canvas2d::get_pattern(*pid as u32);
4346                ctx.state.fill_gradient = None;
4347            } else {
4348                ctx.state.fill_gradient = None;
4349                ctx.state.fill_pattern = None;
4350            }
4351        } else {
4352            ctx.state.fill_gradient = None;
4353            ctx.state.fill_pattern = None;
4354        }
4355        if let Some(Value::Str(s)) = b.props.get("strokeStyle") {
4356            ctx.state.stroke_str = (**s).clone();
4357            ctx.state.stroke_gradient = None;
4358            ctx.state.stroke_pattern = None;
4359        } else if let Some(Value::Object(style_obj)) = b.props.get("strokeStyle") {
4360            let sb = style_obj.borrow();
4361            if let Some(Value::Number(gid)) = sb.props.get("_gradient_id") {
4362                ctx.state.stroke_gradient = crate::os_lib::canvas2d::get_gradient(*gid as u32);
4363                ctx.state.stroke_pattern = None;
4364            } else if let Some(Value::Number(pid)) = sb.props.get("_pattern_id") {
4365                ctx.state.stroke_pattern = crate::os_lib::canvas2d::get_pattern(*pid as u32);
4366                ctx.state.stroke_gradient = None;
4367            } else {
4368                ctx.state.stroke_gradient = None;
4369                ctx.state.stroke_pattern = None;
4370            }
4371        } else {
4372            ctx.state.stroke_gradient = None;
4373            ctx.state.stroke_pattern = None;
4374        }
4375        if let Some(Value::Str(s)) = b.props.get("font") {
4376            ctx.state.font_str = (**s).clone();
4377        }
4378        if let Some(Value::Str(s)) = b.props.get("textAlign") {
4379            ctx.state.text_align_str = (**s).clone();
4380        }
4381        if let Some(Value::Str(s)) = b.props.get("textBaseline") {
4382            ctx.state.text_baseline_str = (**s).clone();
4383        }
4384        if let Some(Value::Bool(sm)) = b.props.get("imageSmoothingEnabled") {
4385            ctx.state.image_smoothing_enabled = *sm;
4386        }
4387        if let Some(Value::Str(s)) = b.props.get("shadowColor") {
4388            ctx.state.shadow_color_str = (**s).clone();
4389            if let Some(c) = crate::os_lib::css::parse_color(s) {
4390                ctx.state.shadow_color = c;
4391            }
4392        }
4393        if let Some(Value::Number(n)) = b.props.get("shadowBlur") {
4394            if n.is_finite() && *n >= 0.0 {
4395                ctx.state.shadow_blur = *n as f32;
4396            }
4397        }
4398        if let Some(Value::Number(n)) = b.props.get("shadowOffsetX") {
4399            if n.is_finite() {
4400                ctx.state.shadow_offset_x = *n as f32;
4401            }
4402        }
4403        if let Some(Value::Number(n)) = b.props.get("shadowOffsetY") {
4404            if n.is_finite() {
4405                ctx.state.shadow_offset_y = *n as f32;
4406            }
4407        }
4408        if let Some(Value::Str(s)) = b.props.get("filter") {
4409            ctx.state.filter = (**s).clone();
4410        }
4411        if let Some(Value::Str(s)) = b.props.get("globalCompositeOperation") {
4412            ctx.state.global_composite_operation_str = (**s).clone();
4413            if let Some(op) = crate::os_lib::canvas2d::parse_composite_operation(s) {
4414                ctx.state.global_composite_operation = op;
4415            }
4416        }
4417        if let Some(Value::Str(s)) = b.props.get("direction") {
4418            if let Some(dir) = crate::os_lib::canvas2d::parse_canvas_direction(s) {
4419                ctx.state.direction = dir;
4420                ctx.state.direction_str = (**s).clone();
4421            }
4422        }
4423    }
4424}
4425
4426pub fn canvas_fill_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4427    if let Some(id) = canvas_ctx_id(&this) {
4428        let (x, y, w, h) = (
4429            canvas_arg_f32(a, 0),
4430            canvas_arg_f32(a, 1),
4431            canvas_arg_f32(a, 2),
4432            canvas_arg_f32(a, 3),
4433        );
4434        crate::os_lib::canvas2d::with_context(id, |c| {
4435            canvas_sync_state(&this, c);
4436            c.fill_rect(x, y, w, h);
4437        });
4438    }
4439    Ok(Value::Undefined)
4440}
4441
4442pub fn canvas_stroke_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4443    if let Some(id) = canvas_ctx_id(&this) {
4444        let (x, y, w, h) = (
4445            canvas_arg_f32(a, 0),
4446            canvas_arg_f32(a, 1),
4447            canvas_arg_f32(a, 2),
4448            canvas_arg_f32(a, 3),
4449        );
4450        crate::os_lib::canvas2d::with_context(id, |c| {
4451            canvas_sync_state(&this, c);
4452            c.stroke_rect(x, y, w, h);
4453        });
4454    }
4455    Ok(Value::Undefined)
4456}
4457
4458pub fn canvas_clear_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4459    if let Some(id) = canvas_ctx_id(&this) {
4460        let (x, y, w, h) = (
4461            canvas_arg_f32(a, 0),
4462            canvas_arg_f32(a, 1),
4463            canvas_arg_f32(a, 2),
4464            canvas_arg_f32(a, 3),
4465        );
4466        crate::os_lib::canvas2d::with_context(id, |c| c.clear_rect(x, y, w, h));
4467    }
4468    Ok(Value::Undefined)
4469}
4470
4471pub fn canvas_begin_path(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
4472    if let Some(id) = canvas_ctx_id(&this) {
4473        crate::os_lib::canvas2d::with_context(id, |c| c.path.begin_path());
4474    }
4475    Ok(Value::Undefined)
4476}
4477
4478pub fn canvas_close_path(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
4479    if let Some(id) = canvas_ctx_id(&this) {
4480        crate::os_lib::canvas2d::with_context(id, |c| c.path.close_path());
4481    }
4482    Ok(Value::Undefined)
4483}
4484
4485pub fn canvas_move_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4486    if let Some(id) = canvas_ctx_id(&this) {
4487        let (x, y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4488        crate::os_lib::canvas2d::with_context(id, |c| {
4489            let m = c.state.transform;
4490            c.path.move_to(&m, x, y);
4491        });
4492    }
4493    Ok(Value::Undefined)
4494}
4495
4496pub fn canvas_line_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4497    if let Some(id) = canvas_ctx_id(&this) {
4498        let (x, y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4499        crate::os_lib::canvas2d::with_context(id, |c| {
4500            let m = c.state.transform;
4501            c.path.line_to(&m, x, y);
4502        });
4503    }
4504    Ok(Value::Undefined)
4505}
4506
4507pub fn canvas_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4508    if let Some(id) = canvas_ctx_id(&this) {
4509        let (x, y, w, h) = (
4510            canvas_arg_f32(a, 0),
4511            canvas_arg_f32(a, 1),
4512            canvas_arg_f32(a, 2),
4513            canvas_arg_f32(a, 3),
4514        );
4515        crate::os_lib::canvas2d::with_context(id, |c| {
4516            let m = c.state.transform;
4517            c.path.rect(&m, x, y, w, h);
4518        });
4519    }
4520    Ok(Value::Undefined)
4521}
4522
4523pub fn canvas_arc(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4524    if let Some(id) = canvas_ctx_id(&this) {
4525        let (cx, cy, r) = (
4526            canvas_arg_f32(a, 0),
4527            canvas_arg_f32(a, 1),
4528            canvas_arg_f32(a, 2),
4529        );
4530        let start = canvas_arg_f32(a, 3);
4531        let end = canvas_arg_f32(a, 4);
4532        let ccw = a.get(5).map(|v| v.truthy()).unwrap_or(false);
4533        crate::os_lib::canvas2d::with_context(id, |c| {
4534            let m = c.state.transform;
4535            c.path.arc(&m, cx, cy, r, start, end, ccw);
4536        });
4537    }
4538    Ok(Value::Undefined)
4539}
4540
4541pub fn canvas_arc_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4542    if let Some(id) = canvas_ctx_id(&this) {
4543        let (x1, y1) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4544        let (x2, y2) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
4545        let radius = canvas_arg_f32(a, 4);
4546        crate::os_lib::canvas2d::with_context(id, |c| {
4547            let m = c.state.transform;
4548            c.path.arc_to(&m, x1, y1, x2, y2, radius);
4549        });
4550    }
4551    Ok(Value::Undefined)
4552}
4553
4554pub fn canvas_fill(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4555    if let Some(id) = canvas_ctx_id(&this) {
4556        if let Some(pid) = a.first().and_then(path2d_id) {
4557            let rule = match a.get(1).map(|v| v.to_js_string()) {
4558                Some(s) if s.eq_ignore_ascii_case("evenodd") => {
4559                    crate::os_lib::canvas2d::FillRule::EvenOdd
4560                }
4561                _ => crate::os_lib::canvas2d::FillRule::NonZero,
4562            };
4563            if let Some(subpaths) = crate::os_lib::canvas2d::get_path2d_subpaths(pid) {
4564                crate::os_lib::canvas2d::with_context(id, |c| {
4565                    canvas_sync_state(&this, c);
4566                    c.fill_subpaths(&subpaths, rule);
4567                });
4568            }
4569        } else {
4570            // 第 1 引数は塗りつぶし規則(省略時は nonzero)。
4571            let rule = match a.first().map(|v| v.to_js_string()) {
4572                Some(s) if s.eq_ignore_ascii_case("evenodd") => {
4573                    crate::os_lib::canvas2d::FillRule::EvenOdd
4574                }
4575                _ => crate::os_lib::canvas2d::FillRule::NonZero,
4576            };
4577            crate::os_lib::canvas2d::with_context(id, |c| {
4578                canvas_sync_state(&this, c);
4579                c.fill(rule);
4580            });
4581        }
4582    }
4583    Ok(Value::Undefined)
4584}
4585
4586pub fn canvas_stroke(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4587    if let Some(id) = canvas_ctx_id(&this) {
4588        if let Some(pid) = a.first().and_then(path2d_id) {
4589            if let Some(subpaths) = crate::os_lib::canvas2d::get_path2d_subpaths(pid) {
4590                crate::os_lib::canvas2d::with_context(id, |c| {
4591                    canvas_sync_state(&this, c);
4592                    c.stroke_subpaths(&subpaths);
4593                });
4594            }
4595        } else {
4596            crate::os_lib::canvas2d::with_context(id, |c| {
4597                canvas_sync_state(&this, c);
4598                // `lineWidth` を反映する(1 以下なら 1px 線の高速路へ落ちる)。
4599                c.stroke_with_width();
4600            });
4601        }
4602    }
4603    Ok(Value::Undefined)
4604}
4605
4606
4607/// `save()` — 現在の描画状態を退避する。
4608pub fn canvas_save(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
4609    if let Some(id) = canvas_ctx_id(&this) {
4610        crate::os_lib::canvas2d::with_context(id, |c| {
4611            // JS 側で書き換えられた状態も含めて退避したいので、
4612            // 先に読み直してから積む。
4613            canvas_sync_state(&this, c);
4614            c.save();
4615        });
4616    }
4617    Ok(Value::Undefined)
4618}
4619
4620/// `restore()` — 直近の退避状態へ戻す。
4621///
4622/// 変換だけでなく `fillStyle` 等も戻すのが仕様だが、それらは JS の素の
4623/// プロパティなので、Rust 側で戻した値を JS へ書き戻す必要がある。
4624pub fn canvas_restore(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
4625    if let Some(id) = canvas_ctx_id(&this) {
4626        let restored = crate::os_lib::canvas2d::with_context(id, |c| {
4627            c.restore();
4628            (
4629                c.state.global_alpha,
4630                c.state.line_width,
4631                c.state.fill_str.clone(),
4632                c.state.stroke_str.clone(),
4633                c.state.font_str.clone(),
4634                c.state.text_align_str.clone(),
4635                c.state.text_baseline_str.clone(),
4636                c.state.miter_limit,
4637                c.state.line_dash_offset,
4638                c.state.line_dash.clone(),
4639                c.state.image_smoothing_enabled,
4640                match c.state.line_cap {
4641                    crate::os_lib::canvas2d::LineCap::Butt => "butt",
4642                    crate::os_lib::canvas2d::LineCap::Round => "round",
4643                    crate::os_lib::canvas2d::LineCap::Square => "square",
4644                },
4645                match c.state.line_join {
4646                    crate::os_lib::canvas2d::LineJoin::Miter => "miter",
4647                    crate::os_lib::canvas2d::LineJoin::Round => "round",
4648                    crate::os_lib::canvas2d::LineJoin::Bevel => "bevel",
4649                },
4650                c.state.shadow_blur,
4651                c.state.shadow_color_str.clone(),
4652                c.state.shadow_offset_x,
4653                c.state.shadow_offset_y,
4654                c.state.filter.clone(),
4655                c.state.global_composite_operation_str.clone(),
4656                c.state.direction_str.clone(),
4657            )
4658        });
4659        if let (Some(r), Value::Object(o)) = (restored, &this) {
4660            let mut b = o.borrow_mut();
4661            b.props.insert("globalAlpha".into(), Value::Number(r.0 as f64));
4662            b.props.insert("lineWidth".into(), Value::Number(r.1 as f64));
4663            b.props.insert("fillStyle".into(), Value::str(&r.2));
4664            b.props.insert("strokeStyle".into(), Value::str(&r.3));
4665            b.props.insert("font".into(), Value::str(&r.4));
4666            b.props.insert("textAlign".into(), Value::str(&r.5));
4667            b.props.insert("textBaseline".into(), Value::str(&r.6));
4668            b.props.insert("miterLimit".into(), Value::Number(r.7 as f64));
4669            b.props.insert("lineDashOffset".into(), Value::Number(r.8 as f64));
4670            b.props.insert("imageSmoothingEnabled".into(), Value::Bool(r.10));
4671            b.props.insert("lineCap".into(), Value::str(r.11));
4672            b.props.insert("lineJoin".into(), Value::str(r.12));
4673            b.props.insert("shadowBlur".into(), Value::Number(r.13 as f64));
4674            b.props.insert("shadowColor".into(), Value::str(&r.14));
4675            b.props.insert("shadowOffsetX".into(), Value::Number(r.15 as f64));
4676            b.props.insert("shadowOffsetY".into(), Value::Number(r.16 as f64));
4677            b.props.insert("filter".into(), Value::str(&r.17));
4678            b.props.insert("globalCompositeOperation".into(), Value::str(&r.18));
4679            b.props.insert("direction".into(), Value::str(&r.19));
4680
4681            let dash_objs = r.9.iter().map(|d| Value::Number(*d as f64)).collect();
4682            b.props.insert("_line_dash".into(), Value::Object(Obj::array(dash_objs)));
4683        }
4684    }
4685    Ok(Value::Undefined)
4686}
4687
4688pub fn canvas_translate(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4689    if let Some(id) = canvas_ctx_id(&this) {
4690        let (tx, ty) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4691        crate::os_lib::canvas2d::with_context(id, |c| c.translate(tx, ty));
4692    }
4693    Ok(Value::Undefined)
4694}
4695
4696pub fn canvas_scale(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4697    if let Some(id) = canvas_ctx_id(&this) {
4698        let (sx, sy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4699        crate::os_lib::canvas2d::with_context(id, |c| c.scale(sx, sy));
4700    }
4701    Ok(Value::Undefined)
4702}
4703
4704pub fn canvas_rotate(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4705    if let Some(id) = canvas_ctx_id(&this) {
4706        let rad = canvas_arg_f32(a, 0);
4707        crate::os_lib::canvas2d::with_context(id, |c| c.rotate(rad));
4708    }
4709    Ok(Value::Undefined)
4710}
4711
4712/// `transform(a,b,c,d,e,f)` — 現在の行列へ右から掛ける。
4713pub fn canvas_transform(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4714    if let Some(id) = canvas_ctx_id(&this) {
4715        let m = crate::os_lib::canvas2d::Matrix {
4716            a: canvas_arg_f32(a, 0),
4717            b: canvas_arg_f32(a, 1),
4718            c: canvas_arg_f32(a, 2),
4719            d: canvas_arg_f32(a, 3),
4720            e: canvas_arg_f32(a, 4),
4721            f: canvas_arg_f32(a, 5),
4722        };
4723        crate::os_lib::canvas2d::with_context(id, |ctx| {
4724            let cur = ctx.state.transform;
4725            ctx.set_transform(cur.multiply(&m));
4726        });
4727    }
4728    Ok(Value::Undefined)
4729}
4730
4731/// `setTransform(a,b,c,d,e,f)` — 行列を置き換える(積まない)。
4732pub fn canvas_set_transform(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4733    if let Some(id) = canvas_ctx_id(&this) {
4734        let m = crate::os_lib::canvas2d::Matrix {
4735            a: canvas_arg_f32(a, 0),
4736            b: canvas_arg_f32(a, 1),
4737            c: canvas_arg_f32(a, 2),
4738            d: canvas_arg_f32(a, 3),
4739            e: canvas_arg_f32(a, 4),
4740            f: canvas_arg_f32(a, 5),
4741        };
4742        crate::os_lib::canvas2d::with_context(id, |ctx| ctx.set_transform(m));
4743    }
4744    Ok(Value::Undefined)
4745}
4746
4747pub fn canvas_reset_transform(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
4748    if let Some(id) = canvas_ctx_id(&this) {
4749        crate::os_lib::canvas2d::with_context(id, |c| c.reset_transform());
4750    }
4751    Ok(Value::Undefined)
4752}
4753
4754pub fn canvas_bezier_curve_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4755    if let Some(id) = canvas_ctx_id(&this) {
4756        let (c1x, c1y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4757        let (c2x, c2y) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
4758        let (x, y) = (canvas_arg_f32(a, 4), canvas_arg_f32(a, 5));
4759        crate::os_lib::canvas2d::with_context(id, |c| {
4760            let m = c.state.transform;
4761            c.path.bezier_curve_to(&m, c1x, c1y, c2x, c2y, x, y);
4762        });
4763    }
4764    Ok(Value::Undefined)
4765}
4766
4767pub fn canvas_quadratic_curve_to(
4768    _it: &mut Interp,
4769    this: Value,
4770    a: &[Value],
4771) -> Result<Value, Value> {
4772    if let Some(id) = canvas_ctx_id(&this) {
4773        let (cx, cy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4774        let (x, y) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
4775        crate::os_lib::canvas2d::with_context(id, |c| {
4776            let m = c.state.transform;
4777            c.path.quadratic_curve_to(&m, cx, cy, x, y);
4778        });
4779    }
4780    Ok(Value::Undefined)
4781}
4782
4783
4784/// `ellipse(cx, cy, rx, ry, rotation, start, end, anticlockwise)`。
4785pub fn canvas_ellipse(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4786    if let Some(id) = canvas_ctx_id(&this) {
4787        let (cx, cy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4788        let (rx, ry) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
4789        let rot = canvas_arg_f32(a, 4);
4790        let start = canvas_arg_f32(a, 5);
4791        let end = canvas_arg_f32(a, 6);
4792        let ccw = a.get(7).map(|v| v.truthy()).unwrap_or(false);
4793        crate::os_lib::canvas2d::with_context(id, |c| {
4794            let m = c.state.transform;
4795            c.path.ellipse(&m, cx, cy, rx, ry, rot, start, end, ccw);
4796        });
4797    }
4798    Ok(Value::Undefined)
4799}
4800
4801/// `roundRect(x, y, w, h, radii)`。
4802///
4803/// `radii` は数値 1 個・配列(1〜4 要素)のどちらも取りうる。
4804/// CSS の `border-radius` と同じ補完規則(1 個なら全隅、2 個なら
4805/// 対角の組、3 個なら左上・対角・右下)に従う。
4806pub fn canvas_round_rect(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4807    if let Some(id) = canvas_ctx_id(&this) {
4808        let (x, y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
4809        let (w, h) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
4810        let radii = parse_round_rect_radii(it, a.get(4))?;
4811        crate::os_lib::canvas2d::with_context(id, |c| {
4812            let m = c.state.transform;
4813            c.path.round_rect(&m, x, y, w, h, radii);
4814        });
4815    }
4816    Ok(Value::Undefined)
4817}
4818
4819
4820/// `clip(fillRule)` または `clip(path, fillRule)`。クリップ領域を狭める。
4821pub fn canvas_clip(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4822    if let Some(id) = canvas_ctx_id(&this) {
4823        if let Some(pid) = a.first().and_then(path2d_id) {
4824            let rule = match a.get(1).map(|v| v.to_js_string()) {
4825                Some(s) if s.eq_ignore_ascii_case("evenodd") => {
4826                    crate::os_lib::canvas2d::FillRule::EvenOdd
4827                }
4828                _ => crate::os_lib::canvas2d::FillRule::NonZero,
4829            };
4830            if let Some(subpaths) = crate::os_lib::canvas2d::get_path2d_subpaths(pid) {
4831                crate::os_lib::canvas2d::with_context(id, |c| {
4832                    c.clip_subpaths(&subpaths, rule);
4833                });
4834            }
4835        } else {
4836            let rule = match a.first().map(|v| v.to_js_string()) {
4837                Some(s) if s.eq_ignore_ascii_case("evenodd") => {
4838                    crate::os_lib::canvas2d::FillRule::EvenOdd
4839                }
4840                _ => crate::os_lib::canvas2d::FillRule::NonZero,
4841            };
4842            crate::os_lib::canvas2d::with_context(id, |c| {
4843                c.clip(rule);
4844            });
4845        }
4846    }
4847    Ok(Value::Undefined)
4848}
4849
4850
4851/// `getImageData(x, y, w, h, [settings])`。面の内容を読んで `ImageData` を返す。
4852pub fn canvas_get_image_data(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4853    let (x, y) = (canvas_arg_f32(a, 0) as i32, canvas_arg_f32(a, 1) as i32);
4854    let w = (canvas_arg_f32(a, 2).abs() as usize).max(1);
4855    let h = (canvas_arg_f32(a, 3).abs() as usize).max(1);
4856    let cs = parse_image_data_color_space(a.get(4));
4857
4858    let items = if let Some(id) = canvas_ctx_id(&this) {
4859        crate::os_lib::canvas2d::with_context(id, |c| {
4860            let bytes = crate::os_lib::canvas2d::get_image_data(&c.surface, x, y, w as i32, h as i32);
4861            bytes.into_iter().map(|b| Value::Number(b as f64)).collect()
4862        }).unwrap_or_else(|| alloc::vec![Value::Number(0.0); w * h * 4])
4863    } else {
4864        alloc::vec![Value::Number(0.0); w * h * 4]
4865    };
4866
4867    Ok(make_image_data_object(w, h, items, cs))
4868}
4869
4870/// `putImageData(imageData, dx, dy)`。
4871///
4872/// 合成せず上書きし、クリップも無視する(仕様。`fill` 系と意味論が違う)。
4873pub fn canvas_put_image_data(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4874    let Some(id) = canvas_ctx_id(&this) else {
4875        return Ok(Value::Undefined);
4876    };
4877    let Some(Value::Object(img)) = a.first() else {
4878        return Ok(Value::Undefined);
4879    };
4880    let (w, h, bytes) = {
4881        let b = img.borrow();
4882        let w = b
4883            .props
4884            .get("width")
4885            .map(|v| v.to_number() as i32)
4886            .unwrap_or(0);
4887        let h = b
4888            .props
4889            .get("height")
4890            .map(|v| v.to_number() as i32)
4891            .unwrap_or(0);
4892        let mut bytes: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
4893        if let Some(Value::Object(arr)) = b.props.get("data") {
4894            let ab = arr.borrow();
4895            if let ObjKind::Bytes(st) = &ab.kind {
4896                bytes = st.to_vec();
4897            }
4898            if let ObjKind::Array(items) = &ab.kind {
4899                bytes.reserve(items.len());
4900                for v in items {
4901                    let n = v.to_number();
4902                    // 仕様どおり 0〜255 へ丸めて収める。
4903                    bytes.push(if n.is_finite() {
4904                        n.clamp(0.0, 255.0) as u8
4905                    } else {
4906                        0
4907                    });
4908                }
4909            }
4910        }
4911        (w, h, bytes)
4912    };
4913    let (dx, dy) = (canvas_arg_f32(a, 1) as i32, canvas_arg_f32(a, 2) as i32);
4914    crate::os_lib::canvas2d::with_context(id, |c| {
4915        crate::os_lib::canvas2d::put_image_data(&mut c.surface, &bytes, w, h, dx, dy);
4916    });
4917    Ok(Value::Undefined)
4918}
4919
4920
4921/// `drawImage` の転送元。`<img>` と別の canvas を扱う。
4922enum DrawSrc {
4923    /// デコード済み画像(`<img>`)。
4924    Image(alloc::sync::Arc<crate::os_lib::web_engine::DecodedImage>),
4925    /// 別の canvas の画素を RGBA8 へ写したもの。
4926    Canvas(u32, u32, alloc::vec::Vec<u8>),
4927}
4928
4929/// 転送元の値から画素を取り出す。
4930///
4931/// `<img>` は `src` 属性で共有の置き場を引く。
4932/// canvas は `_c2d_id` からコンテキストを引いて画素を写す
4933/// (同じコンテキストを二重に借りないよう、ここで写しを作る)。
4934fn draw_src_from(it: &mut Interp, v: &Value) -> Option<DrawSrc> {
4935    let Value::Object(o) = v else {
4936        return None;
4937    };
4938    let idx = match o.borrow().kind {
4939        ObjKind::DomElement(i) => Some(i),
4940        _ => None,
4941    }?;
4942    let (tag, src, cid) = {
4943        let dom = it.dom.borrow();
4944        let tag = dom.nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
4945        let src = dom.get_attr(idx, "src").unwrap_or_default();
4946        let cid = dom
4947            .get_attr(idx, "_c2d_id")
4948            .and_then(|s| s.parse::<u32>().ok());
4949        (tag, src, cid)
4950    };
4951    if tag == "canvas" {
4952        let id = cid?;
4953        return crate::os_lib::canvas2d::with_context(id, |c| {
4954            let (w, h) = (c.surface.width, c.surface.height);
4955            DrawSrc::Canvas(w, h, crate::os_lib::canvas2d::get_image_data(
4956                &c.surface, 0, 0, w as i32, h as i32,
4957            ))
4958        });
4959    }
4960    crate::os_lib::web_engine::shared_image(&src).map(DrawSrc::Image)
4961}
4962
4963/// `drawImage(img, dx, dy)` / `(img, dx, dy, dw, dh)` /
4964/// `(img, sx, sy, sw, sh, dx, dy, dw, dh)`。
4965pub fn canvas_draw_image(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4966    let Some(id) = canvas_ctx_id(&this) else {
4967        return Ok(Value::Undefined);
4968    };
4969    let Some(src) = a.first().and_then(|v| draw_src_from(it, v)) else {
4970        return Ok(Value::Undefined);
4971    };
4972    let (sw, sh, bytes): (u32, u32, &[u8]) = match &src {
4973        DrawSrc::Image(img) => (img.width, img.height, &img.rgba),
4974        DrawSrc::Canvas(w, h, b) => (*w, *h, b),
4975    };
4976    // 引数の個数で意味が変わる(仕様)。3・5・9 個のいずれか。
4977    let spec = match a.len() {
4978        0..=2 => return Ok(Value::Undefined),
4979        3 | 4 => crate::os_lib::canvas2d::BlitSpec {
4980            sx: 0.0,
4981            sy: 0.0,
4982            sw: sw as f32,
4983            sh: sh as f32,
4984            dx: canvas_arg_f32(a, 1),
4985            dy: canvas_arg_f32(a, 2),
4986            dw: sw as f32,
4987            dh: sh as f32,
4988        },
4989        5..=8 => crate::os_lib::canvas2d::BlitSpec {
4990            sx: 0.0,
4991            sy: 0.0,
4992            sw: sw as f32,
4993            sh: sh as f32,
4994            dx: canvas_arg_f32(a, 1),
4995            dy: canvas_arg_f32(a, 2),
4996            dw: canvas_arg_f32(a, 3),
4997            dh: canvas_arg_f32(a, 4),
4998        },
4999        _ => crate::os_lib::canvas2d::BlitSpec {
5000            sx: canvas_arg_f32(a, 1),
5001            sy: canvas_arg_f32(a, 2),
5002            sw: canvas_arg_f32(a, 3),
5003            sh: canvas_arg_f32(a, 4),
5004            dx: canvas_arg_f32(a, 5),
5005            dy: canvas_arg_f32(a, 6),
5006            dw: canvas_arg_f32(a, 7),
5007            dh: canvas_arg_f32(a, 8),
5008        },
5009    };
5010    let image = crate::os_lib::canvas2d::ImageSource {
5011        width: sw,
5012        height: sh,
5013        rgba: bytes,
5014    };
5015    crate::os_lib::canvas2d::with_context(id, |c| {
5016        canvas_sync_state(&this, c);
5017        c.sync_clip();
5018        let m = c.state.transform;
5019        let alpha = c.state.global_alpha;
5020        let smooth = c.state.image_smoothing_enabled;
5021        let filter = c.state.filter.clone();
5022        c.surface.draw_image_with_filter(&image, &spec, &m, alpha, smooth, &filter);
5023    });
5024    Ok(Value::Undefined)
5025}
5026
5027
5028/// 1 文字ぶんの字形を、フォントの錠を**持たずに**扱えるよう写したもの。
5029struct OwnedGlyph {
5030    width: u32,
5031    height: u32,
5032    x_offset: i32,
5033    y_offset: i32,
5034    advance: f32,
5035    alpha: alloc::vec::Vec<u8>,
5036}
5037
5038/// 文字列ぶんの字形を取り出す。
5039///
5040/// 【重要】フォントの錠を**描画中に持ち続けない**。
5041/// `GLOBAL_VECTOR_FONT` は毎フレームの文字描画が使う。低優先度の側が
5042/// 握ったまま長く走ると、提示ループが `spin::Mutex` で回り続けて
5043/// 優先度逆転を起こす(`image_results_ready` で実際に踏んだ)。
5044/// ここで写しを作って錠を放し、描画は錠の外で行う。
5045fn glyphs_for(text: &str, size_px: u32) -> alloc::vec::Vec<OwnedGlyph> {
5046    let mut out = alloc::vec::Vec::new();
5047    let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
5048    let Some(font) = font_lock.as_mut() else {
5049        return out;
5050    };
5051    for c in text.chars() {
5052        if let Some(g) = font.get_glyph(c, size_px) {
5053            out.push(OwnedGlyph {
5054                width: g.width,
5055                height: g.height,
5056                x_offset: g.x_offset,
5057                y_offset: g.y_offset,
5058                advance: g.advance as f32,
5059                alpha: g.data.clone(),
5060            });
5061        }
5062    }
5063    out
5064}
5065
5066/// コンテキストの `font` から文字の大きさ(px)を得る。既定は 10px。
5067fn canvas_font_size(this: &Value) -> f32 {
5068    let Value::Object(o) = this else {
5069        return 10.0;
5070    };
5071    let b = o.borrow();
5072    let Some(Value::Str(s)) = b.props.get("font") else {
5073        return 10.0;
5074    };
5075    crate::os_lib::canvas2d::parse_font_size(s).unwrap_or(10.0)
5076}
5077
5078/// `fillText(text, x, y)`。
5079///
5080/// `strokeText` も同じ処理へ回す。線でなぞる形は未対応で、
5081/// 塗りで代用する(何も描かないよりは近い)。
5082
5083fn canvas_text_layout_params(
5084    this: &Value,
5085) -> (
5086    crate::os_lib::canvas2d::TextAlign,
5087    crate::os_lib::canvas2d::TextBaseline,
5088    crate::os_lib::canvas2d::CanvasDirection,
5089) {
5090    let mut al = crate::os_lib::canvas2d::TextAlign::Start;
5091    let mut bl = crate::os_lib::canvas2d::TextBaseline::Alphabetic;
5092    let mut dir = crate::os_lib::canvas2d::CanvasDirection::Inherit;
5093    if let Value::Object(o) = this {
5094        let b = o.borrow();
5095        if let Some(Value::Str(s)) = b.props.get("textAlign") {
5096            if let Some(v) = crate::os_lib::canvas2d::parse_text_align(s) {
5097                al = v;
5098            }
5099        }
5100        if let Some(Value::Str(s)) = b.props.get("textBaseline") {
5101            if let Some(v) = crate::os_lib::canvas2d::parse_text_baseline(s) {
5102                bl = v;
5103            }
5104        }
5105        if let Some(Value::Str(s)) = b.props.get("direction") {
5106            if let Some(v) = crate::os_lib::canvas2d::parse_canvas_direction(s) {
5107                dir = v;
5108            }
5109        }
5110    }
5111    (al, bl, dir)
5112}
5113
5114/// `strokeText(text, x, y)`。
5115///
5116/// `strokeStyle` と `lineWidth` を反映して輪郭線を描く。
5117pub fn canvas_stroke_text(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
5118    let Some(id) = canvas_ctx_id(&this) else {
5119        return Ok(Value::Undefined);
5120    };
5121    let text = a.first().map(|v| v.to_js_string()).unwrap_or_default();
5122    if text.is_empty() {
5123        return Ok(Value::Undefined);
5124    }
5125    let (x, y) = (canvas_arg_f32(a, 1), canvas_arg_f32(a, 2));
5126    if !x.is_finite() || !y.is_finite() {
5127        return Ok(Value::Undefined);
5128    }
5129    let size = canvas_font_size(&this);
5130    let glyphs = glyphs_for(&text, size as u32);
5131    if glyphs.is_empty() {
5132        return Ok(Value::Undefined);
5133    }
5134    let total: f32 = glyphs.iter().map(|g| g.advance).sum();
5135
5136    let (align, baseline, dir) = canvas_text_layout_params(&this);
5137    let x0 = x + crate::os_lib::canvas2d::align_offset_with_direction(align, total, dir);
5138    let y0 = y + crate::os_lib::canvas2d::baseline_offset(baseline, size);
5139
5140    crate::os_lib::canvas2d::with_context(id, |c| {
5141        canvas_sync_state(&this, c);
5142        c.sync_clip();
5143        let stroke_color = c.apply_alpha(c.state.stroke);
5144        let line_width = c.state.line_width;
5145        let m = c.state.transform;
5146        if c.state.has_shadow() {
5147            let shadow_color = c.shadow_color_argb();
5148            let mut pen = x0 + c.state.shadow_offset_x;
5149            let sy = y0 + c.state.shadow_offset_y;
5150            for g in &glyphs {
5151                let bm = crate::os_lib::canvas2d::GlyphBitmap {
5152                    width: g.width,
5153                    height: g.height,
5154                    x_offset: g.x_offset,
5155                    y_offset: g.y_offset,
5156                    advance: g.advance,
5157                    alpha: &g.alpha,
5158                };
5159                c.surface.draw_glyph_stroke(&bm, pen, sy, shadow_color, line_width, &m);
5160                pen += g.advance;
5161            }
5162        }
5163        let mut pen = x0;
5164        for g in &glyphs {
5165            let bm = crate::os_lib::canvas2d::GlyphBitmap {
5166                width: g.width,
5167                height: g.height,
5168                x_offset: g.x_offset,
5169                y_offset: g.y_offset,
5170                advance: g.advance,
5171                alpha: &g.alpha,
5172            };
5173            c.surface.draw_glyph_stroke(&bm, pen, y0, stroke_color, line_width, &m);
5174            pen += g.advance;
5175        }
5176    });
5177    Ok(Value::Undefined)
5178}
5179
5180pub fn canvas_fill_text(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
5181    let Some(id) = canvas_ctx_id(&this) else {
5182        return Ok(Value::Undefined);
5183    };
5184    let text = a.first().map(|v| v.to_js_string()).unwrap_or_default();
5185    if text.is_empty() {
5186        return Ok(Value::Undefined);
5187    }
5188    let (x, y) = (canvas_arg_f32(a, 1), canvas_arg_f32(a, 2));
5189    if !x.is_finite() || !y.is_finite() {
5190        return Ok(Value::Undefined);
5191    }
5192    let size = canvas_font_size(&this);
5193    let glyphs = glyphs_for(&text, size as u32);
5194    if glyphs.is_empty() {
5195        return Ok(Value::Undefined);
5196    }
5197    let total: f32 = glyphs.iter().map(|g| g.advance).sum();
5198
5199    let (align, baseline, dir) = canvas_text_layout_params(&this);
5200    let x0 = x + crate::os_lib::canvas2d::align_offset_with_direction(align, total, dir);
5201    let y0 = y + crate::os_lib::canvas2d::baseline_offset(baseline, size);
5202
5203    crate::os_lib::canvas2d::with_context(id, |c| {
5204        canvas_sync_state(&this, c);
5205        c.sync_clip();
5206        let color = c.apply_alpha(c.state.fill);
5207        let m = c.state.transform;
5208        if c.state.has_shadow() {
5209            let shadow_color = c.shadow_color_argb();
5210            let mut pen = x0 + c.state.shadow_offset_x;
5211            let sy = y0 + c.state.shadow_offset_y;
5212            for g in &glyphs {
5213                let bm = crate::os_lib::canvas2d::GlyphBitmap {
5214                    width: g.width,
5215                    height: g.height,
5216                    x_offset: g.x_offset,
5217                    y_offset: g.y_offset,
5218                    advance: g.advance,
5219                    alpha: &g.alpha,
5220                };
5221                c.surface.draw_glyph(&bm, pen, sy, shadow_color, &m);
5222                pen += g.advance;
5223            }
5224        }
5225        let mut pen = x0;
5226        for g in &glyphs {
5227            let bm = crate::os_lib::canvas2d::GlyphBitmap {
5228                width: g.width,
5229                height: g.height,
5230                x_offset: g.x_offset,
5231                y_offset: g.y_offset,
5232                advance: g.advance,
5233                alpha: &g.alpha,
5234            };
5235            c.surface.draw_glyph(&bm, pen, y0, color, &m);
5236            pen += g.advance;
5237        }
5238    });
5239    Ok(Value::Undefined)
5240}
5241
5242/// `measureText(text)`。
5243///
5244/// 従来はバイト長 × 8 を返していた。日本語では実際と大きくずれ、
5245/// `font` の大きさも無視していた。字形の送り幅を実測する。
5246pub fn canvas_measure_text(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
5247    let text = a.first().map(|v| v.to_js_string()).unwrap_or_default();
5248    let size = canvas_font_size(&this);
5249    let glyphs = glyphs_for(&text, size as u32);
5250    if glyphs.is_empty() && !text.is_empty() {
5251        // フォントが無い(未初期化)ときは従来の概算へ落ちる。
5252        return dom_canvas_measure_text(it, this, a);
5253    }
5254    let width: f32 = glyphs.iter().map(|g| g.advance).sum();
5255    let obj = Obj::plain();
5256    {
5257        let mut b = obj.borrow_mut();
5258        b.props.insert("width".into(), Value::Number(width as f64));
5259        // 上下の張り出しは字形の実測から出す。
5260        let ascent = glyphs
5261            .iter()
5262            .map(|g| -g.y_offset)
5263            .max()
5264            .unwrap_or(0)
5265            .max(0) as f64;
5266        let descent = glyphs
5267            .iter()
5268            .map(|g| g.y_offset + g.height as i32)
5269            .max()
5270            .unwrap_or(0)
5271            .max(0) as f64;
5272        b.props
5273            .insert("actualBoundingBoxAscent".into(), Value::Number(ascent));
5274        b.props
5275            .insert("actualBoundingBoxDescent".into(), Value::Number(descent));
5276        b.props.insert(
5277            "fontBoundingBoxAscent".into(),
5278            Value::Number((size * 0.85) as f64),
5279        );
5280        b.props.insert(
5281            "fontBoundingBoxDescent".into(),
5282            Value::Number((size * 0.15) as f64),
5283        );
5284        let (align, _, dir) = canvas_text_layout_params(&this);
5285        let align_off = crate::os_lib::canvas2d::align_offset_with_direction(align, width, dir);
5286        let actual_left = -align_off;
5287        let actual_right = width + align_off;
5288        b.props
5289            .insert("actualBoundingBoxLeft".into(), Value::Number(actual_left as f64));
5290        b.props.insert(
5291            "actualBoundingBoxRight".into(),
5292            Value::Number(actual_right as f64),
5293        );
5294    }
5295    Ok(Value::Object(obj))
5296}
5297
5298/// Web Components: `element.attachShadow({ mode: 'open' | 'closed' })`
5299pub(crate) fn dom_attach_shadow(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
5300    let host_idx = match this_dom_idx(&this) {
5301        Some(i) => i,
5302        None => return Err(it.type_error("attachShadow must be called on an Element")),
5303    };
5304
5305    let opt = arg(a, 0);
5306    let mode_str = match &opt {
5307        Value::Object(o) => o
5308            .borrow()
5309            .props
5310            .get("mode")
5311            .map(|v| v.to_js_string())
5312            .unwrap_or_default(),
5313        _ => alloc::string::String::new(),
5314    };
5315
5316    let mode = match crate::os_lib::shadow_dom::ShadowRootMode::from_str(&mode_str) {
5317        Some(m) => m,
5318        None => {
5319            return Err(it.type_error(
5320                "Failed to execute 'attachShadow' on 'Element': The provided value is not a valid enum value of type ShadowRootMode ('open' or 'closed')",
5321            ))
5322        }
5323    };
5324
5325    match it.dom.borrow_mut().attach_shadow(host_idx, mode) {
5326        Ok(root_idx) => {
5327            let root_obj = Obj::dom(root_idx);
5328            {
5329                let mut ro = root_obj.borrow_mut();
5330                ro.props.insert("mode".into(), Value::str(mode.as_str()));
5331                ro.props.insert("host".into(), this.clone());
5332            }
5333            Ok(Value::Object(root_obj))
5334        }
5335        Err(crate::os_lib::shadow_dom::AttachShadowError::AlreadyHasShadowRoot) => {
5336            Err(make_dom_exception(
5337                "InvalidStateError",
5338                "Shadow root cannot be created on a host which already hosts a shadow tree",
5339            ))
5340        }
5341        Err(crate::os_lib::shadow_dom::AttachShadowError::DisallowedTag) => {
5342            Err(make_dom_exception(
5343                "NotSupportedError",
5344                "This element does not support attachShadow",
5345            ))
5346        }
5347        Err(_) => Err(make_dom_exception(
5348            "NotSupportedError",
5349            "Failed to attach shadow root",
5350        )),
5351    }
5352}
5353
5354/// Web Components: `customElements.define(name, constructor, options)`
5355pub(crate) fn custom_elements_define(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
5356    let name = arg(a, 0).to_js_string();
5357    let ctor = arg(a, 1);
5358
5359    if !crate::os_lib::custom_elements::is_valid_custom_element_name(&name) {
5360        return Err(it.type_error(alloc::format!("Failed to execute 'define' on 'CustomElementRegistry': \"{}\" is not a valid custom element name", name)));
5361    }
5362
5363    if !matches!(ctor, Value::Object(_)) {
5364        return Err(it.type_error("Failed to execute 'define' on 'CustomElementRegistry': The referenced script is not a constructor"));
5365    }
5366
5367    // observedAttributes をコンストラクタから取得(静的 getter もある。Polymer は
5368    // `static get observedAttributes()` で返す。【2026-10-20】従来は props しか見ず空だった)。
5369    let mut observed_attrs = alloc::vec::Vec::new();
5370    let obs_val = it.get_property(&ctor, "observedAttributes")?;
5371    if !matches!(obs_val, Value::Undefined | Value::Null) {
5372        for item in array_like_items(it, &obs_val) {
5373            observed_attrs.push(item.to_js_string());
5374        }
5375    }
5376
5377    // DomBridge の custom_elements レジストリに登録
5378    match it.dom.borrow_mut().custom_elements.define(&name, observed_attrs) {
5379        Ok(()) => {}
5380        Err(crate::os_lib::custom_elements::DefineError::AlreadyDefined) => {
5381            return Err(make_dom_exception(
5382                "NotSupportedError",
5383                &alloc::format!("Failed to execute 'define' on 'CustomElementRegistry': the name \"{}\" has already been used with this registry", name),
5384            ));
5385        }
5386        Err(crate::os_lib::custom_elements::DefineError::InvalidName) => {
5387            return Err(it.type_error(alloc::format!("\"{}\" is not a valid custom element name", name)));
5388        }
5389    }
5390
5391    // JS 側の this (CustomElementRegistry オブジェクト) の `_registry` にコンストラクタを保持
5392    if let Value::Object(reg_obj) = &this {
5393        if let Some(Value::Object(map)) = reg_obj.borrow().props.get("_registry") {
5394            map.borrow_mut().props.insert(name.clone(), ctor.clone());
5395        }
5396    }
5397
5398    // 文書に接続済みの同名要素を木順に昇格する(コンストラクタ → connectedCallback)。
5399    // 【2026-10-20】従来は使い捨てのハンドルに原型を付けるだけで、昇格していなかった。
5400    let matching_indices: alloc::vec::Vec<usize> = {
5401        let mut dom = it.dom.borrow_mut();
5402        for n in dom.nodes.iter_mut().filter(|n| !n.is_text && n.tag.eq_ignore_ascii_case(&name)) {
5403            n.attrs.insert(alloc::string::String::from("_defined"), alloc::string::String::from("true"));
5404        }
5405        let root = 0;
5406        dom.subtree_elements(root)
5407            .into_iter()
5408            .filter(|&i| dom.nodes[i].tag.eq_ignore_ascii_case(&name))
5409            .collect()
5410    };
5411    for idx in matching_indices {
5412        it.ce_try_upgrade(idx, true);
5413    }
5414
5415    Ok(Value::Undefined)
5416}
5417
5418/// Web Components: `customElements.get(name)`
5419pub(crate) fn custom_elements_get(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
5420    let name = arg(a, 0).to_js_string();
5421    if let Value::Object(reg_obj) = this {
5422        if let Some(Value::Object(map)) = reg_obj.borrow().props.get("_registry") {
5423            if let Some(ctor) = map.borrow().props.get(&name) {
5424                return Ok(ctor.clone());
5425            }
5426        }
5427    }
5428    Ok(Value::Undefined)
5429}
5430
5431/// Web Components: `customElements.whenDefined(name)`
5432pub(crate) fn custom_elements_when_defined(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
5433    let name = arg(a, 0).to_js_string();
5434    if !crate::os_lib::custom_elements::is_valid_custom_element_name(&name) {
5435        let err = it.type_error(alloc::format!("\"{}\" is not a valid custom element name", name));
5436        return Ok(rejected_promise(it, err));
5437    }
5438    let is_def = if let Value::Object(reg_obj) = &this {
5439        if let Some(Value::Object(map)) = reg_obj.borrow().props.get("_registry") {
5440            map.borrow().props.contains_key(&name)
5441        } else {
5442            false
5443        }
5444    } else {
5445        false
5446    };
5447
5448    if is_def {
5449        Ok(resolved_promise(it, Value::Undefined))
5450    } else {
5451        Ok(resolved_promise(it, Value::Undefined))
5452    }
5453}
5454
5455/// Web Components: `customElements.upgrade(root)`
5456pub(crate) fn custom_elements_upgrade(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
5457    Ok(Value::Undefined)
5458}
5459
5460
5461
5462/// `new Image(width?, height?)`: `<img>` 要素を作る(`document.createElement('img')` と同じ)。
5463pub(crate) fn image_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
5464    let idx = it.dom.borrow_mut().create_element("img");
5465    for (i, name) in ["width", "height"].iter().enumerate() {
5466        if let Some(v) = a.get(i).filter(|v| !matches!(v, Value::Undefined)) {
5467            let n = v.to_number();
5468            if n.is_finite() {
5469                it.dom.borrow_mut().set_attr(idx, name, &alloc::format!("{}", n as i64));
5470            }
5471        }
5472    }
5473    Ok(Value::Object(Obj::dom(idx)))
5474}
5475
5476/// `new Audio(src?)`: `<audio preload="auto">` 要素を作る。
5477pub(crate) fn audio_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
5478    let idx = it.dom.borrow_mut().create_element("audio");
5479    it.dom.borrow_mut().set_attr(idx, "preload", "auto");
5480    if let Some(v) = a.first().filter(|v| !matches!(v, Value::Undefined)) {
5481        let src = v.to_js_string();
5482        it.dom.borrow_mut().set_attr(idx, "src", &src);
5483    }
5484    Ok(Value::Object(Obj::dom(idx)))
5485}