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    }
1773    Ok(Value::Undefined)
1774}
1775
1776/// element.checkValidity() / reportValidity(): 現在値で制約バリデーションを実行し、
1777/// 妥当なら true、違反があれば false を返す。
1778/// (invalid イベントはフォーム送信経路 submit_form 側で WebEngine が発火する。
1779///  Interp からは DOM イベント発火経路が無いため、ここでは bool 判定のみ。)
1780/// **重要**: `<form>` 自身に対する `checkValidity()` が、フォームの子孫コントロール
1781/// を一切見ず `validate_field(form_idx, ...)` を直接呼んでいたため、`<form>` タグは
1782/// `type` が email/url/number のいずれにも一致せず常に無条件で `true`(妥当)を
1783/// 返してしまうバグだった(`WebEngine::submit_form` 側は `descendants_by_tags` で
1784/// 子孫を正しく走査していたのに、JS から呼ぶ `checkValidity()` だけが非対称だった)。
1785pub fn dom_check_validity(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1786    if let Some(idx) = this_dom_idx(&this) {
1787        let tag = it.dom.borrow().nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
1788        if tag == "form" {
1789            let dom = it.dom.borrow();
1790            // `form_associated_controls`は子孫に加え、`form="このformのid"`属性で
1791            // フォーム外から明示的に関連付けられたコントロール(HTML5仕様の
1792            // 「form owner」)も含む。以前は`descendants_by_tags`(子孫のみ)
1793            // だったため、`<form>`の外に置かれた`<input form="...">`が
1794            // `checkValidity()`の対象から漏れる兄弟ギャップだった。
1795            let fields = dom.form_associated_controls(idx, &["input", "textarea", "select"]);
1796            let all_valid = fields.iter().all(|&node| {
1797                let value = dom.effective_form_value(node);
1798                dom.validate_field(node, &value).is_none()
1799            });
1800            return Ok(Value::Bool(all_valid));
1801        }
1802        let value = it.dom.borrow().effective_form_value(idx);
1803        let valid = it.dom.borrow().validate_field(idx, &value).is_none();
1804        return Ok(Value::Bool(valid));
1805    }
1806    Ok(Value::Bool(true))
1807}
1808
1809/// element.setCustomValidity(msg): カスタム検証メッセージを設定。空文字でクリア。
1810/// 内部的には隠し属性 `_custom_validity` に保存し、validate_field が最優先で参照する。
1811pub fn dom_set_custom_validity(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1812    if let Some(idx) = this_dom_idx(&this) {
1813        let msg = arg(a, 0).to_js_string();
1814        if msg.is_empty() {
1815            it.dom.borrow_mut().remove_attr(idx, "_custom_validity");
1816        } else {
1817            it.dom.borrow_mut().set_attr(idx, "_custom_validity", &msg);
1818        }
1819    }
1820    Ok(Value::Undefined)
1821}
1822
1823/// `classList.forEach(fn)`(丸ごと未対応だった。`DOMTokenList` は仕様上イテラブル/
1824/// `forEach` を持つ配列風のオブジェクトだが、この処理系の `classList` は専用の
1825/// `Host("classList:N")` プロキシで表現されており、`Symbol.iterator`/`forEach` が
1826/// 一切配線されていなかった。`for (const c of el.classList)` も同じ理由で常に
1827/// 何も反復しない黙殺バグだった — `iterate_values()` 側の修正と対)。
1828pub fn dom_classlist_for_each(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1829    if let Some(idx) = this_host_idx(&this, "classList:") {
1830        let classes = it.dom.borrow().nodes.get(idx).map(|n| n.classes.clone()).unwrap_or_default();
1831        let cb = arg(a, 0);
1832        for (i, c) in classes.into_iter().enumerate() {
1833            it.call_value(&cb, Value::Undefined, &[Value::str(c), Value::Number(i as f64)])?;
1834        }
1835    }
1836    Ok(Value::Undefined)
1837}
1838pub fn dom_classlist_add(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1839    if let Some(idx) = this_host_idx(&this, "classList:") {
1840        for v in a {
1841            it.dom
1842                .borrow_mut()
1843                .class_add(idx, &v.to_js_string());
1844        }
1845    }
1846    Ok(Value::Undefined)
1847}
1848
1849pub fn dom_classlist_remove(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1850    if let Some(idx) = this_host_idx(&this, "classList:") {
1851        for v in a {
1852            it.dom
1853                .borrow_mut()
1854                .class_remove(idx, &v.to_js_string());
1855        }
1856    }
1857    Ok(Value::Undefined)
1858}
1859
1860/// `classList.supports(token)`(DOM Standard §4.3)。
1861pub fn dom_classlist_supports(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
1862    Ok(Value::Bool(false))
1863}
1864
1865
1866/// `classList.toggle(name, force)`。以前は第2引数 `force`(真偽値を渡すと現在の状態に
1867/// 関係なく強制的に付与/除去する)を完全に無視し、常に単純なトグルだけを行っていた
1868/// (`el.classList.toggle('active', isActive)` という定番パターンが動かなかった)。
1869pub fn dom_classlist_toggle(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1870    if let Some(idx) = this_host_idx(&this, "classList:") {
1871        let cls = arg(a, 0).to_js_string();
1872        if matches!(arg(a, 1), Value::Undefined) {
1873            let on = it.dom.borrow_mut().class_toggle(idx, &cls);
1874            return Ok(Value::Bool(on));
1875        }
1876        let force = arg(a, 1).truthy();
1877        if force {
1878            it.dom.borrow_mut().class_add(idx, &cls);
1879        } else {
1880            it.dom.borrow_mut().class_remove(idx, &cls);
1881        }
1882        return Ok(Value::Bool(force));
1883    }
1884    Ok(Value::Bool(false))
1885}
1886
1887pub fn dom_classlist_contains(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
1888    if let Some(idx) = this_host_idx(&this, "classList:") {
1889        return Ok(Value::Bool(
1890            it.dom
1891                .borrow()
1892                .class_contains(idx, &arg(a, 0).to_js_string()),
1893        ));
1894    }
1895    Ok(Value::Bool(false))
1896}
1897
1898/// `classList.entries()`/`.keys()`/`.values()`(丸ごと未対応だった。`DOMTokenList`
1899/// は仕様上 `forEach`/`Symbol.iterator` に加えこの3メソッドも持つが、`forEach`と
1900/// for-of 反復(`iterable_values`)のみ配線され、これらの明示イテレータ取得
1901/// メソッドは未配線だった。`Array.prototype` の同名メソッドと同じく既存の
1902/// `make_iterator` ヘルパーへ委譲する薄い実装。
1903pub fn dom_classlist_entries(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1904    if let Some(idx) = this_host_idx(&this, "classList:") {
1905        let classes = it.dom.borrow().nodes.get(idx).map(|n| n.classes.clone()).unwrap_or_default();
1906        let out: Vec<Value> = classes
1907            .into_iter()
1908            .enumerate()
1909            .map(|(i, c)| Value::Object(Obj::array(alloc::vec![Value::Number(i as f64), Value::str(c)])))
1910            .collect();
1911        return Ok(Value::Object(make_iterator(out)));
1912    }
1913    Ok(Value::Object(make_iterator(alloc::vec![])))
1914}
1915pub fn dom_classlist_keys(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1916    if let Some(idx) = this_host_idx(&this, "classList:") {
1917        let n = it.dom.borrow().nodes.get(idx).map(|n| n.classes.len()).unwrap_or(0);
1918        return Ok(Value::Object(make_iterator(
1919            (0..n).map(|i| Value::Number(i as f64)).collect(),
1920        )));
1921    }
1922    Ok(Value::Object(make_iterator(alloc::vec![])))
1923}
1924pub fn dom_classlist_values(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
1925    if let Some(idx) = this_host_idx(&this, "classList:") {
1926        let classes = it.dom.borrow().nodes.get(idx).map(|n| n.classes.clone()).unwrap_or_default();
1927        return Ok(Value::Object(make_iterator(
1928            classes.into_iter().map(Value::str).collect(),
1929        )));
1930    }
1931    Ok(Value::Object(make_iterator(alloc::vec![])))
1932}
1933
1934pub(crate) fn document_create_element(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1935    let tag = arg(a, 0).to_js_string();
1936    let idx = it.dom.borrow_mut().create_element(&tag);
1937    Ok(Value::Object(Obj::dom(idx)))
1938}
1939/// `document.createElementNS(namespaceURI, qualifiedName)`(丸ごと未対応
1940/// だった。SVG 要素(`document.createElementNS('http://www.w3.org/2000/svg',
1941/// 'circle')` 等)を JS から動的生成する標準的な定番パターン)。この処理系は
1942/// `*AttributeNS` 系と同じく XML 名前空間を一切モデル化していないため、
1943/// 第1引数(namespace)は無視して非 NS 版の `createElement` へ委譲する
1944/// 簡略実装(多くの実用コードは SVG 名前空間しか渡さないため実害は小さい)。
1945pub(crate) fn document_create_element_ns(it: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
1946    document_create_element(it, t, &[arg(a, 1)])
1947}
1948
1949/// `new Option(text?, value?, defaultSelected?, selected?)`(HTML5。丸ごと
1950/// 未対応だった)。`document.createElement('option')` を土台に組み立てる。
1951pub(crate) fn option_ctor(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1952    let idx = it.dom.borrow_mut().create_element("option");
1953    let text = arg(a, 0);
1954    if !matches!(text, Value::Undefined) {
1955        it.dom.borrow_mut().set_text_content(idx, &text.to_js_string());
1956    }
1957    let value = arg(a, 1);
1958    if !matches!(value, Value::Undefined) {
1959        it.dom.borrow_mut().set_attr(idx, "value", &value.to_js_string());
1960    }
1961    if arg(a, 2).truthy() || arg(a, 3).truthy() {
1962        it.dom.borrow_mut().set_attr(idx, "selected", "selected");
1963    }
1964    Ok(Value::Object(Obj::dom(idx)))
1965}
1966
1967pub(crate) fn document_create_text_node(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1968    let text = arg(a, 0).to_js_string();
1969    let idx = it.dom.borrow_mut().create_text_node(&text);
1970    Ok(Value::Object(Obj::dom(idx)))
1971}
1972
1973pub(crate) fn document_create_comment(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
1974    let text = arg(a, 0).to_js_string();
1975    let idx = it.dom.borrow_mut().create_comment(&text);
1976    Ok(Value::Object(Obj::dom(idx)))
1977}
1978
1979pub(crate) fn document_create_document_fragment(it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
1980    let idx = it.dom.borrow_mut().create_document_fragment();
1981    Ok(Value::Object(Obj::dom(idx)))
1982}
1983
1984pub(crate) fn split_css_rules(css: &str) -> alloc::vec::Vec<String> {
1985    let mut rules = alloc::vec![];
1986    let mut current_rule = String::new();
1987    let mut depth = 0;
1988    for c in css.chars() {
1989        current_rule.push(c);
1990        if c == '{' {
1991            depth += 1;
1992        } else if c == '}' {
1993            if depth > 0 {
1994                depth -= 1;
1995            }
1996            if depth == 0 {
1997                let trimmed = current_rule.trim();
1998                if !trimmed.is_empty() {
1999                    rules.push(trimmed.to_string());
2000                }
2001                current_rule.clear();
2002            }
2003        }
2004    }
2005    let trimmed = current_rule.trim();
2006    if !trimmed.is_empty() {
2007        rules.push(trimmed.to_string());
2008    }
2009    rules
2010}
2011
2012pub(crate) fn dom_stylesheet_insert_rule(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2013    let rule_text = arg(a, 0).to_js_string();
2014    let insert_index = arg(a, 1).to_number() as usize;
2015    if let Value::Object(o) = this {
2016        if let ObjKind::Host(t) = &o.borrow().kind {
2017            if let Some(rest) = t.strip_prefix("stylesheet:") {
2018                let idx: usize = rest.parse().unwrap_or(usize::MAX);
2019                let text = it.dom.borrow().get_text_content(idx);
2020                let mut rules = split_css_rules(&text);
2021                let actual_index = insert_index.min(rules.len());
2022                rules.insert(actual_index, rule_text);
2023                let new_css = rules.join("\n");
2024                it.dom.borrow_mut().set_text_content(idx, &new_css);
2025                return Ok(Value::Number(actual_index as f64));
2026            }
2027        }
2028    }
2029    Ok(Value::Number(0.0))
2030}
2031
2032pub(crate) fn dom_stylesheet_delete_rule(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2033    let delete_index = arg(a, 0).to_number() as usize;
2034    if let Value::Object(o) = this {
2035        if let ObjKind::Host(t) = &o.borrow().kind {
2036            if let Some(rest) = t.strip_prefix("stylesheet:") {
2037                let idx: usize = rest.parse().unwrap_or(usize::MAX);
2038                let text = it.dom.borrow().get_text_content(idx);
2039                let mut rules = split_css_rules(&text);
2040                if delete_index < rules.len() {
2041                    rules.remove(delete_index);
2042                    let new_css = rules.join("\n");
2043                    it.dom.borrow_mut().set_text_content(idx, &new_css);
2044                }
2045            }
2046        }
2047    }
2048    Ok(Value::Undefined)
2049}
2050
2051pub fn dom_append_child(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2052    let child = arg(a, 0);
2053    if let (Some(p), Some(c)) = (this_dom_idx(&this), this_dom_idx(&child)) {
2054        it.dom.borrow_mut().append_child(p, c);
2055    }
2056    Ok(child) // appendChild は追加した子を返す。
2057}
2058
2059/// parent.insertBefore(newChild, refChild)。ref が null/undefined なら末尾。
2060pub fn dom_insert_before(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2061    let new_child = arg(a, 0);
2062    if let (Some(p), Some(nc)) = (this_dom_idx(&this), this_dom_idx(&new_child)) {
2063        let rc = this_dom_idx(&arg(a, 1)); // null/undefined → None
2064        if let Some(r) = rc {
2065            let is_child =
2066                it.dom.borrow().nodes.get(r).and_then(|n| n.parent) == Some(p);
2067            if !is_child {
2068                return Err(make_dom_exception(
2069                    "NotFoundError",
2070                    "The node before which the new node is to be inserted is not a child of this node",
2071                ));
2072            }
2073        }
2074        it.dom.borrow_mut().insert_before(p, nc, rc);
2075    }
2076    Ok(new_child)
2077}
2078
2079/// `parent.moveBefore(movedNode, referenceNode)`(DOM。2025年にChromeへ
2080/// 実装が入った比較的新しいAPI。丸ごと未対応だった)。仕様上の主眼は
2081/// 「`remove()`してから`insertBefore()`する」場合と異なり、`<iframe>`の
2082/// 読み込み状態・`<video>`の再生状態・カスタム要素のライフサイクル
2083/// コールバック発火等を保持したまま木構造内を移動できる点にあるが、
2084/// この処理系にはそれらの「保持すべき生きた状態」自体が実装されて
2085/// いない(`<iframe>`は実際のナビゲーション状態を持たず、`<video>`は
2086/// 実再生をしない等)ため、実務上は`insertBefore`と同じ「detachしてから
2087/// 指定位置へ挿入」の`insert_before`(`dom_bridge.rs`。既に自動で元の
2088/// 親から外す実装)をそのまま再利用する誠実な簡略実装で十分に仕様どおり
2089/// 動作する。`referenceNode`(`null`可)が`this`の子でない場合は
2090/// `insertBefore`と同じ`NotFoundError`規則を踏襲。2026-07-18 発見・実装。
2091pub fn dom_move_before(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2092    let moved_node = arg(a, 0);
2093    if let (Some(p), Some(mn)) = (this_dom_idx(&this), this_dom_idx(&moved_node)) {
2094        let rc = this_dom_idx(&arg(a, 1));
2095        if let Some(r) = rc {
2096            let is_child = it.dom.borrow().nodes.get(r).and_then(|n| n.parent) == Some(p);
2097            if !is_child {
2098                return Err(make_dom_exception(
2099                    "NotFoundError",
2100                    "The node before which the moved node is to be inserted is not a child of this node",
2101                ));
2102            }
2103        }
2104        it.dom.borrow_mut().insert_before(p, mn, rc);
2105    }
2106    Ok(moved_node)
2107}
2108
2109/// `select.options.add(option, before?)`(HTML5 `HTMLOptionsCollection`。
2110/// 動的にドロップダウンを構築する `select.options.add(new Option(...))` という
2111/// 定番イディオムが、`options` が素の配列(メソッドを一切持たない)を返す
2112/// だけだったため丸ごと未対応(`TypeError: not a function`)だった。`options`
2113/// 取得時に配列オブジェクト自身へ `_select_idx`(隠しプロパティ。既存の
2114/// `_is_headers`/`_ta_kind` 等と同じ内部タグ付けパターン)を仕込んでおき、
2115/// この配列を経由して元の `<select>` を辿れるようにする。`before` は仕様どおり
2116/// 省略時は末尾追加、数値ならその位置の既存 option の前、`<option>` 要素
2117/// そのものでもよい。
2118pub fn select_options_add(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2119    let select_idx = match &this {
2120        Value::Object(o) => o.borrow().props.get("_select_idx").map(|v| v.to_number() as usize),
2121        _ => None,
2122    };
2123    let (Some(select_idx), Some(opt_idx)) = (select_idx, this_dom_idx(&arg(a, 0))) else {
2124        return Ok(Value::Undefined);
2125    };
2126    let before = arg(a, 1);
2127    let ref_idx = match &before {
2128        Value::Number(n) => it.dom.borrow().select_options(select_idx).get(*n as usize).copied(),
2129        Value::Object(_) => this_dom_idx(&before),
2130        _ => None,
2131    };
2132    it.dom.borrow_mut().insert_before(select_idx, opt_idx, ref_idx);
2133    Ok(Value::Undefined)
2134}
2135/// `select.add(element, before)`(`HTMLSelectElement`直下の便利メソッド。
2136/// `select.options.add`と全く同じ意味だが、`this`が`options`コレクション
2137/// 配列ではなく`<select>`要素自身になる点だけが違う。丸ごと未対応
2138/// だった。`select_options_add`とほぼ同じロジックだが、`_select_idx`
2139/// 隠しプロパティ経由ではなく`this_dom_idx`で直接`<select>`のインデックス
2140/// を取る。2026-07-18 発見・実装)。
2141pub fn dom_select_add(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2142    let (Some(select_idx), Some(opt_idx)) = (this_dom_idx(&this), this_dom_idx(&arg(a, 0))) else {
2143        return Ok(Value::Undefined);
2144    };
2145    let before = arg(a, 1);
2146    let ref_idx = match &before {
2147        Value::Number(n) => it.dom.borrow().select_options(select_idx).get(*n as usize).copied(),
2148        Value::Object(_) => this_dom_idx(&before),
2149        _ => None,
2150    };
2151    it.dom.borrow_mut().insert_before(select_idx, opt_idx, ref_idx);
2152    Ok(Value::Undefined)
2153}
2154/// `select.options.remove(index)`(`HTMLOptionsCollection`。上記 `add` と対に
2155/// なる、指定インデックスの `<option>` を取り除く定番メソッド)。
2156pub fn select_options_remove(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2157    let select_idx = match &this {
2158        Value::Object(o) => o.borrow().props.get("_select_idx").map(|v| v.to_number() as usize),
2159        _ => None,
2160    };
2161    let Some(select_idx) = select_idx else {
2162        return Ok(Value::Undefined);
2163    };
2164    let n = arg(a, 0).to_number() as usize;
2165    // `if let Some(x) = obj.borrow()...` は scrutinee の一時 `Ref` が if-let ブロック
2166    // 終端まで生き続けるため、ブロック内で `borrow_mut()` すると「RefCell already
2167    // borrowed」で panic する(このファイルの `report_error` 修正と同型のバグ。
2168    // 一度 owned な値へ materialize してから `if let` することで回避する)。
2169    let opt_idx = it.dom.borrow().select_options(select_idx).get(n).copied();
2170    if let Some(opt_idx) = opt_idx {
2171        it.dom.borrow_mut().remove_child(select_idx, opt_idx);
2172    }
2173    Ok(Value::Undefined)
2174}
2175/// `select.options.namedItem(name)`(`HTMLOptionsCollection`。`id`/`name`
2176/// 属性が一致する `<option>` を探す定番メソッド。見つからなければ仕様どおり
2177/// `null`)。
2178pub fn select_options_named_item(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::Null);
2185    };
2186    let name = arg(a, 0).to_js_string();
2187    let dom = it.dom.borrow();
2188    for opt_idx in dom.select_options(select_idx) {
2189        if dom.get_attr(opt_idx, "id").as_deref() == Some(name.as_str())
2190            || dom.get_attr(opt_idx, "name").as_deref() == Some(name.as_str())
2191        {
2192            return Ok(Value::Object(Obj::dom(opt_idx)));
2193        }
2194    }
2195    Ok(Value::Null)
2196}
2197
2198/// parent.replaceChild(newChild, oldChild)。置換された old を返す。
2199pub fn dom_replace_child(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2200    let old_child = arg(a, 1);
2201    if let (Some(p), Some(nc), Some(oc)) = (
2202        this_dom_idx(&this),
2203        this_dom_idx(&arg(a, 0)),
2204        this_dom_idx(&old_child),
2205    ) {
2206        // `insertBefore`と同じ理由(詳細はそちらの同種コメント参照)で、
2207        // `oldChild`が`this`の子でない場合は仕様上`NotFoundError`を投げる
2208        // べきだが、以前は`replace_child`(`dom_bridge.rs`)が黙って何もせず
2209        // 成功したかのように振る舞っていた。
2210        let is_child = it.dom.borrow().nodes.get(oc).and_then(|n| n.parent) == Some(p);
2211        if !is_child {
2212            return Err(make_dom_exception(
2213                "NotFoundError",
2214                "The node to be replaced is not a child of this node",
2215            ));
2216        }
2217        it.dom.borrow_mut().replace_child(p, nc, oc);
2218    }
2219    Ok(old_child)
2220}
2221
2222/// 引数を DOM ノード idx に解決(要素はそのまま、文字列はテキストノード生成)。
2223pub(crate) fn arg_to_node(it: &mut Interp, v: &Value) -> Option<usize> {
2224    match this_dom_idx(v) {
2225        Some(i) => Some(i),
2226        None => match v {
2227            Value::Undefined | Value::Null => None,
2228            other => Some(it.dom.borrow_mut().create_text_node(&other.to_js_string())),
2229        },
2230    }
2231}
2232
2233/// element.insertAdjacentHTML(position, html)。
2234/// `element.getHTML(options?)`(DOM。`innerHTML`ゲッターの明示的メソッド版
2235/// として2024年頃に標準化。丸ごと未対応だった)。仕様上の`options`
2236/// (`{serializableShadowRoots, shadowRoots}`)はShadow DOM自体が
2237/// この処理系に存在しないため意味を持たず無視する。`innerHTML`と全く同じ
2238/// `get_inner_html`をそのまま呼ぶだけの簡略実装。2026-07-18 発見・実装。
2239pub fn dom_get_html(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2240    if let Some(idx) = this_dom_idx(&this) {
2241        return Ok(Value::str(it.dom.borrow().get_inner_html(idx)));
2242    }
2243    Ok(Value::str(""))
2244}
2245
2246/// `element.setHTMLUnsafe(html)`(DOM。`innerHTML`セッターのTrusted
2247/// Types検証を明示的にバイパスする版として標準化。丸ごと未対応だった)。
2248/// この処理系にはTrusted Typesの強制自体が実装されていないため、
2249/// `innerHTML`セッターと全く同じ`set_inner_html`をそのまま呼ぶだけの
2250/// 簡略実装で仕様上の可視的な違いは無い。2026-07-18 発見・実装。
2251pub fn dom_set_html_unsafe(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2252    if let Some(idx) = this_dom_idx(&this) {
2253        let html = arg(a, 0).to_js_string();
2254        it.dom.borrow_mut().set_inner_html(idx, &html);
2255    }
2256    Ok(Value::Undefined)
2257}
2258
2259pub fn dom_insert_adjacent_html(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2260    if let Some(idx) = this_dom_idx(&this) {
2261        // `position` は仕様上 ASCII 大文字小文字を無視して照合すべきだが
2262        // (`"beforeBegin"`/`"BEFOREEND"` 等も有効)、以前は完全一致のみで
2263        // 小文字以外を渡すと静かに no-op になっていた。
2264        let pos = arg(a, 0).to_js_string().to_lowercase();
2265        let html = arg(a, 1).to_js_string();
2266        it.dom.borrow_mut().insert_adjacent_html(idx, &pos, &html);
2267    }
2268    Ok(Value::Undefined)
2269}
2270/// element.insertAdjacentElement(position, element)。
2271pub fn dom_insert_adjacent_element(
2272    it: &mut Interp,
2273    this: Value,
2274    a: &[Value],
2275) -> Result<Value, Value> {
2276    let el = arg(a, 1);
2277    if let (Some(idx), Some(node)) = (this_dom_idx(&this), this_dom_idx(&el)) {
2278        let pos = arg(a, 0).to_js_string().to_lowercase();
2279        it.dom.borrow_mut().insert_adjacent_node(idx, &pos, node);
2280    }
2281    Ok(el)
2282}
2283/// element.insertAdjacentText(position, text)。
2284pub fn dom_insert_adjacent_text(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2285    if let Some(idx) = this_dom_idx(&this) {
2286        let pos = arg(a, 0).to_js_string().to_lowercase();
2287        let node = it
2288            .dom
2289            .borrow_mut()
2290            .create_text_node(&arg(a, 1).to_js_string());
2291        it.dom.borrow_mut().insert_adjacent_node(idx, &pos, node);
2292    }
2293    Ok(Value::Undefined)
2294}
2295/// element.toggleAttribute(name[, force]) → 操作後の存在を返す。
2296pub fn dom_toggle_attribute(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2297    if let Some(idx) = this_dom_idx(&this) {
2298        let name = arg(a, 0).to_js_string();
2299        let has = it.dom.borrow().has_attr(idx, &name);
2300        let new_state = if a.len() > 1 && !matches!(arg(a, 1), Value::Undefined) {
2301            arg(a, 1).truthy()
2302        } else {
2303            !has
2304        };
2305        if new_state {
2306            it.dom.borrow_mut().set_attr(idx, &name, "");
2307        } else {
2308            it.dom.borrow_mut().remove_attr(idx, &name);
2309        }
2310        return Ok(Value::Bool(new_state));
2311    }
2312    Ok(Value::Bool(false))
2313}
2314
2315/// `dialog.show()`(`<dialog>` が丸ごと未対応だった)。`open` 属性を付ける
2316/// だけの簡略実装。
2317pub fn dom_dialog_show(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2318    if let Some(idx) = this_dom_idx(&this) {
2319        it.dom.borrow_mut().set_attr(idx, "open", "open");
2320    }
2321    Ok(Value::Undefined)
2322}
2323
2324/// `dialog.showModal()`。以前は`.show()`と全く同じ`dom_dialog_show`を共有して
2325/// おり、モーダル/非モーダルの区別自体が存在しなかった(`showModal()`本来の
2326/// モーダルフォーカストラップ・`::backdrop`描画は依然対象外だが対象外)。CSS
2327/// `:modal`疑似クラス(丸ごと未対応だった。2026-07-18 発見・実装)が判定
2328/// できるよう、`open`属性に加え`:fullscreen`と同じ内部専用属性パターンで
2329/// `_modal`を書き込む点だけが`.show()`と異なる。
2330pub fn dom_dialog_show_modal(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2331    if let Some(idx) = this_dom_idx(&this) {
2332        let mut dom = it.dom.borrow_mut();
2333        dom.set_attr(idx, "open", "open");
2334        dom.set_attr(idx, "_modal", "1");
2335    }
2336    Ok(Value::Undefined)
2337}
2338
2339/// `dialog.close(returnValue?)`。`open` 属性を外し、`returnValue` を設定してから
2340/// `close` イベントを発火する(仕様どおり)。DOM 要素は汎用の `Obj.props` ではなく
2341/// `interp.rs` 側の明示的なプロパティ一覧を介してアクセスされるため、`checked`/
2342/// `indeterminate` と同じ「内部専用の `_` プレフィックス属性キー」パターン
2343/// (`_return_value`)で持たせる(`this.props` へ直接書いても読み出し経路が
2344/// 無く反映されない)。
2345pub fn dom_dialog_close(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2346    if let Some(idx) = this_dom_idx(&this) {
2347        {
2348            let mut dom = it.dom.borrow_mut();
2349            dom.remove_attr(idx, "open");
2350            dom.remove_attr(idx, "_modal");
2351        }
2352        if !a.is_empty() {
2353            it.dom
2354                .borrow_mut()
2355                .set_attr(idx, "_return_value", &arg(a, 0).to_js_string());
2356        }
2357        let _ = it.dispatch_event_in_interp(idx, "close", &[]);
2358    }
2359    Ok(Value::Undefined)
2360}
2361
2362// `dialog.requestClose(returnValue?)`(HTML Living Standard。`.close()`と
2363// 違い、閉じる直前に取消可能な`cancel`イベントを発火し、リスナが
2364// `preventDefault()`すればダイアログを開いたままにできる。丸ごと
2365// 未対応だった。`.close()`自体は無条件クローズのため対になるこちらの
2366// 「ユーザーの閉じる操作を横取り可能にする」経路が漏れていた。
2367// `popover_toggle_extra`と同じ「取消可能イベント→未阻止なら実処理」
2368// パターンを再利用する。2026-07-18 発見・実装)。
2369pub fn dom_dialog_request_close(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2370    if let Some(idx) = this_dom_idx(&this) {
2371        let (_, prevented) = it.dispatch_event_in_interp(idx, "cancel", &[]);
2372        if !prevented {
2373            {
2374                let mut dom = it.dom.borrow_mut();
2375                dom.remove_attr(idx, "open");
2376                dom.remove_attr(idx, "_modal");
2377            }
2378            if !a.is_empty() {
2379                it.dom
2380                    .borrow_mut()
2381                    .set_attr(idx, "_return_value", &arg(a, 0).to_js_string());
2382            }
2383            let _ = it.dispatch_event_in_interp(idx, "close", &[]);
2384        }
2385    }
2386    Ok(Value::Undefined)
2387}
2388
2389/// `element.showPopover()`/`.hidePopover()`/`.togglePopover(force?)`(Popover
2390/// API。丸ごと未対応だった)。`<dialog>` の `open` 属性と同じく、内部専用属性
2391/// (`_popover_open`)を付け外しするだけの簡略実装(`css.rs` 側の UA 既定
2392/// display 切り替えと対)。`beforetoggle`/`toggle` イベント発火・
2393/// `:popover-open` 疑似クラス・ライトディスミスは対象外。
2394// `showPopover`/`hidePopover`/`togglePopover`が状態変化を`toggle`イベント
2395// として発火していなかった(丸ごと未対応だった。`<dialog>.close()`は既に
2396// `"close"`イベントを発火していたのに、同じ「開閉状態変化を通知する」
2397// 役目のPopover側だけ漏れていた非対称ギャップ。2026-07-17 発見)。仕様上は
2398// `oldState`/`newState`を持つ`ToggleEvent`だが、この処理系に`ToggleEvent`
2399// コンストラクタ自体が無いため、`<details>`の`toggle`イベント修正と同じ
2400// 簡略方針で`dispatch_event_in_interp`にプレーンな`Event`へ同名プロパティ
2401// を追加する形(`extra`引数)で代用する。対になる取消可能な`beforetoggle`
2402// (状態変更「前」に発火し`preventDefault()`で開閉を中止できる。同じく
2403// 丸ごと未対応だった)も同時に実装。
2404fn popover_toggle_extra(old_open: bool, new_open: bool) -> alloc::vec::Vec<(String, Value)> {
2405    alloc::vec![
2406        (String::from("oldState"), Value::str(if old_open { "open" } else { "closed" })),
2407        (String::from("newState"), Value::str(if new_open { "open" } else { "closed" })),
2408    ]
2409}
2410pub fn dom_show_popover(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2411    if let Some(idx) = this_dom_idx(&this) {
2412        let was_open = it.dom.borrow().has_attr(idx, "_popover_open");
2413        // `beforetoggle`(状態変更「前」に発火する取消可能なイベント。
2414        // `preventDefault()`で開閉自体を中止できる。丸ごと未対応だった。
2415        // 2026-07-17 発見・実装)。
2416        let (_, prevented) =
2417            it.dispatch_event_in_interp(idx, "beforetoggle", &popover_toggle_extra(was_open, true));
2418        if prevented {
2419            return Ok(Value::Undefined);
2420        }
2421        it.dom.borrow_mut().set_attr(idx, "_popover_open", "");
2422        let _ = it.dispatch_event_in_interp(idx, "toggle", &popover_toggle_extra(was_open, true));
2423    }
2424    Ok(Value::Undefined)
2425}
2426pub fn dom_hide_popover(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2427    if let Some(idx) = this_dom_idx(&this) {
2428        let was_open = it.dom.borrow().has_attr(idx, "_popover_open");
2429        let (_, prevented) =
2430            it.dispatch_event_in_interp(idx, "beforetoggle", &popover_toggle_extra(was_open, false));
2431        if prevented {
2432            return Ok(Value::Undefined);
2433        }
2434        it.dom.borrow_mut().remove_attr(idx, "_popover_open");
2435        let _ = it.dispatch_event_in_interp(idx, "toggle", &popover_toggle_extra(was_open, false));
2436    }
2437    Ok(Value::Undefined)
2438}
2439pub fn dom_toggle_popover(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2440    if let Some(idx) = this_dom_idx(&this) {
2441        let was_open = it.dom.borrow().has_attr(idx, "_popover_open");
2442        let force = if matches!(arg(a, 0), Value::Undefined) {
2443            !was_open
2444        } else {
2445            arg(a, 0).truthy()
2446        };
2447        let (_, prevented) =
2448            it.dispatch_event_in_interp(idx, "beforetoggle", &popover_toggle_extra(was_open, force));
2449        if prevented {
2450            return Ok(Value::Bool(was_open));
2451        }
2452        if force {
2453            it.dom.borrow_mut().set_attr(idx, "_popover_open", "");
2454        } else {
2455            it.dom.borrow_mut().remove_attr(idx, "_popover_open");
2456        }
2457        let _ = it.dispatch_event_in_interp(idx, "toggle", &popover_toggle_extra(was_open, force));
2458        return Ok(Value::Bool(force));
2459    }
2460    Ok(Value::Bool(false))
2461}
2462
2463/// element.append(...nodes):末尾に複数追加(文字列はテキスト)。
2464/// `element.replaceChildren(...nodes)`(DOM 標準。`el.innerHTML=''; el.append(...)`
2465/// の1行版として使われる定番イディオム)が丸ごと未対応だった。既存の
2466/// `set_inner_html(idx, "")`(子を丸ごとクリア)と `append`/`arg_to_node` と同じ
2467/// 「文字列は text node、DOM ノードはそのまま」変換を組み合わせて実装する。
2468pub fn dom_replace_children(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2469    if let Some(p) = this_dom_idx(&this) {
2470        it.dom.borrow_mut().set_inner_html(p, "");
2471        for v in a {
2472            if let Some(c) = arg_to_node(it, v) {
2473                it.dom.borrow_mut().append_child(p, c);
2474            }
2475        }
2476    }
2477    Ok(Value::Undefined)
2478}
2479pub fn dom_append(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2480    if let Some(p) = this_dom_idx(&this) {
2481        for v in a {
2482            if let Some(c) = arg_to_node(it, v) {
2483                it.dom.borrow_mut().append_child(p, c);
2484            }
2485        }
2486    }
2487    Ok(Value::Undefined)
2488}
2489/// element.prepend(...nodes):先頭に複数追加(順序保持)。
2490pub fn dom_prepend(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2491    if let Some(p) = this_dom_idx(&this) {
2492        let refnode = it
2493            .dom
2494            .borrow()
2495            .nodes
2496            .get(p)
2497            .and_then(|n| n.children.first().copied());
2498        for v in a {
2499            if let Some(c) = arg_to_node(it, v) {
2500                it.dom.borrow_mut().insert_before(p, c, refnode);
2501            }
2502        }
2503    }
2504    Ok(Value::Undefined)
2505}
2506/// element.before(...nodes):自身の前に兄弟として挿入。
2507pub fn dom_before(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2508    if let Some(s) = this_dom_idx(&this) {
2509        let parent = it.dom.borrow().nodes.get(s).and_then(|n| n.parent);
2510        if let Some(p) = parent {
2511            for v in a {
2512                if let Some(c) = arg_to_node(it, v) {
2513                    it.dom.borrow_mut().insert_before(p, c, Some(s));
2514                }
2515            }
2516        }
2517    }
2518    Ok(Value::Undefined)
2519}
2520/// element.after(...nodes):自身の後に兄弟として挿入。
2521pub fn dom_after(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2522    if let Some(s) = this_dom_idx(&this) {
2523        let (parent, next) = {
2524            let dom = it.dom.borrow();
2525            let parent = dom.nodes.get(s).and_then(|n| n.parent);
2526            let next = parent.and_then(|p| dom.nodes.get(p)).and_then(|n| {
2527                n.children
2528                    .iter()
2529                    .position(|&c| c == s)
2530                    .and_then(|i| n.children.get(i + 1).copied())
2531            });
2532            (parent, next)
2533        };
2534        if let Some(p) = parent {
2535            for v in a {
2536                if let Some(c) = arg_to_node(it, v) {
2537                    it.dom.borrow_mut().insert_before(p, c, next);
2538                }
2539            }
2540        }
2541    }
2542    Ok(Value::Undefined)
2543}
2544/// element.replaceWith(...nodes):自身を nodes で置換。
2545pub fn dom_replace_with(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2546    if let Some(s) = this_dom_idx(&this) {
2547        let parent = it.dom.borrow().nodes.get(s).and_then(|n| n.parent);
2548        if let Some(p) = parent {
2549            for v in a {
2550                if let Some(c) = arg_to_node(it, v) {
2551                    it.dom.borrow_mut().insert_before(p, c, Some(s));
2552                }
2553            }
2554            it.dom.borrow_mut().remove_node(s);
2555        }
2556    }
2557    Ok(Value::Undefined)
2558}
2559
2560/// node.cloneNode(deep)。複製ノード(親なし)を返す。
2561pub fn dom_clone_node(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2562    if let Some(idx) = this_dom_idx(&this) {
2563        let deep = arg(a, 0).truthy();
2564        let new_idx = it.dom.borrow_mut().clone_node(idx, deep);
2565        return Ok(dom_handle(Some(new_idx)));
2566    }
2567    Ok(Value::Null)
2568}
2569/// `document.importNode(externalNode, deep)`(丸ごと未対応だった。
2570/// `<template>` の中身を複製して自文書へ取り込む定番パターン
2571/// `document.importNode(template.content, true)` で使われる)。この処理系は
2572/// 単一文書(複数 `Document` オブジェクトという概念自体が無い)のため、
2573/// 「別文書から取り込む」という仕様上の意味は無く、実質 `node.cloneNode(deep)`
2574/// と同じ結果になる簡略実装。仕様上 `deep` は省略時 `false` だが、実務では
2575/// ほぼ常に明示的に `true` が渡される。
2576pub fn document_import_node(it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
2577    dom_clone_node(it, arg(a, 0), a.get(1..).unwrap_or(&[]))
2578}
2579/// `document.adoptNode(node)`(丸ごと未対応だった)。単一文書のこの処理系
2580/// では「別文書から養子縁組する」という意味は無いため、仕様の副作用
2581/// (現在の親から取り外す)のみを再現し、渡されたノードをそのまま返す。
2582pub fn document_adopt_node(it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
2583    let node = arg(a, 0);
2584    if let Some(idx) = this_dom_idx(&node) {
2585        it.dom.borrow_mut().remove_node(idx);
2586    }
2587    Ok(node)
2588}
2589
2590/// `element.click()`。以前は単なる no-op で、実際のクリック
2591/// (`onclick`/`addEventListener('click', ...)` リスナの発火)を一切引き起こさな
2592/// かった。実マウス操作からも呼ばれる既存の `dispatch_click()` を再利用する。
2593/// `checkbox`/`radio` は仕様上クリックすると(リスナの実行前に)自身の選択状態が
2594/// 切り替わる。以前は `element.checked` という JS プロパティ自体が丸ごと未実装
2595/// だったため、この状態切替もできていなかった。**重要**: 同じ `name` を持つ
2596/// ラジオボタン間の排他選択(1つを選ぶと他が自動で解除される、ラジオボタンの
2597/// 最も基本的な仕様上の挙動)も以前は非対応の簡略化として明示的に見送られていたが、
2598/// `uncheck_radio_group_siblings` を新設し配線した。
2599/// `element.scrollTo(x, y)`/`.scrollTo({top, left, behavior})` が丸ごと未対応
2600/// だった。`element.scrollTop` 自体は既に対応済み(`scroll_tops` に反映して再描画
2601/// 要求する実装)なので、それをそのまま呼び出す薄いラッパーとして実装する。
2602/// この処理系に横スクロールモデルは存在しないため `left`/`x` は無視する(`behavior`
2603/// も同様、実時間アニメーションの概念が無いため即座に反映する簡略実装)。
2604pub(crate) fn scroll_top_target(a: &[Value]) -> Option<i32> {
2605    match arg(a, 0) {
2606        Value::Object(_) => obj_prop(&arg(a, 0), "top").map(|v| v.to_number() as i32),
2607        Value::Undefined => None,
2608        _ => Some(arg(a, 1).to_number() as i32),
2609    }
2610}
2611pub fn dom_scroll_to(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2612    if let Some(idx) = this_dom_idx(&this) {
2613        if let Some(top) = scroll_top_target(a) {
2614            let mut dom = it.dom.borrow_mut();
2615            dom.scroll_tops.insert(idx, top.max(0));
2616            dom.dirty = true;
2617        }
2618    }
2619    Ok(Value::Undefined)
2620}
2621/// `element.scrollBy(x, y)`/`.scrollBy({top, left})`。現在値からの相対移動である点
2622/// のみ `scrollTo` と異なる。
2623pub fn dom_scroll_by(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2624    if let Some(idx) = this_dom_idx(&this) {
2625        if let Some(delta) = scroll_top_target(a) {
2626            let mut dom = it.dom.borrow_mut();
2627            let cur = dom.scroll_tops.get(&idx).copied().unwrap_or(0);
2628            dom.scroll_tops.insert(idx, (cur + delta).max(0));
2629            dom.dirty = true;
2630        }
2631    }
2632    Ok(Value::Undefined)
2633}
2634/// `element.scrollIntoView()`/`.scrollIntoView({behavior, block})`が丸ごと
2635/// no-opだった(HTML Standard §"scroll into view"の簡略実装が丸ごと欠落。
2636/// 実サイト www.sugi-lab.net のナビゲーションリンク(`target.scrollIntoView(
2637/// {behavior:'smooth', block:'start'})`)が完全に無効化されており、クリック
2638/// しても一切スクロールしないという実害があった。2026-07-22発見・修正)。
2639/// 既存の`window.scrollTo`と同じ`pending_scroll_abs_y`機構(`render.rs`の
2640/// `draw()`冒頭で消費・`fire_scroll()`呼び出しまで行う、既に動作確認済みの
2641/// 経路)を再利用する。対象要素の現在のビューポート相対Y座標
2642/// (`get_rect`。`getBoundingClientRect`と同じ値)に現在のスクロール量
2643/// (`window.scrollY`)を加算し、文書内の絶対Y座標を求めて要素の上端が
2644/// ビューポート上端に来るようにスクロールする(`block:'start'`相当。
2645/// `center`/`end`/`nearest`は簡略化しすべて`start`と同一視する。`behavior`
2646/// は既存の`scrollTo`と同様、実時間アニメーションの概念が無いため即座に
2647/// 反映する)。
2648pub fn dom_scroll_into_view(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2649    let Some(idx) = this_dom_idx(&this) else {
2650        return Ok(Value::Undefined);
2651    };
2652    let Some((_, y, _, _)) = it.dom.borrow().get_rect(idx) else {
2653        return Ok(Value::Undefined);
2654    };
2655    let current_scroll_y = it
2656        .global
2657        .borrow()
2658        .vars
2659        .get("scrollY")
2660        .map(|v| v.to_number())
2661        .unwrap_or(0.0);
2662    let target_abs_y = (current_scroll_y + y as f64).max(0.0);
2663    it.dom.borrow_mut().pending_scroll_abs_y = Some(target_abs_y as i32);
2664    Ok(Value::Undefined)
2665}
2666pub fn dom_click(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2667    if let Some(idx) = this_dom_idx(&this) {
2668        let ty = it.dom.borrow().get_attr(idx, "type").unwrap_or_default();
2669        match ty.as_str() {
2670            "checkbox" => {
2671                let checked = {
2672                    let dom = it.dom.borrow();
2673                    if let Some(lc) = dom.get_attr(idx, "_live_checked") {
2674                        lc == "true"
2675                    } else {
2676                        dom.has_attr(idx, "checked")
2677                    }
2678                };
2679                let mut dom = it.dom.borrow_mut();
2680                if checked {
2681                    dom.set_attr(idx, "_live_checked", "false");
2682                } else {
2683                    dom.set_attr(idx, "_live_checked", "true");
2684                }
2685            }
2686            "radio" => {
2687                let mut dom = it.dom.borrow_mut();
2688                dom.set_attr(idx, "_live_checked", "true");
2689                dom.uncheck_radio_group_siblings(idx);
2690            }
2691            _ => {}
2692        }
2693        it.dispatch_event_in_interp(idx, "click", &[]);
2694    }
2695    Ok(Value::Undefined)
2696}
2697
2698/// `element.focus()`(HTML5。丸ごと未対応だった。`document.activeElement`
2699/// と対になる書き込み側。以前は `scrollIntoView` と同じ no-op で
2700/// `document.activeElement` が常に `null` 固定になっていた)。`DomBridge::
2701/// focused_idx` を直接更新し、仕様どおりの順序(旧要素の `blur`/
2702/// `focusout` → 新要素の `focus`/`focusin`)でイベントを発火する。レンダラ側
2703/// の `WebEngine::focused_id`(実クリック/`autofocus` 用の文字列キー版状態)
2704/// との同期は対象外(詳細は `DomBridge::focused_idx` のコメント参照)。
2705pub fn dom_focus(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2706    if let Some(idx) = this_dom_idx(&this) {
2707        let prev = it.dom.borrow().focused_idx;
2708        if prev == Some(idx) {
2709            return Ok(Value::Undefined);
2710        }
2711        if let Some(p) = prev {
2712            it.dom.borrow_mut().focused_idx = None;
2713            let _ = it.dispatch_event_in_interp(p, "blur", &[]);
2714            let _ = it.dispatch_event_in_interp(p, "focusout", &[]);
2715        }
2716        it.dom.borrow_mut().focused_idx = Some(idx);
2717        let _ = it.dispatch_event_in_interp(idx, "focus", &[]);
2718        let _ = it.dispatch_event_in_interp(idx, "focusin", &[]);
2719    }
2720    Ok(Value::Undefined)
2721}
2722/// `element.blur()`(HTML5。丸ごと未対応だった。`dom_focus` の対)。自身が
2723/// 現在のフォーカス対象である場合のみ解除して `blur`/`focusout` を発火する
2724/// (仕様どおり、フォーカスされていない要素での `.blur()` は no-op)。
2725pub fn dom_blur(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2726    if let Some(idx) = this_dom_idx(&this) {
2727        let is_focused = it.dom.borrow().focused_idx == Some(idx);
2728        if is_focused {
2729            it.dom.borrow_mut().focused_idx = None;
2730            let _ = it.dispatch_event_in_interp(idx, "blur", &[]);
2731            let _ = it.dispatch_event_in_interp(idx, "focusout", &[]);
2732        }
2733    }
2734    Ok(Value::Undefined)
2735}
2736
2737/// `input.stepUp(n?)`/`.stepDown(n?)`(HTML5。`<input type="number"/"range">`
2738/// で値を `step` 刻みで増減する定番メソッド)が丸ごと未対応だった。`value`/
2739/// `step`/`min`/`max` 属性を読み、`min`/`max` があればクランプしてから
2740/// `value` 属性へ書き戻す(`step="any"`等パース不能な場合は既定値 1 に
2741/// フォールバックする、この処理系の他の数値属性パースと同じ寛容な簡略化)。
2742pub(crate) fn dom_step_by(it: &mut Interp, this: Value, a: &[Value], sign: f64) -> Result<Value, Value> {
2743    if let Some(idx) = this_dom_idx(&this) {
2744        let (cur, step, min, max) = {
2745            let dom = it.dom.borrow();
2746            let raw_val = dom.get_attr(idx, "_live_value")
2747                .or_else(|| dom.get_attr(idx, "value"));
2748            (
2749                raw_val.and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(0.0),
2750                dom.get_attr(idx, "step").and_then(|s| s.trim().parse::<f64>().ok()).unwrap_or(1.0),
2751                dom.get_attr(idx, "min").and_then(|s| s.trim().parse::<f64>().ok()),
2752                dom.get_attr(idx, "max").and_then(|s| s.trim().parse::<f64>().ok()),
2753            )
2754        };
2755        let multiplier = match arg(a, 0) {
2756            Value::Undefined => 1.0,
2757            n => n.to_number(),
2758        };
2759        let mut new_val = cur + sign * step * multiplier;
2760        if let Some(mn) = min {
2761            if new_val < mn {
2762                new_val = mn;
2763            }
2764        }
2765        if let Some(mx) = max {
2766            if new_val > mx {
2767                new_val = mx;
2768            }
2769        }
2770        let str_val = Value::Number(new_val).to_js_string();
2771        it.dom.borrow_mut().set_attr(idx, "_live_value", &str_val);
2772    }
2773    Ok(Value::Undefined)
2774}
2775pub fn dom_step_up(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2776    dom_step_by(it, this, a, 1.0)
2777}
2778pub fn dom_step_down(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2779    dom_step_by(it, this, a, -1.0)
2780}
2781
2782/// `form.requestSubmit(submitter?)`(HTML5)が丸ごと未対応だった。既存の
2783/// `dom_click` と同じ `dispatch_event_in_interp` 経由で `submit` イベントを
2784/// 発火する(`preventDefault()` で中止可能。`submitter` 引数は現状無視)。
2785pub fn dom_request_submit(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2786    if let Some(idx) = this_dom_idx(&this) {
2787        it.dispatch_event_in_interp(idx, "submit", &[]);
2788    }
2789    Ok(Value::Undefined)
2790}
2791
2792/// `node.contains(other)`(DOM 標準。`other` が `node` 自身または子孫かどうか。
2793/// click-outside 判定などで広く使われる)が丸ごと未対応だった。既存の
2794/// `is_ancestor_of()` は「`idx` 自身は含まない」仕様のため、仕様どおり自身も
2795/// 含めるにはここで等価チェックを補う。
2796pub fn dom_contains(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2797    let other = arg(a, 0);
2798    if let (Some(anc), Some(desc)) = (this_dom_idx(&this), this_dom_idx(&other)) {
2799        let contains = anc == desc || it.dom.borrow().is_ancestor_of(anc, desc);
2800        return Ok(Value::Bool(contains));
2801    }
2802    Ok(Value::Bool(false))
2803}
2804
2805/// `node.getRootNode()`(DOM標準。`el.getRootNode() === document` という
2806/// 「接続済みか」の定番判定や、shadow DOM 対応コードの分岐で使われる)が丸ごと
2807/// 未対応だった。この処理系は shadow DOM を持たないため、親を辿れる限り辿った
2808/// 最上位ノードを返す単純な実装で仕様の意図(最も遠い祖先)を満たす。
2809pub fn dom_get_root_node(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2810    if let Some(idx) = this_dom_idx(&this) {
2811        let mut cur = idx;
2812        let mut guard = 0;
2813        loop {
2814            let parent = it.dom.borrow().nodes.get(cur).and_then(|n| n.parent);
2815            match parent {
2816                Some(p) => {
2817                    cur = p;
2818                    guard += 1;
2819                    if guard > it.dom.borrow().nodes.len() {
2820                        break;
2821                    }
2822                }
2823                None => break,
2824            }
2825        }
2826        if cur == 0 {
2827            if let Some(doc) = it.global.borrow().vars.get("document").cloned() {
2828                return Ok(doc);
2829            }
2830        }
2831        return Ok(Value::Object(Obj::dom(cur)));
2832    }
2833    if matches!(this, Value::Object(_)) {
2834        if let Ok(parent) = it.get_property(&this, "parentNode") {
2835            if !matches!(parent, Value::Null | Value::Undefined) {
2836                return dom_get_root_node(it, parent, _a);
2837            }
2838        }
2839        return Ok(this);
2840    }
2841    Ok(Value::Undefined)
2842}
2843
2844/// `element.checkVisibility()`(DOM 標準。丸ごと未対応だった)。オプション引数
2845/// (`checkOpacity`/`checkVisibilityCSS`/`contentVisibilityAuto` 等)は未対応で
2846/// 常に無視する簡略実装。自身の算出 `visibility` が `hidden`/`collapse` の場合、
2847/// および自身または祖先のいずれかの算出 `display` が `none` の場合に `false` を返す。
2848/// `node.lookupNamespaceURI(prefix)`/`.lookupPrefix(namespace)`/
2849/// `.isDefaultNamespace(namespace)`(丸ごと未対応だった。2026-07-16
2850/// 発見)。この処理系は `*AttributeNS`/`createElementNS` と同じく XML
2851/// 名前空間を一切モデル化していない(`document_create_element_ns` は
2852/// namespace 引数を無視して委譲するのみ)ため、`xmlns`/`xmlns:*` 属性を
2853/// 祖先チェーンから探索する誠実な実装は行わず、常に「名前空間なし」を表す
2854/// 値(`lookupNamespaceURI`/`lookupPrefix` は `null`)を返す簡略実装。
2855/// `isDefaultNamespace(namespace)` は仕様上 `lookupNamespaceURI(null) ===
2856/// namespace` と等価なので、`lookupNamespaceURI` が常に `null` を返す
2857/// このモデルでは `namespace` が `null`/`undefined` の時のみ `true` になる。
2858pub fn dom_lookup_namespace_uri(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
2859    Ok(Value::Null)
2860}
2861
2862pub fn dom_lookup_prefix(_it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
2863    Ok(Value::Null)
2864}
2865
2866pub fn dom_is_default_namespace(_it: &mut Interp, _this: Value, a: &[Value]) -> Result<Value, Value> {
2867    Ok(Value::Bool(matches!(arg(a, 0), Value::Null | Value::Undefined)))
2868}
2869
2870pub fn dom_check_visibility(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
2871    if let Some(idx) = this_dom_idx(&this) {
2872        let visibility = it.dom.borrow().get_computed(idx, "visibility");
2873        if visibility == "hidden" || visibility == "collapse" {
2874            return Ok(Value::Bool(false));
2875        }
2876        let mut cur = idx;
2877        let mut guard = 0;
2878        loop {
2879            if it.dom.borrow().get_computed(cur, "display") == "none" {
2880                return Ok(Value::Bool(false));
2881            }
2882            let parent = it.dom.borrow().nodes.get(cur).and_then(|n| n.parent);
2883            match parent {
2884                Some(p) => {
2885                    cur = p;
2886                    guard += 1;
2887                    if guard > it.dom.borrow().nodes.len() {
2888                        break;
2889                    }
2890                }
2891                None => break,
2892            }
2893        }
2894        return Ok(Value::Bool(true));
2895    }
2896    Ok(Value::Bool(false))
2897}
2898
2899/// `node.compareDocumentPosition(other)`(DOM標準)が丸ごと未対応だった。
2900/// `DomBridge::document_position` にビットマスク計算を委譲する。`other` が DOM ノードで
2901/// なければ仕様上 `TypeError` だが、この処理系では簡略化して disconnected 扱いにする。
2902pub fn dom_compare_document_position(
2903    it: &mut Interp,
2904    this: Value,
2905    a: &[Value],
2906) -> Result<Value, Value> {
2907    let other = arg(a, 0);
2908    if let (Some(this_idx), Some(other_idx)) = (this_dom_idx(&this), this_dom_idx(&other)) {
2909        let mask = it.dom.borrow().document_position(this_idx, other_idx);
2910        return Ok(Value::Number(mask as f64));
2911    }
2912    // DOM ノードでない引数(この処理系では TypeError の代わりに DISCONNECTED 扱い)。
2913    Ok(Value::Number(1.0))
2914}
2915
2916/// classList.replace(oldClass, newClass)。old があれば new に置換し true、無ければ false。
2917pub fn dom_classlist_replace(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2918    if let Some(idx) = this_host_idx(&this, "classList:") {
2919        let old = arg(a, 0).to_js_string();
2920        let new = arg(a, 1).to_js_string();
2921        let mut dom = it.dom.borrow_mut();
2922        let had = dom.class_contains(idx, &old);
2923        if had {
2924            dom.class_remove(idx, &old);
2925            dom.class_add(idx, &new);
2926        }
2927        return Ok(Value::Bool(had));
2928    }
2929    Ok(Value::Bool(false))
2930}
2931
2932pub fn dom_remove_child(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2933    let child = arg(a, 0);
2934    if let (Some(p), Some(c)) = (this_dom_idx(&this), this_dom_idx(&child)) {
2935        // `insertBefore`/`replaceChild`と同じ理由(詳細はそちらの同種
2936        // コメント参照)で、`child`が`this`の子でない場合は仕様上
2937        // `NotFoundError`を投げるべきだが、以前は黙って何もせず成功した
2938        // かのように振る舞っていた。
2939        let is_child = it.dom.borrow().nodes.get(c).and_then(|n| n.parent) == Some(p);
2940        if !is_child {
2941            return Err(make_dom_exception(
2942                "NotFoundError",
2943                "The node to be removed is not a child of this node",
2944            ));
2945        }
2946        it.dom.borrow_mut().remove_child(p, c);
2947    }
2948    Ok(child)
2949}
2950
2951/// `element.remove()`(`ChildNode.remove()`。引数無しで自身を親から取り外す)。
2952/// ただし`<select>`だけは`HTMLSelectElement.remove(index)`という別物の
2953/// オーバーロードを持ち、数値引数が渡された場合は「自身を削除」ではなく
2954/// 「指定インデックスのoptionを削除」という全く異なる意味になる(仕様上の
2955/// 別メソッドがたまたま同名なだけ)。以前はこの区別が無く、`select.remove(1)`
2956/// が常にselect自身を親から削除してしまう(`options`側の削除ではなく)
2957/// バグだった。
2958pub fn dom_remove(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2959    if let Some(idx) = this_dom_idx(&this) {
2960        let is_select = it.dom.borrow().nodes.get(idx).map(|n| n.tag == "select").unwrap_or(false);
2961        if is_select {
2962            if let Some(v) = a.first() {
2963                let n = v.to_number();
2964                if n.is_finite() {
2965                    // 負のインデックスは仕様上 no-op(`as usize` は負の f64 を 0 へ
2966                    // 飽和させてしまい、`remove(-1)` が誤って先頭optionを削除する
2967                    // バグになるため明示的に弾く)。
2968                    if n >= 0.0 {
2969                        let options = it.dom.borrow().select_options(idx);
2970                        if let Some(&opt) = options.get(n as usize) {
2971                            it.dom.borrow_mut().remove_child(idx, opt);
2972                        }
2973                    }
2974                    return Ok(Value::Undefined);
2975                }
2976            }
2977        }
2978        it.dom.borrow_mut().remove_node(idx);
2979    }
2980    Ok(Value::Undefined)
2981}
2982
2983/// ネイティブ関数値を作るショートカット。
2984pub(crate) fn nv(name: &str, f: NativeFn) -> Value {
2985    Value::Object(Obj::native(name, f))
2986}
2987pub(crate) fn arg(args: &[Value], i: usize) -> Value {
2988    args.get(i).cloned().unwrap_or(Value::Undefined)
2989}
2990
2991/// `HTMLCollection.prototype.item(index)`(DOM Standard §4.2.1)。
2992pub fn html_collection_item(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
2993    if let Value::Object(o) = &this {
2994        let idx_num = arg(a, 0).to_number();
2995        if idx_num >= 0.0 && idx_num.is_finite() {
2996            let i = idx_num as usize;
2997            let borrow = o.borrow();
2998            if let ObjKind::Array(ref vec) = borrow.kind {
2999                if let Some(val) = vec.get(i) {
3000                    return Ok(val.clone());
3001                }
3002            }
3003        }
3004    }
3005    Ok(Value::Null)
3006}
3007
3008/// `HTMLCollection.prototype.namedItem(name)`(DOM Standard §4.2.1 / HTML Standard §4.10.21)。
3009/// 同名/同IDのコントロールが複数ある場合は `RadioNodeList` を返す。
3010pub fn html_collection_named_item(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3011    if let Value::Object(o) = &this {
3012        let target = arg(a, 0).to_js_string();
3013        if !target.is_empty() {
3014            let borrow = o.borrow();
3015            if let ObjKind::Array(ref vec) = borrow.kind {
3016                let dom = it.dom.borrow();
3017                let mut matches: Vec<Value> = Vec::new();
3018                let mut match_indices: Vec<usize> = Vec::new();
3019                for val in vec {
3020                    if let Value::Object(item_obj) = val {
3021                        if let ObjKind::DomElement(elem_idx) = item_obj.borrow().kind {
3022                            let id_match = dom.get_attr(elem_idx, "id").map(|id| id == target).unwrap_or(false);
3023                            let name_match = dom.get_attr(elem_idx, "name").map(|name| name == target).unwrap_or(false);
3024                            if id_match || name_match {
3025                                matches.push(val.clone());
3026                                match_indices.push(elem_idx);
3027                            }
3028                        }
3029                    }
3030                }
3031                if matches.len() == 1 {
3032                    return Ok(matches[0].clone());
3033                } else if matches.len() > 1 {
3034                    return Ok(make_radio_node_list(it, match_indices));
3035                }
3036            }
3037        }
3038    }
3039    Ok(Value::Null)
3040}
3041
3042/// `RadioNodeList` オブジェクトを生成するヘルパー(HTML Standard §4.10.21.1)。
3043/// 配列構造に加え `.value` (getter/setter) アクセスおよび `item(index)` メソッドをサポートする。
3044pub fn make_radio_node_list(_it: &Interp, elements: Vec<usize>) -> Value {
3045    let mut arr: Vec<Value> = Vec::with_capacity(elements.len());
3046    for &idx in &elements {
3047        arr.push(Value::Object(Obj::dom(idx)));
3048    }
3049    let obj = Obj::array(arr);
3050    obj.borrow_mut().props.insert("_is_radio_nodelist".into(), Value::Bool(true));
3051    obj.borrow_mut().props.insert("item".into(), nv("item", html_collection_item));
3052    Value::Object(obj)
3053}
3054
3055/// `HTMLCollection` / `HTMLFormControlsCollection` オブジェクトを生成するヘルパー。
3056/// 配列機能に加え、DOM Standard §4.2.1 に準拠して `item(index)` および
3057/// `namedItem(name)` メソッドを持つ。
3058pub fn make_html_collection(_it: &Interp, elements: Vec<usize>) -> Value {
3059    let mut arr: Vec<Value> = Vec::with_capacity(elements.len());
3060    for &idx in &elements {
3061        arr.push(Value::Object(Obj::dom(idx)));
3062    }
3063    let obj = Obj::array(arr);
3064    obj.borrow_mut().props.insert("item".into(), nv("item", html_collection_item));
3065    obj.borrow_mut().props.insert("namedItem".into(), nv("namedItem", html_collection_named_item));
3066    Value::Object(obj)
3067}
3068
3069/// `DOMParser.prototype.parseFromString(str, type)` (DOM Parsing and Serialization §4.1)。
3070/// 入力された HTML 文字列を parse_html でパースし、DomBridge へ組み込んで文書ノードを返却する。
3071pub fn dom_parser_parse_from_string(it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3072    let html = args.first().map(|v| v.to_js_string()).unwrap_or_default();
3073    let root = crate::os_lib::dom::parse_html(&html);
3074    it.dom.borrow_mut().build_from(&root);
3075    let doc = Obj::host("document");
3076    {
3077        let mut d = doc.borrow_mut();
3078        d.props.insert(
3079            "getElementById".into(),
3080            nv("getElementById", document_get_element_by_id),
3081        );
3082        d.props.insert(
3083            "querySelector".into(),
3084            nv("querySelector", document_query_selector),
3085        );
3086        d.props.insert(
3087            "querySelectorAll".into(),
3088            nv("querySelectorAll", document_query_selector_all),
3089        );
3090    }
3091    Ok(Value::Object(doc))
3092}
3093
3094/// `DOMParser` コンストラクタ。
3095pub fn dom_parser_ctor(_it: &mut Interp, _this: Value, _args: &[Value]) -> Result<Value, Value> {
3096    let obj = Obj::plain();
3097    obj.borrow_mut().props.insert(
3098        "parseFromString".into(),
3099        nv("parseFromString", dom_parser_parse_from_string),
3100    );
3101    Ok(Value::Object(obj))
3102}
3103
3104/// `form.reset()`(HTML5)の実装。
3105/// フォーム要素配下のすべての `<input>`, `<select>`, `<textarea>` のフォームライブ値を初期状態にリセットする。
3106pub fn dom_form_reset(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
3107    let Some(form_idx) = this_dom_idx(&this) else {
3108        return Ok(Value::Undefined);
3109    };
3110    let child_indices: alloc::vec::Vec<usize> = {
3111        let dom = it.dom.borrow();
3112        let mut list = alloc::vec::Vec::new();
3113        fn collect_descendants(dom: &crate::os_lib::js::dom_bridge::DomBridge, node_idx: usize, out: &mut alloc::vec::Vec<usize>) {
3114            if let Some(node) = dom.nodes.get(node_idx) {
3115                for &child in &node.children {
3116                    out.push(child);
3117                    collect_descendants(dom, child, out);
3118                }
3119            }
3120        }
3121        collect_descendants(&dom, form_idx, &mut list);
3122        list
3123    };
3124
3125    let mut dom = it.dom.borrow_mut();
3126    for idx in child_indices {
3127        let tag = dom.nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
3128        match tag.as_str() {
3129            "input" => {
3130                let ty = dom.get_attr(idx, "type").unwrap_or_default().to_lowercase();
3131                if ty == "checkbox" || ty == "radio" {
3132                    let default_checked = dom.has_attr(idx, "checked");
3133                    dom.set_attr(idx, "_live_checked", if default_checked { "true" } else { "false" });
3134                } else {
3135                    let default_val = dom.get_attr(idx, "value").unwrap_or_default();
3136                    dom.set_attr(idx, "_live_value", &default_val);
3137                }
3138            }
3139            "textarea" => {
3140                let default_val = dom.get_text_content(idx);
3141                dom.set_attr(idx, "_live_value", &default_val);
3142            }
3143            "select" => {
3144                dom.remove_attr(idx, "_live_value");
3145            }
3146            _ => {}
3147        }
3148    }
3149    Ok(Value::Undefined)
3150}
3151
3152/// `<canvas>` 2D context の `measureText(text)` メソッドの実装。
3153pub fn dom_canvas_measure_text(_it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3154    let text = args.first().map(|v| v.to_js_string()).unwrap_or_default();
3155    let width = (text.len() * 8) as f64;
3156    let obj = Obj::plain();
3157    {
3158        let mut b = obj.borrow_mut();
3159        b.props.insert("width".into(), Value::Number(width));
3160        b.props.insert("actualBoundingBoxAscent".into(), Value::Number(10.0));
3161        b.props.insert("actualBoundingBoxDescent".into(), Value::Number(2.0));
3162        b.props.insert("fontBoundingBoxAscent".into(), Value::Number(10.0));
3163        b.props.insert("fontBoundingBoxDescent".into(), Value::Number(2.0));
3164        b.props.insert("actualBoundingBoxLeft".into(), Value::Number(0.0));
3165        b.props.insert("actualBoundingBoxRight".into(), Value::Number(width));
3166    }
3167    Ok(Value::Object(obj))
3168}
3169
3170pub fn dom_canvas_get_image_data(_it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3171    let w = args.get(2).map(|v| v.to_number() as usize).unwrap_or(1).max(1);
3172    let h = args.get(3).map(|v| v.to_number() as usize).unwrap_or(1).max(1);
3173    let len = w * h * 4;
3174    let data_arr = Obj::array(alloc::vec![Value::Number(0.0); len]);
3175    let img_data = Obj::plain();
3176    {
3177        let mut b = img_data.borrow_mut();
3178        b.props.insert("width".into(), Value::Number(w as f64));
3179        b.props.insert("height".into(), Value::Number(h as f64));
3180        b.props.insert("data".into(), Value::Object(data_arr));
3181    }
3182    Ok(Value::Object(img_data))
3183}
3184
3185/// `createImageData` は**透明で埋めた**新しい `ImageData` を返す(仕様)。
3186/// 面の内容は読まない。`getImageData` と混同しないこと。
3187pub fn dom_canvas_create_image_data(
3188    it: &mut Interp,
3189    this: Value,
3190    args: &[Value],
3191) -> Result<Value, Value> {
3192    dom_canvas_get_image_data(it, this, args)
3193}
3194
3195pub fn dom_canvas_is_point_in_path(_it: &mut Interp, _this: Value, _args: &[Value]) -> Result<Value, Value> {
3196    Ok(Value::Bool(false))
3197}
3198
3199pub fn dom_canvas_get_transform(it: &mut Interp, _this: Value, _args: &[Value]) -> Result<Value, Value> {
3200    dom_matrix_ctor(it, Value::Undefined, &[])
3201}
3202
3203pub fn dom_canvas_set_line_dash(_it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3204    if let Value::Object(o) = &this {
3205        let segs = args.first().cloned().unwrap_or(Value::Undefined);
3206        let items = this_items(&segs);
3207        let mut nums: alloc::vec::Vec<Value> = items.into_iter().map(|v| Value::Number(v.to_number())).collect();
3208        if nums.len() % 2 != 0 {
3209            let dup = nums.clone();
3210            nums.extend(dup);
3211        }
3212        o.borrow_mut().props.insert("_line_dash".into(), Value::Object(Obj::array(nums)));
3213    }
3214    Ok(Value::Undefined)
3215}
3216
3217pub fn dom_canvas_get_line_dash(_it: &mut Interp, this: Value, _args: &[Value]) -> Result<Value, Value> {
3218    if let Value::Object(o) = &this {
3219        if let Some(Value::Object(arr)) = o.borrow().props.get("_line_dash") {
3220            let items = this_items(&Value::Object(arr.clone()));
3221            return Ok(Value::Object(Obj::array(items)));
3222        }
3223    }
3224    Ok(Value::Object(Obj::array(alloc::vec![])))
3225}
3226
3227pub fn dom_canvas_round_rect(_it: &mut Interp, _this: Value, _args: &[Value]) -> Result<Value, Value> {
3228    Ok(Value::Undefined)
3229}
3230
3231/// `<canvas>` 2D context の `reset()` メソッド (WHATWG HTML Standard)。
3232pub fn dom_canvas_reset(_it: &mut Interp, this: Value, _args: &[Value]) -> Result<Value, Value> {
3233    if let Value::Object(ref ctx) = this {
3234        let mut b = ctx.borrow_mut();
3235        b.props.insert("fillStyle".into(), Value::str("#000000"));
3236        b.props.insert("strokeStyle".into(), Value::str("#000000"));
3237        b.props.insert("lineWidth".into(), Value::Number(1.0));
3238        b.props.insert("lineCap".into(), Value::str("butt"));
3239        b.props.insert("lineJoin".into(), Value::str("miter"));
3240        b.props.insert("miterLimit".into(), Value::Number(10.0));
3241        b.props.insert("lineDashOffset".into(), Value::Number(0.0));
3242        b.props.insert("font".into(), Value::str("10px sans-serif"));
3243        b.props.insert("textAlign".into(), Value::str("start"));
3244        b.props.insert("textBaseline".into(), Value::str("alphabetic"));
3245        b.props.insert("globalAlpha".into(), Value::Number(1.0));
3246        b.props.insert("globalCompositeOperation".into(), Value::str("source-over"));
3247        b.props.insert("shadowBlur".into(), Value::Number(0.0));
3248        b.props.insert("shadowColor".into(), Value::str("rgba(0, 0, 0, 0)"));
3249        b.props.insert("shadowOffsetX".into(), Value::Number(0.0));
3250        b.props.insert("shadowOffsetY".into(), Value::Number(0.0));
3251        b.props.insert("imageSmoothingEnabled".into(), Value::Bool(true));
3252        b.props.insert("direction".into(), Value::str("inherit"));
3253        b.props.insert("filter".into(), Value::str("none"));
3254        b.props.shift_remove("_line_dash");
3255    }
3256    Ok(Value::Undefined)
3257}
3258
3259/// `Path2D` コンストラクタ (WHATWG HTML Canvas 2D Path2D API)。
3260pub fn dom_path2d_ctor(_it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3261    let p = Obj::plain();
3262    {
3263        let mut b = p.borrow_mut();
3264        let path_data = args.first().map(|v| v.to_js_string()).unwrap_or_default();
3265        b.props.insert("_path_data".into(), Value::str(&path_data));
3266        for m in [
3267            "addPath", "closePath", "moveTo", "lineTo", "arc", "arcTo",
3268            "rect", "roundRect", "ellipse", "bezierCurveTo", "quadraticCurveTo"
3269        ] {
3270            b.props.insert(m.into(), nv(m, dom_noop));
3271        }
3272    }
3273    Ok(Value::Object(p))
3274}
3275
3276/// `<canvas>` 2D context の `createLinearGradient` / `createRadialGradient` / `createConicGradient` のヘルパー。
3277pub fn dom_canvas_create_gradient(_it: &mut Interp, _this: Value, _args: &[Value]) -> Result<Value, Value> {
3278    let grad = Obj::plain();
3279    grad.borrow_mut().props.insert(
3280        "addColorStop".into(),
3281        nv("addColorStop", dom_noop),
3282    );
3283    Ok(Value::Object(grad))
3284}
3285
3286/// `<canvas>` 要素の `getContext(contextId, options?)` (HTML5 Canvas 2D Context API)。
3287pub fn dom_canvas_get_context(_it: &mut Interp, this: Value, args: &[Value]) -> Result<Value, Value> {
3288    let ctx_type = args.first().map(|v| v.to_js_string()).unwrap_or_default();
3289    if ctx_type.eq_ignore_ascii_case("2d") {
3290        // 面のサイズは `<canvas width height>` 属性から取る。
3291        // 属性が無い場合の既定は仕様どおり 300x150。
3292        // `this` はこの後 props へ move されるので、その前に読む。
3293        let (cw, ch) = match &this {
3294            Value::Object(o) => {
3295                let kind = o.borrow().kind.clone();
3296                if let ObjKind::DomElement(idx) = kind {
3297                    let d = _it.dom.borrow();
3298                    let w = d
3299                        .get_attr(idx, "width")
3300                        .and_then(|s| s.trim().parse::<u32>().ok())
3301                        .unwrap_or(300);
3302                    let h = d
3303                        .get_attr(idx, "height")
3304                        .and_then(|s| s.trim().parse::<u32>().ok())
3305                        .unwrap_or(150);
3306                    (w, h)
3307                } else {
3308                    (300, 150)
3309                }
3310            }
3311            _ => (300, 150),
3312        };
3313        let c2d_id = crate::os_lib::canvas2d::create_context(cw, ch);
3314        // レイアウト側(`web_engine/layout.rs`)は `StyledNode` しか見ないので、
3315        // id を DOM 属性へ書いて橋渡しする。JS のオブジェクトを直接引かせると
3316        // 層をまたぐ依存になる。
3317        if let (Some(id), Value::Object(o)) = (c2d_id, &this) {
3318            let kind = o.borrow().kind.clone();
3319            if let ObjKind::DomElement(idx) = kind {
3320                _it.dom
3321                    .borrow_mut()
3322                    .set_attr(idx, "_c2d_id", &alloc::format!("{}", id));
3323            }
3324        }
3325        let ctx = Obj::plain();
3326        {
3327            let mut b = ctx.borrow_mut();
3328            b.props.insert("canvas".into(), this);
3329            b.props.insert("fillStyle".into(), Value::str("#000000"));
3330            b.props.insert("strokeStyle".into(), Value::str("#000000"));
3331            b.props.insert("lineWidth".into(), Value::Number(1.0));
3332            b.props.insert("lineCap".into(), Value::str("butt"));
3333            b.props.insert("lineJoin".into(), Value::str("miter"));
3334            b.props.insert("miterLimit".into(), Value::Number(10.0));
3335            b.props.insert("lineDashOffset".into(), Value::Number(0.0));
3336            b.props.insert("font".into(), Value::str("10px sans-serif"));
3337            b.props.insert("textAlign".into(), Value::str("start"));
3338            b.props.insert("textBaseline".into(), Value::str("alphabetic"));
3339            b.props.insert("globalAlpha".into(), Value::Number(1.0));
3340            b.props.insert("globalCompositeOperation".into(), Value::str("source-over"));
3341            b.props.insert("shadowBlur".into(), Value::Number(0.0));
3342            b.props.insert("shadowColor".into(), Value::str("rgba(0, 0, 0, 0)"));
3343            b.props.insert("shadowOffsetX".into(), Value::Number(0.0));
3344            b.props.insert("shadowOffsetY".into(), Value::Number(0.0));
3345            b.props.insert("imageSmoothingEnabled".into(), Value::Bool(true));
3346            b.props.insert("direction".into(), Value::str("inherit"));
3347            b.props.insert("filter".into(), Value::str("none"));
3348            // 【2026-09-03】実描画へ接続する。従来これらは全て `dom_noop` で、
3349            // API 表面だけが存在し canvas は一切描画されなかった。
3350            // 幾何・ラスタライズは `os_lib/canvas2d.rs`(試験済み)にあり、
3351            // ここは registry の id を持たせて各メソッドから引くだけにする。
3352            if let Some(id) = c2d_id {
3353                b.props.insert("_c2d_id".into(), Value::Number(id as f64));
3354            }
3355
3356            // 【2026-09-03】実描画へ接続する。
3357            //
3358            // 従来これらは全て `dom_noop` で、API 表面だけが存在し
3359            // canvas は一切描画されなかった。`os_lib/canvas2d.rs` の
3360            // `Canvas2dContext`(試験済み)を registry へ作り、その id を
3361            // `_c2d_id` として持たせて各メソッドから引く。
3362            //
3363            b.props.insert("fillRect".into(), nv("fillRect", canvas_fill_rect));
3364            b.props.insert("strokeRect".into(), nv("strokeRect", canvas_stroke_rect));
3365            b.props.insert("clearRect".into(), nv("clearRect", canvas_clear_rect));
3366
3367            b.props.insert("beginPath".into(), nv("beginPath", canvas_begin_path));
3368            b.props.insert("closePath".into(), nv("closePath", canvas_close_path));
3369            b.props.insert("moveTo".into(), nv("moveTo", canvas_move_to));
3370            b.props.insert("lineTo".into(), nv("lineTo", canvas_line_to));
3371            b.props.insert("arc".into(), nv("arc", canvas_arc));
3372            b.props.insert("arcTo".into(), nv("arcTo", dom_noop));
3373            b.props.insert("bezierCurveTo".into(), nv("bezierCurveTo", canvas_bezier_curve_to));
3374            b.props.insert("quadraticCurveTo".into(), nv("quadraticCurveTo", canvas_quadratic_curve_to));
3375            b.props.insert("ellipse".into(), nv("ellipse", canvas_ellipse));
3376            b.props.insert("rect".into(), nv("rect", canvas_rect));
3377            b.props.insert("roundRect".into(), nv("roundRect", canvas_round_rect));
3378
3379            b.props.insert("fill".into(), nv("fill", canvas_fill));
3380            b.props.insert("stroke".into(), nv("stroke", canvas_stroke));
3381            b.props.insert("clip".into(), nv("clip", canvas_clip));
3382
3383            b.props.insert("fillText".into(), nv("fillText", canvas_fill_text));
3384            b.props.insert("strokeText".into(), nv("strokeText", canvas_fill_text));
3385            b.props.insert("measureText".into(), nv("measureText", canvas_measure_text));
3386
3387            b.props.insert("drawImage".into(), nv("drawImage", canvas_draw_image));
3388            b.props.insert("getImageData".into(), nv("getImageData", canvas_get_image_data));
3389            b.props.insert("createImageData".into(), nv("createImageData", dom_canvas_create_image_data));
3390            b.props.insert("putImageData".into(), nv("putImageData", canvas_put_image_data));
3391            b.props.insert("isPointInPath".into(), nv("isPointInPath", dom_canvas_is_point_in_path));
3392            b.props.insert("isPointInStroke".into(), nv("isPointInStroke", dom_canvas_is_point_in_path));
3393            b.props.insert("getTransform".into(), nv("getTransform", dom_canvas_get_transform));
3394            b.props.insert("setLineDash".into(), nv("setLineDash", dom_canvas_set_line_dash));
3395            b.props.insert("getLineDash".into(), nv("getLineDash", dom_canvas_get_line_dash));
3396            b.props.insert("reset".into(), nv("reset", dom_canvas_reset));
3397
3398            b.props.insert("createLinearGradient".into(), nv("createLinearGradient", dom_canvas_create_gradient));
3399            b.props.insert("createRadialGradient".into(), nv("createRadialGradient", dom_canvas_create_gradient));
3400            b.props.insert("createConicGradient".into(), nv("createConicGradient", dom_canvas_create_gradient));
3401            b.props.insert("createPattern".into(), nv("createPattern", dom_canvas_create_gradient));
3402
3403            b.props.insert("save".into(), nv("save", canvas_save));
3404            b.props.insert("restore".into(), nv("restore", canvas_restore));
3405            b.props.insert("scale".into(), nv("scale", canvas_scale));
3406            b.props.insert("rotate".into(), nv("rotate", canvas_rotate));
3407            b.props.insert("translate".into(), nv("translate", canvas_translate));
3408            b.props.insert("transform".into(), nv("transform", canvas_transform));
3409            b.props.insert("setTransform".into(), nv("setTransform", canvas_set_transform));
3410            b.props.insert("resetTransform".into(), nv("resetTransform", canvas_reset_transform));
3411        }
3412        return Ok(Value::Object(ctx));
3413    } else if ctx_type.eq_ignore_ascii_case("webgl") || ctx_type.eq_ignore_ascii_case("webgl2") || ctx_type.eq_ignore_ascii_case("bitmaprenderer") {
3414        let ctx = Obj::plain();
3415        ctx.borrow_mut().props.insert("canvas".into(), this);
3416        return Ok(Value::Object(ctx));
3417    }
3418    Ok(Value::Null)
3419}
3420
3421/// `<canvas>` 要素の `toDataURL(type, quality)` (HTML5 Canvas)。
3422pub fn dom_canvas_to_data_url(_it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3423    let mime = args.first().map(|v| v.to_js_string()).unwrap_or_else(|| String::from("image/png"));
3424    let header = if mime.eq_ignore_ascii_case("image/jpeg") {
3425        "data:image/jpeg;base64,"
3426    } else if mime.eq_ignore_ascii_case("image/webp") {
3427        "data:image/webp;base64,"
3428    } else {
3429        "data:image/png;base64,"
3430    };
3431    // 1x1 透明 PNG の最小限データ URL
3432    let dummy_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
3433    Ok(Value::str(alloc::format!("{}{}", header, dummy_base64)))
3434}
3435
3436/// `<canvas>` 要素の `toBlob(callback, type, quality)` (HTML5 Canvas)。
3437pub fn dom_canvas_to_blob(it: &mut Interp, _this: Value, args: &[Value]) -> Result<Value, Value> {
3438    let cb = args.first().cloned().unwrap_or(Value::Undefined);
3439    let mime = args.get(1).map(|v| v.to_js_string()).unwrap_or_else(|| String::from("image/png"));
3440    if matches!(cb, Value::Object(_)) {
3441        let blob = Obj::plain();
3442        {
3443            let mut b = blob.borrow_mut();
3444            b.props.insert("size".into(), Value::Number(68.0));
3445            b.props.insert("type".into(), Value::str(mime));
3446        }
3447        it.call_value(&cb, Value::Undefined, &[Value::Object(blob)])?;
3448    }
3449    Ok(Value::Undefined)
3450}
3451
3452
3453// ───────────── Canvas 2D の実描画(2026-09-03 追加) ─────────────
3454//
3455// これらは以前すべて `dom_noop` に接続されており、API 表面だけが存在して
3456// 一切描画されなかった。幾何とラスタライズは `os_lib/canvas2d.rs`
3457// (純粋モジュール・試験済み)にあり、ここは「JS の値を取り出して
3458// コンテキストへ渡す」薄い層に徹する。
3459
3460/// コンテキストオブジェクトから registry の id を取り出す。
3461fn canvas_ctx_id(this: &Value) -> Option<u32> {
3462    match this {
3463        Value::Object(o) => match o.borrow().props.get("_c2d_id") {
3464            Some(Value::Number(n)) if *n >= 0.0 => Some(*n as u32),
3465            _ => None,
3466        },
3467        _ => None,
3468    }
3469}
3470
3471/// 引数を f32 で取る。非有限値は 0 とみなす(描画系でパニックさせない)。
3472fn canvas_arg_f32(a: &[Value], i: usize) -> f32 {
3473    let v = a.get(i).map(|v| v.to_number()).unwrap_or(0.0);
3474    if v.is_finite() {
3475        v as f32
3476    } else {
3477        0.0
3478    }
3479}
3480
3481/// JS 側のスタイル文字列を ARGB へ解決する。
3482///
3483/// CSS の色解析(`css::parse_color`)をそのまま使う。解釈できない値は
3484/// 仕様上「無視して直前の値を保つ」だが、ここでは不透明な黒へ倒す
3485/// (描かれないより、既定色で描かれた方が原因に気づきやすい)。
3486fn canvas_style_argb(this: &Value, key: &str) -> u32 {
3487    let s = match this {
3488        Value::Object(o) => o
3489            .borrow()
3490            .props
3491            .get(key)
3492            .map(|v| v.to_js_string())
3493            .unwrap_or_default(),
3494        _ => String::new(),
3495    };
3496    crate::os_lib::css::parse_color(&s).unwrap_or(0xFF00_0000)
3497}
3498
3499/// 描画前に、JS 側で書き換えられた状態をコンテキストへ反映する。
3500///
3501/// `fillStyle` 等は JS の素のプロパティなので、代入は検知できない。
3502/// 各描画メソッドの入口で毎回読み直すのが確実。
3503fn canvas_sync_state(this: &Value, ctx: &mut crate::os_lib::canvas2d::Canvas2dContext) {
3504    ctx.state.fill = canvas_style_argb(this, "fillStyle");
3505    ctx.state.stroke = canvas_style_argb(this, "strokeStyle");
3506    if let Value::Object(o) = this {
3507        let b = o.borrow();
3508        if let Some(Value::Number(n)) = b.props.get("globalAlpha") {
3509            if n.is_finite() {
3510                ctx.state.global_alpha = (*n as f32).clamp(0.0, 1.0);
3511            }
3512        }
3513        if let Some(Value::Number(n)) = b.props.get("lineWidth") {
3514            if n.is_finite() && *n > 0.0 {
3515                ctx.state.line_width = *n as f32;
3516            }
3517        }
3518        // 未知の値は無視して既定を保つ(仕様どおり代入を捨てる)。
3519        if let Some(Value::Str(s)) = b.props.get("lineCap") {
3520            if let Some(v) = crate::os_lib::canvas2d::parse_line_cap(s) {
3521                ctx.state.line_cap = v;
3522            }
3523        }
3524        if let Some(Value::Str(s)) = b.props.get("lineJoin") {
3525            if let Some(v) = crate::os_lib::canvas2d::parse_line_join(s) {
3526                ctx.state.line_join = v;
3527            }
3528        }
3529        if let Some(Value::Number(n)) = b.props.get("miterLimit") {
3530            if n.is_finite() && *n > 0.0 {
3531                ctx.state.miter_limit = *n as f32;
3532            }
3533        }
3534        // `setLineDash` が入れた配列。値の検査は `normalize_dash` に任せる
3535        // (不正なパターンはまるごと捨てるのが仕様)。
3536        ctx.state.line_dash.clear();
3537        if let Some(Value::Object(arr)) = b.props.get("_line_dash") {
3538            let ab = arr.borrow();
3539            if let ObjKind::Array(items) = &ab.kind {
3540                for v in items {
3541                    ctx.state.line_dash.push(v.to_number() as f32);
3542                }
3543            }
3544        }
3545        if let Some(Value::Number(n)) = b.props.get("lineDashOffset") {
3546            if n.is_finite() {
3547                ctx.state.line_dash_offset = *n as f32;
3548            }
3549        }
3550    }
3551}
3552
3553pub fn canvas_fill_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3554    if let Some(id) = canvas_ctx_id(&this) {
3555        let (x, y, w, h) = (
3556            canvas_arg_f32(a, 0),
3557            canvas_arg_f32(a, 1),
3558            canvas_arg_f32(a, 2),
3559            canvas_arg_f32(a, 3),
3560        );
3561        crate::os_lib::canvas2d::with_context(id, |c| {
3562            canvas_sync_state(&this, c);
3563            c.fill_rect(x, y, w, h);
3564        });
3565    }
3566    Ok(Value::Undefined)
3567}
3568
3569pub fn canvas_stroke_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3570    if let Some(id) = canvas_ctx_id(&this) {
3571        let (x, y, w, h) = (
3572            canvas_arg_f32(a, 0),
3573            canvas_arg_f32(a, 1),
3574            canvas_arg_f32(a, 2),
3575            canvas_arg_f32(a, 3),
3576        );
3577        crate::os_lib::canvas2d::with_context(id, |c| {
3578            canvas_sync_state(&this, c);
3579            // `strokeRect` は現在のパスを変更しない(仕様)。
3580            // 別のパスを組んで線描する。
3581            let mut p = crate::os_lib::canvas2d::PathBuilder::new();
3582            p.rect(&c.state.transform, x, y, w, h);
3583            let color = c.apply_alpha(c.state.stroke);
3584            let lw = c.state.line_width;
3585            let (cap, join, limit) =
3586                (c.state.line_cap, c.state.line_join, c.state.miter_limit);
3587            let sub = c.apply_dash(&p.finish());
3588            c.surface
3589                .stroke_path_styled(&sub, color, lw, cap, join, limit);
3590        });
3591    }
3592    Ok(Value::Undefined)
3593}
3594
3595pub fn canvas_clear_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3596    if let Some(id) = canvas_ctx_id(&this) {
3597        let (x, y, w, h) = (
3598            canvas_arg_f32(a, 0),
3599            canvas_arg_f32(a, 1),
3600            canvas_arg_f32(a, 2),
3601            canvas_arg_f32(a, 3),
3602        );
3603        crate::os_lib::canvas2d::with_context(id, |c| c.clear_rect(x, y, w, h));
3604    }
3605    Ok(Value::Undefined)
3606}
3607
3608pub fn canvas_begin_path(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
3609    if let Some(id) = canvas_ctx_id(&this) {
3610        crate::os_lib::canvas2d::with_context(id, |c| c.path.begin_path());
3611    }
3612    Ok(Value::Undefined)
3613}
3614
3615pub fn canvas_close_path(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
3616    if let Some(id) = canvas_ctx_id(&this) {
3617        crate::os_lib::canvas2d::with_context(id, |c| c.path.close_path());
3618    }
3619    Ok(Value::Undefined)
3620}
3621
3622pub fn canvas_move_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3623    if let Some(id) = canvas_ctx_id(&this) {
3624        let (x, y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3625        crate::os_lib::canvas2d::with_context(id, |c| {
3626            let m = c.state.transform;
3627            c.path.move_to(&m, x, y);
3628        });
3629    }
3630    Ok(Value::Undefined)
3631}
3632
3633pub fn canvas_line_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3634    if let Some(id) = canvas_ctx_id(&this) {
3635        let (x, y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3636        crate::os_lib::canvas2d::with_context(id, |c| {
3637            let m = c.state.transform;
3638            c.path.line_to(&m, x, y);
3639        });
3640    }
3641    Ok(Value::Undefined)
3642}
3643
3644pub fn canvas_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3645    if let Some(id) = canvas_ctx_id(&this) {
3646        let (x, y, w, h) = (
3647            canvas_arg_f32(a, 0),
3648            canvas_arg_f32(a, 1),
3649            canvas_arg_f32(a, 2),
3650            canvas_arg_f32(a, 3),
3651        );
3652        crate::os_lib::canvas2d::with_context(id, |c| {
3653            let m = c.state.transform;
3654            c.path.rect(&m, x, y, w, h);
3655        });
3656    }
3657    Ok(Value::Undefined)
3658}
3659
3660pub fn canvas_arc(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3661    if let Some(id) = canvas_ctx_id(&this) {
3662        let (cx, cy, r) = (
3663            canvas_arg_f32(a, 0),
3664            canvas_arg_f32(a, 1),
3665            canvas_arg_f32(a, 2),
3666        );
3667        let start = canvas_arg_f32(a, 3);
3668        let end = canvas_arg_f32(a, 4);
3669        let ccw = a.get(5).map(|v| v.truthy()).unwrap_or(false);
3670        crate::os_lib::canvas2d::with_context(id, |c| {
3671            let m = c.state.transform;
3672            c.path.arc(&m, cx, cy, r, start, end, ccw);
3673        });
3674    }
3675    Ok(Value::Undefined)
3676}
3677
3678pub fn canvas_fill(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3679    if let Some(id) = canvas_ctx_id(&this) {
3680        // 第 1 引数は塗りつぶし規則(省略時は nonzero)。
3681        let rule = match a.first().map(|v| v.to_js_string()) {
3682            Some(s) if s.eq_ignore_ascii_case("evenodd") => {
3683                crate::os_lib::canvas2d::FillRule::EvenOdd
3684            }
3685            _ => crate::os_lib::canvas2d::FillRule::NonZero,
3686        };
3687        crate::os_lib::canvas2d::with_context(id, |c| {
3688            canvas_sync_state(&this, c);
3689            c.fill(rule);
3690        });
3691    }
3692    Ok(Value::Undefined)
3693}
3694
3695pub fn canvas_stroke(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
3696    if let Some(id) = canvas_ctx_id(&this) {
3697        crate::os_lib::canvas2d::with_context(id, |c| {
3698            canvas_sync_state(&this, c);
3699            // `lineWidth` を反映する(1 以下なら 1px 線の高速路へ落ちる)。
3700            c.stroke_with_width();
3701        });
3702    }
3703    Ok(Value::Undefined)
3704}
3705
3706
3707/// `save()` — 現在の描画状態を退避する。
3708pub fn canvas_save(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
3709    if let Some(id) = canvas_ctx_id(&this) {
3710        crate::os_lib::canvas2d::with_context(id, |c| {
3711            // JS 側で書き換えられた状態も含めて退避したいので、
3712            // 先に読み直してから積む。
3713            canvas_sync_state(&this, c);
3714            c.save();
3715        });
3716    }
3717    Ok(Value::Undefined)
3718}
3719
3720/// `restore()` — 直近の退避状態へ戻す。
3721///
3722/// 変換だけでなく `fillStyle` 等も戻すのが仕様だが、それらは JS の素の
3723/// プロパティなので、Rust 側で戻した値を JS へ書き戻す必要がある。
3724pub fn canvas_restore(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
3725    if let Some(id) = canvas_ctx_id(&this) {
3726        let restored = crate::os_lib::canvas2d::with_context(id, |c| {
3727            c.restore();
3728            (c.state.global_alpha, c.state.line_width)
3729        });
3730        // `globalAlpha`/`lineWidth` は JS からも読めるので書き戻す。
3731        // 色は文字列表現を保持していないため戻せない(既知の割り切り)。
3732        if let (Some((ga, lw)), Value::Object(o)) = (restored, &this) {
3733            let mut b = o.borrow_mut();
3734            b.props.insert("globalAlpha".into(), Value::Number(ga as f64));
3735            b.props.insert("lineWidth".into(), Value::Number(lw as f64));
3736        }
3737    }
3738    Ok(Value::Undefined)
3739}
3740
3741pub fn canvas_translate(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3742    if let Some(id) = canvas_ctx_id(&this) {
3743        let (tx, ty) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3744        crate::os_lib::canvas2d::with_context(id, |c| c.translate(tx, ty));
3745    }
3746    Ok(Value::Undefined)
3747}
3748
3749pub fn canvas_scale(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3750    if let Some(id) = canvas_ctx_id(&this) {
3751        let (sx, sy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3752        crate::os_lib::canvas2d::with_context(id, |c| c.scale(sx, sy));
3753    }
3754    Ok(Value::Undefined)
3755}
3756
3757pub fn canvas_rotate(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3758    if let Some(id) = canvas_ctx_id(&this) {
3759        let rad = canvas_arg_f32(a, 0);
3760        crate::os_lib::canvas2d::with_context(id, |c| c.rotate(rad));
3761    }
3762    Ok(Value::Undefined)
3763}
3764
3765/// `transform(a,b,c,d,e,f)` — 現在の行列へ右から掛ける。
3766pub fn canvas_transform(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3767    if let Some(id) = canvas_ctx_id(&this) {
3768        let m = crate::os_lib::canvas2d::Matrix {
3769            a: canvas_arg_f32(a, 0),
3770            b: canvas_arg_f32(a, 1),
3771            c: canvas_arg_f32(a, 2),
3772            d: canvas_arg_f32(a, 3),
3773            e: canvas_arg_f32(a, 4),
3774            f: canvas_arg_f32(a, 5),
3775        };
3776        crate::os_lib::canvas2d::with_context(id, |ctx| {
3777            let cur = ctx.state.transform;
3778            ctx.set_transform(cur.multiply(&m));
3779        });
3780    }
3781    Ok(Value::Undefined)
3782}
3783
3784/// `setTransform(a,b,c,d,e,f)` — 行列を置き換える(積まない)。
3785pub fn canvas_set_transform(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3786    if let Some(id) = canvas_ctx_id(&this) {
3787        let m = crate::os_lib::canvas2d::Matrix {
3788            a: canvas_arg_f32(a, 0),
3789            b: canvas_arg_f32(a, 1),
3790            c: canvas_arg_f32(a, 2),
3791            d: canvas_arg_f32(a, 3),
3792            e: canvas_arg_f32(a, 4),
3793            f: canvas_arg_f32(a, 5),
3794        };
3795        crate::os_lib::canvas2d::with_context(id, |ctx| ctx.set_transform(m));
3796    }
3797    Ok(Value::Undefined)
3798}
3799
3800pub fn canvas_reset_transform(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
3801    if let Some(id) = canvas_ctx_id(&this) {
3802        crate::os_lib::canvas2d::with_context(id, |c| c.reset_transform());
3803    }
3804    Ok(Value::Undefined)
3805}
3806
3807pub fn canvas_bezier_curve_to(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3808    if let Some(id) = canvas_ctx_id(&this) {
3809        let (c1x, c1y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3810        let (c2x, c2y) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3811        let (x, y) = (canvas_arg_f32(a, 4), canvas_arg_f32(a, 5));
3812        crate::os_lib::canvas2d::with_context(id, |c| {
3813            let m = c.state.transform;
3814            c.path.bezier_curve_to(&m, c1x, c1y, c2x, c2y, x, y);
3815        });
3816    }
3817    Ok(Value::Undefined)
3818}
3819
3820pub fn canvas_quadratic_curve_to(
3821    _it: &mut Interp,
3822    this: Value,
3823    a: &[Value],
3824) -> Result<Value, Value> {
3825    if let Some(id) = canvas_ctx_id(&this) {
3826        let (cx, cy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3827        let (x, y) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3828        crate::os_lib::canvas2d::with_context(id, |c| {
3829            let m = c.state.transform;
3830            c.path.quadratic_curve_to(&m, cx, cy, x, y);
3831        });
3832    }
3833    Ok(Value::Undefined)
3834}
3835
3836
3837/// `ellipse(cx, cy, rx, ry, rotation, start, end, anticlockwise)`。
3838pub fn canvas_ellipse(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3839    if let Some(id) = canvas_ctx_id(&this) {
3840        let (cx, cy) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3841        let (rx, ry) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3842        let rot = canvas_arg_f32(a, 4);
3843        let start = canvas_arg_f32(a, 5);
3844        let end = canvas_arg_f32(a, 6);
3845        let ccw = a.get(7).map(|v| v.truthy()).unwrap_or(false);
3846        crate::os_lib::canvas2d::with_context(id, |c| {
3847            let m = c.state.transform;
3848            c.path.ellipse(&m, cx, cy, rx, ry, rot, start, end, ccw);
3849        });
3850    }
3851    Ok(Value::Undefined)
3852}
3853
3854/// `roundRect(x, y, w, h, radii)`。
3855///
3856/// `radii` は数値 1 個・配列(1〜4 要素)のどちらも取りうる。
3857/// CSS の `border-radius` と同じ補完規則(1 個なら全隅、2 個なら
3858/// 対角の組、3 個なら左上・対角・右下)に従う。
3859pub fn canvas_round_rect(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3860    if let Some(id) = canvas_ctx_id(&this) {
3861        let (x, y) = (canvas_arg_f32(a, 0), canvas_arg_f32(a, 1));
3862        let (w, h) = (canvas_arg_f32(a, 2), canvas_arg_f32(a, 3));
3863        // 半径の取り出し。配列でなければ単一値として扱う。
3864        let vals: alloc::vec::Vec<f32> = match a.get(4) {
3865            Some(Value::Object(o)) => {
3866                let b = o.borrow();
3867                if let ObjKind::Array(items) = &b.kind {
3868                    items
3869                        .iter()
3870                        .map(|v| {
3871                            let n = v.to_number();
3872                            if n.is_finite() {
3873                                n as f32
3874                            } else {
3875                                0.0
3876                            }
3877                        })
3878                        .collect()
3879                } else {
3880                    alloc::vec![0.0]
3881                }
3882            }
3883            Some(v) => {
3884                let n = v.to_number();
3885                alloc::vec![if n.is_finite() { n as f32 } else { 0.0 }]
3886            }
3887            None => alloc::vec![0.0],
3888        };
3889        // CSS の border-radius と同じ補完規則。
3890        let radii = match vals.len() {
3891            0 => [0.0; 4],
3892            1 => [vals[0]; 4],
3893            2 => [vals[0], vals[1], vals[0], vals[1]],
3894            3 => [vals[0], vals[1], vals[2], vals[1]],
3895            _ => [vals[0], vals[1], vals[2], vals[3]],
3896        };
3897        crate::os_lib::canvas2d::with_context(id, |c| {
3898            let m = c.state.transform;
3899            c.path.round_rect(&m, x, y, w, h, radii);
3900        });
3901    }
3902    Ok(Value::Undefined)
3903}
3904
3905
3906/// `clip(fillRule)`。現在のパスでクリップ領域を狭める。
3907///
3908/// 引数にパスを渡す形(`clip(path, rule)`)は `Path2D` が未実装のため
3909/// 対応しない。現在のパスを使う形だけを扱う。
3910pub fn canvas_clip(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3911    if let Some(id) = canvas_ctx_id(&this) {
3912        let rule = match a.first().map(|v| v.to_js_string()) {
3913            Some(s) if s.eq_ignore_ascii_case("evenodd") => {
3914                crate::os_lib::canvas2d::FillRule::EvenOdd
3915            }
3916            _ => crate::os_lib::canvas2d::FillRule::NonZero,
3917        };
3918        crate::os_lib::canvas2d::with_context(id, |c| {
3919            c.clip(rule);
3920        });
3921    }
3922    Ok(Value::Undefined)
3923}
3924
3925
3926/// `getImageData(x, y, w, h)`。面の内容を読んで `ImageData` を返す。
3927///
3928/// コンテキストが無い(`getContext` を経ていない)場合は、
3929/// 従来どおり透明で埋めた配列を返す。
3930pub fn canvas_get_image_data(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3931    let Some(id) = canvas_ctx_id(&this) else {
3932        return dom_canvas_get_image_data(it, this, a);
3933    };
3934    let (x, y) = (canvas_arg_f32(a, 0) as i32, canvas_arg_f32(a, 1) as i32);
3935    let w = (canvas_arg_f32(a, 2) as i32).max(0);
3936    let h = (canvas_arg_f32(a, 3) as i32).max(0);
3937    let bytes = crate::os_lib::canvas2d::with_context(id, |c| {
3938        crate::os_lib::canvas2d::get_image_data(&c.surface, x, y, w, h)
3939    });
3940    let Some(bytes) = bytes else {
3941        return dom_canvas_get_image_data(it, this, a);
3942    };
3943    let items: alloc::vec::Vec<Value> = bytes
3944        .into_iter()
3945        .map(|b| Value::Number(b as f64))
3946        .collect();
3947    let img = Obj::plain();
3948    {
3949        let mut b = img.borrow_mut();
3950        b.props.insert("width".into(), Value::Number(w as f64));
3951        b.props.insert("height".into(), Value::Number(h as f64));
3952        b.props.insert("data".into(), Value::Object(Obj::array(items)));
3953    }
3954    Ok(Value::Object(img))
3955}
3956
3957/// `putImageData(imageData, dx, dy)`。
3958///
3959/// 合成せず上書きし、クリップも無視する(仕様。`fill` 系と意味論が違う)。
3960pub fn canvas_put_image_data(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
3961    let Some(id) = canvas_ctx_id(&this) else {
3962        return Ok(Value::Undefined);
3963    };
3964    let Some(Value::Object(img)) = a.first() else {
3965        return Ok(Value::Undefined);
3966    };
3967    let (w, h, bytes) = {
3968        let b = img.borrow();
3969        let w = b
3970            .props
3971            .get("width")
3972            .map(|v| v.to_number() as i32)
3973            .unwrap_or(0);
3974        let h = b
3975            .props
3976            .get("height")
3977            .map(|v| v.to_number() as i32)
3978            .unwrap_or(0);
3979        let mut bytes: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
3980        if let Some(Value::Object(arr)) = b.props.get("data") {
3981            let ab = arr.borrow();
3982            if let ObjKind::Array(items) = &ab.kind {
3983                bytes.reserve(items.len());
3984                for v in items {
3985                    let n = v.to_number();
3986                    // 仕様どおり 0〜255 へ丸めて収める。
3987                    bytes.push(if n.is_finite() {
3988                        n.clamp(0.0, 255.0) as u8
3989                    } else {
3990                        0
3991                    });
3992                }
3993            }
3994        }
3995        (w, h, bytes)
3996    };
3997    let (dx, dy) = (canvas_arg_f32(a, 1) as i32, canvas_arg_f32(a, 2) as i32);
3998    crate::os_lib::canvas2d::with_context(id, |c| {
3999        crate::os_lib::canvas2d::put_image_data(&mut c.surface, &bytes, w, h, dx, dy);
4000    });
4001    Ok(Value::Undefined)
4002}
4003
4004
4005/// `drawImage` の転送元。`<img>` と別の canvas を扱う。
4006enum DrawSrc {
4007    /// デコード済み画像(`<img>`)。
4008    Image(alloc::sync::Arc<crate::os_lib::web_engine::DecodedImage>),
4009    /// 別の canvas の画素を RGBA8 へ写したもの。
4010    Canvas(u32, u32, alloc::vec::Vec<u8>),
4011}
4012
4013/// 転送元の値から画素を取り出す。
4014///
4015/// `<img>` は `src` 属性で共有の置き場を引く。
4016/// canvas は `_c2d_id` からコンテキストを引いて画素を写す
4017/// (同じコンテキストを二重に借りないよう、ここで写しを作る)。
4018fn draw_src_from(it: &mut Interp, v: &Value) -> Option<DrawSrc> {
4019    let Value::Object(o) = v else {
4020        return None;
4021    };
4022    let idx = match o.borrow().kind {
4023        ObjKind::DomElement(i) => Some(i),
4024        _ => None,
4025    }?;
4026    let (tag, src, cid) = {
4027        let dom = it.dom.borrow();
4028        let tag = dom.nodes.get(idx).map(|n| n.tag.clone()).unwrap_or_default();
4029        let src = dom.get_attr(idx, "src").unwrap_or_default();
4030        let cid = dom
4031            .get_attr(idx, "_c2d_id")
4032            .and_then(|s| s.parse::<u32>().ok());
4033        (tag, src, cid)
4034    };
4035    if tag == "canvas" {
4036        let id = cid?;
4037        return crate::os_lib::canvas2d::with_context(id, |c| {
4038            let (w, h) = (c.surface.width, c.surface.height);
4039            DrawSrc::Canvas(w, h, crate::os_lib::canvas2d::get_image_data(
4040                &c.surface, 0, 0, w as i32, h as i32,
4041            ))
4042        });
4043    }
4044    crate::os_lib::web_engine::shared_image(&src).map(DrawSrc::Image)
4045}
4046
4047/// `drawImage(img, dx, dy)` / `(img, dx, dy, dw, dh)` /
4048/// `(img, sx, sy, sw, sh, dx, dy, dw, dh)`。
4049pub fn canvas_draw_image(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4050    let Some(id) = canvas_ctx_id(&this) else {
4051        return Ok(Value::Undefined);
4052    };
4053    let Some(src) = a.first().and_then(|v| draw_src_from(it, v)) else {
4054        return Ok(Value::Undefined);
4055    };
4056    let (sw, sh, bytes): (u32, u32, &[u8]) = match &src {
4057        DrawSrc::Image(img) => (img.width, img.height, &img.rgba),
4058        DrawSrc::Canvas(w, h, b) => (*w, *h, b),
4059    };
4060    // 引数の個数で意味が変わる(仕様)。3・5・9 個のいずれか。
4061    let spec = match a.len() {
4062        0..=2 => return Ok(Value::Undefined),
4063        3 | 4 => crate::os_lib::canvas2d::BlitSpec {
4064            sx: 0.0,
4065            sy: 0.0,
4066            sw: sw as f32,
4067            sh: sh as f32,
4068            dx: canvas_arg_f32(a, 1),
4069            dy: canvas_arg_f32(a, 2),
4070            dw: sw as f32,
4071            dh: sh as f32,
4072        },
4073        5..=8 => crate::os_lib::canvas2d::BlitSpec {
4074            sx: 0.0,
4075            sy: 0.0,
4076            sw: sw as f32,
4077            sh: sh as f32,
4078            dx: canvas_arg_f32(a, 1),
4079            dy: canvas_arg_f32(a, 2),
4080            dw: canvas_arg_f32(a, 3),
4081            dh: canvas_arg_f32(a, 4),
4082        },
4083        _ => crate::os_lib::canvas2d::BlitSpec {
4084            sx: canvas_arg_f32(a, 1),
4085            sy: canvas_arg_f32(a, 2),
4086            sw: canvas_arg_f32(a, 3),
4087            sh: canvas_arg_f32(a, 4),
4088            dx: canvas_arg_f32(a, 5),
4089            dy: canvas_arg_f32(a, 6),
4090            dw: canvas_arg_f32(a, 7),
4091            dh: canvas_arg_f32(a, 8),
4092        },
4093    };
4094    let image = crate::os_lib::canvas2d::ImageSource {
4095        width: sw,
4096        height: sh,
4097        rgba: bytes,
4098    };
4099    crate::os_lib::canvas2d::with_context(id, |c| {
4100        canvas_sync_state(&this, c);
4101        c.sync_clip();
4102        let m = c.state.transform;
4103        let alpha = c.state.global_alpha;
4104        c.surface.draw_image(&image, &spec, &m, alpha);
4105    });
4106    Ok(Value::Undefined)
4107}
4108
4109
4110/// 1 文字ぶんの字形を、フォントの錠を**持たずに**扱えるよう写したもの。
4111struct OwnedGlyph {
4112    width: u32,
4113    height: u32,
4114    x_offset: i32,
4115    y_offset: i32,
4116    advance: f32,
4117    alpha: alloc::vec::Vec<u8>,
4118}
4119
4120/// 文字列ぶんの字形を取り出す。
4121///
4122/// 【重要】フォントの錠を**描画中に持ち続けない**。
4123/// `GLOBAL_VECTOR_FONT` は毎フレームの文字描画が使う。低優先度の側が
4124/// 握ったまま長く走ると、提示ループが `spin::Mutex` で回り続けて
4125/// 優先度逆転を起こす(`image_results_ready` で実際に踏んだ)。
4126/// ここで写しを作って錠を放し、描画は錠の外で行う。
4127fn glyphs_for(text: &str, size_px: u32) -> alloc::vec::Vec<OwnedGlyph> {
4128    let mut out = alloc::vec::Vec::new();
4129    let mut font_lock = crate::kernel::vector_font::GLOBAL_VECTOR_FONT.lock();
4130    let Some(font) = font_lock.as_mut() else {
4131        return out;
4132    };
4133    for c in text.chars() {
4134        if let Some(g) = font.get_glyph(c, size_px) {
4135            out.push(OwnedGlyph {
4136                width: g.width,
4137                height: g.height,
4138                x_offset: g.x_offset,
4139                y_offset: g.y_offset,
4140                advance: g.advance as f32,
4141                alpha: g.data.clone(),
4142            });
4143        }
4144    }
4145    out
4146}
4147
4148/// コンテキストの `font` から文字の大きさ(px)を得る。既定は 10px。
4149fn canvas_font_size(this: &Value) -> f32 {
4150    let Value::Object(o) = this else {
4151        return 10.0;
4152    };
4153    let b = o.borrow();
4154    let Some(Value::Str(s)) = b.props.get("font") else {
4155        return 10.0;
4156    };
4157    crate::os_lib::canvas2d::parse_font_size(s).unwrap_or(10.0)
4158}
4159
4160/// `fillText(text, x, y)`。
4161///
4162/// `strokeText` も同じ処理へ回す。線でなぞる形は未対応で、
4163/// 塗りで代用する(何も描かないよりは近い)。
4164pub fn canvas_fill_text(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4165    let Some(id) = canvas_ctx_id(&this) else {
4166        return Ok(Value::Undefined);
4167    };
4168    let text = a.first().map(|v| v.to_js_string()).unwrap_or_default();
4169    if text.is_empty() {
4170        return Ok(Value::Undefined);
4171    }
4172    let (x, y) = (canvas_arg_f32(a, 1), canvas_arg_f32(a, 2));
4173    if !x.is_finite() || !y.is_finite() {
4174        return Ok(Value::Undefined);
4175    }
4176    let size = canvas_font_size(&this);
4177    let glyphs = glyphs_for(&text, size as u32);
4178    if glyphs.is_empty() {
4179        return Ok(Value::Undefined);
4180    }
4181    let total: f32 = glyphs.iter().map(|g| g.advance).sum();
4182
4183    let (align, baseline) = {
4184        let mut al = crate::os_lib::canvas2d::TextAlign::Start;
4185        let mut bl = crate::os_lib::canvas2d::TextBaseline::Alphabetic;
4186        if let Value::Object(o) = &this {
4187            let b = o.borrow();
4188            if let Some(Value::Str(s)) = b.props.get("textAlign") {
4189                if let Some(v) = crate::os_lib::canvas2d::parse_text_align(s) {
4190                    al = v;
4191                }
4192            }
4193            if let Some(Value::Str(s)) = b.props.get("textBaseline") {
4194                if let Some(v) = crate::os_lib::canvas2d::parse_text_baseline(s) {
4195                    bl = v;
4196                }
4197            }
4198        }
4199        (al, bl)
4200    };
4201    let x0 = x + crate::os_lib::canvas2d::align_offset(align, total);
4202    let y0 = y + crate::os_lib::canvas2d::baseline_offset(baseline, size);
4203
4204    crate::os_lib::canvas2d::with_context(id, |c| {
4205        canvas_sync_state(&this, c);
4206        c.sync_clip();
4207        let color = c.apply_alpha(c.state.fill);
4208        let m = c.state.transform;
4209        let mut pen = x0;
4210        for g in &glyphs {
4211            let bm = crate::os_lib::canvas2d::GlyphBitmap {
4212                width: g.width,
4213                height: g.height,
4214                x_offset: g.x_offset,
4215                y_offset: g.y_offset,
4216                advance: g.advance,
4217                alpha: &g.alpha,
4218            };
4219            c.surface.draw_glyph(&bm, pen, y0, color, &m);
4220            pen += g.advance;
4221        }
4222    });
4223    Ok(Value::Undefined)
4224}
4225
4226/// `measureText(text)`。
4227///
4228/// 従来はバイト長 × 8 を返していた。日本語では実際と大きくずれ、
4229/// `font` の大きさも無視していた。字形の送り幅を実測する。
4230pub fn canvas_measure_text(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
4231    let text = a.first().map(|v| v.to_js_string()).unwrap_or_default();
4232    let size = canvas_font_size(&this);
4233    let glyphs = glyphs_for(&text, size as u32);
4234    if glyphs.is_empty() && !text.is_empty() {
4235        // フォントが無い(未初期化)ときは従来の概算へ落ちる。
4236        return dom_canvas_measure_text(it, this, a);
4237    }
4238    let width: f32 = glyphs.iter().map(|g| g.advance).sum();
4239    let obj = Obj::plain();
4240    {
4241        let mut b = obj.borrow_mut();
4242        b.props.insert("width".into(), Value::Number(width as f64));
4243        // 上下の張り出しは字形の実測から出す。
4244        let ascent = glyphs
4245            .iter()
4246            .map(|g| -g.y_offset)
4247            .max()
4248            .unwrap_or(0)
4249            .max(0) as f64;
4250        let descent = glyphs
4251            .iter()
4252            .map(|g| g.y_offset + g.height as i32)
4253            .max()
4254            .unwrap_or(0)
4255            .max(0) as f64;
4256        b.props
4257            .insert("actualBoundingBoxAscent".into(), Value::Number(ascent));
4258        b.props
4259            .insert("actualBoundingBoxDescent".into(), Value::Number(descent));
4260        b.props.insert(
4261            "fontBoundingBoxAscent".into(),
4262            Value::Number((size * 0.85) as f64),
4263        );
4264        b.props.insert(
4265            "fontBoundingBoxDescent".into(),
4266            Value::Number((size * 0.15) as f64),
4267        );
4268        b.props
4269            .insert("actualBoundingBoxLeft".into(), Value::Number(0.0));
4270        b.props.insert(
4271            "actualBoundingBoxRight".into(),
4272            Value::Number(width as f64),
4273        );
4274    }
4275    Ok(Value::Object(obj))
4276}